Servlet 是在服務器上運行的小程序,其主要功能在于交互式地瀏覽和修改數據,生成動態Web內容。狹義的Servlet是指Java語言實現的一個接口,廣義的Servlet是指任何實現了這個Servlet接口的類。
本文內容:
1.Servlet開發步驟
學習Servlet,我們先看一下Servlet程序怎么開發,大體上可以分為五步,如下所示:
1)編寫java類,繼承HttpServlet類,重寫doGet和doPost方法
public class FirstServlet extends HttpServlet{
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse response)
throws ServletException, IOException {
//向瀏覽器輸出內容
response.getWriter().write("this is first servlet!");
}
}
2)在web.xml文件中進行配置
<!-- 配置一個servlet -->
<!-- servlet的配置 -->
<servlet>
<!-- servlet的內部名稱,自定義。盡量有意義 -->
<servlet-name>FirstServlet</servlet-name>
<!-- servlet的類全名: 包名+簡單類名 -->
<servlet-class>cn.acamy.FirstServlet</servlet-class>
</servlet>
<!-- servlet的映射配置 -->
<servlet-mapping>
<!-- servlet的內部名稱,一定要和上面的內部名稱保持一致!! -->
<servlet-name>FirstServlet</servlet-name>
<!-- servlet的映射路徑(訪問servlet的名稱) -->
<url-pattern>/first</url-pattern>
</servlet-mapping>
3)部署,servlet程序的class碼拷貝到WEB-INF/classes目錄
4)啟動tomcat服務器
5)通過URL訪問
http://localhost:8080/demo/first
http:// :http協議
localhost: :到本地的hosts文件中查找是否存在該域名對應的IP地址(127.0.0.1)
8080 : 找到tomcat服務器
/demo : 在tomcat的webapps目錄下找 demo的目錄
/first: 資源名稱。
1)在demo的web.xml中查找是否有匹配的url-pattern的內容(/first)
2)如果找到匹配的url-pattern,則使用當前servlet-name的名稱到web.xml文件中查詢是否相同名稱的servlet配置
3)如果找到,則取出對應的servlet配置信息中的servlet-class內容:
字符串: cn.acamy.FirstServlet
通過反射:
a)構造FirstServlet的對象
b)然后調用FirstServlet里面的方法
2.本文涉及到的相關類
2.1 HttpServlet繼承體系
我們寫的Servlet程序都是要直接繼續HttpServlet,由下面的類圖可以看出該類有service方法的重寫和重載,以及doGet,doPost...方法,繼承于抽象類GenericServlet,GenericServlet實現了接口Servlet和ServletConfig,Servlet接口里面定義了init,service,destroy,getServletConfig,getServletInfo方法,ServletConfig里面定義了獲取配置信息的相關方法。
2.2 HttpServletRequest與HttpServletResponse
HttpServletRequest繼承于ServletRequest,封裝了Servlet程序的請求信息,HttpServletResponse繼承于ServletResponse,封裝了Servlet程序的響應信息。
2.3 ServletContext
ServletContext是Servlet上下文對象,封裝了當前的web應用環境。
3. Servlet的生命周期
Servlet程序的生命周期是由tomcat服務器控制的!
我們先來看一個Demo:
public class LifeDemo extends HttpServlet {
/**
* 1.構造方法
*/
public LifeDemo(){
System.out.println("1.servlet對象被創建了。");
}
/**
* 2.init方法
*/
@Override
public void init(ServletConfig config) throws ServletException {
System.out.println("2.init方法被調用");
}
/**
* 3.service方法
*/
@Override
public void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException {
System.out.println("3.service方法被調用");
}
/**
* 4.destroy方法
*/
@Override
public void destroy() {
System.out.println("4.servlet對象銷毀了");
}
}
效果如下:
1. 構造方法: 創建servlet對象的時候調用。默認情況下,第一次訪問servlet的時候創建servlet對象。只調用1次。證明servlet對象在tomcat是單實例的。
2. init方法: 創建完servlet對象的時候調用。只調用1次。
3. service方法: 每次發出請求時調用。調用n次。
4. destroy方法: 銷毀servlet對象的時候調用。停止服務器或者重新部署web應用時銷毀servlet對象。 只調用1次。
Servlet生命周期如下圖所示:
4.HttpServlet的service方法源碼
從上面的生命周期可以看出,service方法在瀏覽器每發送一次請求就被調用一次。但我如果要調用doGet,doPost,doHead等方法時,又該怎么實現呢?先看一下HttpServlet里面與該方法相關的源碼:
private static final String METHOD_DELETE = "DELETE";
private static final String METHOD_HEAD = "HEAD";
private static final String METHOD_GET = "GET";
private static final String METHOD_OPTIONS = "OPTIONS";
private static final String METHOD_POST = "POST";
private static final String METHOD_PUT = "PUT";
private static final String METHOD_TRACE = "TRACE";
//該方法是重寫Servlet接口中的方法,可以看到先把ServletRequest和
//ServletResponse轉化為其子類HttpServletRequest,HttpServletResponse
//然后調用重載的service(HttpServletRequest req, HttpServletResponse resp)
@Override
public void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException {
HttpServletRequest request;
HttpServletResponse response;
try {
request = (HttpServletRequest) req;
response = (HttpServletResponse) res;
} catch (ClassCastException e) {
throw new ServletException("non-HTTP request or response");
}
service(request, response);
}
//注意這個方法是上面service方法的重載,并且是protected面不是public修飾符修
//飾,說明該方法只能被自身或子類來調用。
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
//首先得到方法名
String method = req.getMethod();
// 然后看該方法名與實現的哪個方法相對應,就執行哪個方法
// 如得到的方法名為POST,則對應到METHOD_POST,
// 執行doPost(req, resp);
if (method.equals(METHOD_GET)) {
long lastModified = getLastModified(req);
if (lastModified == -1) {
// servlet doesn't support if-modified-since, no reason
// to go through further expensive logic
doGet(req, resp);
} else {
long ifModifiedSince;
try {
ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
} catch (IllegalArgumentException iae) {
// Invalid date header - proceed as if none was set
ifModifiedSince = -1;
}
if (ifModifiedSince < (lastModified / 1000 * 1000)) {
// If the servlet mod time is later, call doGet()
// Round down to the nearest second for a proper compare
// A ifModifiedSince of -1 will always be less
maybeSetLastModified(resp, lastModified);
doGet(req, resp);
} else {
resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
}
}
} else if (method.equals(METHOD_HEAD)) {
long lastModified = getLastModified(req);
maybeSetLastModified(resp, lastModified);
doHead(req, resp);
} else if (method.equals(METHOD_POST)) {
doPost(req, resp);
} else if (method.equals(METHOD_PUT)) {
doPut(req, resp);
} else if (method.equals(METHOD_DELETE)) {
doDelete(req, resp);
} else if (method.equals(METHOD_OPTIONS)) {
doOptions(req,resp);
} else if (method.equals(METHOD_TRACE)) {
doTrace(req,resp);
} else {
//
// Note that this means NO servlet supports whatever
// method was requested, anywhere on this server.
//
//如果沒有找到則說明沒有該方法的實現,給出錯誤提示
String errMsg = lStrings.getString("http.method_not_implemented");
Object[] errArgs = new Object[1];
errArgs[0] = method;
errMsg = MessageFormat.format(errMsg, errArgs);
resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);
}
}
結論:無論我們需要調用的是GET,POST還是其它方法,我們首先通過調用service(ServletRequest req, ServletResponse res),然后指向service(HttpServletRequest req, HttpServletResponse resp),最終通過方法名來進行匹配,實現轉向,調用想doGET或POST或其它的方法。
5.Servlet的路徑問題與自動加載
5.1 Servlet 的映射路徑
<servlet-mapping>
<!-- servlet的內部名稱,一定要和上面的內部名稱保持一致!! -->
<servlet-name>FirstServlet</servlet-name>
<!-- servlet的映射路徑(訪問servlet的名稱) -->
<url-pattern>/first</url-pattern>
</servlet-mapping>
Servlet的映射路徑分為精確匹配和模糊匹配
注意:
1)url-pattern要么以 / 開頭,要么以*開頭。 例如, itcast是非法路徑。
2)不能同時使用兩種模糊匹配,例如 /demo/*.do是非法路徑
3)當有輸入的URL有多個servlet同時被匹配的情況下:
3.1 精確匹配優先。(長的最像優先被匹配)
3.2 以后綴名結尾的模糊url-pattern優先級最低!!!
5.2 Servlet 的缺省路徑
servlet的缺省路徑(<url-pattern>/</url-pattern>)是在tomcat服務器內置的一個路徑。該路徑對應的是一個DefaultServlet(缺省Servlet)。這個缺省的Servlet的作用是用于解析web應用的靜態資源文件。
問題: URL輸入http://localhost:8080/demo/index.html 如何讀取文件?
1)到當前demo應用下的web.xml文件查找是否有匹配的url-pattern。
2)如果沒有匹配的url-pattern,則交給tomcat的內置的DefaultServlet處理
3)DefaultServlet程序到demo應用的根目錄下查找是存在一個名稱為index.html的靜態文件。
4)如果找到該文件,則讀取該文件內容,返回給瀏覽器。
5)如果找不到該文件,則返回404錯誤頁面。
結論: 先找動態資源,再找靜態資源。
5.3 Servlet 的自動加載
默認情況下,第一次訪問servlet的時候創建servlet對象。如果servlet的構造方法或init方法中執行了比較多的邏輯代碼,那么導致用戶第一次訪問sevrlet的時候比較慢。
改變servlet創建對象的時機: 提前到加載web應用的時候!
在servlet的配置信息中,加上一個<load-on-startup>即可!
<servlet>
<servlet-name>LifeDemo</servlet-name>
<servlet-class>gz.itcast.c_life.LifeDemo</servlet-class>
<!-- 讓servlet對象自動加載 -->
<load-on-startup>1</load-on-startup> 注意: 整數值越大,創建優先級越低!!
</servlet>
自動加載效果如下圖所示,可以看到Servlet在Tomcat服務器啟動時就執行了構造方法和init方法:
6. Servlet的有參和無參init方法
有參數的init方法是servlet的生命周期方法,一定會被tomcat服務器調用。如果要編寫初始代碼,不需要覆蓋。無參數的init方法是servlet的編寫初始化代碼的方法。是Sun公司設計出來專門給開發者進行覆蓋,然后在里面編寫servlet的初始邏輯代碼的方法。
public class InitDemo extends HttpServlet {
/*
* @Override public void init(ServletConfig config) throws ServletException
* { System.out.println("有參數的init方法"); }
*/
@Override
public void init() throws ServletException {
// System.out.println("無參數的init方法");
}
}
7.Servlet的多線程并發問題
servlet對象在tomcat服務器是單實例多線程的。因為servlet是多線程的,所以當多個servlet的線程同時訪問了servlet的共享數據,如成員變量,可能會引發線程安全問題。
解決辦法:
1)把使用到共享數據的代碼塊進行同步(使用synchronized關鍵字進行同步)
2)建議在servlet類中盡量不要使用成員變量。如果確實要使用成員,必須同步。而且盡量縮小同步代碼塊的范圍。(哪里使用到了成員變量,就同步哪里!!),以避免因為同步而導致并發效率降低。
public class TheradDemo extends HttpServlet {
int count = 1;
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html;charset=utf-8");
synchronized (TheradDemo.class) {//鎖對象必須唯一。建議使用類對象
response.getWriter().write("你現在是當前網站的第"+count+"個訪客"); //線程1執行完 , 線程2執行
//線程1還沒有執行count++
/*try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}*/
count++;
}
}
}
8.ServletConfig對象
ServletConfig對象: 主要是用于加載servlet的初始化參數。在一個web應用可以存在多個ServletConfig對象(一個Servlet對應一個ServletConfig對象)
8.1 對象創建和得到
創建時機: 在創建完servlet對象之后,在調用init方法之前創建。
得到對象: 直接從有參數的init方法中得到!!!
8.2 servlet的初始化參數配置
<servlet>
<servlet-name>ConfigDemo</servlet-name>
<servlet-class>gz.itcast.f_config.ConfigDemo</servlet-class>
<!-- 初始參數: 這些參數會在加載web應用的時候,封裝到ServletConfig對象中 -->
<init-param>
<param-name>path</param-name>
<param-value>e:/b.txt</param-value>
</init-param>
</servlet>
** Demo:**
public class ConfigDemo extends HttpServlet {
/**
* 以下兩段代碼GenericServlet已經寫了,我們無需編寫!!
*/
/*private ServletConfig config;*/
/**
* 1)tomcat服務器把這些參數會在加載web應用的時候,封裝到ServletConfig對象中
* 2)tomcat服務器調用init方法傳入ServletConfig對象
*/
/*@Override
public void init(ServletConfig config) throws ServletException {
this.config = config;
}*/
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
/**
* 讀取servlet的初始參數
*/
String path = this.getServletConfig().getInitParameter("path");
File file = new File(path);
//讀取內容
BufferedReader br = new BufferedReader(new FileReader(file));
String str = null;
while( (str=br.readLine())!=null ){
System.out.println(str);
}
//查詢當前servlet的所有初始化參數
Enumeration<String> enums = this.getServletConfig().getInitParameterNames();
while(enums.hasMoreElements()){
String paramName = enums.nextElement();
String paramValue = this.getServletConfig().getInitParameter(paramName);
System.out.println(paramName+"="+paramValue);
}
//得到servlet的名稱
String servletName = this.getServletConfig().getServletName();
System.out.println(servletName);
}
}
9.ServletContext對象
Servlet的上下文對象。表示一個當前的web應用環境。一個web應用中只有一個ServletContext對象。
9.1 對象創建和得到
創建時機:加載web應用時創建ServletContext對象。
得到對象: 從當前Servlet或者ServletConfig對象的getServletContext方法得到
** Demo:**
public class ContextDemo extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 1.得到ServletContext對象
// ServletContext context = this.getServletConfig().getServletContext();
ServletContext context = this.getServletContext(); // (推薦使用)
// 2.得到web應用路徑
/**
* web應用路徑:部署到tomcat服務器上運行的web應用名稱
*/
String contextPath = context.getContextPath();
System.out.println(contextPath);
System.out.println("參數" + context.getInitParameter("AAA"));
Enumeration<String> enums = context.getInitParameterNames();
while (enums.hasMoreElements()) {
String paramName = enums.nextElement();
String paramValue = context.getInitParameter(paramName);
System.out.println(paramName + "=" + paramValue);
}
// 嘗試得到ConfigDemo中的servlet參數
String path = this.getServletConfig().getInitParameter("path");
System.out.println("path=" + path);
// 2.把數據保存到域對象中
context.setAttribute("name", "eric");
// 2.從域對象中取出數據
// String name = (String) context.getAttribute("name");
/**
* 案例:應用到請求重定向
*/
response.sendRedirect(contextPath + "/index.html");
}
}
9.2 轉發與重定向
轉發與重定向都能實現頁面的跳轉,但在實際應用中大不相同,比較如下;
注意: 如果要使用request域對象進行數據共享,只能用轉發技術!!!