Spring的左膀是IoC,右臂是AOP。
一、AOP的定义
AOP(Aspect-Oriented Programming,面向切面编程)中,“切面”是个关键词。什么是切面?
我们知道,Java语言是面向对象的,是OOP(Object-Oriented Programing,面向对象编程)。OOP引入封装、继承和多态性等概念来建立一种对象层次结构,用以模拟公共行为的一个集合。当我们需要为分散的对象引入公共行为的时候,OOP则显得无能为力。也就是说,OOP允许你定义从上到下的关系,但并不适合定义从左到右的关系。例如日志功能。日志代码往往水平地散布在所有对象层次中,而与它所散布到的对象的核心功能毫无关系。对于其他类型的代码,如安全性、异常处理和透明的持续性也是如此。这种散布在各处的无关的代码被称为横切(cross-cutting)代码,在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。
而AOP技术则恰恰相反,它利用一种称为“横切”的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其名为“Aspect”,即切面。所谓“切面”,简单地说,就是将那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可操作性和可维护性。AOP代表的是一个横向的关系,如果说“对象”是一个空心的圆柱体,其中封装的是对象的属性和行为;那么面向方面编程的方法,就仿佛一把利刃,将这些空心圆柱体剖开,以获得其内部的消息。而剖开的切面,也就是所谓的“方面”了。然后它又以巧夺天功的妙手将这些剖开的切面复原,不留痕迹。
使用“横切”技术,AOP把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处都基本相似。比如权限认证、日志、事务处理。AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。正如Avanade公司的高级方案构架师Adam Magee所说,AOP的核心思想就是“将应用程序中的商业逻辑同对其提供支持的通用服务进行分离。”
实现AOP的技术,主要分为两大类:一是采用动态代理技术,利用拦截方法的方式,对该方法进行装饰,以取代原有对象行为的执行;二是采用静态织入的方式,引入特定的语法创建“切面”,从而使得编译器可以在编译期间织入有关“切面”的代码。
二、AOP框架的实现
目前最流行的AOP框架有两类:Spring AOP(使用纯Java实现)和AspectJ(扩展了Java语言,提供了一个专门的编译器,在编译的时候提供横向代码的切入)。
AOP包含以下几个专业术语:
AOP的实现方式整体目录:
Spring APO:
1.1 手动代理
1.1.1 JDK动态代理
1.1.2 CGLIB
1.2 声明工厂Bean
2. AspectJ
2.1 基于XML声明 AspectJ
2.2 基于Annotation声明 AspectJ
三、手动代理
代理模式是Java中常用的设计模式,代理类通过调用被代理类的相关方法,提供预处理、过滤、事后处理等服务。AOP使用手动代理有两个典型的例子:
1.JDK动态代理
2.CGLIB代理
3.1、JDK动态代理
JDK动态代理是通过JDK中的java.lang.reflect.Proxy类来实现的。我们看一个JDK动态代理的具体实现步骤:
首先创建一个名为UserDao的接口类,接口类中定义了:增删查存四个方法。
接口实现类UserDaoImpl对UserDao接口进行了实现,分别实现了save(),update(),delete()方法。
上上图中,我们看到了@Override
@Override是伪代码,表示重写(当然不写也可以),不过写上有如下好处:
1、可以当注释用,方便阅读;
2、编译器可以给你验证@Override下面的方法名是否是你父类中所有的,如果没有则报错。例如,你如果没写@Override,而你下面的方法名又写错了,这时你的编译器是可以编译通过的,因为编译器以为这个方法是你的子类中自己增加的方法。
新创建一个包,然后创建一个切面类:MyAspect。
类中有两个增强的方法:myBefore()、myAfter(),它们主要是通知(Advice)的内容。
接下来,创建MybeanFactory类,在该类中通过Proxy实现动态代理。
该类代码中,标红部分是对UserDaoImpl()和new MyAspect()进行实例化。
该类代码中,标黄部分是返回Proxy.newProxyInstance。
Proxy.newProxyInstance()方法有三个参数:
1. 类加载器(Class Loader)
2. 需要实现的接口数组
3. InvocationHandler接口。所有动态代理类的方法调用,都会交由InvocationHandler接口实现类里的invoke()方法去处理。这是动态代理的关键所在。
我们看到invoke()调用的是MyAspect的两个方法,即分别打印“方法执行前”
“方法执行后”
package cn.itcast.jdk;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import cn.itcast.dao.UserDao;
import cn.itcast.dao.UserDaoImpl;
public class MyBeanFactory {
public static UserDao getBean(){
final UserDao userDao = new UserDaoImpl();
final MyAspect myAspect = new MyAspect();
return (UserDao)(
MyBeanFactory.class.getClassLoader(),
new Class[]{UserDao.class},
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//增强前
myAspect.myBefore();
//目标类的方式
Object obj = method.invoke(userDao, args);
//增强后
myAspect.myAfter();
return obj;
}
});
}
书写测试类:
测试类运行结果:
我们看得到了预想的结果,即实现了方法的增强。
3.2、CGLIB代理
JDK的动态代理使用起来很方便,但他的局限性在于使用对象代理的对象必须实现一个或多个接口,如果想代理没有实现接口的类,可以使用CGLIB。CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final,那么它是无法使用CGLIB做动态代理的。
CGLIB的全称是:Code Generation Library,它是一个高性能开源的代码生成包,它的底层通过使用ASM来转换字节码,为一个类创建子类,然后对子类进行增强,解决无接口的问题。
ASM在spring-core*.jar中。
# jar -xvf spring-core-5.3.1.jar
上图中,我们看到asm中包含很多类文件。
我们通过一个代码展示CGLIB代理。
首先创建一个BookDao.java类,里面包含update、delete、find方法。每个方法打印不同的字段。
我们在另外一个包里创建MyBeanFactory类。
CGLIB模式下,Enhancer是核心类。通过它进行类增强,并增强回调函数。
上图中16步是调用的cglib类库中的如下类:
[root@repo proxy]# ls -ak /spring-framework-5.3.1/libs/org/springframework/cglib/proxy/Enhancer.class
/spring-framework-5.3.1/libs/org/springframework/cglib/proxy/Enhancer.class
接下来,创建测试类:
运行测试类,结果与我们期望相同:
四、声明式工程Bean
通知(Advice)就是对其目标切入点增强的内容。AOP联盟为Advice定义了org.aopalliance.aop.Advice接口,Spring通知按照在目标类方法的连接点位置,可以分为5种类型。
我们看下图列出的5种Advice接口类型,他们的通知位置和作用都有所区别。
在Spring中创建一个AOP代理的基本方法是:使用org.springframework.aop.framework.ProxyFactoryBean这个类对应的切入点和通知提供了完整的控制能力、生成指定的内容。
ProxyFactoryBean这个类常用的可配置属性如下图所示:
从上图我们可以ProxyFactoryBean的具体实现,要么通过JDK动态代理(有接口类的时候),要么通过CGLIB(没有接口类的时候)。
ProxyFactoryBean依赖如下jar包:
spring-aop-*.jar:这个在Spring的包中已经提供:spring-aop-5.3.1.jar
com.springsource.org.aopalliance*jar
这个包在网上可以直接搜到。
接下来,我们看代码。
我们接着上一小节的实验,创建切面类MyAspect。
我们看到切面类实现了MethodInterceptor接口,并实现了该接口的invoke方法。
在Spring Aop框架中,MethodInterceptor接口被用来拦截指定的方法,对方法进行增强。
接下来,书写应用上下文配置文件。
需要注意的是,下图11-12行,是直接对一个类进行实现方式的注释,即将ProxyFactoryBean注释为Bean ID为:userDaoProxy。这是创建代理的核心配置。
第14-15行标记标记的是要实现哪个接口,即UserDao(接口的内容见3.1,包含4个方法)。
第17-18行表示的是要代理的目标,是userDao
第19行表示的是要植入的Advice,即名为myAspect的切面类。
第21行表示使用jdk动态代理。
接下来,创建测试类TestFactoryBean。
测试类中有一个名为demo01的方法。这个方法从Spring中获取到UserDaoProxy的实例---->对应上图的第11行--->代理UerDao接口--->执行切面类--->执行切面类:MyAspect中的内容。
执行结果如下:
结果符合我们的预期。
五、AspectJ开发
新版本的Spring,建议使用AspectJ来开发AOP。
AspectJ中又包含两种实现方式:
5.1 基于XML声明式的AspectJ
5.2 基于Annoitation的声明式AspectJ。
这两种实现方式中,第二种用的更多。因为第一种方式需要在XML中需要配置大量的信息。第一种方式我们不展开介绍。
Annotation的注解如下:
接下来,我们看代码。
首先定义一个切面类。
下图16行@Aspect用于声明这是一个切面类,该类作为组件使用,因此必须在第17行添加@Component才能生效。
22行@Pointcut注解用来配置切入点,然后在每个通知相应的的方法上添加注解声明,并且将切入点方法名"myPointCut“作为参数传递,指定要在哪个切入点执行这个方法。
在目标类UserDaoImpl中添加注解@Repository(),将其标识为Spring中的Bean。
书写配置文件如下。
13、14行添加扫描包,使注解生效。
16行开启切面的自动代理。
书写测试类如下:
8行@RunWith注解标识这个是一个JUnit4的测试程序
@Autowired注解将UserDao接口的实现类对象类注入到该测试类中,最后调用save()方法进行测试。
执行结果如下: