AOP 【java全端课30】

10 阅读3分钟

AOP前奏

计算器案例

package com.mytest.aopbefore;

public interface Calc {
    //加法
    public int add(int i,int j);

    //减法
    public int sub(int i,int j);

    //乘法
    public int mul(int i,int j);

    //除法
    public int div(int i,int j);

}
package com.mytest.aopbefore;

public class CalcImpl implements Calc{

    /**
     * 为加减乘除方法,添加如下功能
            1.记录日志
                计算之前:参数
                计算之后:结果
            2.验证功能
            3.
                ...
    */
    @Override
    public int add(int i, int j) {
//        System.out.println("==>add()加法执行之前,记录参数:i:"+i+",j:"+j);
        //验证i和j合法性
        //MyLogging.methodBefore("add",i,j);

        int rs = i + j;         //核心计算代码

//        MyLogging.methodAfter("add",rs);
//        System.out.println("==>add()加法执行之后,记录结果:"+rs);
        return rs;
    }

    @Override
    public int sub(int i, int j) {
//        System.out.println("==>sub()加法执行之前,记录参数:i:"+i+",j:"+j);
        MyLogging.methodBefore("sub",i,j);
        int rs = i - j;
        MyLogging.methodAfter("sub",rs);
//        System.out.println("==>sub()加法执行之后,记录结果:"+rs);
        return rs;
    }

    @Override
    public int mul(int i, int j) {
        int rs = i * j;
        return rs;
    }

    @Override
    public int div(int i, int j) {
        int rs = i / j;
        return rs;
    }

}

问题发现及解决方案

  • 发现问题

    • 核心业务代码(计算操作)中,直接书写非核心业务代码(日志,验证等)
    • 导致代码分散代码混乱问题
    • 代码分散:每个计算方法都书写相同代码
    • 代码混乱:核心代码与非核心代码耦合(书写在一处)
      • 高内聚,低耦合
  • 解决方案

    • 解决代码分散:将非核心业务代码,提取到工具类中(切面类)
    • 解决代码混乱:将非核心业务代码先横向提取到工具类,再动态织入回核心代码

静态代理与动态代理

代理模式

  • 生活中代理
    • 各种中介(房屋中介)
    • 你需要找房子,但不能亲力亲为
  • 程序中代理
    • 静态代理:代理对象固定
    • 动态代理:代理对象非固定

静态代理

package com.mytest.aopbefore;

public class MyStaticProxy implements Calc {

    //目标对象
    private CalcImpl calcImpl;

    public MyStaticProxy(CalcImpl calcImpl) {
        this.calcImpl = calcImpl;
    }

    @Override
    public int add(int i, int j) {
        //执行方法之前,记录日志
        MyLogging.methodBefore("add",i,j);
        //执行目标对象的加法
        int rs = calcImpl.add(i, j);
        //执行方法之后,记录日志
        MyLogging.methodAfter("add",rs);
        return rs;
    }

    @Override
    public int sub(int i, int j) {
        MyLogging.methodBefore("sub",i,j);
        //执行目标对象的减法
        int rs = calcImpl.sub(i, j);
        MyLogging.methodAfter("sub",rs);
        return rs;
    }

    @Override
    public int mul(int i, int j) {
        return 0;
    }

    @Override
    public int div(int i, int j) {
        return 0;
    }
}
    @Test
    void contextLoads() {
        //创建目标对象
        CalcImpl calcImpl = new CalcImpl();
        //(为目标对象)创建代理对象
        Calc calc = new MyStaticProxy(calcImpl);
        //错误的,代理对象与目标对象是兄弟关系,不能相互转换
//        CalcImpl calc = new MyStaticProxy(calcImpl);

        int rs = calc.add(1, 2);
        System.out.println("rs = " + rs);

    }

动态代理

package com.mytest.aopbefore;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class MyDynimacProxy {

    //目标对象
    private Object target;

    public MyDynimacProxy(Object target) {
        this.target = target;
    }

    public Object getProxy(){
        //代理对象
        Object proxy = null;
        //获取目标对象的类加载器
        ClassLoader classLoader = target.getClass().getClassLoader();
        //获取目标对象的接口
        Class<?>[] interfaces = target.getClass().getInterfaces();

        proxy = Proxy.newProxyInstance(classLoader, interfaces, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                //获取方法名称
                String methodName = method.getName();
                //执行之前,日志功能
                MyLogging.methodBefore(methodName,args);
                //执行目标对象的方法
                Object rs = method.invoke(target, args);
                //执行之后,日志功能
                MyLogging.methodAfter(methodName,rs);
                return rs;
            }
        });

        return proxy;
    }

}

    //测试动态代理
    @Test
    void testDynamicProxy() {
        //目标对象
        CalcImpl calcImpl = new CalcImpl();
        //创建工具类型
        MyDynimacProxy proxy = new MyDynimacProxy(calcImpl);
        //获取代理对象
        Calc calc = (Calc) proxy.getProxy();
        calc.add(5,6);

        //错误的,代理对象与目标对象是兄弟关系,不能相互转换
        //java.lang.ClassCastException: class jdk.proxy2.$Proxy98 cannot be cast to class com.mytest.aopbefore.CalcImpl
//        CalcImpl calc = (CalcImpl) proxy.getProxy();
//        calc.add(5,6);

    }

小结

Spring中支持两种代理实现方式

  1. JDK动态代理
    • 目标对象与代理对象均实现同一个接口
    • 目标对象与代理对象是兄弟关系,兄弟关系不能相互转换,如转换会报错:ClassCastException
  2. CGLIB动态代理
    • 代理对象"继承"目标对象
    • 代理对象与目标对象是"父子"关系,可以相互转换

Spring框架默认使用JDK动态代理,SpringBoot环境中默认代理模式CGLIB

基于注解实现AOP

AOP概念

AOP概念

  • AOP:Aspect Oriented Programming(面向切面编程设计)
    • 横向扩展机制
  • OOP:Object Oriented Programming(面向对象编程设计)
    • 纵向继承机制

AOP相关术语

  • 横切关注点:非核心业务代码(提取类之前)

    • 如:日志代码
  • 通知:将非核心业务代码提取到类中后,称之为通知(提取类之后)

  • 连接点:非核心业务代码织入到核心业务代码的位置(通知之前)

    • 日志功能在加减乘除方法中书写位置
  • 切入点:非核心业务代码织入到核心业务代码的位置(通知之后)

  • 切面类(Aspect):将非核心业务代码提取到类中,这个类称之为切面类

    • 将日志功能提取MyLoggin,MyLogging就是切面类
  • 目标对象:被代理的对象称之为目标对象

    • 如:CalcImpl是目标对象
  • 代理对象:通过代理类中getProxy()方法获取的对象,称之为代理对象

使用AspectJ基于注解实现AOP

导入依赖

 <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-aop</artifactId>
 </dependency>

定义切面类(通知)

package com.mytest.aop;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

import java.util.Arrays;

@Component                  //将组件装配到IOC容器
@Aspect                     //标识当前类是一个切面类
public class MyLogging {

    //前置通知:日志(方法前)通知
    @Before("execution(public int com.mytest.aop.CalcImpl.add(int,int))")
    public void methodBefore(JoinPoint joinPoint){
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        System.out.println("==>"+methodName+"()正在执行,参数:"+ Arrays.toString(args));
    }
}

开启AspectJ动态代理(SpringBoot环境中可省略)

package com.mytest.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@ComponentScan(basePackages = "com.mytest.aop")
//开启AspectJ动态代理
@EnableAspectJAutoProxy				
public class SpringConfigAop {
}

测试

package com.mytest;

import com.mytest.aop.Calc;
import com.mytest.aop.CalcImpl;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

//@SpringBootTest注解,自动整合Junit
@SpringBootTest
public class TestAOP {

    @Autowired
    private CalcImpl calc;
//    private Calc calc;

    @Test
    void testAOP(){
        System.out.println("calc.getClass().getName() = " + calc.getClass().getName());
        calc.add(1,2);
    }

}

切入点表达式详解

切入点表达式语法

语法:execution(public int com.mytest.aop.CalcImpl.add(int,int))

通配符

..可以代表任意参数类型及参数数量

*可以代表如下细节:

  • *可以代表任意访问修饰符及返回值类型
  • *可以代表任意包名及类名
  • *可以代表任意方法名

重用切入点表达式

  • 提取可重用切入点表达式

    //提取可重用,切入点表达式
    @Pointcut("execution(* com.mytest.aop.CalcImpl.*(..))")
    public void myPointCut(){}
    
  • 引用切入点表达式

    @Before("myPointCut()")
    public void methodBefore(JoinPoint joinPoint){}
    

JoinPoint接口

joinPoint.getSignature():获取方法签名

  • 方法签名=方法名称+形参列表

joinPoint.getSignature().getName():获取方法名称

joinPoint.getArgs():获取参数列表

AOP的五大通知(通知)

  • 前置通知:@Before

    • 执行时机:在目标方法执行之前执行

    • 注意:如目标方法有异常,前置通知会执行

      @Before("myJoinPoint()")
      public void methodBefore(JoinPoint joinPoint){}
      
  • 后置通知:@After

    • 执行时机:在目标方法执行之后执行(最后)

    • 注意:如目标方法有异常,后置通知执行

      @After("myJoinPoint()")
      public void methodAfter(JoinPoint joinPoint){}
      
  • 返回通知:@AfterReturning

    • 执行时机:在目标方法返回结果执行

    • 注意:与目标方法有无异常没有关系(一般有异常时不返回结果,所以返回通知不执行)

      @AfterReturning(value = "myJoinPoint()",returning = "rs")
      public void methodAfterRetuning(JoinPoint joinPoint,Object rs){
          String methodName = joinPoint.getSignature().getName();
          System.out.println("返回通知==>"+methodName+"()方法返回结果rs:"+rs);
      }
      
  • 异常通知:@AfterThrowing

    • 执行时机:在目标方法抛出异常时执行

    • 注意:有异常执行,无异常不执行,且throwing = "ex"与形参列表中的ex一致

      @AfterThrowing(value = "myJoinPoint()",throwing = "ex")
      public void methodAfterThrowing(JoinPoint joinPoint,Exception ex){
          String methodName = joinPoint.getSignature().getName();
          System.out.println("异常通知==>"+methodName+"()方法的异常ex:"+ex);
      }
      
  • 环绕通知:

    • 环绕通知:整合以上四个通知(前置&后置&返回&异常)

    • 注意

      • 环绕通知参数必须使用ProceedingJoinPoint,

        • 使用ProceedingJoinPoint的joinPoint.proceed();可以手动触发目标对象的相应方法
      • 环绕通知必须将目标方法的返回值返回,如不设置返回值,会报如下错:

      org.springframework.aop.AopInvocationException: Null return value from advice does not match primitive return type for: public abstract int com.mytest.aop.Calc.add(int,int)

    • 示例代码

      //环绕通知(四合一)
      @Around("myJoinPoint()")
      public Object methodAround(ProceedingJoinPoint pjp){
          String methodName = pjp.getSignature().getName();
          Object[] args = pjp.getArgs();
          Object rs = null;
          try {
              //前置通知
              System.out.println("前置通知==>"+methodName+"()正在执行,参数:"+ Arrays.toString(args));
              // 执行目标方法,如:add() sub() mul() div()
              rs = pjp.proceed();
              //返回通知
              System.out.println("返回通知==>"+methodName+"()方法返回结果rs:"+rs);
          } catch (Throwable e) {
              //异常通知
              System.out.println("异常通知==>"+methodName+"()方法的异常ex:"+e);
              throw new RuntimeException(e);
          } finally {
              //后置通知
              System.out.println("后置通知==>"+methodName+"()执行完毕!");
          }
          return rs;
      }
      
  • 小结

    • 通知执行顺序
      • 有异常:前置通知 -> 异常通知 -> 后置通知
      • 无异常:前置通知 -> 返回通知 -> 后置通知

定义切面优先级

  • 语法:@Order(index)

    • index数值越小优先级越高,但一般推荐正整数
  • 源码

    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
    @Documented
    public @interface Order {
        int value() default Integer.MAX_VALUE;
    }
    

AOP实现方式(动态代理实现方式)

  • JDK支持动态代理,底层目标对象实现接口方式**(代理对象目标对象关系:兄弟)**

    • springboot设置jdk动态代理方式:spring.aop.proxy-target-class=false
    • jdk.proxy2.$Proxy98
  • CGLIB(cglib)默认方式,底层目标对象不实现接口方式(认干爹:代理对象与目标对象关系:父子)

    • com.mytest.aop.CalcImplSpringCGLIB**SpringCGLIB**0