¿Cómo cerrar una aplicación Spring Boot de forma correcta?

120

En el Spring Boot Document, dijeron que 'Cada SpringApplication registrará un gancho de apagado con la JVM para garantizar que ApplicationContext se cierre correctamente al salir'.

Cuando hago clic ctrl+cen el comando de shell, la aplicación se puede cerrar sin problemas. Si ejecuto la aplicación en una máquina de producción, tengo que usar el comando java -jar ProApplicaton.jar. Pero no puedo cerrar la terminal de shell, de lo contrario cerrará el proceso.

Si ejecuto un comando como nohup java -jar ProApplicaton.jar &, no puedo usarlo ctrl+cpara apagarlo con gracia.

¿Cuál es la forma correcta de iniciar y detener una aplicación Spring Boot en el entorno de producción?

Chris
fuente
Dependiendo de su configuración, el PID de la aplicación se escribe en el archivo. Puede enviar una señal de muerte a ese PID. Vea también los comentarios en este número .
M. Deinum
¿Qué señal debo usar? No creo que kill -9 sea una buena idea, ¿verdad?
Chris
5
Por eso te señalé ese hilo ... Pero algo como kill -SIGTERM <PID>debería funcionar.
M. Deinum
matar con pid, no -9
Dapeng
1
kill $ (lsof -ti tcp: <port>) - en caso de que no quiera usar el actuador y necesite una muerte rápida
Alex Nolasco

Respuestas:

62

Si está utilizando el módulo actuador, puede apagar la aplicación a través de JMXo HTTPsi el punto final está habilitado.

agregar a application.properties:

endpoints.shutdown.enabled = true

La siguiente URL estará disponible:

/actuator/shutdown - Permite que la aplicación se cierre correctamente (no habilitado de forma predeterminada).

Dependiendo de cómo se exponga un punto final, el parámetro sensible puede usarse como una sugerencia de seguridad.

Por ejemplo, los puntos finales sensibles requerirán un nombre de usuario / contraseña cuando se acceda a ellos HTTP(o simplemente deshabilitados si la seguridad web no está habilitada).

De la documentación de Spring Boot

Jean-Philippe Bond
fuente
1
No quiero incluir módulo actuador. Pero encontré que, en el registro de mi consola, Spring Boot imprime el PID en la primera línea. ¿Hay alguna forma de permitir que Spring Boot imprima el PID en otro archivo sin agregar el módulo actuador (ApplicationPidListener)?
Chris
2
Tenga en cuenta que esto debe ser una publicación http para este punto final. Puede habilitarlo con endpoints.shutdown.enabled = true
sparkyspider
54

Aquí hay otra opción que no requiere que cambie el código o exponga un punto final apagado. Cree los siguientes scripts y utilícelos para iniciar y detener su aplicación.

start.sh

#!/bin/bash
java -jar myapp.jar & echo $! > ./pid.file &

Inicia su aplicación y guarda la identificación del proceso en un archivo

stop.sh

#!/bin/bash
kill $(cat ./pid.file)

Detiene su aplicación usando el ID de proceso guardado

start_silent.sh

#!/bin/bash
nohup ./start.sh > foo.out 2> foo.err < /dev/null &

Si necesita iniciar la aplicación usando ssh desde una máquina remota o una canalización de CI, utilice este script en su lugar para iniciar su aplicación. Usar start.sh directamente puede dejar el shell colgando.

Después, por ejemplo. re / implementando su aplicación puede reiniciarla usando:

sshpass -p password ssh -oStrictHostKeyChecking=no userName@www.domain.com 'cd /home/user/pathToApp; ./stop.sh; ./start_silent.sh'
Jens
fuente
Esta debería ser la respuesta. Acabo de confirmar que una señal de apagado le dice a Spring que cague con gracia.
Chad
1
¿Por qué no llama a la ejecución java - jar con nohup dentro de start.sh, en lugar de llamar a la ejecución java - jar dentro de start.sh que se llama con nohup dentro de otro script de shell externo?
Anand Varkey Philips
2
@AnandVarkeyPhilips La única razón es que a veces llamo a start.sh desde la línea de comando con fines de prueba, pero si siempre lo necesita nohup, puede combinar los comandos
Jens
@Jens, Gracias por la información. ¿Puedes decirme que haces esto? Foo.out 2> foo.err </ dev / null &
Anand Varkey Philips
1
@jens, gracias. Lo he hecho con éxito y he publicado mi secuencia de comandos de inicio y parada aquí abajo. ( stackoverflow.com/questions/26547532/… )
Anand Varkey Philips
52

En cuanto a la respuesta de @ Jean-Philippe Bond,

aquí hay un ejemplo rápido de maven para que el usuario de maven configure el punto final HTTP para apagar una aplicación web de arranque de primavera usando spring-boot-starter-actuator para que pueda copiar y pegar:

1.Maven pom.xml:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

2.aplicaciones.propiedades:

#No auth  protected 
endpoints.shutdown.sensitive=false

#Enable shutdown endpoint
endpoints.shutdown.enabled=true

Todos los puntos finales se enumeran aquí :

3.Envíe un método de publicación para cerrar la aplicación:

curl -X POST localhost:port/shutdown

Nota de seguridad:

si necesita el método de apagado protegido por autenticación, también puede necesitar

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
</dependency>

configurar detalles :

Jaskey
fuente
Después del segundo paso, al intentar implementar, se encontró este mensaje de error: Descripción: El parámetro 0 del método setAuthenticationConfiguration en org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter requería un bean de tipo 'org.springframework.security.config .annotation.authentication.configuration.AuthenticationConfiguration 'que no se pudo encontrar. Acción: considere definir un bean de tipo 'org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration' en su configuración.
Roberto
2
Tenga en cuenta: si he incluido algo como server.contextPath=/appNameen mi application.properties Entonces, el comando de apagado se convertiría en: curl -X POST localhost:8080/appName/shutdown Espero que pueda ayudar a alguien. Tuve que luchar mucho por este error.
Naveen Kumar
Con Spring Boot 1.5.8 se sugiere, si no tiene seguridad, tener en application.properties endpoints.shutdown.enabled=true management.security.enabled=false.
Stefano Scarpanti
Si no desea exponer el punto final y usar el archivo PID para detener e iniciar a través de un script de shell, intente esto: stackoverflow.com/questions/26547532/…
Anand Varkey Philips
27

Puede hacer que la aplicación springboot escriba el PID en un archivo y puede usar el archivo pid para detener o reiniciar u obtener el estado usando un script bash. Para escribir el PID en un archivo, registre un oyente en SpringApplication usando ApplicationPidFileWriter como se muestra a continuación:

SpringApplication application = new SpringApplication(Application.class);
application.addListeners(new ApplicationPidFileWriter("./bin/app.pid"));
application.run();

Luego, escriba un script bash para ejecutar la aplicación Spring Boot. Referencia .

Ahora puede utilizar el script para iniciar, detener o reiniciar.

nav3916872
fuente
El script de shell de inicio y detención completo compatible con Jenkins y genérico se puede encontrar aquí ( stackoverflow.com/questions/26547532/… )
Anand Varkey Philips
14

Todas las respuestas parecen no tener en cuenta el hecho de que es posible que deba completar una parte del trabajo de manera coordinada durante el cierre ordenado (por ejemplo, en una aplicación empresarial).

@PreDestroyle permite ejecutar código de apagado en los beans individuales. Algo más sofisticado se vería así:

@Component
public class ApplicationShutdown implements ApplicationListener<ContextClosedEvent> {
     @Autowired ... //various components and services

     @Override
     public void onApplicationEvent(ContextClosedEvent event) {
         service1.changeHeartBeatMessage(); // allows loadbalancers & clusters to prepare for the impending shutdown
         service2.deregisterQueueListeners();
         service3.finishProcessingTasksAtHand();
         service2.reportFailedTasks();
         service4.gracefullyShutdownNativeSystemProcessesThatMayHaveBeenLaunched(); 
         service1.eventLogGracefulShutdownComplete();
     }
}
Michal
fuente
Esto es justo lo que necesitaba. Después de ejecutar la aplicación, presione ctrl-c Gracias @Michal
Claudio Moscoso
8

No expongo ningún punto final y comienzo ( con nohup en segundo plano y sin los archivos creados a través de nohup ) y me detengo con el script de shell (con KILL PID con gracia y fuerzo la eliminación si la aplicación aún se está ejecutando después de 3 minutos ). Acabo de crear jar ejecutable y uso el escritor de archivos PID para escribir el archivo PID y almacenar Jar y Pid en la carpeta con el mismo nombre que el nombre de la aplicación y los scripts de shell también tienen el mismo nombre con inicio y parada al final. También llamo a estos script de parada y script de inicio a través de la tubería de jenkins. Hasta ahora no hay problemas. Funciona perfectamente para 8 aplicaciones (scripts muy genéricos y fáciles de aplicar para cualquier aplicación).

Clase principal

@SpringBootApplication
public class MyApplication {

    public static final void main(String[] args) {
        SpringApplicationBuilder app = new SpringApplicationBuilder(MyApplication.class);
        app.build().addListeners(new ApplicationPidFileWriter());
        app.run();
    }
}

ARCHIVO YML

spring.pid.fail-on-write-error: true
spring.pid.file: /server-path-with-folder-as-app-name-for-ID/appName/appName.pid

Aquí está el script de inicio (start-appname.sh):

#Active Profile(YAML)
ACTIVE_PROFILE="preprod"
# JVM Parameters and Spring boot initialization parameters
JVM_PARAM="-Xms512m -Xmx1024m -Dspring.profiles.active=${ACTIVE_PROFILE} -Dcom.webmethods.jms.clientIDSharing=true"
# Base Folder Path like "/folder/packages"
CURRENT_DIR=$(readlink -f "$0")
BASE_PACKAGE="${CURRENT_DIR%/bin/*}"
# Shell Script file name after removing path like "start-yaml-validator.sh"
SHELL_SCRIPT_FILE_NAME=$(basename -- "$0")
# Shell Script file name after removing extension like "start-yaml-validator"
SHELL_SCRIPT_FILE_NAME_WITHOUT_EXT="${SHELL_SCRIPT_FILE_NAME%.sh}"
# App name after removing start/stop strings like "yaml-validator"
APP_NAME=${SHELL_SCRIPT_FILE_NAME_WITHOUT_EXT#start-}

PIDS=`ps aux |grep [j]ava.*-Dspring.profiles.active=$ACTIVE_PROFILE.*$APP_NAME.*jar | awk {'print $2'}`
if [ -z "$PIDS" ]; then
  echo "No instances of $APP_NAME with profile:$ACTIVE_PROFILE is running..." 1>&2
else
  for PROCESS_ID in $PIDS; do
        echo "Please stop the process($PROCESS_ID) using the shell script: stop-$APP_NAME.sh"
  done
  exit 1
fi

# Preparing the java home path for execution
JAVA_EXEC='/usr/bin/java'
# Java Executable - Jar Path Obtained from latest file in directory
JAVA_APP=$(ls -t $BASE_PACKAGE/apps/$APP_NAME/$APP_NAME*.jar | head -n1)
# To execute the application.
FINAL_EXEC="$JAVA_EXEC $JVM_PARAM -jar $JAVA_APP"
# Making executable command using tilde symbol and running completely detached from terminal
`nohup $FINAL_EXEC  </dev/null >/dev/null 2>&1 &`
echo "$APP_NAME start script is  completed."

Aquí está el script de detención (stop-appname.sh):

#Active Profile(YAML)
ACTIVE_PROFILE="preprod"
#Base Folder Path like "/folder/packages"
CURRENT_DIR=$(readlink -f "$0")
BASE_PACKAGE="${CURRENT_DIR%/bin/*}"
# Shell Script file name after removing path like "start-yaml-validator.sh"
SHELL_SCRIPT_FILE_NAME=$(basename -- "$0")
# Shell Script file name after removing extension like "start-yaml-validator"
SHELL_SCRIPT_FILE_NAME_WITHOUT_EXT="${SHELL_SCRIPT_FILE_NAME%.*}"
# App name after removing start/stop strings like "yaml-validator"
APP_NAME=${SHELL_SCRIPT_FILE_NAME_WITHOUT_EXT:5}

# Script to stop the application
PID_PATH="$BASE_PACKAGE/config/$APP_NAME/$APP_NAME.pid"

if [ ! -f "$PID_PATH" ]; then
   echo "Process Id FilePath($PID_PATH) Not found"
else
    PROCESS_ID=`cat $PID_PATH`
    if [ ! -e /proc/$PROCESS_ID -a /proc/$PROCESS_ID/exe ]; then
        echo "$APP_NAME was not running with PROCESS_ID:$PROCESS_ID.";
    else
        kill $PROCESS_ID;
        echo "Gracefully stopping $APP_NAME with PROCESS_ID:$PROCESS_ID..."
        sleep 5s
    fi
fi
PIDS=`/bin/ps aux |/bin/grep [j]ava.*-Dspring.profiles.active=$ACTIVE_PROFILE.*$APP_NAME.*jar | /bin/awk {'print $2'}`
if [ -z "$PIDS" ]; then
  echo "All instances of $APP_NAME with profile:$ACTIVE_PROFILE has has been successfully stopped now..." 1>&2
else
  for PROCESS_ID in $PIDS; do
    counter=1
    until [ $counter -gt 150 ]
        do
            if ps -p $PROCESS_ID > /dev/null; then
                echo "Waiting for the process($PROCESS_ID) to finish on it's own for $(( 300 - $(( $counter*5)) ))seconds..."
                sleep 2s
                ((counter++))
            else
                echo "$APP_NAME with PROCESS_ID:$PROCESS_ID is stopped now.."
                exit 0;
            fi
    done
    echo "Forcefully Killing $APP_NAME with PROCESS_ID:$PROCESS_ID."
    kill -9 $PROCESS_ID
  done
fi
Anand Varkey Philips
fuente
7

Spring Boot proporcionó varios oyentes de aplicaciones mientras intentaba crear el contexto de la aplicación, uno de ellos es ApplicationFailedEvent. Podemos utilizar para saber si el contexto de la aplicación se ha inicializado o no.

    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.boot.context.event.ApplicationFailedEvent; 
    import org.springframework.context.ApplicationListener;

    public class ApplicationErrorListener implements 
                    ApplicationListener<ApplicationFailedEvent> {

        private static final Logger LOGGER = 
        LoggerFactory.getLogger(ApplicationErrorListener.class);

        @Override
        public void onApplicationEvent(ApplicationFailedEvent event) {
           if (event.getException() != null) {
                LOGGER.info("!!!!!!Looks like something not working as 
                                expected so stoping application.!!!!!!");
                         event.getApplicationContext().close();
                  System.exit(-1);
           } 
        }
    }

Agregue a la clase de escucha anterior a SpringApplication.

    new SpringApplicationBuilder(Application.class)
            .listeners(new ApplicationErrorListener())
            .run(args);  
usuario3137438
fuente
¡La mejor respuesta que encontré! ¡Gracias!
Vagif
[@ user3137438], ¿En qué se diferencia de iniciar sesión dentro de la anotación antes de destruir?
Anand Varkey Philips
6

A partir de Spring Boot 2.3 y versiones posteriores, hay un elegante mecanismo de apagado integrado .

Antes de Spring Boot 2.3 , no hay un mecanismo de apagado elegante listo para usar. Algunos arrancadores de arranque de primavera proporcionan esta funcionalidad:

  1. https://github.com/jihor/hiatus-spring-boot
  2. https://github.com/gesellix/graceful-shutdown-spring-boot
  3. https://github.com/corentin59/spring-boot-graceful-shutdown

Soy el autor del nr. 1. El motor de arranque se llama "Hiatus for Spring Boot". Funciona en el nivel del equilibrador de carga, es decir, simplemente marca el servicio como OUT_OF_SERVICE, sin interferir con el contexto de la aplicación de ninguna manera. Esto permite hacer un cierre ordenado y significa que, si es necesario, el servicio se puede poner fuera de servicio durante algún tiempo y luego volver a la vida. La desventaja es que no detiene la JVM, tendrás que hacerlo con el killcomando. Como ejecuto todo en contenedores, esto no fue un gran problema para mí, porque tendré que detenerme y retirar el contenedor de todos modos.

Los números 2 y 3 se basan más o menos en esta publicación de Andy Wilkinson. Funcionan en un solo sentido: una vez que se activan, finalmente cierran el contexto.

jihor
fuente
5

SpringApplication registra implícitamente un gancho de cierre con la JVM para garantizar que ApplicationContext se cierre correctamente al salir. Eso también llamará a todos los métodos bean anotados con @PreDestroy. Eso significa que no tenemos que usar explícitamente el registerShutdownHook()método de a ConfigurableApplicationContexten una aplicación de arranque, como tenemos que hacer en la aplicación Spring Core.

@SpringBootConfiguration
public class ExampleMain {
    @Bean
    MyBean myBean() {
        return new MyBean();
    }

    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(ExampleMain.class, args);
        MyBean myBean = context.getBean(MyBean.class);
        myBean.doSomething();

        //no need to call context.registerShutdownHook();
    }

    private static class MyBean {

        @PostConstruct
        public void init() {
            System.out.println("init");
        }

        public void doSomething() {
            System.out.println("in doSomething()");
        }

        @PreDestroy
        public void destroy() {
            System.out.println("destroy");
        }
    }
}
Shruti Gupta
fuente
Como alternativa a @PostConstructy @PreDestroyutilicé los atributos initMethody destroyMethoddentro de la @Beananotación. Así que para este ejemplo: @Bean(initMethod="init", destroyMethod="destroy").
acker9
Un inconveniente que @PreDestroyalgunos desarrolladores pueden no saber es que estos métodos solo se llaman para beans con alcance Singleton. Los desarrolladores deben administrar la parte de limpieza del ciclo de vida del bean para otros ámbitos
asgs
2

Hay muchas formas de cerrar una aplicación de primavera. Uno es llamar a close () en ApplicationContext:

ApplicationContext ctx =
    SpringApplication.run(HelloWorldApplication.class, args);
// ...
ctx.close()

Su pregunta sugiere que desea cerrar su aplicación haciendo esto Ctrl+C, que se usa frecuentemente para terminar un comando. En este caso...

El uso endpoints.shutdown.enabled=trueno es la mejor receta. Significa que expone un punto final para finalizar su aplicación. Entonces, dependiendo de su caso de uso y su entorno, tendrá que protegerlo ...

Ctrl+Cdebería funcionar muy bien en tu caso. Supongo que su problema es causado por el signo comercial (&) Más explicación:

Un contexto de aplicación Spring puede haber registrado un gancho de cierre con el tiempo de ejecución de JVM. Consulte la documentación de ApplicationContext .

No sé si Spring Boot configura este gancho automáticamente como dijiste. Asumo que lo es.

Encendido Ctrl+C, su shell envía una INTseñal a la aplicación de primer plano. Significa "interrumpa su ejecución". La aplicación puede atrapar esta señal y hacer una limpieza antes de su terminación (el gancho registrado por Spring), o simplemente ignorarla (mal).

nohupEs un comando que ejecuta el siguiente programa con una trampa para ignorar la señal HUP. HUP se usa para terminar el programa cuando cuelga (cierre su conexión ssh, por ejemplo). Además, redirige las salidas para evitar que su programa se bloquee en un TTY desaparecido. nohupNO ignora la señal INT. Entonces NO impide Ctrl+Cque funcione.

Supongo que su problema es causado por el ampersand (&), no por nohup. Ctrl+Cenvía una señal a los procesos en primer plano. El ampersand hace que su aplicación se ejecute en segundo plano. Una solución: hacer

kill -INT pid

Use kill -9o kill -KILLes malo porque la aplicación (aquí la JVM) no puede atraparlo para terminar con gracia.

Otra solución es volver a poner su aplicación en primer plano. Entonces Ctrl+Cfuncionará. Eche un vistazo a Bash Job control, más precisamente en fg.

mcoolive
fuente
2

Use el exit()método estático en la clase SpringApplication para cerrar su aplicación de arranque de primavera con gracia.

public class SomeClass {
    @Autowire
    private ApplicationContext context

    public void close() {
        SpringApplication.exit(context);
    }
}
rw026
fuente
Esto funciona para mi. Muchas gracias.
Khachornchit Songsaen
1

Spring Boot ahora admite un apagado ordenado (actualmente en versiones preliminares, 2.3.0.BUILD-SNAPSHOT)

Cuando está habilitado, el cierre de la aplicación incluirá un período de gracia de duración configurable. Durante este período de gracia, se permitirá que se completen las solicitudes existentes, pero no se permitirán nuevas solicitudes

Puede habilitarlo con:

server.shutdown.grace-period=30s

https://docs.spring.io/spring-boot/docs/2.3.0.BUILD-SNAPSHOT/reference/html/spring-boot-features.html#boot-features-graceful-shutdown

cmlonder
fuente
0

Si está utilizando maven, puede utilizar el complemento del ensamblador de la aplicación Maven .

El demonio mojo (que incrusta JSW ) generará un script de shell con un argumento de inicio / detención. El stopapagará / matará elegantemente su aplicación Spring.

El mismo script se puede usar para usar su aplicación maven como un servicio de Linux.

Nicolas Labrot
fuente
0

Si está en un entorno Linux, todo lo que tiene que hacer es crear un enlace simbólico a su archivo .jar desde dentro /etc/init.d/

sudo ln -s /path/to/your/myboot-app.jar /etc/init.d/myboot-app

Entonces puedes iniciar la aplicación como cualquier otro servicio

sudo /etc/init.d/myboot-app start

Para cerrar la aplicación

sudo /etc/init.d/myboot-app stop

De esta forma, la aplicación no terminará cuando salga de la terminal. Y la aplicación se cerrará con gracia con el comando de parada.

Johna
fuente