AOP简介
AOP意为面向切面编程,通过预编译方式和运行期间通过动态代理实现程序功能的统一维护的技术。利用AOP可以对业务逻辑的各个部分进行隔离,从而使业务逻辑各部分之间的耦合度降低,提高程序的可重用性。
AOP与代理模式
Spring中的AOP本质上也就是使用了动态代理模式,只不过在动态代理模式的实现上进行了封装,简化了动态代理模式的步骤。
动态代理模式的实现步骤:
1)声明接口:注册需要被监听行为的名称,定义主要业务和次要业务
2)接口实现类:具体被监听的类,被监听行为的具体实现,也就是主要业务和次要业务的具体实现
3)InvocationHanler接口实现类:将主要业务和次要业务绑定起来,控制执行顺序或增强业务逻辑
4)代理监控对象:被监控的对象以及需要被监控的行为 具体通知类实例对象,向JVM申请拦截
而Spring中AOP简化后的代理模式实现步骤:
1)声明接口:注册需要被监听行为的名称,定义主要业务和次要业务
2)接口实现类:具体被监听的类,被监听行为的具体实现,也就是主要业务和次要业务的具体实现
3)区分主要业务和次要业务
其中AOP 底层采用两种动态代理模式实现:JDK 的动态代理,与 CGLIB 动态代理。根据是否实现接口来选择哪种代理方式。
AOP术语
(1)切面(Aspect):交叉业务逻辑,例如事务、安全、日志等,称为切面。
(2)目标对象(Target):业务类的对象,称为目标对象。
(3)织入(Weaving):将切面插入到目标对象的目标方法的过程,称为织入。
(4)连接点(JoinPoint):目标对象中可以被切面织入的方法。
(5)切入点(Pointcut):目标对象中真正被切面织入的方法。切入点一定是连接点,但连接点不一定是切入点。被标记为 final 的方法是不能作用连接点与切入点的。
(6)通知(Advice):通知是切面的一种,可以完成简单的织入功能。通知可以定义切面织入的时间点,切入点定义了切面织入的位置。
(7)顾问(Advisor):顾问是切面的一种,能够将通知以更为复杂的方式织入到目标对象中,是将通知包装为更复杂的切面的装配器。
通知(Advice)的使用
与顾问(Advisor)相比,通知(Advice)使用起来较为简单,但局限性也很明显,Advice 接口只能对当前接口下所有的实现类进行次要业务绑定执行,无法动态指定,所以灵活性较差。
Advice 的种类
前置通知:
切面:次要业务方法
执行切入点:被拦截的主要业务方法
后置通知:
执行切入点:被拦截的主要业务方法
切面:次要业务方法
环绕通知:
切面1:次要业务方法
执行切入点:被拦截的主要业务方法
切面2:次要业务方法
异常通知:
try{
执行切入点:被拦截的主要业务方法
}catch(Exception ){
切面
}
由上可看出,次要业务方法与被拦截的主要业务方法的顺序是固定不变的,无法动态指定。
下面通过一个简单的例子来进行分析,我们都知道“饭前便后要洗手” ,但是使用 Advice 是做不到这个效果的,我们能做到“饭前便前要洗手”或者“饭后便后要洗手”,这就是他最大的不足。
1)创建连接点,也即是接口定义的行为,可以被切面织入的方法
public interface BaseService {
//连接点
void eat();
//连接点
void wc();
}
2)创建切入点,也即是连接点的实现,即目标对象中真正被切面织入的方法
public class Person implements BaseService{
//切入点
@Override
public void eat() {
System.out.println("....吃饭....");
}
//切入点
@Override
public void wc() {
System.out.println("....wc....");
}
}
3)构建通知类(Advice),在上面的介绍中可知,通知分为四种,这里我们使用前置通知为例,需要实现 MethodBeforeAdvice 接口,并重写 before 方法,这就是切面,对应代理模式中的次要业务,也就是我们案例中的“洗手”环节。
public class MyBeforeAdvice implements MethodBeforeAdvice{
// 切面,对应代理模式中的次要业务
@Override
public void before(Method method, Object[] objects, @Nullable Object o) throws Throwable {
System.out.println("....洗手....");
}
}
4)Spring配置文件中注册被监控的实现类以及通知实现类,对应代理模式中的主要业务类和次要业务类,然后注册 Spring 为我们提供的代理工厂 ProxyFactoryBean,用于代理对象的创建, 这里我们给出了两个属性,一个是 target,指的是目标对象;另一个是 interceptorNames,指的是拦截器的名称。
<!-- 注册被监控的实现类 -->
<bean id="person" class="com.lks.aop.Pointcut.Person"></bean>
<!-- 注册通知实现类 -->
<bean id="before" class="com.lks.aop.advice.MyBeforeAdvice"></bean>
<!-- 注册代理监控对象生产工厂 -->
<bean id="personProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
<!-- 目标对象,为 person 创建一个代理对象 -->
<property name="target" ref="person"/>
<property name="interceptorNames">
<array>
<value>before</value>
</array>
</property>
</bean>
5)测试代码:
@Test
public void testAdviceAop() throws Exception {
ApplicationContext context = new ClassPathXmlApplicationContext("spring_config.xml");
com.lks.aop.joincut.BaseService personProxy = (com.lks.aop.joincut.BaseService) context.getBean("personProxy");
personProxy.eat();
personProxy.wc();
}
从测试结果可以看出,我们在切入点执行前都加入了切面的操作,从而达到了“饭前便前要洗手”的效果,这也是前置通知的作用,即切面都会在切入点之前,较为局限。
顾问(Advisor)的使用
顾问概括要点
(1)一种织入方式
(2)实际上Adivce封装版
(3)可以动态的将切面指定对应切入点
顾问(Advisor)本质上就是通知(advice)的前置通知和后置通知的组合。
PointCutAdvisor:可以灵活的指定当前接口下的哪一个实现类中哪一个方法与次要业务进行绑定
Advice:次要业务,这个就是我们之前讲过的通知
PointCut:目标对象和目标方法
ClassFilter:判断当前被拦截对象是不是当前顾问需要管理对象
MethodMatcher:当前被拦截的方法是不是我们所需要主要业务方法
PointCut 需要由 ClassFilter 和 MethodMatcher 组合构成,用来确定目标对象和目标方法,然后结合 Advice 就组成了 PointCutAdvisor,由于 Advice 不够灵活,只能对当前接口下所有的实现类进行次要业务绑定执行,无法动态指定,有了 PointCut 的协助,就让动态制指定变成了可能,下面我们来看一下实现方法。
1)和通知(Advice)的实现一样,实现连接点和切入点,也就是接口定义的行为以及具体的实现
2)创建我们自己的类过滤器 MyClassFilter.java
public class MyClassFilter implements ClassFilter{
@Override
public boolean matches(Class<?> aClass) {
if (aClass == Person.class) {
// 告诉顾问,当前类是需要我们提供织入服务的
return true;
}
return false;
}
}
此处我们只关心 Person 类,通过 matches() 方法告知顾问(Advisor),当前类是需要我们提供织入服务的。
3)创建我们自己的方法匹配类,“饭前”和“便后”是主要业务,对于“饭前”我们需要前置通知进行处理,“便后”需要后置通知处理,所以我们写了两个 MethodMatcher 类,用来拦截 eat() 和 wc() 两个方法。
public class EatMethodMatcher implements MethodMatcher{
@Override
public boolean matches(Method method, @Nullable Class<?> aClass) {
String methodName = method.getName();
if ("eat".equals(methodName)) {
return true;
}
return false;
}
@Override
public boolean isRuntime() {
return false;
}
@Override
public boolean matches(Method method, @Nullable Class<?> aClass, Object... objects) {
return false;
}
}
public class WcMethodMatcher implements MethodMatcher{
@Override
public boolean matches(Method method, @Nullable Class<?> aClass) {
String methodName = method.getName();
if ("wc".equals(methodName)) {
return true;
}
return false;
}
@Override
public boolean isRuntime() {
return false;
}
@Override
public boolean matches(Method method, @Nullable Class<?> aClass, Object... objects) {
return false;
}
4)有了 ClassFilter 和 MethodMatcher,我们就可以构建 Pointcut 了
public class MyPointCut implements Pointcut{
// 使用依赖注入,需要提供 setter() 方法
private ClassFilter classFilter;
private MethodMatcher methodMatcher;
public void setClassFilter(ClassFilter classFilter) {
this.classFilter = classFilter;
}
public void setMethodMatcher(MethodMatcher methodMatcher) {
this.methodMatcher = methodMatcher;
}
@Override
public ClassFilter getClassFilter() {
return classFilter;
}
@Override
public MethodMatcher getMethodMatcher() {
return methodMatcher;
}
}
5)前置通知和后置通知类
MyBeforeAdvice.java(前置通知)
public class MyBeforeAdvice implements MethodBeforeAdvice {
// 切面,对应代理模式中的次要业务
@Override
public void before(Method method, Object[] objects, Object o) throws Throwable {
System.out.println("···洗手···");
}
}
MyAfterAdvice.java(后置通知)
public class MyAfterAdvice implements AfterReturningAdvice {
@Override
public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
System.out.println("···洗手···");
}
}
6)构建 PointcutAdvisor 类
public class MyPointCutAdvisor implements PointcutAdvisor{
// 次要业务以及次要业务与主要业务执行顺序
private Advice advice;
// 当前拦截对象和对象调用主要业务方法 Person 和 .eat() 方法
private Pointcut pointcut;
public void setAdvice(Advice advice) {
this.advice = advice;
}
public void setPointcut(Pointcut pointcut) {
this.pointcut = pointcut;
}
@Override
public Pointcut getPointcut() {
return pointcut;
}
@Override
public Advice getAdvice() {
return advice;
}
@Override
public boolean isPerInstance() {
return false;
}
}
这里的 Advice 和 Pointcut 使用依赖注入的方式交给 Spring 进行管理,所以这里一定要提供 setter() 方法。
7)Spring 配置文件
<!--advisor-->
<!-- 注册被监控的实现类 -->
<bean id="personAdvisor" class="com.lks.aop.pointcut.Person"></bean>
<!-- 注册通知实现类 -->
<bean id="beforeAdvisor" class="com.lks.aop.advice.MyBeforeAdvice"></bean>
<bean id="after" class="com.lks.aop.advice.MyAfterAdvice"></bean>
<!-- 注册类型过滤器 -->
<bean id="classFilter" class="com.lks.aop.filter.MyClassFilter"></bean>
<!-- 注册方法匹配器 -->
<bean id="eatMethodMatcher" class="com.lks.aop.matcher.EatMethodMatcher"></bean>
<bean id="wcMethodMatcher" class="com.lks.aop.matcher.WcMethodMatcher"></bean>
<!-- 注册切入点 -->
<bean id="eatPointcut" class="com.lks.aop.pointcut.MyPointCut">
<property name="classFilter" ref="classFilter"></property>
<property name="methodMatcher" ref="eatMethodMatcher"></property>
</bean>
<bean id="wcPointcut" class="com.lks.aop.pointcut.MyPointCut">
<property name="classFilter" ref="classFilter"></property>
<property name="methodMatcher" ref="wcMethodMatcher"></property>
</bean>
<!-- 注册顾问 -->
<bean id="eatAdvisor" class="com.lks.aop.advisor.MyPointCutAdvisor">
<property name="advice" ref="beforeAdvisor"></property>
<property name="pointcut" ref="eatPointcut"></property>
</bean>
<bean id="wcAdvisor" class="com.lks.aop.advisor.MyPointCutAdvisor">
<property name="advice" ref="after"></property>
<property name="pointcut" ref="wcPointcut"></property>
</bean>
<!-- 注册代理对象工厂 -->
<!--
此时生成的代理对象,只会负责 Person.eat() 方法进行监控,
与 Advice 不同,不会对 BaseService 所有的方法进行监控
-->
<bean id="personProxyAdisor" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target" ref="person"></property>
<property name="interceptorNames" value="eatAdvisor,wcAdvisor"></property>
</bean>
8)测试代码
@Test
public void testAdvisorAop() throws Exception {
ApplicationContext context = new ClassPathXmlApplicationContext("spring_config.xml");
com.lks.aop.joincut.BaseService personProxy = (com.lks.aop.joincut.BaseService) context.getBean("personProxyAdisor");
personProxy.eat();
personProxy.wc();
}
这次讲解的知识点偏多,有的也不是很容易理解,手动走一下步骤,一是为了让读者了解 Spring AOP 的相关知识,二是为了让读者体会一下代理模式。