AOP之获取Controller请求(Request)、返回(Response)参数、报错信息实现日志记录

需求:为系统中所有的提交,修改,删除等等操作(除查询以外的所有操作)做日志记录,记录的内容包括:请求参数,返回参数,如果报错就存储报错信息。日志要添加一个日志类型。
方案:最好的选择就是用注解标记切点,用AOP实现此需求。


一、准备

版本:

  • JDK1.8
  • Spring-5.0.5.RELEASE
  • SpringBoot-2.0.1.RELEASE
  • mybatis-3.4.5
  • mybatis-spring-boot-starter-1.3.2
  • mysql8

日志表(log)表结构:
AOP之获取Controller请求(Request)、返回(Response)参数、报错信息实现日志记录_第1张图片


二、解析

日志按功能分类,定义一个枚举类,用于在切点注解上标记日志类型。存储日志信息的时候存储到type字段上。
日志类型枚举类:LogType.java
标记此操作是什么类型,比如,增加用户,删除用户、、、、

/**
 * Description:
 * User: RoronoraZoro丶WangRui
 * Date: 2018-09-03
 * Time: 下午4:20
 */
public enum LogType {
    /**
     * 应用-增加
     */
    APP_ADD(30001),
    /**
     * 部署-部署应用
     */
    DEP_ADD(40001),
    /**
     * 部署-回滚应用
     */
    DEP_ROLLBACK(40002),
	
    private int value;

    LogType(int value) {
        this.value = value;
    }

    LogType(String value) {
        for (LogType item : values()) {
            if (item.name().equals(value)) {
                this.value = item.value;
            }
        }
        throw new IllegalArgumentException("Invalid type value");
    }

    public int value() {
        return value;
    }

    public static LogType valueOf(int value) {
        for (LogType item : values()) {
            if (item.value() == value) {
                return item;
            }
        }
        throw new IllegalArgumentException("Invalid type value");
    }
}

日志切点注解:LogInfo.java
用来标记某个方法需要添加日志。

/**
 * Description:
 * User: RoronoraZoro丶WangRui
 * Date: 2018-09-03
 * Time: 下午2:19
 */
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LogInfo {

    /**
     * 描述
     * @return
     */
    public String description() default "";

    /**
     * 日志类型
     * @return
     */
    public LogType logType();
}

需要被记录日志的某个Controller方法:

@ApiOperation("添加应用")
@Auth(allowRoles = {RoleType.ADMIN, RoleType.MASTER})
@PostMapping("/apps")
@LogInfo(logType = LogType.APP_ADD)
public GenericResult<AppDTO> add(@RequestBody @Valid AddAppRequest request) {
    return appService.add(request);
}

为其添加了@LogInfo(logType = LogType.APP_ADD)注解,标记为一个切点,表明了方法类型:LogType.APP_ADD 添加应用。
Log实体类:Log.java


/**
 * Description:
 * User: RoronoraZoro丶WangRui
 * Date: 2018-09-03
 * Time: 下午3:27
 */
@Data
public class Log extends BaseEntity{

    private int id;

    private int type;

    private String request = "{}";

    private String response = "{}";

    private String error = "{}";

}

LogService(及Impl),LogMapper(及xml)这些的代码就不贴了,直接看最关键的切面类:LogAop.java

import cn.hutool.core.util.ReflectUtil;
import com.mistra.api.aspect.annotaion.LogInfo;
import com.mistra.core.service.LogService;
import com.mistra.domain.entity.Log;
import lombok.extern.slf4j.Slf4j;
import net.sf.json.JSONObject;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

/**
 * Description: 采集日志
 * User: RoronoraZoro丶WangRui
 * Date: 2018-09-03
 * Time: 下午2:22
 */
@Aspect
@Component
@Slf4j
@Order(Integer.MIN_VALUE)
public class LogAop {

    @Autowired
    private LogService logService;

    private ThreadLocal<Log> threadLocal = new ThreadLocal<>();

    @Pointcut("@annotation(com.t4f.eunomia.api.aspect.annotaion.LogInfo)")
    public void controllerMethodPointcut() {
    }

    /**
     * 前置advice
     * @param point
     */
    @Before("controllerMethodPointcut()")
    public void before(JoinPoint point) {
        Log logEntity = new Log();
        //将当前实体保存到threadLocal
        threadLocal.set(logEntity);
        //获取连接点的方法签名对象,在该对象中可以获取到目标方法名,所属类的Class等信息
        MethodSignature signature = (MethodSignature) point.getSignature();
        //获取到该方法@LogInfo注解中的日志类型:枚举类LogType的值,保存到log实体中
        logEntity.setType(signature.getMethod().getAnnotation(LogInfo.class).logType().value());

        //RequestContextHolder:持有上下文的Request容器,获取到当前请求的request
        RequestAttributes ra = RequestContextHolder.getRequestAttributes();
        ServletRequestAttributes sra = (ServletRequestAttributes) ra;
        HttpServletRequest httpServletRequest = sra.getRequest();

        JSONObject jsonObject = new JSONObject();
        //存储uri到json中
        jsonObject.accumulate("uri", httpServletRequest.getRequestURI().toString());

        //这一步获取到的方法有可能是代理方法也有可能是真实方法
        Method m = ((MethodSignature) point.getSignature()).getMethod();
        //判断代理对象本身是否是连接点所在的目标对象,不是的话就要通过反射重新获取真实方法
        if (point.getThis().getClass() != point.getTarget().getClass()) {
            m = ReflectUtil.getMethod(point.getTarget().getClass(), m.getName(), m.getParameterTypes());
        }
        //通过真实方法获取该方法的参数名称
        LocalVariableTableParameterNameDiscoverer paramNames = new LocalVariableTableParameterNameDiscoverer();
        String[] parameterNames = paramNames.getParameterNames(m);

        //获取连接点方法运行时的入参列表
        Object[] args = point.getArgs();
        //将参数名称与入参值一一对应起来
        Map<String, Object> params = new HashMap<>();
        for (int i = 0; i < parameterNames.length; i++) {
            params.put(parameterNames[i], args[i]);
        }
        jsonObject.accumulate("params", params);
        //为log实体类的request字段赋值
        logEntity.setRequest(jsonObject.toString());
        System.out.println("============================ 》Before : " + logEntity.toString());
    }

    /**
     * 方法成功return之后的advice
     * @param point
     * @param rtv
     */
    @AfterReturning(value = "controllerMethodPointcut()", returning = "rtv")
    public void after(JoinPoint point, Object rtv) {
        //得到当前线程的log对象
        Log log = threadLocal.get();
        //rtv为controller方法返回数据
        JSONObject jsonObject = JSONObject.fromObject(rtv);
        //为log实体的response字段赋值
        log.setResponse(jsonObject.toString());
        //插入一条log信息
        logService.add(threadLocal.get());
        //移除当前log实体
        threadLocal.remove();
        System.out.println("============================ 》AfterReturning : " + log.toString());
    }

    /**
     * 报错之后的advice
     * @param throwing
     */
    @AfterThrowing(value = "controllerMethodPointcut()", throwing = "throwing")
    public void error(Throwable throwing) {
        Log log = threadLocal.get();
        try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
            //将报错信息写入error字段
            throwing.printStackTrace(new PrintStream(byteArrayOutputStream));
            log.setError(byteArrayOutputStream.toString());
        } catch (IOException e) {
            e.printStackTrace();
        }
        logService.add(threadLocal.get());
        threadLocal.remove();
        System.out.println("============================ 》AfterThrowing : " + log.toString());
    }
}


三、相关API

JoinPoint

AspectJ中的切入点匹配的执行点称作连接的(Join Point),在通知方法中可以声明一个JoinPoint类型的参数。通过JoinPoint可以访问连接点的细节。下面简要介绍JponPoint的方法:

1.java.lang.Object[] getArgs():获取连接点方法运行时的入参列表;
2.Signature getSignature() :获取连接点的方法签名对象在该对象中可以获取到目标方法名,所属类的Class等信息;
3.java.lang.Object getTarget() :获取连接点所在的目标对象;
4.java.lang.Object getThis() :获取代理对象本身;

RequestContextHolder

持有上下文的Request容器,RequestContextHolder里面有两个ThreadLocal保存当前线程下的request。getRequestAttributes()方法,相当于直接获取ThreadLocal里面的值,这样就保证了每一次获取到的Request是该请求的request。
AOP之获取Controller请求(Request)、返回(Response)参数、报错信息实现日志记录_第2张图片

JSONObject

public Object put (Object key, Object value) 将value映射到key下。如果此JSONObject对象之前存在一个value在这个key下,当前的value会替换掉之前的value。
public JSONObject accumulate (String key, Object value) 追加value到这个key下。这个方法同element()方法类似,特殊的是,如果当前已经存在一个value在这个key下,那么一个JSONArray将会存储在这个key下,来保存所有追加的value。如果已经存在一个JSONArray,那么当前的value就会添加到这个JSONArray中。
public JSONObject element (String key, Object value) 将键值对放到这个JSONObject对象里面。如果当前value为空(null),那么如果这个key存在的话,这个key就会移除掉。如果这个key之前有value值,那么此方法会调用accumulate()方法。


我的:

  • >>>>>>>CSDN<<<<<<<
  • >>>>>>>GitHub<<<<<<<
  • >>>>>>>个人博客<<<<<<<

AOP之获取Controller请求(Request)、返回(Response)参数、报错信息实现日志记录_第3张图片

你可能感兴趣的:(Spring)