学习本章节除了需要完成Readme中要求的前置基础外,还需要完成Tomcat 基础笔记的学习。
在Web后端中,资源可以分为静态资源和动态资源两种。
静态资源是指在服务端响应请求时不需要使用代码去动态生成的资源,例如 .html
文件和 .css
文件等。
动态资源是指在服务端响应请求时需要使用代码去动态生成的资源。
Servlet(Server applet)是一个技术标准,由Sun公司定义的一套动态资源规范,用于在处理客户端请求时协同调度和响应数据。是Web应用中的控制器。
从代码上来讲,Servlet是一套接口,其必须运行于特定的容器中(通常是Tomcat),不能独立运行。
Servlet容器(通常为Tomcat)在接收到http请求后,其会使用如下的流程将请求转化为Servlet所规定的对象,交由实现了Servlet Service的APP完成请求内容的生成。
其工作内容主要如下:
HttpServletRequest
对象,该对象中包含了http请求中的所有信息(例如http请求头和请求体)。HttpServletRequest
对象的同时会创建一个 HttpServletResponse
对象,用于承装需要响应给客户端的信息,该对象会被转换为http的响应报文。该对象包含http响应行、响应头和响应体。HttpServletRequest
和 HttpServletResponse
对象引用传递给service方法。如上一章节所述,Servlet是一套接口,因此需要定义一个class来实现这套接口。该接口要求实现 service
方法,原型为:
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
service
方法主要需要实现的工作内容为:
0. 创建一个类,实现 Servlet
接口或者继承 HttpServlet
或完成其他实现接口的方式。
request
中获取http请求的所有参数及信息。response
对象中。package indi.h13.servlet;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
public class UserServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String username = req.getParameter("username");
if("root".equals(username)) {
resp.getWriter().println("username is root");
} else {
resp.getWriter().println("username is not root");
}
}
}
在完成了Servlet接口的实现后,需要让Tomcat将对应请求转发到对应的Servlet实例中。
建立映射有如下两种方式,通常采用第二种注解的方式。
随后需要在 WEB-INF/web.xml
中映射Servlet的请求路径,其需要在 web-app
块下添加如下代码:
<!--
1. 配置Servlet类,其配置项及其含义为:
servlet-name: 用于关联请求的映射路径
servlet-class: 完成该请求所需要实例化的Servlet类
-->
<servlet>
<servlet-name>userServlet</servlet-name>
<servlet-class>indi.h13.servlet.UserServlet</servlet-class>
</servlet>
<!--
2. 配置和完成请求路径和servlet-name之间的映射。
-->
<servlet-mapping>
<servlet-name>userServlet</servlet-name>
<url-pattern>/isRoot</url-pattern>
</servlet-mapping>
使用注解方式进行映射配置要求:
@WebServlet("/isRoot")
即:
@WebServlet("/isRoot")
public class UserServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// ...
}
}
注意点:
/
,不然启动会报错value
或 urlPatterns
(这两个参数名互为别名,等价),即 @WebServlet(value="/isRoot")
与上述等价。@WebServlet(value={ "/isRoot", "isRootV1", "isRootV2" })
(大部分情况并无此需求)在启动Tomcat App之后,使用Postman发送如下请求,即可验证上述实现成功运行。
Servlet APP的开发依赖于jar库,具体的库为 servlet-api.jar
,存放于Tomcat的 lib\
下。
在使用IDEA开发时,引入Tomcat Server时就已经引入对应的jar包。而手动引入jar包的方式和普通开发一致。
而在项目依赖中可以可看到Tomcat的作用域为 provided
,其含义为编译时不携带该依赖(部署环境已提供)。
HTTP Header中常见的属性有:
Content-Length
获取资源大小Content-Type
获取资源类型,应当为MIME格式Last-Modified
获取资源最后修改时间而在Servlet中可以使用如下的方法设置HTTP Header:
responce.setHeader("key", "value");
例如:
responce.setHeader("Content-Type", "image/jpeg");
而在基于Tomcat的SpringBoot中也是这样配置的。
而在上述代码中,Tomcat完成了如下工作:
url-pattern
找到了响应该url的 servlet-name
servlet-name
找到了响应该请求需要实例化的类( servlet-class
)而需要拓展的用法有:
servlet-mapping
中可以绑定多个 url-pattern
,从而响应多个url请求,例如:<!--
1. 配置Servlet类,其配置项及其含义为:
servlet-name: 用于关联请求的映射路径
servlet-class: 完成该请求所需要实例化的Servlet类
-->
<servlet>
<servlet-name>userServlet</servlet-name>
<servlet-class>indi.h13.servlet.UserServlet</servlet-class>
</servlet>
<!--
2. 配置和完成请求路径和servlet-name之间的映射。
-->
<servlet-mapping>
<servlet-name>userServlet</servlet-name>
<url-pattern>/isRoot</url-pattern>
<url-pattern>/checkUserName</url-pattern>
</servlet-mapping>
servlet
标签可以对应多个 servlet-mapping
,从而响应多个url请求,例如:<!--
1. 配置Servlet类,其配置项及其含义为:
servlet-name: 用于关联请求的映射路径
servlet-class: 完成该请求所需要实例化的Servlet类
-->
<servlet>
<servlet-name>userServlet</servlet-name>
<servlet-class>indi.h13.servlet.UserServlet</servlet-class>
</servlet>
<!--
2. 配置和完成请求路径和servlet-name之间的映射。
-->
<servlet-mapping>
<servlet-name>userServlet</servlet-name>
<url-pattern>/isRoot</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>userServlet</servlet-name>
<url-pattern>/checkUserName</url-pattern>
</servlet-mapping>
不过一般还是用第一种方法。
综上,基本规则如下:
servlet-name
可以对应多个 url-pattern
,反之不可。servlet
标签可以对应多个 servlet-mapping
,反之不可。模糊匹配主要有如下两种规则:
/
:匹配全部,但是不包含jsp文件/*
:匹配全部,包含jsp文件例如:
<servlet>
<servlet-name>userServlet</servlet-name>
<servlet-class>indi.h13.servlet.UserServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>userServlet</servlet-name>
<url-pattern>/isRoot/</url-pattern>
</servlet-mapping>
则会匹配 /isRoot/
下除了 *.jsp
的所有路径,即:
localhost/isRoot/aaa
-> 可被匹配localhost/isRoot/aaa.jsp
-> 不可匹配而:
<servlet>
<servlet-name>userServlet</servlet-name>
<servlet-class>indi.h13.servlet.UserServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>userServlet</servlet-name>
<url-pattern>/isRoot/*</url-pattern>
</servlet-mapping>
则会同时匹配 localhost/isRoot/aaa
和 localhost/isRoot/aaa.jsp
。
同样的,模糊匹配也可以用于模糊匹配固定的后缀路径:
<servlet>
<servlet-name>userServlet</servlet-name>
<servlet-class>indi.h13.servlet.UserServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>userServlet</servlet-name>
<url-pattern>*.txt</url-pattern>
</servlet-mapping>
则会把所有后缀为 .txt
的请求均映射到对应接口。但是需要注意 *.txt
前面不可加 /
,因为 /*
会导致歧义。
Servlet的生命大致有如下若干阶段:
Servlet.init
方法。Servlet.service
方法。Servlet.destory
方法。@WebServlet("/ServletLifeCycleTest")
public class ServletLifeCycle extends HttpServlet {
/**
* @brief 重写实例化方法
*/
public ServletLifeCycle() {
System.out.println("ServletLifeCycle obj created.");
}
/**
* @brief 重写初始化方法
* @throws ServletException
* @note 注意需要重写的是无参数的 `init` 方法
*/
@Override
public void init() throws ServletException {
super.init();
System.out.println("ServletLifeCycle obj inited.");
}
/**
* @brief 重写service方法
* @param req
* @param resp
* @throws ServletException
* @throws IOException
*/ @Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("ServletLifeCycle.service() has been executed.");
}
/**
* @brief 重写销毁方法
*/
@Override
public void destroy() {
super.destroy();
System.out.println("ServletLifeCycle obj has been destroyed.");
}
}
编译并执行,会发现在默认情况下:
service
方法会在每一次被请求时调用。destory
方法仅会在Tomcat退出时调用。同时,Servlet为了能够同时响应和服务多个客户端,Servlet将被存放于堆中,而非各个线程的栈中。而 service
方法会在每个线程的栈中执行。
因此类的成员变量会被多个线程和客户共享,而 service
方法中的变量则只会被每个线程或客户独占。因此Servlet的类内对象应当注意并发控制,同时强烈不建议使用可写变量(即仅使用只读变量)。
在上一章节中提到,Servlet在默认情况下会在该服务第一次被请求时实例化。而若需要在Tomcat启动时就实例化则需要按照如下方式配置:
web.xml
配置方式:在 servlet
块中添加 load-on-startup
属性,并将该属性配置为一个正整数即可(默认值为 -1
,意味着延迟加载)。
<servlet>
<servlet-name>userServlet</servlet-name>
<servlet-class>indi.h13.servlet.UserServlet</servlet-class>
<!-- 配置启动时实例化,1表示第一个被实例化 -->
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>userServlet</servlet-name>
<url-pattern>/isRoot/</url-pattern>
</servlet-mapping>
在 @WebServlet()
中添加 loadOnStartup
属性,即:
// 基础配置
@WebServlet(loadOnStartup = 1);
// 多属性配置
@WebServlet(value="/isRoot", loadOnStartup = 1);
关于 load-on-startup
的取值:
load-on-startup
的默认值为 -1
,意味着延迟加载。即第一次被调用时加载。查看Tomcat根目录下的 \conf\web.xml
,可以看到一个名为default的servlet的相关配置:
<servlet>
<servlet-name>default</servlet-name>
<servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
<init-param>
<param-name>debug</param-name>
<param-value>0</param-value>
</init-param>
<init-param>
<param-name>listings</param-name>
<param-value>false</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<!-- The mapping for the default servlet -->
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
其 url-pattern
可以匹配所有非 *.jsp
路径。
当请求发生时,其工作逻辑为:
servlet-mapping
进行匹配并转发。DefaultServlet
会去查找该Servlet App下的静态资源路径并转发。需要注意的是,当后续使用 SpringMVC
进行开发时,该 DefaultServlet
不再生效。如果有需求需要重新配置并使能 DefaultServlet
(往往会在非前后端分离的项目中出现该需求)。
Tomcat的Servlet提供的若干开发接口有如下的继承结构:
HttpServlet
拓展了 GenericServlet
:GenericServlet
实现了 Servlet
、 ServletConfig
、 Serializable
等接口。Servlet
接口定义了如下的方法:void init(ServletConfig var1) throws ServletException;
ServletConfig
对象。详见Servlet 简介 > 11 ServletConfigServletConfig getServletConfig();
ServletConfig
对象的方法。void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;
String getServletInfo();
void destroy();
Servlet
对象被回收前,由Tomcat调用。用于做资源的释放工作。ServletConfig
接口定义了如下的方法:String getServletName();
String getServletName();
String getInitParameter(String var1);
Enumeration<String> getInitParameterNames();
Serializable
接口为序列化接口,其没有任何方法或者字段,只是用于标识可序列化的语义。序列化是将对象状态转换为可保持或传输的格式的过程。与序列化相对的是反序列化,它将流转换为对象。这两个过程结合起来,可以轻松地存储和传输数据。HttpServlet
,也可以选择直接实现 Servlet
接口。不过当选择后者时,需要手动实现 Servlet
接口中的每一个方法1。所以通常选择继承 HttpServlet
或 GenericServlet
。其继承关系图解如下图所示:
如上一章节所述,抽象类 GenericServlet
实现了 Servlet
、 ServletConfig
、 Serializable
等接口的大部分方法(除了 service
方法)。本类侧重于除了Service方法以外的方法的处理。
该抽象类的参考代码如下:
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) {
ServletContext var10000 = this.getServletContext();
String var10001 = this.getServletName();
var10000.log(var10001 + ": " + 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();
}
}
在上述代码中:
public void destroy()
public void init()
init
实现:public void init(ServletConfig config) throws ServletException {
this.config = config;
this.init();
}
public void init() throws ServletException {
}
而含参的 public void init(ServletConfig config)
的本质就是转存参数,然后调用无参版本初始化(即 public void init()
)。
需要注意的是,当开发者通过GenericServlet实现Servlet时,应当优先考虑重写无参版本的初始化方法。因为:
0. 含参版本的初始化通常由Tomcat调用,在Tomcat初始化该类时会将配置文件中设置的参数传递进去。
GenericServlet
实现的含参初始化已经完成参数拷贝的工作(存入 this.config
),随后并调用无参的初始化。this.config
的工作,而后续或从前的版本是否仍使用的是 this.config
这个变量并无强制性规定,可能随版本而发生改变。此外, GenericServlet
仍未具体实现 service
方法,因此该类本质也是一个抽象类。
虽然在 GenericServlet
中只剩 service
方法就已经完全实现 Servlet
接口中所有的方法,但是 HttpServlet
依旧是一个抽象类,在该类中侧重于 service
方法的处理。
查看源码可以看到 HttpServlet
中共计实现了如下两个名为 service
的方法:
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);
}
}
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);
}
需要注意的是:
public void service(ServletRequest req, ServletResponse res)
方法。ServletRequest
和 ServletResponse
分别强制转换为 HttpServletRequest
和 HttpServletResponse
类型(因为前者是后者的父类)。protected void service(HttpServletRequest req, HttpServletResponse resp)
方法。protected void service(HttpServletRequest req, HttpServletResponse resp)
方法:
因此若需要基于 HttpServlet
进行开发时,通常有两种选择:
service
方法doGet
、 doPost
...方法。在使用 web.xml
配置初始化参数时,应当将参数填写到 servlet
块中,示例如下:
<!--
1. 配置Servlet类,其配置项及其含义为:
servlet-name: 用于关联请求的映射路径
servlet-class: 完成该请求所需要实例化的Servlet类
-->
<servlet>
<servlet-name>userServlet</servlet-name>
<servlet-class>indi.h13.servlet.UserServlet</servlet-class>
<!-- 设置Servlet的初始化参数 -->
<init-param>
<param-name>key</param-name>
<param-value>value</param-value>
</init-param>
</servlet>
<!--
2. 配置和完成请求路径和servlet-name之间的映射。
-->
<servlet-mapping>
<servlet-name>userServlet</servlet-name>
<url-pattern>/isRoot</url-pattern>
</servlet-mapping>
随后上述配置参数会被Tomcat转换成一个 ServletConfig
对象,该对象会被传递给 Servlet
接口所规定的 void init(ServletConfig var1) throws ServletException;
方法。若程序的Servlet继承自 GenericServlet
对象,则在运行时可以使用 getServletConfig
方法获取配置参数。
使用注解方式配置Servlet初始化参数可以直接在Servlet实现类前使用如下方法:
// 基础配置
@WebServlet(
initParams = {@WebInitParam(name = "key", value = "value")}
);
至于同时在一个 WebServlet
注解中同时配置多个配置项的方法可以参照前文的章节。
当使用上述配置时,在程序中使用如下方法即可获得初始化参数:
String value = getServletConfig().getInitParameter("key");
即可获得初始化参数。
此外, ServletConfig
对象还有获取所有已配置的配置项名称的方法:
// 获取配置项
ServletConfig servletConfig = getServletConfig();
// 获取参数名的迭代器(枚举)
Enumeration<String> initParameterNames = servletConfig.getInitParameterNames();
// 迭代遍历配置项名称
while(initParameterNames.hasMoreElements()) {
String keyName = initParameterNames.nextElements();
String value = servletConfig.getInitParameter("key");
}
ServletContext基础知识:
ServletContext存储于 web.xml
中,无论该App是否有Servlet,该对象均会被生成。该对象使用 <context-param>
块,该块直接存储于 <web-app>
块下,与 <servlet>
块平级:
<?xml version="1.0" encoding="UTF-8"?>
<web-app ...>
<!-- 配置ServletContext的参数 -->
<context-param>
<param-name>username</param-name>
<param-value>root</param-value>
</context-param>
</web-app>
当在Java中使用该对象时应使用如下代码:
// 方法1:在Servlet类中直接获取ServletContext对象
ServletContext servletContext = getServletContext();
// 方法2:通过ServletConfig获取ServletContext对象
ServletContext servletContext = getServletConfig.getServletContext();
// 方法3:通过HttpServletRequest获取ServletContext对象
ServletContext servletContext = req.getServletContext();
// 获取InitParameter
String value = servletContext.getInitParameter("key");