Spring AOP说明

AOP(Aspect Oriented Pragraming)面向切面编程,AOP采用横向抽取机制,取代了传统纵向继承体系的重复性代码(性能监视、事务管理、安全检查、缓存)。

所谓的传统纵向继承体系是指当你需要实现某个功能的时候需要去继承某个类,而横向抽取机制则是通过使用动态代理机制产生一个与之同级的对象,然后在代理对象中进行功能增强。

Spring AOP使用纯Java实现,不需要专门的编译过程和类加载器,在运行期通过代理方式向目标类织入增强代码。

Spring的AOP代理

JDK动态代理:对实现了接口的类生成代理
CGLib代理机制:对类生成代理

AOP相关术语

**Joinpoint(连接点):**所谓连接点是指那些被拦截到的点。在spring中,这些点指的是方法,因为spring只支持方法类型的连接点;
**Pointcut(切入点):**所谓切入点是指我们要对哪些Joinpoint进行拦截的定义;
**Advice(通知/增强):**所谓通知是指拦截到Joinpoint之后所要做的事情就是通知。通知分为前置通知,后置通知,异常通知,最终通知,环绕通知(切面要完成的功能);
**Introduction(引介):**引介是一种特殊的通知在不修改类代码的前提下, Introduction可以在运行期为类动态地添加一些方法或Field;
**Target(目标对象):**代理的目标对象;
**Weaving(织入):**是指把增强应用到目标对象来创建新的代理对象的过程。spring采用动态代理织入,而AspectJ采用编译期织入和类装载期织入;
**Proxy(代理):**一个类被AOP织入增强后,就产生一个结果代理类;
**Aspect(切面) :**是切入点和通知(引介)的结合。

AOP的底层实现

前面说过AOP代理有两种,一种是JDK动态代理,用于对实现了接口的类生成代理;另一种是CGLib代理机制,用于对类生成代理。

JDK动态代理

新建一个Maven项目,名称为SpringAOP,导入Junit依赖。新建com/envy/aop/demo文件夹,新建接口文件UserDao.java,里面的代码为:

1
2
3
4
public interface UserDao {
public void add();
public void update();
}

以及实现类UserDaoImpl.java文件:

1
2
3
4
5
6
7
8
9
public class UserDaoImpl implements UserDao {
public void add() {
System.out.println("this is add method");
}

public void update() {
System.out.println("this is update method");
}
}

接着新建com/envy/aop/test文件夹,新建测试文件AopTest.java,里面的代码为:

1
2
3
4
5
6
7
8
public class AopTest {
@Test
public void testOne(){
UserDao userDao = new UserDaoImpl();
userDao.add();
userDao.update();
}
}

运行结果:

1
2
this is add method
this is update method

上述是传统的编程方式,接下来是使用JDK动态代理模式的相关步骤,JDK动态代理模式用于对实现了接口的类生成代理,而此处的UserDao本身就是一个接口。

新建MyJDKProxy.java文件,里面代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class MyJDKProxy implements InvocationHandler {
private UserDao userDao;

public MyJDKProxy(UserDao userDao){
this.userDao=userDao;
}

public Object createProxy(){
Object proxy= Proxy.newProxyInstance(userDao.getClass().getClassLoader(),userDao.getClass().getInterfaces(),this);
return proxy;
}

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if("add".equals(method.getName())){
//一些增强功能,如权限校验
System.out.println("开始权限校验操作");
return method.invoke(proxy,args);
}
return method.invoke(userDao,args);
}
}

接着新建testTwo方法,里面代码为:

1
2
3
4
5
6
7
@Test
public void testTwo(){
UserDao userDao = new UserDaoImpl();
UserDao proxy = (UserDao) new MyJDKProxy(userDao).createProxy();
proxy.add();
proxy.update();
}

运行结果:

1
2
3
Start permission verification
this is add method
this is update method

CGLIB动态代理

CGLIB(Code Generation Library)是一个开源项目!是一个强大的,高性能,高质量的Code生成类库,它可以在运行期扩展Java类与实现Java接口。 Hibernate支持它来实现PO(Persistent Object 持久化对象)字节码的动态生成(Hibernate实际使用Javassist生成代理,该代理对象就是持久化类的子类的实例),现在做cglib的开发,可以不用直接引入cglib的包,因为spring核心中已经集成了cglib。

CGLIB生成代理机制:其实生成了一个真实对象的子类。

新建ProductDao.java文件,里面的代码为:

1
2
3
4
5
6
7
8
9
public class ProductDao {
public void add(){
System.out.println("add product");
}

public void update(){
System.out.println("update product");
}
}

接着新建测试方法testThree,里面的代码为:

1
2
3
4
5
6
@Test
public void testThree(){
ProductDao productDao = new ProductDao();
productDao.add();
productDao.update();
}

运行结果:

1
2
add product
update product

上述是传统的编程方式,接下来是使用CGLIB动态代理模式的相关步骤,CGLIB动态代理模式用于对类生成代理,而此处的ProductDao本身就是一个类。

新建MyCGLibProxy.java文件,里面代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class MyCGLibProxy implements MethodInterceptor {
private ProductDao productDao;

public MyCGLibProxy(ProductDao productDao){
this.productDao=productDao;
}

public Object createProxy(){
//1、设置核心类
Enhancer enhancer = new Enhancer();
//2、设置父类
enhancer.setSuperclass(productDao.getClass());
//3、设置回调
enhancer.setCallback(this);
//4、生成代理
Object proxy = enhancer.create();
return proxy;
}

public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
if("add".equals(method.getName())){
//一些增强功能,如权限校验
System.out.println("Start permission verification");
return methodProxy.invokeSuper(proxy,args);
}
return methodProxy.invokeSuper(proxy,args);
}
}

接着新建testFour方法,里面代码为:

1
2
3
4
5
6
7
@Test
public void testFour(){
ProductDao productDao = new ProductDao();
ProductDao proxy = (ProductDao) new MyCGLibProxy(productDao).createProxy();
proxy.add();
proxy.update();
}

运行结果:

1
2
3
Start permission verification
add product
update product

代理知识总结:1、Spring在运行期生成动态代理对象,不需要使用特殊的编译器;2、SpringAOP的底层就是通过JDK动态代理或CGLib动态代理技术为目标Bean执行横向织入。

若目标对象实现了若干接口,Spring使用JDK的java.lang.reflect.Proxy类代理;若目标对象没有实现任何接口,Spring使用CGLIB库生成目标对象的子类。在程序中应优先对接口创建代理,便于程序解耦维护。标记为final的方法不能被代理,因为该方法不能被重写。因此JDK动态代理是针对接口生成子类,接口中的方法不能使用final来修饰;CGLIb是针对目标类生成子类,类或者方法不能使用final来修饰。

Spring只支持方法连接点,不支持属性连接点。

Spring中的AOP

SpringAOP增强类型

AOP不是由Spring定义,而是AOP联盟组织来定义的。Spring按照通知Advice在目标类方法的连接点位置,可以分为5个位置:
前置通知:org.springframework.aop.MethodBeforeAdvice:在目标方法执行前实施增强;
后置通知:org.springframework.aop.AfterReturningAdvice:在目标方法执行后实施增强;
环绕通知:org.aopalliance.intercept.MethodInterceptor:在目标方法执行前后实施增强;
异常抛出通知:org.springframework.aop.ThrowsAdvice:在方法抛出异常后实施增强;
引介通知:org.springframework.aop.IntroductionInterceptor(不研究):在目标类中添加一些新的方法和属性。

SpringAOP切面类型

Advisor(Spring中传统切面)就是对PointCut应用Advice。(通常所说的Advisor是指只有一个Point和一个Advice,而Aspect是具有多个切点和多个通知的组合);
Advisor : 代表一般切面,Advice本身就是一个切面,对目标类所有方法进行拦截 (没有切点);
PointcutAdvisor : 代表具有切点的切面,可以指定拦截目标类哪些方法;
IntroductionAdvisor : 代表引介切面,针对引介通知而使用切面(不要求掌握)。

SpringAOP的开发

针对所有方法的增强(不带切点的切面)

此处演示针对所有方法的增强也就是不带切点的切面,相应的操作步骤如下:

第一步:导入相应的包。首先在pom.xml文件中引入spring-aopspring-testcom.springsource.org.aopalliance

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>4.2.4.RELEASE</version>
</dependency>

<dependency>
<groupId>aopalliance</groupId>
<artifactId>aopalliance</artifactId>
<version>1.0</version>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>4.2.4.RELEASE</version>
</dependency>

第二步:编写被代理对象。新建com.envy.aop.student包,接着在里面新建接口StudentDao.java文件,里面代码为:

1
2
3
4
5
public interface StudentDao {
public void add();

public void update();
}

以及它的实现类StudentDaoImpl.java:

1
2
3
4
5
6
7
8
9
public class StudentDaoImpl implements StudentDao {
public void add() {
System.out.println("this is add method");
}

public void update() {
System.out.println("this is update method");
}
}

第三步,编写增强的代码。此处仅仅以前置通知为例演示如何编写增强代码(前置通知:org.springframework.aop.MethodBeforeAdvice:在目标方法执行前实施增强)。新建MyBeforeAdvice.java文件:

1
2
3
4
5
public class MyBeforeAdvice implements MethodBeforeAdvice {
public void before(Method method, Object[] objects, Object o) throws Throwable {
System.out.println("前置增强的方法");
}
}

第四步,生成代理(配置生成代理)。新建applicationContext.xml文件,里面的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- 不带有切点的切面 -->
<!-- 配置目标类(目标对象,即需要给哪个类进行增强) -->
<bean id="studentDao" class="com.envy.aop.student.StudentDaoImpl"/>

<!-- 前置通知类型 -->
<bean id="myBeforeAdvice" class="com.envy.aop.student.MyBeforeAdvice"/>

<!-- SpringAOP产生代理对象 -->
<bean id="studentDaoProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
<!-- 配置目标类(目标对象,即需要给哪个类进行增强) -->
<property name="target" ref="studentDao"/>
<!-- 实现的接口 -->
<property name="proxyInterfaces" value="com.envy.aop.student.StudentDao"/>
<!-- 采用拦截的名称 -->
<property name="interceptorNames" value="myBeforeAdvice"/>
<!-- 是否强制使用CGLib,true是,false则是JDK -->
<property name="optimize" value="true"/>
</bean>

第五步,注入测试。新建SpringAOPAdvisor.java测试文件,里面的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class SpringAOPAdvisor {
// @Resource(name="studentDao") //这是不使用AOP代理对象
@Resource(name="studentDaoProxy")
private StudentDao studentDao;

@Test
public void testOne(){
studentDao.add();
studentDao.update();
}
}

运行结果:

1
2
3
4
前置增强的方法
this is add method
前置增强的方法
this is update method

在第四步,生成代理对象时我们在applicationContext.xml文件中配置了一些属性。其实就是生成代理Spring基于ProxyFactoryBean类,底层自动选择使用JDK的动态代理还是CGLIB的代理。下面是其中的几个属性介绍:target : 代理的目标对象;proxyInterfaces : 代理要实现的接口,如果多个接口可以使用以下格式赋值:

1
2
3
4
<list>
  <value></value>
    ....
</list>

proxyTargetClass : 是否对类代理而不是接口,设置为true时,使用CGLib代理;interceptorNames : 需要织入目标的Advice;singleton : 返回代理是否为单实例,默认为单例。optimize :当设置为true时,强制使用CGLib。

针对特定方法的增强(带切点的切面)

PointcutAdvisor(切点切面)。使用普通的Advisor作为切面,会将目标类所有的方法进行拦截,这种方式其实并不够灵活,在实际开发中经常会采用带有切点的切面。常用的PointcutAdvisor实现类有:DefaultPointcutAdvisorJdkRegexpMethodPointcutDefaultPointcutAdvisor是最常用的PointcutAdvisor类型,它可以通过任意Pointcut和Advice组合定义切面;而JdkRegexpMethodPointcut通常用于构造正则表达式切入点。