zoukankan      html  css  js  c++  java
  • Spring AOP

    通俗解释

    有A,B,C三个方法,但是在调用每一个方法之前,要求打印一个日志:某一个方法被开始调用了!

    在调用每个方法之后,也要求打印日志:某个方法被调用完了!

    一般人会在每一个方法的开始和结尾部分都会添加一句日志打印吧,这样做如果方法多了,就会有很多重复的代码,显得很麻烦,这时候有人会想到,为什么不把打印日志这个功能封装一下,然后让它能在指定的地方(比如执行方法前,或者执行方法后)自动的去调用呢?如果可以的话,业务功能代码中就不会掺杂这一下其他的代码,所以AOP就是做了这一类的工作,比如,日志输出,事务控制,异常的处理等。。

    如果把AOP当做成给我们写的“业务功能”增添一些特效,就会有这么几个问题:

    1.我们要制作哪些特效

    2.这些特效使用在什么地方

    3.这些特效什么时候来使用

    通知(Advice)
      就是你想要的功能,也就是上面说的 安全,事物,日志等。你给先定义好,然后在想用的地方用一下。

    连接点(JoinPoint)
      这个更好解释了,就是spring允许你使用通知的地方,那可真就多了,基本每个方法的前,后(两者都有也行),或抛出异常时都可以是连接点,spring只支持方法连接点.其他如aspectJ还可以让你在构造器或属性注入时都行,不过那不是咱关注的,只要记住,和方法有关的前前后后(抛出异常),都是连接点。

    切入点(Pointcut)

      上面说的连接点的基础上,来定义切入点,你的一个类里,有15个方法,那就有几十个连接点了对把,但是你并不想在所有方法附近都使用通知(使用叫织入,以后再说),你只想让其中的几个,在调用这几个方法之前,之后或者抛出异常时干点什么,那么就用切点来定义这几个方法,让切点来筛选连接点,选中那几个你想要的方法。

    切面(Aspect)
      切面是通知和切入点的结合。现在发现了吧,没连接点什么事情,连接点就是为了让你好理解切点,搞出来的,明白这个概念就行了。通知说明了干什么和什么时候干(什么时候通过方法名中的before,after,around等就能知道),而切入点说明了在哪干(指定到底是哪个方法),这就是一个完整的切面定义。

    引入(introduction)
      允许我们向现有的类添加新方法属性。这不就是把切面(也就是新方法属性:通知定义的)用到目标类中吗

    目标(target)
      引入中所提到的目标类,也就是要被通知的对象,也就是真正的业务逻辑,他可以在毫不知情的情况下,被咱们织入切面。而自己专注于业务本身的逻辑。

    代理(proxy)
      怎么实现整套aop机制的,都是通过代理,这个一会给细说。

    织入(weaving)
      把切面应用到目标对象来创建新的代理对象的过程。有3种方式,spring采用的是运行时,为什么是运行时,后面解释。

      关键就是:切点定义了哪些连接点会得到通知。

    静态代理:由程序员创建或特定工具自动生成源代码,再对其编译。在程序运行前,代理类的.class文件就已经存在了。
    动态代理:在程序运行时,运用反射机制动态创建而成,无需手动编写代码。动态代理不仅简化了编程工作,而且提高了软件系统的可扩展性,因为Java反射机制可以生成任意类型的动态代理类。


    AOP的实现原理

    AOP的实现关键在于AOP框架自动创建的AOP代理。AOP代理主要分为两大类:
    静态代理:使用AOP框架提供的命令进行编译,从而在编译阶段就可以生成AOP代理类,因此也称为编译时增强;静态代理一Aspectj为代表。
    动态代理:在运行时借助于JDK动态代理,CGLIB等在内存中临时生成AOP动态代理类,因此也被称为运行时增强,Spring AOP用的就是动态代理。

    AOP分为静态AOP和动态AOP。静态AOP是指AspectJ实现的AOP,他是将切面代码直接编译到Java类文件中。动态AOP是指将切面代码进行动态织入实现的AOP。Spring的AOP为动态AOP,实现的技术为:JDK动态代理  CGLIB(动态字节码增强技术)尽管实现技术不一样,但都是基于代理模式,都是生成一个代理对象。

    JDK动态代理

    a. JDK动态代理是面向接口的,必须提供一个委托类和代理类都要实现的接口,只有接口中的方法才能够被代理。
    b. JDK动态代理的实现主要使用java.lang.reflect包里的Proxy类和InvocationHandler接口。

    InvocationHandler接口:

    每一个动态代理类都必须要实现InvocationHandler这个接口,并且每个代理类的实例都关联到了一个handler,当我们通过代理对象调用一个方法的时候,这个方法的调用就会被转发为由InvocationHandler这个接口的 invoke 方法来进行调用。同时在invoke的方法里 我们可以对被代理对象的方法调用做增强处理(如添加事务、日志、权限验证等操作)。我们来看看InvocationHandler这个接口的唯一一个方法 invoke 方法:

    public interface InvocationHandler { 
         public Object invoke(Object proxy,Method method,Object[] args) throws Throwable; 
    }

    参数说明
    Object proxy:指被代理的对象。
    Method method:要调用的方法。(指代的是我们所要调用代理对象的某个方法的Method对象)
    Object[] args:方法调用时所需要的参数。(指代的是调用真实对象某个方法时接受的参数)

    可以将InvocationHandler接口的子类想象成一个代理的最终操作类,替换掉ProxySubject。 

    Proxy类:
    Proxy类是专门完成代理的操作类,可以通过此类为一个或多个接口动态地生成实现类,此类提供了如下的操作方法:

    public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException 

    参数说明
    ClassLoader loader:类加载器
    Class<?>[] interfaces:得到全部的接口
    InvocationHandler h:得到InvocationHandler接口的子类实例

    JDK动态代理示例:

    定义一个业务接口IUserService,如下:

    package com.spring.aop;
    
    public interface IUserService {
        //添加用户
        public void addUser();
        //删除用户
        public void deleteUser();
    }

    一个简单的实现类UserServiceImpl,如下:

    package com.spring.aop;
    
    public class UserServiceImpl implements IUserService{
        
        public void addUser(){
            System.out.println("新增了一个用户!");
        }
        
        public void deleteUser(){
            System.out.println("删除了一个用户!");
        }
    }

    现在我们要实现的是,在addUser和deleteUser之前和之后分别动态植入处理。
    JDK动态代理主要用到java.lang.reflect包中的两个类:Proxy和InvocationHandler。
    InvocationHandler是一个接口,通过实现该接口定义横切逻辑,并通过反射机制调用目标类的代码,动态的将横切逻辑和业务逻辑编织在一起。
    Proxy利用InvocationHandler动态创建一个符合某一接口的实例,生成目标类的代理对象。
    如下,我们创建一个InvocationHandler实例DynamicProxy:(当执行动态代理对象里的目标方法时,实际上会替换成调用DynamicProxy的invoke方法)

    package com.spring.aop;
    
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    
    public class DynamicProxy implements InvocationHandler{
        
        //被代理对象(就是要给这个目标类创建代理对象)
        private Object target;
        
        //传递代理目标的实例,因为代理处理器需要,也可以用set等方法。
        public DynamicProxy(Object target){
            this.target=target;
        }
        
        /**
         * 覆盖java.lang.reflect.InvocationHandler的方法invoke()进行织入(增强)的操作。
         * 这个方法是给代理对象调用的,留心的是内部的method调用的对象是目标对象,可别写错。
         * 参数说明:
         * proxy是生成的代理对象,method是代理的方法,args是方法接收的参数
         */
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable{
            //目标方法之前执行
            System.out.println("do sth Before...");
            //通过反射机制来调用目标类方法
            Object result = method.invoke(target, args);
            //目标方法之后执行
            System.out.println("do sth After...
    ");
            return result;
        }
    }

     下面是测试:

    package com.spring.aop;
    
    //用java.lang.reflect.Proxy.newProxyInstance()方法创建动态实例来调用代理实例的方法
    import java.lang.reflect.Proxy;
    
    public class DynamicTest {
        
        public static void main(String[] args){
            //希望被代理的目标业务类
            IUserService target = new UserServiceImpl();
            //将目标类和横切类编织在一起
            DynamicProxy handler= new DynamicProxy(target);
            //创建代理实例,它可以看作是要代理的目标业务类的加多了横切代码(方法)的一个子类
            //创建代理实例(使用Proxy类和自定义的调用处理逻辑(handler)来生成一个代理对象)
            IUserService proxy = (IUserService)Proxy.newProxyInstance(
                    target.getClass().getClassLoader(),//目标类的类加载器
                    target.getClass().getInterfaces(), //目标类的接口
                    handler); //横切类
            proxy.addUser();
            proxy.deleteUser();
        }
    }

    说明:上面的代码完成业务类代码和横切代码的编制工作,并生成了代理实例,newProxyInstance方法的第一个参数为类加载器,第二个参数为目标类所实现的一组接口,第三个参数是整合了业务逻辑和横切逻辑的编织器对象。

    每一个动态代理实例的调用都要通过InvocationHandler接口的handler(调用处理器)来调用,动态代理不做任何执行操作,只是在创建动态代理时,把要实现的接口和handler关联,动态代理要帮助被代理执行的任务,要转交给handler来执行。其实就是调用invoke方法。(可以看到执行代理实例的addUser()和deleteUser()方法时执行的是DynamicProxy的invoke()方法。)

    基本流程:用Proxy类创建目标类的动态代理,创建时需要指定一个自己实现InvocationHandler接口的回调类的对象,这个回调类中有一个invoke()用于拦截对目标类各个方法的调用。创建好代理后就可以直接在代理上调用目标对象的各个方法。

    实现动态代理步骤:
    A. 创建一个实现接口InvocationHandler的类,他必须实现invoke方法。
    B.创建被代理的类以及接口。
    C.通过Proxy的静态方法newProxyInstance(ClassLoader loader, Class<?>[]interfaces, InvocationHandler handler)创建一个代理。
    D.通过代理调用方法。

    使用JDK动态代理有一个很大的限制,就是它要求目标类必须实现了对应方法的接口,它只能为接口创建代理实例我们在上文测试类中的Proxy的newProxyInstance方法中可以看到,该方法第二个参数便是目标类的接口。如果该类没有实现接口,这就要靠cglib动态代理了。

    CGLIB动态代理

    CGLib采用非常底层的字节码技术,可以为一个类创建一个子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,并顺势植入横切逻辑。

    字节码生成技术实现AOP,其实就是继承被代理对象,然后Override需要被代理的方法,在覆盖该方法时,自然是可以插入我们自己的代码的。因为需要Override被代理对象的方法,所以自然用CGLIB技术实现AOP时,就必须要求需要被代理的方法不能是final方法,因为final方法不能被子类覆盖。

    a.使用CGLIB动态代理不要求必须有接口,生成的代理对象是目标对象的子类对象,所以需要代理的方法不能是private或者final或者static的。
    b.使用CGLIB动态代理需要有对cglib的jar包依赖(导入asm.jar和cglib-nodep-2.1_3.jar)

    CGLibProxy与JDKProxy的代理机制基本类似,只是其动态代理的代理对象并非某个接口的实现,而是针对目标类扩展的子类。换句话说JDKProxy返回动态代理类,是目标类所实现接口的另一个实现版本,它实现了对目标类的代理(如同UserDAOProxy与UserDAOImp的关系),而CGLibProxy返回的动态代理类,则是目标代理类的一个子类(代理类扩展了UserDaoImpl类)

    cglib 代理特点:
    CGLIB 是针对类来实现代理,它的原理是对指定的目标类生成一个子类,并覆盖其中方法。因为采用的是继承,所以不能对 finall 类进行继承

    我们使用CGLIB实现上面的例子:

    代理的最终操作类:

    package com.spring.aop;
    
    import java.lang.reflect.Method;
    
    import net.sf.cglib.proxy.Enhancer;
    import net.sf.cglib.proxy.MethodInterceptor;
    import net.sf.cglib.proxy.MethodProxy;
    
    public class CglibProxy implements MethodInterceptor{
        
        //增强器,动态代码生成器
        Enhancer enhancer = new Enhancer();
        
        /**
         * 创建代理对象
         * @param clazz
         * @return 返回代理对象
         */
        public Object getProxy(Class clazz){
            //设置父类,也就是被代理的类(目标类)
            enhancer.setSuperclass(clazz);
            //设置回调(在调用父类方法时,回调this.intercept())
            enhancer.setCallback(this);
            //通过字节码技术动态创建子类实例(动态扩展了UserServiceImpl类)
            return enhancer.create();
        }
        
        /**
         * 拦截方法:在代理实例上拦截并处理目标方法的调用,返回结果
         * obj:目标对象代理的实例;
         * method:目标对象调用父类方法的method实例;
         * args:调用父类方法传递参数;
         * proxy:代理的方法去调用目标方法
         */
        public Object intercept(Object obj,Method method,Object[] args,MethodProxy proxy) 
            throws Throwable{
            
            System.out.println("--------测试intercept方法的四个参数的含义-----------");
            System.out.println("obj:"+obj.getClass());
            System.out.println("method:"+method.getName());
            System.out.println("proxy:"+proxy.getSuperName());
            if(args!=null&&args.length>0){
                for(Object value : args){
                    System.out.println("args:"+value);
                }
            }
    
            //目标方法之前执行
            System.out.println("do sth Before...");
            //目标方法调用
            //通过代理类实例调用父类的方法,即是目标业务类方法的调用
            Object result = proxy.invokeSuper(obj, args);
            //目标方法之后执行
            System.out.println("do sth After...
    ");
            return result;
        }
    }

    测试类:

    package com.spring.aop;
    
    public class CglibProxyTest {
        
        public static void main(String[] args){
            CglibProxy proxy=new CglibProxy();
            //通过java.lang.reflect.Proxy的getProxy()动态生成目标业务类的子类,即是代理类,再由此得到代理实例
            //通过动态生成子类的方式创建代理类
            IUserService target=(IUserService)proxy.getProxy(UserServiceImpl.class);
            target.addUser();
            target.deleteUser();
        }
    }

    基本流程:需要自己写代理类,它实现MethodInterceptor接口,有一个intercept()回调方法用于拦截对目标方法的调用,里面使用methodProxy来调用目标方法。创建代理对象要用Enhance类,用它设置好代理的目标类、由intercept()回调的代理类实例、最后用create()创建并返回代理实例。

    输出:

    我们看到达到了同样的效果。它的原理是生成一个父类enhancer.setSuperclass(clazz)的子类enhancer.create(),然后对父类的方法进行拦截enhancer.setCallback(this). 对父类的方法进行覆盖,所以父类方法不能是final的。

    总结:
      (1).通过输出可以看出,最终调用的是com.spring.aop.UserServiceImpl的子类(也是代理类)com.spring.aop.UserServiceImpl$$EnhancerByCGLIB$$43831205的方法。
      (2). private,final和static修饰的方法不能被代理。

    注意:
      (1).CGLIB是通过实现目标类的子类来实现代理,不需要定义接口。
      (2).生成代理对象使用最多的是通过Enhancer和继承了Callback接口的MethodInterceptor接口来生成代理对象,设置callback对象的作用是当调用代理对象方法的时候会交给callback对象的来处理。
      (3).创建子类对象是通过使用Enhancer类的对象,通过设置enhancer.setSuperClass(Class class)和enhancer.setCallback(Callback callback)来创建代理对象。

    解释MethodInterceptor接口的intercept方法:

    Object intercept(Object var1, Method var2, Object[] var3, MethodProxy var4) throws Throwable;

    参数说明:Object var1代表的是子类代理对象,Method var2代表的是要调用的方法反射对象,第三个参数是传递给调用方法的参数,前三个参数和JDK的InvocationHandler接口的invoke方法中参数含义是一样的,第四个参数MethodProxy对象是cglib生成的用来代替method对象的,使用此对象会比jdk的method对象的效率要高。

    如果使用method对象来调用目标对象的方法: method.invoke(var1, var3),则会陷入无限递归循环中, 因为此时的目标对象是目标类的子代理类对象。

    MethodProxy类提供了两个invoke方法:

    public Object invokeSuper(Object obj, Object[] args) throws Throwable;
    public Object invoke(Object obj, Object[] args) throws Throwable;

    注意此时应该使用invokeSuper()方法,顾名思义调用的是父类的方法,若使用invoke方法,则需要提供一个目标类对象,但我们只有目标类子类代理对象,所以会陷入无限递归循环中。

    CGLIB所创建的动态代理对象的性能比JDK所创建的动态代理对象的性能高很多,但创建动态代理对象时比JDK创建动态代理对象要花费更长的时间。

    JDK代理和CGLIB代理的总结(生成代理对象的前提是有AOP切入)
    (1)、如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP。 如果就是单纯的用IOC生成一个对象,也没有AOP的切入不会生成代理的,只会NEW一个实例,给Spring的Bean工厂。
    (2)、如果目标对象实现了接口,可以强制使用CGLIB实现AOP
    如何强制使用CGLIB实现AOP
    * 添加CGLIB库
    * 在spring配置文件中加入<aop:aspectj-autoproxy proxy-target-class="true"/>就能强制使用
    (3)、如果目标对象没有实现了接口,必须采用CGLIB库,spring会自动在JDK动态代理和CGLIB之间转换(没有实现接口的就用CGLIB代理,使用了接口的类就用JDK动态代理)

    JDK动态代理和CGLIB字节码生成的区别:
    (1)、JDK动态代理只能对实现了接口的类生成代理,而不能针对类。CGLIB是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法。因为是继承,所以该类或方法最好不要声明成final。
    (2)、JDK代理是不需要依赖第三方的库,只要JDK环境就可以进行代理,它有几个要求
    * 实现InvocationHandler;
    * 使用Proxy.newProxyInstance产生代理对象;
    * 被代理的对象必须要实现接口;
    CGLib 必须依赖于CGLib的类库,但是它需要类来实现任何接口代理的是指定的类生成一个子类,覆盖其中的方法,是一种继承。
    (3)、jdk的核心是实现InvocationHandler接口,使用invoke()方法进行面向切面的处理,调用相应的通知。cglib的核心是实现MethodInterceptor接口,使用intercept()方法进行面向切面的处理,调用相应的通知。

    五、小结

         AOP 广泛应用于处理一些具有横切性质的系统级服务,AOP 的出现是对 OOP 的良好补充,它使得开发者能用更优雅的方式处理具有横切性质的服务。不管是哪种 AOP 实现,不论是 AspectJ、还是 Spring AOP,它们都需要动态地生成一个 AOP 代理类,区别只是生成 AOP 代理类的时机不同:AspectJ 采用编译时生成 AOP 代理类,因此具有更好的性能,但需要使用特定的编译器进行处理;而 Spring AOP 则采用运行时生成 AOP 代理类,因此无需使用特定编译器进行处理。由于 Spring AOP 需要在每次运行时生成 AOP 代理,因此性能略差一些。

    原文链接:https://www.cnblogs.com/xiaoxi/p/5945707.html

                      https://blog.csdn.net/qq_32317661/article/details/82878679

                      https://www.jianshu.com/p/830e799e099b

                    

  • 相关阅读:
    错题
    URL和URI区别
    适配器
    JAVA 反射机制
    JAVA 面试题
    JAVA 继承
    多态 JAVA
    Java面向对象编辑
    [LeetCode] Merge k Sorted Lists
    [LeetCode] Valid Palindrome
  • 原文地址:https://www.cnblogs.com/dingpeng9055/p/11571619.html
Copyright © 2011-2022 走看看