美文网首页JDK源码分析
java之动态代理源码分析

java之动态代理源码分析

作者: Sophie12138 | 来源:发表于2018-07-16 17:04 被阅读23次

    java的动态代理机制是在运行期间为目标对象生成一个代理对象,而将自己格外需要处理的业务逻辑进行“插入”,以达到运行自己业务功能的目的,最典型的运用为Spring AOP技术——权限过滤、日志记录等等。

    说到java的动态代理机制,就需要说到其最底层的技术——设计模式之代理模式。


    image.png

    代理模式的核心为将“请求”进行转发,不直接和SubjectImpl发生交互,在交互前,先和SubjectProxy代理类进行交互,再由代理类决定,本次交互是否转发到SubjectImpl。

    如上图所示编写代码:

    ISubject.java
    public interface ISubject {
        void request();
    }
    
    
    SubjectImpl.java
    public class SubjectImpl implements ISubject {
        @Override
        public void request() {
            System.out.println("Hello world!");
        }
    }
    
    SubjectProxy.java
    public class SubjectProxy implements ISubject {
        //要代理的真实对象
        private ISubject subject;
        //给代理的真实对象赋值
        public SubjectProxy(ISubject subject) {
            this.subject = subject;
        }
        
        @Override
        public void request() {
            //添加逻辑业务1
            subject.request();
            //添加逻辑业务2
        }
    }
    
    
    Client .java
    public class Client {
        public static void main(String[] args) {
            ISubject realSubject = new SubjectImpl();
            ISubject finalSubject = new SubjectProxy(realSubject);
            finalSubject.request();
        }
    }
    

    SubjectImpl和SubjectProxy都实现了相同的接口ISubject,而SubjectProxy内部持有SubjectImpl的引用。当Client通过request()请求服务的时候,SubjectProxy将转发该请求给SubjectImpl。从这个角度来说SubjectProxy反而有多此一举之嫌。不过,SubjectProxy的作用不止限于请求的转发,更多的时候是对请求添加更多的逻辑业务。

    在将请求转发给被代理对象SubjectImpl之前或者之后,都可以根据情况插入其他处理逻辑,比如在转发之前记录方法执行开始时间,在转发之后记录结束时间,这样就能够对SubjectImpl的request()执行的时间进行检测。或者,可以只转发之后对SubjectImpl的request()方法返回结果进行覆盖,返回不同的值。甚至,可以不做请求转发,这样,就不会有SubjectImpl的访问发生。

    回到正题,何为java动态代理?有什么用处?

    java动态代理功能,可以使你在不修改源代码的情况下,动态的去修改你的业务逻辑,在方法执行前后,做你想做的功能。使其作为方法的增强。

    再次上图:


    image.png

    根据上面的基础设计模式代码改为java动态代理代码:

    ISubject.java
    public interface ISubject {
        void request();
    }
    
    SubjectImpl.java
    public class SubjectImpl implements ISubject {
        @Override
        public void request() {
            System.out.println("Hello world!");
        }
    }
    
    public class SubjectProxy implements InvocationHandler { 
        //要代理的真实对象 
        private Object subject; 
        //给代理的真实对象赋值 
        public SubjectProxy(Object subject) { 
            this.subject = subject; 
        } 
        
        @Override 
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 
            System.out.println("before control"); 
            System.out.println("Method " + method); 
            method.invoke(subject, args); 
            System.out.println("after control"); return null; 
        } 
    }
    
    Client .java
    public class Client { 
        public static void main(String[] args) {
            ISubject realSubject = new SubjectImpl(); 
            InvocationHandler handler = new SubjectProxy(realSubject);
            //其中第一个参数为classload类加载器,其作用为通过此加载器参数进行寻找,
            //若存在相应的代理类,则直接返回,若不存在再新建。
            ISubject subject = (ISubject) 
            Proxy.newProxyInstance(handler.getClass().getClassLoader(), 
                                    realSubject.getClass().getInterfaces(), handler); 
                                    System.out.println(subject.getClass().getName()); 
            subject.request(); 
        } 
    }
    

    方法newProxyInstance中主要是验证相应传入参数,生成代理类Class(注:
    final Constructor<?> cons = cl.getConstructor(constructorParams);为什么需要此代码是因为,采用反射,因为代理类的构造函数都为有参数,所以要对构造函数的参数,进行类型传入,然后再cons.newInstance(new Object[]{h});返回实例对象。)。

    public static Object newProxyInstance(ClassLoader loader, 
    Class<?>[] interfaces, InvocationHandler h) 
    throws IllegalArgumentException { 
        Objects.requireNonNull(h); 
        final Class<?>[] intfs = interfaces.clone(); 
        final SecurityManager sm = System.getSecurityManager(); 
        if (sm != null) { 
            /* * 验证一些参数 */
            checkProxyAccess(Reflection.getCallerClass(), loader, intfs); 
        } 
        /* * 查找或生成指定的代理类 */ 
            Class<?> cl = getProxyClass0(loader, intfs); 
        try { if (sm != null) {
            /* * 验证是否有权限去生成代理类 */
            checkNewProxyPermission(Reflection.getCallerClass(), cl); 
        }
        /* * 设置生成代理类的构造函数参数为:InvocationHandler类型 */
        final Constructor<?> cons = cl.getConstructor(constructorParams);
        final InvocationHandler ih = h; 
        if (!Modifier.isPublic(cl.getModifiers())) { 
            AccessController.doPrivileged(new PrivilegedAction<Void>() { 
                public Void run() { 
                cons.setAccessible(true); 
                return null; }}); 
        } 
        return cons.newInstance(new Object[]{h}); 
        } catch (IllegalAccessException|InstantiationException e) { 
            throw new InternalError(e.toString(), e); 
        } catch (InvocationTargetException e) { 
            Throwable t = e.getCause(); 
            if (t instanceof RuntimeException) { 
                throw (RuntimeException) t; 
            } else { 
                throw new InternalError(t.toString(), t); 
            } 
        } catch (NoSuchMethodException e) { 
            throw new InternalError(e.toString(), e); 
        } 
    }
    
    
    /** * 生成代理类 */ 
    private static Class<?> getProxyClass0(ClassLoader loader, Class<?>... interfaces) { 
        if (interfaces.length > 65535) { 
            throw new IllegalArgumentException("interface limit exceeded"); 
        } 
        // 如果在缓存中存在该代理类,则直接返回,如果无,则通过ProxyClassFactory生成
        return proxyClassCache.get(loader, interfaces); 
    }
    

    静态代理与动态代理的区别?

    • 静态代理即为普通的设计模式之代理模式,之所以为静态,是因为每次有新的需求变更,代理类都得重新修改,每个Subject需要对应一个代理类,以此达到代理的效果。


      image.png
    • 动态代理,即在有多个Subject使用此代理时,可以不修改代理类,进行“动态”的修改。例如, Hibernate Lazy Initialization(延迟加载)。

    懒加载为Hibernate中比较常用的特性之一,下面我们详细来了解下懒加载的原理和注意事项

    Load()方法的懒加载原理
    在Hibernate中,查询方法有两个,分别是get()和load(),这两种方法的不同就是load()拥有懒加载的特性。Load()方法就是在查询某一条数据的时候并不会直接将这条数据以指定对象的形式来返回,而是在你真正需要使用该对象里面的一些属性的时候才会去数据库访问并得到数据。他的好处就是可以减少程序本身因为与数据库频繁的交互造成的处理速度缓慢。

    以一个Person类做例子,我们写一个查询的方法如下:

    public static void query(int id){
           Session session=null;
            try{
                session=HibernateUtil.getSession();
                Person person=(Person) session.load(Person.class, id);
                //System.out.println(person.getName());
            }catch(HibernateExceptionex){
                ex.printStackTrace();
            }finally{
                if(session!=null){
                   session.close();
                }
            }
        }
    
    

    然后在Hibernate配置文件中添加以下属性

    <property name="hibernate.show_sql">true</property>
    

    这条属性的作用为将Hibernate运行时产生的每一条SQL语句都打印出来

    运行上述方法后,我们并没有看到Hibernate打印任何查询语句,这时我们可以将注释的语句重新调回来,让他打印查询到的person的name。这时我们可以看到Hibernate产生的查询语句并看到person的name属性。这就是懒加载了。

    那么在Hibernate懒加载的时候,返回的对象是空的吗?答案是否定的,我们可以通过打印person.getClass()方法来验证,打印出来的结果并不是null,而是一个Person后面加了一堆很奇怪的字符的类。可以肯定的是懒加载的对象并不是空,而且这个对象的类型不是Person类。那么他到底是什么呢?

    其实这个兑现我们管它叫做代理对象,而这个对象所属的类是Person类的子类,是Hibernate自动实现的一个子类。这个子类的特点是:当你访问person对象的某一个属性的时候,他会自动查询数据库中对应这个对象的数据并返回,这就是为什么在创建对象关系映射的时候要求实体类不能够为final类型的原因了。

    但是这个对象是有一个生命周期的,我们可以改写上述方法如下:

    public static Person query(int id){
           Session session=null;
           Person person=null;
            try{
                session=HibernateUtil.getSession();
                person=(Person)session.load(Person.class, id);
                //System.out.println(person.getName());
            }catch(HibernateExceptionex){
                ex.printStackTrace();
            }finally{
                if(session!=null){
                   session.close();
                }
            }
            return person;
        }
    

    调用这个方法并返回查询到的代理对象,我们可以在返回该对象后打印该对象的name属性,这时会抛出一个org.hibernate.LazyInitializationException异常

    这就是懒加载不能初始化异常,这就说明了一件事,懒加载的时候如果想通过代理对象查询数据库,需要在该session关闭以前才可以。但如果一定要在session关闭以后再使用代理对象的话,Hibernate中定义了一个初始化代理对象的方法initialize(),通过该方法即可将代理对象初始化。

    注:在使用代理对象的getId()方法和getClass()方法的时候,并不会抛出不能初始化异常,因为这两个属性并不用查询数据库。

    懒加载可以用于关系映射和集合属性的操作,而且懒加载是可以关闭并打开的,下面我们会根据不同的情况分析一下懒加载。

    一对一的懒加载分析
    一对一的懒加载并不常用,因为懒加载的目的是为了减少与数据库的交互,从而提高执行效率,而在一对一关系中,主表中的每一条数据只对应从表的一条数据库,就算都查询也不会增加多少交互的成本,而且主表不能有contrained=true,所以主表是不能懒加载的。(从表可以有)

    注:当fetch设置为join时,懒加载就会失效。因为fetch的作用是抓取方式,他有两个值分别问select和join,默认值为select。即在设为join时,他会直接将从表信息以join方式查询到而不是再次使用select查询,这样导致了懒加载的失效。

    一对多和多对多的懒加载分析
    与一对一关联不同,一对多和一对多的关联下,主表的每一条属性都会对应从表的多条数据,这个时候懒加载就显得非常有效了。比如一个部门里面有多个员工,如果没有懒加载,每查询这个部门的时候都会查询出多个员工,这会大大增加与数据库交互的成本。所以Hbernate默认的是加入懒加载的。这就是查询集合属性的时候返回的是一个PersistentIndexed*类型对象的原因。该对象其实就是一个代理对象。当然,可以在映射文件中通过将lazy属性设为假来禁用。

    多对一的懒加载分析
    虽然多对一与一对一关系方式相同,但是在Hibernate中多对一时,默认是进行懒加载的。另外有一点需要注意的是懒加载并不会区分集合属性里面是否有值,即使是没有值,他依然会使用懒加载,这也是懒加载不够完善的地方之一。

    懒加载的一些细节扩充
    有的时候,我们在session关闭后仍需要使用懒加载的代理对象来查询数据库,这时我们就需要将代理对象初始化,不过问题是,当我们不能确定初始化后就一定使用该对象的时候怎么办,这样不是又白白浪费了资源吗?我们可以在使用代理对象的方法里面加入一个布尔值参数,这样当我们不需要初始化代理对象的时候只要将布尔参数设为假。但也是有缺点的,因为在调用的时候,尤其是在别人使用的时候,参数越多,方法学习成本就会增加,所以请酌情处理。

    懒加载也可以用于某些简单属性,但是因为实现起来比较复杂,而且效果并不明显,所以并不推荐。

    image.png

    最终为什么可以调用代理对象逻辑,也即是:

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    
            System.out.println("before control");
            System.out.println("Method " + method);
            method.invoke(subject, args);
            System.out.println("after control");
            return null;
        }
    

    是因为真正生成的代理类,反编译后,其中的调用的方法变成了

    public void request() {
      this.invoke(...)
    }
    

    真实的方法直接就被生成了这样,这样就会直接调用invoke()方法,以至于产生代理效果。

    PS:为何java的代理模式需要接口可以在Proxy.java的以下代码找到结论:Proxy.java文件:byte[] proxyClassFile = ProxyGenerator.generateProxyClass( proxyName, interfaces, accessFlags);

    PS:所谓的动态代理就是,入参为实际主题类的接口类型,内部通过接口找到方法体,执行主题接口类型的方法体前后,就可以进行动态的代理方法代码。-2019.1.26

    相关文章

      网友评论

        本文标题:java之动态代理源码分析

        本文链接:https://www.haomeiwen.com/subject/qsbbpftx.html