简介:之前写了《aop实现日志持久化记录》一文,主要介绍自定义aop标注方法上,通过切面方法对用户操作插入mysql。思路正确但是实际操作上存在一些小问题,本文将从项目出发,对细节进行补充。
另外值得一提的是,因为是基于AOP对controller方法做环绕通知实现的日志持久化记录,所以如果请求在Filter或者Interceptor中被拦截,则不会进入环绕通知,也就无法记录日志
数据库表结构大致如下
建表语句(基于MySQL 5.7)
CREATE TABLE `admin_log` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '日志id',
`ip` VARCHAR(20) DEFAULT NULL COMMENT '操作ip',
`uri` VARCHAR(100) DEFAULT NULL COMMENT '请求URI',
`method_type` VARCHAR(10) DEFAULT NULL COMMENT '请求类型(GET,POST)',
`method_name` VARCHAR(100) DEFAULT NULL COMMENT '目标方法名',
`method_desc` VARCHAR(20) DEFAULT NULL COMMENT '接口介绍',
`request_param` TEXT COMMENT '请求参数',
`status` VARCHAR(20) DEFAULT NULL COMMENT '请求状态',
`result` TEXT COMMENT '返回结果',
`user_id` VARCHAR(20) DEFAULT NULL COMMENT '操作者id',
`execution_time` BIGINT(20) DEFAULT NULL COMMENT '方法耗时(ms)',
`create_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间',
PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4
<!-- 其他依赖在此不列出,springboot,mybatis等等 …… -->
<!-- aop依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
@Data
public class AdminLog {
private Integer id;
private String userId; // 用户id
private String ip; // 操作者ip
private String uri; // 请求URI
private String methodType; // 请求类型【GET,POST】
private String methodName; // 方法名称
private String methodDesc; // 接口简介
private String requestParam; // post请求参数
private String status; // 方法执行最终状态
private String result; // 返回结果
private Long executionTime; // 方法耗时
private String createTime; // 执行时间
}
自定义注解,标注在要保存用户操作日志的controller方法上,被标注的方法会通过下面写的环绕通知进行日志记录
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface WriteLog {
/**
* 方法描述(描述目标方法的作用)
*/
String value();
}
从上面数据库表可以看出,日志类需要的ip、uri、methodType等参数需要在请求的request参数中获取,为记录这些参数信息,通过ThreadLocal设置上下文对象,方便获取。
先创建需要的请求信息的实体类
@Data
public class RequestBaseInfo {
private String ip;
private String methodType; // 请求类型,如【GET,POST,PUT,DELETE】
private String uri;
public RequestBaseInfo(){}
public RequestBaseInfo(String ip, String methodType, String uri){
this.ip = ip;
this.methodType = methodType;
this.uri = uri;
}
}
然后创建上下文对象,保存该类的对象
public class RequestContextHolder {
private static final ThreadLocal<RequestBaseInfo> ipThreadLocal = new ThreadLocal<>();
public static void setRequestBaseInfo(RequestBaseInfo requestBaseInfo) {
ipThreadLocal.set(requestBaseInfo);
}
public static RequestBaseInfo getRequestBaseInfo() {
return ipThreadLocal.get();
}
public static void clear() {
ipThreadLocal.remove();
}
}
创建拦截器,为每个线程保存上下文对象
/**
* 该拦截器主要为线程保存请求的基本信息,例如来源ip,请求uri,请求方法等
*/
@Slf4j
@Component
public class SaveRequestBaseInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 将请求基本信息存到ThreadLocal,[ip,method,uri]
RequestBaseInfo rbi = new RequestBaseInfo(request.getRemoteAddr(),request.getMethod(),request.getRequestURI());
RequestContextHolder.setRequestBaseInfo(rbi);
// 另外自己的框架里是否可以获取用户已登录的ip信息,没有的话这里还可以多设置一个获取登录用户的id
// 因为我们的数据库日志表中有userId字段,如果没办法在后续的aop切面方法中获取,亦可以在这里拦截器中获取
// 例如我项目中的spring security可以在aop切面中获取登录主体,拦截器就不需要获取了
// 具体思路就是设置多一个上下文对象或者在RequestBaseInfo中设置多一个userId字段
// 然后在这里获取请求token,然后在缓存中获取登录信息,获取登录者id
// 当然实现的方式有多种,根据实际项目配置,或者不记录userId也可以
// ……todo
return true;
}
}
将拦截器注册生效(配置进WebMvcConfigurer)
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
SaveRequestBaseInterceptor saveRequestBaseInterceptor;
/**
* 添加自定义拦截器
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(saveRequestBaseInterceptor).addPathPatterns("/**");
}
}
在创建切面方法前还需要创建AdminLog表dao操作的相关代码,例如具体插入的service文件,mapper文件,这里就省略不说了,很基础的东西
aop实现针对以上自定义注解@WriteLog标注切面的环绕通知
@Aspect
@Component
public class WriteLogAspect {
@Autowired
AdminLogService adminLogService;
// com.jankin.inoteadmin.system.annotation.WriteLog 是我定义接口的文件路径
@Pointcut("@annotation(com.jankin.inoteadmin.system.annotation.WriteLog)")
public void writeLogAspect() {}
/**
* 返回通知切面方法
* @param joinPoint 切点,就是被注解的目标方法
*/
@Around("writeLogAspect()")
public Object logPostMapping(ProceedingJoinPoint joinPoint) throws Throwable {
String userId = null; // 获取操作用户Id
// //我这里用的是SpringSecurity框架,这样获取UserId
// Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// if (authentication!=null){
// // SecurityUser是自定义的UserDetails类,其中包含了UserId
// SecurityUser principal = (SecurityUser)authentication.getPrincipal();
// userId = principal.getUserId();
// }
String status = "ERROR";
String resultStr = "";
Object result = null;
long startTime = System.currentTimeMillis(); // 执行前时间
try {
result = joinPoint.proceed();
} catch (Throwable e) {
resultStr = e.getMessage();
throw e;
}finally {
long finishTime = System.currentTimeMillis(); // 目标方法执行后时间
if (result instanceof Result) {
resultStr = result.toString(); // 返回结果字符串
status = ((Result) result).getCode()==200? "SUCCESS":"EXCEPTION";
}
// 其他to do ……
AdminLog sysLog = new AdminLog();
sysLog.setUserId(userId);
RequestBaseInfo rbi = RequestContextHolder.getRequestBaseInfo();
sysLog.setIp(rbi.getIp());
sysLog.setUri(rbi.getUri());
sysLog.setMethodType(rbi.getMethodType());
sysLog.setMethodName(joinPoint.getSignature().toShortString());
// 获取注解上的方法描述
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
WriteLog annotation = signature.getMethod().getAnnotation(WriteLog.class);
sysLog.setMethodDesc(annotation.value());
sysLog.setRequestParam(Arrays.toString(joinPoint.getArgs()));
sysLog.setStatus(status);
sysLog.setResult(resultStr);
sysLog.setExecutionTime(finishTime-startTime);
adminLogService.addLog(sysLog);
}
return result;
}
}
在接口(controller方法)上标注自定义注解(@WriteLog),即可完成接口日志的插入
@WriteLog("测试接口Get")
@GetMapping
public Result get(){
return Result.success("测试成功");
}
@WriteLog("测试接口Post")
@PostMapping("post")
public Result post(TestDto testDto){
return Result.success("测试成功");
}
测试结果
至此,全篇结束