Web 日志注解
清单 3. Web 日志注解代码
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ControllerWebLog {String name();boolean intoDb() default false;}
显示更多
其中 name 为所调用接口的名称, intoDb 则标识该条操作日志是否需要持久化存储,Spring Boot 连接数据库的配置,可以参考 SpringBoot 项目配置多数据源 这篇文章,具体的数据库结构可以 点击这里获取 。现在注解有了,我们接下来需要编写与该注解配套的 AOP 切面。
实现 WebLogAspect 切面
第 1 步,我们定义了一个切面类 WebLogAspect 如清单 4 所示。其中@Aspect 注解是告诉 Spring 将该类作为一个切面管理,@Component 注解是说明该类作为一个 Spring 组件。
清单 4. WebLogAspect
@Aspect@Component@Order(100)public class WebLogAspect {}
显示更多
第 2 步,接下来我们需要定义一个切点。
清单 5. Web 日志 AOP 切点
@Pointcut("execution(* cn.itweknow.sbaop.controller..*.*(..))")public void webLog() {}
显示更多
对于 execution 表达式, 官网 的介绍为(翻译后):
清单 6. 官网对 execution 表达式的介绍
execution(<修饰符模式>?<返回类型模式><方法名模式>(<参数模式>)<异常模式>?)
显示更多
其中除了返回类型模式、方法名模式和参数模式外,其它项都是可选的。这个解释可能有点难理解,下面我们通过一个具体的例子来了解一下。在 WebLogAspect 中我们定义了一个切点,其 execution 表达式为 * cn.itweknow.sbaop.controller..*.*(..) ,下表为该表达式比较通俗的解析:
表 1. execution() 表达式解析
| 标识符 | 含义 |
|---|---|
execution() |
表达式的主体 |
第一个 * 符号 |
表示返回值的类型, * 代表所有返回类型 |
cn.itweknow.sbaop.controller |
AOP 所切的服务的包名,即需要进行横切的业务类 |
包名后面的 .. |
表示当前包及子包 |
第二个 * |
表示类名, * 表示所有类 |
最后的 .*(..) |
第一个 . 表示任何方法名,括号内为参数类型, .. 代表任何类型参数 |
第 3 步,@Before 修饰的方法中的内容会在进入切点之前执行,在这个部分我们需要打印一个开始执行的日志,并将请求参数和开始调用的时间存储在 ThreadLocal 中,方便在后面结束调用时打印参数和计算接口耗时。
清单 7. @Before 代码
@Before(value = "webLog()& & @annotation(controllerWebLog)")public void doBefore(JoinPoint joinPoint, ControllerWebLog controllerWebLog) {// 开始时间。long startTime = System.currentTimeMillis();Map<String, Object> threadInfo = new HashMap<>();threadInfo.put(START_TIME, startTime);// 请求参数。StringBuilder requestStr = new StringBuilder();Object[] args = joinPoint.getArgs();if (args != null && args.length > 0) {for (Object arg : args) {requestStr.append(arg.toString());}}threadInfo.put(REQUEST_PARAMS, requestStr.toString());threadLocal.set(threadInfo);logger.info("{}接口开始调用:requestData={}", controllerWebLog.name(), threadInfo.get(REQUEST_PARAMS));}
显示更多
第 4 步,@AfterReturning ,当程序正常执行有正确的返回时执行,我们在这里打印结束日志,最后不能忘的是清除 ThreadLocal 里的内容。
清单 8. @AfterReturning 代码
@AfterReturning(value = "webLog()&& @annotation(controllerWebLog)", returning = "res")public void doAfterReturning(ControllerWebLog controllerWebLog, Object res) {Map<String, Object> threadInfo = threadLocal.get();long takeTime = System.currentTimeMillis() - (long) threadInfo.getOrDefault(START_TIME, System.currentTimeMillis());if (controllerWebLog.intoDb()) {insertResult(controllerWebLog.name(), (String) threadInfo.getOrDefault(REQUEST_PARAMS, ""),JSON.toJSONString(res), takeTime);}threadLocal.remove();logger.info("{}接口结束调用:耗时={}ms,result={}", controllerWebLog.name(),takeTime, res);}
显示更多
第 5 步,当程序发生异常时,我们也需要将异常日志打印出来:
清单 9. @AfterThrowing 代码
@AfterThrowing(value = "webLog()& & @annotation(controllerWebLog)", throwing = "throwable")public void doAfterThrowing(ControllerWebLog controllerWebLog, Throwable throwable) {Map< String, Object> threadInfo = threadLocal.get();if (controllerWebLog.intoDb()) {insertError(controllerWebLog.name(), (String)threadInfo.getOrDefault(REQUEST_PARAMS, ""),throwable);}threadLocal.remove();logger.error("{}接口调用异常,异常信息{}",controllerWebLog.name(), throwable);}
显示较少
第 6 步,至此,我们的切面已经编写完成了。下面我们需要将 ControllerWebLog 注解使用在我们的测试接口上,接口内部的代码已省略,如有需要的话,请参照 本文源码 。
清单 10. 测试接口代码
@PostMapping("/post-test")@ApiOperation("接口日志 POST 请求测试")@ControllerWebLog(name = "接口日志 POST 请求测试", intoDb = true)public BaseResponse postTest(@RequestBody BaseRequest baseRequest) {}
显示较少
第 7 步,最后,启动项目,然后打开 Swagger 文档进行测试,调用接口后在控制台就会看到类似图 1 这样的日志。