前文讲了很多原理性的东西,使用起来比较繁琐,通过Spring框架提供的注解能很方便的实现切面功能。
Spring中使用注解方式实现AOP,采用@AspectJ方式实现,首先确定需要切入的方法,也就是连接点
@Service public class UserServiceMethod { public void add(String name) { System.out.println("UserServiceMethod add name is:" + name); } }
开发切面
有了连接点,还需要切面通过切面描述AOP其他信息,来描述流程的织入
@Aspect @Component public class LogAgent { @Before("execution(* com.niu.dao.UserServiceMethod.add(..))") public void beforeAdd(JoinPoint joinPoint) { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); System.out.println("before 方法规则拦截:" + method.getName()); } @After("execution(* com.niu.dao.UserServiceMethod.add(..))") public void afterAdd(JoinPoint joinPoint) { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); System.out.println("after 方法规则拦截:" + method.getName()); } @AfterReturning("execution(* com.niu.dao.UserServiceMethod.add(..))") public void afterReturnAdd(JoinPoint joinPoint) { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); System.out.println("afterReturn 方法规则拦截:" + method.getName()); } @AfterThrowing("execution(* com.niu.dao.UserServiceMethod.add(..))") public void afterThrowsAdd(JoinPoint joinPoint) { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); System.out.println("afterThrows 方法规则拦截:" + method.getName()); } }
其中注解中execution(* com.niu.dao.UserServiceMethod.add(..))是正则匹配,
execution表示正在执行的时候,拦截里面的正则匹配方法
*表示任意返回类型的方法
com.niu.dao.UserServiceMethod指定目标对象的全限定名称
add指定目标对象的方法
(..)表示任意参数进行匹配
AssertJ关于SpringAop切点的指示器:
- args() 限定连接点方法参数
- @args() 通过连接点方法参数上的注解
- execution()用于匹配连接点上的执行方法
- this()限制连接点匹配AOP代理bean引用为指定的类型
- target目标对象
- @target限制目标对象的配置
- within限制连接点匹配指定的类型
- @within() 限定连接点带有匹配注解类型
- @annotation()限定带有指定注解的连接点
定义切点
在上面切面定义中,可以看到@before,@After等注解,还会定义一个正则,这个正则作用就是定义什么时候启用AOP,毕竟不是所有的功能都需要AOP,在上述代码中每个注解都有一个正则,这显得很冗余。为了解决这个问题,Spring定义了切点的概念(PointCut),切点的作用就是向Spring描述哪些类的哪些方法需要启动AOP编程。有了切点的概念,就可以吧代码写的更简洁:
@Aspect @Component public class LogAgent2 { @Pointcut("execution(* com.niu.dao.UserServiceMethod.add(..))") public void pointCut() { System.out.println("pointCut"); } @Before("pointCut()") public void beforeAdd(JoinPoint joinPoint) { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); System.out.println("before 方法规则拦截:" + method.getName()); } @After("pointCut()") public void afterAdd(JoinPoint joinPoint) { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); System.out.println("after 方法规则拦截:" + method.getName()); } @AfterReturning("pointCut()") public void afterReturnAdd(JoinPoint joinPoint) { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); System.out.println("after Return 方法规则拦截:" + method.getName()); } }
测试AOP,两种方式输出完全一致
@SpringBootApplication public class App { public static void main(String[] args) throws Exception { ConfigurableApplicationContext context = SpringApplication.run(App.class, args); System.out.println("Start app success."); UserServiceMethod userServiceMethod = context.getBean(UserServiceMethod.class); userServiceMethod.add(); } } //输出: Start app success. before 方法规则拦截:add UserServiceMethod add after 方法规则拦截:add after Return 方法规则拦截:add
引入
如果想检测用户信息是否为空,入参为空时则不打印,但是原接口中没有该校验,该怎么办呢? 这时应该引入Spring增强接口功能,为接口引入新的接口
首先提供校验接口:
public interface UserValidator { boolean validate(String name); } public class UserValidatorImpl implements UserValidator { @Override public boolean validate(String name) { System.out.println("引入校验"); return StringUtils.isEmpty(name); } }
引入新的接口:
@Aspect @Component public class MyAspect { @DeclareParents(value = "com.niu.dao.UserService+", defaultImpl = UserValidatorImpl.class) public UserValidator userValidator; }
这里@DeclareParents作用是引入新的类来增强服务,必须有value和defaultImpl配置:
value:指向要增强功能的目标对象,配置为UserService defaultImpl:引入增强功能的类,这里配置为UserValidatorImpl
public static void main(String[] args) throws Exception { ConfigurableApplicationContext context = SpringApplication.run(App.class, args); System.out.println("Start app success."); UserValidator userService = (UserValidator)context.getBean(UserService.class); if(userService.validate("steven")){ System.out.println("Valid name"); } } 输出:Valid name
说明接口强转类型后,可以使用新方法,证明已有UserValidator的方法。
通知获取参数
在上述的通知中,大部分没有通知传递参数,可以通过使用连接点(Joinpoint)获取参数
@Before("pointCut() && args(name)") public void beforeAdd(JoinPoint joinPoint, String name) { System.out.println(Arrays.toString(joinPoint.getArgs()));; System.out.println(name);; } 输出: [steven] steven 复制代码
正则式pointCut() && args(name)中pointCut()表示原来定义的切点规则,并且约定将连接点(目标对象方法)名称为name的参数传递进来,通过连接点getArgs可以获取到所有参数,对于连接点参数还可以获取到目标对象的信息,从而完成需要的切面任务,下面是不用入参的形式:
@Before("pointCut()") public void beforeAdd(JoinPoint joinPoint) { System.out.println(Arrays.toString(joinPoint.getArgs()));; } 输出: [steven]
织入
织入是一个生成动态代理对象并将切面的目标对象方法编织为约定的流程,一般都是采用接口加实现类的模式,这也是Spring推荐的方式,前问介绍过动态代理有很多实现方式JDK、CGLIB等,查资料说在Spring中如果要实现AOP的类有接口则用JDK动态代理执行,如果没有接口则用CGLib运行
跟下断点验证一下,我试过两种方式,不管实现类有没有接口都是使用的CGlib,不知道是不是网上解释有误还是SpringBoot2采用方式变更
多个切面
Spring支持多个切面运行
提供三个切面
@Aspect @Component public class LogAgent { @Before("execution(* com.niu.dao.UserServiceMethod.add(..))") public void beforeAdd(JoinPoint joinPoint) { System.out.println("LogAgent 拦截"); } } @Aspect @Component public class LogAgent2 { @Pointcut("execution(* com.niu.dao.UsrTestService.add(..))") public void pointCut() { System.out.println("pointCut"); } @Before("pointCut()") public void beforeAdd(JoinPoint joinPoint) { System.out.println("LogAgent2 拦截");; } } @Aspect @Component public class LogAgent3 { @Pointcut("execution(* com.niu.dao.UsrTestService.add(..))") public void pointCut() { System.out.println("pointCut"); } @Before("pointCut()") public void beforeAdd(JoinPoint joinPoint) { System.out.println("LogAgent3 拦截");; } } 输出: LogAgent3 拦截 LogAgent 拦截 LogAgent2 拦截
结果每次输出拦截的顺序是不固定的,如果需要固定的执行的顺序可以在类上加Order注解,比如:
@Aspect @Component @Order(1) public class LogAgent2 { @Pointcut("execution(* com.niu.dao.UsrTestService.add(..))") public void pointCut() { System.out.println("pointCut"); } } 输出: LogAgent2 拦截 LogAgent3 拦截 LogAgent 拦截
可以根据需要排序,如果再加上after方法的打印,可以看到这是一个典型的职责链模型的顺序,有兴趣的可以自己在研究
LogAgent2 拦截 LogAgent3 拦截 LogAgent 拦截 UserServiceMethod add name is:steven LogAgent1 :after 方法规则拦截:add LogAgent3:after 方法规则拦截:add LogAgent2:after 方法规则拦截:add
结语
关于AOP的文章就写到这了,功能其实并不难,重要的思想,平常开发中也可多考虑这种解耦方式。关于这两种实现的效率问题找了一些资料:在1.6和1.7的时候,JDK动态代理的速度要比CGLib动态代理的速度要慢,但是并没有教科书上的10倍差距,在JDK1.8的时候,JDK动态代理的速度已经比CGLib动态代理的速度快,这块有兴趣的可以再研究下,目前效率应该是差不多的。