Spring源码解读——第一节
- 1.AOP, AspectJ, Spring AOP的区别
-
- 1.1 AOP
- 1.2 Spring AOP
- 1.3 AspectJ:
- 2.SpringAOP的配置方法
-
- 2.1 概念解释:
- 2.2 基于接口的配置
- 2.3 基于Spring 2.0 @AspectJ 配置
-
- 1.首先我们引入依赖
- 2.开启 @AspectJ 的注解配置方式:
- 3.配置 Pointcut
- 4.配置 Advice
- 5.使用JoinPoint获取参数
1.AOP, AspectJ, Spring AOP的区别
1.1 AOP
- AOP 要实现的是在我们原来写的代码的基础上,进行一定的包装,如在方法执行前、方法返回后、方法抛出异常后等地方进行一定的拦截处理或者叫增强处理。
- AOP 的实现并不是因为 Java提供了什么神奇的钩子,可以把方法的几个生命周期告诉我们,而是我们要实现一个代理,实际运行的实例其实是生成的代理类的实例。
1.2 Spring AOP
- 它基于动态代理来实现。默认地,如果使用接口的,用 JDK 提供的动态代理实现,如果没有接口,使用 CGLIB实现。大家一定要明白背后的意思,包括什么时候会不用 JDK 提供的动态代理,而用 CGLIB 实现。
- Spring 3.2 以后,spring-core 直接就把 CGLIB 和 ASM的源码包括进来了,这也是为什么我们不需要显式引入这两个依赖。
-
Spring 的 IOC 容器和 AOP 都很重要,Spring AOP 需要依赖于 IOC 容器来管理。 如果你是 web
开发者,有些时候,你可能需要的是一个 Filter 或一个 Interceptor,而不一定是 AOP。 Spring AOP 只能作用于Spring 容器中的 Bean,它是使用纯粹的 Java 代码实现的,只能作用于 bean 的方法。
- Spring 提供了 AspectJ 的支持,后面我们会单独介绍怎么使用,一般来说我们用纯的 Spring AOP 就够了。
- 很多人会对比 Spring AOP 和 AspectJ 的性能,Spring AOP是基于代理实现的,在容器启动的时候需要生成代理实例,在方法调用上也会增加栈的深度,使得 Spring AOP 的性能不如 AspectJ那么好。
1.3 AspectJ:
- AspectJ 出身也是名门,来自于 Eclipse 基金会,link:https://www.eclipse.org/aspectj
属于静态织入,它是通过修改代码来实现的,它的织入时机可以是:
-
Compile-time weaving:编译期织入,如类 A 使用 AspectJ 添加了一个属性,类 B
引用了它,这个场景就需要编译期的时候就进行织入,否则没法编译类 B。
-
Post-compile weaving:也就是已经生成了 .class 文件,或已经打成 jar
包了,这种情况我们需要增强处理的话,就要用到编译后织入。
-
Load-time weaving:指的是在加载类的时候进行织入,要实现这个时期的织入,有几种常见的方法。1、自定义类加载器来干这个,这个应该是最容易想到的办法,在被织入类加载到JVM 前去对它进行加载,这样就可以在加载的时候定义行为了。2、在 JVM 启动的时候指定 AspectJ 提供的
agent:-javaagent:xxx/xxx/aspectjweaver.jar。
-
AspectJ 能干很多 Spring AOP 干不了的事情,它是 AOP 编程的完全解决方案。Spring AOP
致力于解决的是企业级开发中最普遍的 AOP 需求(方法织入),而不是力求成为一个像 AspectJ 一样的 AOP 编程完全解决方案。
- 因为 AspectJ 在实际代码运行前完成了织入,所以大家会说它生成的类是没有额外运行时开销的。
2.SpringAOP的配置方法
2.1 概念解释:
首先要说明的是,这里介绍的 Spring AOP 是纯的 Spring 代码,和 AspectJ 没什么关系,但是 Spring 延用了 AspectJ 中的概念,包括使用了 AspectJ 提供的 jar 包中的注解,但是不依赖于其实现功能。
后面介绍的如 @Aspect、@Pointcut、@Before、@After 等注解都是来自于 AspectJ,但是功能的实现是纯 Spring AOP 自己实现的。
下面我们来介绍 Spring AOP 的使用方法,先从最简单的配置方式开始说起,这样读者想看源码也会比较容易。
目前 Spring AOP 一共有三种配置方式,Spring 做到了很好地向下兼容,所以大家可以放心使用。
- Spring 1.2 基于接口的配置:最早的 Spring AOP 是完全基于几个接口的。
- Spring 2.0 schema-based 配置:Spring 2.0 以后使用 XML 的方式来配置,使用 命名空间
-
Spring 2.0 @AspectJ 配置:使用注解的方式来配置,这种方式感觉是最方便的,还有,这里虽然叫做
@AspectJ,但是这个和 AspectJ 其实没啥关系。
2.2 基于接口的配置
1.定义一个接口和实现类:
2.接下来,我们定义两个 advice(通知),分别用于拦截方法执行前和方法返回后:
上面的两个 Advice 分别用于方法调用前输出参数和方法调用后输出结果。
3.现在可以开始配置了,我们配置一个名为 spring_1_2.xml 的文件:
4.接下来,我们跑起来看看:
查看输出结果:
准备执行方法: createUser, 参数列表:[Tom, Cruise, 55]
方法返回:User{firstName=‘Tom’, lastName=‘Cruise’, age=55, address=‘null’}
准备执行方法: queryUser, 参数列表:[]
方法返回:User{firstName=‘Tom’, lastName=‘Cruise’, age=55, address=‘null’}
缺点分析:
此中方法有个致命的问题,如果我们需要拦截 OrderService 中的方法,那么我们还需要定义一个 OrderService 的代理。如果还要拦截 PostService,得定义一个 PostService 的代理…
而且上面会对类的所有方法进行拦截,我们需要粒度小一点的,对其中的方法进行拦截:
在上面的配置中,配置拦截器的时候,interceptorNames 除了指定为 Advice,是还可以指定为 Interceptor 和 Advisor 的。
这里我们来理解 Advisor 的概念,它也比较简单,它内部需要指定一个 Advice,Advisor 决定该拦截哪些方法,拦截后需要完成的工作还是内部的 Advice 来做。
它有好几个实现类,这里我们使用实现类 NameMatchMethodPointcutAdvisor 来演示,从名字上就可以看出来,它需要我们给它提供方法名字,这样符合该配置的方法才会做拦截。
我们可以看到,userServiceProxy 这个 bean 配置了一个 advisor,advisor 内部有一个 advice。advisor 负责匹配方法,内部的 advice 负责实现方法包装。
注意,这里的 mappedNames 配置是可以指定多个的,用逗号分隔,可以是不同类中的方法。相比直接指定 advice,advisor 实现了更细粒度的控制,因为在这里配置 advice 的话,所有方法都会被拦截。
上面我们对每一个bean都会生成一个代理,我们需要一个自动生成代理的类:
下面介绍 autoproxy 的解决方案。
autoproxy:从名字我们也可以看出来,它是实现自动代理,也就是说当 Spring 发现一个 bean 需要被切面织入的时候,Spring 会自动生成这个 bean 的一个代理来拦截方法的执行,确保定义的切面能被执行。
这里强调自动,也就是说 Spring 会自动做这件事,而不用像前面介绍的,我们需要显式地指定代理类的 bean。
我们去掉原来的 ProxyFactoryBean 的配置,改为使用 BeanNameAutoProxyCreator 来配置:
然后我们实验代码:(不需要从容器中得到我们的代理类,代理类对我们来说是透明的)
发现没有,我们在使用的时候,完全不需要关心代理了,直接使用原来的类型就可以了,这是非常方便的。
输出结果就是 UserService 中的每个方法都得到了拦截:
准备执行方法: createUser, 参数列表:[Tom, Cruise, 55]
方法返回:User{firstName=‘Tom’, lastName=‘Cruise’, age=55, address=‘null’}
准备执行方法: queryUser, 参数列表:[]
方法返回:User{firstName=‘Tom’, lastName=‘Cruise’, age=55, address=‘null’}
2.3 基于Spring 2.0 @AspectJ 配置
1.首先我们引入依赖
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.11</version>
</dependency>
如果是使用 Spring Boot 的话,添加以下依赖即可:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2.开启 @AspectJ 的注解配置方式:
- 在 xml 中配置:
- 使用 @EnableAspectJAutoProxy
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
}
一旦开启了上面的配置,那么所有使用 @Aspect 注解的 bean 都会被 Spring 当做用来实现 AOP 的配置类,我们称之为一个 Aspect。
**注意了,@Aspect 注解要作用在 bean 上面,不管是使用 @Component 等注解方式,还是在 xml 中配置 bean,首先它需要是一个 bean。**我们要首先保证他是一个Bean,进行IOC注册。
3.配置 Pointcut
Pointcut 在大部分地方被翻译成切点,用于定义哪些方法需要被增强或者说需要被拦截,有点类似于之前介绍的 Advisor 的方法匹配。
Spring AOP 只支持 bean 中的方法(不像 AspectJ 那么强大),所以我们可以认为 Pointcut 就是用来匹配 Spring 容器中的所有 bean 的方法的。
@Pointcut("execution(* transfer(..))")// the pointcut expression
private void anyOldTransfer() {}// the pointcut signature
我们看到,@Pointcut 中使用了 execution 来正则匹配方法签名,这也是最常用的,除了 execution,我们再看看其他的几个比较常用的匹配方式:
其中项目开发中灵活度较大的还有一种:基于注解的方式:
@Pointcut里面就可以使用注解来进行切点的定义,灵活度比较大,有注解的地方就会调用方法
- within:指定所在类或所在包下面的方法(Spring AOP 独有)
- @annotation:方法上具有特定的注解,如 @Subscribe 用于订阅特定的事件。
- bean(idOrNameOfBean):匹配 bean 的名字(Spring AOP 独有)
Tips:上面匹配中,通常 “.” 代表一个包名,"…" 代表包及其子包,方法参数任意匹配使用两个点 “…”。
对于 web 开发者,Spring 有个很好的建议,就是定义一个 SystemArchitecture:(将所有的前面打包成一个类)
@Aspect
public class SystemArchitecture {
// web 层
@Pointcut("within(com.javadoop.web..*)")
public void inWebLayer() {}
// service 层
@Pointcut("within(com.javadoop.service..*)")
public void inServiceLayer() {}
// service 实现,注意这里指的是方法实现,其实通常也可以使用 bean(*ServiceImpl)
@Pointcut("execution(* com.javadoop..service.*.*(..))")
public void businessService() {}
// dao 层
@Pointcut("within(com.javadoop.dao..*)")
public void inDataAccessLayer() {}
// dao 实现
@Pointcut("execution(* com.javadoop.dao.*.*(..))")
public void dataAccessOperation() {}
}
使用样例:
@AfterReturning("com.javadoop.aop.SystemArchitecture.dataAccessOperation()")
public void doAccessCheck() {
// ...
}
上面这个 SystemArchitecture 很好理解,该 Aspect 定义了一堆的 Pointcut,随后在任何需要 Pointcut 的地方都可以直接引用(如 xml 中的 pointcut-ref="")。
配置 pointcut 就是配置我们需要拦截哪些方法,接下来,我们要配置需要对这些被拦截的方法做什么,也就是前面介绍的 Advice。
4.配置 Advice
下面这块代码示例了各种常用的情况:
注意,实际写代码的时候,不要把所有的切面都揉在一个 class 中。
@Aspect
public class AdviceExample {
// 这里会用到我们前面说的 SystemArchitecture
// 下面方法就是写拦截 "dao层实现"
@Before("com.javadoop.aop.SystemArchitecture.dataAccessOperation()")
public void doAccessCheck() {
// ... 实现代码
}
// 当然,我们也可以直接"内联"Pointcut,直接在这里定义 Pointcut
// 把 Advice 和 Pointcut 合在一起了,但是这两个概念我们还是要区分清楚的
@Before("execution(* com.javadoop.dao.*.*(..))")
public void doAccessCheck() {
// ... 实现代码
}
@AfterReturning("com.javadoop.aop.SystemArchitecture.dataAccessOperation()")
public void doAccessCheck() {
// ...
}
@AfterReturning(
pointcut="com.javadoop.aop.SystemArchitecture.dataAccessOperation()",
returning="retVal")
public void doAccessCheck(Object retVal) {
// 这样,进来这个方法的处理时候,retVal 就是相应方法的返回值,是不是非常方便
// ... 实现代码
}
// 异常返回
@AfterThrowing("com.javadoop.aop.SystemArchitecture.dataAccessOperation()")
public void doRecoveryActions() {
// ... 实现代码
}
@AfterThrowing(
pointcut="com.javadoop.aop.SystemArchitecture.dataAccessOperation()",
throwing="ex")
public void doRecoveryActions(DataAccessException ex) {
// ... 实现代码
}
// 注意理解它和 @AfterReturning 之间的区别,这里会拦截正常返回和异常的情况
@After("com.javadoop.aop.SystemArchitecture.dataAccessOperation()")
public void doReleaseLock() {
// 通常就像 finally 块一样使用,用来释放资源。
// 无论正常返回还是异常退出,都会被拦截到
}
// 感觉这个很有用吧,既能做 @Before 的事情,也可以做 @AfterReturning 的事情
@Around("com.javadoop.aop.SystemArchitecture.businessService()")
public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
// start stopwatch
Object retVal = pjp.proceed();
// stop stopwatch
return retVal;
}
}
细心的读者可能发现了有些 Advice 缺少方法传参,如在 @Before 场景中参数往往是非常有用的,比如我们要用日志记录下来被拦截方法的入参情况。
5.使用JoinPoint获取参数
Spring 提供了非常简单的获取入参的方法,使用 org.aspectj.lang.JoinPoint 作为 Advice 的第一个参数即可,如:
@Before("com.javadoop.springaoplearning.aop_spring_2_aspectj.SystemArchitecture.businessService()")
public void logArgs(JoinPoint joinPoint) {
System.out.println("方法执行前,打印入参:" + Arrays.toString(joinPoint.getArgs()));
}
注意:第一,必须放置在第一个参数上;第二,如果是 @Around,我们通常会使用其子类 ProceedingJoinPoint,因为它有 procceed()/procceed(args[]) 方法。
到这里,我们介绍完了 @AspectJ 配置方式中的 Pointcut 和 Advice 的配置。对于开发者来说,其实最重要的就是这两个了,定义 Pointcut 和使用合适的 Advice 在各个 Pointcut 上。
下面,我们用这一节介绍的 @AspectJ 来实现上一节实现的记录方法传参和记录方法返回值。
xml 的配置非常简单:
这里是示例,所以 bean 的配置还是使用了 xml 的配置方式。
输出结果:
方法执行前,打印入参:[Tom, Cruise, 55]
User{firstName='Tom', lastName='Cruise', age=55, address='null'}
方法执行前,打印入参:[]
User{firstName='Tom', lastName='Cruise', age=55, address='null'}
JoinPoint 除了 getArgs() 外还有一些有用的方法,大家可以进去稍微看一眼。
最后提一点,@Aspect 中的配置不会作用于使用 @Aspect 注解的 bean。
最后感谢https://javadoop.com/post/spring-aop-intro博主的无私分享自己的知识结果。