Spring4.x 笔记(13):Spring 编程式 AOP

问题回顾

上一节,讲解了一Spring AOP 实现的原理,如动态代理、CGLib。具体有一些问题需要解决:

  1. Advice 增强没有分类。通过硬编码的方式指定织入横切逻辑,在目标方法的开始前、开始后织入代码,没有灵活实现各个不同的横切,如方法前、方法后、异常等。
  2. Pointcut 不灵活。目标类的所有方法都添加了监控的横切逻辑,要不所有方法都监控,要不都不监控,没有实现灵活的切点。
  3. 代理创建麻烦。手工编写代码获取实例的过程,对于不同的类创建代理时,需要使用不同的方式(动态代理和 CGLib )

Spring 编程式 AOP 的主要工作就是围绕以上三点问题展开的。

  1. 通过 Advice 描述横切逻辑和方法的具体织入点(方法前、方法后、方法的两端等)
  2. 通过 Pointcut 指定在哪些类的哪些方法上织入 Advice
  3. 通过 Advisor 切面将 Pointcut 和 Advice 组装起来

Spring Advice 增强

  1. Spring 使用Advice类定义横切逻辑,并且包含在哪一个点加入横切代码的方位信息(一部分连接点信息)
  2. Spring 只支持方法连接点,即方法级别的 aop实现(引介除外)

Advice 类型

  1. AOP联盟定义了标准的Advice接口 org.aopalliance.aop.Advice,Spring 支持5中类型的Advice,都是实现自该接口

Advice

  1. 具体5类 Advice 为:
  • 前置。org.springframework.aop.BeforeAdviceorg.springframework.aop.MethodBeforeAdvice 代表前置Advice,表示在方法执行前实施 Advice。真正实现可以继承MethodBeforeAdvice,而BeforeAdvice是为后期扩展而定义的

  • 后置。org.springframework.aop.AfterAdviceorg.springframework.aop.AfterReturningAdvice 代表后置 Advice,表示在方法执行后实施Advice。

  • 环绕。org.aopalliance.intercept.Interceptororg.aopalliance.intercept.MethodInterceptor 代表环绕增强,表示在目标方法执行前后实施增强,是 aopalliance 实现的。

  • 异常抛出。org.springframework.aop.AfterAdviceorg.springframework.aop.ThrowsAdvice代表抛出异常Advice,表示在目标方法抛出异常后实施Advice

  • 引介。org.springframework.aop.IntroductionInterceptororg.springframework.aop.support.DelegatingIntroductionInterceptor 代表引介Advice,表示在目标类中添加一些新的方法和属性

具体Advice 使用介绍

前置 Advice
  1. 定义业务类。无特殊情况,业务类都以该例为准,后不赘述。
# 业务接口
public interface AopService {
    void add(String name);
    void update(String name);
}

# 业务实现
public class AopServiceImpl implements AopService {
    @Override
    public void add(String name) {
        System.out.println("** 操作新增的业务** " + name);
    }

    @Override
    public void update(String name) {
        System.out.println("** 操作修改的业务** " + name);
    }
}
  1. 定义前置 Advice
  • 实现 org.springframework.aop.MethodBeforeAdvice接口
  • 实现唯一方法void before(Method method, Object[] args, Object target) throws Throwable;
public class AopBeforeAdvice implements MethodBeforeAdvice {

    /**
     * 方法前置增强
     *
     * @param method 目标类的方法
     * @param args   目标类方法的入参
     * @param target 目标类实例
     * @throws Throwable
     */
    @Override
    public void before(Method method, Object[] args, Object target) throws Throwable {
        String clientName = (String) args[0];
        # 横切逻辑
        System.out.println("Hello Mr. " + clientName + ", Before Advice");
    }
}
  1. 编码织入实现
  • 使用ProxyFactory代理工厂,将AopBeforeAdvice织入目标类AopService中。ProxyFactory内部使用JDK动态代理或者CGLib实现
  • Spring 定义了 org.springframework.aop.framework.AopProxy 接口,并有两个实现类org.springframework.aop.framework.JdkDynamicAopProxyorg.springframework.aop.framework.CglibAopProxy
    AopService aopService = new AopServiceImpl();

    BeforeAdvice beforeAdvice = new AopBeforeAdvice();

    // Spring 提供的代理工厂
    ProxyFactory proxyFactory = new ProxyFactory();

    // 设置代理目标
    proxyFactory.setTarget(aopService);

    // 为代理目标添加advice
    proxyFactory.addAdvice(beforeAdvice);

    // 启动优化代理(针对接口也会使用CGLib)
    proxyFactory.setOptimize(true);

    // 生成代理实例
    AopService proxy = (AopService) proxyFactory.getProxy();

    proxy.add("Sam");
    proxy.update("Eason");
  1. Spring 配置织入实现,使用JavaConfig配置实现
  • ProxyFactoryBeanFactoryBean的实现类,FactoryBean是Spring提供一种特殊的Bean的管理类,可以通过该接口定制化 Bean 的逻辑。简单可参考先关博客文章 Spring4.x 笔记(4):Bean 在容器中的装配-XML(Schema)

  • ProxyFactoryBean 负责为其他Bean创建代理实例,内部使用ProxyFactory来处理

    # 业务配置
    @Bean
    public AopService target() {
        return new AopServiceImpl();
    }
    # 前置Advice配置
    @Bean
    public AopBeforeAdvice aopBeforeAdvice() {
        return new AopBeforeAdvice();
    }
    #  ProxyFactoryBean配置,获取代理
    @Bean
    public ProxyFactoryBean beforeProxyFactoryBean() throws ClassNotFoundException {
        ProxyFactoryBean proxyFactoryBean = new ProxyFactoryBean();
        // 代理实现的接口。指定代理接口,与 setInterfaces()方法一样
        proxyFactoryBean.setProxyInterfaces(new Class[]{AopService.class});
        // Advice。指定使用的增强(接收名称而不是实例,原理在于内部需要使用的是Bean,而不是实例)
        proxyFactoryBean.setInterceptorNames("aopBeforeAdvice");
        // 目标。指定对哪个Bean进行代理
        proxyFactoryBean.setTarget(target());
        // 需要强制使用CGLib,这时无需配置代理接口 setProxyInterfaces
        proxyFactoryBean.setProxyTargetClass(true);
        return proxyFactoryBean;
    }
    # 测试
    ApplicationContext context = new AnnotationConfigApplicationContext(AopConfig.class);
    // 使用 ProxyFactoryBean 配置,直接获取代理类
    AopService proxy = context.getBean("beforeProxyFactoryBean", AopService.class);
    proxy.add("Sam");
后置 Advice
  1. 后置Advice,实现接口 AfterReturningAdvice
# 后置Advice
public class AopAfterAdvice implements AfterReturningAdvice {
    @Override
    public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
        # 后置横切业务
        System.out.println("Thanks Sir" + ", After Advice");
    }
}
  1. 使用Java配置织入
    # 后置Advice配置
    @Bean
    public AopAfterAdvice aopAfterAdvice() {
        return new AopAfterAdvice();
    }
    
    # ProxyFactoryBean 配置代理
    @Bean
    public ProxyFactoryBean afterProxyFactoryBean() throws ClassNotFoundException {
        ProxyFactoryBean proxyFactoryBean = new ProxyFactoryBean();
        // 指定代理接口,与 setProxyInterfaces 一样
        proxyFactoryBean.setInterfaces(AopService.class);
        // 指定使用的增强(接收名称而不是实例,原理在于内部需要使用的是Bean,而不是实例)
        proxyFactoryBean.setInterceptorNames("aopAfterAdvice");
        // 指定对哪个Bean进行代理
        proxyFactoryBean.setTarget(target());
        return proxyFactoryBean;
    }
    ApplicationContext context = new AnnotationConfigApplicationContext(AopConfig.class);
    // 使用 ProxyFactoryBean 配置,直接获取代理类
    AopService proxy = context.getBean("afterProxyFactoryBean", AopService.class);
    proxy.add("Sam");
环绕 Advice
  1. 环绕Advice,允许在目标类方法调用前后织入横切逻辑,综合了前置与后置的功能
  2. 实现AOP联盟定义的接口MethodInterceptorMethodInvocation对象封装了目标方法、参数、目标方法所在的实例对象等
public class AopRoundAdvice implements MethodInterceptor {
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {

        Object[] arguments = invocation.getArguments();
        String clientName = (String) arguments[0];
        # 前置横切逻辑
        System.out.println("How are you! Mr. " + clientName);
        
        # 目标方法调用
        Object proceed = invocation.proceed();

        # 后置横切逻辑
        System.out.println("Please enjoy yourself");

        return proceed;
    }
}
  1. 使用Java配置织入
    @Bean
    public AopRoundAdvice aopRoundAdvice() {
        return new AopRoundAdvice();
    }

    @Bean
    public ProxyFactoryBean roundProxyFactoryBean() throws ClassNotFoundException {
        ProxyFactoryBean proxyFactoryBean = new ProxyFactoryBean();
        // 指定代理接口,与 setProxyInterfaces 一样
        proxyFactoryBean.setInterfaces(AopService.class);
        // 指定使用的增强
        proxyFactoryBean.setInterceptorNames("aopRoundAdvice");
        // 指定对哪个Bean进行代理
        proxyFactoryBean.setTarget(target());
        return proxyFactoryBean;
    }
    # 测试
    ApplicationContext context = new AnnotationConfigApplicationContext(AopConfig.class);
    // 使用 ProxyFactoryBean 配置,直接获取代理类
    AopService proxy = context.getBean("roundProxyFactoryBean", AopService.class);
    proxy.add("Sam");
异常 Advice
  1. 修改业务类add()方法,增加异常部分。
    @Override
    public void add(String name) {
        System.out.println("** 操作新增的业务** " + name);

        // 增加异常
        int i = 1 / 0;
    }
  1. 异常Advice,实现ThrowsAdvice接口。
  • 该接口是一个标签接口,没有定义任何方法,Spring根据反射自行判断。
  • 实现接口的格式固定,必须按照下面的格式定义Advice的方法;且该方法可以定义多个。
public class ExceptionAdvice implements ThrowsAdvice {

    # 方法定义格式固定,方法名称为 afterThrowing ,参数定义如下(前三个参数集体可选)
    # 方法可以定义多个(参数不同)
    public void afterThrowing(Method method, Object[] args, Object target, Exception ex) throws Exception {
        System.out.println("method:" + method.getName());
        System.out.println("抛出异常" + ex.getMessage());
    }
    
    public void afterThrowing(Exception ex) throws Exception {
        System.out.println("抛出异常" + ex.getMessage());
    }
}
  1. 使用Java配置织入
    @Bean
    public ExceptionAdvice exceptionAdvice() {
        return new ExceptionAdvice();
    }
    
    @Bean
    public ProxyFactoryBean exceptionProxyFactoryBean() {
        ProxyFactoryBean proxyFactoryBean = new ProxyFactoryBean();
        // 指定代理接口,与 setProxyInterfaces 一样
        proxyFactoryBean.setInterfaces(AopService.class);
        // 指定使用的增强
        proxyFactoryBean.setInterceptorNames("exceptionAdvice");
        // 指定对哪个Bean进行代理
        proxyFactoryBean.setTarget(target());
        return proxyFactoryBean;
    }
    # 测试
    ApplicationContext context = new AnnotationConfigApplicationContext(AopConfig.class);
    AopService aopService = context.getBean("exceptionProxyFactoryBean", AopService.class);
    aopService.add("Sam");
引介 Advice
  1. 引介 Advice 是一种比较特殊的Advice,它并不是在目标方法周围织入增强,而是为目标类创建新的方法与属性,所以引介的连接点是类级别的。

  2. 通过引介可以为目标类添加一个接口的实现。原来目标类没有实现某个接口,通过引介Advice可以为目标类创建实现某个接口的代理。

  3. Spring 定义了引介Advice 接口IntroductionInterceptor以及实现类DelegatingIntroductionInterceptor

  4. 回顾前面的例子,需要在业务类AopService中加入监控 AopMonitor,因为监控有可能会影响业务性能,所以是否启用监控应该是可控的,可以使用引介Advice实现。

  5. 定义用于标识目标类是否支持性能监控的接口:

public interface Monitorable {
    void setMonitorActive(boolean active);
}
  1. 引介Advice,为目标类引入性能监控的可控功能:
  • 继承 DelegatingIntroductionInterceptor 引介基础类
  • 实现前面定义的新增功能的接口 Monitorable,提供接口方法放入实现。
public class AopIntroductionInterceptor extends DelegatingIntroductionInterceptor implements Monitorable {

    # 线程特殊处理,如果不处理,则对象只能为 prototype(会影响性能)
    private ThreadLocal<Boolean> monitorStatusMap = new ThreadLocal<>();

    @Override
    public void setMonitorActive(boolean active) {
        monitorStatusMap.set(active);
    }

    @Override
    public Object invoke(MethodInvocation mi) throws Throwable {
        Object invoke;

        if (monitorStatusMap.get() != null && monitorStatusMap.get()) {
            AopMonitor.start();
            invoke = super.invoke(mi);
            AopMonitor.end();
        } else {
            invoke = super.invoke(mi);
        }
        return invoke;
    }
}
  1. 使用Java配置织入
    @Bean
    public AopIntroductionInterceptor aopIntroductionInterceptor() {
        return new AopIntroductionInterceptor();
    }
    
    @Bean
    public ProxyFactoryBean introductionProxyFactoryBean() {
        ProxyFactoryBean proxyFactoryBean = new ProxyFactoryBean();

        // 引介增强实现的接口
        proxyFactoryBean.setInterfaces(Monitorable.class);

        // 指定使用的增强(接收名称而不是实例,原理在于内部需要使用的是Bean,而不是实例)
        proxyFactoryBean.setInterceptorNames("aopIntroductionInterceptor");

        // 指定对哪个Bean进行代理
        proxyFactoryBean.setTarget(target());


        // 由于引介增强一定要通过创建子类来生成代理,所以需要强制使用CGLib
        proxyFactoryBean.setProxyTargetClass(true);
        return proxyFactoryBean;
    }
    # 测试
    ApplicationContext context = new AnnotationConfigApplicationContext(AopConfig.class);

    AopService aopService = context.getBean("introductionProxyFactoryBean", AopService.class);
    // 默认是没有监控的
    aopService.add("Sam");

    Monitorable monitorable = (Monitorable) aopService;
    monitorable.setMonitorActive(true);
    // 开启后,有监控
    aopService.add("Sam");

Spring Pointcut 切点

切点概述

  1. Advice 解决了横切逻辑和方法的具体织入点的方位问题,但是目前来说Advice 会被织入目标类的所有方法中。如果想要有选择性的织入目标类的某个特定方法,就需要使用切点进行目标连接点的定位。
  2. Spring 中定义了切点(Pointcut),即指定在哪些类的哪些方法上织入 Advice。通过org.springframework.aop.Pointcut 接口描述切点。
  3. Pointcut 由 ClassFilter 和 MethodMatcher 构成,ClassFilter负责定位到特定类;MethodMatcher 负责定位到特定方法。

Pointcut

  1. Spring 中的 MethodMatcher 支持两种方法匹配器:静态方法匹配与动态方法匹配。方法匹配器的类型由 MethodMatcher 中的isRuntime() 方法决定,true表示动态,反之则为静态。
  • 静态方法匹配仅对方法名签名(包括方法名和入参类型及顺序)进行匹配,值需要匹配一次。
  • 动态方法匹配会在运行期检查方法入参的值。需要每次调用方法都必须判断。

切点类型

  1. 静态方法切点。org.springframework.aop.support.StaticMethodMatcherPointcut 是静态方法切点的抽象基类,默认情况下匹配所有类。其包含两个主要子类,分别是org.springframework.aop.support.NameMatchMethodPointcutorg.springframework.aop.support.AbstractRegexpMethodPointcut,前者提供简单字符串匹配方法签名,后者使用正则表达式匹配方法签名。

  2. 动态方法切点。org.springframework.aop.support.DynamicMethodMatcherPointcut 是动态方法切点的抽象基类,默认情况下匹配所有的类。

  3. 流程切点。org.springframework.aop.support.ControlFlowPointcut 实现类标识控制流程切点。该类是一种特殊的切点,根据程序执行堆栈的信息查看目标方法是否由某一个方法直接或间接发起调用,以此判断是否为匹配的连接点

  4. 复合切点。org.springframework.aop.support.ComposablePointcut 实现类创建多个切点而提供的功能操作类。

  5. 注解切点。org.springframework.aop.support.annotation.AnnotationMatchingPointcut 实现类表示注解切点。使用支持在Bean中直接通过注解标签定义的切点。主要在于其两个方法forClassAnnotationforMethodAnnotation。将在后续声明式AOP中涉及讲解。

  6. 表达式切点。org.springframework.aop.support.ExpressionPointcut 接口主要是为了支持AspectJ切点表达式语法而定义的接口。将在后续声明式AOP中涉及讲解。

Spring Advisor 切面

  1. 增强 Advice 既包含横切逻辑,又包含部分连接点信息(方法前、方法后主方位信息);切点 Pointcut 代表目标类连接点的部分信息(类和方法的定位);切面 Advisor 就是结合增强 Advice和切点 Pointcut的结合。既包含横切逻辑,又可以把这部分逻辑织入定位到具体哪些个类、那些个方法的前、后、异常等位置。

  2. 把Advice 和 Pointcut 整合起来,既有横切逻辑,又有具体目标切点定位,就是一个完整的切面(Advisor)。

切面 Advisor 类型

切面类型概述

Spring 使用 org.springframework.aop.Advisor 接口表示切面,按类型分可以分为一般切面、切点切面和引介切面。具体可以通过API了解:

  1. Advisor:代表一般切面,只包含一个 Advice。这个切面太宽泛,一般不会用。
  2. PointcutAdvisor:具有切点的切面,即具有了切点的功能。包含 Advice、Pointcut两个类,提供更具适用性的切面。
  3. IntroductionAdvisor:引介切面。是对应引介Advice的特殊切面,应用于类层面上,适用 ClassFilter 进行定义。

image

切点切面 PointcutAdvisor

PointcutAdvisor 具有了切点的功能,在使用过程中配置好 Advice,可以提供更具适用性的切面。主要有以下下可以常用:

  1. DefaultPointcutAdvisor:最常用的切面类型,可以通过任意的 Pointcut和Advice定义一个切面,不支持引介切面。可以扩展该类自定义切面。
  2. NameMatchMethodPointcutAdvisor:定义按方法名定义切点的切面
  3. RegexpMethodPointcutAdvisor:正则表达式匹配方法名进行切点定义的切面。内部是通过JdkRegexpMethodPointcut 构造正则表达式方法名切点。
  4. StaticMethodMatcherPointcutAdvisor:静态方法匹配器切点定义的切面,默认匹配所有的目标类。
  5. AspectJExpressionPointcutAdvisor:用于AspectJ 切点表达式定义切点的切面
  6. AspectJPointcutAdvisor:用于AspectJ语法定义切点的切面

PointcutAdvisor 为什么具有切点的功能?原因在于其实现类都继承了对应的 Pointcut 接口实现。如 StaticMethodMatcherPointcutAdvisor 继承了 StaticMethodMatcherPointcut 类。

具体切面用法介绍

静态普通方法名匹配切面- StaticMethodMatcherPointcutAdvisor
  1. StaticMethodMatcherPointcutAdvisor 代表一个静态方法匹配切面,它通过 StaticMethodMatcherPointcut 来定义切点,并通过类过滤器和方法名来匹配所定义的切点。

  2. 定义业务类。无特殊情况,业务类都以该例为准,后不赘述。

# 业务接口
public interface AopService {
    void add(String name);
    void update(String name);
}

# 业务实现
public class AopServiceImpl implements AopService {
    @Override
    public void add(String name) {
        System.out.println("** 操作新增的业务** " + name);
    }

    @Override
    public void update(String name) {
        System.out.println("** 操作修改的业务** " + name);
    }
}
  1. 定义一个前置Advice。没有特殊说明,以下所有的切面都使用该Advice,后不赘述。
public class AopBeforeAdvice implements MethodBeforeAdvice {
    @Override
    public void before(Method method, Object[] args, Object target) throws Throwable {
        String clientName = (String) args[0];

        System.out.println("Hello Mr. " + clientName + ", Before Advice");
    }
}
  1. 定义切面。通过StaticMethodMatcherPointcutAdvisor定义的切面,在AopService#add()前织入Advice增强。
  • 默认匹配所有的类,可以覆盖getClassFilter方法重写
  • matches()方法中,可以自定义匹配规则,如模糊匹配、正则等(正则更推荐 StaticMethodMatcherPointcutAdvisor 切面)。
public class AopStaticMethodNameAdvisor extends StaticMethodMatcherPointcutAdvisor {

    private static final String METHOD_NAME = "add";

    @Override
    public boolean matches(Method method, Class<?> targetClass) {
        // 切点方法匹配规则,方法名称为"add"。这边也可以做一些模糊匹配。
        return Objects.equals(METHOD_NAME, method.getName());
    }

    /**
     * 默认情况是匹配所有的类,重写定制
     *
     * @return
     */
    @Override
    public ClassFilter getClassFilter() {
        // 切点类匹配规则,为 AopService 的类或者子类
        return AopService.class::isAssignableFrom;
    }
}
  1. 使用JavaConfig方式配置,如定义一个名称为AdvisorConfig的配置类。
    # 目标类
    @Bean
    public AopService target() {
        return new AopServiceImpl();
    }
    
    # Advice 配置
    @Bean
    public AopBeforeAdvice aopBeforeAdvice() {
        return new AopBeforeAdvice();
    }
    
    # 切面配置。已经聚类切点的功能,只需要注入一个advice即可
    @Bean
    public AopStaticMethodNameAdvisor aopMethodNameAdvisor() {
        AopStaticMethodNameAdvisor advisor = new AopStaticMethodNameAdvisor();
        // 向切面注入一个前置advice
        advisor.setAdvice(aopBeforeAdvice());
        return advisor;
    }
    
    # 配置代理
    @Bean
    public ProxyFactoryBean staticMethodProxyFactoryBean() {
        ProxyFactoryBean proxyFactoryBean = new ProxyFactoryBean();

        // 指定使用Advice/Advisor(Set the list of Advice/Advisor bean names.)
        proxyFactoryBean.setInterceptorNames("aopMethodNameAdvisor");

        // 指定对哪个Bean进行代理
        proxyFactoryBean.setTarget(target());

        // 强制使用CGLib
        proxyFactoryBean.setProxyTargetClass(true);
        return proxyFactoryBean;
    }
    # 测试
    ApplicationContext context = new AnnotationConfigApplicationContext(AdvisorConfig.class);
    AopService aopService = context.getBean("staticMethodProxyFactoryBean", AopService.class);

    // add 该方法会织入 Advice
    aopService.add("Sam");
    // 该方法不会织入
    aopService.update("Eason");
  1. 使用Schema配置实现,后面其他的切面不再实现。
    <!--定义目标-->
    <bean id="aopServiceTarget" class="com.learning.spring.aop.AopServiceImpl"/>

    <!--定义 Advice-->
    <bean id="aopBeforeAdvice" class="com.learning.spring.aop.advice.before.AopBeforeAdvice"/>

    <!--定义Advisor-->
    <bean id="aopStaticMethodNameAdvisor" class="com.learning.spring.aop.advisor.staticmethodname.AopStaticMethodNameAdvisor">
        <property name="advice" ref="aopBeforeAdvice"/>
    </bean>

    <!--代理-->
    <bean id="staticMethodProxyFactoryBean" class="org.springframework.aop.framework.ProxyFactoryBean">
        <property name="target" ref="aopServiceTarget"/>
        <property name="interceptorNames" value="aopStaticMethodNameAdvisor"/>
        <property name="proxyTargetClass" value="true"/>
    </bean>
    ApplicationContext context = new ClassPathXmlApplicationContext("classpath:schema-aop.xml");
    AopService aopService = context.getBean("staticMethodProxyFactoryBean", AopService.class);

    // 该方法会织入Advice
    aopService.add("Sam");
    // 该方法不会织入
    aopService.update("Eason");
静态正则表达式方法匹配切面- RegexpMethodPointcutAdvisor
  1. 相比较于 StaticMethodMatcherPointcutAdvisor 通过方法名定义切点,RegexpMethodPointcutAdvisor 支持对于方法名使用过正则匹配,其功能比较完备,直接使用即可。

  2. 使用JavaConfig方式配置。

  • setPatterns() 多个匹配串之间是’或’的关系
  • setOrder() 切面在织入时对应的顺序
    # 正则表达式切面。直接使用 RegexpMethodPointcutAdvisor 即可
    @Bean
    public RegexpMethodPointcutAdvisor regexpMethodPointcutAdvisor() {
        RegexpMethodPointcutAdvisor advisor = new RegexpMethodPointcutAdvisor();
        advisor.setAdvice(aopBeforeAdvice());
        // 正则匹配方法名称(目标类方法放入全限定名称,带类名的方法名)
        // setPatterns 多个匹配串之间是'或'的关系
        advisor.setPatterns(".*add.*");
        return advisor;
    }
    
    # 配置代理
    @Bean
    public ProxyFactoryBean regexpProxyFactoryBean() {
        ProxyFactoryBean proxyFactoryBean = new ProxyFactoryBean();

        // 指定使用Advice/Advisor(Set the list of Advice/Advisor bean names.)
        proxyFactoryBean.setInterceptorNames("regexpMethodPointcutAdvisor");
        
        // 指定对哪个Bean进行代理
        proxyFactoryBean.setTarget(target());

        proxyFactoryBean.setProxyTargetClass(true);
        return proxyFactoryBean;
    }
    ApplicationContext context = new AnnotationConfigApplicationContext(AdvisorConfig.class);
    AopService aopService = context.getBean("regexpProxyFactoryBean", AopService.class);

    // add 该方法会织入Advice
    aopService.add("Sam");
    // 该方法不会织入
    aopService.update("Eason");
动态切面- DynamicMethodMatcherPointcut、DefaultPointcutAdvisor
  1. Spring 使用 DefaultPointcutAdvisorDynamicMethodMatcherPointcut 来实现动态切面的功能。
  2. DynamicMethodMatcherPointcut是动态切点,实现了DynamicMethodMatcher抽象类:
  • DynamicMethodMatcher 类重写了MethodMatcher#isRuntime()方法,默认返回true,为一个动态切点。
  • DynamicMethodMatcher 类的matches()方法默认匹配所有类和方法,需要自定义扩展。
  1. 动态切面由于其本身功能的实现限制,对于性能的影响比较大,不建议使用,后者配合静态切点检查一起使用。

  2. 定义一个动态切点

  • 重写getClassFilter方法,限定类
  • 重写matches(Method method, Class<?> targetClass) 方法,进行静态切点检查。建议所有的动态切点都重写静态切点检查,提高效率。
  • 重写matches(Method method, Class<?> targetClass, Object... args) 方法,进行动态切点检查。
public class AopDynamicPointcut extends DynamicMethodMatcherPointcut {
    private static final String METHOD_NAME = "add";
    private static List<String> specialClientList = new ArrayList<>();

    static {
        // 模拟参数,是由add方法传入参数是Sam时才匹配
        specialClientList.add("Sam");
    }

    @Override
    public ClassFilter getClassFilter() {
        return AopService.class::isAssignableFrom;
    }

    # 静态切点检查,对方法名进行限制。这边不通过,则不必要进行动态检查,提高效率
    @Override
    public boolean matches(Method method, Class<?> targetClass) {
        System.out.println("++ 对方法 " + method.getName() + " 做静态检查");
        return Objects.equals(METHOD_NAME, method.getName());
    }

    # 动态切点检查。性能影响较大,不建议使用。
    @Override
    public boolean matches(Method method, Class<?> targetClass, Object... args) {
        System.out.println("~~ 对方法 " + method.getName() + " 做动态检查");

        String clientName = (String) args[0];
        return specialClientList.contains(clientName);
    }
}

  1. 使用JavaConfig方式配置。使用DefaultPointcutAdvisor直接定义。
    # 定义切点
    @Bean
    public AopDynamicPointcut aopDynamicPointcut() {
        return new AopDynamicPointcut();
    }
    
    # 定义切面,设置advice与Pointcut
    @Bean
    public DefaultPointcutAdvisor dynamicPointcutAdvisor() {
        DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor();
        advisor.setPointcut(aopDynamicPointcut());
        advisor.setAdvice(aopBeforeAdvice());
        return advisor;
    }
    # 定义代理配置
    @Bean
    public ProxyFactoryBean dynamicProxyFactoryBean() {
        ProxyFactoryBean proxyFactoryBean = new ProxyFactoryBean();
        // 指定使用的增强
        proxyFactoryBean.setInterceptorNames("dynamicPointcutAdvisor");
        // 指定对哪个Bean进行代理
        proxyFactoryBean.setTarget(target());
        proxyFactoryBean.setProxyTargetClass(true);
        return proxyFactoryBean;
    }
    ApplicationContext context = new AnnotationConfigApplicationContext(AdvisorConfig.class);
    AopService aopService = context.getBean("dynamicProxyFactoryBean", AopService.class);

    // add方法传入参数Sam 才会匹配织入Advice
    aopService.add("Sam");
    // 不会匹配
    aopService.add("Eason");
流程切面- ControlFlowPointcut、DefaultPointcutAdvisor
  1. 流程切面由 DefaultPointcutAdvisorControlFlowPointcut 配合完成功能。

  2. ControlFlowPointcut 代表流程切点,是指由某个方法直接或者间接发起调用的其他方法。

  3. 流程切面与动态切面一样都需要在运行期判断动态的环境,对性能影响较大。

  4. 定义流程切点类,如果希望由 AopServiceDelegate#service() 方法发起调用的其他方法都织入 Advice 增强,就必须使用流程切面。

public class AopServiceDelegate {
    private AopService aopService;

    public void service(String name) {
        # 调用两个方法
        aopService.add(name);
        aopService.update(name);
    }

    public void setAopService(AopService aopService) {
        this.aopService = aopService;
    }
}
  1. 使用JavaConfig方式配置。
    # 流程切点。指定流程切点以及方法
    @Bean
    public ControlFlowPointcut controlFlowPointcut() {
        // 两个参数:指定流程切点的类;指定流程切点的方法
        return new ControlFlowPointcut(AopServiceDelegate.class, "service");
    }
    
    # 切面配置
    @Bean
    public DefaultPointcutAdvisor controlFlowAdvisor() {
        DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor();
        advisor.setPointcut(controlFlowPointcut());
        advisor.setAdvice(aopBeforeAdvice());
        return advisor;
    }
    
    @Bean
    public ProxyFactoryBean controlFlowProxyFactoryBean() {
        ProxyFactoryBean proxyFactoryBean = new ProxyFactoryBean();
        // 指定使用的增强
        proxyFactoryBean.setInterceptorNames("controlFlowAdvisor");
        // 指定对哪个Bean进行代理
        proxyFactoryBean.setTarget(target());
        proxyFactoryBean.setProxyTargetClass(true);
        return proxyFactoryBean;
    }
    ApplicationContext context = new AnnotationConfigApplicationContext(AdvisorConfig.class);
    AopService aopService = context.getBean("controlFlowProxyFactoryBean", AopService.class);
    // 该方法单独调用不会织入Advice
    aopService.add("Sam");

    // 流程切点发起的调用才会被织入
    AopServiceDelegate aopServiceDelegate = new AopServiceDelegate();
    aopServiceDelegate.setAopService(aopService);
    # service方法中调用add、update两个方法,都会被织入
    aopServiceDelegate.service("Eason");
复合切面- ComposablePointcut、DefaultPointcutAdvisor
  1. 有时候一个切点可能难以描述目标连接点的信息,假如我们想AopServiceDelegate#service()发起调用且被调用的方法是 AopService#add()时才织入 Advice,这时就需要复合切点。
  2. ComposablePointcut 可以把多个切点以并集或者交集的方式组合起来,提供了切点之间复合运算的功能。
  • ComposablePointcut 本身也是一个切点,可以自定义 ClassFilter、MethodMatcher
  • intersection:交集运算方法
  • union:并集运算方法
  • org.springframework.aop.support.Pointcuts:交并集运算,可以使用该工具类
  1. 定义复合切点,做流程切点和方法名切点的交集。
public class AopComposablePointcut {
    
    # 该方法可以获取 ComposablePointcut 对象
    public Pointcut getPointcut() {
        // 创建复合切点
        ComposablePointcut pointcut = new ComposablePointcut();

        // 创建流程切点
        Pointcut pt1 = new ControlFlowPointcut(AopServiceDelegate.class, "service");

        // 创建一个方法名切点
        NameMatchMethodPointcut pt2 = new NameMatchMethodPointcut();
        pt2.setMappedNames("add");

        // 将两个切点交集操作
        return pointcut.intersection(pt1).intersection((Pointcut) pt2);
    }
}
  1. 使用JavaConfig方式配置。
    # 复合切点
    @Bean
    public AopComposablePointcut aopComposablePointcut() {
        return new AopComposablePointcut();
    }
    
    # 复合切面
    @Bean
    public DefaultPointcutAdvisor composableAdvisor() {
        DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor();
        advisor.setPointcut(aopComposablePointcut().getPointcut());
        advisor.setAdvice(aopBeforeAdvice());
        return advisor;
    }
    
    @Bean
    public ProxyFactoryBean composableProxyFactoryBean() {
        ProxyFactoryBean proxyFactoryBean = new ProxyFactoryBean();
        // 指定使用的增强
        proxyFactoryBean.setInterceptorNames("composableAdvisor");
        // 指定对哪个Bean进行代理
        proxyFactoryBean.setTarget(target());
        proxyFactoryBean.setProxyTargetClass(true);
        return proxyFactoryBean;
    }
    # 结果:
    ApplicationContext context = new AnnotationConfigApplicationContext(AdvisorConfig.class);
    AopService aopService = context.getBean("composableProxyFactoryBean", AopService.class);

    // 流程发起的调用才会被织入,且只有add()方法才会被织入
    AopServiceDelegate aopServiceDelegate = new AopServiceDelegate();
    aopServiceDelegate.setAopService(aopService);
    aopServiceDelegate.service("Eason");
引介切面- IntroductionAdvisor
  1. 引介切面是引介Advice的封装器,通过引介切面可以更加简单的为目标对象添加任何接口的实现

IntroductionAdvisor

  1. IntroductionAdvisor 接口代表了引介切面:
  • 继承了 IntroductionInfo接口,该接口是描述了目标类需要实现的新接口
  • 与Pointcut 不同,IntroductionAdvisor 没有方法匹配器 MethodMatcher,只有类过滤器 ClassFilter,正说明引介切面的切点是类级别的
  • 实现类有两个:分别为DefaultIntroductionAdvisor 常用类和 DeclareParentsAdvisor 用于实现使用AspectJ语言@DeclareParents 注解表示的引介切面
  1. 定义一个引介Advice
public class AopIntroductionInterceptor extends DelegatingIntroductionInterceptor implements Monitorable {

    private ThreadLocal<Boolean> monitorStatusMap = new ThreadLocal<>();

    @Override
    public void setMonitorActive(boolean active) {
        monitorStatusMap.set(active);
    }

    @Override
    public Object invoke(MethodInvocation mi) throws Throwable {
        Object invoke;

        if (monitorStatusMap.get() != null && monitorStatusMap.get()) {
            AopMonitor.start();
            invoke = super.invoke(mi);
            AopMonitor.end();
        } else {
            invoke = super.invoke(mi);
        }
        return invoke;
    }
}
  1. 使用JavaConfig方式配置。下面的配置可见,使用引介切面的方式更加简洁、清晰。
    # 定义引介Advice
    @Bean
    public Advice introductionAdvice() {
        return new AopIntroductionInterceptor();
    }
    
    # 引介切面
    @Bean
    public DefaultIntroductionAdvisor introductionAdvisor() {
        return new DefaultIntroductionAdvisor(introductionAdvice());
    }
    
    @Bean
    public ProxyFactoryBean introductionProxyFactoryBean() {
        ProxyFactoryBean proxyFactoryBean = new ProxyFactoryBean();
        // 指定使用的增强
        proxyFactoryBean.setInterceptorNames("introductionAdvisor");
        // 指定对哪个Bean进行代理
        proxyFactoryBean.setTarget(target());
        proxyFactoryBean.setProxyTargetClass(true);
        return proxyFactoryBean;
    }

自动创建代理

概述

  1. 目前为止,在创建代理一个对象时,都是使用ProxyFactoryBean类来完成,如果每一个需求都需要创建一个代理对象,那将是相当麻烦的,将会出现大量的ProxyFactoryBeanBean配置。
  2. 为解决上述的问题,Spring 提供了自动代理机制,让容器自动生成代理,其内部是使用 BeanPostProcessor 后置处理接口。

自动创建代理实现类

image

  1. 基于 BeanPostProcessor 的自动代理创建器的实现类,将根据一些规则自动在容器实例化Bean时为匹配的Bean生成代理实例,自动创建代理对象

  2. 三种代理创建器的类型

  • BeanNameAutoProxyCreator:基于 Bean 配置名规则的自动代理创建器,允许为一组特定配置名的 Bean 自动创建代理实例的代理创建器
  • DefaultAdvisorAutoProxyCreator:基于Advisor匹配机制的自动代理创建器,它会对容器中所有的Advisor进行扫描,自动将这些切面应用到匹配的Bean中,自动创建这些匹配的Bean的代理对象。
  • AnnotationAwareAspectJAutoProxyCreator:基于Bean中的AspectJ注解标签的自动代理创建器,为包含AspectJ注解的Bean自动创建代理实例。将在后续声明式AOP中涉及讲解。

具体自动创建代理介绍

根据Bean名称自动创建 BeanNameAutoProxyCreator
  1. 业务、前置Advice。定义一个配置类 ProxyConfig
    # 目标实例
    @Bean
    public AopService aopServiceTarget() {
        return new AopServiceImpl();
    }
    # 前置增强
    @Bean
    public AopBeforeAdvice aopBeforeAdvice() {
        return new AopBeforeAdvice();
    }
    
  1. 定义自动创建代理 BeanNameAutoProxyCreator
  • beanNames 属性,允许指定一组需要自动代理的Bean的名称,可以使用通配符。
  • 容器在创建 aopServiceTarget 这个Bean的时候,自动会为他们创建织入了前置Advice的代理对象
    # 自动根据BeanName 创建代理对象
    @Bean
    public BeanNameAutoProxyCreator beanNameAutoProxyCreator() {
        BeanNameAutoProxyCreator proxyCreator = new BeanNameAutoProxyCreator();

        // 指定被代理的bean,使用通配符或者数据
        // proxyCreator.setBeanNames("*Target"); 正则
        proxyCreator.setBeanNames("aopServiceTarget", "aopServiceTarget"); // 目标Bean 的名称
        proxyCreator.setInterceptorNames("aopBeforeAdvice");

        proxyCreator.setProxyTargetClass(true);
        return proxyCreator;
    }
    ApplicationContext context = new AnnotationConfigApplicationContext(ProxyConfig.class);
    // 容器在创建aopServiceTarget 的Bean的时候,自动会为他们创建了代理对象。
    // 这边拿到的就是代理对象
    AopService aopService = context.getBean("aopServiceTarget", AopService.class);
    aopService.add("Sam");
默认切面代理创建器 DefaultAdvisorAutoProxyCreator
  1. 定义正则匹配切面
    @Bean
    public RegexpMethodPointcutAdvisor regexpMethodPointcutAdvisor() {
        RegexpMethodPointcutAdvisor advisor = new RegexpMethodPointcutAdvisor();
        advisor.setAdvice(aopBeforeAdvice());
        // 正则匹配方法名称(目标类方法放入全限定名称,带类名的方法名)
        advisor.setPatterns(".*add.*");
        return advisor;
    }
  1. 定义默认切面代理创建器。DefaultAdvisorAutoProxyCreator 能够自动扫描容器中的 Advisor,并将Advisor 自动织入到匹配的目标Bean中,并为匹配的目标Bean自动创建代理
    @Bean
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
        return new DefaultAdvisorAutoProxyCreator();
    }
    ApplicationContext context = new AnnotationConfigApplicationContext(ProxyConfig.class);
    // 这边拿到的就是代理对象
    AopService aopService = context.getBean("aopServiceTarget", AopService.class);
    aopService.add("Sam");

总结

  1. Aop 为我们提供了一种新的思路,把重复的横切逻辑抽取到统一的模块中,采用织入的方式实现重复性代码问题。

  2. Spring 采用JDK动态代理和CGLib技术织入横切逻辑,不需要额外的其他依赖。使用JDK动态代理,目标类必须使用接口,而CGlib不对目标类做任何限制,通过动态生成目标类子类的方式提供代理对象。如果一般是单例(或参加实例不多),建议使用CGLib 。

  3. Spring 提供了5中类型的Advice,分别是前置、后置、环绕、异常、引介。引介是一种特殊Advice,它是类级别的Advice(其他都是方法级别),为目标类织入新的接口实现。Advice 从广义上来是一种最简单的切面,因为其既包含了横切逻辑又包含切点信息,只是其切点信息只是简单的方法的相对方位信息。

  4. Spring 中提供详细的切点控制实现,普通的切点通过目标类名和方法名描述连接点的信息。在此基础上,spring还支持流程切点、复合切点实现负责的情况。

  5. 切面Advisor 是增强Advice与切点Pointcut的结合体

  6. 可以通过spring 提供的 ProxyFactoryBean 将切面织入不同的目标类中,创建代理。为每一目标都手工创建代理是很麻烦的事情。Spring 利用BeanPostProcessor 可干预Bean生命周期的机制,为我们提供了自动创建代理的功能,其中BeanNameAutoProxyCreator可以根据目标Bean的名称自动创建,DefaultAdvisorAutoProxyCreator 可以将容器中所有的切面Advisor自动织入到匹配的目标Bean中,创建代理。

  7. 综上,Spring 提供的编程式AOP需要大量的编码工作,使用上来说非常麻烦,更推荐基于@AspectJ注解的实现,但编程式的实现在对AOP的理解上,有很大的帮助。

参考

  1. 文章涉及源码 Spring-Aop
  2. Spring AOP-API 官方文档
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值