动态代理和类加载
本节主要复习动态代理和类加载机制。这2个知识点是非常重要的,也是很常见的,可能我们自己用的并不多,但是很多框架中的基础都是它们2个。如果不知道这些知识 那么看那些开源框架的源码就会很吃力。是谓 基础不牢地动山摇
类加载
还是按标准的灵魂5问来学习:
- 什么是类加载
- 怎么使用类加载
- 类加载的优缺点
- 类加载的原理
- 类加载的使用场景
什么是类加载
类加载是一种机制是一套流程和动作。我们写的源码其实就是文本文件,如我们写的第一个java程序 HelloWorld.java 。这个就是源文件给人看的。
但是这个文件并不能直接运行, 需要编译成HelloWorld.class 这个是字节码文件 打开里面其实是二进制,但是一般不会显示0101这种而是会以16进制显示。这个给java虚拟机看的。
你要看到话需要对着-java虚拟机规范表才知道代表什么意思。这个也是不能直接在电脑上运行的。它只能运行在JVM中,怎么运行,当我们执行java HelloWorld
这里java 就代表运行虚拟机,后面的HelloWorld就是 我们的字节码,意思就是运行虚拟机 然后在虚拟机上运行HelloWorld 字节码。虚拟机会把字节码 翻译成对应操作系统能识别的机器码就可以在电脑上运行了。
类加载就是:虚拟机通过全限定名(就是路径)把字节码文件读取进内存,怎么读?肯定是IO啊,然后进行必要的验证、准备、解析、初始化等工作 最后转为规定的数据结构存储在内存中方法区,并且生成一个Class对象来描述这个类文件,后续通过这个Class就可以访问到这个类文件。
怎么使用类加载
首先需要搞清楚 类加载器有哪些,并不是所有的 类都是又一种类加载器 去加载的,比如JDK自带的类,我们自己写的类。根据官方文档类加载器有3种:
-
BootStrap ClassLoder —BootstrapClassLoader引导类加载器,这个是最顶层的类加载器,它主要加载核心类库,JRE中的库 具体就是%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等。这里提一句JDK:java develop kit 就是开发工具包。 JRE:Java Runtime Enviroment是指Java的运行环境。
-
Extention ClassLoader --ExtClassLoader扩展类加载器,加载我们开发用的到基础库,不懂的可以点进去看看有哪些类具体目录%JRE_HOME%\lib\ext目录下的jar包和class文件。
-
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;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
//这个name是全限定名 什么意思?
//其实就是包名加类名。如本类的全限定名就是 com.zh.loader.AppleLoader.class。
//本质其实还是路径 因为你编译后的路径会变成 磁盘路径+com/zh/loader/AppleLoader.class
MainLoader.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类我们可以放到本地,也可以放到网络上,用的时候再 下载过来。这个就是热修复和插件化的基础。还可以动态替换掉类。
类加载的优缺点
优点:
- 动态的替换和增加额外的类。
- 热修复热更新插件化
缺点:
- 需要反射使用很慢
- 非常规手段有风险
类加载的原理
原理就是多啦,涉及了虚拟机的类加载机制。主要的源码就是ClassLoader中的loadClass()方法。它涉及双亲加载机制。源码如下:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> 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 stats
sun.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{
@Override
public void descriabe() {
System.out.println("我是苹果!!!");
}
}
如果是静态代理的话:
public class AppleProxy implements IFruit {
private IFruit mFruit;
public AppleProxy(IFruit mFruit) {
this.mFruit = mFruit;
}
@Override
public 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;
}
@Override
public 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 的实现类。
代理模式本质上的目的是为了增强现有代码的功能。