学习 Java Web 的时候,经常会碰到 Context:ServletContext、PageContext,还有 Spring 的 ApplicationContext。查词典得到“上下文”,似乎翻译出来了,却还是不知道代码里为什么需要它。
我现在更愿意把它理解成:一个对象进行工作时,用来取得相关环境信息和能力的入口。 这只是帮助理解的说法,具体保存什么信息、能做哪些事,还得看前面的名字和所在框架。
比如一个处理请求的方法,需要知道应用的配置、当前用户的会话,还可能需要调用业务对象。这些信息各有自己的范围,框架便提供了相应的访问入口。看到 Context 时,把它放回具体场景里,比单独记一个中文解释更容易理解。
一、ServletContext:当前 Web 应用的环境
Servlet 接收请求以后,有时需要读取整个应用的配置,访问应用里的资源,或者取得其他组件放入的共享数据。ServletContext 提供了这类入口。
例如,在继承 HttpServlet 的类中,引入 javax.servlet.ServletContext 后,可以在处理请求的方法里写:
ServletContext context = getServletContext();
String appName = context.getInitParameter("appName");其中 appName 可以通过 web.xml 的应用级参数配置。下面的片段放在 <web-app> 元素内:
<context-param>
<param-name>appName</param-name>
<param-value>学习笔记</param-value>
</context-param>getInitParameter() 读取的是应用初始化参数。运行期间需要共享对象时,则使用属性接口 setAttribute() 和 getAttribute(),它们是另一套存取方式:
context.setAttribute("siteName", "学习笔记");
String siteName = (String) context.getAttribute("siteName");同一 Web 应用、同一 JVM 内的 Servlet 可以通过这个应用上下文访问共享属性。它不是整个服务器上所有应用共用的对象;应用部署到多个 JVM 时,各个实例也不会因为使用了 ServletContext 就自动共享内存。这个范围在 ServletContext API 中有明确说明。
“共享”也不代表存进去的对象自动具备线程安全性。例如把普通集合放进去,多条请求同时修改集合,仍然需要处理并发问题。当前用户信息则不适合放在应用级属性里,否则不同用户会共用同一份数据。
二、PageContext:JSP 页面执行时的访问入口
JSP 中的 pageContext 可以访问当前请求、响应、Session、ServletContext,以及页面输出对象。它把页面执行时常用的对象和操作集中到一个入口里。
数据能被谁看到,取决于放在哪个作用域。下面是一段 JSP 示例,用 EL 分别读取页面属性和请求属性:
<%
pageContext.setAttribute("message", "当前页面可见");
pageContext.setAttribute("notice", "当前请求可见",
javax.servlet.jsp.PageContext.REQUEST_SCOPE);
%>
<p>${pageScope.message}</p>
<p>${requestScope.notice}</p>第一个调用默认使用 page 作用域;第二个明确使用 request 作用域。这里用脚本设置属性是为了演示接口,实际页面展示通常配合 EL、JSTL 使用。
| 作用域 | 数据的有效范围 | 常见用途 |
|---|---|---|
| page | 当前 JSP 的本次执行 | 页面临时变量 |
| request | 同一次请求,包括请求转发 | 本次查询结果、提示信息 |
| session | 同一个用户会话 | 会话中的用户状态 |
| application | 当前 Web 应用、当前 JVM | 应用级共享信息 |
例如,将数据放进 request 后转发到另一个 JSP,仍可在该请求中读取;如果改为重定向,浏览器会发起新请求,原来的 request 属性不会自动带过去。
调用 pageContext.findAttribute("name") 时,会依次从 page、request、session、application 作用域查找,找到就返回。因此,同名属性可能来自不同范围,排查页面取值问题时需要注意这一点。作用域和访问方法可以对照 PageContext API。
PageContext 让页面可以访问这些不同范围,但这些范围并不会因此合并在一起。
三、ApplicationContext:Spring 管理对象的环境
Spring 的 ApplicationContext 也是一个环境入口,不过关注点换成了 Bean 的创建、依赖关系和管理。
下面假定项目已引入 Spring,类路径里的 applicationContext.xml 已定义一个名为 userService 的 Bean:
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class ContextDemo {
public static void main(String[] args) {
ClassPathXmlApplicationContext context =
new ClassPathXmlApplicationContext("applicationContext.xml");
try {
Object userService = context.getBean("userService");
System.out.println(userService.getClass().getName());
} finally {
context.close();
}
}
}getBean() 取得的是 Spring 根据配置创建和管理的对象,它的依赖关系也由容器处理。ApplicationContext 因而不只是保存一组键值对,还负责组织这些对象的创建、使用和销毁。
除了 BeanFactory 的基本能力,它还提供资源访问、消息解析和事件机制。这里先记住这些用途,不必为了理解 Context 就把整个容器实现读完。相关能力见 Spring 4.2.5 ApplicationContext 文档。
实际业务代码通常通过依赖注入取得所需对象,而不是每个方法都创建一个 ApplicationContext。上面的代码只是展示入口;如果不断新建容器,就可能得到多套 Bean 和资源。
四、Android Context:另一个环境入口的例子
Android 也有名为 Context 的抽象类,用于访问应用资源和系统服务等。比如,引入 android.widget.Toast 后,在 Activity 的实例方法中可以显示一个简单提示:
Toast.makeText(this, "保存成功", Toast.LENGTH_SHORT).show();这里能够传 this,是因为 Activity 本身属于 Context 类型体系,并不是任意对象的 this 都能作为 Context。相关类型关系可见 Activity API。
Android 的例子只是帮助对照概念。它和 ServletContext、ApplicationContext 没有共同的 Context 接口,也不能因为名字相似就认为它们管理同一种东西。
遇到 Context 时,先看它管什么
把这几个例子放在一起,我觉得理解 Context 可以先问三个问题:
- 它代表哪个范围的环境:页面、Web 应用,还是一个 Spring 容器?
- 它能提供哪些信息或操作:读配置、访问对象、找资源,还是调用系统能力?
- 它什么时候有效,数据由谁共享,又由谁负责清理?
这样再读具体 API,会比单独琢磨“上下文”三个字容易一些。“容器”是某些场景下有用的比喻,但环境信息、资源访问和生命周期,也都是这个概念里需要留意的部分。