Spring boot JAR as windows service

19.4k Views Asked by At

I am trying to wrap a spring boot "uber JAR" with procrun.

Running the following works as expected:

java -jar my.jar

I need my spring boot jar to automatically start on windows boot. The nicest solution for this would be to run the jar as a service (same as a standalone tomcat).

When I try to run this I am getting "Commons Daemon procrun failed with exit value: 3"

Looking at the spring-boot source it looks as if it uses a custom classloader:

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

I also get a "ClassNotFoundException" when trying to run my main method directly.

java -cp my.jar my.MainClass

Is there a method I can use to run my main method in a spring boot jar (not via JarLauncher)?

Has anyone successfully integrated spring-boot with procrun?

I am aware of http://wrapper.tanukisoftware.com/. However due to their licence I can't use it.

UPDATE

I have now managed to start the service using 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%

I now just need to workout how to stop the service. I am thinking of doing someting with the spring-boot actuator shutdown JMX Bean.

What happens when I stop the service at the moment is; windows fails to stop the service (but marks it as stopped), the service is still running (I can browse to localhost), There is no mention of the process in task manager (Not very good! unless I am being blind).

6

There are 6 best solutions below

2
On BEST ANSWER

This is now possible since Spring Boot 1.3 using winsw.

The documentation directs you to a reference implementation which shows how to setup a service.

3
On

Just ran into this and wanted to share, I fixed this issue a while back and issued a pull request. https://github.com/spring-projects/spring-boot/pull/2520

You can use my forked version until it gets merged to start/stop using procrun.

3
On

I ran into similar issues but found someone else (Francesco Zanutto) was gracious enough to write a blog post about their efforts. Their solution worked for me. I take no credit for the time they put into realizing this code.

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

He's using the jvm start and stop mode, compared to the exe mode I see in your example. With this, he's able to extend Spring Boot's JarLauncher to handle both "start" and "stop" commands from Windows services' which I believe you're looking to do for a graceful shutdown.

As with his examples you'll be adding multiple main methods, depending on your implementation, you will need to indicate which should now be invoked by the launcher. I'm using Gradle and simply had to add the following to my build.gradle:

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

My Procrun installation script:

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 extension class:

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

}

My main Spring application class:

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

Update as of 2023.

For Spring Boot applications (currently using 2.7.9) built with the spring-boot-plugin (the uber-jar as mentioned by @andrew-wynham), an option could be to use procrun's StartMode as exe instead of the other options.

This is conceptually a similar approach to that of Unix/Linux using init.d services providing a script.

The whole solution includes precompiling a couple of executables to start/stop the java application, and let Spring write the PID to a file. Let's go to it!

The Spring application

The only change here is on the main method. Use ApplicationPidFileWriter to write the PID (Process Id) to a file that we'll use later to shutdown the application (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);
}

The start/stop executables

I chose C since it's the easiest way to get executables for whichever platform you will run your java application, but you can just as easily write them in any other language that compiles to an executable.

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

Note that we are passing to java -jar all of the parameters. This will be important.

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

Note: Unfortunately, in Windows we have to use /F option to forcefully shutdown the application, because Java doesn't handle correctly the WM_CLOSE signal that taskkill sends (ref). In other platforms you may be able to send the correct SIGINT to your Spring Boot application.

Compile to stop.exe

Procrun

I'm using a script to interact with the service, but you can easily use the command line options.

The relevant parameters are:

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"

Note that the StartParams include the path to the Spring Boot jar and any other options needed separated by #. This is why the C application passes all parameters to the java -jar command.

Pros/Cons of this whole mess

Pros

  • The changes to your spring application are minimal, just to use the PID writer.
  • The executables are actually very generic since they don't rely on anything specific (I put them in github: https://github.com/vinceynhz/procrun-springboot-exe), so they can be used with many applications in your deployment servers.

Cons

  • You must precompile the start/stop and deploy them to your deployment server for this to work. Depending on your CI/CD or deployment setup this may or may not be easy to achieve.
  • You need to guarantee write permissions on the folder where the jar is located so the PID file can be written by Spring Boot.
1
On

As of springboot v1.2.2 there is no clean way to shutdown a Spring Boot application, packaged as an uber jar, using procrun. Be sure to follow these issues as this is something others are asking about as well:

It's not clear if/how springboot maintainers are going to handle it. In the meantime, consider unzipping the uber jar and ignoring Spring Boot's JarLauncher.

My original answer to this question (viewable in history) proposed a way that should work (and I thought did), but does not due to how the classloader is handled in JarLauncher.

1
On

Stay away of winsw, It's made with .NET, and I experience a lot of problems about my customers environment about out-of-date Windows.

I recommend NSSM, it's made using pure C, and I used it on all my out-of-date Windows without problems. It has the same features and more...

Here is a batch script (.bat) sample how to use it:

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\""