spring的两个核心思想就是LOC和AOP,前面已经总结了LOC,这一部分则是关于AOP的知识。
AOP概述
AOP翻译过来叫做面向切面编程,与面向对象编程类似,是一种编程思想。AOP 的全称是“Aspect Oriented Programming”,它将业务逻辑的各个部分进行隔离,使开发人员在编写业务逻辑时可以专心于核心业务,从而提高了开发效率。
查了很多资料,如何理解这个面向切面是一个比较难解释的问题。做一个可能不太合理的假设,首先我们将类的执行看做是一条直线,而许多个类就对应许多条直线,这些类都需要同一个功能,按照传统的编程思想,我们的做法是在每个类中都添加这样一个方法,而面向切面就是将这些统一的方法提取出来,不需要在每个类中执行,而是在类执行到一定关键点的时候转而执行这个共同的方法,于是模型就可以理解成有一个切面,所有的直线都穿过了这个切面并且仅有一个交点,即每个类的执行过程在一定的时间点被切面横切,这个切面就是公共部分的代码。
在AOP的知识中常使用下面的概念:
这里可以结合课本上的图示来理解:
程序流程用一个直线来表示,那么切面就是一个横向的切割,这个切面实际上就是一段程序代码,这段代码将会被植入到程序流程中。连接点与切入点是两个容易混淆的点,个人是这样理解的,连接点是程序流程中的一些阶段点,可以看做一些关键的时间点,而切入点则是需要插入切面的点,也就是说连接点不一定都是切入点,反过切入点也不一定都是连接点。
通知则可以看做是某个切入点被横切后所采取的处理逻辑,在切入点处拦截程序之后,通过通知来执行切面。
这么多的概念之中,一定要记住,切面就是要插入的代码,切入点就是要插入代码的位置。剩下的概念在实际使用时多使用就能理解了。
Spring JDK动态代理
AOP的实现有很多种方式,这一节主要是利用java.lang.reflect.Proxy来实现,下面结合实际的例子来演示JDK的动态代理。
首先在MyEclipse中创建一个名称为springDemo03的Web项目,将Spring支持和依赖的JAR包复制到Web项目的WEB-INF/lib目录中,并发布到类路径下。在项目的src目录下创建一个名为com.mengma.dao的包,在该包下创建一个CustomerDao接口,代码如下:
package com.mengma.dao;
public interface CustomerDao {
public void add(); // 添加
public void update(); // 修改
public void delete(); // 删除
public void find(); // 查询
}
之后在同一路径下编写这个接口的实现类,代码如下:
package com.mengma.dao;
public class CustomerDaoImpl implements CustomerDao {
@Override
public void add() {
System.out.println("添加客户...");
}
@Override
public void update() {
System.out.println("修改客户...");
}
@Override
public void delete() {
System.out.println("删除客户...");
}
@Override
public void find() {
System.out.println("修改客户...");
}
}
现在我们编写好了程序的执行的过程,即上面图中的那条直线就搞定了,但是还需要编写切面代码,即要插入的代码,在src目录下,创建一个名为com.mengma.jdk的包,在该包下创建一个切面类MyAspect,编辑后如下:
package com.mengma.jdk;
public class MyAspect {
public void myBefore() {
System.out.println("方法执行之前");
}
public void myAfter() {
System.out.println("方法执行之后");
}
}
现在切面和程序执行都准备好了,需要的是将这两个合并在一起,这里采用java.lang.reflect.Proxy实现JDK动态代理,在com.mengma.jdk包下创建一个名为MyBeanFactory的类,代码如下:
package com.mengma.jdk;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import com.mengma.dao.CustomerDao;
import com.mengma.dao.CustomerDaoImpl;
public class MyBeanFactory {
public static CustomerDao getBean() {
// 准备目标类
final CustomerDao customerDao = new CustomerDaoImpl();
// 创建切面类实例
final MyAspect myAspect = new MyAspect();
// 使用代理类,进行增强
return (CustomerDao) Proxy.newProxyInstance(
MyBeanFactory.class.getClassLoader(),
new Class[] {
CustomerDao.class }, new InvocationHandler() {
public Object invoke(Object proxy, Method method,
Object[] args) throws Throwable {
myAspect.myBefore(); // 前增强
Object obj = method.invoke(customerDao, args);
myAspect.myAfter(); // 后增强
return obj;
}
});
}
}
这段代码是其中最重要的,就是利用切面和程序流程,制作一个代理类,所谓代理类,可以看作是买房的时候的房屋中介,你不和房主直接沟通,一切都是经由房屋中介来交流,这里也是一个道理,代理类将切面整合入原有的代码,再交给程序去使用,从而实现了AOP,参考这里的代码,这段代码是仿照了loC中的beanfactory,实际上不一定要这么写,再看详细的代码,通过编写方法getBean来实现代理,在这个方法中创建一个目标类,即要插入切面的对象,之后再实例化一个切面对象,之后利用Proxy来整合。
这段代码有很多种写法,首先明确的是返回的结果一定是一个目标类的对象,这一点是毋庸置疑的,而我们要做的就是将切面类对象整合进去,利用的就是Proxy的newProxyInstance()方法,这个方法的第一个参数是当前类的类加载器,本人也不知道什么是类加载器,所以直接按照代码的写法:当前类名.class.getClassLoader(),第二参数是所创建实例的实现类的接口,即代理类需要实现的所有接口的类名,由于所有类名都需要写进去,所以使用了一个数组的形式,第三个参数就是需要增强的方法,在这个方法中我们要编写整合的顺序,按照这个格式写即可。
编写好这个类之后通过junit编写测试类,测试代码和运行结果如下:

package com.mengma.jdk;
import org.junit.Test;
import com.mengma.dao.CustomerDao;
public class JDKProxyTest {
@Test
public void test() {
// 从工厂获得指定的内容(相当于spring获得,但此内容时代理对象)
CustomerDao customerDao = MyBeanFactory.getBean();
// 执行方法
customerDao.add();
customerDao.update();
customerDao.delete();
customerDao.find();
}
}
可见增强之前并没有“方法执行之前/后”的输出,现在增强后补上了,实际上就是我们的切面对象的代码整合进了目标对象中。
除此之外,代理类的编写也可以采用下面这种写法:
package com.mengma.jdk;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import com.mengma.dao.CustomerDao;
import com.mengma.dao.CustomerDaoImpl;
public class MyBeanFactory {
public static CustomerDao getBean() {
// 准备目标类
final CustomerDao customerDao = new CustomerDaoImpl();
// 创建切面类实例
final MyAspect myAspect = new MyAspect();
// 使用代理类,进行增强
CustomerDao proxySvice = (CustomerDao) Proxy.newProxyInstance(
MyBeanFactory.class.getClassLoader(),
customerDao.getClass().getInterfaces() , new InvocationHandler() {
public Object invoke(Object proxy, Method method,
Object[] args) throws Throwable {
myAspect.myBefore(); // 前增强
Object obj = method.invoke(customerDao, args);
myAspect.myAfter(); // 后增强
return obj;
}
});
return proxySvice;
}
}
Proxy.newProxyInstance方法中的第二个参数换成目标类对象名.getClass().getInterfaces(),这样就不用写成数组的形式了,此外只要最后返回的是一个目标类对象就可以,所以我们也完全可以先获得对象再返回,效果上是一样的。
Spring CGLlB动态代理
CGLIB动态代理是Spring的另一种代理方式,JDK动态代理必须要实现一个或多个接口,如果不希望实现接口,则可以使用CGLIB代理。
两种代理的区别在于JDK动态代理是利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理。而CGLIB动态代理是利用asm开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP,如果目标对象没有实现了接口,则必须采用CGLIB库。
总而言之,两种动态代理的区别就在于是对类做代理还是对接口做代理。
下面结合实例来演示,在com.mengma.dao包下创建目标类GoodsDao,在类中定义增、删、改、查方法,并在每个方法编写输出语句,由于CGLIB是对类做代理,所以不需要实现接口,直接用类即可,代码如下所示:
package com.mengma.dao;
public class GoodsDao {
public void add() {
System.out.println("添加商品...");
}
public void update() {
System.out.println("修改商品...");
}
public void delete() {
System.out.println("删除商品...");
}
public void find() {
System.out.println("修改商品...");
}
}
之后创建代理类,在src目录下创建一个名为com.mengma.cglib的包,该包下创建类MyBeanFactory,如下所示:
package com.mengma.cglib;
import java.lang.reflect.Method;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import com.mengma.dao.GoodsDao;
import com.mengma.jdk.MyAspect;
public class MyBeanFactory {
public static GoodsDao getBean() {
// 准备目标类
final GoodsDao goodsDao = new GoodsDao();
// 创建切面类实例
final MyAspect myAspect = new MyAspect();
// 生成代理类,CGLIB在运行时,生成指定对象的子类,增强
Enhancer enhancer = new Enhancer();
// 确定需要增强的类
enhancer.setSuperclass(goodsDao.getClass());
// 添加回调函数
enhancer.setCallback(new MethodInterceptor() {
// intercept 相当于 jdk invoke,前三个参数与 jdk invoke—致
@Override
public Object intercept(Object proxy, Method method, Object[] args,
MethodProxy methodProxy) throws Throwable {
myAspect.myBefore(); // 前增强
Object obj = method.invoke(goodsDao, args); // 目标方法执行
myAspect.myAfter(); // 后增强
return obj;
}
});
// 创建代理类
GoodsDao goodsDaoProxy = (GoodsDao) enhancer.create();
return goodsDaoProxy;
}
}
最后在com.mengma.cglib包下创建测试类CGLIBProxyTest,编辑后如下所示:
package com.mengma.cglib;
import org.junit.Test;
import com.mengma.dao.GoodsDao;
public class CGLIBProxyTest {
@Test
public void test() {
// 从工厂获得指定的内容(相当于spring获得,但此内容时代理对象)
GoodsDao goodsDao = MyBeanFactory.getBean();
// 执行方法
goodsDao.add();
goodsDao.update();
goodsDao.delete();
goodsDao.find();
}
}
最终运行结果如下:
两种方式的区别主要在于创建代理类,CGLIB采用的是enhancer。
Spring通知类型及使用ProxyFactoryBean创建AOP代理
前面的两节捯饬了半天,实在是搞不清楚这两节和现在这一节是什么关系,JDK动态代理和CGLIB代理在实际使用时貌似不需要由用户来实现,而是利用ProxyFactoryBean通过实现接口的方式来创建AOP代理,所以上面两节难不成讲的是实现的具体细节?个人已经蒙逼,后面边学边用吧。
所谓通知,就是要插入进切入点的代码,切入点和通知一起构成了切面,SpringAOP为通知(Advice)提供了org.aopalliance.aop.Advice接口。
Spring 通知按照在目标类方法的连接点位置,可以分为以下五种类型:
需要采用哪一种方式,就编写切面方法来实现对应的接口。
Spring创建一个AOP代理的基本方法是使用org.springframework.aop.framework.ProxyFactoryBean,这个类对应的切入点和通知提供了完整的控制能力,并可以生成指定的内容。
ProxyFactoryBean类中的常用可配置属性如下表所示:
这些属性要在xml文件里面进行修改和配置。
下面结合一个环绕通知的实例来演示一下创建AOP代理,首先,创建AOP代理需要在原来spring的jar包的基础上导入新的两个包,一个在原来spring的大合集里面有,直接搜索导入即可,另一个需要现下载,直接百度搜索包的名字就有下载方式。
spring-aop-3.2.13.RELEASE.jar:是 Spring 为 AOP 提供的实现,在 Spring 的包中已经提供。
com.springsource.org.aopalliance-1.0.0.jar:是 AOP 提供的规范。
将这两个包下载下来放在lib文件夹下即可。
在src目录下创建一个名为com.mengma.factorybean的包,在该包下创建切面类MyAspect,由于这里演示的是环绕通知,所以查前面的表格可以得到我们需要继承MethodInterceptor接口,实现接口中的invoke()方法,而invoke()方法用于确定目标方法mi,并告诉Spring要在目标方法前后执行哪些方法,这里为了简单只输出了一段代码,可以根据需要换作其他的操作,具体代码如下:
package com.mengma.factorybean;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
public class MyAspect implements MethodInterceptor {
public Object invoke(MethodInvocation mi) throws Throwable {
System.out.println("方法执行之前");
// 执行目标方法
Object obj = mi.proceed();
System.out.println("方法执行之后");
return obj;
}
}
之后修改配置文件,在com.mengma.factorybean包下创建配置文件applicationContext.xml,如下所示:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!--目标类 -->
<bean id="customerDao" class="com.mengma.dao.CustomerDaoImpl" />
<!-- 通知 advice -->
<bean id="myAspect" class="com.mengma.factorybean.MyAspect" />
<!--生成代理对象 -->
<bean id="customerDaoProxy"
class="org.springframework.aop.framework.ProxyFactoryBean">
<!--代理实现的接口 -->
<property name="proxyInterfaces" value="com.mengma.dao.CustomerDao" />
<!--代理的目标对象 -->
<property name="target" ref="customerDao" />
<!--用通知增强目标 -->
<property name="interceptorNames" value="myAspect" />
<!-- 如何生成代理,true:使用cglib; false :使用jdk动态代理 -->
<property name="proxyTargetClass" value="true" />
</bean>
</beans>
根据代码可以看出,首先我们在容器里面实例化了一个目标类对象,又实例化了一个通知对象,用这个通知去增强目标对象,增强后的仍然是一个实例化的bean,通过实现spring里面的jar包的接口,来完成代理的配置。
最后在com.mengma.factorybean包下创建一个名为FactoryBeanTest的测试类,代码如下:
package com.mengma.factorybean;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.mengma.dao.CustomerDao;
public class FactoryBeanTest {
@Test
public void test() {
String xmlPath = "com/mengma/factorybean/applicationContext.xml";
ApplicationContext applicationContext = new ClassPathXmlApplicationContext(
xmlPath);
CustomerDao customerDao = (CustomerDao) applicationContext
.getBean("customerDaoProxy");
customerDao.add();
customerDao.update();
customerDao.delete();
customerDao.find();
}
}
这里可以看出,我们从spring容器里面取出来的是增强后的bean,而这个增强的过程是由spring实现的,编程人员并没有进行操作,只是实现了通知类和一些配置而已,这样就大大节省了时间。直接获得增强后的bean,使用其中的方法,会在关键的切入点使用通知中的方法,效果如下图: