这一节来看一下容器中的Servlet相关的内容, 包括容器为Servlet提供的配置, 容器环境上下文对象, 以及一些辅助Servlet完成服务工作的内容.

  1. ServletConfig
  2. ServletContext
  3. 监听器
  4. 属性

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进行处理.

现在就来好好的看看属性. 在容器内部, 属性只能被绑定到三个地方:

  1. ServletContext对象上, 全局都可以看到和获取, 但是不特别注意的话, 线程不安全, 所以一般考虑放入只读的内容
  2. HttpSession对象上, 跟着会话走, 附着在会话上之后, 对于相同的会话对象可以取到相同的数据
  3. HttpServletRequest对象上, 跟随每个请求, 粒度比前两个还要细, 即使是sessionID相同的两个会话, 每次的请求对象也不同

当然这里还没有学session对象, 不过马上就要了解了.

这三个对象, 都有统一的setAttr, getAttr, removeAttr系列方法. 注意如果需要同步, 加锁的对象不要加在Servlet上, 而是要加在需要同步的对象上, 比如ServletContext, 整个容器里都是同一个对象, 在Servlet的代码中对其加上锁, 就可以保证这个Servlet对其同步.

Session也不是线程安全的, 加锁的方法也是同样的.

一个Servlet的实例, 在一个JVM中只有一个, 多个线程使用这个实例的方法. 所以为了避免线程竞争, 尽量都使用局部变量, 如果涉及到跨线程的内容, 比如session对象和Servlet的实例变量, 都要用同步保护起来.

最后补充一个小知识点, 通过HttpServletRequest获取的RequestDispatcher对象可以使用相对或者绝对地址, 而从ServletContext对象获取的RequestDispatcher只能使用斜线开头的绝对地址.