用一个小故事模拟Spring Aop(三): Advice&适配器

Advice&Advisor

承接上文
上文最终使用的例子如下

public class ImitateApplication {
    public static void main(String[] args) {
        // 厂家的冰淇淋机
        IceCreamMachine2 machine = new IceCreamMachine2();
        // 厂家定制食品监督计划
        MethodInterceptor interceptor1 = new MethodInterceptor() {
            @Override
            public Object invoke(MethodInvocation invocation) throws Throwable {
                System.out.println("记录需求至食品监督本:"+invocation.getArguments()[0]);
                Object proceed = invocation.proceed();
                System.out.println("拍照传给厂家微信:"+proceed);
                return proceed;
            }
        };
        // 厂家定制市场调研计划
        MethodInterceptor interceptor2 = new MethodInterceptor() {
            @Override
            public Object invoke(MethodInvocation invocation) throws Throwable {
                System.out.println("记录需求至市场调研本:"+invocation.getArguments()[0]);
                return invocation.proceed();
            }
        };
        // 代理工厂
        ProxyFactory proxyFactory = new ProxyFactory();
        // 绑定冰淇淋机
        proxyFactory.setTarget(machine);
        // 没有规范
        proxyFactory.setImpl(false);
        // 绑定两个拦截计划
        proxyFactory.addInterceptor(interceptor1);
        proxyFactory.addInterceptor(interceptor2);
        // 生成售货员(机器的代理)
        IceCreamMachine2 saler = (IceCreamMachine2) proxyFactory.getProxy();
        String iceCream = saler.eggCone("原味", "中");
    }
}

这样下去厂家代理工厂又配合了一段时间,厂家定制了很多种拦截计划,总结出一个规律:其实我们这些拦截计划无外乎就两种吗,一个是制作冰淇淋之前干点事,一个是制作后干点事,再不就是两种的结合,原拦截计划一般如下

Object invoke(MethodInvocation invocation) { 
//1.从打包信息invocation获取需求(invocation.getArguments())记在小本上 
//2.开始生产冰淇淋(invocation.proceed()) 
//3.把生产出的冰淇淋拍个照发给厂家微信
}

发现每次我们都要写一遍开始生产冰淇淋(invocation.proceed())这句话(对照代码就是每次都要写nvocation.proceed()这句话,十分冗余),既然固定几种,那就告诉你选哪种,然后干什么,你们代理工厂自己去写拦截计划吧,代理工厂没办法,谁叫你是客户,满足你。

为了应对这个问题,代理工厂相出个方案来,约定好两种新格式拦截计划(比原来简单)before和after,你给我before我就在制作冰淇淋前干,给我after我就在之后干,为了用得方便,新格式只需要包含什么机器,什么产品,什么口味规格这些完全易于理解的信息。

新的拦截计划格式A(在制作冰淇淋前干)

//target是机器,method是产品(蛋筒或杯装) args是用户的需求(口味和规格)
void before(Method method, Object[] args, @Nullable Object target) {
    // 在这里自己填想干什么...
}

新的拦截计划格式B(在制作冰淇淋后干)

//target是机器,method是产品(蛋筒或杯装) args是用户的需求(口味和规格),returnValue是制作出的冰淇淋
void afterReturning(@Nullable Object returnValue, Method method, Object[] args, @Nullable Object target) {
    // 在这里自己填想干什么...
}

这样的新格式拦截计划比之前简单多了,节省了学习成本

用代码模拟下
新格式拦截计划的抽象

public interface Advice {
}

然后之前和之后分别建一个,之前叫MethodBeforeAdvice,之后叫AfterReturningAdvice

public interface MethodBeforeAdvice extends Advice {
    //target是机器,method是产品(蛋筒或杯装) args是用户的需求(口味和规格)
    void before(Method method, Object[] args, Object target);

}
public interface AfterReturningAdvice extends Advice {
    //target是机器,method是产品(蛋筒或杯装) args是用户的需求(口味和规格),returnValue是制作出的冰淇淋
    void afterReturning(Object returnValue, Method method, Object[] args, Object target);

}

这是厂家在定制拦截计划就类似这样写就可以了

new MethodBeforeAdvice() {
    @Override
    public void before(Method method, Object[] args, Object target) throws Throwable {
        System.out.println("记录需求至市场调研本:"+args[0]);
    }
};

对比原来的

MethodInterceptor interceptor2 = new MethodInterceptor() {
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        System.out.println("记录需求至市场调研本:"+invocation.getArguments()[0]);
        return invocation.proceed();
    }
};

轻松多了,参数也更好理解了(不用理解MethodInvocation是什么了)

这个方案提出来后,厂家十分满意,问题又回到代理工厂了,工厂的员工只识别原来的拦截计划(MethodInterceptor),要让他们全重新学新拦截计划格式吗?代价有点大(相当于把之前的工厂代码都改了),于是想到个注意,找一个适配人员专门负责把新拦截计划格式转成老拦截计划格式(Advice转换成MethodInterceptor),其它人还是该干嘛干嘛就行了

image.png

解决问题之前,首先要修改的就是需求人员,原来的需求人员接受的是老拦截计划,现在要接受新的,
而且需求人员留了一个心眼,你这总变来变去的,难保以后不再弄点什么,所以用个盒子把新拦截计划包起来,以后再改我直接往盒子里加东西
这个装新拦截工作计划的盒子抽象模拟如下

/**
 * @Author wmf
 * @Date 2022/1/22 17:15
 * @Description 装工作计划的盒
 */
public interface Advisor {
    // 获取新工作计划
    Advice getAdvice();
}

需求人员名字由ProxyConfig变为AdvisedSupport为了和spring对应

/**
 * @Author wmf
 * @Date 2022/1/19 17:05
 * @Description 需求人员
 */
public class AdvisedSupport {
    /**
     * 附加新拦截计划盒子列表(改)
     */
    List<Advisor> advisors = new ArrayList<>();
    /**
     * 绑定的机器
     */
    Object target;
    /**
     * 是否有规范(是否有实现的接口)
     */
    Boolean isImpl;
    /**
     * 设置新拦截计划(改)
     * @param advice
     */
    public void addAdvice(Advice advice) {
        this.advisors.add(() -> advice);
    }
    /**
     * 绑定机器
     * @param target
     */
    public void setTarget(Object target) {
        this.target = target;
    }

    /**
     * 设置是否有规范(是否有实现的接口)
     * @param impl
     */
    public void setImpl(Boolean impl) {
        isImpl = impl;
    }
}

然后就是加个人专门做新拦截计划老拦截计划的映射,怎么映射呐,两种Advice,只需要分别包装成两个Interceptor

public class MethodBeforeInterceptor implements MethodInterceptor {

    private final MethodBeforeAdvice advice;
    
    public MethodBeforeInterceptor(Advice advice) {
        this.advice = (MethodBeforeAdvice) advice;
    }
    
    @Override
    public Object invoke(MethodInvocation mi) throws Throwable {
        this.advice.before(mi.getMethod(), mi.getArguments(), mi.getThis());
        return mi.proceed();
    }
}
public class AfterReturningInterceptor implements MethodInterceptor {

    private AfterReturningAdvice advice;

    public AfterReturningInterceptor(Advice advice) {
        this.advice = (AfterReturningAdvice) advice;
    }

    @Override
    public Object invoke(MethodInvocation mi) throws Throwable {
        Object proceed = mi.proceed();
        advice.afterReturning(proceed, mi.getMethod(), mi.getArguments(), mi.getThis());
        return proceed;
    }
}

这样转换就方便了,比如要把MethodBeforeAdvice转换为MethodBeforeInterceptor,只需

new MethodBeforeInterceptor(advice);

就搞定了!
下面模拟这个负责映射的适配人员,命名为DefaultAdvisorChainFactory(对应spring), 他的主要工作就是沟通需求人员,获得新拦截计划转换为老拦截计划

/**
 * 模拟负责适配新拦截计划到老拦截计划的工作人员
 */
public class DefaultAdvisorChainFactory {
    /**
     * 主要工作就是查看需求配置,获得新拦截计划转换为老拦截计划
     * @param config
     * @return
     */
    public List<MethodInterceptor> getInterceptors(AdvisedSupport config) {
        List<MethodInterceptor> interceptors = new ArrayList<>();
        for (Advisor advisor : config.advisors) {     
            Advice advice = advisor.getAdvice();
            // MethodBeforeAdvice转换为MethodBeforeInterceptor
            // AfterReturningAdvice转换为AfterReturningInterceptor
            if (advice instanceof MethodBeforeAdvice) {
                interceptors.add(new MethodBeforeInterceptor((MethodBeforeAdvice) advice));
            } else if (advice instanceof AfterReturningAdvice) {
                interceptors.add(new AfterReturningInterceptor((AfterReturningAdvice) advice));
            }
        }
        return interceptors;
    }
}

把这个负责映射的适配人员的电话给需求人员,于是需求人员就有了新能力,利用用这个适配人员新格式拦截计划转为老格式拦截计划

/**
 * @Author wmf
 * @Date 2022/1/19 17:05
 * @Description 配置
 */
public class AdvisedSupport {
    /**
     * 附加新拦截计划盒子列表(改)
     */
    List<Advisor> advisors = new ArrayList<>();
    /**
     * 绑定的机器
     */
    Object target;
    /**
     * 是否有规范(是否有实现的接口)
     */
    Boolean isImpl;
    /**
     * 负责映射新老拦截计划的人(新增)
     */
    DefaultAdvisorChainFactory chainFactory = new DefaultAdvisorChainFactory();

    /**
     * 获取拦截计划(由原来的的直接获取变为用映射人转换完之后再获取)
     * @return
     */
    List<MethodInterceptor> getInterceptors() {
        return chainFactory.getInterceptors(this);
    }
    /**
     * 设置新拦截计划(改)
     * @param advice
     */
    public void addAdvice(Advice advice) {
        this.advisors.add(() -> advice);
    }
    /**
     * 绑定机器
     * @param target
     */
    public void setTarget(Object target) {
        this.target = target;
    }

    /**
     * 设置是否有规范(是否有实现的接口)
     * @param impl
     */
    public void setImpl(Boolean impl) {
        isImpl = impl;
    }
}

测试一下

public class ImitateApplication {
    public static void main(String[] args) {
        // 厂家的冰淇淋机
        IceCreamMachine1 machine = new IceCreamMachine1();
        // 厂家按新格式定制市场调研计划
        MethodBeforeAdvice advice1 = (method, args1, target) -> System.out.println("记录需求至市场调研本:" + args1[0]);
        // 代理工厂
        ProxyFactory proxyFactory = new ProxyFactory();
        // 绑定冰淇淋机
        proxyFactory.setTarget(machine);
        // 没有规范
        proxyFactory.setImpl(true);
        // 绑定两个拦截计划
        proxyFactory.addAdvice(advice1);
        // 生成售货员(机器的代理)
        IceCreamMachine saler = (IceCreamMachine) proxyFactory.getProxy();
        String iceCream = saler.eggCone("原味", "中");
    }
}

输出

记录需求至市场调研本:原味
开始生产蛋筒冰淇淋

看到效果了,现在厂家干的事比原来简单多了,写法简洁明了

适配器

代理工厂完成了改造满足了厂家的需求,但代理工厂有前瞻性的领导提前发现了个潜在的问题: 这样做确实方便了,但是缺乏灵活性了,现在有两种Advice对应两种MethodInterceptor,将来可能出现第三种或更多种,甚至一个Advice对应多个MethodInterceptor,每次新增,适配人员都要新学习对应关系(需要修改DefaultAdvisorChainFactory的代码),适配人员的工作变的很繁琐

既然提前预见了这个问题,那就做好应对方法,方案:做一些适配器,每个适配器都有如下两个功能
1.给适配器某个类型的新格式拦截计划(Advice)适配器会告诉你它是否支持这种Advice
2.给适配器某个支持的类型的格式拦截计划(Advice),适配器还会给你自动转换为老格式的工作计化thodInterceptor)

下面对适配器做一个抽象

/**
 * @Author wmf
 * @Date 2022/1/22 14:34
 * @Description 适配器的抽象
 */
public interface AdvisorAdapter {
    /**
     * 传入advice,返回是否支持
     */
    boolean supportsAdvice(Advice advice);
    /**
     * 传入advice,返回MethodInterceptor
     */
    MethodInterceptor getInterceptor(Advisor advisor);
    
}

下面分别做两个Advice的适配器

class MethodBeforeAdviceAdapter implements AdvisorAdapter {
    @Override
    public boolean supportsAdvice(Advice advice) {
        return (advice instanceof MethodBeforeAdvice);
    }

    @Override
    public MethodInterceptor getInterceptor(Advisor advisor) {
        return new MethodBeforeInterceptor(advisor.getAdvice());
    }
}
class MethodBeforeAdviceAdapter implements AdvisorAdapter {
    @Override
    public boolean supportsAdvice(Advice advice) {
        return (advice instanceof MethodBeforeAdvice);
    }

    @Override
    public MethodInterceptor getInterceptor(Advisor advisor) {
        return new AfterReturningInterceptor(advisor.getAdvice());
    }
}

有了这些适配器,代理工厂在适配人员下面设一个适配器管理人员,原适配人员升值为适配负责人,他的职责管理所有适配器,当适配负责人给他一个新格式拦截计划(Advice),他一个个查看适配器,哪个支持用哪个,而适配负责人就不用自己判断了,只负责沟通需求人员,拿到Advice交给适配器管理人员并获取结果即可

image.png

下面抽象一下适配器管理人员

/**
 * 适配器管理人员的抽象
 */
public interface AdvisorAdapterRegistry {

    /**
     * 能力1.添加适配器
     * @param adapter
     */
    void registerAdvisorAdapter(AdvisorAdapter adapter);

    /**
     * 能力2.给一个老格式工作计划转换为n个新格式工作计划
     * @param advisor
     * @return
     */
    MethodInterceptor[] getInterceptors(Advisor advisor);

}

实现如下

/**
 * 适配器管理人员
 */
public class DefaultAdvisorAdapterRegistry implements AdvisorAdapterRegistry {

    private final List<AdvisorAdapter> adapters = new ArrayList<>(3);

    /**
     * 先添加两个现有的适配器
     */
    public DefaultAdvisorAdapterRegistry() {
        registerAdvisorAdapter(new MethodBeforeAdviceAdapter());
        registerAdvisorAdapter(new AfterReturningAdviceAdapter());
    }

    /**
     * 转换
     * @param advice
     * @return
     */
    @Override
    public MethodInterceptor[] getInterceptors(Advisor advisor) {
        List<MethodInterceptor> interceptors = new ArrayList<>();
        // 一个个适配器看
        for (AdvisorAdapter adapter : this.adapters) {
            // 如果支持
            if (adapter.supportsAdvice(advisor.getAdvice())) {
                // 转换
                interceptors.add(adapter.getInterceptor(advisor));
            }
        }
        return interceptors.toArray(new MethodInterceptor[0]);
    }

    /**
     * 注册适配器
     * @param adapter
     */
    @Override
    public void registerAdvisorAdapter(AdvisorAdapter adapter) {
        this.adapters.add(adapter);
    }

}

由于适配器这种事是公司统一定的,为了防止多个人员所持适配器不同混乱,也为了节约资源,所以公司规定,整个公司只能有一个适配器管理人员

实现这种想法就是单例模式(大家都会,就不模拟了)
回到适配负责人,他的工作就很简单了,只负责沟通需求人员,拿到Advice交给适配器管理人员并获取结果即可

/**
 * 模拟负责映射新拦截计划到老拦截计划的适配负责人
 */
public class DefaultAdvisorChainFactory {
    /**
     * 给分配一个适配器管理员(这里spring用的单例模式)
     */
    DefaultAdvisorAdapterRegistry registry = new DefaultAdvisorAdapterRegistry();
    /**
     * 主要工作就是查看需求配置,获得新拦截计划转换为老拦截计划
     * @param config
     * @return
     */
    public List<MethodInterceptor> getInterceptors(AdvisedSupport config) {
        List<MethodInterceptor> interceptors = new ArrayList<>();
        for (Advice advice : config.advices) {
            // 让适配其人员去实际转换
            MethodInterceptor[] inters = registry.getInterceptors(advice);
            interceptors.addAll(Arrays.asList(inters));
        }
        return interceptors;
    }
}

可以看到代码也没有if elseif了(阿里不推荐if else)

回到问题本身,现在问题解决了,如果需要新增Advice和Interceptor,只需要新增然后定义一个适配器即可,原来的人还是该干嘛干嘛,不用变了(符合了开闭原则)

上面的很多代码命名都有Advisor,其实Spring实现也有很多Advisor,只不过被我替换为Advice,因为Advisor下一章再讲

解决了这个问题,代理工厂发现了虽然服务升级,但是老格式的拦截计划还是要支持,毕竟还是最灵活的方式,所以规定需求人员也可以接受老格式的拦截计划,只要适配负责人看到老格式的拦截计划不做转换即可

老格式的拦截计划也是新格式拦截计划的一种,体现在代码上就是MethodInterceptor继承了Advice

@FunctionalInterface
public interface MethodInterceptor extends Advice {
    Object invoke(MethodInvocation invocation) throws Throwable;
}

适配器管理人员修改为

/**
 * 适配器管理人员
 */
public class DefaultAdvisorAdapterRegistry implements AdvisorAdapterRegistry {

    private final List<AdvisorAdapter> adapters = new ArrayList<>(3);

    /**
     * 先添加两个现有的适配器
     */
    public DefaultAdvisorAdapterRegistry() {
        registerAdvisorAdapter(new MethodBeforeAdviceAdapter());
        registerAdvisorAdapter(new AfterReturningAdviceAdapter());
    }

    /**
     * 转换
     * @param advisor
     * @return
     */
    @Override
    public MethodInterceptor[] getInterceptors(Advisor advisor) {
        List<MethodInterceptor> interceptors = new ArrayList<>();
        Advice advice = advisor.getAdvice();
        // 如果是老格式不转换(新增)
        if (advice instanceof MethodInterceptor) {
            interceptors.add((MethodInterceptor) advice);
        }
        // 一个个适配器看
        for (AdvisorAdapter adapter : this.adapters) {
            // 如果支持
            if (adapter.supportsAdvice(advice)) {
                // 转换
                interceptors.add(adapter.getInterceptor(advisor));
            }
        }
        return interceptors.toArray(new MethodInterceptor[0]);
    }

    /**
     * 注册适配器
     * @param adapter
     */
    @Override
    public void registerAdvisorAdapter(AdvisorAdapter adapter) {
        this.adapters.add(adapter);
    }

}

再来测试

public class ImitateApplication {
    public static void main(String[] args) {
        // 厂家的冰淇淋机
        IceCreamMachine1 machine = new IceCreamMachine1();
        // 厂家按新格式定制市场调研计划
        MethodBeforeAdvice advice1 = (method, args1, target) -> System.out.println("记录需求至市场调研本:" + args1[0]);
        // 厂家定制老格式食品监督计划
        MethodInterceptor interceptor1 = new MethodInterceptor() {
            @Override
            public Object invoke(MethodInvocation invocation) throws Throwable {
                System.out.println("记录需求至食品监督本:"+invocation.getArguments()[0]);
                Object proceed = invocation.proceed();
                System.out.println("拍照传给厂家微信:"+proceed);
                return proceed;
            }
        };
        // 代理工厂
        ProxyFactory proxyFactory = new ProxyFactory();
        // 绑定冰淇淋机
        proxyFactory.setTarget(machine);
        // 没有规范
        proxyFactory.setImpl(true);
        // 绑定两个拦截计划
        proxyFactory.addAdvice(advice1);
        proxyFactory.addAdvice(interceptor1);
        // 生成售货员(机器的代理)
        IceCreamMachine saler = (IceCreamMachine) proxyFactory.getProxy();
        String iceCream = saler.eggCone("原味", "中");
    }
}

输出:

记录需求至市场调研本:原味
记录需求至食品监督本:原味
开始生产蛋筒冰淇淋
拍照传给厂家微信:原味 蛋筒冰淇淋(中)

都生效了

对比spring

命名都一样,自行比对即可

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,744评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,505评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,105评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,242评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,269评论 6 389
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,215评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,096评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,939评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,354评论 1 311
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,573评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,745评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,448评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,048评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,683评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,838评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,776评论 2 369
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,652评论 2 354

推荐阅读更多精彩内容