设计模式中的珠联璧合

[TOC]

首先在开篇之前,我想先要解决一个问题:为什么要学设计模式?按正经的来说需要解决封装性、松耦合、可扩展等问题,但这里我先抛开这些不谈,就我个人体会而言,一个很明显的好处就是代码的逼格变高了......随之带来的是“可读性”变差了。之所以会这样,其实就像一般人看不懂框架源码一样,但你并不能说人家写的不好,只不过你的理解和那些大牛的理解不在同一个层次上而已罢了。因此想要从一届码农翻身艺术设计者,设计模式会是必要的取经之路。

然而设计模式本身是一种抽象的设计概念,并没有真正的代码模板可以套用,至少网上能看到或书上翻到的代码模板都是不适用的,生搬硬套是很多初学者的误区,无法理解设计模式的精髓。它真正的使用方式还必须结合实际的代码设计场景灵活搭配,所以如果非要说有什么模板,那本文推荐的例子会是不错的选择。

工厂+策略 优化IF-ELSE

在大多数的业务代码里少不了这样的判断:

public void doBusiness(TradeContext tradeContext) {

    String busiType = tradeContext.getBusiType();
    if ("1".equals(busiType)) {
        doBusi01(tradeContext);
    } else if ("2".equals(busiType)) {
        doBusi02(tradeContext);
    } else if ("3".equals(busiType)) {
        doBusi03(tradeContext);
    } else if ("4".equals(busiType) || "5".equals(busiType)) {
        doBusi045(tradeContext);
    } else if ("6".equals(busiType) || "7".equals(busiType)){
        doBusi067(tradeContext);
    } else if ("8".equals(busiType)) {
        doBusi08(tradeContext);
    } else if ("9".equals(busiType)) {
        doBusi09(tradeContext);
    } else {
        doBusiDefault(tradeContext);
    }
}

正是因为常见,所以这套组合也许会是常用的打法,众所周知,策略模式能将各个子业务逻辑拆到一个个类里面实现,但是对调用者来说它必须得知道每一种类型对应的实现类类名是什么,然而工厂模式又恰好能够屏蔽这个细节,让调用者可以优雅简化一大层的逻辑判断。

而说到工厂模式,你可能会联想三种:简单工厂+抽象工厂+工厂方法,简单工厂毕竟简单自有它的实用之处,但是它对“新增注册”产品不友好,而后两种并不是这里需要阐述的,因为其内容混杂即难以实用,而且有更强大的工厂可代替,那就是这里主要要阐述的Spring工厂

为什么也把它算在内,因为现在的Java项目无论大小都已经脱离不了Spring框架支撑了,把它作为必选套餐没什么毛病,而Spring天生就有强大的容器可以作为工厂的存在,已经包含了各种工厂的实现并支持混合运用,因此把它作为工厂模式的最佳实现我觉得没什么毛病。

因此接下来就说一下如何通过Spring工厂+策略模式来优化上述IF-ELSE结构

首先先定义产品接口,这个产品其实就是策略的抽象,但相对于策略抽象的接口,多了一个方法,待会就知道它是做什么用的了:

public interface BusiStrategy {

    /**
     * 具体业务逻辑的抽象方法
     * @param tradeContext
     */
    void doBusi(TradeContext tradeContext);

    /**
     * 用于匹配该类适用于哪种业务类型
     * @return
     */
    String[] supports();

}

接下来就完成这个接口的实现,也就是具体的策略产品,这里以Busi01举例,当然实际要根据自己的业务场景给个好听的类名哦

@Component
public class Busi01Strategy implements BusiStrategy {

    @Override
    public void doBusi(TradeContext tradeContext) {
        // TODO handle busi01
    }

    @Override
    public String[] supports() {
        return new String[] {"1"};
    }
}

其他的照猫画虎就行,由于support返回的是String[]数组,因此如果有多个类型的也同样适用哦~

最后有个重要的点,就是这些策略产品必须注册到Spring容器上,无论用什么样的方式均可,比如这里用最常见的注解方式注册

最后就要创建策略工厂了,也是整个策略模式实现的核心,同样这个工厂也需要注册到spring容器上,而且还要在spring bean的生命周期上做点事情

@Component
public class BusiStrategyFactory implements InitializingBean {

    /**
     * 这个是从容器中获取得到的所有BusiStrategy实现类的Map
     * 这个Map的key值对应的是容器中的beanName,没有业务含义,也不需要赋予业务含义
     */
    @Autowired
    private Map<String, BusiStrategy> busiStrategyContextMap;

    /**
     * 这个是该工厂方法需要获取的Map,这个Map也包含了所有BusiStrategy实现类
     * 但这个Map的key值是带有业务含义的,是busiType。
     */
    private Map<String, BusiStrategy> busiStrategyFactoryMap = new HashMap<>();

    /**
     * 获取产品的方法,根据busiType直接获取具体的业务实现类
     * @param busiType
     * @return
     */
    public BusiStrategy getBusiStrategy(String busiType) {
        BusiStrategy busiStrategy = busiStrategyFactoryMap.get(busiType);
        // The following code will support on Java8 Optional Class
        // return Optional.ofNullable(busiStrategy).orElse(DefaultBusiStrategy.instance);
        if (busiStrategy != null) {
            return busiStrategy;
        } else {
            return DefaultBusiStrategy.instance;
        }
    }

    /**
     * 产品列表注册初始化,该方法会在spring容器启动时加载该工厂的初始化阶段调用
     * (这个时候已经完成了属性注入和@Autowire的注解注入了)
     * @throws Exception
     */
    @Override
    public void afterPropertiesSet() throws Exception {
        for (BusiStrategy busiStrategy : busiStrategyContextMap.values()) {
            String[] supports = busiStrategy.supports();
            for (String support : supports) {
                busiStrategyFactoryMap.put(support, busiStrategy);
            }
        }
    }

    /**
     * 默认的策略单例实现
     */
    private static class DefaultBusiStrategy implements BusiStrategy {

        private static BusiStrategy instance = new DefaultBusiStrategy();

        private DefaultBusiStrategy() {}

        @Override
        public void doBusi(TradeContext tradeContext) {
            System.out.println("could not find strategy in factory");
        }

        @Override
        public String[] supports() {
            return new String[0];
        }
    }
}

在这里涉及到两个知识点,一个是@Autowired注解注入的类型为Map类型时,会把所有符合类型的beans注入,其key值是唯一的beanName。

另外就是InitializingBean的作用了,实现了这个接口的bean会在加载该bean的初始化阶段回调afterPropertiesSet方法,这是spring bean生命周期内的一部分内容,我会在下一个专题详细讲述。注意这段初始化逻辑不能写在构造函数里,因为在构造函数阶段属性还没赋值,@Autowired未生效,会造成空指针异常。

最后该工厂还提供了一个默认单例实现,是为了避免从容器中获取不到实例而导致空指针异常。这个默认单例就不需要依托于spring容器的管理了,而关于如何自己实现单例模式这里也就不具体再展开。这里采用的是内部类单例模式,既有懒加载的作用也还保证了线程安全,在大多数非极端场合已经足够使用。

也许到这里大家还是会对这个核心实现类很懵,不过没关系,记住就行了,以后都是这样的固定化模板,换个名字就可以直接套用了。

最后我们再来看看,通过这样改造后,核心业务逻辑会得到怎样的优化:

public void optimizeBusiness(TradeContext tradeContext) {
    String busiType = tradeContext.getBusiType();
    BusiStrategy busiStrategy = busiFactory.getBusiStrategy(busiType);

    busiStrategy.doBusi(tradeContext);
}

可以看到主流程及其简单,不需要任何IF-ELSE,十多行判断的死代码瞬间化为3行,而且以后要是新增类型,新增策略产品类就可以了,不需要改主流程的代码,也不需要改策略工厂的代码,这也体现了设计模式设计的总体大原则:多新增-少修改

装饰者+适配器 源码扩展神器

实际业务编码中可能都有过想改源码的冲动,可是要么这源码是编译好的.class文件不可修改,要么就是公共代码被其他模块大量引用不敢修改,那有没有办法优雅的改源码——即能满足当前功能的扩展需求,又不去改动旧有代码而兼容呢?

曾采访过很多同学,他们的答案如出一辙:继承要改的类,然后重写掉要改的方法。没错,就是这么简单,(那还需要我哔哔啥)

其实所谓适配器不过是高尚人士的说法而已了,其实它的核心体现就是继承或实现,它能使得被适配的类能够扩展功能,并能在旧有的API接口定义不变的情况下让新类得到调用。

话说起来是那么简单,但实际上操作起来其实没那么方便,具体什么原因我也道不出什么所以然,我只知道这个时候装饰者要出场了。

其实所谓装饰者不过也是高尚人士的说法罢了,说白一点它就是包装,再说直白一点它就是组合的设计。把要适配的类作为新类的成员变量,用构造函数的方式将它传入就可以了。

据我“改”源码经验来看,这套打法不像工厂+策略那样,能总结出有什么标准化使用场景和模板,这也许就是设计模式中的那种抽象艺术所在吧,我只能说当你冲动的时候不妨再冲动点,想要继承改源码的时候不妨再配个组合,如果被适配的类是有接口实现的,除非被适配的API定义的类型不是接口类型,否则更推荐去实现接口而不要继承来完成适配器,最后你会发现如此设计的代码蕴含着何等的艺术。

吹了那么多,我以实际场景来总结一下这套组合打法会是怎样的效果吧:

先简单介绍一下实际项目的背景:项目采用的是Mybatis + PageHelper 做分页实现的,PageHelper中默认自带了一个分页类PageInfo,用于携带数据库分页查询返回结果信息。而在项目框架中是不会直接将PageInfo对象作为接口返回字段的,因此整个项目规范了分页返回对象为PageVo,这个类定义在公共的依赖包里,为了让PageInfo对象快速化为PageVo对象,就定义了对应的构造方法:

/**
 *  公共Vo对象之,分页数据存储的Vo对象
 * @param <T>
 */
@Data
public class PageVo<T> implements Serializable {

    private static final long serialVersionUID = -2207112935012444854L;

    private Page pageInfo;
    
    private List<T> data;
    
    public PageVo() {
        pageInfo = new Page();
    }
    
    public PageVo(PageInfo<T> pageInfo) {
        this();
        this.pageInfo.currentPage = pageInfo.getPageNum();
        this.pageInfo.pageSize = pageInfo.getPageSize();
        this.pageInfo.length = pageInfo.getSize();
        this.pageInfo.total = pageInfo.getTotal();
        this.pageInfo.totalPage = pageInfo.getPages();
        this.data = pageInfo.getList();
        this.pageInfo.isFirst = pageInfo.isIsFirstPage();
        this.pageInfo.hasNext = pageInfo.isHasNextPage();
    }
    
    public PageVo(PageInfo<?> pageInfo, List<T> rows) {
        this();
        this.pageInfo.currentPage = pageInfo.getPageNum();
        this.pageInfo.pageSize = pageInfo.getPageSize();
        this.pageInfo.length = pageInfo.getSize();
        this.pageInfo.total = pageInfo.getTotal();
        this.pageInfo.totalPage = pageInfo.getPages();
        this.data = rows;
        this.pageInfo.isFirst = pageInfo.isIsFirstPage();
        this.pageInfo.hasNext = pageInfo.isHasNextPage();
    }
    
    @Data
    private class Page implements Serializable {

        private static final long serialVersionUID = -4019585481529601742L;

        private Integer currentPage;
        /** 每页条数 **/
        private Integer pageSize;
        /** 当前页的条数 **/
        private Integer length;
        /** 总记录数 **/
        private Long total;
        /** 总页数 **/
        private Integer totalPage;
        
        private Boolean isFirst;
        
        private Boolean hasNext;
    }
    
}

在大多数情况,这样设计完全没问题,数据库查询出来的分页对象通过构造方法转为PageVo返回给接口调用层。但后来项目用到了elasticSearch这种高级的东西,数据源不再是数据库了,elasticSearch使用的API是spring-data提供的,它又定义了一套分页查询结果对象标准Page接口,此时很自然地我需要为PageVo添加一个构造方法兼容返回。

但在项目中,PageVo是定义在公共依赖包里的一套规范之一,先不说实际项目能不能改,即使开放出来改,在公共依赖级别引入别的依赖,如果不做好评估,搞不好对其他模块造成依赖污染和依赖冲突问题。

因此想要入手解决这个问题,只能另辟蹊径,找准适配点,因此用于适配spring-dataPage接口和PageHelperPageInfo的适配器就诞生了,通过适配PageInfo进而间接完成了到PageVo的转换。

/**
 * Mybatis-PageHelper 的 PageInfo 和 JPA 的 Page 适配器
 *  同时是装饰类  解决elasticsearch不分页的痛
 *
 * @param <T>
 */
public class PageInfoAdaptor<T> extends PageInfo<T> {

    private static final long serialVersionUID = 1L;
    
    private Page<T> pageInfo;
    
    public PageInfoAdaptor(Page<T> pageInfo) {
        this(pageInfo, null);
    }

    public PageInfoAdaptor(Page<T> pageInfo, Pageable page) {
        super();
        // 这里可以先简单理解为 this.pageInfo = pageInfo  此处的目的是让elasticsearch分页结果支持更多的信息
        this.pageInfo = page == null ? pageInfo : new PageImpl<>(pageInfo.getContent(), page, pageInfo.getTotalElements());
    }

    @Override
    public int getPageNum() {
        return pageInfo.getNumber() + 1;
    }
    
    @Override
    public int getPageSize() {
        return pageInfo.getSize();
    }
    
    @Override
    public int getSize() {
        return pageInfo.getNumberOfElements();
    }

    @Override
    public long getTotal() {
        return pageInfo.getTotalElements();
    }
    
    @Override
    public int getPages() {
        return pageInfo.getTotalPages();
    }

    @Override
    public List<T> getList() {
        return pageInfo.getContent();
    }
    // 省略其他操作...
    
    @Override
    public boolean isHasPreviousPage() {
        throw new UnsupportedOperationException("适配器不支持set操作");
    }

    @Override
    public void setPageNum(int pageNum) {
        throw new UnsupportedOperationException("适配器不支持setPageNum操作");
    }

    // 省略其他不支持的操作
    
    @Override
    public String toString() {
        return pageInfo.toString();
    }
}

这里可以对比一下,Spring-data设计的Page接口,哪怕它自己只有一个实现类,但相对于PageHelper这个插件的PageInfo实体类显然更具有扩展性,这也可以体现出Spring框架之所以能包容万象的魅力所在。

模板方法 抽取公共逻辑

说到抽取公共逻辑,减少冗余代码,想到的应该更多是抽取公共方法和工具类,其实模板方法设计模式也可以做这件事情,但严格来讲它和常规的抽取方法有所不同,它侧重于抽取流程框架/算法骨架,而让具体的分支细节由不同的子类去实现,从这点上看它和抽取方法所达到的效果是不同的。

所以这套设计其实也没什么模板和特定的使用场景,只能说在设计的时候可以多一种思路,多一份意识,并有意为之,有时候甚至可以把它当作万能胶水,配合工厂和策略能设计出更美妙的艺术。

构造器 不定参数构造

一般来说几个参数就直接定义,多个固定参数就封装实体,但参数可多可少,可有可无,这时候就免不了考虑构造器模式了。

在一般框架中,构造器还是用的挺多的,而且要构造的对象不仅仅是传统意义上的"对象",还有可能是String或者特定集合的包装。用好构造器模式,你就可以享受函数式编程的快感,更有甚者,可以参考Java8中的stream API流式编程,你会得到双份的快乐。

装饰者+代理模式 批量增强

装饰者模式可以做到单个方法或单个类的增强,而要想多个方法或多个类一起增强,那就少不了代理模式了,其实严格意义上说并不存在装饰+代理这种组合,因为代理模式本身就需要包装,它没有那么强大的能力让对象本身自行增强,在这里提出来也就想要强调一点:代理必包装。

代理模式谈起来只是一个抽象,动态代理更是这个抽象上的一层分支,在java中它的实现主要有两种形态,这里鉴于大家平时用得比较少,就简单来过一遍:

JDK代理

JDK本身就已经实现了一套自带的代理工具,它的代理需要基于被代理对象有接口实现,首先需要定义一个增强器,实现InvocationHandler接口,在它里面实现需要增强的操作

public class JDKProxy implements InvocationHandler {

    private Object target; // 传入目标代理对象

    /**
    * @param target: 被代理对象,从这里也体现出一种包装,但真正的包装不是这个类,这个类只是个增强器,包装的目的是让代理方法能调用到被代理对象的方法
    **/
    public JDKProxy(Object target) {
        this.target = target;
    }

    /**
    * @param proxy: 经过代理后的对象
    * @param method: 需要被代理的方法,这里指的是被代理对象实现的所有接口方法
    * @param args: 方法参数
    **/
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("方法执行前....");
        Object invoke = method.invoke(target, args);
        System.out.println("方法执行后!!!");
        return invoke;
    }
}

有了这个增强器后,接下来就是把被代理对象搭配增强器生成最终的代理对象,这里就需要用到JDK的代理工具类Proxy

IUserDao userDao = new UserDaoImpl();
InvocationHandler jdkProxy = new JDKProxy(userDao);
ClassLoader classLoader = userDao.getClass().getClassLoader();
Class<?>[] interfaces = userDao.getClass().getInterfaces();

// 通过JDK的代理工具类Proxy创建出了最终被代理对象
IUserDao proxyUserDao = (IUserDao) Proxy.newProxyInstance(classLoader, interfaces, jdkProxy);

通过newProxyInstance源码可以看出,里面其实做了三个核心的步骤:

// 创建动态代理类的Class对象
Class<?> proxyClazz = Proxy.getProxyClass(classLoader, interfaces);
// 通过反射技术获得动态代理类的构造函数
Constructor<?> constructor = proxyClazz.getConstructor(new Class[]{InvocationHandler.class});
// 通过构造函数创建出代理对象进行调用
IUserDao proxyUserDao =  (IUserDao) constructor.newInstance(new Object[]{jdkProxy});

后两个步骤知道反射操作的都可以理解,核心是在第一步操作getProxyClass,继续深入理解它就知道,它的本质原理就是生成一个新的类,这个类将实现被代理对象接口的所有方法(同时还包括Objectequals/hashCode/toString),为每个方法重写并统一调用增强器中定义的方法,最后通过classloader加载进来并使用。

所以其实动态代理最终也是变成静态代理,再本质点就是包装,只不过开发者懒,或者说不想写那么多重复代码,于是便让JVM帮忙写,就演变成看似神奇又很牛逼的技术。

cglib代理

cglib代理也是动态代理的一种实现,它是解决JDK代理中被代理对象必须依赖于接口的局限,cglib代理能给任意对象进行代理,而且其实现原理是直接基于字节码操作一步到位的,有学者认为会更高效,当然在性能上的争议不是这里需要讨论的问题,大家只要知道它不需要依赖接口实现即可。

使用它需要引入cglib包,它的使用思路也是跟JDK代理一致,也需要创建一个增强器(实现MethodInterceptor)并做一层包装,这里其实可以放在一起操作:

public class CglibProxy implements MethodInterceptor {

    private Object target;

    public Object getInstance(Object target) {
        this.target = target;
        Enhancer enhancer = new Enhancer();
        // 1. 设置代理对象为被代理对象的子类
        enhancer.setSuperclass(target.getClass());
        // 2. 重写被代理对象的方法
        enhancer.setCallback(this);
        // 3. 创建出被代理对象
        return enhancer.create();
    }

    public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        System.out.println("代理后对象:" + o.getClass());
        Object result = method.invoke(target, args);
        System.out.println("执行结束!!!");
        return result;
    }
}

这段代码可以成为模板代码,甚至可以改造成单例后做成工具类,为每一个需要代理的对象做一层包装。cglib代理的原理要想深入理解起来确实有点复杂,但其实可以简单理解成就是生成一个新的类继承了被代理对象,重写了所有被代理对象的方法实现,使之调用了增强器定义的方法。

以上两种都是动态代理的基础实现,实际用到的地方也不多,所以不是这里要介绍的重点,因为有个更强大的代替实现方案,那还是依赖于Spring的AOP代理

spring AOP代理

严谨一点:AOP其实是面向切面编程的思想,不属于代理模式的范畴

以上的代理能做到的是对方法的批量增量,而要想做到对多个类的批量增量,那还可以借助更强大的基于容器管理的替代方案:Spring的AOP代理。

先来看下用法,这里介绍的是基于注解的用法,因此除了spring-context外还需要spring-aspects依赖

首先定义好切面类,所谓切面,即包含切点和通知两个要素,理解这两个概念对AOP实现有很大帮助。

@Aspect
public class LogAspect {

    /**
     * 切点表达式,通常是一个空方法,标记@Ponitcut
     */
    @Pointcut("execution(public * com.edu.springlearn.aop.*.*(..))")
    public void pointCut() {

    }

    /**
     * 前置通知@Before,方法调用前执行
     * @param joinPoint 连接点对象,可以从中获取到对应的方法签名/类信息
     */
    @Before("pointCut()")
    public void logStart(JoinPoint joinPoint) {
        String name = joinPoint.getSignature().getName();
        List<Object> args = Arrays.asList(joinPoint.getArgs());
        System.out.println("BusinessService start, methodName is "+ name +" ,param is" + args);
    }

    /**
     * 结束通知,无论正常还是异常都会执行
     */
    @After("com.edu.springlearn.aop.LogAspect.pointCut()")
    public void logEnd() {
        System.out.println("BusinessService end");
    }

    /**
     * 返回通知,在方法正常返回时执行
     * @param joinPoint
     * @param result  返回结果
     */
    @AfterReturning(value = "pointCut()", returning = "result")
    public void logReturn(JoinPoint joinPoint, Object result) {
        System.out.println("BusinessService success return, method name is : "+ joinPoint.getSignature().getName() +" ,result is" + result);
    }

    /**
     * 异常通知,方法执行异常时执行
     * @param ex
     */
    @AfterThrowing(value = "pointCut()", throwing = "ex")
    public void logException(Exception ex) {
        System.out.println("BusinessService occur an exception: " + ex.getMessage());
        ex.printStackTrace();
    }

}

以上切面类介绍了几个必要的注解@Aspect表示切面,@Pointcut表示切点,通知类注解包括@Before/@After/@AfterReturning/@AfterThrowing以及还有更强大的环绕通知@Around

除此之外,还需要将写好的这个切面类注册到Spring容器中去,至于怎么注册那就有很多办法了,自己选择。

最后最关键的地方在于开启@EnableAspectJAutoProxy注解功能,必须要在启动类上带上这个注解,当然现代的springboot项目以及默认带上了。

因此整个AOP实现原理都在这个@EnableAspectJAutoProxy注解上,这里就简单快速过一遍AOP的实现原理,先来看一张整体的时序图:

SpringAOP.png

@EnableAspectJAutoProxy注解上标记了一个@Import(AspectJAutoProxyRegistrar.class)AspectJAutoProxyRegistrar这个类是ImportBeanDefinitionRegistrar的实现类,在我另外一个专题:Spring注解驱动开发一节会阐述,它会在Spring注册阶段执行registerBeanDefinitions方法,因此就会执行关键代码:

AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry);

跟进这段代码,就会发现它额外注册了另一个关键类:AnnotationAwareAspectJAutoProxyCreator这个bean的名称是org.springframework.aop.config.internalAutoProxyCreator

AnnotationAwareAspectJAutoProxyCreator.png

而从这个类AnnotationAwareAspectJAutoProxyCreator继承关系可以知道,它是SmartInstantiationAwareBeanPostProcessor的实现类,在我另外一个专题:Spring注解驱动开发一节会阐述,它是一个经典的后置处理器,但它处理的阶段是在Springbean的创建阶段

那具体是在那个地方实现的呢?可以通过AbstractAutowireCapableBeanFactory#createBean源码中有这么一段代码和一段注释:

// Give BeanPostProcessors a chance to return a proxy instead of the target bean instance.
Object bean = resolveBeforeInstantiation(beanName, mbdToUse);
if (bean != null) {
    return bean;
}

也就是说,剩余的所有springbean在创建前都会经历一遍InstantiationAwareBeanPostProcessor后置处理器的洗礼,如果它能返回一个代理对象,则直接返回,不会再经历正常的创建过程。

Object beanInstance = doCreateBean(beanName, mbdToUse, args);

因此最后我们很自然地跟到AbstractAutoProxyCreator#postProcessBeforeInstantiation方法,在里面有个关键方法:shouldSkip里的findCandidateAdvisors()最后来到:BeanFactoryAspectJAdvisorsBuilder#buildAspectJAdvisors

到这一步,它会将所有候选的@Aspect类中的增强器找出来,最终返回一个List<Advisor>增强器集合的结果。其中Advisor就是我们定义出来的通知方法的一个包装。

想知道具体怎么找出来的,可以继续跟进,如果只是想知道主流程,那建议跳过

如果继续跟进,可以看到它会遍历所有已注册的bean,拿到Class类型进入判断:this.advisorFactory.isAspect(beanType),这里就可以筛选出哪些是切面注解的类了。接着对于切面类又继续进入this.advisorFactory.getAdvisors(factory)在里面真正能把通知方法筛选出来的是AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(candidateAdviceMethod)内的实现:

private static final Class<?>[] ASPECTJ_ANNOTATION_CLASSES = new Class<?>[] {
            Pointcut.class, Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.class};
/**
* Find and return the first AspectJ annotation on the given method(作者注:其中是已经过滤掉Pointcut的)
*/
protected static AspectJAnnotation<?> findAspectJAnnotationOnMethod(Method method) {
    for (Class<?> clazz : ASPECTJ_ANNOTATION_CLASSES) {
        AspectJAnnotation<?> foundAnnotation = findAnnotation(method, (Class<Annotation>) clazz);
        if (foundAnnotation != null) {
            return foundAnnotation;
        }
    }
    return null;
}

最后找到的结果都会包装成Advisor其具体实现类描述:

return new InstantiationModelAwarePointcutAdvisorImpl(expressionPointcut, candidateAdviceMethod,
this, aspectInstanceFactory, declarationOrderInAspect, aspectName);

0 = {InstantiationModelAwarePointcutAdvisorImpl@2004} "InstantiationModelAwarePointcutAdvisor: expression [pointCut()]; advice method [public void com.edu.springlearn.aop.LogAspect.logStart(org.aspectj.lang.JoinPoint)]; perClauseKind=SINGLETON"
1 = {InstantiationModelAwarePointcutAdvisorImpl@2101} "InstantiationModelAwarePointcutAdvisor: expression [com.edu.springlearn.aop.LogAspect.pointCut()]; advice method [public void com.edu.springlearn.aop.LogAspect.logEnd()]; perClauseKind=SINGLETON"
2 = {InstantiationModelAwarePointcutAdvisorImpl@2353} "InstantiationModelAwarePointcutAdvisor: expression [pointCut()]; advice method [public void com.edu.springlearn.aop.LogAspect.logReturn(org.aspectj.lang.JoinPoint,java.lang.Object)]; perClauseKind=SINGLETON"
3 = {InstantiationModelAwarePointcutAdvisorImpl@2354} "InstantiationModelAwarePointcutAdvisor: expression [pointCut()]; advice method [public void com.edu.springlearn.aop.LogAspect.logException(java.lang.Exception)]; perClauseKind=SINGLETON"

前面说到,在创建阶段后置处理器会优先执行,如果有返回bean则直接返回,则不再进入正常的bean创建过程。

protected Object resolveBeforeInstantiation(String beanName, RootBeanDefinition mbd) {
    // 省略了其它干扰代码...
    Object bean = null;
    if (targetType != null) {
    bean = applyBeanPostProcessorsBeforeInstantiation(targetType, beanName);
        if (bean != null) {
            bean = applyBeanPostProcessorsAfterInitialization(bean, beanName);
        }
    }
    return bean;
}

但遗憾的是这里真正代理后的bean不是在这里返回的,这个前置处理器的主要任务就是找出了所有增强器而已。因此这些bean还是会经历正常的创建过程,只不过在初始化阶段会进入AbstractAutoProxyCreator#postProcessAfterInitialization后置处理器处理,一个很明显的代码标示就在这里了:

/**
* Create a proxy with the configured interceptors if the bean is identified as one to proxy by the subclass.
* @see #getAdvicesAndAdvisorsForBean
*/
@Override
public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
    if (bean != null) {
        Object cacheKey = getCacheKey(bean.getClass(), beanName);
        if (this.earlyProxyReferences.remove(cacheKey) != bean) {
            return wrapIfNecessary(bean, beanName, cacheKey);
        }
    }
    return bean;
}

其中wrapIfNecessary实现有两个关键操作:

Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null); // 第一步:根据所有通知方法的切点表达式判断该bean能应用哪些通知方法(增强器)
if (specificInterceptors != DO_NOT_PROXY) {
    this.advisedBeans.put(cacheKey, Boolean.TRUE);
    Object proxy = createProxy(
            bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean)); // 创建代理对象
    this.proxyTypes.put(cacheKey, proxy.getClass());
    return proxy;
}

跳过第一步,来看第二步操作,最关键的地方在于DefaultAopProxyFactory#createAopProxy

@Override
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
    if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
        Class<?> targetClass = config.getTargetClass();
        if (targetClass == null) {
            throw new AopConfigException("TargetSource cannot determine target class: " +
                    "Either an interface or a target is required for proxy creation.");
        }
        if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
            return new JdkDynamicAopProxy(config);
        }
        return new ObjenesisCglibAopProxy(config);
    }
    else {
        return new JdkDynamicAopProxy(config);
    }
}

到这里就可以看到一些很熟悉的影子,spring会根据bean的类型自动判断使用JDK代理还是CGLIB代理。以CGLIB代理为例,继续进入CglibAopProxy#getCallbacks就知道,它其实设定了DynamicAdvisedInterceptorMethodInterceptorcallback,所以到这里是不是已经有了CGLIB熟悉的味道了?

总结一下:首先由AbstractAutoProxyCreator这个后置处理器对容器创建的bean做了一层包装wrapIfNecessary,这个方法内会根据被代理对象的特性决定使用JDK动态代理还是CGLIB动态代理。

如果使用CGLIB动态代理,使用的是CglibAopProxygetProxy方法会返回enhance.create返回的对象,并设置callbackDynamicAdvisedInterceptorMethodInterceptor

这样一来,容器中获取到的被代理对象就为CGLIB代理后的对象了,接下来当调用这个对象中符合切点表达式的方法时,自然会进入DynamicAdvisedInterceptorintercept方法,那么Spring是如何设计使得这些杂乱无章的增强器能够按照用户给定希望的顺序来执行呢?预知更多精彩,请看下节分解。

责任链模式

道理很简单,用着很高端,这种神奇的效果以至于你只能在源码里头才能看得到它的影子,因此上面拖了一大段在叙述SpringAOP的原理,只不过为了引出这种设计模式

接着上节,继续分析DynamicAdvisedInterceptorintercept方法,首先通过下面语句获得了一条增强器链,通过上节分析可知,所谓增强器链就是被代理bean可适配的通知方法

List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);

最后获取到的增强器链就如下所示:

0 = {ExposeInvocationInterceptor@1966}
1 = {AspectJAfterThrowingAdvice@1967} "org.springframework.aop.aspectj.AspectJAfterThrowingAdvice: advice method [public void com.edu.springlearn.aop.LogAspect.logException(java.lang.Exception)]; aspect name 'logAspect'"
2 = {AfterReturningAdviceInterceptor@1968}
3 = {AspectJAfterAdvice@1969} "org.springframework.aop.aspectj.AspectJAfterAdvice: advice method [public void com.edu.springlearn.aop.LogAspect.logEnd()]; aspect name 'logAspect'"
4 = {MethodBeforeAdviceInterceptor@1970}

接下来核心步骤就是进入CglibMethodInvocation#proceed方法了,把它整一块贴出来看看:

public Object proceed() throws Throwable {
    // We start with an index of -1 and increment early.
    if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
        return invokeJoinpoint();
    }

    Object interceptorOrInterceptionAdvice =
            this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
    if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) {
        // Evaluate dynamic method matcher here: static part will already have
        // been evaluated and found to match.
        InterceptorAndDynamicMethodMatcher dm =
                (InterceptorAndDynamicMethodMatcher) interceptorOrInterceptionAdvice;
        Class<?> targetClass = (this.targetClass != null ? this.targetClass : this.method.getDeclaringClass());
        if (dm.methodMatcher.matches(this.method, targetClass, this.arguments)) {
            return dm.interceptor.invoke(this);
        }
        else {
            // Dynamic matching failed.
            // Skip this interceptor and invoke the next in the chain.
            return proceed();
        }
    }
    else {
        // It's an interceptor, so we just invoke it: The pointcut will have
        // been evaluated statically before this object was constructed.
        return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);
    }
}

首先是判断当前增强器链索引已经是最后一个时,执行被代理对象的对应方法:invokeJoinpoint。当然,由于一开始索引值为-1,所以不会执行。

接下来无论那个分支,其实都是进入每一个MethodInterceptorinvoke方法,这个方法传入的参数是this

索引值递增1,取第零个MethodInterceptor,它是默认的ExposeInvocationInterceptor,看看他的实现:

@Override
public Object invoke(MethodInvocation mi) throws Throwable {
    MethodInvocation oldInvocation = invocation.get();
    invocation.set(mi);
    try {
        return mi.proceed();
    }
    finally {
        invocation.set(oldInvocation);
    }
}

它好像没做什么,直接执行了mi.proceed()其中mi是刚才传入的参数this,这不就又递归回到了上面标记的函数去了吗?

所以接下来要做的事情也一样,判断当前增强器链索引已经是最后一个时,执行被代理对象的对应方法:invokeJoinpoint,此时的索引值是零,由于还有其他增强器,所以不会执行。

索引值递增1,取第一个增强器是AspectJAfterThrowingAdvice来看看它的invoke实现:

@Override
public Object invoke(MethodInvocation mi) throws Throwable {
    try {
        return mi.proceed();
    }
    catch (Throwable ex) {
        if (shouldInvokeOnThrowing(ex)) {
            invokeAdviceMethod(getJoinPointMatch(), null, ex);
        }
        throw ex;
    }
}

这个实现似乎也一样,直接调用了mi.proceed(),只有当它异常时才会调用异常通知所保存的通知方法invokeAdviceMethod。mi是刚才传入的参数this,这不就又递归回到了上面标记的函数去了吗?

所以接下来要做的事情也一样,判断当前增强器链索引已经是最后一个时,执行被代理对象的对应方法:invokeJoinpoint,此时的索引值是1,由于还有其他增强器,所以不会执行。

索引值递增1,取第二个增强器是AfterReturningAdviceInterceptor来看看它的invoke实现:

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

非常简单,也是先调用mi.proceed()执行完后再去回调AfterReturningAdviceInterceptor所保存的正常返回的通知方法。mi是刚才传入的参数this,这不就又递归回到了上面标记的函数去了吗?

所以接下来要做的事情也一样,判断当前增强器链索引已经是最后一个时,执行被代理对象的对应方法:invokeJoinpoint,此时的索引值是2,由于还有其他增强器,所以不会执行。

索引值递增1,取第三个增强器是AspectJAfterAdvice来看看它的invoke实现:

try {
    return mi.proceed();
}
finally {
    invokeAdviceMethod(getJoinPointMatch(), null, null);
}

同样也是先直接调用mi.proceed(),执行完最终才会调用该advice所保存的通知方法。mi是刚才传入的参数this,这不就又递归回到了上面标记的函数去了吗?

所以接下来要做的事情也一样,判断当前增强器链索引已经是最后一个时,执行被代理对象的对应方法:invokeJoinpoint,此时的索引值是3,由于还有其他增强器,所以不会执行。

索引值递增1,取第四个增强器是MethodBeforeAdviceInterceptor来看看它的invoke实现:

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

此时不一样了,它先调用了该AdviceInterceptor对应的通知方法,也就是@Before标记的前置通知,然后再进行调用mi.proceed()。mi是刚才传入的参数this,这不就又递归回到了上面标记的函数去了吗?

所以接下来要做的事情也一样,判断当前增强器链索引已经是最后一个时,执行被代理对象的对应方法:invokeJoinpoint,此时的索引值是4,已经是最后一个了,所以调用完成了。

此时mi.proceed()执行返回了,随即回到上一个MethodBeforeAdviceInterceptor也对应成功返回了,接着又回到由AspectJAfterAdvice调用的mi.proceed()也成功返回了,接着AspectJAfterAdviceinvoke函数也正常执行完成,调用finally代码块函数执行了通知方法。接着回到了由AfterReturningAdviceInterceptor调用的mi.proceed()也成功返回了,接着AspectJAfterAdviceinvoke函数执行了后面的通知方法,并返回了执行结果。接着回到了AspectJAfterThrowingAdvice调用的mi.proceed()也成功返回了,接着AspectJAfterAdviceinvoke函数没有异常抛出,catch代码块不会执行。最后就是默认的adviceInterceptor,也成功执行完成了。

面向切面编程是构建大型项目或框架封装都少不了的重要思想,在业务上日志记录/权限拦截都可能需要它,在框架上事务控制/RPC远程调用也都少不了它。Spring结合它自身强大的容器实现的AOP,在神奇之余也困扰着大众开发者,例如最经典的事务注解@Transactional,用着它却抱怨事务不生效,虽然归总起来原因有很多,但实际上大多数情况都是一个大原则没把握住,那就是:代理必装饰

观察者模式

观察者模式这词眼确实挺拗口的,它更像是一种事件委派机制或者说是一种通知响应机制,它的主要目的就是为了解耦并方便扩展,在前端开发的领域可能更有深刻体会,例如在用户点击鼠标事件时需要通知到各个事件回调函数去处理,或例如在Vue中当js变量发生变化时需要通知到视图中使用到这个变量的位置都发生改变。

观察者模式的实现本身不难,核心就是要设计一个观察者并维护一个观察者对象的集合,调用观察者的事件触发函数时需要将集合中的每个对象进行响应回调,在JDK中就已经封装了观察者模式的实现,主要核心类就是Observable观察者

// 这里来研究观察者的核心源码
public class Observable {
    private boolean changed = false;
    private Vector<Observer> obs; // 观察者对象集合

    /** Construct an Observable with zero Observers. */
    public Observable() {
        obs = new Vector<>();
    }

    public synchronized void addObserver(Observer o) {
        // 观察者对象维护方法之 添加观察者对象 (这例如在js中的addEventListener)
        obs.addElement(o);
    }

    public synchronized void deleteObserver(Observer o) {
        // 观察者对象维护方法之 删除观察者对象 (这例如在js中的removeEventListener)
        obs.removeElement(o);
    }

    public void notifyObservers() {
        notifyObservers(null);
    }

    public void notifyObservers(Object arg) {
        // 派发通知的方法 (这例如在js中的各种事件函数click等)
        for (int i = arrLocal.length-1; i>=0; i--)
            ((Observer)arrLocal[i]).update(this, arg);
    }
}

那观察者对象是什么呢,它其实就是一个接口定义,实现了这个接口的都可以作为观察者对象:

public interface Observer {
    // 事件响应回调
    void update(Observable o, Object arg);
}

以上就是JDK观察者的基本实现,可以看到还是非常好理解的,但是在实际项目中确并不会用这种默认实现的观察者,这种基本实现很明显可以看出几个问题:

  1. 为了保证线程安全而使用了重量级的synchronized + Vector 方式(先不说其本身的优化)。
  2. Observer的回调中直接将Observable作为参数传递本身不够优雅,在回调中又开放了adddelete方法。
  3. 要手动区分不同类型的事件,虽然可以通过创建多个Observable解决,但缺乏语义,不够优雅。
  4. 扩展性还有优化空间,新增或删除观察者对象时仍需手动维护Observable,也不够优雅。
  5. 事件响应的方式是同步的,更多希望是有异步支持。

为此基于强大容器加持的Spring设计的观察者模式才会是实际真正可用的模式,也是本文所想介绍的重点,先来看看基于Spring的观察者应如何开发:

先来看下最基本的事件响应接口实现

public class SpringEventListener implements ApplicationListener<ApplicationEvent> {

    @Override
    public void onApplicationEvent(ApplicationEvent event) {
        System.out.println("收到事件 :" + event.getClass());
        System.out.println("事件参数 :" + event.getSource());
    }
}

当然这个类要起作用也需要注册到spring容器中去

ApplicationEvent是事件类型的接口,按照这种默认的方式在最基础的容器可以收到两大事件,分别为容器启动完成事件和容器销毁事件。

收到事件 :class org.springframework.context.event.ContextRefreshedEvent
事件参数 :org.springframework.context.annotation.AnnotationConfigApplicationContext@17f052a3, started on Sat Oct 17 23:01:09 CST 2020
收到事件 :class org.springframework.context.event.ContextClosedEvent
事件参数 :org.springframework.context.annotation.AnnotationConfigApplicationContext@17f052a3, started on Sat Oct 17 23:01:09 CST 2020

以上是Spring默认给出的事件,那我们又如何定义自己的事件并派发和响应呢?

首先先来自定义个事件:

public class MyEvent extends ApplicationEvent {


    /**
     * Create a new {@code ApplicationEvent}.
     *
     * @param source the object on which the event initially occurred or with which the event is associated (never {@code null})
     */
    public MyEvent(Object source) {
        super(source);
    }
}

接着在业务代码里如何派发这个事件呢,要想派发事件就需要有事件派发器ApplicationEventPublisher,而这个事件派发器可以通过Aware接口注入,也可以通过@Autowire注解自动注入,因为它是Spring的内部bean(实际上就是ApplicationContext)

@Service
public class MyEventPublisher implements ApplicationEventPublisherAware {

    //@Autowired
    private ApplicationEventPublisher eventPublisher;

    public void publishMyEvent() {
        eventPublisher.publishEvent(new MyEvent("我的自定义事件"));
    }

    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
        this.eventPublisher = applicationEventPublisher;
    }
}

那么在响应自定义事件时就可以按照上面的方式进行响应了:

@Service
public class MyEventListener implements ApplicationListener<MyEvent> {


    @Override
    public void onApplicationEvent(MyEvent event) {
        System.out.println("收到事件 :" + event.getClass());
        System.out.println("事件参数 :" + event.getSource());
    }
}

这样以后要扩展事件监听器的时候只需要新增一个类,并将其注册到Spring容器中即可,不需要改变原有的业务代码。

那么Spring的事件派发和响应又是什么个原理呢?换句话说Spring如何利用自己强大的容器优势实现这种观察者模式的呢?接下来我们进入publishEvent方法一探究竟。

很快我们就进入到了AbstractApplicationContext.publishEvent方法:

protected void publishEvent(Object event, @Nullable ResolvableType eventType) {
    Assert.notNull(event, "Event must not be null");

    // Decorate event as an ApplicationEvent if necessary
    ApplicationEvent applicationEvent;
    if (event instanceof ApplicationEvent) {
        applicationEvent = (ApplicationEvent) event;
    }
    else {
        applicationEvent = new PayloadApplicationEvent<>(this, event);
        if (eventType == null) {
            eventType = ((PayloadApplicationEvent<?>) applicationEvent).getResolvableType();
        }
    }

    // Multicast right now if possible - or lazily once the multicaster is initialized
    if (this.earlyApplicationEvents != null) {
        this.earlyApplicationEvents.add(applicationEvent);
    }
    else {
        getApplicationEventMulticaster().multicastEvent(applicationEvent, eventType);
    }

    // Publish event via parent context as well...
    if (this.parent != null) {
        if (this.parent instanceof AbstractApplicationContext) {
            ((AbstractApplicationContext) this.parent).publishEvent(event, eventType);
        }
        else {
            this.parent.publishEvent(event);
        }
    }
}

先来关注其重点代码:

getApplicationEventMulticaster().multicastEvent(applicationEvent, eventType);

这是一个事件广播器,其默认初始化的是SimpleApplicationEventMulticaster,进入其multicastEvent方法

public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
    ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
    Executor executor = getTaskExecutor();
    for (ApplicationListener<?> listener : getApplicationListeners(event, type)) {
        if (executor != null) {
            executor.execute(() -> invokeListener(listener, event));
        }
        else {
            invokeListener(listener, event);
        }
    }
}

首先可以看到它会匹配所有该事件类型的事件监听器:

getApplicationListeners(event, type)

其次会去找异步处理器

getTaskExecutor()

如果能找到则通过异步的方式回调事件响应invokeListener,如果没有则通过同步的方式响应invokeListener,最终都会回调自己写的事件响应方法了。

那么事件广播器EventMulticaster又是怎么初始化的呢?同时事件广播器又如何注册所有的EventListener监听器的呢?这就得回到容器初始化的代码开始走起了:

来进入AbstractApplicationContext.refresh方法,里面有两个阶段是值得关注的:

@Override
public void refresh() throws BeansException, IllegalStateException {
// ... 省略万行代码
    // Initialize event multicaster for this context.
    initApplicationEventMulticaster();

    // Initialize other special beans in specific context subclasses.
    onRefresh();

    // Check for listener beans and register them.
    registerListeners();
// ... 省略万行代码
}

函数字面意思已经解释得很清楚了,initApplicationEventMulticaster就是初始化事件广播器:

protected void initApplicationEventMulticaster() {
    ConfigurableListableBeanFactory beanFactory = getBeanFactory();
    if (beanFactory.containsLocalBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME)) {
        this.applicationEventMulticaster =
                beanFactory.getBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, ApplicationEventMulticaster.class);
    }
    else {
        this.applicationEventMulticaster = new SimpleApplicationEventMulticaster(beanFactory);
        beanFactory.registerSingleton(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, this.applicationEventMulticaster);
    }
}

可以看着这个方法很简单,如果容器中有APPLICATION_EVENT_MULTICASTER_BEAN_NAME定义的广播器则用它,如果没有则使用默认的广播器SimpleApplicationEventMulticaster并注册到容器中(保证了广播器的单例)。

接下来registerListeners就是为这个广播器注册所有的Listener

protected void registerListeners() {
    // Register statically specified listeners first.
    for (ApplicationListener<?> listener : getApplicationListeners()) {
        getApplicationEventMulticaster().addApplicationListener(listener);
    }

    // Do not initialize FactoryBeans here: We need to leave all regular beans uninitialized to let post-processors apply to them!
    String[] listenerBeanNames = getBeanNamesForType(ApplicationListener.class, true, false);
    for (String listenerBeanName : listenerBeanNames) {
        getApplicationEventMulticaster().addApplicationListenerBean(listenerBeanName);
    }

    // Publish early application events now that we finally have a multicaster...
    Set<ApplicationEvent> earlyEventsToProcess = this.earlyApplicationEvents;
    this.earlyApplicationEvents = null;
    if (earlyEventsToProcess != null) {
        for (ApplicationEvent earlyEvent : earlyEventsToProcess) {
            getApplicationEventMulticaster().multicastEvent(earlyEvent);
        }
    }
}

这里首先通过getApplicationListeners得到的监听器放进广播器中,当然一开始没有,其次通过getBeanNamesForTypebeanName放进广播器中,这里就有了一个细节问题:为什么上面是将Listener实例对象放进广播器的,那下面为什么只放beanName呢?为什么不通过getBean方法将实例对象创建出来放进广播器里去呢?同时这一段注释又说明了什么?咋们先存下这些问题待会再说

Do not initialize FactoryBeans here: We need to leave all regular beans uninitialized to let post-processors apply to them!

EventListener 和 SmartInitializingSingleton

到这里看似这个原理分析就结束了,但是仔细一看还有一个问题,从容器初始化refresh流程可以看出,Listener 的初始化是先于大部分业务bean的(业务bean的初始化理应在finishBeanFactoryInitialization阶段),但我们实际运用的时候常常会将Listener作为业务bean使用的,这时候就触犯了一个禁区,如果业务bean被错误地提前初始化,会使得某些未得初始化的后置处理器postProcessors无法应用(疯狂暗示),这样创建出来的bean是不完整的,也是很危险的,会使得业务bean的部分功能缺失,例如@Autowire/@Transactional注解失效等。

@Override
public void refresh() throws BeansException, IllegalStateException {
// ... 省略万行代码
    // Check for listener beans and register them.
    registerListeners();

    // Instantiate all remaining (non-lazy-init) singletons.
    finishBeanFactoryInitialization(beanFactory);

    // Last step: publish corresponding event.
    finishRefresh();
// ... 省略万行代码
}

这也是在开发Spring应用需要注意的问题,即要将实现特殊接口的bean和业务bean分开,不能耦合在一起。

为此Spring也考虑到了,并为我们设计出了一个新的方式可以在业务bean中监听事件处理,那就是@EventListener注解,这也是实际运用比较多的一种方式。

先来看一下使用@EventListener注解监听方式:

@Service
public class BusiEventListener {
    
    @Autowired
    private BusinessService businessService;
    
    @EventListener(classes = {MyEvent.class})
    public void listenEvent(MyEvent myEvent) {
        System.out.println(myEvent.getSource());
        businessService.div(2, 1);    
    }
    
}

其中可以通过classes属性来指定需要监听的事件类型

那么这个注解又是如何监听的呢?它主要通过EventListenerMethodProcessor完成处理的,来研究一下这个处理器,它是一个接口的实现类SmartInitializingSingleton,而这个接口是干嘛,又是如何工作的呢?来看一下其注释介绍

Callback interface triggered at the end of the singleton pre-instantiation phase during {@link BeanFactory} bootstrap. This interface can be implemented by singleton beans in order to perform some initialization after the regular singleton instantiation algorithm, avoiding side effects with accidental early initialization

This callback variant is somewhat similar to ContextRefreshedEvent but doesn't require an implementation of ApplicationListener

Invoked right at the end of the singleton pre-instantiation phase, with a guarantee that all regular singleton beans have been created already.

粗略用不标准的英文翻译解释就是:在保证所有单例bean创建完成后触发,这个时机类似于容器刷新完成事件触发的时机,但不需要实现ApplicationListener监听器。

说了那么多那具体是哪里呢?通过创建剩余单例bean过程进入到这个方法:DefaultListableBeanFactory.preInstantiateSingletons 的最后一段

public void preInstantiateSingletons() throws BeansException {

    List<String> beanNames = new ArrayList<>(this.beanDefinitionNames);

    // Trigger initialization of all non-lazy singleton beans...
    for (String beanName : beanNames) {
        // ...此处省略万行代码
    }

    // Trigger post-initialization callback for all applicable beans...
    for (String beanName : beanNames) {
        Object singletonInstance = getSingleton(beanName);
        if (singletonInstance instanceof SmartInitializingSingleton) {
            final SmartInitializingSingleton smartSingleton = (SmartInitializingSingleton) singletonInstance;
            if (System.getSecurityManager() != null) {
                AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
                    smartSingleton.afterSingletonsInstantiated();
                    return null;
                }, getAccessControlContext());
            }
            else {
                smartSingleton.afterSingletonsInstantiated();
            }
        }
    }
}

此处可以看到两顿遍历,在第一次遍历的时候就已经初始化所有的单例bean了,接着又来一顿遍历,这个时候就可以看到SmartInitializingSingleton接口的回调触发时机了。

接着再回头看这个类EventListenerMethodProcessor的回调,这个方法afterSingletonsInstantiated所做之事非常之多,很多不是本节所需要介绍的重点,只需要看到最后有处理applicationListener并添加进容器中的广播器操作就可以了。

context.addApplicationListener(applicationListener);

最后再来回看之前存留的问题(其实已经疯狂暗示了),先来对比以下基于接口的方式和基于注解方式实现的Listener在注册阶段的不同,基于接口方式的Listener是在业务bean初始化之前注册的,但为了避免业务bean被提前错误地初始化,注册Listener时只是将beanName注册好,延后到getListeners的时候才真正初始化了Listener。这种巧妙的处理在极大多数情况都不会有问题 (自己脑补在极端情况的问题) ,但基于注解的方式恰好是在所有单例bean初始化完成时机进行注册工作的,所注册的直接就是Listener实例对象,这种恰到好处的做法也许也是它被更推荐的原因吧~

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