Spring AOP 注解开发
AOP 注解开发
值得说明的是 AOP 不是 Spring 特有的,而是 Spring 支持 AOP。介绍两个 AOP 的实现者
AspectJ
AspectJ 是语言级的 AOP 实现,2001年由 Xerox PARC 的 AOP 小组发布。AspectJ 扩展了 Java 语言,定义了 AOP 语法,能给在编译期提供横切代码的织入,所以它有一个专门的编译期用来生成遵守 Java 字节编码规范的 Class 文件。其主页位于 http://www.eclipse.org/aspectj 。
Spring AOP
Spring AOP 使用纯 Java 实现,τ在运行期通过代码方式向目标类注入增强代码。Spring 并不尝试提供最完整的 AOP 实现,相反,它侧重于提供一种和 Spring IOC 容器整合的 AOP 实现,用以解决企业级开发中的常见问题。在 Spring 中科院无缝地将 Spring AOP、IOC 和 AspectJ 整合在一起。
AspectJ 相比于 Spring AOP 功能更加强大,但是 Spring AOP 相对来说更简单,
如果我们的切面比较少,那么两者性能差异不大。但是,当切面太多的话,最好选择 AspectJ ,它比Spring AOP 快很多。
@AspectJ 使用 Java 5.0 注解和正规的 AspectJ 的切点表达式语言描述切面。
@Aspect // 1. 通过该注解将 PreGreetingAspect标识为一个切面
public calss PreGreetingAspect{
@Before("execution(* greetTo(..))") // 2. 定义切点和增强类型
public void befroeGreeting(){ // 3. 增强的逻辑
System.out.println("Nice to meet you.");
}
}
1. 切点表达式
AspectJ 的切点表达式由关键字和操作参数组成。
如切点表达式 execution(* greetTo(...))
,execution
为关键字,而 * greetTo(...)
为操作参数。execution
代表目标类执行某一方法,而 * greetTo(...)
描述目标方法的匹配模式串,二者联合起来表示目标类 greetTo() 方法的连接点。
为了方便,execution
称作函数,而 * greetTo(...)
称作函数的入参。
Spring 支持9个 @AspectJ 切点表达式函数,大致可以分为4类:
- 方法切点函数
- 方法入参切点函数
- 目标类切点函数
- 代理类切点函数
具体描述如下表:
2. 在函数入参中使用通配符
有些函数的入参可以接受通配符去匹配接入点,@AspectJ 支持3种通配符:
*
:匹配任意字符,但它只能匹配上下文中的一个元素。..
:匹配任意字符,可以匹配上下文中的多个元素。但在表示类时,必须与*
联合使用,在表示入参时可以单独使用。+
:表示按类型匹配指定类的所有类,必须跟在类名后面。如com.smart.Car+
,可匹配继承或扩展指定类的所有类,同时还可以包括指定类本身。
不过在 @AspectJ 函数中支持通配符的函数也不多:
- 支持所有通配符:
execution()
和within()
。如 within(com.smart.*)。 - 仅支持
+
通配符:args()
、this()
和target()
。不过对于这些函数来说,使用和不使用 "+" 都一样的,意义不大。
3. 切点运算符
切入点表达式由切点函数组成,切点函数之间可以进行逻辑运算,组成符合切点。Spring 支持以下切点运算符:
&&
:与运算符,相当于切点的交集运算。Spring 还提供了一个等效的运算符and
。||
:或运算符,相当于切点的并集运算,or
是等效的操作符。!
:非运算符,相当于切点的反集运算,not
是等效的操作符。
4. 增强类型
@AspectJ 为各种增强类型提供了注解类,他们位于 org.aspectj.lang.annotation.* 包中。
1. @Before
前置增强,相当于 BeforeAdvice ,拥有两个成员。
value
:用于定义切点。aegNames
:由于无法通过 JAVA 反射机制获取方法入参名,所以如果在 Java 编译时未启用调试信息,或者需要在运行期解析切点,就必须通过这个成员指定注解所标注增强方法的参数名(二者名字必须完全相同),多个参数名用逗号分隔。
2. @AfterReturning
后置增强,相当于 AfterReturningAdvice,拥有4个成员。
value
:用于定义切点。pointcut
:表示切点信息,可以看作 value 的同义词。如果显式指定 pointcut 值,它将会覆盖 value 的设置值。returning
:将目标对象方法的返回值绑定给增强的方法。argName
:如前面所述。
3. @Around
环绕增强,相当于 MethodInterceptor,拥有两个成员。
value
:用于定义切点。argNames
:如前面所述。
4. @AfterThrowing
抛出异常增强,相当于 ThrowsAdvice。拥有4个成员。
value
:用于定义切点。pointcut
:表示切点信息,可以看作 value 的同义词。如果显式指定 pointcut 值,它将会覆盖 value 的设置值。throwing
:将抛出的异常绑定到增强方法中。argNames
:如前面所述。
5. @After
final 增强,不管是抛出异常还是正常退出,这个增强都会得到执行。相当于 try{}finally{} 的控制流,有两个成员。
value
:用于定义切点。argNames
:如前面所述。
6. @DeclareParents
引介增强,相当于 IntroductionInterceptor。拥有两个成员。
value
:用于定义切点,表示在哪个目标类上添加引介增强。defaultImpl
:默认的接口实现类。
5. 常用切点函数
@Annotation()
@Annotation() 表示标注了某个注解的所有方法。将增强应用到标注了该注解的方法中。
举个实例
@Aspect
@Component
public class TestAspect {
//后置增强切面。 AnnoTest 为自定义的一个注解
@AfterReturning("@annotation(AnnoTest)")
public void needTestFun(){
System.out.println("needTestFun() executed!");
System.out.println("增加一个后置通知");
System.out.println("--------------------");
}
}
使用注解的切面
@Component
public class TestImpl {
@AnnoTest
public void greetTo(String Name) {
System.out.println("TestImpl:greet to " + Name);
}
}
在运行这个 greetTo() 方法时,会添加一个后置增强。
@execution()
execution() 是最常用到的切点函数,语法:
execution(<修饰符模式>? 返回类型模式 方法名模式(参数模式) <异常模式>?)
修饰符模式和异常模式是可选的。通过实例具体说明用法:
-
通过方法签名定义切点
execution(public * *(..))
:匹配所以目标类的 public 方法。第一个*
代表返回类型;第二个*
代表方法名;..
代表任意入参的方法。execution(* *To(..))
:匹配目标类所有以 To 为后缀的方法。第一个*
代表返回类型;*To
代表任意以 To 为后缀的方法。
-
通过类定义切点
execution(* com.smart.Waiter.*(..))
:匹配 Waiter 接口的所有方法。第一个*
代表返回类型。execution(* com.smart.Waiter+.*(..))
:匹配 Waiter 接口及其所有实现类的方法。
-
通过类包定义切点
在类名模式串中,
.*
表示包下的所有类,而..*
表示包、子孙包下的所有类。execution(* com.smart.*(..))
:匹配 com.smart 包下所有类的所有方法。execution(* com.smart..*(..))
:匹配 com.smart 包、子孙包下的所有类的所有方法。execution(* com..*.*Dao.find*(..))
-
通过方法入参定义切点
这个部分比较复杂,可以用
*
和..
通配符。*
表示任意类型的参数;..
表示任意类型的参数且参数个数不限。execution(* joke(String, int))
:匹配 joke(String, int) 方法。如果方法中的参数类型是 java.lang 包下的类,则可以直接使用类名;否则必须使用全限定类名,如 joke(java.util.List, int)execution(* joke(String, *))
:第二参数类型不限。execution(* joke(String, ..))
:后面参数类型和个数不限。execution(* joke(Object+))
:匹配目标类的 joke() 方法,方法拥有一个入参,且入参是 Object 类型或者该类的之类。
其他切点跟表格描述的差不多,就不过多介绍了。
6. 切点复合运算
应该都看得懂吧
@Aspect
public class TestAspect {
@After("within(com.smart.*) && execution(* greetTo(..))")
public void needTestFun(){
System.out.println("与运算");
}
@Before("!target(com.smart.NaiveWaiter) "
+ "&& execution(* serveTo(..))")
public void needTestFun2(){
System.out.println("非与运算");
}
@AfterReturning("target(com.smart.Waiter) || " +
" target(com.smart.Seller)")
public void needTestFun3(){
System.out.println("或运算");
}
}
7. 绑定连接点方法入参
在所有切点函数中,args() 、this()、target()、@args()、@within()、@target() 和 @annotation() 这 7 个函数除了可以指定类名外,还可以指定参数名,将目标对象连接上的方法入参绑定到增强的方法中。
其中:
args() 用于绑定连接点方法上的入参;
@annotation() 用于绑定连接点方法的注解对象;
而 @args() 用于绑定连接点方法入参的注解。
@Aspect
public class TestAspect {
//1.通过 args(name,num,..)进行连接点参数绑定,就是目标连接的参数值可以绑定到这个增强方法的参数上
@Before("target(com.smart.NaiveWaiter) && args(name,num,..)")
public void bindJoinPointParams(int num, String name) { //2.接受连接点的参数
System.out.println("----bindJoinPointParams()----");
System.out.println("num:" + num);
System.out.println("name:" + name);
System.out.println("----bindJoinPointParams()----");
}
}
首先,args(name,num,..) 通过 bindJoinPointParams(int num, String name)的入参类型,得到确切的切点表达式:target(com.smart.NaiveWaiter) && args(int, String, ..);
然后,在增强方法织入目标连接点时,增强方法就可以通过 num 和 name 访问到连接点的入参。
绑定过程:
- args() 根据参数名称在增强方法中查找到名称相同的入参类型并获知对应的类型,这样就知道了匹配连接点方法的入参类型;
- 连接点方法入参类型所在的位置则由参数名在 args() 函数中声明的位置决定。
当然,除了 args() ,其他切点函数也可以绑定切点函数(如 @args()、@target() 等),当指定参数名时,就同时具有匹配切点和绑定参数双重功能。
8. 绑定代理对象
和绑入参类似,可以使用 this() 或 target() 来绑定被代理对象实例。
@Aspect
public class TestAspect {
/*1.通过 bindProxyObj(Waiter waiter) 的入参得到确切的表达式:this(Waiter);
当增强方法织入目标连接点时,增强方法通过waiter入参绑定目标对象
*/
@Before("this(waiter)")
public void bindProxyObj(Waiter waiter) { //2.
System.out.println("----bindProxyObj()----");
System.out.println(waiter.getClass().getName());
System.out.println("----bindProxyObj()----");
}
}
9. 绑定类注解对象
@within() 和 @target() 函数可以将目标类的注解对象绑定到增强方法中。
@Aspect
public class TestAspect {
/*1.通过 bindTypeAnnoObj()的入参得到确切的表达式:within(Monitorable);
当增强方法织入目标连接点时,增强方法通过waiter入参绑定目标对象
*/
@Before("within(m)")
public void bindTypeAnnoObj(Monitorable m) {//2.Monitorable 为注解@Monitorable
System.out.println("----bindTypeAnnoObj()----");
System.out.println(m.getClass().getName());
System.out.println("----bindTypeAnnoObj()----");
}
}
如在 Waiter 类中标注了 @Monitorable 注解,所有的 Waiter Bean 都匹配切点,其 Monitorable 注解对象将绑定到增强方法中。
10. 绑定返回值
在后置增强中,可以通过 returning 绑定连接点方法的返回值。
@AfterReturning(value="target(com.smart.Waiter)", returning = "reVal") //1
public void bindReturnValue(int reVal) {//2
System.out.println("----bindReturnValue()----");
System.out.println("returnValue = " + reVal); //3. 可以取到连接点方法的返回值
System.out.println("----bindReturnValue()----");
}
①处 和 ②处名字必须相同
②处 reVal 的类型必须与连接点方法的返回值类型匹配
11. 小结
Spring AOP 除了注解配置,还有其它几种配置方式,可以适当去了解。在项目支持的Spring 版本比较低时,会用到其它的配置方式。不过,目前大多数项目都是使用注解配置方式。
掌握切点表达式语法和切点函数的学习 @AspectJ 的重心。