springboot基于AOP实现操作日志记录

一、需求

在开发系统时,尤其是后台管理系统,几乎每一个操作,都要求记录其操作日志。

二、实现

如果在每一个操作结束之后,都加上一个记录日志的操作,那样代码会非常臃肿,耦合度高、代码可读性差,维护难。本例中,采用AOP来实现日志记录功能,一个注解即可实现同样的效果。

1、新建一个注解SysLogPoint,用于标识需要记录日志的切面

package com.yclouds.common.core.aspect;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @author ye17186
 * @version 2019/3/26 16:18
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface SysLogPoint {

    /**
     * 操作名
     */
    String actionName() default "unknown";

    /**
     * 是否忽略结果
     */
    boolean ignoreOutput() default false;

    /**
     * 敏感参数
     */
    String[] sensitiveParams() default {};

    /**
     * 目标类型:CONTROLLER:controller日志, SERVICE:service日志, DAO:dao日志, METHOD:普通方法日志
     */
    SysLogTarget target() default SysLogTarget.CONTROLLER;
}

1.1 actionName:每一个操作,都需指定一个操作名

1.2 ignoreOutput:是否忽略输出,true的情况下,将不记录目标处理的输出结果

1.3 sensitiveParams:敏感参数,像password这类参数在记录时,需要脱敏

1.4 target:目标类型,其中的SysLogTarget是一个枚举,在项目分层时,常常分为controller、service、dao等,记录不同层的日志,最后区别开来,更利于后期的日志分析。在日志开发中,往往需要记录controller层的操作日志,所以这里的target默认值为CONTROLLER。

package com.yclouds.common.core.aspect;

/**
 * @author ye17186
 * @version 2019/3/26 16:26
 */
public enum SysLogTarget {
    CONTROLLER, SERVICE, DAO, METHOD
}

2、定义一个切面处理类SysLogAspect,用@Aspect注解标注它是一个切面,用@Component注解注册到spring中,具体逻辑实现全在里面

package com.yclouds.service.demo.aspect;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.TextNode;
import com.google.common.collect.Maps;
import com.yclouds.common.core.aspect.SysLogModel;
import com.yclouds.common.core.aspect.SysLogPoint;
import com.yclouds.common.core.aspect.SysLogType;
import com.yclouds.common.core.utils.JsonUtils;
import java.io.IOException;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;

/**
 * @author ye17186
 * @version 2019/3/26 16:22
 */
@Slf4j
@Component
@Aspect
public class SysLogAspect {

    /**
     * 正常返回处理
     *
     * @param jp 连接点
     * @param point 注解
     * @param result 返回结果
     */
    @AfterReturning(value = "@annotation(point)", returning = "result")
    public void afterReturn(JoinPoint jp, SysLogPoint point, Object result) {

        SysLogModel sysLog = buildLog(jp, point, result, null);
        saveLog(sysLog);
    }

    /**
     * 抛出异常的处理
     *
     * @param jp 连接点
     * @param point 注解
     * @param ex 异常对象
     */
    @AfterThrowing(value = "@annotation(point)", throwing = "ex")
    public void afterThrowing(JoinPoint jp, SysLogPoint point, Throwable ex) {

        SysLogModel sysLog = buildLog(jp, point, null, ex);
        saveLog(sysLog);
    }

    private void saveLog(SysLogModel sysLog) {

        // 本例中直接打印日志,生产环境中可采用异步的方式,保存到DB等媒介中
        log.info("[SysLog]: {}", JsonUtils.toJson(sysLog));
    }

    /**
     * 构建日志对象
     *
     * @param jp 连接点
     * @param point 注解
     * @param result 处理结果对象
     * @param ex 处理异常对象
     * @return 日志日志对象
     */
    private SysLogModel buildLog(JoinPoint jp, SysLogPoint point, Object result, Throwable ex) {

        SysLogModel sysLog = new SysLogModel();
        sysLog.setActionName(point.actionName());
        sysLog.setTarget(point.target().name());
        sysLog.setType(ex == null ? SysLogType.RETURN.name() : SysLogType.THROWING.name());

        sysLog.setInput(handleInput(jp.getArgs(), Arrays.asList(point.sensitiveParams())));
        sysLog.setOutput(handleOutput(result, point.ignoreOutput()));
        sysLog.setExMsg(handleException(ex));

        return sysLog;
    }


    /**
     * 处理输入参数
     *
     * @param args 入参
     * @param sensitiveParams 敏感参数关键字
     * @return 特殊处理都的入参
     */
    private String handleInput(Object[] args, List sensitiveParams) {

        Map argMap = Maps.newTreeMap();
        ObjectMapper om = new ObjectMapper();
        if (!ObjectUtils.isEmpty(args)) {
            for (int i = 0; i < args.length; i++) {
                if (args[i] != null && !ObjectUtils.isEmpty(sensitiveParams)) {
                    try {
                        JsonNode root = om.readTree(JsonUtils.toJson(args[i]));
                        handleSensitiveParams(root, sensitiveParams);
                        argMap.put("arg" + (i + 1), root);
                    } catch (IOException e) {
                        argMap.put("arg" + (i + 1), "[exception]");
                    }
                } else {
                    argMap.put("arg" + (i + 1), args[i]);
                }
            }
        }
        return JsonUtils.toJson(argMap);
    }

    /**
     * 处理输出结果
     *
     * @param result 源输出结果
     * @param ignore 是否忽略结果
     * @return 处理后的输出结果
     */
    private String handleOutput(Object result, boolean ignore) {

        return (ignore || result == null) ? null : JsonUtils.toJson(result);
    }


    /**
     * 处理异常信息
     *
     * @param ex 异常对象
     * @return 处理后的异常信息
     */
    private String handleException(Throwable ex) {
        return ex == null ? null : ex.toString();
    }

    /**
     * 处理敏感参数
     *
     * @param root jackson节点
     * @param params 敏感参数名列表
     */
    private void handleSensitiveParams(JsonNode root, List params) {

        if (root.isObject()) {
            Iterator> rootIt = root.fields();
            while (rootIt.hasNext()) {
                Entry node = rootIt.next();
                if (params.contains(node.getKey())) {
                    node.setValue(new TextNode("[hidden]"));
                } else {
                    JsonNode tmpNode = node.getValue();
                    if (tmpNode.isObject()) {
                        handleSensitiveParams(tmpNode, params);
                    } else if (tmpNode.isArray()) {
                        for (JsonNode jsonNode : tmpNode) {
                            handleSensitiveParams(jsonNode, params);
                        }
                    }
                }
            }
        } else if (root.isArray()) {
            for (JsonNode jsonNode : root) {
                handleSensitiveParams(jsonNode, params);
            }
        }
    }
}

2.1 其中的方法afterReturn和afterThrowing分别处理目标在正确返回和抛出异常的日志记录。

2.2 日志对象SysLogModel,可根据项目需求自行定制,例如加上处理时间、客户端ip等等信息

package com.yclouds.common.core.aspect;

import java.io.Serializable;
import lombok.Data;

/**
 * @author ye17186
 * @version 2019/3/26 16:53
 */
@Data
public class SysLogModel implements Serializable {

    /**
     * 操作名
     */
    private String actionName;

    /**
     * 目标类型:CONTROLLER、SERVICE、DAO、METHOD
     */
    private String target;

    /**
     * 日志类型:RETURN、THROWING
     */
    private String type;

    /**
     * 输入
     */
    private String input;

    /**
     * 输出
     */
    private String output;

    /**
     * 异常信息
     */
    private String exMsg;
}

三、测试

在需要记录日志的地方,加上@SysLogPoint注解即可,如代码中的syaHello5()方法

package com.yclouds.service.demo.modules.hello.controller;

import com.yclouds.common.core.aspect.SysLogPoint;
import com.yclouds.common.core.aspect.SysLogTarget;
import com.yclouds.common.core.response.ApiResp;
import com.yclouds.common.core.web.YRestController;
import com.yclouds.service.demo.modules.hello.dto.HelloInDTO;
import com.yclouds.service.demo.modules.hello.service.HelloService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

/**
 * @author ye17186
 * @version 2019/3/22 13:50
 */
@Slf4j
@YRestController("/hello")
public class HelloController {

    @SysLogPoint(actionName = "Say4", sensitiveParams = "password")
    @PostMapping("/say4")
    public ApiResp sayHello4(@RequestBody HelloInDTO inDTO) {
        log.info("业务处理...");
        return ApiResp.retOK();
    }

    @SysLogPoint(actionName = "Say5", sensitiveParams = "password")
    @PostMapping("/say5")
    public ApiResp sayHello5(@RequestBody HelloInDTO inDTO) {
        log.info("业务处理...");
        System.out.println(1 / 0);
        return ApiResp.retOK();
    }
}

参数对象HelloInDTO

package com.yclouds.service.demo.modules.hello.dto;

import java.io.Serializable;
import lombok.Data;

/**
 * @author ye17186
 * @version 2019/3/26 16:36
 */
@Data
public class HelloInDTO implements Serializable {

    private static final long serialVersionUID = -5901714862103467412L;

    private String title;

    private String password;

    private HelloInDTO sub;
}

发送两个请求,看看日志效果:

springboot基于AOP实现操作日志记录_第1张图片

不管是图一中的正确返回,还是图二中的抛出异常,日志都正确的记录下来,而且入参中的敏感参数password,成功的隐藏了。

GitHub地址:https://github.com/ye17186/spring-boot-learn

你可能感兴趣的:(SpringBoot)