使用 Espresso 增强 HotSwap 功能

借助 Espresso,您可以受益于增强的 HotSwap 功能,使代码在开发过程中自然演进,而无需重新启动正在运行的应用程序。除了以调试模式启动应用程序并附加标准 IDE 调试器外,您无需进行任何特定配置即可获得增强的 HotSwap 优势。

使用 Espresso 调试 #

您可以使用自己喜欢的 IDE 调试器来调试在 Espresso 运行时中运行的 Java 应用程序。例如,从 IntelliJ IDEA 启动调试器会话是基于“运行配置”(Run Configurations)的。为确保您将调试器附加到相同环境中的 Java 应用程序,请在主菜单中导航到 运行 (Run),调试… (Debug…),编辑配置 (Edit Configurations),展开“环境”(Environment),检查 JRE 值和 VM 选项值。它应该显示 GraalVM 作为项目的 JRE,并且 VM 选项应包含 -truffle -XX:+IgnoreUnrecognizedVMOptions。有必要指定 -XX:+IgnoreUnrecognizedVMOptions,因为 IntelliJ 会自动添加一个目前尚不支持的 -javaagent 参数。按“调试”(Debug)。

这将运行应用程序并在后台启动一个调试器会话。

在调试会话期间使用 HotSwap #

一旦您的调试会话运行起来,您将能够应用广泛的代码更改(HotSwap),而无需重新启动会话。您可以随意在自己的应用程序上尝试,或按照以下说明操作:

  1. 创建一个新的 Java 应用程序。
  2. 使用以下 main 方法作为起点:
     public class HotSwapDemo {
    
         private static final int ITERATIONS = 100;
    
         public static void main(String[] args) {
             HotSwapDemo demo = new HotSwapDemo();
             System.out.println("Starting HotSwap demo with Espresso: 'java.vm.name' = " + System.getProperty("java.vm.name"));
             // run something in a loop
             for (int i = 1; i <= ITERATIONS; i++) {
                 demo.runDemo(i);
             }
             System.out.println("Completed HotSwap demo with Espresso");
         }
    
         public void runDemo(int iteration) {
             int random = new Random().nextInt(iteration);
             System.out.printf("\titeration %d ran with result: %d\n", iteration, random);
         }
     }
    
  3. 检查 java.vm.name 属性是否显示您正在 Espresso 上运行。
  4. runDemo() 的第一行放置一个行断点。
  5. 设置运行配置以使用 Espresso 运行并按“调试”(Debug)。您将看到:

    HotSwap Debugging Session: Debug Output

  6. 在断点处暂停时,从 runDemo() 的主体中提取一个方法。

    HotSwap Debugging Session: Extract Method

  7. 通过导航到“运行”(Run)->“调试操作”(Debugging Actions)->“重新加载更改的类”(Reload Changed Classes)来重新加载更改。

    HotSwap Debugging Session: Reload Changed Classes

  8. 通过注意“调试”(Debug)->“帧”(Frames)视图中的 <obsolete>:-1 当前帧来验证更改已应用。

    HotSwap Debugging Session: Frames View

  9. 在新提取的方法的第一行放置一个断点,然后按“恢复程序”(Resume Program)。断点将命中。

    HotSwap Debugging Session: Set a Breakpoint and Resume Program

  10. 尝试将 printRandom() 的访问修饰符从 private 更改为 public static。重新加载更改。按“恢复程序”(Resume Program)验证更改已应用。

    HotSwap Debugging Session: Change Access Modifiers

观看 使用 Espresso 增强 HotSwap 功能演示的视频版本

支持的更改 #

Espresso 的增强 HotSwap 几乎功能完备。支持以下更改:

  • 添加和删除方法
  • 添加和删除构造函数
  • 从接口添加和删除方法
  • 更改方法的访问修饰符
  • 更改构造函数的访问修饰符
  • 添加和删除字段
  • 更改字段类型
  • 在继承层级中移动字段并保留状态(参见下面的注释)
  • 更改类访问修饰符,例如,abstract 和 final 修饰符
  • 对 Lambda 表达式的更改
  • 添加新的匿名内部类
  • 删除匿名内部类
  • 更改超类
  • 更改已实现的接口

注意:当实例字段在类继承层级中移动时,状态会尽可能地保留。例如,“上拉字段”重构,其中源子类的所有现有实例都将能够从超类字段读取先前存储的值。另一方面,对于更改前字段不存在的无关子类实例,新字段的值将是语言默认值(对象类型字段为 null,int 为 0 等等)。

以下限制仍然存在:

  • 对枚举(Enums)的更改

HotSwap 插件 API #

借助 Espresso,您可以受益于增强的 HotSwap 功能,使代码在开发过程中自然演进,而无需重新启动正在运行的应用程序。虽然代码重新加载 (HotSwap) 是一个强大的工具,但它不足以反映所有类型的更改,例如对注解的更改、框架特定的更改(如已实现的服务或 Bean)。对于这些情况,代码通常需要执行以重新加载配置或上下文,然后更改才能在运行实例中完全反映出来。这就是 Espresso HotSwap 插件 API 派上用场的地方。

HotSwap 插件 API 旨在供框架开发者使用,通过设置适当的钩子来响应 IDE 中源代码编辑的更改。主要设计原则是您可以注册各种 HotSwap 监听器,它们将在指定的 HotSwap 事件上触发。示例包括重新运行静态初始化器、通用的 HotSwap 后回调,以及当某个服务提供者的实现发生变化时触发的钩子。

注意:HotSwap 插件 API 正在开发中,可能会根据社区请求添加更细粒度的 HotSwap 监听器注册。欢迎您通过我们的社区支持渠道发送增强请求,以帮助塑造 API。

通过一个运行中的示例来审查 HotSwap 插件 API,该示例将为 Micronaut 提供更强大的重新加载支持。

Micronaut HotSwap 插件 #

Micronaut HotSwap 插件示例实现作为 Micronaut-core 的一个 分支 托管。以下说明基于 macOS X 设置,Windows 仅需微小改动。要开始:

  1. 克隆仓库
      git clone git@github.com:javeleon/micronaut-core.git
    
  2. 构建并发布到本地 Maven 仓库
      cd micronaut-core
      ./gradlew publishMavenPublicationToMavenLocal
    

现在您将拥有一个支持 HotSwap 的 Micronaut 版本。在设置使用增强版 Micronaut 的示例应用程序之前,先了解一下插件在幕后做了什么。

有趣的类是 MicronautHotSwapPlugin,它持有一个应用程序上下文,当应用程序源代码发生某些更改时,该上下文可以重新加载。该类如下所示:

final class MicronautHotSwapPlugin implements HotSwapPlugin {

    private final ApplicationContext context;
    private boolean needsBeenRefresh = false;

    MicronautHotSwapPlugin(ApplicationContext context) {
        this.context = context;
        // register class re-init for classes that provide annotation metadata
        EspressoHotSwap.registerClassInitHotSwap(
                AnnotationMetadataProvider.class,
                true,
                () -> needsBeenRefresh = true);
        // register ServiceLoader listener for declared bean definitions
        EspressoHotSwap.registerMetaInfServicesListener(
                BeanDefinitionReference.class,
                context.getClassLoader(),
                () -> reloadContext());
        EspressoHotSwap.registerMetaInfServicesListener(
                BeanIntrospectionReference.class,
                context.getClassLoader(),
                () -> reloadContext());
    }

    @Override
    public String getName() {
        return "Micronaut HotSwap Plugin";
    }

    @Override
    public void postHotSwap(Class<?>[] changedClasses) {
        if (needsBeenRefresh) {
            reloadContext();
        }
        needsBeenRefresh = false;
    }

    private void reloadContext() {
        if (Micronaut.LOG.isInfoEnabled()) {
            Micronaut.LOG.info("Reloading app context");
        }
        context.stop();
        context.flushBeanCaches();
        context.start();

        // fetch new embedded application bean which will re-wire beans
        Optional<EmbeddedApplication> bean = context.findBean(EmbeddedApplication.class);
        // now restart the embedded app/server
        bean.ifPresent(ApplicationContextLifeCycle::start);
    }
}

关于 HotSwap API 的逻辑位于这个类的构造函数中。Micronaut 是围绕编译时注解处理构建的,注解元数据被收集并存储到生成的类中的静态字段中。每当开发人员更改 Micronaut 注解的类时,相应的元数据类就会重新生成。由于标准 HotSwap 不会(也不应该)重新运行静态初始化器,因此借助 HotSwap 插件,所有提供元数据(Micronaut 生成的类)的类的静态初始化器都会重新运行。因此,使用了这个 API 方法 EspressoHotSwap.registerClassInitHotSwap

public static boolean registerClassInitHotSwap(Class<?> klass, boolean onChange, HotSwapAction action)

这将在特定类以及任何其子类的类更改上注册一个监听器。onChange 变量指示是否仅在代码内部发生更改时才重新运行静态初始化器。action 参数是一个钩子,用于在静态初始化器重新运行后触发特定的操作。在这里,我们传递一个函数,当静态初始化器重新运行时,将 needsBeenRefresh 字段设置为 true。在 HotSwap 操作完成后,插件会收到一个 postHotSwap 调用,该调用响应 needsBeenRefresh 为 true 的情况,执行 Micronaut 特定代码以在 reloadContext 方法中重新加载应用程序上下文。

检测并注入新类 #

HotSwap 旨在允许类在运行中的应用程序中进行 HotSwap。但是,如果开发人员引入了一个全新的类(例如,Micronaut 中的一个新的 @Controller 类),HotSwap 不会神奇地注入新类,因为这样做至少需要了解内部类加载逻辑。

框架发现类的一种标准方式是通过 ServiceLoader 机制。HotSwap API 通过方法 EspressoHotSwap.registerMetaInfServicesListener 内置支持注册服务实现更改监听器:

public static boolean registerMetaInfServicesListener(Class<?> serviceType, ClassLoader loader, HotSwapAction action)

目前的支持仅限于监听 META-INF/services 中基于类路径的服务部署的实现更改。每当注册类类型的服务实现集发生更改时,action 就会触发。在 Micronaut HotSwap 插件中,会执行 reloadContext,然后它会自动拾取这些更改。

注意:由服务实现更改引起的 HotSwap 操作是独立于 HotSwap 触发的。作为开发人员,您无需从 IDE 执行 HotSwap 即可在运行中的应用程序中看到新功能。

Micronaut 的下一级 HotSwap #

现在您已经了解 Micronaut HotSwap 插件的工作原理,让我们在实际应用程序中使用此功能。这里有一个从教程 “创建您的第一个 Micronaut Graal 应用程序” 中创建的示例应用程序。示例源代码可以作为现成的 Gradle 项目从 这里 下载。下载、解压缩并在您的 IDE 中打开项目。

在继续之前,请确保您已安装 Espresso 并将 GraalVM 设置为项目 SDK。

  1. 在您的 IDE 中导航到示例项目中的根 build.gradle。添加:
     run.jvmArgs+="-truffle"
    
  2. 此外,添加我们之前发布增强型 Micronaut 框架的本地 Maven 仓库。例如:
     repositories {
     mavenLocal()
     ...
     }
    
  3. gradle.properties 中更新您发布的 Micronaut 版本。例如:
     micronautVersion=2.5.8-SNAPSHOT
    

    现在您已全部设置完毕。

  4. 执行 assemble 任务并使用定义的 run Gradle 任务创建运行配置。

  5. 按下“调试”(Debug)按钮以调试模式启动应用程序,这将启用增强的 HotSwap 支持。

  6. 应用程序启动后,通过访问 https://:8080/conferences/random 来验证您是否从 ConferenceController 获得了响应。

  7. 尝试对示例应用程序中的类进行各种更改,例如,将 @Controller 映射更改为不同的值,或添加一个新的 @Get 注解方法,然后应用 HotSwap 以查看神奇的效果。如果您定义了一个新的 @Controller 类,您只需编译该类,一旦文件系统监视器检测到更改,您就会看到重新加载,而无需显式执行 HotSwap。

联系我们