本文将带领大家利用Java的类加载器加SPI服务发现机制实现一个简易的代码热更新工具。
类加载相关知识可以参考: 深入理解JVM虚拟机第三版, 深入理解JVM虚拟机(第二版)—国外的,自己动手写JVM
JVM通过ClassLoader将.class二进制流读取到内存中,然后为其建立对应的数据结构:
/*
ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
*/
//伪代码,不全
type Class struct {
accessFlags uint16
name string // thisClassName
superClassName string
interfaceNames []string
constantPool *ConstantPool
fields []*Field
methods []*Method
sourceFile string
loader *ClassLoader
superClass *Class
interfaces []*Class
instanceSlotCount uint
staticSlotCount uint
staticVars Slots
initStarted bool
jClass *Object
...
}
接着对Class执行验证,准备和解析,当然将符号引用解析为直接引用的过程一般用到的时候才会去解析,这也说明了为什么类只会在用到的时候才会进行初始化。
如果想要在内存中唯一确定一个类,需要通过加载该类的类加载实例和当前类本身来唯一确定,因为每个类加载器都有自己的命名空间:
//伪代码
type ClassLoader struct {
//负责从哪些路径下加载class文件
cp *classpath.Classpath
//简易版本命令空间隔离实现
classMap map[string]*Class // loaded classes
}
对于由不同类加载实例对象加载的类而言,他们是不相等的,这里的不相等包括Class对象的equals方法,isAssignableFrom方法,isInstance方法,Instanceof关键字,包括checkcast类型转换指令。
同一个类加载实例不能重复加载同一个类两次,否则会抛出连接异常。
/**
* @author 大忽悠
* @create 2023/1/10 10:31
*/
public class DynamicClassLoader extends ClassLoader{
...
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
Class<?> c=null;
//0.确保当前类加载不会重复加载已经加载过的类
if((c=findLoadedClass(name))!=null){
return c;
}
//1.父类加载
if (getParent() != null) {
try{
c = getParent().loadClass(name);
}catch (ClassNotFoundException e){
}
}
//2.自己加载
if(c==null){
c = findClass(name);
}
//3.是否对当前class进行连接
if (resolve) {
resolveClass(c);
}
return c;
}
}
@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classBytes=getClassBytes(name);
return defineClass(name,classBytes, 0, classBytes.length);
}
/**
* @param name 全类名
* @param resolve 是否需要对加载得到类进行link过程--验证,准备,解析(一般都是懒解析)
*/
public static Class<?> dynamicLoadClass(String name,Boolean resolve) throws ClassNotFoundException {
DynamicClassLoader dynamicClassLoader = new DynamicClassLoader();
return dynamicClassLoader.loadClass(name,resolve);
}
/**
* @param name 全类名
*/
public static Class<?> dynamicLoadClass(String name) throws ClassNotFoundException {
return dynamicLoadClass(name,false);
}
...
}
dynamicLoadClass作为新增的静态方法,每次都会重新创建一个DynamicClassLoader自定义类加载器实例,并利用该实例去加载我们指定的类:
public static void main(String[] args) throws InterruptedException, ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
invokeSay();
Thread.sleep(15000);
invokeSay();
}
private static void invokeSay() throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
Class<?> aClass = DynamicClassLoader.dynamicLoadClass("com.exm.A");
Object newInstance = aClass.newInstance();
Method method = aClass.getMethod("say");
method.invoke(newInstance);
}
我们只需要在休眠的这15秒内,替换掉对应的class文件实现,即可完成代码的热更新,并且同时确保父类加载器不能够找到同类路径的类,否则就不能让自定义加载器得到机会重新读取二进制流到内存并建立相应的数据结构了。
默认的父类加载器是类路径加载器,也被称作系统类路径加载器
该系统类加载器就是默认创建用来加载启动类的加载器,因为我们在启动类中通过方法调用引用了DynamicClassLoader,因此我们自定义的类加载器也是通过加载启动类的加载器进行加载的。
在本类中引用到的类都会使用加载本类的加载器进行加载
class二进制流数据可以来自于文件,网络,数据库或者其他地方,因此为了支持多种多样的加载来源,我们可以定义一个ClassDataLoader接口:
/**
* @author 大忽悠
* @create 2023/1/10 11:37
*/
public interface ClassDataLoader {
/**
* @param name 全类名
* @return 加载得到的二进制文件流
*/
byte[] loadClassData(String name);
}
package com;
import java.io.*;
/**
* @author 大忽悠
* @create 2023/1/10 11:48
*/
public class FileClassDataLoader implements ClassDataLoader{
/**
* 默认从当前项目路径找起
*/
private String basePath="";
/**
* @param name 全类名
* @return 加载得到的二进制文件流
*/
@Override
public byte[] loadClassData(String name) {
return getClassData(new File(basePath+name.replace(".","/")+".class"));
}
private static byte[] getClassData(File file) {
try (InputStream ins = new FileInputStream(file); ByteArrayOutputStream baos = new
ByteArrayOutputStream()) {
byte[] buffer = new byte[4096];
int bytesNumRead = 0;
while ((bytesNumRead = ins.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return new byte[] {};
}
}
DynamicClassLoader自定义加载器内部新增两个属性:
/**
* 负责根据全类名加载class二进制流
*/
private final static List<ClassDataLoader> classDataLoaderList=new ArrayList<>();
/**
* 所有DynamicClassLoader加载器共享一个缓存
*/
private final static Map<String,byte[]> classBytesCache =new HashMap<>();
public static void registerClasDataLoader(ClassDataLoader classDataLoader){
classDataLoaderList.add(classDataLoader);
}
public static void cacheUpdateHook(String name,byte[] classData){
classBytesCache.put(name,classData);
}
对应的loadClass方法被修改为如下:
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
Class<?> c=null;
//0.确保当前类加载不会重复加载已经加载过的类
if((c=findLoadedClass(name))!=null){
return c;
}
//1.父类加载--如果缓存中存在,那么父类也就无需再次寻找了
if (classBytesCache.get(name)==null && getParent() != null) {
try{
c = getParent().loadClass(name);
}catch (ClassNotFoundException e){
}
}
//2.自己加载
if(c==null){
c = findClass(name);
}
//3.是否对当前class进行连接
if (resolve) {
resolveClass(c);
}
return c;
}
}
@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classBytes = classBytesCache.get(name);
if(classBytes==null){
for (ClassDataLoader classDataLoader : classDataLoaderList) {
if((classBytes=classDataLoader.loadClassData(name))!=null){
break;
}
}
}
if (classBytes==null || classBytes.length == 0) {
throw new ClassNotFoundException();
}
classBytesCache.put(name,classBytes);
return defineClass(name,classBytes, 0, classBytes.length);
}
DynamicClassLoader内部内置了多个ClassData数据源,我们通过遍历数据源列表,只要其中一个返回结果不为空,我们就立刻返回。
为了避免每次都需要重新从数据源中读取数据,我们可以将从数据源中获取到的二进制字节码缓存起来,然后让ClassDataLoader通过cacheUpdateHook钩子函数更新缓存达到动态更新的效果。
我们自定义的FileClassDataLoader通过回调registerClassDataLoader接口,将自身注册到DynamicClassLoader的数据源列表中去:
static {
DynamicClassLoader.registerClasDataLoader(new FileClassDataLoader());
}
但是如何让FileClassDataLoader静态代码块能够执行,也就是FileClassDataLoader类需要被初始化,如何做到?
在不通过new指令,不调用类里面的方法和访问类中字段的情况下,想要类能够被初始化,我们可以通过Class.forName完成:
forName的重载方法有一个Initialize参数,表明加载了当前类后,是否需要初始化该类,如果我们调用单参数的forName,那么默认为true。
所以,现在,我们只需要通过一种方式获取到ClassDataLoader的所有实现类类名,然后挨个使用Class.forName方法,完成实现类的初始化,就可以让实现类都注册到DynamicClassLoader中去。
SPI可以使用Java提供的serviceLoader,或者参考Spring的spring.factories实现,这里我给出一个简单的实现方案:
/**
* @author 大忽悠
* @create 2023/1/10 12:03
*/
public class SPIService {
/**
* 服务文件地址
*/
private static final String SERVICE_PATH = "META-INF" + File.separator + "SPI.properties";
/**
* 服务信息存储
*/
private static Properties SERVICE_MAP;
static {
try {
SERVICE_MAP = new Properties();
SERVICE_MAP.load(SPIService.class.getClassLoader().getResourceAsStream(SERVICE_PATH));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* @param name 需要寻找的服务实现的接口的全类名
* @return 找寻到的所有服务实现类
*/
public List<Class<?>> loadService(String name) {
if (SERVICE_MAP == null) {
return null;
}
String[] classNameList = SERVICE_MAP.getProperty(name).split(",");
ArrayList<Class<?>> classList = new ArrayList<>(classNameList.length);
for (String classDataClassName : classNameList) {
try {
classList.add(Class.forName(classDataClassName));
} catch (ClassNotFoundException e) {
//忽略不可被解析的服务实现类
e.printStackTrace();
}
}
return classList;
}
}
DynamicClassLoader新增代码:
/**
* 负责提供SPI服务发现机制
*/
private final static SPIService spiService=new SPIService();
static {
//通过SPI机制寻找classDataLoader
spiService.loadService(ClassDataLoader.class.getName());
}
package com;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author 大忽悠
* @create 2023/1/10 10:31
*/
public class DynamicClassLoader extends ClassLoader{
/**
* 负责根据全类名加载class二进制流
*/
private final static List<ClassDataLoader> classDataLoaderList=new ArrayList<>();
/**
* 所有DynamicClassLoader加载器共享一个缓存
*/
private final static Map<String,byte[]> classBytesCache =new HashMap<>();
/**
* 负责提供SPI服务发现机制
*/
private final static SPIService spiService=new SPIService();
static {
//通过SPI机制寻找classDataLoader
spiService.loadService(ClassDataLoader.class.getName());
}
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
Class<?> c=null;
//0.确保当前类加载不会重复加载已经加载过的类
if((c=findLoadedClass(name))!=null){
return c;
}
//1.父类加载--如果缓存中存在,那么父类也就无需再次寻找了
if (classBytesCache.get(name)==null && getParent() != null) {
try{
c = getParent().loadClass(name);
}catch (ClassNotFoundException e){
}
}
//2.自己加载
if(c==null){
c = findClass(name);
}
//3.是否对当前class进行连接
if (resolve) {
resolveClass(c);
}
return c;
}
}
@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classBytes = classBytesCache.get(name);
if(classBytes==null){
for (ClassDataLoader classDataLoader : classDataLoaderList) {
if((classBytes=classDataLoader.loadClassData(name))!=null){
break;
}
}
}
if (classBytes==null || classBytes.length == 0) {
throw new ClassNotFoundException();
}
classBytesCache.put(name,classBytes);
return defineClass(name,classBytes, 0, classBytes.length);
}
/**
* @param name 全类名
* @param resolve 是否需要对加载得到类进行link过程--验证,准备,解析(一般都是懒解析)
*/
public static Class<?> dynamicLoadClass(String name,Boolean resolve) throws ClassNotFoundException {
DynamicClassLoader dynamicClassLoader = new DynamicClassLoader();
return dynamicClassLoader.loadClass(name,resolve);
}
/**
* @param name 全类名
*/
public static Class<?> dynamicLoadClass(String name) throws ClassNotFoundException {
return dynamicLoadClass(name,false);
}
public static void registerClasDataLoader(ClassDataLoader classDataLoader){
classDataLoaderList.add(classDataLoader);
}
public static void cacheUpdateHook(String name,byte[] classData){
classBytesCache.put(name,classData);
}
}
/**
* @author 大忽悠
* @create 2023/1/10 11:37
*/
public interface ClassDataLoader {
/**
* @param name 全类名
* @return 加载得到的二进制文件流
*/
byte[] loadClassData(String name);
}
package com;
import java.io.*;
/**
* @author 大忽悠
* @create 2023/1/10 11:48
*/
public class FileClassDataLoader implements ClassDataLoader{
/**
* 默认从当前项目路径找起
*/
private String basePath="";
static {
DynamicClassLoader.registerClasDataLoader(new FileClassDataLoader());
}
/**
* @param name 全类名
* @return 加载得到的二进制文件流
*/
@Override
public byte[] loadClassData(String name) {
return getClassData(new File(basePath+name.replace(".","/")+".class"));
}
private static byte[] getClassData(File file) {
try (InputStream ins = new FileInputStream(file); ByteArrayOutputStream baos = new
ByteArrayOutputStream()) {
byte[] buffer = new byte[4096];
int bytesNumRead = 0;
while ((bytesNumRead = ins.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return new byte[] {};
}
}
package com;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
/**
* @author 大忽悠
* @create 2023/1/10 12:03
*/
public class SPIService {
/**
* 服务文件地址
*/
private static final String SERVICE_PATH = "META-INF" + File.separator + "SPI.properties";
/**
* 服务信息存储
*/
private static Properties SERVICE_MAP;
static {
try {
SERVICE_MAP = new Properties();
SERVICE_MAP.load(SPIService.class.getClassLoader().getResourceAsStream(SERVICE_PATH));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* @param name 需要寻找的服务实现的接口的全类名
* @return 找寻到的所有服务实现类
*/
public List<Class<?>> loadService(String name) {
if (SERVICE_MAP == null) {
return null;
}
String[] classNameList = SERVICE_MAP.getProperty(name).split(",");
ArrayList<Class<?>> classList = new ArrayList<>(classNameList.length);
for (String classDataClassName : classNameList) {
try {
classList.add(Class.forName(classDataClassName));
} catch (ClassNotFoundException e) {
//忽略不可被解析的服务实现类
e.printStackTrace();
}
}
return classList;
}
}
完整项目架构:
请注意,在类加载器之间具有了委派关系,首先发起装载要求的类加载器不必是定义该类型的加载器。
实际定义了那个类型的类装载器被称为该类型的定义类装载器,要求某个类装载器去装载一个类型,但是却返回了其他类装载器装载的类型,这种装载器被称为是那个类型的初始类装载器。
被委派的类装载器装载的这个类型,会在所有被标记为该类型的初始类装载器的命名空间中共享。
最后通过一个案例理解一下类加载器共享空间机制:
测试:
当我们通过自定义的加载器加载A类时,首先会确保A类的父类和实现的接口都会被先被加载,类加载的简易版本代码如下所示:
func (self *ClassLoader) LoadClass(name string) *Class {
if class, ok := self.classMap[name]; ok {
// already loaded
return class
}
var class *Class
//加载数组对象需要区别对待
if name[0] == '[' { // array class
class = self.loadArrayClass(name)
} else {
//
class = self.loadNonArrayClass(name)
}
if jlClassClass, ok := self.classMap["java/lang/Class"]; ok {
class.jClass = jlClassClass.NewObject()
class.jClass.extra = class
}
return class
}
func (self *ClassLoader) loadArrayClass(name string) *Class {
class := &Class{
accessFlags: ACC_PUBLIC, // todo
name: name,
loader: self,
initStarted: true,
superClass: self.LoadClass("java/lang/Object"),
interfaces: []*Class{
self.LoadClass("java/lang/Cloneable"),
self.LoadClass("java/io/Serializable"),
},
}
self.classMap[name] = class
return class
}
func (self *ClassLoader) loadNonArrayClass(name string) *Class {
data, entry := self.readClass(name)
class := self.defineClass(data)
link(class)
if self.verboseFlag {
fmt.Printf("[Loaded %s from %s]\n", name, entry)
}
return class
}
func (self *ClassLoader) readClass(name string) ([]byte, classpath.Entry) {
data, entry, err := self.cp.ReadClass(name)
if err != nil {
panic("java.lang.ClassNotFoundException: " + name)
}
return data, entry
}
// jvms 5.3.5
func (self *ClassLoader) defineClass(data []byte) *Class {
class := parseClass(data)
hackClass(class)
class.loader = self
resolveSuperClass(class)
resolveInterfaces(class)
self.classMap[class.name] = class
return class
}
func parseClass(data []byte) *Class {
cf, err := classfile.Parse(data)
if err != nil {
//panic("java.lang.ClassFormatError")
panic(err)
}
return newClass(cf)
}
// jvms 5.4.3.1
func resolveSuperClass(class *Class) {
if class.name != "java/lang/Object" {
class.superClass = class.loader.LoadClass(class.superClassName)
}
}
func resolveInterfaces(class *Class) {
interfaceCount := len(class.interfaceNames)
if interfaceCount > 0 {
class.interfaces = make([]*Class, interfaceCount)
for i, interfaceName := range class.interfaceNames {
class.interfaces[i] = class.loader.LoadClass(interfaceName)
}
}
}
本例中很明显,Interface接口是被DynamicClassLoader委托给了父类系统类加载器进行加载的,因此DynamicClassLoader和系统类加载器是Interface的初始类加载器.
此处在启动类中使用Interface类型时,还是会交给当前系统类加载器进行加载,此时因为系统类加载器已经加载过了,因此直接返回先前加载得到的Class对象。
这里因为DynamicClassLoader负责加载的A类,其在内存中生成的class数据结构,其中
//假设这是类A在内存中对应的class数据结构
type Class struct {
...
//值为DynamicClassLoader
loader *ClassLoader
//Object--委派给了启动类加载器加载
superClass *Class
//继承的Inteface委派给了系统类加载器加载
interfaces []*Class
...
}
因为被委派的类装载器装载的这个类型,会在所有被标记为该类型的初始类装载器的命名空间中共享。
所以superClass指向的实际是启动类加载器命名空间下的Object Class内存数据结构:
//假设这是启动类加载器
type ClassLoader struct {
cp *classpath.Classpath
verboseFlag bool
//A Class中的superClass实际是Object Class,指向的是启动类加载器命令空间下的同一个Object Class
classMap map[string]*Class // loaded classes
}
//假设这是系统类加载器
type ClassLoader struct {
cp *classpath.Classpath
verboseFlag bool
//A Class中的interfaceClass实际是Interface Class,指向的是系统类加载器命令空间下的同一个Interface Class
classMap map[string]*Class // loaded classes
}
这也就是为什么执行checkCast强制类型转换指令不会报错的原因。
这部分内容没看明白的,可以去参考一下下面这本书的第8章内容:
这里给出一张深入理解JVM虚拟机第三版中第9章关于tomcat 6之前的类加载器体系结构:
JasperLoader的加载范围仅仅是这个JSP文件编译出来的那一个class文件,当服务器检测到JSP文件修改时,会创建一个新的JasperLoader来加载被修改的JSP文件实现HotSwap功能。
注意: 每一个JSP文件都会被转换为一个Servlet类,JSP在JSP文件被解析后,得到对应的html字节流文件,servelt类负责将html字节流文件响应给浏览器.
所以过程就是 JSP–>JspServlet.java–>JspServlet.class–>JasperLoader加载—>因为JspServlet.class实现了Servlet相关规范接口,所以直接转换为对应的接口(为什么可以直接转换,参考上面案例),然后调用接口方法,完成响应内容输出。
大家联系上面我给出的案例,会发现其实本质是一样的,JSP热更新只需要每次重新创建一个JasperLoader实例,然后加载解析生成的JspServlet.class文件即可
该问题是在深入理解JVM虚拟机第三版第9章被提出的:
要解决这个问题,我们还是先回顾一下Tomcat类加载器体系结构:
Tomcat的类加载体系结构并没有破坏原本的双亲委派机制,而是进一步扩展了原本的双亲链,WebApp类加载器负责加载/WebApp/WEB-INF下.class文件,而加载Spring jar包的工作是通过CommonClassLoader完成,CommonClassLoader作为WebAppClassLoader的父类加载器(是双亲链上的父类,不是继承上的),因此应用程序类可以委托父类加载器,访问到Spring jar下的类,但是目前Spring jar下的类似乎无法反向访问应用程序的类。
父类加载器将加载类请求委派给子类加载器做一做法显然打破了双亲委派机制,因此我们的思路就是如何打破双亲委派机制,让CommonClassLoader将加载应用程序类的请求委派给子类WebAppClassLoader呢?
思路有两种:
显然,如果Spring底层的servlet服务器实现是Tomcat,然后整合Tomcat时,只需要将自身当前线程上下文加载器设置为WebAppClassLoader即可。