从 Servlet 到 SpringMvc
下图为 SpringMvc 的 DispatcherServlet 到 Servlet 的继承体系结构,从 HttpServletBean 开始的子类,便属于 Spring 的体系结构,Spring 框架中类似这种以 XXXBean 结尾是用于和其它框架进行整合的 JavaBean 对象,类似还有和 MyBatis 框架进行整合的 SqlSessionFactoryBean。这里只需要关注 HttpServlet 到 Servlet 的这一部分。
Servlet 接口
java
public interface Servlet {
// 容器调用,且只调用一次
public void init(ServletConfig config) throws ServletException;
public void destroy();
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException;
// <init-param>标签中配置的key-value键值对保存在ServletConfig中
public ServletConfig getServletConfig();
public String getServletInfo();
}
方法 | 描述 |
---|---|
init() | 容器启动时,被容器调用,并且只调用一次 |
(当 load-on-startup 为负数时懒加载,即第一次调用 Servlet 时才调用该方法,load-on-startup默认为负数) | |
destroy() | 在 Servlet 销毁(一般指关闭服务器)时,用于释放资源,和 init 一样只会调用一次 |
service() | Servlet 对请求的具体处理逻辑 |
getServletConfig() | ServletConfig 是重点 |
ServletConfig 配置
对应某个 Servlet 的配置,保存 <init-param>
标签中的值,相当于 Map<String, String>,其中一个 <init-param>
代表一个键值对(key-value)。
java
public interface ServletConfig {
//使用@WebServlet注解方式时,默认是全类名
String getServletName();
// ServletContext代表应用程序,用于各个Servlet之间的参数共享
ServletContext getServletContext();
// 获取所有的key(迭代器)
// Enumeration是遗留类,Iterator的前身
Enumeration<String> getInitParameterNames();
// 根据key获取value
String getInitParameter(String name);
}
方法 | 描述 |
---|---|
getServletName() | 获取 Servlet 的名字,注解方式下默认使用全类名(@WebServlet) |
getServletContext() | ServletContext 代表应用程序本身,在 Tomcat 中是ApplicationContextFacade。 |
在 ServletContext 中的参数被当前应用所有的 Servlet 共享 | |
在 ServletContext 中还可以通过 getContext 方法来获取同主机下其它应用的 ServletContext(需要开启额外设置,默认返回 null) |
ServletContext
应用程序本身,不仅包含应用程序级别的配置,还可以由用户自定义用于共享的属性(setAttribute())。
initParameter 和 attribute 包含在两个不同的 Map 中,互不干扰。
GenericServlet
Servlet 本身是一个接口,而对于 XXXServlet 的配置则保存在 ServletConfig 中,因此 GenericServlet 是实现 Servlet(业务功能)和 ServletConfig(配置属性)的一个顶级类。
从设计模式的角度来看,这种设计很经典也很实用,设计思路如下:
- Servlet 既是一个接口,在其基础上构建的实现类也必然是整个项目的核心。其主要体现业务逻辑,而不关注配置属性,在 Servlet 类中通过 getServletConfig() 来获取其配置类,从而实现业务(Servlet)和配置(ServletConfig)分离。
- 在顶层实现类 GenericServlet 中引入 ServletConfig 配置对象,重写其中的方法,但方法内容都是委托给内部的 ServletConfig 对象,从而将分离的功能重新整合为一体。(如果在 GenericServlet 中不重写,那每次获取一个 Servlet 的配置项,需要先通过该 Servlet 类来获取其配置类,然后才能获取配置项,这样给人感觉就很奇怪。)
总结:针对这种设计模式,可以认为设计思路上最先出现的是 GnericServlet,它才是真正对应一个 标签的类,然后为了进行配置管理,将其中的功能拆分到两个类中(Servlet 和 ServletConfig),并为了连接二者,在 Servlet 的接口中必须添加 getServletConfig() 方法。
java
public abstract class GenericServlet implements Servlet, ServletConfig, java.io.Serializable {
// 从整合两个功能来看,符合适配器模式的设计(假设Servlet和ServletConfig都是类而不是接口的话)
private transient ServletConfig config;
@Override
public String getInitParameter(String name) {
// 针对ServletConfig接口中的方法,委托给ServletConfig对象,其它方法都是一样的处理
return getServletConfig().getInitParameter(name);
}
// 如果子类重写该带参方法,那么需要手动设置config,但是由于config是private修饰的,所以需要通过在子类中使用super.init(config)来进行设置
@Override
public void init(ServletConfig config) throws ServletException {
this.config = config;
this.init();
}
// 一般情况下,子类只需要重写无参的init()即可,除非需要对config进行一些额外的操作
public void init() throws ServletException {
// NOOP by default
}
}
如果子类重写 init(ServletConfig) 方法,那么需要手动设置 config,否则 config 为 null。但是由于 config 是 private 修饰的,所以需要通过在子类中使用super.init(config)来进行设置。
HttpServlet(实现 Http 协议的 Servlet)
如果开发一个应用,客户端和服务端需要使用自定义协议,也可以继承 GenericServlet 来实现一个 XXXServlet 用来表示一个实现自定义协议的 Servlet。
java
public abstract class HttpServlet extends GenericServlet {
private static final long serialVersionUID = 1L;
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";
private static final String HEADER_IFMODSINCE = "If-Modified-Since";
private static final String HEADER_LASTMOD = "Last-Modified";
private static final String LSTRING_FILE = "javax.servlet.http.LocalStrings";
private static final ResourceBundle lStrings = ResourceBundle.getBundle(LSTRING_FILE);
private static final List<String> SENSITIVE_HTTP_HEADERS =
Arrays.asList("authorization", "cookie", "x-forwarded", "forwarded", "proxy-authorization");
// 这个service真正体现出这个类(HttpServlet)是针对HTTP协议而设计的
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;
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 {
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);
}
}
// Servlet不是只能用来实现HTTP协议,而这个service()是Servlet和HTTP的桥梁,是个桥接方法
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(lStrings.getString("http.non_http"));
}
service(request, response);
}
}
Get、Post、Put、Delete 请求
具体业务逻辑每个 Servlet 都不相同,所以交给子类根据自己的场景去实现,HttpServlet 中直接抛出异常。
java
//get、post、put、delete这四种请求方式的处理逻辑交给子类实现,这里只是抛出异常(子类使用这些方法的业务逻辑都不相同)
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String msg = lStrings.getString("http.method_get_not_supported");
sendMethodNotAllowed(req, resp, msg);
}
Head 请求
本质上还是 Get 请求,只是客户端只需要服务端返回响应消息(Response)的响应头(Response-Head),不需要响应体。
java
protected void doHead(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
if (DispatcherType.INCLUDE.equals(req.getDispatcherType())) {
doGet(req, resp);
} else {
//服务端对response进行处理,相当于response.setBody(null);
NoBodyResponse response = new NoBodyResponse(resp);
doGet(req, response);
// ...
}
}
Options 请求
在响应消息中设置了一个 Allow
响应头,表示允许的请求方式。Options 和 Trace 正常情况下不需要使用,主要用于进行一些调试工作,可能存在安全漏洞被黑客利用,最好禁用。
java
protected void doOptions(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
Method[] methods = getAllDeclaredMethods(this.getClass());
// get、head、post、put、delete、trace、options
boolean[] ALLOW_METHODS = {false, false, false, false, false, true, true};
String[] METHODS = {"GET", "HEAD", "POST", "PUT", "DELETE", "TRACE", "OPTIONS"};
Class<?> clazz = null;
try {
clazz = Class.forName("org.apache.catalina.connector.RequestFacade");
Method getAllowTrace = clazz.getMethod("getAllowTrace", (Class<?>[]) null);
ALLOW_METHODS[5] = (Boolean) getAllowTrace.invoke(req, (Object[]) null);
} catch (ClassNotFoundException | NoSuchMethodException | SecurityException | IllegalAccessException |
IllegalArgumentException | InvocationTargetException ignored) {
}
for (Method m : methods) {
if (m.getName().equals("doGet")) {
ALLOW_METHODS[0] = true;
ALLOW_METHODS[1] = true;
}
if (m.getName().equals("doPost")) {
ALLOW_METHODS[2] = true;
}
if (m.getName().equals("doPut")) {
ALLOW_METHODS[3] = true;
}
if (m.getName().equals("doDelete")) {
ALLOW_METHODS[4] = true;
}
}
// 源码写得比较恶心,简单改进一下
StringBuilder allowBuilder = new StringBuilder();
for (int i = 0; i < ALLOW_METHODS.length; i++) {
if (ALLOW_METHODS[i]) {
allowBuilder.append(", ").append(METHODS[i]);
}
}
resp.setHeader("Allow", allowBuilder.substring(2));
}
Trace 请求
java
protected void doTrace(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
int responseLength;
String CRLF = "\r\n";
StringBuilder buffer = new StringBuilder("TRACE ")
.append(req.getRequestURI())
.append(' ')
.append(req.getProtocol());
Enumeration<String> reqHeaderNames = req.getHeaderNames();
while (reqHeaderNames.hasMoreElements()) {
String headerName = reqHeaderNames.nextElement();
// RFC 7231, 4.3.8 - skip 'sensitive' headers
if (!isSensitiveHeader(headerName)) {
Enumeration<String> headerValues = req.getHeaders(headerName);
while (headerValues.hasMoreElements()) {
String headerValue = headerValues.nextElement();
buffer.append(CRLF).append(headerName).append(": ").append(headerValue);
}
}
}
buffer.append(CRLF);
responseLength = buffer.length();
resp.setContentType("message/http");
resp.setContentLength(responseLength);
ServletOutputStream out = resp.getOutputStream();
out.print(buffer.toString());
out.close();
}