从本篇博客开始,后续几篇文章会重点学习Java中另一个重量级的高级特性Servlet。
1、 Servlet概念及如何创建Servlet
(1)Servlet概念
Servlet是J2SE中的一个类,在启用 Java 的 Web 服务器上或应用服务器上运行并扩展了服务器的能力。Servlet和用户的通信采用请求/响应模式,用于以动态响应客户机请求形式扩展Web服务器的功能。。虽然servlet可以对任何类型的请求产生响应,但通常只用来扩展Web服务器的应用程序。
(2)Servlet的运行流程
1)客户端发送请求至服务器端;
2)服务端根据客户端的URL,再结合配置文件web.xml中Servlet的映射关系,将客户端的请求转发给相应的Servlet;
3)Servlet引擎调用Service()方法,然后根据HTTP请求报文方法,将请求转到doGet()或者doPost()中动态处理,产生客户端需要的数据,并封装到应答Response具体的实例中;
4)服务器端将应答报文沿原路返回给客户端。
(3)利用Eclipse创建一个Servlet
构造一个Web工程,然后新建一个Servlet
package com.wygu.servlet;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class FirstServlet extends HttpServlet{
private static final long serialVersionUID = 1L;
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String strResponse = "Hello World!";
System.out.println("发送客户端的应答报文为:"+strResponse);
//向response返回应答报文
PrintWriter out = response.getWriter();
out.print(strResponse);
//刷新缓冲流,使其立即发送
out.flush();
out.close();
}
}
新建web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
id="WebApp_ID" version="2.5">
<servlet>
<servlet-name>FirstServlet</servlet-name>
<servlet-class>com.wygu.servlet.FirstServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>FirstServlet</servlet-name>
<url-pattern>/FirstServlet</url-pattern>
</servlet-mapping>
</web-app>
程序运行后,在浏览器输入:http://localhost:8080/first-project/FirstServlet,结果为:
2、 Servlet的URL访问及两种特殊Servlet
(1)Servlet的URL匹配
在上面的例子web.xml中 …标签声明一个Servlet,包括Servlet的类名及对应的包名。
客户端发送请求至Servlet容器时,容器会去掉当前应用的上下文(工程名),利用剩下的作为Servlet的映射关系映射到具体的Servlet上。比如:上述URL地址:http://localhost:8080/first-project/FirstServlet,去掉上下文后FirstServlet映射到对应真实的FirstServlet上。
显然如果存在一对多(一个真实的Servlet存在多个映射关系),容器按照什么样的规则匹配,下面观察常见的有以下几种匹配规则:
1)精确匹配
精确匹配要求url必须和的配置项完全匹配
<servlet-mapping>
<servlet-name>FirstServlet</servlet-name>
<url-pattern>/FirstServlet</url-pattern>
<url-pattern>/aaa</url-pattern>
<url-pattern>/bbb</url-pattern>
</servlet-mapping>
在浏览器中输入以下URL都会被匹配:
http://localhost:8080/first-project/bbb
http://localhost:8080/first-project/aaa
可以正常加载页面
但是如果输入:http://localhost:8080/first-project/aac出现匹配失败
3)模糊匹配
如果按照如下方式添加配置项:
<servlet-mapping>
<servlet-name>FirstServlet</servlet-name>
<url-pattern>/FirstServlet</url-pattern>
<url-pattern>/hello/*</url-pattern>
</servlet-mapping>
在浏览器中输入以下URL都会被匹配:
http://localhost:8080/first-project/hello/
http://localhost:8080/first-project/hello/world
http://localhost:8080/first-project/hello/kitty
或者按照下述配置:
<servlet-mapping>
<servlet-name>FirstServlet</servlet-name>
<url-pattern>/FirstServlet</url-pattern>
<url-pattern>*.jsp</url-pattern>
</servlet-mapping>
在浏览器中输入以下URL都会被匹配:
http://localhost:8080/first-project/hello.jsp
http://localhost:8080/first-project/hello/world.jsp
但是http://localhost:8080/first-project/hello.jspa就不匹配
(2)两种特殊的Servlet
1)默认Servlet
如果将写成如下形式:
<servlet-mapping>
<servlet-name>FirstServlet</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
我们把上述FirstServlet称为默认Servlet,可以匹配任一种URL(上下文相同),比如:
http://localhost:8080/first-project/
http://localhost:8080/first-project/hello
http://localhost:8080/first-project/dog/cat
2)自启动Servlet
如果将web.xml中的配置项加入1可以使用部署应用到容器时会自动加载该Servlet:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
id="WebApp_ID" version="2.5">
<servlet>
<servlet-name>FirstServlet</servlet-name>
<servlet-class>com.wygu.servlet.FirstServlet</servlet-class>
<!-- 部署应用时,自动加载 -->
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>FirstServlet</servlet-name>
<url-pattern>/FirstServlet</url-pattern>
</servlet-mapping>
</web-app>
然后在FirstServlet中加入init()方法(在Servlet的生命周期中,仅执行一次init()方法)
public void init(){
System.out.println("自启动Servlet......");
}
在启动tomcat时,程序输出:自启动Servlet……
value,参数value默认值为负数,在加载web应用时,servlet实例不会被创建和加载,而在客户端第一次请求时被创建并被加载;若参数value为0或者正数,则在启动web应用时,servlet实例会被创建,且值越小越早被创建。
我们可以利用这种方式做一些预处理的操作,比如构造数据库连接池,加载数据库配置参数,建立和其它系统的socket连接等。
3、 Servlet的生命周期
Servlet生命周期如下图:
(1)实例化
在以下情况发生时,Web容器会创建Servlet实例
1)Servlet为自启动Servlet,即在web.xml中Servlet配置项被设置为:value ,其中value值为正数或0;
2)Servlet类文件被更新,容器重新加载;
3)Web容器启动后,客户端首次发送Servlet请求。
(2)初始化
Servlet一旦被创建后,容器会调用Servlet的init(ServletConfig config)方法,进行部分初始化的工作(比如连接数据库)。注意:在Servlet整个生命周期中init()只会被调用一次。
(3)服务
客户端发送请求到Web容器后,Servlet调用service(ServletRequest req, ServletResponse res)方法响应请求,然后根据Servlet的请求方式(GET,POST)调用不同的服务方法doGet(),doPost()等。
HttpServlet继承自抽象类GenericServlet,GenericServlet继承了接口Servlet的方法。但是Servlet和GenericServlet不特定于任何协议的,而HttpServlet是特定于HTTP协议的类,因此在HttpServlet中需要实现service()方法,将请求的ServletRequest,ServletResponse强转为HttpRequest和HttpResponse。
/* */ public void service(ServletRequest req, ServletResponse res)
/* */ throws ServletException, IOException
/* */ {
/* */ try
/* */ {
/* 724 */ HttpServletRequest request = (HttpServletRequest)req;
/* 725 */ response = (HttpServletResponse)res;
/* */ } catch (ClassCastException e) { HttpServletResponse response;
/* 727 */ throw new ServletException("non-HTTP request or response"); }
/* */ HttpServletResponse response;
/* 729 */ HttpServletRequest request; service(request, response);
/* */ }
/* */ }
在HttpServlet中重写service(HttpServletRequest req, HttpServletResponse resp)方法,然后根据请求方式调用doXXX()方法:
/* */ protected void service(HttpServletRequest req, HttpServletResponse resp)
/* */ throws ServletException, IOException
/* */ {
/* 615 */ String method = req.getMethod();
/* */
/* 617 */ if (method.equals("GET")) {
/* 618 */ long lastModified = getLastModified(req);
/* 619 */ if (lastModified == -1L)
/* */ {
/* */
/* 622 */ doGet(req, resp);
/* */ } else {
/* */ long ifModifiedSince;
/* */ try {
/* 626 */ ifModifiedSince = req.getDateHeader("If-Modified-Since");
/* */ } catch (IllegalArgumentException iae) {
/* */ long ifModifiedSince;
/* 629 */ ifModifiedSince = -1L;
/* */ }
/* 631 */ if (ifModifiedSince < lastModified / 1000L * 1000L)
/* */ {
/* */
/* */
/* 635 */ maybeSetLastModified(resp, lastModified);
/* 636 */ doGet(req, resp);
/* */ } else {
/* 638 */ resp.setStatus(304);
/* */ }
/* */ }
/* */ }
/* 642 */ else if (method.equals("HEAD")) {
/* 643 */ long lastModified = getLastModified(req);
/* 644 */ maybeSetLastModified(resp, lastModified);
/* 645 */ doHead(req, resp);
/* */ }
/* 647 */ else if (method.equals("POST")) {
/* 648 */ doPost(req, resp);
/* */ }
/* 650 */ else if (method.equals("PUT")) {
/* 651 */ doPut(req, resp);
/* */ }
/* 653 */ else if (method.equals("DELETE")) {
/* 654 */ doDelete(req, resp);
/* */ }
/* 656 */ else if (method.equals("OPTIONS")) {
/* 657 */ doOptions(req, resp);
/* */ }
/* 659 */ else if (method.equals("TRACE")) {
/* 660 */ doTrace(req, resp);
/* */
/* */
/* */ }
/* */ else
/* */ {
/* */
/* */
/* 668 */ String errMsg = lStrings.getString("http.method_not_implemented");
/* 669 */ Object[] errArgs = new Object[1];
/* 670 */ errArgs[0] = method;
/* 671 */ errMsg = MessageFormat.format(errMsg, errArgs);
/* */
/* 673 */ resp.sendError(501, errMsg);
/* */ }
/* */ }
(4)销毁
当Servlet容器终止运行,或者Servlet容器重新装载Servlet实例时,容器会调用Servlet的destroy()方法,在destroy()方法中释放掉Servlet所占用的资源。
4、 Servlet的线程安全问题
容器启动时,会创建一个线程池来服务请求,其中容器中一个调度线程负责管理线程池。每个客户端发送请求到容器时,调度线程会为该请求分配一个线程,并且创建一组新的Request和Respose实例。
通过上一节的介绍,我们知道在整个Servlet生命周期中,容器只会在处理客户端第一次请求时,创建Servlet实例,后面的请求会复用该实例——单例模式的懒汉模式。因而自定义Servlet类(继承HTTPServlet)中如果定义了一些成员变量会导致不同的线程间共享这些成员变量,导致出现线程不安全的问题。
创建服务端ServletSafty:
package com.wygu.servlet;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.wygu.model.Bank;
public class ServletSafty extends HttpServlet{
private static final long serialVersionUID = 1L;
private Bank bank = null;
public void init(ServletConfig config){
bank = new Bank(1000);
System.out.println("银行初始化资金为1000");
}
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
//设置请求的编码格式
request.setCharacterEncoding("UTF-8");
//获得HTTP请求参数中的内容
String money = request.getParameter("money");
int restMoney = bank.withdrawMoney(Integer.parseInt(money));
System.out.println("rest money is:"+restMoney);
PrintWriter out = response.getWriter();
out.print(String.valueOf(restMoney));
//刷新缓冲流,使其立即发送
out.flush();
out.close();
}
}
定义共享成员变量类Bank:
package com.wygu.model;
public class Bank {
private int totalMoney;
public Bank(int totalMoney){
this.setTotalMoney(totalMoney);
}
public int withdrawMoney(int payMoney){
if(payMoney<0){
return -1;
}else if(payMoney>this.getTotalMoney()){
return -2;
}else{
int restMoney = this.getTotalMoney()-payMoney;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.setTotalMoney(restMoney);
}
return this.getTotalMoney();
}
public int getTotalMoney() {
return totalMoney;
}
public void setTotalMoney(int totalMoney) {
this.totalMoney = totalMoney;
}
}
利用Jemeter,1秒内发送10次客户端请求,程序运行结果为:
银行初始化资金为1000
rest money is:950
rest money is:950
rest money is:900
rest money is:850
rest money is:850
rest money is:800
rest money is:750
rest money is:750
rest money is:700
rest money is:700
首先上述程序验证了:(1)Servlet只会在第一次客户端请求时被创建,(2)init()方法在整个Servlet生命周期只被调用一次,(3)多个请求构造的多线程共享成员变量bank。然而程序运行的结果出现了剩余金额相同的情况,出现了线程不安全问题。
根据前面的博客知道,要想解决多线程运行的不安全问题,可以加入关键字synchronized 修饰类Bank中的方法withdrawMoney为,volatile 修饰成员变量totalMoney,保证可见性:
private volatile int totalMoney;
public int withdrawMoney(int payMoney){
synchronized (this) {
if(payMoney<0){
return -1;
}else if(payMoney>this.getTotalMoney()){
return -2;
}else{
int restMoney = this.getTotalMoney()-payMoney;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.setTotalMoney(restMoney);
}
return this.getTotalMoney();
}
}
程序运行结果为:
银行初始化资金为1000
rest money is:950
rest money is:900
rest money is:850
rest money is:800
rest money is:750
rest money is:700
rest money is:650
rest money is:600
rest money is:550
**注意:**Servlet的线程安全性主要是由Servlet实例的成员变量引起的,因而在实际开发中尽量避免使用成员变量。如果无法避免时,可以使用多线程的同步机制确保线程的安全性。
总结:本篇博客从Servlet的概念,第一个Servlet实例,Servlet的访问控制,常见的两种特殊Servlet,Servlet的生命周期,Servlet的线程安全性方面深入的剖析的Servlet。