Spring中的AOP(一)

1. Spring AOP实现机制

Spring采用动态代理机制和字节码生成技术实现AOP。与最初的AspectJ采用编译器将横切逻辑织入目标对象不同,动态代理机制和字节码生成都是在运行期间为目标对象生成一个代理对象,而将横切逻辑织入到这个代理对象中,系统最终使用的是织入了横切逻辑的代理对象,而不是真正的目标对象。

1.1 动态代理

动态代理机制的实现主要由一个类和一个接口组成,即java.lang.reflect.Proxy类和java.lang.reflect.InvocationHandler接口。下面让我们看一下怎么用动态代理来实现“request服务时间控制”功能。


public class RequestCtrlInvocationHandler implements InvocationHandler {
    private static final Log logger = LogFactory.getLog(RequestCtrlInvocationHandler.class);
    // 将被目标对象通过构造方法加入到代理对象中
    private Object target;
    public RequestCtrlInvocationHandler(Object target) {
        this.target = target;
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable{
        if (method.getName().equals("request")) {
            TimeOfDay startTime = new TimeOfDay(0, 0, 0);
            TimeOfDay endTime = new TimeOfDay(5, 59, 59);
            TimeOfDay currentTime = new TimeOfDay();
            if (currentTime.isAfter(startTime) && currentTime.isBefore(endTime)) {
                logger.warn("service is not available now.");
                return null;
            }
            return method.invoke(target, args);
        }
        return null;
    }
}

然后我们就可以使用Proxy类根据RequestCtrlInvocationHandler的逻辑,为ISubject和IRequestable两种类型生成相应的代理对象实例。

//第一个参数是目标对象的类加载器,第二个参数是需要增强的接口,这里可直接使用ISubject,最后一个参数是横切逻辑的类,并且需要持有一个目标对象实例
ISubject subject = (ISubject)Proxy.newProxyInstance(ProxyRunner.class.getClassLoader(), new Class[]{ISubject.class}, new RequestCtrlInvocationHandler(new SubjectImpl()));
subject.request();

即使还有更多的目标对象类型,只要它们的织入的横切逻辑都相同,用RequestCtrlInvocationHandler一个类并通过Proxy为它们生成相应的动态代理实例就可以满足要求。当Proxy动态生成的代理对象上相应的接口方法被调用时,对应的InvocationHandler就会拦截相应的方法调用,并进行相应处理。

1.2 动态字节码生成

动态代理虽好,但是只能对实现了相应接口的类使用,如果某个类没有实现任何的接口,就无法使用动态代理为其生成相应的动态代理对象。这时候就轮到我们的动态字节码生成出场了。
使用动态字节码生成技术扩展对象行为的原理,我们可以为其生成相应的子类,而子类可以通过重写来扩展父类的行为,只要将横切逻辑的实现放到子类中,然后让系统使用扩展后的目标对象的子类,就可以达到与代理模式同样的效果了。
但是,使用继承的方式来扩展对象定义,要借助于CGLIB这样的动态字节码生成库,在系统运行期间动态地为目标对象生成相应的扩展子类。

2. Spring AOP中的概念实体

2.1 Jointpoint

我们在前面提到,AOP的Jointpoint可以有许多类型,但是在Spring AOP中,仅支持方法级别的Jointpoint。更确切地说,只支持方法执行类型的Jointpoint。

2.2 Pointcut

Spring以接口定义org.springframework.aop.Pointcut作为其AOP框架中所有Pointcut的最顶层抽象,该接口定义了两个方法用来帮助捕捉系统中的相应Jointpoint,并提供了一个TruePointcut类型实例(何为捕捉?如果我寻找到某个目标对象符合Pointcut的定义,则对其进行织入)。如果Pointcut类型为TruePointcut,默认会对系统中的所有对象,以及对象上所有被支持的Jointpoint进行匹配。接口定义如下所示:

public interface Pointcut {
    Pointcut TRUE = TruePointcut.INSTANCE;
    ClassFilter getClassFilter();
    MethodMatcher getMethodMatcher();
}

ClassFilterMethodMatcher分别用于匹配将被执行织入操作的对象以及相应的方法。ClassFilter的作用是对Jointpoint所处的对象进行Class级别的类型匹配。其定义如下:

public interface ClassFilter {
    ClassFilter TRUE = TrueClassFilter.INSTANCE;
    boolean matches(Class<?> var1);
}

当织入的目标对象的Class类型与Pointcut规定的类型相符时返回true,反之为false,即意味着不会对这个类型的对象做织入操作。举个栗子,我们仅希望对系统中Foo类型的类执行织入,则可以如下定义ClassFilter

public class FooClassFilter {
    public boolean matches(Class clazz) {
        return Foo.class.equals(clazz);
    }
}

相比于ClassFilter的简单定义,MethodMatcher则要复杂地多。MathodMatcher实现的就是方法级别的拦截,定义如下:

public interface MethodMatcher {
    MethodMatcher TRUE = TrueMethodMatcher.INSTANCE;
    boolean matches(Method var1, Class<?> var2);
    boolean isRuntime();
    boolean matches(Method var1, Class<?> var2, Object... var3);
}

MethodMatcher通过重载,定义了两个matches方法,而这两个方法的分界线是isRuntime()方法。在对对象具体方法进行拦截的时候,可以忽略调用者传入的参数,也可以检查这些方法的调用参数,以强化拦截条件。举个例子:如果只想在login方法之前插入计数功能,那么login方法的参数对于Jointpoint捕捉就是可以忽略的;如果想在用户登陆的时候对某个用户进行单独的处理,如不让其登陆或者给予特殊权限,那么这个方法的参数就是在匹配Jointpoint的时候必须要考虑的。
在前一种情况下,isRuntime()返回false,表示不会考虑具体Jointpoint的方法参数,这种类型的MethodMatcher成为StaticMethodMatcher。因为不用每次都检查参数,对于同样类型的方法匹配结果,就可以在框架内部缓存以提高性能,Spring对该种类型的Pointcut提供了更多的支持。后一种情况下,isRuntime()返回true,表明该MethodMatcher将会每次都对方法调用的参数进行匹配检查,称之为DynamicMathodMatcher
常见的Pointcut有NameMatchMethodPointcutJdkRegexMethodPointAnnotationMatchingPointcut,在此简要介绍一下NameMatchMethodPointcut
NameMatchMethodPointcut是最简单的Pointcut实现,可以根据自身指定的一组方法名称与Jointpoint处方法的方法名称进行匹配,比如:

NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
pointcut.setMappedName("matches");
// 或者传入多个方法名
pointcut.setMappedName(new String[]{"matches", "isRunTime"});

我们来看一下NameMatchMethodPointcut内部匹配方法matches()的实现:

    // method是各个类中,用来被比较匹配的方法对象,如果与我们传入的将要拦截的方法名相同或满足通配符,则匹配成功
    public boolean matches(Method method, Class<?> targetClass) {
        // mappedNames是我们传入的需要匹配的方法名
        Iterator var3 = this.mappedNames.iterator();
        String mappedName;
        do {
            if (!var3.hasNext()) {
                return false;
            }
            mappedName = (String)var3.next();
            // equals()方法表示字符串匹配,后面的isMatch()方法表示用通配符进行模糊匹配
        } while(!mappedName.equals(method.getName()) && !this.isMatch(method.getName(), mappedName));
        return true;
    }

2.3 Advice

Advice实现了将被织入到Pointcut规定到Jointpoint处的横切逻辑。在Spring中,Advice按照其自身实例(instance)能否在目标对象类的所有实例中共享这一标准,可以划分为两大类,即per-class类型和per-instance

2.3.1 per-class类型的Advice

per-class类型的Advice是指,该类型的Advice的实例可以在目标对象类的所有实例之间共享。这种类型的Advice通只是提供方法拦截的功能,不会为目标对象类保存任何状态或者添加新的特性。

  • Before Advice:Before Advice所实现的横切逻辑将在相应的Jointpoint之前执行。在Spring中,我们通常只需要实现org.springframework.aop.MethodBeforeAdvice接口即可。该接口定义如下:
public interface MethodBeforeAdvice extends BeforeAdvice {
    void before(Method var1, Object[] var2, Object var3) throws Throwable;
}

我们可以使用Before Advice进行整个系统的某些初始化资源或者其他一些准备性的工作。比如,假设我们的系统需要在某些指定位置生成一些数据文件,创建之前,我们需要检查这些指定位置是否存在,不存在则需要去创建它们。为了避免不必要的代码散落,我们可以为系统中相应目标类提供一个Before Advice,对文件系统的指定路径进行统一的检查或者初始化。代码如下:

public class ResourceSetupBeforeAdvice implements MethodBeforeAdvice {
    private Resource resource;
    public ResourceSetupBeforeAdvice(Resource resource) {
        this.resoource = resource;
    }
    public void before(Method method, Object[] args, Object target) throws Throwable {
        if (!resource.exists()) {
            FileUtils.forceMkdir(resouce.getFile());
        }
    }
}
  • ThrowsAdvice:该接口并未定义任何方法,但是我们的方法定义需要遵循如下规则
void afterThrowing([Method, args, target], ThrowableSubclass)

ThrowAdvice通常用于对系统中特定的异常情况进行监控,以统一的方式对所发生的异常进行处理。

  • AfterReturningAdvice:只有在方法正常返回的情况下,AfterReturningAdvice才会执行,所有并不能用来处理资源清理之类的工作。另外,虽然AfterReturningAdvice可以访问到方法的返回值,但是它并不能改变返回值。其定义如下:
public interface AfterReturningAdvice extends AfterAdvice {
    void afterReturning(Object var1, Method var2, Object[] var3, Object var4) throws Throwable;
}
  • AroundAdvice:Spring中没有直接定义对应Around Service的实现接口,而是直接采用AOP Alliance的标准接口,org.aopalliance.intercept.MethodInterceptor,该接口定义如下:
public interface MethodInterceptor extends Interceptor {
    Object invoke(MethodInvocation var1) throws Throwable;
}

之前提到的几种Advice能完成的事情,对于MethodInterceptor都不在话下!口说无凭,我们来看一个栗子吧!

// 简单的检测系统某些方法的执行性能
public class PerformanceMethodInterceptor implements MethodInterceptor {
    private final Log logger = LogFactory.getLog(this.getClass());
    // 通过MethodInvocation参数,我们可以控制对相应Jointpoint的拦截行为
    public Object invoke(MethodInvocation invocation) throws Throwable {
        StopWatch watch = new StopWatch();
        try {
            watch.start();
            // 调用proceed方法可以让程序执行继续沿调用链执行
            return invocation.proceed();
        } catch (Exception e) {
            //TODO: handle exception
        } finally {
            watch.stop();
            if (logger.isInfoEnabled()) {
                logger.info(watch.toString());
            }
        }
    }
}

通过MethodInvocation参数,我们可以控制对相应Jointpoint的拦截行为。通过调用MethodIvocationproceed()方法,可以让程序继续沿着调用链传播。如果我们在某一个MethodInterceptor没有调用proceed()方法,那么程序将会在当前MethodInterceptor处短路,同一Jointpoint处的其他MethodInterceptor以及Jointpoint处的方法逻辑将都不会执行!正如在PerformanceMethodInterceptor看到的那样,我们可以在proceed()方法的前后加入相应的逻辑,甚至可以捕获proceed()抛出的异常。现在,你是否理解了为什么MethodInterceptor可以完成其他类型Advice可以完成的任务了?
我们可以直接通过编程的方式来使用该类,如下所示:

PerformanceMethodInterceptor interceptor = new PerformanceMethodInterceptor();
...
// 可以将其添加到相应的Aspect中使用

既然我们使用了Spring框架并且这些Advice实现都是普通的POJO,更多时候会将其集成到IoC容器中,如下:

<bean id="performanceInterceptor" class="...PerformanceMethodInterceptor"/>

2.3.2 per-instance类型的Advice

与per-class类型的Advice不同,per-instance类型的Advice不会再目标类所有对象实例之间共享,而是会为不同的实例对象保存它们各自的状态和相关逻辑。就拿上班族为例,如果员工是一类人的话,那么公司的每一类员工就是员工类的不同对象实例。每个员工上班之前,公司设置了一个per-class类型的Advice进行“上班活动”的一个拦截,即打卡机,所有员工共用一个打卡机。当每个员工进入各自的位置之后,他们就会使用各自的电脑进行工作,而他们各自的电脑就好像per-instance类型的Advice一样,每个电脑保存了每个员工自己的资料。在Spring中,Introduction就是唯一的一种per-instance类型的Advice。以后会专门写一篇文章讲关于Introduction(逃。

2.4 Spring AOP中的Aspect

我们在之前提到过,AOP的概念中,一个Aspect是由多个Pointcut和Advice组成的概念实体。但是在Spring中,Aspect被称为Advisor,它通常只持有一个Pointcut和一个Advice。Spring中的Advisor实现方式可以简单划分为两个分支。一个分支以PointcutAdvisor接口为首,另一个分支以IntroductionAdvisor为首。下图是Advisor家族的UML类图。
Advisor家族

2.4.1 PointcutAdvisor家族

实际上,org.springframework.aop.PointcutAdvisor才是真正定义一个Pointcut和一个Advice的Advisor,大部分的Advisor实现全都是PointcutAdvisor的部下。下面我们来看几个常用的PointcutAdvisor实现。

  • DefaultPointcutAdvisor:它是最通用的Advisor类型,除了不能为其指定Introduction类型的Advice之外,剩下任何类型的Pointcut和Advice都可以通过DefaultPointcutAdvisor来使用。我们可以在构造DefaultPointcutAdvisor的时候,就明确指定属于当前DefaultPointcutAdvisor实例的Advisor实例的Point和Advice,也可以在该实例构造完成后再通过set方法设置。代码如下:
Pointcut pointcut = ...;// 任何Pointcut类型
Advice advice = ...;
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut, advice);
// 或者
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor();
advisor.setPointcut(pointcut);
advisor.setAdvice(advice);

但是这个代码不是让你在实际环境中这么用的,如果在Spring的XML配置文件中,我们需要这么做:

<bean id="pointcut" class="...">
...
</bean>
<bean id="advice" class="...">
...
</bean>
<bean id="advisor" class="org.springframework.aop.support.DefaultPointcutAdvisor">
    <property name="pointcut" ref="pointcut"/>
    <property name="advice" ref="advice">
</bean>
  • NameMatchMethodPointcutAdvisor是细化后的DefaultPointcutAdvisor,它限定了自身可以使用的Pointcut类型为NameMatchMethodPointcut,并且外部不可更改。关于这个类型的Pointcut上面有介绍,在此不做赘述。可以通过该Advisor公开的setMappedName()setMappedNames()方法设置将被拦截的方法名称(实际上是在操作持有的NameMatchMethodPointcut实例)。
  • RegexMethodPointcutAdvisor:也限定了自身可以使用的Pointcut类型,即只能通过正则表达式为其设置相应的Pointcut。

2.4.2 IntroductionAdvisor分支

IntroductionAdvisorPointcutAdvisor最本质的区别是IntroductionAdvisor只能应用于类级别的拦截,以及只能使用Introduction型的Advice。它只有一个默认的实现类DefaultIntroductionAdvisor,其继承关系也在上面的UML类图中提到。

2.4.3 Ordered的作用

大多数时候,我们的系统中都会有多个横切关注点需要处理,那么系统中就会有多个Advisor存在。当其中某些Advisor的Point匹配了同一个Jointpoint的时候,就会在这同一个Jointpoint处执行多个Advice的逻辑。Spring在处理同一Jointpoint处的多个Advisor时,实际上会按照指定的顺序和优先级来执行它们,如果我们不明确指定各个Advisor的执行顺序,那么Spring会按照他们的声明顺序来应用它们。那么我们如何指定执行的顺序或优先级呢?
在Spring框架中,我们可以让相应的Advisor以及其他顺序紧要的bean实现org.springframework.core.Ordered接口来明确指定相应的顺序号。不过,在UML图中可以看到,各个Advisor的实现类已经默认实现了Ordered接口,我们唯一需要做的就是在配置的时候指定顺序号。如下代码所示:

<bean id="xxxxAdvisor" class="xxxx">
    <property name="order" value="1"/>
</bean>

参考资料:《Spring揭秘》王福强

原文地址:https://www.cnblogs.com/muuu520/p/12828290.html