Java AOP
AOP 概述
AOP,也就是面向切面编程 。面向切面编程将程序的运行过程分解成各个切面 。AOP就是在运行时通过动态的代理技术对目标方法进行增强(无侵入性解耦),可以在目标方法调用前后或调用过程中执行其他额外的逻辑。
AOP的优势
- 代码无侵入:没有修改原始的业务方法,就已经对原始的业务进行了功能的增强或者功能的改变
- 减少了重复代码
- 提高了开发效率
- 维护方便
AOP 使用场景
假设我们现在有几个简单的方法,加减乘除计算。
public class Calc {
public int add(int a, int b) {
return a+b;
}
public int sub(int a, int b) {
return a-b;
}
public int mul(int a, int b) {
return a*b;
}
public int div(int a, int b) {
return a/b;
}
}
假设我们希望,无论调用加减乘除,都在调入时写入日志。没有用AOP时,写法:
@Slf4j
public class Calc {
public int add(int a, int b) {
log.info("调用了方法【add】,参数[{},{}]",a,b);
return a+b;
}
public int sub(int a, int b) {
log.info("调用了方法【sub】,参数[{},{}]",a,b);
return a-b;
}
public int mul(int a, int b) {
log.info("调用了方法【mul】,参数[{},{}]",a,b);
return a*b;
}
public int div(int a, int b) {
log.info("调用了方法【div】,参数[{},{}]",a,b);
return a/b;
}
}
这样的写法,显然代码入侵性太强了。不利于维护,如果使用AOP该怎么做呢?
步骤
- 引入依赖项
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
-
创建Services。
com.example.demo.services
下方法:Calc 和com.example.demo.service.impl
下方法Calc。@Service public interface Calc { public int add(int a, int b); public int sub(int a, int b) ; public int mul(int a, int b); public int div(int a, int b) ; } @Component public class CalImpl implements Calc { @Override public int add(int a, int b) { return a + b; } @Override public int sub(int a, int b) { return a-b; } @Override public int mul(int a, int b) { return a*b; } @Override public int div(int a, int b) { return a/b; } }
-
创建切面方法。
@Slf4j @Component @Aspect public class CalLogAspct { @Before("execution(* com.example.demo.services.*.*(..))") public void before(JoinPoint joinPoint) { log.info("调用了方法【" +joinPoint.getSignature().getName() + "】,使用参数参数[{},{}]",joinPoint.getArgs()[0],joinPoint.getArgs()[1]); }
@Before("execution(* com.example.demo.services.*.*(..))")
表示:com.exaple.dem.services下的所有方法都使用了@Before拦截器。即services下的所有方法都执行前都会先调用@Before注解对应的方法。具体的切入点表达式,我会在后面详解。 -
测试
@SpringBootTest public class CalcTest { @Autowired private Calc calc; /** * * @author lyj * @date 2024-11-20 */ @Test public void test(){ System.out.println(calc.add(1,2)); System.out.println(calc.sub(1,2)); System.out.println(calc.mul(1,2)); System.out.println(calc.div(1,2)); } }
运行结果如下:
我们可以看到。方法之前,调用了切面方法。
切入点表达式
从AOP的入门程序到现在,我们一直都在使用切入点表达式来描述切入点。下面我们就来详细的介绍一下切入点表达式的具体写法 。
常用形式
execution(……)
根据方法名来匹配@annotation(……)
根据注解来匹配
execution
execution主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配,语法为:
execution(访问修饰符? 返回值 包名.类名.?方法名(方法参数) throws 异常?)
- 访问修饰符:可省略(比如:public、protected)
- 包名.类名 :可省略
- throws 异常 可省略(注意是方法声明抛出的异常,不是实际抛出的而异常)
例如,完整的切点表单式:
@Before("execution(public * com.example.demo.services.Calc.*(..) throws ArithmeticException)")
可以使用通配符描述切入点:
*
:单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,也可以通配包、类、方法名的一部分..
:多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数
切入点表达式语法规则:
-
方法的访问修饰符可以省略
-
返回值可以使用*代替(任意返回值类型)
-
包名可以使用
*
号代替,代表任意包(一层包使用一个*
) -
使用
..
匹配包名,标识此包以及此包下的所有子包 -
类名可以使用
*
代替,标识任意类 -
方法名可以使用
*
代替,表示任意方法 -
可以使用
*
配置参数,一个任意类型的参数 -
可以使用
..
配置参数,任意个任意类型的参数。 -
根据业务需要,可以使用 且(&&)、或(||)、非(!) 来组合比较复杂的切入点表达式。
execution(* com.itheima.service.DeptService.list(..)) || execution(* com.itheima.service.DeptService.delete(..))
切入点表达式书写建议
- 所有业务方法名在命名是尽量规范,方便切入点表达式快速匹配。如:查询方法都是find开头,更新方法都是update开头。
- 描述切入点方法通常基于接口描述,而不是直接描述实现类、增强拓展性。
- 在满足业务需要的前提下,尽量缩小切入点的匹配方法。如:包名尽量不要使用
..
,使用*
匹配单个包。
@annotation
已经学习了execution切入点表达式的语法。那么如果我们要匹配多个无规则的方法,比如:list()和 delete()这两个方法。这个时候我们基于execution这种切入点表达式来描述就不是很方便了。而在之前我们是将两个切入点表达式组合在了一起完成的需求,这个是比较繁琐的。
我们可以借助于另一种切入点表达式annotation来描述这一类的切入点,从而来简化切入点表达式的书写。
实现步骤:
1️⃣编写自定义注解
2️⃣在业务类做为连接点的方法上添加自定义注解的方法
Java中自定义注解的使用这里不在赘述。例如:添加注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyLog {
}
3️⃣ 要引用的方法添加注释
@Service
public interface CalC2 {
public int add(int a, int b);
public int sub(int a, int b) ;
public int mul(int a, int b);
public int div(int a, int b) ;
}
@Component
public class Cal2Impl implements CalC2 {
@Override
@MyLog
public int add(int a, int b) {
return a + b;
}
@Override
public int sub(int a, int b) {
return a-b;
}
@Override
public int mul(int a, int b) {
return a*b;
}
@Override
public int div(int a, int b) throws ArithmeticException {
return a/b;
}
}
测试后,引用注释@MyLog
调用了切面:
通知类型
@Around
:环绕通知,此注解标注的通知方法在目标方法前、后都被执行
Before
: 前置通知,此注解标准的方法在目标方法前被执行
After
:后置通知,此注解标准的通知方法在目标方法后被执行,无论是否有异常都会执行
AfterReturning
:返回后通知,此注解标准的方法在目标方法后被执行,有异常通常不会被执行
AfterThrowing
:异常通知,此注解标准的通知方法发生
https://blog.csdn.net/qq_42192693/article/details/113378787
通知顺序
当我们定义了多个切面,而多个切面匹配到同一个目标方法的时候,应该使用什么顺序
- 目标方法前的通知方法:字母排名靠前的先执行
- 目标方法后的通知方法:字母排名靠前的后执行。
如果我们想控制通知的执行顺序有两种方式:
- 修改切面类的类名(这种方式非常繁琐,而且不便管理)
- 使用Spring提供的
@Order
注解(value值月小,优先等级越高)
连接点
连接点可以简单理解为可以被AOP控制的方法。
我们目标对象当中所有的方法是不是都是可以被AOP控制的方法。而在SpringAOP当中,连接点又特指方法的执行。
在Spring中用JoinPoint抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名、方法名、方法参数等。
- 对于@Around通知,获取连接点信息只能使用ProceedingJoinPoint类型
- 对于其他四种通知,获取连接点信息只能使用JoinPoint,它是ProceedingJoinPoint的父类型
@Slf4j
@Component
@Aspect
public class MyAspect7 {
@Pointcut("@annotation(com.itheima.anno.MyLog)")
private void pt(){}
//前置通知
@Before("pt()")
public void before(JoinPoint joinPoint){
log.info(joinPoint.getSignature().getName() + " MyAspect7 -> before ...");
}
//后置通知
@After("pt()")
public void after(JoinPoint joinPoint){
log.info(joinPoint.getSignature().getName() + " MyAspect7 -> after ...");
}
//环绕通知
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
//获取目标类名
String name = pjp.getTarget().getClass().getName();
log.info("目标类名:{}",name);
//目标方法名
String methodName = pjp.getSignature().getName();
log.info("目标方法名:{}",methodName);
//获取方法执行时需要的参数
Object[] args = pjp.getArgs();
log.info("目标方法参数:{}", Arrays.toString(args));
//执行原始方法
Object returnValue = pjp.proceed();
return returnValue;
}
}