《精通Spring4.x》阅读笔记(二)- SpringAOP读这一篇就够了

基本概念

AOP(Aspect Oriented Programming): 面向切面编程,将重复性的横切性质逻辑模块化,织入到目标对象中。

产生背景

在程序设计中,会遇到一些不能通过纵向继承解决的重复代码,比如事务控制、性能监控等,这些代码并非业务逻辑所需要关注、却又不得不掺杂在业务逻辑中,造成了业务程序不够清晰、简单,并且需要重复去编写。为了解决这个问题,AOP的设计思路独辟蹊径,通过抽取这些横向逻辑到独立模块,然后再在编译、加载或运行时将这些横向逻辑插入到原业务代码中,实现了横向统一逻辑与业务逻辑的解耦,这也是其名称的由来。

应用场景

AOP有其特定应用场景,不是OOP的替代方案,而是OOP的有益补充。主要应用于横切性逻辑的处理,包括事务控制、性能监控、访问控制等等。

实现方案

AOP的实现方案有:

  • AspectJ
  • AspectWerkz(已与AspectJ合并)
  • JBoss AOP
  • Spring AOP
    本文主要是对Spring AOP进行介绍。

上一篇文章我们介绍了Spring IoC的具体细节,而Spring AOP是建立在Spring IoC的基础上的,与Spring IoC、AspectJ有很好的整合,对AspectJ有部分功能的支持。

相关术语

  1. 连接点(Joinpoint)
    • 连接点是指一个类或者一段程序代码拥有的一些具有边界性质的程序执行的特定位置。
    • Spring只支持方法层面的连接点,比如方法执行前、执行后、抛出异常时、方法调用前后等。这些点可以理解为我们要插入横切逻辑的候选锚点。
    • 构成:1. 用方法表示的程序执行点;2. 用相对位置表示的方位。Spring中用切点表示前者,后者在增强中定义
  2. 切点(Pointcut)
    • 如前所述,一个程序中有很多连接点,我们用切点定位要插入横切逻辑的指定连接点。类比一下,可以把连接点看作是数据库中的具体数据记录,而切点则可看作查询语句,用于匹配特定的条目。
    • Spring中切点只定位到了方法上。也就是连接点中的执行点信息,不包括具体的方位
  3. 增强(Advice)
    • 增强是指织入目标类连接点上的一段程序代码,也就是封装了我们前文说的横切逻辑的代码。
    • Spring中的增强除了包含要织入的代码外,还包含有定位连接点的方位信息
    • 引介(Introduction)是一类特殊的增强,为类添加属性和方法。即使一个原本没有实现接口的类,通过引介增强也能动态为该类添加接口的实现逻辑。
  4. 切面(Aspect)
    切面由切点和增强(引介)组成,定义有横切的逻辑及定位连接点的信息。Spring AOP就是负责实施切面的框架。
  5. 目标对象(Target)
    等待织入横切逻辑的目标类。即目标连接点附属的类。
  6. 织入(Weaving)
    将横切逻辑插入到目标对象中的过程称为织入。
    织入方式:
    • 编译期织入
    • 加载期织入
    • 动态代理织入(运行期)
      编译期织入需要提供特殊的编译期,加载期织入需要提供特殊的类加载器。Spring AOP采用动态代理织入方式。AspectJ采用前两种织入方式。
  7. 代理(Proxy)
    目标类被AOP织入增强后产生的新类,融合有原类和增强逻辑。根据不同代理方式,代理类可能是与目标类实现同一个接口的新类,也可能是目标类的子类。

理解AOP相关的术语是极其重要的,后面的具体实施其实就是定义切面(切点、增强)的过程,即两个方面的内容:1.如何定位连接点;2.如何编写增强代码。

底层技术

Spring AOP底层原理:基于动态代理技术,具体使用JDK或者是CGLib动态代理

JDK的动态代理

适用场景:JDK为目标类创建实现同一接口的代理对象,适用于接口实现类的代理场景,对于没有实现接口的类代理则无计可施。

使用步骤:

  1. 定义InvocationHandler接口的实现类
  2. 通过Proxy的newProxyInstance方法创建代理实例
/**
 * InvocationHandler接口的实现类,包含有横切逻辑
 */
public class PerformaceHandler implements InvocationHandler {
    private Object target;

    public PerformaceHandler(Object target) {
        this.target = target;
    }

    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
        PerformanceMonitor.begin(target.getClass().getName() + "." + method.getName());
        Object obj = method.invoke(target, args);
        PerformanceMonitor.end();
        return obj;
    }
}
/**
 * 使用JDK动态代理
 */
@Test
public void proxy() {
    
    // 定义目标类实例
    ForumService target = new ForumServiceImpl();
    // 定义InvocationHandler的具体实现类,其包含有具体的横切逻辑
    PerformaceHandler handler = new PerformaceHandler(target);
    // 通过Proxy创建代理对象,可强制转换为目标类的类型
    ForumService proxy = (ForumService) Proxy.newProxyInstance(target
                    .getClass().getClassLoader(),
            target.getClass().getInterfaces(), handler);
    // 调用代理对象的方法,其中已经包含有横切逻辑
    proxy.removeForum(10);
    proxy.removeTopic(1012);

}

CGLib的动态代理

适用场景:采用为目标类生成子类的方式进行代理,对于没有实现接口的目标类进行代理。
底层技术:基于底层字节码技术,在子类中采用方法拦截技术拦截所有父类方法的调用并顺势织入横切逻辑。

/**
 * CGLib动态代理实现
 */
public class CglibProxy implements MethodInterceptor {
    private Enhancer enhancer = new Enhancer();

    public Object getProxy(Class clazz) {
        // 设置需要创建子类的类(目标类)
        enhancer.setSuperclass(clazz);
        enhancer.setCallback(this);
        // 创建子类实例
        return enhancer.create();
    }

    /**
     * 拦截父类所有方法调用
     * @param obj 
     * @param method 
     * @param args
     * @param proxy 
     * @return
     * @throws Throwable
     */
    public Object intercept(Object obj, Method method, Object[] args,
                            MethodProxy proxy) throws Throwable {
        PerformanceMonitor.begin(obj.getClass().getName() + "." + method.getName());
        Object result = proxy.invokeSuper(obj, args);
        PerformanceMonitor.end();
        return result;
    }
}
/**
 * 使用CGLib动态代理
 */
@Test
public void proxy() {
    //使用CGLib动态代理
    CglibProxy cglibProxy = new CglibProxy();
    ForumService forumService = (ForumService)cglibProxy.getProxy(ForumServiceImpl.class);
    forumService.removeForum(10);
    forumService.removeTopic(1023);
}

对比

  1. 相较CGLib,使用JDK动态代理在创建代理对象时性能较高,但执行对象方法时性能不高。
  2. 与之相反,使用CGLib时创建代理对象性能不高,但执行对象方法时性能要高。

使用建议:在代理单例对象或具有实例池的对象时,可采用CGLib动态代理,而使用prototype类型对象代理时,使用JDK动态代理。(在SpringAOP中可通过参数配置)

优劣势

  • 优势
    动态代理技术实现AOP功能,不需要额外的Java编译器或者类加载器,直接依赖JDK或CGLib库就可完成;
  • 劣势
    直接使用动态代理,存在一些不足:
    1. 作用范围上,为目标类的所有方法都添加了增强逻辑,除了在增强逻辑中添加一些方法名称或签名的判断外,无法实现对某个目标类部分方法的增强;
    2. 通用性上,硬编码的方式指定具体目标类和增强逻辑,无法实现通用、统一的处理目标;
    3. 连接点定位上,也是采用的硬编码方式,无法做到通用。

Spring AOP的使用

根据不同场景,Spring AOP有多种使用方式:Advisor、@AspectJ、Schema。

Advisor

实现增强

Spring AOP中的增强包含有连接点的方位信息,同时目前只支持方法层面的增强,因此根据不同方位提供了五种类型的增强接口,通过实现不同的接口得到横切逻辑不同的触发时机。
增强主要接口的继承关系如图:
《精通Spring4.x》阅读笔记(二)- SpringAOP读这一篇就够了_第1张图片

  1. 前置增强:MethodBeforeAdvice
  2. 后置增强:AfterReturningAdvice
  3. 环绕增强:MethodInterceptor
  4. 抛出异常增强:ThrowsAdvice
  5. 引介增强:IntroductionInterceptor
    (接口方法声明中包括了目标类的各项信息:方法、入参、目标类实例等)

编码方式使用增强
测试一个增强或通过编码方式使用增强,可通过ProxyFactory类将增强织入到目标类中创建代理:

@Test
public void before() {
    Waiter target = new NaiveWaiter();
    BeforeAdvice  advice = new GreetingBeforeAdvice();
    ProxyFactory pf = new ProxyFactory();
    //pf.setInterfaces(target.getClass().getInterfaces());
    //pf.setOptimize(true);
    pf.setTarget(target);
    pf.addAdvice(advice);

    Waiter proxy = (Waiter)pf.getProxy(); 
    proxy.greetTo("John");
    proxy.serveTo("Tom");
    System.out.println(proxy.getClass());
}

ProxyFactory类中依赖了AopProxy接口,该接口的实现类有CglibAopProxy、JdkDynamicAopProxy,分别对应不同的代理技术,具体使用哪一个,通过以下策略控制:

  • setInterfaces方法设置代理类要实现的接口,此时使用JDK动态代理;
  • setOptimize方法启用优化,设置为true时,忽略interfaces属性,使用CGLib动态代理
    通过addAdvice方法可添加多个增强,具体增强顺序与调用顺序一致,也可通过重载方法决定其增强执行的顺序。

Spring配置方式使用增强
也可在xml配置文件中为目标类添加增强生成代理Bean:

<bean id="greetingBefore" class="com.smart.advice.GreetingBeforeAdvice" />
<bean id="target" class="com.smart.advice.NaiveWaiter" />
<bean id="waiter"
        class="org.springframework.aop.framework.ProxyFactoryBean"
        p:proxyInterfaces="com.smart.advice.Waiter" p:target-ref="target"
        p:interceptorNames="greetingBefore" />

配置代理Bean时,class属性指定为ProxyFactoryBean类,实际是使用SpringIoC中的FactoryBean接口功能,为复杂Bean提供灵活的代码实例化的功能。属性包括:

  • proxyInterfaces 目标对象实现的接口
  • target 具体目标对象
  • interceptorNames 增强对象信息(Advice或Advisor接口的实现类,可配置多个)

另外,还有singleton、optimize、proxyTargetClass布尔属性值,分别是定是否为单例(默认)、启用CGLib动态代理优化、对类进行代理。

配置好后,启动Spring容器,从容器获取对应Bean即为代理对象的Bean,为此需要将目标Bean配置为其他名称。

后置增强、环绕增强的使用方式大体相似,不同的是接口方法参数定义有所不同,但也都包含了必要的信息。下面简单介绍下抛出异常增强、引介增强的不同之处。

抛出异常时增强
主要应用于事务管理的场景,在抛出异常的情况下回滚事务。ThrowsAdvice是一个标签接口,运行时Spring通过反射机制,查找符合以下规则的方法:

  1. 方法名为afterThrowing;
  2. 前三个入参要么都提供,要么都省略,具体有Method method、Object[] args、Object target;
  3. 最后一个入参为Throwable或其子类,必须提供。

引介增强
引介增强的连接点为类级别,增强类需要实现目标接口的方法(提供增强的实现,也是目标类动态实现接口的默认实现),同时,直接继承DelegatingIntroductionInterceptor,覆盖invoke方法。由于只能通过生成子类的方式创建代理,必须指定proxyTargetClass为true。

创建切面

前面我们只实现了增强的逻辑,通过Spring提供的FactoryBean为目标类创建代理,此时为目标类所有方法织入了横切逻辑。对应AOP术语,我们尚未指定具体的切点,执行更个性化的增强动作。

切点在Spring中通过Pointcut表示,查看一下接口定义

public interface Pointcut {
    Pointcut TRUE = TruePointcut.INSTANCE;

    ClassFilter getClassFilter();

    MethodMatcher getMethodMatcher();
}

可以看到Pointcut引用了ClassFilter、MethodMatcher两个接口,通过这两个接口描述要对哪些目标类进行过滤。其中,MethodMatcher分为静态和动态方法匹配,静态仅对方法签名进行匹配,而动态则支持检查运行时方法的入参值,动态匹配对运行时的性能影响很大。静态、动态可通过isRuntime方法进行区分。

切点划分

  1. 静态方法切点
  2. 动态。。。。
  3. 注解切点
  4. 表达式切点
  5. 流程。。
  6. 复合。。

切点类型也体现在了切面类型的划分中,下面通过介绍切面,使用具体的切点。

切面划分
切面的概念包含有增强和切点,因为增强中包含了部分连接点的配置信息、也有横切代码逻辑,因此可以将增强也看作一个切面,这也就是在配置代理类时interceptorNames属性可以直接引用advice的原因

大的分类上,除了只包含Advice的切面外(因为匹配目标类所有方法,一般不会使用),切面还包括切点切面、引介切面,其中切点切面时最常用的切面类型。切点切面具体有6个具体的实现类,都可以对应到Pointcut上,最常用的切面类型为DefaultPointcutAdvisor,动态切点、流程切点、复合切点都通过该实现类得到具体的切面。

具体使用上,虽然每种类型的切面属性上稍有差异,但与只使用advice类似,增加了Advisor的定义、配置,在代理工厂Bean配置时,interceptorNames属性要指定我们配置的advisorBean即可。

自动代理创建
配置代理时,还有一个痛点,目前只能通过ProxyFactoryBean为指定的目标类创建代理,如果我们需要创建代理的目标类很多,岂不是要配置很多类似的Bean,即使通过Spring的父子配置简化,但工作量还是很大的,Spring为我们提供了三类自动创建代理的策略:

  1. 基于Bean配置名规则创建代理BeanNameAutoProxyCreator(beanNames指定匹配规则)
  2. 基于Advisor匹配机制DefaultAdvisorAutoProxyCreator
  3. 基于Bean中AspectJ注解AnnotationAwareAspectJAutoProxyCreator(@AspectJ使用)

类自身方法代理
书中还介绍类自身方法之间调用时无法通过代理对象执行,只能在目标类中直接调用,也就是无法插入增强的逻辑,书中给出了作者的解决方案,思路是通过注入自身Bean调用自己的方法实现,有兴趣的读者可自行查阅。

@AspectJ

@AspectJ是我们最常使用也是优先使用的方式,其配置的内容与Advisor本质上是相同的,只不过对我们的程序侵入性更低。

定义切面

先看使用@AspectJ定义一个切面的示例:

@Aspect
public class OperationLogAspect {

    @Pointcut("execution(public * com.iic.service.*.*(..)) && @annotation(com.iic.service.OperationLogCatcher)")
    public void recordLog() {
    }

    @Around("recordLog() && @annotation(com.doumi.logmanager.service.oplog.OperationLogCatcher)")
    public Object saveOptLog(ProceedingJoinPoint pjp) throws Throwable {
        Object result = pjp.proceed(); // 执行目标类方法
        // operationLogService.addOperationLog(“记录操作历史”);
    }
}

示例中应用了以下几个注解:

  • @Aspect 表示该类为切面的定义
  • @Pointcut 定义一个切点
  • @Around 定义环绕增强,对应的方法体为横切逻辑的执行

声明后,类似于ProxyFactory,可以通过AspectJProxyFactory对象编程方式使用生成代理;也可通过配置AnnotationAwareAspectJAutoProxyCreator自动代理创建Bean的方式生成代理Bean(或使用简洁配置方式:aop:aspectj-autoproxy)

切点表达式

切点表达式包括有函数和入参,同时入参支持通配符的使用,表达式之间可通过逻辑运算符构建更复杂的表达式,所支持的切点函数明细如下:

增强类型

  • @Before
  • @AfterReturning
  • @Around
  • @AfterThrowing
  • @After(Final增强,相当于try…finally控制,即使抛出异常也会得到执行)
  • @DeclareParents(引介增强)

参数绑定

另外,在对于连接点方法入参、返回值、抛出异常的绑定,本文没有详细介绍,简单来说是可以通过这种机制实现切点函数入参的精简,需要保证参数名称的一致,规则引擎会自动解析对应参数的类型。

如果不使用@Aspect注解定义切面,也能通过Schema配置的方式使用切点表达式。在配置文件中应用节点配置切面,其中子节点配置切点和增强方位。pointcut属性定义表达式,method属性指定具体使用的增强方法。

使用切点表达式的同时,想引用基于Spring增强接口实现类的方式配置Advisor可使用

小结

Spring AOP为我们提供了多种配置、使用的方式,有基于实现接口的、基于注解配置以及基于XML Schema。新项目中,可采用简洁的注解配置的方式,为了对老项目兼容,可采用Schema的配置。由于考虑到实现Spring提供的接口导致一定程度的代码与框架耦合,所以一般不采用基于Advisor类的配置方式,但有一种情况除外:基于ControlFlowPointcut的流程切面只能使用基于Advisor类的方式。

虽然配置形式很多,但本质上需要指定的内容还是切面、切点、增强这些,理解了AOP的这些基本概念,相信对无论使用何种配置都会做到心中有数。

你可能感兴趣的:(Spring)