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该怎么做呢?

步骤

  1. 引入依赖项
<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));
        }
    }
    

    运行结果如下:
    image

    我们可以看到。方法之前,调用了切面方法。

切入点表达式

从AOP的入门程序到现在,我们一直都在使用切入点表达式来描述切入点。下面我们就来详细的介绍一下切入点表达式的具体写法 。

常用形式

  • execution(……) 根据方法名来匹配
  • @annotation(……)根据注解来匹配

execution

execution主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配,语法为:

execution(访问修饰符?  返回值  包名.类名.?方法名(方法参数) throws 异常?)
  • 访问修饰符:可省略(比如:public、protected)
  • 包名.类名 :可省略
  • throws 异常 可省略(注意是方法声明抛出的异常,不是实际抛出的而异常)

例如,完整的切点表单式:

@Before("execution(public * com.example.demo.services.Calc.*(..) throws  ArithmeticException)")

可以使用通配符描述切入点:

  • * :单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,也可以通配包、类、方法名的一部分
  • .. :多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数

切入点表达式语法规则:

  1. 方法的访问修饰符可以省略

  2. 返回值可以使用*代替(任意返回值类型)

  3. 包名可以使用*号代替,代表任意包(一层包使用一个*

  4. 使用..匹配包名,标识此包以及此包下的所有子包

  5. 类名可以使用*代替,标识任意类

  6. 方法名可以使用*代替,表示任意方法

  7. 可以使用*配置参数,一个任意类型的参数

  8. 可以使用..配置参数,任意个任意类型的参数。

  9. 根据业务需要,可以使用 且(&&)、或(||)、非(!) 来组合比较复杂的切入点表达式。

    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调用了切面:
image

通知类型

@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;
    }
}
posted @ 2024-11-18 16:00  陆陆无为而治者  阅读(2)  评论(0编辑  收藏  举报