spring boot 全局异常处理无法直接捕获 apollo 配置刷新产生的异常,因为 apollo 客户端的监听器运行在独立的后台线程中,脱离了 spring mvc 的请求上下文。要解决这个问题,需要手动实现一个 `threadfactory` 来包装 apollo 创建的线程,并在其中绑定当前的 `requestcontextholder`,从而让全局异常处理器能够接管这些线程抛出的异常。

Apollo 监听器线程为何脱离全局异常处理
Apollo 客户端在启动时会初始化一个定时任务线程池,用于轮询远程配置并触发本地监听器的回调。这个线程池由 Apollo 内部定义,默认使用普通的 `ThreadFactory` 创建线程。当配置发生变化时,Apollo 会调用你注册的 `ConfigChangeListener` 或 `NotificationListener`。由于这些回调是在 Apollo 的后台线程中执行的,它们没有处于 Spring DispatcherServlet 管理的请求上下文中。因此,如果监听器代码抛出未捕获的异常,Spring 的全局异常处理器(如 `@ControllerAdvice`)无法感知到这些异常,导致异常被静默丢弃或仅打印到日志中,而不会触发预期的错误响应逻辑。

import com.ctrip.framework.apollo.ConfigService;
import com.ctrip.framework.apollo.model.ConfigChange;
import com.ctrip.framework.apollo.model.ConfigChangeEvent;
import org.springframework.stereotype.Component;
@Component
public class MyApolloListener implements ConfigChangeListener {
@Override
public void onChange(ConfigChangeEvent changeEvent) {
// 模拟一个会在 Apollo 后台线程中抛出的异常
throw new RuntimeException("Apollo config changed error");
}
}
上述代码展示了典型的 Apollo 监听器实现。当配置变更时,`onChange` 方法会被 Apollo 的后台线程调用。如果这里抛出异常,由于不在 Web 请求线程中,Spring 的全局异常处理机制不会介入。这验证了为什么我们需要干预线程的执行环境。
通过自定义 ThreadFactory 绑定 RequestContext
解决该问题的核心思路是拦截 Apollo 线程的创建过程,将当前请求的上下文信息传递到新创建的线程中。Spring 提供了 `RequestContextAttribute` 和 `RequestContextListener` 来处理这种跨线程的上下文传递,但在 Apollo 的场景下,更直接的方式是实现一个自定义的 `ThreadFactory`。在这个工厂中,我们可以获取当前线程的 `RequestAttributes`,并将其设置到新建的线程中,或者使用 `InheritableThreadLocal` 来传递必要的上下文。然而,Apollo 的线程池通常是单例且长期存在的,并不一定伴随每个 HTTP 请求。因此,更稳健的做法是让 Apollo 的线程在执行回调前,尝试从 `RequestContextHolder` 中获取当前的 Servlet 请求上下文。如果存在,则将其绑定;如果不存在,则创建一个临空的上下文。这样,当异常发生时,全局异常处理器可以通过检查 `RequestContextHolder` 来判断是否处于 Web 请求环境中,从而决定如何处理异常。
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.lang.reflect.Field;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
public class RequestContextAwareThreadFactory implements ThreadFactory {
private final ThreadFactory defaultFactory;
private final AtomicInteger threadNumber = new AtomicInteger(1);
public RequestContextAwareThreadFactory(ThreadFactory defaultFactory) {
this.defaultFactory = defaultFactory;
}
@Override
public Thread newThread(Runnable r) {
Thread originalThread = defaultFactory.newThread(r);
return new Thread(originalThread.getThreadGroup(), new Runnable() {
@Override
public void run() {
// 尝试获取当前请求的上下文
RequestAttributes currentContext = RequestContextHolder.getRequestAttributes();
try {
// 将当前上下文传递给新线程
if (currentContext != null) {
RequestContextHolder.setRequestAttributes(currentContext, true);
}
r.run();
} finally {
// 清理上下文,防止内存泄漏
RequestContextHolder.resetRequestAttributes();
}
}
}, originalThread.getName());
}
}
这段代码定义了一个 `RequestContextAwareThreadFactory`。它包装了原始的 `Runnable`,在新线程执行前,从主线程获取 `RequestAttributes` 并设置到子线程的 `RequestContextHolder` 中。执行完毕后,立即重置上下文。这使得 Apollo 的后台线程能够“看到”当前的 Web 请求上下文,从而在全局异常处理器中可以被正确识别和处理。需要注意的是,Apollo 客户端本身不直接支持注入自定义的 `ThreadFactory` 给其内部线程池,因此通常需要通过反射或扩展 Apollo 的初始化流程来实现这一替换,或者在应用启动时通过配置 Apollo 的底层线程池属性(如果支持)来应用此工厂。
全局异常处理器如何区分并处理此类异常
一旦通过自定义 `ThreadFactory` 将 Web 上下文绑定到了 Apollo 的后台线程,全局异常处理器就需要能够识别这些异常并做出相应处理。在 `@ControllerAdvice` 中,我们可以捕获所有未处理的异常,并检查当前是否存在有效的 `RequestAttributes`。如果存在,说明该异常可能来自与 Web 请求相关的线程(包括我们绑定的 Apollo 线程),此时可以返回标准的错误响应。如果不存在,则可能是纯后台任务异常,可以选择记录日志或忽略。此外,为了更精确地处理 Apollo 刷新异常,可以在全局异常处理器中专门针对 Apollo 相关的异常类型进行捕获,例如 `com.ctrip.framework.apollo.common.exception.ApolloException`。结合上下文检查,可以实现更细粒度的控制。
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.RequestAttributes;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public void handleException(Exception e) {
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
if (attributes != null) {
// 如果存在请求上下文,说明可能来自绑定的 Apollo 线程或其他 Web 相关线程
// 这里可以记录详细日志,或者根据业务需求返回特定的错误码
System.err.println("Caught exception in web context: " + e.getMessage());
} else {
// 如果没有请求上下文,可能是纯后台线程异常
System.err.println("Caught exception in background thread: " + e.getMessage());
}
}
}
上述全局异常处理器演示了如何根据 `RequestContextHolder` 的状态来区分异常的来源。当 Apollo 的后台线程通过自定义 `ThreadFactory` 绑定了 Web 上下文后,其抛出的异常将被视为“Web 上下文中的异常”,从而进入第一个分支进行处理。这确保了即使异常发生在非请求线程,只要它与当前的 Web 会话有关联,就能得到适当的处理。这种方法不仅解决了 Apollo 刷新异常的捕获问题,也为其他类似场景(如异步任务、定时任务)提供了统一的异常处理框架。


















