与 AspectJ 相同的是,Spring AOP 同样需要对目标类进行增强,也就是生成新的 AOP 代理类;与 AspectJ 不同的是,Spring AOP 无需使用任何特殊命令对 Java 源代码进行编译,它采用运行时动态地、在内存中临时生成“代理类”的方式来生成 AOP 代理。
Spring 允许使用 AspectJ Annotation 用于定义方面(Aspect)、切入点(Pointcut)和增强处理(Advice),Spring 框架则可识别并根据这些 Annotation 来生成 AOP 代理。Spring 只是使用了和 AspectJ 5 一样的注解,但并没有使用 AspectJ 的编译器或者织入器(Weaver),底层依然使用的是 Spring AOP,依然是在运行时动态生成 AOP 代理,并不依赖于 AspectJ 的编译器或者织入器。
简单地说,Spring 依然采用运行时生成动态代理的方式来增强目标对象,所以它不需要增加额外的编译,也不需要 AspectJ 的织入器支持;而 AspectJ 在采用编译时增强,所以 AspectJ 需要使用自己的编译器来编译 Java 文件,还需要织入器。
为了启用 Spring 对 @AspectJ 方面配置的支持,并保证 Spring 容器中的目标 Bean 被一个或多个方面自动增强,必须在 Spring 配置文件中配置如下片段,
AOP的作用这里就不再作说明了,下面开始讲解一个很简单的入门级例子。
引用一个猴子偷桃,守护者守护果园抓住猴子的小情节。
1、猴子偷桃类(普通类):
package com.samter.common;
/**
* 猴子
* @author Administrator
*
*/
public class Monkey {
public void stealPeaches(String name){
System.out.println("【猴子】"+name+"正在偷桃...");
}
}
2、守护者类(声明为Aspect):
package com.samter.aspect;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
/**
* 桃园守护者
* @author Administrator
*
*/
@Aspect
public class Guardian {
@Pointcut("execution(* com.samter.common.Monkey.stealPeaches(..))")
public void foundMonkey(){}
@Before(value="foundMonkey()")
public void foundBefore(){
System.out.println("【守护者】发现猴子正在进入果园...");
}
@AfterReturning("foundMonkey() && args(name,..)")
public void foundAfter(String name){
System.out.println("【守护者】抓住了猴子,守护者审问出了猴子的名字叫“"+name+"”...");
}
}
3、XML配置文件:
4、测试类:
package com.samter.common;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Main {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("config.xml");
Monkey monkey = (Monkey) context.getBean("monkey");
try {
monkey.stealPeaches("孙大圣的大徒弟");
}
catch(Exception e) {}
}
}
5、控制台输出:
【守护者】发现猴子正在进入果园...
【猴子】孙大圣的大徒弟正在偷桃...
【守护者】抓住了猴子,守护者审问出了猴子的名字叫“孙大圣的大徒弟”...
解说:
1写了一个猴子正在偷桃的方法。
2写了一个标志为@Aspect的类,它是守护者。它会在猴子偷桃之前发现猴子,并在猴子偷桃之后抓住猴子。
原理:
A、@Aspect的声明表示这是一个切面类。
B、@Pointcut使用这个方法可以将com.samter.common.Monkey.stealPeaches(..)方法声明为poincut即切入点。作用,在stealPeaches方法被调用的时候执行2的foundMonkey方法。其中execution是匹配方法执行的切入点,也就是spring最常用的切入点定义方式。
C、@Before(value="foundMonkey()"):@Before声明为在切入点方法执行之前执行,而后面没有直接声明切入点,而是value="foundMonkey()",是因为如果@afterReturning等都有所改动的时候都必须全部改动,所以统一用Pointcut的foundMonkey代替,这样子有改动的时候仅需改动一个地方。其他@AfterReturning类同。
3是xml配置文件,里面有具体的注释。
特别说明:Guardian类里面的@Pointcut("execution(* com.samter.common.Monkey.stealPeaches(..))"),如果stealPeaches有参数则..表示所有参数,@AfterReturning("foundMonkey() && args(name,..)")的&& args(name,..)可以获取切入点方法stealPeaches的参数。
总结:这里列举了一个简单的例子,但是不难引申到应用中,当你写一个登陆系统的时候,你或许要记录谁成功登陆了系统,谁登陆系统密码错误等等的信息,这样子你用切面是再合适不过的了,总之当你的事务逻辑都设计到日志、安全检查、事务管理等等共同的内容的时候,用切面是要比你没有一个事务逻辑类都有相关代码或者相关引用好得多。
1、通过 Spring 的 XML Schema 配置方式:
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-2.0.xsd">
当然,如果我们希望完全启动 Spring 的“零配置”功能,则还需要启用 Spring 的“零配置”支持,让 Spring 自动搜索指定路径下 Bean 类。
所谓自动增强,指的是 Spring 会判断一个或多个方面是否需要对指定 Bean 进行增强,并据此自动生成相应的代理,从而使得增强处理在合适的时候被调用。
如果不打算使用 Spring 的 XML Schema 配置方式,则应该在 Spring 配置文件中增加如下片段来启用 @AspectJ 支持。
2、通过
上面配置文件中的 AnnotationAwareAspectJAutoProxyCreator 是一个 Bean 后处理器(BeanPostProcessor),该 Bean 后处理器将会为容器中 Bean 生成 AOP 代理,当启动了 @AspectJ 支持后,只要我们在 Spring 容器中配置一个带 @Aspect 注释的 Bean,Spring 将会自动识别该 Bean,并将该 Bean 作为方面 Bean 处理。
在 Spring 容器中配置方面 Bean(即带 @Aspect 注释的 Bean),与配置普通 Bean 没有任何区别,一样使用
不用xml配置文件的情况下,通过@Configuration来装配Spring bean,@EnableAspectJAutoProxy来启动spring AOP功能。见《Spring 3.1新特性之二:@Enable*注解的源码,spring源码分析之定时任务Scheduled注解》
使用 @Aspect 标注一个 Java 类,该 Java 类将会作为方面 Bean,如下面代码片段所示:
package com.dxz.aop.demo6;
import org.aspectj.lang.annotation.Aspect;
// 使用 @Aspect 定义一个方面类
@Aspect
public class LogAspect {
// 定义该类的其他内容
//...
}
方面类(用 @Aspect 修饰的类)和其他类一样可以有方法、属性定义,还可能包括切入点、增强处理定义。
当我们使用 @Aspect 来修饰一个 Java 类之后,Spring 将不会把该 Bean 当成组件 Bean 处理,因此负责自动增强的后处理 Bean 将会略过该 Bean,不会对该 Bean 进行任何增强处理。
开发时无须担心使用 @Aspect 定义的方面类被增强处理,当 Spring 容器检测到某个 Bean 类使用了 @Aspect 标注之后,Spring 容器不会对该 Bean 类进行增强。
下面将会考虑采用 Spring AOP 来改写前面介绍的例子:
下面例子使用一个简单的 Chinese 类来模拟业务逻辑组件:
Chinese.java
package com.dxz.aop.demo6;
import org.springframework.stereotype.Component;
@Component
public class Chinese {
// 实现 Person 接口的 sayHello() 方法
public String sayHello(String name) {
String ret = name + " Hello , Spring AOP";
System.out.println(ret);
return ret;
}
// 定义一个 eat() 方法
public void eat(String food) {
System.out.println("我正在吃 :" + food);
}
}
提供了上面 Chinese 类之后,接下来假设同样需要为上面 Chinese 类的每个方法增加事务控制、日志记录,此时可以考虑使用 Around、AfterReturning 两种增强处理。
先看 AfterReturning 增强处理代码。
AfterReturningAdviceTest.java
package com.dxz.aop.demo6;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
// 定义一个方面
@Aspect
public class AfterReturningAdviceTest {
// 匹配 com.dxz.aop.demo6 包下所有类的下的所有方法的执行作为切入点
@AfterReturning(returning = "rvt", pointcut = "execution(* com.dxz.aop.demo6.*.*(..))")
public void log(Object rvt) {
System.out.println("AfterReturningAdviceTest==获取目标方法返回值 :" + rvt);
}
}
上面 Aspect 类使用了 @Aspect 修饰,这样 Spring 会将它当成一个方面 Bean 进行处理。其中程序中粗体字代码指定将会在调用 org.crazyit.app.service.impl 包下的所有类的所有方法之后织入 log(Object rvt) 方法。
再看 Around 增强处理代码:
package com.dxz.aop.demo6;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
// 定义一个方面
@Aspect
public class AroundAdviceTest {
// 匹配 com.dxz.aop.demo6 包下所有类的下的所有方法的执行作为切入点
@Around("execution(* com.dxz.aop.demo6.*.*(..))")
public Object processTx(ProceedingJoinPoint jp) throws java.lang.Throwable {
System.out.println("AroundAdviceTest==执行目标方法之前,模拟开始事务 ...");
// 执行目标方法,并保存目标方法执行后的返回值
Object rvt = jp.proceed(new String[] { "被改变的参数" });
System.out.println("AroundAdviceTest==执行目标方法之后,模拟结束事务 ...");
return rvt + " 新增的内容";
}
}
与前面的 AfterReturning 增强处理类似的,此处同样使用了 @Aspect 来修饰前面 Bean,其中粗体字代码指定在调用com.dxz.aop.demo6 包下的所有类的所有方法的“前后(Around)” 织入 processTx(ProceedingJoinPoint jp) 方法需要指出的是,虽然此处只介绍了 Spring AOP 的 AfterReturning、Around 两种增强处理,但实际上 Spring 还支持 Before、After、AfterThrowing 等增强处理,关于 Spring AOP 编程更多、更细致的编程细节,可以参考《轻量级 Java EE 企业应用实战》一书。
本示例采用了 Spring 的零配置来开启 Spring AOP,因此上面 Chinese 类使用了 @Component 修饰,而方面 Bean 则使用了 @Aspect 修饰,方面 Bean 中的 Advice 则分别使用了 @AfterReturning、@Around 修饰。接下来只要为 Spring 提供如下配置文件即可:
applicationContext-aop6.xml
接下来按传统方式来获取 Spring 容器中 chinese Bean、并调用该 Bean 的两个方法,程序代码如下:
BeanTest.java
package com.dxz.aop.demo6;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class BeanTest {
public static void main(String[] args) {
// 创建 Spring 容器
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext-aop6.xml");
Chinese p = ctx.getBean("chinese", Chinese.class);
System.out.println(p.sayHello("张三"));
p.eat("西瓜");
}
}
从上面开发过程可以看出,对于 Spring AOP 而言,开发者提供的业务组件、方面 Bean 并没有任何特别的地方。只是方面 Bean 需要使用 @Aspect 修饰即可。程序不需要使用特别的编译器、织入器进行处理。
运行上面程序,将可以看到如下执行结果:
虽然程序是在调用 Chinese 对象的 sayHello、eat 两个方法,但从上面运行结果不难看出:实际执行的绝对不是 Chinese 对象的方法,而是 AOP 代理的方法。也就是说,Spring AOP 同样为 Chinese 类生成了 AOP 代理类。这一点可通过在程序中增加如下代码看出:
System.out.println(p.getClass());
上面代码可以输出 p 变量所引用对象的实现类,再次执行程序将可以看到上面代码产生class com.dxz.aop.demo6.Chinese$$EnhancerBySpringCGLIB$$7d0b6d20的输出,这才是 p 变量所引用的对象的实现类,这个类也就是 Spring AOP 动态生成的 AOP 代理类。从 AOP 代理类的类名可以看出,AOP 代理类是由 CGLIB 来生成的。
如果将上面程序程序稍作修改:只要让上面业务逻辑类 Chinese 类实现一个任意接口——这种做法更符合 Spring 所倡导的“面向接口编程”的原则。假设程序为 Chinese 类提供如下 Person 接口,并让 Chinese 类实现该接口:
Person.java
package com.dxz.aop.demo6;
public interface Person {
String sayHello(String name);
void eat(String food);
}
Chinese修改实现Person接口:
@Component
public class Chinese implements Person {
接下来让 BeanTest 类面向 Person 接口、而不是 Chinese 类编程。即将 BeanTest 类改为如下形式:
BeanTest.java
package com.dxz.aop.demo6;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class BeanTest {
public static void main(String[] args) {
// 创建 Spring 容器
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext-aop6.xml");
Person p = ctx.getBean("chinese", Person.class);
System.out.println(p.sayHello("张三"));
p.eat("西瓜");
System.out.println(p.getClass());
}
}
原来的程序是将面向 Chinese 类编程,现在将该程序改为面向 Person 接口编程,再次运行该程序,程序运行结果没有发生改变。只是 System.out.println(p.getClass()); 将会输出 class com.sun.proxy.$Proxy10,这说明此时的 AOP 代理并不是由 CGLIB 生成的,而是由 JDK 动态代理生成的。
Spring AOP 框架对 AOP 代理类的处理原则是:如果目标对象的实现类实现了接口,Spring AOP 将会采用 JDK 动态代理来生成 AOP 代理类;如果目标对象的实现类没有实现接口,Spring AOP 将会采用 CGLIB 来生成 AOP 代理类——不过这个选择过程对开发者完全透明、开发者也无需关心。
Spring AOP 会动态选择使用 JDK 动态代理、CGLIB 来生成 AOP 代理,如果目标类实现了接口,Spring AOP 则无需 CGLIB 的支持,直接使用 JDK 提供的 Proxy 和 InvocationHandler 来生成 AOP 代理即可。关于如何 Proxy 和 InvocationHandler 来生成动态代理不在本文介绍范围之内,如果读者对 Proxy 和 InvocationHandler 的用法感兴趣则可自行参考 Java API 文档或《疯狂 Java 讲义》。
在spring3.1及以上,spring可以不用xml配置装载bean了,在@Configuration注解的环境里,可以通过@EnableAspectJAutoProxy启动spring AOP功能。微调上面的示例如下:
在@Aspect中
package com.dxz.aop.demo6;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.context.annotation.Configuration;
// 定义一个方面
@Aspect
@Configuration
public class AfterReturningAdviceTest {
// 匹配 com.dxz.aop.demo6 包下所有类的下的所有方法的执行作为切入点
@AfterReturning(returning = "rvt", pointcut = "execution(* com.dxz.aop.demo6.*.*(..))")
public void log(Object rvt) {
System.out.println("AfterReturningAdviceTest==获取目标方法返回值 :" + rvt);
}
}
增加spring配置类
package com.dxz.aop.demo6;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.context.annotation.Import;
@Configuration
@ComponentScan
@EnableAspectJAutoProxy
@Import({AfterReturningAdviceTest.class})/*@Aspect可以生效,相当于Configuration类作用,都是配置类*/
public class AppConfig {
@Bean(name = "chinese")
public Chinese chinese() {
return new Chinese();
}
}
启动类:
package com.dxz.aop.demo6;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class Test6 {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
Person outPut = (Person) context.getBean("chinese");
outPut.sayHello("duan");
}
}
结果:
信息: Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@c4437c4: startup date [Tue Dec 26 17:03:32 CST 2017]; root of context hierarchy
AfterReturningAdviceTest==获取目标方法返回值 :com.dxz.aop.demo6.Chinese@72967906
duan Hello , Spring AOP
AfterReturningAdviceTest==获取目标方法返回值 :duan Hello , Spring AOP
三、注解
2.1 @Aspect
作用是把当前类标识为一个切面供容器读取
2.2 @Before
标识一个前置增强方法,相当于BeforeAdvice的功能,相似功能的还有
2.3 @AfterReturning
后置增强,相当于AfterReturningAdvice,方法正常退出时执行
2.4 @AfterThrowing
异常抛出增强,相当于ThrowsAdvice
2.5 @After
final增强,不管是抛出异常或者正常退出都会执行
2.6 @Around
环绕增强,相当于MethodInterceptor
2.7 @DeclareParents
引介增强,相当于IntroductionInterceptor
execution函数用于匹配方法执行的连接点,语法为:
execution(方法修饰符(可选) 返回类型 方法名 参数 异常模式(可选))
参数部分允许使用通配符:
* 匹配任意字符,但只能匹配一个元素
.. 匹配任意字符,可以匹配任意多个元素,表示类时,必须和*联合使用
+ 必须跟在类名后面,如Horseman+,表示类本身和继承或扩展指定类的所有类
示例中的* chop(..)解读为:
方法修饰符 无
返回类型 *匹配任意数量字符,表示返回类型不限
方法名 chop表示匹配名称为chop的方法
参数 (..)表示匹配任意数量和类型的输入参数
异常模式 不限
更多示例:
void chop(String,int)
匹配目标类任意修饰符方法、返回void、方法名chop、带有一个String和一个int型参数的方法
public void chop(*)
匹配目标类public修饰、返回void、方法名chop、带有一个任意类型参数的方法
public String *o*(..)
匹配目标类public修饰、返回String类型、方法名中带有一个o字符、带有任意数量任意类型参数的方法
public void *o*(String,..)
匹配目标类public修饰、返回void、方法名中带有一个o字符、带有任意数量任意类型参数,但第一个参数必须有且为String型的方法
也可以指定类:
public void examples.chap03.Horseman.*(..)
匹配Horseman的public修饰、返回void、不限方法名、带有任意数量任意类型参数的方法
public void examples.chap03.*man.*(..)
匹配以man结尾的类中public修饰、返回void、不限方法名、带有任意数量任意类型参数的方法
指定包:
public void examples.chap03.*.chop(..)
匹配examples.chap03包下所有类中public修饰、返回void、方法名chop、带有任意数量任意类型参数的方法
public void examples..*.chop(..)
匹配examples.包下和所有子包中的类中public修饰、返回void、方法名chop、带有任意数量任意类型参数的方法
可以用这些表达式替换StorageAdvisor中的代码并观察效果
除了execution(),Spring中还支持其他多个函数,这里列出名称和简单介绍,以方便根据需要进行更详细的查询
4.1 @annotation()
表示标注了指定注解的目标类方法
例如 @annotation(org.springframework.transaction.annotation.Transactional) 表示标注了@Transactional的方法
4.2 args()
通过目标类方法的参数类型指定切点
例如 args(String) 表示有且仅有一个String型参数的方法
4.3 @args()
通过目标类参数的对象类型是否标注了指定注解指定切点
如 @args(org.springframework.stereotype.Service) 表示有且仅有一个标注了@Service的类参数的方法
4.4 within()
通过类名指定切点
如 with(examples.chap03.Horseman) 表示Horseman的所有方法
4.5 target()
通过类名指定,同时包含所有子类
如 target(examples.chap03.Horseman) 且Elephantman extends Horseman,则两个类的所有方法都匹配
4.6 @within()
匹配标注了指定注解的类及其所有子类
如 @within(org.springframework.stereotype.Service) 给Horseman加上@Service标注,则Horseman和Elephantman 的所有方法都匹配
4.7 @target()
所有标注了指定注解的类
如 @target(org.springframework.stereotype.Service) 表示所有标注了@Service的类的所有方法
4.8 this()
大部分时候和target()相同,区别是this是在运行时生成代理类后,才判断代理类与指定的对象类型是否匹配
表达式可由多个切点函数通过逻辑运算组成
5.1 &&
与操作,求交集,也可以写成and
例如 execution(* chop(..)) && target(Horseman) 表示Horseman及其子类的chop方法
5.2 ||
或操作,求并集,也可以写成or
例如 execution(* chop(..)) || args(String) 表示名称为chop的方法或者有一个String型参数的方法
5.3 !
非操作,求反集,也可以写成not
例如 execution(* chop(..)) and !args(String) 表示名称为chop的方法但是不能是只有一个String型参数的方法