1. Servlet开发要点
1.1. 重定向
1.1.1. 什么是重定向
在服务器为浏览器提供响应时,回传的数据包中的状态行里面是302状态码,同时在消息头内会增加一个键值对,名称为Location,值是一个新的URL地址。当这个响应到达浏览器的时候,这一次的请求响应过程并未结束,浏览器遇见302状态码之后,会立即按照Location头信息中指定的URL地址发送新的一个请求,这样一个在接到响应后又立即发出请求的过程叫做重定向。对于客户端用户来讲,中间的变化过程不会被察觉,因为这个过程是由浏览器自动完成的。
1.1.2. 重定向原理
在重定向的过程中,影响浏览器做出动作的关键点即响应中的状态码及Location这个消息头。302状态就像一道命令一样,使得浏览器做出新的一次请求,而请求的地址会从头信息中查找。由于这个新的请求动作是由浏览器发出的,所以浏览器的地址栏上的地址会变成Location消息头中的地址。
1.1.3. 如何重定向
由于发回的响应信息由response对象控制,所以使用如下代码即可实现重定向的过程:
- response.sendRedirect(String url);
该方法的参数值url即Location消息头中的重定向地址。注意,该段代码后面如果还有其他代码的话也会被继续执行的。
1.1.4. 重定向特点
由于重定向动作的执行者为浏览器,所以请求的地址可以是任意地址,哪怕是当前应用以外的应用;浏览器发出请求时一定会保持地址栏与目标地址的一致,所以发生重定向时可以从地址栏中看到地址的改变;由于整个跳转过程是在浏览器收到响应后重新发起请求,所以涉及到的Web组件并不会共享同一个request和response。
图- 1
在图 – 1中,1和4是两个完全不同的请求,如果在1号请求中曾经携带了某些表单数据,但4号这个全新请求中则不会获取到这些表单数据,也就是两次请求涉及到的Web组件不会共享request和response。
1.2. Servlet容器如何处理请求资源路径
1.2.1. 什么是请求资源路径
在地址栏中输入的请求地址中,端口号之后的部分都是请求资源路径。紧跟端口号的是部署到Web服务器上的应用名(appName),紧跟应用名的则是具体的应用内的组件路径。
1.2.2. Web服务器对请求地址的处理过程
浏览器依据地址中的IP和端口号与Web服务器建立连接,服务器会获取到请求资源路径信息。根据端口号后面的应用名找到服务器上对应的应用。默认情况下容器会认为应用名后面的是一个Servlet,所以回到web.xml文件中所有是否有与该值匹配的<url-pattern>,找到匹配的值之后再按照<servlet-name>完成对应关系的查找,进而找到要执行的Servlet。如果没有找到匹配的资源服务器就会返回404错误。
1.2.3. 匹配Servlet的规则
容器在进行url-pattern比对的时候是遵循一定的匹配原则的。这些原则主要有:
精确匹配
即具体资源名称与web.xml文件中的url-pattern严格匹配相等才执行。如,配置的内容如下:
- <servlet>
- <servlet-name>someServlet</servlet-name>
- <servlet-class>test.MyServlet</servlet-class>
- </servlet>
- <servlet-mapping>
- <servlet-name>someServlet</servlet-name>
- <url-pattern>/abc.html</url-pattern>
- </servlet-mapping>
则在地址栏中输入 http://ip:port/appName/abc.html 时,服务器就会去执行test.MyServlet这个组件,就算是在应用的根目录下的确有abc.html这个文件,也不会执行。
通配符匹配
使用“*”这个符号来匹配0个或多个字符,已达到路径的批量匹配的效果。
如配置文件中的节点为如下代码所示:
- <servlet>
- <servlet-name>someServlet</servlet-name>
- <servlet-class>test.MyServlet</servlet-class>
- </servlet>
- <servlet-mapping>
- <servlet-name>someServlet</servlet-name>
- <url-pattern>/*</url-pattern>
- </servlet-mapping>
则,在地址栏中输入以下任何地址时都是匹配成功的。
- http://ip:port/appName/abc.html
- http://ip:port/appName/abc/def/ghi.html
后缀匹配
在配置url-pattern节点时,不使用斜杠开头,用“*.”开头来匹配任意多个字符的模式叫做后缀匹配。
如配置文件中的节点为如下代码所示:
- <servlet>
- <servlet-name>someServlet</servlet-name>
- <servlet-class>test.MyServlet</servlet-class>
- </servlet>
- <servlet-mapping>
- <servlet-name>someServlet</servlet-name>
- <url-pattern>*.do</url-pattern>
- </servlet-mapping>
则,在地址栏中输入以下任何地址时都是匹配成功的。
- http://ip:port/appName/abc.do
- http://ip:port/appName/abc/def/ghi.do
在这三种匹配方式中,优先级最高的是精确匹配。如果容器在使用以上原则都不能找到相匹配的资源来执行时,就按照地址到应用中查找对应的文件。此时如果找到文件则返回,找不到资源来执行就返回404错误。
1.3. 一个Servlet实现多请求
1.3.1. 为什么要将多Servlet合并
Servlet作为Web应用中最核心的环节是因为这个组件不仅能接受请求,还能够为该请求提供响应,所以Servlet一般都会充当整个应用的控制器来进行请求的分发,为不同的请求找到对应的资源。于是程序中大多只需要一个Servlet完成这个分发工作即可,合并多个Servlet为一个Servlet会让程序的处理逻辑更加明确。
要想完成多个Servlet合并为一个Servlet,需要完成以下两个步骤:
- 使用后缀模式完成请求资源路径的匹配
- 分析请求资源路径中的请求目标,完成分发的动作
1.3.2. 使用后缀匹配模式完成请求资源路径的匹配
修改web.xml文件,将更多的servlet配置节点删除,只保留一个节点即可,代码如下:
- <?xml version="1.0" encoding="UTF-8"?>
- <web-app version="2.4"
- xmlns="http://java.sun.com/xml/ns/j2ee"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
- http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
- <servlet>
- <servlet-name>someServlet</servlet-name>
- <servlet-class>web.SomeServlet</servlet-class>
- </servlet>
- <servlet-mapping>
- <servlet-name>someServlet</servlet-name>
- <url-pattern>*.do</url-pattern>
- </servlet-mapping>
- </web-app>
1.3.3. 分析请求资源后分发
配置完web.xml文件后,不同请求都会发送到Web.SomeServlet来处理,要想起到分发的作用,则需要分析调过来的请求中具体的请求目标是什么。使用如下代码逻辑来完成分发动作。
- package web;
- import java.io.IOException;
- import javax.servlet.ServletException;
- import javax.servlet.http.HttpServlet;
- import javax.servlet.http.HttpServletRequest;
- import javax.servlet.http.HttpServletResponse;
- public class SomeServlet extends HttpServlet{
- public void service(HttpServletRequest request,
- HttpServletResponse response) throws
- ServletException,IOException{
- //获得请求资源路径
- String uri = request.getRequestURI();
- System.out.println("uri:" + uri);
- if(uri.equals("/test/list.do")){
- System.out.println("进行员工列表的处理...");
- }else if(uri.equals("/test/add.do")){
- System.out.println("添加员工的处理...");
- }
- }
- }
1.4. Servlet的生命周期
1.4.1. 什么是Servlet生命周期
Servlet容器如何创建Servlet对象、如何为Servlet对象分配、准备资源、如何调用对应的方法来处理请求以及如何销毁Servlet对象的整个过程即Servlet的生命周期。
1.4.2. 生命周期的四个阶段
阶段一、实例化
实例化阶段是Servlet生命周期中的第一步,由Servlet容器调用Servlet的构造器创建一个具体的Servlet对象的过程。而这个创建的时机可以是在容器收到针对这个组件的请求之后,即用了才创建;也可以在容器启动之后立刻创建实例,而不管此时Servlet是否使用的上。使用如下代码可以设置Servlet是否在服务器启动时就执行创建。
- <servlet>
- <servlet-name>someServlet</servlet-name>
- <servlet-class>test/SomeServlet</servlet-class>
- <load-on-startup>1</load-on-startup>
- </servlet>
- <servlet-mapping>
- <servlet-name>someServlet</servlet-name>
- <url-pattern>/*</url-pattern>
- </servlet-mapping>
配置文件中的load-on-startup节点用于设置该Servlet的创建时机。
当其中的值大于等于0时,表示容器在启动时就会创建实例
小于0时或没有指定时,代表容器在该Servlet被请求时再执行创建
正数的值越小,优先级越高,应用启动时就越先被创建。
阶段二、初始化
Servlet在被加载实例化之后,必须要初始化它。在初始化阶段,init()方法会被调用。这个方法在javax.servlet.Servlet接口中定义。其中,方法以一个ServletConfig类型的对象作为参数。ServletConfig对象由Servlet引擎负责创建,从中可以读取到事先在web.xml文件中通过<init-param>节点配置的多个name-value名值对。ServletConfig对象还可以让Servlet接受一个ServletContext对象。
一般情况下,init方法不需要编写,因GenericServlet已经提供了init方法的实现,并且提供了getServletConfig方法来获得ServletConfig对象。
注:init方法只被执行一次。
以下代码为在servlet配置中,增加初始化参数
- <?xml version="1.0" encoding="UTF-8"?>
- <web-app version="2.4"
- xmlns="http://java.sun.com/xml/ns/j2ee"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
- http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
- <servlet>
- <servlet-name>someServlet</servlet-name>
- <servlet-class>test/SomeServlet</servlet-class>
- <init-param>
- <param-name>debug</param-name>
- <param-value>true</param-valule>
- </init-param>
- </servlet>
- <servlet-mapping>
- <servlet-name>someServlet</servlet-name>
- <url-pattern>/*</url-pattern>
- </servlet-mapping>
- </web-app>
使用以下代码可以读取Servlet配置中增加的初始化参数
- package test;
- import java.io.IOException;
- import javax.servlet.ServletConfig;
- import javax.servlet.ServletException;
- import javax.servlet.http.HttpServlet;
- import javax.servlet.http.HttpServletRequest;
- import javax.servlet.http.HttpServletResponse;
- public class SomeServlet extends HttpServlet{
- public void service(HttpServletRequest request,
- HttpServletResponse response)
- throws ServletException,IOException{
- System.out.println("SomeServlet‘s service...");
- ServletConfig config = getServletConfig();
- String debug = config.getInitParameter("debug");
- System.out.println("debug:" + debug);
- }
- }
阶段三、就绪
Servlet被初始化以后就处于能够响应请求的就绪状态。每个对Servlet的请求由一个ServletRequest对象代表,Servlet给客户端的响应由一个ServletResponse对象代表。当客户端有一个请求时,容器就会将请求与响应对象转给Servlet,以参数的形式传给service方法。service方法由javax.servlet.Servlet定义,由具体的Servlet实现。
阶段四、销毁
Servlet容器在销毁Servlet对象时会调用destroy方法来释放资源。通常情况下Servlet容器停止或者重新启动都会引起销毁Servlet对象的动作,但除此之外,Servlet容器也有自身管理Servlet对象的准则,整个生命周期并不需要人为进行干预。
1.4.3. Servlet接口
在ServletAPI中最重要的是Servlet接口,所有Servlet都会直接或间接的与该接口发生联系,或是直接实现该接口,或间接继承自实现了该接口的类。
该接口包括以下四个方法:
- init(ServletConfig config)
- service(ServletRequest req,ServletResponse res)
- destroy( )
在最开始制定Servlet规范时,设计者希望这套规范能够支持多种协议的组件开发,所以Servlet接口是最重要的一个接口。虽然我们写的程序中编写的Servlet都是继承自HttpServlet,但本质上都是对该接口的实现,因为HttpServlet就是针对Servlet这个接口的一个抽象的实现类。可以理解为HttpServlet是支持HTTP协议的分支的一部分。设计Servlet接口中的service方法时,也是希望该方法能够处理多种协议下的请求及响应,所以参数类型是ServletRequest,而在HttpServlet这个支持HTTP协议的分支中,service方法的参数则变成了HttpServletRequest和HttpServletResponse,这两个类分别继承于ServletRequest和ServletResponse,也就是对这两个类的一个具体协议的包装,区别是增加了很多与HTTP协议相关的使用API。
制定的这种规范在实际使用中发现,并不会扩展为HTTP协议之外,所以有了过度设计的缺陷,也为在编写HTTP协议的Web应用时添加了一些不必要的操作。
1.4.4. Servlet涉及到的抽象类
Servlet API中另一个重要的类就是GenericServlet这个抽象类,它对Servlet接口中的部分方法(init和destroy)添加了实现,使得开发时只需要考虑针对service方法的业务实现即可。
HttpServlet又是在继承GenericServlet的基础上进一步的扩展,一个是public voidinit(ServletConfig config),另一个是 public void init()。他们有如下的关系: init(ServletConfig config)方法由tomcat自动调用,它读取web工程下的web.xml,将读取的信息打包传给此参数,此方法的参数同时将接收的信息传递给GenericServlet类中的成员变量config,同时调用init()。以后程序员想重写init方法可以选择init(ServletConfig
config)或者init(),但是选择init(ServletConfig config)势必会覆盖此方法已实现的内容,没有为config变量赋值,此后若是调用getServletConfig()方法返回config时会产生空指针异常的,所以想重写init(ServletConfig config)方法,必须在方法体中第一句写上 super.init(config),为了防止程序员忘记重写super.init(config)方法sun公司自动为用户生成一个public void init()的方法。GenericServlet具体的定义如下所示
- GenericServlet{
- ServletConfig config;
- public void init()
- { } //此方法什么也没做,可以说是为编程人员预留的接口
- public void init(ServletConfig config)
- {
- this.config=config;
- this.init();
- }
- getServletConfig()
- {
- return config;
- }
- }
1.5. ServletContext
1.5.1. 什么是Servlet上下文
WEB容器在启动时,它会为每个WEB应用程序都创建一个对应的ServletContext对象,它代表当前web应用,是一个全局的环境变量。该应用中的任何组件,在任何时候都可以访问到该对象,所以Servlet上下文具有唯一性。
1.5.2. 如何获得Servlet上下文
获取该对象的方式有以下四种:
- GenericeServlet(HttpServlet)提供的 getServletContext()
- ServletConfig提供的 getServletContext()
- HttpSession提供的 getServletContext()
- FilterConfig提供的 getServletContext()
1.5.3. Servlet上下文的作用及特点
Servlet上下文的作用:
- 在Web应用范围内存取共享数据:如 setAttribute(),getAttribute()
- 访问Web应用的静态资源:如getRealPath(String path)
- 跨多个请求、用户和Servlet
例如,以下是两个Servlet的完整代码,实现了跨Servlet的数据共享
- import java.io.IOException;
- import java.io.PrintWriter;
- import javax.servlet.*
- import javax.servlet.http.*
- public class SomeServlet extends HttpServlet {
- public void service(HttpServletRequest request,
- HttpServletResponse response)
- throws ServletException, IOException {
- response.setContentType("text/html;charset=utf-8");
- PrintWriter out = response.getWriter();
- ServletContext sctx = getServletContext();
- sctx.setAttribute("name", "Lisa");
- out.close();
- }
- }
- import java.io.IOException;
- import java.io.PrintWriter;
- import javax.servlet.*
- import javax.servlet.http.*
- public class OtherServlet extends HttpServlet {
- public void service(HttpServletRequest request,
- HttpServletResponse response)
- throws ServletException, IOException {
- response.setContentType("text/html;charset=utf-8");
- ServletContext sctx = getServletContext();
- String name = (String) sctx.getAttribute("name");
- out.close();
- }
- }
1.6. Servlet线程安全问题
1.6.1. 为什么会有线程安全问题
当浏览器访问服务器的通讯模块SomeServlet时,会启动一个线程T1来进行一系列的创建动作来处理这个请求。一般的web服务器的编程模型如下:
- while(flag){
- Socket s = ss.accept();
- Thread t = new Thread(s);
- t.start();
- }
如果刚好同时也有一个请求来访问SomeServlet,但是服务器只有一个servlet实例,所以服务器会启动线程T2,此时就有可能产生T1和T2同时访问someservlet的情况,如果要修改属性就会有安全隐患
1.6.2. 如何保证Servlet线程安全
使用synchronized对代码加锁即可。代码结构如下
- import java.io.IOException;
- import java.io.PrintWriter;
- import javax.servlet.*;
- import javax.servlet.*;
- public class SomeServlet extends HttpServlet {
- private int count = 0;
- public void service(HttpServletRequest request,
- HttpServletResponse response)
- throws ServletException, IOException {
- synchronized(this){
- count ++;
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println(
- Thread.currentThread().getName()
- + ":" + count);
- }
- }
- }