AspectJ with Xml 和 AspectJ with Annotation 中介绍了 2 中实现 AoP 的方式:
这里我们介绍实现 AoP 的第三种方法: 使用注解配置切面和切入点,主要有以下几个部分:
Gradle 依赖
自定义注解
自定义 AOP 使用注解配置切入点
使用自定义注解
Xml 文件中配置自动扫描包路径
测试
三种实现切面方式的比较
使用 SpEL 增强注解
下面就以实现一个分布式锁的注解来进行介绍。
一、Gradle 依赖 项目里通常会使用 Lombok 简化编码,JUnit + Spring Test 进行测试,Logback 作为日志框架,这几个依赖都不是必须的,可以视具体情况而定是否使用:
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 ext.versions = [ spring : '5.0.2.RELEASE' , aspectj: '1.9.4' , lombok : '1.16.20' , logback: '1.3.0-alpha4' , slf4j : '1.7.25' , junit : '4.12' ] dependencies { compile( "org.springframework:spring-context-support:${versions.spring}" , "org.springframework:spring-aop:${versions.spring}" , "org.aspectj:aspectjweaver:${versions.aspectj}" , ) compile( "ch.qos.logback:logback-classic:${versions.logback}" , "org.slf4j:jcl-over-slf4j:${versions.slf4j}" , "org.slf4j:jul-to-slf4j:${versions.slf4j}" , "org.slf4j:log4j-over-slf4j:${versions.slf4j}" , ) testCompile "junit:junit:${versions.junit}" testCompile "org.springframework:spring-test:${versions.spring}" compileOnly "org.projectlombok:lombok:${versions.lombok}" annotationProcessor "org.projectlombok:lombok:${versions.lombok}" }
二、自定义注解 定义了注解 Lock,某个方法上使用 Lock 进行注解时,这个方法在执行时就会使用分布式锁进行保护。
1 2 3 4 5 6 7 8 9 10 11 12 package com.xtuer.annotation;import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Lock { String value () default "" ; }
注解的实例对象是用来存储数据的 (静态数据,编译期确定),Spring 会在方法调用的时候自动创建这个注解的实例对象,我们可以在 AoP 的 Advice 方法中获取到这些数据。
注解实例对象的数据可以是写代码时写死的 SpEL 表达式的静态字符串,运行时根据方法的参数动态计算它要表达的值。
三、自定义 AoP 使用注解配置切入点 Spring AoP 使用自定义注解配置切入点很简单, 设置 @Pointcut
的值为我们自定义的注解的全路径名即可,在 Advice 方法 (@Around
注解的方法) 处直接引用 pointcut 和注解 Lock 的对象 (Spring 自动注入):
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 package com.xtuer.aspect; import com.xtuer.annotation.Lock;import lombok.extern .slf4j.Slf4j;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.Around;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Pointcut;import org.springframework.stereotype.Component;@Aspect @Slf4j @Component public class LockAspect { @Pointcut ("@annotation(com.xtuer.annotation.Lock)" ) public void pointcut () {} @Around ("pointcut() && @annotation(lock)" ) public Object doAround (ProceedingJoinPoint pjp, Lock lock) throws Throwable { try { log.info ("Lock: {}" , lock.value ()); return pjp.proceed (); } finally { log.info ("Unlock: {}" , lock.value ()); } } }
提示:
分布式锁的实现部分不是这里讨论的重点,所以使用了伪代码,具体实现可参考 ZooKeeper 实现分布式锁 。
AOP 代理对象的创建是 Spring 初始化 Bean 完成后在 BeanPostProcessor.postProcessAfterInitialization()
的调用中进行的。
四、使用自定义注解 在 Service.foo()
上使注解 Lock
,期待 Service.foo()
执行时使用上面定义的 LockAspect.doAround()
,Service.bar()
上不使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package com.xtuer.service;import com.xtuer.annotation.Lock;import lombok.extern.slf4j.Slf4j;import org.springframework.stereotype.Service;@Service @Slf4j public class XService { @Lock("lock-foo") public void foo () { log.info("XService::foo()" ); } public void bar () { log.info("XService::bar()" ); } }
五、Xml 文件中配置自动扫描包路径 Service 和 Aspect 对象通过 Spring 的包扫描自动生成,就不再使用 <bean>
的方式一个一个的手动创建了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <?xml version="1.0" encoding="UTF-8"?> <beans xmlns ="http://www.springframework.org/schema/beans" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xmlns:context ="http://www.springframework.org/schema/context" xmlns:aop ="http://www.springframework.org/schema/aop" xsi:schemaLocation =" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd" > <aop:aspectj-autoproxy /> <context:component-scan base-package ="com.xtuer.service, com.xtuer.aspect" /> </beans >
在 Spring 容器中创建 LockAspect 的实例后我们自定义的注解才会被 Spring AoP 执行。
六、测试 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import com.xtuer.service.XService;import org.junit.Test;import org.junit.runner.RunWith;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.test.context.ContextConfiguration;import org.springframework.test.context.junit4.SpringRunner;@RunWith(SpringRunner.class) @ContextConfiguration({"classpath:bean.xml"}) public class XServiceTest { @Autowired private XService service; @Test public void testFoo () { service.foo(); } @Test public void testBar () { service.bar(); } }
输出:
1 2 3 4 5 [2019-07-31 22:30:28] [INFO ] [LockAspect.java-doAround:28] - Lock: lock-foo [2019-07-31 22:30:28] [INFO ] [XService.java-foo:12] - XService::foo() [2019-07-31 22:30:28] [INFO ] [LockAspect.java-doAround:34] - Unlock: lock-foo [2019-07-31 22:30:28] [INFO ] [XService.java-bar:16] - XService::bar()
从输出的日志中可以看到 foo() 上的注解 Lock 生效了,bar() 不受影响,符合我们的期望。
七、三种实现切面方式的比较 到目前我们了解了三种实现切面的方式,它们各有优缺点,适用于不同的场景:
AspectJ with Xml : 开启、关闭、修改切入点都只需要修改 xml 配置文件,不需要修改和编译代码,重启程序后就能生效,但是 AoP 配置和逻辑代码分开了,不够直观,不过胜在灵活。
AspectJ with Annotation : AoP 配置写死在代码里,修改时需要修改和编译代码,然后发布以及启动程序,并且不利于提供给第三方使用,因为不知道第三方是怎么命名类和方法的,优点是 AoP 配置和逻辑代码放在一起,只是自己使用的时候比较方便一点,但 AoP 和使用它的地方是分离开的。
AspectJ with Annotation - 自定义注解 : AoP 里只和注解名关联,不限制类和方法名等,非常适合作为库提供给第三方使用,第三方只需要在自己的代码里使用我们提供的注解即可,当然第三方要使用或者删除这个注解就需要修改他们自己的代码了。
当然也有优雅的方式,通过注解来开启是否使 @Lock
生效: 去掉上面 LockAspect 的 @Component
注解,并创建注解 EnableLock。使用 EnableLock 注解时再 @Import({ LockAspect.class })
创建 LockAspect 的对象。更多详情请参考 SpringBoot 中的 @Import 注解 。
每种方式都有优缺点,但是根据观察,现在使用自定义注解实现 AoP 的方式更受欢迎,方便以库的方式提供给第三方使用,例如阿里的 JetCache (简单使用可参考 Spring MVC 中使用 JetCache )。
八、使用 SpEL 增强注解 上面在代码里写死了锁的名字: @Lock("lock-foo")
,也即是说同一个功能在分布式系统里同时只能在一个服务器中执行。例如我们有一个视频转换服务,有 10 台执行视频转换的服务器,使用目前这个分布式锁的话,同时就只能有一台服务器执行转换操作,其他几台都在围观,我们期望的是同名文件的视频同时只能在一个服务器上转换 (避免重复转换),不同的服务器可以同时转换不同名的视频,也就是说锁的名字与文件名有关,而不是写死的,可以借助 SpEL 来实现这个功能。
先看使用 SpEL 计算一个表达式的值的简单例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import org.springframework.expression.EvaluationContext;import org.springframework.expression.Expression;import org.springframework.expression.ExpressionParser;import org.springframework.expression.spel.standard.SpelExpressionParser;import org.springframework.expression.spel.support.StandardEvaluationContext;public class Test { public static void main (String[] args) { ExpressionParser parser = new SpelExpressionParser(); EvaluationContext context = new StandardEvaluationContext(); Expression expression = parser.parseExpression("'hello-' + #id" ); context.setVariable("id" , 123 ); String result = expression.getValue(context, String.class); System.out.println(result); } }
通过上面这个例子,我们可以把 SpEL 表达式作为 Lock 的 value (如 @Lock("#filename")
),使用反射从方法的参数中获得文件名,使用 SpEL 动态的计算出文件名相关的锁的名字,这样就能够实现转换不同的文件时使用不同的分布式锁,多个服务器就能并发的进行转换了:
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 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 package com.xtuer.aspect;import com.xtuer.annotation.Lock;import lombok.extern.slf4j.Slf4j;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.Around;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Pointcut;import org.aspectj.lang.reflect.MethodSignature;import org.springframework.core.LocalVariableTableParameterNameDiscoverer;import org.springframework.expression.EvaluationContext;import org.springframework.expression.Expression;import org.springframework.expression.ExpressionParser;import org.springframework.expression.spel.standard.SpelExpressionParser;import org.springframework.expression.spel.support.StandardEvaluationContext;import org.springframework.stereotype.Component;import java.lang.reflect.Method;@Aspect @Slf4j @Component public class LockAspect { private ExpressionParser parser = new SpelExpressionParser(); private LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer(); @Pointcut("@annotation(com.xtuer.annotation.Lock)") public void pointcut () {} @Around("pointcut() && @annotation(lock)") public Object doAround (ProceedingJoinPoint pjp, Lock lock) throws Throwable { String lockName = buildLockName(pjp, lock); try { log.info("Lock: {}" , lockName); return pjp.proceed(); } finally { log.info("Unlock: {}" , lockName); } } private String buildLockName (ProceedingJoinPoint pjp, Lock lock) { String spel = lock.value(); if (!spel.contains("#" )) { return spel; } Method method = ((MethodSignature) pjp.getSignature()).getMethod(); String[] params = discoverer.getParameterNames(method); Object[] args = pjp.getArgs(); EvaluationContext context = new StandardEvaluationContext(); if (params != null ) { for (int len = 0 ; len < params.length; len++) { context.setVariable(params[len], args[len]); } } Expression expression = parser.parseExpression(spel); return expression.getValue(context, String.class); } }
注解 + SpEL 的使用:
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 package com.xtuer.service;import com.xtuer.annotation.Lock;import com.xtuer.bean.User;import lombok.extern.slf4j.Slf4j;import org.springframework.stereotype.Service;@Service @Slf4j public class XService { @Lock("noom") public void noom () { log.info("XService::noom()" ); } @Lock("#id") public void foo (int id) { log.info("XService::foo()" ); } @Lock("#user.username") public void bar (User user) { log.info("XService::bar()" ); } @Lock("'lock-' + #id") public void goo (int id) { log.info("XService::goo()" ); } }
附上测试用例:
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 import com.xtuer.bean.User;import com.xtuer.service.XService;import org.junit.Test;import org.junit.runner.RunWith;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.test.context.ContextConfiguration;import org.springframework.test.context.junit4.SpringRunner;@RunWith(SpringRunner.class) @ContextConfiguration({"classpath:bean.xml"}) public class XServiceTest { @Autowired private XService service; @Test public void testFoo () { service.foo(10010 ); } @Test public void testBar () { service.bar(new User("Bob" )); } @Test public void testNoom () { service.noom(); } @Test public void testGoo () { service.goo(10010 ); } }
不同的 SpEL 和参数,计算得到的锁名不同,输出如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 2019-08-02 13:35:48.896 INFO [42-LockAspect.java-doAround()] - Lock: 10010 2019-08-02 13:35:48.896 INFO [20-XService.java-foo() ] - XService::foo() 2019-08-02 13:35:48.896 INFO [48-LockAspect.java-doAround()] - Unlock: 10010 2019-08-02 13:35:48.877 INFO [42-LockAspect.java-doAround()] - Lock: Bob 2019-08-02 13:35:48.893 INFO [26-XService.java-bar() ] - XService::bar() 2019-08-02 13:35:48.893 INFO [48-LockAspect.java-doAround()] - Unlock: Bob 2019-08-02 13:35:48.900 INFO [42-LockAspect.java-doAround()] - Lock: noom 2019-08-02 13:35:48.900 INFO [14-XService.java-noom() ] - XService::noom() 2019-08-02 13:35:48.901 INFO [48-LockAspect.java-doAround()] - Unlock: noom 2019-08-02 13:35:48.898 INFO [42-LockAspect.java-doAround()] - Lock: lock-10010 2019-08-02 13:35:48.899 INFO [32-XService.java-goo() ] - XService::goo() 2019-08-02 13:35:48.899 INFO [48-LockAspect.java-doAround()] - Unlock: lock-10010
参考资料