初始工程
使用 Spring Boot 和 web,thymeleaf 的 starter 来设置初始工程. xml 配置如下:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.1</version> <relativePath/></parent><dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency></dependencies>
测试项目
为了理解 Spring Web MVC 是如何工作的, 可以先实现一个简单的 Login 功能. 创建一个由 @Controller 来修饰的类 InternalController, 这个类包含一个处理 GET 请求的方法. hello() 返回一个由 Spring 解释的视图名字的字符串.(在本例中是 login.html)
@GetMapping("/")public String hello() { return "login";}
为了处理用户登陆逻辑, 创建另一个接受 POST 请求的带有 Login 数据的方法. 然后根据处理结果返回成功或者失败页面. 注意, login() 函数接受一个领域对象作为参数, 返回的是 ModelAndView 对象.
@PostMapping("/login")public ModelAndView login(LoginData loginData) { if (LOGIN.equals(loginData.getLogin()) && PASSWORD.equals(loginData.getPassword())) { return new ModelAndView("success", Collections.singletonMap("login", loginData.getLogin())); } else { return new ModelAndView("failure", Collections.singletonMap("login", loginData.getLogin())); }}
ModelAndView 保存了两个不同的对象:
Model: 用来渲染页面用的的 map
View: 模版页面.
将它们合并起来是为了方便, 这样 controller 的方法就可以同时返回这两个了. 最后使用 Thymeleaf 作为模版引擎来渲染页面.
Java Web 应用的基础 - Servlet
当你在浏览器里键入
http://localhost:8080/
, 然后按回车键, 请求到达服务器的时候到底发生了什么? 是如何在浏览器中看到这个 web 请求的数据的? 因为这个项目是一个简单的 Spring Boot 应用, 所以可以通过 Spring5Application 的 main 方法运行项目. Spring Boot 默认使用 Apache Tomcat 运行程序, 运行成功后可能会看到如下的相同的日志:
- 2018-04-10 20:36:11.626 INFO 57414 --- [main]
- o.s.b.w.embedded.tomcat.TomcatWebServer :
- Tomcat initialized with port(s): 8080 (http)2018-04-10 20:36:11.634 INFO 57414 --- [main]
- o.apache.catalina.core.StandardService :
- Starting service [Tomcat]2018-04-10 20:36:11.635 INFO 57414 --- [main]
- org.apache.catalina.core.StandardEngine :
- Starting Servlet Engine: Apache Tomcat/8.5.23
因为 Tomcat 是一个 Servlet 容器, 所以几乎所有的 HTTP 请求都是由 Java Servlet 处理的. 自然的 Spring Web 的入口就是一个 Servlet.Servlet 是所有 Java Web 应用的核心组件; 它非常的底层, 并且没有暴露任何具体的编程模式, 例如 MVC. 一个 HTTP 的 Servelt 只能接受 HTTP 请求, 处理请求后返回响应. 最新的 Servlet 3.0 的 API, 可以不再使用 XML 配置, 直接可以使用 Java 配置.
Spring MVC 的核心 - DispatcherServlet
作为 Web 开发者, 我们希望抽象出以下枯燥的任务, 而关注于有用的业务逻辑
将 HTTP 请求映射到对应的处理函数
将 HTTP 请求数据和 header 解析成 DTO 或者领域对象
使用 model-view-controller 设计模式
从 DTO, 领域对象等直接生成响应
Spring 的 DispatcherServlet 提供了以上的功能, 它是 Spring WEB MVC 框架的核心, 是应用接受所有请求的核心组件. DispatcherServlet 可扩展性非常强. 例如: 它允许你添加现有或者新的适配器来适应不同的任务:
将请求映射到处理它的类或者函数 (由 HandlerMapping 实现)
使用特定模式来处理请求, 例如一个普通的 Servlet, 一个复杂的 MVC 工作流, 或者只是一个方法.(由 HandlerAdapter 实现)
通过名字解析试图对象, 允许你使用不同的模版引擎, 例如: XML,XSLT 或者其他视图技术 (由 ViewResolver 实现)
默认使用 Apache Commons 的文件上传组件解析文件上传, 也可以自己实现.
由 LocalResolver 实现本地化, 包括 cookie,session,HTTP 的 Accept Header, 或者其他由用户定义的本地化.
处理 HTTP 请求
DispatcherServlet 有一个很长的继承层级. 自顶向下理解每个单独的概念是非常有必要的.
GenericServlet
GenericServlet 是 Servlet 规范中的一部分, 它定义了 service() 方法, 来接受请求和返回响应.
public abstract void service(ServletRequest req, ServletResponse res) throws ServletException, IOException;
服务器所有的请求都会调用这个方法.
HttpServlet
正如其名, HttpServelt 是 Servlet 规范中关于 HTTP 请求的实现. 更确切的说, HttpServlet 是一个实现了 service() 的抽象类. 通过将不同的 HTTP 请求类型分开, 由不同的函数处理, 实现大约如下所示:
- protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String method = req.getMethod(); if (method.equals(METHOD_GET)) { // ... doGet(req, resp); } else if (method.equals(METHOD_HEAD)) { // ... doHead(req, resp); } else if (method.equals(METHOD_POST)) { doPost(req, resp); // ... }
- HttpServletBean
在这个继承关系中 HttpServletBean 是第一个 Spring 的类. 从 web.xml 或者 WebApplicationInitialzer 获取的初始参数来注入 bean 的属性. 在应用中的请求分别调用 doGet,doPost 等方法来处理不同的 HTTP 请求.
FrameworkServlet
FrameworkServlet 实现了
ApplicationContextAware
, 集成 Web 的 Application Context. 不过它也可以创建自己的 Application Context. 正如上述所言, 父类 HttpServletBean 通过将初始参数作为 bean 的属性注入. 因此如果 contex 的类名在 contextClass 这个初始参数中, 那么就有这个参数创建 application context 的实例, 否则默认使用
XmlWebApplicationContext
. 由于 XML 配置现在并不是 Spring 推荐的配置方式了. Spring Boot 默认使用
AnnotationConfigWebApplicationContext
来配置 DispatcherServlet.
DispatcherServlet: 统一处理请求
HttpServlet.service()
通过 HTTP 的请求类型将不同的请求发送到不同的方法, 这个在底层的 servlet 实现的很好. 但是, 在 SpringMVC 的抽象层次中, 不能仅靠方法类型来路由请求. 同样的, FrameworkServlet 的另一个主要功能就是将不同的处理使用 processRequest() 组合在一起.
@Overrideprotected final void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { processRequest(request, response);}@Overrideprotected final void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { processRequest(request, response);}
DispatcherServlet: 对请求加入有用的信息
DispatcherServlet 实现 doService() 方法. 它向请求中加入了一些有用的对象, 然后在 web 的请求处理中传递下去, 例如: web application context, locale resolver, theme resolver, theme source 等
request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext());request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver);request.setAttribute(THEME_SOURCE_ATTRIBUTE, getThemeSource());
同时, doService() 加入了输入输出的 Flash Map,Flash Map 是将参数从一个请求传递到另一个请求的基本模式. 在重定向中很有用.(例如在重定向之后向用户展示一段简单的信息)
FlashMap inputFlashMap = this.flashMapManager .retrieveAndUpdate(request, response);if (inputFlashMap != null) { request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap));}request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());
接着 doService() 将会调用 doDispatch() 方法来分发请求.
DispatcherServlet: 分发请求
dispatch() 的主要目的就是找到一个合适的处理请求的处理器并且传递 request/response 参数. 处理器可以是任何对象, 并不局限于一个特定的接口. 同样也意味着 Spring 需要找到如何使用这个处理器的适配器. 为了给请求找到合适的处理器, Spring 会遍历实现 HandlerMapping 接口的注册的实现. 有很多不同的实现可以满足我们各种需求.
SimpleUrlHandlerMapping
使用 URL 将请求映射到处理 bean 中.
RequestMappingHandlerMapping
可能是最广泛使用的映射处理器. 它将请求映射到 @Controller 类下的 @RequestMapping 修饰的方法上. 这个就是上面那个例子中的 hello() 和 login().
注意, 上面两个方法分别是 @GetMapping 和 @PostMapping 修饰的. 这两个注解来源于 @RequestMapping. dispatch() 同时也可以处理一些其他的 HTTP 的任务:
如果资源不存在, 对 GET 请求进行短路处理.
对相应的请求使用 multipart 解析.
如果处理器选择异步处理请求, 对请求进行短路处理.
处理请求
现在 Spring 确定了处理请求的处理器和处理器的适配器, 是时候处理请求了. 下面是
HandlerAdapter.handle()
的签名. 比较重要的一点是处理器可以选择如何处理请求:
直接将响应写入到 response body 然后返回 null
返回一个由 DispatcherServlet 渲染的 ModelAndView 对象.
@NullableModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;
Spring 提供了很多类型的处理器, 下面是
SimpleControllerHandlerAdapter
如何处理 Spring MVC 的 controller 实例的 (不要和 @Controller 搞混, 这里是一个类).
public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { return ((Controller) handler).handleRequest(request, response);}
第二个是
SimpleServletHandlerAdapter
它对一个普通的 servlet 适配. servlet 并不知道 ModelAndView, 完全自己处理请求, 将返回写入到相应的 body 中. 因此它的适配器就直接返回 null.
public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { ((Servlet) handler).service(request, response); return null;}
在本例中, controller 是由 @RequestMapping 修饰的 POJO, 因此处理器会使用 HandlerMethod 来封装它的方法. Spring 使用
RequestMappingHandlerAdapter
来适配这种处理器类型.
处理参数, 返回处理器函数的值
注意, 一般来说 controller 并不会接收 HttpServletRequest 和
HttpServletResponse
作为参数, 但是它可以接收和返回很多种其他类型, 例如: 领域对象, 路径参数等. 同样, 也不强求一个 controller 返回一个 ModelAndView 实例. 可以选择返回一个视图名称, ResponseEntity, 或者是一个可以被转换成 JSON 的 POJO.
RequestMappingHandlerAdapter
可以保证从 HttpServletRequest 中解析方法需要的参数, 同时创建 ModelAndView 对象返回. 下面这段代码就是
RequestMappingHandlerAdapter
中保证这件事情的:
ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);if (this.argumentResolvers != null) { invocableMethod.setHandlerMethodArgumentResolvers( this.argumentResolvers);}if (this.returnValueHandlers != null) { invocableMethod.setHandlerMethodReturnValueHandlers( this.returnValueHandlers);}
argumentResolvers 在
HandlerMethodArgumentResolver
实例中有不同实现. 一共有 30 多种不同的参数解析器的实现. 他们可以从请求参数将函数需要的参数解析出来. 包括: url 路径变量, 请求体参数, 请求头, cookies,session 等.
returnValueHandlers
在
HandlerMethodArgumentResolver
实例中有不同实现. 同样也有很多不同的返回值处理器来处理方法返回的结果, 创建 ModelAndView 对象. 例如: 当函数 hello() 返回一个 string 的时候,
ViewNameMethodReturnValueHandler
处理这个值. login() 返回一个 ModelAndView 对象的时候, Sring 使用
ModelAndViewMethodReturnValueHandler
处理这个值.
渲染视图
现在 Spring 已经处理了 HTTP 请求, 获取了 ModelAndView 实例, 现在它需要在用户浏览器渲染 HTML 页面了. 它依赖于由 Model 和选择的模版组成的 ModelAndView 对象. 同样的, Spring 也可以渲染 JSON ,XML 或者其他 HTTP 协议接受的类型. 这些将在接下来的 REST 相关了解更多. 现在回去看一下 DispatcherServlet. render() 首先使用 LocaleResolver 实例设置返回的 Local. 首先假设浏览器已经正确设置 Accetp 头. 默认使用
AcceptHeaderLocaleResolver
来处理. 在渲染过程中, ModelAndView 可以包含一个视图的名字或者是已经选择的视图, 或者如果 controller 依赖于默认视图也可以没有. 既然 hello() 和 login() 方法制定了字符串名字作为视图名称, 所以需要使用 viewResolvers 来查找视图.
for (ViewResolver viewResolver : this.viewResolvers) { View view = viewResolver.resolveViewName(viewName, locale); if (view != null) { return view; }}
ViewResolver 的实现有很多, 这里使用了由 thymeleaf-spring5 提供的
ThymeleafViewResolver
实现. 解析器知道去哪里查找视图, 并且提供相应的视图实例. 调用完 render() 之后, Spring 就完成了将 HTML 页面渲染到用户浏览器的任务.
来源: https://juejin.im/entry/5acd71af518825364001c7e1