上下文模式(Context):上下文对象、线程上下文、请求上下文,以及 Spring ApplicationContext

说实话,上下文模式是我在日常开发中用到最频繁的模式之一。它不像单例、工厂那样显眼,但几乎每个系统里都有它的影子。我刚开始接触这个概念时,觉得不就是个全局变量吗?后来踩过坑才明白——上下文模式的核心不是「存数据」,而是「解耦」。

说白了,上下文就是一个共享数据的容器。它让不同模块之间不用显式传递参数,就能访问到当前运行环境的信息。你想想看,一个请求从 Controller 到 Service 再到 Repository,如果每个方法都要把用户信息、请求 ID、语言偏好传一遍,那代码得多难看?

什么是上下文对象?

上下文对象,就是一个封装了当前环境信息的类。它通常包含请求相关的数据、配置信息、或者线程级别的状态。我习惯把它理解成「当前场景的快照」。

举个例子,一个电商系统里,用户下单时需要知道:当前是谁在操作、用的什么语言、来自哪个渠道。这些信息如果散落在各个方法参数里,维护起来就是噩梦。

核心要点:上下文对象把「环境信息」和「业务逻辑」分离。业务代码只关心自己的事,环境信息由上下文统一管理。

线程上下文:每个线程的「小世界」

多线程环境下,每个线程都有自己的上下文。我记得有一次排查线上问题,发现某个请求的日志里混入了另一个请求的数据。查了半天,原来是 ThreadLocal 没清理干净。

Java 里最常见的线程上下文实现就是 ThreadLocal。每个线程往里面存数据,其他线程看不到。这玩意儿用好了是神器,用不好就是定时炸弹。

// 一个简单的线程上下文示例
public class RequestContext {
    private static final ThreadLocal<RequestContext> CONTEXT_HOLDER = 
        new ThreadLocal<>();
    
    private String requestId;
    private Long userId;
    private String language;
    
    public static void set(RequestContext context) {
        CONTEXT_HOLDER.set(context);
    }
    
    public static RequestContext get() {
        return CONTEXT_HOLDER.get();
    }
    
    public static void clear() {
        CONTEXT_HOLDER.remove();
    }
    
    // getter/setter 省略
}
我曾经踩过的坑:线程池里的线程会被复用。如果你在任务执行完后没有调用 clear(),下一个任务就会读到上一个任务的上下文数据。这会导致数据错乱,而且极难排查。

我个人的习惯是:在过滤器或拦截器里统一设置上下文,在请求结束后统一清理。这样能保证每个请求的上下文都是干净的。

请求上下文:一次请求的生命周期

请求上下文,说白了就是一次 HTTP 请求范围内的共享数据。从请求进入网关,到返回响应,整个链路里都能访问到。

Web 框架里都有类似的概念。比如 Servlet 里的 HttpServletRequestsetAttribute 方法,或者 Spring MVC 里的 RequestContextHolder

// Spring 中的请求上下文使用
@Service
public class OrderService {
    
    public void createOrder(OrderDTO order) {
        // 从请求上下文中获取当前用户
        HttpServletRequest request = ((ServletRequestAttributes) 
            RequestContextHolder.currentRequestAttributes()).getRequest();
        Long userId = (Long) request.getAttribute("userId");
        
        // 业务逻辑...
    }
}

不过我不太建议直接在业务代码里这么写。耦合度太高了。更好的做法是封装一个工具类,或者用 AOP 把上下文数据注入到业务对象里。

Spring ApplicationContext:IoC 容器的「大脑」

Spring 的 ApplicationContext 是上下文模式的集大成者。它不只是存数据,还管理着整个 Bean 的生命周期、事件发布、资源加载、国际化等功能。

我刚开始学 Spring 时,总觉得 ApplicationContext 就是个高级的 Map。后来深入源码才发现,它背后是一整套 IoC 容器的实现。

功能 说明 我常用的场景
Bean 管理 创建、配置、组装 Bean 获取某个接口的所有实现类
事件发布 发布和监听应用事件 用户注册后发送通知
资源加载 统一访问文件、类路径资源 读取配置文件
国际化 根据 Locale 获取消息 多语言提示信息
// 获取 ApplicationContext 的几种方式
@Component
public class ContextUtil implements ApplicationContextAware {
    
    private static ApplicationContext context;
    
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) {
        context = applicationContext;
    }
    
    public static <T> T getBean(Class<T> clazz) {
        return context.getBean(clazz);
    }
    
    public static void publishEvent(ApplicationEvent event) {
        context.publishEvent(event);
    }
}
小技巧:如果你在非 Spring 管理的类里需要获取 Bean,可以用 ApplicationContextAware 或者 @Autowired 注入。但要注意,不要在构造函数里使用,因为那时 Bean 还没初始化完成。

上下文模式的核心结构

下面这张图展示了上下文模式的核心逻辑。你可以看到,上下文对象处于中间层,连接着各个模块。

上下文模式核心结构 上下文对象 Context HTTP 请求 Request 线程 Thread 应用配置 Application Controller 控制器 Service 服务层 Repository 数据层 左侧:上下文数据来源 | 中心:统一管理 | 右侧:业务消费方 数据流向:来源 → 上下文 → 业务模块

实际项目中的最佳实践

我在项目中总结了几条上下文模式的使用原则,分享给你:

  • 明确生命周期:上下文数据的存活时间要清晰。请求级别的数据不要放到应用级别的上下文里。
  • 最小化数据:只放真正需要共享的数据。放太多无关数据,上下文会变成「垃圾场」。
  • 提供清晰的 API:不要直接暴露 Map 或 ThreadLocal。封装成 getUserId()、getRequestId() 这样的方法。
  • 注意线程安全:多线程环境下,读写上下文要考虑并发问题。ThreadLocal 是线程隔离的,但如果是共享的 Map,需要加锁或用 ConcurrentHashMap。
我的经验之谈:上下文模式最大的价值在于「隐式传递」。它让业务代码不再需要显式传递那些「每个方法都需要但又跟业务无关」的参数。但代价是代码的显式性降低了——你没法一眼看出某个方法依赖了哪些上下文数据。所以,文档和命名规范很重要。

避坑指南

我曾经在一个微服务项目里,看到有人把数据库连接放到了请求上下文里。结果请求结束后连接没释放,数据库连接池很快就满了。嗯,这里要注意:上下文里只放轻量级的数据,不要放重量级资源。

另一个常见问题是上下文数据被意外修改。比如 A 模块往上下文里放了用户信息,B 模块不小心改了这个信息,后续的 C 模块就拿到了错误的数据。我建议把上下文对象设计成不可变的,或者至少提供只读视图。

特别注意:在异步场景下,ThreadLocal 会失效。因为异步任务会在另一个线程里执行,拿不到原线程的上下文。解决方案是:在提交异步任务前,把上下文数据复制一份传过去。

好了,关于上下文模式就聊这么多。它不像其他设计模式那样有固定的类结构,但它的思想贯穿了整个软件开发。从线程上下文到请求上下文,再到 Spring 的 ApplicationContext,本质上都是在解决同一个问题:如何优雅地共享环境信息。


公众号:蓝海资料掘金营,微信deep3321