Spring/Boot/Cloud系列知识(7)——Spring Aspectj EL(3)
================
(接上文《Spring/Boot/Cloud系列知识(6)——Spring EL(2)》)
3.3、Spring EL 与 AOP(Aspectj)
3.3.1、Spring 和 AOP的关系
AOP是面向切面编程的简称,Spring的设计思路受到这个思想的指导。所以我们在使用Spring各种组建的时候都能看到这个设计思路的影子。
再举一些实际的例子:我们使用Spring托管hibernate就是一个典型的AOP例子,事务的开启、提交、回滚操作无需业务开发人员进行,全部在业务方法之外自动完成;Spring Cache组件的使用也是一个典型的AOP实例,完成Spring Cache EL的配置后,对Redis/Memcache或者Google Cache的操作完全不需要书写额外代码,全部在业务方法以外完成;而我们之前介绍的Spring中使用的两种代理模式,也是基于AOP思想,无论是JAVA原生动态代理还是Cglib动态代理,都无需业务开发者书写一行额外代码即可完成代理过程。由此可见AOP思想在Spring中的体现是深入的、延续的且广泛的。
上一段文字已经清楚的表明,AOP是一种思想。既然是一种思想就需要具体的手段来进行实现,在Spring中对于AOP思想的实现可以归纳为两种主要手段:
1. 基于JDK动态代理或者Cglib动态代理实现的AOP
实际上本专题之前花了若干篇文章进行介绍的,Spring中基于JDK原生的动态代理和基于Cglib的动态代理实现就是一种实现AOP思想的手段:正常的业务过程方法被代理后,可以在业务执行前、正常执行后、抛出异常后触动一些其它方法过程的执行。
2. 基于AspectJ实现的AOP:
Spring中还集成了一个第三方组件AspectJ,这是一款独立的面向切面框架,Spring在自己的核心组件Spring-aop中对这个第三方组件AspectJ进行了封装(请参见spring-aop组件的源代码)。本篇文章我们主要介绍AspectJ中使用的EL表达式,关于更多AspectJ使用和实现原理的讨论,我们将在后续章节中进行。实际上AspectJ EL表达式不能算作原生的Spring EL范畴(因为它实际上是由AspectJ组件提供的表达式功能),但是目前大家在实际使用时,都将AspectJ当作Spring的一部分,所以在这里本文将AspectJ EL作为一种Spring EL的扩展进行讲解。
3.3.2、 Aspectj EL 示例
AspectJ在Spring的使用方式,主要是两种。一种是在基于XML的Spring配置文件中使用,另一种是基于@AspectJ形式的注解。当然本专题主要基于Spring Boot环境讲解,所以本文主要讲解的还是基于@AspectJ注解的使用形式。首先先介绍AspectJ组件中几种关键的注解形态(它们的定义全部在org.aspectj.lang.annotation包下):
@org.aspectj.lang.annotation.Aspect
这个注解只能在Class上进行标注,表示该类作为一个切面定义存在于Spring容器中。@org.aspectj.lang.annotation.Before
该注解只能使用在标注了@Aspect的类的方法上,表示该注解所代表的方法将在符合条件的业务方法(被代理方法)执行前被执行。@org.aspectj.lang.annotation.After
该注解只能使用在标注了@Aspect的类的方法上,表示该注解所代表的方法将在符合条件的业务方法(被代理方法)执行后被执行,无论业务方法是正常返回还是发生异常退出。@org.aspectj.lang.annotation.AfterReturning
该注解只能使用在标注了@Aspect的类的方法上,表示该注解所代表的方法将在符合条件的业务方法(被代理方法)执行后被执行,且只当业务方法正常退出时执行。@org.aspectj.lang.annotation.AfterThrowing
该注解只能使用在标注了@Aspect的类的方法上,表示该注解所代表的方法将在符合条件的业务方法(被代理方法)执行后被执行,且只当业务方法抛出异常退出时执行。@org.aspectj.lang.annotation.Around
该注解只能使用在标注了@Aspect的类的方法上,表示符合条件的业务方法(被代理方法),将视该注解所代表的方法的内部执行过程被动执行,否则就不会执行业务方法(被代理方法)。@org.aspectj.lang.annotation.Pointcut
该注解只能使用在标注了@Aspect的类的方法上,专门用于定义一个可共享的切面执行条件(切面位置)。这样一来保证多个切面点(Before、After、Around等)就可以共享一个切面条件。
3.3.2.1、基本使用
以下我们举例说明一些典型的使用示例。首先,以下代码是一个正常情况下的Spring标准代码,MyService接口中有两个方法,doSomething和doExceptionThing这两个方法分别模拟了一个正常的业务过程和模拟了一个异常的业务过程。代码如下所示:
package yinwenjie.test.proxy.interceptor;
// 这是一个bean层的spring代码
// @author yinwenjie
@Service("MyServiceImpl")
public class MyServiceImpl implements MyService {
private static final Logger LOGGER = LoggerFactory.getLogger(MyServiceImpl.class);
@Override
public void doSomething() {
// 这是执行正常业务的代码方法
LOGGER.info("123456");
}
@Override
public void doExceptionThing() {
// 这是一个模拟执行异常的代码方法
throw new IllegalArgumentException("抛出了异常!!");
}
}
我们通过JUnit运行对MyService接口的调用,可得到如下结果:
......
y.test.proxy.service.MyServiceImpl : 123456
// 或者以下结果:
......
java.lang.IllegalArgumentException: 抛出了异常!!
at yinwenjie.test.proxy.service.MyServiceImpl.doExceptionThing(MyServiceImpl.java:25)
at testSpring.service.MyServiceTest.testException(MyServiceTest.java:27)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
......
那么如果我们需要在执行业务方法之前或者之后,或者出现异常等状况下执行一些其它过程,那么我们可以创建一个类似如下的代理器:
/**
* 创建一个AOP切面拦截器
* @author yinwenjie
*/
@Aspect
@Component
public class MyAspect {
private static final Logger LOGGER = LoggerFactory.getLogger(MyAspect.class);
/**
* 在被代理的业务方法执行返回后,该方法被调用。<br>
* 注意只有业务方法正常执行退出的情况下才执行
* @param joinPoint 切点的信息在这里,可以有也可以没有
*/
@AfterReturning("execution(* yinwenjie.test.proxy.service..*.*(..))")
public void doAfterReturning(JoinPoint joinPoint) {
LOGGER.info("拦截器作用:doAfterReturning");
}
/**
* 在被代理的业务方法执行返回后,该方法被调用。<br>
* 注意只有业务方法异常退出的情况下,才执行。
* @param joinPoint 切点的信息在这里,可以有也可以没有
*/
@AfterThrowing("execution(* yinwenjie.test.proxy.service..*.*(..))")
public void doAfterThrowing(JoinPoint joinPoint) {
LOGGER.info("拦截器作用:doAfterThrowing");
}
/**
* 在被代理的业务方法执行前,该方法被调用。<br>
* @param joinPoint 可以有可以没有
*/
@Before("execution(* yinwenjie.test.proxy.service..*.*(..))")
public void doBeforeAdvice(){
LOGGER.info("拦截器作用:doBeforeAdvice");
}
/**
* 在被代理的业务方法执行返回后,该方法被调用。<br>
* 无论业务方法执行是正常退出还是抛出异常
* @param joinPoint
*/
@After("execution(* yinwenjie.test.proxy.service..*.*(..))")
public void doAfterAdvice(){
LOGGER.info("拦截器作用:doAfterAdvice");
}
}
以上示例代码中使用的Aspect相关注解已经说明得比较清楚了,这里的文字就不再进行赘述了。不过注解中的execution表达式要进行一些说明,execution中是一种标准的Aspect EL表达式,这是一种在AspectJ组件中使用的表达式。表达式中“* yinwenjie.test.proxy.service..*.*(..)”字符串的含义是“yinwenjie.test.proxy.service”程序包或者其子包下的任意方法(无论这个方法是否有传参要求,无论是否有返回值,无论方法访问修饰符如何),都使用Aspect相关注解规定的方法被代理。
以下是设定AspectJ注解后再次执行doSomething方法的日志输出效果:
......
14:22:08.137 INFO 451044 --- [main] y.t.p.interceptor.MyAspect : 拦截器作用:doBeforeAdvice
14:22:08.152 INFO 451044 --- [main] y.t.p.service.MyServiceImpl : 123456
14:22:08.152 INFO 451044 --- [main] y.t.p.interceptor.MyAspect : 拦截器作用:doAfterAdvice
14:22:08.153 INFO 451044 --- [main] y.t.p.interceptor.MyAspect : 拦截器作用:doAfterReturning
......
以下是设定AspectJ注解后再次执行doExceptionThing方法的日志输出效果:
......
14:23:21.915 INFO 450936 --- [main] y.t.p.interceptor.MyAspect : 拦截器作用:doBeforeAdvice
14:23:21.924 INFO 450936 --- [main] y.t.p.interceptor.MyAspect : 拦截器作用:doAfterAdvice
14:23:21.924 INFO 450936 --- [main] y.t.p.interceptor.MyAspect : 拦截器作用:doAfterThrowing
......
接着我们可以使用“@Pointcut”注解对相关拦截配置进行设置:
public class MyAspect {
@AfterReturning("doExecute()")
public void doAfterReturning(JoinPoint joinPoint) {
......
}
@AfterThrowing("doExecute()")
public void doAfterThrowing(JoinPoint joinPoint) {
......
}
@Before("doExecute()")
public void doBeforeAdvice(){
......
}
@After("doExecute()")
public void doAfterAdvice(){
......
}
@Pointcut("execution(* yinwenjie.test.proxy.service..*.*(..))")
public void doExecute() {
}
}
3.3.2.2、Around注解的使用
那么@Around注解又是怎么使用的呢?@Around注解的作用效果类似于本专题前文介绍过的Cglib动态代理中的MethodInterceptor。如果您使用了@Around注解,那么就需要在使用了@Around注解的方法中手动调用业务方法。如下所示:
/**
* 一个使用Around的实例
* @author yinwenjie
*/
@Aspect
@Component
public class MyAround {
/**
* 日志
*/
private static final Logger LOGGER = LoggerFactory.getLogger(MyAround.class);
/**
* 在被代理的业务方法执行返回后,该方法被调用。<br>
* 注意只有业务方法正常执行退出的情况下才执行
* @param joinPoint 切点的信息在这里,可以有也可以没有
*/
@Around("execution(* yinwenjie.test.proxy.service..*.*(..))")
public void doAround(ProceedingJoinPoint point) {
LOGGER.info("方法执行前执行的动作:doAround");
try {
point.proceed();
LOGGER.info("方法执行正常退出,要执行这个动作:doDone");
} catch(Throwable e) {
LOGGER.info("方法执行异常,要执行这个动作:doException");
} finally {
LOGGER.info("无论方法执行的结果怎样,都要执行这个动作:doReturn");
}
}
}
这里注意一下,被AspectJ AOP代理的MyService接口实现MyServiceImpl类由于代码没有变化,所以在之上示例中就不再赘述了。以下是使用@Around后的测试效果:
......
# doSomething方法的调用执行效果
11:19:46.589 INFO 460908 --- [main] y.t.p.i.MyAround : 方法执行前执行的动作:doAround
11:19:50.253 INFO 460908 --- [main] y.t.p.s.MyServiceImpl : 123456
11:19:50.254 INFO 460908 --- [main] y.t.p.i.MyAround : 方法执行正常退出,要执行这个动作:doDone
11:19:50.254 INFO 460908 --- [main] y.t.p.i.MyAround : 无论方法执行的结果怎样,都要执行这个动作:doReturn
......
# doExceptionThing方法的调用执行效果
11:02:21.930 INFO 479260 --- [main] y.t.p.i.MyAround : 方法执行前执行的动作:doAround
11:02:24.562 INFO 479260 --- [main] y.t.p.i.MyAround : 方法执行异常,要执行这个动作:doException
11:02:25.066 INFO 479260 --- [main] y.t.p.i.MyAround : 无论方法执行的结果怎样,都要执行这个动作:doReturn
......
3.3.3、Aspectj EL 中的表达式
Aspectj EL中的表达式由几个关键字符构成,如下所示:
- “*”该符号是Aspectj EL 中最重要的通配符,可以匹配任意字符,但只限于一个连续的单词。例如在不同语境下,出现一个”*”可以表示一个类名、表示一个方法名或者表示一个包目录的某一层。
- “..”该符号匹配任意字符,并且可以是多个连续的单次。例如在不同语境下,出现”..”可以表示任意深度的包目录、表示方法中任意多个入参类型
- “+”该符号必须伴随类/接口名使用,表示类本身或者继承/实现该类的某个子类。
以下我们给出一些表达式的使用示例,以帮助读者对以上通配符进行理解:
......
// 表示匹配test包下,第一层子包下的任何类,任何方法,无论这个方法是否带有入参,也无论这个方法的返回值是什么
* test.*.*.*(..)
// 表示匹配test当层包下的任何类,任何方法。无论这个方法是否带有入参,也无论这个方法的返回值是什么
* test.*.*(..)
// 表示匹配test当层包下的任何类,任何方法。这个方法不能带有入参,但可以是任意返回值。
* test.*.*()
// 表示匹配test当前包或任意子包下的任何方法,无论这个方法是否带有入参,但必须没有返回值。
// 相同于void test..*.*(..)
void test..*(..)
// 表示匹配test当前包或任意子包下的任何方法,无论这个方法是否带有入参,但必须没有返回值,且方法的访问级别必须是使用public进行定义的
public void test..*(..)
// 表示匹配test当前包或任意子包下的开头以“do”命名的方法,无论这个方法是否带有入参,且无论有没有返回值。
* test..do*(..)
// 表示匹配test当前包或任意子包下的任何方法,无论这个方法有没有返回值。
// 但是该方法必须有两个入参,且第一个入参的类型为String
* test..*(String,*)
......
===============================
(接下文,Aspectj 中的函数、运算符、Aspectj 新特性)