Java web基础总结四之—— Servlet基础
一.什么是Servlet?
通过名字就能看出来,Servlet 就是在服务器上运行的小程序。Servlet是sun公司(现在已经属于oracle了)实现的一门用于开发动态java web资源的技术。Sun公司在其API中提供了一个servlet接口,如果你想开发一个动态的java web资源,需要完成以下2个步骤:编写一个Java类,实现servlet接口。把开发好的Java类部署到web服务器中。
Servlet接口已经有了两个默认的实现类,分别为:GenericServlet、HttpServlet。由于是web开发,所以常用HttpServlet,我们编写的Servlet一般都继承于它。HttpServlet就是能够处理HTTP请求的servlet,它在原有Servlet接口上添加了一些与HTTP协议处理方法,所以功能更为强大。因此我们在编写Servlet时,通常都应该继承这个类,而避免直接去实现Servlet接口。HttpServlet在实现Servlet接口时,覆写了service方法,该方法体内的代码会自动判断用户的请求方式,如为GET请求,则调用HttpServlet的doGet方法,如为Post请求,则调用doPost方法。下面是HttpServlet的service方法的部分源码。
所以,我们编写Servlet时,通常只需要覆写doGet或doPost方法就可以了。
二.实现一个Servlet版本的helloworld,对于所有客户端的get和post请求,都会向浏览器返回hello world.
package com.cc; 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.io.PrintWriter; /** * Created by c.c on 2015/5/9. */ public class HelloWorldServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { PrintWriter printWriter=resp.getWriter(); //发送hello world printWriter.println("<h1>hello world</h1>"); printWriter.flush(); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { this.doGet(req,resp); } }
三. Servlet的调用过程
由于Servlet不是独立的应用程序,没有main() 方法。所以Servlet不是由用户或程序员调用,而是由另外一个应用程序(Servlet容器)调用。容器也就是我们常说的web服务器,比如最常见的Tomcat.下面就来分析一下容器调用Servlet的过程。
1. 用户通过浏览器发送http数据到web服务器,请求某个Servlet。此时,如果这个Servlet没有配置load-on-startup,即应用启动就加载的话。Web容器首先会加载并实例化这个Servlet。
2. 调用Servlet实例对象的init()方法,进行Servlet的初始化操作。
3. 创建一个用于封装HTTP请求消息的HttpServletRequest对象和一个代表HTTP响应消息的HttpServletResponse对象,通过http请求的信息填充这个HttpServletRequest对象。当然,此时的HttpServletResponse对象内容基本为空,然后调用Servlet的service()方法并将请求和响应对象作为参数传递进去。
4. Servlet的service()方法执行完毕以后,会对上一步创建的HttpServletResponse对象的内容进行填充,service()方法返回。
5. Web容器获取HttpServletResponse对象的内容,并且组装http响应报文,发送给客户端。
6. 最后,web应用程序被停止或重新启动之前,Servlet引擎将卸载Servlet,并在卸载之前调用Servlet的destroy()方法。
四.Servlet的生命周期
通过上面的叙述,我们知道,Servlet是一个供其他Java程序(Servlet引擎)调用的Java类,它不能独立运行,它的运行完全由Servlet引擎来控制和调度。
1.加载与实例化
首先看Servlet类的加载和实例化的时机。如果这个Servlet配置了load-on-startup,并且load-on-startup> 0时,web容器启动的时候做实例化处理,顺序是由小到大,正整数小的先被实例化。load-on-startup= 0时,就会被最后实例化。小于0,相当于没有配置。针对客户端的多次Servlet请求,通常情况下,服务器只会创建一个Servlet实例对象,也就是说Servlet实例对象一旦创建,它就会驻留在内存中,为后续的其它请求服务,直至web容器退出,servlet实例对象才会销毁。
2. init()方法
实例化完成以后,就会调用Servlet的init方法。在Servlet的整个生命周期内,Servlet的init方法只被调用一次。需要注意的是,由于默认的 init() 方法设置了 Servlet 的初始化参数,并用它的 ServletConfig 对象参数来启动配置,因此当你需要实现自己的init() 方法时,应调用 super.init() 以确保仍然执行这些任务。在调用 service()
方法之前,应确保已完成了 init() 方法。
3.service() 方法
接着就是Servlet 的核心service() 方法。对一个Servlet的每次访问请求都导致Servlet引擎调用一次servlet的service方法。对于每次访问请求,Servlet引擎都会创建一个新的HttpServletRequest请求对象和一个新的HttpServletResponse响应对象,然后将这两个对象作为参数传递给它调用的Servlet的service()方法,service方法再根据请求方式分别调用doXXX方法。
4. destroy() 方法
destroy() 方法和init()方法一样,也是仅执行一次,即在服务器停止且卸装 Servlet 时执行该方法。通常,将 Servlet 作为服务器进程的一部分来关闭。用于释放资源等操作。
五.Servlet的配置
由于客户端是通过URL地址访问web服务器中的资源,所以Servlet程序若想被外界访问,必须把servlet程序映射到一个URL地址上,这就需要在web.xml文件中使用<servlet>元素和<servlet-mapping>元素完成。
<servlet>元素用于注册Servlet,它包含有两个主要的子元素:<servlet-name>和<servlet-class>,分别用于设置Servlet的注册名称和Servlet的完整类名。
一个<servlet-mapping>元素用于映射一个已注册的Servlet的一个对外访问路径,它包含有两个子元素:<servlet-name>和<url-pattern>,分别用于指定Servlet的注册名称和Servlet的对外访问路径。例如spring MVC的DispatcherServlet的配置:
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
需要注意的是:
同一个Servlet可以被映射到多个URL上。 而且在Servlet映射到的URL中也可以使用*通配符,但是只能有两种固定的格式:一种格式是“*.扩展名”,另一种格式是以正斜杠(/)开头并以“/*”结尾。对于url的映射,web容器会进行最优匹配。例如,有下面的一些映射关系:
ServletA 映射到 /xxx/*
ServletB 映射到 /*
ServletC 映射到 /xxx
当请求URL为“/xxx/a.html”,虽然“/xxx/*”和“/*”都匹配,但是 Servlet引擎将调用ServletA。
当请求URL为“/xxx”时,同样的“/xxx/*”和“/xxx”都匹配,但是Servlet引擎将调用ServletC。
在上面spring MVC的DispatcherServlet的配置的例子中,配置了一个<load-on-startup>元素,那么WEB应用程序在启动时,就会装载并创建DispatcherServlet的实例对象、以及调用Servlet实例对象的init()方法。
六.Servlet的线程安全问题
1.为什么Servlet会存在线程安全的问题。
由于在 Web 应用程序中,默认Servlet只用一个实例,一个Servlet 在一个时刻可能被多个用户同时访问。这时 Web 容器将为每个用户创建一个线程来执行 Servlet。这时如果 Servlet 需要共享资源,就会出现线程安全问题。
如果Servlet是无状态的,它不包含全局的实例变量,也没有引用其它类的域,一次特定计算的瞬时状态,会唯一的存储在本地变量中,这些本地变量存在线程的栈中,只有执行线程才能访问,一个执行该Servlet的线程不会影响访问同一个Servlet的其它线程的计算结果,因为两个线程不共享状态,他们如同在访问不同的实例。
但是如果在Servlet中定义了一个全局的实例变量,就成了有状态的Servlet。这个变量保存在主内存中,通常为堆内存中,当只有一个用户访问该Servlet时,程序会正常的运行。但当多个用户并发访问时,由于jvm的内存模型,线程从主内存这个线程的工作内存中进行数据交换不是原子性的操作。所以就会出现线程安全的问题。
2.如何解决Servlet的线程安全的问题。
(1).实现SingleThreadModel接口,那么Servlet引擎将以单线程模式来调用其service方法。对于实现了SingleThreadModel接口的Servlet,Servlet引擎仍然支持对该Servlet的多线程并发访问,其采用的方式是产生多个Servlet实例对象,并发的每个线程分别调用一个独立的Servlet实例对象。所以并不能认为是解决Servlet的线程安全问题,是一种自欺欺人的方法,在Servlet
API 2.4中,已经将SingleThreadModel标记为Deprecated(过时的)。
(2). 取消Servlet的实例变量,变成无状态的Servlet。对方法中定义的局部变量,进入方法的每个线程都有自己的一份方法变量拷贝。任何线程都不会修改其他线程的局部变量。
(3). 对共享数据的操作进行同步。比如使用synchronized 关键字来保证一次只有一个线程可以访问被保护的区段。
七.ServletConfig和ServletContext
1. ServletConfig简介
ServletConfig是比较重要的一个类,我们可以看到,在Servlet的配置文件中,可以使用一个或多个<init-param>标签为servlet配置一些初始化参数。例如:
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
如果servlet配置了初始化参数,web容器在创建servlet实例对象时,会自动将这些初始化参数封装到ServletConfig对象中,并在调用servlet的init方法时,将ServletConfig对象传递给servlet。进而,开发人员就通过ServletConfig对象来得到当前servlet的初始化参数信息。
2. ServletContext简介
当WEB容器启动时,它会为每个WEB应用程序都创建一个对应的ServletContext对象,它代表当前web应用。ServletConfig对象中维护了ServletContext对象的引用,我们在编写servlet时,可以通过ServletConfig.getServletContext方法获得ServletContext对象。
由于一个WEB应用中的所有Servlet共享同一个ServletContext对象,因此Servlet对象之间可以通过ServletContext对象来实现通讯。所以ServletContext对象通常也被称之为context域对象。它有很多作用,比如实现Servlet的转发,获取WEB应用的初始化参数等等。