SpringAOP 实现记录日志到数据库
前言:
记录使用SpringAOP记录日志到mysql数据库,
本次主要是新建一个demo记录,思路和重要代码书写流程;
完整代码地址:在文末
具体建表语句在下面会有板书.
1 pom
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>show.mrkay</groupId>
<artifactId>bootAopDaily</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<parent>
<artifactId>spring-boot-parent</artifactId>
<groupId>org.springframework.boot</groupId>
<version>2.2.0.RELEASE</version>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!--springboot-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<!--用于记录日志-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!--数据库&持久层-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.0</version>
</dependency>
<!--热部署-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
<!--fastJson-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.67</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2 application.yml
server:
port: 80
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/daily?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT
username: root
password: lk0313
mybatis:
mapper-locations: classpath*:show/mrkay/mapper/*.xml
3 整合Mybatis和druid连接池
过程省略: 导入坐标在yml文集中配置之后,并在启动类上开启@MapperScan注解即可,
此步骤主要是为了操作数据库,将数据写入到数据库中,至于pojo,xml,和mapper接口的生成可以参考,之前文章
3.1 建表语句
执行如下语句即可创建数据库和相关表
/*
SQLyog Enterprise v12.09 (64 bit)
MySQL - 5.5.40 : Database - daily
*********************************************************************
*/
/*!40101 SET NAMES utf8 */;
/*!40101 SET SQL_MODE=''*/;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
CREATE DATABASE /*!32312 IF NOT EXISTS*/`daily` /*!40100 DEFAULT CHARACTER SET utf8 */;
/*Table structure for table `exc_daily` */
DROP TABLE IF EXISTS `exc_daily`;
CREATE TABLE `exc_daily` (
`exc_id` varchar(64) NOT NULL COMMENT '主键ID',
`exc_req_param` text COMMENT '请求参数',
`exc_name` varchar(256) DEFAULT NULL COMMENT '异常名',
`exc_mes` text COMMENT '异常信息',
`operate_user_id` varchar(64) DEFAULT NULL COMMENT '操作者ID',
`operate_user_name` varchar(256) DEFAULT NULL COMMENT '操作者名称',
`operate_method` varchar(256) DEFAULT NULL COMMENT '操作方法',
`operate_uri` varchar(256) DEFAULT NULL COMMENT '请求URI',
`operate_ip` varchar(64) DEFAULT NULL COMMENT '请求IP',
`operate_time` datetime DEFAULT NULL COMMENT '操作时间',
`operate_ver` varchar(64) DEFAULT NULL COMMENT '操作版本'
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*Data for the table `exc_daily` */
/*Table structure for table `operate_daily` */
DROP TABLE IF EXISTS `operate_daily`;
CREATE TABLE `operate_daily` (
`operate_id` varchar(64) NOT NULL COMMENT '主键ID',
`operate_modul` varchar(64) DEFAULT NULL COMMENT '操作模块',
`operate_type` varchar(64) DEFAULT NULL COMMENT '操作类型',
`operate_desc` varchar(600) DEFAULT NULL COMMENT '操作详情',
`operate_req_param` text COMMENT '请求参数',
`operate_res_param` text COMMENT '响应参数',
`operate_user_id` varchar(64) DEFAULT NULL COMMENT '操作者ID',
`operate_user_name` varchar(64) DEFAULT NULL COMMENT '操作者名字',
`operate_method` varchar(255) DEFAULT NULL COMMENT '操作方法',
`operate_uri` varchar(255) DEFAULT NULL COMMENT '请求URI',
`operate_ip` varchar(64) DEFAULT NULL COMMENT '请求IP',
`operate_time` datetime DEFAULT NULL COMMENT '操作时间',
`operate_ver` varchar(125) DEFAULT NULL COMMENT '版本号',
PRIMARY KEY (`operate_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*Data for the table `operate_daily` */
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
4 创建日志注解
package show.mrkay.annotations;
import show.mrkay.common.constants.logconstants.LogConstants;
import java.lang.annotation.*;
/**
* @ClassName: OperateLog
* @description: AOP操作日志注解
* @Author: MrKay
* @Date: 2020/11/14
*/
@Target(ElementType.METHOD)//注解作用范围(这里使用方法级别)
@Retention(RetentionPolicy.RUNTIME)//注解在方法运行阶段执行
@Documented//指明修饰的注解,可以被例如javadoc此类的工具文档化,只负责标记,没有成员取值
public @interface OperateLog {
/**
*操作模块
*/
public String operateModule() default "";
/**
*操作类型
*/
public String operateType() default "";
/**
*操作描述
*/
public String operateDesc() default "";
/**
* 请求风格(默认为普通请求风格)
*/
public String requestStyle() default LogConstants.REQUEST_RESTFUL_COMMON_STYLE;
}
5 创建切面处理具体日志内容
package show.mrkay.aspect;
import com.alibaba.fastjson.JSON;
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.aspectj.lang.annotation.Pointcut;
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.springframework.web.servlet.HandlerMapping;
import show.mrkay.annotations.OperateLog;
import show.mrkay.common.constants.logconstants.LogConstants;
import show.mrkay.pojo.ExcDailyWithBLOBs;
import show.mrkay.pojo.OperateDailyWithBLOBs;
import show.mrkay.service.ExcDailyService;
import show.mrkay.service.OperateDailyService;
import show.mrkay.utils.maputils.MapUtils;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* @ClassName: OperateLogsAspect
* @description: 日志操作切面
* @Author: MrKay
* @Date: 2020/11/14
*/
@Aspect
@Component
public class OperateLogsAspect {
@Autowired
private OperateDailyService operateDailyService;
@Autowired
private ExcDailyService excDailyService;
/**
* 操作版本号:
* 启东时从命令行自动传入 java - jar xxx-1.0.jar
*/
// @Value("${version}")
// private String operateVer;
/**
* 设置操作日志切入点 记录操作日志 (在注解位置会切入代码)
*/
@Pointcut("@annotation(show.mrkay.annotations.OperateLog)")
public void operateLogPointCut() {
}
/**
* 设置操作异常切入点记录异常日志 扫描所有controller包下操作
*/
@Pointcut("execution(* show.mrkay.controller..*.*(..))")
public void operateExceptionLogPointCut() {
}
/**
* @MethodName: saveOperateLog
* @Params: joinPoint 切入点
* @Params: keys 返回结果
* @return: void
* @Description: 记录普通操作日志, 此方法需要在调用方法执行完毕后执行
* @Author: MrKay
* @Date: 2020/11/14
*/
@AfterReturning(value = "operateLogPointCut()", returning = "keys")
public void saveOperateLog(JoinPoint joinPoint, Object keys) {
//获取request
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
HttpServletRequest reuqest = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);
//创建操作日志对象
OperateDailyWithBLOBs operateDaily = new OperateDailyWithBLOBs();
try {
String uuid = UUID.randomUUID().toString().replaceAll("-", "");
//设置pojo参数
operateDaily.setOperateId(uuid);
// 从切面织入点处通过反射机制获取织入点处的方法
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
//获取切入点的方法
Method method = methodSignature.getMethod();
OperateLog operateLog = method.getAnnotation(OperateLog.class);
//获取操作
if (operateLog != null) {
String operateModule = operateLog.operateModule();
String operateDesc = operateLog.operateDesc();
String operateType = operateLog.operateType();
//设置pojo参数
operateDaily.setOperateDesc(operateDesc);
operateDaily.setOperateType(operateType);
operateDaily.setOperateModul(operateModule);
}
//获取请求的类名
String className = joinPoint.getClass().getName();
//获取方法名
String methodName = method.getName();
methodName = className + "." + methodName;
operateDaily.setOperateMethod(methodName);
//获取请求参数
String requestStyle = operateLog.requestStyle();
if (requestStyle.equals(LogConstants.REQUEST_RESTFUL_STYLE)) {
Map attribute = (Map) reuqest.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
operateDaily.setOperateReqParam(JSON.toJSONString(attribute));
} else if (requestStyle.equals(LogConstants.REQUEST_COMMON_STYLE)) {
Map<String, String[]> parameterMap = reuqest.getParameterMap();
Map<String, String> map = MapUtils.converMap(parameterMap);
operateDaily.setOperateReqParam(JSON.toJSONString(map));
} else {
Map<String, String> resultMap = new HashMap<String, String>();
Map attribute = (Map) reuqest.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
String restfulParam = JSON.toJSONString(attribute);
resultMap.put(LogConstants.REQUEST_RESTFUL_STYLE, restfulParam);
Map<String, String[]> parameterMap = reuqest.getParameterMap();
Map<String, String> map = MapUtils.converMap(parameterMap);
String kvParam = JSON.toJSONString(map);
resultMap.put(LogConstants.REQUEST_COMMON_STYLE, kvParam);
operateDaily.setOperateReqParam(JSON.toJSONString(resultMap));
}
//TODO 获取用户ID如果使用了Shior等安全框架,可以使用其他方式比如工具类进行获取用户ID,这里演示直接写死
operateDaily.setOperateUserId("test123456");
//TODO 设置用户名称,直接演示直接写死(同上)
operateDaily.setOperateUserName("testName");
//TODO 获取请求IP (演示直接写死,实际可以使用工具类进行获取)
operateDaily.setOperateIp("http://127.0.0.1:80");
//设置请求URI
operateDaily.setOperateUri(reuqest.getRequestURI());
// 设置创建时间
operateDaily.setOperateTime(new Date());
//设置版本 (开发环境可以直接注释)
// operateDaily.setOperateVer(operateVer);
operateDailyService.insert(operateDaily);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* @MethodName: saveExceptionDaily
* @Params: [joinPoint, throwable]
* @return: void
* @Description: 记录异常日志到数据库 第一个参数切入点,第二个参数异常信息,异常抛出后执行
* @Author: MrKay
* @Date: 2020/11/15
*/
@AfterThrowing(pointcut = "operateExceptionLogPointCut()", throwing = "throwable")
public void saveExceptionDaily(JoinPoint joinPoint, Throwable throwable) {
//获取request
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
HttpServletRequest reuqest = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);
//创建异常日志对象
ExcDailyWithBLOBs excDaily = new ExcDailyWithBLOBs();
try {
//从切入点反射获取切入点的Method
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
excDaily.setExcId(UUID.randomUUID().toString().replaceAll("-", ""));
//设置请求的方法
String className = joinPoint.getClass().getName();
String methodName = method.getName();
methodName = className + "." + methodName;
excDaily.setOperateMethod(methodName);
OperateLog operateLog = method.getAnnotation(OperateLog.class);
//获取请求参数
String requestStyle = operateLog.requestStyle();
if (requestStyle.equals(LogConstants.REQUEST_RESTFUL_STYLE)) {
Map attribute = (Map) reuqest.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
excDaily.setExcReqParam(JSON.toJSONString(attribute));
} else if (requestStyle.equals(LogConstants.REQUEST_COMMON_STYLE)) {
Map<String, String[]> parameterMap = reuqest.getParameterMap();
Map<String, String> map = MapUtils.converMap(parameterMap);
excDaily.setExcReqParam(JSON.toJSONString(map));
} else {
Map<String, String> resultMap = new HashMap<String, String>();
Map attribute = (Map) reuqest.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
String restfulParam = JSON.toJSONString(attribute);
resultMap.put(LogConstants.REQUEST_RESTFUL_STYLE, restfulParam);
Map<String, String[]> parameterMap = reuqest.getParameterMap();
Map<String, String> map = MapUtils.converMap(parameterMap);
String kvParam = JSON.toJSONString(map);
resultMap.put(LogConstants.REQUEST_COMMON_STYLE, kvParam);
excDaily.setExcReqParam(JSON.toJSONString(resultMap));
}
//设置异常名称
excDaily.setExcName(throwable.getClass().getName());
//设置异常信息
excDaily.setExcMes(stackTraceToString(throwable.getClass().getName(), throwable.getMessage(), throwable.getStackTrace()));
//TODO 获取用户ID如果使用了Shior等安全框架,可以使用其他方式比如工具类进行获取用户ID,这里演示直接写死
excDaily.setOperateUserId("test654321");
//TODO 设置用户名称,直接演示直接写死(同上)
excDaily.setOperateUserName("testName");
//TODO 设置操作IP
excDaily.setOperateIp("http://localhost:80");
//设置操作URI
excDaily.setOperateUri(reuqest.getRequestURI());
//设置操作时间
excDaily.setOperateTime(new Date());
//设置版本号
// excDaily.setOperateVer(operateVer);
excDailyService.insert(excDaily);
} catch (Exception e) {
e.printStackTrace();
}
}
public String stackTraceToString(String exceptionName, String exceptionMessage, StackTraceElement[] elements) {
StringBuffer sb = new StringBuffer();
for (StackTraceElement element : elements) {
sb.append(element + "\n");
}
String message = exceptionName + ":" + exceptionMessage + "\n\t" + sb.toString();
return message;
}
}
注意事项:
使用request.getParameterMap();方法获取请求参数的时候有一个问题就是只能获取到键值对形式的请求参数,如果使用restful风格进行请求就会获取不到参数,所以这里想要获取到restful风格url传递的参数可以使用:
Map attribute = (Map)reuqest.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
6 测试Controller
package show.mrkay.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import show.mrkay.annotations.OperateLog;
import show.mrkay.common.constants.logconstants.OperateTypeContains;
import show.mrkay.service.ExcDailyService;
import show.mrkay.service.OperateDailyService;
import java.util.HashMap;
import java.util.Map;
/**
* @ClassName: TestController
* @description: 测试Controller
* @Author: MrKay
* @Date: 2020/11/15
*/
@RestController
@RequestMapping("/test")
public class TestController {
@Autowired
private OperateDailyService operateDailyService;
@Autowired
private ExcDailyService excDailyService;
@RequestMapping("/operate/{num}")
@ResponseBody
@OperateLog(operateModule = "OPERATE", operateType = OperateTypeContains.OPERATE_INSERT, operateDesc = "测试普通操作日志")
public Map<String, String> testOperateDaily(@PathVariable("num") int num, String username) {
Map<String, String> resultMap = new HashMap<String, String>();
resultMap.put("test", num + "");
return resultMap;
}
@RequestMapping("/exception/{num}")
@ResponseBody
@OperateLog(operateModule = "OPERATE", operateType = OperateTypeContains.OPERATE_INSERT, operateDesc = "测试普通操作日志")
public Map<String, String> testExceptionDaily(@PathVariable("num") int num, String username) {
Map<String, String> resultMap = new HashMap<String, String>();
int number = 1/0;
resultMap.put("test", num + "");
return resultMap;
}
}
7 日志操作Controller
package show.mrkay.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import show.mrkay.annotations.OperateLog;
import show.mrkay.common.constants.logconstants.OperateTypeContains;
import show.mrkay.pojo.ExcDailyWithBLOBs;
import show.mrkay.pojo.OperateDailyWithBLOBs;
import show.mrkay.service.ExcDailyService;
import show.mrkay.service.OperateDailyService;
import java.util.List;
/**
* @ClassName: LogController
* @description: 日志操作Controller
* @Author: MrKay
* @Date: 2020/11/15
*/
@RestController
@RequestMapping("/logs")
public class LogController {
@Autowired
private ExcDailyService excDailyService;
@Autowired
private OperateDailyService operateDailyService;
@ResponseBody
@RequestMapping("/operate/all")
@OperateLog(operateModule = "LOG", operateType = OperateTypeContains.OPERATE_SELECT, operateDesc = "查询所有记录日志")
public List<OperateDailyWithBLOBs> selectAll() {
List<OperateDailyWithBLOBs> resultList = operateDailyService.selectAll();
return resultList;
}
@ResponseBody
@RequestMapping("/exception/all")
@OperateLog(operateModule = "LOG", operateType = OperateTypeContains.OPERATE_SELECT, operateDesc = "查询所有异常日志")
public List<ExcDailyWithBLOBs> excAll() {
List<ExcDailyWithBLOBs> resultList = excDailyService.selectAll();
return resultList;
}
}
8 测试:
启动之后访问:相对应地址,查询数据库看数据是否正确保存;
9 其他说明
cidemo还可以进一步优化:
1 @OperateLog注解的operateModule 不应写死,可以创建常量类进行封装;
2 返回值,controller返回值没有错统一规范,可以定义通用Response实体来完成数据的返回:
可以参考我之前文章:
Springboot 搭建后台接口
10 开发工具及完整代码
IDEA2020.1
sqlyog
maven