文章目录
- 前言
- 准备阶段
- 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();
}
- 日志记录代码与业务代码强耦合,万一哪天需要多记录一个字段到数据库的话,所有调用的地方都需要修改
- 许多参数需要花费很大代价才能记录到数据库,比如:请求方法全路径、请求方式(get还是post等)、方法执行耗时、入参、出参、方法执行状态等
- 非常不优雅,难维护
接下来给大家分享一种非常优雅的方式记录日志,就是采用自定义注解+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 {
@Autowired
private 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) {
// 获取RequestAttributes
RequestAttributes 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());//设置日志内容
}
// 将入参转换成json
String 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()); // 请求URI
operlog.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) {
// 获取RequestAttributes
RequestAttributes 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());//设置日志内容
}
// 将入参转换成json
String 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()); // 请求URI
operlog.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 {
@Autowired
private 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里负责往数据库写,方便日后维护
完整代码