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

版权申明:本文为原创文章,转载请注明原文出处

原文链接:https://blog.it-follower.com/posts/55284975.html

需求

在开发系统时,尤其是后台管理系统,几乎每一个操作,都要求记录其操作日志。如果在每一个操作结束之后,都加上一个记录日志的操作,那样代码会非常臃肿,耦合度高、代码可读性差,维护难。本例中,采用AOP来实现日志记录功能,一个注解即可实现同样的效果。

实现

  • 新建一个注解SysLogPoint,用于标识需要记录日志的切面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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;
}

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

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

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

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

1
2
3
4
5
6
7
8
9
package com.yclouds.common.core.aspect;

/**
* @author ye17186
* @version 2019/3/26 16:26
*/
public enum SysLogTarget {
CONTROLLER, SERVICE, DAO, METHOD
}
  • 定义一个切面处理类SysLogAspect,用@Aspect注解标注它是一个切面,用@Component注解注册到spring中,具体逻辑实现全在里面

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    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<String> sensitiveParams) {

    Map<String, Object> 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<String> params) {

    if (root.isObject()) {
    Iterator<Entry<String, JsonNode>> rootIt = root.fields();
    while (rootIt.hasNext()) {
    Entry<String, JsonNode> 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);
    }
    }
    }
    }

    其中的方法afterReturn和afterThrowing分别处理目标在正确返回和抛出异常的日志记录。日志对象SysLogModel,可根据项目需求自行定制,例如加上处理时间、客户端ip等等信息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    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()方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    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;
    }

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

    日志输出:

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

评论