200303-如何优雅的在java中统计代码块耗时

文章目录
  1. 1. 代理方式
  2. 2. AutoCloseable
  3. 3. 小结
  • II. 其他
    1. 1. 一灰灰Blog: https://liuyueyi.github.io/hexblog
    2. 2. 声明
    3. 3. 扫描关注
  • 在我们的实际开发中,多多少少会遇到统计一段代码片段的耗时的情况,我们一般的写法如下

    1
    2
    3
    4
    5
    6
    long start = System.currentTimeMillis();
    try {
    // .... 具体的代码段
    } finally {
    System.out.println("cost: " + (System.currentTimeMillis() - start));
    }

    上面的写法没有什么毛病,但是看起来就不太美观了,那么有没有什么更优雅的写法呢?

    1. 代理方式

    了解Spring AOP的同学可能立马会想到一个解决方法,如果想要统计某个方法耗时,使用切面可以无侵入的实现,如

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 定义切点,拦截所有满足条件的方法
    @Pointcut("execution(public * com.git.hui.boot.aop.demo.*.*(*))")
    public void point() {
    }

    @Around("point()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
    long start = System.currentTimeMillis();
    try{
    return joinPoint.proceed();
    } finally {
    System.out.println("cost: " + (System.currentTimeMillis() - start));
    }
    }

    Spring AOP的底层支持原理为代理模式,为目标对象提供增强功能;在Spring的生态体系下,使用aop的方式来统计方法耗时,可以说少侵入且实现简单,但是有以下几个问题

    2. AutoCloseable

    在JDK1.7引入了一个新的接口AutoCloseable, 通常它的实现类配合try{}使用,可在IO流的使用上,经常可以看到下面这种写法

    1
    2
    3
    4
    5
    6
    7
    // 读取文件内容并输出
    try (Reader stream = new BufferedReader(new InputStreamReader(new FileInputStream("/tmp")))) {
    List<String> list = ((BufferedReader) stream).lines().collect(Collectors.toList());
    System.out.println(list);
    } catch (IOException e) {
    e.printStackTrace();
    }

    注意上面的写法中,最值得关注一点是,不需要再主动的写stream.close了,主要原因就是在try(){}执行完毕之后,会调用方法AutoCloseable#close方法;

    基于此,我们就会有一个大单的想法,下一个Cost类实现AutoCloseable接口,创建时记录一个时间,close方法中记录一个时间,并输出时间差值;将需要统计耗时的逻辑放入try(){}代码块

    下面是一个具体的实现:

    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
    public static class Cost implements AutoCloseable {
    private long start;

    public Cost() {
    this.start = System.currentTimeMillis();
    }

    @Override
    public void close() {
    System.out.println("cost: " + (System.currentTimeMillis() - start));
    }
    }

    public static void testPrint() {
    for (int i = 0; i < 5; i++) {
    System.out.println("now " + i);
    try {
    Thread.sleep(10);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    }

    public static void main(String[] args) {
    try (Cost c = new Cost()) {
    testPrint();
    }
    System.out.println("------over-------");
    }

    执行后输出如下:

    1
    2
    3
    4
    5
    6
    7
    now 0
    now 1
    now 2
    now 3
    now 4
    cost: 55
    ------over-------

    如果代码块抛异常,也会正常输出耗时么?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public static void testPrint() {
    for (int i = 0; i < 5; i++) {
    System.out.println("now " + i);
    try {
    Thread.sleep(10);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    if (i == 3) {
    throw new RuntimeException("some exception!");
    }
    }
    }

    再次输出如下,并没有问题

    1
    2
    3
    4
    5
    6
    7
    8
    now 0
    now 1
    now 2
    now 3
    cost: 46
    Exception in thread "main" java.lang.RuntimeException: some exception!
    at com.git.hui.boot.order.Application.testPrint(Application.java:43)
    at com.git.hui.boot.order.Application.main(Application.java:50)

    3. 小结

    除了上面介绍的两种方式,还有一种在业务开发中不太常见,但是在中间件、偏基础服务的功能组件中可以看到,利用Java Agent探针技术来实现,比如阿里的arthas就是在JavaAgent的基础上做了各种上天的功能,后续介绍java探针技术时会专门介绍

    下面小结一下三种统计耗时的方式

    基本写法

    1
    2
    3
    4
    5
    6
    long start = System.currentTimeMillis();
    try {
    // .... 具体的代码段
    } finally {
    System.out.println("cost: " + (System.currentTimeMillis() - start));
    }

    优点是简单,适用范围广泛;缺点是侵入性强,大量的重复代码

    Spring AOP

    在Spring生态下,可以借助AOP来拦截目标方法,统计耗时

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Around("...")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
    long start = System.currentTimeMillis();
    try{
    return joinPoint.proceed();
    } finally {
    System.out.println("cost: " + (System.currentTimeMillis() - start));
    }
    }

    优点:无侵入,适合统一管理(比如测试环境输出统计耗时,生产环境不输出);缺点是适用范围小,且粒度为方法级别,并受限于AOP的使用范围

    AutoCloseable

    这种方式可以看做是第一种写法的进阶版

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // 定义类
    public static class Cost implements AutoCloseable {
    private long start;

    public Cost() {
    this.start = System.currentTimeMillis();
    }

    @Override
    public void close() {
    System.out.println("cost: " + (System.currentTimeMillis() - start));
    }
    }

    // 使用姿势
    try (Cost c = new Cost()) {
    ...
    }

    优点是:简单,适用范围广泛,且适合统一管理;缺点是依然有代码侵入

    说明

    上面第二种方法看着属于最优雅的方式,但是限制性强;如果有更灵活的需求,建议考虑第三种写法,在代码的简洁性和统一管理上都要优雅很多,相比较第一种可以减少大量冗余代码

    II. 其他

    1. 一灰灰Bloghttps://liuyueyi.github.io/hexblog

    一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

    2. 声明

    尽信书则不如,已上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激

    3. 扫描关注

    一灰灰blog

    QrCode

    评论

    Your browser is out-of-date!

    Update your browser to view this website correctly. Update my browser now

    ×