作者:田力
1. 创建数据表
- 操作日志表
CREATE TABLE `aop_operate_log` (
`operate_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '主键ID',
`operate_module` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '功能模块',
`operate_type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '操作类型',
`operate_desc` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '操作描述',
`operate_method_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '请求方法名',
`operate_request_mode` varchar(255) DEFAULT NULL COMMENT '请求方式',
`operate_request_param` json DEFAULT NULL COMMENT '请求参数',
`operate_response_param` json DEFAULT NULL COMMENT '响应参数',
`operate_perf_stats` int DEFAULT NULL COMMENT '性能统计',
`operate_user_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '操作人ID',
`operate_user_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '操作人名称',
`operate_uri` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '请求URI',
`operate_ip` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '请求IP',
`operate_system` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '操作系统名',
`operate_time` datetime DEFAULT NULL COMMENT '操作时间',
PRIMARY KEY (`operate_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AOP操作日志表';
- 异常日志表
CREATE TABLE `aop_except_log` (
`except_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '主键ID',
`operate_module` varchar(255) DEFAULT NULL COMMENT '功能模块',
`operate_type` varchar(255) DEFAULT NULL COMMENT '操作类型',
`operate_desc` varchar(255) DEFAULT NULL COMMENT '操作描述',
`except_method_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '请求方法名',
`except_request_mode` varchar(255) DEFAULT NULL COMMENT '请求方式',
`except_request_param` json DEFAULT NULL COMMENT '请求参数',
`except_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '异常名称',
`except_message` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci COMMENT '异常信息',
`operate_user_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '操作人ID',
`operate_user_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '操作人名称',
`operate_uri` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '操作URI',
`operate_ip` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '请求IP',
`operate_system` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '操作系统',
`except_time` datetime DEFAULT NULL COMMENT '报错时间',
PRIMARY KEY (`except_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AOP异常日志表';
2. 引入pom依赖
<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>
<!-- Hutool:Java工具类库 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.15</version>
</dependency>
3. 创建自定义注解类
package com.zhigong.heavenearth.aop;
import java.lang.annotation.*;
/**
* @author 田治功
* @create 2022-04-21 10:49
* @Description 自定义注解
*/
@Target(ElementType.METHOD) //注解放置的目标位置,METHOD即方法级别
@Retention(RetentionPolicy.RUNTIME) //注解执行政策,运行时执行
@Documented //标记这些注解是否包含在用户文档中
public @interface LogAnnotation {
/**
* 操作模块注解
*
* @return
*/
String OperateModule() default "";
/**
* 操作类型注解
*
* @return
*/
String OperateType() default "";
/**
* 操作说明注解
*
* @return
*/
String OperateDesc() default "";
}
4. 创建日志切面类
package com.zhigong.heavenearth.aop;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.zhigong.heavenearth.dto.user.LoginDetailsDTO;
import com.zhigong.heavenearth.dto.user.UserAccountDTO;
import com.zhigong.heavenearth.mapper.AopExceptLogMapper;
import com.zhigong.heavenearth.mapper.AopOperateLogMapper;
import com.zhigong.heavenearth.pojo.aop.AopExceptLog;
import com.zhigong.heavenearth.pojo.aop.AopOperateLog;
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.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* @author 田治功
* @create 2022-04-21 11:40
* @Description 日志切面处理类
*/
@Aspect
@Component
public class LogAspect {
//定义独立线程副本,为每一个线程提供独立的时间
ThreadLocal<Long> startTime = new ThreadLocal<>();
@Value("${spring.application.name}")
private String operateSystem;
@Autowired
private AopOperateLogMapper aopOperateLogMapper;
@Autowired
private AopExceptLogMapper aopExceptLogMapper;
@Autowired
RedisTemplate redisTemplate;
//请求头
@Value("${token.tokenHeader}")
private String tokenHeader;
//请求令牌中特定的字符序列
@Value("${token.tokenHead}")
private String tokenHead;
//redis路由键
@Value("${token.redis_token_key}")
private String redisTokenKey;
/**
* 设置操作日志切点,在自定义注解位置切入
*/
@Pointcut("@annotation(com.zhigong.heavenearth.aop.LogAnnotation)")
public void operationLogPointcut() {
}
/**
* 设置异常日志切点,在扫描所有controller包下切入
*/
@Pointcut("execution(* com.zhigong.heavenearth.controller..*.*(..))")
public void exceptionLogPointcut() {
}
/**
* 前置通知:此处切面在进入切点之前进行操作
* <p>
* 此前置为性能统计设置:在进入切点之时初始化当前线程的时间,从而在切面处理后进行性能运算统计入库
* 使用前置通知计算性能则会将切面处理的时间加赋在Controller切点下的时间上
*/
@Before(value = "execution(* com.zhigong.heavenearth.controller.*.*(..))")
public void initializationStartTime() {
startTime.set(System.currentTimeMillis());
}
/**
* 后置通知:操作日志切面在切点正常处理后进行
*
* @param joinPoint 连接点静态信息反射代理
* @param response 切面方法返回值
*/
@AfterReturning(pointcut = "operationLogPointcut()", returning = "response")
public void saveOperationLog(JoinPoint joinPoint, Object response) {
//0.连接到切点的时间, 单位毫秒
long endTime = System.currentTimeMillis();
long durationTime = endTime - startTime.get();
//1.获取当前请求线程的请求属性
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
//2.从请求属性获取request请求参数
HttpServletRequest httpServletRequest = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);
//3.创建操作日志写入对象,逐步设置值
AopOperateLog aopOperateLog = new AopOperateLog();
try {
aopOperateLog.setOperateId(IdUtil.simpleUUID());
//3.1.从切面织入点处通过反射获取织入点的方法属性
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
//3.2.获取织入点所在的方法属性
Method signatureMethod = signature.getMethod();
//3.3.获取织入点方法上的自定义注解的内容
LogAnnotation annotation = signatureMethod.getAnnotation(LogAnnotation.class);
if (ObjectUtil.isNotEmpty(annotation)) {
aopOperateLog.setOperateModule(annotation.OperateModule());
aopOperateLog.setOperateType(annotation.OperateType());
aopOperateLog.setOperateDesc(annotation.OperateDesc());
}
//3.4.拼接类名和方法名,组成存入库中的方法名,携带类名便于定位和同名方法干扰
String className = joinPoint.getTarget().getClass().getName();
String methodName = signatureMethod.getName();
aopOperateLog.setOperateMethodName(className + "." + methodName);
//3.5.获取请求方式如GET或者POST
String methodType = httpServletRequest.getMethod();
aopOperateLog.setOperateRequestMode(httpServletRequest.getMethod());
//3.6.获取请求参数,并进行格式转换,而后JSON序列化处理
String requestParamJson;
if (methodType.equals("GET")) {
requestParamJson = JSONUtil.toJsonStr(convertMap(httpServletRequest.getParameterMap()));
} else if (methodType.equals("POST")) {
requestParamJson = StrUtil.sub(JSONUtil.toJsonStr(joinPoint.getArgs()), 1, -1);
} else {
requestParamJson = StrUtil.sub(JSONUtil.toJsonStr(joinPoint.getArgs()), 1, -1);
}
aopOperateLog.setOperateRequestParam(requestParamJson);
//3.7.响应参数JSON序列化处理
String responseParamJson = JSONUtil.toJsonStr(response);
aopOperateLog.setOperateResponseParam(responseParamJson);
aopOperateLog.setOperatePerfStats(new Long(durationTime).intValue());
//3.8.获取操作人信息,设置账户和名称
UserAccountDTO userAccountDTO = parsingToken(httpServletRequest);
aopOperateLog.setOperateUserId(userAccountDTO.getAccount());
aopOperateLog.setOperateUserName(userAccountDTO.getAccountNickname());
aopOperateLog.setOperateUri(httpServletRequest.getRequestURI());
aopOperateLog.setOperateIp(httpServletRequest.getRemoteAddr());
aopOperateLog.setOperateSystem(operateSystem);
aopOperateLog.setOperateTime(new Date());
aopOperateLogMapper.insert(aopOperateLog);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 异常通知:异常日志切面在切点发生异常时进行
*
* @param joinPoint 连接点静态信息反射代理
* @param throwable 异常
*/
@AfterThrowing(pointcut = "exceptionLogPointcut()", throwing = "throwable")
public void saveExceptionLog(JoinPoint joinPoint, Throwable throwable) {
//1.获取当前请求线程的请求属性
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
//2.从请求属性获取request请求参数
HttpServletRequest httpServletRequest = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);
//3.创建操作日志写入对象,逐步设置值
AopExceptLog aopExceptLog = new AopExceptLog();
try {
aopExceptLog.setExceptId(IdUtil.simpleUUID());
//3.1.从切面织入点处通过反射获取织入点的方法属性
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
//3.2.获取织入点所在的方法属性
Method signatureMethod = signature.getMethod();
//3.3.获取织入点方法上的自定义注解的内容
LogAnnotation annotation = signatureMethod.getAnnotation(LogAnnotation.class);
if (ObjectUtil.isNotEmpty(annotation)) {
aopExceptLog.setOperateModule(annotation.OperateModule());
aopExceptLog.setOperateType(annotation.OperateType());
aopExceptLog.setOperateDesc(annotation.OperateDesc());
}
//3.4.拼接类名和方法名,组成存入库中的方法名,携带类名便于定位和同名方法干扰
String className = joinPoint.getTarget().getClass().getName();
String methodName = signatureMethod.getName();
aopExceptLog.setExceptMethodName(className + "." + methodName);
//3.5.获取请求方式如GET或者POST
String methodType = httpServletRequest.getMethod();
aopExceptLog.setExceptRequestMode(methodType);
//3.6.获取请求参数,并进行格式转换,而后JSON序列化处理
String requestParamJson;
if (methodType.equals("GET")) {
requestParamJson = JSONUtil.toJsonStr(convertMap(httpServletRequest.getParameterMap()));
} else if (methodType.equals("POST")) {
requestParamJson = StrUtil.sub(JSONUtil.toJsonStr(joinPoint.getArgs()), 1, -1);
} else {
requestParamJson = StrUtil.sub(JSONUtil.toJsonStr(joinPoint.getArgs()), 1, -1);
}
aopExceptLog.setExceptRequestParam(requestParamJson);
//3.7获取错误异常名称和信息
aopExceptLog.setExceptName(throwable.getClass().getName());
String messages = exceptionMessageToStr(throwable.getClass().getName(), throwable.getMessage(), throwable.getStackTrace());
aopExceptLog.setExceptMessage(messages);
//3.8获取操作人信息,设置账户和名称
UserAccountDTO userAccountDTO = parsingToken(httpServletRequest);
aopExceptLog.setOperateUserId(userAccountDTO.getAccount());
aopExceptLog.setOperateUserName(userAccountDTO.getAccountNickname());
aopExceptLog.setOperateUri(httpServletRequest.getRequestURI());
aopExceptLog.setOperateIp(httpServletRequest.getRemoteAddr());
aopExceptLog.setOperateSystem(operateSystem);
aopExceptLog.setExceptTime(new Date());
aopExceptLogMapper.insert(aopExceptLog);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 请求HTTP参数转换
*
* @param parameterMap
* @return
*/
private HashMap<String, String> convertMap(Map<String, String[]> parameterMap) {
HashMap<String, String> returnParameterMap = new HashMap<>();
for (String key : parameterMap.keySet()) {
returnParameterMap.put(key, parameterMap.get(key)[0]);
}
return returnParameterMap;
}
/**
* 根据HTTP令牌获取操作人信息
*
* @param request
* @return
*/
private UserAccountDTO parsingToken(HttpServletRequest request) {
UserAccountDTO userAccountDTO = new UserAccountDTO();
//1.从请求头部获取授权令牌
final String authHead = request.getHeader(this.tokenHeader);
//2.验证授权令牌是否为空,不为空再验证传入授权令牌字符串中手头存在指定的前缀(特定的字符序列)
if (authHead != null && authHead.startsWith(tokenHead)) {
//2.0 删除请求的授权令牌中的特定字符序列
final String authToken = authHead.substring(tokenHead.length());
//2.1 判断redis中是否存在相应的token信息
if (redisTemplate.hasKey(redisTokenKey + ":" + authToken)) {
//2.1.2 从redis中取出相应的用户信息详情
ValueOperations<String, LoginDetailsDTO> valueOperations = redisTemplate.opsForValue();
LoginDetailsDTO loginDetailsDTO = valueOperations.get(redisTokenKey + ":" + authToken);
userAccountDTO = loginDetailsDTO.getUserAccountDTO();
}
}
return userAccountDTO;
}
/**
* 异常信息转字符串处理
*
* @param exceptionName 异常名称
* @param exceptionMessage 异常信息
* @param stackTraceElements 堆栈数据
* @return
*/
public String exceptionMessageToStr(String exceptionName, String exceptionMessage, StackTraceElement[] stackTraceElements) {
StringBuffer stringBuffer = new StringBuffer();
for (StackTraceElement stackTraceElement : stackTraceElements) {
stringBuffer.append(stackTraceElement + "\n");
}
String messages = exceptionMessage + ":" + exceptionName + "\n" + stringBuffer;
return messages;
}
}
5. 扫描级别处增设自定义注解
/**
* 登录服务
*
* @param loginParamDTO
* @return
*/
@ApiOperation("登录")
@PostMapping("login")
@LogAnnotation(OperateModule = "登录", OperateType = "登录", OperateDesc = "系统登录")
public Result login(@RequestBody LoginParamDTO loginParamDTO) {
LoginDetailsDTO loginDetailsDTO = securityUserDetailService.loadUserByUsername(loginParamDTO.getUserName());
return Result.success(loginDetailsDTO);
}
--------------------------------------------------------------------------------------------------------------------
@GetMapping("verification")
@ApiOperation("验证码")
@LogAnnotation(OperateModule = "登录", OperateType = "验证", OperateDesc = "验证码生成")
public Result<Map<String, String>> getVerification() {
return Result.success(null);
}
6. 效果预览
- 操作日志
- 异常日志
注:日志入库之后可根据需求进行查询等业务管理操作,此处不在赘述