动态代理和类加载

本节主要复习动态代理和类加载机制。这2个知识点是非常重要的,也是很常见的,可能我们自己用的并不多,但是很多框架中的基础都是它们2个。如果不知道这些知识 那么看那些开源框架的源码就会很吃力。是谓 基础不牢地动山摇

类加载

还是按标准的灵魂5问来学习:

  • 什么是类加载
  • 怎么使用类加载
  • 类加载的优缺点
  • 类加载的原理
  • 类加载的使用场景

什么是类加载
类加载是一种机制是一套流程和动作。我们写的源码其实就是文本文件,如我们写的第一个java程序 HelloWorld.java 。这个就是源文件给人看的。

但是这个文件并不能直接运行, 需要编译成HelloWorld.class 这个是字节码文件 打开里面其实是二进制,但是一般不会显示0101这种而是会以16进制显示。这个给java虚拟机看的。

你要看到话需要对着-java虚拟机规范表才知道代表什么意思。这个也是不能直接在电脑上运行的。它只能运行在JVM中,怎么运行,当我们执行java HelloWorld

这里java 就代表运行虚拟机,后面的HelloWorld就是 我们的字节码,意思就是运行虚拟机 然后在虚拟机上运行HelloWorld 字节码。虚拟机会把字节码 翻译成对应操作系统能识别的机器码就可以在电脑上运行了。
类加载就是:虚拟机通过全限定名(就是路径)把字节码文件读取进内存,怎么读?肯定是IO啊,然后进行必要的验证、准备、解析、初始化等工作 最后转为规定的数据结构存储在内存中方法区,并且生成一个Class对象来描述这个类文件,后续通过这个Class就可以访问到这个类文件。

怎么使用类加载
首先需要搞清楚 类加载器有哪些,并不是所有的 类都是又一种类加载器 去加载的,比如JDK自带的类,我们自己写的类。根据官方文档类加载器有3种:

  1. BootStrap ClassLoder —BootstrapClassLoader引导类加载器,这个是最顶层的类加载器,它主要加载核心类库,JRE中的库 具体就是%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等。这里提一句JDK:java develop kit 就是开发工具包。 JRE:Java Runtime Enviroment是指Java的运行环境。

  2. Extention ClassLoader --ExtClassLoader扩展类加载器,加载我们开发用的到基础库,不懂的可以点进去看看有哪些类具体目录%JRE_HOME%\lib\ext目录下的jar包和class文件。

  3. Appclass ClassLoader—AppClassLoader也称为SystemAppClass 加载当前应用的classpath的所有类。就是你自己写的类。

    BootstrapClassLoader这个类在虚拟机中是C++编写的。不过在Launcher中也有一个BootClassPathHolder 私有的静态内部类
    AppClassLoader和ExtClassLoader 是java编写的在 sun.misc包下 其实是Launcher中的2个静态内部类。
    这3个类我们都改了不了什么,系统的东西。
    不过我们可以自定义自己的类加载器 只要继承ClassLoader就行了。举个栗子:
    步骤也很简单:
    1.写一个类继承自ClassLoader抽象类。
    2.复写它的findClass()方法。
    3.在findClass()方法中调用defineClass()。
    如:

package com.zh.loader;import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;/*** @descriable 自定义苹果 类 加载器*/
public class AppleLoader extends ClassLoader {private static String TAG = "AppleLoader";/**就是.class文件的路径 如果是放在D盘 则 D:/*/private String mDir;public AppleLoader(String dir) {MainLoader.println(TAG, dir);this.mDir = dir;}@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {//这个name是全限定名 什么意思?//其实就是包名加类名。如本类的全限定名就是 com.zh.loader.AppleLoader.class。//本质其实还是路径  因为你编译后的路径会变成 磁盘路径+com/zh/loader/AppleLoader.classMainLoader.println(TAG, name);byte[] data = new byte[1024];InputStream inputStream;ByteArrayOutputStream byteArrayOutputStream;try {File file = new File(mDir, getFileName(name));inputStream = new FileInputStream(file);byteArrayOutputStream = new ByteArrayOutputStream();int len = 0;while ((len = inputStream.read(data)) != -1) {MainLoader.println(TAG, len);byteArrayOutputStream.write(data, 0, len);}//这部分就是把Apple.class文件从磁盘读取出来 byte[] buff = byteArrayOutputStream.toByteArray();inputStream.close();byteArrayOutputStream.close();MainLoader.println(TAG, buff.length);//defineClass是原生方法:可以把符合虚拟机规则的 byte[]数组 转化成为描述类文件的Class对象return defineClass(name, buff, 0, buff.length);} catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();}return super.findClass(name);}// 获取要加载 的class文件名private String getFileName(String name) {int index = name.lastIndexOf('.');if (index == -1) {return name + ".class";} else {return name.substring(index + 1) + ".class";}}
}

上面的注释已经很清楚了,关键点就是全限定名的理解。加载了类有什么用?什么都看不见啊 所以还需要进行测试:
先编写一个Apple.java 编译成Apple.class,然后把Apple.class放到电脑E盘根目录下(随便你放到哪)。

package com.zh.loader;public class Apple {public void describe() {System.out.println("我是苹果");}
}

这样我们就从APK外部加载了一个类进我们的虚拟机,如何用这个类?只能通过反射如:

public static void main(String[] args) {try {println(TAG, "测试自定义类加载");AppleLoader loader = new AppleLoader("E:\\");Class<?> aClass = loader.loadClass("com.zh.loader.Apple");Method[] declaredMethods = aClass.getDeclaredMethods();declaredMethods[0].invoke(aClass.newInstance());} catch (Exception e) {println(TAG, e);}}

运行结果:


可以看到我们的工程中没Apple类,但是却可以使用。这个Apple类我们可以放到本地,也可以放到网络上,用的时候再 下载过来。这个就是热修复和插件化的基础。还可以动态替换掉类。

类加载的优缺点
优点:

  1. 动态的替换和增加额外的类。
  2. 热修复热更新插件化

缺点:

  1. 需要反射使用很慢
  2. 非常规手段有风险

类加载的原理
原理就是多啦,涉及了虚拟机的类加载机制。主要的源码就是ClassLoader中的loadClass()方法。它涉及双亲加载机制。源码如下:

protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{synchronized (getClassLoadingLock(name)) {// First, check if the class has already been loadedClass<?> c = findLoadedClass(name);if (c == null) {long t0 = System.nanoTime();try {if (parent != null) {c = parent.loadClass(name, false);} else {c = findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {// ClassNotFoundException thrown if class not found// from the non-null parent class loader}if (c == null) {// If still not found, then invoke findClass in order// to find the class.long t1 = System.nanoTime();c = findClass(name);// this is the defining class loader; record the statssun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}}if (resolve) {resolveClass(c);}return c;}}

可以看到它是通过递归的方式去加载类,这就是双亲加载机制。
具体解释
类从被加载到虚拟机内存中开始,直到卸载出内存为止,它的整个生命周期包括了:加载、验证、准备、解析、初始化、使用和卸载这7个阶段。其中,验证、准备和解析这三个部分统称为连接(linking)。
resolveClass:就是用来连接的。这个方法给Classloader用来链接一个类,如果这个类已经被链接过了,那么这个方法只做一个简单的返回。否则,这个类将被按照 Java规范中的Execution描述进行链接。等下我们来研究这个resolveClass()方法什么时候才会执行。
比如我们自定义的这个类加载器:
resolve一直都是会是false;所以其实我们调用loadClass时,并不会出发类的初始化。可以验证如下:
我们修改一下Apple类 增加一段静态代码块,

package com.zh.loader;public class Apple {static{System.out.println("我被初始化了");}public void describe() {System.out.println("我是苹果");}
}

测试,先注释 反射调用相关,就只加载

public static void main(String[] args) {try {println(TAG, "测试自定义类加载");AppleLoader loader = new AppleLoader("E:\\");Class<?> aClass = loader.loadClass("com.zh.loader.Apple");
//          Method[] declaredMethods = aClass.getDeclaredMethods();
//          declaredMethods[0].invoke(aClass.newInstance());} catch (Exception e) {println(TAG, e);}}

运行结果:

接着加上反射调用方法:

public static void main(String[] args) {try {println(TAG, "测试自定义类加载");AppleLoader loader = new AppleLoader("E:\\");Class<?> aClass = loader.loadClass("com.zh.loader.Apple");Method[] declaredMethods = aClass.getDeclaredMethods();declaredMethods[0].invoke(aClass.newInstance());} catch (Exception e) {println(TAG, e);}}

运行结果:

类的加载 和类的初始化
这里要搞清楚,类的加载和类的初始化是2个不同的东西。加载只是把类文件读取进内存并生成一个Class对象。初始化 才是运行类中的代码。类中的静态代码。

什么情况下需要开始类加载过程的第一个阶段:“加载”。虚拟机规范中并没强行约束,这点可以交给虚拟机的的具体实现自由把握,但是对于初始化阶段虚拟机规范是严格规定了如下几种情况,如果类未初始化会对类进行初始化。
1.遇到new,getstatic,putstatic,invokestatic这4条指令;
2.初始化一个类的时候,如果发现其父类没有进行过初始化,则先初始化其父类(注意!如果其父类是接口的话,则不要求初始化父类);
3.访问类的静态变量(除常量【被final修辞的静态变量】原因:常量一种特殊的变量,因为编译器把他们当作值(value)而不是域(field)来对待。如果你的代码中用到了常变量(constant variable),编译器并不会生成字节码来从对象中载入域的值,而是直接把这个值插入到字节码中。这是一种很有用的优化,但是如果你需要改变final域的值那么每一块用到那个域的代码都需要重新编译。
4.使用java.lang.reflect包的方法,对垒进行反射调用的时候,如果没有初始化,则先触发初始化
5.虚拟机启动时,定义了main()方法的那个类先初始化

以上情况称为称对一个类进行“主动引用”,除此种情况之外,均不会触发类的初始化,称为“被动引用”
接口的加载过程与类的加载过程稍有不同。接口中不能使用static{}块。当一个接口在初始化时,并不要求其父接口全部都完成了初始化,只有真正在使用到父接口时(例如引用接口中定义的常量)才会初始化。

以下情况会触发类的初始化:

1.遇到new,getstatic,putstatic,invokestatic这4条指令;
2.使用java.lang.reflect包的方法对类进行反射调用;
3.初始化一个类的时候,如果发现其父类没有进行过初始化,则先初始化其父类(注意!如果其父类是接口的话,则不要求初始化父类);

以下情况不会触发类的初始化:
1同类子类引用父类的静态字段,不会导致子类初始化。至于是否会触发子类的加载和验证,取决于虚拟机的具体实现;
2通过数组定义来引用类,也不会触发类的初始化;例如:People[] ps = new People[100];
3.引用一个类的常量也不会触发类的初始化

双亲委派:
前面说过,系统的类加载器有3种,具体的关系:
BootstrapClassLoader(祖父)–>ExtClassLoader(爷爷)–>AppClassLoader(也称为SystemClassLoader)(爸爸)–>自定义类加载器(儿子)
彼此相邻的两个为父子关系,前为父,后为子。
比如现在我们写了一个类Apple,整个加载过程如下:

简单描述一下:
1
一个AppClassLoader查找资源时,先看看缓存是否有,缓存有从缓存中获取,否则委托给父加载器。
2
递归,重复第1部的操作。
3
如果ExtClassLoader也没有加载过,则由Bootstrap ClassLoader出面,它首先查找缓存,如果没有找到的话,就去找自己的规定的路径下,也就是
sun.mic.boot.class下面的路径。找到就返回,没有找到,让子加载器自己去找。
4
Bootstrap ClassLoader如果没有查找成功,则ExtClassLoader自己在java.ext.dirs路径中去查找,查找成功就返回,查找不成功,再向下让子加载器找。
5
ExtClassLoader查找不成功,AppClassLoader就自己查找,在java.class.path路径下查找。找到就返回。如果没有找到就让子类找,如果没有子类会怎么样?抛出各种异常。
上面的序列,详细说明了双亲委托的加载流程。我们可以发现委托是从下向上,然后具体查找过程却是自上至下。

双亲委托模型好处
因为这样可以避免重复加载,当父亲已经加载了该类的时候,就没有必要 ClassLoader再加载一次。考虑到安全因素,我们试想一下,如果不使用这种委托模式,那我们就可以随时使用自定义的String来动态替代java核心api中定义的类型,这样会存在非常大的安全隐患,而双亲委托的方式,就可以避免这种情况,因为String已经在启动时就被引导类加载器(Bootstrcp ClassLoader)加载,所以用户自定义的ClassLoader永远也无法加载一个自己写的String,除非你改变JDK中ClassLoader搜索类的默认算法。

类与类加载器
类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远远不限于类加载阶段。对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。这句话可以表达更通俗一些:比较两个类是否”相等”,只有再这两个类是有同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。

类隔离机制
同一个类Dog可以加载两次(只要loader1和loader3不是父子关系即可,加载出的 Class 对象不同),不同运行空间内的类不能互相访问(eg. loader1和loader3不是父子关系,则Loader1加载的Dog不能访问lodaer3加载的Sample)
父类加载器无法访问到子类加载器加载的类,除非使用反射。Eg. Loader1 的父加载器是 系统类加载器,假设 Sample 类由 loader1 加载, 使用 loader1 的类 Test 是由系统类加载器加载的,例如下面这段代码属于 Test 类,那么如果直接使用注释部分的代码(即通过常规的方式使用 Sample 是不行的),必须通过反射。

类加载的使用情景
热修复,插件化的开源框架使用的比较多。我们平时开发基本不用。但是如果你不理解的 那么插件化等框架就用不好。

其实具体可以去看研究《深入理解Java虚拟机》第二版。讲得非常详细,同时也很比较难理解需要多看几遍。

动态代理

  • 什么是动态代理
  • 怎么使用动态代理
  • 动态代理的优缺点
  • 类加载的动态代理
  • 动态代理的使用场景

什么是动态代理
代理生活中常见的就是 被告-律师-法官。法官提问题,被告回答一点,然后就是律师去回答。
代码中的代理也类似。有一种模式就叫代理模式。

动态代理就是不需要提前写代理类的代理模式。
怎么使用动态代理
这个也简单,和静态代理类似有个基本套路在、举个例子:现在有个接口描述水果。

public interface IFruit {void descriabe();
}
public class Apple implements IFruit{@Overridepublic void descriabe() {System.out.println("我是苹果!!!");}}

如果是静态代理的话:

public class AppleProxy implements IFruit {private IFruit mFruit;public AppleProxy(IFruit mFruit) {this.mFruit = mFruit;}@Overridepublic void descriabe() {System.out.println("吃的");mFruit.descriabe();System.out.println("甜的");}}

测试:

 public static void main(String[] args) {IFruit fruit = new AppleProxy(new Apple());fruit.descriabe();}

运行结果:

这个比较简单。那怎么处理不写AppleProxy也可以增强desciable();
答案就是动态代理。其实java提供好了一套 动态代理API流程如下:
定义一个类实现InvocationHandler接口

public class AppleDynamicProxy implements InvocationHandler {private Object object;public AppleDynamicProxy(Object object) {this.object = object;}@Overridepublic Object invoke(Object proxy, Method method, Object[] args)throws Throwable {System.out.println("吃的");method.invoke(object, args);System.out.println("甜的");return null;}}

然后测试

 public static void main(String[] args) {Apple apple = new Apple();InvocationHandler invocationHandler = new AppleDynamicProxy(apple);Class<?> class1 = Apple.class;IFruit fruit = (IFruit) Proxy.newProxyInstance(class1.getClassLoader(),class1.getInterfaces(), invocationHandler);fruit.descriabe();}

运行结果:

可以看到我们的AppleDynamicProxy并没有继承IFruit接口,但是收到describe()方法,并且在它调用前后进行了增强。
动态代理的优缺点
优点:不修改被代理对象的源码上,进行功能的增强。
缺点:暂无-可能用到反射会比较慢
动态代理的原理
主要用到了反射,如果对反射不熟悉的,看起来可能比较吃力。

动态代理的使用场景
这取决于你自己想干什么。主要看业务的需求。


关键就这个newProxyInstance通过名字就知道 创建一个代理类的实例。getProxyClass0会根据你传入的类加载器和接口 去生成一个代理类。
总结
代理分为静态代理和动态代理两种。
静态代理,代理类需要自己编写代码写成。
动态代理,代理类通过 Proxy.newInstance() 方法生成。
不管是静态代理还是动态代理,代理与被代理者都要实现两样接口,它们的实质是面向接口编程。
静态代理和动态代理的区别是在于要不要开发者自己定义 Proxy 类。
动态代理通过 Proxy 动态生成 proxy class,但是它也指定了一个 InvocationHandler 的实现类。
代理模式本质上的目的是为了增强现有代码的功能。

Java进阶04-动态代理、类加载相关推荐

  1. Java进阶 | Proxy动态代理机制详解

    一.Jvm加载对象 在说Java动态代理之前,还是要说一下Jvm加载对象的过程,这个依旧是理解动态代理的基础性原理: Java类即源代码程序.java类型文件,经过编译器编译之后就被转换成字节代码.c ...

  2. JAVA 进阶篇 动态代理 JDK动态代理和CGlib动态代理

    JDK动态代理和CGlib动态代理 JDK动态代理: 利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理. CGlib动态代理: 利用ASM(开源的Java ...

  3. 吃透Java中的动态代理

    动态代理在Java中是很重要的一部分,在很多框架中都会用到,如Spring中的AOP.Hadoop中的RPC等.为此在这把我对Java中的动态代理的理解分享给大家,同时写了一个模拟AOP编程的实例.( ...

  4. Java基础:动态代理

    系列阅读 Java基础:类加载器 Java基础:反射 Java基础:注解 Java基础:动态代理 概述 在运行时,动态创建一组指定的接口的实现类对象(代理对象)! 代理是实现AOP(面向切面编程)的核 ...

  5. 学习spring必须java基础知识-动态代理

    2019独角兽企业重金招聘Python工程师标准>>> Spring AOP使用动态代理技术在运行期织入增强的代码,为了揭示Spring AOP底层的工作机理,有必要对涉及到的Jav ...

  6. java 笔记(3) —— 动态代理,静态代理,cglib代理

    0.代理模式 代理模式是常用的java设计模式,他的特征是代理类与委托类有同样的接口. 代理类主要负责为委托类预处理消息.过滤消息.把消息转发给委托类,以及事后处理消息等. 代理类与委托类之间通常会存 ...

  7. Java se之动态代理

    转载自 Java se之动态代理 jdk动态代理: jdk动态代理是 需要提供一个实现了InvocationHandler接口的处理类: 通过Proxy的newProxyInstance()方法,参数 ...

  8. java | 什么是动态代理?

    java | 什么是动态代理? 代理模式在 Java 领域很多地方都有应用,它分为静态代理和动态代理,其中 Spring AOP 就是动态代理的典型例子.动态代理又分为接口代理和 cglib (子类代 ...

  9. 码农翻身——Java帝国之动态代理

    已经快三更天了, Java帝国的国王还在看着IO大臣的奏章发呆,他有点想不明白, 帝国已经给臣民了提供了这么多的东西,他们为什么还不满意呢? 集合.IO.反射.网络.线程.泛型.JDBC ...... ...

  10. Java设计模式-----Cglib动态代理(Cglib Proxy)

    接上文:4.2Java设计模式-----JDK动态代理(Dynamic Proxy) Cglib动态代理 百度百科:Cglib是一个强大的,高性能,高质量的Code生成类库,它可以在运行期扩展Java ...

最新文章

  1. 微软开源的自动机器学习工具上新了:NNI概览及新功能详解
  2. MFC中的CAsyncSocket类实现网络通信
  3. linux中 /dev/null命令
  4. Javascript 原型链
  5. JavaScript的调用栈、回调队列和事件循环
  6. [Ext JS 4] 实战之Load Mask(加载遮罩)的显示与隐藏
  7. python的常量_如何给python中设定常量
  8. 2.1 数字图像处理——图像基础
  9. 3D视觉是CV技术的未来!对话奥比中光CEO黄源浩
  10. Edraw Max(亿图图示):新手如何快速美化思维导图?
  11. 计算机报名验证码不出现怎么办,电脑显示验证码很慢或验证码显示不出来怎么办...
  12. C++笔试题目大全(笔试宝典)
  13. IE下载文件时,中文文件名乱码问题
  14. 宁皓网 react native 视频教程 ECMAScript6
  15. 安卓手机拨号键盘隐藏工程代码大全
  16. Android数据存储安全实践
  17. [Python] 让AI来解决数独和数独谜题
  18. 丢失LDF文件怎么办?
  19. 【HEVC简介】CTU、CU、PU、TU结构
  20. CSS雪碧图demo(含雪碧代码)

热门文章

  1. 今日头条留个人微信号或微信公众号的方法
  2. Invalid bound statement (not found): com.exam.mapper.UserMapper.findbyid
  3. iota 的 优点与吐槽
  4. JS 将对象拆开拼接成 URL
  5. 品达通用权限系统(Day 1~Day 2)
  6. 【haoi2009】毛毛虫
  7. 用谷歌还是火狐?手机端还是另外选择其他浏览器吧
  8. APP开发从需求到产品—APP产品经理成长日记
  9. Zeepelin系列(一)Zeepelin安装和配置以及对Hive的基本使用
  10. Apache2 Windows安装与HTTP Server Digest 认证