本文链接:用aspectj实现日志记录
简介
记录日志在很多情况下都是必需的。但很多时候,日志与逻辑的代码混杂,让人难以阅读代码。如果能将记录日志与代码逻辑分离,就可以在满足日志需求的同时,让代码更加简明了。可以使用aspectj来达到这一目的。
aspectj提供了面向切面编程的功能。他可以“切开”原本的代码,并在切面上插入另一些代码,达到更改代码的目的。这个功能正适合于记录日志的场景。
在项目中使用aspectj
因为aspectj自己的编译过程,所以并不是加一个库就可以的。如果使用IDE编程,还需要设置相应的环境。eclipse有相应的插件,intellij pro支持aspectj功能,maven也有相应的插件。
我自己使用的是intellij community,所以并没有原生支持aspectj,不过它支持maven,所以我用了maven项目,通过一些配置,也可以达到在IDE中使用aspectj的目的。
在maven中加入aspectj的运行时库:
1 2 3 4 5 6 7 |
<dependencies> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjrt</artifactId> <version>1.8.10</version> </dependency> </dependencies> |
在maven中加入aspectj编译的插件,这里使用了一个第三方的插件:
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 31 32 33 34 35 36 37 38 39 40 41 |
<build> <plugins> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>aspectj-maven-plugin</artifactId> <version>1.10</version> <configuration> <source>1.7</source> <target>1.7</target> <complianceLevel>1.7</complianceLevel> <!-- 以上三个选项设置代码的Java版本 --> <verbose>true</verbose> <showWeaveInfo>true</showWeaveInfo> <!-- 以上两个选项用来显示更多信息,方便调试 --> </configuration> <executions> <execution> <phase>process-classes</phase> <goals> <goal>compile</goal> <goal>test-compile</goal> </goals> </execution> <!-- 在compile和test-compile过程中加入aspectj编译 --> </executions> <dependencies> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.8.10</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjtools</artifactId> <version>1.8.10</version> </dependency> <!-- 使用aspectj的编译工具 --> </dependencies> </plugin> </plugins> </build> |
加入这些之后,就可以编写和编译包含aspectj的程序了。但是仍然不能在intellij里运行和调试,因为intellij默认不会编译aspectj相关内容的。为此,可以在maven project窗口中将apectj compile设置成为运行前自动执行(做法如下图)。这样每次运行时就会自动编译aspectj了。
编写aspectj
aspectj有两种写法,一是使用独特的语法,文件扩展名为aj,二是使用aspectj的注解。因为intellij community没有支持aspectj,aj文件也没有编辑器,所以在这里我使用了注解的方式来编写aspectj。
首先我定义了一个aspect类,这个类带有@Aspect
注解,aspectj在编译时会查看所有的类,如果它带有这个注解,就认为这个类不是普通的类,而是定义了切面相关的信息。
1 2 3 4 5 6 |
package me.herbix.aspectjtest.aspect; @Aspect public class JustAnotherAspect { } |
下一步就是定义切点,在刚才的类中,加入切点的定义。切点使用空方法加上@Pointcut
注解来定义,注解中有一个字符串参数,定义了切点的信息。在allMethodCutpoint
中使用了execution
和within
两个配置。execution
定义切点是切在执行方法时,其参数为方法的全定义,可以用通配符来定义。这里的* *..*(..)
中的第一个*
表示这是一个方法而不是类,*..*
是方法全称,*
代替任何不含.
的字符串,..
代替含.
的字符串,(..)
表示参数列表。within
定义切点所在的位置,配置间也可以用逻辑运算符连接。所以这个切点的意思是匹配所有方法,除了当前切面内的方法。对于一些切面,如果不去掉切面内的方法的话,可能会产生栈溢出的问题。
1 2 |
@Pointcut("execution(* *..*(..)) && !within(me.herbix.aspectjtest.aspect.JustAnotherAspect)") private void allMethodPointcut() {} |
最后是定义插入的内容,在aspectj中,这些内容被称为advice。还是在刚才的切面中,编写一个方法,方法的内容是要插入的内容。给方法加入@Before
注解,这个注解表示在切点之前加入内容,它包含了一个参数,表示要插入的切点。
1 2 3 4 |
@Before("allMethodPointcut()") public void beforeAllMethod() { System.out.println("I call a method! "); } |
另外建一个类,加入空的main
方法,运行,发现输出:
1 |
I call a method! |
这说明在main
方法调用前这一切点上,加入了我们想运行的代码。
处理运行错误
如果出现了Duplicate Name&Signature的错误,可以修改maven的编译过程,让切面类不被Java编译,而是之后被aspectj编译:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.6.2</version> <configuration> <source>1.7</source> <target>1.7</target> <excludes> <exclude>me/herbix/aspectjtest/aspect/**</exclude> </excludes> </configuration> </plugin> </plugins> </build> |
记录方法信息
可以看到这段程序中没有任何外部信息,比如被调用的方法等等,这样是无法记录成日志的。aspectj提供了JoinPoint
类,用来记录切点的信息,只需要将它加入参数,就可以使用它。
1 2 3 4 |
@Before("allMethodPointcut()") public void beforeAllMethod(JoinPoint joinPoint) { System.out.println("I call " + joinPoint.getSignature() + "!"); } |
重新运行,输出:
1 |
I call void me.herbix.aspectjtest.Entry.main(String[])! |
过滤日志和自定义信息
上面的例子里会给所有方法加上日志,但很多时候大部分方法我们都不需要给他们加日志。另一方面,日志的内容应该能够让用户自己定义,给不同的类加上不同的信息,而不是千篇一律。为了达到这个目的,可以自定义注解,并且使用aspectj在切点处读取参数的功能。
首先自定义注解:
1 2 3 4 5 6 7 8 9 |
package me.herbix.aspectjtest.aspect; import java.lang.annotation.*; @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface GodLog { String value() default "Default Message"; } |
然后定义切点和要插入的内容:
1 2 3 4 5 6 7 |
@Pointcut("execution(* *..*(..)) && @annotation(logAnno) && !within(me.herbix.aspectjtest.aspect.JustAnotherAspect)") private void godLogPointcut(GodLog logAnno) {} @Before("godLogPointcut(logAnno)") public void beforeGodLogMethod(GodLog logAnno, JoinPoint joinPoint) { System.out.println("I call " + joinPoint.getSignature() + "! Message: " + logAnno.value()); } |
相比之前的切点,这个切点增加了一个配置:@annotation
,这个配置会检查切点对应目标的注解,配置参数为一个变量名,这个变量名对应的变量在切点方法里作为参数传入,并指定了类型GodLog
。所以,这个配置的意思是匹配含有GodLog
注解的目标。同样,这个注解可以在@Before指定的方法内得到,也是基于变量名相同的准则。
因为限定了条件,所以要在main方法前加上@GodLog
:
1 |
@GodLog("main start") |
执行,就会输出:
1 |
I call void me.herbix.aspectjtest.Entry.main(String[])! Message: main start |
总结
aspectj能够做到的功能远远不止于此,它可以在几乎任何地方插入切点,也就意味着可以用这个方法在很多地方插入日志。本文只是简单应用了aspectj,如果能够深入了解并使用aspectj,一定能够达到方便神奇的效果。
不起作用,请指教
刚开始的demo
@Before(“allMethodPointcut()”)
public void beforeAllMethod() {
System.out.println(“I call a method! “);
}
这里不起作用,运行空main后没有任何输出。
望能指教
个人skype: [email protected]
或邮件: [email protected]
可以反编译一下编译后的class文件看看,可能的原因是aspectj的编译指令没有运行。如果运行了的话可以看到main方法中有语句调用aspectj相关方法。
似乎已经找到原因。
要先运行”mvn clean package”或”mvn clean install”, 然后再运行空的main方法