美文网首页java高级开发
Advanced Java Class Tutorial: A

Advanced Java Class Tutorial: A

作者: 老鼠AI大米_Java全栈 | 来源:发表于2022-12-23 14:37 被阅读0次

    在Java开发中,一个典型的工作流程是在每次类更改时重新启动服务器,没有人对此抱怨?解决这个问题是否既具有挑战性又令人兴奋?在本文中,我将尝试解决这个问题,帮助您获得动态类重载的所有好处,并极大地提高您的生产力。

    在Java开发中,一个典型的工作流程是在每次类更改时重新启动服务器,没有人对此抱怨?解决这个问题是否既具有挑战性又令人兴奋?在本文中,我将尝试解决这个问题,帮助您获得动态类重载的所有好处,并极大地提高您的生产力。

    Java类重载不常被讨论,而且很少有文档探讨这个过程。我是来改变这一点的。本Java课程教程将逐步解释这一过程,并帮助您掌握这一令人难以置信的技术。请记住,实现Java类重载需要非常小心,但学习如何实现它将使您成为Java开发人员和软件架构师的一员。了解如何避免最常见的10个Java错误也不会有什么害处。

    Work-Space Setup

    All source code for this tutorial is uploaded on GitHub here.

    To run the code while you follow this tutorial, you will need Maven, Git and either Eclipse or IntelliJ IDEA.

    If you are using Eclipse:

    • Run the command mvn eclipse:eclipse to generate Eclipse’s project files.
    • Load the generated project.
    • Set output path to target/classes.

    If you are using IntelliJ:

    • Import the project’s pom file.
    • IntelliJ will not auto-compile when you are running any example, so you have to either:
    • Run the examples inside IntelliJ, then every time you want to compile, you’ll have to press Alt+B E
    • Run the examples outside IntelliJ with the run_example*.bat. Set IntelliJ’s compiler auto-compile to true. Then, every time you change any java file, IntelliJ will auto-compile it.

    Example 1: Reloading a Class with Java Class Loader

    The first example will give you a general understanding of the Java class loader. Here is the source code.

    Given the following User class definition:

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

    We can do the following:

    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加载,DynamicClass加载器是一个自定义类加载器,其源代码也在GitHub项目中提供,我将在下面详细描述。

    Here is the rest of the main method:

      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));
    }
    

    And the output:

    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类示例中,名为DynamicClassLoader的自定义ClassLoader用于加载User类的第二个版本。如果不是DynamicClassLoader,而是再次使用默认的类加载器(使用命令StaticInt.class.getClassLoader()),那么将使用相同的User类,因为所有加载的类都将被缓存。

    image.png

    The DynamicClassLoader

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

    与ClassLoader的默认行为不同,我们的DynamicClassLoader继承了更具侵略性的策略。一个普通的类加载器会给其父类加载器优先权,只加载其父类无法加载的类。这适用于正常情况,但不适用于我们的情况。相反,DynamicClassLoader将尝试查看其所有类路径,并在放弃其父类的权限之前解析目标类。

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

    The following code is from AggressiveClassLoader, the parent class of DynamicClassLoader, and shows where this behavior is defined.

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

    Take note of the following properties of DynamicClassLoader:

    • The loaded classes have the same performance and other attributes as other classes loaded by the default class loader.
    • The DynamicClassLoader can be garbage-collected together with all of its loaded classes and objects.
      由于能够加载和使用同一类的两个版本,我们现在正在考虑转储旧版本并加载新版本来替换它。在下一个示例中,我们将继续这样做。

    Example 2: Reloading a Class Continuously

    下一个Java示例将向您展示JRE可以永远加载和重新加载类,将旧类转储并回收垃圾,并从硬盘加载全新的类并投入使用

    Here is the main loop:

    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);
      }
    }
    

    Every two seconds, the old User class will be dumped, a new one will be loaded and its method hobby invoked.

    Here is the User class definition:

    @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");
      //  }
    }
    

    When running this application, you should try to comment and uncomment the code indicated code in the User class. You will see that the newest definition will always be used.

    Here is some example output:

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

    每次创建DynamicClassLoader的新实例时,它都会从target/classes文件夹中加载User类,我们已将Eclipse或IntelliJ设置为输出最新的类文件。所有旧的DynamicClassLoader和旧的User类都将被取消链接,并接受垃圾收集器的处理。


    image.png

    如果您熟悉JVM HotSpot,那么这里值得注意的是,类结构也可以更改和重新加载:将删除playFootball方法,并添加playBasketball方法。这与HotSpot不同,HotSpot只允许更改方法内容,或者不能重新加载类。

    既然我们已经能够重新加载一个类,那么是时候尝试一次重新加载多个类了。让我们在下一个示例中尝试一下。

    Example 3: Reloading Multiple Classes

    此示例的输出将与示例2相同,但将展示如何在具有上下文、服务和模型对象的更类似于应用程序的结构中实现此行为。这个例子的源代码相当大,所以我在这里只展示了它的一部分。

    Here is is the main method:

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

    And the method 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;
    }
    

    The method invokeHobbyService:

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

    And here is the Context class:

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

    And the HobbyService class:

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

    本示例中的Context类比前面示例中的User类复杂得多:它具有指向其他类的链接,并且每次实例化时都会调用init方法。基本上,它非常类似于真实世界应用程序的上下文类(它跟踪应用程序的模块并执行依赖注入)。因此,能够将此Context类及其所有链接类一起重新加载,是将此技术应用于现实生活的一大步。


    image.png

    随着类和对象数量的增加,我们“删除旧版本”的步骤也将变得更加复杂。这也是类重载如此困难的最大原因。为了可能删除旧版本,我们必须确保在创建新上下文后,删除对旧类和对象的所有引用。我们如何优雅地处理这个问题?

    这里的main方法将持有上下文对象,这是需要删除的所有内容的唯一链接。如果我们断开该链接,上下文对象、上下文类和服务对象……都将受到垃圾收集器的处理。

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

    • Normally, we load all our classes into the default Java classloader.
    • 类类加载器关系是双向关系,类加载器还缓存它加载的所有类。
    • 只要类加载器仍然连接到任何活动线程,所有内容(所有加载的类)都不会受到垃圾收集器的影响。
    • 除非我们能够将要重新加载的代码与默认类加载器已经加载的代码分开,否则我们的新代码更改将永远不会在运行时应用。

    在这个示例中,我们看到重新加载所有应用程序的类实际上相当容易。目标仅仅是保持从活动线程到使用中的动态类加载器的瘦的、可丢弃的连接。但是,如果我们希望某些对象(及其类)不被重新加载,并且在重新加载周期之间被重用呢?让我们看下一个示例。

    Example 4: Separating Persisted and Reloaded Class Spaces

    The main method:

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

    因此,您可以看到这里的技巧是加载ConnectionPool类,并在重载循环外实例化它,将其保存在持久化空间中,并将引用传递给Context对象.

    The createContext method is also a little bit different:

    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;
    }
    

    从现在起,我们将在每个循环中重新加载的对象和类称为“可重新加载空间”,而其他对象和类-在重新加载循环中未回收和未更新的对象和类别-称为“持久化空间”。我们必须非常清楚哪些对象或类停留在哪个空间中,从而在这两个空间之间画出一条分隔线。


    image.png

    从图中可以看出,不仅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”文件夹中。防止ConnectionPool类被DynamicClassLoader获取的另一种方法是将ConnectionPool类别编译到不同的文件夹中,可能在不同的模块中,并且将单独编译。

    Rules for Choosing Space

    现在,Java类加载作业变得非常混乱。我们如何确定哪些类应该在持久化空间中,哪些类在可重载空间中?

    Here are the rules:

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

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

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

    Example 5: Little Phone Book

    This example will be very similar to what a normal web application should look like. It is a Single Page Application with AngularJS, SQLite, Maven, and Jetty Embedded Web Server.

    Here is the reloadable space in the web server’s structure:


    image.png

    web服务器不会保存对真实servlet的引用,这些引用必须保留在可重新加载的空间中,以便重新加载。它保存的是存根servlet,每次调用它的服务方法时,都会解析实际上下文中要运行的实际servlet。

    这个示例还引入了一个新的对象ReloadingWebContext,它向web服务器提供了与普通Context类似的所有值,但在内部保存了对可由DynamicClassLoader重新加载的实际上下文对象的引用。正是这个ReloadingWebContext为web服务器提供存根servlet。


    image.png

    The ReloadingWebContext will be the wrapper of the actual context, and:

    • Will reload the actual context when an HTTP GET to “/” is called.
    • Will provide stub servlets to the web server.
    • Will set values and invoke methods every time the actual context is initialized or destroyed.
    • Can be configured to reload the context or not, and which classloader is used for reloading. This will help when running the application in production.

    Because it’s very important to understand how we isolate the persisted space and reloadable space, here are the two classes that are crossing between the two spaces:

    Class qj.util.funct.F0 for object public F0<Connection> connF in Context

    • Function object, will return a Connection each time the function is invoked. This class resides in the qj.util package, which is excluded from the DynamicClassLoader.

    Class java.sql.Connection for object public F0<Connection> connF in Context

    Normal SQL connection object. This class does not reside in our DynamicClassLoader’s class path so it won’t be picked up.

    Summary

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

    实现Java类重载并不是世界上最简单的事情。但如果你尝试一下,并且在某个时刻发现你的类正在快速加载,那么你就已经快到了。在您可以为系统实现完美的清洁设计之前,您只需要做很少的事情。

    Good luck my friends and enjoy your newfound superpower!

    参考:https://www.toptal.com/java/java-wizardry-101-a-guide-to-java-class-reloading

    相关文章

      网友评论

        本文标题:Advanced Java Class Tutorial: A

        本文链接:https://www.haomeiwen.com/subject/ydmvqdtx.html