晚上到了家里, 上一篇博客最后的问题找到了, 是因为Advisor类出错了. 既然Advisor是由Pointcut和Advice组成的, 所以还需要额外的覆盖一个方法, Spring4.x这本书里没讲明白.

下边就先解决问题, 然后把Spring提供的这些切面类都看一下.

  1. StaticMethodMatcherPointcutAdvisor
  2. RegexpMethodPointcutAdvisor
  3. 动态切面 – 其实是动态切点
  4. 流程切面 – 使用流程切点
  5. 复合切面 – 使用复合切点
  6. 其他一些内容

Advisor类的使用方法

先来解决上一篇文章里的遗留问题. 我仔细看了一下书上还有网上的XML配置, 发现配置切面类的方法是这样:

<bean id="myadvisor" class="cc.conyli.MyStaticAspect" p:advice-ref="greetingAdvice"/>

我就想到, 是不是切面类里有一个东西需要包含一个Advice才行, 后来查看覆盖的方法, 再一搜, 发现果然是这样, 需要覆盖其中的getAdvice()就可以正常工作了.

getAdvice(), 返回一个自己编写的匿名类或者已经编写好的其他Advice实例都可以.

于是切面类的代码需要改成这样:

import org.aopalliance.aop.Advice;
import org.springframework.aop.ClassFilter;
import org.springframework.aop.MethodBeforeAdvice;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.StaticMethodMatcherPointcutAdvisor;

import java.lang.reflect.Method;

public class ServiceOnlyAdvisor extends StaticMethodMatcherPointcutAdvisor {


    //类过滤, 必须是StandardService的实现类才可以
    @Override
    public ClassFilter getClassFilter() {
        return new ClassFilter() {
            @Override
            public boolean matches(Class clazz) {
                return StandardService.class.isAssignableFrom(clazz);
            }
        };
    }

    //这个内部是利用了切点的方法, 判断方法名称是否等于service
    @Override
    public boolean matches(Method method, Class targetClass) {
        return method.getName().equals("service");
    }

    @Override
    public Advice getAdvice() {
        return new GreetingBefore();
    }

    public static void main(String[] args) {
        //目标对象
        StandardService service1 = new StandardServiceImpl();
        //切面
        StaticMethodMatcherPointcutAdvisor advisor = new ServiceOnlyAdvisor();
        //原来的前置增强
        MethodBeforeAdvice advice = new GreetingBefore();

        ProxyFactory proxyFactory = new ProxyFactory();
        proxyFactory.setTarget(service1);
        proxyFactory.setInterfaces(service1.getClass().getInterfaces());

        //不需直接添加Advice, 否则就是全局的
        //proxyFactory.addAdvice(advice);
        //添加Advisor就可以了
        proxyFactory.addAdvisor(advisor);

        StandardService service1proxy = (StandardService) proxyFactory.getProxy();

        service1proxy.service(10);
    }

}

经过测试一切正常了, 可见一个切面类里必须要把Pointcut的规则, 和其中附带的Advice都规定好, 才能够正常生效.

现在可以来总结一下StaticMethodMatcherPointcutAdvisor这个切面类了, 覆盖三个方法, 分别是静态匹配方法, 类, 以及返回所需的Advice, 就可以正常工作了. 前边两个方法属于切点, 最后一个方法属于增强, 都配置好才能工作.

猜想其他的类, 其实也只是匹配的手段和方式不同, 其本质是相同的.

RegexpMethodPointcutAdvisor

这个类功能齐备, 无需扩展了, 看了一下类的方法, 有四个构造器, 一个无参, 一个带一个advice, 一个接受一个字符串形式的pattern和一个advice, 还一个接受字符串数组和advice.

还有setPattern()和setAdvice()方法, 这基本上一看也差不多明白了. 主要就是这个正则表达式如何匹配的问题了.

正则的匹配我自己看一了一下, 核心是包和方法之间的分隔符要用转义\., 然后最靠右边的就是方法名.

编写一个针对cc.conyli.MyImplClass的m2方法的切面如下:

public static void main(String[] args) {
    MyImplClass impl = new MyImplClass();

    MyWelcomeAdvice advice = new MyWelcomeAdvice();

    RegexpMethodPointcutAdvisor advisor = new RegexpMethodPointcutAdvisor("cc\\.conyli\\.MyImplClass\\.m2", advice);

    ProxyFactory factory = new ProxyFactory();

    factory.setTarget(impl);

    factory.addAdvisor(advisor);

    MyInterFace myInterFace = (MyInterFace) factory.getProxy();

    myInterFace.m1();
    myInterFace.m2();

}

正则表达式的本质, 还是匹配名称, 灵活度比匹配类和方法要更高一些, 可以精准匹配, 也可以批量匹配.

动态切面 – 其实是动态切点

前边两个是静态切面, 仔细观察的话, 会发现只能匹配到类名和方法名, 无法匹配参数. 如果需要匹配参数, 就需要动态匹配上场了.

使用动态切面需要使用DefaultPointcutAdvisor, 然后搭配一个DynamicMethodMatcherPointcut切点以及一个Advice来使用.

先创建切点, 切点需要继承DynamicMethodMatcherPointcut. 现在的接口如下

public class MyImplClass implements MyInterFace {
    public void m1() {
        System.out.println("m1方法执行");
    }

    public void m2() {
        System.out.println("m2方法执行");
    }

    public void m3(String name) {
        System.out.println("m3方法执行, 参数是" + name);
    }
}

我们要来匹配一个当m3的方法接受的参数是saner的时候, 进行增强, 否则不进行增强, 先编写继承DynamicMethodMatcherPointcut的类:

import org.springframework.aop.ClassFilter;
import org.springframework.aop.support.DynamicMethodMatcherPointcut;
import java.lang.reflect.Method;

public class MyDynamicPointcut extends DynamicMethodMatcherPointcut {

    //带参数的方法是动态匹配
    @Override
    public boolean matches(Method method, Class<?> aClass, Object... objects) {
        return ((String) objects[0]).equals("saner");
    }


    //静态检查类
    @Override
    public ClassFilter getClassFilter() {
        return new ClassFilter() {
            @Override
            public boolean matches(Class<?> aClass) {
                return MyInterFace.class.isAssignableFrom(aClass);
            }
        };
    }

    //静态匹配方法
    @Override
    public boolean matches(Method method, Class targetClass) {
        return method.getName().equals("m3");
    }
}

这个类里既有动态检查又有静态检查, Spring的机制是先静态再动态, 如果静态检查匹配, 才使用动态, 否则就直接不匹配.

这个切点类里已经将isRuntime()设置成了true, 而且是final的, 也就是说一定是具备动态功能的.

这毕竟是切点类, 使用的时候还需要加上一个Advice一起装配到DefaultPointcutAdvisor中:

public static void main(String[] args) {
    MyImplClass impl = new MyImplClass();
    MyWelcomeAdvice advice = new MyWelcomeAdvice();

    //组装动态切面
    Pointcut dynamic = new MyDynamicPointcut();
    DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor();
    advisor.setAdvice(advice);
    advisor.setPointcut(dynamic);

    ProxyFactory factory = new ProxyFactory();
    factory.setTarget(impl);
    factory.addAdvisor(advisor);
    MyInterFace myInterFace = (MyInterFace) factory.getProxy();

    myInterFace.m3("owl");
    myInterFace.m3("saner");
    }

XML配置其实就多出来一个Pointcut的Bean需要配置给Advisor, 如下:

<?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
           xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <!--实现类-->
    <bean id="id1" class="cc.conyli.MyImplClass"/>
    <!--    前置增强-->
    <bean id="id2" class="cc.conyli.MyWelcomeAdvice"/>
    <!--    后置增强-->
    <bean id="id3" class="cc.conyli.MyAfterAdvice"/>

    <!--动态切点-->
    <bean id="mydynamicpointcut" class="cc.conyli.MyDynamicPointcut"/>
    <!--用动态切点和Advice组装Advisor Bean-->
    <bean id="mydynamicadvisor" class="org.springframework.aop.support.DefaultPointcutAdvisor" p:pointcut-ref="mydynamicpointcut" p:advice-ref="id2"/>
    <!--代理工厂里使用动态切面类mydynamicadvisor-->
    <bean id="id4" class="org.springframework.aop.framework.ProxyFactoryBean" p:interceptorNames="mydynamicadvisor" p:proxyTargetClass="true" p:target-ref="id1"/>

</beans>

可见知道了原理之后, XML配置都是大同小异. 切面等于切点+增强, 切面的动态指的是切面中的切点动态匹配.

流程切面

这个流程切点略微有点抽象. 之前的切面, 我们都是找到匹配的类和方法, 然后在那个类的方法处进行切入.

流程切点就不是直接在那个类的方法处进行切入, 而是在调用这个目标类的方法的方法处切入.

举个例子, A一个方法a中调用B类的方法b, 这里B类已经是一个经过AOP的类, 我们不针对B类的b方法进行切面, 因为这样会让所有的b方法都得到增强, 只想让A调用B的b方法时候增强, 这样切面其实切在A而不是B上.

流程切面的实现需要ControlFlowPointcut切点和DefaultPointcutAdvisor切面类共同完成, 不用说, 肯定核心又在ControlFlowPointcut上.

我们举个例子, 有另外一个类:

public class MyControl {

    private MyInterFace myInterFace;

    public MyControl(MyInterFace myInterFace) {
        this.myInterFace = myInterFace;
    }

    public void service() {
        myInterFace.m1();
        myInterFace.m2();
    }
}

想在service()调用其中的两个方法的时候, 给m1和m2增强, 其他时候不增强, 这个时候很显然不能切在MyInterFace的实现类上, 怎么办呢, 就使用流程切面, 切在MyControl类上:

public static void main(String[] args) {
    MyImplClass impl = new MyImplClass();
    MyWelcomeAdvice advice = new MyWelcomeAdvice();

    //流程切点, 第一个是MyControl, 第二个参数是字符串形式的方法名称
    Pointcut controlFlow = new ControlFlowPointcut(MyControl.class, "service");
    //依然是老样子组装advisor
    DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor();
    advisor.setAdvice(advice);
    advisor.setPointcut(controlFlow);

    ProxyFactory factory = new ProxyFactory();
    factory.setTarget(impl);
    factory.addAdvisor(advisor);
    MyInterFace myInterFace = (MyInterFace) factory.getProxy();

    //使用代理类调用方法, 不出现增强效果
    myInterFace.m1();
    myInterFace.m2();

    //指定的类和方法中, 调用代理类, 出现增强效果
    MyControl control = new MyControl(myInterFace);
    control.service();
}

注意, 代理类创建的时候, target依然是最开始的那个实现类, 而流程控制类使用的是代理类, 不是最开始的那个没有经过任何增强的实现类.

这个也属于动态匹配, 因此运行较慢, 这个主要用在让使用代理类的其他类的某个环节, 可以应用增强效果, 所以叫做流程类. 实际上仔细分析可以想明白, 被增强的依然是代理类, 但是让增强的效果, 只出现在使用代理类的某个指定的环节, 所以叫做流程切面.

复合切面 – 使用复合切点

一个切点就是一个规则, 如果需要编写很复杂的规则, 可以重复利用切点规则就可以了. ComposablePointcut就是这样一个复合切点类, 而且其本身也是Pointcut接口类型, 所以复核切点也就可以当做一个切点来使用.

搭配复合切点使用的切面类依然是DefaultPointcutAdvisor.

复合切点类已经是一个成熟的类了, 可以直接使用构造器, 不过这个类的构造器嘛, Hmm…..:

  1. 空参构造器, 匹配所有类所有方法, 和直接上Advice也没什么区别
  2. 单参 ClassFilter , 匹配特定类的全部方法, 用这个我还不如用静态匹配了.
  3. 单参 MethodMatcher , 匹配全部类的特定方法, 和上边一样, 如果这么用, 还不如上静态匹配来得快.
  4. 两参数 MethodMatcher ClassFilter, 匹配特定类的特定方法, 这也是吃饱了撑的, 还不如用静态匹配.

可见这些构造器单独使用没有什么用, 关键在于下边的这些方法:

  1. intersection(ClassFilter), 与一个ClassFilter进行交集运算, 返回一个ComposablePointcut类型
  2. intersection(MethodMatcher), 与一个MethodMatcher进行交集运算, 返回一个ComposablePointcut类型
  3. intersection(Pointcut), 与一个Pointcut进行交集运算, 返回一个ComposablePointcut类型
  4. union(ClassFilter), 与一个ClassFilter进行并集运算, 返回一个ComposablePointcut类型
  5. union(MethodMatcher), 与一个MethodMatcher进行并集运算, 返回一个ComposablePointcut类型

咦, 不能对另外一个切点使用并集运算啊, 可以使用 org.springframework.aop.support.Pointcuts 工具类, 其中两个静态方法如下:

  1. Pointcus.intersection(Pointcut a, Pointcut b), 两个切点的交集, 返回一个ComposablePointcut类的实例
  2. Pointcus.union(Pointcut a, Pointcut b), 两个切点的并集, 返回一个ComposablePointcut类的实例

现在用代码来试验一下. 先来试验单独的效果:

public static void main(String[] args) {
    MyImplClass impl = new MyImplClass();
    MyWelcomeAdvice advice = new MyWelcomeAdvice();

    //构造一个匹配MyInterFace接口及其实现类的复合切点
    ComposablePointcut pointcut = new ComposablePointcut(new ClassFilter() {
        @Override
        public boolean matches(Class<?> aClass) {
            return MyInterFace.class.isAssignableFrom(aClass);
        }
    });

    //用DefaultPointcutAdvisor组装切面
    DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor();
    advisor.setAdvice(advice);
    advisor.setPointcut(pointcut);

    ProxyFactory factory = new ProxyFactory();
    factory.setTarget(impl);
    factory.addAdvisor(advisor);
    MyInterFace myInterFace = (MyInterFace) factory.getProxy();

    myInterFace.m1();
    myInterFace.m2();
}

可见使用复合切点, 创建了一个匹配MyInterFace及其实现类的所有方法的切面, 现在给其复合一下, 仅针对m2方法增强:

public static void main(String[] args) {
    MyImplClass impl = new MyImplClass();
    MyWelcomeAdvice advice = new MyWelcomeAdvice();

    //构造一个匹配MyInterFace接口及其实现类的复合切点
    ComposablePointcut pointcut = new ComposablePointcut(new ClassFilter() {
        @Override
        public boolean matches(Class<?> aClass) {
            return MyInterFace.class.isAssignableFrom(aClass);
        }
    });

    pointcut.intersection(new MethodMatcher() {
        @Override
        public boolean matches(Method method, Class<?> aClass) {
            return method.getName().equals("m2");
        }

        @Override
        public boolean isRuntime() {
            return false;
        }

        @Override
        public boolean matches(Method method, Class<?> aClass, Object[] objects) {
            return false;
        }
    });

    //用DefaultPointcutAdvisor组装切面
    DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor();
    advisor.setAdvice(advice);
    advisor.setPointcut(pointcut);

    ProxyFactory factory = new ProxyFactory();
    factory.setTarget(impl);
    factory.addAdvisor(advisor);
    MyInterFace myInterFace = (MyInterFace) factory.getProxy();

    myInterFace.m1();
    myInterFace.m2();
}

此时取匹配m2方法的并集, 结果就是MyInterFace及其实现类中的m2方法才有增强效果了.

配置XML的时候, 就不能像上边这样直接调用方法, 而是需要配置好不同的Bean, 在XML里组装成最终的ComposablePointcut对象, 再交给DefaultPointcutAdvisor的Bean就可以了.

其他一些内容

引介的切面和之前的切面不同, 是类级别的, 因为给类添加新的实现接口. 这里就暂时先忽略了.

一般使用AOP, 不会像这样直接写代码, 肯定是通过手工配置XML, 生成所需的Bean然后自动生成代理类. 再用代理类替换成原来的类. Spring还提供了自动创建代理的类.
这个实际上就是根据一些规则, 进行自动扫描. 比如可以扫描Bean的id名称, 扫描Advisor类型的所有类, 扫描注解等. 其实本质也是一样, 就是不直接使用ProxyFactoryBean, 而改用另外一个Bean. 但是这些自动创建代理的类, 是后边扫描注解等的基础.

回顾一下Spring的这些切面类, 都是需要先设置切点, 不管是直接覆盖方法, 使用setter方法, 都需要设置. 然后还需要设置增强, 最后组装好切面对象并且应用. 切面的本质是生成代理类, 在容器中使用的真正的东西就是那些代理类.

还有一个显而易见的问题就是, AOP方面, Spring原生的这些类只能用XML来配置或者用代码直接写, 实际上在容器里我们基本上不需要原始的类, 只需要增强之后的代理类. 所以希望Spring可以自动装配, 但是自动装配还是需要配置XML文件, 因此Spring还提供了对于AspectJ的支持, 这样就可以使用注解的方式了.