编写Java注解以记录方法调用时间

33

我想编写一个Java注解以计时方法调用。类似于这样:

@TimeIt
public int someMethod() { ... }

当调用此方法时,应在控制台上输出此方法所需的时间。

我知道如何在Python中实现这一点,这就是我想要它做的:

from time import time, sleep

def time_it(func):
    def wrapper(*args, **kwargs):
        start = time()
        func(*args, **kwargs)
        stop = time()
        print "The function", func.__name__, " took %.3f" % (stop - start)
    wrapper.__name__ = func.__name__
    return wrapper

@time_it
def print_something(*args, **kwargs):
    print "before sleeping"
    print args, kwargs
    sleep(3) # wait 3 seconds
    print "after sleeping"

print_something(1, 2, 3, a="what is this?")

那么我的问题是? 我在尝试使用apt文档来编写类似的内容,但没有成功。 有人可以帮助我编写类似的东西吗?


我制作了一个类似Python的简单装饰器注解处理工具,它可以通过包装方法来转换您的方法。您可以在https://github.com/eshizhan/funcwraps找到它。 - eshizhan
9个回答

13
据我所知,Tomasz在说这不能使用注解来完成是正确的。我认为混淆的原因在于Python装饰器和Java注解具有相同的语法,但它们在提供的行为方面完全不同!
注解是附加到您的类/方法/字段上的元数据。 这篇博客文章讨论了使用AOP计时方法的方法。虽然它使用Spring,但基本原理仍然相同。如果您愿意使用AOP编译器,将代码翻译成另一种语言并不太困难。 另一个参考(Spring特定)在这里编辑: 如果您的目标是对应用程序进行总体方法计时而不使用完整的分析器,则可以使用hprof收集总执行统计信息。

9

简单来说:你不能这样做!

注解不是随着代码自动启动的代码片段,它们只是注解,信息片段,可以被其他程序使用,比如加载或运行代码。

你需要的是AOP:面向切面编程。


那么,最干净的方法是什么,可以找出某个方法所花费的时间? - roopesh
看起来不像你和Sanjay建议的那样简单粗暴,我会研究一下Spring中的AOP。 - roopesh
1
另一个选项是使用分析器,比学习AOP容易得多,并且可以准确地测量方法执行时间(不仅如此,它还可以测量内存使用情况,查找内存泄漏,线程死锁等)。 - Tomasz Stanczak
我刚刚看到Sanjay已经建议使用性能分析器,他绝对是正确的。 - Tomasz Stanczak

5
截至2016年,有一个很棒的方面注释库jcabi-aspects
从文档中可以看到:
使用@Loggable注释注解你的方法,每次调用时,你的SLF4J日志记录设施都会收到一条包含执行细节和总执行时间的消息。
public class Resource {
  @Loggable(Loggable.DEBUG)
  public String load(URL url) {
    return url.openConnection().getContent();
  }
}

类似这样的内容会出现在日志中:

[DEBUG] #load('http://www.google.com'): returned "<html ..." in 23ms

关于@Loggable的更多信息,请点击这里


5
我曾多次想知道这个问题,并最终写出以下开头:
注释:
package main;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Clocking {

}

对象的接口:

package main;

public interface Examples {
    @Clocking
    void thisIsAMethod();

    void thisIsAnotherMethod(String something);

    @Clocking
    void thisIsALongRunningMethod();
}

一个调用处理程序:
package main;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.time.Duration;
import java.time.Instant;

public class ExamplesInvocationHandler implements InvocationHandler {
    // ******************************
    // Fields
    // ******************************
    private Examples examples = new ExamplesImpl();

    // ******************************
    // Public methods
    // ******************************
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // If the annotation is not present, just redirect the method call to its origin...
        if(!method.isAnnotationPresent(Clocking.class)) {
            return method.invoke(examples, args);
        }

        // ... otherwise log the execution time of it.
        Instant start = Instant.now();
        Object returnObj = method.invoke(examples, args);
        Instant end = Instant.now();

        // TODO: This is for demonstration purpose only and should use the application's logging system.
        System.out.println("Method " + method.getName() + " executed in " + Duration.between(end, start) + ".");

        return returnObj;
    }

    // ******************************
    // Inner classes
    // ******************************
    private static class ExamplesImpl implements Examples {
        @Override
        public void thisIsAMethod() {
            System.out.println("thisIsAMethod called!");
        }

        @Override
        public void thisIsAnotherMethod(String something) {
            System.out.println("thisIsAnotherMethod called!");
        }

        @Override
        public void thisIsALongRunningMethod() {
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println("thisIsALongRunningMethod called!");
        }
    }
}

最后,这是一个测试此功能的入口点:
package main;
import java.lang.reflect.Proxy;

public class Main {
    public static void main(String[] args) {
        Examples examples = (Examples) Proxy.newProxyInstance(Examples.class.getClassLoader(), new Class[]{Examples.class}, new ExamplesInvocationHandler());

        examples.thisIsAMethod();
        examples.thisIsAnotherMethod("");
        examples.thisIsALongRunningMethod();
    }
}

这需要改进,因为它需要代理来实例化我们的对象,所以你不能真正用它来处理 "通用已编写" 的代码。但它可能会带领你找到更完整的解决方案。


4

请查看Coda Hale Metrics库。它提供了一个@Timed注释,用于提供此功能的方法。在此过程中,请查看Code Hale Dropwizard,其中包含如何将其集成到他们的服务框架中的示例。

@GET
@Timed
public Saying sayHello(@QueryParam("name") Optional<String> name) {
    return new Saying(counter.incrementAndGet(),
                      String.format(template, name.or(defaultName)));
}

2

尽管有许多反对者,但您仍然可以做到这一点。Java注释不能更改它们所操作的源文件或类文件,因此您的选项是:

1)使用超类。注释处理器可以生成一个超类来计时抽象方法。您的实际类实现此方法。缺点是您想计时的方法必须重命名,以便超类可以提供实现。结果可能看起来像这样:

@BenchmarkMe( extend="MySuperClass" )
public class MyClass extends BenchmarkMyClass {
    public void normalMethod() { ... }
    public void bench_myMethod() { ... }
}  

注释过程将生成:

public class BenchmarkMyClass extends MySuperClass {
    public abstract void bench_myMethod();
    public void myMethod() {
       benchmarkStart();
       try {
          bench_myMethod();
       } finally { benchmarkStop(); }
    }
}

使用命名约定来指示应计时哪些方法,例如在我的示例中使用前缀“bench_”。
2)同时使用ClassFileTranformer和注释 该方法是创建一个可用于标记您有兴趣计时的方法的运行时注释。在运行时,指定了一个ClassFileTransformer,并且它会转换字节码以插入计时代码。
除非你喜欢使用字节码,否则使用AOP是更好的选择,但这是可能的。

2
我很惊讶地发现没有人指出java.lang.reflect.Proxy。虽然这是一个旧的线程,但我认为这些信息对某些人会有所帮助。
Proxy具有一个有趣的属性,可以使proxy instanceof Foo返回true。
您可以在调用处理程序中拥有一个方法,该方法首先打印时间,然后再从对象中触发实际方法。
您可以通过使它们实现某些接口或使用Comparable来为所有对象创建此代理。
查找Dynamic proxies作为装饰器的部分。 http://www.ibm.com/developerworks/library/j-jtp08305/

1

在Java中这并不是那么容易。基本的想法是:

  1. 创建一个注解,表示“计时这个方法”
  2. 创建一个Java代理程序,使用字节码转换来: a. 查找带有注解的方法 b. 为它们添加计时代码
  3. 在运行Java时设置javaagent选项以使用您的新代理程序

这篇文章可以让您入门:http://today.java.net/pub/a/today/2008/04/24/add-logging-at-class-load-time-with-instrumentation.html

您还可以尝试使用BTrace使其更加容易:http://kenai.com/projects/btrace/pages/Home


0
如前所述,您无法使用AOP或hprof来满足大部分需求,但如果您坚持,可以使用JSR269进行解决。值得一提的是,apt已经过时了,并且一个名为JSR269的注解处理API和工具已经并入了1.6版本中。
解决方法是创建一个注解处理器,生成一个继承包含带有@TimeIt注解的方法的类。这个生成的类必须重写计时方法,它看起来像Python的time_it,但func(*args, **kwargs)这一行将被替换为super.methodName(arg1, arg2, ...)
然而,有两个注意事项:
  1. 在代码其他地方,您必须确保创建生成类的实例而不是原始类的实例。这是一个问题,因为您引用了一个尚不存在的类:它将在第一轮处理结束时创建。
  2. 您需要熟悉javax.annotation.processing和javax.lang.model包,它们有些不太方便,我个人认为。

是的,我知道apt已经过时了,现在已经成为1.6的一部分。 - roopesh

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接