当前位置: 首页 > news >正文

SpringBoot+AOP+自定义注解,优雅实现日志记录

文章目录

    • 前言
    • 准备阶段
    • 1、数据库日志表
    • 2、自定义注解编写
    • 3、AOP切面类编写
    • 4、业务层
      • 4.1、Service 层:
      • 4.2 Service 实现层:
    • 5、测试

前言

首先我们看下传统记录日志的方式是什么样的:

@DeleteMapping("/deleteUserById/{userId}")
public JSONResult deleteUserById(@PathVariable("userId") Long userId){//调用Service实现类方法做删除操作userService.deleteUserById(userId);//记录操作日志LogUtils.addLog("用户模块", "删除用户操作", "12");return JSONResult.success();
}
  1. 日志记录代码与业务代码强耦合,万一哪天需要多记录一个字段到数据库的话,所有调用的地方都需要修改
  2. 许多参数需要花费很大代价才能记录到数据库,比如:请求方法全路径、请求方式(get还是post等)、方法执行耗时、入参、出参、方法执行状态等
  3. 非常不优雅,难维护

接下来给大家分享一种非常优雅的方式记录日志,就是采用自定义注解+AOP切面编程技术,实现日志记录,现在记录日志的方式就是这样了:

@PostMapping("/save")
@MyLog(title = "用户模块", content = "新增用户信息")
public JSONResult save(@RequestBody UserDto dto){//业务逻辑代码这里,省略return JSONResult.success(dto);
}

可以看到,直接使用自定义注解@MyLog完成日志记录即可,与业务代码没有任何耦合,是不是看着非常优雅呢?

好了,废话不多说,接下来跟着下面的步骤,将这个功能集成到你的项目中的

准备阶段

1、数据库日志表

我们数据库先准备一张记录日志信息的表,建表语句如下:

CREATE TABLE `sys_oper_log` (`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '日志主键',`title` varchar(50) DEFAULT '' COMMENT '模块标题',`content` varchar(100) DEFAULT NULL COMMENT '日志内容',`method` varchar(100) DEFAULT '' COMMENT '方法名称',`request_method` varchar(10) DEFAULT '' COMMENT '请求方式',`oper_name` varchar(50) DEFAULT '' COMMENT '操作人员',`request_url` varchar(255) DEFAULT '' COMMENT '请求URL',`ip` varchar(128) DEFAULT '' COMMENT '请求IP地址',`ip_location` varchar(255) DEFAULT '' COMMENT 'IP归属地',`request_param` varchar(2000) DEFAULT '' COMMENT '请求参数',`response_result` varchar(2000) DEFAULT '' COMMENT '方法响应参数',`status` int(1) DEFAULT NULL COMMENT '操作状态(0正常 1异常)',`error_msg` varchar(2000) DEFAULT NULL COMMENT '错误消息',`oper_time` datetime DEFAULT NULL COMMENT '操作时间',`take_time` bigint(20) DEFAULT NULL COMMENT '方法执行耗时(单位:毫秒)',PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='操作日志记录';

2、自定义注解编写

好,表已经准备好了,下面是下载到本地到项目:

在这里插入图片描述

这里对项目结构就不多做介绍了,在此基础上,我们新建一个包,用来写自定义注解,代码如下:

package org.js.annotation;import java.lang.annotation.*;/*** 自定义注解记录系统操作日志*/
//Target注解决定 MyLog 注解可以加在哪些成分上,如加在类身上,或者属性身上,或者方法身上等成分
@Target({ ElementType.PARAMETER, ElementType.METHOD })
//Retention注解括号中的"RetentionPolicy.RUNTIME"意思是让 MyLog 这个注解的生命周期一直程序运行时都存在
@Retention(RetentionPolicy.RUNTIME)
public @interface MyLog
{/*** 模块标题*/String title() default "";/*** 日志内容*/String content() default "";
}

OK,到目前为止,我们就新增了一个自定义注解类,现在项目结构变成这样了:

在这里插入图片描述

3、AOP切面类编写

好,自定义注解写好后,我们开始写AOP切面类,需要先导入AOP相关依赖jar包,所以需要在pom.xml中加入下面依赖

<!-- aop切面 -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
</dependency>

然后切面类代码如下:

package org.js.aop;import com.alibaba.fastjson.JSON;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.js.annotation.MyLog;
import org.js.domain.OperLog;
import org.js.service.IOperLogService;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;/*** 切面处理类,记录操作日志到数据库*/
@Aspect
@Component
public class OperLogAspect {@Autowiredprivate IOperLogService operLogService;//为了记录方法的执行时间ThreadLocal<Long> startTime = new ThreadLocal<>();/*** 设置操作日志切入点,这里介绍两种方式:* 1、基于注解切入(也就是打了自定义注解的方法才会切入)*    @Pointcut("@annotation(org.js.annotation.MyLog)")* 2、基于包扫描切入*    @Pointcut("execution(public * org.js.controller..*.*(..))")*/@Pointcut("@annotation(org.js.annotation.MyLog)")//在注解的位置切入代码//@Pointcut("execution(public * org.js.controller..*.*(..))")//从controller切入public void operLogPoinCut() {}@Before("operLogPoinCut()")public void beforMethod(JoinPoint point){startTime.set(System.currentTimeMillis());}/*** 设置操作异常切入点记录异常日志 扫描所有controller包下操作*/@Pointcut("execution(* org.js.controller..*.*(..))")public void operExceptionLogPoinCut() {}
​
​/*** 正常返回通知,拦截用户操作日志,连接点正常执行完成后执行, 如果连接点抛出异常,则不会执行** @param joinPoint 切入点* @param result      返回结果*/@AfterReturning(value = "operLogPoinCut()", returning = "result")public void saveOperLog(JoinPoint joinPoint, Object result) {// 获取RequestAttributesRequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();// 从获取RequestAttributes中获取HttpServletRequest的信息HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);try {// 从切面织入点处通过反射机制获取织入点处的方法MethodSignature signature = (MethodSignature) joinPoint.getSignature();// 获取切入点所在的方法Method method = signature.getMethod();// 获取操作MyLog myLog = method.getAnnotation(MyLog.class);OperLog operlog = new OperLog();if (myLog != null) {operlog.setTitle(myLog.title());//设置模块名称operlog.setContent(myLog.content());//设置日志内容}// 将入参转换成jsonString params = argsArrayToString(joinPoint.getArgs());// 获取请求的类名String className = joinPoint.getTarget().getClass().getName();// 获取请求的方法名String methodName = method.getName();methodName = className + "." + methodName + "()";operlog.setMethod(methodName); //设置请求方法operlog.setRequestMethod(request.getMethod());//设置请求方式operlog.setRequestParam(params); // 请求参数operlog.setResponseResult(JSON.toJSONString(result)); // 返回结果operlog.setOperName("张三"); // 获取用户名(真实环境中,肯定有工具类获取当前登录者的账号或ID的,或者从token中解析而来)operlog.setIp(getIp(request)); // IP地址operlog.setIpLocation("湖北武汉"); // IP归属地(真是环境中可以调用第三方API根据IP地址,查询归属地)operlog.setRequestUrl(request.getRequestURI()); // 请求URIoperlog.setOperTime(new Date()); // 时间operlog.setStatus(0);//操作状态(0正常 1异常)Long takeTime = System.currentTimeMillis() - startTime.get();//记录方法执行耗时时间(单位:毫秒)operlog.setTakeTime(takeTime);//插入数据库operLogService.insert(operlog);} catch (Exception e) {e.printStackTrace();}}/*** 异常返回通知,用于拦截异常日志信息 连接点抛出异常后执行*/@AfterThrowing(pointcut = "operExceptionLogPoinCut()", throwing = "e")public void saveExceptionLog(JoinPoint joinPoint, Throwable e) {// 获取RequestAttributesRequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();// 从获取RequestAttributes中获取HttpServletRequest的信息HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);OperLog operlog = new OperLog();try {// 从切面织入点处通过反射机制获取织入点处的方法MethodSignature signature = (MethodSignature) joinPoint.getSignature();// 获取切入点所在的方法Method method = signature.getMethod();// 获取请求的类名String className = joinPoint.getTarget().getClass().getName();// 获取请求的方法名String methodName = method.getName();methodName = className + "." + methodName + "()";// 获取操作MyLog myLog = method.getAnnotation(MyLog.class);if (myLog != null) {operlog.setTitle(myLog.title());//设置模块名称operlog.setContent(myLog.content());//设置日志内容}// 将入参转换成jsonString params = argsArrayToString(joinPoint.getArgs());operlog.setMethod(methodName); //设置请求方法operlog.setRequestMethod(request.getMethod());//设置请求方式operlog.setRequestParam(params); // 请求参数operlog.setOperName("张三"); // 获取用户名(真实环境中,肯定有工具类获取当前登录者的账号或ID的,或者从token中解析而来)operlog.setIp(getIp(request)); // IP地址operlog.setIpLocation("湖北武汉"); // IP归属地(真是环境中可以调用第三方API根据IP地址,查询归属地)operlog.setRequestUrl(request.getRequestURI()); // 请求URIoperlog.setOperTime(new Date()); // 时间operlog.setStatus(1);//操作状态(0正常 1异常)operlog.setErrorMsg(stackTraceToString(e.getClass().getName(), e.getMessage(), e.getStackTrace()));//记录异常信息//插入数据库operLogService.insert(operlog);} catch (Exception e2) {e2.printStackTrace();}}/*** 转换异常信息为字符串*/public String stackTraceToString(String exceptionName, String exceptionMessage, StackTraceElement[] elements) {StringBuffer strbuff = new StringBuffer();for (StackTraceElement stet : elements) {strbuff.append(stet + "\n");}String message = exceptionName + ":" + exceptionMessage + "\n\t" + strbuff.toString();message = substring(message,0 ,2000);return message;}/*** 参数拼装*/private String argsArrayToString(Object[] paramsArray){String params = "";if (paramsArray != null && paramsArray.length > 0){for (Object o : paramsArray){if (o != null){try{Object jsonObj = JSON.toJSON(o);params += jsonObj.toString() + " ";}catch (Exception e){e.printStackTrace();}}}}return params.trim();}//字符串截取public static String substring(String str, int start, int end) {if (str == null) {return null;} else {if (end < 0) {end += str.length();}if (start < 0) {start += str.length();}if (end > str.length()) {end = str.length();}if (start > end) {return "";} else {if (start < 0) {start = 0;}if (end < 0) {end = 0;}return str.substring(start, end);}}}/*** 转换request 请求参数* @param paramMap request获取的参数数组*/public Map<String, String> converMap(Map<String, String[]> paramMap) {Map<String, String> returnMap = new HashMap<>();for (String key : paramMap.keySet()) {returnMap.put(key, paramMap.get(key)[0]);}return returnMap;}//根据HttpServletRequest获取访问者的IP地址public static String getIp(HttpServletRequest request) {String ip = request.getHeader("x-forwarded-for");if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {ip = request.getHeader("Proxy-Client-IP");}if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {ip = request.getHeader("WL-Proxy-Client-IP");}if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {ip = request.getHeader("HTTP_CLIENT_IP");}if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {ip = request.getHeader("HTTP_X_FORWARDED_FOR");}if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {ip = request.getRemoteAddr();}return ip;}
}

代码里面的逻辑我就不赘述了,里面的注释写的非常全,大家应该看得懂,不懂的评论区留言即可

现在项目结构如下:

在这里插入图片描述

4、业务层

4.1、Service 层:

package cn.js.service;import cn.js.domain.SysOperLog;
import cn.js.query.SysOperLogQuery;
import com.baomidou.mybatisplus.extension.service.IService;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;/*** <p>* 操作日志记录 服务类* </p>** @author js* @date 2023-11-02*/
public interface SysOperLogService extends IService<SysOperLog> {IPage<SysOperLog> selectMyPage(SysOperLogQuery query);Page<SysOperLog> selectMySqlPage(SysOperLogQuery query);}

4.2 Service 实现层:

package cn.js.service.impl;import cn.hutool.core.util.StrUtil;
import cn.js.Mapper.SysOperLogMapper;
import cn.js.domain.SysOperLog;
import cn.js.query.SysOperLogQuery;
import cn.js.service.SysOperLogService;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.beans.factory.annotation.Autowired;/*** <p>* 操作日志记录 服务实现类* </p>** @author js* @date 2023-11-02*/
@Transactional
@Service
@Slf4j
public class SysOperLogServiceImpl extends ServiceImpl<SysOperLogMapper, SysOperLog> implements SysOperLogService {@Autowiredprivate SysOperLogMapper sysOperLogMapper;//查询分页列表数据public IPage<SysOperLog> selectMyPage(SysOperLogQuery query) {QueryWrapper<SysOperLog> wrapper = new QueryWrapper<>();if (StrUtil.isNotEmpty(query.getKeyword())) {//下面条件根据实际情况修改wrapper.and(i -> i.like("user_name", query.getKeyword()).or().like("login_name", query.getKeyword()));}//排序(默认根据主键ID降序排序,根据实际情况修改)wrapper.orderByDesc("id");Page<SysOperLog> page = new Page<>(query.getCurrent(), query.getSize());return super.page(page, wrapper);}//查询分页列表数据(自己写SQL)public Page<SysOperLog> selectMySqlPage(SysOperLogQuery query) {Page<SysOperLog> page = new Page<>(query.getCurrent(), query.getSize());List<SysOperLog> list = sysOperLogMapper.selectMySqlPage(page, query);return page.setRecords(list);}
}

5、测试

接下来我们就可以测试了,在Controller接口中直接用自定义注解开始记录日志,如下方法使用:

@GetMapping("/deleteUserById/{userId}")
@MyLog(title = "用户模块", content = "删除用户操作")
public JSONResult deleteUserById(@PathVariable("userId") Long userId){//这里具体删除用户代码 省略.....return JSONResult.success();
}

然后启动项目,浏览器输入地址:http://localhost:8001/deleteUserById/123

显示结果如下:

在这里插入图片描述

说明接口调用成功,看下数据库是否记录了日志:

在这里插入图片描述

数据库已经新增了一条日志记录,而且里面记录到信息非常全

OK,至此,我们以后项目中再记录日志就非常方便了,只需要在方法上面打一个注解就可以了,在AOP里负责往数据库写,方便日后维护

完整代码

相关文章:

  • web:[CISCN2019 华北赛区 Day2 Web1]Hack World
  • NUUO网络摄像头(NVR)RCE漏洞复现
  • CentOS 7 通过 yum 安装 MariaDB(Mysql)
  • 【入门Flink】- 03Flink部署
  • nodelist 与 HTMLCollection 的区别
  • 2024 Android Framework学习大纲之基础理论篇
  • Nginx服务器安装证书并启用SSL(acme.sh)
  • MySQL 表的增删查改(CRUD)
  • 基于级联延迟信号消除的锁相环(CDSC_PLL)技术MATLAB仿真
  • mediasoup udp端口分配策略
  • [笔记] 汉字判断
  • 学电脑编程零基础,计算机编程入门先学什么
  • Android startActivity流程
  • ruby语言怎么写个通用爬虫程序?
  • gcc -static 在centos stream8 和centos stream9中运行报错的解决办法
  • 【162天】黑马程序员27天视频学习笔记【Day02-上】
  • centos安装java运行环境jdk+tomcat
  • CSS相对定位
  • Django 博客开发教程 16 - 统计文章阅读量
  • HTTP 简介
  • js如何打印object对象
  • MySQL常见的两种存储引擎:MyISAM与InnoDB的爱恨情仇
  • Redash本地开发环境搭建
  • seaborn 安装成功 + ImportError: DLL load failed: 找不到指定的模块 问题解决
  • spring boot下thymeleaf全局静态变量配置
  • supervisor 永不挂掉的进程 安装以及使用
  • webpack入门学习手记(二)
  • 爬虫进阶 -- 神级程序员:让你的爬虫就像人类的用户行为!
  • 算法系列——算法入门之递归分而治之思想的实现
  • 用Visual Studio开发以太坊智能合约
  • 《码出高效》学习笔记与书中错误记录
  • Linux权限管理(week1_day5)--技术流ken
  • 如何正确理解,内页权重高于首页?
  • # 数论-逆元
  • ### Error querying database. Cause: com.mysql.jdbc.exceptions.jdbc4.CommunicationsException
  • #includecmath
  • #pragma once
  • $ git push -u origin master 推送到远程库出错
  • (1)(1.11) SiK Radio v2(一)
  • (10)STL算法之搜索(二) 二分查找
  • (13):Silverlight 2 数据与通信之WebRequest
  • (3)(3.5) 遥测无线电区域条例
  • (C语言)输入一个序列,判断是否为奇偶交叉数
  • (done) ROC曲线 和 AUC值 分别是什么?
  • (解决办法)ASP.NET导出Excel,打开时提示“您尝试打开文件'XXX.xls'的格式与文件扩展名指定文件不一致
  • (免费领源码)python+django+mysql线上兼职平台系统83320-计算机毕业设计项目选题推荐
  • (算法)前K大的和
  • (五)c52学习之旅-静态数码管
  • (转) ns2/nam与nam实现相关的文件
  • (转)IIS6 ASP 0251超过响应缓冲区限制错误的解决方法
  • (转)Scala的“=”符号简介
  • (转)程序员技术练级攻略
  • .Net Web窗口页属性
  • .net安装_还在用第三方安装.NET?Win10自带.NET3.5安装
  • .php结尾的域名,【php】php正则截取url中域名后的内容