0
点赞
收藏
分享

微信扫一扫

SpringBoot——异常报告器解析

异常报告器类介绍

接口规范

@FunctionalInterface
public interface SpringBootExceptionReporter {

    /**
     * Report a startup failure to the user.
     * @param failure the source failure
     * @return {@code true} if the failure was reported or {@code false} if default
     * reporting should occur.
     */
    boolean reportException(Throwable failure);

}

SpringBootExceptionReporter初始化情况

public ConfigurableApplicationContext run(String... args) {
    ......
    //初始化构造一个SpringBootExceptionReporter集合
    Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
    
    try {
        ...
        //给exceptionReporters集合填充具体的对象
        exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
                new Class[] { ConfigurableApplicationContext.class }, context);
        ...
    }
    catch (Throwable ex) {
        handleRunFailure(context, ex, exceptionReporters, listeners);
        throw new IllegalStateException(ex);
    }

    ...
    return context;
}

通过SpringFactoriesLoader加载

private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
    ClassLoader classLoader = getClassLoader();
    // Use names and ensure unique to protect against duplicates
    Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
    // 也是通过SpringFactoriesLoader技术来加载,见下图,默认就一个,这边实例化会调用其构造函数
    List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
    AnnotationAwareOrderComparator.sort(instances);
    return instances;
}
# Error Reporters
org.springframework.boot.SpringBootExceptionReporter=\
org.springframework.boot.diagnostics.FailureAnalyzers

SpringBoot通过SPI技术为SpringBootExceptionReporter接口指定了唯一的实现FailureAnalyzers

final class FailureAnalyzers implements SpringBootExceptionReporter {
    private final ClassLoader classLoader;

    private final List<FailureAnalyzer> analyzers;

    FailureAnalyzers(ConfigurableApplicationContext context, ClassLoader classLoader) {
        Assert.notNull(context, "Context must not be null");
        //设置类加载器
        this.classLoader = (classLoader != null) ? classLoader : context.getClassLoader();
        //获取容器中FailureAnalyzer实例对象并排序
        this.analyzers = loadFailureAnalyzers(this.classLoader);
        prepareFailureAnalyzers(this.analyzers, context);
    }

    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("Failed to load " + analyzerName, ex);
            }
        }
        AnnotationAwareOrderComparator.sort(analyzers);
        return analyzers;
    }

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

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

再看List<FailureAnalyzer> analyzers集合中的FailureAnalyzer

@FunctionalInterface
public interface FailureAnalyzer {

    /**
     * Returns an analysis of the given {@code failure}, or {@code null} if no analysis
     * was possible.
     * @param failure the failure
     * @return the analysis or {@code null}
     */
     //对异常进行分析返回FailureAnalysis
    FailureAnalysis analyze(Throwable failure);

}

public class FailureAnalysis {
    //错误描述
    private final String description;
    //引起错误的动作
    private final String action;
    //错误本身
    private final Throwable cause;
}

下面我们看下FailureAnalyzers实现的SpringBootExceptionReporter的异常报告方法

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

private FailureAnalysis analyze(Throwable failure, List<FailureAnalyzer> analyzers) {
    for (FailureAnalyzer analyzer : analyzers) {
        try {
            // 依次遍历每个analyzer分析,有结果就返回,这边走到AbstractFailureAnalyzer的analyze方法
            FailureAnalysis analysis = analyzer.analyze(failure);
            if (analysis != null) {
                return analysis;
            }
        }
        catch (Throwable ex) {
            if (logger.isDebugEnabled()) {
                logger.debug("FailureAnalyzer " + analyzer + " failed", ex);
            }
        }
    }
    return null;
}

AbstractFailureAnalyzer实现了FailureAnalyzer接口

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

    @Override
    public FailureAnalysis analyze(Throwable failure) {
        // 会先走到这里
        T cause = findCause(failure, getCauseType());
        if (cause != null) {
            // 找到子类感兴趣的异常后,调用具体实现类的分析方法
            return analyze(failure, cause);
        }
        return null;
    }

    protected abstract FailureAnalysis analyze(Throwable rootFailure, T cause);

    // 这个就是解析我的泛型参数,表明我对哪个异常感兴趣
    protected Class<? extends T> getCauseType() {
        return (Class<? extends T>) ResolvableType.forClass(AbstractFailureAnalyzer.class, getClass()).resolveGeneric();
    }

    protected final <E extends Throwable> E findCause(Throwable failure, Class<E> type) {
        // 依次遍历异常堆栈,如果有对当前异常感兴趣,就返回,否则返回null
        while (failure != null) {
            if (type.isInstance(failure)) {
                return (E) failure;
            }
            failure = failure.getCause();
        }
        return null;
    }
}

再来看看report(analysis, this.classLoader)方法

// 分析出结果之后,调用report方法之后报告异常
private boolean report(FailureAnalysis analysis, ClassLoader classLoader) {
    // FailureAnalysisReporter这个实现类只有一个,是LoggingFailureAnalysisReporter
    List<FailureAnalysisReporter> reporters = SpringFactoriesLoader.loadFactories(FailureAnalysisReporter.class,
            classLoader);
    if (analysis == null || reporters.isEmpty()) {
        return false;
    }
    for (FailureAnalysisReporter reporter : reporters) {
        // 输出异常报告
        reporter.report(analysis);
    }
    return true;
}
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));
        }
    }

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

}

SpringBoot异常处理流程

public ConfigurableApplicationContext run(String... args) {
    ......
    Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
    ......
    try {
        ......
        exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
                new Class[] { ConfigurableApplicationContext.class }, context);
        ......
    }
    catch (Throwable ex) {
        //对启动过程中的失败进行处理
        handleRunFailure(context, ex, exceptionReporters, listeners);
        throw new IllegalStateException(ex);
    }

    ......
    return context;
}

点进去handleRunFailure(context, ex, exceptionReporters, listeners)

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);
}
handleRunFailure(context, ex, exceptionReporters, listeners)逻辑

handleExitCode(context, exception)

private void handleExitCode(ConfigurableApplicationContext context, Throwable exception) {
    // 获取框架内对这个异常定义的exitCode,代表退出状态码,为0代表正常退出,不为0标识异常退出
    int exitCode = getExitCodeFromException(context, exception);
    if (exitCode != 0) {
        if (context != null) {
            // 发布一个ExitCodeEvent事件
            context.publishEvent(new ExitCodeEvent(context, exitCode));
        }
        SpringBootExceptionHandler handler = getSpringBootExceptionHandler();
        if (handler != null) {
            //记录exitCode
            handler.registerExitCode(exitCode);
        }
    }
}

listeners.failed(context, exception)

  • 发布ApplicationFailedEvent事件
public void failed(ConfigurableApplicationContext context, Throwable exception) {
    for (SpringApplicationRunListener listener : this.listeners) {
        callFailedListener(listener, context, exception);
    }
}

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

public class EventPublishingRunListener implements SpringApplicationRunListener, Ordered {
    @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);
        }
    }
}

reportFailure(exceptionReporters, exception)

  • SpringBootExceptionReporter实现类调用reportException方法
  • 成功处理的话,记录已处理异常
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);
    }
}

context.close()

  • 更改应用上下文状态
  • 销毁单例bean
  • 将beanFactory置为空
  • 关闭web容器(web环境)
  • 移除shutdownHook
public abstract class AbstractApplicationContext extends DefaultResourceLoader
        implements ConfigurableApplicationContext { 
    @Override
    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
                }
            }
        }
    }

    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 {
                // Publish shutdown event.
                // 发布容器关闭事件
                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);
        }
    }
}

ReflectionUtils.rethrowRuntimeException(exception)

  • 重新抛出异常
public static void rethrowRuntimeException(Throwable ex) {
    if (ex instanceof RuntimeException) {
        throw (RuntimeException) ex;
    }
    if (ex instanceof Error) {
        throw (Error) ex;
    }
    throw new UndeclaredThrowableException(ex);
}

shutdownHook介绍

  • 作用:JVM退出时执行的业务逻辑
  • 添加:Runtime.getRuntime().addShutdownHook()
  • 移除:Runtime.getRuntime().removeShutdownHook(this.shutdownHook)

背景
在开发中,遇到这种情况,多个线程同时工作,突然一个线程遇到了fetal的错误,需要立即终止程序,等人工排查解决了问题之后重新启动。但是这样会有一个问题,程序终止时,其他线程可能正在进行重要操作,比如发一个message到另一个模块,并更新数据库状态。突然终止,可能会让这个操作只完成一半,从而导致数据不一致。

解决方案是:参考数据库Transaction原子性的概念,将这一系列重要操作看作一个整体,要么全部完成,要么全部不完成。为方便表述,我们把这一系列重要操作记为操作X。
当程序即将退出时,查看当前是否有操作X在执行中,

如果有,等待其完成然后退出。且期间不再接受新的操作X。如果操作X执行之间过长,终止并回滚所有状态。
如果没有,则可以立即退出。
在程序退出的时候,做一些Check,保证已经开始的操作X的原子性,这里就用到了Runtime.ShutdownHook。

什么是Shutdown Hook

Shutdown hook是一个initialized but unstarted thread。当JVM开始执行shutdown sequence时,会并发运行所有registered Shutdown Hook。这时,在Shutdown Hook这个线程里定义的操作便会开始执行。

需要注意的是,在Shutdown Hook里执行的操作应当是不太耗时的。因为在用户注销或者操作系统关机导致的JVM shutdown的例子中,系统只会预留有限的时间给未完成的工作,超时之后还是会强制关闭。

什么时候会调用Shutdown Hook

程序正常停止
  • Reach the end of program
  • System.exit
程序异常退出
  • NPE
  • OutOfMemory
受到外界影响停止
  • Ctrl+C
  • 用户注销或者关机

如何使用Shutdown Hook

调用java.lang.Runtime这个类的addShutdownHook(Thread hook)方法即可注册一个Shutdown Hook,然后在Thread中定义需要在system exit时进行的操作。如下:

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

测试例子

首先,注册了一个Shutdown Hook。
然后,系统Sleep 3秒,模拟进行某些操作。
然后,调用一个空的List,抛出异常,准备结束程序。
在程序将要结束的时候,执行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);
}

结果如下:

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

需要注意的点

  • 当System.exit之后,当Shutdown Hook开始执行时,其他的线程还是会继续执行。
  • 应当保证Shutdown Hook的线程安全。
  • 在使用多个Shutdown Hook时一定要特别小心,保证其调用的服务不会被其他Hook影响。否则会出现当前Hook所依赖的服务被另外一个Hook终止了的情况。

参考:
https://my.oschina.net/liwanghong/blog/3167733

https://www.cnblogs.com/maxstack/p/9112711.html

https://www.cnblogs.com/tracer-dhy/p/10041643.html

举报

相关推荐

0 条评论