上下文模式(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 省略
}
我个人的习惯是:在过滤器或拦截器里统一设置上下文,在请求结束后统一清理。这样能保证每个请求的上下文都是干净的。
请求上下文:一次请求的生命周期
请求上下文,说白了就是一次 HTTP 请求范围内的共享数据。从请求进入网关,到返回响应,整个链路里都能访问到。
Web 框架里都有类似的概念。比如 Servlet 里的 HttpServletRequest 的 setAttribute 方法,或者 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);
}
}
上下文模式的核心结构
下面这张图展示了上下文模式的核心逻辑。你可以看到,上下文对象处于中间层,连接着各个模块。
实际项目中的最佳实践
我在项目中总结了几条上下文模式的使用原则,分享给你:
- 明确生命周期:上下文数据的存活时间要清晰。请求级别的数据不要放到应用级别的上下文里。
- 最小化数据:只放真正需要共享的数据。放太多无关数据,上下文会变成「垃圾场」。
- 提供清晰的 API:不要直接暴露 Map 或 ThreadLocal。封装成 getUserId()、getRequestId() 这样的方法。
- 注意线程安全:多线程环境下,读写上下文要考虑并发问题。ThreadLocal 是线程隔离的,但如果是共享的 Map,需要加锁或用 ConcurrentHashMap。
避坑指南
我曾经在一个微服务项目里,看到有人把数据库连接放到了请求上下文里。结果请求结束后连接没释放,数据库连接池很快就满了。嗯,这里要注意:上下文里只放轻量级的数据,不要放重量级资源。
另一个常见问题是上下文数据被意外修改。比如 A 模块往上下文里放了用户信息,B 模块不小心改了这个信息,后续的 C 模块就拿到了错误的数据。我建议把上下文对象设计成不可变的,或者至少提供只读视图。
好了,关于上下文模式就聊这么多。它不像其他设计模式那样有固定的类结构,但它的思想贯穿了整个软件开发。从线程上下文到请求上下文,再到 Spring 的 ApplicationContext,本质上都是在解决同一个问题:如何优雅地共享环境信息。
公众号:蓝海资料掘金营,微信deep3321