从 Servlet 到 SpringMvc

ops/2024/10/11 13:30:37/

从 Servlet 到 SpringMvc

下图为 SpringMvc 的 DispatcherServlet 到 Servlet 的继承体系结构,从 HttpServletBean 开始的子类,便属于 Spring 的体系结构,Spring 框架中类似这种以 XXXBean 结尾是用于和其它框架进行整合的 JavaBean 对象,类似还有和 MyBatis 框架进行整合的 SqlSessionFactoryBean。这里只需要关注 HttpServlet 到 Servlet 的这一部分。
HttpServlet 的继承体系

Servlet 接口

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)。

public interface ServletConfig {//使用@WebServlet注解方式时,默认是全类名String getServletName();// ServletContext代表应用程序,用于各个Servlet之间的参数共享ServletContext getServletContext();// 获取所有的key(迭代器)// Enumeration是遗留类,Iterator的前身Enumeration<String> getInitParameterNames();// 根据key获取valueString 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(配置属性)的一个顶级类。
从设计模式的角度来看,这种设计很经典也很实用,设计思路如下:

  1. Servlet 既是一个接口,在其基础上构建的实现类也必然是整个项目的核心。其主要体现业务逻辑,而不关注配置属性,在 Servlet 类中通过 getServletConfig() 来获取其配置类,从而实现业务(Servlet)和配置(ServletConfig)分离。
  2. 在顶层实现类 GenericServlet 中引入 ServletConfig 配置对象,重写其中的方法,但方法内容都是委托给内部的 ServletConfig 对象,从而将分离的功能重新整合为一体。(如果在 GenericServlet 中不重写,那每次获取一个 Servlet 的配置项,需要先通过该 Servlet 类来获取其配置类,然后才能获取配置项,这样给人感觉就很奇怪。)

总结:针对这种设计模式,可以认为设计思路上最先出现的是 GnericServlet,它才是真正对应一个 标签的类,然后为了进行配置管理,将其中的功能拆分到两个类中(Servlet 和 ServletConfig),并为了连接二者,在 Servlet 的接口中必须添加 getServletConfig() 方法。

public abstract class GenericServlet implements Servlet, ServletConfig, java.io.Serializable { // 从整合两个功能来看,符合适配器模式的设计(假设Servlet和ServletConfig都是类而不是接口的话)private transient ServletConfig config;@Overridepublic String getInitParameter(String name) {// 针对ServletConfig接口中的方法,委托给ServletConfig对象,其它方法都是一样的处理return getServletConfig().getInitParameter(name);}// 如果子类重写该带参方法,那么需要手动设置config,但是由于config是private修饰的,所以需要通过在子类中使用super.init(config)来进行设置@Overridepublic 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。

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 logicdoGet(req, resp);} else {long ifModifiedSince;try {ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE); } catch (IllegalArgumentException iae) {// Invalid date header - proceed as if none was setifModifiedSince = -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 lessmaybeSetLastModified(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 中直接抛出异常。

//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),不需要响应体。

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 正常情况下不需要使用,主要用于进行一些调试工作,可能存在安全漏洞被黑客利用,最好禁用。

protected void doOptions(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { Method[] methods = getAllDeclaredMethods(this.getClass());// get、head、post、put、delete、trace、optionsboolean[] 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 请求

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' headersif (!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(); 
}

总结

从 HttpServlet 学习构建自定义协议的 Servlet


http://www.ppmy.cn/ops/39089.html

相关文章

SQL-Server数据库--视图

1.创建视图 create view as 子查询 子查询可以是任意发杂的select语句&#xff0c;但通常不允许含有order by和distinct短语 --使用T-SQL语句创建新视图view_score, 要求只显示学生的学号、姓名、课号、课程名称及成绩。 create view view_score as select from tb_stude…

力扣数据库题库学习(5.4日)--1667. 修复表中的名字

1667. 修复表中的名字 问题链接 解题思路 使用 SUBSTRING() 函数获取每个名字的第一个字符和剩余字符。 使用 UPPER() 函数将第一个字符转换为大写。 使用 LOWER() 函数将剩余字符转换为小写。 使用 CONCAT() 函数将第一个字符和剩余字符组合成名字。 最后按照 user_id 对结…

一对一WebRTC视频通话系列(三)——leave和peer-leave信令实现

本篇博客主要分为两部分&#xff0c;第一部分为leave信令的实现&#xff0c;即当有客户端离开房间后&#xff0c;服务端和其他在房间内的客户需知晓。第二部分为媒体协商和网络协商相关API。 本系列博客主要记录一对一WebRTC视频通话实现过程中的一些重点&#xff0c;代码全部进…

Kubernetes——基础认识

一、简介 1.Kubernetes是什么 Kubernetes 是一个全新的基于容器技术的分布式架构解决方案&#xff0c;是 Google 开源的一个容器集群管理系统&#xff0c;Kubernetes 简称 K8S。 Kubernetes 是一个一站式的完备的分布式系统开发和支撑平台&#xff0c;更是一个开放平台&#x…

玩comfyui踩过的坑之使用ComfyUI_Custom_NODES_ALEKPET翻译组件问题

环境&#xff1a; 秋叶安装包&#xff0c;安装ComfyUI_Custom_NODES_ALEKPET组件或者直接下载网盘中的包&#xff0c;直接解压包到comfyui根目录/custom_nodes/&#xff0c;重启后&#xff0c;按指导文件操作。 注意&#xff1a;网盘指导包中有配置好的流程json文件&#xff0…

JavaWeb入门-HTML

一、HTML 1.HTML 网络的骨架 超文本标记语言 ①超文本 图片、音频、视频、普通文本。。。 ②标记语言 语法&#xff1a;通过标签的形式展示 a.双标签 <html>内容</html> b.单标签 <br> 2.HelloWorld ①新建网页文件&#xff08;后…

飞天使-k8s知识点29-kubernetes安装1.28.0版本

文章目录 选用版本初始化服务器,自己修改里面的ipreboot haproxy安装 &#xff0c;可以参考我之前写的内核参数调整&#xff0c;安装docker 安装cri-dockerd开始安装集群工具下载镜像以及启用完毕之后 此时的coredns 不通结果展示 选用版本 k8s 1.24版本之前还可以使用docker&…

Kettle连接Mysql数据库时报错——Driver class ‘org.gjt.mm.mysql.Driver‘ could not be found

一、问题描述 当我们使用ETL工具Kettle需要连接Mysql数据库进行数据清洗操作,在配置好Mysql的连接串内容后,点击【测试】按钮时报错【错误连接数据库 [MysqlTestConnection] : org.pentaho.di.core.exception.KettleDatabaseException: Error occurred while trying to conne…