JavaEE學習總結(十六)— Servlet


一、Servlet簡介

  Servlet是sun公司提供的一門用於開發動態web資源的技術。
  Sun公司在其API中提供了一個servlet接口,用戶若想用發一個動態web資源(即開發一個Java程序向瀏覽器輸出數據),需要完成以下2個步驟:
  1、編寫一個Java類,實現servlet接口。
  2、把開發好的Java類部署到web服務器中。
  按照一種約定俗成的稱呼習慣,通常我們也把實現了servlet接口的java程序,稱之為Servlet

二、Servlet的運行過程

Servlet程序是由WEB服務器調用,web服務器收到客戶端的Servlet訪問請求后:
  ①Web服務器首先檢查是否已經裝載並創建了該Servlet的實例對象。如果是,則直接執行第④步,否則,執行第②步。
  ②裝載並創建該Servlet的一個實例對象。
  ③調用Servlet實例對象的init()方法。
  ④創建一個用於封裝HTTP請求消息的HttpServletRequest對象和一個代表HTTP響應消息的HttpServletResponse對象,然后調用Servlet的service()方法並將請求和響應對象作為參數傳遞進去。
  ⑤WEB應用程序被停止或重新啟動之前,Servlet引擎將卸載Servlet,並在卸載之前調用Servlet的destroy()方法。


 

 

 

三、Servlet生命周期與原理

1、Servlet 生命周期:Servlet 加載--->實例化--->服務--->銷毀。

2、init():在Servlet的生命周期中,僅執行一次init()方法。它是在服務器裝入Servlet時執行的,負責初始化Servlet對象。可以配置服務器,以在啟動服務器或客戶機首次訪問Servlet時裝入Servlet。無論有多少客戶機訪問Servlet,都不會重復執行init()。

3、service():它是Servlet的核心,負責響應客戶的請求。每當一個客戶請求一個HttpServlet對象,該對象的Service()方法就要調用,而且傳遞給這個方法一個“請求”(ServletRequest)對象和一個“響應”(ServletResponse)對象作為參數。在HttpServlet中已存在Service()方法。默認的服務功能是調用與HTTP請求的方法相應的do功能。

4、destroy(): 僅執行一次,在服務器端停止且卸載Servlet時執行該方法。當Servlet對象退出生命周期時,負責釋放占用的資源。一個Servlet在運行service()方法時可能會產生其他的線程,因此需要確認在調用destroy()方法時,這些線程已經終止或完成。

 

步驟:

  1. Web Client 向Servlet容器(Tomcat)發出Http請求
  2. Servlet容器接收Web Client的請求
  3. Servlet容器創建一個HttpServletRequest 對象,將Web Client請求的信息封裝到這個對象中。
  4. Servlet容器創建一個HttpServletResponse對象
  5. Servlet容器調用HttpServlet對象的service方法,把HttpRequest對象與HttpResponse對象作為參數傳給 HttpServlet 對象。
  6. HttpServlet調用HttpRequest對象的有關方法,獲取Http請求信息。
  7. HttpServlet調用HttpResponse對象的有關方法,生成響應數據。
  8. Servlet容器把HttpServlet的響應結果傳給Web Client。

 

 

1、首先簡單解釋一下Servlet接收和響應客戶請求的過程,首先客戶發送一個請求,Servlet是調用service()方法對請求進行響應的,通過源代碼可見,service()方法中對請求的方式進行了匹配,選擇調用doGet,doPost等這些方法,然后再進入對應的方法中調用邏輯層的方法,實現對客戶的響應。在Servlet接口和GenericServlet中是沒有doGet()、doPost()等等這些方法的,HttpServlet中定義了這些方法,但是都是返回error信息,所以,我們每次定義一個Servlet的時候,都必須實現doGet或doPost等這些方法。

2、每一個自定義的Servlet都必須實現Servlet的接口,Servlet接口中定義了五個方法,其中比較重要的三個方法涉及到Servlet的生命周期,分別是上文提到的init(),service(),destroy()方法。GenericServlet是一個通用的,不特定於任何協議的Servlet,它實現了Servlet接口。而HttpServlet繼承於GenericServlet,因此HttpServlet也實現了Servlet接口。所以我們定義Servlet的時候只需要繼承HttpServlet即可。

3、Servlet接口和GenericServlet是不特定於任何協議的,而HttpServlet是特定於HTTP協議的類,所以HttpServlet中實現了service()方法,並將請求ServletRequest、ServletResponse 強轉為HttpRequest 和 HttpResponse。

創建Servlet對象的時機:

Servlet容器啟動時:讀取web.xml配置文件中的信息,構造指定的Servlet對象,創建ServletConfig對象,同時將ServletConfig對象作為參數來調用Servlet對象的init方法。

在Servlet容器啟動后:客戶首次向Servlet發出請求,Servlet容器會判斷內存中是否存在指定的Servlet對象,如果沒有則創建它,然后根據客戶的請求創建HttpRequest、HttpResponse對象,從而調用Servlet 對象的service方法。

Servlet Servlet容器在啟動時自動創建Servlet,這是由在web.xml文件中為Servlet設置的<load-on-startup>屬性決定的。從中我們也能看到同一個類型的Servlet對象在Servlet容器中以單例的形式存在。  

<servlet>

        <servlet-name>Init</servlet-name>
        <servlet-class>org.xl.servlet.InitServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
</servlet>

 

Servlet初始化階段,在下列時刻Servlet容器裝載Servlet:

    1,Servlet容器啟動時自動裝載某些Servlet,實現它只需要在web.XML文件中的<Servlet></Servlet>之間添加如下代碼:<loadon-startup>1</loadon-startup> 

    2,在Servlet容器啟動后,客戶首次向Servlet發送請求

    3,Servlet類文件被更新后,重新裝載Servlet

Servlet被裝載后,Servlet容器創建一個Servlet實例並且調用Servlet的init()方法進行初始化。在Servlet的整個生命周期內,init()方法只被調用一次。

示例:

package com.nf;

import java.io.IOException;

import javax.jws.soap.InitParam;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebInitParam;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet(value = "/Hello", loadOnStartup = -1, initParams = { @WebInitParam(name = "size", value = "100"),
        @WebInitParam(name = "color", value = "red") })
public class Hello extends HttpServlet {
    private static final long serialVersionUID = 1L;

    static {
        System.out.println("靜態代碼塊,static");
    }

    public Hello() {
        super();
        System.out.println("構造方法Hello()");
    }

    @Override
    public void init(ServletConfig config) throws ServletException {
        System.out.println("初始化方法init(),參數color=" + config.getInitParameter("color"));
    }

    // 響應請求
    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        System.out.println("-----響應請求service方法-----");
        super.service(request, response);
    }

    // 響應Get請求doGet方法
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        System.out.println("響應Get請求doGet方法");
        response.getWriter().append("Served at: ").append(request.getContextPath());
    }
    
    @Override
    public void destroy() {
        System.out.println("Servlet銷毀方法destroy()");
        super.destroy();
    }

    //析構方法 final finally finalize,當對象被回收時,GC
    @Override
    protected void finalize() throws Throwable {
        System.out.println("析構方法finalize()");
    }
}

結果:

Chrome第一次訪問

 FireFox模擬第2個用戶訪問

 控制台輸出:

四、在Eclipse中開發Servlet

  在eclipse中新建一個web project工程,eclipse會自動創建下圖所示目錄結構:

 

4.1、Servlet接口實現類

  Servlet接口SUN公司定義了兩個默認實現類,分別為:GenericServlet、HttpServlet。

  HttpServlet指能夠處理HTTP請求的servlet,它在原有Servlet接口上添加了一些與HTTP協議處理方法,它比Servlet接口的功能更為強大。因此開發人員在編寫Servlet時,通常應繼承這個類,而避免直接去實現Servlet接口。
  HttpServlet在實現Servlet接口時,覆寫了service方法,該方法體內的代碼會自動判斷用戶的請求方式,如為GET請求,則調用HttpServlet的doGet方法,如為Post請求,則調用doPost方法。因此,開發人員在編寫Servlet時,通常只需要覆寫doGet或doPost方法,而不要去覆寫service方法。

4.2、通過Eclipse創建和編寫Servlet

從Servlet3.0開始,配置Servlet支持注解方式,但還是保留了配置web.xml方式,所有使用Servlet有兩種方式:

4.2.1、Servlet類上使用@WebServlet注解進行配置

@WebServlet常用屬性

屬性 類型 是否必須 說明
asyncSupported boolean 指定Servlet是否支持異步操作模式
displayName String 指定Servlet顯示名稱
initParams WebInitParam[] 配置初始化參數
loadOnStartup int 標記容器是否在應用啟動時就加載這個Servlet
name String 指定Servlet名稱
urlPatterns/value String[] 這兩個屬性作用相同,指定Servlet處理的url
@WebServlet(name = "myUserServlet", 
    urlPatterns = "/user/test", 
    loadOnStartup = 1,  
    initParams = {
            @WebInitParam(name="name", value="小明"), 
            @WebInitParam(name="pwd", value="123456")
    }
)
public class UserServlet extends HttpServlet {
    private static final long serialVersionUID = 7109220574468622594L;
    
    @Override
    public void init(ServletConfig config) throws ServletException {
        super.init(config);
        
        System.out.println("servlet初始化...");
    }
    
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        response.setContentType("text/html");
        response.setCharacterEncoding("utf-8");
        PrintWriter pw = response.getWriter();
        
        pw.append("Hello Servlet!<br>" );
        
        //servletName
        pw.append("servletName:" + getServletName() + "<br>");
        
        //initParam
        ServletConfig servletConfig = this.getServletConfig();
        Enumeration<String> paramNames = servletConfig.getInitParameterNames();
        while (paramNames.hasMoreElements()) {
            String paramName = paramNames.nextElement();
            pw.append(paramName + ":" + servletConfig.getInitParameter(paramName) + "<br>");
        }
        
        pw.close();
        
    }
 
 
    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doGet(request, response);
    }
}

loadOnStartup:

標記容器是否在啟動的時候就加載這個servlet。

當值為0或者大於0時,表示容器在應用啟動時就加載這個servlet;

當是一個負數時或者沒有指定時,則指示容器在該servlet被選擇時才加載。

正數的值越小,啟動該servlet的優先級越高。

 

 

4.2.2、web.xml文件中配置

  選中gacl.servlet.study包,右鍵→New→Servlet,如下圖所示:

  

  

  

  這樣,我們就通過Eclipse幫我們創建好一個名字為ServletDemo1的Servlet,創建好的ServletDemo01里面會有如下代碼:

復制代碼
 1 package gacl.servlet.study;
 2 
 3 import java.io.IOException;
 4 import java.io.PrintWriter;
 5 
 6 import javax.servlet.ServletException;
 7 import javax.servlet.http.HttpServlet;
 8 import javax.servlet.http.HttpServletRequest;
 9 import javax.servlet.http.HttpServletResponse;
10 
11 public class ServletDemo1 extends HttpServlet {
12 
13     /**
14      * The doGet method of the servlet. <br>
15      *
16      * This method is called when a form has its tag value method equals to get.
17      * 
18      * @param request the request send by the client to the server
19      * @param response the response send by the server to the client
20      * @throws ServletException if an error occurred
21      * @throws IOException if an error occurred
22      */
23     public void doGet(HttpServletRequest request, HttpServletResponse response)
24             throws ServletException, IOException {
25 
26         response.setContentType("text/html");
27         PrintWriter out = response.getWriter();
28         out.println("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">");
29         out.println("<HTML>");
30         out.println("  <HEAD><TITLE>A Servlet</TITLE></HEAD>");
31         out.println("  <BODY>");
32         out.print("    This is ");
33         out.print(this.getClass());
34         out.println(", using the GET method");
35         out.println("  </BODY>");
36         out.println("</HTML>");
37         out.flush();
38         out.close();
39     }
40 
41     /**
42      * The doPost method of the servlet. <br>
43      *
44      * This method is called when a form has its tag value method equals to post.
45      * 
46      * @param request the request send by the client to the server
47      * @param response the response send by the server to the client
48      * @throws ServletException if an error occurred
49      * @throws IOException if an error occurred
50      */
51     public void doPost(HttpServletRequest request, HttpServletResponse response)
52             throws ServletException, IOException {
53 
54         response.setContentType("text/html");
55         PrintWriter out = response.getWriter();
56         out.println("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">");
57         out.println("<HTML>");
58         out.println("  <HEAD><TITLE>A Servlet</TITLE></HEAD>");
59         out.println("  <BODY>");
60         out.print("    This is ");
61         out.print(this.getClass());
62         out.println(", using the POST method");
63         out.println("  </BODY>");
64         out.println("</HTML>");
65         out.flush();
66         out.close();
67     }
68 
69 }
復制代碼

  這些代碼都是Eclipse自動生成的,而web.xml文件中也多了<servlet></servlet>和<servlet-mapping></servlet-mapping>兩對標簽,這兩對標簽是配置ServletDemo1的,如下圖所示:

然后我們就可以通過瀏覽器訪問ServletDemo1這個Servlet,如下圖所示:

  

示例:

package com.nf;

import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebInitParam;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * Servlet implementation class Hi
 */
//@WebServlet(
//        asyncSupported = true, 
//        description = "我的Servlet", 
//        urlPatterns = { 
//                "/Hi", 
//                "/Hi.do"
//        }, 
//        initParams = { 
//                @WebInitParam(name = "color", value = "blue", description = "顏色")
//        })
public class Hi extends HttpServlet {
    private static final long serialVersionUID = 1L;
       
    public Hi() {
        super();
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        response.getWriter().append("Hello Servlet");
    }

}

 

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"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
    version="3.0">
    <display-name>JavaEE_143_01</display-name>
    <welcome-file-list>
        <welcome-file>index.html</welcome-file>
        <welcome-file>index.htm</welcome-file>
        <welcome-file>index.jsp</welcome-file>
        <welcome-file>default.html</welcome-file>
        <welcome-file>default.htm</welcome-file>
        <welcome-file>default.jsp</welcome-file>
    </welcome-file-list>

    <servlet>
        <servlet-name>Hi</servlet-name>
        <servlet-class>com.nf.Hi</servlet-class>
        <init-param>
            <param-name>color</param-name>
            <param-value>green</param-value>
            <description>顏色</description>
        </init-param>
    </servlet>

    <servlet-mapping>
        <servlet-name>Hi</servlet-name>
        <url-pattern>/Hi</url-pattern>
        <url-pattern>/Hi.do</url-pattern>
    </servlet-mapping>
</web-app>

結果:

五、Servlet開發注意細節

5.1、Servlet訪問URL映射配置

  由於客戶端是通過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的對外訪問路徑。例如:

復制代碼
1   <servlet>
2     <servlet-name>ServletDemo1</servlet-name>
3     <servlet-class>gacl.servlet.study.ServletDemo1</servlet-class>
4   </servlet>
5 
6   <servlet-mapping>
7     <servlet-name>ServletDemo1</servlet-name>
8     <url-pattern>/servlet/ServletDemo1</url-pattern>
9   </servlet-mapping>
復制代碼

  同一個Servlet可以被映射到多個URL上,即多個<servlet-mapping>元素的<servlet-name>子元素的設置值可以是同一個Servlet的注冊名。 例如:

復制代碼
 1  <servlet>
 2     <servlet-name>ServletDemo1</servlet-name>
 3     <servlet-class>gacl.servlet.study.ServletDemo1</servlet-class>
 4   </servlet>
 5 
 6   <servlet-mapping>
 7     <servlet-name>ServletDemo1</servlet-name>
 8     <url-pattern>/servlet/ServletDemo1</url-pattern>
 9   </servlet-mapping>
10  <servlet-mapping>
11     <servlet-name>ServletDemo1</servlet-name>
12     <url-pattern>/1.htm</url-pattern>
13   </servlet-mapping>
14    <servlet-mapping>
15     <servlet-name>ServletDemo1</servlet-name>
16     <url-pattern>/2.jsp</url-pattern>
17   </servlet-mapping>
18    <servlet-mapping>
19     <servlet-name>ServletDemo1</servlet-name>
20     <url-pattern>/3.php</url-pattern>
21   </servlet-mapping>
22    <servlet-mapping>
23     <servlet-name>ServletDemo1</servlet-name>
24     <url-pattern>/4.ASPX</url-pattern>
25   </servlet-mapping>
復制代碼

  通過上面的配置,當我們想訪問名稱是ServletDemo1的Servlet,可以使用如下的幾個地址去訪問:

  http://localhost:8080/JavaWeb_Servlet_Study_20140531/servlet/ServletDemo1

  http://localhost:8080/JavaWeb_Servlet_Study_20140531/1.htm

  http://localhost:8080/JavaWeb_Servlet_Study_20140531/2.jsp

  http://localhost:8080/JavaWeb_Servlet_Study_20140531/3.php

  http://localhost:8080/JavaWeb_Servlet_Study_20140531/4.ASPX

  ServletDemo1被映射到了多個URL上。

5.2、Servlet訪問URL使用*通配符映射  

在Servlet映射到的URL中也可以使用*通配符,但是只能有兩種固定的格式:一種格式是"*.擴展名",另一種格式是以正斜杠(/)開頭並以"/*"結尾。例如:

  

復制代碼
1  <servlet>
2     <servlet-name>ServletDemo1</servlet-name>
3     <servlet-class>gacl.servlet.study.ServletDemo1</servlet-class>
4   </servlet>
5 
6    <servlet-mapping>
7     <servlet-name>ServletDemo1</servlet-name>
8    <url-pattern>/*</url-pattern>
復制代碼

  *可以匹配任意的字符,所以此時可以用任意的URL去訪問ServletDemo1這個Servlet,如下圖所示:

  

對於如下的一些映射關系:
  Servlet1 映射到 /abc/*
  Servlet2 映射到 /*
  Servlet3 映射到 /abc
  Servlet4 映射到 *.do
問題:
  當請求URL為“/abc/a.html”,“/abc/*”和“/*”都匹配,哪個servlet響應
      Servlet引擎將調用Servlet1。
  當請求URL為“/abc”時,“/abc/*”和“/abc”都匹配,哪個servlet響應
      Servlet引擎將調用Servlet3。
  當請求URL為“/abc/a.do”時,“/abc/*”和“*.do”都匹配,哪個servlet響應
      Servlet引擎將調用Servlet1。
  當請求URL為“/a.do”時,“/*”和“*.do”都匹配,哪個servlet響應
      Servlet引擎將調用Servlet2。
  當請求URL為“/xxx/yyy/a.do”時,“/*”和“*.do”都匹配,哪個servlet響應
      Servlet引擎將調用Servlet2。
  匹配的原則就是"誰長得更像就找誰"

5.3、Servlet與普通Java類的區別  

  Servlet是一個供其他Java程序(Servlet引擎)調用的Java類,它不能獨立運行,它的運行完全由Servlet引擎來控制和調度。
  針對客戶端的多次Servlet請求,通常情況下,服務器只會創建一個Servlet實例對象,也就是說Servlet實例對象一旦創建,它就會駐留在內存中,為后續的其它請求服務,直至web容器退出,servlet實例對象才會銷毀。
  在Servlet的整個生命周期內,Servlet的init方法只被調用一次。而對一個Servlet的每次訪問請求都導致Servlet引擎調用一次servlet的service方法。對於每次訪問請求,Servlet引擎都會創建一個新的HttpServletRequest請求對象和一個新的HttpServletResponse響應對象,然后將這兩個對象作為參數傳遞給它調用的Servlet的service()方法,service方法再根據請求方式分別調用doXXX方法。

  如果在<servlet>元素中配置了一個<load-on-startup>元素,那么WEB應用程序在啟動時,就會裝載並創建Servlet的實例對象、以及調用Servlet實例對象的init()方法。
    舉例:
    <servlet>
        <servlet-name>invoker</servlet-name>
        <servlet-class>
            org.apache.catalina.servlets.InvokerServlet
        </servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>

  用途:為web應用寫一個InitServlet,這個servlet配置為啟動時裝載,為整個web應用創建必要的數據庫表和數據。

5.4、缺省Servlet

  如果某個Servlet的映射路徑僅僅為一個正斜杠(/),那么這個Servlet就成為當前Web應用程序的缺省Servlet。
  凡是在web.xml文件中找不到匹配的<servlet-mapping>元素的URL,它們的訪問請求都將交給缺省Servlet處理,也就是說,缺省Servlet用於處理所有其他Servlet都不處理的訪問請求。 例如:

復制代碼
 1  <servlet>
 2     <servlet-name>ServletDemo2</servlet-name>
 3     <servlet-class>gacl.servlet.study.ServletDemo2</servlet-class>
 4     <load-on-startup>1</load-on-startup>
 5   </servlet>
 6   
 7   <!-- 將ServletDemo2配置成缺省Servlet -->
 8   <servlet-mapping>
 9     <servlet-name>ServletDemo2</servlet-name>
10     <url-pattern>/</url-pattern>
11   </servlet-mapping>
復制代碼

  當訪問不存在的Servlet時,就使用配置的默認Servlet進行處理,如下圖所示:

  

  在<tomcat的安裝目錄>\conf\web.xml文件中,注冊了一個名稱為org.apache.catalina.servlets.DefaultServlet的Servlet,並將這個Servlet設置為了缺省Servlet。

復制代碼
 1     <servlet>
 2         <servlet-name>default</servlet-name>
 3         <servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
 4         <init-param>
 5             <param-name>debug</param-name>
 6             <param-value>0</param-value>
 7         </init-param>
 8         <init-param>
 9             <param-name>listings</param-name>
10             <param-value>false</param-value>
11         </init-param>
12         <load-on-startup>1</load-on-startup>
13     </servlet>
14 
15  <!-- The mapping for the default servlet -->
16     <servlet-mapping>
17         <servlet-name>default</servlet-name>
18         <url-pattern>/</url-pattern>
19     </servlet-mapping>
復制代碼

  當訪問Tomcat服務器中的某個靜態HTML文件和圖片時,實際上是在訪問這個缺省Servlet。

5.5、Servlet的線程安全問題

  當多個客戶端並發訪問同一個Servlet時,web服務器會為每一個客戶端的訪問請求創建一個線程,並在這個線程上調用Servlet的service方法,因此service方法內如果訪問了同一個資源的話,就有可能引發線程安全問題。例如下面的代碼:

不存在線程安全問題的代碼:

復制代碼
 1 package gacl.servlet.study;
 2 
 3 import java.io.IOException;
 4 
 5 import javax.servlet.ServletException;
 6 import javax.servlet.http.HttpServlet;
 7 import javax.servlet.http.HttpServletRequest;
 8 import javax.servlet.http.HttpServletResponse;
 9 
10 public class ServletDemo3 extends HttpServlet {
11 
12     
13     public void doGet(HttpServletRequest request, HttpServletResponse response)
14             throws ServletException, IOException {
15         
16         /**
17          * 當多線程並發訪問這個方法里面的代碼時,會存在線程安全問題嗎
18          * i變量被多個線程並發訪問,但是沒有線程安全問題,因為i是doGet方法里面的局部變量,
19          * 當有多個線程並發訪問doGet方法時,每一個線程里面都有自己的i變量,
20          * 各個線程操作的都是自己的i變量,所以不存在線程安全問題
21          * 多線程並發訪問某一個方法的時候,如果在方法內部定義了一些資源(變量,集合等)
22          * 那么每一個線程都有這些東西,所以就不存在線程安全問題了
23          */
24         int i=1;
25         i++;
26         response.getWriter().write(i);
27     }
28 
29     public void doPost(HttpServletRequest request, HttpServletResponse response)
30             throws ServletException, IOException {
31         doGet(request, response);
32     }
33 
34 }
復制代碼

存在線程安全問題的代碼:

復制代碼
 1 package gacl.servlet.study;
 2 
 3 import java.io.IOException;
 4 
 5 import javax.servlet.ServletException;
 6 import javax.servlet.http.HttpServlet;
 7 import javax.servlet.http.HttpServletRequest;
 8 import javax.servlet.http.HttpServletResponse;
 9 
10 public class ServletDemo3 extends HttpServlet {
11 
12     int i=1;
13     public void doGet(HttpServletRequest request, HttpServletResponse response)
14             throws ServletException, IOException {
15         
16         i++;
17         try {
18             Thread.sleep(1000*4);
19         } catch (InterruptedException e) {
20             e.printStackTrace();
21         }
22         response.getWriter().write(i+"");
23     }
24 
25     public void doPost(HttpServletRequest request, HttpServletResponse response)
26             throws ServletException, IOException {
27         doGet(request, response);
28     }
29 
30 }
復制代碼

  把i定義成全局變量,當多個線程並發訪問變量i時,就會存在線程安全問題了,如下圖所示:同時開啟兩個瀏覽器模擬並發訪問同一個Servlet,本來正常來說,第一個瀏覽器應該看到2,而第二個瀏覽器應該看到3的,結果兩個瀏覽器都看到了3,這就不正常。

  

  線程安全問題只存在多個線程並發操作同一個資源的情況下,所以在編寫Servlet的時候,如果並發訪問某一個資源(變量,集合等),就會存在線程安全問題,那么該如何解決這個問題呢?

先看看下面的代碼:

復制代碼
 1 package gacl.servlet.study;
 2 
 3 import java.io.IOException;
 4 
 5 import javax.servlet.ServletException;
 6 import javax.servlet.http.HttpServlet;
 7 import javax.servlet.http.HttpServletRequest;
 8 import javax.servlet.http.HttpServletResponse;
 9 
10 
11 public class ServletDemo3 extends HttpServlet {
12 
13     int i=1;
14     public void doGet(HttpServletRequest request, HttpServletResponse response)
15             throws ServletException, IOException {
16         /**
17          * 加了synchronized后,並發訪問i時就不存在線程安全問題了,
18          * 為什么加了synchronized后就沒有線程安全問題了呢?
19          * 假如現在有一個線程訪問Servlet對象,那么它就先拿到了Servlet對象的那把鎖
20          * 等到它執行完之后才會把鎖還給Servlet對象,由於是它先拿到了Servlet對象的那把鎖,
21          * 所以當有別的線程來訪問這個Servlet對象時,由於鎖已經被之前的線程拿走了,后面的線程只能排隊等候了
22          * 
23          */
24         synchronized (this) {//在java中,每一個對象都有一把鎖,這里的this指的就是Servlet對象
25             i++;
26             try {
27                 Thread.sleep(1000*4);
28             } catch (InterruptedException e) {
29                 e.printStackTrace();
30             }
31             response.getWriter().write(i+"");
32         }
33         
34     }
35 
36     public void doPost(HttpServletRequest request, HttpServletResponse response)
37             throws ServletException, IOException {
38         doGet(request, response);
39     }
40 
41 }
復制代碼

  現在這種做法是給Servlet對象加了一把鎖,保證任何時候都只有一個線程在訪問該Servlet對象里面的資源,這樣就不存在線程安全問題了,如下圖所示:

  

  這種做法雖然解決了線程安全問題,但是編寫Servlet卻萬萬不能用這種方式處理線程安全問題,假如有9999個人同時訪問這個Servlet,那么這9999個人必須按先后順序排隊輪流訪問。

  針對Servlet的線程安全問題,Sun公司是提供有解決方案的:讓Servlet去實現一個SingleThreadModel接口,如果某個Servlet實現了SingleThreadModel接口,那么Servlet引擎將以單線程模式來調用其service方法。
  查看Sevlet的API可以看到,SingleThreadModel接口中沒有定義任何方法和常量,在Java中,把沒有定義任何方法和常量的接口稱之為標記接口,經常看到的一個最典型的標記接口就是"Serializable",這個接口也是沒有定義任何方法和常量的,標記接口在Java中有什么用呢?主要作用就是給某個對象打上一個標志,告訴JVM,這個對象可以做什么,比如實現了"Serializable"接口的類的對象就可以被序列化,還有一個"Cloneable"接口,這個也是一個標記接口,在默認情況下,Java中的對象是不允許被克隆的,就像現實生活中的人一樣,不允許克隆,但是只要實現了"Cloneable"接口,那么對象就可以被克隆了。

  讓Servlet實現了SingleThreadModel接口,只要在Servlet類的定義中增加實現SingleThreadModel接口的聲明即可。  
  對於實現了SingleThreadModel接口的Servlet,Servlet引擎仍然支持對該Servlet的多線程並發訪問,其采用的方式是產生多個Servlet實例對象,並發的每個線程分別調用一個獨立的Servlet實例對象
  實現SingleThreadModel接口並不能真正解決Servlet的線程安全問題,因為Servlet引擎會創建多個Servlet實例對象,而真正意義上解決多線程安全問題是指一個Servlet實例對象被多個線程同時調用的問題。事實上,在Servlet API 2.4中,已經將SingleThreadModel標記為Deprecated(過時的)。  

 

當Tomcat接收到Client的HTTP請求時,Tomcat從線程池中取出一個線程,之后找到該請求對應的Servlet對象並進行初始化,之后調用service()方法。要注意的是每一個Servlet對象再Tomcat容器中只有一個實例對象,即是單例模式。如果多個HTTP請求請求的是同一個Servlet,那么着兩個HTTP請求對應的線程將並發調用Servlet的service()方法。

上圖中的Thread1和Thread2調用了同一個Servlet1,所以此時如果Servlet1中定義了實例變量或靜態變量,那么可能會發生線程安全問題(因為所有的線程都可能使用這些變量)。

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.text.SimpleDateFormat;
import java.util.Date;

public class ThreadSafeServlet extends HttpServlet {

    public static String name = "Hello";   //靜態變量,可能發生線程安全問題
    int i;  //實例變量,可能發生線程安全問題
    SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");

    @Override
    public void init() throws ServletException {
        super.init();
        System.out.println("Servlet初始化");
    }

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.printf("%s:%s[%s]\n", Thread.currentThread().getName(), i, format.format(new Date()));
        i++;
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.printf("%s:%s[%s]\n", Thread.currentThread().getName(), i, format.format(new Date()));
        resp.getWriter().println("<html><body><h1>" + i + "</h1></body></html>");
    }
}

 

 

Servlet與JSP的比較:

  有許多相似之處,都可以生成動態網頁。

  JSP的優點是擅長於網頁制作,生成動態頁面比較直觀,缺點是不容易跟蹤與排錯。

  Servlet是純Java語言,擅長於處理流程和業務邏輯,缺點是生成動態網頁不直觀。

六、Servlet改進

6.1、 反射簡化Servlet操作

 為了讓Servlet可以處理不同類型的請求我們可能需要像下面這樣寫代碼:

package com.nf.controller;

import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/UserController")
public class UserController extends HttpServlet {
    private static final long serialVersionUID = 1L;

    protected void service(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        String action = request.getParameter("act");
        if (action.equals("add")) {
            System.out.println("執行添加");
        } else if (action.equals("delete")) {
            System.out.println("執行刪除");
        } else if (action.equals("edit")) {
            System.out.println("執行更新");
        } else {
            System.out.println("沒有改動作");
        }
    }
}

 

這樣做會使單個方法變得非常復雜,不便於維護與擴展,結合反射可以簡化成如下:

package com.nf.controller;

import java.io.IOException;
import java.lang.reflect.Method;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/UserController")
public class UserController extends HttpServlet {
    private static final long serialVersionUID = 1L;

    protected void service(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        String action = request.getParameter("act");
        request.setCharacterEncoding("utf-8");
        response.setCharacterEncoding("utf-8");
        response.setContentType("text/html;charset=utf-8");

        if (action != null) {
            try {
                // 在當前Servlet實例中根據action找到方法信息
                Method method = getClass().getDeclaredMethod(action, HttpServletRequest.class,
                        HttpServletResponse.class);
                if (method != null) {
                    // 在當前實例上調用方法method,指定參數request,response
                    method.invoke(this, request, response);
                } else {
                    response.getWriter().write("您請求的action不存在");
                }
            } catch (Exception e) {
                response.getWriter().write("調用發生了錯誤,錯誤:" + e.getMessage());
                e.printStackTrace();
            }

        } else {
            try {
                response.getWriter().write("請指定參數act");
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /** 執行添加 */
    public void add(HttpServletRequest request, HttpServletResponse response) {
        try {
            response.getWriter().write("執行添加");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /** 執行刪除 */
    public void delete(HttpServletRequest request, HttpServletResponse response) {
        try {
            response.getWriter().write("執行刪除");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /** 執行修改 */
    public void edit(HttpServletRequest request, HttpServletResponse response) {
        try {
            response.getWriter().write("執行修改");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    /** 執行查詢 */
    public void list(HttpServletRequest request, HttpServletResponse response) {
        try {
            response.getWriter().write("執行查詢");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

運行結果:

add添加:

edit編輯:

 

方法不存在時:

 

6.2、使用反射映射參數到實體類中

 

6.3、BaseServlet基類

 在6.1中通過反射可以實現簡化Servlet,但是如果項目中有多個控制器,則每一個都需要設置編碼,反射等操作,面對這種問題可以使用BaseServlet解決,簡單說就是自定義一個公共的基類,讓所有的Servlet繼承該類,代碼如下:

BaseServlet

package com.nf.controller;

import java.io.IOException;
import java.lang.reflect.Method;

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 BaseServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;

    public void init(ServletConfig config) throws ServletException {
        
    }
    
    public String commonObject;
   //Write once only once!
    protected void service(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        String action = request.getParameter("act");
        request.setCharacterEncoding("utf-8");
        response.setCharacterEncoding("utf-8");
        response.setContentType("text/html;charset=utf-8");
        
        if (action != null) {
            try {
                // 在當前Servlet實例中根據action找到方法信息
                Method method = getClass().getDeclaredMethod(action, HttpServletRequest.class,
                        HttpServletResponse.class);
                if (method != null) {
                    // 在當前實例上調用方法method,指定參數request,response
                    method.invoke(this, request, response);
                } else {
                    response.getWriter().write("您請求的action不存在");
                }
            } catch (Exception e) {
                response.getWriter().write("調用發生了錯誤,錯誤:" + e.getMessage());
                e.printStackTrace();
            }

        } else {
            try {
                response.getWriter().write("請指定參數act");
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

}

 

UserController

package com.nf.controller;

import java.io.IOException;

import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/UserController")
public class UserController extends BaseServlet {
    private static final long serialVersionUID = 1L;

    /** 執行添加 */
    public void add(HttpServletRequest request, HttpServletResponse response) {
        try {
            response.getWriter().write("執行添加");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /** 執行刪除 */
    public void delete(HttpServletRequest request, HttpServletResponse response) {
        try {
            response.getWriter().write("執行刪除");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /** 執行修改 */
    public void edit(HttpServletRequest request, HttpServletResponse response) {
        try {
            response.getWriter().write("執行修改");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    /** 執行查詢 */
    public void list(HttpServletRequest request, HttpServletResponse response) {
        try {
            response.getWriter().write("執行查詢");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

 

OrderController

package com.nf.controller;

import java.io.IOException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/OrderController")
public class OrderController extends BaseServlet {
    private static final long serialVersionUID = 1L;

    public void add(HttpServletRequest request, HttpServletResponse response) {
        try {
            response.getWriter().write("操作添加訂單");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void del(HttpServletRequest request, HttpServletResponse response) {
        try {
            response.getWriter().write("操作刪除訂單");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

 

執行結果:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM