Spring AOP学习
AOP的概述
在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
AOP:Aspect Oriented Programing 面向切面编程
AOP采取了横向抽取机制,取代了传统纵向继承体系重复性代码(性能监视、事务管理、安全检查、缓存)
AOP相关术语
术语 | 解释 |
---|---|
Joinpoint(连接点) | 所谓连接点是指那些被拦截到的点。在spring中,这些点指的是方法,因为spring只支持方法类型的连接点。 |
Pointcut(切入点) | 所谓切入点是指我们要对哪些Joinpoint进行拦截的定义。 |
Advice(通知/增强) | 所谓通知是指拦截到Joinpoint之后所要做的事情就是通知。通知分为前置通知,后置通知,异常通知,环绕通知(切面要完成的功能) |
Introduction(引介) | 引介是一种特殊的通知在不修改类代码的前提下,Introduction可以在运行期为类动态地添加一些方法或Field |
Target(目标对象) | 代理的目标对象 |
Weaving(织入) | 是指把增强应用到目标对象来创建新的代理对象的过程。spring采用动态代理织入,而AspectJ采用编译期织入和类装载期织入 |
Proxy(代理) | 一个类被AOP织入增强后,就产生一个结果代理类 |
Aspect(切面) | 是切入点和通知(引介)的结合 |
AOP代理的底层实现
使用JDK自带的代理
先来定义接口和实现类
//UserDao接口
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("查询用户...");
}
}
通过Java自带的代理机制,实现AOP
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
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);
}
}
演示
import org.junit.Test;
public class SpringDemo {
@Test
public void demo1(){
UserDao userDAO = new UserDAOImpl();//创建一个对象
UserDao proxy = (UserDao) new MyJdkProxy(userDAO).createProxy();//创建一个代理对象
proxy.save();
proxy.update();
proxy.delete();
proxy.find();
}
}
运行结果:
权限校验…
保存用户…
修改用户…
删除用户…
查询用户…
在save方法的调用前,进行了权限校验
使用CGLIB生成代理
对于不使用接口的业务类,无法使用JDK动态代理
CGlib采用非常底层的字节码技术,可以为一个类创建子类,解决无接口代理问题
引入spring的jar包,里面包含有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("查询商品...");
}
}
CGlib代理
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
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 ("save".equals(method.getName())){
System.out.println("权限校验...");
return methodProxy.invokeSuper(proxy,args);
}
return methodProxy.invokeSuper(proxy,args);
}
}
测试
import org.junit.Test;
public class SpringDemo {
@Test
public void demo(){
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 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 AOP切面类型
类型 | 介绍 |
---|---|
Advisor | Advice本身就是一个切面,对目标类所有方法进行拦截 |
PointcutAdvisor | 代表具有切点的切面,可以指定拦截目标类哪些方法 |
IntroductionAdvisor | 代表引介切面,针对引介通知而使用切面 |
Spring的一般切面
Maven加入配置
<dependency>
<groupId>aopalliance</groupId>
<artifactId>aopalliance</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>4.3.18.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>4.3.18.RELEASE</version>
</dependency>
bean:
public class StudentDaoImpl implements StudentDao {
public void find() {
System.out.println("学生查询...");
}
public void save() {
System.out.println("学生保存...");
}
public void update() {
System.out.println("学生修改...");
}
public void delete() {
System.out.println("学生删除...");
}
}
代理对象:
import org.springframework.aop.MethodBeforeAdvice;
import java.lang.reflect.Method;
public class MyBeforeAdvice implements MethodBeforeAdvice {
public void before(Method method, Object[] objects, Object o) throws Throwable {
System.out.println("前置增强");
}
}
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="chauncy.demo3.StudentDaoImpl"/>
<!--配置通知-->
<bean id="myBeforeAdvice" class="chauncy.demo3.MyBeforeAdvice"/>
<!--Spring的AOP产生代理对象-->
<bean id="studentDaoProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
<!--配置目标类-->
<property name="target" ref="studentDao"/>
<!--实现的接口-->
<property name="proxyInterfaces" value="chauncy.demo3.StudentDao"/>
<!--采用拦截的名称-->
<property name="interceptorNames" value="myBeforeAdvice"/>
</bean>
</beans>
演示:
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import javax.annotation.Resource;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class SpringDemo {
//注入
@Resource(name = "studentDaoProxy")
private StudentDao studentDao;
@Test
public void demo1(){
studentDao.find();
studentDao.save();
studentDao.update();
studentDao.delete();
}
}
其他代理属性配置
name | value |
---|---|
proxyTargetClass | 是否对类代理而不是接口,设置为CGLib代理 |
interceptorNames | 需要织入目标的Advice |
singleton | 返回代理是否为单实例,默认为单例 |
optimize | 当设置为true时,强制使用CGLib |
带有切入点的切面
- 使用普通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("删除用户");
}
}
通知类
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
public class MyAroundAdvice implements MethodInterceptor {
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
System.out.println("环绕前增强测试");
Object obj = methodInvocation.proceed();
System.out.println("环绕后增强测试");
return obj;
}
}
applicationContext配置
<?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="chauncy.demo4.customerDao"/>
<!--配置通知-->
<bean id="myAroundAdvice" class="chauncy.demo4.MyAroundAdvice"/>
<!--一般的切面是使用通知作为切面的,因为要对目标类的某个方法进行增强就需要配置一个带有切入点的切面-->
<bean id="myAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
<!--<property name="pattern" value=".*"/>-->
<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>
Demo
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import javax.annotation.Resource;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext2.xml")
public class SpringDemo {
@Resource(name="customerDaoProxy")
private customerDao customerDao;
@Test
public void demo(){
customerDao.find();
customerDao.save();
customerDao.update();
customerDao.delete();
}
}
运行结果
查询用户
环绕前增强测试
保存用户
环绕后增强测试
修改用户
环绕前增强测试
删除用户
环绕后增强测试
自动创建代理
基于Bean名称的自动代理
- 前面的案例中,每个代理都是通过ProxyFactoryBean织入切面代理,在实际开发中,非常多的Bean每个都配置ProxyFactoryBean开发维护量巨大
- 解决方案:自动创建代理
- BeanNameAutoProxyCreator根据Bean名称创建代理
- DefaultAdvisorAutoProxyCreator根据Advisor本身包含信息创建代理
- AnnotationAwareAspectJAutoProxyCreator基于Bean中的AspectJ注解进行自动代理
<?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="chauncy.demo5.customerDao"/>
<bean id="studentDao" class="chauncy.demo5.StudentDaoImpl"/>
<!--通知-->
<bean id="MyBeforeAdvice" class="chauncy.demo5.MyBeforeAdvice"/>
<bean id="MyAroundAdvice" class="chauncy.demo5.MyAroundAdvice"/>
<!--自动代理-->
<bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
<!--对所有以Dao结尾的Bean代理-->
<property name="beanNames" value="*Dao"/>
<property name="interceptorNames" value="MyBeforeAdvice"/>
</bean>
</beans>
基于切面信息的自动代理
<?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="chauncy.demo5.customerDao"/>
<bean id="studentDao" class="chauncy.demo5.StudentDaoImpl"/>
<!--通知-->
<bean id="MyBeforeAdvice" class="chauncy.demo5.MyBeforeAdvice"/>
<bean id="MyAroundAdvice" class="chauncy.demo5.MyAroundAdvice"/>
<!--配置切面-->
<bean id="myAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
<property name="pattern" value="chauncy\.demo5\.customerDao\.update"/>
<property name="advice" ref="MyAroundAdvice"/>
</bean>
<!--自动代理-->
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"/>
</beans>