一、需求
1.1 问题
后台一些涉及到新增、编辑、删除等敏感操作的需要记录下操作日志,包含操作人、操作内容、请求参数等等信息。
1.2 思路
- 统一对Controller层的方法进行拦截,记录下请求信息
- 通过自定义注解,定义操作内容
- 通过Spring AOP集成AspectJ来实现,利用AspectJ里的相关注解可以方便的配置切面、定义切点
二、实现
2.1 环境准备
Maven引入 spring-boot-starter-aop
stater ,这里会自动引入spring-aop
和 aspectjweaver
两个jar包:
- spring-aop:基于代理的AOP支持
- aspectjweaver:AspectJ,一个AOP框架,扩展了Java语言。通过AspectJ注解,简称@AspectJ,可以快速配置切面,定义切点。由于Spring支持方法级的切点,所以仅对@AspectJ提供了有限的支持,也就是只使用了AspectJ的一部分功能。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2.2 定义注解
定义个方法级OptLog注解,该注解注释的方法表示要记录操作日志。
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OptLogger {
/**
* 描述信息
*/
String value() default "";
}
2.3 使用@AspectJ配置切面、定义切点
主要用到了@Aspect、@Pointcut、@Before这三个注解:
- @Aspect:定义切面
- @Pointcut:定义切点,借助@annotation(com.xxx.OptLogger)扫描注释了指定注解的方法
- @Before:前置通知,在方法执行前执行,类似还有@After、@Around等等
实现思路:
- 通过@Pointcut("@annotation(com.xxx.OptLogger)")定义切点,表示注释了OptLogger注解的方法
- 通过前置通知@Before("")拦截
- 获取请求相关信息
- 假设请求参数中有个token可以获取到用户信息,记录下用户名(不管是什么机制,只要能获取到用户信息即可)
- 获取请求ip
- 通过切点获取拦截方法上的OptLogger注解的描述信息
- 通过切点获取拦截方法的参数
- 线程池异步记录操作信息
@Aspect
@Component
public class OptLogAspect {
private static final Logger LOGGER = LoggerFactory.getLogger(OptLogAspect.class);
@Autowired
private UserService userService;
@Autowired
private OptLogService optLogService;
@Resource(name = "taskExecutor")
private ThreadPoolTaskExecutor taskExecutor;
/**
* 定义切点,表示注释了OptLogger注解的目标类方法
*/
@Pointcut("@annotation(com.momo.optlog.web.optlog.annotation.OptLogger)")
public void optLogAspect() {
}
/**
* 前置通知 用于拦截有OptLogger注解注释的方法
*
* @param joinPoint 连接点
* @return
*/
@Before("optLogAspect()")
public void before(JoinPoint joinPoint) {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
try {
String token = request.getParameter("token");
if (StringUtils.isBlank(token)) {
return;
}
User user = userService.getByToken(token);
if (user == null) {
return;
}
String ip = IpUtil.getRemoteHost(request);
String opt = getOptLogMethodDesc(joinPoint);
String args = JsonUtil.obj2json(getMethodArgs(joinPoint));
this.recordOptLog(ip, user.getUserName(), opt, args);
} catch (Exception e) {
LOGGER.error("record admin opt fail", e);
}
}
private void recordOptLog(String ip, String userName, String opt, String args) {
taskExecutor.submit(() -> {
OptLogAddDTO optLogDTO = OptLogAddDTO.builder()
.userName(userName)
.opt(opt)
.ip(ip)
.args(args)
.build();
if (!optLogService.add(optLogDTO)) {
LOGGER.error("save opt log fail, params={}", JsonUtil.obj2json(optLogDTO));
}
});
}
/**
* 获取OptLogger注解的描述信息
*
* @param joinPoint 切点
* @return 方法描述
* @throws NoSuchMethodException
*/
public static String getOptLogMethodDesc(JoinPoint joinPoint) throws NoSuchMethodException {
// 获取拦截的方法
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
String methodName = methodSignature.getName();
Class[] parameterTypes = methodSignature.getMethod().getParameterTypes();
Method method = methodSignature.getDeclaringType().getMethod(methodName, parameterTypes);
// 方法上的注解
boolean isPresent = method.isAnnotationPresent(OptLogger.class);
if (isPresent) {
OptLogger optLogger = method.getAnnotation(OptLogger.class);
return optLogger.value();
}
return "";
}
/**
* 获取方法参数
*
* @param joinPoint
* @return
*/
public static List<Object> getMethodArgs(JoinPoint joinPoint) {
Object[] args = joinPoint.getArgs();
if (args == null || args.length == 0) {
return new ArrayList<>(0);
}
return Arrays.stream(args)
.filter(arg ->
!(arg instanceof Model)
&& !(arg instanceof ModelMap)
&& !(arg instanceof BeanPropertyBindingResult)
&& !(arg instanceof MultipartFile))
.collect(Collectors.toList());
}
}
2.4 记录操作日志
以UserController为例,在需要记录日志的接口上添加注解@OptLogger(“描述信息”)。
- 例如新增用户接口,在执行addUser()方法前,会先执行OptLogAspect#before()方法,并记录操作人、ip、操作内容(新增用户)、请求参数等信息。其他接口都类似。
@RestController
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/user")
public CommonResponse list() {
List<User> userList = userService.getUesrList();
return CommonResponse.okResult(userList);
}
@PostMapping("/user")
@OptLogger("新增用户")
public CommonResponse addUser(@RequestBody UserAddReqVO req) {
boolean isSuccess = userService.add(req);
return CommonResponse.okResult(isSuccess);
}
@PutMapping("/user")
@OptLogger("编辑用户")
public CommonResponse updateUser(@RequestBody UserUpdateReqVO req) {
boolean isSuccess = userService.update(req);
return CommonResponse.okResult(isSuccess);
}
@DeleteMapping("/user/{id}")
@OptLogger("删除用户")
public CommonResponse deleteUser(@PathVariable long id) {
boolean isSuccess = userService.delete(id);
return CommonResponse.okResult(isSuccess);
}
@DeleteMapping("/user/{id}")
@OptLogger("删除用户")
public CommonResponse deleteUser(@PathVariable long id) {
boolean isSuccess = userService.delete(id);
return CommonResponse.okResult(isSuccess);
}
}
2.5 总结
实现的核心逻辑基本上都列出来了,实现的比较粗糙,仅供参考。
项目源码:https://github.com/mytt-10566/springboot-optlog