java之动态代理源码分析

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,194评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,058评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,780评论 0 346
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,388评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,430评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,764评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,907评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,679评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,122评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,459评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,605评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,270评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,867评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,734评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,961评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,297评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,472评论 2 348

推荐阅读更多精彩内容

  • 地点:北京 职位:内容运营(一名) 活动运营(一名) 公司:比邻 职位描述: (内容运营)1,从内容层面...
    晚甘侯阅读 503评论 1 3
  • 本周读的兴起多读了一些,完成了Part2的章节,对以下观点有一些共鸣记录如下: 刘炽平回忆说:我那时粗粗算了一下,...
    蛋壳记忆阅读 251评论 0 1
  • “快乐的日子使人睿智。” --- 约翰•梅斯菲尔德 第一次读到英国桂冠诗人梅斯菲尔德的这行诗时,我感到十分震惊。他...
    纹叶_阅读 233评论 0 0