一、需求
在开发系统时,尤其是后台管理系统,几乎每一个操作,都要求记录其操作日志。
二、实现
如果在每一个操作结束之后,都加上一个记录日志的操作,那样代码会非常臃肿,耦合度高、代码可读性差,维护难。本例中,采用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;
}
发送两个请求,看看日志效果:
不管是图一中的正确返回,还是图二中的抛出异常,日志都正确的记录下来,而且入参中的敏感参数password,成功的隐藏了。
GitHub地址:https://github.com/ye17186/spring-boot-learn