有时,我想记录(通过slf4j和log4j )方法的每次执行,查看其接收的参数,返回的内容以及每次执行需要多少时间。 这是我在AspectJ , jcabi-aspects和Java 6注释的帮助下进行的操作:
public class Foo {@Loggablepublic int power(int x, int p) {return Math.pow(x, p);}
}
这是我在log4j输出中看到的:
[INFO] com.example.Foo #power(2, 10): 1024 in 12μs
[INFO] com.example.Foo #power(3, 3): 27 in 4μs
很好,不是吗? 现在,让我们看看它是如何工作的。
带有运行时保留的注释
注释是Java 6中引入的一种技术。它是一种元编程工具,它不会改变代码的工作方式,但会为某些元素(方法,类或变量)提供标记。 换句话说,注释只是附加到可以查看和阅读的代码的标记。 某些注释仅在编译时可见-编译后在.class
文件中不存在。 其他的在编译后仍然可见,并且可以在运行时访问。
例如, @Override
是第一种类型(其保留类型是SOURCE
),而来自JUnit的@Test
是第二种类型(保留类型是RUNTIME
)。 @Loggable
(我在上面的脚本中使用的那个)是jcabi-aspects中第二种类型的注释。 编译后,它与.class
文件中的字节码保持在一起。
再次强调,重要的是要理解,即使方法power()
已注释和编译,它到目前为止也不会向slf4j发送任何内容。 它只包含一个标记,上面写着“请记录我的执行情况”。
面向方面的编程(AOP)
AOP是一种有用的技术,它允许在不显式更改源代码的情况下将可执行块添加到源代码。 在我们的示例中,我们不想在类内部记录方法执行。 相反,我们希望其他类拦截对方法power()
每次调用,测量其执行时间,然后将此信息发送给slf4j。
我们希望拦截器理解我们的@Loggable
批注并记录对该特定方法power()
每次调用。 而且,当然,其他的方法应使用相同的拦截器,以后我们将在其中放置相同的注释。
这种情况完全符合AOP的初衷-避免在多个类中重新实现某些常见行为。
日志记录是我们主要功能的补充功能,我们不想使用多个日志记录指令来污染我们的代码。 相反,我们希望在后台进行日志记录。
就AOP而言,我们的解决方案可以解释为创建一个方面 ,该方面在某些连接点处横切代码,并应用环绕建议以实现所需的功能。
AspectJ
让我们看看这些神奇的词是什么意思。 但是,首先,让我们看一下jcabi-aspects如何使用AspectJ实现它们(这是一个简化的示例,您可以在MethodLogger.java
找到完整的代码):
@Aspect
public class MethodLogger {@Around("execution(* *(..)) && @annotation(Loggable)")public Object around(ProceedingJoinPoint point) {long start = System.currentTimeMillis();Object result = point.proceed();Logger.info("#%s(%s): %s in %[msec]s",MethodSignature.class.cast(point.getSignature()).getMethod().getName(),point.getArgs(),result,System.currentTimeMillis() - start);return result;}
}
这是一个方面 ,里面有一个关于 around()
建议 。 方面用@Aspect
注释,而建议用@Around
注释。 如上所述,这些注释只是.class
文件中的标记。 除了为那些对运行时感兴趣的人提供一些元信息外,他们什么也不做。
注释@Around
有一个参数,在这种情况下,该参数表示在以下情况下建议应适用于方法:
- 其可见性修饰符为
*
(public
,protected
或private
); - 它的名字是名字
*
(任何名字); - 它的参数是
..
(任何参数); 和 - 它用
@Loggable
注释
如果要截获对带注释的方法的调用,则在执行实际方法之前,先执行方法around()
。 当要拦截对power()
方法的调用时, around()
方法将接收类ProceedingJoinPoint
的实例,并且必须返回一个对象,该对象将作为power()
方法的结果使用。
为了调用原始方法power()
,建议必须调用连接点对象的proceed()
。
我们编译此方面,并使其与主文件Foo.class
一起在classpath中可用。 到目前为止,一切都很好,但是我们需要采取最后一步,以便将我们的方面付诸实践-我们应该应用我们的建议。
二元方面编织
方面编织是建议应用过程的名称。 Aspect Weaver通过注入对Aspect的调用来修改原始代码。 AspectJ正是这样做的。 我们给它提供了两个二进制Java类Foo.class
和MethodLogger.class
; 它给退三-修改Foo.class
, Foo$AjcClosure1.class
和未修改MethodLogger.class
。
为了了解哪些建议应应用于哪些方法,AspectJ weaver使用了.class
文件中的注释。 同样,它使用反射来浏览类路径上的所有类。 它通过@Around
注释分析哪些方法满足条件。 当然,它找到我们的方法power()
。
因此,有两个步骤。 首先,我们使用javac
编译.java
文件,并获得两个文件。 然后,AspectJ编织/修改它们并创建自己的额外类。 编织后,我们的Foo
类如下所示:
public class Foo {private final MethodLogger logger;@Loggablepublic int power(int x, int p) {return this.logger.around(point);}private int power_aroundBody(int x, int p) {return Math.pow(x, p);}
}
AspectJ weaver将我们的原始功能移动到新方法power_aroundBody()
,并将所有power()
调用重定向到方面类MethodLogger
。
现在,我们有四个类一起工作,而不是Foo
类中的power()
方法。 从现在开始,这就是每次调用power()
幕后发生的事情:
方法power()
原始功能由图中的绿色小生命线指示。
如您所见,方面编织过程将类和方面连接在一起,并通过连接点在它们之间转移调用。 无需编织,类和方面都只是带有附加注释的已编译Java二进制文件。
jcabi方面
jcabi-aspects是一个JAR库,其中包含Loggable
注释和MethodLogger
方面(顺便说一句,还有更多方面和注释)。 您不需要编写自己的方面来进行方法记录。 只需在类路径中添加一些依赖项,然后配置jcabi-maven-plugin进行编织(在Maven Central中获取其最新版本):
<project><depenencies><dependency><dependency><groupId>com.jcabi</groupId><artifactId>jcabi-aspects</artifactId></dependency><dependency><groupId>org.aspectj</groupId><artifactId>aspectjrt</artifactId></dependency></dependency></depenencies><build><plugins><plugin><groupId>com.jcabi</groupId><artifactId>jcabi-maven-plugin</artifactId><executions><execution><goals><goal>ajc</goal></goals></execution></executions></plugin></plugins></build>
</project>
由于该编织过程需要大量的配置工作,因此我创建了一个方便的带有ajc
目标的Maven插件,该插件可以完成整个方面的编织工作。 您可以直接使用AspectJ,但是我建议您使用jcabi-maven-plugin 。
而已。 现在,您可以使用@com.jcabi.aspects.Loggable
注释,您的方法将通过slf4j记录。
如果某些内容无法按照说明进行操作,请随时提交Github问题 。
相关文章
您可能还会发现以下有趣的帖子:
- 如何在异常上重试Java方法
- 缓存Java方法结果
- 摆脱Java静态记录器
- 限制Java方法执行时间
- 简单的Java SSH客户端
翻译自: https://www.javacodegeeks.com/2014/09/java-method-logging-with-aop-and-annotations.html