1. Spring AOP 系列1 —— 初识Spring AOP
发布于 2022年 03月 15日 16:26
1. 什么是AOP
AOP(Aspect Oriented Programming) 面向切面编程,是目前软件开发中的一个热点,是Spring框架内容,利用AOP可以对业务逻辑的各个部分隔离,从而使的业务逻辑各部分的耦合性降低,提高程序的可重用性,提升开发效率。
AOP的拦截功能是由java中的动态代理来实现的。说白了,就是在目标类的基础上增加切面逻辑,生成增强的目标类(该切面逻辑或者在目标类函数执行之前,或者目标类函数执行之后,或者在目标类函数抛出异常时候执行。不同的切入时机对应不同的Interceptor的种类,如BeforeAdviseInterceptor,AfterAdviseInterceptor以及ThrowsAdviseInterceptor等)。那么动态代理是如何实现将切面逻辑(advise)织入到目标类方法中去的呢?下面我们就来详细介绍并实现AOP中用到的两种动态代理。AOP的源码中用到了两种动态代理来实现拦截切入功能:jdk动态代理和cglib动态代理。两种方法同时存在,各有优劣。jdk动态代理是由java内部的反射机制来实现的,cglib动态代理底层则是借助asm来实现的。总的来说,反射机制在生成类的过程中比较高效,而asm在生成类之后的相关执行过程中比较高效(可以通过将asm生成的类进行缓存,这样解决asm生成类过程低效问题)。还有一点必须注意:jdk动态代理的应用前提,必须是目标类基于统一的接口。如果没有上述前提,jdk动态代理不能应用。由此可以看出,jdk动态代理有一定的局限性,cglib这种第三方类库实现的动态代理应用更加广泛,且在效率上更有优势。
2. 应该场景
AOP是处理一些横切行问题。这些横切性问题不会影响到主逻辑的实现,但是会散落到代码的各个部分,难以维护。AOP就是把这些问题和主业务逻辑分开,达到与主业务逻辑解耦的目的。
- Authentication 权限
- Caching 缓存
- Context passing 内容传递
- Error handling 错误处理
- Lazy loading 懒加载
- Debugging 调试
- logging, tracing, profiling and monitoring 记录跟踪 优化 校准
- Performance optimization 性能优化
- Persistence 持久化
- Resource pooling 资源池
- Synchronization 同步
- Transactions 事务
3. AOP与OOP的区别:
OOP面向对象编程,针对业务处理过程的实体及其属性和行为进行抽象封装,以获得更加清晰高效的逻辑单元划分。而AOP则是针对业务处理过程中的切面进行提取,它所面对的是处理过程的某个步骤或阶段,以获得逻辑过程的中各部分之间低耦合的隔离效果。这两种设计思想在目标上有着本质的差异。
通过下面的图可以清晰的理解AOP与OOP的区别:
4. AOP中的概念
- AOP代理(AOP Proxy):AOP框架创建的对象,代理就是目标对象的加强。Spring中的AOP代理可以使JDK动态代理,也可以是CGLIB代理,前者基于接口,后者基于子类。
- Join point(连接点):连接点就是Advice在应用程序上执行的点或时机,表示在程序中明确定义的点,一般是方法的调用。被拦截到的点,因为Spring只支持方法类型的连接点,所以在Spring中连接点指的就是被拦截到的方法,实际上连接点还可以是字段或者构造器。
- Advice(通知):Advice 定义了在 Pointcut里面定义的程序点具体要做的操作,AOP在特定的切入点上执行的增强处理,有before(前置),after(后置),afterReturning(最终),afterThrowing(异常),around(环绕)。
- Before:在目标方法被调用之前做增强处理,@Before只需要指定切入点表达式即
- AfterReturning:在目标方法正常完成后做增强,@AfterReturning除了指定切入点表达式后,还可以指定一个返回值形参名returning,代表目标方法的返回值
- AfterThrowing:主要用来处理程序中未处理的异常,@AfterThrowing除了指定切入点表达式后,还可以指定一个throwing的返回值形参名,可以通过该形参名来访问目标方法中所抛出的异常对象
- After:在目标方法完成之后做增强,无论目标方法是否成功完成。@After可以指定一个切入点表达式
- Around:环绕通知,在目标方法完成前后做增强处理,环绕通知是最重要的通知类型,像事务,日志等都是环绕通知,注意编程中核心是一个ProceedingJoinPoint
- Aspect(切面): Aspect 声明类似于 Java 中的类声明,在 Aspect 中会包含着一些 Pointcut 以及相应的 Advice。
- Pointcut(切点):表示一组 joint point,这些 joint point 或是通过逻辑关系组合起来,或是通过通配、正则表达式等方式集中起来,它定义了相应的 Advice 将要发生的地方。
- Target(目标对象):织入 Advice 的目标对象。
- Weave(织入):将 Aspect 和其他对象连接起来, 并创建 Adviced object 的过程。
- Target Object(目标对象): 包含连接点的对象。也被称作被通知或被代理对象。POJO(Plain Ordinary Java Object)简单的Java对象,实际就是普通JavaBeans。
- introduction(引入):在不修改代码的前提下,引入可以在运行期为类动态地添加一些方法或字段
AOP中的Joinpoint可以有多种类型:构造方法调用,字段的设置和获取,方法的调用,方法的执行,异常的处理执行,类的初始化。也就是说在AOP的概念中我们可以在上面的这些Joinpoint上织入我们自定义的Advice,但是在Spring中却没有实现上面所有的joinpoint,确切的说,Spring只支持方法执行类型的Joinpoint
5. 实战
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>2.2.4.RELEASE</version>
</dependency>
四种实现方式
- 经典的基于代理的AOP
- @AspectJ注解
- 纯POJO切面,通过aop:config标签配置
AOP配置元素 | 描述 |
---|---|
aop:advisor | 定义AOP通知器 |
aop:after | 定义AOP后置通知(不管该方法是否执行成功) |
aop:after-returning | 在方法成功执行后调用通知 |
aop:after-throwing | 在方法抛出异常后调用通知 |
aop:around | 定义AOP环绕通知 |
aop:aspect | 定义切面 |
aop:aspect-autoproxy | 定义@AspectJ注解驱动的切面 |
aop:before | 定义AOP前置通知 |
aop:config | 顶层的AOP配置元素,大多数的aop:*包含在aop:config元素内 |
aop:declare-parent | 为被通知的对象引入额外的接口,并透明的实现 |
aop:pointcut | 定义切点 |
- 注入式AspectJ切面
AspectJ指示器 | 描述 |
---|---|
arg() | 限制连接点匹配参数为指定类型的执行方法 |
@args() | 限制连接点匹配参数由指定注解标注的执行方法 |
execution() | 用于匹配是连接点的执行方法 |
this() | 限制连接点匹配AOP代理的Bean引用为指定类型的类 |
target() | 限制连接点匹配目标对象为执行类型的类 |
@target() | 限制连接点匹配特定的执行对象,这些对象对应的类要具备指定类型的注解 |
within() | 限制连接点匹配指定的类型 |
@within() | 限制连接点匹配指定注解所标注的类型 |
@annotation() | 限制匹配带有指定注解连接点 |
1. 原生spring实现
定义一个通用接口,所有实现此接口的类都有一个咸鱼方法和一个测试aop的方法
public interface HelloWorld {
void saltedFish();
void testPrintTime();
}
实现1
public class HelloWorldImpl1 implements HelloWorld{
@Override
public void saltedFish() {
System.out.println("this is a salted fish =========== 1");
}
@Override
public void testPrintTime() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("testPrintTime 1=============111111111");
}
}
实现2
public class HelloWorldImpl2 implements HelloWorld{
@Override
public void saltedFish() {
System.out.println("this is a salted fish =========== 2");
}
@Override
public void testPrintTime() {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("testPrintTime 2=============22222222");
}
}
定义一个Advice,实现在连接点之前之后该干的事
public class TimeHandler implements MethodBeforeAdvice, AfterReturningAdvice {
Long before = 0L;
@Override
public void afterReturning(Object o, Method method, Object[] objects, Object o1) throws Throwable {
Long after = System.currentTimeMillis();
System.out.println("==========代理后time, " + after + " ======= 间隔: " + (after - before) + "==========");
}
@Override
public void before(Method method, Object[] objects, Object o) throws Throwable {
before = System.currentTimeMillis();
System.out.println("==========代理前time:" + before + "===========");
}
}
通过xml配置切面
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 定义 -->
<bean id="h1" class="com.example.demo.aop.HelloWorldImpl1"></bean>
<bean id="h2" class="com.example.demo.aop.HelloWorldImpl2"></bean>
<!-- 定义advice -->
<bean id="timeHandler" class="com.example.demo.aop.TimeHandler"></bean>
<!-- 定义point cut -->
<bean id="timePointCut" class="org.springframework.aop.support.JdkRegexpMethodPointcut">
<property name="pattern" value=".*testPrintTime"></property>
</bean>
<!-- 切面 关联切入点与通知 -->
<bean id="timeHandlerAdvisor" class="org.springframework.aop.support.DefaultPointcutAdvisor">
<property name="advice" ref="timeHandler"></property>
<property name="pointcut" ref="timePointCut"></property>
</bean>
<!-- 设置代理-->
<bean id="proxy1" class="org.springframework.aop.framework.ProxyFactoryBean">
<!-- 代理的对象 -->
<property name="target" ref="h1"></property>
<!-- 使用的切面 -->
<property name="interceptorNames" value="timeHandlerAdvisor"></property>
<!-- 代理接口 -->
<property name="interfaces" value="com.example.demo.aop.HelloWorld"></property>
</bean>
<!-- 设置代理-->
<bean id="proxy2" class="org.springframework.aop.framework.ProxyFactoryBean">
<!-- 代理的对象 -->
<property name="target" ref="h2"></property>
<!-- 使用的切面 -->
<property name="interceptorNames" value="timeHandlerAdvisor"></property>
<!-- 代理接口 -->
<property name="interfaces" value="com.example.demo.aop.HelloWorld"></property>
</bean>
</beans>
测试类
public class AOPTest {
public static void main(String[] args) {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("./application.xml");
HelloWorld helloWorld1 = (HelloWorld) applicationContext.getBean("proxy1");
HelloWorld helloWorld2 = (HelloWorld) applicationContext.getBean("proxy2");
helloWorld1.saltedFish();
System.out.println("---------------------");
helloWorld1.testPrintTime();
System.out.println("=======================");
helloWorld2.saltedFish();
System.out.println("---------------------");
helloWorld2.testPrintTime();
}
}
测试结果
this is a salted fish =========== 1
---------------------
==========代理前time:1582477901748===========
testPrintTime 1=============111111111
==========代理后time, 1582477902750 ======= 间隔: 1002==========
=======================
this is a salted fish =========== 2
---------------------
==========代理前time:1582477902750===========
testPrintTime 2=============22222222
==========代理后time, 1582477903250 ======= 间隔: 500==========
2. 基于AspectJ注解实现
对于将纯POJO申明成切面的方式中,如果不使用@AspectJ,那么就需要使用使用繁琐的XML配置,因此Spring借鉴了AspectJ的切面,以提供注解驱动的AOP,但是本质上依然是使用的SpringAop的动态代理的方式,只是变成模型几乎与AspectJ完全一样。
要在 Springboot中声明 AspectJ 切面, 需在 IOC 容器中将切面声明为 Bean 实例 即加入@Component 注解;当在 Spring IOC 容器中初始化 AspectJ 切面之后, Spring IOC 容器就会为那些与 AspectJ 切面相匹配的 Bean 创建代理。在 AspectJ 注解中, 切面只是一个带有 @Aspect 注解的 Java 类。
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
配置类
@Aspect
@Configuration // 一定要加此注解,或者在入口引入
public class AopLogger {
/**
* 标识这个方法是个前置通知, 切点表达式表示执行任意类的任意方法.
* 第一个 * 代表匹配任意修饰符及任意返回值,
* 第二个 * 代表任意类的对象,
* 第三个 * 代表任意方法,
* 参数列表中的 .. 匹配任意数量的参数
*
* @param joinPoint
*/
@Before("execution(* com.example.demo.aop..*.*(..))")
public void before(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
Object result = Arrays.asList(joinPoint.getArgs());
System.out.printf("method name is: %s, args is: %s%n", methodName, result);
}
@After("execution (* com.example.demo.aop..*.*(..))")
public void after(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
System.out.println("after log method name is: " + methodName);
}
// @AfterReturning(value = "execution(**.*(..))", returning = "result")
@AfterReturning(value = "execution (* com.example.demo.aop..*.*(..))", returning = "result")
public void afterReturn(JoinPoint joinPoint, Object result) {
String methodName = joinPoint.getSignature().getName();
System.out.printf("method name is: %s, and the result is: %s%n", methodName, result);
}
// @AfterThrowing(value = "execution(**.*(..))", throwing = "e")
@AfterThrowing(value = "execution (* com.example.demo.aop..*.*(..))", throwing = "e")
public void afterThrow(JoinPoint joinPoint, Exception e) {
String methodName = joinPoint.getSignature().getName();
System.out.printf("method name is: %s, and the exception is: %s%n", methodName, e);
}
// @Around("execution(**.*(..))")
@Around("execution (* com.example.demo.aop..*.*(..))")
public Object around(ProceedingJoinPoint joinPoint) {
StopWatch stopWatch = new StopWatch();
String name = joinPoint.getSignature().getName();
System.out.println("===============↓↓↓↓↓↓↓↓ " + name + " ↓↓↓↓↓↓↓↓=================");
stopWatch.start();
try {
return joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
} finally {
stopWatch.stop();
System.out.println("===============↑↑↑↑↑↑↑↑ " + name + " ↑↑↑↑↑↑↑↑=================");
}
return null;
}
}
测试方法
@Test
public void testAspectJ() {
helloWorldImpl1.saltedFish();
helloWorldImpl2.testPrintTime();
}
测试结果
===============↓↓↓↓↓↓↓↓ saltedFish ↓↓↓↓↓↓↓↓=================
method name is: saltedFish, args is: []
this is a salted fish =========== 1
===============↑↑↑↑↑↑↑↑ saltedFish ↑↑↑↑↑↑↑↑=================
after log method name is: saltedFish
method name is: saltedFish, and the result is: null
===============↓↓↓↓↓↓↓↓ testPrintTime ↓↓↓↓↓↓↓↓=================
method name is: testPrintTime, args is: []
testPrintTime 2=============22222222
===============↑↑↑↑↑↑↑↑ testPrintTime ↑↑↑↑↑↑↑↑=================
after log method name is: testPrintTime
method name is: testPrintTime, and the result is: null