Top
  1. Servlet开发要点

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都会直接或间接的与该接口发生联系,或是直接实现该接口,或间接继承自实现了该接口的类。

该接口包括以下四个方法:

在最开始制定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上下文

获取该对象的方式有以下四种:

1.5.3. Servlet上下文的作用及特点

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);
		}
		
	}

}