10 Spring AOP源码解析
目录
Pt1 Spring AOP基础知识
AOP是Aspect Oriented Programming的缩写,意思是面向切面编程,通过预编译和运行时的动态代理,实现在不修改源代码的情况下给程序动态添加功能。同时AOP也为服务方和调用方实现解耦提供了很有效的方式。
在日常编码中,通常分为业务代码和非业务代码,比如事务、日志等,这些代码和业务代码混在一起。举个最常见的例子,就是JDBC的事务处理,有大量的模板代码是的事务处理代码,这些大量重复的、复制粘贴的、和业务处理无关的代码,为维护带来了风险和困难。AOP通过代理机制,可以将这些非业务代码剥离,使得他们可以独立于业务代码存在,从而降低维护的难度和风险。
所以,Spring AOP是一种编程范式,主要目的是将非功能性需求从功能型需求中分离出来,达到解耦的目标。
Pt1.1 AOP核心概念
-
Aspect(切面)
Aspect 声明类似于 Java 中的类声明,在 Aspect 中会包含着一些 Pointcut 以及相应的 Advice。
-
Jointpoint(连接点)
表示在程序中明确定义的点,典型的包括方法调用,对类成员的访问以及异常处理程序块的执行等等,它自身还可以嵌套其它 joint point。
-
Pointcut(切点)
表示一组 joint point,这些 joint point 或是通过逻辑关系组合起来,或是通过通配、正则表达式等方式集中起来,它定义了相应的 Advice 将要发生的地方。
-
Advice(增强)
Advice 定义了在 Pointcut 里面定义的程序点具体要做的操作,它通过 before、after 和 around 来区别是在每个 joint point 之前、之后还是代替执行的代码。
-
@Before:该注解标注的方法在业务模块代码执行之前执行,其不能阻止业务模块的执行,除非抛出异常;
-
@AfterReturning:该注解标注的方法在业务模块代码执行之后执(正常返回)行;
-
@AfterThrowing:该注解标注的方法在业务模块抛出指定异常后执行;
-
@After:该注解标注的方法在所有的Advice执行完成后执行,无论业务模块是否抛出异常,类似于finally的作用;
-
@Around:该注解功能最为强大,其所标注的方法用于编写包裹业务模块执行的代码,其可以传入一个ProceedingJoinPoint用于调用业务模块的代码,无论是调用前逻辑还是调用后逻辑,都可以在该方法中编写,甚至其可以根据一定的条件而阻断业务模块的调用;
-
@DeclareParents:其是一种Introduction类型的模型,在属性声明上使用,主要用于为指定的业务模块添加新的接口和相应的实现。
-
@Aspect:严格来说,其不属于一种Advice,该注解主要用在类声明上,指明当前类是一个组织了切面逻辑的类,并且该注解中可以指定当前类是何种实例化方式,主要有三种:singleton、perthis和pertarget,具体的使用方式后面会进行讲解。
-
-
Target(目标对象)
织入 Advice 的目标对象.。目标对象可以被一个或者多个切面织入。
-
Weaving(织入)
将 Aspect 和其他对象连接起来, 并创建 Adviced object 的过程,就是执行method同时执行advice。
-
AOP代理(AopProxy)
在Spring中有两种形式的代理:JDK动态代理和CGLib代理。后面会详细介绍。
Pt1.2 AOP切点配置规则
(1) execution
由于Spring切面粒度最小是达到方法级别,而execution表达式可以用于明确指定方法返回类型,类名,方法名和参数名等与方法相关的部件,并且在Spring中,大部分需要使用AOP的业务场景也只需要达到方法级别即可,因而execution表达式的使用是最为广泛的。
如下是execution表达式的语法:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)
这里问号表示当前项可以有也可以没有,其中各项的语义如下:
-
modifiers-pattern:方法的可见性,如public,protected;
-
ret-type-pattern:方法的返回值类型,如int,void等;
-
declaring-type-pattern:方法所在类的全路径名,如com.spring.Aspect;
-
name-pattern:方法名类型,如buisinessService();
-
param-pattern:方法的参数类型,如java.lang.String;
-
throws-pattern:方法抛出的异常类型,如java.lang.Exception;
如下是一个使用execution表达式的例子:
execution(public * com.spring.service.BusinessObject.businessService(java.lang.String,..))
上述切点表达式将会匹配使用public修饰,返回值为任意类型,并且是com.spring.BusinessObject类中名称为businessService的方法,方法可以有多个参数,但是第一个参数必须是java.lang.String类型的方法。上述示例中我们使用了..通配符,关于通配符的类型,主要有两种:
-
*通配符,该通配符主要用于匹配单个单词,或者是以某个词为前缀或后缀的单词。
如下示例表示返回值为任意类型,在com.spring.service.BusinessObject类中,并且参数个数为零的方法:
execution(* com.spring.service.BusinessObject.*())
下述示例表示返回值为任意类型,在com.spring.service包中,以Business为前缀的类,并且是类中参数个数为零方法:
execution(* com.spring.service.Business*.*())
-
..通配符,该通配符表示0个或多个项,主要用于declaring-type-pattern和param-pattern中,如果用于declaring-type-pattern中,则表示匹配当前包及其子包,如果用于param-pattern中,则表示匹配0个或多个参数。
如下示例表示匹配返回值为任意类型,并且是com.spring.service包及其子包下的任意类的名称为businessService的方法,而且该方法不能有任何参数:
execution(* com.spring.service..*.businessService())
这里需要说明的是,包路径service...businessService()中的..应该理解为延续前面的service路径,表示到service路径为止,或者继续延续service路径,从而包括其子包路径;后面的.businessService(),这里的*表示匹配一个单词,因为是在方法名前,因而表示匹配任意的类。
如下示例是使用..表示任意个数的参数的示例,需要注意,表示参数的时候可以在括号中事先指定某些类型的参数,而其余的参数则由..进行匹配:
execution(* com.spring.service.BusinessObject.businessService(java.lang.String,..))
(2) within
within表达式的粒度为类,其参数为全路径的类名(可使用通配符),表示匹配当前表达式的所有类都将被当前方法环绕。如下是within表达式的语法:
within(declaring-type-pattern)
within表达式只能指定到类级别,如下示例表示匹配com.spring.service.BusinessObject中的所有方法:
within(com.spring.service.BusinessObject)
within表达式路径和类名都可以使用通配符进行匹配,比如如下表达式将匹配com.spring.service包下的所有类,不包括子包中的类:
within(com.spring.service.*)
如下表达式表示匹配com.spring.service包及子包下的所有类:
within(com.spring.service..*)
(3) args
args表达式的作用是匹配指定参数类型和指定参数数量的方法,无论其类路径或者是方法名是什么。这里需要注意的是,args指定的参数必须是全路径的。如下是args表达式的语法:
args(param-pattern)
如下示例表示匹配所有只有一个参数,并且参数类型是java.lang.String类型的方法:
args(java.lang.String)
也可以使用通配符,但这里通配符只能使用..,而不能使用*。如下是使用通配符的实例,该切点表达式将匹配第一个参数为java.lang.String,最后一个参数为java.lang.Integer,并且中间可以有任意个数和类型参数的方法:
args(java.lang.String,..,java.lang.Integer)
Pt2 Spring AOP使用示例
Pt2.1 AOP日志(XML)
-
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>spring-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.6</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
-
spring.xml配置AOP信息
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 自动扫描装配 -->
<context:component-scan base-package="com.example.springdemo"/>
<!--启用spring的一些annotation -->
<context:annotation-config/>
<!-- AOP配置 -->
<bean id="logAspect" class="com.example.springdemo.aop.log.LogAspect"></bean>
<aop:config>
<aop:aspect id="log" ref="logAspect">
<aop:pointcut id="logPointcut" expression="execution(* com.example.springdemo.controller.LogController.LogDemo(..))"/>
<aop:before method="logBefore" pointcut-ref="logPointcut"/>
<aop:after method="logAfter" pointcut-ref="logPointcut"/>
</aop:aspect>
</aop:config>
</beans>
-
加载spring.xml
@Configuration
@ImportResource(locations = {"classpath:spring.xml"})
public class ConfigClass {
}
-
定义切面
public class LogAspect {
public LogAspect() {
System.out.println("init");
}
public void logBefore() {
System.out.println("Before execution log.");
}
public void logAfter() {
System.out.println("After execution log.");
}
}
-
定义Controller
@RestController
public class LogController {
@RequestMapping(value = "logAop", method = RequestMethod.GET)
public String LogDemo() {
return "Execute Log Aop test.";
}
}
-
运行Controller
Before execution log.
After execution log.
Pt2.2 JDBC(Annotation)
-
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>spring-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.6</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
-
AOP配置
@Component
@Aspect
public class JdbcAspect {
// 定义切点
@Pointcut("execution(* com.example.springdemo.controller.JdbcController.jdbcDemo(..))")
public void jdbcPointcut() {
}
@Before("JdbcAspect.jdbcPointcut()")
public void jdbcBefore() {
System.out.println("begin transaction");
}
@AfterReturning("JdbcAspect.jdbcPointcut()")
public void jdbcAfter() {
System.out.println("commit transaction");
}
@AfterThrowing("JdbcAspect.jdbcPointcut()")
public void jdbcException() {
System.out.println("rollback transaction");
}
}
-
Controller
@RestController
public class JdbcController {
@RequestMapping(value = "jdbcAop", method = RequestMethod.GET)
public String jdbcDemo() {
return "Execute jdbc Aop test.";
}
}
-
测试请求
begin transaction
commit transaction
Pt3 Spring AOP核心类
Pt3.1 BeanPostProcessor
BeanPostProcessor是Spring IOC容器给我们提供的一个扩展接口,它可以监听容器触发的Bean生命周期事件。向容器中注册BeanPostProcessor之后,容器中管理