SpringAOP的使用

1 什么是AOP

AOP(Aspect Orient Programming),直译过来就是面向切面编程。AOP是一种编程思想,是面向对象(OOP)的一种补充和完善。对于面向对象来说是将程序抽象成各个层次的对象,而面向切面则是将程序抽象成各个切面。通俗一点来讲就是面向切面就是将面向对象的一些通用性的功能或非核心功能单独抽取出来。

2 为什么要使用AOP

在聊这个问题之前,我们先看下假如在项目中有如下需求:

  • 记录每个方法的入参及出参
  • 记录每个方法的耗时
  • 检查某些方法的入参是否合法

真实场景可能远不止这些,使用面向对象来处理的话,我们是否要在每个方法中都要加相同的逻辑才能达到这样的效果,随着项目业务增大,后期维护起来可以用灾难来形容。那么如果使用AOP来实现上面的需求,我们只需要在想监控的方法定义切面类即可,这样重复性的工作切不是核心业务就可以与我们的核心业务分离开来,后期维护起来只维护切面类即可。

3 AOP的前置知识

了解过AOP的实现,都知道底层使用的是动态代理,在Spring中,使用了两种动态代理方式:

  • JDK动态代理
  • CGLIB动态代理

3.1 JDK动态代理

动态代理是相对于静态代理而提出的设计模式,对于静态代理,一个代理类只能代理一个对象,如果有多个对象需要代理,那么就需要有多个代理类,造成代码的冗余,代码维护性也差,JDK动态代理,从字面意思可以看出,JDK动态代理的对象是动态生成的。那么要实现JDK动态代理必须有个前提条件,就是被代理的对象必须实现接口。既然说到静态代理,那么下面通过一个简单的例子看下静态代理的缺点。

实现静态代理步骤:

  1. 创建UserService接口
public interface UserService {
    String getUserName(String id);
}
  1. 创建UserServiceImpl类并实现UserService接口
public class UserServiceImpl implements UserService {
    @Override
    public String getUserName(String id) {
        System.out.println("入参用户ID:"+id);
        return "小鹏";
    }
}
  1. 创建UserServiceProxy代理类并实现UserService接口并将UserService的接口传入到代理类
public class UserServiceProxy implements UserService {
    private UserService userService;

    public UserServiceProxy(UserService userService){
        this.userService = userService;
    }

    @Override
    public String getUserName(String id) {
        System.out.println("静态代理开始------------");
        String result = this.userService.getUserName(id);
        System.out.println("静态代理结束------------获取被代理的结果:"+result);
        return result+"【代理】";
    }
}
  1. 测试案例
UserServiceProxy userServiceProxy = new UserServiceProxy(new UserServiceImpl());
System.out.println("被代理类处理后的结果:"+userServiceProxy.getUserName("1"));
---------------
测试结果:
静态代理开始------------
入参用户ID:1
静态代理结束------------获取被代理的结果:小鹏
被代理类处理后的结果:小鹏【代理】

从上面代码可以看出,如果有多个接口实现,想要代理,那么就需要写相对应的代理类,后期接口方法增加或修改,实现类修改那是无可厚非的,但是静态代理对应的代理类也要一起修改。使用JDK动态代理来改造上面的案例,实现步骤:

  1. 创建UserService接口
  2. 创建UserServiceImpl类并实现UserService接口
  3. 创建JdkAutoProxy
    第一、二步参考上面案例。使用JDK动态代理,必须实现InvocationHandler接口。
public class JdkAutoProxy implements InvocationHandler {
    private Object target;

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

    public Object getNewInstance() {
        return Proxy.newProxyInstance(this.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("JDK动态代理开始-------------");
        Object result = method.invoke(target, args);
        System.out.println("JDK动态代理结束-------------,入参:" + JSON.toJSONString(args));
        return result + "-JDK动态代理";
    }
}
或者这样写
public class JdkAutoProxy {
    private Object target;

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

    public Object getProxy() {
        return Proxy.newProxyInstance(this.getClass().getClassLoader(), target.getClass().getInterfaces(), (proxy, method, args) -> {
            System.out.println("JDK动态代理开始-------------");
            Object result = method.invoke(target, args);
            System.out.println("JDK动态代理结束-------------,入参:" + JSON.toJSONString(args));
            return result + "-JDK动态代理";
        });
    }
}

JDK动态代理的newProxyInstance有三个参数:

  • loader:用哪个类加载器去加载代理对象
  • interfaces:动态代理类需要实现的接口,从这里就看出JDK动态代理的类必须有实现接口
  • h:动态代理方法在执行是,会调用h里面的invoke方法去执行
  1. 测试案例
JdkAutoProxy jdkAutoProxy = new JdkAutoProxy(new UserServiceImpl());
UserService userService = (UserService)jdkAutoProxy.getProxy();
System.out.println(userService.getUserName("1"));
-----------
测试结果:
JDK动态代理开始-------------
入参用户ID:1
JDK动态代理结束-------------,入参:["1"]
小鹏-JDK动态代理

通过上面动态代理的案例可以得出结论,代理类只创建一个就行,其他需要被代理的类,传入代理类就行。

3.2 CGLIB动态代理

从上面的JDK动态代理的实现可以发现,JDK动态代理有一个缺点,就是被代理的类必须实现接口。在实际开发过程这显然是不满足需要,没有实现接口的类想被代理怎么办呢?接下来就是CGLIB发挥作用了。

由于CGLIB不是JAVA自带功能,需要引入第三方jar

<dependency>
  <groupId>cglib</groupId>
  <artifactId>cglib</artifactId>
  <version>3.2.11</version>
</dependency>

实现步骤如下:

  1. 创建UserBaseService
public class UserBaseService {
    public String getUserName(String id){
        return "小鹏";
    }
}
  1. 创建CGLIBProxy代理类,并实现MethodInterceptor接口
public class CGLIBProxy implements MethodInterceptor {
    private Object target;

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

    public Object getProxy() {
        Enhancer enhancer = new Enhancer();
        //设置父类
        enhancer.setSuperclass(target.getClass());
        //设置回调
        enhancer.setCallback(this);
        //创建对象
        return enhancer.create();
    }

    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("CGLIB动态代理开始--------------");
        System.out.println(String.format("调用类:%s,调用方法:%s,入参信息:%s", o.getClass().getName(), method.getName(), JSON.toJSONString(objects)));
        Object result = methodProxy.invoke(this.target, objects);
        System.out.println(String.format("调用结果:%s", result));
        return result + "【CGLIB】";
    }
}
  1. 测试
CGLIBProxy cglibProxy = new CGLIBProxy(new UserBaseService());
UserBaseService userBaseService = (UserBaseService) cglibProxy.getProxy();
System.out.println(String.format("被代理类处理后的结果:%s" ,userBaseService.getUserName("1")));
-------------
测试结果:
CGLIB动态代理开始--------------
调用类:com.tenghu.sa.service.UserBaseService$$EnhancerByCGLIB$$a0441b92,调用方法:getUserName,入参信息:["1"]
调用结果:小鹏
被代理类处理后的结果:小鹏【CGLIB】

JDK动态代理与CGLIB动态代理,通过案例可以看出区别了,可以试一下使用CGLIB动态代理将实现了接口的类传入看下会不会执行成功。接下来我们来看下SpringAOP怎么使用。

4 Spring AOP

4.1 相关概念

  • 横切关注点:一些具有横切多个不同软件模块的行为,通过传统的软件开发方法不能够有效地实现模块化一类特殊关注点。横切关注点可以对某些方法进行拦截,拦截后对原方法进行增强处理。
  • 切面(Aspect):切面就是对横切关注点的抽象,这个关注点可以横切多个对象,在代码中用一个类来表示。
  • 连接点(JoinPoint):连接点是在程序执行过程中某个特定的点,比如某个方法调用的时候或处理异常的时候,由于Spring只支持方法类型的连接点,所以在Spring AOP中的一个连接点表示一个方法的执行。
  • 切入点(Pointcut):切入点是匹配连接点的拦截规则,在满足这个切入点的连接点上运行通知。切入点的表达式如何与连接点相匹配是AOP的核心,Spring默认使用AspectJ切入点语法。
  • 通知(Advice):在切面上拦截到某个特定的连接点之后执行的操作
  • 目标对象(Target Object):目标对象,被一个或多个切面所通知的对象,及业务中需要进行增强的业务对象。
  • 织入(Weaving):织入是把切面作用到目标对象,然后产生一个代理对象的过程。
  • 引入(Introduction):引入是用来在运行时给一个类声明额外的方法或属性,即不需为实现一个接口,就能使用接口中的方法。

SpringAOP有以下通知类型:

  • 前置通知[Before advice]:在连接点前面执行,前置通知不会影响连接点的执行,除非此处抛出异常。
  • 正常返回通知[After returning advice]:在连接点正常执行完成后执行,如果连接点抛出异常,则不会执行。
  • 异常返回通知[After throwing advice]:在连接点抛出异常后执行。
  • 返回通知[After (finally) advice]:在连接点执行完成后执行,不管是正常执行完成,还是抛出异常,都会执行返回通知中的内容。
  • 环绕通知[Around advice]:环绕通知围绕在连接点前后,比如一个方法调用的前后。这是最强大的通知类型,能在方法调用前后自定义一些操作。环绕通知还需要负责决定是继续处理join point(调用ProceedingJoinPoint的proceed方法)还是中断执行。

Spring中使用AOP可以通过XML配置的方式,也可以通过注解的方式,下面通过简单的案例分别使用。下面的案例是Spring集成了AspectJ,因此需要引入AspectJjar

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-aspects</artifactId>
  <version>5.3.9</version>
</dependency>

Spring的这个包就包含了AspectJ,因此直接引入即可,不需要额外的引入,另外还需要引入Spring的相关包。

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-aop</artifactId>
  <version>5.3.9</version>
</dependency>

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-context</artifactId>
  <version>5.3.9</version>
</dependency>

4.2 基于XML配置方式

准备接口及实现类参考上面案例即可,创建一个LogProxy代理类

public class LogProxy {
    public void before(JoinPoint joinPoint){
        System.out.println("1、在连接点前面执行,前置通知不会影响连接点的执行,除非此处抛出异常。入参信息: "+ JSON.toJSONString(joinPoint.getArgs()));
    }

    public void after(JoinPoint joinPoint){
        System.out.println("4、在连接点执行完成后执行,不管是正常执行完成,还是抛出异常,都会执行返回通知中的内容。 。入参信息: "+ JSON.toJSONString(joinPoint.getArgs()));
    }

    public void afterReturn(JoinPoint joinPoint,String result){
        System.out.println("3、在连接点正常执行完成后执行,如果连接点抛出异常,则不会执行,入参信息:"+JSON.toJSONString(joinPoint.getArgs())+" 返回结果:"+result);
    }

    public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        System.out.println("2、环绕增强开始-----------入参:"+JSON.toJSONString(proceedingJoinPoint.getArgs()));
        Object result = proceedingJoinPoint.proceed();
        System.out.println("环绕增强结束-----------");
        return result+"环绕增强后";
    }

    public void afterThrow(JoinPoint joinPoint,Throwable throwable){
        System.out.println("在连接点抛出异常后执行,入参信息:"+JSON.toJSONString(joinPoint.getArgs())+":异常信息:"+throwable.getLocalizedMessage());
    }
}

准备Spring的配置文件

<beans xmlns="//www.springframework.org/schema/beans"
       xmlns:xsi="//www.w3.org/2001/XMLSchema-instance" xmlns:aop="//www.springframework.org/schema/aop"
       xmlns:context="//www.springframework.org/schema/context"
       xsi:schemaLocation="//www.springframework.org/schema/beans
       //www.springframework.org/schema/beans/spring-beans.xsd
       //www.springframework.org/schema/aop
       //www.springframework.org/schema/aop/spring-aop.xsd
       //www.springframework.org/schema/context
       //www.springframework.org/schema/context/spring-context.xsd">
    <context:component-scan base-package="com.tenghu.sa"/>
    <bean id="userService" class="com.tenghu.sa.service.impl.UserServiceImpl"/>
    <bean id="userBaseService" class="com.tenghu.sa.service.UserBaseService"/>
    <bean id="logProxy" class="com.tenghu.sa.proxy.LogProxy"/>
    <aop:config proxy-target-class="true">
        <aop:aspect id="logAspect" ref="logProxy">
            <aop:pointcut id="log" expression="execution(* com.tenghu.sa.service.*.*(..))"/>
            <aop:before method="before" pointcut-ref="log"/>
            <aop:after method="after" pointcut-ref="log"/>
            <aop:after-returning method="afterReturn" pointcut-ref="log" returning="result"/>
            <aop:after-throwing method="afterThrow" pointcut-ref="log" throwing="throwable"/>
            <aop:around method="around" pointcut-ref="log"/>
        </aop:aspect>
    </aop:config>
</beans>

配置文件中的bean配置,可以使用注解的方式,在UserServiceImplUserBaseService类使用@Service注解,在代理类LogProxy上使用@Component注解。
测试案例

ApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:spring-aop.xml");
UserService userService = applicationContext.getBean("userService",UserService.class);
System.out.println(userService.getUserName("12"));
-------------
测试结果:
1、在连接点前面执行,前置通知不会影响连接点的执行,除非此处抛出异常。入参信息: ["12"]
2、环绕增强开始-----------入参:["12"]
入参用户ID:12
环绕增强结束-----------
3、在连接点正常执行完成后执行,如果连接点抛出异常,则不会执行,入参信息:["12"] 返回结果:小鹏环绕增强后
4、在连接点执行完成后执行,不管是正常执行完成,还是抛出异常,都会执行返回通知中的内容。 。入参信息: ["12"]
小鹏环绕增强后

从结果上面可以看出,SpringAOP已经正常输出日志,日志信息包含了入参及出参信息,如果我们被代理的方法报异常,则会走到afterThrow方法。

4.3 基于注解的方式

对上面的代理类进行改造

@Aspect
@Component
public class LogProxy {
    @Pointcut(value = "execution(* com.tenghu.sa.service.*.*(..))")
    public void log() {

    }

    @Before(value = "log()")
    public void before(JoinPoint joinPoint) {
        System.out.println("1、在连接点前面执行,前置通知不会影响连接点的执行,除非此处抛出异常。入参信息: " + JSON.toJSONString(joinPoint.getArgs()));
    }

    @After(value = "log()")
    public void after(JoinPoint joinPoint) {
        System.out.println("4、在连接点执行完成后执行,不管是正常执行完成,还是抛出异常,都会执行返回通知中的内容。 。入参信息: " + JSON.toJSONString(joinPoint.getArgs()));
    }

    @AfterReturning(value = "log()", returning = "result")
    public void afterReturn(JoinPoint joinPoint, String result) {
        System.out.println("3、在连接点正常执行完成后执行,如果连接点抛出异常,则不会执行,入参信息:" + JSON.toJSONString(joinPoint.getArgs()) + " 返回结果:" + result);
    }

    @Around(value = "log()")
    public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        System.out.println("2、环绕增强开始-----------入参:" + JSON.toJSONString(proceedingJoinPoint.getArgs()));
        Object result = proceedingJoinPoint.proceed();
        System.out.println("环绕增强结束-----------");
        return result + "环绕增强后";
    }

    @AfterThrowing(value = "log()", throwing = "throwable")
    public void afterThrow(JoinPoint joinPoint, Throwable throwable) {
        System.out.println("在连接点抛出异常后执行,入参信息:" + JSON.toJSONString(joinPoint.getArgs()) + ":异常信息:" + throwable.getLocalizedMessage());
    }
}

XML的配置就简单了,配置将AOP配置修改为<aop:aspectj-autoproxy/>

<beans xmlns="//www.springframework.org/schema/beans"
       xmlns:xsi="//www.w3.org/2001/XMLSchema-instance" xmlns:aop="//www.springframework.org/schema/aop"
       xmlns:context="//www.springframework.org/schema/context"
       xsi:schemaLocation="//www.springframework.org/schema/beans
       //www.springframework.org/schema/beans/spring-beans.xsd
       //www.springframework.org/schema/aop
       //www.springframework.org/schema/aop/spring-aop.xsd
       //www.springframework.org/schema/context
       //www.springframework.org/schema/context/spring-context.xsd">
    <context:component-scan base-package="com.tenghu.sa"/>
    <aop:aspectj-autoproxy/>
</beans>

不出异常的情况下,运行结果与XML的方式一致,这里就不单独贴出来了。从上面案例中使用到了切入点的注解@PointcutXML中配置的aop:pointcut,里面使用到了execution表达式,该表达式的作用就是Spring启动时会根据表达式配置的规则扫描那些类下面的方法符合规则,将其对应的类存入到Spring的代理工厂中。execution表达式各个部分含义说明:

execution(<修饰符模式>?<返回类型模式><方法名模式>(<参数模式>)<异常模式>?)

其中,除了返回类型模式、方法名模式和参数模式外,其他项都是可选的。以上面案例中的表达式为例execution(* com.tenghu.sa.service.*.*(..)),其中含义是匹配com.tenghu.sa.service这个package下的任意类的任意方法名、任意方法入参和任意方法返回值的这部分方法。除了execution表达式,还支持其他的表达式。

4.4 切入点表达式

Spring AOP 支持以下 AspectJ 切入点指示符 (PCD),用于切入点表达式:

  • execution:用于匹配方法执行连接点。这是使用 Spring AOP 时要使用的主要切入点指示符。
  • within:限制匹配以连接某些类型中的点(使用Spring AOP时在匹配类型中声明的方法的执行)。
  • this:限制匹配到连接点(使用Spring AOP时方法的执行),其中Bean引用(Spring AOP代理)是给定类型的实例。
  • target:限制匹配到连接点(使用Spring AOP时方法的执行),其中目标对象(正在代理的应用程序对象)是给定类型的实例。
  • args:限制匹配到连接点(使用Spring AOP时方法的执行),其中参数是给定类型的实例。
  • @target:限制匹配到连接点(使用Spring AOP时方法的执行),其中执行对象的类具有给定类型的注释。
  • @args:限制匹配到连接点(使用Spring AOP时方法的执行),其中传递的实际参数的运行时类型具有给定类型的注释。
  • @within:限制匹配以连接具有给定注释的类型中的点(使用Spring AOP时在具有给定注释的类型中声明的方法的执行)。
  • @annotation:限制匹配到连接点的主题(在Spring AOP中运行的方法)具有给定注释的连接点。

下面通过简单的例子来说明表达式的用法:

  • execution:使用execution(方法表达式)匹配方法执行。
表达式 描述
execution(public * *(..)) 匹配任意类的公用方法
execution(* set*(..)) 匹配以set开头的任意方法
execution(* com.tenghu.sa.service.UserService.*(..)) 匹配UserService下的任意方法
  • within:使用within(类型表达式)用于匹配指定的类的任何方法。

注意:within只能指定类,然后该类内的所有方法都将被匹配

表达式 描述
within(com.tenghu..*) cn.javass包及子包下的任何类的任何方法执行
within(com.tenghu..UserService+) cn.javass包或所有子包下IPointcutService类型及子类型的任何方法
within(@com.tenghu..Secure *) 持有cn.javass..Secure注解的任何类型的任何方法,必须是在目标对象上声明这个注解,在接口上声明的对它不起作用
  • this:使用this(type)
表达式 描述
this(com.tenghu.sa.service.UserService) 当前AOP对象实现了 IPointcutService接口的任何方法,也可能是引入接口
  • target:使用target(type)

type指的是一个类或者接口的完整包路径
功能:匹配type类型的目标对象的所有方法。即目标对象可以向上转型为type类型就算是匹配成功

表达式 描述
target(com.tenghu.sa.service.UserService) 当前目标对象(非AOP代理对象)实现了 IPointcutService接口的任何方法
  • args:使用args(参数类型列表)匹配当前执行的方法传入时的参数类型为指定类型的执行方法。

注意:是匹配传入的参数类型,不是匹配方法签名的参数类型;参数类型列表中的参数必须是类型全限定名,通配符不支持;args属于动态切入点,这种切入点开销非常大,非特殊情况最好不要使用;

表达式 描述
args(java.lang.String,..) 任何一个以接受传入参数类型为java.lang.String 开头,且其后可跟任意个任意类型的参数的方法执行,args指定的参数类型是在运行时动态匹配的
  • @target:使用@target(注解类型)匹配持有指定注解的类型的目标对象。

注意:注解类型也必须是全限定类型名

表达式 描述
@target(com.tenghu.sa.annotation.Secure) 任何目标对象持有Secure注解的类方法;必须是在目标对象上声明这个注解,在接口上声明的对它不起作用
  • @args:使用@args(注解列表)匹配运行时传入的参数的类型持有指定注解的方法,并且args括号内可以指定多个arg;

注意:注解类型也必须是全限定类型名;

表达式 描述
@args(com.tenghu.sa.annotation.Secure) 任何一个只接受一个参数的方法,且方法运行时传入的参数类型持有com.tenghu.sa.annotation.Secure注解
  • @within:使用@within(注解类型)匹配所以持有指定注解类型内的方法。

注意:注解类型也必须是全限定类型名;

表达式 描述
@within(com.tenghu.sa.annotation.Secure) 任何目标对象对应的类型持有Secure注解的类方法;必须是在目标对象上声明这个注解,在接口上声明的对它不起作用
  • @annotation:使用@annotation(注解类型)匹配持有指定注解的方法;

注意:注解类型也必须是全限定类型名;

表达式 描述
@annotation(com.tenghu.sa.annotation.Secure) 方法上持有注解 com.tenghu.sa.annotation.Secure将被匹配
Tags: