1 yaml
YAML是"YAML Ain’t a Markup Language"(YAML不是一种标记语言)的递归缩写。在开发的这种语言时,YAML 的意思其实是:“Yet Another Markup Language”(仍是一种标记语言),但为了强调这种语言以数据做为中心,而不是以标记语言为重点,而用反向缩略语重命名。
说明YAML就是为了配置数据而专门设计的。在SpringBoot中用于替代xml配置文件。
语法注意是k:v形式,层级以空格分隔。
SpringBoot中默认命名为application.yaml,演示xml配置文件与yaml文件对比,明显看出yaml文件易于阅读:
server.port=8888
spring.banner.charset=UTF-8
spring.banner.location=xxx
spring.cache.cache-names=xxx
server:port:8811spring:banner:charset: utf-8cache:cache-names: xxx
2 静态资源
2.1 访问静态资源
从官网文档可知,只需要是放在resources路径下的这些目录都是可访问的静态资源:/static
(or/public
or/resources
or/META-INF/resources
)
比如test.jpg的访问路径为:localhost:8080/test.jpg
简单原理:
SpringBoot默认情况下,会匹配/**访问路径。如果存在动态的访问路径,如配置了controller的访问路径也是/test.jpg,就会访问动态的路径,否则会匹配静态资源的路径。
可以修改配置来更改静态资源的访问路径:之后访问就是localhost:8080/resources/test.jpg
spring:mvc:static-path-pattern: /resources/**
比如:在/static目录下放一个heart.png,用浏览器打开:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Cv0b4rHl-1614753646372)(C:\Users\zl\AppData\Roaming\Typora\typora-user-images\image-20210227214702794.png)]
2.2 静态资源访问源码解析
分析源码,入口都是在相应的autoConfiguration自动配置类中个,SpringMVC的内容,在WebMVCAutoConfiguration类中,进入查看:
package org.springframework.boot.autoconfigure.web.servlet;@Configuration(proxyBeanMethods=false)@ConditionalOnWebApplication(type= Type.SERVLET)@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class})@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE+10)@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
ValidationAutoConfiguration.class})publicclassWebMvcAutoConfiguration{
。。。}
以上分析可以得知:
- 这是一个配置类,type类型必须为Type.SERVLET
- 容器中必须存在Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class 三个类型的组件。
- 容器中不存在WebMvcConfigurationSupport.class类型组件,说明可以自定义WebMvcConfiguration
- @AutoConfigureOrder和@AutoConfigureAfter指示了创建顺序
分析WebMvcAutoConfiguration里面,有一个内部类EnableWebMvcConfiguration,也是一个配置类,它的构造方法如下:
publicEnableWebMvcConfiguration(
org.springframework.boot.autoconfigure.web.ResourceProperties resourceProperties,
WebMvcProperties mvcProperties, WebProperties webProperties,
ObjectProvider<WebMvcRegistrations> mvcRegistrationsProvider,
ObjectProvider<ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider,
ListableBeanFactory beanFactory){this.resourceProperties= resourceProperties.hasBeenCustomized()? resourceProperties: webProperties.getResources();this.mvcProperties= mvcProperties;this.webProperties= webProperties;this.mvcRegistrations= mvcRegistrationsProvider.getIfUnique();this.resourceHandlerRegistrationCustomizer= resourceHandlerRegistrationCustomizerProvider.getIfAvailable();this.beanFactory= beanFactory;}
在SpringBoot中,如果配置类只有一个有参构造器,构造器的参数值会从容器中自动确定。
这个构造器方法,是配置类组件获得了各种配置资源。
在EnableWebMvcConfiguration配置类中有个addResourceHandlers,用于确定静态资源访问路径的。
@OverrideprotectedvoidaddResourceHandlers(ResourceHandlerRegistry registry){super.addResourceHandlers(registry);if(!this.resourceProperties.isAddMappings()){
logger.debug("Default resource handling disabled");return;}
ServletContext servletContext=getServletContext();addResourceHandler(registry,"/webjars/**","classpath:/META-INF/resources/webjars/");addResourceHandler(registry,this.mvcProperties.getStaticPathPattern(),(registration)->{
registration.addResourceLocations(this.resourceProperties.getStaticLocations());if(servletContext!= null){
registration.addResourceLocations(newServletContextResource(servletContext, SERVLET_LOCATION));}});}
从中可以看出:
- "/webjars/**"请求会匹配到 "classpath:/META-INF/resources/webjars/"路径下的资源。
- 静态资源的请求默认是匹配"/**",处理的路径是{ “classpath:/META-INF/resources/”,
“classpath:/resources/”, “classpath:/static/”, “classpath:/public/” }- this.mvcProperties.getStaticPathPattern()对应是"/**"
- this.resourceProperties.getStaticLocations()对应的是{ “classpath:/META-INF/resources/”,
“classpath:/resources/”, “classpath:/static/”, “classpath:/public/” }
3 web请求
3.1 请求映射的原理
首先我们知道SpringMVC处理请求都是通过DispatcherServlet来处理。通过DispatchServlet的继承关系可以得知,它是一个HttpServlet,那么处理请求的方法就是doGet和doPost。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4xUB7faY-1614753646376)(C:\Users\zl\AppData\Roaming\Typora\typora-user-images\image-20210228123415457.png)]
分析源码可得到如下的调用关系:
doGet/doPost —> processRequest(request, response) --> doService(request, response) --> doDispatch(request, response)
重点分析doDispatch方法:(代码只保留了关键的步骤)
protectedvoiddoDispatch(HttpServletRequest request, HttpServletResponse response)throws Exception{
HttpServletRequest processedRequest= request;
HandlerExecutionChain mappedHandler= null;// 根据request获取执行的handler,也就是Controller的具体执行方法.
mappedHandler=getHandler(processedRequest);// 根据handler获取handler适配器.
HandlerAdapter ha=getHandlerAdapter(mappedHandler.getHandler());// 真正执行handler的地方.
mv= ha.handle(processedRequest, response, mappedHandler.getHandler());}
DispatchServlet如何根据请求找到执行方法,就是通过请求映射。
解析getHandler方法:
@Nullableprotected HandlerExecutionChaingetHandler(HttpServletRequest request)throws Exception{if(this.handlerMappings!= null){for(HandlerMapping mapping:this.handlerMappings){
HandlerExecutionChain handler= mapping.getHandler(request);if(handler!= null){return handler;}}}return null;}
有一个handlerMappings属性,它是List,说明有多个HandlerMapping。对于请求,会循环所有的HandlerMapping,只需要有一个HandlerMapping里能匹配到响应的方法即可。
也可自己添加自定义的HandlerMapping到容器中,自定义HandlerMapping
通过打断点,可以看到,SpringBoot中有个5个HandlerMapping,第一个RequestMappingHandlerMapping中有/hello对应的getHello方法。所以访问localhost:8080/hello就执行这个方法,即handler。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nKpmBHuG-1614753646378)(C:\Users\zl\AppData\Roaming\Typora\typora-user-images\image-20210228125519102.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gbtBVJ6p-1614753646383)(C:\Users\zl\AppData\Roaming\Typora\typora-user-images\image-20210228125250889.png)]
总结:HandlerMapping的作用其实就是请求路径与执行方法的映射。
3.2 常用请求参数注解
@PathVariable:路径变量
- 使用RESTFUL风格时使用,可以获取请求路径中的变量。如localhost:8080/hello/zhangsan/10
- 两种方式:一种是使用value,一种使用Map获取所有路径参数。
@RequestHeader: 请求头
@RequestParam: 请求参数
- 传统的参数,如localhost:8080/hello?name=zhangsan&age=10
@CookieValue:请求的Cookie
@RequestBody:请求体
- 针对于post请求,获取post请求的请求体。
@RequestMapping("/param/{name}/{age}")public StringgetTest(@PathVariable("name") String name,@PathVariable("age")int age1,@PathVariable Map<String,String> pathVariable,@RequestHeader("User-Agent") String agent,@RequestHeader Map<String, String> header,@RequestParam("name") String name2,@RequestParam("age")int age2,@RequestParam Map<String,String> requestParam,@CookieValue("_ga") String ga,@CookieValue Cookie cookie,@RequestBody String content){return"hello";}
通过实例可知,很多注解除了使用value值获取单个值外,还可以一次性全部获取相关的值。如@PathVariable Map<String ,String> pathVariable,一次性获取所有路径请求的值。
- @RequestAttribute: 请求属性
- 在转发、过滤器、拦截器中想HttpServletRequest对象中使用setAttribute添加值时,可以通过@RequestAttribute注解获取值。
- 和使用getAttribute方法获取值的效果是一样的。
@ControllerpublicclassParamController{@RequestMapping("/goto")public StringtestA(HttpServletRequest request){
request.setAttribute("msg","hello test B");return"forward:/success";//转发}@ResponseBody@RequestMapping("/success")public StringtestB(@RequestAttribute("msg") String msg){
request.getAttribute("msg");return msg;}}
- @MatrixVariable: 矩阵变量
- 可以在请求路径中放置更多的值。如localhost:8080/hello/boss;name=zhangsan;age=50/10
- 不太常用,更多语法及使用上网查查。
- @ModelAttribute
3.3 参数解析 - 源码分析
在编写controller类里的方法的时候,即handler方法时,参数是比较随意和多样的,SpringBoot在反射执行该方法时,是需要给参数赋值的,如果确定各个参数的值是这次需要分析的点。
之前分析请求映射的原理时,分析了如何通过handlerMapping获取执行的handler。获取到mappedHandler之后,会根据handler获取handler适配器,再通过适配器执行handler。如下所示:
protectedvoiddoDispatch(HttpServletRequest request, HttpServletResponse response)throws Exception{
HttpServletRequest processedRequest= request;
HandlerExecutionChain mappedHandler= null;// 根据request获取执行的handler,也就是Controller的具体执行方法.
mappedHandler=getHandler(processedRequest);// 根据handler获取handler适配器.
HandlerAdapter ha=getHandlerAdapter(mappedHandler.getHandler());// 真正执行handler的地方.
mv= ha.handle(processedRequest, response, mappedHandler.getHandler());}
分析getHandlerAdapter(mappedHandler.getHandler())
protected HandlerAdaptergetHandlerAdapter(Object handler)throws ServletException{if(this.handlerAdapters!= null){for(HandlerAdapter adapter:this.handlerAdapters){if(adapter.supports(handler)){return adapter;}}}}
同handlerMapping,也有多个handlerAdapter,打断点可知,有四个适配器。其中,RequestMappingHandlerAdapter就是适配RequestMapping这种注解形式的访问的。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PHueoMgO-1614753646385)(C:\Users\zl\AppData\Roaming\Typora\typora-user-images\image-20210228184607906.png)]
获取handlerAdapter之后,会执行ha.handle方法,该方法的执行路径是:
ha.handle --> handleInternal --> invokeHandlerMethod
invokeHandlerMethod方法的简化版如下:
protected ModelAndViewinvokeHandlerMethod(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod)throws Exception{
ServletInvocableHandlerMethod invocableMethod=createInvocableHandlerMethod(handlerMethod);if(this.argumentResolvers!= null){
invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);}if(this.returnValueHandlers!= null){
invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);}
invocableMethod.invokeAndHandle(webRequest, mavContainer);}
可以看到:
- this.argumentResolvers: 参数解析器
- 通过断点可知,有27种参数解析器。
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zCP8huhL-1614753646386)(C:\Users\zl\AppData\Roaming\Typora\typora-user-images\image-20210228185513535.png)]
- this.returnValueHandlers: 返回值处理器
- 这里有15中返回值处理器。
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-epvgHDys-1614753646388)(C:\Users\zl\AppData\Roaming\Typora\typora-user-images\image-20210228185705116.png)]
继续探究invocableMethod.invokeAndHandle(webRequest, mavContainer):
执行路径:invokeAndHandle --> invokeForRequest --> getMethodArgumentValues
在getMethodArgumentValues方法中,就是确定每个变量的值了。
protected Object[]getMethodArgumentValues(NativeWebRequest request,@Nullable ModelAndViewContainer mavContainer,
Object... providedArgs)throws Exception{
Object[] args=newObject[parameters.length];for(int i=0; i< parameters.length; i++){if(!this.resolvers.supportsParameter(parameter)){throw xxx;}
args[i]=this.resolvers.resolveArgument(parameter, mavContainer, request,this.dataBinderFactory);}return args;}
先来看一下参数处理器的接口结构:
publicinterfaceHandlerMethodArgumentResolver{booleansupportsParameter(MethodParameter parameter);@Nullable
ObjectresolveArgument(MethodParameter parameter,@Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,@Nullable WebDataBinderFactory binderFactory)throws Exception;}
可以知道,handler方法参数处理器的执行过程就是先调用supportsParameter查看是否支持,再调用resolveArgument确定参数。
在getMethodArgumentValues方法中调用this.resolvers.resolveArgument获取参数:
public ObjectresolveArgument(MethodParameter parameter,@Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,@Nullable WebDataBinderFactory binderFactory)throws Exception{//根据参数,获取相应的参数处理器
HandlerMethodArgumentResolver resolver=getArgumentResolver(parameter);//调用resolveArgument确定参数return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);}
分析getArgumentResolver方法:
private HandlerMethodArgumentResolvergetArgumentResolver(MethodParameter parameter){
HandlerMethodArgumentResolver result=this.argumentResolverCache.get(parameter);if(result== null){for(HandlerMethodArgumentResolver resolver:this.argumentResolvers){if(resolver.supportsParameter(parameter)){
result= resolver;this.argumentResolverCache.put(parameter, result);break;}}}return result;}
可以看出:
- 从之前提到的27种参数解析器中选择一个支持该参数的,并返回。
- 这里有一个缓存优化,this.argumentResolverCache.put(parameter, result),这样下次访问就不要再遍历一次所有的参数处理器了。
至此,参数就是解析完成,会得到一个Object[] args,并且附上了正确的值。在invokeForRequest方法调用doInvoke(args)执行。
3.4 常用参数的解析器
Servlet API
- WebRequest、ServletRequest、MultipartRequest、 HttpSession、javax.servlet.http.PushBuilder、Principal、InputStream、Reader、HttpMethod、Locale、TimeZone、ZoneId
- 解析器是:ServletRequestMethodArgumentResolver
Map
- MapMethodArgumentResolver
Model
- ModelMethodArgumentResolver
应该能发现规律了。
map和model在作为参数时,在视图返回时,都会放到request.setAttribute中。
例如:在/goto转发到/success之后,可以通过request获取值。
@RequestMapping("/goto")public StringtestA(Map<String,Object> map, Model model, HttpServletRequest request){
map.put("k1","val1");
model.addAttribute("k2","val2");
request.setAttribute("k3","val3");return"forward:/success";}@ResponseBody@RequestMapping("/success")public StringtestB(HttpServletRequest request){
request.getAttribute("k1");//val1
request.getAttribute("k2");//val2
request.getAttribute("k3");//val3return"msg";}
原理在processDispatchResult解析视图中的exposeModelAsRequestAttributes方法:
protectedvoidexposeModelAsRequestAttributes(Map<String, Object> model,
HttpServletRequest request)throws Exception{
model.forEach((name, value)->{if(value!= null){
request.setAttribute(name, value);}else{
request.removeAttribute(name);}});}
3.5 自定义参数解析 - 源码
@ControllerpublicclassParamController{@ResponseBody@RequestMapping("/user")public UsergetUser(User user){//user就是自定义参数return user;}}
自定义参数使用了参数解析器是:ServletModelAttributeMethodProcessor
在该处理器的resolveArgument方法中(删除不必要的部分):
publicfinal ObjectresolveArgument(MethodParameter parameter,@Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,@Nullable WebDataBinderFactory binderFactory)throws Exception{
Object attribute= null;
BindingResult bindingResult= null;
attribute=createAttribute(name, parameter, binderFactory, webRequest);if(bindingResult== null){// binder -- 代码01
WebDataBinder binder= binderFactory.createBinder(webRequest, attribute, name);if(binder.getTarget()!= null){if(!mavContainer.isBindingDisabled(name)){bindRequestParameters(binder, webRequest);//-- 代码02}validateIfApplicable(binder, parameter);}
bindingResult= binder.getBindingResult();}// Add resolved attribute and BindingResult at the end of the model
Map<String, Object> bindingResultModel= bindingResult.getModel();
mavContainer.removeAttributes(bindingResultModel);
mavContainer.addAllAttributes(bindingResultModel);return attribute;}
在代码01处,创建了一个WebDataBinder对象,查看内部结构,可知:http传回参数是字符串文本,要把它解析为具体的对象,都是使用相应的转换器的。
WebDataBinder对象的内部数据:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AIJQx5zH-1614753646390)(C:\Users\zl\AppData\Roaming\Typora\typora-user-images\image-20210301110625217.png)]
看一下StringtoNumber最终执行的地方,也是工厂模式,最终执行是convert方法。
finalclassStringToNumberConverterFactoryimplementsConverterFactory<String, Number>{@Overridepublic<TextendsNumber> Converter<String, T>getConverter(Class<T> targetType){returnnewStringToNumber<>(targetType);}privatestaticfinalclassStringToNumber<TextendsNumber>implementsConverter<String, T>{privatefinal Class<T> targetType;publicStringToNumber(Class<T> targetType){this.targetType= targetType;}@Override@Nullablepublic Tconvert(String source){if(source.isEmpty()){return null;}return NumberUtils.parseNumber(source,this.targetType);}}}