一、概述
面向切面编程(AOP)是针对面向对象编程(OOP)的补充,可以非侵入式的为多个不具有继承关系的对象引入相同的公共行为例如日志、安全、事务、性能监控等等。SpringAOP允许将公共行为从业务逻辑中抽离出来,并将这些行为以一种非侵入的方式织入到所有需要的业务逻辑中,相较于OOP纵向模式的业务逻辑实现,其关注的方向是横向的切面。
从Spring2.0开始,引入AspectJ注释来对POJO进行标注,支持通过切点函数、逻辑运算符、通配符等高级功能来对切点进行灵活的定义,结合各种类型的通知来形成强大的连接点描述能力。
二、使用示例
2.1、基础示例,拦截指定类与方法
1、pom
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> </dependency>
或者
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
核心模块:两者效果都一样,将Spring核心模块Spring core、Spring beans、Spring context、Spring aop以及AspectJ注释提供库aspectjweaver引入项目。(另外包括SpringBoot特有的自动配置模块等等)
2、正常的业务逻辑
@Service public class MyTestService { Logger logger = LoggerFactory.getLogger(MyTestService.class); public String doSomething1(){ logger.info("invoking doSomething1......"); return "doSomething1"; } public String doSomething2(){ logger.info("invoking doSomething2......"); return "doSomething2"; } }
3、定义切面
@Aspect @Component public class MyIntercepter { private static final Logger logger = LoggerFactory.getLogger(MyIntercepter.class); @Pointcut("execution(public * com.github.bjlhx15.springaop.service.MyTestService.doSomething1*(..))") public void doSomethingPointcut(){}; @Before("doSomethingPointcut()") public void auth(JoinPoint pjp) throws Throwable{ MethodSignature signature = (MethodSignature) pjp.getSignature(); Method method = signature.getMethod(); //获取被拦截的方法 String methodName = method.getName(); //获取被拦截的方法名 logger.info("权限认证:调用方法为:{}", methodName); }; @AfterReturning(value = "doSomethingPointcut()", returning = "returnVal") public void logNormal(JoinPoint pjp, Object returnVal) throws Throwable{ MethodSignature signature = (MethodSignature) pjp.getSignature(); Method method = signature.getMethod(); //获取被拦截的方法 String methodName = method.getName(); //获取被拦截的方法名 logger.info("正常返回记日志:调用方法为:{};返回结果为:{}", methodName, returnVal); }; @AfterThrowing(value = "doSomethingPointcut()", throwing = "e") public void logThrowing(JoinPoint pjp, Throwable e) throws Throwable{ MethodSignature signature = (MethodSignature) pjp.getSignature(); Method method = signature.getMethod(); //获取被拦截的方法 String methodName = method.getName(); //获取被拦截的方法名 logger.info("抛出异常记日志:调用方法为:{};异常信息为:{}", methodName, e.getMessage()); }; @After(value = "doSomethingPointcut()") public void afterall(JoinPoint pjp) throws Throwable{ MethodSignature signature = (MethodSignature) pjp.getSignature(); Method method = signature.getMethod(); //获取被拦截的方法 String methodName = method.getName(); //获取被拦截的方法名 logger.info("方法调用完成:调用方法为:{}", methodName); } @Around("doSomethingPointcut()") public Object timer(ProceedingJoinPoint pjp) throws Throwable{ long beginTime = System.currentTimeMillis(); MethodSignature signature = (MethodSignature) pjp.getSignature(); Method method = signature.getMethod(); //获取被拦截的方法 String methodName = method.getName(); //获取被拦截的方法名 logger.info("计时切面:请求开始,方法:{}", methodName); Object result = null; try { // 一切正常的情况下,继续执行被拦截的方法 result = pjp.proceed(); } catch (Throwable e) { logger.info("exception: ", e); } long endTime = System.currentTimeMillis(); logger.info("计时切面:请求结束,方法:{},执行时间:{}", methodName, (endTime-beginTime)); return result; } }
上面定义了一个切点Pointcut,并围绕这个切点定义了5中不同类型的通知Advice,每个切点及其通知以及通知执行的逻辑共同构成了一个切面Advisor,用以在方法执行过程的各个时间点切入,执行一些特定逻辑。
4、调用
这时程序调用即有对应的切面信息,测试
@RunWith(SpringJUnit4ClassRunner.class) @SpringBootTest(classes = ApplicationMain.class) public class MyTestServiceTest { @Autowired MyTestService myTestService; @Test public void doSomething1() { myTestService.doSomething1(); } }
输出
c.g.b.s.interceptor.MyIntercepter : 计时切面:请求开始,方法:doSomething1
c.g.b.s.interceptor.MyIntercepter : 权限认证:调用方法为:doSomething1
c.g.b.springaop.service.MyTestService : invoking doSomething1......
c.g.b.s.interceptor.MyIntercepter : c.g.b.s.interceptor.MyIntercepter : 方法调用完成:调用方法为:doSomething1
c.g.b.s.interceptor.MyIntercepter : 正常返回记日志:调用方法为:doSomething1;返回结果为:doSomething1
以无侵入的形式在方法调用的前后增加了很多横切向的业务逻辑,业务逻辑代码并不必关心这些横切逻辑,只需要专注于自己的业务逻辑的实现就好。
在实际执行当中,在方法调用前后我们定义的切面都开始执行了。SpringAOP确保我们定义的切面织入到业务逻辑代码中,并在执行时发挥作用。
另外如结果所示多个切面的执行顺序也并不是按照方法定义的顺序执行。
2.2、基于自定义注解的切面
直接使用execution(public * com.github.bjlhx15.springaop.service.MyTestService.doSomething1*(..))这种切面定义方式与实际的类路径、类名或方法名紧密绑定,不利于扩展,后续使用需要编写切面的人才能继续。
我们希望像SpringCache那样基于自定义注解的方式启动各种切面,SpringAOP通过切点函数@annotation和@Within来支持这种方式。
1、pom同上
2、定义注解
@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface TestLogger { } @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface TestTimer { }
其中TestLogger是定义在类上的注释;TestTimer是定义在方法上的注释。
3、定义基于自定义注解的切面
@Aspect @Component public class MyIntercepter2 { private static final Logger logger = LoggerFactory.getLogger(MyIntercepter2.class); @Pointcut("@annotation(com.github.bjlhx15.springaop.anno.TestTimer)") public void timerPointcut(){}; @Pointcut("@within(com.github.bjlhx15.springaop.anno.TestLogger)") public void recordLogPointcut(){}; @Before("recordLogPointcut()") public void log(JoinPoint pjp) throws Throwable{ MethodSignature signature = (MethodSignature) pjp.getSignature(); Method method = signature.getMethod(); //获取被拦截的方法 String methodName = method.getName(); //获取被拦截的方法名 logger.info("开始记日志:调用方法为:{}", methodName); } @Around("timerPointcut()") public Object timer(ProceedingJoinPoint pjp) throws Throwable{ long beginTime = System.currentTimeMillis(); MethodSignature signature = (MethodSignature) pjp.getSignature(); Method method = signature.getMethod(); //获取被拦截的方法 String methodName = method.getName(); //获取被拦截的方法名 logger.info("请求开始,方法:{}", methodName); Object result = null; try { // 一切正常的情况下,继续执行被拦截的方法 result = pjp.proceed(); } catch (Throwable e) { logger.info("exception: ", e); } long endTime = System.currentTimeMillis(); logger.info("请求结束,方法:{},执行时间:{}", methodName, (endTime-beginTime)); return result; } }
上述代码表示打了@TestLogger注释的类,其中的所有方法被调用时都会记日志;而不管什么类,其打了@TestTimer注释的方法都会监控其执行时间。
切点函数@annotation表示匹配方法上的注解,切点函数@within表示匹配类上的注解。
4、编写业务逻辑并使用切面注解
@Service @TestLogger public class MyTestService2 { Logger logger = LoggerFactory.getLogger(MyTestService2.class); public String sayHello(){ logger.info("invoking method sayHello......"); return "Hello world!"; } @TestTimer public int count(){ logger.info("invoking method count......"); return 10; } }
根据服务MyTestService2中的注释,其表达的意思是MyTestService2中所有方法调用时都需要记日志,另外count()方法被调用时候需要监控执行时间。
5、测试输出
c.g.b.s.interceptor.MyIntercepter2 : 开始记日志:调用方法为:sayHello
c.g.b.springaop.service.MyTestService2 : invoking method sayHello......
c.g.b.s.interceptor.MyIntercepter2 : 请求开始,方法:count
c.g.b.s.interceptor.MyIntercepter2 : 开始记日志:调用方法为:count
c.g.b.springaop.service.MyTestService2 : invoking method count......
c.g.b.s.interceptor.MyIntercepter2 : 请求结束,方法:count,执行时间:0
由上可见,由于我们标注的注解的不同,在调用方法sayHello时只将记日志的逻辑切入进来,而在调用方法count时,将记日志和监控执行时间的逻辑都切入进来了。