一、基本概念
说起ServletContext,一些人会产生误解,以为一个servlet对应一个ServletContext。其实不是这样的,事实是一个web应用对应一个ServletContext,所以ServletContext的作用范围是整个应用,明确这点很重要,这是基础中的基础。
我曾经想,为什么不起名叫WebContext或者ApplicationContext或者WebApplicationContext?这样见名知意多好。后来我想这也可能是有历史原因的:最初的客户端-服务端的架构模型非常简单,服务端运行着一些servlet用来处理客户端的请求。那个时候服务器很轻量级,运行一个应用,应用就由一堆servlet组成。所以这样简单的服务器也被称作servlet容器,主要作用就是运行servlet的。那么提供给应用的上下文就叫做ServletContext。(这个纯属个人意淫_,不对勿喷)
一个web应用对应一个ServletContext实例,这个实例是应用部署启动后,servlet容器为应用创建的。ServletContext实例包含了所有servlet共享的资源信息。通过提供一组方法给servlet使用,用来和servlet容器通讯,比如获取文件的MIME类型、分发请求、记录日志等。
这里需要注意一点,如果你的应用是分布式部署的,那么每台服务器实例上部署的应用实例都各自拥有一个ServletContext实例。
二、源码分析
1 ServletContext(上)
下面我们先逐个分析ServletContext中servlet3.0之前的规范定义的方法(其中有三个方法是servlet3.0规范定义的,放在一起讲是出于方便的考虑,讲到的时候会特别说明)。
public interface ServletContext {
public String getContextPath();
public ServletContext getContext(String uripath);
public int getMajorVersion();
public int getMinorVersion();
public int getEffectiveMajorVersion();
public int getEffectiveMinorVersion();
public String getServerInfo();
public String getServletContextName();
public String getMimeType(String file);
public void log(String msg);
public void log(String message, Throwable throwable);
public Set<String> getResourcePaths(String path);
public URL getResource(String path) throws MalformedURLException;
public InputStream getResourceAsStream(String path);
public RequestDispatcher getRequestDispatcher(String path);
public RequestDispatcher getNamedDispatcher(String name);
public String getRealPath(String path);
public String getInitParameter(String name);
public Enumeration<String> getInitParameterNames();
public boolean setInitParameter(String name, String value);
public Object getAttribute(String name);
public Enumeration<String> getAttributeNames();
public void setAttribute(String name, Object object);
public void removeAttribute(String name);
// 省略servlet3.0及3.1规范定义的方法
}
1.1 getContextPath
方法返回web应用的上下文路径。就是我们部署的应用的根目录名称。拿Tomcat举例,我们在webapps部署了应用demo。那么方法返 回"/demo"。如果是部署在ROOT下,那么方法返回空字符串""。这里的路径可以再server.xml里面修改,比如我们的demo应用路径修改 为"/test":
<Context docBase="demo" path="/test" reloadable="true" source="org.eclipse.jst.j2ee.server:demo"/>
那么方法将会返回"/test"。
1.2 getContext
方法入参为uriPath(String),是一个资源定位符的路径。返回一个ServletContext实例。我们说过一个web应用对应一个ServletContext实例,那么这个方法根据资源的路径返回其servlet上下文。
比如说我们当前应用是demo,这个时候我们要访问servlet容器中的另外一个应用test中的资源index.jsp,假使资源的路径为/test/index.jsp。那么我们就可用通过调用getContext("/test/index.jsp")去获取test应用的上下文。如果在servlet容器中找不到该资源或者该资源限制了外部的访问,那么方法返回null。(这个方法一般配合RequestDispatcher使用,实现请求转发)。
1.3 getMajorVersion、getMinorVersion、getEffectiveMajorVersion、getEffectiveMinorVersion
getMajorVersion和getMinorVersion分别返回当前servlet容器支持的Servlet规范最高版本和最低版本。
getEffectiveMajorVersion和getEffectiveMinorVersion分别返回当前应用基于的Servlet规范最高版本和最低版本,是servlet3.0规范增加的新特性。
所以一般情况下:getMajorVersion>=getEffectiveMajorVersion>getEffectiveMinorVersion>=getMinorVersion。
1.4 getServerInfo、getServletContextName
getServerInfo返回servlet容器的名称和版本,格式为servername/versionnumber。比如我在Tomcat下测试,输出的信息是:Apache Tomcat/9.0.0.M10。当然容器也可以多返回些额外的信息,这个就看各个servlet容器的实现了。
getServletContextName返回应用的名称,这里的名称是web.xml里面配置的display-name,如果没配置则返回null。
<display-name>Archetype Created Web Application</display-name>
1.5 getMimeType
方法返回文件的MIME类型,MIME类型是容器配置的。可用通过web.xml进行配置,比如:
<mime-mapping>
<extension>doc</extension>
<mime-type>application/vnd.ms-word</mime-type>
</mime-mapping>
那么我们用浏览器打开文件的时候发现如果是doc文件,则会调用相应的word程序去打开。
1.6 log
两个重载的log方法都是记录日志到servlet日志文件,这个对于有编程经验的来说没什么好解释的。需要注意的是servlet日志文件的路径由具体的 servlet容器自己去决定。如果你是在MyEclipse、STS这类的IDE中跑应用的话,那么日志信息将在控制台(Console)输出。如果是 发布到Tomcat下的话,日志文件是Tomcat目录下的/logs/localhost.yyyy-MM-dd.log。
1.7 getResourcePaths
根据传入的路径,列出该路径下的所有资源路径。返回的路径是相对于web应用的上下文根或者相对于/WEB-INF/lib目录下的各个JAR包里面的/META-INF/resources目录。
比如我们的web应用下有这些资源:/welcome.html,/catalog/index.html,/catalog/products.html, /catalog/offers/books.html,/catalog/offers/music.html,/customer /login.jsp,/WEB-INF/web.xml,/WEB-INF/classes /com.acme.OrderServlet.class,/WEB-INF/lib/catalog.jar!/META-INF/resources/catalog/moreOffers/books.html。
如果调用方法getResourcePaths("/"),那么返回的是{"/welcome.html", "/catalog/", "/customer/", "/WEB-INF/"}。
如果调用方法getResourcePaths("/catalog/"),那么返回的是{"/catalog/index.html", "/catalog/products.html", "/catalog/offers/"}。
这里需要注意的是:1.路径一定要以"/"开头,结尾的"/"可要可不要。2.路径要从应用根目录下开始,如果调用方法getResourcePaths("/offers/"),此时返回的是null。
1.8 getResource和getResourceAsStream
getResource将指定路径的资源封装成URL实例并返回,getResourceAsStream获取指定路径资源的输入流InputStream并返回。关于URL和InputStream的解释和使用,不在本篇博文的关注点。
这里和上面一样,资源的路径以"/"开头,这个路径是相对于应用上下文根目录或者相对于/WEB-INF/lib目录下的各个JAR包里面的/META-INF/resources目录。在搜索资源的时候,servlet容器先是从应用上下文根目录下搜索再从JAR文件搜索,对于JAR文件的搜索顺序(哪个JAR先,哪个JAR后),servlet规范没有规定。
1.9 getRequestDispatcher和getNamedDispatcher
将指定的资源包装成RequestDispatcher实例并返回。区别是前者根据资源路径(该路径相对于当前应用上下文根),后者根据资源的名称(通过服务器控制台或者web.xml里面配置的,比如web.xml里面配置servlet的名称)。
RequestDispatcher这个接口,看名字就知道主要用来进行分发的。所有的资源都可以包装成RequestDispatcher实例(主要是用于包装servlet),然后调用它的方法进行转发和包含。这里比较简单,不过过多赘述,直接上源码。(这里只列关键的两个方法,简单介绍下forward。如有兴趣深入,可自行去研究RequestDispatcher相关知识)
public interface RequestDispatcher {
public void forward(ServletRequest request, ServletResponse response) throws ServletException, IOException;
public void include(ServletRequest request, ServletResponse response) throws ServletException, IOException;
}
还拿我们上面的demo应用例子来说:客户端访问http://xxx.xxx.xxx.xxx:xxxx/demo/test,这个时候访问我们的 TestServlet。这个时候如果要把请求转发到另外一个servlet,假使这个servlet的资源路径是/demo/test2。那么我们可以 调用getRequestDispatcher("/demo/test2")把资源包装成RequestDispatcher实例,再调用forward的方法。就实现了请求的转发。
请求的转发使用起来很简单,因为servlet容器提供了ServletContext实例,我们在应用中只需要调用它的API就行。这个时候容器为我们做了很多事情,容器会根据资源的路径去获取ServletContext实例,正如上面的getContext方法。这里不一定就是当前 ServletContext,可以使其他应用的。如果找到了ServletContext,容器再将资源包装成RequestDispatcher实例进行转发。
1.10 getRealPath
根据资源虚拟路径,返回实际路径。
比如说应用中有个JSP页面index.jsp,调用getRealPath("index.jsp"),则返回index.jsp文件在文件系统中的绝对路径。在windows下或许是这样:D:\xxx\xxx\index.jsp,在linux下或许是这样:/root/xxx/index.jsp。
这里可能存在应用中有多个index.jsp,它们在不同的路径下。这时候servlet容器是先从应用根目录下向下查找,再从/WEB-INF/lib目录下的各个JAR包里面的/META-INF/resources目录查找(前提是servlet容器解压了这些JAR),把找到的第一个资源绝对路径返回。
1.11 getInitParameter、getInitParameterNames、setInitParameter
getInitParameter和getInitParameterNames是用来获取应用的初始化参数相关数据的,参数的作用域是整个应用。这个参数是在web.xml里面配置的(如下所示)或者使用setInitParameter方法设置。getInitParameter是根据参数名获取参数值,getInitParameterNames获取参数名集合。对于setInitParameter需要注意的是,如果设置的参数名已经存在会设置失败,这是servlet3.0规范增加的新特性。
这里需要注意的是在web.xml配置多个初始化参数时,应该写多个<context-param></context-param>对,而不是在一个<context-param></context-param>对里面写多个<param-name></param-name>和<param-value></param-value>对。
<context-param>
<param-name>param1</param-name>
<param-value>1</param-value>
</context-param>
<context-param>
<param-name>param2</param-name>
<param-value>2</param-value>
</context-param>
1.12 getAttribute、getAttributeNames、setAttribute、removeAttribute
应用的属性相关操作,建议属性名遵循java包名的风格。这里需要讲的是setAttribute,当设置的属性名已经存在,则会替换掉就的属性值。如果设置属性值为null,那么效果和removeAttribute是一样的。
这里需要注意的是,这些属性都是应用级的,在一个地方设置的属性可以被应用中其他地方使用。
2 ServletContext(下)
下面我们逐个分析ServletContext中servlet3.0及3.1的规范中定义的方法。
public interface ServletContext {
// 省略servlet3.0之前的规范定义的方法
public static final String TEMPDIR = "javax.servlet.context.tempdir";
public static final String ORDERED_LIBS = "javax.servlet.context.orderedLibs";
public ServletRegistration.Dynamic addServlet(String servletName, String className);
public ServletRegistration.Dynamic addServlet(String servletName, Servlet servlet);
public ServletRegistration.Dynamic addServlet(String servletName, Class <? extends Servlet> servletClass);
public <T extends Servlet> T createServlet(Class<T> clazz) throws ServletException;
public ServletRegistration getServletRegistration(String servletName);
public Map<String, ? extends ServletRegistration> getServletRegistrations();
public FilterRegistration.Dynamic addFilter(String filterName, String className);
public FilterRegistration.Dynamic addFilter(String filterName, Filter filter);
public FilterRegistration.Dynamic addFilter(String filterName, Class <? extends Filter> filterClass);
public <T extends Filter> T createFilter(Class<T> clazz) throws ServletException;
public FilterRegistration getFilterRegistration(String filterName);
public Map<String, ? extends FilterRegistration> getFilterRegistrations();
public void addListener(String className);
public <T extends EventListener> void addListener(T t);
public void addListener(Class <? extends EventListener> listenerClass);
public <T extends EventListener> T createListener(Class<T> clazz) throws ServletException;
public SessionCookieConfig getSessionCookieConfig();
public void setSessionTrackingModes(Set<SessionTrackingMode> sessionTrackingModes);
public Set<SessionTrackingMode> getDefaultSessionTrackingModes();
public Set<SessionTrackingMode> getEffectiveSessionTrackingModes();
public JspConfigDescriptor getJspConfigDescriptor();
public ClassLoader getClassLoader();
public void declareRoles(String... roleNames);
public String getVirtualServerName();
}
2.1 TEMPDIR和ORDERED_LIBS
这是两个ServletContext的属性名,Servlet规范建议属性名遵循java包名的风格。
TEMPDIR 对应的属性值是个java.io.File对象,表示该ServletContext对应的临时目录。拿tomcat来说,比如有个应用叫demo,那么 demo对应的ServletContext临时目录就是{Tomcat目录}\work\Catalina\localhost\demo。
ORDERED_LIBS 对应的属性值是个java.util.List<java.lang.String>对象,每个元素表示WEB-INF/lib目录下的 JAR包名称。这些名称的排序是根据它们的web-fragment名称排序的,如果<absolute-ordering>里面没有配 置<others>的话,可能会存在冲突。如果没有在web.xml里面配置<absolute-ordering>或在web-fragment.xml里面配置<ordering>,那么属性值为null。这里涉及到servlet3.0增加的新特性————web模块化,感兴趣的读者自行去了解,这里不做引申。
2.2 addServlet、createServlet、getServletRegistration、getServletRegistrations
三个重载方法addServlet提供了编程式的向servlet容器中注入servlet的方式,其达到的效果和在web.xml中配置或者使用WebServlet注解配置servlet是一样的。只不过这种方式更加灵活、动态。addServlet方法返回的是ServletRegistration实例(ervletRegistration.Dynamic集成ServletRegistration),使用这个实例可以进一步配置servlet的注册信息,比如配置url-pattern。
createServlet的所用是实例化一个servlet,得到一个servlet实例。这个传入的表示servlet类的Class实例必须要有个无参构造函数,因为这个方法的底层实现就是利用发射机制调用默认的无参构造函数进行实例化。这个方法返回的servlet实例没有多大的使用意义,可能还是需要调用相应的addServlet进行注册。
getServletRegistration根据servlet名称查找其注册信息,即ServletRegistration实例。
getServletRegistrations是查询当前servlet上下文中所有的servlet的注册信息。
至于这几个方法的使用,这里不作详解。
2.3 addFilter、createFilter、getFilterRegistration、getFilterRegistrations
这里的几个方法和上面的servlet的几个方法很类似,也是提供编程的方式实现filter。不作赘述。
2.4 addListener、createListener
这里的几个方法和上面的servlet的几个方法很类似,也是提供编程的方式实现listener。不作赘述。注意观察可以发现,这里没有类似getXxxRegistration的方法,这是因为listener不需要像url-pattern的这类注册信息。
2.5 getSessionCookieConfig、setSessionTrackingModes、getDefaultSessionTrackingModes、getEffectiveSessionTrackingModes
getSessionCookieConfig方法返回SessionCookieConfig实例,这个实例可以用于获取和设置会话跟踪的cookie的属性。多次调用getSessionCookieConfig方法返回的SessionCookieConfig实例是同一个,说明SessionCookieConfig是单例的。
setSessionTrackingModes用于设置会话的跟踪模式,getDefaultSessionTrackingModes用于获取默认的会话跟踪模式,getEffectiveSessionTrackingModes用于获取有效的会话跟踪模式。默认情况下,getDefaultSessionTrackingModes返回的会话跟踪模式就是有效的。
关于session和cookie的知识这里不做扩展。
2.6 getJspConfigDescriptor
获取web.xml和web-fragment.xml中配置的<jsp-config>数据,关于<jsp-config>配置这里不做扩展。
2.7 getClassLoader
获取当前servlet上文的类加载器,关于类加载器知识这里不做扩展。
2.8 declareRoles
该方法用于申明安全角色,至于servlet3.0规范中关于应用安全模型的知识这里不做扩展。
2.9 getVirtualServerName
方法返回servlet上下文(即应用)部署的逻辑主机名,比如在本机跑Tomcat,那么这个方法返回"Catalina/localhost"。
三、总结
至此,ServletContext的相关属性和方法大致讲解完了。ServletContext是个很重要的东西,在每次的servlet规范更新中,这个接口都有较大的变化。因为ServletContext是容器和应用沟通的桥梁,从一定程度上讲ServletContext就是servlet规范的体现。
这篇博文中关于servlet3.0和3.1提供的新特性,大都没有细讲,这里请读者勿喷,实在是因为每个点扩展开来都可以单独写一篇博文。如果后续有机会,我会单独去介绍。