温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

如何优雅打印接口调用时长

发布时间:2021-10-20 14:28:10 来源:亿速云 阅读:194 作者:iii 栏目:编程语言

这篇文章主要介绍“如何优雅打印接口调用时长 ”,在日常操作中,相信很多人在如何优雅打印接口调用时长 问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”如何优雅打印接口调用时长 ”的疑惑有所帮助!接下来,请跟着小编一起来学习吧!

引言

优雅的API设计不仅仅是代码层面的书写规范.几乎不可能API开发完毕就能正常投入使用,更多的是对细节的打磨.例如接口的每次执行时间,入参都会在API测试中反复的推敲

思考

如何设计一个方案使开发者能一目了然的可视化接口的处理时间以及入参是否正确呢?

思路

首先想到的是Spring的AOP切面,现在我们编写API接口,一般都会把接口写在controller控制层里,按照不同的业务,分为写在不同业务包下的controller类中.大致的架构如下: 如何优雅打印接口调用时长 按照这种控制层的编写规范,只需要用切面找到每个业务包下的controller类,监控类下面的每个方法的入参和执行时间,打印在log日志中便可以在控制台中可视化每个接口的实时状态了.

实践

导包

<dependency>
    <!--spring启动包-->
    <groupid>org.springframework.boot</groupid>
    <artifactid>spring-boot-starter-web</artifactid>
</dependency>
<dependency>
     <!--spring aop核心包-->
    <groupid>org.springframework.boot</groupid>
    <artifactid>spring-boot-starter-aop</artifactid>
</dependency>

AOP核心

aop的核心在于切点通知类型.结合我们所需要实现的方案,我们所关注的切点就是每个业务下控制层包的每个类方法. 通知的主要类型分为:

  • 前置通知[Before advice]:在连接点前面执行,前置通知不会影响连接点的执行,除非此处抛出异常。

  • 正常返回通知[After returning advice]:在连接点正常执行完成后执行,如果连接点抛出异常,则不会执行。

  • 异常返回通知[After throwing advice]:在连接点抛出异常后执行。

  • 返回通知[After (finally) advice]:在连接点执行完成后执行,不管是正常执行完成,还是抛出异常,都会执行返回通知中的内容。

  • 环绕通知[Around advice]:环绕通知围绕在连接点前后,比如一个方法调用的前后。这是最强大的通知类型,能在方法调用前后自定义一些操作。环绕通知还需要负责决定是继续处理join point(调用ProceedingJoinPoint的proceed方法)还是中断执行。

这里因为我们需要记录入参和接口处理时间,选用Before 前置通知Around 环绕通知

定义切点

切面第一步,我们需要找准切点 新建RuntimeMethod类,用@Aspect @Component修饰定义这是由spring管理的切面入口类,@Log4j2 注释方便后续打印日志

@Aspect
@Component
@Log4j2
public class RuntimeMethod {
    //定义aopPoint私有方法,用@Pointcut修饰并标识该切面的切点
    //以execution(* com.staging.business.*.controller.*.*(..))为例
    //execution()是切面的主体
    //第一个" * "符号,表示返回值的类型任意
    //com.staging.business表示AOP所切的服务的包名,即需要进行横切的业务类
    //包名后面的" .. ",表示当前包及子包
    //之后的" * ",表示类名,*即所有类
    // .*(..) 表示任何方法名,括号内表示参数,两个点表示匹配任何参数类型
    @Pointcut("execution(* com.staging.business.*.controller.*.*(..))")
    private void aopPoint() {
    } 
}

切面第二步,定义前置和环绕通知,并声明通知的切点为aopPoint()

    /**
     * 功能描述: 前置通知
     */
    @Before("aopPoint()")
    public void before(JoinPoint joinPoint) throws Throwable {
        //在调用切面管理的接口前会进入这里
    }

    /**
     * 功能描述: 环绕通知
     */
    @Around("aopPoint()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {  
        //在before通知后会走入这里,直到返回result对象后,客户端才可以拿到回参
        Object result = joinPoint.proceed();
        return result;
    }

前面两步实现了两个需要用到的通知并简要说明了他的作用.接下来还需要使用到spring包中的ServletRequestAttributes对象用于获取HttpServletRequest对象,获取到我们想要的一些打印参数.

    public void before(JoinPoint joinPoint) throws Throwable {
        //在调用切面管理的接口前会进入这里
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = requestAttributes.getRequest();
        Enumeration<string> e = request.getHeaderNames();
        JSONObject headers = new JSONObject();
        if (null != e) {
            while (e.hasMoreElements()) {
                String headerName = e.nextElement();
                Enumeration<string> headerValues = request.getHeaders(headerName);
                while (headerValues.hasMoreElements()) {
                    headers.put(headerName, headerValues.nextElement());
                }
            }
        }
        //参数依次代表请求方法,请求地址,参数,头参数,调用时间
        log.info("-in- {} {} -{}{}",request.getMethod(),request.getRequestURI(),joinPoint.getArgs(),headers.toJSONString()}
    }

接口调用时间也能很轻松的在环绕通知中打印

    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {  
        long begin=System.currentTimeMillis();
        //在before通知后会走入这里,直到返回result对象后,客户端才可以拿到回参
        Object result = joinPoint.proceed();
        long end= System.currentTimeMillis();
        log.info("-out -time:{}ms", end - begin}
        return result;
    }

运行起来,调用API接口,我们都会输出以下日志

    -in- GET /user/info -id=123  header:{"content-length":"0",......}
    -out- -time:91ms
    ......

测试完全没有问题,当然这不是最终版本,尝试放在测试环境,调用的人多起来,就会非常混乱,类似下面的画风

    -in- GET /user/info -id=123  header:{"content-length":"0",......}
    -in- GET /teacher/info -id=123  header:{"content-length":"0",......}
    -out- -time:91ms
    -in- GET /user/info -id=321  header:{"content-length":"0",......}
    -out- -time:191ms
    ......

可以看到问题出现在并发操作上,在同一时间调用多个接口时,日志会乱掉,这可不是我想要的结果.必须想办法解决这个问题.翻阅资料,想到用ThreadLocal线程局部变量以及Tuple元组对象解决这个问题.接下来改造代码. 在RuntimeMethod类中定义一个私有变量ThreadLocal.

    private ThreadLocal<tuple6<string, string, object[], long, string>&gt; threadLocal = new ThreadLocal&lt;&gt;();

再改造通知部分

    @Before("aopPoint()")
    public void before(JoinPoint joinPoint) throws Throwable {
        //打印请求体
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (null != requestAttributes) {
            //在loadingThreadLocal用ThreadLocal和Tuple对象存储参数.这样就可以方便的取出接口的必要参数
            loadingThreadLocal(requestAttributes, joinPoint.getArgs());
                log.info("-in- {} {} -{}",
                        threadLocal.get().getT1(),
                        threadLocal.get().getT2(),
                        threadLocal.get().getT6());
                log.info("Method arguments:{} -{}",
                        threadLocal.get().getT3(),
                        threadLocal.get().getT6());
                log.info("Request header:{} -{}",
                        threadLocal.get().getT4(),
                        threadLocal.get().getT6());
        }
    }
    
    @Around("aopPoint()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
         // 调用目标方法
        Object result = joinPoint.proceed();
        String requestUrl = threadLocal.get().getT2();
        // 注意在out的时候,取出调用的接口名称,这样可以用接口名称去方便过滤,就不用害怕日志错乱的问题了.return回参在生产环境中尽量不要加进去,因为是测试阶段排查问题打的日志所以越详细越好.
        log.info("-out- {} return:{} -time:{}ms -{}", requestUrl, JSONObject.toJSONString(result), System.currentTimeMillis() - threadLocal.get().getT5(), threadLocal.get().getT6());
        //接口出参处理
        return delReturnData(result);
    }
    
    private void loadingThreadLocal(ServletRequestAttributes requestAttributes, Object[] args) {
        HttpServletRequest request = requestAttributes.getRequest();
        Enumeration<string> e = request.getHeaderNames();
        JSONObject headers = new JSONObject();
        if (null != e) {
            while (e.hasMoreElements()) {
                String headerName = e.nextElement();
                Enumeration<string> headerValues = request.getHeaders(headerName);
                while (headerValues.hasMoreElements()) {
                    headers.put(headerName, headerValues.nextElement());
                }
            }
        }
        //此处追加了一个调用链的id,可返回客户端,让客户端在下一次请求中带入这个id,方法统计一个业务闭环.
        String businessId = IdUtil.getSnowflake(1, 1).nextIdStr();
        //请求方法,请求地址,参数,头参数,调用时间,调用链id
        threadLocal.set(Tuples.of(request.getMethod(), request.getRequestURI(), args, headers.toJSONString(), System.currentTimeMillis(), businessId));
    }

再看看使用此方案后的接口调用日志

2021-01-11 20:16:39.565 [http-nio-8080-exec-7] INFO  cn.mc.apd[86] - -in- GET /activityArea/getUserPrize -1348604735921459200
2021-01-11 20:16:39.565 [http-nio-8080-exec-7] INFO  cn.mc.appod[90] - Method arguments:[1] -1348604735921459200
2021-01-11 20:16:39.566 [http-nio-8080-exec-7] INFO  cn.mc.app.tood[93] - Request header:{"content-length":"0","idfa":"00000",x-nondec-sign":"d93207ba","host":"80""} -1348604735921459200
2021-01-11 20:16:39.593 [http-nio-8080-exec-7] INFO  cn.mc.app.tools.interceptor.RuntimeMethod[126] - -out- /activityArea/getUserPrize return:{"code":0,"data":{"userActivePrizeRec":"0","message":"成功"} -time:28ms

到此,关于“如何优雅打印接口调用时长 ”的学习就结束了,希望能够解决大家的疑惑。理论与实践的搭配能更好的帮助大家学习,快去试试吧!若想继续学习更多相关知识,请继续关注亿速云网站,小编会继续努力为大家带来更多实用的文章!

向AI问一下细节

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

api
AI