Fork me on GitHub

notes_for_HeadFirstServletsAndJsp5

Head First Servlets&JSP 读书笔记_5

如何将某些参数写在DD中,而非写死在类中

初版解决办法(以Email地址为例):

1
2
3
4
5
6
7
8
9
10
//DD中:
<servlet>
...

<init-param>
<param-name>adminEmail</param-name>
<param-value>***@163.com</param-value>
</init-param>

</servlet>

同时

1
2
//servlet中:
getServletConfig().getInitParameter("adminEmail");
  • 一般Servlet都是继承自HttpServlet,而HttpServletGenericServlet的子类。getInitParameter()方法来自于GenericServlet,所以Servlet可以调用此方法去获取web.xml配置文件中的配置信息。
  • 容器初始化一个servlet时,会为这个servlet建一个唯一的ServletConfig。其出场顺序为:先由容器加载servlet类,然后init()初始化,初始化完成前得到了ServletConfig对象,同时可以调用getInitParameter方法,从DD中读取初始化参数,经由ServletConfig传递给init()方法。
  • 划重点:先 init(),后得到ServletConfig,同时使用初始化参数(只能读一次)!可参考Head First Servlets&JSP 读书笔记_3中关于init()方法的讲解
  • 以上的代码只是专属于此Servlet的初始化参数,无法全局使用;若采取保存下来的方式,又必须使此Servlet最早先运行,这么一来可维护性极差

除了该Servlet,如何让Web中其他部分也得到此参数?

升级版解决办法:使用ServletContext,对整个Web应用中的所有servlet和JSP都可用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//DD中:
<web-app>

<context-param>
<!-- 在web-app标签中,
但位于所有servlet标签之外 -->
<param-name>adminEmail</param-name>
<param-value>***@163.com</param-value>
</context-param>

</web-app>
<servlet>
...
</servlet>

同时

1
2
//servlet中:
getServletContext().getInitParameter("adminEmail");

说明:

  1. 每个servlet有一个ServletConfig,每个Web应用有一个ServletContext
  2. 如果应用是分布式应用,那么每个JVM有一个ServletContext
  3. ServletContext也是在Servlet发生初始化时,对初始化参数完成设置
  4. 初始化参数=部署时常量;运行时可以得到,但无法设置
  5. 一般所说初始化参数,默认指“Servlet初始化参数”
  6. ServletContext还可以得到:有关服务器/容器的信息;写到服务器的日志文件,RequestDispatcher等
  7. 以上的初始化参数只能是String,其他参数怎么玩?使用对象来初始化怎么玩?见下文

使用对象来初始化Web应用的办法:listener

理论

  • 需求:使用非String类型的参数,对Web进行初始化。
  • 问题:1.初始化参数只能是String类型,
    2.想把对象转成String,但转换代码无法塞到servlet或JSP之前运行
  • 解决办法:建立一个ServletContextListener类,用以监听ServletContext的创建和撤销。
interface
ServletContextListener
contextInitialized(ServletContextEvent)
contextDestroyed(ServletContextEvent)

以下以DataSource举例,若需要传递对象,可见下文对Dog的举例。两者原理步骤相通

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//ServletContextListener类
import javax.servlet.*;

public class MyServletContextListener implements ServletContextListener{

//实现接口的两个方法
//上下文初始化时得到通知
public void contextInitialized(ServletContextEvent event){
//Todo 使用初始化参数查找名(由ServletContext得到)建立一个数据库连接
//将数据库连接存储为一个属性,使得Web应用的各个部分都能访问

}

//上下文撤销时得到通知
public void contextDestroyed(ServletContextEvent event){
//Todo 关闭数据库连接

}
}

举例Dog之步骤陈述

把Dog类放到ServletContext中

  1. 新建一个监听类放在WEB-INF/classes目录(或其他目录)下,此类实现ServletContextListener接口,并在DD中放一个告知容器此处有监听者

    1
    2
    3
    4
    5
    6
    <listener>
    <listener-class>
    <!--监听类的全路径-->
    com.example.MyServletContextListener
    </listener-class>
    </listener>
  2. 新建Dog类,普通java类,由ServletContextListener初始化,设置在SerletContext,由servlet获取

  3. 新建Servlet类,扩展HttpServlet,验证此监听类。此Servlet从ServletContext中得到Dog属性,然后调用Dog的另外的方法,打印到响应中

举例Dog之代码实现

1
2
3
4
5
6
7
8
9
10
11
//MyServletContextListener类中的contextInitialized()方法:
public void contextInitialized(ServletContextEvent event){
//由事件使用ServletContext得到初始化参数
ServletContext sc = event.getServletContext();
String dogBreed = sc.getInitParameter("breed");

Dog d = new Dog(dogBreed);

//使用ServletContext来设置一个属性(名/对象对),供其他应用得到此属性
sc.setAttribute("dog",d);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//Dog类,平平无奇
package com.example

public class Dog{
private String breed;

//含参构造
public Dog(String breed){
this.breed=breed;
}
public String getBreed(){
return breed;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//Servlet类,测试类
package com.example;
import ...

public class ListenerTester extends HttpServlet{
public void doGet(HttpServlet Request request,HttpServletResponse response)
throws IOException,ServletException{
response.setContentType("text/html");
PrintWriter out = response.getWriter();

out.println("test context attributes set by listener");
out.println("<br>");
//从ServletContext中得到Dog,在第一次调用服务前,Dog已经放入上下文中了
Dog dog = (Dog)getServletContext().getAttribute("dog");
out.println(dog.getBreed());

}
}

完整的故事

  1. 容器读DD,包括listenercontext-param元素,容器为Web创建新的ServletContext
  2. 容器为每个ServletContext初始化参数创建一个String 名/值对,容器将该名/值对(举例为breed)的引用交给ServletContext
  3. 容器创建MyServletContextListener类的实例对象
  4. 容器调用监听者的contextInitialized() 方法,传入新的ServletContextEvent 。注:这个event有ServletContext的一个引用,所以事件处理代码可以得到上下文,即event.getServletContext(),进而得到上下文初始化参数
  5. 按照4的event,监听者向ServletContextEvent 请求ServletContext的一个引用,再向ServletContext请求上下文初始化参数“breed”
  6. 监听者使用该初始化参数来构造一个新的Dog对象
  7. 监听者将Dog设置为ServletContext的一个属性:sc.setAttribute("dog",d);正式调用Servlet前的准备工作就此完毕
  8. 容器建立一个新的Servlet,利用上面的ServletConfig->ServletContext->init()方法,新建Servlet完毕
  9. Servlet获得请求,并向ServletContext请求属性dog:getServletContext().getAttribute("dog"),Servlet在Dog上调用getBreed(),并将结果打印到HttpResponse中

上下文作用域的非线程安全

  1. Web应用的每个部分都可以访问上下文属性,多个servlet说明可能有多个线程,线程并发会引起非线程安全
  2. 对doGet进行同步服务synchronized,意味着servlet一次仅能处理一个客户,但并不能阻止不同servlet或JSP访问上下文属性,所以需要对上下文加锁,而非servlet
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//doGet()方法
public void doGet(HttpServlet Request request,HttpServletResponse response)
throws IOException,ServletException{
response.setContentType("text/html");
PrintWriter out = response.getWriter(); out.println("test context attributes ");

//将设置和提取部分加锁,使得获取同一个上下文属性的代码对此同步
synchronized(getServletContext){
getServletContext().setAttribute("foo","22");
out.println(getServletContext().getAttribute("foo"));
}

}

已完成全书第5章197页,读书笔记未完待续。。

-------------The End-------------