20

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).

roblovelock
  • 1,971
  • 2
  • 23
  • 41
  • 1
    Spring Boot needs the custom class loader as that is specify build for the Spring Boot jar format (i.e. including the nested jars). So no you will need to execute `java -jar my.jar` somehow to start the service. When on windows you can always use a batch file to launch the service… See http://stackoverflow.com/questions/415409/run-batch-file-as-a-windows-service – M. Deinum Jun 09 '14 at 18:22
  • I suspected that was the case. I originally thought spring boot was only using the maven shade plugin. I will look into RunAsService as suggested in your link – roblovelock Jun 10 '14 at 08:00
  • There are more suggestions in the thread you might want to try. [NSSM](http://nssm.cc/usage) looks promising. – M. Deinum Jun 10 '14 at 08:08
  • Hopefully the solution I mentioned works for you, I had been through the same and was able to get this set up without too much trouble and has been working without an issue for some time now. – ethesx Jul 22 '15 at 16:06
  • This is possible using Spring 1.3 http://docs.spring.io/spring-boot/docs/1.3.x/reference/htmlsingle/#deployment-windows – roblovelock Mar 14 '16 at 11:10
  • This should do the trick to stop the service: `REM Stop service` `%PR_INSTALL% //SS//%SERVICE_NAME%` Reference: https://commons.apache.org/proper/commons-daemon/procrun.html – Hames Oct 14 '17 at 14:13

6 Answers6

9

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.

roblovelock
  • 1,971
  • 2
  • 23
  • 41
  • 2
    We switched to WinSw so that code changes were not required. It seems a lot simpler and easier to use! – r590 May 08 '18 at 22:14
  • Same here, we use WinSw to deploy all our Spring Boot (2.3) apps on Windows OS (Win10 and Win Server) – Camille Apr 16 '21 at 10:50
7

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);
            }
        }
    }
}
ethesx
  • 1,339
  • 5
  • 19
  • 35
  • Thanks for this answer. I changed my deployment to not use the fat jar (so there's no classloader). I borrowed your Spring application class and I just use that as the StartClass and StopClass in the Procrun script. – Nathan Jul 08 '16 at 04:39
  • This works really fine up to Spring Boot version 1.3.X. In 1.4.0 it generates compile error as createMainMethodRunner(mainClass, args, classLoader); doesn't return Runnable any more. – Michał Maciej Gałuszka Aug 23 '16 at 08:14
  • 1
    Solution updated by Francesco Zanutto for Spring Boot 1.4 is here https://github.com/francesc79/test-procrun – Michał Maciej Gałuszka Aug 24 '16 at 09:04
5

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.

kaliatech
  • 17,579
  • 5
  • 72
  • 84
  • my answer fixes exactly this classloading issue you mention. The pull request has "Shared" launchers that "share" the classloader between invocations of start and stop (i.e. `org.springframework.boot.loader.SharedWarLauncher` or in your case `org.springframework.boot.loader.SharedJarLauncher`) They decided not to merge citing that they have added ways of handling it with MBeans. – Andrew Wynham Jul 07 '16 at 11:48
3

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.

Andrew Wynham
  • 2,310
  • 21
  • 25
  • I used your procrun config as per https://github.com/spring-projects/spring-boot/issues/519#issuecomment-74736497 and it worked for me. I like it better than the other answers because it doesn't need a custom bootstrap class. Thanks for contributing this fix! – Nathan Jul 06 '16 at 00:40
  • 2
    I am happy it helped you! I wish the pull request had been merged but they decided to use MBeans for shutdown. So FYI, while the config may look like it works, if you package your application with the spring-boot-plugin it won't shut down cleanly because it will create a brand new ApplicationContext when running the `StopMethod`. That's why I added `org.springframework.boot.loader.SharedWarLauncher` which holds on to the classloader between invoking start or stop. I could have easily just modified `org.springframework.boot.loader.WarLauncher` instead but was trying to be unobtrusive. – Andrew Wynham Jul 07 '16 at 11:44
  • Thanks very much for your reply. I had thought everything was working as expected, but when I checked the logs I found that the app wasn't being shutdown cleanly as you mention. I don't need the fat jar in my deployment, so I switched to a deployment with all the jars in a lib folder and implemented a "stop" function similar to the answer from @ethesx – Nathan Jul 08 '16 at 04:37
3

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\""
Beto Neto
  • 3,962
  • 7
  • 47
  • 81
  • Yet I didn't experiment bad behavior with WinSW, but indeed we use only 'new' version of Windows. Once, service was delete by a windows update, reinstall it after patch update did the trick. – Camille Apr 16 '21 at 10:54
0

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.
Vince
  • 1