之前的源码解析章节,本人讲解了Spring IOC 的核心部分的源码。如果你熟悉Spring AOP的使用的话,在了解Spring IOC的核心源码之后,学习Spring AOP 的源码,应该可以说是水到渠成,不会有什么困难。
但是直接开始讲Spring AOP的源码,本人又觉得有点突兀,所以便有了这一章。Spring AOP 的入门使用介绍:包括Spring AOP的一些概念性介绍和配置使用方法。
这里先贴一下思维导图。
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有了两种配置方式。
<aop />
PS:个人比较钟情于@AspectJ 这种方式,使用下来是最方面的。也可能是因为我觉得XML方式配置的Spring Bean很不简洁、写起来不好看吧,所以有点排斥吧。23333~
本文主要针对注解方式讲解,并且给出对应的DEMO;之后的源码解析也会以注解的这种方式为范例讲解Spring AOP的源码(整个源码解析看完,会对其他方式触类旁通,因为原理都是一样的)
如果对其他配置方式感兴趣的同学可以google其他的学习资料。
来一条分割线, 正式开始
@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 { //.... }
Pointcut 在大部分地方被翻译成切点,用于定义哪些方法需要被增强或者说需要被拦截。
在Spring 中,我们可以认为 Pointcut 是用来匹配Spring 容器中所有满足指定条件的bean的方法。
比如下面的写法,
// 指定的方法 @Pointcut("execution(* testExecution(..))") public void anyTestMethod() {}
下面完整列举一下 Pointcut 的匹配方式:
这个最简单的方式就是上面的例子, "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/spri...
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。
这里是为了演示方便,所以写在了一起。
先直接上示例代码,里面包含了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来实际演示一遍。这里把顺序变一下;
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)); } }
@Aspect @Component public class PointCutConfig { @Pointcut("within(ric.study.demo.aop.svc..*)") public void inSvcLayer() {} }
/** * @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 次日志"); } }
@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()); } }
在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
前面有提到过,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 发布!