Spring AOP - 注解方式使用介绍(长文详解)

16,566

前言

之前的源码解析章节,本人讲解了Spring IOC 的核心部分的源码。如果你熟悉Spring AOP的使用的话,在了解Spring IOC的核心源码之后,学习Spring AOP 的源码,应该可以说是水到渠成,不会有什么困难。

但是直接开始讲Spring AOP的源码,本人又觉得有点突兀,所以便有了这一章。Spring AOP 的入门使用介绍:包括Spring AOP的一些概念性介绍和配置使用方法。

这里先贴一下思维导图。

Spring AOP.png

AOP 是什么

AOP : 面向切面编程(Aspect Oriented Programming)

Aspect是一种新的模块化机制,用来描述分散在对象、类或函数中的横切关注点(crosscutting concern)。从关注点中分离出横切关注点是面向切面的程序设计的核心概念。分离关注点使解决特定领域问题的代码从业务逻辑中独立出来,业务逻辑的代码中不再含有针对特定领域问题代码的调用,业务逻辑同特定领域问题的关系通过切面来封装、维护,这样原本分散在整个应用程序中的变动就可以很好地管理起来。

最近在看李智慧的《大型网站技术架构》一书中,作者提到,开发低耦合系统是软件设计的终极目标之一。AOP这种面向切面编程的的方式就体现了这样的理念。将一些重复的、和业务主逻辑不相关的功能性代码(日志记录、安全管理等)通过切面模块化地抽离出来进行封装,实现关注点分离、模块解耦,使得整个系统更易于维护管理。

这样分而治之的设计,让我感觉到了一种美感。

AOP 要实现的是在我们原来写的代码的基础上,进行一定的包装,如在方法执行前、方法返回后、方法抛出异常后等地方进行一定的拦截处理或者叫增强处理。

AOP 的实现并不是因为 Java 提供了什么神奇的钩子,可以把方法的几个生命周期告诉我们,而是我们要实现一个代理,实际运行的实例其实是生成的代理类的实例

名词概念

前面提到过,Spring AOP 延用了 AspectJ 中的概念,使用了 AspectJ 提供的 jar 包中的注解。也就是Spring AOP里面的概念和术语,并不是Spring独有的,而是和AOP相关的。

概念可以草草看过,在看了之后的章节之后再回来看会对概念理解的更深。

术语 概念
Aspect 切面是PointcutAdvice的集合,一般单独作为一个类。PointcutAdvice共同定义了关于切面的全部内容,它是什么时候,在何时和何处完成功能。
Joinpoint 这表示你的应用程序中可以插入AOP方面的一点。也可以说,这是应用程序中使用Spring AOP框架采取操作的实际位置。
Advice 这是在方法执行之前或之后采取的实际操作。 这是在Spring AOP框架的程序执行期间调用的实际代码片段。
Pointcut 这是一组一个或多个切入点,在切点应该执行Advice。 您可以使用表达式或模式指定切入点,后面示例会提到。
Introduction 引用允许我们向现有的类添加新的方法或者属性
Weaving 创建一个被增强对象的过程。这可以在编译时完成(例如使用AspectJ编译器),也可以在运行时完成。Spring和其他纯Java AOP框架一样,在运行时完成织入。

PS:在整理概念的时候有个疑问,为什么网上这么多中文文章把advice 翻译成“通知”呢???概念上说得通吗???我更愿意翻译成“增强”(并发中文网ifeve.com 也是翻译成增强)

还有一些注解,表示Advice的类型,或者说增强的时机,看过之后的示例之后会更加的清楚。

术语 概念
Before 在方法被调用之前执行增强
After 在方法被调用之后执行增强
After-returning 在方法成功执行之后执行增强
After-throwing 在方法抛出指定异常后执行增强
Around 在方法调用的前后执行自定义的增强行为(最灵活的方式)

使用方式

Spring 2.0 之后,Spring AOP有了两种配置方式。

  1. schema-based:Spring 2.0 以后使用 XML 的方式来配置,使用 命名空间 <aop />

  2. @AspectJ 配置:Spring 2.0 以后提供的注解方式。这里虽然叫做 @AspectJ,但是这个和 AspectJ 其实没啥关系。

PS:个人比较钟情于@AspectJ 这种方式,使用下来是最方面的。也可能是因为我觉得XML方式配置的Spring Bean很不简洁、写起来不好看吧,所以有点排斥吧。23333~

本文主要针对注解方式讲解,并且给出对应的DEMO;之后的源码解析也会以注解的这种方式为范例讲解Spring AOP的源码(整个源码解析看完,会对其他方式触类旁通,因为原理都是一样的)

如果对其他配置方式感兴趣的同学可以google其他的学习资料。


来一条分割线,正式开始

1. 开启@AspectJ注解配置方式

开启@AspectJ的注解配置方式,有两种方式

  1. 在XML中配置:

    <aop:aspectj-autoproxy/>
    
  2. 使用@EnableAspectJAutoProxy注解

    @Configuration
    @EnableAspectJAutoProxy
    public class Config {
    
    }
    

开启了上述配置之后,所有在容器中@AspectJ注解的 bean 都会被 Spring 当做是 AOP 配置类,称为一个 Aspect。

NOTE:这里有个要注意的地方,@AspectJ 注解只能作用于Spring Bean 上面,所以你用 @Aspect 修饰的类要么是用 @Component注解修饰,要么是在 XML中配置过的。

比如下面的写法,

// 有效的AOP配置类
@Aspect
@Component
public class MyAspect {
 	//....   
}

// 如果没有在XML配置过,那这个就是无效的AOP配置类
@Aspect
public class MyAspect {
 	//....   
}

2. 配置 Pointcut (增强的切入点)

Pointcut 在大部分地方被翻译成切点,用于定义哪些方法需要被增强或者说需要被拦截。

在Spring 中,我们可以认为 Pointcut 是用来匹配Spring 容器中所有满足指定条件的bean的方法。

比如下面的写法,

    // 指定的方法
    @Pointcut("execution(* testExecution(..))")
    public void anyTestMethod() {}

下面完整列举一下 Pointcut 的匹配方式:

  1. execution:匹配方法签名

    这个最简单的方式就是上面的例子,"execution(* testExecution(..))"表示的是匹配名为testExecution的方法,*代表任意返回值,(..)表示零个或多个任意参数。

  2. **within:**指定所在类或所在包下面的方法(Spring AOP 独有)

        // service 层
        // ".." 代表包及其子包
        @Pointcut("within(ric.study.demo.aop.svc..*)")
        public void inSvcLayer() {}
    
  3. @annotation:方法上具有特定的注解

        // 指定注解
        @Pointcut("@annotation(ric.study.demo.aop.HaveAop)")
        public void withAnnotation() {}
    
  4. bean(idOrNameOfBean):匹配 bean 的名字(Spring AOP 独有)

        // controller 层
        @Pointcut("bean(testController)")
        public void inControllerLayer() {}
    

上述是日常使用中常见的几种配置方式

有更细的匹配需求的,可以参考这篇文章:www.baeldung.com/spring-aop-…

关于 Pointcut 的配置,Spring 官方有这么一段建议:

When working with enterprise applications, you often want to refer to modules of the application and particular sets of operations from within several aspects. We recommend defining a "SystemArchitecture" aspect that captures common pointcut expressions for this purpose. A typical such aspect would look as follows:

意思就是,如果你是在开发企业级应用,Spring 建议你使用 SystemArchitecture这种切面配置方式,即将一些公共的PointCut 配置全部写在这个一个类里面维护。官网文档给的例子像下面这样(它文中使用 XML 配置的,所以没加@Component注解)

package com.xyz.someapp;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class SystemArchitecture {

  /**
   * A join point is in the web layer if the method is defined
   * in a type in the com.xyz.someapp.web package or any sub-package
   * under that.
   */
  @Pointcut("within(com.xyz.someapp.web..*)")
  public void inWebLayer() {}

  /**
   * A join point is in the service layer if the method is defined
   * in a type in the com.xyz.someapp.service package or any sub-package
   * under that.
   */
  @Pointcut("within(com.xyz.someapp.service..*)")
  public void inServiceLayer() {}

  /**
   * A join point is in the data access layer if the method is defined
   * in a type in the com.xyz.someapp.dao package or any sub-package
   * under that.
   */
  @Pointcut("within(com.xyz.someapp.dao..*)")
  public void inDataAccessLayer() {}

  /**
   * A business service is the execution of any method defined on a service
   * interface. This definition assumes that interfaces are placed in the
   * "service" package, and that implementation types are in sub-packages.
   * 
   * If you group service interfaces by functional area (for example, 
   * in packages com.xyz.someapp.abc.service and com.xyz.def.service) then
   * the pointcut expression "execution(* com.xyz.someapp..service.*.*(..))"
   * could be used instead.
   */
  @Pointcut("execution(* com.xyz.someapp.service.*.*(..))")
  public void businessService() {}
  
  /**
   * A data access operation is the execution of any method defined on a 
   * dao interface. This definition assumes that interfaces are placed in the
   * "dao" package, and that implementation types are in sub-packages.
   */
  @Pointcut("execution(* com.xyz.someapp.dao.*.*(..))")
  public void dataAccessOperation() {}

}

上面这个 SystemArchitecture 很好理解,该 Aspect 定义了一堆的 Pointcut,随后在任何需要 Pointcut 的地方都可以直接引用。

配置切点,代表着我们想让程序拦截哪一些方法,但程序需要怎么对拦截的方法进行增强,就是后面要介绍的配置 Advice。

3. 配置Advice

注意,实际开发过程当中,Aspect 类应该遵守单一职责原则,不要把所有的Advice配置全部写在一个Aspect类里面。

这里是为了演示方便,所以写在了一起。

先直接上示例代码,里面包含了Advice 的几种配置方式(上文名词概念小节中有提到)。

/**
 * 注:实际开发过程当中,Advice应遵循单一职责,不应混在一起
 *
 * @author Richard_yyf
 * @version 1.0 2019/10/28
 */
@Aspect
@Component
public class GlobalAopAdvice {

    @Before("ric.study.demo.aop.SystemArchitecture.dataAccessOperation()")
    public void doAccessCheck() {
        // ... 实现代码
    }

    // 实际使用过程当中 可以像这样把Advice 和 Pointcut 合在一起,直接在Advice上面定义切入点
    @Before("execution(* ric.study.demo.dao.*.*(..))")
    public void doAccessCheck() {
        // ... 实现代码
    }

    // 在方法
    @AfterReturning("ric.study.demo.aop.SystemArchitecture.dataAccessOperation()")
    public void doAccessCheck() {
        // ... 实现代码
    }

    // returnVal 就是相应方法的返回值
    @AfterReturning(
        pointcut="ric.study.demo.aop.SystemArchitecture.dataAccessOperation()",
        returning="returnVal")
    public void doAccessCheck(Object returnVal) {
        //  ... 实现代码
    }

    // 异常返回的时候
    @AfterThrowing("ric.study.demo.aop.SystemArchitecture.dataAccessOperation()")
    public void doRecoveryActions() {
        // ... 实现代码
    }

    // 注意理解它和 @AfterReturning 之间的区别,这里会拦截正常返回和异常的情况
    @After("ric.study.demo.aop.SystemArchitecture.dataAccessOperation()")
    public void doReleaseLock() {
        // 通常就像 finally 块一样使用,用来释放资源。
        // 无论正常返回还是异常退出,都会被拦截到
    }

    // 这种最灵活,既能做 @Before 的事情,也可以做 @AfterReturning 的事情
    @Around("ric.study.demo.aop.SystemArchitecture.businessService()")
    public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
       	//  target 方法执行前... 实现代码
        Object retVal = pjp.proceed();
        //  target 方法执行后... 实现代码
        return retVal;
    }
}

在某些场景下,我们想在@Before的时候,去获取方法的入参,比如进行一些日志的记录,我们可以通过 org.aspectj.lang.JoinPoint来实现。上文中的ProceedingJoinPoint就是其子类。

@Before("...")
public void logArgs(JoinPoint joinPoint) {
    System.out.println("方法执行前,打印入参:" + Arrays.toString(joinPoint.getArgs()));
}

再举个与之对应的,方法返参打印:

@AfterReturning( pointcut="...", returning="returnVal")
public void logReturnVal(Object returnVal) {
    System.out.println("方法执行后,打印返参:" + returnVal));
}

快速Demo

介绍完上述的配置过程之后,我们用一个快速的Demo来实际演示一遍。这里把顺序变一下;

1. 编写 目标类

package ric.study.demo.aop.svc;

public interface TestSvc {

    void process();
}

@Service("testSvc")
public class TestSvcImpl implements TestSvc {
    @Override
    public void process() {
        System.out.println("test svc is working");
    }
}

public interface DateSvc {

    void printDate(Date date);
}

@Service("dateSvc")
public class DateSvcImpl implements DateSvc {

    @Override
    public void printDate(Date date) {
        System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date));
    }
}

2. 配置 Pointcut

@Aspect
@Component
public class PointCutConfig {
    @Pointcut("within(ric.study.demo.aop.svc..*)")
    public void inSvcLayer() {}   
}

3. 配置Advice

/**
 * @author Richard_yyf
 * @version 1.0 2019/10/29
 */
@Component
@Aspect
public class ServiceLogAspect {

    // 拦截,打印日志,并且通过JoinPoint 获取方法参数
    @Before("ric.study.demo.aop.PointCutConfig.inSvcLayer()")
    public void logBeforeSvc(JoinPoint joinPoint) {
        System.out.println("在service 方法执行前 打印第 1 次日志");
        System.out.println("拦截的service 方法的方法签名: " + joinPoint.getSignature());
        System.out.println("拦截的service 方法的方法入参: " + Arrays.toString(joinPoint.getArgs()));
    }

    // 这里是Advice和Pointcut 合在一起配置的方式
    @Before("within(ric.study.demo.aop.svc..*)")
    public void logBeforeSvc2() {
        System.out.println("在service的方法执行前 打印第 2 次日志");
    }
}

4. 开启@AspectJ注解配置方式,并启动

这里为了图方便,把配置类和启动类写在了一起,

/**
 * @author Richard_yyf
 * @version 1.0 2019/10/28
 */
@Configuration
@EnableAspectJAutoProxy
@ComponentScan("ric.study.demo.aop")
public class Boostrap {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Boostrap.class);
        TestSvc svc = (TestSvc) context.getBean("testSvc");
        svc.process();
        System.out.println("==================");
        DateSvc dateSvc = (DateSvc) context.getBean("dateSvc");
        dateSvc.printDate(new Date());
    }
}

5. 输出

在service 方法执行前 打印第 1 次日志
拦截的service 方法的方法签名: void ric.study.demo.aop.svc.TestSvcImpl.process()
拦截的service 方法的方法入参: []
在service的方法执行前 打印第 2 次日志
test svc is working
==================
在service 方法执行前 打印第 1 次日志
拦截的service 方法的方法签名: void ric.study.demo.aop.svc.DateSvcImpl.printDate(Date)
拦截的service 方法的方法入参: [Mon Nov 04 18:11:34 CST 2019]
在service的方法执行前 打印第 2 次日志
2019-11-04 18:11:34

JDK 动态代理和 Cglib

前面有提到过,Spring AOP在目标类有实现接口的时候,会使用JDK 动态代理来生成代理类,我们结合上面的DEMO看看,

image.png

如果我们想不管是否有实现接口,都是强制使用Cglib的方式来实现怎么办?

Spring 提供给了我们对应的配置方式,也就是proxy-target-class.

注解方式:
//@EnableAspectJAutoProxy(proxyTargetClass = true) // 这样子就是默认使用CGLIB
XML方式:
<aop:config proxy-target-class="true">

改了之后,

image.png

小结

本文详细介绍了Spring AOP的起源、名词概念以及基于注解的使用方式。

本文按照作者的写作习惯,是源码解析章节的前置学习章节。在下一章中,我们会以注解方式为入口,介绍Spring AOP 的源码设计,解读相关核心源码(整个源码解析看完,会对其他方式触类旁通,因为原理都是一样的)。

感兴趣的可以翻到【前言】部分,再看一下思维导图。

如果本文有帮助到你,希望能点个赞,这是对我的最大动力。