🚩 需求描述
能够在每次访问接口时,自动记录入参、出参的全局统一日志。
📈 方案与分析
说明:日志作用的所有接口上,且需要统一形式,因此是不考虑在接口中手写的。
📋 方案一
理想的情况下是通过 spring aop 快速进入开发状态,然后在打包时通过 aspectj 处理器在编译期织入代码。因为 spring aop 采用 aspectj 语法,且提供了强大但简单的注解支持,因此可以很轻松的实现切面。但是 spring 的 aop 是运行时的(反射),相对于静态织入的 aspectj 来说,性能差距是极大的。而使用 aspectj 处理器每次需要手动清除之前编译的代码,否则新的代码无法实时编译的,比较麻烦,因此比较适合在打包时提供 aspectj 编译处理
📋 方案二
spring 其他内置增强接口, @RestControllerAdive
,可以为注解类提升作用域,而 ResponseBodyAdive
,RequestBodyAdive
接口,则可以为实现类提供预处理出参、入参的接口数据,两相结合,则可以实现类似的环绕式的出入参日志记录
📋 方案三
使用拦截器或过滤器,但其匹配规则是作用在 url 上,粒度太大不够细致。
📄 代码实现
此处通过 aop 实现,需要注意的是,若通过 ResponseBodyAdive
接口来处理统一返回值,则不能使用 @Around
的环绕切面,因为该切面要求方法的出参类型是不能修改的,而使用被统一返回类型包装的对象,是与切面方法的实际返回值是不一样的,故下面使用 @Before
、@AfterReturning
方法分别处理接口入参、和参数两个状态。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84
| @Aspect @Component @Slf4j public class LogProcessor {
@AllArgsConstructor private static class LogInfo {
String requestId;
long startTime;
Gson gson; }
ThreadLocal<LogInfo> logInfo = new ThreadLocal<>();
@Pointcut(value = "@annotation(com.包名.annotation.Log)") private void point() { }
@Before(value = "point()") public void LogRequestInfo(JoinPoint joinPoint) { String uuid = UUID.randomUUID().toString(); Gson gson = new Gson(); logInfo.set(new LogInfo(uuid, System.currentTimeMillis(), gson)); ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = Objects.requireNonNull(attributes).getRequest(); StringBuilder requestLog = new StringBuilder(); Signature signature = joinPoint.getSignature(); requestLog.append("[").append(uuid).append("] -> ") .append("请求信息:").append("URL = {").append(request.getRequestURI()).append("}, ") .append("请求方式 = {").append(request.getMethod()).append("}, ") .append("请求IP = {").append(request.getRemoteAddr()).append("}, ") .append("类方法 = {").append(signature.getDeclaringTypeName()).append(".") .append(signature.getName()).append("}, "); String[] paramNames = ((MethodSignature) signature).getParameterNames(); Object[] paramValues = joinPoint.getArgs(); int paramLength = null == paramNames ? 0 : paramNames.length; if (paramLength == 0) { requestLog.append("请求参数 = {} "); } else { requestLog.append("请求参数 = ["); for (int i = 0; i < paramLength - 1; i++) { requestLog.append(paramNames[i]).append("=").append(gson.toJson(paramValues[i])).append(","); } requestLog.append(paramNames[paramLength - 1]).append("=").append(gson.toJson(paramValues[paramLength - 1])).append("]"); } log.info(requestLog.toString()); }
@AfterReturning(returning = "target", pointcut = "point()") public void logResultVOInfo(Object target) { LogInfo logInfo = this.logInfo.get(); this.logInfo.remove(); long offTime = System.currentTimeMillis() - logInfo.startTime; log.info(String.format("[%s] -> 请求结果:%s [%dms]", logInfo.requestId, logInfo.gson.toJson(target), offTime)); } }
|
因为实际上,两个切面方法是独立的作用域,为了线程安全,故使用线程变量来存储请求的日志信息,主要记录该请求的唯一编号(此处使用 UUID),开始时间,同时又都需要将对象 json 化,所以也缓存了 gson 对象。
同样的方法,也可以通过实现增强接口,重写其预处理入参、出参的方法来提供日志支持。
java spring log web aop