Servlet生命週期以及工作原理
最近感覺到用久了SpringMVC、Struts2等框架,反而對它們的底層實現,即Servlet,的相關知識有了許多遺忘。現在參考了網上的一些部落格,來進行一次知識點總結。
Servlet響應客戶端請求的過程
Servlet生命週期
- init方法:當Servlet容器第一次載入並建立Servlet例項時,在呼叫該Servlet的建構函式後立即呼叫init方法對該Servlet物件進行初始化。構造器和init方法都只會被呼叫一次,這說明Servlet是單例項的(需要考慮執行緒安全的問題,不推薦在其中寫全域性變數)。
- service方法:每次請求都會呼叫service方法,它是實際用於響應請求的方法,可以被多次呼叫。
- destroy方法:在伺服器端停止且解除安裝Servlet時執行該方法,用於釋放當前Servlet所佔用的資源,只會被呼叫一次。
以上方法都由Servlet容器(例如Tomcat)負責呼叫。
ServletConfig
Servlet介面的init方法會接收一個ServletConfig物件作為引數,即init(ServletConfig servletConfig),ServletConfig物件封裝了該Serlvet的配置資訊,並且可以獲取ServletContext物件。例如:
在web.xml中配置Servlet的初始化引數:
<servlet >
<!-- Servlet註冊的名字 -->
<servlet-name>helloServlet</servlet-name>
<!-- Servlet全類名 -->
<servlet-class>servlet.HelloServlet</servlet-class>
<!-- 配置該Servlet的初始化引數 -->
<init-param>
<param-name >username</param-name>
<param-value>root</param-value>
</init-param>
<init-param>
<param-name>password</param-name>
<param-value>123456</param-value>
</init-param>
</servlet>
可以在HelloServlet的init方法中獲取這些引數的資訊(在實際開發中,通常使用的Servlet都繼承了HttpServlet類,可以通過getServletConfig()在該Servlet物件中獲取到ServletConfig物件,不一定只能在init方法中使用,下面的getServletContext()也是一樣):
@Override
public void init(ServletConfig servletConfig) throws ServletException {
//獲取Servlet初始化引數相關的資訊
String username = servletConfig.getInitParameter("username");
System.out.println("username is "+username);
Enumeration<String> names = servletConfig.getInitParameterNames();
while(names.hasMoreElements()){
String name = names.nextElement();
System.out.println("name is "+name);
System.out.println("value is "+servletConfig.getInitParameter(name));
}
當有請求傳送到該Servlet後可以在控制檯看到輸出:
ServletContext
ServletContext物件代表著當前的WEB應用。可以認為SerlvetContext是當前 WEB應用的一個大管家,可以從中獲取到當前WEB應用的各個方面的資訊。
ServletContext物件可以由SerlvetConfig獲取:
ServletContext servletContext = servletConfig.getServletContext();
ServletContext主要的作用有:
① 獲取當前WEB應用的初始化引數:
在web.xml中配置當前WEB應用的初始化引數:(這些引數可以被所有的 Servlet通過ServletContext獲取,而上面配置在Servlet中的引數只能由對應的Servlet獲取)
<context-param>
<param-name>driver</param-name>
<param-value>com.mysql.jdbc.Driver</param-value>
</context-param>
<context-param>
<param-name>jdbcUrl</param-name>
<param-value>jdbc:mysql:///xiangwanpeng</param-value>
</context-param>
在init方法中獲取到這些引數:
@Override
public void init(ServletConfig servletConfig) throws ServletException {
//獲取ServletContext物件,它包含了當前WEB應用的各種資訊
ServletContext servletContext = servletConfig.getServletContext();
//獲取WEB應用的初始化資訊,方法和servletConfig類似
String driver = servletContext.getInitParameter("driver");
System.out.println("driver is "+driver);
Enumeration<String> names2 = servletContext.getInitParameterNames();
while(names2.hasMoreElements()){
String name2 = names2.nextElement();
System.out.println("name2 is "+name2);
System.out.println("value2 is "+servletContext.getInitParameter(name2));
}
}
當有請求傳送到該Servlet後可以在控制檯看到輸出:
② 獲取當前WEB應用的某一個檔案在伺服器上的絕對路徑,而不是部署前的路徑。
例如現在在根目錄下新建一個note.txt:
在Servlet中獲取:
String realPath = servletContext.getRealPath("/note.txt");
System.out.println("realPath is "+realPath);
獲取請求後在控制檯輸出:
realPath is G:\Eclipse\J2EE\workspace.metadata.plugins\org.eclipse.wst.server.core\tmp0\wtpwebapps\JavaWeb\note.txt
③ 獲取當前 WEB應用的名稱:
String contextPath = servletContext.getContextPath();
System.out.println(contextPath);
④ 獲取當前 WEB應用的某一個檔案對應的輸入流:
在src目錄下新建一個jdbc.properties:
在Servlet中獲取該檔案的輸入流:
InputStream is = servletContext.getResourceAsStream("/WEB-INF/classes/jdbc.properties");
System.out.println("is is "+is);
輸出:
注意區別另一種方法,即通過getClassLoader獲取,這種方法的路徑是沒有/WEB-INF/classes/字首的:
InputStream is2 = this.getClass().getClassLoader().getResourceAsStream("jdbc.properties");
System.out.println("is2 is "+is2);
⑤ 和attribute相關的幾個方法。
GenericServlet
GenericServlet是Servlet介面和ServletConfig介面的實現類,是一個抽象類,因為其中的service()方法為抽象方法。它的作用是:如果新建的 Servlet程式直接繼承GenericSerlvet會使開發更簡潔。
具體實現:
① 在GenericServlet中聲明瞭一個SerlvetConfig型別的成員變數,在init(ServletConfig)方法中對其進行了初始化。
② 利用servletConfig成員變數的方法實現了 ServletConfig介面的方法。
③ 還定義了一個 init()方法,在init(SerlvetConfig)方法中對其進行呼叫,子類可以直接覆蓋init()在其中實現對Servlet的初始化。
④ 不建議直接覆蓋 init(ServletConfig),因為如果忘記編寫super.init(config),而還是用了SerlvetConfig介面的方法,則會出現空指標異常。
⑤ 新建的 init(){}並非Serlvet的生命週期方法,而init(ServletConfig)是生命週期相關的方法。
以下是原始碼:
public abstract class GenericServlet
implements Servlet, ServletConfig, java.io.Serializable
{
private transient ServletConfig config;
public GenericServlet() { }
public void destroy() {
}
public String getInitParameter(String name) {
return getServletConfig().getInitParameter(name);
}
public Enumeration getInitParameterNames() {
return getServletConfig().getInitParameterNames();
}
public ServletConfig getServletConfig() {
return config;
}
public ServletContext getServletContext() {
return 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 msg) {
getServletContext().log(getServletName() + ": "+ msg);
}
public void log(String message, Throwable t) {
getServletContext().log(getServletName() + ": " + message, t);
}
public abstract void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException;
public String getServletName() {
return config.getServletName();
}
}
HttpServlet
HttpServlet是一個繼承自 GenericServlet的Servlet,而且它是針對於 HTTP協議所定製。
HttpServlet在service(ServletRequest,ServletResponse)方法中直接把ServletReuqest和 ServletResponse強制轉換為HttpServletRequest和HttpServletResponse。並呼叫了過載的service(HttpServletRequest, HttpServletResponse)方法。
在service(HttpServletRequest, HttpServletResponse)中,通過request.getMethod()獲取請求方式,並根據請求方式建立了doXxx()方法(xxx為具體的請求方式,比如 doGet,doPost)。
實際開發中, 我們通常直接繼承HttpServlet,這樣做的好處是可以直接有針對性的覆蓋doXxx()方法;直接使用HttpServletRequest和HttpServletResponse,而不再需要進行型別的強制轉換。
主要原始碼如下:
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);
}
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException
{
String method = req.getMethod();
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 = req.getDateHeader(HEADER_IFMODSINCE);
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);
}
}