• 浅析Java源码之HttpServlet


      纯粹是闲的,在慕课网看了几集的Servlet入门,刚写了1个小demo,就想看看源码,好在也不难

      主要是介绍一下里面的主要方法,真的没什么内容啊~

      源码来源于apache-tomcat-7.0.52,servlet-api.jar包

    继承树

      首先来看一下HttpServlet类的继承关系:

      // javax.servlet.http
      public abstract class HttpServlet extends GenericServlet implements java.io.Serializable {
          //...
      }
    
      // javax.servlet
      public abstract class GenericServlet implements Servlet, ServletConfig, java.io.Serializable {
          //...
      }

      先不看HttpServlet本身,它的父类是GenericServlet,该类主要是对Servlet、ServletConfig两个接口中的部分方法做了简单实现,并没有多少东西。

      这里列举一下ServletConfig与Servlet接口中的方法:

    ServletConfig

      public interface ServletConfig {
          // 获取servlet名字
          public String getServletName();
          // 获取servlet上下文
          public ServletContext getServletContext();
          // 获取初始化参数列表
          public String getInitParameter(String name);
          // 获取初始化参数名
          public Enumeration getInitParameterNames();
      }

    Servlet

        public interface Servlet {
            // servlet初始化方法
            public void init(ServletConfig config) throws ServletException;
            // 获取配置信息
            public ServletConfig getServletConfig();
            // 处理请求
            public void service(ServletRequest req, ServletResponse res)
            throws ServletException, IOException;
            public String getServletInfo();
            // 销毁
            public void destroy();
        }

      从Servlet接口中可以简单看到Servlet的生命周期:constructor、init、service、destroy =>构造、 初始化、处理请求、销毁。

      值得注意的是,在GenericServlet中,init、destroy方法都未实现:

        public void init(ServletConfig config) throws ServletException {
            this.config = config;
            this.init();
        }
        public void destroy() {
        }

      也就是在实际运行中,会根据定义方法来进行初始化与销毁。

      接下来就看看HttpServlet本身,这里就不一个一个过,挑一些方法来看:

    1、为什么继承类需要重写doGet/doPost

      在看视频的时候,讲课老师提到了我们需要override这两个方法,看了源码就明白原因了:

        protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            // 获取请求协议 => HTTP/1.1
            String protocol = req.getProtocol();
            // 默认响应返回信息
            String msg = lStrings.getString("http.method_get_not_supported");
            // 直接返回错误
            if (protocol.endsWith("1.1")) {
                resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, msg);
            } else {
                resp.sendError(HttpServletResponse.SC_BAD_REQUEST, msg);
            }
        }

      其余诸如doPost、doPut方法类似,这里就不贴出来了。

      未重写的方法只是简单的获取了请求协议,并根据协议返回一个错误提示信息,所以所有继承的方法都有必要重写对应的响应方法。

    2、通用方法service

      在请求处理中,内置了一个通用的方法,名为service。

        protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            // 获取请求方式
            String method = req.getMethod();
            // 开始匹配响应方法
            if (method.equals(METHOD_GET)) {
                // 获取请求里lastModified值 默认为-1
                long lastModified = getLastModified(req);
                if (lastModified == -1) {
                    // 未处理缓存相关
                    // 直接响应
                    doGet(req, resp);
                } else {
                    // 获取请求头中的If-Modified-Since值
                    // private static final String HEADER_IFMODSINCE = "If-Modified-Since";
                    long ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
                    // 过了缓存期 返回最新资源内容
                    if (ifModifiedSince < (lastModified / 1000 * 1000)) {
                        maybeSetLastModified(resp, lastModified);
                        doGet(req, resp);
                    }
                    // 告知浏览器可以直接从缓存获取
                    else {
                        resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
                    }
                }
    
            } else if (method.equals(METHOD_POST)) {
                // doPost
            }
            // 其余请求方法处理
    
            // 报错
            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);
            }
        }

      这个方法总的来说只有两部:

    1、获取的方法,get?post?

    2、匹配对应的响应处理方法,doGet or doPost

      这里唯有get响应有一些复杂,主要原因在于所有页面的请求默认是get请求,这涉及到协商缓存问题,详细的可以自己去网上查。

      中间有一个maybeSetLastModified是一个检测方法,判断响应头中是否有设置Last-Modified,如下:

        private void maybeSetLastModified(HttpServletResponse resp, long lastModified) {
            // 如果已有直接返回
            if (resp.containsHeader(HEADER_LASTMOD))
                return;
            // 大于0说明有做处理 设置响应头的Last-Modified
            if (lastModified >= 0)
                resp.setDateHeader(HEADER_LASTMOD, lastModified);
        }

      这个比较简单,就不解释了。

      另外,注意到上面的service方法权限是protected,其实还有看起来一样的public版本提供了外部访问途径,参数不太一样:

        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("non-HTTP request or response");
            }
            service(request, response);
        }

      看一下就行了。

    3、doTrace

      类中还内置了一个特殊方法,可以详细展示了请求的头部信息。

        protected void doTrace(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            int responseLength;
            // 换行
            String CRLF = "
    ";
            // 地址 + 协议名
            String responseString = "TRACE " + req.getRequestURI() + " " + req.getProtocol();
            // 获取所有请求头
            Enumeration reqHeaderEnum = req.getHeaderNames();
            // 遍历拼接key: value
            while (reqHeaderEnum.hasMoreElements()) {
                String headerName = (String) reqHeaderEnum.nextElement();
                responseString += CRLF + headerName + ": " + req.getHeader(headerName);
            }
            responseString += CRLF;
            responseLength = responseString.length();
            // 这个响应类型查都查不到
            // 表现形式为下载一个文件 内容为拼接的字符串
            resp.setContentType("message/http");
            resp.setContentLength(responseLength);
            // 内置的输出流 与PrintWriter类似
            ServletOutputStream out = resp.getOutputStream();
            out.print(responseString);
            out.close();
            return;
        }

      这个方法调用后,就不能继续用视频里的out.print输出内容了,如果在doGet中调用此方法,例如:

        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp)
                throws ServletException, IOException {
            doTrace(req,resp);
        }

      将会下载一个名为servlet的文件:

      里面的内容如下:

        TRACE /Myservlet/MyServlet/servlet HTTP/1.1
        host: localhost:8080
        connection: keep-alive
        cache-control: max-age=0
        upgrade-insecure-requests: 1
        user-agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.75 Safari/537.36
        accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
        referer: http://localhost:8080/Myservlet/
        accept-encoding: gzip, deflate, br
        accept-language: zh-CN,zh;q=0.9
        cookie: JSESSIONID=46E569152C4D155D266B790E09604F30

      很明显,就是请求头的键值对打印信息。

    4、响应头Allow

      有一个方法专门设置Allow的响应头,该字段表明可以处理的请求方式。

      不过在此之前,需要看一下getAllDeclaredMethods方法,该方法获取继承链(除了根类javax.servlet.http.HttpServlet)上所有类方法:

        private static Method[] getAllDeclaredMethods(Class c) {
            // 该类为终点
            if (c.equals(javax.servlet.http.HttpServlet.class)) {
                return null;
            }
            // 递归获取父类方法
            Method[] parentMethods = getAllDeclaredMethods(c.getSuperclass());
            // 通过反射获取本类中的方法
            Method[] thisMethods = c.getDeclaredMethods();
            // 如果父类存在方法 拷贝到数组中
            if ((parentMethods != null) && (parentMethods.length > 0)) {
                Method[] allMethods = new Method[parentMethods.length + thisMethods.length];
                System.arraycopy(parentMethods, 0, allMethods, 0, parentMethods.length);
                System.arraycopy(thisMethods, 0, allMethods, parentMethods.length, thisMethods.length);
    
                thisMethods = allMethods;
            }
            return thisMethods;
        }

      该方法通过反射机制,获取到本类向上直到HttpServlet类中间的所有方法,用一个Method数组保存起来。

      接下来就可以看这个doOptions是如何设置这个头信息的:

        protected void doOptions(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            // 获取methods
            Method[] methods = getAllDeclaredMethods(this.getClass());
            // 假设请求方法均为false
            // trace、options默认为true
            boolean ALLOW_GET = false;
            boolean ALLOW_HEAD = false;
            boolean ALLOW_POST = false;
            boolean ALLOW_PUT = false;
            boolean ALLOW_DELETE = false;
            boolean ALLOW_TRACE = true;
            boolean ALLOW_OPTIONS = true;
            // 遍历methods
            // 如果存在对应的方法名 说明有重写方法处理对应的请求
            // getName方法获取对应的字符串
            for (int i = 0; i < methods.length; i++) {
                Method m = methods[i];
                if (m.getName().equals("doGet")) {
                    ALLOW_GET = true;
                    ALLOW_HEAD = true;
                }
                if (m.getName().equals("doPost"))
                    ALLOW_POST = true;
                if (m.getName().equals("doPut"))
                    ALLOW_PUT = true;
                if (m.getName().equals("doDelete"))
                    ALLOW_DELETE = true;
            }
            // 进行字符串拼接
            String allow = null;
            if (ALLOW_GET)
                if (allow == null)
                    allow = METHOD_GET;
            // 很多if
            // ...
            if (ALLOW_TRACE)
                if (allow == null)
                    allow = METHOD_TRACE;
                else
                    allow += ", " + METHOD_TRACE;
            if (ALLOW_OPTIONS)
                // 这个分支不可能达到的吧……
                if (allow == null)
                    allow = METHOD_OPTIONS;
                else
                    allow += ", " + METHOD_OPTIONS;
            // 设置头
            resp.setHeader("Allow", allow);
        }
    }

      很简单,遍历methods,有对应的方法,就将对应的控制变量设为true,最后进行拼接,设置响应头Allow。

      测试代码如下:

        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp)
                throws ServletException, IOException {
            PrintWriter out = resp.getWriter();
            this.doOptions(req,resp);
            out.println("123");
        }

      打开网页,查看Network中的Headers,可以看到:

      基本上讲完了,里面还有两个内部类:NoBodyResponse、NoBodyOutputStream,看起来没什么营养就不看了。

  • 相关阅读:
    cogs 826. Feb11] GF打dota
    cogs 133. [USACO Mar08] 牛跑步 A*k短路算法
    luogu cogs 1437. [NOIP2013]转圈游戏
    RESTful
    中间件
    回顾基础知识,类,fbv,cbv
    Vue
    ES6的一些说明
    小试牛刀2
    小试牛刀
  • 原文地址:https://www.cnblogs.com/QH-Jimmy/p/7825869.html
Copyright © 2020-2023  润新知