总结前端控制器DispatcherServlet(上)-Servlet规范

把Spring MVC工作流实现完走了一遍后,从DispatcherServlet开始看它的具体实现,前端控制器(或叫分发器)作为整个流程的核心,依靠它完成HTTP请求的拦截和分发处理,翻看了DispatcherServlet的源码,看到它实现了多级继承,于是决定写一篇日志,从上往下一步一步总结每一层的作用和实现,先从Servlet规范开始。

组件之间的通信

前端控制器DispatcherServlet可以说是保证整个Spring MVC工作流的最核心组件,DispatcherServlet根据web.xml中的配置拦截到用户的HTTP请求后,首先初始化,加载springmvc.xml配置文件中的配置,例如各种组件,之后,一个Spring MVC工作流开始。

第一步,DispatcherServlet会去遍历所有的处理器映射器,寻找一个可以处理该HTTP请求的处理器。匹配成功的处理器映射器会向DispatcherServlet返回一个处理器执行链,里面包含了一个处理器。

第二步,DispatcherServlet拿到处理器后,再去遍历所有的处理器适配器,寻找一个支持自己手中处理器的处理器适配器,因为只有处理器适配器才知道如何使用这个处理器处理请求。

第三步,DispatcherServlet将控制权交给处理器适配器,处理器适配器将HTTP请求HttpServletRequest和HTTP响应HttpServletResponse传递给处理器(或者说控制器Controller),控制器完成请求处理后,返回带有数据模型和逻辑视图的ModelAndView对象到处理器适配器,最终由处理器适配器返回给DispatcherServlet。

第四步,DispatcherServlet遍历所有的视图解析器ViewResolver,得到一个确定的视图,并将数据模型传递到视图中,完成数据填充,生成最终返回给用户的界面,通过HTTP响应HttpServletResponse发送给用户。

      由此可见,前端控制器DipsatcherServlet是十分关键的组件,它是一个分发器,几乎参与了整个Spring MVC工作流的每一步。

 

DispatcherServlet的多级继承

作为Spring MVC的入口,DispatcherServlet其实就是一个Servlet,DispatcherServlet经过多级继承,最终继承自符合Servlet规范的HttpServlet。HttpServlet又继承自GenericServlet,GenericServlet最终实现Servlet接口,它们之间的关系如下图:

看到如此多级的继承关系,你可能会有疑问,为什么需要这么多级的继承和实现?实现这样多层次的继承,是为了每一级完成特定的任务,如初始化,请求分发,请求处理,清理资源等。下面就来看看各个类和接口里面都有那些实现,以及它们之间的关系。

HTTP和Servlet规范

HTTP请求中包含了用户信息,URI(统一资源标识符,标识Web上的一种可用资源,例如HTML文档,图片和视频),协议版本protocol,和请求的方法(GET、PUT、POST、DELETE等)。HTTP支持的方法有GET、PUT、POST、DELETE、HEAD、OPTIONS,TRACE。

  • GET方法把请求参数放到HTTP请求头中,发送到服务器,请求服务器进行处理并返回HTTP响应。
  • PUT方法用来请求将某个资源放在服务器的某一路径下。
  • POST方法向服务器传送数据,可以要求服务器对其做处理并返回响应。
  • DELETE方法请求删除服务器某一路径下的某个资源。
  • HEAD方法用于查找服务器中某个对象的头部信息。
  • OPTIONS方法用来查询Servlet中实现的方法信息。
  • TRACE方法用于调试操作。

在请求/响应模型下,客户端用户的一个HTTP请求发送到Web容器中后,Web容器就会封装这个HTTP请求,并创建一个HTTP响应,用来回应客户端。Web容器将HTTP请求和自己创建的HTTP响应传递到Servlet的service()方法中。

Servlet是Servlet规范中定义的一个服务器组件接口,所有用于处理用户请求的服务器组件都要实现这个接口。Servlet规范包括一下内容:

可以看到,Servlet接口中定义了三个方法:

  • init()方法用来初始化Servlet,例如注册组件。
  • service()方法用来处理Web传递过来的用户请求。
  • destroy()方法用来释放Servlet组件资源。

 

GenericServlet和HttpServlet

由上面的Servlet规范可以看到,GenericServlet是Servlet的一个抽象实现,作用是保存Servlet的配置,为后面实现的Servlet提供初始化参数和信息。GenericServlet实现了Servlet接口方法init(),并提供了一个无参数的init()方法供子类去重写,实现了对Servlet规范中Servlet的初始化信息保存,并且让子类去初始化自己的配置(如注册组件),来看看GenericServlet中的init(ServletConfig config)代码:

public void init(Servlet config) throws ServletException{
	//保存Servlet的配置信息
	this.config = config;
	
	//另一个无参数的init()方法,该方法是GenericServlet类中的一个抽象方法,
	//提供给子类重写,实现初始化。
	this.init();
}

Init()方法中将Servlet的配置config保存在自己的成员变量中,最后调用一个无参数的init()方法,子类就是通过重写这个无参数的init()方法实现特定行为的初始化。

      GenericServlet还实现了Servlet接口的十分重要的方法,service(),该方法负责分发和处理客户端的请求,GenericServlet提供的是一个通用协议的service()方法,子类必须按照自己的协议,重写这个service方法(例如下面会看到的HttpServlet类重写了该方法,实现支持HTTP协议的Servlet)。看看GenericServlet提供的通用协议的service()方法:

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);
}

      HttpServlet继承了GenericServlet,得到了Servlet的配置信息,并在此基础上,提供了一些基本方法的实现,即上面提到的HTTP支持的各种方法:GET、PUT、POST、DELETE、HEAD、OPTIONS和TRACE。HttpServlet会根据用户HTTP请求中的请求方法,将HTTP请求分发到不同类型的方法中去处理,对于HEAD、OPTIONS、和TRACE这类通用的方法,HttpServlet提供了通用的实现,而对于GET、POST、PUT、和DELETE这里业务逻辑处理方法,则提供了模板方法,子类需要有选择地重写这些方法来完成请求处理。

      HttpServlet重写了父类GenericServlet中的service()方法, 前面看到,在顶级接口Servlet中,service()方法是处理Web传递的用户请求,GenericServlet实现了Servlet接口,但没有重写该方法,因为GenericServlet的任务是保留Servlet的初始化信息,提供空参数的init()方法供子类复写,完成初始化工作。到了HttpServlet,其任务是实现service()方法,根据HTTP请求中标识的方法,完成HTTP请求的分发,分发到相应的处理方法中(GET、PUT、POST、DELETE等)。来看看HttpServlet中的service()方法:

protected void service(HttpServletRequest req, HttpServletResponse resp) 
		throws ServletException, IOException {
		// 从HTTP请求中获取请求方法
		String method = req.getMethod();
		
		if (method.equals(METHOD_GET)) {
			// 如果请求的是GET方法,先获取这个Servlet的最后修改时间
			long lastModified = getLastModified(req);
			
			if (lastModified == -1) {
				// -1表示Servlet不支持修改最后的修改时间,则直接调用doGet()方法处理这个HTTP请求
				doGet(req, resp);
			} else {
				// 如果支持修改最后的修改时间,则修改为HTTP请求头中的最后修改时间
				// 先获取HTTP请求头中的最后修改时间
				long ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
				
				if (ifModifiedSince < (lastModified /1000 * 1000)) {
					// 如果HTTP请求头中的修改时间早于Servlet的修改时间,表明这个Servlet在用户进行
					// 上一次HTTP请求时已被修改,则将最新的修改时间放到响应头中
					maybeSetLastModified(resp, lastModified);
					
					// 调用doGet()方法处理HTTP请求
					doGet(req, resp);
				} else {
					// 如果HTTP请求头中的修改时间晚于Servlet的修改时间,表明这个Servlet在请求的最后
					// 修改时间后都没有被修改,则返回一个HTTP响应SC_NOT_MODIFIED
					resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
				}
			}
		} else if (method.equals(METHOD_HEAD)) {
			// 如果请求的时HEAD方法
			// HEAD方法无论HTTP请求头中的修改时间是早于还是晚于Servlet的最后修改时间,都会将Servlet的
			// 最后修改时间修改到响应头中(如果这个Servlet支持最后修改时间的修改操作)
			
			long lastModified = getLastModified(req);
			maybeSetLastModified(resp, lastModified);
			
			doHead(req, resp);
		} else if (method.equals(METHOD_POST)) {
			// 请求使用POST方法
			doPost(req, resp);
		} else if (method.equals(METHOD_PUT)) {
			// 请求使用PUT方法
			doPut(req, resp);
		} else if (method.equals(METHOD_DELETE)) {
			// 请求使用DELETE方法
			doDelete(req, resp);
		} else if (method.equals(METHOD_OPTIONS)) {
			// 请求使用OPTIONS方法
			doOptions(req, resp);
		} else if (method.equals(METHOD_TRACE)) {
			// 请求使用TRACE方法
			doTrace(req, resp);
		} else {
			// 如果请求使用了未定义方法,则返回错误代码SC_NOT_IMPLEMENTED响应,并且显示错误信息
			String errMsg = Strings.getString("http.method_not_implemented");
			Object[] errArgs = new Object[1];
			errArgs[0] = method;
			errMsg = MessageFormat.format(errMsg, errArgs);
			
			resp.sendError(HttpServletResponse.SC_NOT_TMPLEMENTED, errMsg);
		}
	} 

可以看到,service()方法首先获取HTTP请求中所请求使用的方法method,然后method.equals()判断并根据不同的请求方法进行HTTP请求的分发。前面说到,对于doHead()、doTrace()和doOptions()这三个通用方法,HttpServlet提供了具体实现,子类可以直接使用,子类需要去重写的是doGet()、doPut()、doPost()和doDelete(),Spring MVC也是有选择地重写了这些方法。

 

占位符方法

例如我们来看看HttpServlet中的doGet()方法:

protected void doGet(HttpServletRequest req, HttpServletResponse resp) 
		throws ServletException, IOException
{
	// 获取请求头中的HTTP版本
	String protocol = req.getProtocol();
	
	// 直接发送错误信息,因为这个方法是占位符,需要子类重写模板方法
	String msg = lStrings.getString("http.method_get_not_supported");
	if (protocol.endsWith("1.1")) {
		// 如果HTTP版本是1.1,则让HTTP回应发送错误信息SC_METHOD_NOT_ALLOWED
		resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, msg);
	} else {
		resp.sendError(HttpServletResponse.SC_BAD_REQUEST, msg);
	}
}

doGet()方法在获取了HTTP版本信息后,直接发送错误消息,原因是对于子类Servlet,若要使用这些模板方法,必须重写。doPut()、doPost()和doDelete()这些方法实现都和doGet()方法类似,这里不贴出来了,它们都是一个占位符,需要子类有选择地重写这些方法,实现自己的HTTP请求服务。

 

TRACE、OPTIONS和HEAD方法

对于不同的Servlet组件,doTrace()和doOptions()方法基本一样,TRACE方法返回服务器信息,OPTIONS方法返回Servlet中实现的方法信息,因此,HttpServlet为其提供了具体的实现,先来看看doTrace()方法:

protected void doTrace(HttpServletRequest req, HttpServletResponse resp) 
		throws ServletException, IOException
{
	// 保存字符串的字节长度
	int responseLength;
	
	// 连接URI和版本信息字符串
	String CRLF = "\r\n";
	String responseString = "TRACE " + req.getRequestURI + 
			" " + req.getProtocol();
	
	// 枚举(一次获得一个)对象集合中的元素
	Enumeration reqHeaderEnum = req.getHeaderNames();
	// 遍历所有的请求头信息
	while (reqHeaderEnum.hasMoreElements()) {
		String headerName = (String)reqHeaderEnum.nextElement();
		
		// 将所有请求头拼接到字符串中,请求头信息之间使用回车换行分隔
		responseString += CRLF + headerName + ":" + req.getHeader(headerName);
		
		// 回车换行
		responseString += CRLF;
		
		// 获取字符串的字节长度
		responseLength = responseString.length();
		
		// 设置响应类型为message/http
		resp.setContentType("message/http");
		
		// 设置响应体的长度
		resp.setContentLength(responseLength);
		
		// 输出字符串信息到响应中
		ServletOutputStream out = resp.getOutputStream();
		out.print(responseString);
		
		// 关闭响应
		out.close();
		return;
	}
}

doTrace()方法返回了HTTP请求reg中的URI(统一资源标识符)和HTTP版本信息,并且便利了HTTP请求头中的信息,一起拼接成字符串,最终通过HTTP响应输出。

      doOptions()方法,返回Servlet实现的方法信息,具体来看代码:

protected void doOptions(HttpServletRequest req, HttpServletResponse resp) 
		throws ServletException, IOException
{
	// 获取当前Servlet和它的父类Servlet声明的所有方法,不包括本类HttpServlet声明的方法
	Method[] methods = getAllDeclareMethods(this.getClass());
	
	// 初始化状态,除了OPTIONS和TRACE方法(HttpServlet为其提供了具体实现),假设其他HTTP方法都不支持
	boolean ALLOW_GET = false;
	boolean ALLOW_PUT = false;
	boolean ALLOW_POST = false;
	boolean ALLOW_DELETE = false;
	boolean ALLOW_HEAD = false;
	boolean ALLOW_OPTIONS = true;
	boolean ALLOW_TRACE = true;
	
	// 根据子类Servlet是否重写了HttpServlet的模板方法,判断这个Servlet是否支持这个HTTP方法
	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("doPut"))
			ALLOW_PUT = true;
		if (m.getName().equals("doPost"))
			ALLOW_POST = true;
		if (m.getName().equals("doDelete"))
			ALLOW_DELETE = true;
		
	}
	
	//把Servlet支持的HTTP方法名拼接成字符串
	String allow = null;
	if (ALLOW_GET)
		if (allow == null) allow = METHOD_GET;
	if (ALLOW_PUT)
		if (allow == null) allow = METHOD_PUT;
		else allow += ", " + METHOD_PUT;
	if (ALLOW_POST)
		if (allow == null) allow = METHOD_POST;
		else allow += ", " + METHOD_POST;
	if (ALLOW_DELETE)
		if (allow == null) allow = METHOD_DELETE;
		else allow += ", " + METHOD_DELETE;
	if (ALLOW_HEAD)
		if (allow == null) allow = METHOD_HEAD;
		else allow += ", " + METHOD_HEAD;
	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;
	
	// 将字符串设置到HTTP响应头中
	resp.setHeader("Allow", allow);
}

在doOptions()方法中,将Servlet实现的HTTP方法(也就是重写了的方法),一一遍历出来,拼接到字符串中,并设置到HTTP响应里,返回给用户。细心的你可能会看到,第21行,对于GET方法的判断,为什么只要判断GET方法是已实现的,则HEAD方法也确定为已实现的?来看看doHead()的源码:

protected void doHead(HttpServletRequest req, HttpServletResponse resp) 
		throws ServletException, IOException
{
	if (DispatcherType.INCLUDE.equals(req.getDispatcherType())) {
		doGet(req, resp);
	} else {
		NoBodyResponse response = new NoBodyResponse(resp);
		
		// 重用doGet()方法
		doGet(req, response);
		
		// 设置响应体字节大小
		response.setContentLength();
	}
}

可以看到,doHead()方法将HTTP响应包装成了NoBodyResponse类,也就是只有响应头而没有响应体(因为HEAD方法就是用来返回HTTP响应头的信息,),然后重用了doGet()方法,既然用到了GET方法,则doGet()一定要被子类重写了,所以doHead()方法也等于实现了,不需要重写。这就是为什么上面的OPTIONS方法中,只要判断了GET方法已实现,则HEAD方法也判断为已实现的原因。

      由上面各个方法的代码可以看出,在HttpServlet中大部分方法都是一个占位符(GET、PUT、POST、DELETE等),这些服务的实现需要子类去重写,Spring MVC就是有选择地重写这些方法来实现服务的。

 

Servlet规范小结

最后总结一下Servlet规范。由前面的图可以看出,Servlet规范有三部分:Servlet接口、GenericServlet和HttpServlet抽象类。

最顶层的Servlet接口提供了三个接口方法,init()方法负责初始化Servlet对象,service()方法负责响应处理客户端请求,destory()方法当Servlet对象退出生命周期后,释放其资源。对于每一个服务器组件,都要去实现这个接口。

GenericServlet是Servlet的一个抽象实现,它的init()方法以赋值给自己成员变量的方式,保存了Servlet的config配置,并且提供了一个无参数的init()方法,这样在子类初始化时,可以重写这个无参数的init()方法。GenericServlet实现了Servlet接口的service()方法,这是一个通用协议的service()方法,供它的子类去重写,例如HttpServlet就把service()方法重写为支持HTTP协议。

HttpServlet对HTTP支持的各种方法,GET、PUT、POST、DELETE提供了占位符,对OPTIONS,TRACE和HEAD提供了具体实现,所以HttpServlet的作用就是提供部分具体实现方法和模板方法,让子类根据业务逻辑去重写来处理HTTP请求。

总结完Servlet规范后,下一篇日志就继续往下走,看看每一层的作用是什么,最后如何到达DispatcherServlet。

 

本文源码已上传: 

https://github.com/justinzengtm/SSM-Framework/tree/master/SpringMVC_Project

发布了97 篇原创文章 · 获赞 71 · 访问量 7万+

猜你喜欢

转载自blog.csdn.net/justinzengTM/article/details/102529881
今日推荐