所谓面向切面编程,实际指的是在不改变已有代码的前提下,通过预编译或运行时动态代理的方式给程序动态统一的添加新功能的一种编程技巧。比如,你要实现请求日志存储便于以后的Bug修复,那么 aop 技术就能很好的帮助你。与其说是技术,不如说是编程技巧,aop 的思想就是这样,今天我们主要看 aop 编程技巧的 spring 实现。
aop 的概念我就不再多解释了,也很难通俗易懂的解释清楚,你就这么理解,通过 aop 技术你可以在不改变已有代码的情况下干预已有代码的执行,比如在已有代码开始执行前,你可以修改某些变量,甚至可以取消已有代码的执行,在其执行结束,你同样可以执行某些操作,还可以修改其返回值。下面,我们进入今天的主题,新建一个 迷你型的 spring boot 项目,然后添加 aop 依赖,即在 pom.xml 里添加:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
然后新建一个类:DataSource.java 源码如下:
import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface DataSource { String name(); }
再建一个 LearnAopAspect.java,源码如下:
import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.*; import org.springframework.stereotype.Component; @Aspect @Slf4j @Component public class LearnAopAspect { @Pointcut("@within(DataSource)") public void dataSource() { } @Before("dataSource()") public void changeDataSource(JoinPoint joinPoint) { log.info("===Before==="); } @After("dataSource()") public void clearDataSource(JoinPoint joinPoint) { log.info("===After==="); } @AfterReturning("dataSource()") public void afterReturn(JoinPoint joinPoint) { log.info("===AfterReturning==="); } @AfterThrowing("dataSource()") public void afterThrow(JoinPoint joinPoint) { log.info("===AfterThrowing==="); } @Around("dataSource()") public Object around(ProceedingJoinPoint joinPoint) { try { log.info("===Around==="); Object object = joinPoint.proceed(); log.info("===Around finish, result is {}", object); return "edit by aop and origin result is : " + object; } catch (Throwable e) { e.printStackTrace(); } return null; } }
新建一个 Test.java 源码如下:
import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import top.kpromise.aop.aop.DataSource; @RestController @RequestMapping("/test") @Slf4j public class Test { @RequestMapping("/test") public String test() { log.info("test method ..."); return "success"; } }
然后直接运行,启动后 浏览器输入:http://localhost:8080/test/test 你看到的应该是 success 字样,但如果你给 Test.java 类添加 @DataSource(name = "test") 注解,修改后大致如下:
@RestController @RequestMapping("/test") @Slf4j @DataSource(name = "test") public class Test {
接着运行,启动后同样的 浏览器输入 http://localhost:8080/test/test 你就看到 edit by aop and origin result is : success 的输出,而这个是在 LearnAopAspect.java 类里写的,具体如下:
@Around("dataSource()") public Object around(ProceedingJoinPoint joinPoint) { try { log.info("===Around==="); Object object = joinPoint.proceed(); log.info("===Around finish, result is {}", object); return "edit by aop and origin result is : " + object; } catch (Throwable e) { e.printStackTrace(); } return null; }
如果你关注 IDEA 的输出,你应该看到如下信息:
2019-11-24 13:34:53.294 INFO 17166 --- [nio-8080-exec-4] top.kpromise.aop.aop.LearnAopAspect : ===Around=== 2019-11-24 13:34:53.295 INFO 17166 --- [nio-8080-exec-4] top.kpromise.aop.aop.LearnAopAspect : ===Before=== 2019-11-24 13:34:53.295 INFO 17166 --- [nio-8080-exec-4] top.kpromise.aop.controller.Test : test method ... 2019-11-24 13:34:53.295 INFO 17166 --- [nio-8080-exec-4] top.kpromise.aop.aop.LearnAopAspect : ===Around finish, result is success 2019-11-24 13:34:53.296 INFO 17166 --- [nio-8080-exec-4] top.kpromise.aop.aop.LearnAopAspect : ===After=== 2019-11-24 13:34:53.296 INFO 17166 --- [nio-8080-exec-4] top.kpromise.aop.aop.LearnAopAspect : ===AfterReturning===
输出的信息说明,spring aop 的生命周期或者说调用顺序是:Around -> Before -> Call Method -> Around -> Arter -> AfterReturning,倘若将 Test.java 里的 test 方法改为:
@RequestMapping("/test") public String test() { log.info("test method ..."); throw new RuntimeException("name is null..."); //return "success"; }
将 LearnAopAspect.java 里的 around 方法 改为:
@Around("dataSource()") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { log.info("===Around==="); Object object = joinPoint.proceed(); log.info("===Around finish, result is {}", object); return "edit by aop and origin result is : " + object; }
然后你看到的调用顺序将是:Around -> Before -> Call Method -> Around -> Arter -> AfterThrowing 似乎又 GET 到了一个点,可以通过 切面技术实现异常拦截,哈哈哈哈哈哈。
如果一个方法被有多个切面,那么执行顺序又是什么样呢?此时可以添加 @Order 注解,比如:
@Aspect @Slf4j @Component @Order(12) public class LearnAopAspect {
其中,order 可以是负数,越小的越先执行,我们做个实验,将 LearnAopAspect.java 复制为 LearnAopAspect2.java ,但是 前者 order 写 12 后者写 22,同时 test 函数还原掉,如下:
@RequestMapping("/test") public String test() { log.info("test method ..."); //throw new RuntimeException("name is null..."); return "success"; }
然后运行项目,并访问:http://localhost:8080/test/test 最后观察控制台输出,你应该看到类似如下的输出:
[nio-8080-exec-1] top.kpromise.aop.aop.LearnAopAspect : ===Around=== [nio-8080-exec-1] top.kpromise.aop.aop.LearnAopAspect : ===Before=== [nio-8080-exec-1] top.kpromise.aop.aop.LearnAopAspect2 : ===Around=== [nio-8080-exec-1] top.kpromise.aop.aop.LearnAopAspect2 : ===Before=== [nio-8080-exec-1] top.kpromise.aop.controller.Test : test method ... [nio-8080-exec-1] top.kpromise.aop.aop.LearnAopAspect2 : ===Around finish, result is success [nio-8080-exec-1] top.kpromise.aop.aop.LearnAopAspect2 : ===After=== [nio-8080-exec-1] top.kpromise.aop.aop.LearnAopAspect2 : ===AfterReturning=== [nio-8080-exec-1] top.kpromise.aop.aop.LearnAopAspect : ===Around finish, result is edit by aop and origin result is : success [nio-8080-exec-1] top.kpromise.aop.aop.LearnAopAspect : ===After=== [nio-8080-exec-1] top.kpromise.aop.aop.LearnAopAspect : ===AfterReturning===
可见,执行顺序是 aspect1.Around -> aspect1.Before -> aspect2.Around -> aspect2.Before -> Method Call -> aspect2. Around finish -> aspect2.After -> aspect2.AfterReturning -> aspect.Around Finish -> aspect.After -> aspect.AfterReturning 其实际就是个同心圆,类似下图(图片来自网络):
篇幅所限,本文写到这里就结束了,关于 aop 的细节,请参考下篇博客:spring aop 之 切点表达式