Spring Aop 详解一

Aop 是一个编程思想,最初是一个理论,最后落地成了很多的技术实现。

我们写一个系统,都希望尽量少写点儿重复的东西。而很多时候呢,又不得不写一些重复的东西。比如访问某些方法的权限执行某些方法性能的日志数据库操作的方法进行事务控制。以上提到的,权限的控制,事务控制,性能监控的日志 可以叫一个切面。像一个横切面穿过这一些列需要控制的方法。通过aop编程,实现了对切面业务的统一处理。

以上是我对aop的一个总体概括


aop的原始实现

通过动态代理和反射实现,又称之为JDK动态代理

  • MyInterceptor.java
package demo.aop.jdkproxy;

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

/**
 * 拦截器
 *    1、目标类导入进来
 *    2、事务导入进来
 *    3、invoke完成
 *        1、开启事务
 *        2、调用目标对象的方法
 *        3、事务的提交
 * @author zd
 *
 */
public class MyInterceptor implements InvocationHandler{
    private Object target;//目标类
    private Transaction transaction;


    public MyInterceptor(Object target, Transaction transaction) {
        super();
        this.target = target;
        this.transaction = transaction;
    }


    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
        String methodName = method.getName();
        if("savePerson".equals(methodName)||"updatePerson".equals(methodName)
                ||"deletePerson".equals(methodName)){
            this.transaction.beginTransaction();//开启事务
            method.invoke(target);//调用目标方法
            this.transaction.commit();//事务的提交
        }else{
            method.invoke(target);
        }
        return null;
    }
}

  • PersonDao.java
package demo.aop.jdkproxy;

public interface PersonDao {
    public void savePerson();
    public void updatePerson();
}

  • PersonDaoImpl.java
package demo.aop.jdkproxy;

public class PersonDaoImpl implements PersonDao{
    public void savePerson() {
        System.out.println("save person");
    }

    public void updatePerson() {
        // TODO Auto-generated method stub
        System.out.println("update person");
    }
}

  • Transaction.java
package demo.aop.jdkproxy;

public class Transaction {
    public void beginTransaction(){
        System.out.println("begin transaction");
    }
    public void commit(){
        System.out.println("commit");
    }
}

  • JDKProxyTest
package demo.aop.jdkproxy;

import org.junit.Test;

import java.lang.reflect.Proxy;

/**
 * 1、拦截器的invoke方法是在时候执行的?
 *     当在客户端,代理对象调用方法的时候,进入到了拦截器的invoke方法
 * 2、代理对象的方法体的内容是什么?
 *     拦截器的invoke方法的内容就是代理对象的方法的内容
 * 3、拦截器中的invoke方法中的参数method是谁在什么时候传递过来的?
 *     代理对象调用方法的时候,进入了拦截器中的invoke方法,所以invoke方法中的参数method就是
 *       代理对象调用的方法
 * @author zd
 *
 */
public class JDKProxyTest {
    @Test
    public void testJDKProxy(){
        /**
         * 1、创建一个目标对象
         * 2、创建一个事务
         * 3、创建一个拦截器
         * 4、动态产生一个代理对象
         */
        Object target = new PersonDaoImpl();
        Transaction transaction = new Transaction();
        MyInterceptor interceptor = new MyInterceptor(target, transaction);
        /**
         * 1、目标类的类加载器
         * 2、目标类实现的所有的接口
         * 3、拦截器
         */
        PersonDao personDao = (PersonDao) Proxy.newProxyInstance(target.getClass().getClassLoader(),
                target.getClass().getInterfaces(), interceptor);
        //personDao.savePerson();
        personDao.updatePerson();
    }
}

运行结果

begin transaction
update person
commit

原始实现部分,想必现在很少会有人再这么写了。但这个对于我们理解Aop的思想很有帮助。

  • 我们可以看到 代理对象 personDao调用的方法updatePerson中没有模拟事务的代码,但最终代理对象却输出了begin transactioncommit

Spring AOP概念核心词

  • 切面(Aspect):一个关注点的模块化,这个关注点可能会横切多个对象。事务管理是J2EE应用中一个关于横切关注点的很好的例子。在Spring AOP中,切面可以使用基于模式)或者基于@Aspect注解的方式来实现。

  • 连接点(Joinpoint):在程序执行过程中某个特定的点,比如某方法调用的时候或者处理异常的时候。在Spring AOP中,一个连接点总是表示一个方法的执行。

  • 通知(Advice):在切面的某个特定的连接点上执行的动作。其中包括了“around”、“before”和“after”等不同类型的通知(通知的类型将在后面部分进行讨论)。许多AOP框架(包括Spring)都是以拦截器做通知模型,并维护一个以连接点为中心的拦截器链。

  • 切入点(Pointcut):匹配连接点的断言。通知和一个切入点表达式关联,并在满足这个切入点的连接点上运行(例如,当执行某个特定名称的方法时)。切入点表达式如何和连接点匹配是AOP的核心:Spring缺省使用AspectJ切入点语法。

  • 引入(Introduction):用来给一个类型声明额外的方法或属性(也被称为连接类型声明(inter-type declaration))。Spring允许引入新的接口(以及一个对应的实现)到任何被代理的对象。例如,你可以使用引入来使一个bean实现IsModified接口,以便简化缓存机制。

  • 目标对象(Target Object): 被一个或者多个切面所通知的对象。也被称做被通知(advised)对象。 既然Spring AOP是通过运行时代理实现的,这个对象永远是一个被代理(proxied)对象。

  • AOP代理(AOP Proxy):AOP框架创建的对象,用来实现切面契约(例如通知方法执行等等)。在Spring中,AOP代理可以是JDK动态代理或者CGLIB代理。

  • 织入(Weaving):把切面连接到其它的应用程序类型或者对象上,并创建一个被通知的对象。这些可以在编译时(例如使用AspectJ编译器),类加载时和运行时完成。Spring和其他纯Java AOP框架一样,在运行时完成织入。


上面为官方文档,有的地方还是很难读懂,毕竟是纯概念。下面我用自己的话来翻译一下,如果有不对的地方,请指正

  • 切面 统一处理的业务,比如上文提到的 权限控制,事务处理
  • 连接点 原本被执行的方法,一个执行的方法可能被多个切面横切
  • 通知 切面方法的执行,比如权限控制的具体执行过程(权限控制可以用前置通知@Before)
  • 切入点 切入点的概念通常和连接点概念容易分不清,切入点其实是一个规则,也就是说什么样的情况下(满足什么规则),
    就会去执行链接点的那些方法,这个规则就是切入点,这种规则用切入点表达式去制定
  • 引入(Introduction) 被代理的对象可以引入新接口,通过默认的实现类,让这个被代理的类增强
  • 目标对象 就是被切面执行了的对象
  • AOP代理 代理包括jdk代理和cglib代理,是aop底层实现过程
  • 织入 就是切面中的方法完成加载执行的过程

这里有8个概念,但真正要完成aop的理解,还不得不再引入两个概念。

  • 被代理对象 我们可以看到,上面说到目标对象永远是一个被代理的对象,也是被通知的对象。
  • 代理对象 代理对象呢, 就是最后通知后,生成的对象。

切入点表达式

  • execution
    用于匹配指定类型内的方法执行,匹配的是方法,可以确切到方法
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern (param-pattern)
          throws-pattern?)
          
modifiers-pattern 修饰符表达式   :public protect private ,可以不写,表示不限制
ret-type-pattern    返回值表达式   如 String代表返回值为String ,*代表任意返回值都可以   必填字段
declaring-type-pattern  类型,可以由完整包名加类名组成   可以只写包名加.*限定包下的所有类   可以不写,表示不限制
name-pattern    方法名表达式,可以由*统配所有字符   必填字段
param-pattern   参数列表,可以用..来表示所有的方法  必填字段
execution(public * * (..))   //所有public的方法
execution(* set*(..))  //所有set开头的方法
execution( * com.xyz.service.AccountService.* (..) ) //AccountService的所有方法,如果AccountService是接口,指实现了这个接口的所有方法
execution(* com.xyz.service.*.*(..)) //com.xyz.service包下的所有类的所有方法
execution(* com.xyz.service..*.*(..)) //com.xyz.service包及子包下的所有类的所有方法
  • within
    用于匹配指定类型内的方法执行,匹配的是类型内的方法,类型下的所有方法
within (com.xyz.service.*) // com.xyz.service包下面的所有类的所有方法
within (com.xyz.service..*) // com.xyz.service包或子包下面的所有类的所有方法
within (com.xyz.service.impl.UserServiceImpl) // UserServiceImpl类下面所有方法
  • this
    用于匹配当前AOP代理对象类型的执行方法,在前文中引入(Introduction)的代理对象使用,可以注入代理对象

  • target
    用于匹配当前目标对象类型的执行方法,可以注入目标对象,被代理的对象

  • args
    用于匹配当前执行的方法传入的参数为指定类型的执行方法,可以注入连接点(方法)的参数列表

  • @target

  • @args

  • @within

  • @annotation

  • bean

代码实战

5种通知的案例

  • DemoAspect.java
    定义切面,及通知,这里为了测试更多的案例,表达式切到AdviceKindTestController.java

package demo.aop.aspect;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Aspect
@Slf4j
@Component //必须是个bean
public class DemoAspect {

    //前置通知
    @Before("execution (* demo.aop.controller.AdviceKindTestController.*(..))")
    public void auth() {
        log.info("前置通知,假装校验了个权限");
    }

    //后置通知
    @AfterReturning("execution (* demo.aop.controller.AdviceKindTestController.*(..))")
    public  Object  afterSomething(){
        log.info("后置通知,不太清楚运用场景");
        return "ok";
    }

    //环绕通知
    //如果环绕通知 不返回执行结果  方法不会返回任何结果,导致接口拿不到任何数据
    //所以一定把proceed 返回
    //ProceedingJoinPoint 是 JoinPoint的子类,仅当环绕通知的时候,可以注入ProceedingJoinPoint的连接点
    @Around("execution (* demo.aop.controller.AdviceKindTestController.*(..))")
    public  Object  getMethodTime(ProceedingJoinPoint point) throws Throwable {
        log.info("环绕通知,统计方法耗时,方法执行前");
        Long beforeMillis = System.currentTimeMillis();
        Object proceed = point.proceed();
        Long taketimes= System.currentTimeMillis()-beforeMillis;
        log.info(String.format("该方法用时%s毫秒",taketimes));
        return proceed;
    }

    //异常通知
    @AfterThrowing("execution (* demo.aop.controller.AdviceKindTestController.*(..))")
    public void throwSomething() {
        log.info("异常通知,只有异常了才会通知。具体场景,不是特别了解");
    }

    //最终通知
    @After("execution (* demo.aop.controller.AdviceKindTestController.*(..))")
    public void closeSomething() {
        log.info("最终通知,官网说,可以用来回收某些资源。无论发不发生异常,都会被执行");
    }

}


  • AdviceKindTestController.java

测试用的接口类

package demo.aop.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class AdviceKindTestController {

    @GetMapping("/advice") ////localhost:8080/advice
    public String test() throws InterruptedException {
        Thread.sleep(4);
        return "ok";
    }


    @GetMapping("/advice/throwing") ////localhost:8080/advice/throwing
    public String test2(){
        int i=1/0;
        return "ok";
    }
}

访问 //localhost:8080/advice 后台输出为

2020-10-18 11:24:01.201                 : 环绕通知,统计方法耗时,方法执行前
2020-10-18 11:24:01.201                 : 前置通知,假装校验了个权限
2020-10-18 11:24:01.201                 : 该方法用时6毫秒
2020-10-18 11:24:01.202                 : 最终通知,官网说,可以用来回收某些资源。无论发不发生异常,都会被执行
2020-10-18 11:24:01.202                 : 后置通知,不太清楚运用场景

执行顺序

- 环绕通知的前面部分
- 前置通知
- 环绕通知的后面部分
- 最终通知
- 后置通知

访问 //localhost:8080/advice/throwing 后台输出为

2020-10-18 11:30:34.935                 : 环绕通知,统计方法耗时,方法执行前
2020-10-18 11:30:34.935                 : 前置通知,假装校验了个权限
2020-10-18 11:30:34.936                 : 最终通知,官网说,可以用来回收某些资源。无论发不发生异常,都会被执行
2020-10-18 11:30:34.936                 : 异常通知,只有异常了才会通知。具体场景,不是特别了解
java.lang.ArithmeticException: / by zero    //test2()方法抛出了异常

执行顺序如下,我们可以看到因为接口出现了异常,所以后置通知并没执行,环绕通知的后面部分也没执行,但最终通知异常通知被执行

- 环绕通知的前面部分
- 前置通知
- 最终通知
- 异常通知

下文预告

  • 切入点表达式详解
  • 通知优先级
  • 通知中引用切点的参数,目标对象,代理对象、切点对象及其方法调用
  • @DeclareParents 实现引入
  • @ControllerAdvice 实现统一错误处理

本文完整代码参考 //gitee.com/haimama/java-study/tree/master/spring-aop
spring aop翻译文档//shouce.jb51.net/spring/aop.html