在实际项目上,我们需要将用户的操作日志进行记录以便寻源,防止系统遭到不明操作导致无迹可循,所以我们需要使用一些手段进行日志的记录并存入缓存或者数据库,实现手段有:
- 每个操作单元都记录日志,这样做的好处是可以实现每个日志都清楚的记录,但是这样的弊端就是我每个业务单元都需要进行日志记录,导致代码可读性差。
- 使用切面进行日志记录,这样的好处是只需要实现我们的切面然后通过切点进行日志获取并记录,弊端是代码的侵入性比较大我们需要实现注解并创建日志切面实现。
常用切面注解有哪些:
- @Pointcut:定义一个切点,标识哪些方法或者注解需要切入
- @Before:前置通知,执行方法前执行切面
- @After:后置通知,执行方法后执行切面
- @AfterReturning:返回通知,执行方法返回后执行切面,异常时不执行
- @AfterThrowing:异常通知,执行方法异常时执行切面
- @Around:环绕通知,执行方法前/方法后执行切面,方法调用需要自行控制
本次我们就以@Around(环绕通知)实现日志切面
一、导入所需依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId></dependency>
二、实现切面注解
package com.easyadmin.pro.common.annotation.log;import java.lang.annotation.*;/** * packageName com.easyadmin.pro.common.annotation.log * * @author 骑着蚂蚁去上天 * @version JDK 17 * @className Log * @date 2024/9/3 * @description 日志注解 */@Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD})@Retention(RetentionPolicy.RUNTIME)@Documented@Inheritedpublic @interface Log { /** * 方法名称 * @return */ String methodName() default "";}
三、实现切面
@Slf4j@Aspect@Order(3)@Component@RequiredArgsConstructor(onConstructor = @__(@Autowired))public class LogAspect {}
注解解析:
-
@Slf4j:lombok 的日志插件
-
@Aspect:标识此为一个切面
-
@Order(3):设置执行顺序
-
@Component:将此类注册到Spring 容器
-
@RequiredArgsConstructor(onConstructor = @__(@Autowired)):是 Lombok 提供的一个注解,用于自动生成一个包含所有 final 字段和带有 @NonNull 注解字段的构造函数
四、配置切点
@Pointcut("@annotation(com.easyadmin.pro.common.annotation.log.Log)")public void pointcut() { }
五、实现环绕通知
@Around("pointcut()")public Object interceptor(ProceedingJoinPoint joinPoint) { try { MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); Method method = methodSignature.getMethod(); Log logAnnotation = method.getAnnotation(Log.class); request.getMethod(); if (Objects.nonNull(logAnnotation)) { // 方法名称 String methodName = logAnnotation.methodName(); // 请求类型 String reqType = request.getMethod(); // 请求参数 String reqParams = getParamValueString(method.getParameters(), joinPoint.getArgs()); // 获取IP地址 String ipAddr = IpUtils.getIpAddr(request); // 实现日志处理业务逻辑 } // 继续执行方法 return joinPoint.proceed(); } catch (Throwable e) { throw new BusinessException(HttpCodeEnum.SYSTEM_ERROR); }}
六、全部代码
package com.easyadmin.pro.common.aspect.log;import com.easyadmin.pro.common.annotation.log.Log;import com.easyadmin.pro.common.enums.HttpCodeEnum;import com.easyadmin.pro.common.exception.BusinessException;import com.easyadmin.pro.tool.IpUtils;import jakarta.servlet.http.HttpServletRequest;import lombok.RequiredArgsConstructor;import lombok.extern.slf4j.Slf4j;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.Around;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.core.annotation.Order;import org.springframework.stereotype.Component;import java.io.Serializable;import java.lang.reflect.Field;import java.lang.reflect.Method;import java.lang.reflect.Parameter;import java.util.Objects;/** * packageName com.easyadmin.pro.common.aspect.log * * @author 骑着蚂蚁去上天 * @version JDK 17 * @className LogAspect * @date 2024/9/3 * @description 日志切面 */@Slf4j@Aspect@Order(3)@Component@RequiredArgsConstructor(onConstructor = @__(@Autowired))public class LogAspect { private final HttpServletRequest request; private final Class<?>[] primitiveType = {int.class, long.class, short.class, byte.class, float.class, double.class, char.class, boolean.class, Serializable.class, String.class}; @Pointcut("@annotation(com.easyadmin.pro.common.annotation.log.Log)") public void pointcut() { } @Around("pointcut()") public Object interceptor(ProceedingJoinPoint joinPoint) { try { MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); Method method = methodSignature.getMethod(); Log logAnnotation = method.getAnnotation(Log.class); request.getMethod(); if (Objects.nonNull(logAnnotation)) { // 方法名称 String methodName = logAnnotation.methodName(); // 请求类型 String reqType = request.getMethod(); // 请求参数 String reqParams = getParamValueString(method.getParameters(), joinPoint.getArgs()); // 获取IP地址 String ipAddr = IpUtils.getIpAddr(request); // 实现日志处理业务逻辑 } // 继续执行方法 return joinPoint.proceed(); } catch (Throwable e) { throw new BusinessException(HttpCodeEnum.SYSTEM_ERROR); } } /** * 获取参数信息 * @param parameters * @param args * @return */ private String getParamValueString(Parameter[] parameters, Object[] args) throws IllegalAccessException { StringBuilder sb = new StringBuilder("{"); int num = 0; for (Parameter parameter : parameters) { Class<?> type = parameter.getType(); Field[] fields = type.getDeclaredFields(); Object obj = args[num]; if (isNotPrimitiveType(obj)) { if (Objects.nonNull(obj)) { for (Field field : fields) { field.setAccessible(Boolean.TRUE); String fieldName = field.getName(); Object value = field.get(obj); if (sb.length() > 1) { sb.append(",").append(fieldName).append(":").append(value); } else { sb.append(fieldName).append(":").append(value); } } } } else { if (sb.length() > 1) { sb.append(",").append(parameter.getName()).append(":").append(obj); } else { sb.append(parameter.getName()).append(":").append(obj); } } num++; } sb.append("}"); return sb.toString(); } /** * 判断是否是基本类型 * @param obj * @return */ private boolean isNotPrimitiveType(Object obj) { for (Class<?> aClass : primitiveType) { if (Objects.equals(obj.getClass(), aClass)) { return false; } } return true; }}
以上就是本次的Spring Boot 集成 aop 实现日志切面实现,属于个人想法与实现,有问题请及时指出,谢谢!!!