• 架构探险笔记4-使框架具备AOP特性(上)


    对方法进行性能监控,在方法调用时统计出方法执行时间。

    原始做法:在内个方法的开头获取系统时间,然后在方法的结尾获取时间,最后把前后台两次分别获取的系统时间做一个减法,即可获取方法执行所消耗的总时间。

    项目中大量的方法,如果对每个方法开头结尾都加上这些代码,工作量会很大。现在不用修改现有代码,在另一个地方做性能监控,AOP(Aspect Oriented Programming,面向方面编程)就是我们寻找的解决方案。

    在AOP中,我们需要定义一个Aspect(切面)类来编写需要横切业务逻辑的代码,也就是性能监控代码。此外,我们需要通过一个条件来匹配想要拦截的类,这个条件在AOP中称为Pointcut(切点)。

    案例思路,统计出执行每个Controller类的各个方法所消耗的时间。每个Controller类都有Controller注解,也就是说,我们只需要拦截所有带有Controller注解的类就行了,切点很容易就能确定下来,剩下的就是做一个切面了。

    代理技术

    代理,或称为Proxy,意思就是你不用去做,别人替你去处理。比如说:赚钱方面,我就是我老婆的Proxy;带小孩方面,我老婆就是我的Proxy;家务事方面,没有Proxy。

    它在程序中开发起到了非常重要的作用,比如AOP,就是针对代理的一种应用。此外,在设计模式中,还有一个“代理模式”,在公司要上网,要在浏览器中设置一个Http代理。

    Hello World例子

    //接口
    public interface Hello{
      void say(String name);  
    }
    
    //实现类
    public class HelloImpl implements Hello{
      @Override
      public void say(String name){
         System.out.println("Hello!"+name);      
      }    
    }

    如果要在println方法前面和后面分别需要处理一些逻辑,怎么做呢?把这些逻辑写死在say方法里面吗?这么做肯定不够优雅,“菜鸟”一般这样干,作为一名资深的程序员,我们坚决不能这么做!

    我们要用代理模式,写一个HelloProxy类,让它去调用HelloImpl的say方法,在调用的前后分别进行逻辑处理。

    public class HelloProxy implements Hello{
      private Hello hello;
    
      private HelloProxy(){
        hello=new HelloImpl();
      }  
        
       private void say(String name){
           before();
           hello.say(name);
           after();
       }
    
       private void before(){
           System.out.println("Before");
       }
    
       private void after(){
           System.out.println("After");
       }
    }

    用HelloProxy类实现了Hello接口(和HelloImpl实现相同的接口),并且在构造方法中new出一个HelloImpl类的实例。这样一来,我们就可以在HelloProxy的say方法里面去调用HelloImpl的say方法了。更重要的是,我们还可以在调用的前后分别加上before和after两个方法,在这两个方法里去实现那些前后逻辑。

    main方法测试

    public static void main(String[] args){
        Hello helloProxy = new HelloProxy();
        helloProxy.say("Jack");
    }
    
    
    //打印结果
    Before
    Hello! Jack
    After

    JDK动态代理

    于是疯狂使用代理模式,项目中到处都是XXXProxy的身影,直到有一天,架构师看到了我的代码,他惊呆了,对我说“你怎么这么喜欢静态代理呢?你就不会用动态代理吗?全部重构!”

    研究了一下,原来一直用的是静态代理(上面的例子),到处都是XXXProxy类。一定要将这些垃圾Proxy都重构为“动态代理”。

    /**
     * 动态代理
     */
    public class DynamicProxy implements InvocationHandler {
        private Object target;
        public DynamicProxy(Object target) {
           this.target = target;
        }
        /* 
         * 用时代理
         */
        @Override
        public Object invoke(Object proxy, Method method, Object[] args)
                throws Throwable {
            before();
            Object result method.invoke(target, args);
            after();
    return result;
        }
    
    }
    
        //运行
        public static void main(String[] args) {
            Hello hello = new HelloImpl();//用时代理
            DynamicProxy dynamicProxy = new DynamicProxy(hello);
    Hello helloProxy
    = (Hello)Proxy.newProxyInstance(
          hello.getClass().getClassLoader(),
          hello.getClass().getInterfaces(),
          dynamicProxy
         );
    //运行run方法 helloProxy.say("Jack"); }

    在这个例子中,DynamicProxy定义了一个Object类型的Object变量,它就是被代理的目标对象,通过构造函数来初始化(“注入”,构造方法初始化叫“正着射”,所以反射初始化叫“反着射”,简称“反射”)。

    通过DynamicProxy类去包装Car实例,然后再调用JDK给我们的提供的Proxy类的工厂方法newProxyInstance去动态的创建一个Hello接口的代理类,最后调用这个代理类的run方法。

    Proxy.newProxyInstance方法的参数

    参数1:ClassLoader

    参数2:该实现类的所有接口

    参数3:动态代理对象

    调用完了用强制类型转换下

    这一块想办法封装一下,避免再次出现到处都是Proxy.newProxyInstance方法的情况。于是将这个DynamicProxy重构一下:

    public class DynamicProxy implements InvocationHandler{    
    private
    Object target; public DynamicProxy(Object target) { this.target = target; }
       @SupressWarnings("unchecked")
    public <T> T getProxyInstance() { return (T)Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(),this); } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { before(); Object result = method.invoke(target, args); after(); return result; } //请求前的操作 public void before(){ //预处理 System.out.println(target.getClass()+"被动态代理了,在它执行之前要执行动态代理加入的预处理方法"); } //请求后的操作 public void after(){ //善后处理 System.out.println(target.getClass()+"被动态代理了,在它执行之后要执行动态代理加入的善后方法"); } } public class DynamicProxyDemo { public static void main(String[] args) {
        DynamicProxy dynamicProxy = new DynamicProxy(new HelloImpl());
        Hello helloProxy = dynamicProxy.getProxy();
        helloProxy.say("Jack");
    } }

    在DynamicProxy里添加了一个getProxy方法,无需传入任何参数,将刚才所说的那块代码放在这个方法中,并且该方法返回一个泛型类型,就不会强制转换类型了。方法头上@SupressWarnings(“unchecked”)注解表示忽略编译时的警告(因为Proxy.newProxyInstance方法返回的是一个Object,这里强制转换为T了,这是向下转型,IDE中就会有警告,编译时也会出现提示)。

    调用时就简单了,用2行代理去掉了前面的7行代码(省了5行)。

    CGlib动态代理

    用了DynamicProxy以后,好处是接口变了,这个动态代理类不用动。而静态代理就不一样了,接口变了,实现类还要动,代理类也要动。但是动态代理并不是万能的,它也有搞不定的时候,比如要代理一个没有任何接口的类,它就没有用武之地了。

    CGlib是一个能代理没有接口的类,虽然看起来不起眼,但Spring、Hibernate这样高端的开源框架都用到了它,它是一个在运行期间动态生成字节码的工具,也就是动态生成代理类了。

    public class CGLibProxy implements MethodInterceptor{
        
        public <T> T getProxy(Class<T> cls){
            return (T) Enhancer.create(cls,this);
        }
    
        public Object intercept(Object obj,Method method,Object[] args,MethodProxy proxy) throws Throwable{
            before();
            Object result = proxy.invokeSuper(obj,args);
            after();
            return result;
        }
    
        ......
    }

    需要实现CGLib给我们提供的MethodInterceptor实现类,并填充intercept方法。方法中最后一个MethodProxy类型的参数proxy值得注意。CGLib给我们提供的是方法级别的代理,也可以理解为堆方法拦截(也就是“方法拦截器”)。这个功能对于我们程序员来说,如同雪中送炭。我们直接调用proxy的invokeSuper方法,将被代理的对象obj以及方法参数args传入其中即可。

    与DynamicProxy类似,在CGLibProxy中也添加了一个泛型的getProxy方法,便于我们可以快速地获取自动生成的代理对象。

    public static void main(){
        CGLibProxy cgLibProxy = new CGLibProxy();
        Hello helloProxy = cgLibProxy.getProxy(HelloImpl.class);
        helloProxy.say(Jack);
    }

    仍然通过2行代码就可以返回代理对象,与JDK动态代理不同的是,这里不需要任何的接口信息,对谁都可以生成动态代理对象

    用2行代码返回代理对象还是有些多余的,不想总是去new这个CGLibProxy对象,最好new一次,以后随时拿随时用,于是想到了“单例模式”:

    public class CGLibProxy implements MethodInterceptor{
        private static CGLibProxy instance = new CGLibProxy();
    
        private CGLibProxy(){
            
        }
        private static CGLibProxy getInstance(){
            return instance;
        }
    ...
    getProxy...
    intercept... }

    加上以上几行代码问题就解决了。需要说明的是,这里有一个private的构造方法,就是为了限制外界不能再去new它了,换句话说,这个类被阉割了。

    public static void main(String[] args){
      Hello helloProxy = CGLibProxy.getInstance().getProxy(HelloImpl.class);
      helloProxy.say("Jack");    
    }

    这里只需要一行代码就可以获取代理对象了.

    AOP技术

    什么是AOP

    AOP(Aspect-Oriented Programming),名字与OOP仅仅差一个字母,其实它是对OOP编程方式的一种补充,并非是取而代之。翻译过来就是“面向切面编程”或“面向方面编程”。最重要的工作就是写这个“切面”,那么什么事“切面”呢?

    切面是AOP中的一个术语,表示从业务逻辑中分离出来的横切逻辑,比如性能监控、日志记录、权限控制等,这些功能都可以从业务逻辑代码中抽离出去。也就是说,通过AOP可以解决代码耦合问题,让职责更加单一。

    需要澄清的是,其实很早以前就出现了AOP这个概念。最知名强大的Java开源项目就是AspectJ了。它的前身是AspectWerkz(AOP真正的的老祖宗)。Rod Johnson写了一个Spring框架,称为Spring之父。他在Spring的IOC框架基础上又实现了一套AOP框架,后来掉进了深渊,在无法自拔的时候被迫使用了AspectJ。所以我们现在用的最多的就是Spring+AspectJ这种AOP框架了。

    写死代码

    public interface Greeting {
        void sayHello(String name);
    }
    
    public class GreetingImpl implements Greeting {
        @Override
        public void sayHello(String name) {
            before();
            System.out.println("Hello! "+name);
            after();
        }
    
        private void before(){
            System.out.println("Before");
        }
    
        private void after(){
            System.out.println("After");
        }
    }

    before与after方法写死在sayHello方法体中了,这样的代码非常不好。如果我们要统计每一个方法的执行时间,以对性能进行评估,那是不是每个方法的一头一尾都做点手脚呢?

    再比如我们要写一个JDBC程序,那是不是也要在方法的开头去连接数据库,方法的末尾去关闭数据库连接呢?

    这样写代码只会把程序员累死,把架构师气死!

    一定要想办法对上面的代码进行重构,首先给出三个解决方案:

    静态代理

    JDK动态代理

    CGLib动态代理

    静态代理

    最简单的解决方案就是使用静态代理模式了,我们单独为GreetingImpl这个类写一个代理类:

    public class GreetingProxy implements Greeting {
        private GreetingImpl greetingImpl;
    
        public GreetingProxy(GreetingImpl greetingImpl) {
            this.greetingImpl = greetingImpl;
        }
    
        @Override
        public void sayHello(String name) {
            before();
            System.out.println("Hello! "+name);
            after();
        }
    
        private void before(){
            System.out.println("Before");
        }
        private void after(){
            System.out.println("After");
        }
    }

    就用这个GreetingProxy去代理GreetingImpl,看看客户端如何来调用:

    public class Client {
        public static void main(String[] args) {
            Greeting greetingProxy = new GreetingProxy(new GreetingImpl());
            greetingProxy.sayHello("Jack");
        }
    }

    这个写的没错,但是有个问题。XxxProxy这样的类会越来越多(这里构造函数参数为GreetingImpl,所以换一个子类就要再次写一个代理类,如果构造函数参数改为接口Greet,这样再使用Greet的子类时可以使用这个类,当换一个接口时又要去写一个代理类),如何才能将这些代理类尽可能减少呢?最好只有一个代理类。

    这时我们需要使用JDK的动态代理了。

    JDK动态代理

    public class JDKDynamicProxy implements InvocationHandler {
        private Object target;
    
        public JDKDynamicProxy(Object target) {
            this.target = target;
        }
    
        @SuppressWarnings("unchecked")
        public <T> T getProxy(){
            return (T) Proxy.newProxyInstance(target.getClass().getClassLoader(),target.getClass().getInterfaces(),this);
        }
    
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            before();
            Object result = method.invoke(target,args);
            after();
            return result;
        }
    
        private void before(){
            System.out.println("Before");
        }
    
        private void after(){
            System.out.println("After");
        }
    }

    这样所有的代理类都合并到动态代理类中了,但这样做仍然存在一个问题:JDK给我们提供的动态代理只能代理接口,而不能代理没有接口的类

    public class Client {
        public static void main(String[] args) {
            //静态代理
            /*Greeting greetingProxy = new GreetingProxy(new GreetingImpl());
            greetingProxy.sayHello("Jack");*/
            //JDK动态代理
            Greeting greeting = new JDKDynamicProxy(new GreetingImpl())
                    .getProxy();
            greeting.sayHello("Jack");
        }
    }

    CGLib动态代理

    我们使用开源的CGLib类库可以代理没有接口的类,这样就弥补了JDK的不足。CGLib动态代理类是这样的:

    public class CGLibDynamicProxy implements MethodInterceptor{
        //单例模式
        private static CGLibDynamicProxy instance = new CGLibDynamicProxy();
        //私有化构造函数,防止new
        private CGLibDynamicProxy(){}
        //提供给外界获取单一实例的方法
        public static CGLibDynamicProxy getInstance(){
            return instance;
        }
    
        @SuppressWarnings("unchecked")
        public <T> T getProxy(Class<T> cls){
            return (T) Enhancer.create(cls,this);
        }
    
        @Override
        public Object intercept(Object target, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
            before();
            Object result = methodProxy.invokeSuper(target,args);
            after();
            return result;
        }
    
        private void before(){
            System.out.println("Before");
        }
        private void after(){
            System.out.println("After");
        }
    }

    注意这里的坐标

            <!--不能超过3.0版本,这里用2.2-->
            <dependency>
                <groupId>cglib</groupId>
                <artifactId>cglib</artifactId>
                <version>2.2</version>
            </dependency>
            <!--CGLib依赖此包-->
            <dependency>
                <groupId>org.ow2.asm</groupId>
                <artifactId>asm</artifactId>
                <version>6.2</version>
            </dependency>

    到此为止,能做的都做了,问题似乎全部都解决了。但事情总不会那么完美,而我们一定要追求完美。

    Spring AOP

    Rod Johnson搞出了一个AOP框架,Spring AOP:前置增强、后置增强、环绕增强(编程式)

    上面例子中提到的before方法,在Spring AOP里就叫Before Advice(前置增强)。有些人将Advice直译为“通知”,这里是不太合适的,因为它没有“通知”的含义,而是对原有代码功能的一种“增强”。再者,CGLib中也有一个Enhancer类,它就是一个增强类。

    此外,像after这样的方法就叫After Advice(后置增强),因为它放在后面来增强代码的功能。

    如果能把before与after结合在一起,那就叫Around Advice(环绕增强),就像汉堡一样。

    前置增强类代码(这个类实现了org.spring.framework.aop.MethodBeforeAdvice):

    import org.springframework.aop.MethodBeforeAdvice;
    
    public class GreetingBeforeAdvice implements MethodBeforeAdvice {
        @Override
        public void before(Method method, Object[] objects, Object o) throws Throwable {
            System.out.println("Before");
        }
    }

    后置增强类:

    import org.springframework.aop.AfterReturningAdvice;
    
    public class GreetingAfterAdvice implements AfterReturningAdvice {
    
        @Override
        public void afterReturning(Object o, Method method, Object[] objects, Object o1) throws Throwable {
            System.out.println("After");
        }
    }

    类似的这里实现了org.springframework.aop.afterReturningAdvice接口。

    调用

    public class Client {
        public static void main(String[] args) {
            ProxyFactory proxyFactory = new ProxyFactory();  //创建代理工厂
            proxyFactory.setTarget(new GreetingImpl());    //摄入目标类对象
            proxyFactory.addAdvice(new GreetingBeforeAdvice());   //添加前置增强
            proxyFactory.addAdvice(new GreetingAfterAdvice());   //添加后置增强
            Greeting greeting = (Greeting) proxyFactory.getProxy();
            greeting.sayHello("Jack");
    
        }
    }

    当然我们完全可以用一个增强类,让它同时实现MethodBeforeAdvice和AfterReturningAdvice这两个接口,代码:

    public class GreetingBeforeAndAfterAdvice implements MethodBeforeAdvice,AfterReturningAdvice {
        @Override
        public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
            System.out.println("before");
        }
    
        @Override
        public void before(Method method, Object[] args, Object target) throws Throwable {
            System.out.println("after");
        }
    }

    这样我们只需要使用一行代码,就可以同时添加前置与后置增强

    proxyFactory.addAdvice(new GreetingBeforeAndAfterAdvice());

    刚才有提到过“环绕增强”,其实它可以把“前置增强”与“后置增强”的功能合并起来,无须让我们同时实现两个接口。

    import org.aopalliance.intercept.MethodInterceptor;
    import org.aopalliance.intercept.MethodInvocation;
    import java.lang.reflect.Method;
    
    
    public class GreetingAroundAdvice implements MethodInterceptor {
    
        @Override
        public Object invoke(MethodInvocation methodInvocation) throws Throwable {
            before();
            Object result = methodInvocation.proceed();
            after();
            return result;
        }
    
        private void before(){
            System.out.println("Before");
        }
    
        private void after(){
            System.out.println("After");
        }
    }

    环绕增强需要实现org.aopalliance.intercept.MethodInterceptor接口。注意,这个接口不是Spring提供的,它是AOP联盟(一个很高大上的技术联盟)写的,Spring只是借用了它,在客户端汇总同样也需要将该增强类的对象添加到代理工厂中。

    public class Client {
        public static void main(String[] args) {
            ProxyFactory proxyFactory = new ProxyFactory();  //创建代理工厂
            proxyFactory.setTarget(new GreetingImpl());    //摄入目标类对象
            //proxyFactory.addAdvice(new GreetingBeforeAdvice());   //添加前置增强
            //proxyFactory.addAdvice(new GreetingAfterAdvice());   //添加后置增强
            //proxyFactory.addAdvice(new GreetingBeforeAndAfterAdvice());   //实现两个接口
            proxyFactory.addAdvice(new GreetingAroundAdvice());  //实现一个Around环绕式接口
            Greeting greeting = (Greeting) proxyFactory.getProxy();
            greeting.sayHello("Jack");
        }
    }

    以上就是SpringAOP的基本用法,单这只是“编程式”而已。Spring AOP如果只是这样,那就太弱了,它曾经也一度宣传用Spring配置文件的方式来定义Bean对象,把代码中的new操作全部解脱出来。

    SpringAOP:前置增强、后置增强、环绕增强(声明式)

    Spring配置文件篇日志

        <!--扫描指定包(将带有Component注解的类自动定义为SpringBean)-->
        <context:component-scan base-package="com.smart4j.framework"/>
    
        <bean id="greetingProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
            <property name="interfaces" value="com.smart4j.framework.Greeting"/>   <!--需要代理的接口-->
            <property name="target" ref="greetingImpl"/>   <!--实现接口类-->
            <property name="interceptorNames">   <!--拦截器名称(也就是增强类名称,SpringBean的Id)-->
                <list>
                    <value>greetingAroundAdvice</value>
                </list>
            </property>
        </bean>

    使用ProxyFactoryFactoryBean就可以取代前面的ProxyFactory,其实他们是一回事。interceptorNames改名为adviceNames或许更容易让人理解。就是网这个属性里添加增强类。

    此外,如果只有一个增强类,可以使用下面这个方法来简化

        <bean id="greetingProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
            <property name="interfaces" value="com.smart4j.framework.Greeting"/>   <!--需要代理的接口  name也可以为proxyInterfaces-->
            <property name="target" ref="greetingImpl"/>   <!--实现接口类   name也可以为targetName-->
            <property name="interceptorNames" value="greetingAroundAdvice">   <!--拦截器名称(也就是增强类名称,SpringBean的Id)-->
            </property>
        </bean>

    需要注意的是,这里使用了Spring2.5+的"Bean扫描"特性,这样我们就无需再Spring配置问加你了不断的定义<bean id="xxx" class="xxx"/>了,从而解脱了我们的双手。

    去掉原本的

        <bean id="greetingImpl" class="com.smart4j.framework.GreetingImpl"></bean>
    
        <bean id="greetingAroundAdvice" class="com.smart4j.framework.aop.GreetingAroundAdvice"></bean>

    改为使用@Compoent

    @Component
    public class GreetingImpl implements Greeting{
    。。。
    }
    
    @Component
    public class GreetingAroundAdvice implements MethodInterceptor {
    。。。
    }

    代码量确实少了,我们将配置性的代码放入配置文件,这样也有助于后期维护。更重要的是,代码值关注于业务逻辑,而将配置放入文件中,这是一条最佳实践!

    除了上面提到的那三个增强意外,其实还有两个增强也需要了解一下,关键的时候要能想到它们才行。

    Spring AOP:抛出增强

    程序报错,抛出异常了,一般的做法是打印控制台到日志文件中,这样很多地方都得去处理,有没有一个一劳永逸的方法呢?那就是Throws Advice(抛出增强)

    @Component
    public class GreetingThrowAdvice implements ThrowsAdvice {
    
        public void afterThrowing(Method method, Object[] args, Object target,Exception e){
            System.out.println("-------------Throw Exception-----------------");
            System.out.println("Target class: "+target.getClass().getName());
            System.out.println("Method Name: "+method.getName());
            System.out.println("Exception Message: "+e.getMessage());
            System.out.println("---------------------------------------------");
        }
    }

    配置spring.xml

        <bean id="greetingProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
            <property name="proxyInterfaces" value="com.smart4j.framework.Greeting"/>   <!--需要代理的接口-->
            <property name="targetName" value="greetingImpl"/>   <!--实现接口类-->
            <property name="interceptorNames">   <!--拦截器名称(也就是增强类名称,SpringBean的Id)-->
                <list>
                   <!-- <value>greetingAroundAdvice</value>-->
                    <value>greetingThrowAdvice</value>
                </list>
            </property>
        </bean>

    结果

     抛出增强需要实现org.springframework.aop.ThrowsAdvice接口,在接口方法中可获取方法、参数、目标对象、异常对象等信息。我们可以把这些信息统一写入到日志中,当然也可以持久化到数据库中。

    Spring AOP:引入增强

    以上提到的都是对方法的增强,那能否对类进行增强呢?用AOP的行话来讲,对方法的增强叫Weaving(织入),而对类的增强叫Introduction(引入),Introduction Advice(引入增强)就是对类的功能增强,它也是Spring AOP提供的最后一种增强。

    定义接口

    public interface Apology {
        void saySorry(String name);
    }

    但是我们不想在代码中让GreetingImpl直接去实现这个接口,而想在程序运行的时候动态地实现它。因为加入实现了这个接口,那么久一定要改写GreetingImpl这个类,关键是我们不想改它,或许在真实场景中,这个类有一万行代码。于是,我们需要借助Spring的引入增强。

    @Component
    public class GreetingIntroAdvice extends DelegatingIntroductionInterceptor implements Apology{
    
        @Override
        public Object invoke(MethodInvocation mi) throws Throwable {
            return super.invoke(mi);
        }
    
        @Override
        public void saySorry(String name) {
            System.out.println("Sorry "+name);
        }
    }

    以上一个引入增强类,扩展了org.springframework.aop.support.DelegatingIntroductionInterceptor类,同时也实现了新定义的Apology接口。在类中首先覆盖了父类的invoke()方法,然后实现了Apology接口的方法。我们相拥这个增强类去丰富GreetingImpl类的功能,那么这个GreetingImpl类无须直接实现Apology接口,就可以直接在程序运行的时候调用Apology接口的方法了。

    配置Spring.xml

        <!--引入增强-->
        <bean id="greetingProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
            <property name="proxyInterfaces" value="com.smart4j.framework.aop.Apology"/>   <!--需要动态实现的接口-->
            <property name="targetName" value="greetingImpl"/>   <!--目标类-->
            <property name="interceptorNames" value="greetingIntroAdvice"/>   <!--拦截器名称(也就是增强类名称,SpringBean的Id)-->
            <property name="proxyTargetClass" value="true"/>   <!--代理目标类,(默认为false,代理接口)-->
        </bean>

    需要注意proxyTargetClass属性,它表明是否代理目标类,默认为false,也就是代理接口,此时Spring就用JDK动态代理;如果为TRUE,那么Spring就用CGLib动态代理。

    调用

            ApplicationContext context = new ClassPathXmlApplicationContext("/spring.xml");  //获取Spring Context
            Greeting greeting = (Greeting) context.getBean("greetingProxy");  //从Context中根据id获取Bean对象(其实也就是一个代理)
            greeting.sayHello("jack");   //调用代理方法
    
            Apology apology = (Apology) greeting;   //将目标类增强向上转型为Apology接口(这是引入增强给我们带来的特性,也是"接口动态实现"功能)
            apology.saySorry("jack");

    sarySorry方法原来是可以被greetingImpl对象来直接调用的,只需将其强制转换为该接口即可。

    SpringAOP:切面

    之前谈到的AOP框架其实可以将它理解为一个拦截器框架,但这个拦截器似乎非常武断。比如说,如果它拦截了一个类,那么它就拦截这个类中所有的方法。类似的,当我们在使用动态代理的时候,其实也遇到了这个问题。需要在代码中对所拦截的方法名加以判断,才能过滤出我们需要拦截的方法,这种做法确实不太优雅。在大量的真实项目中,似乎我们只需要拦截特定的方法就行了,没必要拦截所有的方法。于是,Spring借助很重要的工具---Advisor(切面),来解决这个问题。它也是AOP中的核心,是我们关注的重点。

    也就是说,我们可以通过切面,将增强类与拦截匹配条件组合在一起,然后将这个切面配置到ProxyFactory中,从而生成代理。

    这里提到这个“拦截匹配条件”在AOP中就叫作Pointcut(切点),其实说白了就是一个基于表达式的拦截条件。Advisor(切面)封装了Advice(增强)与Pointcut(切点)。

    @Component
    public class GreetingImpl implements Greeting {
        @Override
        public void sayHello(String name) {
            System.out.println("Hello! "+name);
        }
    
        /*切面新增方法*/
        public void goodMorning(String name){
            System.out.println("Good Morning!"+name);
        }
    
        public void goodNight(String name){
            System.out.println("Good Night!"+name);
        }
    }

    在Spring AOP中,最好用的是基于正则表达式的切面类。

    配置

        <bean id="greetingAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
            <property name="advice" ref="greetingAroundAdvice"/>  <!--增强-->
            <property name="pattern" value="com.smart4j.framework.GreetingImpl.good.*"/>    <!--切点(正则表达式)-->
        </bean>
        <!--配置一个代理类-->
        <bean id="greetingProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
            <property name="targetName" value="greetingImpl"/>   <!--目标类-->
            <property name="interceptorNames" value="greetingAdvisor"/>   <!--切面(替换之前的拦截器名称)-->
            <property name="proxyTargetClass" value="true"/>   <!--代理目标类,(默认为false,代理接口)-->
        </bean>

    调用

            ApplicationContext context = new ClassPathXmlApplicationContext("/spring.xml");  //获取Spring Context
            GreetingImpl greeting = (GreetingImpl) context.getBean("greetingProxy");  //从Context中根据id获取Bean对象(其实也就是一个代理)
            greeting.sayHello("jack");   //调用未被代理方法
            greeting.goodMorning("Jhon");
            greeting.goodNight("Sawer");

    结果

    注意以上代理对象中的配置的interceptorNames,它不再是一个增强,而是一个切面,因为已经将增强封装到该切面中了。此外,切面还定义了一个切点(正则表达式),其目的是为了只对满足切点匹配条件的方法进行拦截。

    这里的切点表达式是基于正则表达式的。其中.*代表匹配所有字符,翻译过来就是匹配GreetingImpl类中以good开头的方法。

    除了RegexpMethodPointcutAdvisor以外,在Spring AOP中还提供了几个切面类,比如:

    • DefaultPointcutAdvisor - 默认切面(可扩展它来自定义切面)
    • NameMatchMethodPointcutAdvisor - 根据方法名称进行匹配的切面
    • StaticMethodMatcherPointcutAdvisor - 用于匹配静态方法的切面

    总的来说,让用户去配置一个或少数几个代理,似乎还可以接受,但随着项目的扩大,代理配置就会越来越多,配置的重复劳动就多了,麻烦不说,还很容易出错。。能否让Spring框架为我们自动生成代理呢?

    Spring AOP:自动代理(扫描Bean名称)

    Spring AOP提供了一个可以根据Bean名称来自动生成代理的工具,它就是BeanNameAutoProxyCreator。配置如下:

        <!--自动代理-->
        <bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
            <property name="beanNames" value="*Impl"/>   <!--为后缀是Impl的Bean生成代理-->
            <property name="interceptorNames" value="greetingAroundAdvice"/>   <!--增强(这里没用切面)-->
            <property name="optimize" value="true"/>   <!--是否对代理生成策略进行优化-->
        </bean>

    调用

            ApplicationContext context = new ClassPathXmlApplicationContext("/spring.xml");  //获取Spring Context
            GreetingImpl greeting = (GreetingImpl) context.getBean("greetingImpl");  //从Context中根据id获取Bean对象(自动扫描的id为首字母小写的类名)
            greeting.sayHello("jack");

    以上使用BeanNameAutoProxyCreator只为后缀为"Impl"的Bean生成代理。需要注意的是,这个地方我们不能定义代理接口,也及时interfaces属性,因为我们根本就不知道这些Bean到底实现了多少接口。此时不能代理接口,而只能代理类。所以这里提供了一个新的配置项,它就是optimize。若为true时,则可对代理生成策略进行优化(默认是false)。也就是说,如果该类有接口,就代理接口(JDK动态代理);如果没有接口,就代理类(使用CGLib动态代理)。并非像之前使用的proxyTargetClass属性那样,强制代理类,而不考虑代理接口的方式。

    既然CGLib可以代理任何类,那为什么还要用JDK的动态代理呢?

    根据实际项目经验得知,CGLib创建代理的速度比较慢,但创建代理后运行的速度却非常快,而JDK动态代理正好相反。如果在运行的时候不断地用CGLib去创建代理,系统的性能会大打折扣,所以建议一般在系统初始化的时候用CGLib去创建代理,并放入Spring的ApplicationContext中以备后用。

    这个例子只能匹配目标类,而不能进一步匹配其中指定的方法,要匹配方法,就要考虑使用切面与切点了。Spring AOP基于切面也提供了一个自动代理生成器:DefaultAdvisorAutoProxyCreator。

    Spring AOP:自动代理(扫描切面配置)

    为了匹配目标类中的指定方法,我们让然需要在Spring中配置切面与切点:

        <!--自动代理 - 扫描切面配置-->
        <bean id="greetingAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
            <property name="pattern" value="com.smart4j.framework.GreetingImpl.good.*"/>
            <property name="advice" ref="greetingAroundAdvice"/>
        </bean>
    
        <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator">
            <property name="optimize" value="true" />
        </bean>

    这里无须再配置代理,因为代理将由DefaultAdvisorAutoProxyCreator自动生成。也就是说,这个类可以扫描所有的切面类,并为其自动生成代理。

    看来不论怎么简化,Rod始终解决不了切面的配置这件繁重的手工劳动。在Spring配置文件中,仍然会存在大量的切面配置。然而在很多情况下,Spring AOP所提供的切面类真的不太够用了,比如像拦截指定注解的方法,我们就必须扩展DefaultPointcutAdvisor类,自定义一个切面类,然后在Spring配置文件中进行切面配置。Rod的解决方案似乎已经掉进了切面类的深渊,最重要的是切面,最麻烦的也是切面。所以要把切面配置给简化掉。

    Spring+AspectJ

    神一样的rod总算认识到了这一点,接受了网友们的建议,集成了AspectJ,同时也保留了以上提到的切面与代理配置方式(为了兼容老项目,更为了维护自己的面子)。将Spring与AspectJ集成与直接使用AspectJ是不同的,我们不需要定义AspectJ类(它扩展了Java语法的一种新的语言,还需要特定的编译器),只需要使用AspectJ切点表达式即可(它是比正则表达式更加友好的表现形式)。

    1.Spring+AspectJ(基于注解:通过AspectJ execution表达式拦截方法)

    定义一个Aspect切面类

    @Aspect    /*切面*/
    @Component
    public class GreetingAspect {
    
        @Around("execution(* com.smart4j.framework.GreetingImpl.*(..))")   /*切点*/
        public Object around(ProceedingJoinPoint pjp) throws Throwable {    /*增强*/
            before();
            Object result = pjp.proceed();
            after();
            return result;
        }
    
        private void before(){
            System.out.println("Before");
        }
        private void after(){
            System.out.println("After");
        }
    }

    注意:类上面标注的Aspect注解表明该类是一个Aspect(其实就是Advisor)。该类无须实现任何的接口,只需定义一个方法(方法叫什么名字都无所谓),在方法上标注Around注解,在注解中使用AspectJ切点表达式。方法的参数中包括一个ProceedingJoinPoint对象,它在AOP中称为Joinpoint(连接点),可以通过该对象获取方法的任何信息,例如,方法名、参数等。

    解析下切点表达式execution(* com.smart4j.framework.GreetingImpl.*(..))

    • execution表示拦截方法,括号中可定义需要匹配的规则。
    • 第一个"*"表示方法的返回值是任意的;
    • 第二个"*"表示匹配该类中的所有方法;
    • (..)表示方法的参数是任意的。

    是不是比正则表达式可读性更强呢?如果想匹配指定的方法,只需将第二个“*”改为指定的方法名即可。

    配置如下

        <!--扫描指定包(将带有Component注解的类自动定义为SpringBean)-->
        <context:component-scan base-package="com.smart4j.framework"/>
        <aop:aspectj-autoproxy proxy-target-class="true"/>

    两行配置就行了,不需要配置大量的代理,更不需要配置大量的切面!proxy-target-class属性,它的值默认是false,默认只能代理接口(使用JDK动态代理),当为true时,才能代理目标类(使用CGLib动态代理)。

    Spring与AspectJ结合功能远远不止这些,我们还可以拦截指定注解的方法。

    2.Spring+AspectJ(基于注解:通过AspectJ @annotation表达式拦截方法)

    注解

    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Tag {
    }

    以上定义一个Tag注解,此注解可标注在方法上,在运行时生效。

    @Aspect    /*切面*/
    @Component
    public class GreetingAspect {
    
        @Around("@annotation(com.smart4j.framework.aspectj.Tag)")   /*切点 - 有Tag标记的Method*/
        public Object around(ProceedingJoinPoint pjp) throws Throwable {    /*增强*/
            before();
            Object result = pjp.proceed();
            after();
            return result;
        }
    
        private void before(){
            System.out.println("Before");
        }
        private void after(){
            System.out.println("After");
        }
    }

    直接将Tag注解定义在想要拦截的方法上

    @Component
    public class GreetingImpl implements Greeting {
        @Tag   /*AspectJ 注解*/
        @Override
        public void sayHello(String name) {
            System.out.println("Hello! "+name);
        }
    }

    在以上实例中只有一个方法,如果有多个方法,我们只想拦截其中的某一些时,这种解决方案会更加有价值

    除了Around注解外,其实还有几个相关的注解,稍微归纳一下:

    • Before - 前置增强
    • After - 后置增强
    • Around - 环绕增强
    • AfterThrowing - 抛出增强
    • DeclareParents - 引入增强

    此外还有一个AfterReturning(返回后增强),也可理解为Finally增强,相当于finally语句,它是在方法结束后执行的,也就是说,它比After晚一些。

    3.Spring+AspectJ(引入增强)

    为了实现基于AspectJ的引入增强,我们同样需要定义一个Aspect类:

    @Aspect    /*切面*/
    @Component
    public class GreetingAspect {
        /*引入增强*/
        @DeclareParents(value = "com.smart4j.framework.GreetingImpl",defaultImpl = ApologyImpl.class)
        private Apology apology;
    }

    在Aspect类中定义一个需要引入增强的接口,它也就是运行时需要动态实现的接口。在这个接口上标注了DeclareParents注解,该注解有两个属性:

    • Value - 目标类;
    • defaultImpl - 引入接口的默认实现类。

    我们只需要对引入的接口提供一个默认实现类即可完成增强:

    public class ApologyImpl implements Apology {
        @Override
        public void saySorry(String name) {
            System.out.println("Sorry! " + name);
        }
    }

    运行

            ApplicationContext context = new ClassPathXmlApplicationContext("/spring.xml");  //获取Spring Context
            GreetingImpl greeting = (GreetingImpl) context.getBean("greetingImpl");  //从Context中根据id获取Bean对象(自动扫描的id为首字母小写的类名)
            greeting.sayHello("jack");
    
            Apology apology = (Apology) greeting;   //将目标类增强向上转型为Apology接口(这是引入增强给我们带来的特性,也是"接口动态实现"功能)
            apology.saySorry("jack");

    从SpringApplicationContext中获取greetingImpl对象(其实是个代理对象),可转型为自己静态实现的接口Greeting,也可转型为自己动态实现的接口Apology,切换起来非常方便。

    使用AspectJ的引入增强比原来的SpringAOP的引入增强更加方便了,而且还可面向接口编程(以前只能面向实现类)。

    这一切已经非常强大并且非常灵活了,但仍然还是由用户不能尝试这些特性,因为他们还在使用JDK1.4(根本就没有注解这个东西),怎么办呢?SpringAOP为那些遗留系统也考虑到了。

    3.Spring+AspectJ(基于配置)

    除了使用Aspect注解来定义切面之外,SpringAOP也提供了基于配置的方式来定义切面类:

        <!--AspectJ - 基于配置-->
        <bean id="greetingImpl" class="com.smart4j.framework.GreetingImpl"/>
        <bean id="greetingAspect" class="com.smart4j.framework.aspectj.GreetingAspect"/>
    
        <aop:config>
            <aop:aspect ref="greetingAspect">
                <aop:around method="around" pointcut="execution(* com.smart4j.framework.GreetingImpl.*(..))"/>
            </aop:aspect>
        </aop:config>

    使用<aop:config>元素来进行AOP配置,在其子元素中配置切面,包括增强类型、目标方法、切点等信息。

    无论用户是不能使用注解,还是不愿意使用注解,SpringAOP都能提供全方位的服务。

    AOP思维导图

    各类增强类型所对应的解决方案

    SpringAOP整体架构UML类图

    源码

  • 相关阅读:
    github使用技巧
    转载---linux运维相关
    session 测试用例详解
    php中使用linux命令四大步骤
    Thinkphp常用的方法和技巧(转)
    转学步园:jquery offset
    jquery冒泡及阻止
    nginx搭建流媒体服务器的方法详解
    SetTimeOut jquery的作用
    再也不要说,jquery动画呆板了
  • 原文地址:https://www.cnblogs.com/aeolian/p/9870805.html
Copyright © 2020-2023  润新知