前言
Servlet(即Server applet服務(wù)端小程序)是一個(gè)會(huì)在服務(wù)端被調(diào)用的Java程序,來(lái)處理請(qǐng)求。前面在分析Tomcat的時(shí)候我們知道,Tomcat本身包含了一個(gè)Servlet容器,用來(lái)存放Servlet實(shí)例,當(dāng)有請(qǐng)求到來(lái)時(shí)就會(huì)調(diào)用對(duì)應(yīng)的Servlet實(shí)例來(lái)處理。Servlet其實(shí)就是一個(gè)Java類,但如果它僅僅只是一個(gè)普通的Java類,Tomcat又如何知道該調(diào)用它的哪些方法呢?所以,我們?cè)O(shè)計(jì)了一個(gè)協(xié)議,或者說(shuō)約定。當(dāng)一個(gè)普通的Java類實(shí)現(xiàn)了某些約定的方法,就可以被看作是一個(gè)Servlet實(shí)例。因此Servlet更像是一座聯(lián)系服務(wù)器和服務(wù)端程序的橋梁。

Servlet 接口
Servlet接口里就給我們定義了一個(gè)合格Servlet類要實(shí)現(xiàn)哪些接口。
Servlet Interface
package javax.servlet;
import java.io.IOException;
public interface Servlet {
void init(ServletConfig var1) throws ServletException;
ServletConfig getServletConfig();
void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;
String getServletInfo();
void destroy();
}
以下給出了幾個(gè)主要方法的介紹
| 方法名 | 介紹 |
|---|---|
| init() | 當(dāng)一個(gè)Servlet類實(shí)例化的時(shí)候會(huì)被調(diào)用的邏輯 |
| service() | 對(duì)一個(gè)到來(lái)請(qǐng)求的具體處理邏輯 |
| destory() | 一個(gè)Servlet類實(shí)例被銷毀時(shí)(通常是在tomcat關(guān)閉時(shí))調(diào)用的邏輯 |
我們可以看到service()方法,Tomcat會(huì)把一個(gè)請(qǐng)求(注意這里請(qǐng)求并不一定是http請(qǐng)求,Servlet可以處理多種請(qǐng)求)包裝成ServletRequest類的實(shí)例傳入,service()處理完,我們?cè)侔烟幚斫Y(jié)果包裝成ServletResponse類的實(shí)例返回出去。理論上只要實(shí)現(xiàn)了這些接口,我們就可以構(gòu)造出一個(gè)Servlet了,但我們卻很少這么做,因?yàn)檫@個(gè)過(guò)程過(guò)于繁瑣,JavaEE已經(jīng)幫我們包裝好了一些實(shí)現(xiàn)了Servlet接口的類,我們使用的時(shí)候只要簡(jiǎn)單的重寫這些類中的某個(gè)方法就可以了。繼承這些類然后重寫一些方法就好了,比如對(duì)于處理http請(qǐng)求的Servlet,就提供了HttpServlet類供我們繼承,我們來(lái)看看這些類的繼承關(guān)系

上圖我們可以看到,
HttpServlet并沒(méi)有直接實(shí)現(xiàn)Servlet接口,而是繼承了GenericServlet類并在它的基礎(chǔ)上針對(duì)http進(jìn)行了定制化。我們看看GenericServlet做了什么。
-
GenericeServlet類
public abstract class GenericServlet implements Servlet, ServletConfig, Serializable {
private static final long serialVersionUID = 1L;
private transient ServletConfig config;
public GenericServlet() {
}
public void destroy() {
}
public String getInitParameter(String name) {
return this.getServletConfig().getInitParameter(name);
}
public Enumeration<String> getInitParameterNames() {
return this.getServletConfig().getInitParameterNames();
}
public ServletConfig getServletConfig() {
return this.config;
}
public ServletContext getServletContext() {
return this.getServletConfig().getServletContext();
}
public String getServletInfo() {
return "";
}
public void init(ServletConfig config) throws ServletException {
this.config = config;
this.init();
}
public void init() throws ServletException {
}
public void log(String message) {
this.getServletContext().log(this.getServletName() + ": " + message);
}
public void log(String message, Throwable t) {
this.getServletContext().log(this.getServletName() + ": " + message, t);
}
public abstract void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;
public String getServletName() {
return this.config.getServletName();
}
}
我們看到GenericServlet類只是簡(jiǎn)單的實(shí)現(xiàn)了3個(gè)借口中的方法,并沒(méi)有什么實(shí)質(zhì)性的代碼,那么GenericServlet這個(gè)抽象類的意義是什么?其實(shí)它更多的是提供一個(gè)模版,供子類去修改,而不是讓每個(gè)子類都去實(shí)現(xiàn)這些接口中的所有方法,避免重復(fù)勞動(dòng)。下面讓我們具體分析繼承了GenericServlet的HttpServlet做了什么。
-
HttpServlet類實(shí)現(xiàn)
HttpServlet
HttpServlet依然是一個(gè)抽象類,提供了一個(gè)模版。我們可以看到其內(nèi)部重載定義了2個(gè)Service方法,我們先看最基礎(chǔ)的以ServletRequest為參數(shù)的service()方法。
public void service(ServletRequest req, ServletResponse res)
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
HttpServletRequest request;
HttpServletResponse response;
try {
request = (HttpServletRequest)req;
response = (HttpServletResponse)res;
} catch (ClassCastException var6) {
throw new ServletException(lStrings.getString("http.non_http"));
}
this.service(request, response);
}
邏輯很簡(jiǎn)單,就是把請(qǐng)求和響應(yīng)都強(qiáng)制類型轉(zhuǎn)換成HttpServletRequest和HttpServletResponse類型,再調(diào)用protected void service(HttpServletRequest req, HttpServletResponse resp)方法。若強(qiáng)制類型轉(zhuǎn)換失敗則說(shuō)明該請(qǐng)求不是一個(gè)http請(qǐng)求,直接拋出異常即可。
protected void service(HttpServletRequest req, HttpServletResponse resp)
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String method = req.getMethod();
long lastModified;
if (method.equals("GET")) {
lastModified = this.getLastModified(req);
if (lastModified == -1L) {
this.doGet(req, resp);
} else {
long ifModifiedSince;
try {
ifModifiedSince = req.getDateHeader("If-Modified-Since");
} catch (IllegalArgumentException var9) {
ifModifiedSince = -1L;
}
if (ifModifiedSince < lastModified / 1000L * 1000L) {
this.maybeSetLastModified(resp, lastModified);
this.doGet(req, resp);
} else {
resp.setStatus(304);
}
}
} else if (method.equals("HEAD")) {
lastModified = this.getLastModified(req);
this.maybeSetLastModified(resp, lastModified);
this.doHead(req, resp);
} else if (method.equals("POST")) {
this.doPost(req, resp);
} else if (method.equals("PUT")) {
this.doPut(req, resp);
} else if (method.equals("DELETE")) {
this.doDelete(req, resp);
} else if (method.equals("OPTIONS")) {
this.doOptions(req, resp);
} else if (method.equals("TRACE")) {
this.doTrace(req, resp);
} else {
String errMsg = lStrings.getString("http.method_not_implemented");
Object[] errArgs = new Object[]{method};
errMsg = MessageFormat.format(errMsg, errArgs);
resp.sendError(501, errMsg);
}
}
這個(gè)service()方法很長(zhǎng),但是邏輯很簡(jiǎn)單,就是根據(jù)http請(qǐng)求中的方法去調(diào)用對(duì)應(yīng)該方法的處理邏輯,比如http請(qǐng)求是GET方法就調(diào)用doGet(),若是POST就調(diào)用doPost()。再讓我們看看假設(shè)http請(qǐng)求是GET方法,在doGet()中會(huì)發(fā)生什么。
-protected void doGet(HttpServletRequest req, HttpServletResponse resp)
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String protocol = req.getProtocol();
String msg = lStrings.getString("http.method_get_not_supported");
if (protocol.endsWith("1.1")) {
resp.sendError(405, msg);
} else {
resp.sendError(400, msg);
}
}
它直接告訴我們這個(gè)方法還未實(shí)現(xiàn),然后返回錯(cuò)誤參數(shù)。所以doGet()、doPost()...等方法才是我們真正要實(shí)現(xiàn)的方法,來(lái)完成對(duì)應(yīng)請(qǐng)求的處理邏輯。(init()和destory()也需要們實(shí)現(xiàn))
配置和使用Servlet
下面利用利用實(shí)現(xiàn)一個(gè)簡(jiǎn)單的web項(xiàng)目,來(lái)實(shí)踐Servlet,首先用IDEA創(chuàng)建一個(gè)默認(rèn)的web項(xiàng)目
-
項(xiàng)目結(jié)構(gòu)
項(xiàng)目結(jié)構(gòu) 在
src下編寫一個(gè)Servlet實(shí)例HelloServlet
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("HelloServlet中doGet()方法被調(diào)用了!");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doPost(req, resp);
}
}
- 在
index.jsp里設(shè)置一個(gè)超鏈接來(lái)指向這個(gè)Servlet
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>$Title$</title>
</head>
<body>
<a href="HelloServlet">點(diǎn)擊調(diào)用HelloServlet</a><br/>
</body>
</html>
讓我們來(lái)運(yùn)行這個(gè)項(xiàng)目, 然后點(diǎn)擊這個(gè)超鏈接,但卻給我們報(bào)了一個(gè)404錯(cuò)誤。我們之前說(shuō)了tomcat內(nèi)部有一個(gè)Servlet容器。就像配置Spring IOC容器一樣,我們需要通過(guò)配置的方式告訴tomcat我們這個(gè)web應(yīng)用里有哪些Servlet,還要告訴tomcat當(dāng)訪問(wèn)哪些路徑時(shí),調(diào)用這個(gè)Servlet實(shí)例。配置以上信息的方式有2種,web.xml和注解的方式,我們先嘗試使用xml的方式。
- 修改
web.xml配置servlet
<servlet>
<servlet-name>hello</servlet-name>
<servlet-class>HelloServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>hello</servlet-name>
<url-pattern>/HelloServlet</url-pattern>
</servlet-mapping>
這里有2部分組成, <servlet></servlet>表示注冊(cè)一個(gè)Servlet實(shí)例到tomcat的 servlet容器中,類的名字叫HelloServlet,并給他取了一個(gè)別名叫hello。第二部分 <servlet-mapping></servlet-mapping>則說(shuō)明了一個(gè)url和對(duì)應(yīng)servlet實(shí)例的映射關(guān)系。表示當(dāng)訪問(wèn)localhost:8080/webstudy/HelloServlet路徑時(shí),會(huì)去調(diào)用一個(gè)別名為hello的Servlet實(shí)例對(duì)象的相應(yīng)方法。配置好后讓我們實(shí)驗(yàn)一下。
Connected to server
[2020-04-16 09:47:09,326] Artifact webstudy:war exploded: Artifact is being deployed, please wait...
[2020-04-16 09:47:09,601] Artifact webstudy:war exploded: Artifact is deployed successfully
[2020-04-16 09:47:09,601] Artifact webstudy:war exploded: Deploy took 275 milliseconds
HelloServlet中doGet()方法被調(diào)用了!
因?yàn)槌溄邮且粋€(gè)GET方法,所以這里HelloServlet的deGet()方法被調(diào)用了,配置生效。
- 利用注解的方式配置
Servlet
Servlet3.0之后開始支持注解的的方式配置Servlet,編寫一個(gè)WelcomeServlet類,并用注解的方式把它配置到tomcat中去。
@WebServlet("/WelcomeServlet")
public class WelcomeServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("WelcomeServlet的doGet()方法被調(diào)用了");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doPost(req, resp);
}
}
@WebServlet("/WelcomeServlet")表示當(dāng)訪問(wèn)localhost:8080/webstudy/WelcomeServlet時(shí),會(huì)調(diào)用這個(gè)對(duì)應(yīng)的Servlet中的對(duì)應(yīng)方法。讓我們添加一個(gè)鏈接到index.jsp中去來(lái)測(cè)試該配置。
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>$Title$</title>
</head>
<body>
<a href="HelloServlet">點(diǎn)擊調(diào)用HelloServlet</a><br/>
<a href="WelcomeServlet">點(diǎn)擊調(diào)用WelcomeServlet</a><br/>
</body>
</html>
運(yùn)行,并點(diǎn)擊點(diǎn)擊調(diào)用WelcomeServlet這個(gè)鏈接,后臺(tái)打印信息如下。
WelcomeServlet的doGet()方法被調(diào)用了
說(shuō)明該配置方法也是生效的。
Servlet生命周期
我們之前提到過(guò)Servlet提供了3個(gè)主要方法,HttpServlet重點(diǎn)重寫了service()方法,而init()和destroy()方法并沒(méi)有重寫,GenericHttp中也只是對(duì)這2個(gè)方法給出了空實(shí)現(xiàn)。我們知道,service()方法會(huì)在每個(gè)請(qǐng)求到來(lái)的時(shí)候被調(diào)用,來(lái)處理請(qǐng)求。那么這init()和destroy()個(gè)方法會(huì)在什么時(shí)候調(diào)用呢,讓我們來(lái)實(shí)驗(yàn)一下。
- 修改
HelloServlet類
public class HelloServlet extends HttpServlet {
@Override
public void init() throws ServletException {
System.out.println("HelloServlet的init()方法被調(diào)用了");
}
@Override
public void destroy() {
System.out.println("HelloServlet的destory()方法被調(diào)用了");
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("HelloServlet中doGet()方法被調(diào)用了!");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doPost(req, resp);
}
}
部署運(yùn)行該項(xiàng)目, 觀察控制臺(tái)的輸出
Connected to server
[2020-04-16 11:22:40,748] Artifact webstudy:war exploded: Artifact is being deployed, please wait...
[2020-04-16 11:22:40,973] Artifact webstudy:war exploded: Artifact is deployed successfully
[2020-04-16 11:22:40,973] Artifact webstudy:war exploded: Deploy took 225 milliseconds
16-Apr-2020 23:22:50.448 信息 [Catalina-utility-1] org.apache.catalina.startup.HostConfig.deployDirectory 把web 應(yīng)用程序部署到目錄 [/Users/LENN/tomcat/apache-tomcat-9.0.34/webapps/manager]
16-Apr-2020 23:22:50.478 信息 [Catalina-utility-1] org.apache.catalina.startup.HostConfig.deployDirectory Deployment of web application directory [/Users/LENN/tomcat/apache-tomcat-9.0.34/webapps/manager] has finished in [30] ms
HelloServlet的init()方法被調(diào)用了
HelloServlet中doGet()方法被調(diào)用了!
我們發(fā)現(xiàn)當(dāng)tomcat啟動(dòng)的時(shí)候,init()方法并沒(méi)有被調(diào)用,也就說(shuō)HelloServlet并沒(méi)有在tomcat的servlet容器中生成一個(gè)實(shí)例對(duì)象,而是直到第一次調(diào)用HelloServlet中的doGet()方法時(shí),這個(gè)Servlet才被實(shí)例化并加入tomcat容器中去,此時(shí)init()方法才被調(diào)用。是一種懶加載的思想。那么有沒(méi)有什么辦法,讓tomcat能夠在啟動(dòng)的時(shí)候就把這些Servlet實(shí)例化然后加載到容器中去呢?我們可以在web.xml添加如下標(biāo)簽。
- 修改
web.xml讓HelloServlet在tomcat啟動(dòng)時(shí)就加載到容器中去
<servlet>
<servlet-name>hello</servlet-name>
<servlet-class>HelloServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>hello</servlet-name>
<url-pattern>/HelloServlet</url-pattern>
</servlet-mapping>
<load-on-startup>1</load-on-startup>表示在tomcat啟動(dòng)時(shí)就實(shí)例化并加載到tomcat的servlet容器里,中間的數(shù)字可以指明多個(gè)Servlet的加載順序,現(xiàn)在讓我們重新啟動(dòng)tomcat。
- 啟動(dòng)時(shí)
init()就被調(diào)用
Connected to server
[2020-04-16 11:38:31,671] Artifact webstudy:war exploded: Artifact is being deployed, please wait...
HelloServlet的init()方法被調(diào)用了
[2020-04-16 11:38:31,913] Artifact webstudy:war exploded: Artifact is deployed successfully
[2020-04-16 11:38:31,914] Artifact webstudy:war exploded: Deploy took 243 milliseconds
ServletConfig類
我們一直重寫的init()方法都是無(wú)參數(shù)的,和Servlet接口中定義的同名方法并不一樣,我們來(lái)看看Servlet接口中是如何定義的。
-
Servlet接口中定義的init()方法
void init(ServletConfig var1) throws ServletException;
我們可以看到這里有一個(gè)ServletConfig類型的形參。我們一直重寫的無(wú)參init()方法實(shí)際上由GenericServlet提供。
-
GenericServlet中的init()
public void init(ServletConfig config) throws ServletException {
this.config = config;
this.init();
}
public void init() throws ServletException {
}
GenericServlet自動(dòng)的幫我們把傳入的ServletConfig保存進(jìn)一個(gè)類變量里,然后再調(diào)用我們重寫的無(wú)參init()的方法。顯然這個(gè)ServletConfig類型的變量是由tomcat生成并傳入進(jìn)來(lái)的,如同它的名字一樣,代表了一個(gè)Servlet的配置。那么如何使用這個(gè)類呢。再看看GenericServlet中的其他方法。
GenericServlet
public ServletConfig getServletConfig() {
return this.config;
}
public String getInitParameter(String name) {
return this.getServletConfig().getInitParameter(name);
}
發(fā)現(xiàn)我們可以利用getInitParameter(String name)來(lái)獲得一個(gè)Servlet的初始參數(shù)配置,而這些配置則是以鍵值對(duì)的形式存在的,我們可以在web.xml定義這些和Servlet配置有關(guān)的鍵值對(duì)。
- 再
web.xml定義InitParameter
<servlet>
<servlet-name>hello</servlet-name>
<servlet-class>HelloServlet</servlet-class>
<init-param>
<param-name>servletParam</param-name>
<param-value>servletValue</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
在些init-param的作用范圍是一個(gè)Servlet內(nèi)部,調(diào)用這個(gè)Servlet的所有請(qǐng)求都可以拿到這些init-param,讓我們做個(gè)實(shí)驗(yàn)。
- 修改
HelloServlet獲取init-param
public class HelloServlet extends HttpServlet {
@Override
public void init() throws ServletException {
System.out.println("HelloServlet的init()方法被調(diào)用了");
System.out.println(super.getInitParameter("servletParam"));
}
}
運(yùn)行tomcat可以在控制臺(tái)看到如下信息
Connected to server
[2020-04-16 11:46:10,610] Artifact webstudy:war exploded: Artifact is being deployed, please wait...
HelloServlet的init()方法被調(diào)用了
servletValue
我們成功獲得了鍵servletParam對(duì)應(yīng)的值servletValue。
除了在web.xml配置一個(gè)Servlet的init-param,我們還可以用注解的方式定義。
- 利用注解定義
init-param
@WebServlet(value = "/WelcomeServlet", initParams = {@WebInitParam(name = "servletParam", value = "servletValue")})
public class WelcomeServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("WelcomeServlet的doGet()方法被調(diào)用了");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doPost(req, resp);
}
}
ServletContext
ServletConfig代表了一個(gè)Servlet的配置信息,其中主要包含了很多初始化參數(shù)。ServletContext與ServletConfig相似,只不過(guò)它的作用范圍是整個(gè)tomcat的Servlet容器,也就是說(shuō)所有的Servlet實(shí)例都可以訪問(wèn)到ServletConfig中的信息。GenericServlet提供了一個(gè)方法可以讓我們拿到這個(gè)類的實(shí)例
public ServletContext getServletContext() {
return this.getServletConfig().getServletContext();
}
可以看到ServletContext由tomcat包裝進(jìn)每一個(gè)Servlet的ServletConfig中,再在初始化的時(shí)候傳入,從而可以共享這些信息。我們可以在web.xml中定義這些信息。
- 定義
context-param
<context-param>
<param-name>globalParam</param-name>
<param-value>globalValue</param-value>
</context-param>
- 獲取這些
context-param
public class HelloServlet extends HttpServlet {
@Override
public void init() throws ServletException {
System.out.println("HelloServlet的init()方法被調(diào)用了");
System.out.println(super.getInitParameter("servletParam"));
ServletContext servletContext = super.getServletContext();
System.out.println(servletContext.getInitParameter("globalParam"));
}
}
啟動(dòng)tomcat,觀察控制臺(tái)
Connected to server
[2020-04-17 02:21:40,825] Artifact webstudy:war exploded: Artifact is being deployed, please wait...
HelloServlet的init()方法被調(diào)用了
servletValue
globalValue
成功獲得context-param

