Spring Boot JAR作为Windows服务

20

我正在尝试使用procrun包装一个Spring Boot“uber JAR”文件。

以下命令按预期运行:

java -jar my.jar

我需要我的Spring Boot JAR文件在Windows启动时自动启动。 对此最好的解决方案是将JAR文件作为服务运行(与独立Tomcat相同)。

当我尝试运行此操作时,出现“Commons Daemon procrun failed with exit value: 3”的错误。

查看Spring Boot源代码,似乎它使用了自定义类加载器:

https://github.com/spring-projects/spring-boot/blob/master/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/JarLauncher.java

直接运行我的main方法时,我也会收到“ClassNotFoundException”错误:

java -cp my.jar my.MainClass

有没有一种方法可以使用Spring Boot JAR文件中的主方法(而不是通过JarLauncher)运行我的主方法?

是否有人已成功地将Spring Boot与procrun集成?

我知道http://wrapper.tanukisoftware.com/,但由于其许可证问题,我无法使用它。

更新:

现在我已成功使用procrun启动服务。

set SERVICE_NAME=MyService
set BASE_DIR=C:\MyService\Path
set PR_INSTALL=%BASE_DIR%prunsrv.exe

REM Service log configuration
set PR_LOGPREFIX=%SERVICE_NAME%
set PR_LOGPATH=%BASE_DIR%
set PR_STDOUTPUT=%BASE_DIR%stdout.txt
set PR_STDERROR=%BASE_DIR%stderr.txt
set PR_LOGLEVEL=Error

REM Path to java installation
set PR_JVM=auto
set PR_CLASSPATH=%BASE_DIR%%SERVICE_NAME%.jar

REM Startup configuration
set PR_STARTUP=auto
set PR_STARTIMAGE=c:\Program Files\Java\jre7\bin\java.exe 
set PR_STARTMODE=exe
set PR_STARTPARAMS=-jar#%PR_CLASSPATH%

REM Shutdown configuration
set PR_STOPMODE=java
set PR_STOPCLASS=TODO
set PR_STOPMETHOD=stop

REM JVM configuration
set PR_JVMMS=64
set PR_JVMMX=256

REM Install service
%PR_INSTALL% //IS//%SERVICE_NAME%

我现在只需要找出如何停止该服务。我考虑使用spring-boot执行器关闭JMX Bean来完成此操作。

目前停止服务时会发生什么是:Windows无法停止服务(但将其标记为已停止),服务仍在运行(我可以浏览到本地主机),任务管理器中没有提到该进程(这不太好!除非我眼瞎了)。


1
Spring Boot需要自定义类加载器,因为它是为Spring Boot jar格式(即包括嵌套的jar)构建的。因此,您需要以某种方式执行java -jar my.jar来启动服务。在Windows上,您可以始终使用批处理文件来启动服务...请参见https://dev59.com/O3RC5IYBdhLWcg3wFNDX - M. Deinum
我怀疑就是这样。我最初认为Spring Boot 只使用了Maven Shade插件。我会像你链接中建议的那样研究RunAsService。 - roblovelock
线程中还有更多建议可以尝试。NSSM 看起来很有前途。 - M. Deinum
希望我提到的解决方案对你有用,我也经历过同样的问题,并成功地设置了它,而且一直没有出现任何问题。 - ethesx
这可以使用Spring 1.3实现。http://docs.spring.io/spring-boot/docs/1.3.x/reference/htmlsingle/#deployment-windows - roblovelock
这应该是停止服务的诀窍: REM 停止服务 %PR_INSTALL% //SS//%SERVICE_NAME%参考: https://commons.apache.org/proper/commons-daemon/procrun.html - Hames
6个回答

9
自Spring Boot 1.3以来,使用winsw现在是可能的。 文档将引导您到一个参考实现,展示如何设置服务。

2
我们转换到WinSw,这样就不需要进行代码更改。看起来更简单易用! - r590
同样,我们使用 WinSw 在 Windows 操作系统(Win10 和 Win Server)上部署我们所有的 Spring Boot(2.3)应用程序。 - Camille

7

我遇到类似的问题,但是发现有人(Francesco Zanutto)慷慨地写了一篇关于他们努力的博客文章。 他们 的解决方案对我很有效。我不会因为他们投入的时间而获得任何荣誉。

http://zazos79.blogspot.com/2015/02/spring-boot-12-run-as-windows-service.html

他使用jvm启动和停止模式,与您的示例中看到的exe模式相比。通过这种方式,他能够扩展Spring Boot的JarLauncher来处理Windows服务的“start”和“stop”命令,这是您要实现优雅关闭的功能。

与他的示例一样,您将添加多个主要方法,具体取决于您的实现,您需要指示哪些现在应该由启动器调用。我正在使用Gradle,并只需将以下内容添加到我的build.gradle中:

springBoot{
    mainClass = 'mydomain.app.MyApplication'
}

我的Procrun安装脚本:

D:\app\prunsrv.exe //IS//MyServiceName ^
--DisplayName="MyServiceDisplayName" ^
--Description="A Java app" ^
--Startup=auto ^
--Install=%CD%\prunsrv.exe ^
--Jvm=%JAVA_HOME%\jre\bin\server\jvm.dll ^
--Classpath=%CD%\SpringBootApp-1.1.0-SNAPSHOT.jar; ^
--StartMode=jvm ^
--StartClass=mydomain.app.Bootstrap ^
--StartMethod=start ^
--StartParams=start ^
--StopMode=jvm ^
--StopClass=mydomain.app.Bootstrap ^
--StopMethod=stop ^
--StopParams=stop ^
--StdOutput=auto ^
--StdError=auto ^
--LogPath=%CD% ^
--LogLevel=Debug

JarLauncher扩展类:

package mydomain.app;


import org.springframework.boot.loader.JarLauncher;
import org.springframework.boot.loader.jar.JarFile;

public class Bootstrap extends JarLauncher {

    private static ClassLoader classLoader = null;
    private static Bootstrap bootstrap = null;

    protected void launch(String[] args, String mainClass, ClassLoader classLoader, boolean wait)
            throws Exception {
        Runnable runner = createMainMethodRunner(mainClass, args, classLoader);
        Thread runnerThread = new Thread(runner);
        runnerThread.setContextClassLoader(classLoader);
        runnerThread.setName(Thread.currentThread().getName());
        runnerThread.start();
        if (wait == true) {
            runnerThread.join();
        }
    }

    public static void start (String []args) {
        bootstrap = new Bootstrap ();
        try {
            JarFile.registerUrlProtocolHandler();
            classLoader = bootstrap.createClassLoader(bootstrap.getClassPathArchives());
            bootstrap.launch(args, bootstrap.getMainClass(), classLoader, true);
        }
        catch (Exception ex) {
            ex.printStackTrace();
            System.exit(1);
        }
    }

    public static void stop (String []args) {
        try {
            if (bootstrap != null) {
                bootstrap.launch(args, bootstrap.getMainClass(), classLoader, true);
                bootstrap = null;
                classLoader = null;
            }
        }
        catch (Exception ex) {
            ex.printStackTrace();
            System.exit(1);
        }
    }

    public static void main(String[] args) {
        String mode = args != null && args.length > 0 ? args[0] : null;
        if ("start".equals(mode)) {
            Bootstrap.start(args);
        }
        else if ("stop".equals(mode)) {
            Bootstrap.stop(args);
        }
    }

}

我的主要Spring应用程序类:

package mydomain.app;

import java.lang.management.ManagementFactory;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.ExitCodeGenerator;
import org.springframework.boot.SpringApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.ComponentScan;

@SpringBootApplication
@ComponentScan
@EnableAutoConfiguration
public class MyApplication {

    private static final Logger logger = LoggerFactory.getLogger(MyApplication.class);
    private static ApplicationContext applicationContext = null;

    public static void main(String[] args) {
        String mode = args != null && args.length > 0 ? args[0] : null;

        if (logger.isDebugEnabled()) {
            logger.debug("PID:" + ManagementFactory.getRuntimeMXBean().getName() + " Application mode:" + mode + " context:" + applicationContext);
        }
        if (applicationContext != null && mode != null && "stop".equals(mode)) {
            System.exit(SpringApplication.exit(applicationContext, new ExitCodeGenerator() {
                @Override
                public int getExitCode() {
                    return 0;
                }
            }));
        }
        else {
            SpringApplication app = new SpringApplication(MyApplication.class);
            applicationContext = app.run(args);
            if (logger.isDebugEnabled()) {
                logger.debug("PID:" + ManagementFactory.getRuntimeMXBean().getName() + " Application started context:" + applicationContext);
            }
        }
    }
}

谢谢您的回答。我更改了我的部署方式,不再使用fat jar(因此没有类加载器)。我借用了您的Spring应用程序类,并将其作为Procrun脚本中的StartClass和StopClass来使用。 - Nathan
这在Spring Boot 1.3.X版本中非常好用。但在1.4.0版本中,它会生成编译错误,因为createMainMethodRunner(mainClass, args, classLoader)不再返回Runnable。 - Michał Maciej Gałuszka
1
Francesco Zanutto为Spring Boot 1.4更新的解决方案在此处https://github.com/francesc79/test-procrun。 - Michał Maciej Gałuszka

5

从springboot v1.2.2开始,使用procrun无法干净地关闭打包为超级jar的Spring Boot应用程序。请确保关注这些问题,因为其他人也在询问此事:

“目前尚不清楚Spring Boot维护者将如何处理此问题。同时,考虑解压超级JAR文件并忽略Spring Boot的JarLauncher。”“我最初对这个问题的回答(可以在历史记录中查看)提出了一种应该可以工作(我认为是可以的)的方法,但由于JarLauncher中类加载器的处理方式,该方法无法实现。”

我的答案正好修复了你提到的类加载问题。该拉取请求具有“共享”启动器,可以在启动和停止调用之间“共享”类加载器(即org.springframework.boot.loader.SharedWarLauncher或在您的情况下是org.springframework.boot.loader.SharedJarLauncher)。他们决定不合并,称他们已经添加了使用MBeans处理它的方法。 - Andrew Wynham

3

我按照https://github.com/spring-projects/spring-boot/issues/519#issuecomment-74736497使用了您的procrun配置,并且它对我很有效。我比其他答案更喜欢它,因为它不需要自定义引导类。感谢您修复此问题! - Nathan
2
我很高兴它对你有帮助!我希望这个拉取请求已经被合并了,但他们决定使用MBeans来进行关闭。所以,顺便提一下,虽然配置看起来像是可以工作的,但如果你使用spring-boot-plugin打包应用程序,它将无法正常关闭,因为在运行StopMethod时会创建一个全新的ApplicationContext。这就是为什么我添加了org.springframework.boot.loader.SharedWarLauncher,它在调用启动或停止时保留类加载器。我本可以轻松修改org.springframework.boot.loader.WarLauncher,但我试图不引人注目。 - Andrew Wynham
非常感谢您的回复。我原本以为一切都按预期工作,但是当我检查日志时发现应用程序没有像您提到的那样被干净地关闭。我不需要在部署中使用fat jar,因此我切换到将所有jar文件放在lib文件夹中的部署,并实现了一个类似于@ethesx答案中的“stop”函数。 - Nathan

3

远离winsw,它是用.NET制作的,我在客户环境中遇到了很多关于过时的Windows的问题。

我推荐NSSM,它是使用纯C编写的,我在所有过时的Windows上都使用它而没有任何问题。 它具有相同的功能以及更多...

这里是一个批处理脚本(.bat)示例如何使用它:

rem Register the service
nssm install my-java-service "C:\Program Files\Java\jre1.8.0_152\bin\java.exe" "-jar" "snapshot.jar"
rem Set the service working dir
nssm set my-java-service AppDirectory "c:\path\to\jar-diretory"
rem Redirect sysout to file
nssm set my-java-service AppStdout "c:\path\to\jar-diretory\my-java-service.out"
rem Redirect syserr to file
nssm set my-java-service AppStderr "c:\path\to\jar-diretory\my-java-service.err"
rem Enable redirection files rotation
nssm set my-java-service AppRotateFiles 1
rem Rotate files while service is running
nssm set my-java-service AppRotateOnline 1
rem Rotate files when they reach 10MB
nssm set my-java-service AppRotateBytes 10485760
rem Stop service when my-java-service exits/stop
nssm set my-java-service AppExit Default Exit
rem Restart service when my-java-service exits with code 2 (self-update)
nssm set my-java-service AppExit 2 Restart
rem Set the display name for the service
nssm set my-java-service DisplayName "My JAVA Service"
rem Set the description for the service
nssm set my-java-service Description "Your Corp"
rem Remove old rotated files (older than 30 days)
nssm set my-java-service AppEvents Rotate/Pre "cmd /c forfiles /p \"c:\path\to\jar-diretory\" /s /m \"my-java-service-*.*\" /d -30 /c \"cmd /c del /q /f @path\""
rem Make a copy of my-java-service.jar to snapshot.jar to leave the original JAR unlocked (for self-update purposes)
nssm set my-java-service AppEvents Start/Pre "cmd /c copy /y \"c:\path\to\jar-diretory\my-java-service.jar\" \"c:\path\to\jar-diretory\snapshot.jar\""

我并没有在WinSW中遇到过不良行为,但我们确实只使用“新”的Windows版本。曾经,一个Windows更新删除了服务,但在打补丁更新后重新安装解决了问题。 - Camille

0
更新截至2023年。
对于使用spring-boot-plugin构建的Spring Boot应用程序(当前版本为2.7.9),可以选择使用procrun的StartMode选项作为exe,而不是其他选项。
从概念上讲,这与Unix/Linux使用init.d服务提供脚本的方法类似。
整个解决方案包括预编译一些可执行文件来启动/停止Java应用程序,并让Spring将PID写入文件。我们开始吧!
Spring应用程序
唯一的变化在于main方法。使用ApplicationPidFileWriter将PID(进程ID)写入文件,稍后我们将使用该文件来关闭应用程序(ref)。
//... main method within a class that uses all the annotations you need.
public static void main(String[] args) {
  SpringApplicationBuilder app = new SpringApplicationBuilder(ServiceMainApp.class);
  app.build().addListeners(new ApplicationPidFileWriter("./shutdown.pid"));
  app.run(args);
}

启动/停止可执行文件

我选择C语言,因为它是获取适用于任何平台上运行Java应用程序的可执行文件最简单的方式,但你也可以轻松地使用任何其他编译成可执行文件的语言来编写它们。

start.c

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main(int argc, char** argv) {
  if (argc < 2) {
    return -1;
  }
  char command[1024];
  memset(&command, 0x00, sizeof(command));
  strcat(command, "java -jar");
  for (int index=1; index < argc; index++) {
    strcat(command, " ");
    strcat(command, argv[index]);
  }
  printf("%s\n", command);
  system(command);
  return 0;
}

请注意,我们将所有参数传递给java -jar。这一点非常重要。
编译为start.exe。

stop.c

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main(int argc, char **argv) {
  if (argc < 2) {
    return -1;
  }
  FILE * pidfile; 
  char pid[50];
  char command[1024];
  
  memset(&command, 0x00, sizeof(command));
  memset(&pid, 0x00, sizeof(pid));
  
  pidfile = fopen(argv[1], "r");
  fgets(pid, sizeof(pid), pidfile);
  fclose(pidfile);
  
  sprintf(command, "taskkill /F /PID %s", pid);
  
  printf("%s\n", command);
  system(command);
  return 0;
}

注意:不幸的是,在Windows中,我们必须使用/F选项来强制关闭应用程序,因为Java无法正确处理WM_CLOSE信号,这是taskkill发送的(ref)。在其他平台上,您可以发送正确的SIGINT给您的Spring Boot应用程序。

编译为stop.exe

Procrun

我正在使用一个脚本与服务进行交互,但您也可以轻松使用命令行选项。

相关参数如下:

rem Startup setup
set "PR_STARTUP=auto"
set "PR_STARTMODE=exe"
set "PR_STARTIMAGE=%PATH_TO%\start.exe"
set "PR_STARTPATH=%YOUR_PATH%"
set "PR_STARTPARAMS=%PATH_TO%\my-application.jar#--spring.profiles.active=...#--server.port=9094"

rem Shutdown setup
set "PR_STOPMODE=exe"
set "PR_STOPIMAGE=%PATH_TO%\stop.exe"
set "PR_STOPPATH=%YOUR_PATH%"
set "PR_STOPPARAMS=%PATH_TO%\shutdown.pid"

注意,StartParams中包括指向Spring Boot jar文件的路径以及其他所需选项,用#分隔。这就是为什么C应用程序将所有参数传递给java -jar命令的原因。

整个混乱的利弊

  • 对您的Spring应用程序的更改很小,只需使用PID写入器。
  • 可执行文件实际上非常通用,因为它们不依赖于任何特定内容(我将它们放在了GitHub上:https://github.com/vinceynhz/procrun-springboot-exe),因此可以与部署服务器上的许多应用程序一起使用。

  • 您必须预编译启动/停止并将其部署到部署服务器上才能使其正常工作。根据您的CI/CD或部署设置,这可能很容易或很难实现。
  • 您需要确保具有写入权限的文件夹中存在jar文件,以便Spring Boot可以写入PID文件。

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