一、 Introduction
在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
通过AOP技术,旨在处理编程中涉及到的日志记录,性能统计,安全控制,事务处理,异常处理等需求时,将这些代码从业务逻辑代码中划分出来,进而将它们独立开来到非指导业务逻辑的方法中,以便处理这些需求时不影响或者减少影响业务逻辑的代码。(资料来自百度百科)
Spring框架的AOP技术底层是动态代理技术,提供了两种方式:1,基于jdk的动态代理,必须要面向接口,只有创建实现了具体接口的类,才能生成代理对象;2,基于CGLIB的动态代理,对于没有实现接口的类,也可以产生代理,方式是创建这个类的子类。
简单解释下代理,就是代替某某处理或办理本来由某某办的事情。比如,烟酒代理,本来卖烟酒是工厂的事情,如果厂商直接销售给顾客商品,当厂商的一批商品出厂后,所有的销售方案都已经定下来,它根据这一套方案去销售,但是,在销售过程中,突然发现,需要根据不同区域顾客的差异性,采取相应的销售方案,但是方案已经初始化,如果更改,将需要很大的工程,这就暴露了厂商与顾客高耦合的缺点。那么,如果使用代理,酒厂把销售的业务委托给代理来做。尽管,酒厂的方案已经初始化,但是代理既可以在酒厂初始化销售方案之前就有一套因地制宜的方案,这可以理解为代码实现中的静态代理,也可以在销售的过程中,根据新的情况,设定新的方案,这可以理解为代码实现中的动态代理,那么,这个动态代理就类似于spring中aop动态代理的实现方式。
二、 JDK的动态代理方式
业务需求:某工厂已经实现了生产,销售和售后的全部过程。突然发现,在生产环节需要记录生产日期并计算获得保质期,假设保质期为6个月。需要将生产日期记录在生产过程之前,将保质期记录在生产过程之后。请在不修改已发布的业务的前提下,为生产环节添加记录生产日期和保质期的业务。
例子:
首先必须要有一个被代理类(委托类)的接口
public interface DynamicProxy {
public voidproduce();
public voidsell();
public voidservice();
}
然后,是这个实现了接口的被代理类
public class DynamicProxyImpl implements DynamicProxy{
public voidproduce() {
System.out.println("生产中");
}
public voidsell() {
System.out.println("销售了");
}
public voidservice() {
System.out.println("售后");
}
}
其次,是创建被代理类的代理类
public class SetProxy {
public staticDynamicProxy setProxy(final DynamicProxy dynProxy){
DynamicProxy newProxy=(DynamicProxy)Proxy.newProxyInstance(dynProxy.getClass().getClassLoader(),dynProxy.getClass().getInterfaces(),newInvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object newProduce=null;
if("produce".equalsIgnoreCase(method.getName())){
Date produceDate=new Date();
SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
System.out.println("生产日期:"+sdf.format(produceDate));
newProduce=method.invoke(dynProxy, args);
Calendar cal=Calendar.getInstance();
cal.setTime(produceDate);
cal.add(Calendar.MONTH, 6);
System.out.println("保质期:"+sdf.format(cal.getTime()));
}else{
newProduce=method.invoke(dynProxy, args);
}
return newProduce;
}
});
return newProxy;
}
}
最后,测试
public class TestProxy {
@Test
public voidtest(){
DynamicProxy dp=new DynamicProxyImpl();
dp.produce();
dp.sell();
dp.service();
System.out.println("----------------------------------------");
DynamicProxy newProxy=SetProxy.setProxy(dp);
newProxy.produce();
newProxy.sell();
newProxy.service();
}
}
三、 CGLIB实现动态代理
CGLIB,CodeGeneraton Library是一个开源项目!!是一个高大上的code生成类库,它可以在运行期扩展java类和实现java接口。使用CGLIB可以实现代理的功能,前提是导入第三方类库cglib.jar和asm.jar。
四、 spring实现AOP
(一) AOP相关术语
1. Joinpoint,连接点,可以被cglib拦截到的点,比如工厂产销后中,生产中,销售,售后,这些都可以作为连接点,另外,属性,构造函数,静态代码块也可以作为连接点。而在spring2.0之后,spring中的连接点只能是方法,是方法,方法。
2. Pointcut,切入点,使用cglib拦截的点。与连接点相比,这个切入点。实际上,一个切入点可以对多个连接点拦截,比如,对汽车限号限行,每个汽车都是连接点,但是指定的尾号,也就是切入点,可以拦截所有的这个尾号的车,而不只是一个。
3. Advice,通知,就是连接点被拦截后需要执行的业务。通知分为前置通知,后置通知,异常通知,最终通知,环绕通知(切面要完成的功能)。
4. Introduction,引介,给一个现有类添加方法或字段属性,引入还可以在不改变现有类代码的情况下,让现有的 Java 类实现新的接口 (以及一个对应的实现 )。
5. target,目标对象,要代理的对象。比如,上例子中的工厂。
6. proxy,代理对象,实现代理业务的对象,可以认为是甲方乙方外的第三方,比如,代理律师。在上述工厂例子上,重现对生产环节的业务进行增强后的结果就形成了一个代理对象,也就是说原来的生产业务不适用了,使用代理它的业务代码。
7. weaving,织入,把通知和切入点合成的切面,应用到目标对象的过程,也就是创建代理的过程,就是织入。一般,织入有运行时织入,编译期织入,类加载器织入3种,spring属于在运行时织入。
8. aspect,AOP的第一个字母a,通知和切入点合成的东西就是切面。
(二) spring实现AOP详细步骤
1. 导入spring相关jar包
spring-aop-4.2.4.RELEASE.jar,com.springsource.org.aopalliance-1.0.0.jar,com.springsource.org.aspectj.weaver-1.6.8.RELEASE.jar,spring-aspects-4.2.4.RELEASE.jar
2. 配置spring核心配置文件xml
引入spring官方提供的AOP的schema约束。
3. 编写业务需要的接口和类
4. 将目标类,即target装配到xml
5. 创建切面类,专门用于处理代理业务的类。
6. 将切面类装配到xml。
7. 在xml中配置切面业务。
8. 测试。
9. 例子。
业务需求同上,某工厂已经实现了生产,销售和售后的全部过程。突然发现,在生产环节需要记录生产日期并计算获得保质期,假设保质期为6个月。需要将生产日期记录在生产过程之前,将保质期记录在生产过程之后。请在不修改已发布的业务的前提下,为生产环节添加记录生产日期和保质期的业务。
创建工厂,可以不需要接口
public class FactoryImpl{
public voidproduce() {
System.out.println("生产中");
}
public voidsell() {
System.out.println("销售了");
}
public voidservice() {
System.out.println("售后");
}
}
创建工厂的切面类
public class FactoryAspect {
Date pruduceDate;
public voidproduceDate(){
Date produceDate=new Date();
SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-ddhh:mm:ss");
System.out.println("生产日期:"+sdf.format(produceDate));
this.pruduceDate=produceDate;
}
public voidqualityDate(){
Date produceDate=this.pruduceDate;
SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-ddhh:mm:ss");
Calendar cal=Calendar.getInstance();
cal.setTime(produceDate);
cal.add(Calendar.MONTH, 6);
System.out.println("保质期:"+sdf.format(cal.getTime()));
}
}
配置xml中的切面业务
<!-- 将目标对象和代理对象的bean组件装配到xml -->
<bean id="factory"class="springAOP2.FactoryImpl" />
<bean id="factoryAspect"class="springAOP2.FactoryAspect" />
<!-- 配置切面 -->
<aop:config>
<!-- 引入切面类 -->
<aop:aspect ref="factoryAspect">
<!-- 配置切面业务
通知类型:before afterafter-returning after-throwing around
定义切点表达式:execution(修饰符返回值类型包名.类名.方法名(参数))
-->
<aop:before method="produceDate" pointcut="execution(public voidspringAOP2.FactoryImpl.produce())" />
<aop:after method="qualityDate" pointcut="execution(public voidspringAOP2.FactoryImpl.produce())" />
</aop:aspect>
</aop:config>
上述的业务,也可以通过环绕通知的方式实现。
创建工厂同上。
创建切面类,在环绕方法中使用ProceedingJoinPoint类将连接点的方法引入。虽然这个连接点也是那个切入点,因为这个连接点还通知,所以在这里引入时不能称之为pointcut,这体现了逻辑的严谨。
public class FactoryAspect {
public voidaddDate(ProceedingJoinPoint joinpoint){
Date produceDate=new Date();
SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-ddhh:mm:ss");
System.out.println("生产日期:"+sdf.format(produceDate));
try {
joinpoint.proceed();
} catch (Throwable e){
e.printStackTrace();
}
Calendar cal=Calendar.getInstance();
cal.setTime(produceDate);
cal.add(Calendar.MONTH, 6);
System.out.println("保质期:"+sdf.format(cal.getTime()));
}
}
<!-- 配置切面 -->
<aop:config>
<!-- 引入切面类 -->
<aop:aspect ref="factoryAspect">
<!-- 配置切面业务
通知类型:before afterafter-returning after-throwing around
定义切点表达式:execution(修饰符返回值类型包名.类名.方法名(参数))
-->
<aop:around method="addDate" pointcut="execution(publicvoid springAOP2.FactoryImpl.produce())" />
</aop:aspect>
</aop:config>
另外,关于pointcut-ref的用法,是先定义一个pointcut,然后在使用pointcut-ref引用这个切入点,好处是可以使代码看起来简洁。
<!-- 配置切面 -->
<!-- 定义切入点 -->
<aop:config>
<aop:pointcut expression="execution(public voidspringAOP2.FactoryImpl.produce())" id="haha"/>
</aop:config>
<aop:config>
<!-- 引入切面类 -->
<aop:aspect ref="factoryAspect">
<!-- 配置切面业务
通知类型:before afterafter-returning after-throwing around
定义切点表达式:execution(修饰符返回值类型包名.类名.方法名(参数))
-->
<!-- <aop:before method="produceDate" pointcut="execution(publicvoid springAOP2.FactoryImpl.produce())" />
<aop:aftermethod="qualityDate" pointcut="execution(public voidspringAOP2.FactoryImpl.produce())" /> -->
<aop:around method="addDate" pointcut-ref="haha"/>
</aop:aspect>
</aop:config>
(三) 关于advice通知的类型
<aop:通知类型 …….. />
1. before
前置通知,在切入点之前执行。
2. after
最终通知,在切入点之后执行,忽略异常,保持执行。
3. after-return
后置通知,在切入点之后执行,异常后不执行。
4. after-throwing
异常通知,在异常抛出后,执行。
5. around
环绕通知,在切入点前后执行,请注意使用ProceedingJoinPoint设置切入点执行时机。
(四) 关于切面表达式,execution()的简写
基本格式:execution([修饰符] 返回值类型 包名.类名.方法名(参数))
1. 修饰符可以省略
2. 返回值类型用*表示,表示任意类型
3. 包名,一个*表示任意一层包的任意名字的包。*..*表示任意的包,前提是这个类的包至少有两层包的情况下,用*..*才有效,一层包无效。
4. 类名,用*表示任意的类,用*作为类名的占位符,如*action,*Impl,User*Impl等形式。
5. 方法名,同样,用*表示任意的方法。这样就在类下所有的方法都加入了切面。也可以用*作为占位符,具体根据需求。
6. 方法的参数,用*表示任意的一个参数,在不确定参数列表的情况下,就是不知道参数个数时,可以使用..表示任意类型任意个数的参数。
例子:execution(* *..*.*Fa*ctoryImpl.pr*oduce(..))
五、 注解方式实现AOP
基本思路是将原来在xml中配置切面类和切入点的动作,由 @ …. 来实现。
1. 导入spring相关jar包
spring-aop-4.2.4.RELEASE.jar,com.springsource.org.aopalliance-1.0.0.jar,com.springsource.org.aspectj.weaver-1.6.8.RELEASE.jar,spring-aspects-4.2.4.RELEASE.jar
2. 配置spring核心配置文件xml
引入spring官方提供的AOP的schema约束。
3. 编写业务需要的接口和类
4. 将目标类,即target装配到xml,也可以使用注解的方式装配目标类
5. 创建切面类,专门用来处理代理业务的类,并装配到xml。也可以使用注解的方式装配切面类
6. 开启aop的自动代理功能,会寻找所有的bean,如果找到被注解为@Aspect的bean,就会执行相应的切面业务,前提是这个切面要装配bean。
7. 使用注解配置切面类的业务。
8. 测试。
9. 例子。
创建目标对象类
public class FactoryImpl{
public voidproduce() {
System.out.println("生产中");
}
public voidsell() {
System.out.println("销售了");
}
public voidservice() {
System.out.println("售后");
}
}
创建代理对象类,并设置注解业务
@Controller
@Aspect
public class FactoryAspect {
Date pruduceDate;
@Before("execution(* *..*.*.produce(..))")
public voidproduceDate(){
Date produceDate=new Date();
SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-ddhh:mm:ss");
System.out.println("生产日期:"+sdf.format(produceDate));
this.pruduceDate=produceDate;
}
@After("execution(**..*.*.produce(..))")
public voidqualityDate(){
Date produceDate=this.pruduceDate;
SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-ddhh:mm:ss");
Calendar cal=Calendar.getInstance();
cal.setTime(produceDate);
cal.add(Calendar.MONTH, 6);
System.out.println("保质期:"+sdf.format(cal.getTime()));
}
}
配置xml开启aop代理功能。如果使用注解bean,请开始注解扫描。
<!-- 开启注解扫描 -->
<context:component-scan base-package="com.springAOP2Annotation"></context:component-scan>
<!-- 开启aop的注解功能,就是自动代理 -->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
<!-- 将目标对象和代理对象的bean组件装配到xml -->
<bean id="factory"class="com.springAOP2Annotation.FactoryImpl"/>
<!-- <bean id="factoryAspect"class="com.springAOP2Annotation.FactoryAspect" /> -->
测试略
10. 关于环绕通知的写法
@Controller
@Aspect
public class FactoryAspect {
Date pruduceDate;
@Before("execution(* *..*.*.produce(..))")
public voidproduceDate(){
Date produceDate=new Date();
SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-ddhh:mm:ss");
System.out.println("生产日期:"+sdf.format(produceDate));
this.pruduceDate=produceDate;
}
@After("FactoryAspect.exe()")
public voidqualityDate(){
Date produceDate=this.pruduceDate;
SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-ddhh:mm:ss");
Calendar cal=Calendar.getInstance();
cal.setTime(produceDate);
cal.add(Calendar.MONTH, 6);
System.out.println("保质期:"+sdf.format(cal.getTime()));
}
@Pointcut("execution(* *..*.*.produce(..))")
public voidexe(){}
@Around("FactoryAspect.exe()")
public voidaddDate(ProceedingJoinPoint joinpoint){
Date produceDate=new Date();
SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-ddhh:mm:ss");
System.out.println("生产日期:"+sdf.format(produceDate));
try {
joinpoint.proceed();
} catch (Throwable e) {
e.printStackTrace();
}
Calendar cal=Calendar.getInstance();
cal.setTime(produceDate);
cal.add(Calendar.MONTH, 6);
System.out.println("保质期:"+sdf.format(cal.getTime()));
}
}
注意:当前置,后置,最终和环绕作用于同一个切入点,环绕的前半部分先于前置执行,其后半部分先于后置通知和最终通知执行。
11. 关于自定义切面表达式的使用。
public classFactoryAspect {
@After("FactoryAspect.exe()")
public voidqualityDate(){
Date produceDate=this.pruduceDate;
SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-ddhh:mm:ss");
Calendar cal=Calendar.getInstance();
cal.setTime(produceDate);
cal.add(Calendar.MONTH, 6);
System.out.println("保质期:"+sdf.format(cal.getTime()));
}
@Pointcut("execution(* *..*.*.produce(..))")
public voidexe(){}
}
12. 关于通知的类型
@Before @After @AfterReturning @AfterThrowing @Around
13. 自定义切面表达式,请使用
@Pointcut("execution(...)")
public void mingzi(){}