👨🏻💻 热爱摄影的程序员
👨🏻🎨 喜欢编码的设计师
🧕🏻 擅长设计的剪辑师
🧑🏻🏫 一位高冷无情的全栈工程师
欢迎分享 / 收藏 / 赞 / 在看!
【需求】
在 Spring Boot 项目中,我们经常需要记录接口的日志,比如请求参数、响应结果、请求时间、响应时间等。为了方便统一处理,我们可以通过自定义注解的方式来实现接口日志的记录。
【解决】
👉 项目地址:Gitee-ControllerLog
- 创建自定义注解
Log.java
,用于标记需要记录日志的接口。
注解本质上是一个接口,它继承自 java.lang.annotation.Annotation。但是,你通常不需要显式地继承这个接口,因为当使用 @interface
关键字时,编译器会自动为你处理。
- @Target 注解用于指定注解可以应用的 Java 元素类型,比如类、方法、字段等。详细的元素类型可以参考 ElementType 枚举类。
- @Retention 注解用于指定注解的生命周期,有三个值可选:SOURCE、CLASS 和 RUNTIME。
- SOURCE 表示注解仅存在于源代码中,编译时会被忽略;
- CLASS 表示注解会被编译到 class 文件中,但在运行时会被忽略;
- RUNTIME 表示注解会被编译到 class 文件中,并在运行时保留,因此可以通过反射读取注解信息。
- @Documented 注解用于指定注解是否包含在 JavaDoc 中。
java">@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {/*** 模块名称*/String title() default "";/*** 功能*/BusinessType businessType() default BusinessType.OTHER;/*** 操作人类别*/OperatorType operatorType() default OperatorType.MANAGE;/*** 是否保存请求的参数*/boolean isSaveRequestData() default true;/*** 是否保存响应的参数*/boolean isSaveResponseData() default true;/*** 排除指定的请求参数*/String[] excludeParamNames() default {};
}
- 创建操作日志记录处理切面
LogAspect.java
,用于处理接口日志的记录。
处理注解通常涉及编写一个能够识别并响应注解的组件。这可以通过多种方式实现,比如使用 Spring AOP、自定义 BeanPostProcessor、或直接在配置类中通过反射读取注解信息。
如果你希望在不修改业务代码的情况下,通过切面(Aspect)来增强被注解的方法,可以使用 Spring AOP。
- @Aspect 注解用于定义一个切面,它包含切入点和通知。
- @Before 注解用于指定一个前置通知,在目标方法执行之前执行。
- @AfterReturning 注解用于指定一个返回通知,在目标方法正常返回后执行。
- @AfterThrowing 注解用于指定一个异常通知,在目标方法抛出异常后执行。
- @Around 注解用于指定一个环绕通知,可以在目标方法执行前后执行自定义的行为。
- @Pointcut 注解用于定义一个切入点,可以在多个通知中共用。
- @AutoConfiguration 注解用于自动配置 Bean。
java">@Slf4j
@Aspect
@AutoConfiguration
public class LogAspect {/*** 排除敏感属性字段*/public static final String[] EXCLUDE_PROPERTIES = {"password", "oldPassword", "newPassword", "confirmPassword"};/*** 计时 key*/private static final ThreadLocal<StopWatch> KEY_CACHE = new ThreadLocal<>();/*** 处理请求前执行** @param joinPoint 切点* @param controllerLog 注解*/@Before(value = "@annotation(controllerLog)")public void doBefore(JoinPoint joinPoint, Log controllerLog) {StopWatch stopWatch = new StopWatch();KEY_CACHE.set(stopWatch);stopWatch.start();}/*** 处理请求后执行** @param joinPoint 切点* @param controllerLog 注解* @param jsonResult 返回结果*/@AfterReturning(pointcut = "@annotation(controllerLog)", returning = "jsonResult")public void doAfterReturning(JoinPoint joinPoint, Log controllerLog, Object jsonResult) {handleLog(joinPoint, controllerLog, null, jsonResult);}/*** 处理日志,持久化到数据库** @param joinPoint 切点* @param controllerLog 注解* @param e 异常* @param jsonResult 返回结果*/protected void handleLog(final JoinPoint joinPoint, Log controllerLog, final Exception e, Object jsonResult) {try {Logs operLog = new Logs();operLog.setStatus(BusinessStatus.SUCCESS.ordinal());// 请求的地址String ip = ServletUtils.getClientIP();operLog.setOperIp(ip);operLog.setOperUrl(StringUtils.substring(ServletUtils.getRequest().getRequestURI(), 0, 255));if (e != null) {operLog.setStatus(BusinessStatus.FAIL.ordinal());operLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000));}// 设置方法名称String className = joinPoint.getTarget().getClass().getName();String methodName = joinPoint.getSignature().getName();operLog.setMethod(className + "." + methodName + "()");// 设置请求方式operLog.setRequestMethod(ServletUtils.getRequest().getMethod());// 处理设置注解上的参数getControllerMethodDescription(joinPoint, controllerLog, operLog, jsonResult);// 设置消耗时间StopWatch stopWatch = KEY_CACHE.get();stopWatch.stop();operLog.setCostTime(stopWatch.getTime());// 发布事件保存数据库SpringUtils.context().publishEvent(operLog);} catch (Exception exp) {// 记录本地异常日志log.error("异常信息:{}", exp.getMessage());exp.printStackTrace();} finally {KEY_CACHE.remove();}}/*** 获取注解中对方法的描述信息 用于Controller层注解** @param joinPoint 切点* @param log 注解* @param operLog 操作日志* @param jsonResult 返回结果* @throws Exception 异常*/public void getControllerMethodDescription(JoinPoint joinPoint, Log log, Logs operLog, Object jsonResult) throws Exception {// 设置action动作operLog.setBusinessType(log.businessType().ordinal());// 设置标题operLog.setTitle(log.title());// 设置操作人类别operLog.setOperatorType(log.operatorType().ordinal());// 是否需要保存request,参数和值if (log.isSaveRequestData()) {// 获取参数的信息,传入到数据库中。setRequestValue(joinPoint, operLog, log.excludeParamNames());}// 是否需要保存response,参数和值if (log.isSaveResponseData() && ObjectUtil.isNotNull(jsonResult)) {operLog.setJsonResult(StringUtils.substring(JsonUtils.toJsonString(jsonResult), 0, 2000));}}/*** 获取请求的参数,放到log中** @param joinPoint 切点* @param operLog 操作日志* @param excludeParamNames 排除字段* @throws Exception 异常*/private void setRequestValue(JoinPoint joinPoint, Logs operLog, String[] excludeParamNames) throws Exception {Map<String, String> paramsMap = ServletUtils.getParamMap(ServletUtils.getRequest());String requestMethod = operLog.getRequestMethod();if (MapUtil.isEmpty(paramsMap) && HttpMethod.PUT.name().equals(requestMethod) || HttpMethod.POST.name().equals(requestMethod)) {String params = argsArrayToString(joinPoint.getArgs(), excludeParamNames);operLog.setOperParam(StringUtils.substring(params, 0, 2000));} else {MapUtil.removeAny(paramsMap, EXCLUDE_PROPERTIES);MapUtil.removeAny(paramsMap, excludeParamNames);operLog.setOperParam(StringUtils.substring(JsonUtils.toJsonString(paramsMap), 0, 2000));}}/*** 参数拼装** @param paramsArray 参数数组* @param excludeParamNames 排除字段* @return 操作参数*/private String argsArrayToString(Object[] paramsArray, String[] excludeParamNames) {StringJoiner params = new StringJoiner(" ");if (ArrayUtil.isEmpty(paramsArray)) {return params.toString();}for (Object o : paramsArray) {if (ObjectUtil.isNotNull(o) && !isFilterObject(o)) {String str = JsonUtils.toJsonString(o);Dict dict = JsonUtils.parseMap(str);if (MapUtil.isNotEmpty(dict)) {MapUtil.removeAny(dict, EXCLUDE_PROPERTIES);MapUtil.removeAny(dict, excludeParamNames);str = JsonUtils.toJsonString(dict);}params.add(str);}}return params.toString();}/*** 判断是否需要过滤** @param o 对象* @return 是否过滤*/@SuppressWarnings("rawtypes")public boolean isFilterObject(final Object o) {Class<?> clazz = o.getClass();if (clazz.isArray()) {return MultipartFile.class.isAssignableFrom(clazz.getComponentType());} else if (Collection.class.isAssignableFrom(clazz)) {Collection collection = (Collection) o;for (Object value : collection) {return value instanceof MultipartFile;}} else if (Map.class.isAssignableFrom(clazz)) {Map map = (Map) o;for (Object value : map.values()) {return value instanceof MultipartFile;}}return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse || o instanceof BindingResult;}
}
注意,在处理日志,持久化到数据库时,使用了 SpringUtils.context().publishEvent(operLog);
发布事件保存数据库,即 Spring 的事件发布机制来异步地保存操作日志到数据库。
当这个事件被发布后,Spring 会寻找所有注册的并且能处理这个事件的监听器。这些监听器通常会实现 ApplicationListener 接口,并且在其 onApplicationEvent 方法中处理这个事件。
这种方式的好处是,事件的发布和处理是异步的,不会阻塞主线程的执行,从而提高了程序的性能。
- @Async 注解用于指定异步方法,被注解的方法会在新的线程中执行。
- @EventListener 注解用于指定一个事件监听器,它会监听所有被 Spring 发布的事件。
java">@Async
@EventListener
public void recordLogs(Logs logs) {// 将日志保存到数据库insertLogs(logs);
}
- 在需要记录日志的接口上添加
@Log
注解。
定义好注解后,你可以在任何符合 @Target
元注解指定的 Java 元素上使用它。
java">@Log(title = "小程序管理员", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(SysAdministratorBo bo, HttpServletResponse response) {List<SysAdministratorVo> list = sysAdministratorService.queryList(bo);ExcelUtil.exportExcel(list, "小程序管理员", SysAdministratorVo.class, response);
}