浅析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 = "\r\n";
        // 地址 + 协议名
        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,看起来没什么营养就不看了。

转载于:https://www.cnblogs.com/QH-Jimmy/p/7825869.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值