Горячая перезагрузка: динамическая замена классов (объектов) без остановки программы
Краткое описание Java ClassLoader
Классы в Java проходят семь этапов от загрузки в память до выгрузки из памяти: загрузка, проверка, подготовка, синтаксический анализ, инициализация, использование и выгрузка.
Далее мы сосредоточимся на двух этапах загрузки и инициализации.
нагрузка
На этапе загрузки виртуальная машина должна сделать три вещи:
- Получить определение класса по его полному именидвоичный поток байтов
- поток байтов, представленныйстатическая структура храненияпревратиться вобласть методаструктура данных времени выполнения
- Генерирует представление этого класса в памяти
java.lang.Class
Объект, как запись доступа к различным данным этого класса в области методов.
Эти три шага реализуются загрузчиком классов. Официально определенные загрузчики классов Java:BootstrapClassLoader
,ExtClassLoader
,AppClassLoader
. Три загрузчика классов отвечают за загрузку классов по разным путям. и сформировать родительско-дочернюю структуру.
имя загрузчика классов | Отвечает за загрузку каталогов |
---|---|
BootstrapClassLoader |
На верхнем уровне иерархии загрузчика классов он отвечает за загрузку классов по пути sun.boot.class.path.По умолчанию используется основной API в каталоге jre/lib или пакет jar, указанный параметром -Xbootclasspath. |
ExtClassLoader |
Путь загрузки — java.ext.dirs, по умолчанию — каталог jre/lib/ext или -Djava.ext.dirs указывает каталог для загрузки пакета jar. |
AppClassLoader |
Путь загрузки — java.class.path, который по умолчанию соответствует значению, установленному в переменной среды CLASSPATH. Его также можно указать с помощью параметра -classpath. |
По умолчанию, например, мы используем ключевое слово
new
илиClass.forName
на всем протяженииAppClassLoader
загружается загрузчиком классов
Из-за этой родительско-дочерней структуры, если класс должен быть загружен по умолчанию, он будет отдавать приоритет для загрузки своему родительскому классу (до тех пор, пока не будет загружен верхний уровень).BootstrapClassLoader
Нет), если в родительском классе его нет, то этот класс будет передан в дочерний класс для загрузки. Это правила родительского делегирования загрузчика классов.
инициализация
Когда мы хотим использовать методы или свойства выполнения класса, класс должен быть загружен в память и инициализирован. Итак, когда инициализируется класс? Бывают следующие ситуации
- Используйте ключевое слово new для создания экземпляров объектов, чтения или установки статических полей класса и вызова статических методов класса.
- использовать
java.lang.reflect
Когда метод пакета делает рефлексивный вызов класса, если класс не был инициализирован, он будет инициализирован первым. - При инициализации класса, если обнаруживается, что его родительский класс не был инициализирован, сначала запускается инициализация родительского класса.
- При запуске виртуальной машины пользователю необходимо указать основной класс для выполнения (класс, содержащий метод main()), виртуальная машина сначала инициализирует основной класс.
Как добиться горячей загрузки?
Из приведенного выше мы знаем, что по умолчанию загрузчик классов следует правилам родительского делегирования. Итак, мы хотим добиться горячей загрузки, тогда те классы, которые нам нужно загрузить, не могут быть переданы системному загрузчику для завершения. Поэтому нам нужно настроить загрузчик классов, чтобы он писал наши собственные правила.
Реализовать свой собственный загрузчик классов
Чтобы реализовать свой собственный загрузчик классов, просто наследуйтеClassLoader
класс подойдет. И мы хотим нарушить правило родительского делегирования, тогда мы должны переписатьloadClass
метод, потому что по умолчаниюloadClass
Методы следуют правилам родительского делегирования.
public class CustomClassLoader extends ClassLoader{
private static final String CLASS_FILE_SUFFIX = ".class";
//AppClassLoader的父类加载器
private ClassLoader extClassLoader;
public CustomClassLoader(){
ClassLoader j = String.class.getClassLoader();
if (j == null) {
j = getSystemClassLoader();
while (j.getParent() != null) {
j = j.getParent();
}
}
this.extClassLoader = j ;
}
protected Class<?> loadClass(String name, boolean resolve){
Class cls = null;
cls = findLoadedClass(name);
if (cls != null){
return cls;
}
//获取ExtClassLoader
ClassLoader extClassLoader = getExtClassLoader() ;
//确保自定义的类不会覆盖Java的核心类
try {
cls = extClassLoader.loadClass(name);
if (cls != null){
return cls;
}
}catch (ClassNotFoundException e ){
}
cls = findClass(name);
return cls;
}
@Override
public Class<?> findClass(String name) {
byte[] bt = loadClassData(name);
return defineClass(name, bt, 0, bt.length);
}
private byte[] loadClassData(String className) {
// 读取Class文件呢
InputStream is = getClass().getClassLoader().getResourceAsStream(className.replace(".", "/")+CLASS_FILE_SUFFIX);
ByteArrayOutputStream byteSt = new ByteArrayOutputStream();
// 写入byteStream
int len =0;
try {
while((len=is.read())!=-1){
byteSt.write(len);
}
} catch (IOException e) {
e.printStackTrace();
}
// 转换为数组
return byteSt.toByteArray();
}
public ClassLoader getExtClassLoader(){
return extClassLoader;
}
}
Зачем получать это первымExtClassLoader
Как насчет загрузчиков классов? На самом деле, это ссылка на дизайн в Tomcat, чтобы предотвратить перезапись некоторых основных классов нашим пользовательским загрузчиком классов. Напримерjava.lang.Object
.
зачем получатьExtClassLoader
загрузчик классов вместо полученияAppClassLoader
Шерстяная ткань? Это потому, что если мы получимAppClassLoader
Загрузка, разве это не правило родительского делегирования?
Отслеживание файлов класса
Здесь мы используемScheduledThreadPoolExecutor
Для периодического контроля за изменением файла. Запишите время последней модификации файла при запуске программы. Затем периодически проверяйте, не изменилось ли время последней модификации файла. Если он изменен, заново создайте загрузчик классов, чтобы заменить его. Затем новый файл загружается в память.
Сначала создаем файл, за которым нужно следить
public class Test {
public void test(){
System.out.println("Hello World! Version one");
}
}
Мы динамически выводим номер версии, изменяя номер версии во время работы программы. Затем мы создаем класс задач, который выполняется периодически.
public class WatchDog implements Runnable{
private Map<String,FileDefine> fileDefineMap;
public WatchDog(Map<String,FileDefine> fileDefineMap){
this.fileDefineMap = fileDefineMap;
}
@Override
public void run() {
File file = new File(FileDefine.WATCH_PACKAGE);
File[] files = file.listFiles();
for (File watchFile : files){
long newTime = watchFile.lastModified();
FileDefine fileDefine = fileDefineMap.get(watchFile.getName());
long oldTime = fileDefine.getLastDefine();
//如果文件被修改了,那么重新生成累加载器加载新文件
if (newTime!=oldTime){
fileDefine.setLastDefine(newTime);
loadMyClass();
}
}
}
public void loadMyClass(){
try {
CustomClassLoader customClassLoader = new CustomClassLoader();
Class<?> cls = customClassLoader.loadClass("com.example.watchfile.Test",false);
Object test = cls.newInstance();
Method method = cls.getMethod("test");
method.invoke(test);
}catch (Exception e){
System.out.println(e);
}
}
}
Вы можете видеть, что в приведенной выше демонстрации gif мы просто реализовали функцию горячей загрузки.
оптимизация
В приведенном выше вызове метода мы используемgetMethod()
способ вызова. В этот момент могут возникнуть сомнения, почему бы не напрямуюnewInstance()
заствилиTest
класс?
Если бы мы использовали приведение, код выглядел бы такTest test = (Test) cls.newInstance()
. Но при запуске выкидываетClassCastException
аномальный. Почему это? Потому что в Java, чтобы определить, равны ли два класса, в дополнение к тому, одинаковы ли их два файла классов, они также видят, одинаковы ли их загрузчики классов. Таким образом, даже если один и тот же файл класса загружается двумя разными загрузчиками классов, их типы различны.
WatchDog
Классы создаются нашими новыми. Так что по умолчаниюAppClassLoader
загрузить. такtest
Объявленный тип переменнойWatchDog
свойство в методе, поэтому также определяетсяAppClassLoader
загрузить. Так что эти два класса не одно и то же.
Как это решить? проблема в=
Классы с обеих сторон разные, можем ли мы сделать их одинаковыми? как? Ответ — интерфейс. По умолчанию, если мы реализуем интерфейс, в этом интерфейсе обычно доминирует загрузчик подкласса. Это означает, что если нет особых требований, таких какA implements B
Если загрузчик A пользовательский. Тогда загрузчик интерфейса B такой же, как и у подкласса.
Итак, мы должны сделать загрузчик классов интерфейса какAppClassLoader
загрузить. Так что добавьте это предложение в пользовательский загрузчик
if ("com.example.watchfile.ITest".equals(name)){
try {
cls = getSystemClassLoader().loadClass(name);
} catch (ClassNotFoundException e) {
}
return cls;
}
Создать интерфейс
public interface ITest {
void test();
}
Так что можно назвать это счастливым. вызвать его метод напрямую. не будет генерировать исключение, потому что=
Нет. Оба класса одинаковы.
CustomClassLoader customClassLoader = new CustomClassLoader();
Class<?> cls = customClassLoader.loadClass("com.example.watchfile.Test",false);
ITest test = (ITest) cls.newInstance();
test.test();