这一节来看一下容器中的Servlet相关的内容, 包括容器为Servlet提供的配置, 容器环境上下文对象, 以及一些辅助Servlet完成服务工作的内容.
- ServletConfig
- ServletContext
- 监听器
- 属性
ServletConfig
在上一篇文章里, 提到GenericServlet 实现了 Servlet, ServletConfig, Serializable 三个接口, 其中就有一个ServletConfig, 这个接口主要用于获取Servlet相关的配置.
这个配置写在哪里呢, 也就是写在web.xml文件中的一个<servlet>标签中, 叫做init-param,可以有多个,每一个标签定义一个键值对:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<servlet>
<servlet-name>Test</servlet-name>
<servlet-class>conyli.cc.Test</servlet-class>
<init-param>
<param-name>name</param-name>
<param-value>saner</param-value>
</init-param>
<init-param>
<param-name>kiki</param-name>
<param-value>wiwi</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>Test</servlet-name>
<url-pattern>/test</url-pattern>
</servlet-mapping>
</web-app>
这里为Test这个servlet配置了两个参数, 分别是name=saner和kiki=wiwi. 然后可以在对应的servlet中获取该参数:
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Enumeration;
public class Test extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
ServletConfig servletConfig = getServletConfig();
System.out.println(servletConfig.getInitParameter("name"));
System.out.println(servletConfig.getInitParameter("kiki"));
Enumeration<String> paras = servletConfig.getInitParameterNames();
while (paras.hasMoreElements()) {
System.out.println(paras.nextElement());
}
}
}
这样就有效的解耦了servlet和其相关的配置, 有一些需要更改的属性, 就完全不需要修改源代码, 仅仅通过修改XML文件的配置, 就可以更改程序的行为. 当然, 这个工作也是容器完成的.
这里要注意的是, 如果覆盖了构造函数, 在构造函数中是无法访问这些属性的, 直到init()执行完成, 这些属性才能通过ServletConfig拿到. 此外如果在装载Servlet之后更改web.xml, 是没有用的, 配置信息在容器加载web.xml的时候就确定好了, 想要更改配置, 只能重新启动容器或者重新部署.
再次记得, ServletConfig是每个servlet都有一个属于自己的, 而不像后边讲到的Web容器上下文, 容器中的所有对象获取的容器上下文对象都是同一个.
ServletContext
如果一个配置需要被多个Servlet甚至是JSP对象共享, 当然可以为某个servlet配置, 然后传递给JSP, 然而这样效率太低.
除了针对单个servlet的ServletConfig之外, 还有一个 context-param ,针对整个web应用来配置初始化参数. 这个标签与servlet标签同级, 要如何访问这个属性呢.
肯定已经想到了, 访问这些容器级别的属性肯定不能通过servlet级别, 而要通过容器上下文, 也就是ServletContext对象.
在web.xml中加入全局参数:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<servlet>
<servlet-name>Test</servlet-name>
<servlet-class>conyli.cc.Test</servlet-class>
<init-param>
<param-name>name</param-name>
<param-value>saner</param-value>
</init-param>
<init-param>
<param-name>kiki</param-name>
<param-value>wiwi</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>Test</servlet-name>
<url-pattern>/test</url-pattern>
</servlet-mapping>
<context-param>
<param-name>fullscope</param-name>
<param-value>gugugu</param-value>
</context-param>
</web-app>
然后在servlet中, 就要通过获取ServletContext对象来获取属性:
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
ServletContext servletContext = getServletContext();
System.out.println(servletContext.getInitParameter("fullscope"));
}
现在还没有学JSP语法, 但是JSP环境中也可以获取到ServletContext对象, 所以也能够访问此参数. 这样一些全局性的配置, 就可以写在context-param中了.
这个参数和之前的ServletConfig一样, 参数的值会在容器启动的时候从web.xml加载, 加载之后再修改web.xml, 就要重新启动容器才可以生效.
实际上这就是两个层次的初始化变量, 在Web容器的生存周期内, 可以认为这些都是常量.
还需要注意的是, ServletContext有一系列方法, 用处远比取得初始变量要重要, 比如getAttribute和setAttribute系列, 想到了什么? 数据可以动态的在整个Web容器中共享.
监听器
在目前为止, 可以为Web容器配置初始化的常量了, 然而如果要初始化进行一些工作需要怎么做呢? 将这些工作交给一个servlet吗? 显然不太可行, 因为有请求到来的时候才会触发servlet.
于是就出现了监听器, 监听器实际上是实现了javax.servlet.ServletContextListener的类. 顾名思义, 这个类监听的是ServletContext的行为.
具体的说, 监听器监听Web容器上下文的初始化和撤销两个事件. 在初始化的时候, 监听器就可以通过上下文对象获取初始化常量, 还可以进行一些工作, 比如创建数据库连接池, 将其存储为容器上下文环境的一个属性, 供所有的容器内部的类使用.
在容器结束的时候, 可以关闭数据库连接. 由于数据库这些东西都是位于容器外部的, 所以在监听器里边进行一些全局性的, 为整个Web容器和其他程序打交道的工作比较合适.
与监听上下文创建与销毁的监听器类似, Java Web规范中还规定了很多监听器. 不过在此之前, 先来实际使用一下这个上下文监听器:
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
public class MyServletContextListener implements ServletContextListener {
//监听上下文初始化
@Override
public void contextInitialized(ServletContextEvent sce) {
System.out.println("监听方法contextInitialized执行了");
//通过事件对象可以获取到上下文对象, 尝试给设置一个属性
sce.getServletContext().setAttribute("name", "saner");
}
//监听上下文销毁
@Override
public void contextDestroyed(ServletContextEvent sce) {
System.out.println("监听方法contextDestroyed执行了");
}
}
实现接口, 然后覆盖两个方法即可. 在编写了类之后, 由于是针对容器的监听, 单独发挥不了作用, 必须在web.xml中配置, 让容器去适当的时候使用才能发挥作用:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
......
<listener>
<listener-class>com.example.listener.MyServletContextListener</listener-class>
</listener>
</web-app>
这么配置之后, 按照我们的意图, ServletContext中已经被设置了name=saner这样一个字段, 然后就可以随便在一个servlet中尝试获取一下.
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().println(getServletContext().getAttribute("name"));
}
实验证明确实可以获取, 实际上在容器初始化的时候, 监听器既然可以获得上下文对象, 连上下文的初始参数也是可以获取的, 因此可以动态的使用初始参数来做一些工作. 然后来看看其他的监听器.
监听器从监听的对象方面可以大致分为: 监听ServletContext, 监听Request, 监听Session, 监听自己; 而从监听的内容方面可以分为:监听属性变化和监听生命周期两类.
下边的分类混合了上边两种分类方式, 以更好的区分.
分类 |
场景 |
接口与方法 |
事件对象 |
监听生命周期 |
ServletContext创建和销毁 |
ServletContextListener
contextInitialized
contextDestroyed |
ServletContextEvent, 通过事件对象可以获取到ServletContext对象 |
有请求到来(新请求对象被创建) |
ServletRequestListener
requestInitialized
requestDestroyed |
ServletRequestEvent, 通过事件对象可以获取到Request对象 |
有新会话开始(新session对象被创建) |
HttpSessionListener
sessionCreated
sessionDestroyed |
HttpSessionEvent, 通过事件对象可以获取到Session对象 |
监听属性变化 |
ServletContext中附加的属性发生变动 |
ServletContextAttributeListener
attributeAdded
attributeRemoved
attributeReplaced |
ServletContextAttributeEvent, 通过事件对象可以获取到变动的键和值 |
Request中附加的属性发生变动 |
ServletRequestAttributeListener
attributeAdded
attributeRemoved
attributeReplaced |
ServletRequestAttributeEvent, 通过事件对象可以获取到变动的键和值 |
Session中附加的属性发生变动 |
HttpSessionAttributeListener
attributeAdded
attributeRemoved
attributeReplaced |
HttpSessionBindingEvent, 通过事件对象可以获取到变动的键和值以及session对象 |
监听自己
这一系列的监听器不是新建一个监听器类, 而是由属性类(被绑定的对象)实现, 所以看上去好像监听自己的变化一样 |
监听自己被绑定到会话上 |
HttpSessionBindingListener
valueBound
valueUnbound |
HttpSessionBindingEvent, 通过事件对象可以获取到变动的键和值以及session对象, 注意这个事件和上一行是一样的. 这可以理解成凡是Session的属性发生变动, 事件对象都可以拿到变动的键值. |
监听自己所绑定的会话发生迁移(序列化=钝化和激活) |
HttpSessionActivationListener
sessionWillPassivate
sessionDidActivate |
HttpSessionEvent, 这个事件对象和监听Session新建与销毁是一样的, 因为本质上钝化再活化就和销毁再创建一样. |
我这里还挨个试验了一下, 试验的代码就不放出来了.
还有一点要注意的是, 在web.xml中的注册顺序, 决定了监听器起作用的顺序.
属性
属性这里就是指在容器内需要传递的数据. 在最开始的例子里, 将模型数据绑定到了请求对象上, 再传递给JSP进行处理.
现在就来好好的看看属性. 在容器内部, 属性只能被绑定到三个地方:
- ServletContext对象上, 全局都可以看到和获取, 但是不特别注意的话, 线程不安全, 所以一般考虑放入只读的内容
- HttpSession对象上, 跟着会话走, 附着在会话上之后, 对于相同的会话对象可以取到相同的数据
- HttpServletRequest对象上, 跟随每个请求, 粒度比前两个还要细, 即使是sessionID相同的两个会话, 每次的请求对象也不同
当然这里还没有学session对象, 不过马上就要了解了.
这三个对象, 都有统一的setAttr, getAttr, removeAttr系列方法. 注意如果需要同步, 加锁的对象不要加在Servlet上, 而是要加在需要同步的对象上, 比如ServletContext, 整个容器里都是同一个对象, 在Servlet的代码中对其加上锁, 就可以保证这个Servlet对其同步.
Session也不是线程安全的, 加锁的方法也是同样的.
一个Servlet的实例, 在一个JVM中只有一个, 多个线程使用这个实例的方法. 所以为了避免线程竞争, 尽量都使用局部变量, 如果涉及到跨线程的内容, 比如session对象和Servlet的实例变量, 都要用同步保护起来.
最后补充一个小知识点, 通过HttpServletRequest获取的RequestDispatcher对象可以使用相对或者绝对地址, 而从ServletContext对象获取的RequestDispatcher只能使用斜线开头的绝对地址.