AOP介绍以及Spring AOP的使用

发布于 2020-01-11  231 次阅读


前言

首先,什么是AOP? AOP(Aspect-Oriented Programming:面向切面编程) 能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如日志记录,性能统计,权限控制,事务处理,异常处理等)封装起来,便于减少系统的重复代码降低模块间的耦合度,并有利于未来的可拓展性和可维护性

AOP和OOP很像,一字母之差,但是两者却是面向不同领域的两种思想。OOP(面向对象)主要是针对业务处理过程中的实体、属性、行为进行抽象封装,OOP的三大特性,封装、继承、多态。

AOP是面向切面,切入的哪?切入的是业务处理过程中的某个步骤或阶段,比如一个用户User类,其有多个行为,你想在这些行为做之前判断这个用户是否有权限,或者记录下他这些行为发生的时间,耗时等相关信息,而又不想在每个行为中都加入这些判断权限,记录日志的相关代码,显得重复代码特别多, 这个时候利用AOP就可以很好的切入,只需要一个AOP的实现类,你就可以切入到这些行为,无论是开始时的位置,还是结束时的位置

关于Spring AOP

Spring AOP就是基于动态代理的,如果要代理的对象,实现了某个接口,那么Spring AOP会使用JDK Proxy,去创建代理对象,而对于没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候Spring AOP会使用Cglib ,这时候Spring AOP会使用 Cglib 生成一个被代理对象的子类来作为代理 ,如下图所示

还有AspectJ,Spring AOP已经集成了AspectJ, AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了,后面讲的AOP的使用就是基于AspectJ的注解方式的切面 Spring AOP 属于运行时增强,而 AspectJ 是编译时增强。 Spring AOP 基于代理(Proxying),而 AspectJ 基于字节码操作(Bytecode Manipulation),所以性能必然是AspectJ更好一些,当然,切面较少时无所谓了。

使用AOP时,真实的情形是,当应用调用被AOP代理的方法时,AOP代理会在自己的方法中回调目标对象的方法(下方代码中的joinPoint.proceed();),从而完成这个被代理方法的调用,而且joinPoint.proceed(); 前后就可以任意根据自己需求进行相关代码添加,比如日志,事务,权限等等。

AOP的实践

代码再最下方,这个代码作用是记录控制器方法的执行时间

说下里面的一些含义

  • join point(连接点):是程序执行中的一个精确执行点,例如类中的一个方法,我们通过joinPoint.proceed()即可执行这个方法
  • point cut(切入点):本质上是一个捕获连接点的结构,代表切入到那些方法内,比如com.example.controller.*.*() 第一个*代表某个类,第二个*()则代表某个方法
  • @Before 前置通知,方法执行前触发
  • @AfterReturning 后置返回通知,在@Around中执行结束返回结果时触发
  • @After 后置通知,方法执行后出发
  • @Around 环绕通知, 最重要的一个, joinPoint.proceed() 即在此内执行
  • @AfterThrowing 异常通知,顾名思义,发生异常时触发

Spring 定义切入点语法

excution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)

除了ret-type-pattern(返回类型模式}、name-pattern(param-pattern}(名字模式和参数模式}外,其他模式都是可选的。返回类型模式决定了方法的返回类型,必须依次匹配一个连接点(即一个方法}。使用最频繁的一个返回类型模式是*,它代表了匹配任意的返回类型,如果写明了返回类型,比如String,那么只能匹配返回String类型的连接点(方法}。名字模式匹配的是方法名,你可以用通配符*表示匹配所有方法名。参数模式中,( )表示匹配了无参数的方法,而(..)表示匹配任意数量参数的方法。模式(*)表示匹配任意类型参数的方法。模式(*,String)表示匹配:第一个为任意参数类型,第二个必须为String类型的方法。

modifiers-pattern:方法的操作权限
ret-type-pattern:返回值
declaring-type-pattern:方法所在的包
name-pattern:方法名
parm-pattern:参数名
throws-pattern:异常

下面是定义切入点的例子:

任意公共方法的执行:excution(public * (..))

任何一个以set开头的方法执行:excution( set(..))

AccountService接口的任意方法的执行:excution( com.example.service.AccountService.(..))

定义在service包的任意方法的执行:excution( com.example.service..(..))

定义在service包或子包的任意方法的执行:excution(* com.example.service...(..))

引用一张比较直观的图
//@Order(1) 可选,如果一个方法被多个Aspect拦截,使用该注解可以设定执行顺序,不然的话顺序不定
@Aspect
@Component
@Slf4j
public class TimerAspect {
    @Autowired
    SysOperationLogService sysOperationLogService;
    private final String pointcut = "pointcut()";

    // 切点
    @Pointcut("execution(* com.loneking.admin.controller.*.*(..))")
    private void pointcut() {
    }

    //前置通知
    @Before(pointcut)
    public void before(){
        System.out.println("前置通知....");
    }

    //后置返回通知, returnVal: 切点方法执行后的返回值
    @AfterReturning(value=pointcut, returning = "returnVal")
    public void AfterReturning(Object returnVal){
        System.out.println("后置通知...." + returnVal);
    }

    // 环绕通知
    @Around(pointcut)
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        Object target = joinPoint.getTarget();
        Class clazz = target.getClass();
        Signature sig = joinPoint.getSignature();
        MethodSignature msig = (MethodSignature) sig;
        String methodName = sig.getName();
        Method currentMethod = target.getClass().getMethod(msig.getName(), msig.getParameterTypes());

        // 记录函数参数
        //log.info("函数: {}, 参数: {}", methodName, Arrays.toString(joinPoint.getArgs()));
        // 记录开始时间
        long start = System.currentTimeMillis();
        // 判断该方法是否有request参数 有的话获取为之后记录URL日志用
        HttpServletRequest tempRequest = null;
        for (Object arg : joinPoint.getArgs()) {
            if (arg instanceof HttpServletRequest) {
                tempRequest = (HttpServletRequest) arg;
                break;
            }
        }
        final HttpServletRequest request = tempRequest;
        // 函数开始执行
        // 执行函数
        Object obj = new Object();
        try {
            // 重点!!! 执行该被切的方法
            obj = joinPoint.proceed();
        } catch (Exception e) {
           throw e;
        }
        // 函数结束时间
        long end = System.currentTimeMillis();

        // 记录函数返回值
        //log.debug("函数: {}, 返回值: {}", methodName, obj);

        // 记录函数耗时
        log.info("{}.{}, 耗时: {} ms", clazz.toString(), methodName, (end - start));
        //注意 这里是存入数据库了,但实际中我发现会影响速度 甚至两秒多延时,所以最好新开线程去异步执行
        ThreadUtil.addTask(() -> {
            sysOperationLogService.addLog(request, "执行时间:" + (end - start) + " ms");
        });
        return obj;
    }

    // 异常通知
    @AfterThrowing(value = pointcut, throwing = "e")
    public void afterThrowable(Throwable e) {
        System.out.println("出现异常:msg=" + e.getMessage());
    }

    // 后置通知
    @After(value=pointcut)
    public void after(){
        System.out.println("后置通知....");
    }
}
执行效果