流程概括

以使用了
@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解析请求中携带的参数,而参数解析器会使用 消息转换器MessageConverter(针对请求体数据) 和WebDataBinder把请求参数中的数据转换为controller方法参数:- 如果是
@RequestParam,会使用WebDataBinder把请求参数中的数据,绑定转换为controller方法参数,并进行Validation校验。 - 如果是
@RequestBody,会使用消息转换器MessageConverter把请求参数中的数据,转换为controller方法参数,并使用WebDataBinder进行Validation校验。
然后适配器会通过反射方式执行
Controller对应的请求处理方法。注意:
WebDataBinder与MessageConverter都可以将http中的参数转换为controller方法的java参数,在不同的注解中有不同的应用。MessageConverter主要作用在请求体或响应体,用于将请求体数据转为controller方法参数,以及将controller执行后的返回值转为json或其他格式数据存入响应体用于返回。 - 如果是
-
在适配器执行完处理器
Controller的方法后,会使用返回值处理器HandlerMethodReturnValueHandler,对Controller方法执行后生成的返回值进行处理,返回值处理器会调用消息转换器MessageConverter,并根据请求头中的accept,将返回值类型转换为json或xml等请求希望接收到的返回类型。最后返回值处理器会将转换格式后的结果数据设置到响应体中并返回。 -
关于视图解析与视图模型:
如果处理器
Controller返回类型不是json或xml、没有使用@ResponseBody或@RestController注解,而是指定返回ModelAndView对象、字符串页面等,则会使用处理视图模型的返回值处理器进行处理(例如ViewNameMethodReturnValueHandler,根据实际的场景有所不同),并将返回值封装成ModelAndView对象,最后由视图解析器ViewResolver解析为视图View返回前端展现(比如代码中return "login",直接返回静态资源login.html登录页面)。
流程解析
1.初始化映射器
示例代码
一个
Controller:
源码入口
在开启自动配置的autoConfigure包的imports文件中,导入了mvc的自动配置类WebMvcAutoConfiguration:

WebMvcAutoConfiguration内配置了EnableWebMvcConfiguration,它继承于DelegatingWebMvcConfiguration:

而DelegatingWebMvcConfiguration又继承了WebMvcConfigurationSupport:

最后在WebMvcConfigurationSupport中注册了处理映射器RequestMappingHandlerMapping(不止有它,拦截器、处理器适配器等关键mvc组件都是在这个类中注册到spring中的):

源码中配置的顺序是:
WebMvcAutoConfiguration
↓ 配置了
EnableWebMvcConfiguration
↓ 继承
DelegatingWebMvcConfiguration
↓ 继承
WebMvcConfigurationSupport
↓ 配置了
RequestMappingHandlerMapping
处理器映射器
RequestMappingHandlerMapping的初始化发生在其父类AbstractHandlerMethodMapping中,因为父类实现了InitializingBean,所以在项目启动后进行bean初始化时,会进入AbstractHandlerMethodMapping类中的initHandlerMethods()方法,进行处理器映射器的初始化(此处具体原理参考bean的生命周期初始化阶段)
afterPropertiesSet是AbstractHandlerMethodMapping 实现InitializingBean后重写的初始化方法,该方法内部调用了initHandlerMethods

进入初始化方法 :AbstractHandlerMethodMapping类 --》initHandlerMethods方法 ,可以看到上面自定义的Controller:

下面看它的初始化逻辑:
执行初始化,保存请求映射关系
进入上图红框中的
processCandidateBean方法。
其中的isHandler会判断bean是否被@Controller、@RequestMapping注解修饰,如果是,就会进入 detectHandlerMethods 方法中。这也是处理器映射器RequestMappingHandlerMapping对应处理 @RequestMapping 注解的原因


如果被
@Controller或@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过程处理

768






