流程概括
以使用了
@RequestMapping
注解的Controller为例,流程概括:
-
spring项目启动,扫描到
@Controller
注解,将其标记的类解析进行bean的初始化,初始化过程中会将Controller类、方法、及其中配置的请求等信息加载到处理器映射器HandlerMapping
中,并将请求URL和对应的处理器Controller映射关系保存到MappingRegistry
里。 -
当http请求来临后,进入前端控制器
DispatcherServlet
进行处理,前端控制器会调用处理映射器HandlerMapping
。处理映射器根据请求信息,去MappingRegistry
映射关系中找到对应处理该请求的处理器Controller,并将Controller处理信息封装到执行链HandlerExecutionChain
中返回给前端控制器。 -
前端控制器
DispatcherServlet
会根据返回的执行链HandlerExecutionChain
,获取对应的处理适配器HandlerAdapter
,取得的处理器适配器会通过反射执行执行链中的处理器Controller
方法。 如果有配置拦截器Interceptor
,那么适配器会在执行Controller的请求方法之前和之后,分别执行拦截器的前置和后置方法。 -
在**处理适配器
HandlerAdapter
执行处理器Controller
的处理方法前,会通过参数解析器HandlerMethodArgumentResolver
解析请求中携带的参数,而参数解析器会使用WebDataBinder
或消息转换器MessageConverter
把请求参数中的数据转换为controller方法参数:- 如果是
@RequestParam
,会使用WebDataBinder
把请求参数中的数据,绑定转换为controller方法参数,并进行Validation
校验。 - 如果是
@RequestBody
,会使用消息转换器MessageConverter
把请求参数中的数据,转换为controller方法参数,并使用WebDataBinder
进行Validation
校验。
然后适配器会通过反射方式执行
Controller
对应的请求处理方法。
注意:WebDataBinder
与MessageConverter
都可以将http中的参数转换为controller方法的java参数,在不同的注解中有不同的应用。 - 如果是
-
在适配器执行完处理器
Controller
的方法后,会使用返回值处理器HandlerMethodReturnValueHandler
,对Controller
方法处理后生成的返回值进行处理,返回值处理器会调用消息转换器MessageConverter
,并根据请求头中的accept
,将返回值类型转换为json
或xml
等请求希望接收到的返回类型。最后返回值处理器会将转换格式后的结果数据设置到响应体中并返回。 -
关于视图解析与视图模型:
如果处理器
Controller
返回类型不是json
或xml
、没有使用@ResponseBody
或@RestController
注解,而是指定返回ModelAndView
对象、字符串页面等,则会使用处理视图模型的返回值处理器进行处理(例如ViewNameMethodReturnValueHandler
,根据实际的场景有所不同),并将返回值封装成ModelAndView
对象,最后由视图解析器ViewResolver
解析为视图View
返回前端展现(比如代码中return "login"
,直接返回静态资源login.html登录页面)。
补充:关于消息转换器MessageConverter
,并不是只在处理返回值进行响应时会用。比如在处理请求时,如果用了@RequestBody
注解,那么RequestResponseBodyMethodProcessor
就会用消息转换器处理请求体参数。除此之外,它还可以在许多其他场景下发挥作用,如消息转发和异步支持等等。
流程解析
1.初始化映射器
示例代码
一个
Controller
:
源码入口
AbstractHandlerMethodMapping
是对应@RequestMapping
注解的处理器映射器RequestMappingHandlerMapping
的父类,在Spring项目启动后,因为它实现了
InitializingBean
,在进行bean初始化时,断点会进入AbstractHandlerMethodMapping
类中的initHandlerMethods()
方法,进行处理器映射器的初始化(此处具体原理参考bean的生命周期初始化阶段)。
进入初始化方法 :AbstractHandlerMethodMapping
类 --》initHandlerMethods
方法
下面看它的初始化逻辑:
执行初始化,保存请求映射关系
进入上图红框中的
processCandidateBean
方法。
其中的isHandler
会判断bean是否被@Controller
、@RequestMapping
注解修饰,如果是,就会进入 detectHandlerMethods
方法中。这也是处理器映射器RequestMappingHandlerMapping
对应处理 @RequestMapping
注解的原因
如果被
@Controlle
r或@RequestMapping
注解修饰,就会进入detectHandlerMethods
方法中,把Controller作为参数传入。然后保存
@RequestMapping
注解配置的url
与Controller
映射关系,后续spring收到请求时,就能根据此映射关系来寻找相应的Controller进行处理了。
进入 detectHandlerMethods
方法:
关键处理步骤:getMappingForMethod
,该方法会进入处理器射器RequestMappingHandlerMapping
的实现中,返回一个有请求处理信息的RequestMappingInfo
。
根据RequestMappingInfo
获取控制器Controller中的请求处理方法,然后将Controller中@RequestMapping
参数里的请求URL和处理方法之间的映射关系注册到MappingRegistry
中。
注册步骤在下图断点处registerHandlerMethod
方法:
可以看到mapping
中包含实际业务开发的Controller中的请求路径。
进入
registerHandlerMethod
方法,一直到AbstractHandlerMethodMapping
的register
方法中:
将mapping
信息put到MappingRegistry
中
后续
前端控制器
收到请求时,就会通过查找MappingRegistry
中的映射信息,获得对应的Controller进行处理了
2.初始化拦截器
自定义的拦截器在实现
HandlerInterceptor
接口、并通过@Configuration
配置后,会在项目启动时自动纳入容器管理,并初始化
有一个WebMvcConfigurationSupport
:mvc配置支持类,对springMVC所有配置做初始化,所有的HandlerMapping
都会添加拦截器,此处只列举RequestMapping
的
//WebMvcConfigurationSupport中所有handlerMapping都有这句代码
mapping.setInterceptors(getInterceptors(conversionService, resourceUrlProvider));
进入
WebMvcConfigurationSupport
的getInterceptors
方法,获得拦截器(拦截器随项目启动自动配置,此处直接获取
)
自己开发的拦截器:
将上一步
getInterceptors
方法获得的拦截器返回进行set,给到RequestMappingHandlerMapping
映射器中
为Handler
添加拦截器的
setInterceptors(getInterceptors(conversionService, resourceUrlProvider));
方法,是RequestMappingHandlerMapping
从 AbstractHandlerMapping
继承来的,
所以断点执行都是在AbstractHandlerMapping
类的代码中,但实际属于RequestMappingHandlerMapping
执行完
setInterceptors
方法后,RequestMappingHandlerMapping
获得拦截器
可以看到,此处的interceptors
集合,包含自定义的拦截器IndexInterceptor
:
执行
RequestMappingHandlerMapping
的拦截器初始化方法initInterceptors
将上一步得到的拦截器,添加到adaptedInterceptors
集合中,用于后续存入执行链。
下图是在遍历添加的过程中,将开发者自定义的拦截器加入集合:
对应上一步,
RequestMappingHandlerMapping
得到了拦截器。
adaptedInterceptors
集合中的拦截器会在后续请求处理时放入执行链
3.mvc请求处理过程
示例代码
@RestController
public class IndexController {
@RequestMapping("/dem")
public Demo demo( @RequestParam(value = "age") String age){
System.out.println(age);
Demo d = new Demo();
d.setOne("1");
d.setTwo("2");
d.setThree("3");
d.setFour("4");
return d;
}
}
浏览器请求/dem,进入下面步骤:
请求进入
DispatcherServlet
前端控制器
DispatcherServlet
由Servlet
继承而来。mvc的功能都从
org.springframework.web.servlet.DispatcherServlet
--》doDispatch
方法开始
获取处理器映射器
进入
DispatcherServlet
--》doDispatch
--》getHandler(processedRequest)
方法,先确定处理请求要使用哪个**
HandlerMapping
处理器映射器**,再用它来获取执行链。
getHandler
具体获取步骤:
- 首先要进入
getHandler
方法,获取HandlerMapping
处理器映射器。 getHandler
方法会挨个遍历所有的HandlerMapping
处理器映射器实现类,看哪个有请求信息,就用哪个。
HandlerMapping
下有5个实现,分别对应不同的请求配置场景:
-
BeanNameUrlHandlerMapping:xml配置,相当于bean的名字就是controller的访问路径,例如:
<bean id="/test" class="com.hrms.controller.TestController"/>
-
RequestMappingHandlerMapping:只需要写
@Controller
以及@RequestMapping
注解即可,无需配置xml,最为常用 -
WelcomePageHandlerMapping:用于处理欢迎页
-
RouterFunctionMapping:主要用于WebFlux响应式处理,是webflux中新引入的HandlerMapping
-
SimpleUrlHandlerMapping:也是xml的方式,在bean中手动配置controller的访问路径,如:
<bean name="helloController" class="com.zhengfa.controller.HelloController"/> <bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping"> <property name="urlMap"> <props> <prop key="/hello.html"> helloController </prop> </props> </property> </bean>
当我们在实际中只使用@RequestMapping
注解,那么代码运行起来后,就能在对应@RequestMapping
注解的映射器RequestMappingHandlerMapping
中获取到请求信息了。下面介绍具体获取逻辑。
获取执行链
有了处理映射器,就需要用它来获得执行链了,接上面
getHandler
方法,下面进入AbstractHandlerMapping
源码中看具体获取逻辑:
进入AbstractHandlerMapping --》 getHandler 方法,再进入黄框getHandlerInternal方法
断点继续执行,会到达AbstractHandlerMethodMapping
类中的getHandlerInternal
方法中,从lookupHandlerMethod
方法中获取到HandlerMethod
lookupHandlerMethod
方法内部:
- 会根据请求信息去
MappingRegistry
保存的映射关系中匹配对应的请求路径,生成一个HandleMethod
,后面用它来封装成执行链HandlerExecutionChain
。
HandlerMethod
中的信息,包含了业务Controller中的内容。也就是要处理当前请求所需要的Controller及其方法等信息
获取到HandlerMethod后,回到
AbstractHandlerMapping
类中的getHandler
方法调用处,将
HandlerMethod
封装到HandlerExecutionChain
执行链的handler
属性中,并返回给前端控制器注意:执行链中有Handler属性,不同的Handler将对应不同的**
HandlerAdapter
**适配器,下面会介绍到
进入
getHandler
--》getHandlerExecutionChain
方法,断点执行过程中:
- 通过构造方法将
HandlerMethod
赋予到执行链HandlerExecutionChain
的Handler
属性中- 遍历
RequestMappingHandlerMapping
的拦截器,将**adaptedInterceptors
**的拦截器放入执行链,并返回包含拦截器的执行链
通过构造方法将HandlerMethod赋予到执行链HandlerExecutionChain
的Handler
属性中
遍历RequestMappingHandlerMapping
的拦截器,将adaptedInterceptors
的拦截器放入执行链(来源在第一步拦截器的初始化),并返回包含拦截器的执行链
如果有自定义的拦截器,就会在这里添加进去
返回执行链
HandlerExecutionChain
给 前端控制器DispatcherServlet
获取适配器
根据执行链来获取适配器,适配器也和处理映射器一样,因请求配置的不同,会得到不同的适配器。
因为示例代码使用的是@RequestMapping
注解,所以会返回 RequestMappingHandlerAdapter
适配器,下面介绍步骤:
还是在 DispatcherServlet --》 doDispatch( ) 中
进入上图
getHandlerAdapter
方法中,来获取适配器
可以看到适配器有多种,分别对应不同的场景:
RequestMappingHandlerAdapter
:当执行链中的handler
属性是HandlerMethod
时,返回此适配器HandlerFunctionAdapter
:当执行链中的handler
属性是HandlerFunction
时,返回此适配器SimpleControllerHandlerAdapter
:当执行链中的handler
属性是Controller
(org.springframework.web.servlet.mvc.Controller)时,返回此适配器HttpRequestHandlerAdapter
:当执行链中的handler
属性是HttpRequestHandler
时,返回此适配器SimpleServletHandlerAdapter
:当执行链中的``handler属性是
Servlet`时,返回此适配器
遍历所有实现,通过supports
方法来获取合适的适配器
:
在遍历到RequestMappingHandlerAdapter
适配器时,因为继承关系, 断点会执行AbstractHandlerMethodAdapter
中的supports
判断执行链的handler
属性是否为HandlerMethod
:
如果handler是HandlerMethod
,那么supportsInternal
方法强制转换将不会失败,返回true
根据文章上一步的执行链,当执行链中的handler是
HandlerMethod
时,会返回RequestMappingHandlerAdapter
适配器
所以最终返回适配器RequestMappingHandlerAdapter
:
适配器执行
在获取到合适的适配器以后,会执行handle
方法,handle
中会利用反射执行controller
,并将controller的业务处理结果封装为ModelAndView
视图模型 或 其他数据类型json、xml等返回
但是在handle之前和之后,会分别执行2个方法:
mappedHandler.applyPreHandle(processedRequest, response); mappedHandler.applyPostHandle(processedRequest, response, mv);
其实就是取执行链
HandlerExecutionChain
中的interceptors
,也就是我们开发的拦截器,分别执行其pre和post方法,来做一些自定义的请求拦截处理
下图红框为适配器获取ModelAndView
视图模型的方法,黄蓝框对应拦截器的请求前置与后置方法:
有拦截器的话,先执行拦截器Pre前置方法,对应上图蓝框,不具体介绍拦截器方法执行,下面重点说handle的执行:
进入上图红框代码handle
方法中,看适配器执行过程:
进入后到达AbstractHandlerMethodAdapter
--》handle
方法,此处传参时将执行链的handler
转为了HandlerMethod
,作为后续使用:
进入handleInternal
,到达RequestMappingHandlerAdapter
--》 handleInternal
中:
继续进入上图蓝色断点代码invokeHandlerMethod
中,如下图:
为Controller方法的调用类invocableMethod
赋予参数解析器与返回值处理器:
参数解析器
会解析请求参数,返回值处理器
会将Controller处理的数据结果进行处理封装,后续添加到response响应中
补充:
注意调用类
ServletInvocableHandlerMethod
,是根据执行链中的HandlerMethod
创建出来的,它包含要调用的Controller相关信息
有了参数解析器与返回值处理器后,下面会执行Controller
的方法:
此方法用于执行目标Controller方法,并获取目标方法返回值,其中涉及到参数解析器、返回值处理器、消息转换器等
继续执行断点,到达位置:RequestMappingHandlerAdapter
--》invokeHandlerMethod
方法,下图红框代码:
进入红框代码内,到达ServletInvocableHandlerMethod
--》 invokeAndHandle
方法,
进入上图断点invokeForRequest
,来到InvocableHandlerMethod
--》invokeForRequest
方法,进行请求处理,大致分为两步:
- 获取参数解析器,并解析请求中的参数;
- 通过参数执行Controller方法;
下面看参数解析器的获取
获取参数解析器
在invokeForRequest
方法里,通过上图的getMethodArgumentValues
方法获取请求参数,进入其中:
getMethodParameters
方法会获取Controller方法参数使用的注解:
演示代码使用的是@RequestParam
,所以得到的也是它:
继续向下走,判断是否有支持处理该注解的参数解析器
,通过supportsParameter
方法判断:
this.resolvers
中有很多参数解析器,supportsParameter
方法会遍历所有参数解析器,判断有没有能解析Controller
方法参数的实现,如果没有能处理的就抛出异常。
supportsParameter
方法代码,在参数解析器接口HandlerMethodArgumentResolver
中:
进入上面的supportsParameter
方法,当循环遍历到解析器实现类RequestParamMethodArgumentResolver
时,执行它重写的supportsParameter
方法,判断Controller方法参数是否使用@RequestParam
注解,下图是它的实现:
因为用了@RequestParam
,所以会在RequestParamMethodArgumentResolver
的参数解析器实现中判断成功,不会抛出异常。
接上一步
supportsParameter
方法之后,执行resolveArgument
:
断点位置:InvocableHandlerMethod --》 this.resolvers.resolveArgument,如下图:
resolveArgument
里面会获取具体的参数解析器实例,并用这个实例进行请求参数解析,并将解析后的参数返回
代码位置到了HandlerMethodArgumentResolverComposite
类中的resolveArgument
方法,如下:
getArgumentResolver
获取解析器实例时,里面还会使用supportsParameter
方法。因为用了@RequestParam
,所以会得到RequestParamMethodArgumentResolver
的参数解析器实现,如上图黄框,就不重复演示了。
然后进行请求参数的解析工作,执行断点位置代码:
return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
进行参数解析
断点进入到**
RequestParamMethodArgumentResolver
解析器**的resolveArgument
方法中,走具体的解析逻辑
因为继承关系,此时开发工具的断点实际是在AbstractNamedValueMethodArgumentResolver
类的resolveArgument
方法上:
然后到达如下断点位置,使用WebDataBinder
进行请求参数的转换,将http请求携带的参数,转为Controller的方法参数并接收:
进入上图断点
binder.convertIfNecessary
中,做两件事:
- 获取DataBinder的实现
- 进行参数类型转换
WebDataBinder
的主要职责
- 请求参数与 Java 对象之间的绑定:将 HTTP 请求的参数转换为控制器方法的参数类型。
- 验证:绑定参数时支持验证(例如通过
@Valid
注解进行 Bean Validation)。 - 类型转换:支持将请求参数转换为复杂类型,例如日期、枚举、自定义类型等。
- 字段格式化:可以通过
@InitBinder
方法指定格式化(如日期格式化)。
WebDataBinder
会调用TypeConverterSupport
来进行类型转换及参数绑定:
TypeConverterSupport
:类型转换,它负责将一种类型的值转换为另一种类型的值。它提供了类型转换的方法,并可以通过注册自定义的转换器来扩展转换功能。
TypeConverterSupport
会使用内置的类型转换器(PropertyEditor
或 Converter
)来进一步进行参数转换处理:
PropertyEditor
:这是 Java 传统的类型转换机制,Spring 提供了默认的PropertyEditor
实现,用于常见的类型(如Date
、Number
)的转换。Converter
和Formatter
:Spring 提供了更强大的Converter
和Formatter
机制,这比PropertyEditor
更加灵活,推荐在自定义类型转换时使用。
根据示例代码,我们使用的是基本类型参数String,进入
TypeConverterSupport
,在如下代码执行转换:
TypeConverterDelegate
类 --》 convertIfNecessary
方法
从断点处看到源类型即http请求参数类型为String
类型,目标类型即Controller方法类型也是String
类型,对应最上面的Controller
示例代码
converters
具体有上百种,对应java中各种各样的类型转换:
因为请求中的参数是String
类型,Controller方法中参数也是String
类型,因为是同类型,所以就用了空转换器NoOpConverter
,不做转换直接返回。看下图的name:NO_OP
:
NoOpConverter
直接返回解析的参数:
如果我将Controller方法参数改为了Integer
,那么他就用字符串转数字的转换器了
最后,回到
InvocableHandlerMethod
--》invokeForRequest
方法的调用位置,完成请求参数的解析,将参数用于doInvoke
方法,执行目标Controller
。
上图参数对应请求url参数:
请求参数解析完,返回到
InvocableHandlerMethod
--》invokeForRequest
,下一步执行目标方法doInvoke
:
处理器执行
接上图进入doInvoke
方法,进入java.lang.reflect
包下的Method
类的invoke
方法(java反射):
上一步doInvoke
方法会执行目标Controller的请求处理方法,最后返回Controller的执行结果:
对应了最开始的示例Controller
然后下一步,用返回值处理器处理返回结果
获取返回值处理器
invoke处理完controller的方法后,获取返回值处理器来处理返回值
断点继续执行到达:ServletInvocableHandlerMethod
--》 invokeAndHandle
--》 this.returnValueHandlers.handleReturnValue
,如下图:
进入断点代码handleReturnValue
方法内:
执行
selectHandler
方法,通过遍历获取对应的返回值处理器
源码中与上面的参数解析器套路一致,使用supportsReturnType
来获得支持的返回值处理器
示例代码中使用了@RestController
注解,它是@Controller
+ @ResponseBody
的组合,所以遍历到返回值处理器的实现类RequestResponseBodyMethodProcessor
时,它的supportsReturnType
方法判断为true。
到这里确定了使用的返回值处理器为RequestResponseBodyMethodProcessor
。
获取消息转换器
得到了返回值处理器后,继续执行断点,到返回值处理方法
handleReturnValue
,进行返回值的处理
位置:HandlerMethodReturnValueHandlerComposite
--》 handleReturnValue
:
进入handleReturnValue
方法中,执行RequestResponseBodyMethodProcessor
返回值处理器中的writeWithMessageConverters
:
再进入writeWithMessageConverters
方法中,遍历所有消息转换器
,获取能将controller
方法返回值转为http请求所需返回类型的转换器:
位置:AbstractMessageConverterMethodProcessor
--》writeWithMessageConverters
方法内
可以看见10个具体的消息转换器实现
断点向下走,执行
canWrite
方法,看哪个消息转换器可以将Controller的返回值-自定义java类型Demo
,转换为http请求头accept中的application/json
- 参数valueType:对应示例Controller的返回值自定义
Demo
类 - 参数selectedMediaType:对应http请求头中
Accept
的application/json
遍历发现,
AbstractJackson2HttpMessageConverter
类会返回true,证明它可以将自定义java类型转为json格式:
下面进行消息转换及响应
消息响应
执行下图的
write
方法,做消息转换并将数据写到Response
中
断点位置:AbstractMessageConverterMethodProcessor --》writeWithMessageConverters方法内,下方断点处:
调用到上面获取的消息转换器
AbstractJackson2HttpMessageConverter
类中的writeInternal
方法,通过http响应将转换完的json字符串返回浏览器:
浏览器显示:
然后断点会回到前端控制器
DispatcherServlet
--》doDispatch
:
因为返回json格式数据不会使用ModelAndView
视图模型,所以代码里的mv为null,后面也不会渲染视图模型。
视图解析
因为返回
json
不涉及视图解析
,这里说下返回html页面等类型的视图视图解析流程处理
改下示例代码,返回login字符串,并且项目静态资源中有此页面。
@Controller
public class IndexController {
@RequestMapping("/login")
public String login(){
return "login";
}
}
浏览器输入
"/login"
请求,进入DispatcherServlet
前端控制器
执行适配器的handle
方法,获取视图模型mv
进入适配器RequestMappingHandlerAdapter
,调用invokeHandlerMethod
执行目标方法
继续走断点到invokeHandlerMethod
方法内,创建ModelAndViewContainer
实例并作为参数传入invokeAndHandle
方法:
ModelAndViewContainer
是Spring MVC框架中的一个容器类,用于存储和传递处理器方法的模型Model
和视图View
断点继续执行到达:ServletInvocableHandlerMethod --》 invokeAndHandle --》 this.returnValueHandlers.handleReturnValue,获取返回值处理器并处理返回值
selectHandler
方法遍历获取支持解析"login"
字符串的返回值处理器
循环到ViewNameMethodReturnValueHandler
时,通过supportsReturnType
方法判断它支持解析"login"
字符串
判断逻辑:参数类型为空,或者是CharSequence及其子类。
CharSequence
是字符串序列,是String
类型的父接口
获得ViewNameMethodReturnValueHandler
返回值处理器实例
用ViewNameMethodReturnValueHandler
对返回值进行处理:
进入ViewNameMethodReturnValueHandler
中的 handleReturnValue
方法:
主要是处理
mavContainer
:为
mavContainer
赋予视图名,即"login"
,并判断是不是重定向视图,因为字符串中没有"redirect"
重定向关键词,所以不是重定向的视图。
回到适配器RequestMappingHandlerAdapter
中调用getModelAndView
方法获取视图模型:
上一步处理了
mavContainer
,这里用它去取获取视图模型ModelAndView
getModelAndView
方法处理完成返回视图模型ModelAndView
最终回到
DispatcherServlet
前端控制器,调用processDispatchResult
方法处理返回结果
上面介绍消息响应时应为用的是@ResponseBody
,返回json数据则mv是空。但是现在这里看到mv视图模型不是空了
processDispatchResult
方法中再调用render
方法进行视图渲染
render
中执行resolveViewName
方法获取视图解析器ViewResolver
并进行渲染
resolveViewName
方法会遍历所有视图解析器的实现类,调用他们的resolveViewName
方法来进行处理
不同的解析器对应了不同的场景
这里只有ContentNegotiatingViewResolver
可以成功处理,它会用于根据客户端的请求内容类型选择合适的视图进行渲染。
进入视图解析器
ContentNegotiatingViewResolver
的resolveViewName
方法,看他如何渲染
getMediaTypes()
:会根据请求属性来获得请求中的所有媒体类型getCandidateViews()
:根据请求的媒体类型(Media Type)选择合适的视图进行渲染,返回所有可以进行渲染的候选视图getBestView()
:根据请求的媒体类型与候选视图的媒体类型进行匹配,选择最合适的视图进行渲染
最后返回ThymeleafView
视图,调用他的renderFragment
方法渲染视图,并在此方法中将视图数据写入response
响应中给回前端,最终会将"login"字符串渲染为"login.html",并将项目static文件夹中静态资源的此页面展现在浏览器
最后通过梳理流程发现,mvc流程中的核心组件大都可以由开发者进行自定义扩展或实现,来定制自己的mvc过程处理