Java-SpringBoot AOP-半小时了解AOP知识及使用注解实现AOP
环境
springboot 2.7.0 jdk 8
AOP是什么?
AOP (Aspect Orient Programming),直译过来就是 面向切面编程,AOP 是一种编程思想,是面向对象编程(OOP)的一种补充。 面向切面编程,实现在不修改源代码的情况下给程序动态统一添加额外功能的一种技术。 AOP可以拦截指定的方法并且对方法增强,而且无需侵入到业务代码中,使业务与非业务处理逻辑分离,比如Spring的事务,通过事务的注解配置,Spring会自动在业务方法中开启、提交业务,并且在业务处理失败时,执行相应的回滚策略。
Spring AOP 与 Aspectj
Spring aop 和 AspectJ, 这是 Java 的两个最受欢迎的 aop 框架,springboot 使用AspectJ中的 @Aspect、@Pointcut、@Before、@After、@Around等常用的注解,可以方便实现切入和增强通知等功能。
常用注解
- @Aspect标识一个切面
- 切入点:@Pointcut
- 通知:@Before、@After、@Around、@AfterReturning、@AfterThrowing
注:AOP = 切入点+通知
实现AOP的两种风格
使用切入点注解实现AOP
使用@Pointcut注解可以在一个切面类中,将多个通知的切入点表达式抽取出来。不需要在每个通知注解中重复定义切入点表达式,代码风格直观。
不使用切入点注解AOP
当然在切面类中可以不用定义一个切入点,直接在通知类注解中使用表达式完成切入+通知操作,在切面类中定义一个通知时,此方法还是比较简单的。
切入点表达式
切入点表单时需要结合AOP注解使用,例如 value 中的 execution:
- @Before(value = “execution(* com.example…getTime(…))”)
以下为注解支持的表达式类型: - execution: 用于匹配方法执行连接点。 这是使用Spring AOP时使用的主要切点标识符。 可以匹配到方法级别 ,细粒度
- within: 只能匹配类这级,只能指定类, 类下面的某个具体的方法无法指定, 粗粒度
- this: 匹配实现了某个接口:this(com.xyz.service.AccountService)
- target: 限制匹配到连接点(使用Spring AOP时方法的执行),其中目标对象(正在代理的应用程序对象)是给定类型的实例。
- args: 限制与连接点的匹配(使用Spring AOP时方法的执行),其中变量是给定类型的实例。 AOP) where the arguments are instances of the given types.
- @target: 限制与连接点的匹配(使用Spring AOP时方法的执行),其中执行对象的类具有给定类型的注解。
- @args: 限制匹配连接点(使用Spring AOP时方法的执行),其中传递的实际参数的运行时类型具有给定类型的注解。
- @within: 限制与具有给定注解的类型中的连接点匹配(使用Spring AOP时在具有给定注解的类型中声明的方法的执行)。
- @annotation:限制匹配连接点(在Spring AOP中执行的方法具有给定的注解)。
springboot中注解方式定义AOP
使用@Pointcut注解
package com.example.learnspringbootaop.aop;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
@Component
@Aspect // 切面 = 切入点 + 通知
public class MyAspect {
@Pointcut("@annotation(com.example.learnspringbootaop.annotation.TestMyAspect)")
public void pointcut(){
System.out.print(this.getClass().getSimpleName()+"\t");
System.out.println("pointcut\t开始执行\t类:"+this.getClass().getSimpleName()+" 方法:pointcut()");
}
@Before("pointcut()")
public void before(JoinPoint joinPoint) {
System.out.print(this.getClass().getSimpleName()+"\t");
System.out.println("before\t开始执行\t类:"+joinPoint.getTarget()+" 方法:"+ joinPoint.getSignature().getName());
}
@After("pointcut()")
public void after(JoinPoint joinPoint) {
System.out.print(this.getClass().getSimpleName()+"\t");
System.out.println("after\t查询结束\t类:"+joinPoint.getTarget()+" 方法:"+ joinPoint.getSignature().getName());
// 获取执行目标类和方法名等等
}
/**
* 环绕通知:
* 1.加入 try..cache 可以避免环绕通知的逻辑被调用方法异常打断;
* 2.catch 中需要将抛出异常,避免将异常吞掉;
* @param joinPoint
* @return
* @throws Throwable
*/
@Around("pointcut()")
public Object aroud(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.print(this.getClass().getSimpleName()+"\t");
System.out.println("aroud\t环绕通知-方法执行前\t类:"+joinPoint.getTarget()+" 方法:"+ joinPoint.getSignature().getName());
// 必须方法目标方法
Object proceed = null;
try {
proceed = joinPoint.proceed();
System.out.print(this.getClass().getSimpleName()+"\t");
}catch (Throwable ex){
System.out.print(this.getClass().getSimpleName()+"\t");
System.out.println("aroud\t执行时产生异常,error="+ex.toString());
throw ex;
}finally {
System.out.println("aroud\t环绕通知-方法执行后\t类:"+joinPoint.getTarget()+" 方法:"+ joinPoint.getSignature().getName());
}
// 将目标方法的返回值进行返回,否则调用目标方法的方法无法获取到返回值
return proceed;
}
@AfterReturning("pointcut()")
public void doAfterSuccess(JoinPoint joinPoint){
System.out.print(this.getClass().getSimpleName()+"\t");
System.out.println("AfterSuccess\t执行返回通知\t类:"+joinPoint.getTarget()+" 方法:"+ joinPoint.getSignature().getName());
}
@AfterThrowing("pointcut()")
public void doAfterError(JoinPoint joinPoint){
System.out.println("AfterError\t执行异常通知\t类:"+joinPoint.getTarget()+" 方法:"+ joinPoint.getSignature().getName());
}
}
不使用@Pointcut注解定义
package com.example.learnspringbootaop.aop;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
// 表示当前的类是一个配置类
@Configuration
//该注解只能用在类上,作用:代表当前类是一个切面类,等价于spring.xml中的<aop:config>标签
//所以现在有了<aop:config>切面,还需要 通知 + 切入点
// 切面 == 通知 + 切面
@Aspect
public class MyAdviceConfig {
/**
* @param joinPoint
* @Before:前置通知
* value:切入点表达式 二者加起来构建成为一个切面
* JoinPoint:连接点:可以理解为两个圆形的切点,从这个切点就可以获取到当前执行的目标类及方法
* 前置通知和后置通知的参数的都是 JoinPoint, 前置后置通知都没有返回值
*/
// 表示com.example包下的所有类所有方法都执行该前置通知
@Before(value = "execution(* com.example..getTime(..))")
public void before(JoinPoint joinPoint) {
System.out.print(this.getClass().getSimpleName()+"\t");
System.out.println("before\t开始执行\t类:"+joinPoint.getTarget()+" 方法:"+ joinPoint.getSignature().getName());
}
/**
* 后置通知,属性参数同上面的前置通知
* @param joinPoint 前置通知和后置通知独有的参数
*/
@After(value = "execution(* com.example..getTime(..))")
public void after(JoinPoint joinPoint) {
System.out.print(this.getClass().getSimpleName()+"\t");
System.out.println("after\t查询结束\t类:"+joinPoint.getTarget()+" 方法:"+ joinPoint.getSignature().getName());
// 获取执行目标类和方法名等等
}
/**
* 环绕通知
* @param joinPoint 环绕通知的正在执行中的连接点(这是环绕通知独有的参数)
* @return 目标方法执行的返回值
* @Around: 环绕通知,有返回值,环绕通知必须进行放行方法(就相当于拦截器),否则目标方法无法执行
*/
@Around(value = "execution(* com.example..getTime(..))")
public Object aroud(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.print(this.getClass().getSimpleName()+"\t");
System.out.println("aroud\t环绕通知-方法执行前\t类:"+joinPoint.getTarget()+" 方法:"+ joinPoint.getSignature().getName());
// 必须方法目标方法
Object proceed = joinPoint.proceed();
System.out.print(this.getClass().getSimpleName()+"\t");
System.out.println("aroud\t环绕通知-方法执行后\t类:"+joinPoint.getTarget()+" 方法:"+ joinPoint.getSignature().getName());
// 将目标方法的返回值进行返回,否则调用目标方法的方法无法获取到返回值
return proceed;
}
@AfterReturning("execution(* com.example..getTime(..))")
public void doAfterSuccess(JoinPoint joinPoint){
System.out.print(this.getClass().getSimpleName()+"\t");
System.out.println("AfterSuccess\t执行返回通知\t类:"+joinPoint.getTarget()+" 方法:"+ joinPoint.getSignature().getName());
}
@AfterThrowing("execution(* com.example..getTime(..))")
public void doAfterError(JoinPoint joinPoint){
System.out.print(this.getClass().getSimpleName()+"\t");
System.out.println("AfterError\t执行异常通知\t类:"+joinPoint.getTarget()+" 方法:"+ joinPoint.getSignature().getName());
}
}
定义一个service类
package com.example.learnspringbootaop.test1;
import com.example.learnspringbootaop.annotation.MyAnnotation;
import com.example.learnspringbootaop.annotation.TestMyAspect;
import org.springframework.stereotype.Service;
import utils.DatetimeUtil;
import java.util.Date;
@Service
public class TestService {
public String getTime(){
System.out.println("执行 getTime() 方法代码。。。");
return DatetimeUtil.getNowTime();
}
@TestMyAspect
public String getTime2(){
System.out.println("执行 getTime2() 方法代码。。。");
return DatetimeUtil.getNowTime();
}
@MyAnnotation
public String getTime3(){
System.out.println("执行 getTime3() 方法代码。。。");
return DatetimeUtil.getNowTime();
}
@TestMyAspect
public Integer getNumber(){
System.out.println("执行 getNumber() 方法代码。。。");
return Integer.parseInt("ABC");
}
}
测试下注解的效果
package com.example.learnspringbootaop.test1;
import com.example.learnspringbootaop.basic.BasicTest;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import static org.junit.jupiter.api.Assertions.*;
class TestServiceTest extends BasicTest {
@Autowired
private TestService testService;
@Test
void getTime() {
testService.getTime();
}
@Test
void getTime2() {
testService.getTime2();
}
@Test
void getTime3() {
testService.getTime3();
}
@Test
void getNumber() {
testService.getNumber();
}
}
package com.example.learnspringbootaop.basic;
import com.example.learnspringbootaop.LearnAopApplication;
import org.junit.After;
import org.junit.Before;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = LearnAopApplication.class)
public class BasicTest {
@Before
public void before(){
System.out.println("----------测试开始-------------");
}
@After
public void after(){
System.out.println("----------测试结束-------------");
}
}
总结
相比原生Spring AOP需要定义拦截器、切入点等操作,而在Springboot中借助Aspectj的注解可以在一个类中实现aop功能。 虽然Aspectj使用任意一个“通知注解”的切入点表达式,可以将"通知+切入点"合二为一,但由于破坏了单一职责原则,所以个人还是习惯在一个切面中定义一个业务的代码。
|