Spring从入门到放弃之基于AOP的日志统计方案
作者:互联网
1.概述
在日常开发中,数据安全一般也是企业关注的重点问题,因此对于数据的操作记录也是关注的重点对象。采集操作日志也成为企业生产中必不可少的一环,如何在不影响业务且不需要新增大量记录日志的代码就能实现日志记录的功能,一直是企业所探索的方案。本文将详细分析利用Spring AOP特性,在不影响业务且不需要新增多余代码的前提下,实现该功能。同时基于Servlet拦截器的功能,实现同样的功能。最后分别对比方案的优劣,并提供有效代码供大家学习。
2.日志统计方案
日志的类型有很多,这里我简要将其分为两类:一类是提供给开发人员,方便线上出现某些故障进行排查问题所用,这类日志一般记录在文件中,存储在某个指定位置;另一类是针对用户的操作,包括对某些关键数据的增、删、改操作,这类日志一般进行持久化操作,方便查询展示。本文的日志统计方案主要针对第二中情况,对用户提供展示日志。
2.1 基于AOP日志统计方案
基于AOP日志统计方案主要包含以下几个步骤:
1.引入依赖
2.添加日志配置
3.自定义注解
4.实现AOP切面和切点
5.设计数据库表与DAO层
本节将按照上述步骤,依次讲解实现。
2.1.1 引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>2.2.6.RELEASE</version>
</dependency>
这里引入的是2.2.6版本的AOP版本,具体版本可依据自己当前项目所使用的springboot版本来进行适配。
2.1.2 添加日志配置
spring.aop.auto=true
上述配置默认是true,也就是意味着即使不配置这行,也是可以使用AOP相关功能的,这里是为了突出这行配置。
2.1.3 自定义注解
package com.hznu.management.annotation;
import com.hznu.management.enums.LogType;
import java.lang.annotation.*;
@Target({ElementType.PARAMETER, ElementType.METHOD})//作用于参数或方法上
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SystemLog {
/**
* 日志名称
*
* @return
*/
String description() default "";
/**
* 日志类型
*
* @return
*/
LogType type() default LogType.OPERATION;
}
基于注解实现的日志记录方案非常灵活,所有添加注解的地方都可以进行日志记录。本文主要在注解中定义了两个属性:日志名称和日志类型。日志名称相当于一个描述,比如可以填新增用户操作,表面这里实现的是一个添加新用户操作。日志类型包括登录、新增、删除、修改等,我这里定义了一个枚举类来记录,方便根据场景进行定制,具体枚举类LogType如下:
package com.hznu.management.enums;
/**
* @Description: 操作枚举类
* @Author Marin
* @Date 2021/4/29 17:03
*/
public enum LogType {
/**
* 0:登陆日志
*/
LOGIN(0, "用户登录"),
/**
* 1:新增操作
*/
ADD(1, "新增操作"),
/**
* 2:删除操作
*/
DELETE(2, "删除操作"),
/**
* 3:修改操作
*/
UPDATE(3, "修改操作"),
/**
* 4:导出操作
*/
EXPORT(4, "导出操作"),
/**
* 5.其它操作
*/
OPERATION(5, "其他操作");
private Integer val;
private String desc;
public Integer getVal() {
return val;
}
public void setVal(Integer val) {
this.val = val;
}
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
LogType(Integer val, String desc) {
this.val = val;
this.desc = desc;
}
public static boolean contains(Integer i) {
LogType[] values = values();
for (LogType value : values) {
if (value.val.equals(i)) {
return true;
}
}
return false;
}
public static String getDescription(Integer i) {
if (contains(i)) {
LogType[] values = values();
for (LogType value : values) {
if (value.val.equals(i)) {
return value.getDesc();
}
}
}
return null;
}
}
2.1.4 实现AOP切面和切点
package com.hznu.management.aop;
import com.alibaba.fastjson.JSON;
import com.hznu.management.annotation.SystemLog;
import com.hznu.management.entity.Log;
import com.hznu.management.service.LogService;
import com.hznu.management.thread.SaveSystemLogThread;
import com.hznu.management.utils.IpInfoUtil;
import com.hznu.management.utils.ThreadPoolUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.NamedThreadLocal;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@Aspect
@Component
public class LogAspect {
private static final ThreadLocal<Date> beginTimeThreadLocal = new NamedThreadLocal<Date>("ThreadLocal beginTime");
@Autowired
private LogService logService;
@Autowired(required = false)
private HttpServletRequest request;
@Autowired
private IpInfoUtil ipInfoUtil;
private final Log systemLog = new Log();
@Pointcut("@annotation(com.hznu.management.annotation.SystemLog)")
public void controllerAspect() {
}
@Before("controllerAspect()")
public void doBefore(JoinPoint joinPoint) throws Exception {
//线程绑定变量(该数据只有当前请求的线程可见)
Date beginTime = new Date();
beginTimeThreadLocal.set(beginTime);
//获取注解中的description属性值
String description = getControllerMethodInfo(joinPoint).get("description").toString();
//日志标题
systemLog.setName(description);
//日志类型
systemLog.setLogType((int) getControllerMethodInfo(joinPoint).get("type"));
//日志请求url
systemLog.setRequestUrl(request.getRequestURI());
//请求方式
systemLog.setRequestType(request.getMethod());
//获取body体中参数
HashMap<String, Object> params = new HashMap<>();
Object[] args = joinPoint.getArgs();
params.put("body", args);
//获取url中的请求参数
Map<String, String[]> requestParams = request.getParameterMap();
params.put("head", requestParams);
//设置请求参数
systemLog.setRequestParam(JSON.toJSONString(params));
//其他属性
systemLog.setIp(ipInfoUtil.getIpAddr(request));
systemLog.setCreateBy("system");
systemLog.setDelFlag(0);
systemLog.setStartTime(beginTime);
}
@Around("controllerAspect()")
public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
Object proceed = null;
try {
proceed = proceedingJoinPoint.proceed();
systemLog.setStatus(0);
} catch (Exception e) {
log.error("AOP环绕通知通知异常", e);
systemLog.setStatus(1);
}
return proceed;
}
/**
* 获取注解中对方法的描述信息 用于Controller层注解
*
* @param joinPoint 切点
* @return 方法描述
* @throws Exception
*/
public static Map<String, Object> getControllerMethodInfo(JoinPoint joinPoint) throws Exception {
Map<String, Object> map = new HashMap<String, Object>(16);
//获取目标类名
String targetName = joinPoint.getTarget().getClass().getName();
//获取方法名
String methodName = joinPoint.getSignature().getName();
//获取相关参数
Object[] arguments = joinPoint.getArgs();
//生成类对象
Class targetClass = Class.forName(targetName);
//获取该类中的方法
Method[] methods = targetClass.getMethods();
String description = "";
Integer type = null;
for (Method method : methods) {
if (!method.getName().equals(methodName)) {
continue;
}
Class[] clazzs = method.getParameterTypes();
if (clazzs.length != arguments.length) {
//比较方法中参数个数与从切点中获取的参数个数是否相同,原因是方法可以重载哦
continue;
}
description = method.getAnnotation(SystemLog.class).description();
type = method.getAnnotation(SystemLog.class).type().getVal();
map.put("description", description);
map.put("type", type);
}
return map;
}
@After("controllerAspect()")
public void doAfter(JoinPoint joinPoint) {
//请求开始时间
long beginTime = beginTimeThreadLocal.get().getTime();
//请求结束时间
long endTime = System.currentTimeMillis();
systemLog.setEndTime(new Date(endTime));
//请求耗时
Long logElapsedTime = endTime - beginTime;
systemLog.setCostTime(logElapsedTime.intValue());
//日志记录创建时间
systemLog.setCreateTime(new Date());
ThreadPoolUtil.getPool().execute(new SaveSystemLogThread(systemLog, logService));
}
}
在AOP切面中主要实现的功能如下:
1.@Before:前置通知,在方法执行前执行,这里利用ThrealLocal记录了方法的开始时间,同时进行了日志
对象的初始化,获取了请求参数(包括url和body体中的参数)以及一些其他参数的封装;
2.@Around:环绕通知,执行方法并记录方法执行状态(0:正常执行,1:执行异常);
3.@After:后置通知,在方法执行后执行,获取执行结束时间,统计方法执行耗时(单位:毫秒),同时记录
方法日志创建时间,最后放入线程池,进行存储入库操作。
2.1.5 设计数据库表与DAO层
数据表的主要结构如下:
-- ----------------------------
-- Table structure for system_log
-- ----------------------------
DROP TABLE IF EXISTS `system_log`;
CREATE TABLE `system_log` (
`id` bigint(12) NOT NULL AUTO_INCREMENT COMMENT '设备自增ID',
`name` varchar(100) DEFAULT NULL,
`log_type` int(10) DEFAULT NULL,
`request_url` varchar(50) DEFAULT NULL COMMENT '请求路径',
`request_type` varchar(20) DEFAULT NULL COMMENT '设备类型(DELETE、PUT、POST、GET)',
`request_param` varchar(255) DEFAULT NULL COMMENT '请求参数',
`user_name` varchar(32) DEFAULT NULL COMMENT '用户名称',
`ip` varchar(32) DEFAULT NULL COMMENT 'ip地址',
`cost_time` int(15) DEFAULT NULL COMMENT '请求时间(ms)',
`del_flag` int(2) DEFAULT '0' COMMENT '删除标志位 0=未删除, 1=删除',
`create_by` varchar(10) DEFAULT NULL COMMENT '记录创建人',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`start_time` datetime DEFAULT NULL COMMENT '创建时间',
`end_time` datetime DEFAULT NULL COMMENT '创建时间',
`operands` varchar(100) DEFAULT NULL COMMENT '操作对象',
`status` tinyint(2) DEFAULT '0' COMMENT '是否成功 0=成功 1=失败',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='日志记录表';
实现层主要如下:
package com.eckey.lab.service.impl;
import com.alibaba.fastjson.JSON;
import com.eckey.lab.entity.Log;
import com.eckey.lab.mapper.LogServiceMapper;
import com.eckey.lab.service.LogService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Slf4j
@Service("logService")
public class LogServiceImpl implements LogService {
@Resource
private LogServiceMapper logServiceMapper;
@Override
public void insert(Log systemLog) {
logServiceMapper.insert(systemLog);
log.info("插入数据成功:{}", JSON.toJSONString(systemLog));
}
}
存储DAO层如下:
package com.eckey.lab.mapper;
import com.eckey.lab.entity.Log;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface LogServiceMapper {
int insert(Log log);
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.eckey.lab.mapper.LogServiceMapper">
<resultMap id="BaseResultMap" type="com.eckey.lab.entity.Log">
<id column="id" jdbcType="BIGINT" property="id"/>
<result column="name" jdbcType="VARCHAR" property="name"/>
<result column="log_type" jdbcType="NUMERIC" property="logType"/>
<result column="request_url" jdbcType="VARCHAR" property="requestUrl"/>
<result column="request_type" jdbcType="VARCHAR" property="requestType"/>
<result column="request_param" jdbcType="VARCHAR" property="requestParam"/>
<result column="user_name" jdbcType="VARCHAR" property="username"/>
<result column="ip" jdbcType="VARCHAR" property="ip"/>
<result column="cost_time" jdbcType="NUMERIC" property="costTime"/>
<result column="del_flag" jdbcType="NUMERIC" property="delFlag"/>
<result column="create_by" jdbcType="VARCHAR" property="createBy"/>
<result column="create_time" jdbcType="TIMESTAMP" property="createTime"/>
<result column="start_time" jdbcType="TIMESTAMP" property="startTime"/>
<result column="end_time" jdbcType="TIMESTAMP" property="endTime"/>
<result column="operands" jdbcType="VARCHAR" property="operands"/>
<result column="status" jdbcType="NUMERIC" property="status"/>
</resultMap>
<insert id="insert" parameterType="com.eckey.lab.entity.Log">
insert into system_log
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="id != null">
id,
</if>
<if test="name != null">
name,
</if>
<if test="logType != null">
log_type,
</if>
<if test="status != null">
status,
</if>
<if test="requestUrl != null">
request_url,
</if>
<if test="requestType != null">
request_type,
</if>
<if test="requestParam != null">
request_param,
</if>
<if test="username != null">
user_name,
</if>
<if test="ip != null">
ip,
</if>
<if test="costTime != null">
cost_time,
</if>
<if test="delFlag != null">
del_flag,
</if>
<if test="createBy != null">
create_by,
</if>
<if test="createTime != null">
create_time,
</if>
<if test="endTime != null">
end_time,
</if>
<if test="operands != null">
operands,
</if>
<if test="startTime != null">
start_time,
</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="id != null">
#{id,jdbcType=NUMERIC},
</if>
<if test="name != null">
#{name,jdbcType=VARCHAR},
</if>
<if test="logType != null">
#{logType,jdbcType=NUMERIC},
</if>
<if test="status != null">
#{status,jdbcType=BIT},
</if>
<if test="requestUrl != null">
#{requestUrl,jdbcType=VARCHAR},
</if>
<if test="requestType != null">
#{requestType,jdbcType=VARCHAR},
</if>
<if test="requestParam != null">
#{requestParam,jdbcType=VARCHAR},
</if>
<if test="username != null">
#{username,jdbcType=VARCHAR},
</if>
<if test="ip != null">
#{ip,jdbcType=VARCHAR},
</if>
<if test="costTime != null">
#{costTime,jdbcType=NUMERIC},
</if>
<if test="delFlag != null">
#{delFlag,jdbcType=NUMERIC},
</if>
<if test="createBy != null">
#{createBy,jdbcType=VARCHAR},
</if>
<if test="endTime != null">
#{endTime,jdbcType=TIMESTAMP},
</if>
<if test="createTime != null">
#{createTime,jdbcType=TIMESTAMP},
</if>
<if test="operands != null">
#{operands,jdbcType=VARCHAR},
</if>
<if test="startTime != null">
#{startTime,jdbcType=TIMESTAMP},
</if>
</trim>
</insert>
</mapper>
至此关键步骤均已完成,新建控制器进行测试,测试代码如下:
package com.hznu.management.controller;
import com.alibaba.fastjson.JSON;
import com.hznu.management.annotation.SystemLog;
import com.hznu.management.enums.LogType;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
@Slf4j
@RestController
@RequestMapping("/system/log")
public class SystemLogController {
@PostMapping("/test1")
@SystemLog(description = "测试日志1", type = LogType.ADD)
public String SystemLogTest1(@RequestBody String params) {
log.info("日志测试,入参:{}", params);
return "success";
}
@PostMapping("/test2")
@SystemLog(description = "测试日志2", type = LogType.ADD)
public String SystemLogTest2(@RequestParam("params") String params) {
log.info("日志测试,入参:{}", params);
return "success";
}
@PostMapping("/test3")
@SystemLog(description = "测试日志2", type = LogType.ADD)
public String SystemLogTest3(@RequestParam("params") String params, @RequestBody String device) {
log.info("日志测试,入参:{},{}", params, JSON.toJSONString(device));
return "success";
}
}
测试结果如下:
数据成功入库,同时区分了url中的参数和body体中的参数,实现了目标。
项目地址为:
https://gitee.com/Marinc/spring-logs/tree/master/spring-aop-logs
2.2 基于拦截器Filter日志统计方案
基于拦截器Filter进行日志记录主要包含以下几个步骤:
1.创建日志拦截器
2.配置日志拦截器
3.编写数据处理配置类(关键步骤)
4.设计数据库表和DAO层
2.2.1 创建日志拦截器
package com.eckey.lab.inteceptor;
import com.alibaba.fastjson.JSON;
import com.eckey.lab.entity.Log;
import com.eckey.lab.service.LogService;
import com.eckey.lab.thread.SaveSystemLogThread;
import com.eckey.lab.utils.IpInfoUtil;
import com.eckey.lab.utils.RequestWrapper;
import com.eckey.lab.utils.SpringHelper;
import com.eckey.lab.utils.ThreadPoolUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.nio.charset.Charset;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@Component
public class LogInterceptor implements HandlerInterceptor {
private static final String SYSTEM_LOG = "SYSTEM_LOG";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object o) throws Exception {
//线程绑定变量(该数据只有当前请求的线程可见)
Date beginTime = new Date();
Log systemLog = new Log();
systemLog.setRequestUrl(request.getRequestURI());
systemLog.setStartTime(beginTime);
systemLog.setIp(IpInfoUtil.getIpAddr(request));
//请求方式
systemLog.setRequestType(request.getMethod());
HashMap<String, Object> params = new HashMap<>();
Map<String, String[]> parameterMap = request.getParameterMap();
params.put("head", parameterMap);
RequestWrapper requestWrapper;
if (request instanceof RequestWrapper) {
// 处理request请求,获取body体中参数
requestWrapper = (RequestWrapper) request;
String requestParam = getBodyString(requestWrapper);
params.put("body", requestParam);
}
//设置请求参数
systemLog.setRequestParam(JSON.toJSONString(params));
//其他属性
systemLog.setCreateBy("system");
systemLog.setDelFlag(0);
systemLog.setStartTime(beginTime);
//日志标题
systemLog.setName(request.getHeader("description"));
//日志类型
systemLog.setLogType(Integer.valueOf(request.getHeader("type")));
request.setAttribute(SYSTEM_LOG, systemLog);
return true;
}
@Override
public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object o, Exception e) throws Exception {
Date endTime = new Date();
Log systemLog = (Log) request.getAttribute(SYSTEM_LOG);
systemLog.setEndTime(endTime);
long costTime = endTime.getTime() - systemLog.getStartTime().getTime();
systemLog.setCostTime((int) costTime);
systemLog.setCreateTime(new Date());
int status = response.getStatus();
if (status == 200) {
systemLog.setStatus(0);
}else {
systemLog.setStatus(1);
}
LogService logService = (LogService) SpringHelper.getBean("logService");
ThreadPoolUtil.getPool().execute(new SaveSystemLogThread(systemLog, logService));
}
//通过request.getReader()的方式来获取body
private String getBodyString(HttpServletRequest request) throws IOException {
StringBuilder sb = new StringBuilder();
InputStream inputStream = null;
BufferedReader reader = null;
try {
inputStream = request.getInputStream();
reader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName("UTF-8")));
String line = "";
while ((line = reader.readLine()) != null) {
sb.append(line);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return sb.toString().trim();
}
}
在日志拦截器中主要实现的功能如下:
1.preHandle方法中进行日志实体的初始化,并获取一些关键属性,包括创建时间、入参等,并从请求头中获
取日志标题和日志类型;
2.afterCompletion方法中主要进行日志耗时、请求状态的设置,并进行入库操作。
2.2.2 配置日志拦截器
package com.eckey.lab.config;
import com.eckey.lab.inteceptor.LogInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
@Configuration
public class WebConfig extends WebMvcConfigurationSupport {
/**
* 注册自定义拦截器,此处为过滤所有请求
*/
@Override
protected void addInterceptors(InterceptorRegistry registry) {
super.addInterceptors(registry);
registry.addInterceptor(new LogInterceptor()).addPathPatterns("/**");
}
}
2.2.3 编写数据处理配置类(关键步骤)
此处先上代码,先编写过滤器,过滤器的作用是获取request请求中的流,将取出来的字符串保存在缓存中,同时再将该字符串再次转换成流,然后把它放入到新request对象中。代码如下:
package com.eckey.lab.filter;
import com.eckey.lab.utils.RequestWrapper;
import org.springframework.core.annotation.Order;
import javax.servlet.*;
import java.io.IOException;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
@Order(1)
@WebFilter(filterName = "httpServletRequestFilter", urlPatterns = "/*")
public class RequestReplaceFilter implements Filter {
@Override
public void destroy() {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
ServletRequest requestWrapper = null;
if (request instanceof HttpServletRequest) {
requestWrapper = new RequestWrapper((HttpServletRequest) request);
}
//获取request请求中的流,将取出来的字符串保存在缓存中,同时再将该字符串再次转换成流,然后把它放入到新request对象中。
if (requestWrapper == null) {
chain.doFilter(request, response);
} else {
chain.doFilter(requestWrapper, response);
}
}
@Override
public void init(FilterConfig arg0) throws ServletException {
}
}
再编写过滤器配置类,提前过滤进入Servlet中的HttpServletRequest,替换成RequestWrapper对象,详细代码如下:
package com.eckey.lab.config;
import com.eckey.lab.filter.RequestReplaceFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @Description: 过滤器配置类
* @Author Marin
* @Date 2021/4/30 15:56
*/
@Configuration
public class HttpServletRequestBodyConfig {
@Bean
public FilterRegistrationBean httpServletRequestReplacedRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new RequestReplaceFilter());
registration.addUrlPatterns("/*");
registration.addInitParameter("paramName", "paramValue");
registration.setName("httpServletRequestReplacedFilter");
registration.setOrder(1);
return registration;
}
}
这里解释一下为什么要进行这一步骤,由于需要获取POST或者PUT等请求body体中的参数,而这两中请求方式body体中的参数是以流的形式存在的,如果在preHandler中直接获取流信息,则Controller层无法再次进行参数的获取,会抛出异常,因而需要在preHandler先进行流信息的获取,再将取出来的字符串保存在缓存中,同时再将该字符串再次转换成流,然后把它放入到新request对象中,确保后面的controller能够再次获取参数信息。
2.2.4 设计数据库表和DAO层
数据表和DAO层设计与上述基于AOP实现的日志记录方案一致,这里不再赘述。
2.2.5 项目地址
https://gitee.com/Marinc/spring-logs/tree/master/spring-interceptor-logs
3.小结
1.基于注解的日志统计方案更加灵活,可以作用在任何添加该注解的方法上,而基于拦截器的日志记录方案,只能拦截从Http请求;
2.基于拦截器的AOP日志记录方案更具有统一性,针对所有的Web请求不需要编写额外的代码,不需要在每个要记录的入口去新增注解;
3.没有银弹,所有的方案都是针对场景的,应根据场景选择合适方案。
4.参考文献
1.https://www.jianshu.com/p/890c23a1b3d7
2.https://blog.csdn.net/liuxiao723846/article/details/81557735
5.源码地址
https://gitee.com/Marinc/spring-logs/
标签:Spring,request,systemLog,AOP,import,日志,com,public 来源: https://blog.csdn.net/qq_33479841/article/details/116306864