我们在日常工作中经常会遇到需要实现操作日志、登录日志、错误日志等等的功能,刚进公司的小伙伴们就不知道要怎么做了,又或者第一个想法是在业务代码里每个方法里写一个往日志表中添加的方法,但是这样的话就太过于麻烦了。
我们在学习Spring的时候就知道,Spring两大核心功能是IOC和AOP,IOC这里就不过多赘述了,我们在学习AOP的时候就学到了AOP的作用是面向切面编程,也就是把与和核心业务不相关的功能一起编织为一个切面,然后把这个切面织入到核心代码中。
而操作日志、错误日志、还有登录日志就可以用AOP来实现,然后我们去自定义一个注解,在需要织入切面的方法上添加此注解,在每次执行的时候都会进入到切面去执行切面里的方法,具体运用如下。
首先我们要先新建三张表,第一张是操作日志表
CREATE TABLE `operation_log` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键id',
`oper_module` varchar(64) DEFAULT NULL COMMENT '功能模块',
`oper_type` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '操作类型',
`oper_desc` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '操作描述',
`oper_requ_param` text CHARACTER SET utf8 COLLATE utf8_general_ci COMMENT '请求参数',
`oper_resp_param` text CHARACTER SET utf8 COLLATE utf8_general_ci COMMENT '返回参数',
`oper_user_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '操作员id',
`oper_user_name` varchar(64) DEFAULT NULL COMMENT '操作员姓名',
`oper_method` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '请求方法',
`oper_uri` varchar(255) DEFAULT NULL COMMENT '请求URI',
`oper_ip` varchar(64) DEFAULT NULL COMMENT '操作ip',
`oper_create_time` datetime DEFAULT NULL COMMENT '操作时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb3;
然后再建个异常日志表
CREATE TABLE `error_log` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键id',
`err_requ_param` text COMMENT '请求参数',
`err_name` varchar(255) DEFAULT NULL COMMENT '异常名称',
`err_message` text COMMENT '异常信息',
`oper_user_id` varchar(64) DEFAULT NULL COMMENT '操作员id',
`oper_user_name` varchar(64) DEFAULT NULL COMMENT '操作员名称',
`oper_method` varchar(255) DEFAULT NULL COMMENT '操作方法',
`oper_uri` varchar(255) DEFAULT NULL COMMENT '请求URI',
`oper_ip` varchar(64) DEFAULT NULL COMMENT '请求IP',
`oper_create_time` datetime DEFAULT NULL COMMENT '操作时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb3;
最后我们再建个用户表,用来做简单的业务操作
CREATE TABLE `user` (
`id` bigint NOT NULL AUTO_INCREMENT,
`username` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`dept_name` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8mb3;
我们在这里把所有的字段都设置为非空是为了模拟后面的错误日志记录。
首先我们要先引入aop的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
我们先用MybatisPlus的代码生成器生成我们需要的架构
然后去新建一个annotation包,在里面新建个Annotation注解类
package com.logtest.annotation;
import java.lang.annotation.*;
/**
* @author az
* @description TODO 自定义记录日志的注解
* @date 2021/12/3 0003
*/
/**
* 此注解表示注解扫描的范围,一般取值如下,这里我们范围设到注解即可
* 1.CONSTRUCTOR:用于描述构造器
* 2.FIELD:用于描述域
* 3.LOCAL_VARIABLE:用于描述局部变量
* 4.METHOD:用于描述方法
* 5.PACKAGE:用于描述包
* 6.PARAMETER:用于描述参数
* 7.TYPE:用于描述类、接口(包括注解类型) 或enum声明
* @author Administrator
*/
@Target(ElementType.METHOD)
/**
* 此注解表示注解在哪个阶段执行,RUNTIME表示在运行时执行
* 1、RetentionPolicy.SOURCE:注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃;
* 如果只是做一些检查性的操作,比如 @Override 和 @SuppressWarnings,选择用RESOURCE注解
* 2、RetentionPolicy.CLASS:注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期;
* 一般用在需要在编译时进行一些预处理操作,比如生成一些辅助代码(如 ButterKnife)
* 3、RetentionPolicy.RUNTIME:注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在;
* 一般用在需要在运行时去动态获取注解信息的场景下
*/
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {
/**
* 操作模块
*/
String operModule() default "";
/**
* 操作类型
*/
String operType() default "";
/**
* 操作说明
*/
String operDesc() default "";
}
接下来我们再新建个aop的包,在里面新建我们处理切面的类
import com.alibaba.fastjson.JSON;
import com.logtest.annotation.Log;
import com.logtest.entity.ErrorLog;
import com.logtest.entity.OperationLog;
import com.logtest.service.ErrorLogService;
import com.logtest.service.OperationLogService;
import com.logtest.util.IpUtils;
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.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* @author az
* @description TODO 切面的处理类,用来处理操作日志和异常日志
* @date 2021/12/3 0003
*/
@Component
/**
* 加上此注解,Spring在扫描到此类的时候会将此类作为AOP容器进行处理
*/
@Aspect
public class OperationLogAspect {
@Resource
private OperationLogService operationLogService;
@Resource
private ErrorLogService errorLogService;
/**
* 设置操作日志的切入点,用来记录操作日志,在标明注解的位置切入
*/
@Pointcut("@annotation(com.logtest.annotation.Log)")
public void operationLogPointCut() {
}
/**
* 设置操作异常切入点记录异常日志 扫描所有controller包下操作
*/
@Pointcut("execution(* com.logtest.controller..*.*(..))")
public void operErrorLogPointCut() {
}
/**
* 设置操作异常切入点,拦截用户的操作日志,连接点正常执行后执行,若连接点抛出异常则不会执行
* @param joinPoint 切入点
* @param keys 返回结果
*/
@AfterReturning(value = "operationLogPointCut()", returning = "keys")
public void saveOperationLog(JoinPoint joinPoint, Object keys) {
//获取RequestAttributes
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
//从获取到的RequestAttributes中获取HttpServletRequest的信息
HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);
OperationLog operationLog = new OperationLog();
try {
//在切面织入点通过反射机制获取织入点的方法
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
//获取织入点的方法
Method method = signature.getMethod();
//获取操作
Log log = method.getAnnotation(Log.class);
if (log != null) {
String operModule = log.operModule();
String operDesc = log.operDesc();
String operType = log.operType();
operationLog.setOperModule(operModule);
operationLog.setOperDesc(operDesc);
operationLog.setOperType(operType);
}
//获取请求的类名
String className = joinPoint.getTarget().getClass().getName();
//获取请求的方法
String methodName = method.getName();
methodName = className + "." + methodName;
//请求方法
operationLog.setOperMethod(methodName);
Map<String, String> rtnMap = converMap(request.getParameterMap());
//将参数所在的数组转为json
String params = JSON.toJSONString(rtnMap);
//请求参数
operationLog.setOperRequParam(params);
//返回结果
operationLog.setOperRespParam(JSON.toJSONString(keys));
//操作员ip地址
operationLog.setOperIp(IpUtils.getIpAddr(request));
//请求URI
operationLog.setOperUri(request.getRequestURI());
//创建时间(操作时间)
operationLog.setOperCreateTime(new Date());
operationLog.setOperUserId("1");
operationLog.setOperUserName("测试");
operationLogService.save(operationLog);
}catch (Exception e){
e.printStackTrace();
}
}
/**
* 异常返回通知,用于拦截异常日志的信息,连接点抛出异常后执行
* @param joinPoint 切入点
* @param e 异常信息
*/
@AfterThrowing(pointcut = "operErrorLogPointCut()", throwing = "e")
public void saveErrorLog(JoinPoint joinPoint,Throwable e){
//获取RequestAttributes
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
//从RequestAttributes中获取HttpServletRequest的信息
HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);
ErrorLog errorLog = new ErrorLog();
try {
//在切面织入点通过反射机制获取织入点的方法
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
//获取织入点的方法
Method method = signature.getMethod();
//获取操作
Log log = method.getAnnotation(Log.class);
//获取请求的类名
String className = joinPoint.getTarget().getClass().getName();
//获取请求的方法
String methodName = method.getName();
methodName = className + "." + methodName;
//请求的参数
Map<String, String> rtnMap = converMap(request.getParameterMap());
String params = JSON.toJSONString(rtnMap);
//请求参数
errorLog.setErrRequParam(params);
//请求方法名
errorLog.setOperMethod(methodName);
//异常名称
errorLog.setErrName(e.getClass().getName());
//异常信息
errorLog.setErrMessage(stackTraceToString(e.getClass().getName(),e.getMessage(),e.getStackTrace()));
//请求URI
errorLog.setOperUri(request.getRequestURI());
//操作员ip地址
errorLog.setOperIp(IpUtils.getIpAddr(request));
//发生异常的时间
errorLog.setOperCreateTime(new Date());
errorLog.setOperUserId("1");
errorLog.setOperUserName("测试");
errorLogService.save(errorLog);
}catch (Exception exception){
exception.printStackTrace();
}
}
/**
* 转换request请求参数
* @param paramMap request中获取的参数数组
* @return 转换后的数组
*/
public Map<String, String> converMap(Map<String,String[]> paramMap){
Map<String, String> rtnMap = new HashMap<>();
for (String key : paramMap.keySet()) {
rtnMap.put(key,paramMap.get(key)[0]);
}
return rtnMap;
}
public String stackTraceToString(String exceptionName,String exceptionMessage,StackTraceElement[] elements){
StringBuilder stringBuilder = new StringBuilder();
for (StackTraceElement element : elements) {
stringBuilder.append(element).append("\n");
}
String message = exceptionName + ":" + exceptionMessage + "\n\t" + stringBuilder.toString();
return message;
}
}
这个类写完之后我们的切面就已经写完了,然后我们再去新建个查询操作人的IP的工具类和一个来保存我们的操作类型的枚举类
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import java.net.InetAddress;
import java.net.UnknownHostException;
/**
* @author az
* @description TODO 获取操作人IP的工具类
* @date 2021/12/3 0003
*/
public class IpUtils {
/**
* 获取当前网络ip
* @param request
* @return
*/
public static String getIpAddr(HttpServletRequest request){
String ipAddress = request.getHeader("x-forwarded-for");
if(ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("Proxy-Client-IP");
}
if(ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("WL-Proxy-Client-IP");
}
if(ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getRemoteAddr();
if("127.0.0.1".equals(ipAddress) || "0:0:0:0:0:0:0:1".equals(ipAddress)){
//根据网卡取本机配置的IP
InetAddress inet=null;
try {
inet = InetAddress.getLocalHost();
} catch (UnknownHostException e) {
e.printStackTrace();
}
ipAddress= inet.getHostAddress();
}
}
//对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
if(ipAddress!=null && ipAddress.length()>15){ //"***.***.***.***".length() = 15
if(ipAddress.indexOf(",")>0){
ipAddress = ipAddress.substring(0,ipAddress.indexOf(","));
}
}
return ipAddress;
}
}
/**
* @author az
* @description TODO 操作类型枚举类
* @date 2021/12/3 0003
*/
public interface OperationLogType {
String QUERY = "查询";
String ADD = "新增";
String MODIFY = "修改";
String WITHDRAW = "撤回";
String DELETE = "删除";
}
接下来我们去编写一些简单的业务来测试我们的日志是否能够成功记录
package com.logtest.controller;
import com.logtest.annotation.Log;
import com.logtest.entity.User;
import com.logtest.service.UserService;
import com.logtest.util.OperationLogType;
import lombok.Data;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Arrays;
import java.util.List;
/**
* @author az
* @since 2021-12-03
*/
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/queryAllUser")
@Log(operModule = "用户管理-查询用户列表",operType = OperationLogType.QUERY,operDesc = "查询所有用户")
public Object queryAllUser(){
return userService.list();
}
@PostMapping("/addUser")
@Log(operModule = "用户管理-新增用户",operType = OperationLogType.ADD,operDesc = "新增用户的功能")
public String addUser(@RequestBody User user){
boolean save = userService.save(user);
if (save) {
return "新增成功";
}else {
return "新增失败";
}
}
@PostMapping("/modifyUser")
@Log(operModule = "用户管理-修改",operType = OperationLogType.MODIFY,operDesc = "根据id修改用户详细信息")
public String modifyUser(@RequestBody User user){
boolean update = userService.updateById(user);
if (update) {
return "修改成功";
}else {
return "修改失败";
}
}
@DeleteMapping("/deleteUser")
@Log(operModule = "用户管理-删除",operType = OperationLogType.DELETE,operDesc = "根据id删除用户")
public String deleteUser(@RequestParam Long id){
boolean delete = userService.removeById(id);
if (delete) {
return "删除成功";
}else {
return "删除失败";
}
}
}
接下来我们跑起来运行一下试试能不能行,在这里我用Postman来做测试
首先我们依次测试一下我们写的四个方法,测完后我们去看一下数据库中的操作日志表
成功写入操作日志,因为我们在设计user表的时候为了方便测试异常日志,我把所有的参数都设为非空了,也就是每个字段都要传,那么我们接下来故意不传其中一个参数试一试
Postman报错了,那么接下来我们去看一看我们的异常日志