前言
是什么?
Servlet是 Server Applet的简称,翻译过来就是服务程序,它是一种运行在服务端的,用来处理服务器接收到的请求的小程序,你也可以将它理解为一套处理网络请求的规范,在Java中被抽象封装成我们所熟知的Servlet 接口,Servlet能够将客户端请求和响应内容封装成Java对象,便于开发者读取客户端请求内容,并编辑响应内容。
为什么?
其实Servlet技术其实已经很古老了,并且已经完全成熟,基本上不会有新的变化,现在也基本上不会有直接基于Servlet开发的应用了,但是,学习了解Servlet的运行原理也是很有必要的,因为现在流行的很多框架,诸如 SpringMVC 之类的,底层都是基于Servlet规范,学习并了解Servlet,能让我们遇到疑难杂症的时候,不至于无从下手。
怎么样?
那么,Servlet我们应该学到什么程度呢?我觉得可以学习如下几个方面:
- Servlet接口的生命周期;
- GenericServlet和HttpServlet;
- ServletRequest和ServletResponse;
- HttpServletRequest;
- HttpServletResponse;
- ServletConfig和@WebServlet;
正文
在开始正文前,我们先在本地构建一个简单的Servlet程序,方便我们通过打断点的形式直观观测Servlet的运行过程。我看过几本讲Servlet的书籍,不过基本上要么是结合JSP来讲,要么就独立式Tomcat来讲,对于类似我这种一开始就学习的Spring Boot的开发者而言,真的很难理解,我也不想回过头去再看那些已经被淘汰的技术了,因此,我决定探索一条基于SpringBoot+嵌入式Tomcat+自定义Servlet的实验方案,来探索Servlet的源码奥秘,废话不多数,开始coding!
首先,在idea里初始化一个maven项目,pom.xml中引入的主要依赖如下:
<dependencies>
<dependency>
<!-- 嵌入式tomcat-->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<version>2.7.5</version>
</dependency>
<dependency>
<!-- SpringWeb核心依赖-->
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>5.3.23</version>
</dependency>
<dependency>
<!-- SpringBoot核心依赖-->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<version>2.7.5</version>
</dependency>
</dependencies>
然后自定义一个Servlet实现,我这里叫做 MyServlet,他的业务逻辑代码就是控制台输出 hello world! ,代码如下:
package indi.demo.api;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet(urlPatterns = "demo")
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp){
System.out.printf("hello world!");
}
}
最后补上启动类,启动即可,启动类代码:
package indi.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@ServletComponentScan
public class Demo {
public static void main(String[] args) {
SpringApplication.run(Demo.class, args);
}
}
启动此Demo代码,可以在浏览器中访问地址: http://localhost:8080/demo,控制台会打印hello world!
需要注意的是,基于注解形式启动Servlet是Servlet3.0引入的特性,此版本之前无法通过注解来配置和启动Servlet,附赠一张此Demo程序的层次结构说明图:
Servlet的生命周期
Servlet的生命周期可以分为以下几个阶段:
- 类加载和实例化;
- Servlet初始化;
- 处理请求;
- Servlet销毁;
Servlet的生命周期是通过Servlet接口的各个方法来体现的,打开Servlet接口,可以看到如下代码:
public interface Servlet {
// 此方法对应Servlet的初始化阶段
public void init(ServletConfig config) throws ServletException;
// 此方法对应Servlet处理请求
public void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException;
// 此方法对应Servlet销毁阶段
public void destroy();
// 其他方法略
}
Servlet的运行过程(以我正文开始前所述的Demo为例):首先,SpringBoot启动时,会检测当前的服务类型,因为我引入了 spring-web 这个依赖,这里的服务类型就会检测为web服务,这是SpringBoot就回去加载Servlet容器,所谓Servlet容器就是Servlet运行的平台,也就是我们在pom文件里引入的嵌入式Tomcat,当嵌入式Tomcat启动的时候,会寻找合适的Servlet实例加载到内存中,并将其实例化。
那什么样的类会被Tomcat选择中呢?在我这个Demo代码,满足了两个条件:
- 我们写的这个类必须是Servlet接口的实现类;
- 启动类需要添加 @ServletComponentScan注解;
- 这个实现类需要添加 @WebServlet 注解,这个注解主要有两个作用:
- 将一个类标记为Servlet类,供Tomcat识别;
- 提供一些Servlet所需配置,比如urlPatterns参数;
GenericServlet和HttpServlet
虽然可以直接实现Servlet接口,就能构造一个简单的Servlet应用,但是我们一般不会直接使用Servlet接口的,更常用的是几次HttpServelt这个父类,来间接实现Servlet接口,那为什么要这么做呢?
Servlet接口是一个具有通用性质的接口,但是在一个服务端应用中,我们可能需要很多个这样的Servlet实现,这就意味着,如果我们直接使用Servlet接口的话,需要对Servlet的中每个方法给出具体实现,但其实,实际使用中,我们更加关注的是其中的service方法,大多数情况下,其他方法都是默认给个空的默认实现即可,所以为了降低冗余,就将除service方法以外的方法给出默认实现,只留service给开发者定制化,这个就是GenericServlet的设计目的。
GenericServlet的核心代码如下:
public abstract class GenericServlet implements Servlet, ServletConfig,
java.io.Serializable {
// 核心抽象方法留给子类实现
@Override
public abstract void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException;
// 其他方法,给出了默认实现,一般是空方法体
}
说完GenericServlet,我们再来看HttpServlet,它是前者的子类,顾名思义,他就是为了Http协议的请求定制化的Servlet实现。在实际开发中,一个请求一般会按照method(方法)的不同,而走不同的业务逻辑,这就意味着,如果仅用GenericServlet的话,我们会经常在service方法里,根据请求的method的不同,而让代码走不同的逻辑或者方法,于是,HttpServlet便应运而生,它重写了service方法,根据method的不同取值,去调用不同的protected方法,开发者根据自身需求直接复写对应方法即可,进而节省了开发者的时间,降低了代码冗余。
public abstract class HttpServlet extends GenericServlet {
// 一些HTTP的method常量
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";
// 实现GenericServlet方法
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String method = req.getMethod();
if (method.equals(METHOD_GET)) {
// 如果method=GET,调用doGet方法
long lastModified = getLastModified(req);
if (lastModified == -1) {
doGet(req, resp);
} else {
long ifModifiedSince;
try {
ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
} catch (IllegalArgumentException iae) {
ifModifiedSince = -1;
}
if (ifModifiedSince < (lastModified / 1000 * 1000)) {
maybeSetLastModified(resp, lastModified);
doGet(req, resp);
} else {
resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
}
}
} else if (method.equals(METHOD_HEAD)) {
// 如果method=HEAD,调用doHead方法
long lastModified = getLastModified(req);
maybeSetLastModified(resp, lastModified);
doHead(req, resp);
} else if (method.equals(METHOD_POST)) {
// 如果method=POST,调用doPost方法
doPost(req, resp);
} else if (method.equals(METHOD_PUT)) {
// 如果method=PUT,调用doPut
doPut(req, resp);
} else if (method.equals(METHOD_DELETE)) {
// 如果method=DELETE,调用doDelete方法
doDelete(req, resp);
} else if (method.equals(METHOD_OPTIONS)) {
// 如果method=OPTIONS,调用doOptions方法
doOptions(req,resp);
} else if (method.equals(METHOD_TRACE)) {
// 如果method=TRACE,调用doTrace方法
doTrace(req,resp);
} else {
// 其他情况,返回错误信息
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);
}
}
// 当method为GET的时候,service方法调用此 doGet方法
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException
{
// 默认情况下,这个方法会返回不支持调用的错误信息,通过复写以实现自定义输出
String msg = lStrings.getString("http.method_get_not_supported");
sendMethodNotAllowed(req, resp, msg);
}
// 此处只举例method=GET时调用doGet方法,其他方法大致同理,略。
}
ServletRequest和ServletResponse
ServletRequest和ServletResponse是Servlet接口的service方法的主要参数,它们分别封装了Http请求的请求头数据和响应头数据。
先来讲讲ServletRequest,它是对请求报文的封装,核心源码如下:
public interface ServletRequest {
// 获取指定属性
public Object getAttribute(String name);
// 获取所有属性
public Enumeration<String> getAttributeNames();
// 获取字符编码格式
public String getCharacterEncoding();
// 设置字符编码格式
public void setCharacterEncoding(String env) throws java.io.UnsupportedEncodingException;
// 获取报文内容长度,返回int结果
public int getContentLength();
// 获取报文内容长度,返回long结果
public long getContentLengthLong();
// 获取内容类型
public String getContentType();
// 获取输入流
public ServletInputStream getInputStream() throws IOException;
// 获取指定参数的第一个值
public String getParameter(String name);
// 获取参数名称枚举
public Enumeration<String> getParameterNames();
// 获取指定参数的所有的值
public String[] getParameterValues(String name);
// 获取所有参数及其参数值
public Map<String, String[]> getParameterMap();
// 获取协议
public String getProtocol();
// 获取URL的主机名
public String getServerName();
// 获取URL的主机端口
public int getServerPort();
// 获取报文Body部分的Reader
public BufferedReader getReader() throws IOException;
// 获取客户端地址,一般是IP
public String getRemoteAddr();
// 获取客户端主机名称,一般是IP
public String getRemoteHost();
// 获取客户端通信端口
public int getRemotePort();
// 获取语言环境
public Locale getLocale();
// 获取Servlet上下文
public ServletContext getServletContext();
}
补充说明:
- **getAttribute **:attribute在Request中是以Map形式存储的,getAttributeNames 方法就是获取这个Map的key的枚举,通过这个枚举值,调用 getAttribute 方法就能获取到attribute值;
- getContentLength :返回值的单位是byte,代表着Content包含的byte数量,若无Content,则返回 -1 ,如果上传的内容过大,比如文件之类的,建议使用 getContentLengthLong 方法;
- getContentType :可以获取到Http请求头的 Content-Type 属性的值;
- getParameter :parameter在Request中是以 Map<String,ArrayList> 的形式存储的,getParameterNames 方法获取Map的key组成的枚举,getParameter 方法可以根据入参匹配Map的key,如果匹配到,则取List的第一个数据返回;getParameterValues 则不是选择第一个,而是全部值,将其转换成字符串数字返回;getParameterMap 则直接返回了所有参数及其参数值;
- getProtocol :返回协议及其版本,比如 HTTP/1.1 ;
ServletResponse 是对响应报文的封装,主要代码如下:
public interface ServletResponse {
// 获取字符编码格式,默认 ISO-8859-1
public String getCharacterEncoding();
// 告诉客户端返回内容采用的字符编码格式
public void setCharacterEncoding(String charset);
// 获取内容类型,比如 text/xml
public String getContentType();
// 设置内容类型,体现在Head的Content-Type属性
public void setContentType(String type);
// 获取输出流,方便向客户端写入二进制数据
public ServletOutputStream getOutputStream() throws IOException;
// 获取打印流,方便向客户端写入文字字符
public PrintWriter getWriter() throws IOException;
// 设置响应内容的长度,体现在Head的Content-Length属性
public void setContentLength(int len);
// 功能同 setContentLength,能够表示的范围更大
public void setContentLengthLong(long length);
// 设置语言环境
public void setLocale(Locale loc);
// 获取置语言环境
public Locale getLocale();
}
HttpServletRequest
我们前面讲了SerlvetRequest之后,不知道你有没有发现,他没有任何与Http协议相关的方法,比如获取Cookie、Session、Header、请求的方法method值等等方法,因为ServletRequest抽象的是网络请求,并非特指Http请求,所以它只包含多个网络协议的公共逻辑,比如字符编码格式之类的。针对HTTP协议的请求,有另外两个类与之对应,就是HttpServletRequest和HttpServletResponse。
我们先来看看HttpServletRequest类,主要代码如下:
public interface HttpServletRequest extends ServletRequest {
// 获取所有Cookie对象
public Cookie[] getCookies();
// 在Header中,获取指定name下的一个值
public String getHeader(String name);
// 在Header中,获取指定name下的所有值
public Enumeration<String> getHeaders(String name);
// 获取Header的name枚举
public Enumeration<String> getHeaderNames();
// 在Header中,获取指定name下的所有值,并转换成int类型数据
public int getIntHeader(String name);
// 获取Http的Method
public String getMethod();
// 获取请求路径
public String getPathInfo();
// 获取查询参数
public String getQueryString();
// 获取Session
public HttpSession getSession();
// 更改sessionId并返回
public String changeSessionId();
// 获取RequestURI
public String getRequestURI();
// 获取RequestURL
public StringBuffer getRequestURL();
// 获取ContextPath
public String getContextPath();
// 获取form表单上传的文件集合,文件以Part形式存在
public Collection<Part> getParts() throws IOException,ServletException;
// 获取指定文件的Part对象
public Part getPart(String name) throws IOException,ServletException;
}
我以一个Demo代码,让大家直观看到这些参数值和输出格式:
@WebServlet(name = "demoServlet", urlPatterns = "/demo/*")
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 打印 getCookies
System.out.println("request.getCookies ->");
for (Cookie cookie : req.getCookies()) {
System.out.println(String.format("%15s : %s", cookie.getName(), cookie.getValue()));
}
// 打印 getHeader
Enumeration<String> hNames = req.getHeaderNames();
System.out.println("request.getHeader ->");
while (hNames.hasMoreElements()) {
String hName = hNames.nextElement();
System.out.println(String.format("%15s : %s", hName, req.getHeader(hName)));
}
// 打印 getMethod
System.out.println("request.getMethod -> "+req.getMethod());
// 打印 getRequestURL
System.out.println("request.getRequestURL -> "+req.getRequestURL());
// 打印 getRequestURI
System.out.println("request.getRequestURI -> "+req.getRequestURI());
// 打印 getPathInfo
System.out.println("request.getPathInfo -> "+req.getPathInfo());
// 打印 getQueryString
System.out.println("request.getQueryString -> "+req.getQueryString());
}
}
Postman请求为 http://127.0.0.1/demo/test?param1=p1_value¶m2=p2_value ,控制台输出内容如下:
request.getCookies ->
c1 : c1_value
c2 : c2_value
request.getHeader ->
user-agent : PostmanRuntime/7.29.2
accept : */*
postman-token : 1e36217c-31d9-4f79-b38a-bff91a5f85cf
host : 127.0.0.1
accept-encoding : gzip, deflate, br
connection : keep-alive
cookie : c1=c1_value; c2=c2_value
request.getMethod -> GET
request.getRequestURL -> http://127.0.0.1/demo/test
request.getRequestURI -> /demo/test
request.getPathInfo -> /test
request.getQueryString -> param1=p1_value¶m2=p2_value
HttpServletResponse
针对 HttpServletResponse,我们也是先讲源码,再举例子。
HttpServletResponse的主要源码如下:
public interface HttpServletResponse extends ServletResponse {
// 添加Cookie
public void addCookie(Cookie cookie);
// 发送错误消息,使用自定义消息
public void sendError(int sc, String msg) throws IOException;
// 发送错误消息
public void sendError(int sc) throws IOException;
// 响应重定向
public void sendRedirect(String location) throws IOException;
// 设置响应头
public void setHeader(String name, String value);
// 添加响应头
public void addHeader(String name, String value);
// 获取响应头指定name的一个值
public String getHeader(String name);
// 获取响应头指定name的所有值
public Collection<String> getHeaders(String name);
// 设置响应状态
public void setStatus(int sc);
// 获取响应状态
public int getStatus();
/*
* 以下是 HTTP Status Code,即Http状态码
*/
public static final int SC_CONTINUE = 100;
public static final int SC_SWITCHING_PROTOCOLS = 101;
public static final int SC_OK = 200;
public static final int SC_CREATED = 201;
public static final int SC_ACCEPTED = 202;
public static final int SC_NON_AUTHORITATIVE_INFORMATION = 203;
public static final int SC_NO_CONTENT = 204;
public static final int SC_RESET_CONTENT = 205;
public static final int SC_PARTIAL_CONTENT = 206;
public static final int SC_MULTIPLE_CHOICES = 300;
public static final int SC_MOVED_PERMANENTLY = 301;
public static final int SC_MOVED_TEMPORARILY = 302;
public static final int SC_FOUND = 302;
public static final int SC_SEE_OTHER = 303;
public static final int SC_NOT_MODIFIED = 304;
public static final int SC_USE_PROXY = 305;
public static final int SC_TEMPORARY_REDIRECT = 307;
public static final int SC_BAD_REQUEST = 400;
public static final int SC_UNAUTHORIZED = 401;
public static final int SC_PAYMENT_REQUIRED = 402;
public static final int SC_FORBIDDEN = 403;
public static final int SC_NOT_FOUND = 404;
public static final int SC_METHOD_NOT_ALLOWED = 405;
public static final int SC_NOT_ACCEPTABLE = 406;
public static final int SC_PROXY_AUTHENTICATION_REQUIRED = 407;
public static final int SC_REQUEST_TIMEOUT = 408;
public static final int SC_CONFLICT = 409;
public static final int SC_GONE = 410;
public static final int SC_LENGTH_REQUIRED = 411;
public static final int SC_PRECONDITION_FAILED = 412;
public static final int SC_REQUEST_ENTITY_TOO_LARGE = 413;
public static final int SC_REQUEST_URI_TOO_LONG = 414;
public static final int SC_UNSUPPORTED_MEDIA_TYPE = 415;
public static final int SC_REQUESTED_RANGE_NOT_SATISFIABLE = 416;
public static final int SC_EXPECTATION_FAILED = 417;
public static final int SC_INTERNAL_SERVER_ERROR = 500;
public static final int SC_NOT_IMPLEMENTED = 501;
public static final int SC_BAD_GATEWAY = 502;
public static final int SC_SERVICE_UNAVAILABLE = 503;
public static final int SC_GATEWAY_TIMEOUT = 504;
public static final int SC_HTTP_VERSION_NOT_SUPPORTED = 505;
}
ServletConfig与@WebServlet
ServletConfig 是Servlet的配置类,在初始化阶段,作为init方法的参数,传递进Servlet内部,ServletConfig的作用主要有:
- 获取Servlet程序的别名;
- 获取初始化参数;
- 获取ServletContext对象;
ServletConfig的主要代码如下:
public interface ServletConfig {
// 获取Servlet程序的名称
public String getServletName();
// 获取ServletContext对象
public ServletContext getServletContext();
// 获取初始化参数值
public String getInitParameter(String name);
// 获取初始化参数的参数名枚举
public Enumeration<String> getInitParameterNames();
}
在SpringBoot中,我们可以不使用web.xml形式配置Servlet了,可以直接使用@WebServlet注解里提供的属性来配置Servlet了,我们先来看看这个注解的源码:
package javax.servlet.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface WebServlet {
// Servlet名称
String name() default "";
// URL匹配模式
tring[] urlPatterns() default {};
// 同 urlPatterns
String[] value() default {};
// 启动优先级
int loadOnStartup() default -1;
// 初始化参数
WebInitParam[] initParams() default {};
// 是否支持异步
boolean asyncSupported() default false;
// 小图标
String smallIcon() default "";
// 大图标
String largeIcon() default "";
// 描述
String description() default "";
// 显示名称
String displayName() default "";
}
其中:
- loadOnStartup是Servlet启动的优先级,如果值 <= 0 时,Servlet被首次请求时才会被加载;如果值 > 0 时,表示Servlet容器启动时加载,取值越小,优先级越高 ;
- initParams 对应于ServletConfig 的 initParameter ;
- name 属性对应于ServletConfig 的 servletName ;
我这里提供了一个小小的Demo代码,可以结合起来理解一下:
@WebServlet(
name = "demoServlet",
urlPatterns = "/demo",
initParams = {
@WebInitParam(name = "param1", value = "1+1=2"),
@WebInitParam(name = "param2", value = "0+0=0")
}
)
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
ServletConfig config = this.getServletConfig();
// 打印ServletName
System.out.println("ServletName -> "+ config.getServletName());
// 打印initParams
Enumeration<String> paramNames = config.getInitParameterNames();
while (paramNames.hasMoreElements()) {
String paramName = paramNames.nextElement();
System.out.println(String.format("%s => %s", paramName, config.getInitParameter(paramName)));
}
}
}
/*
* 正常情况下,控制台输出如下:
* ServletName -> demoServlet
* param1 => 1+1=2
* param2 => 0+0=0
*/