其实很多年前就做过如此的实验,一翻开自己的日志有关于aspectj site:unmi.cc, 可以找到 2008 年写的日志。真是流光容易把人抛,红了樱桃,绿了巴蕉。只是那时候 Spring 刚步入 2.0, 才翻开强大 AOP 的篇章,还记得彼时只要是直接使用 AspectJ 就要写 *.aj 文件。而如今 Spring 都到 5.0 了,也就是一年前才重拾起 Spring, 这期间 AspectJ 早就可以不用 *.aj 文件,只需普通 Java 文件,加上 @Aspect 和 @Pointcut 之类的注解就行。
本文内容与几年前写过的日志大体相差不大,再缀上一篇纯粹是个人笔记。这里不以 Spring 5.0 为例,仍然是最新的 4.3.11.RELEASE, 并且直接用 Spring, 而非选择 Spring Boot, 因为用了 Spring Boot 常常搞不清楚哪些是自动配置了的。原生的 Spring 可以使自己掌握一个 Spring AOP 的基本要素。
需求:@LogStartTime 注解的方法,在每次进入该方法时把当前时间写入 ThreadLocal 中去,被 @LogStartTime 注解的方法中随时可以获得进入方法的时间
接下来怎么能离开最具说明问题的代码呢?我们会通过 Main 方法和测试用例来验证。
这是项目所有的文件,下面逐一列出内容(为不值钱的篇幅考虑还是会对内容有所裁剪,比如省略 import 部分)
org.springframework:spring-context:4.3.11.RELEASE:runtime org.aspectj:aspectjweaver:1.8.9:runtime javax.inject:javax.inject:1:runtime junit:junit:4.12:test org.springframework:spring-test:4.3.11.RELEASE:runtime
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface LogStartTime { }
@Named //它也必须是一个 Spring bean,它要能被 Spring 扫描到 @Aspect //说明这是一个切面定义类 public class MethodStartAspect { private static ThreadLocal<Long> startTime = new ThreadLocal<>(); @Pointcut("@annotation(cc.unmi.aspects.LogStartTime)") private void logStartTime() { //凡是被 LogStartTime 注解的方法都是 logStartTime() 切面 } @Before("logStartTime()") //进入切面 logStartTime() 之前把当前系统时间存入 startTime, 以备后用 public void setStartTimeInThreadLocal() { startTime.set(System.currentTimeMillis()); System.out.println("saved method start time in threadLocal"); } public static Long getStartTime() { return startTime.get(); } public static void clearStartTime() { startTime.set(null); } }
这个类里就是用的 AspectJ 的语法来定义切面和 Advice 的,也可以定义普通方法。它同样是一个必须由 Spring 来管理的 Bean, 所以有 @Name 注解。代码中有详细的注释。
@Configuration @EnableAspectJAutoProxy //在原生 Spring 中这个是必须的,在 Spring Boot 中默认是被启用了的 @ComponentScan(basePackages = "cc.unmi") //注意这个要兼顾到 @AspectJ 注释的类 public class AppConfig { }
这个配置要能扫描到切面定义(@Aspect) 的类,如上面的 MethodStartAspect
@Named public class UserService { @LogStartTime //因为有了这个注解,在方法中便随时能拿到进入该方法的时间,在 ThreadLocal 中 public String fetchUserById(int userId) { try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("start time: " + MethodStartAspect.getStartTime() + ", execution time: " + (System.currentTimeMillis() - MethodStartAspect.getStartTime())); return "nameOf" + userId; } }
public class HelloAop { public static void main(String[] args) { ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class); UserService userService = context.getBean(UserService.class); System.out.println(userService.fetchUserById(234)); } }
执行后输出如下:
saved method start time in threadLocal
start time: 1506746277397, execution time: 2033
nameOf234
@ContextConfiguration(classes = AppConfig.class) @RunWith(SpringJUnit4ClassRunner.class) public class HelloAopTest { @Inject private UserService userService; @Before public void setup() { MethodStartAspect.clearStartTime(); } @Test public void testSettingMethodStartTimeInThreadLocal() { userService.fetchUserById(9999); assertThat(MethodStartAspect.getStartTime(), notNullValue()); } }
执行后绿了
今天测试中碰到过在多模块的 Maven 项目中,把切面定义类 MethodStartAspect 放到另一个模块中方法拦截便失效了,不过刚刚的实验却是正常的,不知为何。
再进一步,在注解 @LogStartTime 中,我们可以定义属性,然后在切面中根据注解属性的不同作出不同的行为响应,如清楚日志的 MDC ContextMap 等。
最好的帮助应该还是 Spring 的官方文档 AOP 篇。