SpringBoot source code analysis (6)--SpringBootExceptionReporter/Exception reporter

I. Introduction

This article analyzes the SpringBootExceptionReporter exception reporter based on spring-boot-2.2.14.BUILD-SNAPSHOT source code

The main content of this article is the exception analyzer during the startup process of the SpringBoot project, that is, the SpringBootExceptionReporter. Looking back when we started the project, will the project fail to start due to various reasons such as lack of database configuration, port occupation, repeated bean naming, etc., such as port When the project is started, the console will print the following log

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.

This log will point out the reasons for the startup failure, as well as suggested solutions, such as adding certain configurations, or setting a certain configuration to true, etc. The function
of SpringBootExceptionReporter is to analyze and report exceptions in the startup process. The code involved is in In the run method of the SpringApplication class:

2. Introduction to exception reporter

2.1. Function

Collect error information and use it to report the cause of the error to the user.

Spring Boot proposes the concepts of failure analyzer (FailureAnalyzer) and error reporter (FailureAnalysisReporter). The former is used to convert error information into a more detailed error analysis report, and the latter is responsible for presenting this report.

2.2. Interface definition

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

}

Interface implementation

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

2.3. FailureAnalyzer error analyzer

Call FailureAnalyzer to obtain the error analysis report FailureAnalysis

The FailureAnalyzer interface in Spring Boot is defined as follows. There is only one analyze method. The input parameter is Throwable, which is the base class of all exceptions. It returns a FailureAnalysis, which is the error analysis report.

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

FailureAnalyzer needs to indicate which exception analyzer it is. AbstractFailureAnalyzer implements the FailureAnalyzer method and declares a generic type on the class. This generic class is the exception class that the analyzer is interested in. The specific code is also very simple. The core is to call the exception's getCause() to loop/traverse to check the source of the exception and its message and determine whether it is the same type as the generic. Most analyzers in Spring Boot will inherit AbstractFailureAnalyzer.

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

Looking back at the error analysis report, this class contains a detailed description of the error (description), the error solution (action) and the exception itself (cause). We can think that this report is Srping Boot's secondary encapsulation of exception classes, adding more detailed exception information without destroying the original exception information.

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. FailureAnalysisReporter error reporter

Responsible for displaying these error analysis reports

FailureAnalysisReporter is also a single-method interface, and the input parameter is the error analysis report.

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

Spring Boot provides a FailureAnalysisReporter by default, which is LoggingFailureAnalysisReporter. This class will call the debug or error method of the log to print based on the current log level.

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

  ...

}

To summarize the Spring Boot exception handling solution: After Spring Boot catches an exception, it will call the FailureAnalyzer corresponding to the exception to analyze it and convert the exception into FailureAnalysis. Then call FailureAnalysisReporter to print out the exception analysis report.

3. SpringBootExceptionReporter source code analysis

The exception reporter is used to capture global exceptions. When an exception occurs in the springboot application, the exception reporter will capture it and handle it accordingly.

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

It should be noted that 这个异常报告器只会捕获启动过程抛出的异常,如果是在启动完成后,在用户请求时报错,异常报告器不会捕获请求中出现的异常
Insert image description here
getSpringFactoriesInstances goes to the META-INF/spring.factories file on the classpath to find the implementation class of SpringBootExceptionReporter, and then calls its constructor with the newly created container as a parameter.


Insert image description here
In the end, only one FailureAnalyzers was found. View the construction method of the FailureAnalyzers class under the spring-boot package.

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

Use the container's class loader to load a specific exception analyzer and enter the loadFailureAnalyzers method.

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

Also load the FailureAnalyzer type implementation class in spring.factories and instantiate it.
This time, a total of 19 implementation classes were found, 14 of which are located under the spring-boot package, and 5 are located under the spring-boot-autoconfigure package. Look at the names. Most of them are relatively familiar, such as circular dependency exceptions, beanDefinition duplication exceptions, port occupation exceptions, etc.
Insert image description here
Return to the FailureAnalyzers construction method, load it into the FailureAnalyzer list, and call the prepareFailureAnalyzers method.

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

Loop through the FailureAnalyzer list and call the prepareAnalyzer method

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

This method checks if FailureAnalyzer implements the BeanFactoryAware interface and EnvironmentAware interface, and assigns the corresponding BeanFactory and Environment to it.

The reason for this step is that the process of processing exception information by some exception analyzers may depend on the environment of the container or project, and the normal execution time of the Aware interface is when the container is refreshed. If during the process of Aware, Or an exception occurred before this, and this part of FailureAnalyzer will not be able to work properly, so the dependencies need to be set in advance.

It should be noted that the environment set here is taken directly from the container. It is newly created in the constructor of the container. It is not the environment we have gone through a series of processing before, although our environment will be used later. The ones in the container are replaced, but the environment held by these FailureAnalyzers is not updated together, so I personally think this step is a bit problematic. (We talked about why there are two sets of environments in the previous article)

After prepareAnalyzer is completed, the process of loading SpringBootExceptionReporter is over. Next, let's see how to use such an analyzer in catch and enter the handleRunFailure method.

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

Insert image description here

Let's look at the first line of handleExitCode. It decides whether to send an exit event based on the exitCode. It also provides some interfaces that allow us to customize the exitCode. 0 means a normal exit, and non-0 means an abnormal exit.

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

The getExitCodeFromException method obtains the exitCode based on the status of the container and the exception type.

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

getExitCodeFromMappedException method, if the container has not been started, directly returns 0, otherwise obtains the Bean of type ExitCodeExceptionMapper from the container, assigns it to ExitCodeGenerators, and calls its getExitCode method to obtain the exit code

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 is a functional interface that provides a method to obtain exit codes from exceptions. We can customize exit codes by implementing this interface.

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

}

This list is traversed through the getExitCode method. According to the conditions in the if, it is actually not sure whether the final response code is positive or negative. There is no relative priority between positive and negative codes. What the program ultimately cares about is whether the exit code is 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;
}

Return to the getExitCodeFromException method. If the exit code obtained in the above step is 0, a judgment will be made based on the exception. Because the container may not be activated in the first step, 0 will be returned directly. Call the method. If the exception class is getExitCodeFromExitCodeGeneratorExceptionimplemented ExitCodeGenerator interface, call its getExitCode method to obtain the exit code

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

If the exit code finally returned is not 0, an ExitCodeEvent event will be published through the container, and the exit code will be registered to SpringBootExceptionHandler for subsequent logging.

After the exit code is processed, return to the handleRunFailure method. Next, look at the listeners. If it is not empty, use it to publish the startup failure event.

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

At this time, listeners are definitely not empty. In the previous article, we have published the application startup event ApplicationStartingEvent and the environment readiness event ApplicationEnvironmentPreparedEvent through it. Here we are going to publish events related to application startup failure and enter the failed method.

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

As before, this listeners list has only one element EventPublishingRunListener, pass it to the callFailedListener method

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

Finally, the fail method of EventPublishingRunListener is called.

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

Here, an event ApplicationFailedEvent is first initialized, and then it is determined whether the container has been started. If so, the container will be responsible for publishing the event. Otherwise, the event listener that already exists in the container will be registered to the current event multicaster, as before The publishing process of several events is the same, and it will continue to publish events.

After processes such as processing exit codes and publishing startup failure events are completed, analyze the cause of the exception and close the container

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

Take a look at the implementation of reportFailure. The input parameter is the SpringBootExceptionReporter found at the beginning. There is only one implementation of 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);
	}
}

Enter the reportException method of the FailureAnalyzers class

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

First call analyze and use the 19 exception parsers found previously to analyze the cause of the exception. Until the parsing result returned by a parser is not empty, the traversal ends.

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

In the exception log at the beginning of the article, the reasons for the startup failure and the recommended solutions are encapsulated in this parsing result.

public class FailureAnalysis {
    
    

	private final String description;

	private final String action;

	private final Throwable cause;
}

The parsing process is implemented by each parser. It determines whether to return a result based on the type of exception, and then passes the parsing result to the report method of the FailureAnalyzers class.

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

This method first goes to spring.factories to find the implementation class of FailureAnalysisReporter, which determines the reporting format of the exception analysis results. By default, only one LoggingFailureAnalysisReporter is found, which is defined under the spring-boot package.

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

That is, the report method of LoggingFailureAnalysisReporter is finally called.

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

Based on the incoming results, call buildMessage to build the output information. This content is very familiar. It is the exception report format shown in the previous log.

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

The printed information is as follows:

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.

Finally, the context.close method is called.
Insert image description here
The close method first calls the doClose method, and then removes the hook method.

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

doClose method. Publish shoutdown broadcasts and close some beans and factory beans to facilitate garbage collection.

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

hook method.

This method is called when the JVM exits

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. Introduction to shutdownHook

  • Function: Business logic executed when the JVM exits
  • Add: Runtime.getRuntime().addShutdownHook()
  • 移除:Runtime.getRuntime().removeShutdownHook(this.shutdownHook)

4.1. Background

During development, when encountering this situation, multiple threads are working at the same time. Suddenly one thread encounters a fetal error and needs to terminate the program immediately and restart it after manual troubleshooting and solving the problem. But there is a problem with this. When the program terminates, other threads may be performing important operations, such as sending a message to another module and updating the database status. Abrupt termination may cause the operation to be only half completed, resulting in data inconsistency.

The solution is: refer to the concept of database Transaction atomicity and treat this series of important operations as a whole, either all of them are completed or none of them are completed. For convenience of expression, we mark this series of important operations as operation X.

When the program is about to exit, check whether there is currently operation X being executed. If so, wait for it to complete and then exit. And no new operation X will be accepted during this period. If there is too long between executions of operation X, terminate and rollback all state.
If not, you can exit immediately.

When the program exits, do some checks to ensure the atomicity of the operation X that has been started. Runtime.ShutdownHook is used here.

4.2. What is Shutdown Hook?

Shutdown hook is an initialized but unstarted thread. When the JVM starts executing the shutdown sequence, all registered Shutdown Hooks will be run concurrently. At this time, the operations defined in the Shutdown Hook thread will begin to be executed.

It should be noted that the operations performed in Shutdown Hook should not be too time-consuming. Because in the case of JVM shutdown caused by user logout or operating system shutdown, the system will only reserve a limited time for unfinished work, and will still be forced to shut down after the timeout.

4.3. When will Shutdown Hook be called?

Program stops normally

  • Reach the end of program
  • System.exit

Program exits abnormally

  • NAME
  • OutOfMemory

Stop due to external influence

  • Ctrl+C
  • kill -9
  • User logs off or shuts down

4.4. How to use Shutdown Hook

Call the addShutdownHook(Thread hook) method of the java.lang.Runtime class to register a Shutdown Hook, and then define the operations that need to be performed during system exit in Thread. as follows:

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

4.5. Test example

  • First, register a Shutdown Hook.
  • Then, the system sleeps for 3 seconds and simulates certain operations.
  • Then, call an empty List, throw an exception, and prepare to end the program.
  • When the program is about to end, execute the content in the 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);
}

The result is as follows:

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

Points to note

  • After System.exit, when the Shutdown Hook begins to execute, other threads will continue to execute.
  • The thread safety of Shutdown Hook should be ensured.
  • Be particularly careful when using multiple Shutdown Hooks to ensure that the services they call will not be affected by other Hooks. Otherwise, there will be a situation where the service that the current Hook depends on is terminated by another Hook.

5. Custom exception reporter

5.1. Method 1: Implement the SpringBootExceptionReporter interface

(1): Simulate exception and create UserService

public class UserService {
    
    

}

Introduce UserService

@RestController
public class UserController {
    
    


    @Autowired
    UserService userService;

}

Since UserService is not added to the IOC container, the following error is reported after normal startup:

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

What should we do if we want to print the format we define?

(2): Custom exception

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

Register the exception reporter in the spring.factories file

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

(3): Run the program, the error output is as follows
Insert image description here

5.2. Method 2: Implement FailureAnalyzer interface

(1): Customize an exception

public class MyException extends RuntimeException{
    
    
}

(2): Implement the FailureAnalyzer interface

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

Register exception analyzer in spring.factories file

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

(3) Testing
needs to throw an exception when Spring Boot starts. For testing, we throw a custom exception when preparing the context and add it to MyApplicationRunListener in the demo.

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

The exception log printed after startup is as follows:

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

Description:

发生自定义异常

Action:

由于自定义了一个异常

6. Summary

  1. In order to present errors during the startup process in a more friendly and flexible way, Spring Boot has designed a set of exception handling solutions.
  2. Spring Boot proposes the concepts of failure analyzer (FailureAnalyzer) and error reporter (FailureAnalysisReporter). The former is used to convert error information into a more detailed error analysis report, and the latter is responsible for presenting this report.
  3. The responsibility of the error analyzer (FailureAnalyzer) is to identify the type of the current error and repackage the errors of interest. The result of the packaging is the error analysis report (FailureAnalysis).
  4. In addition to the original error information, the error analysis report (FailureAnalysis) adds a description and an action to prompt the user for subsequent processing.
  5. The Spring Boot framework exception handling system uses SPI extensively to load specific classes, which facilitates the framework's subsequent expansion of exception handling solutions, specific exception checks, and exception display methods.

Reference articles:
https://blog.csdn.net/m0_37298252/article/details/122879031
https://blog.51cto.com/u_14014612/6007663

Guess you like

Origin blog.csdn.net/weixin_49114503/article/details/131727677