Análisis del código fuente de SpringBoot (6) -SpringBootExceptionReporter/Reportero de excepciones

I. Introducción

Este artículo analiza el reportero de excepciones SpringBootExceptionReporter basado en el código fuente spring-boot-2.2.14.BUILD-SNAPSHOT

El contenido principal de este artículo es el analizador de excepciones durante el proceso de inicio del proyecto SpringBoot, es decir, SpringBootExceptionReporter. Mirando hacia atrás cuando iniciamos el proyecto, ¿el proyecto no se pudo iniciar debido a varias razones, como la falta de configuración de la base de datos? ocupación del puerto, nombres repetidos de beans, etc., como el puerto Cuando se inicia el proyecto, la consola imprimirá el siguiente registro

Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
2023-07-14 15:12:35.836 ERROR 21456 --- [           main] o.s.b.d.LoggingFailureAnalysisReporter   : 

***************************
APPLICATION FAILED TO START
***************************

Description:

Web server failed to start. Port 80 was already in use.

Action:

Identify and stop the process that's listening on port 80 or configure this application to listen on another port.

Este registro señalará los motivos del error de inicio, así como las soluciones sugeridas, como agregar ciertas configuraciones o establecer una determinada configuración en verdadera, etc. La función
de SpringBootExceptionReporter es analizar e informar excepciones en el proceso de inicio. El código involucrado está en el método de ejecución de la clase SpringApplication:

2. Introducción al reportero de excepciones

2.1 Función

Recopile información de error y utilícela para informar la causa del error al usuario.

Spring Boot propone los conceptos de analizador de fallas (FailureAnalyzer) y reportador de errores (FailureAnalysisReporter), el primero se utiliza para convertir la información de errores en un informe de análisis de errores más detallado y el segundo es responsable de presentar este informe.

2.2 Definición de interfaz

@FunctionalInterface
public interface SpringBootExceptionReporter {
    
    
	// 向用户报告失败信息
	boolean reportException(Throwable failure);

}

Implementación de interfaz

@Override
public boolean reportException(Throwable failure) {
    
    
    //调用FailureAnalyzer获得错误分析报告FailureAnalysis 
	FailureAnalysis analysis = analyze(failure, this.analyzers);
	//调用FailureAnalysisReporter将报告呈现出来
	return report(analysis, this.classLoader);
}

2.3 Analizador de errores FailureAnalyzer

Llame a FailureAnalyzer para obtener el informe de análisis de errores FailureAnalysis

La interfaz FailureAnalyzer en Spring Boot se define de la siguiente manera. Solo hay un método de análisis. El parámetro de entrada es Throwable, que es la clase base de todas las excepciones. Devuelve un FailureAnalysis, que es el informe de análisis de errores.

@FunctionalInterface
public interface FailureAnalyzer {
    
    
	FailureAnalysis analyze(Throwable failure);
}

FailureAnalyzer necesita indicar qué analizador de excepciones es. AbstractFailureAnalyzer implementa el método FailureAnalyzer y declara un tipo genérico en la clase. Esta clase genérica es la clase de excepción que le interesa al analizador. El código específico también es muy simple. El núcleo es llamar a getCause() de la excepción para realizar un bucle/atravesar para verificar el origen de la excepción y su mensaje y determinar si es del mismo tipo que el genérico. La mayoría de los analizadores en Spring Boot heredar AbstractFailureAnalyzer.

public abstract class AbstractFailureAnalyzer<T extends Throwable> implements FailureAnalyzer {
    
    
  ...
}

Mirando hacia atrás en el informe de análisis de errores, esta clase contiene una descripción detallada del error (descripción), la solución del error (acción) y la excepción en sí (causa). Podemos pensar que este informe es la encapsulación secundaria de clases de excepción de Srping Boot, que agrega información de excepción más detallada sin destruir la información de excepción original.

public class FailureAnalysis {
    
    
    //错误的详细描述
	private final String description;
    //错误的解决方式/优化建议
	private final String action;
	//异常本身
	private final Throwable cause;

	public FailureAnalysis(String description, String action, Throwable cause) {
    
    
		this.description = description;
		this.action = action;
		this.cause = cause;
	}

  ...

}

2.4 Informe de errores de FailureAnalysisReporter

Responsable de mostrar estos informes de análisis de errores.

FailureAnalysisReporter también es una interfaz de método único y el parámetro de entrada es el informe de análisis de errores.

@FunctionalInterface
public interface FailureAnalysisReporter {
    
    
	void report(FailureAnalysis analysis);
}

Spring Boot proporciona un FailureAnalysisReporter de forma predeterminada, que es LoggingFailureAnalysisReporter. Esta clase llamará al método de depuración o error del registro para imprimir según el nivel de registro actual.

public final class LoggingFailureAnalysisReporter implements FailureAnalysisReporter {
    
    

	private static final Log logger = LogFactory.getLog(LoggingFailureAnalysisReporter.class);

	@Override
	public void report(FailureAnalysis failureAnalysis) {
    
    
		if (logger.isDebugEnabled()) {
    
    
			logger.debug("Application failed to start due to an exception", failureAnalysis.getCause());
		}
		if (logger.isErrorEnabled()) {
    
    
			logger.error(buildMessage(failureAnalysis));
		}
	}

  ...

}

Para resumir la solución de manejo de excepciones de Spring Boot: después de que Spring Boot detecta una excepción, llamará al FailureAnalyzer correspondiente a la excepción para analizarla y convertir la excepción en FailureAnalysis. Luego llame a FailureAnalysisReporter para imprimir el informe de análisis de excepciones.

3. Análisis del código fuente de SpringBootExceptionReporter

El reportero de excepciones se utiliza para capturar excepciones globales. Cuando ocurre una excepción en la aplicación Springboot, el reportero de excepciones la capturará y la manejará en consecuencia.

public ConfigurableApplicationContext run(String... args) {
    
    
	......
	Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
	
	try {
    
    
		......
        // 获取所有 SpringBootExceptionReporter 实现类
		exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
				new Class[] {
    
     ConfigurableApplicationContext.class }, context);
		......
	}
	catch (Throwable ex) {
    
    
		handleRunFailure(context, ex, exceptionReporters, listeners);
		throw new IllegalStateException(ex);
	}

	try {
    
    
		......
	}
	catch (Throwable ex) {
    
    
		handleRunFailure(context, ex, exceptionReporters, null);
		throw new IllegalStateException(ex);
	}
	return context;
}

Cabe señalar que 这个异常报告器只会捕获启动过程抛出的异常,如果是在启动完成后,在用户请求时报错,异常报告器不会捕获请求中出现的异常
Insertar descripción de la imagen aquí
getSpringFactoriesInstances va al archivo META-INF/spring.factories en el classpath para encontrar la clase de implementación de SpringBootExceptionReporter y luego llama a su constructor con el contenedor recién creado como parámetro.


Insertar descripción de la imagen aquí
Al final, solo se encontró un FailureAnalyzers. Vea el método de construcción de la clase FailureAnalyzers en el paquete spring-boot.

final class FailureAnalyzers implements SpringBootExceptionReporter {
    
    
    private static final Log logger = LogFactory.getLog(FailureAnalyzers.class);
    private final ClassLoader classLoader;
    private final List<FailureAnalyzer> analyzers;

    FailureAnalyzers(ConfigurableApplicationContext context) {
    
    
        this(context, (ClassLoader)null);
    }

    FailureAnalyzers(ConfigurableApplicationContext context, ClassLoader classLoader) {
    
    
        Assert.notNull(context, "Context must not be null");
        this.classLoader = classLoader != null ? classLoader : context.getClassLoader();
        this.analyzers = this.loadFailureAnalyzers(this.classLoader);
        prepareFailureAnalyzers(this.analyzers, context);
    }

Utilice el cargador de clases del contenedor para cargar un analizador de excepciones específico e ingrese el método loadFailureAnalyzers.

private List<FailureAnalyzer> loadFailureAnalyzers(ClassLoader classLoader) {
    
    
	List<String> analyzerNames = SpringFactoriesLoader.loadFactoryNames(FailureAnalyzer.class, classLoader);
	List<FailureAnalyzer> analyzers = new ArrayList<>();
	for (String analyzerName : analyzerNames) {
    
    
		try {
    
    
			Constructor<?> constructor = ClassUtils.forName(analyzerName, classLoader).getDeclaredConstructor();
			ReflectionUtils.makeAccessible(constructor);
			analyzers.add((FailureAnalyzer) constructor.newInstance());
		}
		catch (Throwable ex) {
    
    
			logger.trace(LogMessage.format("Failed to load %s", analyzerName), ex);
		}
	}
	AnnotationAwareOrderComparator.sort(analyzers);
	return analyzers;
}

También cargue la clase de implementación del tipo FailureAnalyzer en spring.factories y cree una instancia.
Esta vez, se encontraron un total de 19 clases de implementación, 14 de las cuales están ubicadas en el paquete spring-boot y 5 están ubicadas en spring-boot-autoconfigure. paquete. Mire los nombres. La mayoría de ellos son relativamente familiares, como excepciones de dependencia circular, excepciones de duplicación de beanDefinition, excepciones de ocupación de puertos, etc.
Insertar descripción de la imagen aquí
Regrese al método de construcción FailureAnalyzers, cárguelo en la lista FailureAnalyzer y llame al método prepareFailureAnalyzers.

private void prepareFailureAnalyzers(List<FailureAnalyzer> analyzers, ConfigurableApplicationContext context) {
    
    
	for (FailureAnalyzer analyzer : analyzers) {
    
    
		prepareAnalyzer(context, analyzer);
	}
}

Recorra la lista FailureAnalyzer y llame al método prepareAnalyzer

private void prepareAnalyzer(ConfigurableApplicationContext context, FailureAnalyzer analyzer) {
    
    
	if (analyzer instanceof BeanFactoryAware) {
    
    
		((BeanFactoryAware) analyzer).setBeanFactory(context.getBeanFactory());
	}
	if (analyzer instanceof EnvironmentAware) {
    
    
		((EnvironmentAware) analyzer).setEnvironment(context.getEnvironment());
	}
}

Este método comprueba si FailureAnalyzer implementa la interfaz BeanFactoryAware y la interfaz EnvironmentAware, y le asigna el BeanFactory y el entorno correspondientes.

El motivo de este paso es que el proceso de procesamiento de información de excepción por parte de algunos analizadores de excepciones puede depender del entorno del contenedor o proyecto, y el tiempo de ejecución normal de la interfaz Aware es cuando se actualiza el contenedor. O se produjo una excepción antes de esto y esta parte de FailureAnalyzer no podrá funcionar correctamente, por lo que las dependencias deben configurarse con anticipación.

Cabe señalar que el entorno establecido aquí se toma directamente del contenedor, se crea recientemente en el constructor del contenedor y no es el entorno por el que hemos pasado por una serie de procesamientos anteriores, aunque nuestro entorno se utilizará más adelante. Los que están en el contenedor se reemplazan, pero el entorno mantenido por estos FailureAnalyzers no se actualiza juntos, por lo que personalmente creo que este paso es un poco problemático. (Hablamos de por qué hay dos conjuntos de entornos en el artículo anterior)

Una vez completado prepareAnalyzer, el proceso de carga de SpringBootExceptionReporter finaliza. A continuación, veamos cómo usar dicho analizador en catch e ingresemos el método handleRunFailure.

private void handleRunFailure(ConfigurableApplicationContext context, Throwable exception, Collection<SpringBootExceptionReporter> exceptionReporters, SpringApplicationRunListeners listeners) {
    
    
  try {
    
    
        try {
    
    
            handleExitCode(context, exception);
            if (listeners != null) {
    
    
                //发送启动失败事件
                listeners.failed(context, exception);
            }
        } finally {
    
    
            // 报告失败信息
            reportFailure(exceptionReporters, exception);
            if (context != null) {
    
    
                //关闭上下文
                context.close();
            }

        }
    } catch (Exception ex) {
    
    
        logger.warn("Unable to close ApplicationContext", ex);
    }

    ReflectionUtils.rethrowRuntimeException(exception);
}

Insertar descripción de la imagen aquí

Veamos la primera línea de handleExitCode. Decide si enviar un evento de salida según el código de salida. También proporciona algunas interfaces que nos permiten personalizar el código de salida. 0 significa una salida normal y no 0 significa una salida anormal.

private void handleExitCode(ConfigurableApplicationContext context, Throwable exception) {
    
    
    // 从异常中获取退出代码
	int exitCode = getExitCodeFromException(context, exception);
	if (exitCode != 0) {
    
    
		if (context != null) {
    
    
			context.publishEvent(new ExitCodeEvent(context, exitCode));
		}
		SpringBootExceptionHandler handler = getSpringBootExceptionHandler();
		if (handler != null) {
    
    
			handler.registerExitCode(exitCode);
		}
	}
}

El método getExitCodeFromException obtiene el código de salida según el estado del contenedor y el tipo de excepción.

private int getExitCodeFromException(ConfigurableApplicationContext context, Throwable exception) {
    
    
	int exitCode = getExitCodeFromMappedException(context, exception);
	if (exitCode == 0) {
    
    
		exitCode = getExitCodeFromExitCodeGeneratorException(exception);
	}
	return exitCode;
}

método getExitCodeFromMappedException, si el contenedor no se ha iniciado, devuelve directamente 0; de lo contrario, obtiene el Bean de tipo ExitCodeExceptionMapper del contenedor, lo asigna a ExitCodeGenerators y llama a su método getExitCode para obtener el código de salida.

private int getExitCodeFromMappedException(ConfigurableApplicationContext context, Throwable exception) {
    
    
	if (context == null || !context.isActive()) {
    
    
		return 0;
	}
	ExitCodeGenerators generators = new ExitCodeGenerators();
	Collection<ExitCodeExceptionMapper> beans = context.getBeansOfType(ExitCodeExceptionMapper.class).values();
	generators.addAll(exception, beans);
	return generators.getExitCode();
}

ExitCodeExceptionMapper es una interfaz funcional que proporciona un método para obtener códigos de salida de excepciones. Podemos personalizar los códigos de salida implementando esta interfaz.

@FunctionalInterface
public interface ExitCodeExceptionMapper {
    
    
	
	int getExitCode(Throwable exception);

}

Esta lista se recorre a través del método getExitCode. De acuerdo con las condiciones en el if, en realidad no está seguro de si el código de respuesta final es positivo o negativo. No hay prioridad relativa entre los códigos positivos y negativos. Lo que en última instancia le importa al programa es si el código de salida es 0.

int getExitCode() {
    
    
	int exitCode = 0;
	for (ExitCodeGenerator generator : this.generators) {
    
    
		try {
    
    
			int value = generator.getExitCode();
			if (value > 0 && value > exitCode || value < 0 && value < exitCode) {
    
    
				exitCode = value;
			}
		}
		catch (Exception ex) {
    
    
			exitCode = (exitCode != 0) ? exitCode : 1;
			ex.printStackTrace();
		}
	}
	return exitCode;
}

Regrese al método getExitCodeFromException. Si el código de salida obtenido en el paso anterior es 0, se realizará un juicio basado en la excepción. Debido a que es posible que el contenedor no se active en el primer paso, se devolverá 0 directamente. Llame al método. Si la clase de excepción getExitCodeFromExitCodeGeneratorExceptionimplementa la interfaz ExitCodeGenerator, llame a su método getExitCode para obtener el código de salida.

private int getExitCodeFromExitCodeGeneratorException(Throwable exception) {
    
    
		if (exception == null) {
    
    
			return 0;
		}
		if (exception instanceof ExitCodeGenerator) {
    
    
			return ((ExitCodeGenerator) exception).getExitCode();
		}
		return getExitCodeFromExitCodeGeneratorException(exception.getCause());
	}

Si el código de salida finalmente devuelto no es 0, se publicará un evento ExitCodeEvent a través del contenedor y el código de salida se registrará en SpringBootExceptionHandler para el registro posterior.

Después de procesar el código de salida, regrese al método handleRunFailure, luego mire los oyentes, si no está vacío, utilícelo para publicar el evento de falla de inicio.

private void handleRunFailure(ConfigurableApplicationContext context, Throwable exception,
			Collection<SpringBootExceptionReporter> exceptionReporters, SpringApplicationRunListeners listeners) {
    
    
	try {
    
    
		try {
    
    
			handleExitCode(context, exception);
			if (listeners != null) {
    
    
				listeners.failed(context, exception);
			}
		}
		finally {
    
    
			reportFailure(exceptionReporters, exception);
			if (context != null) {
    
    
				context.close();
			}
		}
	}
	catch (Exception ex) {
    
    
		logger.warn("Unable to close ApplicationContext", ex);
	}
	ReflectionUtils.rethrowRuntimeException(exception);
}

En este momento, los oyentes definitivamente no están vacíos. En el artículo anterior, publicamos el evento de inicio de la aplicación ApplicationStartingEvent y el evento de preparación del entorno ApplicationEnvironmentPreparedEvent a través de él. Aquí publicaremos eventos relacionados con fallas de inicio de la aplicación e ingresaremos el método fallido.

void failed(ConfigurableApplicationContext context, Throwable exception) {
    
    
	for (SpringApplicationRunListener listener : this.listeners) {
    
    
		callFailedListener(listener, context, exception);
	}
}

Como antes, esta lista de oyentes tiene solo un elemento EventPublishingRunListener, páselo al método callFailedListener

private void callFailedListener(SpringApplicationRunListener listener, ConfigurableApplicationContext context,
			Throwable exception) {
    
    
	try {
    
    
		listener.failed(context, exception);
	}
	catch (Throwable ex) {
    
    
		if (exception == null) {
    
    
			ReflectionUtils.rethrowRuntimeException(ex);
		}
		if (this.log.isDebugEnabled()) {
    
    
			this.log.error("Error handling failed", ex);
		}
		else {
    
    
			String message = ex.getMessage();
			message = (message != null) ? message : "no error message";
			this.log.warn("Error handling failed (" + message + ")");
		}
	}
}

Finalmente, se llama al método fallido de EventPublishingRunListener.

@Override
public void failed(ConfigurableApplicationContext context, Throwable exception) {
    
    
	ApplicationFailedEvent event = new ApplicationFailedEvent(this.application, this.args, context, exception);
	if (context != null && context.isActive()) {
    
    
		// Listeners have been registered to the application context so we should
		// use it at this point if we can
		context.publishEvent(event);
	}
	else {
    
    
		// An inactive context may not have a multicaster so we use our multicaster to
		// call all of the context's listeners instead
		if (context instanceof AbstractApplicationContext) {
    
    
			for (ApplicationListener<?> listener : ((AbstractApplicationContext) context)
					.getApplicationListeners()) {
    
    
				this.initialMulticaster.addApplicationListener(listener);
			}
		}
		this.initialMulticaster.setErrorHandler(new LoggingErrorHandler());
		this.initialMulticaster.multicastEvent(event);
	}
}

Aquí, primero se inicializa un evento ApplicationFailedEvent y luego se determina si el contenedor se ha iniciado. Si es así, el contenedor será responsable de publicar el evento. De lo contrario, el detector de eventos que ya existe en el contenedor se registrará en el Multidifusión de eventos actuales, como antes. El proceso de publicación de varios eventos es el mismo y continuará publicando eventos.

Una vez completados procesos como el procesamiento de códigos de salida y la publicación de eventos de falla de inicio, analice la causa de la excepción y cierre el contenedor.

   //...... 省略其他代码
   finally {
    
    
        this.reportFailure(exceptionReporters, exception);
        if (context != null) {
    
    
            context.close();
        }
    }
   //...... 省略其他代码

Eche un vistazo a la implementación de reportFailure. El parámetro de entrada es el SpringBootExceptionReporter que se encuentra al principio. Solo hay una implementación de FailureAnalyzers.

private void reportFailure(Collection<SpringBootExceptionReporter> exceptionReporters, Throwable failure) {
    
    
	try {
    
    
		for (SpringBootExceptionReporter reporter : exceptionReporters) {
    
    
			if (reporter.reportException(failure)) {
    
    
				registerLoggedException(failure);
				return;
			}
		}
	}
	catch (Throwable ex) {
    
    
		// Continue with normal handling of the original failure
	}
	if (logger.isErrorEnabled()) {
    
    
		logger.error("Application run failed", failure);
		registerLoggedException(failure);
	}
}

Ingrese el método reportException de la clase FailureAnalyzers

public boolean reportException(Throwable failure) {
    
    
	FailureAnalysis analysis = analyze(failure, this.analyzers);
	return report(analysis, this.classLoader);
}

Primero llame a analizar y utilice los 19 analizadores de excepciones encontrados anteriormente para analizar la causa de la excepción. Hasta que el resultado del análisis devuelto por un analizador no esté vacío, el recorrido finaliza.

private FailureAnalysis analyze(Throwable failure, List<FailureAnalyzer> analyzers) {
    
    
	for (FailureAnalyzer analyzer : analyzers) {
    
    
		try {
    
    
			FailureAnalysis analysis = analyzer.analyze(failure);
			if (analysis != null) {
    
    
				return analysis;
			}
		}
		catch (Throwable ex) {
    
    
			logger.debug(LogMessage.format("FailureAnalyzer %s failed", analyzer), ex);
		}
	}
	return null;
}

En el registro de excepciones al comienzo del artículo, los motivos del error de inicio y las soluciones recomendadas se resumen en este resultado de análisis.

public class FailureAnalysis {
    
    

	private final String description;

	private final String action;

	private final Throwable cause;
}

El proceso de análisis lo implementa cada analizador, determina si devolver un resultado según el tipo de excepción y luego pasa el resultado del análisis al método de informe de la clase FailureAnalyzers.

private boolean report(FailureAnalysis analysis, ClassLoader classLoader) {
    
    
	List<FailureAnalysisReporter> reporters = SpringFactoriesLoader.loadFactories(FailureAnalysisReporter.class,
			classLoader);
	if (analysis == null || reporters.isEmpty()) {
    
    
		return false;
	}
	for (FailureAnalysisReporter reporter : reporters) {
    
    
		reporter.report(analysis);
	}
	return true;
}

Este método primero va a spring.factories para encontrar la clase de implementación de FailureAnalysisReporter, que determina el formato de informe de los resultados del análisis de excepciones. De forma predeterminada, solo se encuentra un LoggingFailureAnalysisReporter, que está definido en el paquete spring-boot.

# FailureAnalysisReporters
org.springframework.boot.diagnostics.FailureAnalysisReporter=\
org.springframework.boot.diagnostics.LoggingFailureAnalysisReporter

Es decir, finalmente se llama al método de informe de LoggingFailureAnalysisReporter.

public void report(FailureAnalysis failureAnalysis) {
    
    
	if (logger.isDebugEnabled()) {
    
    
		logger.debug("Application failed to start due to an exception", failureAnalysis.getCause());
	}
	if (logger.isErrorEnabled()) {
    
    
		logger.error(buildMessage(failureAnalysis));
	}
}

Según los resultados entrantes, llame a buildMessage para generar la información de salida. Este contenido es muy familiar. Es el formato de informe de excepción que se muestra en el registro anterior.

private String buildMessage(FailureAnalysis failureAnalysis) {
    
    
	StringBuilder builder = new StringBuilder();
	builder.append(String.format("%n%n"));
	builder.append(String.format("***************************%n"));
	builder.append(String.format("APPLICATION FAILED TO START%n"));
	builder.append(String.format("***************************%n%n"));
	builder.append(String.format("Description:%n%n"));
	builder.append(String.format("%s%n", failureAnalysis.getDescription()));
	if (StringUtils.hasText(failureAnalysis.getAction())) {
    
    
		builder.append(String.format("%nAction:%n%n"));
		builder.append(String.format("%s%n", failureAnalysis.getAction()));
	}
	return builder.toString();
}

La información impresa es la siguiente:

Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
2023-07-14 15:12:35.836 ERROR 21456 --- [           main] o.s.b.d.LoggingFailureAnalysisReporter   : 

***************************
APPLICATION FAILED TO START
***************************

Description:

Web server failed to start. Port 80 was already in use.

Action:

Identify and stop the process that's listening on port 80 or configure this application to listen on another port.

Finalmente, se llama al método context.close,
Insertar descripción de la imagen aquí
que primero llama al método doClose y luego elimina el método de enlace.

public void close() {
    
    
	synchronized (this.startupShutdownMonitor) {
    
    
		doClose();
		// If we registered a JVM shutdown hook, we don't need it anymore now:
		// We've already explicitly closed the context.
		if (this.shutdownHook != null) {
    
    
			try {
    
    
			    // 移除钩子方法
				Runtime.getRuntime().removeShutdownHook(this.shutdownHook);
			}
			catch (IllegalStateException ex) {
    
    
				// ignore - VM is already shutting down
			}
		}
	}
}

Método doClose. Publique transmisiones de aviso y cierre algunos frijoles y frijoles de fábrica para facilitar la recolección de basura.

protected void doClose() {
    
    
	// Check whether an actual close attempt is necessary...
	if (this.active.get() && this.closed.compareAndSet(false, true)) {
    
    
		if (logger.isDebugEnabled()) {
    
    
			logger.debug("Closing " + this);
		}

		LiveBeansView.unregisterApplicationContext(this);

		try {
    
    
			// 发布容器关闭事件
			publishEvent(new ContextClosedEvent(this));
		}
		catch (Throwable ex) {
    
    
			logger.warn("Exception thrown from ApplicationListener handling ContextClosedEvent", ex);
		}

		// Stop all Lifecycle beans, to avoid delays during individual destruction.
		if (this.lifecycleProcessor != null) {
    
    
			try {
    
    
				this.lifecycleProcessor.onClose();
			}
			catch (Throwable ex) {
    
    
				logger.warn("Exception thrown from LifecycleProcessor on context close", ex);
			}
		}

		// Destroy all cached singletons in the context's BeanFactory.
		// 销毁所有的单例bean
		destroyBeans();

		// Close the state of this context itself.
		// 关闭容器
		closeBeanFactory();

		// Let subclasses do some final clean-up if they wish...
		// 调用子类的重写方法,关闭web服务器
		onClose();

		// Reset local application listeners to pre-refresh state.
		if (this.earlyApplicationListeners != null) {
    
    
			this.applicationListeners.clear();
			this.applicationListeners.addAll(this.earlyApplicationListeners);
		}

		// Switch to inactive.
		this.active.set(false);
	}
}

método de gancho.

Este método se llama cuando la JVM sale.

public static void main(String[] args) {
    
    
    System.out.println("hello");
    Thread close_jvm = new Thread(()-> System.out.println("close jvm"));
    Runtime.getRuntime().addShutdownHook(close_jvm);
    System.out.println("world");
}
hello
world
close jvm

4. Introducción a ShutdownHook

  • Función: Lógica empresarial ejecutada cuando sale la JVM
  • Agregar: Runtime.getRuntime().addShutdownHook()
  • Ejemplo: Runtime.getRuntime().removeShutdownHook(this.shutdownHook)

4.1 Antecedentes

Durante el desarrollo, cuando se encuentra con esta situación, varios subprocesos están trabajando al mismo tiempo, de repente un subproceso encuentra un error fetal y necesita finalizar el programa inmediatamente y reiniciarlo después de solucionar el problema manualmente. Pero hay un problema con esto: cuando el programa finaliza, otros subprocesos pueden estar realizando operaciones importantes, como enviar un mensaje a otro módulo y actualizar el estado de la base de datos. La terminación abrupta puede hacer que la operación se complete solo a la mitad, lo que resultará en inconsistencia de los datos.

La solución es: referirse al concepto de atomicidad de transacciones de la base de datos y tratar esta serie de operaciones importantes como un todo, ya sea que se completen todas o no se complete ninguna. Para facilitar la expresión, marcamos esta serie de operaciones importantes como operación X.

Cuando el programa esté a punto de salir, verifique si actualmente se está ejecutando la operación X. Si es así, espere a que se complete y luego salga. Y durante este periodo no se aceptará ninguna nueva operación X. Si pasa demasiado tiempo entre ejecuciones de la operación X, finalice y revierta todo el estado.
Si no, puedes salir inmediatamente.

Cuando el programa salga, realice algunas comprobaciones para garantizar la atomicidad de la operación X que se ha iniciado. Aquí se utiliza Runtime.ShutdownHook.

4.2 ¿Qué es el gancho de apagado?

El gancho de apagado es un hilo inicializado pero no iniciado. Cuando la JVM comience a ejecutar la secuencia de apagado, todos los Shutdown Hooks registrados se ejecutarán simultáneamente. En este momento, las operaciones definidas en el hilo Shutdown Hook comenzarán a ejecutarse.

Cabe señalar que las operaciones realizadas en Shutdown Hook no deberían llevar demasiado tiempo. Porque en el caso del cierre de JVM causado por el cierre de sesión del usuario o el cierre del sistema operativo, el sistema solo reservará un tiempo limitado para el trabajo sin terminar y aún se verá obligado a cerrarse después del tiempo de espera.

4.3 ¿Cuándo se llamará Shutdown Hook?

El programa se detiene normalmente

  • Llegar al final del programa
  • Salida del sistema

El programa sale anormalmente

  • NOMBRE
  • Sin memoria

Detener debido a influencia externa.

  • Ctrl+C
  • matar -9
  • El usuario cierra sesión o se apaga

4.4 Cómo utilizar el gancho de apagado

Llame al método addShutdownHook (Thread Hook) de la clase java.lang.Runtime para registrar un Shutdown Hook y luego defina las operaciones que deben realizarse durante la salida del sistema en Thread. como sigue:

Runtime.getRuntime().addShutdownHook(new Thread(() -> 
    System.out.println("Do something in Shutdown Hook")
));

4.5 Ejemplo de prueba

  • Primero, registre un Shutdown Hook.
  • Luego, el sistema duerme durante 3 segundos y simula determinadas operaciones.
  • Luego, llame a una Lista vacía, genere una excepción y prepárese para finalizar el programa.
  • Cuando el programa esté a punto de finalizar, ejecute el contenido en Shutdown Hook.
public static void main(String[] args)
{
    
    
    // register shutdown hook
    Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println("Do something in Shutdown Hook")));

    // sleep for some time
    try {
    
    
        for (int i=0; i<3; i++) {
    
    
            System.out.println("Count: " + i + "...");
            TimeUnit.MILLISECONDS.sleep(1000);
        }
        List nullList = new ArrayList<>();
        System.out.println("Trying to print null list's first element: " + nullList.get(0).toString());
    } catch (InterruptedException e) {
    
    
        e.printStackTrace();
    }

    System.out.println("Ready to exit.");
    System.exit(0);
}

El resultado es el siguiente:

Count: 0...
Count: 1...
Count: 2...
Exception in thread "main" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
    at java.util.ArrayList.rangeCheck(ArrayList.java:653)
    at java.util.ArrayList.get(ArrayList.java:429)
    at HookTest.main(HookTest.java:18)
Do something in Shutdown Hook

Process finished with exit code 1

Puntos a tener en cuenta

  • Después de System.exit, cuando el Shutdown Hook comienza a ejecutarse, otros subprocesos continuarán ejecutándose.
  • Se debe garantizar la seguridad del hilo del Shutdown Hook.
  • Tenga especial cuidado al utilizar varios Shutdown Hooks para garantizar que los servicios a los que llaman no se vean afectados por otros Hooks. De lo contrario, habrá una situación en la que el servicio del que depende el Hook actual será cancelado por otro Hook.

5. Reportero de excepciones personalizado

5.1 Método 1: implementar la interfaz SpringBootExceptionReporter

(1): simular excepción y crear servicio de usuario

public class UserService {
    
    

}

Introducir el servicio de usuario

@RestController
public class UserController {
    
    


    @Autowired
    UserService userService;

}

Dado que UserService no se agrega al contenedor IOC, se informa el siguiente error después del inicio normal:

***************************
APPLICATION FAILED TO START
***************************

Description:

Field userService in com.example.demo.controller.UserController required a bean of type 'com.example.demo.service.UserService' that could not be found.

The injection point has the following annotations:
	- @org.springframework.beans.factory.annotation.Autowired(required=true)


Action:

Consider defining a bean of type 'com.example.demo.service.UserService' in your configuration.

¿Qué debemos hacer si queremos imprimir el formato que definimos?

(2): excepción personalizada

package com.example.demo.exception;

import org.springframework.beans.factory.UnsatisfiedDependencyException;
import org.springframework.boot.SpringBootExceptionReporter;
import org.springframework.context.ConfigurableApplicationContext;

//自定义异常报告器
public class MyExceptionReporter  implements SpringBootExceptionReporter{
    
    
 
 
    private ConfigurableApplicationContext context;
 
    //实例化构造方法 如果不实例化会报错 报 Cannot instantiate interface
    //java.lang.NoSuchMethodException: com.example.demo.exception.MyExceptionReporter.
    // <init>(org.springframework.context.ConfigurableApplicationContext)
    public MyExceptionReporter(ConfigurableApplicationContext context) {
    
    
        this.context = context;
    }
 
    @Override
    public boolean reportException(Throwable failure) {
    
    
        if(failure instanceof UnsatisfiedDependencyException){
    
    
            UnsatisfiedDependencyException exception = (UnsatisfiedDependencyException)failure;
            System.out.println("no such bean " + exception.getInjectionPoint().getField().getName());
        }
        //返回false打印详细信息 返回true只打印异常信息
        return false;
    }
}

Registre el reportero de excepciones en el archivo spring.factories

# 注册异常报告器
org.springframework.boot.SpringBootExceptionReporter=\
com.example.demo.exception.MyExceptionReporter

(3): Ejecute el programa, el resultado de error es el siguiente
Insertar descripción de la imagen aquí

5.2 Método 2: implementar la interfaz FailureAnalyzer

(1): personalizar una excepción

public class MyException extends RuntimeException{
    
    
}

(2): implementar la interfaz FailureAnalyzer

package com.example.demo.exception;

public class MyFailureAnalyzer extends AbstractFailureAnalyzer<MyException> {
    
    
    @Override
    protected FailureAnalysis analyze(Throwable rootFailure, MyException cause) {
    
    
        String des = "发生自定义异常";
        String action = "由于自定义了一个异常";
        return new FailureAnalysis(des, action, rootFailure);
    }
}

Registre el analizador de excepciones en el archivo spring.factories

org.springframework.boot.diagnostics.FailureAnalyzer=\
com.example.demo.exception.MyFailureAnalyzer 

(3) Las pruebas
deben generar una excepción cuando se inicia Spring Boot. Para las pruebas, lanzamos una excepción personalizada al preparar el contexto y la agregamos a MyApplicationRunListener en la demostración.

public void contextPrepared(ConfigurableApplicationContext context) {
    
    
    System.out.println("在创建和准备ApplicationContext之后,但在加载源之前调用");
    throw new MyException();
}

El registro de excepciones impreso después del inicio es el siguiente:

***************************
APPLICATION FAILED TO START
***************************

Description:

发生自定义异常

Action:

由于自定义了一个异常

6. Resumen

  1. Para presentar los errores durante el proceso de inicio de una manera más amigable y flexible, Spring Boot ha diseñado un conjunto de soluciones de manejo de excepciones.
  2. Spring Boot propone los conceptos de analizador de fallas (FailureAnalyzer) y reportador de errores (FailureAnalysisReporter), el primero se utiliza para convertir la información de errores en un informe de análisis de errores más detallado y el segundo es responsable de presentar este informe.
  3. La responsabilidad del analizador de errores (FailureAnalyzer) es identificar el tipo de error actual y reempaquetar los errores de interés. El resultado del empaquetado es el informe de análisis de errores (FailureAnalysis).
  4. Además de la información de error original, el informe de análisis de errores (FailureAnalysis) agrega una descripción y una acción para solicitar al usuario un procesamiento posterior.
  5. El sistema de manejo de excepciones del marco Spring Boot utiliza SPI ampliamente para cargar clases específicas, lo que facilita la expansión posterior del marco de soluciones de manejo de excepciones, verificaciones de excepciones específicas y métodos de visualización de excepciones.

Artículos de referencia:
https://blog.csdn.net/m0_37298252/article/details/122879031
https://blog.51cto.com/u_14014612/6007663

Supongo que te gusta

Origin blog.csdn.net/weixin_49114503/article/details/131727677
Recomendado
Clasificación