Расширенное руководство по классам Java: руководство по перезагрузке классов

Опубликовано: 2022-03-11

В проектах разработки Java типичный рабочий процесс включает перезапуск сервера при каждом изменении класса, и никто не жалуется на это. Это факт о разработке Java. Мы работали так с нашего первого дня работы с Java. Но так ли сложно добиться перезагрузки класса Java? И может ли эта проблема быть сложной и увлекательной для опытных разработчиков Java? В этом учебном пособии по классу Java я попытаюсь решить эту проблему, помочь вам получить все преимущества перезагрузки классов на лету и значительно повысить вашу производительность.

Перезагрузка классов Java не часто обсуждается, и очень мало документации, посвященной этому процессу. Я здесь, чтобы изменить это. Этот учебник по классам Java предоставит пошаговое объяснение этого процесса и поможет вам освоить эту невероятную технику. Имейте в виду, что реализация перезагрузки классов Java требует большой осторожности, но изучение того, как это сделать, выведет вас в высшую лигу как разработчика Java, так и архитектора программного обеспечения. Также не помешает понять, как избежать 10 самых распространенных ошибок Java.

Настройка рабочего пространства

Весь исходный код для этого руководства загружен на GitHub здесь.

Чтобы запустить код во время выполнения этого руководства, вам потребуются Maven, Git и либо Eclipse, либо IntelliJ IDEA.

Если вы используете Eclipse:

  • Запустите команду mvn eclipse:eclipse , чтобы сгенерировать файлы проекта Eclipse.
  • Загрузите сгенерированный проект.
  • Установите выходной путь к target/classes .

Если вы используете IntelliJ:

  • Импортируйте pom -файл проекта.
  • IntelliJ не будет автоматически компилироваться при запуске любого примера, поэтому вам нужно:
  • Запустите примеры внутри IntelliJ, затем каждый раз, когда вы захотите скомпилировать, вам придется нажимать Alt+BE
  • Запустите примеры вне IntelliJ с помощью run_example*.bat . Установите для автоматической компиляции компилятора IntelliJ значение true. Затем каждый раз, когда вы меняете какой-либо java-файл, IntelliJ будет автоматически его компилировать.

Пример 1. Перезагрузка класса с помощью загрузчика классов Java

Первый пример даст вам общее представление о загрузчике классов Java. Вот исходный код.

Учитывая следующее определение класса User :

 public static class User { public static int age = 10; }

Мы можем сделать следующее:

 public static void main(String[] args) { Class<?> userClass1 = User.class; Class<?> userClass2 = new DynamicClassLoader("target/classes") .load("qj.blog.classreloading.example1.StaticInt$User"); ...

В этом учебном примере в память будут загружены два класса User . userClass1 будет загружаться загрузчиком классов JVM по умолчанию, а userClass2 — с помощью DynamicClassLoader , пользовательского загрузчика классов, исходный код которого также предоставлен в проекте GitHub, и который я подробно опишу ниже.

Вот остальная часть main метода:

 out.println("Seems to be the same class:"); out.println(userClass1.getName()); out.println(userClass2.getName()); out.println(); out.println("But why there are 2 different class loaders:"); out.println(userClass1.getClassLoader()); out.println(userClass2.getClassLoader()); out.println(); User.age = 11; out.println("And different age values:"); out.println((int) ReflectUtil.getStaticFieldValue("age", userClass1)); out.println((int) ReflectUtil.getStaticFieldValue("age", userClass2)); }

И вывод:

 Seems to be the same class: qj.blog.classreloading.example1.StaticInt$User qj.blog.classreloading.example1.StaticInt$User But why there are 2 different class loaders: qj.util.lang.DynamicClassLoader@3941a79c sun.misc.Launcher$AppClassLoader@1f32e575 And different age values: 11 10

Как вы можете видеть здесь, хотя классы User имеют одно и то же имя, на самом деле это два разных класса, и ими можно управлять и манипулировать ими независимо друг от друга. Значение age, хотя и объявлено как статическое, существует в двух версиях, прикрепляемых отдельно к каждому классу, и также может быть изменено независимо.

В обычной Java-программе ClassLoader — это портал, переносящий классы в JVM. Когда один класс требует загрузки другого класса, задача ClassLoader выполнить загрузку.

Однако в этом примере класса Java пользовательский ClassLoader именем DynamicClassLoader используется для загрузки второй версии класса User . Если вместо DynamicClassLoader мы снова использовали загрузчик классов по умолчанию (с командой StaticInt.class.getClassLoader() ), то будет использоваться тот же класс User , поскольку все загруженные классы кэшируются.

Изучение того, как работает Java ClassLoader по умолчанию по сравнению с DynamicClassLoader, является ключом к получению пользы от этого руководства по классам Java.

DynamicClassLoader

В обычной Java-программе может быть несколько загрузчиков классов. Тот, который загружает ваш основной класс, ClassLoader , является классом по умолчанию, и из вашего кода вы можете создавать и использовать столько загрузчиков классов, сколько захотите. Это и есть ключ к перезагрузке классов в Java. DynamicClassLoader , возможно, самая важная часть всего этого руководства, поэтому мы должны понять, как работает динамическая загрузка классов, прежде чем мы сможем достичь нашей цели.

В отличие от поведения ClassLoader по умолчанию, наш DynamicClassLoader наследует более агрессивную стратегию. Обычный загрузчик классов отдал бы своему родительскому ClassLoader классов приоритет и загружал бы только те классы, которые его родитель не может загрузить. Это подходит для обычных обстоятельств, но не для нашего случая. Вместо этого DynamicClassLoader попытается просмотреть все пути к своим классам и разрешить целевой класс, прежде чем передать права своему родителю.

В нашем примере выше DynamicClassLoader создается только с одним путем к классу: "target/classes" (в нашем текущем каталоге), поэтому он способен загружать все классы, находящиеся в этом месте. Для всех классов, которых нет, он должен будет ссылаться на родительский загрузчик классов. Например, нам нужно загрузить класс String в наш класс StaticInt , а наш загрузчик классов не имеет доступа к rt.jar в нашей папке JRE, поэтому будет использоваться класс String родительского загрузчика классов.

Следующий код взят из AggressiveClassLoader , родительского класса DynamicClassLoader , и показывает, где определено это поведение.

 byte[] newClassData = loadNewClass(name); if (newClassData != null) { loadedClasses.add(name); return loadClass(newClassData, name); } else { unavaiClasses.add(name); return parent.loadClass(name); }

Обратите внимание на следующие свойства DynamicClassLoader :

  • Загруженные классы имеют ту же производительность и другие атрибуты, что и другие классы, загруженные загрузчиком классов по умолчанию.
  • DynamicClassLoader может быть удален вместе со всеми загруженными классами и объектами.

Имея возможность загружать и использовать две версии одного и того же класса, мы теперь думаем о том, чтобы сбросить старую версию и загрузить новую, чтобы заменить ее. В следующем примере мы будем делать именно это… непрерывно.

Пример 2: непрерывная перезагрузка класса

Следующий пример Java покажет вам, что JRE может бесконечно загружать и перезагружать классы, при этом старые классы сбрасываются и удаляются со сбором мусора, а новый класс загружается с жесткого диска и используется. Вот исходный код.

Вот основной цикл:

 public static void main(String[] args) { for (;;) { Class<?> userClass = new DynamicClassLoader("target/classes") .load("qj.blog.classreloading.example2.ReloadingContinuously$User"); ReflectUtil.invokeStatic("hobby", userClass); ThreadUtil.sleep(2000); } }

Каждые две секунды старый класс User будет сбрасываться, будет загружаться новый и вызываться его метод hobby .

Вот определение класса User :

 @SuppressWarnings("UnusedDeclaration") public static class User { public static void hobby() { playFootball(); // will comment during runtime // playBasketball(); // will uncomment during runtime } // will comment during runtime public static void playFootball() { System.out.println("Play Football"); } // will uncomment during runtime // public static void playBasketball() { // System.out.println("Play Basketball"); // } }

При запуске этого приложения вы должны попытаться прокомментировать и раскомментировать код, указанный кодом в классе User . Вы увидите, что всегда будет использоваться новейшее определение.

Вот пример вывода:

 ... Play Football Play Football Play Football Play Basketball Play Basketball Play Basketball

Каждый раз, когда создается новый экземпляр DynamicClassLoader , он загружает класс User из папки target/classes , где мы установили Eclipse или IntelliJ для вывода последнего файла класса. Все старые классы DynamicClassLoader и старые классы User будут отсоединены и подвергнуты сборщику мусора.

Крайне важно, чтобы продвинутые Java-разработчики понимали динамическую перезагрузку классов, независимо от того, активна она или несвязана.

Если вы знакомы с JVM HotSpot, то здесь стоит отметить, что структура класса также может быть изменена и перезагружена: метод playFootball необходимо удалить и добавить метод playBasketball . Это отличается от HotSpot, который позволяет изменять только содержимое метода, иначе класс не может быть перезагружен.

Теперь, когда мы можем перезагружать класс, пришло время попробовать перезагрузить несколько классов одновременно. Давайте попробуем это на следующем примере.

Пример 3: Перезагрузка нескольких классов

Вывод этого примера будет таким же, как и в примере 2, но будет показано, как реализовать это поведение в структуре, более похожей на приложение, с объектами контекста, службы и модели. Исходный код этого примера довольно большой, поэтому я показал здесь только его часть. Полный исходный код здесь.

Вот main метод:

 public static void main(String[] args) { for (;;) { Object context = createContext(); invokeHobbyService(context); ThreadUtil.sleep(2000); } }

И метод createContext :

 private static Object createContext() { Class<?> contextClass = new DynamicClassLoader("target/classes") .load("qj.blog.classreloading.example3.ContextReloading$Context"); Object context = newInstance(contextClass); invoke("init", context); return context; }

Метод invokeHobbyService :

 private static void invokeHobbyService(Object context) { Object hobbyService = getFieldValue("hobbyService", context); invoke("hobby", hobbyService); }

А вот и класс Context :

 public static class Context { public HobbyService hobbyService = new HobbyService(); public void init() { // Init your services here hobbyService.user = new User(); } }

И класс HobbyService :

 public static class HobbyService { public User user; public void hobby() { user.hobby(); } }

Класс Context в этом примере намного сложнее, чем класс User в предыдущих примерах: у него есть ссылки на другие классы, и у него есть метод init , который вызывается при каждом его создании. По сути, это очень похоже на классы контекста реального приложения (которые отслеживают модули приложения и осуществляют внедрение зависимостей). Таким образом, возможность перезагрузить этот класс Context вместе со всеми связанными с ним классами — отличный шаг к применению этой техники в реальной жизни.

Перезагрузка классов Java сложна даже для опытных инженеров Java.

По мере роста количества классов и объектов наш шаг «удаления старых версий» также будет усложняться. Это также главная причина, по которой перезагрузка класса так сложна. Чтобы возможно удалить старые версии, мы должны убедиться, что после создания нового контекста все ссылки на старые классы и объекты будут удалены. Как мы справимся с этим элегантно?

main метод здесь будет иметь объект контекста, и это единственная ссылка на все, что нужно отбросить. Если мы разорвем эту связь, объект контекста, класс контекста и объект службы… все попадут в сборщик мусора.

Небольшое объяснение того, почему обычно классы такие стойкие и не собирают мусор:

  • Обычно мы загружаем все наши классы в загрузчик классов Java по умолчанию.
  • Связь класс-загрузчик классов является двусторонней, при этом загрузчик классов также кэширует все загруженные им классы.
  • Поэтому, пока загрузчик классов все еще подключен к любому живому потоку, все (все загруженные классы) будет невосприимчиво к сборщику мусора.
  • Тем не менее, если мы не сможем отделить код, который хотим перезагрузить, от кода, уже загруженного загрузчиком классов по умолчанию, наши новые изменения кода никогда не будут применены во время выполнения.

В этом примере мы видим, что перезагрузка всех классов приложения на самом деле довольно проста. Цель состоит в том, чтобы просто сохранить тонкое, сбрасываемое соединение от живого потока к используемому динамическому загрузчику классов. Но что, если мы хотим, чтобы некоторые объекты (и их классы) не перезагружались, а использовались повторно между циклами перезагрузки? Давайте посмотрим на следующий пример.

Пример 4: Разделение сохраненных и перезагруженных пространств классов

Вот исходный код..

main метод:

 public static void main(String[] args) { ConnectionPool pool = new ConnectionPool(); for (;;) { Object context = createContext(pool); invokeService(context); ThreadUtil.sleep(2000); } }

Таким образом, вы можете видеть, что трюк здесь заключается в загрузке класса ConnectionPool и создании его экземпляра вне цикла перезагрузки, сохранении его в постоянном пространстве и передаче ссылки на объекты Context .

Метод createContext также немного отличается:

 private static Object createContext(ConnectionPool pool) { ExceptingClassLoader classLoader = new ExceptingClassLoader( (className) -> className.contains(".crossing."), "target/classes"); Class<?> contextClass = classLoader.load("qj.blog.classreloading.example4.reloadable.Context"); Object context = newInstance(contextClass); setFieldValue(pool, "pool", context); invoke("init", context); return context; }

Отныне мы будем называть объекты и классы, перезагружаемые при каждом цикле, «перезагружаемым пространством», а другие объекты и классы, не перезагружаемые и не обновляющиеся во время циклов перезагрузки, — «сохраняемым пространством». Нам нужно будет очень четко определить, какие объекты или классы остаются в каком пространстве, тем самым проводя разделительную линию между этими двумя пространствами.

Без правильного обращения такое разделение загрузки классов Java может привести к сбою.

Как видно из рисунка, не только объект Context и объект UserService ссылаются на объект ConnectionPool , но и классы Context и UserService также ссылаются на класс ConnectionPool . Это очень опасная ситуация, которая часто приводит к путанице и неудачам. Класс ConnectionPool не должен загружаться нашим DynamicClassLoader , в памяти должен быть только один класс ConnectionPool , который загружается ClassLoader по умолчанию. Это один из примеров того, почему так важно быть осторожным при проектировании архитектуры с перезагрузкой классов в Java.

Что, если наш DynamicClassLoader случайно загрузит класс ConnectionPool ? Тогда объект ConnectionPool из сохраняемого пространства не может быть передан объекту Context , потому что объект Context ожидает объект другого класса, который также называется ConnectionPool , но на самом деле является другим классом!

Так как же предотвратить загрузку класса ConnectionPool нашим DynamicClassLoader ? Вместо использования DynamicClassLoader в этом примере используется его подкласс с именем: ExceptingClassLoader , который передает загрузку суперзагрузчику классов на основе функции условия:

 (className) -> className.contains("$Connection")

Если мы не используем здесь ExceptingClassLoader , то DynamicClassLoader загрузит класс ConnectionPool , потому что этот класс находится в папке « target/classes ». Еще один способ предотвратить захват класса ConnectionPool нашим DynamicClassLoader — скомпилировать класс ConnectionPool в другую папку, возможно, в другой модуль, и он будет скомпилирован отдельно.

Правила выбора пространства

Теперь задание по загрузке класса Java становится действительно запутанным. Как мы определяем, какие классы должны быть в сохраняемом пространстве, а какие в перезагружаемом пространстве? Вот правила:

  1. Класс в перезагружаемом пространстве может ссылаться на класс в сохраняемом пространстве, но класс в сохраняемом пространстве никогда не может ссылаться на класс в перезагружаемом пространстве. В предыдущем примере перезагружаемый класс Context ссылается на сохраняемый класс ConnectionPool , но ConnectionPool не имеет ссылки на Context .
  2. Класс может существовать в любом пространстве, если он не ссылается ни на один класс в другом пространстве. Например, служебный класс со всеми статическими методами, такими как StringUtils , можно загрузить один раз в постоянное пространство и отдельно загрузить в перезагружаемое пространство.

Таким образом, вы можете видеть, что правила не очень ограничительные. За исключением пересекающихся классов, у которых есть ссылки на объекты в двух пространствах, все остальные классы могут свободно использоваться либо в сохраненном пространстве, либо в перезагружаемом пространстве, либо в обоих. Конечно, только классы в перезагружаемом пространстве будут перезагружаться циклами перезагрузки.

Итак, самая сложная проблема с перезагрузкой классов решена. В следующем примере мы попробуем применить эту технику к простому веб-приложению и насладимся перезагрузкой классов Java, как и любого языка сценариев.

Пример 5: Маленькая телефонная книга

Вот исходный код..

Этот пример будет очень похож на то, как должно выглядеть обычное веб-приложение. Это одностраничное приложение с AngularJS, SQLite, Maven и встроенным веб-сервером Jetty.

Вот перезагружаемое пространство в структуре веб-сервера:

Полное понимание перезагружаемого пространства в структуре веб-сервера поможет вам освоить загрузку классов Java.

Веб-сервер не будет хранить ссылки на настоящие сервлеты, которые должны оставаться в перезагружаемом пространстве, чтобы их можно было перезагрузить. То, что он содержит, — это сервлеты-заглушки, которые при каждом вызове своего метода обслуживания будут разрешать фактический сервлет в реальном контексте для запуска.

В этом примере также представлен новый объект ReloadingWebContext , который предоставляет веб-серверу все значения, такие как обычный контекст, но внутри содержит ссылки на фактический объект контекста, который может быть перезагружен с помощью DynamicClassLoader . Именно этот ReloadingWebContext предоставляет сервлеты-заглушки для веб-сервера.

ReloadingWebContext обрабатывает сервлеты-заглушки для веб-сервера в процессе перезагрузки класса Java.

ReloadingWebContext будет оболочкой фактического контекста и:

  • Перезагрузит фактический контекст при вызове HTTP GET для «/».
  • Предоставляет сервлеты-заглушки для веб-сервера.
  • Будет устанавливать значения и вызывать методы каждый раз, когда фактический контекст инициализируется или уничтожается.
  • Можно настроить перезагрузку контекста или нет, а также какой загрузчик классов используется для перезагрузки. Это поможет при запуске приложения в продакшене.

Поскольку очень важно понимать, как мы изолируем постоянное пространство и перезагружаемое пространство, вот два класса, которые пересекаются между двумя пространствами:

Класс qj.util.funct.F0 для объекта public F0<Connection> connF в Context

  • Объект Function будет возвращать Connection каждый раз при вызове функции. Этот класс находится в пакете qj.util, который исключен из DynamicClassLoader .

Класс java.sql.Connection для объекта public F0<Connection> connF в Context

  • Обычный объект соединения SQL. Этот класс не находится в пути класса нашего DynamicClassLoader , поэтому он не будет выбран.

Резюме

В этом руководстве по классам Java мы увидели, как перезагружать один класс, непрерывно перезагружать один класс, перезагружать все пространство из нескольких классов и перезагружать несколько классов отдельно от классов, которые должны быть сохранены. С этими инструментами ключевым фактором для достижения надежной перезагрузки классов является супер чистый дизайн. Затем вы можете свободно манипулировать своими классами и всей JVM.

Реализация перезагрузки классов Java — не самая простая вещь в мире. Но если вы попытаетесь и в какой-то момент обнаружите, что ваши классы загружаются на лету, то вы уже почти у цели. Остается совсем немного, прежде чем вы сможете добиться абсолютно безупречного дизайна вашей системы.

Удачи, друзья, и наслаждайтесь своей новообретенной суперсилой!