高级Java类教程:类重载指南

本文概述

在Java开发项目中, 一种典型的工作流程是在每次类更改时都重新启动服务器, 没有人抱怨。那是关于Java开发的事实。从使用Java的第一天开始, 我们就一直这样工作。但是Java类重载难于实现吗?对于熟练的Java开发人员来说, 这个问题是否既具有挑战性又令人兴奋?在这个Java类教程中, 我将尝试解决该问题, 帮助你获得即时重新加载类的所有好处, 并极大地提高你的生产率。

不经常讨论Java类重载, 并且很少有文档探讨此过程。我是来改变这个的。该Java类教程将逐步解释该过程, 并帮助你掌握这一不可思议的技术。请记住, 实现Java类重载需要很多注意, 但是学习如何做到这一点将使你跻身Java开发人员和软件架构师的行列。了解如何避免10个最常见的Java错误也不会有任何伤害。

工作空间设置

本教程的所有源代码都在此处GitHub上载。

要在遵循本教程的同时运行代码, 你将需要Maven, Git以及Eclipse或IntelliJ IDEA。

如果你使用的是Eclipse:

  • 运行命令mvn eclipse:eclipse生成Eclipse的项目文件。
  • 加载生成的项目。
  • 将输出路径设置为目标/类。

如果你使用的是IntelliJ:

  • 导入项目的pom文件。
  • 运行任何示例时, IntelliJ都不会自动编译, 因此你必须:
  • 运行IntelliJ中的示例, 然后每次要编译时, 都必须按Alt + B E
  • 使用run_example * .bat在IntelliJ外部运行示例。将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类。 JVM的默认类加载器将对userClass1进行加载, 而DynamicClassLoader将对userClass2进行加载, DynamicClassLoader是一种自定义类加载器, 其源代码也在GitHub项目中提供, 下面将对其进行详细描述。

这是其余的主要方法:

  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:
[email protected]
[email protected]

And different age values:
11
10

如你在此处看到的, 尽管User类具有相同的名称, 但它们实际上是两个不同的类, 并且可以独立地对其进行管理和操纵。年龄值虽然声明为静态, 但存在两个版本, 分别附加到每个类, 并且也可以独立更改。

在普通的Java程序中, ClassLoader是将类带入JVM的门户。当一个班级需要加载另一个班级时, 这是ClassLoader的任务。

但是, 在此Java类示例中, 名为DynamicClassLoader的自定义ClassLoader用于加载User类的第二版本。如果代替DynamicClassLoader, 我们将再次使用默认的类加载器(通过命令StaticInt.class.getClassLoader()), 则将使用相同的User类, 因为所有加载的类均被缓存。

检查默认Java ClassLoader与DynamicClassLoader的工作方式是从此Java类教程中受益的关键。

DynamicClassLoader

普通的Java程序中可以有多个类加载器。默认情况下, 一个加载主类的类是ClassLoader, 从代码中, 你可以创建和使用任意数量的类加载器。因此, 这是Java中重新加载类的关键。 DynamicClassLoader可能是整个教程中最重要的部分, 因此在实现目标之前, 我们必须了解动态类加载的工作方式。

与ClassLoader的默认行为不同, 我们的DynamicClassLoader继承了更积极的策略。普通的类加载器将为其父类ClassLoader赋予优先级, 并且仅加载其父类无法加载的类。这适合正常情况, 但不适用于我们的情况。而是, DynamicClassLoader将尝试遍历所有类路径, 并在放弃其父级权限之前解析目标类。

在上面的示例中, 创建的DynamicClassLoader仅使用一个类路径:” target / classes”(在当前目录中), 因此能够加载该位置中的所有类。对于不在此处的所有类, 都必须引用父类加载器。例如, 我们需要在我们的StaticInt类中加载String类, 而我们的类加载器无权访问JRE文件夹中的rt.jar, 因此将使用父类加载器的String类。

以下代码来自DynamicClassLoader的父类AggressiveClassLoader, 并显示了定义此行为的位置。

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类将被转储, 新的User类将被加载并调用其方法嗜好。

这是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的新实例时, 它将从target / classes文件夹加载User类, 在该文件夹中, 我们已将Eclipse或IntelliJ设置为输出最新的类文件。所有旧的DynamicClassLoaders和旧的User类都将取消链接并接受垃圾回收器。

至关重要的是,高级Java开发人员必须了解动态类的重新加载,无论是活动的还是未链接的。

如果你熟悉JVM HotSpot, 那么在这里值得注意的是, 也可以更改并重新加载类结构:将删除playFootball方法, 并添加playBasketball方法。这与HotSpot不同, 后者仅允许更改方法内容, 否则无法重新加载该类。

现在我们可以重新加载一个类了, 是时候尝试一次重新加载许多类了。让我们在下一个示例中进行尝试。

示例3:重新加载多个类

该示例的输出与示例2相同, 但是将显示如何在具有上下文, 服务和模型对象的, 更像应用程序的结构中实现此行为。这个示例的源代码很大, 因此在这里只显示了其中的一部分。完整的源代码在这里。

这是主要方法:

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方法将拥有context对象, 这是指向所有需要删除的东西的唯一链接。如果我们断开该链接, 则上下文对象和上下文类以及服务对象……都将受到垃圾回收器的处理。

关于为什么通常的类如此持久并且不收集垃圾的一些解释:

  • 通常, 我们将所有类加载到默认的Java类加载器中。
  • class-classloader关系是一种双向关系, 类加载器还缓存它已加载的所有类。
  • 因此, 只要类加载器仍连接到任何活动线程, 所有内容(所有已加载的类)都将不受垃圾收集器的影响。
  • 也就是说, 除非我们可以将要重新加载的代码与默认类加载器已经加载的代码分开, 否则我们的新代码更改将永远不会在运行时应用。

通过这个示例, 我们看到重新加载所有应用程序的类实际上很容易。目的仅仅是保持从活动线程到正在使用的动态类加载器的瘦的, 可丢弃的连接。但是, 如果我们希望不重载某些对象(及其类), 而在重载周期之间重用该怎么办?让我们看下一个例子。

示例4:分离持久和重载的类空间

这是源代码。

主要方法:

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, 但实际上是另一个类!

那么如何防止我们的DynamicClassLoader加载ConnectionPool类呢?此示例不使用DynamicClassLoader, 而是使用其子类ExceptingClassLoader, 它将基于条件函数将加载传递给超级类加载器:

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

如果此处不使用ExceptingClassLoader, 则DynamicClassLoader将加载ConnectionPool类, 因为该类位于” target / classes”文件夹中。防止我们的DynamicClassLoader拾取ConnectionPool类的另一种方法是将ConnectionPool类编译到另一个文件夹(可能在另一个模块中), 并将其单独编译。

选择空间的规则

现在, Java类加载工作变得非常混乱。我们如何确定持久性空间中应包含哪些类, 以及可重载空间中应包含哪些类?规则如下:

  1. 可重载空间中的类可以引用持久性空间中的类, 但是持久性空间中的类可能永远不会引用可重载空间中的类。在前面的示例中, 可重载Context类引用了持久化的ConnectionPool类, 但是ConnectionPool没有引用Context。
  2. 如果一个类没有引用另一个空间中的任何类, 则它可以存在于任何一个空间中。例如, 具有所有静态方法(如StringUtils)的实用程序类可以在持久化空间中加载一次, 然后在可重载空间中单独加载。

因此, 你可以看到规则不是很严格。除了在两个空间中引用了对象的交叉类之外, 所有其他类都可以在持久化空间或可重装空间或两者中自由使用。当然, 只有可重载空间中的类才能享受重载循环的重载。

因此, 解决了类重载中最具挑战性的问题。在下一个示例中, 我们将尝试将此技术应用于简单的Web应用程序, 并像任何脚本语言一样享受重载Java类的乐趣。

示例5:小电话簿

这是源代码。

该示例与普通Web应用程序的外观非常相似。它是具有AngularJS, SQLite, Maven和Jetty嵌入式Web服务器的单页应用程序。

这是网络服务器结构中的可重新加载空间:

全面了解Web服务器结构中的可重载空间将有助于你掌握Java类的加载。

Web服务器将不保存对真实servlet的引用, 这些引用必须保留在可重载空间中才能被重载。它拥有的是存根servlet, 通过对其服务方法的每次调用, 它将在要运行的实际上下文中解析实际的servlet。

此示例还引入了一个新的对象ReloadingWebContext, 该对象向Web服务器提供了与常规Context类似的所有值, 但在内部保留了对可以由DynamicClassLoader重新加载的实际上下文对象的引用。正是ReloadingWebContext向Web服务器提供了stub servlet。

ReloadingWebContext在Java类重新加载过程中处理到Web服务器的存根servlet。

ReloadingWebContext将是实际上下文的包装, 并且:

  • 当调用HTTP GET到” /”时将重新加载实际上下文。
  • 将提供存根servlet到Web服务器。
  • 每当实际上下文被初始化或销毁时, 将设置值并调用方法。
  • 可以配置为重新加载上下文或不重新加载上下文, 以及使用哪个类加载器进行重新加载。这将在生产环境中运行应用程序时有所帮助。

因为了解我们如何隔离持久空间和可重载空间非常重要, 所以下面是两个在两个空间之间交叉的类:

上下文中对象public F0 <Connection> connF的类qj.util.funct.F0

  • 函数对象, 每次调用函数时都会返回一个Connection。此类位于qj.util包中, 该包已从DynamicClassLoader中排除。

上下文中对象public F0 <Connection> connF的类java.sql.Connection

  • 普通的SQL连接对象。该类不在我们的DynamicClassLoader的类路径中, 因此不会被拾取。

摘要

在此Java类教程中, 我们已经了解了如何重新加载单个类, 连续重新加载单个类, 重新加载多个类的整个空间以及独立于必须持久化的类重新加载多个类。使用这些工具, 实现可靠的类重新加载的关键因素是拥有超干净的设计。然后, 你可以自由地操作你的类和整个JVM。

实现Java类重载并不是世界上最容易的事情。但是, 如果你试一试, 并且在某个时刻发现正在动态加载你的类, 那么你已经快要准备就绪了。在为系统实现完全精湛的清洁设计之前, 几乎没有什么可做的了。

祝你好运, 我的朋友们, 并享受你新发现的超级大国!

微信公众号
手机浏览(小程序)
0
分享到:
没有账号? 忘记密码?