文章目录
1、AOP是什么
在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
AOP是面向切面编程的,它采取的是横向抽取的机制,取代了纵向继承体系重复性代码(性能监视、事务管理、安全检查、缓存:
1.1、AOP相关术语
Joinpoint(连接点):所谓连接点是指那些被拦截到的点,在Spring中,这些点指的是方法,因为Spring只支持方法类型的连接点.
Pointcut(切入点):所谓切入点是指我们要对哪些Joinpoint进行拦截的定义.
Advice(通知):所谓通知是指拦截到Joinpoint之后要做的事就是通知.
Introduction(引介):引介是一种特殊的通知在不修改类代码的前提下,Introduction可以在运行期间为类动态的添加一些方法或属性
Target(目标对象):代理的目标对象
Weawing(织入):是指把增强应用到目标对象来创建新的代理对象的过程.Spring采用动态代理织入,而AspectJ采用编译期织入和类装载期织入
Proxy(代理):一个类被AOP织入增强后,就产生一个结果代理类、
Aspect(切面):是切入点和通知(引介)的结合
2、AOP的底层实现
其实AOP底层就是两种动态代理,一种是JDK动态代理,一种是CGlib动态代理,可以参考我这两篇博客:
https://blog.csdn.net/u011679785/article/details/99218116
https://blog.csdn.net/u011679785/article/details/99095491
2.1、SpringAOP切面类型
- advisor:代表一般切面,Advice本身就是一个切面,对目标类所有方法进行拦截
- PointcutAdvisor:代表具有切点的切面,可以指定拦截目标类哪些方法
- IntroductionAdvisor:代表引介切面,针对引介通知而使用切面(可以不掌握)
2.2、SpringAOP增强类型
- 前置通知,org.springframework.aop.MethodBeforeAdvice
在目标方法执行前实施增强 - 后置通知,org.springframework.aop.AfterReturningAdvice
在目标方法执行后实施增强 - 环绕通知,org.aopalliance.intercept.MethodInterceptor
在目标方法执行前后实施增强、 - 异常抛出通知,org.springframework.aop.ThrowsAdvice
在方法抛出异常后执行 - 引介通知 org.springframework.aop.IntroductionInterceptor
在目标类中添加一些新方法和属性
(第五种通知不需要掌握)
3、Spring的传统AOP
3.1、不带切入点的切面
所谓不带切入点的切面,有点像过滤器,会把所有的Advice作为切面,会拦截所有的方法并且使用增强方法,如果要使用AOP,除了Spring本身的四个必要包和需要AOP包还需要导入两个包:
分别是AOP联盟的包和spring-test包:
假设还是UserDao这个这个接口:
package com.bean.demo3;
public interface UserDao {
void insert();
void update();
void delete();
void find();
}
巨简单的实现类:
package com.bean.demo3;
public class UserDaoImpl implements UserDao {
@Override
public void insert() {
System.out.println("插入用户...");
}
@Override
public void update() {
System.out.println("更新用户...");
}
@Override
public void delete() {
System.out.println("删除用户...");
}
@Override
public void find() {
System.out.println("查找用户...");
}
}
然后我们需要自己实现一下增强方法,假设我们搞一个前置通知,前面也写到了,需要实现MethodBeforeAdvice类,然后写一下实现方法:
package com.bean.demo3;
import org.springframework.aop.MethodBeforeAdvice;
import java.lang.reflect.Method;
public class MyBeforeAdvice implements MethodBeforeAdvice {
@Override
public void before(Method method, Object[] objects, Object o) throws Throwable {
System.out.println("前置通知");
}
}
假设我们前置通知就一句话,我们推测一下,结果应该是在每个方法执行前都会打印这句话“前置通知”。
OK,然后我们在applicationContext.xml中配置一下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<!--设置目标类-->
<bean name="userDao" class="com.bean.demo3.UserDaoImpl"/>
<!--设置前置通知类-->
<bean id="beforeAdvice" class="com.bean.demo3.MyBeforeAdvice"/>
<!--设置代理类-->
<bean id="proxyUserDao" class="org.springframework.aop.framework.ProxyFactoryBean">
<!--代理目标类-->
<property name="target" ref="userDao"/>
<!--设置需要代理的接口-->
<property name="proxyInterfaces" value="com.bean.demo3.UserDao"/>
<!--设置通知方法-->
<property name="interceptorNames" value="beforeAdvice"/>
</bean>
</beans>
如此,我们就配置好了一个相当简单的无切入点的切面,测试一下:
package com.bean.demo3;
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 = "userDao")
private UserDao userDao;
@Test
public void demo1() {
userDao.delete();
userDao.find();
userDao.insert();
userDao.update();
}
}
简单说一下两个新注解啥意思:
- @Runwith()表示的是使用什么测试环境,我在里面配置SpringJUnit4ClassRunner.class,就表示Spring环境下的测试,以便在测试开始的时候自动创建Spring的应用上下文。这样就不需要像之前那样必须ApplicationContext a = new ClassPathXml…,然后getBean这样的。
- @ContextConfiguration(“classpath:applicationContext.xml”)表示的是你的配置文件叫啥名。
注意我的Resource是配置的userDao,所以并不会有环绕通知,这就跟以前普通的配置Bean一样,所以打印结果也显而易见:
倘若我们把Resource中的bean换成proxyUserDao的话,就可以使用代理类的bean进行执行方法和通知方法了:
3.2、带有切入点的切面
Obviously,上面那种没有切入点的切面很不针对,很多现实的需求是仅仅对某一个方法进行通知,所以来看看怎么实现:
先介绍两种常用的PointcutAdvisor实现类:
- DefaultPointcutAdvisor 最常用的切面类型。,他可以通过任意Pointcut和Advice组合定义切面
- JdkRegexpMethodPointcut 构造正则表达式切点
本篇博客只详细介绍后者,前者用的比较少了现在
假设我们只使用CGlib动态代理,只写一个目标类不写接口
package com.bean.demo4;
public class CustomerDao {
public void insert() {
System.out.println("插入用户...");
}
public void update() {
System.out.println("更新用户...");
}
public void delete() {
System.out.println("删除用户...");
}
public void find() {
System.out.println("查找用户...");
}
}
这次来写一个环绕通知:
package com.bean.demo4;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
public class MyRoundAdvice implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
System.out.println("环绕增强前");
Object proceed = methodInvocation.proceed();
System.out.println("环绕增强后");
return proceed;
}
}
可要小心这个包是org.aopalliance.intercept,是Spring联盟中的包,不是Springframework这个包下的方法,小心写错了搞不成了,然后我们配置一下applicationContext2.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="customDao" class="com.bean.demo4.CustomerDao"/>
<!--配置环绕通知-->
<bean id="roundAdvice" class="com.bean.demo4.MyRoundAdvice"/>
<!--一般的切面是使用通知作为切面的,因为要对目标类的某个方法进行增强就需要配置一个有切入点的bean-->
<!--配置目标-->
<bean id="myAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
<!--pattern中配置正则表达式-->
<property name="pattern" value=".*"/>
<property name="advice" ref="roundAdvice"/>
</bean>
<!--配置产生代理-->
<bean id="customDaoProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target" ref="customDao"/>
<property name="proxyTargetClass" value="true"/>
<property name="interceptorNames" value="myAdvisor"/>
</bean>
</beans>
由于要设置切入点,所以需要在代理对象
<property name="interceptorNames" value=""/>
这个value中不能直接写通知id了,否则就又跟没切入点通知一样了,所以我们再多加一个切点:
<bean id="myAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
<!--pattern中配置正则表达式-->
<property name="pattern" value=".*"/>
<property name="advice" ref="roundAdvice"/>
</bean>
通过如此配置正则表达式可以匹配所有的字符,换言之这个切点是匹配所有的方法,假如要是想匹配个别方法,比如insert方法,就需要这样配置:
<bean id="myAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
<!--pattern中配置正则表达式-->
<property name="pattern" value=".*insert.*"/>
<property name="advice" ref="roundAdvice"/>
</bean>
配置成 .* insert* 就可以了,想多个切点,则只需要后面加个逗号就行了类似.* insert.*, . * find. *
4、Spring的传统AOP的自动代理
前面的例子中,每个代理都是通过ProxyFactoryBean来配置,如果有很多的代理对象的话,那就太麻烦了,每次都得在applicationContext.xml中配置,开发量和维护量仍然是很大的。
解决方式:自动创建代理
代理包括
- BeanNameAutoProxyCreator 根据Bean名称创建代理
- DefaultAdvisorAutoProxyCreatoe 根据Advisor本身包含信息创建代理
- AnnotationAwareAspectJAutoProxyCreator 基于Bean中的AspectJ注解进行自动代理
这里只介绍前两种方式:
4.1、基于Bean名称的自动代理
举个栗子,比如我们可以对所有以dao结尾的bean进行自动创建代理
我们搞两个目标类,一个是不实现接口的customerDao,一个是实现接口的studentDao,跟上面例子一样,就不放图了,然后直接看配置文件:
<?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.bean.demo5.CustomerDao"/>
<bean id="studentDao" class="com.bean.demo5.StudentDaoImpl"/>
<bean id="userDao" class="com.bean.demo5.UserDaoImpl"/>
<!--配置增强-->
<bean id="afterAdvice" class="com.bean.demo5.MyAfterAdvice"/>
<bean id="roundAdvice" class="com.bean.demo5.MyRoundProxy"/>
<bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
<property name="beanNames" value="*Dao"/>
<property name="interceptorNames" value="afterAdvice"/>
</bean>
</beans>
通过这种自动创建代理对象,可以根据Bean的名称自动创建,很方便,看看运行结果:
想改成环绕通知只需要把
<property name="interceptorNames" value="afterAdvice"/>
改成环绕通知的bean就可以了:
4.2、基于切面信息的自动代理
相似地,上面这种还是没有切点的通知,很不方便,一通知就必须是通知Dao中所有方法,想要只修改一个方法,则需要org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator这个类去自动代理:
还是跟上个例子一样,只需要更改以下代码:
<bean id="myAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
<property name="pattern" value="com.bean.demo6.StudentDaoImpl.find"/>
<property name="advice" ref="afterAdvice"/>
</bean>
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"/>
还是通过正则表达式切点这个类来告诉自动代理类你要增强具体的哪个方法,需要注意的是pattern中的value是正则表达式,需要转义,".“前面应该加”",来看下测试结果:
只增强find方法成功!