java aop实现http接口日志记录

Posted 莫比乌斯的code

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java aop实现http接口日志记录相关的知识,希望对你有一定的参考价值。

↑ 点击上方“莫比乌斯的code”关注我们

"今天晚上,很好的月光。" —— 鲁迅《狂人日记》

http log

本文旨在讨论如何利用aop打印springboot http日志,包括访问日志、接口返回response,以及异常堆栈信息。

背景

为什么需要log

有过线上问题排查经历的同学都知道,日志可以给你提供最直白、最明确的信息。此外,通过对日志的收集、审计,可以对BI以及系统健壮性提供建议。尤其是新系统上线之初,系统稳定性不足,更需要日志监控。

为什么不实用Greys+access log

Greys 作为Java线上问题诊断工具,可以监控实时访问参数,但是对历史访问无记录,尤其是在前后分离or微服务架构下,日志是可以作为呈堂证供的存在。而access log不够灵活和强大。

方案优势

  1. 简化日志打印

  2. 统一的日志格式,可以配合收集机制

  3. 灵活,可以自由选择是否记录日志、是否忽略response、是否忽略某些参数(如file参数显然不适合记录日志)、以及是否打印header参数

  4. 日志记录比例可配置(系统稳定,流量增大之后,可以通过配置降低记录比例)

  5. 方便回溯异常请求参数

  6. 记录接口耗时

实现

AOP-面向切面编程

一句话描述,在java对象方法中增加切点,在不改变对象领域的前提下通过代理扩展功能。

切面选择

网上常见教程会选择拦截所有http请求,并打印request.parameters。但是存在问题:

  1. 不够灵活,部分参数不想打印,如文件数据(过大)、敏感数据(身份证)等。

  2. 隐式的aop,容易给维护人员造成疑惑。(个人习惯偏向于避免隐藏的aop逻辑)

  3. 部分参数通过header传输

因此,自定义日志输出 @annotation HttpLog作为切点

 
   
   
 
  1. /**

  2. * 用于打印http请求访问日志

  3. *

  4. * @author yuheng.wang

  5. */

  6. @Target(ElementType.METHOD)

  7. @Retention(RetentionPolicy.RUNTIME)

  8. public @interface HttpLog {

  9.    /**

  10.     * 忽略参数,避免文件or无意义参数打印

  11.     *

  12.     * @return 忽略参数数组

  13.     */

  14.    String[] exclude() default {};

  15.    /**

  16.     * 需要打印的header参数

  17.     *

  18.     * @return header参数名数组

  19.     */

  20.    String[] headerParams() default {};

  21.    boolean ignoreResponse() default false;

  22.    /**

  23.     * 日志记录比例

  24.     *  >100 or < 0时,每次请求都会被记录

  25.     *  = 0 时,不会记录

  26.     */

  27.    int percent() default 100;

  28. }

 
   
   
 
  1. @Aspect

  2. @Slf4j

  3. public class HttpLogHandler {

  4.    @Pointcut("@annotation(com.yitutech.scusbackend.aop.HttpLog)")

  5.    public void logAnnotation() {

  6.    }

  7.    private Optional<HttpLog> getLogAnnotation(JoinPoint joinPoint) {

  8.    if (joinPoint instanceof MethodInvocationProceedingJoinPoint) {

  9.        Signature signature = joinPoint.getSignature();

  10.        if (signature instanceof MethodSignature) {

  11.            MethodSignature methodSignature = (MethodSignature) signature;

  12.            Method method = methodSignature.getMethod();

  13.            if (method.isAnnotationPresent(HttpLog.class)) {

  14.                return Optional.of(method.getAnnotation(HttpLog.class));

  15.            }

  16.        }

  17.    }

  18.        return Optional.empty();

  19.    }

  20. }

打印异常信息

  1. 为了方便回溯,定义UUID形式的traceId,并通过ThreadLocal持有。

  2. 为了记录接口相应时间,定义startTime,并通过ThreadLocal持有。

  3. AfterThrowing方法不会阻碍Exception的抛出,不会影响全局异常捕获。

 
   
   
 
  1. @AfterThrowing(throwing = "e", pointcut = "logAnnotation()")

  2. public void throwing(JoinPoint joinPoint, Exception e) {

  3.    try {

  4.        Optional<HttpLog> httpLog = getLogAnnotation(joinPoint);

  5.        httpLog.ifPresent(anno -> {

  6.            if (!anno.ignoreResponse()) {

  7.                log.info("ERROR_LOG. traceId:{}, cost:{}",

  8.                        traceIdThreadLocal.get(), costTime(), e);

  9.            }

  10.        });

  11.    } catch (Exception exception) {

  12.        log.warn("print error log fail!", exception);

  13.    } finally {

  14.        startTimeThreadLocal.remove();

  15.        traceIdThreadLocal.remove();

  16.    }

  17. }

打印response

  1. 通过JSONObject.toJSONString()转换,因为restful接口常见为返回json格式。

  2. 需要判断是否忽略了response打印

 
   
   
 
  1. @AfterReturning(returning = "restApiResponse", pointcut = "logAnnotation()")

  2. public void response(JoinPoint joinPoint, Object restApiResponse) {

  3.    try {

  4.        Optional<HttpLog> httpLog = getLogAnnotation(joinPoint);

  5.        httpLog.ifPresent(anno -> {

  6.            if (!anno.ignoreResponse()) {

  7.                log.info("RESPONSE_LOG. traceId:{}, result:{}, cost:{}",

  8.                        traceIdThreadLocal.get(), JSONObject.toJSONString(restApiResponse), costTime());

  9.            }

  10.        });

  11.    } catch (Exception exception) {

  12.        log.warn("print response log fail!", exception);

  13.    } finally {

  14.        startTimeThreadLocal.remove();

  15.        traceIdThreadLocal.remove();

  16.    }

  17. }

获取请求日志

获取HttpServletRequest

重点在于获取HttpServletRequest对象,而spring已经很贴心的在initContextHolders时通过RequestContextHolder持有了RequestAttributes参数(基于ThreadLocal)。 所以我们可以很轻松的获取请求参数

 
   
   
 
  1. private HttpServletRequest getRequest() {

  2.    RequestAttributes ra = RequestContextHolder.getRequestAttributes();

  3.    ServletRequestAttributes sra = (ServletRequestAttributes) ra;

  4.    return sra.getRequest();

  5. }

获取RequestBody

!!上面获取参数的方式有一个缺陷,无法获取RequestBody参数,因为RequestBody是以流的形式读取,只能被读取一次,如果在aop中的提前读取了内容信息,后面的业务方法则无法获取。 一般的解决方案为封装一层HttpServletRequestWrapper暂存流的信息,实现多次读取。但是此种方案对需要修改HttpServletRequest,侵入性较大,所以我们采取另一种方案,基于aop+反射的特性获取java方法参数,遍历判断是否为@RequestBody参数。

 
   
   
 
  1. private Optional<Object> getRequestBodyParam(JoinPoint joinPoint){

  2.    if (joinPoint instanceof MethodInvocationProceedingJoinPoint) {

  3.        Signature signature = joinPoint.getSignature();

  4.        if (signature instanceof MethodSignature) {

  5.            MethodSignature methodSignature = (MethodSignature) signature;

  6.            Method method = methodSignature.getMethod();

  7.            Parameter[] methodParameters = method.getParameters();

  8.            if (null != methodParameters

  9.                    && Arrays.stream(methodParameters).anyMatch(p-> AnnotationUtils.findAnnotation(p, RequestBody.class) != null)) {

  10.                return Optional.of(joinPoint.getArgs());

  11.            }

  12.        }

  13.    }

  14.    return Optional.empty();

  15. }

获取uri

根据拦截规则不同,getServletPath()和request.getPathInfo()可能为空,简单的做一次健壮性判断。

 
   
   
 
  1. private String getRequestPath(HttpServletRequest request) {

  2.   return (null != request.getServletPath() && request.getServletPath().length() > 0)

  3.           ? request.getServletPath() : request.getPathInfo();

  4. }

日志记录百分比

  1. 接口请求量一般为平稳持续

  2. 并不需要十分精确的比例

所以通过接口访问时间戳(startTime)尾数进行判断

 
   
   
 
  1. private boolean needLog(int percent, long startTimeMillis){

  2.    if (percent< 0 || percent >= 100) {

  3.        return true;

  4.    } else if (percent == 0) {

  5.        return false;

  6.    } else {

  7.        return percent > startTimeMillis % 100;

  8.    }

  9. }

使用

加载HttpLogAspect对象

  1. 可以在@SpringBootApplication类下直接加载,也可以在HttpLogAspect中增加@Component注解。

  2. 不需要显示声明@EnableAspectJAutoProxy注解

 
   
   
 
  1. @Bean

  2. public HttpLogAspect httpLogAspect(){

  3.    return new HttpLogAspect();

  4. }

Controller配置日志打印

在controller中在接口方法中通过HttpLog配置,即可实现日志记录

 
   
   
 
  1. @GetMapping("/log/pwdExcludeResponse/{id}")

  2. @HttpLog(headerParams="username", ignoreResponse = true, percent = 50, exclude={"avatar"})

  3. public RestApiResponse<User> getInfoWithPwd(@PathVariable("id") int id,

  4.                                            @RequestParam("age") int age,

  5.                                            @RequestParam("avatar") MultipartFile avatar,

  6.                                            @RequestHeader("password") String password){

  7.    User user = new User();

  8.    user.setId(id);

  9.    user.setPassword(password);

  10.    user.setAge(age);

  11.    return RestApiResponse.success(user);

  12. }

效果

 
   
   
 
  1. 2018-10-22 12:51:16.814  INFO 1234 --- [           main] w.crick.study.httplog.aop.HttpLogAspect  : REQUEST_LOG. traceId:737001cf-f546-4092-9c77-f7762275ddcf. requestUrl: /user/log/7 -PARAMS- age: [6],

  2. 2018-10-22 12:51:16.965  INFO 1234 --- [           main] w.crick.study.httplog.aop.HttpLogAspect  : RESPONSE_LOG. traceId:737001cf-f546-4092-9c77-f7762275ddcf, result:{"code":10000,"data":{"age":6,"id":7,"username":"8025373076402792850"}}, cost:171

  3. 2018-10-22 12:51:17.112  INFO 1234 --- [           main] w.crick.study.httplog.aop.HttpLogAspect  : REQUEST_LOG. traceId:8c907a7e-f36a-4ccc-a931-f167b55697ee. requestUrl: /user/log/pwd/0 -PARAMS- age: [7],  -HEADER_PARAMS- password: 6460630360418207541,

  4. 2018-10-22 12:51:17.113  INFO 1234 --- [           main] w.crick.study.httplog.aop.HttpLogAspect  : RESPONSE_LOG. traceId:8c907a7e-f36a-4ccc-a931-f167b55697ee, result:{"code":10000,"data":{"id":0,"password":"6460630360418207541","username":"2250855379452327045"}}, cost:9

以上是关于java aop实现http接口日志记录的主要内容,如果未能解决你的问题,请参考以下文章

Spring Boot AOP 扫盲,实现接口访问的统一日志记录

JAVA之AOP

Spring Boot 中使用Spring Aop实现日志记录功能

springboot系列(二十一):基于AOP实现自定义注解且记录接口日志|超级超级详细,建议收藏

springboot系列(二十一):基于AOP实现自定义注解且记录接口日志|超级超级详细,建议收藏

Spring Boot# 使用AOP实现接口鉴权访问白名单限制记录接口访问日志限制接口请求次数