网站首页 > 技术文章 正文
一、步骤概览
二、步骤说明
1.引入依赖包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.22</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
2.定义注解
定义日志注解,用于明确标记哪些方法需要进行日志记录,便于识别和管理。
- Log:日志注解
@Target({ElementType.PARAMETER,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {
/** 所属模块名称 **/
String title() default "";
/** 操作类型,如 INSERT、UPDATE、SELECT、DELETE等 **/
BusinessType businessType() default BusinessType.OTHER;
/** 操作人员类别,如后台用户、手机端用户等 **/
OperatorType operatorType() default OperatorType.MANAGE;
/** 是否保存请求参数 **/
boolean isSaveReqData() default true;
/** 是否保存响应参数 **/
boolean isSaveResData() default true;
}
3.实现日志切面
日志切面完成对请求方法的拦截,主要功能包括:
- 前置通知:在方法执行前记录当前时间,用于计算操作消耗的时间。
- 后置通知:在方法执行后将请求信息传递给日志服务进行日志处理。
- 异常通知:在方法抛出异常时,将异常信息传递给日志服务进行日志处理。
@Aspect
@Component
@Slf4j
public class LogAspect {
public static final ThreadLocal<Long> TIME_THREADLOCAL = new NamedThreadLocal<Long>("Cost Time");
@Resource
private LogHandleService logHandleService;
}
①.前置通知
使用 ThreadLocal 记录方法执行前当前时间戳。
- LogAspect#doBefore
@Before(value = "@annotation(controllerLog)")
public void doBefore(JoinPoint jp, Log controllerLog) {
TIME_THREADLOCAL.set(DateUtil.current());
}
②.后置通知
在方法执行后,将请求信息委托给日志服务实现日志逻辑处理。
- LogAspect#doAfterReturning
@AfterReturning(pointcut = "@annotation(controllerLog)", returning = "jsonRes")
public void doAfterReturning(JoinPoint jp, Log controllerLog, Object jsonRes) {
handleLog(jp, controllerLog, null, jsonRes);
}
- LogAspect#handleLog:日志处理,最终调用日志接口处理具体逻辑,由于我们在前置通知时,使用 ThreadLocal 记录了请求时间戳,因此在处理完需要移除,避免内存泄漏。
protected void handleLog(final JoinPoint jp, Log controllerLog,
Exception e, Object jsonRes) {
try {
logHandleService.handle(jp,controllerLog,e,jsonRes);
} catch (Exception exp) {
log.error("异常信息:{}", exp.getMessage(),e);
} finally {
TIME_THREADLOCAL.remove();
}
}
③.异常通知
在请求发生异常后,将异常信息传递给日志服务进行日志逻辑处理。
- LogAspect#doAfterThrowing
@AfterThrowing(value = "@annotation(controllerLog)", throwing = "e")
public void doAfterThrowing(JoinPoint jp, Log controllerLog, Exception e) {
handleLog(jp, controllerLog, e, null);
}
4.实现日志记录逻辑
单独封装日志记录处理逻辑,避免切面包含太多繁杂的业务代码。日志记录处理逻辑主要负责解析获取日志信息并进行日志打印,当然我们也可以将日志信息保存至数据库中。
①. 日志信息
日志信息主要包含打印和保存的信息。
@Data
public class SysOperLog implements Serializable {
private static final long serialVersionUID = 1L;
/** 日志主键 */
private Long operId;
/** 操作模块 */
private String title;
/** 业务类型(0其它 1新增 2修改 3删除) */
private Integer businessType;
/** 业务类型数组 */
private Integer[] businessTypes;
/** 请求方法 */
private String method;
/** 请求方式 */
private String requestMethod;
/** 操作类别(0其它 1后台用户 2手机端用户) */
private Integer operatorType;
/** 操作人员 */
private String operName;
/** 部门名称 */
private String deptName;
/** 请求url */
private String operUrl;
/** 操作地址 */
private String operIp;
/** 操作地点 */
private String operLocation;
/** 请求参数 */
private String operParam;
/** 返回参数 */
private String jsonResult;
/** 操作状态(0正常 1异常) */
private Integer status;
/** 错误消息 */
private String errorMsg;
/** 操作时间 */
private Date operTime;
/** 消耗时间 */
private Long costTime;
}
②. 日志逻辑处理
日志逻辑处理,主要实现获取客户端IP、获取用户信息、获取异常信息,解析请求参数等逻辑。
- LogHandleService#handle:日志处理方法
public void handle(JoinPoint jp, Log log, Exception e, Object jsonRes) {
// 1. 获取客户端IP和访问接口地址
SysOperLog operLog = new SysOperLog();
operLog.setStatus(BusinessStatus.SUCCESS.ordinal());
operLog.setOperIp(IpUtils.getIpAddr());
operLog.setOperUrl(ServletUtils.getRequest().getRequestURI());
// 2. 根据实际情况获取用户信息
// operLog.setOperName(loginUser.getUsername());
// 3. 若是存在异常,则记录异常信息
if (ObjectUtil.isNotNull(e)) {
operLog.setStatus(BusinessStatus.FAIL.ordinal());
operLog.setErrorMsg(StrUtil.sub(e.getMessage(), 0, 2000));
}
// 4.设置方法名称
String className = jp.getTarget().getClass().getName();
String methodName = jp.getSignature().getName();
operLog.setMethod(className + "." + methodName + "()");
// 5.设置请求方式
operLog.setRequestMethod(ServletUtils.getRequest().getMethod());
// 6.处理设置注解上的参数
getControllerMethodDescription(jp, log, operLog, jsonRes);
// 6.设置消耗时间
operLog.setCostTime(DateUtil.current() - LogAspect.TIME_THREADLOCAL.get());
// 7.打印日志
logger.info(String.format("operLog : [%s]", JSONUtil.toJsonStr(operLog)));
}
- LogHandleService#getControllerMethodDescription:获取Controller 方法描述信息
public void getControllerMethodDescription(JoinPoint jp, Log log, SysOperLog operLog, Object jsonRes) {
// 设置 action 动作
operLog.setBusinessType(log.businessType().ordinal());
// 设置标题
operLog.setTitle(log.title());
// 设置操作人类别
operLog.setOperatorType(log.operatorType().ordinal());
// 是否需要保存 request,参数和值
if (log.isSaveReqData()) {
operLog.setOperParam(getReqValue(jp, operLog.getMethod()));
}
// 是否需要保存 response,参数和值
if (log.isSaveResData() && Objects.nonNull(jsonRes)) {
String trimRes = StrUtil.sub(JSONUtil.toJsonStr(jsonRes), 0, 2000);
operLog.setJsonResult(trimRes);
}
}
- LogHandleService#getReqValue:获取请求参数,将其拼接成字符串。
private String getReqValue(JoinPoint joinPoint, String reqMethodName) {
Map<?, ?> paramsMap = ServletUtils.getParamMap(ServletUtils.getRequest());
boolean isParamEmpty = MapUtil.isEmpty(paramsMap);
boolean isPutOrPost = HttpMethod.PUT.name().equals(reqMethodName) ||
HttpMethod.POST.name().equals(reqMethodName);
String params;
if (isParamEmpty && isPutOrPost) {
params = argsArrayToString(joinPoint.getArgs());
} else {
params = JSONUtil.toJsonStr(paramsMap);
}
return StrUtil.sub(params, 0, 2000);
}
- LogHandleService#argsArrayToString:数组参数转为字符串
private String argsArrayToString(Object[] paramsArray) {
String params = "";
if (ArrayUtil.isEmpty(paramsArray)) {
return params;
}
for (Object o : paramsArray) {
if (Objects.nonNull(o) && !isFilterObject(o)) {
try {
params += JSONUtil.toJsonStr(o) + " ";
} catch (Exception e) {
}
}
}
return params.trim();
}
- LogHandleService#isFilterObject:判断参数对象是否需要过滤掉,这边主要实现过滤掉 MultipartFile、HttpServletRequest、HttpServletResponse、BindingResult对象
public boolean isFilterObject(Object o) {
Class<?> clazz = o.getClass();
if (clazz.isArray()) {
// 如果是数组类型且数组元素的类型是 MultipartFile 的子类或子接口,则需要过滤
return clazz.getComponentType().isAssignableFrom(MultipartFile.class);
} else if (Collection.class.isAssignableFrom(clazz)) {
// 如果是集合类型,且存在 MultipartFile元素,如果是则返回 true
Collection collection = (Collection) o;
for (Object value : collection) {
return value instanceof MultipartFile;
}
} else if (Map.class.isAssignableFrom(clazz)) {
// 如果是Map类型,且存在MultipartFile 的元素值,则返回 true。
Map map = (Map) o;
for (Object value : map.entrySet()) {
Map.Entry entry = (Map.Entry) value;
return entry.getValue() instanceof MultipartFile;
}
}
return o instanceof MultipartFile || o instanceof HttpServletRequest
|| o instanceof HttpServletResponse
|| o instanceof BindingResult;
}
三、代码测试
1.测试代码
- UserController:测试控制层,在 addUser 方法上添加日志标记注解
@RestController
@RequestMapping("/user")
public class UserController {
@PostMapping(value = "/add")
@Log(title = "用户管理模块", businessType = BusinessType.INSERT)
public GenericResponse<UserDTO> addUser(@RequestBody UserDTO user) {
// 数据库插入操作。。。
return GenericResponse.success(user);
}
}
- UserDTO:测试入参实体
@Getter
@Setter
public class UserDTO implements Serializable {
private static final long serialVersionUID = 1L;
private String userName;
// 用户昵称
private String nickName;
// 用户邮箱
private String email;
// 手机号码
private String phonenumber;
// 用户性别
private String sex;
}
2.测试结果
- postman 请求
- 后台日志
猜你喜欢
- 2024-12-29 基于 SLF4J 的 MDC 实现日志链路追踪详解
- 2024-12-29 使用Flume同步日志到Kafka flume收集日志到hdfs
- 2024-12-29 Python日志模块logging python logger日志级别
- 2024-12-29 在Spring Boot中通过AOP技术实现日志拦截操作
- 2024-12-29 [编程基础] Python日志记录库logging总结
- 2024-12-29 如何将日志记录到 Windows事件日志 中
- 2024-12-29 SpringBoot集成logback异步日志 springboot集成日志log4j2
- 2024-12-29 Spring Boot中的Logback日志配置详解
- 2024-12-29 Linux 系统日志写入记录命令用法(logger)
- 2024-12-29 Python logging模块日志相关功能及应用示例
- 02-21走进git时代, 你该怎么玩?_gits
- 02-21GitHub是什么?它可不仅仅是云中的Git版本控制器
- 02-21Git常用操作总结_git基本用法
- 02-21为什么互联网巨头使用Git而放弃SVN?(含核心命令与原理)
- 02-21Git 高级用法,喜欢就拿去用_git基本用法
- 02-21Git常用命令和Git团队使用规范指南
- 02-21总结几个常用的Git命令的使用方法
- 02-21Git工作原理和常用指令_git原理详解
- 最近发表
- 标签列表
-
- cmd/c (57)
- c++中::是什么意思 (57)
- sqlset (59)
- ps可以打开pdf格式吗 (58)
- phprequire_once (61)
- localstorage.removeitem (74)
- routermode (59)
- vector线程安全吗 (70)
- & (66)
- java (73)
- org.redisson (64)
- log.warn (60)
- cannotinstantiatethetype (62)
- js数组插入 (83)
- resttemplateokhttp (59)
- gormwherein (64)
- linux删除一个文件夹 (65)
- mac安装java (72)
- reader.onload (61)
- outofmemoryerror是什么意思 (64)
- flask文件上传 (63)
- eacces (67)
- 查看mysql是否启动 (70)
- java是值传递还是引用传递 (58)
- 无效的列索引 (74)