从 Servlet 到 SpringMvc

从 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(配置属性)的一个顶级类。

从设计模式的角度来看,这种设计很经典也很实用,设计思路如下:

  1. Servlet 既是一个接口,在其基础上构建的实现类也必然是整个项目的核心。其主要体现业务逻辑,而不关注配置属性,在 Servlet 类中通过 getServletConfig() 来获取其配置类,从而实现业务(Servlet)和配置(ServletConfig)分离。
  2. 在顶层实现类 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(); 
}

总结

相关推荐
00Allen0033 分钟前
mybatis/mybatisplus
java·spring·mybatis
编码浪子2 小时前
Springboot3国际化
java·spring·mybatis
HUNAG-DA-PAO2 小时前
Spring AOP是什么
java·jvm·spring
带刺的坐椅2 小时前
Solon v3.0.5 发布!(Spring 生态可以退休了吗?)
java·spring·solon
小林想被监督学习4 小时前
Spring Boot 整合 RabbitMQ(在Spring项目中使用RabbitMQ)
spring boot·spring·java-rabbitmq
爱学习的小羊啊4 小时前
从零开始掌握Spring MVC:深入解析@Controller与@RequestMapping注解的使用
java·spring·mvc
敲代码娶不了六花8 小时前
jsp | servlet | spring forEach读取不了对象List
java·spring·servlet·tomcat·list·jsp
路在脚下@13 小时前
Spring如何处理循环依赖
java·后端·spring
丁总学Java13 小时前
--spring.profiles.active=prod
java·spring
weisian15114 小时前
Redis篇--常见问题篇8--缓存一致性3(注解式缓存Spring Cache)
redis·spring·缓存