👨🏻💻 热爱摄影的程序员
👨🏻🎨 喜欢编码的设计师
🧕🏻 擅长设计的剪辑师
🧑🏻🏫 一位高冷无情的全栈工程师
欢迎分享 / 收藏 / 赞 / 在看!
【需求】
在 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);
}