Spring AOP简单样例

简介

AOP (Aspect Oriented Programming) 即 面向切面编程,是一种编程典范,它通过分离横切关注点来增加程序的模块化。通俗说就是 AOP 可以在不修改现有代码的情况下对现有代码增加一些功能,那么这就是 AOP 最强大的功能。

目前最受欢迎的 AOP 库有两个,一个是 AspectJ, 另外一个是 Spring AOP。

核心概念

  • Aspect:即切面,切面一般定义为一个 Java 类, 每个切面连接点所采用的处理逻辑,也就是向连接点注入的代码, AOP在特定的切入点上执行的增强处理。
    1. @Before: 标识一个前置增强方法,相当于BeforeAdvice的功能.
    2. @After: final增强,不管是抛出异常或者正常退出都会执行.
    3. @AfterReturning: 后置增强,似于AfterReturningAdvice, 方法正常退出时执行.
    4. @AfterThrowing: 异常抛出增强,相当于ThrowsAdvice.
    5. @Around: 环绕增强,相当于MethodInterceptor.
  • Joinpoint:即连接点,程序执行的某个点,比如方法执行。构造函数调用或者字段赋值等。
  • Pointcut:即切点,一个匹配连接点的正则表达式。指明Advice要在什么样的条件下才能被触发,当一个连接点匹配到切点时,一个关联到这个切点的特定的 (Advice) 会被执行。
  • Advisor(增强): 是PointCut和Advice的综合体,完整描述了一个advice将会在Pointcut所定义的位置被触发。
  • AOP Proxy:AOP框架创建的对象,代理就是目标对象的加强。Spring中的AOP代理可以使JDK动态代理,也可以是CGLIB代理,前者基于接口,后者基于子类。

使用实践

按照整体规划,这里需要给项目增加一个基于自定义注解的日志切面程序。实现目标:任何请求都要记录到日志,目前不引入数据库,先组织成对应的格式,在控制台输出。
目录组织形式:


image.png

1 引入Spring boot AOP的包

maven文件增加依赖

        
            org.springframework.boot
            spring-boot-starter-aop
        

2 定义切入点要干啥

定义记录那些信息Log类(Log.java)

package com.springboot.action.saas.common.logging.domain;

import lombok.Data;

import java.sql.Timestamp;

@Data
public class Log {
    //描述
    private String description;
    //方法名
    private String method;
    //参数
    private String params;
    //日志类型
    private String logType;
    //请求ip
    private String requestIp;
    //请求耗时
    private Long time;
    //异常详细
    private String exceptionDetail;
    //请求实践
    private Timestamp createTime;
    //构造函数(代参)
    public Log(String logType, Long time) {
        this.logType = logType;
        this.time = time;
    }
}

日志记录业务接口(LogService.java)

package com.springboot.action.saas.common.logging.service;

import com.springboot.action.saas.common.logging.domain.Log;
import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.scheduling.annotation.Async;

/*
* 日志记录业务
*
*/
public interface LogService {
    /*
    * 记录日志,异步执行,不影响正常业务流程
    * 参数 joinPoint 切面方法的信息,当前切入点各种信息
    *     log 要记录的日志信息有那些
    * */
    @Async
    void save(ProceedingJoinPoint joinPoint, Log log);
}

日志记录业务接口实现(LogServiceImpl.java)

package com.springboot.action.saas.common.logging.service.impl;

import com.springboot.action.saas.common.logging.domain.Log;
import com.springboot.action.saas.common.logging.service.LogService;
import com.springboot.action.saas.common.utils.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Service;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;

@Service
public class LogServiceImpl implements LogService {
    /*
    * 记录日志接口实现
    **/
    @Override
    public void save(ProceedingJoinPoint joinPoint, Log log) {
        //获取request 请求对象
        HttpServletRequest request = ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes())
                .getRequest();
        //getSignature获取切面相关信息,比如方法名、目标方法参数等信息
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        //获取抽象类(代理对象)方法
        Method method = signature.getMethod();
        //返回该元素的指定类型的注释,这里是Log注解
        com.springboot.action.saas.common.logging.annotation.Log aopLog = method.getAnnotation(com.springboot.action.saas.common.logging.annotation.Log.class);
        //获取注解传递的参数
        if (log != null) {
            log.setDescription(aopLog.value());
        }
        //通过最笨的反射方法,获取方法路径
        String methodName = joinPoint.getTarget().getClass().getName()+"."+signature.getName()+"()";
        log.setMethod(methodName);
        //参数处理
        //获取参数值
        Object[] argValues = joinPoint.getArgs();
        //获取参数名
        String[] argNames = ((MethodSignature)joinPoint.getSignature()).getParameterNames();
        //组织参数列表
        String params = "{";
        if(argValues != null){
            for (int i = 0; i < argValues.length; i++) {
                params += " " + argNames[i] + ": " + argValues[i];
            }
        }
        log.setParams(params + " }");
        //获取IP地址
        log.setRequestIp(StringUtils.getIP(request));
        //输出下日志到控制台
        System.out.println(log.toString());
    }
}

3 定义切入点以及切入点具体动作

切点的表达式以指示器指定的类型,配合类似正则表达式的用来告诉 Spring AOP 如何匹配连接点,Spring AOP 提供了以下几种指示器:

  • execution
  • within
  • this和target
  • args
  • @within
  • @target
  • @args
  • @annotation

下面依次说明指示器的作用

execution

该指示器用来匹配方法执行连接点,即匹配哪个方法执行,如

@Pointcut("execution(public String aaric.springaopdemo.UserDao.findById(Long))")

上面这个切点会匹配在 UserDao 类中 findById 方法的调用,并且需要该方法是 public 的,返回值类型为 String,只有一个 Long 的参数。
切点的表达式同时还支持宽字符匹配,如

@Pointcut("execution(* aaric.springaopdemo.UserDao.*(..))")

上面的表达式中,第一个宽字符 * 匹配 任何返回类型,第二个宽字符 * 匹配 任何方法名,最后的参数 (..) 表达式匹配 任意数量任意类型 的参数,也就是说该切点会匹配类中所有方法的调用。

within

如果要匹配一个类中所有方法的调用,便可以使用 within 指示器

@Pointcut("within(aaric.springaopdemo.UserDao)")

这样便可以匹配该类中所有方法的调用了。同时,我们还可以匹配某个包下面的所有类的所有方法调用,如下面的例子
@Pointcut("within(aaric.springaopdemo..*)")

this 和 target

如果目标对象实现了任何接口,Spring AOP 会创建基于CGLIB 的动态代理,这时候需要使用 target 指示器
如果目标对象没有实现任何接口,Spring AOP 会创建基于JDK的动态代理,这时候需要使用 this 指示器

@Pointcut("target(aaric.springaopdemo.A)") A 实现了某个接口
@Pointcut("this(aaric.springaopdemo.B)") B 没有实现任何一个接口

args

该指示器用来匹配具体的方法参数

@Pointcut("execution(* *..find*(Long))")

这个切点会匹配任何以 find 开头并且只有一个 Long 类型的参数的方法。
如果我们想匹配一个以 Long 类型开始的参数,后面的参数类型不做限制,我们可以使用如下的表达式

@Pointcut("execution(* *..find*(Long,..))")

@target
该指示器不要和 target 指示器混淆,该指示器用于匹配连接点所在的类是否拥有指定类型的注解,如

@Pointcut("@target(org.springframework.stereotype.Repository)")

@annotation

该指示器用于匹配连接点的方法是否有某个注解

@Pointcut("@annotation(org.springframework.scheduling.annotation.Async)")

配置切点和切点对应的动作(好多文档都说是通知)
LogAspect.java

package com.springboot.action.saas.common.logging.aspect;

import com.springboot.action.saas.common.logging.domain.Log;
import com.springboot.action.saas.common.logging.service.LogService;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class LogAspect {
    //日志业务
    @Autowired
    private LogService logService;
    //当前时间
    private long currentTime = 0L;

    /**
     * 配置切入点, 匹配连接点的方法是否有Log注解,定义切点
     */
    @Pointcut("@annotation(com.springboot.action.saas.common.logging.annotation.Log)")
    public void logPointcut() {
        // 该方法无方法体,主要为了让同类中其他方法使用此切入点
    }

    /**
     * 配置环绕通知,使用在方法logPointcut()上注册的切入点,具体要通知在什么条件下执行和执行什么动作
     *
     * @param joinPoint join point for advice
     */
    @Around("logPointcut()")
    public Object logAround(ProceedingJoinPoint joinPoint){
        //返回值
        Object result = null;
        //获取当前时间
        currentTime = System.currentTimeMillis();
        try {
            //执行目标方法
            result = joinPoint.proceed();
        } catch (Throwable e) {
            //抛异常
            throw new RuntimeException(e.getMessage());
        }
        //创建日志对象
        Log log = new Log("INFO",System.currentTimeMillis() - currentTime);
        //记录日志
        logService.save(joinPoint, log);
        //返回目标方法的返回值
        return result;
    }

    /**
     * 配置异常通知,异常的日志也要记录
     *
     * @param joinPoint join point for advice
     * @param e exception
     */
    @AfterThrowing(pointcut = "logPointcut()", throwing = "e")
    public void logAfterThrowing(JoinPoint joinPoint, Throwable e) {
        //创建日志对象
        Log log = new Log("ERROR",System.currentTimeMillis() - currentTime);
        //异常设置
        log.setExceptionDetail(e.getMessage());
        //记录异常
        logService.save((ProceedingJoinPoint)joinPoint, log);
    }
}

你可能感兴趣的:(Spring AOP简单样例)