以编程方式重新启动Spring Boot应用程序/刷新Spring上下文

16

我正在尝试在不需要用户干预的情况下以编程方式重新启动我的Spring应用程序。

基本上,我有一个页面,允许切换应用程序的模式(实际上是切换当前活动配置文件),据我所知,我必须重新启动上下文。

目前我的代码非常简单,仅涉及重启部分(此代码使用Kotlin编写):

    context.close()
    application.setEnvironment(context.environment)
    ClassUtils.overrideThreadContextClassLoader(application.javaClass.classLoader)
    context = application.run(*argsArray)

然而,一旦我执行context.close(),JVM就会立即退出。我也尝试过context.refresh(),但那似乎只是杀死了Tomcat/Jetty(都试过以防它是Tomcat的问题),然后什么也没发生。

我还看到过以编程方式重启Spring Boot应用程序,但这些答案似乎对我都不起作用。此外,我还研究了Spring Actuator,据说它有/restart端点,但似乎不再存在?


我最近读到了一些关于Spring Cloud的内容,其中有一个“刷新端点”,可以刷新上下文中的所有bean。也许你会在这里找到一些有用的东西。刷新范围 - Patrick
为什么这被标记为“Java”? - Olivier Gérardin
2
因为我不在意有人用Java或Kotlin解决问题。Kotlin在这个问题中没有任何意义。 - Crembo
那么Java也不重要了...如果你标记了Java,至少提供一个Java版本的解决方案会很好。(我可以翻译,但可能并非所有在这里编程的人都能做到) - Olivier Gérardin
6个回答

10

虽然Alex的解决方案有效,但我不认为为了执行一个操作就要包括2个额外的依赖项(ActuatorCloud Context)。相反,我将他的答案与修改后的代码结合起来以实现我想要的功能。

因此,首先,非常关键的是使用new Thread()setDaemon(false);来执行代码。我有以下处理重启的端点方法:

val restartThread = Thread {
    logger.info("Restarting...")
    Thread.sleep(1000)
    SpringMain.restartToMode(AppMode.valueOf(change.newMode.toUpperCase()))
    logger.info("Restarting... Done.")
}
restartThread.isDaemon = false
restartThread.start()

Thread.sleep(1000)并非必须的,但我想在重新启动应用程序之前使控制器输出视图。

SpringMain.restartToMode具有以下内容:

@Synchronized fun restartToMode(mode: AppMode) {
    requireNotNull(context)
    requireNotNull(application)

    // internal logic to potentially produce a new arguments array

    // close previous context
    context.close()

    // and build new one using the new mode
    val builder = SpringApplicationBuilder(SpringMain::class.java)
    application = builder.application()
    context = builder.build().run(*argsArray)
}

当应用程序启动时,contextapplicationmain方法中获取:

val args = ArrayList<String>()
lateinit var context: ConfigurableApplicationContext
lateinit var application: SpringApplication

@Throws(Exception::class)
@JvmStatic fun main(args: Array<String>) {
    this.args += args

    val builder = SpringApplicationBuilder(SpringMain::class.java)
    application = builder.application()
    context = builder.build().run(*args)
}

我不完全确定这是否会造成任何问题。如果有的话,我会更新这个答案。希望这能对其他人有所帮助。


1
如上所述,显然这只能起作用一次,你有观察到相同的行为吗? - Olivier Gérardin

9

如果有帮助的话,这是Crembo所接受答案的一个纯Java翻译。

控制器方法:

@GetMapping("/restart")
void restart() {
    Thread restartThread = new Thread(() -> {
        try {
            Thread.sleep(1000);
            Main.restart();
        } catch (InterruptedException ignored) {
        }
    });
    restartThread.setDaemon(false);
    restartThread.start();
}

主类(仅包含重要部分):

private static String[] args;
private static ConfigurableApplicationContext context;

public static void main(String[] args) {
    Main.args = args;
    Main.context = SpringApplication.run(Main.class, args);
}

public static void restart() {
    // close previous context
    context.close();

    // and build new one
    Main.context = SpringApplication.run(Main.class, args);

}

2
好的,显然这种方法只能用一次...第二次我在context.close()上得到了一个NPE。既然它是Main类的静态成员,它怎么可能为空呢?我在这里感到困惑。 - Olivier Gérardin
它对我来说完美地运行,而且多次运行也没有问题。睡眠真的必要吗? - moritz.vieli
2
引用被接受的答案:“Thread.sleep(1000) 不是必需的,但我希望我的控制器在重新启动应用程序之前输出视图。” - Olivier Gérardin

7

如已评论,之前给出的通过线程重启实现仅能运行一次,在第二次时会抛出 NPE 错误,因为上下文为空。

可以通过让重启线程使用与初始主调用线程相同的类加载器来避免此 NPE 错误:

private static volatile ConfigurableApplicationContext context;
private static ClassLoader mainThreadClassLoader;

public static void main(String[] args) {
    mainThreadClassLoader = Thread.currentThread().getContextClassLoader();
    context = SpringApplication.run(Application.class, args);
}

public static void restart() {
    ApplicationArguments args = context.getBean(ApplicationArguments.class);

    Thread thread = new Thread(() -> {
        context.close();
        context = SpringApplication.run(Application.class, args.getSourceArgs());
    });

    thread.setContextClassLoader(mainThreadClassLoader);
    thread.setDaemon(false);
    thread.start();
}

最佳答案,它完美地运行。 - NewProgrammer
最好的答案,它完美地起作用。 - undefined

6
我使用Spring Devtools中的Restarter解决了这个问题。 将以下内容添加到pom.xml文件中:
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <scope>runtime</scope>
</dependency>

然后使用org.springframework.boot.devtools.restart.Restarter来调用此方法:
Restarter.getInstance().restart();

这对我来说可行。希望这能帮到您。


使用一些与众不同的东西比重新发明轮子更好。 - undefined

3
您可以使用spring-cloud-context依赖中的RestartEndPoint来实现编程式重启Spring Boot应用程序:
@Autowired
private RestartEndpoint restartEndpoint;

...

Thread restartThread = new Thread(() -> restartEndpoint.restart());
restartThread.setDaemon(false);
restartThread.start();

即使会抛出异常以提示可能会导致内存泄漏,但它确实有效:

Web 应用程序 [xyx] 似乎已经启动了一个名为 [Thread-6] 的线程,但未能停止它。这很可能会创建内存泄漏。 线程的堆栈跟踪:

对于另一个不同措辞的问题,提供了相同的答案: 使用 Java 函数从 Spring boot 调用 Spring 执行器 /restart 端点


1
尽管你的回答不完全符合我的需求,但我用了你的建议找到了一个适合我的解决方案。请看我的自我发布的答案。谢谢。 - Crembo

0
以下重启方法将起作用。
`@SpringBootApplication public class Application {`
private static ConfigurableApplicationContext context;

public static void main(String[] args) {
    context = SpringApplication.run(Application.class, args);
}

public static void restart() {
    ApplicationArguments args = context.getBean(ApplicationArguments.class);

    Thread thread = new Thread(() -> {
        context.close();
        context = SpringApplication.run(Application.class, args.getSourceArgs());
    });

    thread.setDaemon(false);
    thread.start();
}

}`


1
为什么你在新线程中关闭上下文?此外,SpringApplication.run(...) 不是自己启动一个线程吗?为什么要明确实例化一个新线程? - heug

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接