Spring AOP
AOP的概述
- AOP Aspect Oriented Programing 面向切面编程
- AOP采取横向抽取机制,取代了传统纵向继承体系重复性代码(性能监视、事务管理、安全检查、缓存)
- Spring AOP使用纯Java实现,不需要专门的编译过程和类加载器,在运行期通过代理方式向目标类注入增强代码
传统纵向继承与AOP相比较
一般情况而言,想要增强save方法,会多写一个权限校验函数,在save时调用,
public class UserDaoImpl implements UserDao{
public void save(User user){
checkPrivilege();
}
public void update(User user){
}
public void delete(User user){
}
public void find(User user){
}
public void checkPrivilege(){
}
}
但是也可以使用纵向继承的方法,即编写一个DaoImpl的父类,子类从父类中继承方法,直接在子类中调用父类的方法即可
public class BaseDaoImpl{
public void checkPrivilege(){}
}
public class UserDaoImpl extends BaseDaoImpl{
public void save(User user){
checkPrivilege();
}
}
AOP采用横向抽取机制
public class UserDaoImpl implements UserDao{
public void save(User user){
//保存用户,并增强方法
}
}
由UserDaoImpl->Proxy进行代理
AOP相关术语
- Joinpoint(连接点):所谓连接点是指那些被拦截到的点。在spring中,这些点指的是方法,因为spring只支持方法类型的连接点
- Pointcut(切入点):所谓切入点是指我们要对哪些Joinpoint进行拦截的定义(真正被增强的方法)
- Advice(通知/增强):所谓通知是指拦截到Joinpoint之后所要做的事情就是通知,通知分为前置通知,后置通知,异常通知,最终通知,环绕通知(切面要完成的功能)
- Introduction(引介):是一种特殊的通知在不修改类代码的前提下,Introduction可以在运行期(不是加载期或者编译期)为类动态地添加一些方法或Field
- Target(目标对象):代理的目标对象
- Weaving(织入):是指把增强应用到目标对象来创建新的代理对象的过程,spring采用动态代理织入,而AspectJ采用编译期织入和类装载期织入
- Proxy(代理):一个类被AOP织入增强后,就产生一个结果代理类
- Aspect(切面):是切入点和通知(引介)的结合
AOP底层实现
Spring底层有两种方式来完成AOP代理
JDK动态代理
使用的是JDK本身的代理类
代码实现
-
创建接口
public interface UserDao { public void save(); public void update(); public void delete(); public void find(); }
创建应用类
public class UserDaoImpl implements UserDao{ public void save() { System.out.println("保存用户"); } public void update() { System.out.println("修改用户"); } public void delete() { System.out.println("删除用户"); } public void find() { System.out.println("查找用户"); } }
创建代理类
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 ("save".equals(method.getName())) { System.out.println("权限校验..."); return method.invoke(userDao, args); } return method.invoke(userDao, args); } }
-
测试
public class SpringDemo1 { @Test public void demo1(){ UserDao userDao = new UserDaoImpl(); UserDao proxy = (UserDao) new MyJdkProxy(userDao).createProxy(); proxy.save(); proxy.update(); proxy.delete(); proxy.find(); } }
输出为
权限校验... 保存用户 修改用户 删除用户 查找用户
使用CGLIB生成代理
- 对于不使用接口的业务类,无法使用JDK动态代理
- CGlib采用非常底层的字节码技术,可以为一个类创建子类,解决无接口代理问题
代码实现
-
创建测试类
public class ProductDao { public void save() { System.out.println("保存用户"); } public void update() { System.out.println("修改用户"); } public void delete() { System.out.println("删除用户"); } public void find() { System.out.println("查找用户"); } }
public class MyCglibProxy implements MethodInterceptor { private ProductDao productDao; public MyCglibProxy(ProductDao productDao) { this.productDao = productDao; } public Object createProxy(){ // 创建核心类 Enhancer enhancer = new Enhancer(); // 设置父类 enhancer.setSuperclass(productDao.getClass()); // 设置回调 enhancer.setCallback(this); // 生成代理 Object proxy = enhancer.create(); return proxy; } public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { if ("save".equals(method.getName())){ System.out.println("权限校验==========="); return methodProxy.invokeSuper(proxy, args); } // 通过invokeSuper调用父类方法,相当于调用productDao中的方法 return methodProxy.invokeSuper(proxy, args); } }
-
测试
public class SpringDemo2 { @Test public void demo1(){ ProductDao productDao = new ProductDao(); // 返回一个代理类 ProductDao proxy = (ProductDao) new MyCglibProxy(productDao).createProxy(); proxy.save(); proxy.update(); proxy.delete(); proxy.find(); } }
代理知识总结
- Spring在运行期,生成动态代理对象,不需要特殊的编译器
- Spring AOP的底层就是通过JDK动态代理或CGLib动态代理技术 为目标Bean执行横向织入
- 若目标对象实现了若干接口,spring使用JDK的java.lang.reflect.Proxy类代理
- 若目标对象没有实现任何接口,spring使用CGLIB库生成目标对象的子类
- 程序中应优先对接口创建代理,便于程序解耦维护
- 标记为final的方法,不能被代理,因为无法进行覆盖
- JDK动态代理,是针对接口生成子类,接口中方法不能使用final修饰
- CGLib是针对目标类生产子类,因此类或方法不能是final
- Spring只支持方法连接点,不提供属性连接点
Spring一般切面编程案例
Spring AOP增强类型
- AOP联盟为通知Advice定义了org.aopalliance.aop.Interface.Advice
- Spring按照通知Advice在目标类方法的连接点位置,可以分为5类
- 前置通知 org.springframework.aop.MethodBeforeAdvice:在目标方法执行前实施增强
- 后置通知 org.springframework.aop.AfterReturningAdvice:在目标方法执行后实施增强
- 环绕通知 org.aopalliance.intercept.MethodInterceptor:在目标方法执行前后实施增强
- 异常抛出通知org.springframework.aop.ThrowsAdvice:在方法抛出异常后实施增强
- 引介通知org.springframework.aop.IntroductionInterceptor(了解即可,spring只支持方法层面的增强):在目标类中添加一些新的方法和属性
Spring AOP切面类型
- Advisor:代表一般切面,Advice本身就是一个切面,对目标类所有方法进行拦截
- PointcutAdvisor:代表具有切点的切面,可以指定拦截目标类哪些方法
- IntroductionAdvisor:代表引介切面,针对引介通知而使用切面(不要求掌握)
Advisor切面案例
- proxyTargetClass:是否对类代理而不是接口,设置为true时,使用CGLib代理
- interceptorNames:需要织入目标的Advice
- singleton:返回代理是否为单例模式,默认为单例
- optimize:当设置为true时,强制使用CGLib
代码实现
-
引入相关依赖
<dependency> <groupId>aopalliance</groupId> <artifactId>aopalliance</artifactId> <version>1.0</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aop</artifactId> <version>4.2.4.RELEASE</version> </dependency>
-
创建测试接口和测试类
public interface StudentDao { public void find(); public void update(); public void save(); public void delete(); }
public class StudentDaoImpl implements StudentDao { public void find() { System.out.println("学生查询..."); } public void update() { System.out.println("学生修改..."); } public void save() { System.out.println("学生保存..."); } public void delete() { System.out.println("学生删除..."); } }
-
创建spring配置文件
<?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="studentDao" class="com.imooc.aop.demo3.StudentDaoImpl"/> </beans>
-
以前置增强为例,创建前置增强类
public class MyBeforeAdvice implements MethodBeforeAdvice { public void before(Method method, Object[] args, Object target) throws Throwable { System.out.println("前置增强======================="); } }
-
在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="studentDao" class="com.imooc.aop.demo3.StudentDaoImpl"/> <!--前置通知类型--> <bean id="myBeforeAdvice" class="com.imooc.aop.demo3.MyBeforeAdvice"/> <!--Spring的AOP 产生代理对象 --> <bean id="studentDaoProxy" class="org.springframework.aop.framework.ProxyFactoryBean"> <!--配置的目标类 --> <property name="target" ref="studentDao"/> <!--实现的接口 --> <property name="proxyInterfaces" value="com.imooc.aop.demo3.StudentDao"/> <!--采用的拦截名称 --> <property name="interceptorNames" value="myBeforeAdvice"/> <!--设置为true则采用CGLib进行代理--> <property name="optimize" value="true"/> </bean> </beans>
-
测试,创建测试类
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration("classpath:applicationContext.xml") public class SpringDemo3 { // 注入原有的对象 // @Resource(name = "studentDao") // 注入代理对象 @Resource(name = "studentDaoProxy") private StudentDao studentDao; @Test public void demo1(){ studentDao.find(); studentDao.save(); studentDao.update(); studentDao.delete(); } }
输出为:
前置增强====================== 学生查询... 前置增强====================== 学生保存... 前置增强====================== 学生修改... 前置增强====================== 学生删除...
PointcutAdvisor切点切面
- 使用普通Advice作为切面,将对目标类所有方法进行拦截,不够灵活,在实际开发中采用带有切点的切面
- 常用PointcutAdvisor实现类
- DefaultPointcutAdvisor最常用的切面类型,它可以通过 任意Pointcut和Advice组合定义切面
- JdkRegexpMethodPointcut构造正则表达式切点(推荐)
代码实现
-
创建测试类
public class CustomerDao { public void find(){ System.out.println("查询客户...."); } public void save(){ System.out.println("保存客户...."); } public void update(){ System.out.println("修改客户...."); } public void delete(){ System.out.println("删除客户...."); } }
public class MyAroundAdvice implements MethodInterceptor { public Object invoke(MethodInvocation invocation) throws Throwable { System.out.println("环绕前增强"); Object obj = invocation.proceed(); System.out.println("环绕后增强"); return obj; } }
-
重新编写测试配置文件,采用正则表达式的方式选定要增强的类
<?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.imooc.aop.demo4.CustomerDao"/> <!--配置通知 --> <bean id="myAroundAdvice" class="com.imooc.aop.demo4.MyAroundAdvice"/> <!--一般的切面是采用通知作为切面的,因为要对目标类的某个方法进行增强就需要配置一个带有切入点的切面 --> <bean id="myAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor"> <!--pattern中配置正则表达式,.任意字符 *任意次数 --> <!-- <property name="pattern" value=".*save.*"/>--> <property name="patterns" value=".*save.*,.*delete.*"/> <property name="advice" ref="myAroundAdvice"/> </bean> <!--配置产生代理 --> <bean id="customerDaoProxy" class="org.springframework.aop.framework.ProxyFactoryBean"> <property name="target" ref="customerDao"/> <property name="proxyTargetClass" value="true"/> <property name="interceptorNames" value="myAdvisor"/> </bean> </beans>
-
测试
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration("classpath:applicationContext2.xml") public class SpringDemo4 { // @Resource(name = "customerDao") // 注入代理对象 @Resource(name = "customerDaoProxy") private CustomerDao customerDao; @Test public void demo1(){ customerDao.find(); customerDao.save(); customerDao.update(); customerDao.delete(); } }
查询客户.... 环绕前增强 保存客户.... 环绕后增强 修改客户.... 环绕前增强 删除客户.... 环绕后增强
Spring的传统AOP的动态代理
自动创建代理
- 前面的例子中,每个代理都通过ProxyFactoryBean织入切面代理,在实际开发中,非常多的Bean每个都配置ProxyFactoryBean开发维护量巨大
- 解决方案:使用自动创建代理
- BeanNameAutoProxyCreator 根据Bean名称创建代理
- DefaultAdvisorAutoProxyCreator 根据Advisor本身包含信息创建代理
- AnnotationAwareAspectJAutoProxyCreator基于Bean中的AspectJ 注解进行自动代理
BeanNameAutoProxyCreator举例
- 对所有以DAO结尾Bean所有方法使用代理
代码实现
-
在原有测试类以及接口的基础上,新建配置文件applicationContext3.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="studentDao" class="com.imooc.aop.demo5.StudentDaoImpl"/> <bean id="customerDao" class="com.imooc.aop.demo5.CustomerDao"/> <!-- 配置增强--> <bean id="myBeforeAdvice" class="com.imooc.aop.demo5.MyBeforeAdvice"/> <bean id="myAroundAdvice" class="com.imooc.aop.demo5.MyAroundAdvice"/> <bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator"> <property name="beanNames" value="*Dao"/> <property name="interceptorNames" value="myBeforeAdvice"/> </bean> </beans>
-
创建测试类
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration("classpath:applicationContext3.xml") public class SpringDemo5 { @Resource(name="studentDao") private StudentDao studentDao; @Resource(name="customerDao") private CustomerDao customerDao; @Test public void demo1(){ studentDao.find(); studentDao.save(); studentDao.update(); studentDao.delete(); customerDao.find(); customerDao.save(); customerDao.update(); customerDao.delete(); } }
前置增强====================== 学生查询... 前置增强====================== 学生保存... 前置增强====================== 学生修改... 前置增强====================== 学生删除... 前置增强====================== 查询客户... 前置增强====================== 保存客户... 前置增强====================== 修改客户... 前置增强====================== 删除客户...
DefaultAdvisorAutoProxyCreator 举例
-
在原有测试类以及接口的基础上,新建配置文件applicationContext4.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="studentDao" class="com.imooc.aop.demo6.StudentDaoImpl"/> <bean id="customerDao" class="com.imooc.aop.demo6.CustomerDao"/> <!-- 配置增强--> <bean id="myBeforeAdvice" class="com.imooc.aop.demo6.MyBeforeAdvice"/> <bean id="myAroundAdvice" class="com.imooc.aop.demo6.MyAroundAdvice"/> <!--配置切面--> <bean id="myAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor"> <property name="pattern" value="com\.imooc\.aop\.demo6\.CustomerDao\.save"/> <property name="advice" ref="myAroundAdvice"/> </bean> <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"></bean> </beans>
-
创建测试类
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration("classpath:applicationContext4.xml") public class SpringDemo6 { @Resource(name="studentDao") private StudentDao studentDao; @Resource(name="customerDao") private CustomerDao customerDao; @Test public void demo1(){ studentDao.find(); studentDao.save(); studentDao.update(); studentDao.delete(); customerDao.find(); customerDao.save(); customerDao.update(); customerDao.delete(); } }
学生查询... 学生保存... 学生修改... 学生删除... 查询客户... 环绕前增强=================== 保存客户... 环绕后增强=================== 修改客户... 删除客户...