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

Posted

技术标签:

【中文标题】以编程方式重新启动 Spring Boot 应用程序/刷新 Spring 上下文【英文标题】:Programmatically restart Spring Boot application / Refresh Spring Context 【发布时间】:2017-01-16 02:31:05 【问题描述】:

我正在尝试以编程方式重新启动我的 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 问题)然后什么也没有发生。

我也见过Programmatically restart Spring Boot application,但这些答案似乎对我没有任何帮助。此外,我查看了 Spring Actuator,它应该具有 /restart 端点,但它似乎不再存在?

【问题讨论】:

我上次刚刚读到一些关于 spring-cloud 的内容,有一个 refresh endpoint 刷新了上下文中的所有 bean。也许你在那里发现了一些有用的东西。 Refresh Scope 为什么这个标签是“java”? 因为我不在乎是否有人有 Java 或 Kotlin 的解决方案。 Kotlin 在这个问题上没有意义。 那么 java 也不重要......如果你标记 Java,至少提供一个 Java 版本的解决方案会很好。 (我可以做翻译,但可能不是所有使用 Java 编程并登陆这里的人都能做到) 【参考方案1】:

正如已经评论的那样,之前给出的通过线程重启的实现只工作一次,第二次抛出 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();

【讨论】:

【参考方案2】:

下面的重启方法将起作用。

`@SpringBootApplication 公共类应用程序

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

`

【讨论】:

为什么要从新线程中关闭上下文?另外,SpringApplication.run(...) 本身不启动线程吗?为什么要为它显式实例化一个新线程?【参考方案3】:

我已经通过使用 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();

它对我有用。希望对您有所帮助。

【讨论】:

【参考方案4】:

如果它可能对某人有帮助,这里是 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);


【讨论】:

好吧,显然这种方法只适用一次......我第二次在 context.close() 上获得 NPE。因为它是 Main 的静态成员,它怎么可能是 null ?我在这里感到困惑。 它非常适合我,也可以多次使用。睡眠真的有必要吗? 从接受的答案中引用:“Thread.sleep(1000) 不是必需的,但我希望我的控制器在实际重新启动应用程序之前输出视图。”【参考方案5】:

尽管 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)

其中contextapplication 在启动应用程序时来自main 方法:

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)

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

【讨论】:

如上所述,显然这只有效一次,您是否观察到相同的行为?【参考方案6】:

您可以使用 RestartEndPoint(在 spring-cloud-context 依赖项中)以编程方式重新启动 Spring Boot 应用程序:

@Autowired
private RestartEndpoint restartEndpoint;

...

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

它可以工作,即使它会抛出异常来通知您这可能会导致内存泄漏:

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

为另一个问题提供了相同的答案(措辞不同):Call Spring actuator /restart endpoint from Spring boot using a java function

【讨论】:

即使您的回答不是我所需要的,我还是使用您的回答找到了适合我的解决方案。请参阅我自己发布的答案。谢谢。

以上是关于以编程方式重新启动 Spring Boot 应用程序/刷新 Spring 上下文的主要内容,如果未能解决你的问题,请参考以下文章

以编程方式配置 Spring Boot 应用程序

Spring Boot 以编程方式设置配置文件

以编程方式增加 Spring Boot 中的线程数

Spring Boot的特性: 以编程方式设置profiles

如何使用 Spring Boot 以编程方式确定当前的活动配置文件 [重复]

如何使用 Spring Boot 以编程方式确定当前的活动配置文件 [重复]