前言
之前的源码解析章节,本人讲解了Spring IOC 的核心部分的源码。如果你熟悉Spring AOP的使用的话,在了解Spring IOC的核心源码之后,学习Spring AOP 的源码,应该可以说是水到渠成,不会有什么困难。
但是直接开始讲Spring AOP的源码,本人又觉得有点突兀,所以便有了这一章。Spring AOP 的入门使用介绍:包括Spring AOP的一些概念性介绍和配置使用方法。
这里先贴一下思维导图。
AOP 是什么
AOP : 面向切面编程(Aspect Oriented Programming)
Aspect是一种新的模块化机制,用来描述分散在对象、类或函数中的横切关注点(crosscutting concern)。从关注点中分离出横切关注点是面向切面的程序设计的核心概念。分离关注点使解决特定领域问题的代码从业务逻辑中独立出来,业务逻辑的代码中不再含有针对特定领域问题代码的调用,业务逻辑同特定领域问题的关系通过切面来封装、维护,这样原本分散在整个应用程序中的变动就可以很好地管理起来。
最近在看李智慧的《大型网站技术架构》一书中,作者提到,开发低耦合系统是软件设计的终极目标之一。AOP这种面向切面编程的的方式就体现了这样的理念。将一些重复的、和业务主逻辑不相关的功能性代码(日志记录、安全管理等)通过切面模块化地抽离出来进行封装,实现关注点分离、模块解耦,使得整个系统更易于维护管理。
这样分而治之的设计,让我感觉到了一种美感。
AOP 要实现的是在我们原来写的代码的基础上,进行一定的包装,如在方法执行前、方法返回后、方法抛出异常后等地方进行一定的拦截处理或者叫增强处理。
AOP 的实现并不是因为 Java 提供了什么神奇的钩子,可以把方法的几个生命周期告诉我们,而是我们要实现一个代理,实际运行的实例其实是生成的代理类的实例。
名词概念
前面提到过,Spring AOP 延用了 AspectJ 中的概念,使用了 AspectJ 提供的 jar 包中的注解。也就是Spring AOP里面的概念和术语,并不是Spring独有的,而是和AOP相关的。
概念可以草草看过,在看了之后的章节之后再回来看会对概念理解的更深。
术语 | 概念 |
---|---|
Aspect |
切面是Pointcut 和Advice 的集合,一般单独作为一个类。Pointcut 和Advice 共同定义了关于切面的全部内容,它是什么时候,在何时和何处完成功能。 |
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有了两种配置方式。
schema-based:Spring 2.0 以后使用 XML 的方式来配置,使用 命名空间
<aop />
@AspectJ 配置:Spring 2.0 以后提供的注解方式。这里虽然叫做 @AspectJ,但是这个和 AspectJ 其实没啥关系。
PS:个人比较钟情于@AspectJ 这种方式,使用下来是最方面的。也可能是因为我觉得XML方式配置的Spring Bean很不简洁、写起来不好看吧,所以有点排斥吧。23333~
本文主要针对注解方式讲解,并且给出对应的DEMO;之后的源码解析也会以注解的这种方式为范例讲解Spring AOP的源码(整个源码解析看完,会对其他方式触类旁通,因为原理都是一样的)
如果对其他配置方式感兴趣的同学可以google其他的学习资料。
来一条分割线,正式开始
1. 开启@AspectJ
注解配置方式
开启@AspectJ
的注解配置方式,有两种方式
-
在XML中配置:
<aop:aspectj-autoproxy/>
-
使用
@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 的匹配方式:
-
execution:匹配方法签名
这个最简单的方式就是上面的例子,
"execution(* testExecution(..))"
表示的是匹配名为testExecution
的方法,*
代表任意返回值,(..)
表示零个或多个任意参数。
-
within:指定所在类或所在包下面的方法(Spring AOP 独有)
// service 层 // ".." 代表包及其子包 @Pointcut("within(ric.study.demo.aop.svc..*)") public void inSvcLayer() {}
-
@annotation:方法上具有特定的注解
// 指定注解 @Pointcut("@annotation(ric.study.demo.aop.HaveAop)") public void withAnnotation() {}
-
bean(idOrNameOfBean):匹配 bean 的名字(Spring AOP 独有)
// controller 层 @Pointcut("bean(testController)") public void inControllerLayer() {}
上述是日常使用中常见的几种配置方式
有更细的匹配需求的,可以参考这篇文章:https://www.baeldung.com/spring-aop-pointcut-tutorial
关于 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看看,
如果我们想不管是否有实现接口,都是强制使用Cglib的方式来实现怎么办?
Spring 提供给了我们对应的配置方式,也就是proxy-target-class
.
注解方式:
//@EnableAspectJAutoProxy(proxyTargetClass = true) // 这样子就是默认使用CGLIB
XML方式:
<aop:config proxy-target-class="true">
改了之后,
小结
本文详细介绍了Spring AOP的起源、名词概念以及基于注解的使用方式。
本文按照作者的写作习惯,是源码解析章节的前置学习章节。在下一章中,我们会以注解方式为入口,介绍Spring AOP 的源码设计,解读相关核心源码(整个源码解析看完,会对其他方式触类旁通,因为原理都是一样的)。
感兴趣的可以翻到【前言】部分,再看一下思维导图。
如果本文有帮助到你,希望能点个赞,这是对我的最大动力。
本文由博客一文多发平台 OpenWrite 发布!