Spring AOP快速上手

发布时间 2023-12-31 11:32:55作者: 残城碎梦

什么是AOP

AOP全称是aspect-oriented programing 面向切面编程。用于解决横向关注点的问题,横向关注点是指多个模块或者模块中的多个功能需要共享的功能,如日志记录、事务管理、安全控制等等。即重复性的代码抽象出来,形成可复用的代码模块。

AOP的核心术语

Joinpoint(连接点):程序执行的某个特定位置,比如(类开始初始化前,初始化后,方法前,方法后,异常后) 一个类或者一段程序代码拥有一些具有边界的特定点,这些特定点称为连接点。

Pointcut(切入点):每个程序都拥有多个连接点,比如一个类有2个方法,这2个方法就是连接点,连接点就是程序中具体的事物,AOP 通过切点来定位特定的连接点,连接点相当于数据库中的记录,而切点相当于是查询记录的条件。在程序中,切点是连接点位置的集合。

Advice(通知/增强):就是我们要具体做的事情,也就在原有的方法之上添加新的能力。

Target(目标对象):就是我们要增强的类。

Introduction(引入):特殊的增强,为类添加属性和方法。哪怕这个类没有任何的属性和方法我们也可以通过aop去添加方法和属性实现逻辑。

Weaving(织入):就是把增强添加到目标类具体的连接点上的过程。

Proxy(代理):一个类被AOP织入增强后产生的结果类,它是原类和增强后的代理类,根据代理不同的方式,代理类可能是和原类具有相同接口的类,也可能是原类的子类。

Aspect(切面):切面由切点和增强组成,他既包含横切的定义,也包括了连接点的定义,spring aop就是负责实施切面的框架,他将切面定义为横切逻辑织入到切面所指定的连接点。

Spring AOP的动态代理模式

当目标类有接口的情况使用JDK动态代理和cglib动态代理,没有接口时只能使用cglib动态代理。

Spring AOP是基于动态代理实现的。对于动态代理,首先得有一个代理类,然后再在目标类上实现代理。根据目标类是否实现了接口来完成动态代理。而在Spring AOP中,根据目标类是否实现了接口分为以下两种情况:

JDK动态代理

JDK动态代理动态生成的代理类会在com.sun.proxy包下,类名为$proxy1,和目标类实现相同的接口。

如果目标类实现了接口,我们可以使用JDK代理实现动态代理(基于接口实现动态代理)。这时候要求代理类需要实现InvocationHander接口:

InvocationHandler(真正地给目标类的目标方法进行增强)
    method.invoke(目标类,目标方法参数)
    Proxy.newProxyInstance(); 
//定义接口,指明需要代理的方法
public interface Subject {
    void doLogin();
}

//让A实现接口Subject
public class A implements Subject{    
    @Override    
    public void doLogin() {        
        System.out.println("doLogin>>>>>>>>>>>>>>>");    
    }

    public void doLogout(){        
        System.out.println("doLogout>>>>>>>>>>>>>");    

    }
}

接下来我们需要实现InvocationHandler接口,它是代理逻辑执行器,不是代理类,它的作用是实现代理类的逻辑:

有一些刚刚了解动态代理的小伙伴会把这个实现类误当做是代理类,InvocationHandler的实现类就是用来实现代理逻辑的,通过重写invoke()方法,对被代理的方法进行增强(比如:上面代码中,我们可以在执行代理方法前后加入其他业务逻辑)。

获取代理对象

通过java.lang.reflect.Proxy的静态方法newProxyInstance(...)获取我们想要的代理对象

newProxyInstance()需要的参数如下:

  • ClassLoader loader:类加载器;
  • Class<?>[] interfaces:代理类需要实现的接口集合(就是被代理类实现的接口);
  • InvocationHandler h:代理类会根据传进来的接口去实现需要实现的接口方法,但是接口方法的内部逻辑需要依赖InvocationHandler(就是上面实现InvocationHandler接口的类的invoke()中的内容);

CGLIB动态代理

cglib动态代理动态生成的代理类默认会和目标在在相同的包下,会继承目标类。

如果目标类没有实现接口,我们可以使用CGLIB实现动态代理(基于继承父类而实现代理对象)。当然,就算目标类实现了接口,我们同样可以使用CGLIB来实现动态代理,其本质是使用继承来实现增强的。要使用CGLIB来实现动态代理,要求代理类需要实现MethodInteceptor接口,代理类产生的对象可以通过Enhancer来产生目标对象的代理对象:

MethodInteceptor 
    method.invoke();   //采用反射机制来完成方法调用
    Enhancer enhancer = new Enhancer();
    enhancer.setSupperClass();
    enhancer.setCallBack();
    enhancer.create();

创建需要被代理的类

public class Student2 {
    
    public String getName() {
        System.out.println("我叫红领巾");
        return "我叫红领巾";
    }

    public Integer getAge() {
        System.out.println("14");
        return 14;
    }
}

生成代理类 需实现MethodInterceptor接口

public class MyProxy implements MethodInterceptor {

    /**
   	 * Enhancer.create(superClass,callback)
     * superClass: 生成代理对象的父类
     * callback:设置enhancer的回调对象
     **/
    public <T> T getProxy(Class<T> clazz){
        return (T) Enhancer.create(clazz,this);
    }

    /**
     * target:cglib生成的代理对象
     * method:被代理对象方法
     * args:方法入参
     * methodProxy: 代理方法
     **/
    @Override
    public Object intercept(Object target, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        System.out.println("前置");
        //用MethodProxy 调用父类方法。
        Object returnObject =  methodProxy.invokeSuper(target,args);
        System.out.println("后置");
        return returnObject;
    }
}

获取代理对象

public class ProxyTest {
    public static void main(String[] args) {
        // 代理类class文件存入本地磁盘方便我们反编译查看源码
        System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "/Users/admin");
        MyProxy myProxy = new MyProxy();
        Student2 student2 =  myProxy.getProxy(Student2.class);
        student2.getName();
    }
}

Spring AOP开发步骤

首先我们需要引入aop的依赖:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>5.0.2.RELEASE</version>
</dependency>
<!-- 提供了与aspects集成的功能 -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>5.0.2.RELEASE</version>
</dependency>

基于xml的AOP配置

在Spring的配置文件中配置:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">

    <!--开启组件扫描-->
    <context:component-scan base-package="com.mcode.xmlaop"/>

    <!--配置aop五种通知类型-->
    <aop:config>
        <!--配置切面类-->
        <aop:aspect ref="logAspect">
            <!--配置切入点-->
            <aop:pointcut id="pointcut"
                          expression="execution(* com.mcode.xmlaop.CalculatorImpl.*(..))"/>
            <!--配置五种通知类型-->
            <!--前置通知-->
            <aop:before method="beforeMethod"
                        pointcut="execution(public int com.mcode.xmlaop.CalculatorImpl.*(..))"/>
            <!--后置通知-->
            <aop:after method="afterMethod" pointcut-ref="pointcut"/>
            <!--返回通知-->
            <aop:after-returning method="afterReturningMethod" pointcut-ref="pointcut" returning="result"/>
            <!--异常通知-->
            <aop:after-throwing method="afterThrowingMethod" pointcut-ref="pointcut" throwing="ex"/>
            <!--环绕通知-->
            <aop:around method="aroundMethod" pointcut-ref="pointcut"/>
        </aop:aspect>
    </aop:config>
</beans>

创建切面类并配置:

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

import java.util.Arrays;

/**
 * Description: 切面类
 */
@Component //加入ioc容器
public class LogAspect {

    // 前置
    public void beforeMethod(JoinPoint joinPoint) {
        //获取连接点的签名信息
        String methodName = joinPoint.getSignature().getName();
        //获取目标方法到的实参信息
        Object[] args = joinPoint.getArgs();
        System.out.println("Logger-->前置通知,方法名称:" + methodName + ",参数:" + Arrays.toString(args));
    }

    // 后置
    public void afterMethod(JoinPoint joinPoint) {
        //获取连接点的签名信息
        String methodName = joinPoint.getSignature().getName();
        //获取目标方法到的实参信息
        Object[] args = joinPoint.getArgs();
        System.out.println("Logger-->后置通知,方法名称:" + methodName + ",参数:" + Arrays.toString(args));
    }

    // 返回
    public void afterReturningMethod(JoinPoint joinPoint, Object result) {
        String methodName = joinPoint.getSignature().getName();
        System.out.println("Logger-->返回通知,方法名称:" + methodName + ",返回结果:" + result);
    }

    // 异常
    public void afterThrowingMethod(JoinPoint joinPoint, Throwable ex) {
        String methodName = joinPoint.getSignature().getName();
        System.out.println("Logger-->异常通知,方法名称:" + methodName + ",异常信息:" + ex);
    }

    // 环绕
    public Object aroundMethod(ProceedingJoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        String argString = Arrays.toString(args);
        Object result = null;
        try {
            System.out.println("环绕通知-->目标对象方法执行之前");
            //目标方法的执行,目标方法的返回值一定要返回给外界调用者,否则会报错
            result = joinPoint.proceed();
            System.out.println("环绕通知-->目标对象方法返回值之后");
        } catch (Throwable e) {
            e.printStackTrace();
            System.out.println("环绕通知-->目标对象方法出现异常时");
        } finally {
            System.out.println("环绕通知-->目标对象方法执行完毕");
        }
        return result;
    }
}

基于注解的AOP配置

1)准备被代理的目标资源

接口:

public interface Calculator {
    int add(int i, int j);

    int sub(int i, int j);

    int mul(int i, int j);

    int div(int i, int j);
}

接口实现类:

import org.springframework.stereotype.Component;

@Component
public class CalculatorImpl implements Calculator {
    @Override
    public int add(int i, int j) {
        int result = i + j;
        System.out.println("方法内部 result = " + result);
        //为了测试,模拟异常出现
        //int a = 1/0;
        return result;
    }

    @Override
    public int sub(int i, int j) {
        int result = i - j;
        System.out.println("方法内部 result = " + result);
        return result;
    }

    @Override
    public int mul(int i, int j) {
        int result = i * j;
        System.out.println("方法内部 result = " + result);
        return result;
    }

    @Override
    public int div(int i, int j) {
        int result = i / j;
        System.out.println("方法内部 result = " + result);
        return result;
    }
}

2)创建切面类并配置

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import java.util.Arrays;

@Aspect //切面类
@Component //ioc容器
public class LogAspect {

    //设置切入点和通知类型
    //切入点表达式: execution(访问修饰符 增强方法返回类型 增强方法所在类全路径.方法名称(方法参数))
    //通知类型:
    // 前置 @Before(value="切入点表达式配置切入点")
    //@Before(value = "execution(* com.mcode.annotationaop.CalculatorImpl.*(..))")
    @Before("execution(public int  com.mcode.annotationaop.CalculatorImpl.*(..))")
    public void beforeMethod(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        System.out.println("Logger-->前置通知,方法名称:" + methodName + ",参数:" + Arrays.toString(args));
    }

    // 后置 @After()
    @After("execution(* com.mcode.annotationaop.CalculatorImpl.*(..))")
    public void afterMethod(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        System.out.println("Logger-->后置通知,方法名称:" + methodName + ",参数:" + Arrays.toString(args));
    }

    // 返回 @AfterReturning
    @AfterReturning(value = "execution(* com.mcode.annotationaop.CalculatorImpl.*(..))",
                    returning = "result")
    public void afterReturningMethod(JoinPoint joinPoint, Object result) {
        String methodName = joinPoint.getSignature().getName();
        System.out.println("Logger-->返回通知,方法名称:" + methodName + ",返回结果:" + result);
    }

    // 异常 @AfterThrowing 获取到目标方法异常信息
    //目标方法出现异常,这个通知执行
    @AfterThrowing(value = "execution(* com.mcode.annotationaop.CalculatorImpl.*(..))", throwing =
                   "ex")
    public void afterThrowingMethod(JoinPoint joinPoint, Throwable ex) {
        String methodName = joinPoint.getSignature().getName();
        System.out.println("Logger-->异常通知,方法名称:" + methodName + ",异常信息:" + ex);
    }

    // 环绕 @Around()
    @Around("execution(* com.mcode.annotationaop.CalculatorImpl.*(..))")
    public Object aroundMethod(ProceedingJoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        String argString = Arrays.toString(args);
        Object result = null;
        try {
            System.out.println("环绕通知-->目标对象方法执行之前");
            //调用目标方法
            result = joinPoint.proceed();
            System.out.println("环绕通知-->目标对象方法返回值之后");
        } catch (Throwable e) {
            e.printStackTrace();
            System.out.println("环绕通知-->目标对象方法出现异常时");
        } finally {
            System.out.println("环绕通知-->目标对象方法执行完毕");
        }
        return result;
    }
}

3)添加Spring配置类

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

@Configuration
@ComponentScan("com.mcode.annotationaop")
@EnableAspectJAutoProxy //开启AspectJ的自动代理,为目标对象自动生成代理
public class SpringConfig {
}

@EnableAspectAutoJAutoProxy属性:

  • proxyTargetClass:表示动态代理实现方式,如果值设置true,表示需要代理类都基于CGLIB来实现;默认情况下值是设置成false,表示如果原类如果定义了接口则通过JDK.Proxy实现否则基于CGLIB来实现。
  • exposeProxy:exposeProxy=true Spring会把当前的代理对象存放在ThreadLocal中,可以通过AopContext.currentProxy()获取。

各种通知

  • 前置通知:使用@Before注解标识,在被代理的目标方法执行
  • 返回通知:使用@AfterReturning注解标识,在被代理的目标方法成功结束后执行(寿终正寝)
  • 异常通知:使用@AfterThrowing注解标识,在被代理的目标方法异常结束后执行(死于非命)
  • 后置通知:使用@After注解标识,在被代理的目标方法最终结束后执行(盖棺定论)
  • 环绕通知:使用@Around注解标识,使用try…catch…finally结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置

各种通知的执行顺序:

  • Spring版本5.3.x以前:
    •  前置通知
    • 目标操作
    • 后置通知
    • 返回通知或异常通知
  • Spring版本5.3.x以后:
    • 前置通知
    • 目标操作
    • 返回通知或异常通知
    • 后置通知

切入点表达式

1)作用

 

2)语法细节

  • 用*号代替“权限修饰符”和“返回值”部分表示“权限修饰符”和“返回值”不限。
  • 在包名的部分,一个“*”号只能代表包的层次结构中的一层,表示这一层是任意的。
    • 例如:*.Hello匹配com.Hello,不匹配com.harvey.Hello
  • 在包名的部分,使用“*…”表示包名任意、包的层次深度任意
  • 在类名的部分,类名部分整体用*号代替,表示类名任意
  • 在类名的部分,可以使用*号代替类名的一部分
    • 例如:*Service匹配所有名称以Service结尾的类或接口
  • 在方法名部分,可以使用*号表示方法名任意
  • 在方法名部分,可以使用*号代替方法名的一部分
    • 例如:*Operation匹配所有方法名以Operation结尾的方法
  • 在方法参数列表部分,使用(…)表示参数列表任意
  • 在方法参数列表部分,使用(int,…)表示参数列表以一个int类型的参数开头
  • 在方法参数列表部分,基本数据类型和对应的包装类型是不一样的
    • 切入点表达式中使用 int 和实际方法中 Integer 是不匹配的
  • 在方法返回值部分,如果想要明确指定一个返回值类型,那么必须同时写明权限修饰符
    • 例如:execution(public int …Service.(…, int)) 正确
    • 例如:execution( int *…Service.(…, int)) 错误

重用切入点表达式

1)声明

@Pointcut("execution(* com.mcode.annotationaop.CalculatorImpl.*(..))")
public void pointCut(){}

2)在同一个切面中使用

@Before("pointCut()")
public void beforeMethod(JoinPoint joinPoint){
    String methodName = joinPoint.getSignature().getName();
    String args = Arrays.toString(joinPoint.getArgs());
    System.out.println("Logger-->前置通知,方法名:"+methodName+",参数:"+args);
}

3)在不同切面中使用

@Before("com.mcode.annotationaop.LogAspect.pointCut()")
public void beforeMethod(JoinPoint joinPoint){
    String methodName = joinPoint.getSignature().getName();
    String args = Arrays.toString(joinPoint.getArgs());
    System.out.println("Logger-->前置通知,方法名:"+methodName+",参数:"+args);
}

获取通知的相关信息

1)获取连接点信息

获取连接点信息可以在通知方法的参数位置设置JoinPoint类型的形参。

@Before("execution(public int  com.mcode.annotationaop.CalculatorImpl.*(..))")
public void beforeMethod(JoinPoint joinPoint) {
    //获取连接点的签名信息
    String methodName = joinPoint.getSignature().getName();
    //获取目标方法到的实参信息
    Object[] args = joinPoint.getArgs();
    System.out.println("Logger-->前置通知,方法名称:" + methodName + ",参数:" + Arrays.toString(args));
}

2)获取目标方法的返回值

@AfterReturning中的属性returning,用来将通知方法的某个形参,接收目标方法的返回值

// 返回 @AfterReturning
@AfterReturning(value = "execution(* com.mcode.annotationaop.CalculatorImpl.*(..))",returning = "result")
public void afterReturningMethod(JoinPoint joinPoint, Object result) {
    String methodName = joinPoint.getSignature().getName();
    System.out.println("Logger-->返回通知,方法名称:" + methodName + ",返回结果:" + result);
}

3)获取目标方法的异常

@AfterThrowing中的属性throwing,用来将通知方法的某个形参,接收目标方法的异常

@AfterThrowing(value = "execution(* com.mcode.annotationaop.CalculatorImpl.*(..))", throwing ="ex")
public void afterThrowingMethod(JoinPoint joinPoint, Throwable ex) {
    String methodName = joinPoint.getSignature().getName();
    System.out.println("Logger-->异常通知,方法名称:" + methodName + ",异常信息:" + ex);
}

环绕通知

@Around("execution(* com.mcode.annotationaop.CalculatorImpl.*(..))")
public Object aroundMethod(ProceedingJoinPoint joinPoint) {
    String methodName = joinPoint.getSignature().getName();
    Object[] args = joinPoint.getArgs();
    String argString = Arrays.toString(args);
    Object result = null;
    try {
        System.out.println("环绕通知-->目标对象方法执行之前");
        //目标方法的执行,目标方法的返回值一定要返回给外界调用者,否则会报错
        result = joinPoint.proceed();
        System.out.println("环绕通知-->目标对象方法返回值之后");
    } catch (Throwable e) {
        e.printStackTrace();
        System.out.println("环绕通知-->目标对象方法出现异常时");
    } finally {
        System.out.println("环绕通知-->目标对象方法执行完毕");
    }
    return result;
}

切面(Aspect)的优先级

相同目标方法上同时存在多个切面时,切面的优先级控制切面的内外嵌套顺序。

  • 优先级高的切面:外面
  • 优先级低的切面:里面

使用@Order注解可以控制切面的优先级:

  • @Order(较小的数):优先级高
  • @Order(较大的数):优先级低
@Order(1) //优先级
public class LogAspect {
}