浅析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,看起来没什么营养就不看了。