在服务器为浏览器提供响应时,回传的数据包中的状态行里面是302状态码,同时在消息头内会增加一个键值对,名称为Location,值是一个新的URL地址。当这个响应到达浏览器的时候,这一次的请求响应过程并未结束,浏览器遇见302状态码之后,会立即按照Location头信息中指定的URL地址发送新的一个请求,这样一个在接到响应后又立即发出请求的过程叫做重定向。对于客户端用户来讲,中间的变化过程不会被察觉,因为这个过程是由浏览器自动完成的。
在重定向的过程中,影响浏览器做出动作的关键点即响应中的状态码及Location这个消息头。302状态就像一道命令一样,使得浏览器做出新的一次请求,而请求的地址会从头信息中查找。由于这个新的请求动作是由浏览器发出的,所以浏览器的地址栏上的地址会变成Location消息头中的地址。
由于发回的响应信息由response对象控制,所以使用如下代码即可实现重定向的过程:
response.sendRedirect(String url);
该方法的参数值url即Location消息头中的重定向地址。注意,该段代码后面如果还有其他代码的话也会被继续执行的。
由于重定向动作的执行者为浏览器,所以请求的地址可以是任意地址,哪怕是当前应用以外的应用;浏览器发出请求时一定会保持地址栏与目标地址的一致,所以发生重定向时可以从地址栏中看到地址的改变;由于整个跳转过程是在浏览器收到响应后重新发起请求,所以涉及到的Web组件并不会共享同一个request和response。
图- 1
在图 – 1中,1和4是两个完全不同的请求,如果在1号请求中曾经携带了某些表单数据,但4号这个全新请求中则不会获取到这些表单数据,也就是两次请求涉及到的Web组件不会共享request和response。
在地址栏中输入的请求地址中,端口号之后的部分都是请求资源路径。紧跟端口号的是部署到Web服务器上的应用名(appName),紧跟应用名的则是具体的应用内的组件路径。
浏览器依据地址中的IP和端口号与Web服务器建立连接,服务器会获取到请求资源路径信息。根据端口号后面的应用名找到服务器上对应的应用。默认情况下容器会认为应用名后面的是一个Servlet,所以回到web.xml文件中所有是否有与该值匹配的<url-pattern>,找到匹配的值之后再按照<servlet-name>完成对应关系的查找,进而找到要执行的Servlet。如果没有找到匹配的资源服务器就会返回404错误。
容器在进行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错误。
Servlet作为Web应用中最核心的环节是因为这个组件不仅能接受请求,还能够为该请求提供响应,所以Servlet一般都会充当整个应用的控制器来进行请求的分发,为不同的请求找到对应的资源。于是程序中大多只需要一个Servlet完成这个分发工作即可,合并多个Servlet为一个Servlet会让程序的处理逻辑更加明确。
要想完成多个Servlet合并为一个Servlet,需要完成以下两个步骤:
修改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>
配置完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("添加员工的处理..."); } } }
Servlet容器如何创建Servlet对象、如何为Servlet对象分配、准备资源、如何调用对应的方法来处理请求以及如何销毁Servlet对象的整个过程即Servlet的生命周期。
阶段一、实例化
实例化阶段是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对象的准则,整个生命周期并不需要人为进行干预。
在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应用时添加了一些不必要的操作。
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; } }
WEB容器在启动时,它会为每个WEB应用程序都创建一个对应的ServletContext对象,它代表当前web应用,是一个全局的环境变量。该应用中的任何组件,在任何时候都可以访问到该对象,所以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(); } }
当浏览器访问服务器的通讯模块SomeServlet时,会启动一个线程T1来进行一系列的创建动作来处理这个请求。一般的web服务器的编程模型如下:
while(flag){ Socket s = ss.accept(); Thread t = new Thread(s); t.start(); }
如果刚好同时也有一个请求来访问SomeServlet,但是服务器只有一个servlet实例,所以服务器会启动线程T2,此时就有可能产生T1和T2同时访问someservlet的情况,如果要修改属性就会有安全隐患
使用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); } } }