二、Web开发
1. SpringMVC自动配置概览
springmvc auto-configuration 自动添加以下功能:
- ContentNegotiatingViewResolver 和 BeanNameViewResolver
- 支持静态资源访问,包括WebJar
- 自动注册
Converter,GenericConverter,Formatter
- 支持
HttpMessageConverters
- 自动注册
MessageCodesResolver
(国际化用) - 静态index.html 页支持
- 自定义
Favicon
- 自动使用
ConfigurableWebBindingInitializer
这都什么呀(⊙ˍ⊙)
自定义规则:
-
@Configuration
+WebMvcConfigurer
部分自定义 -
@EnableWebMvc+@Configuration+DelegatingWebMvcConfiguration
全面自定义 -
声明
WebMvcRegistrations
改变默认底层组件
2. 简单功能分析
2.1 静态资源访问
2.1.1 静态资源目录
默认位置:classpath下的
- static/
- public/
- resources/
- META-INF/resources
如何访问?举个例子:
- 文件A位于
static/data/fileA
- 浏览器访问:http://localhost:8080/data/fileA
- 文件B位于
public/data/fileB
- 浏览器访问:http://localhost:8080/data/fileB
原理:
- 请求进来,先去找Controller看能不能处理。不能处理的所有请求又都交给静态资源处理器。静态资源也找不到则响应404页面
- 所以如果Controller的Mapping和静态资源一样,只会访问Controller
自定义静态资源路径:
spring:
# 映射地址,从此访问静态资源都要加static前缀
mvc:
static-path-pattern: /static/**
# 静态资源目录,修改后只有classpath:/myresources/下的资源才能被访问到
web:
resources:
static-locations: [classpath:/myresources/]
2.1.2 webjar
什么是webjar:
- webjar就是前端框架打成的jar包,它的访问不同于普通静态资源,自定义静态资源路径也不会影响到它
默认映射地址:
- /webjars/**
例子:访问jQuery
- 引入jQuery
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.5.1</version>
</dependency>
- 访问地址:http://localhost:8080/webjars/jquery/3.5.1/jquery.js 后面地址要按照依赖里面的包路径
- Tips:如果访问不到,可能是依赖没导入成功,重启idea解决
2.2 首页
首页位置:
- 静态资源路径/index.html
- 为了能成功访问,不要重新配置静态资源的映射地址
2.3 自定义Favicon
- 将favicon.ico放在静态资源目录下就行了
- 直接改后缀名没有影响
- 访问不到,不是服务器缓存就是浏览器缓存,可以Ctrl + shift + R刷新浏览器,还不行就重启服务器。
2.4 静态资源配置原理
- SpringBoot启动加载 xxxAutoConfiguration 类(自动配置类)
- 因为写了web-starter,所以WebMvcAutoConfiguration生效
WebMvcAutoConfiguration部分代码
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({Servlet.class, DispatcherServlet.class,WebMvcConfigurer.class})
@ConditionalOnMissingBean({WebMvcConfigurationSupport.class})
@AutoConfigureOrder(-2147483638)
@AutoConfigureAfter({DispatcherServletAutoConfiguration.class,
TaskExecutionAutoConfiguration.class,
ValidationAutoConfiguration.class})
public class WebMvcAutoConfiguration {}
- 里面存在一个内部类
- WebMvcAutoConfiguration部分代码
@Configuration(proxyBeanMethods = false)
@Import(EnableWebMvcConfiguration.class)
@EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class })
@Order(0)
public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer {}
- 配置文件的相关属性和属性类进行了绑定。WebMvcProperties==spring.mvc、ResourceProperties==spring.resources
- 该内部类只有一个有参构造,传入的参数是什么(⊙ˍ⊙)?
public WebMvcAutoConfigurationAdapter(
WebProperties webProperties,
WebMvcProperties mvcProperties,
ListableBeanFactory beanFactory,
ObjectProvider<HttpMessageConverters> messageConvertersProvider,
ObjectProvider<WebMvcAutoConfiguration.ResourceHandlerRegistrationCustomizer>
resourceHandlerRegistrationCustomizerProvider,
ObjectProvider<DispatcherServletPath> dispatcherServletPath,
ObjectProvider<ServletRegistrationBean<?>> servletRegistrations
) {
this.mvcProperties = mvcProperties;
this.beanFactory = beanFactory;
this.messageConvertersProvider = messageConvertersProvider;
this.resourceHandlerRegistrationCustomizer = (WebMvcAutoConfiguration.ResourceHandlerRegistrationCustomizer)resourceHandlerRegistrationCustomizerProvider.getIfAvailable();
this.dispatcherServletPath = dispatcherServletPath;
this.servletRegistrations = servletRegistrations;
this.mvcProperties.checkConfiguration();
}
- webjar 和 静态资源 的处理
WebMvcAutoConfiguration部分代码
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
super.addResourceHandlers(registry);
// resources.add-mappings=false 禁用所有静态资源规则
if (!this.resourceProperties.isAddMappings()) {
logger.debug("Default resource handling disabled");
} else {
ServletContext servletContext = this.getServletContext();
// webjar
this.addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/");
// 静态资源
this.addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> {
registration.addResourceLocations(this.resourceProperties.getStaticLocations());
if (servletContext != null) {
registration.addResourceLocations(new Resource[]{new ServletContextResource(servletContext, "/")});
}
});
}
}
- 欢迎页的处理
WebMvcAutoConfiguration部分代码
@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext, FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) {
WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(new TemplateAvailabilityProviders(applicationContext), applicationContext, this.getWelcomePage(), this.mvcProperties.getStaticPathPattern());
welcomePageHandlerMapping.setInterceptors(this.getInterceptors(mvcConversionService, mvcResourceUrlProvider));
welcomePageHandlerMapping.setCorsConfigurations(this.getCorsConfigurations());
return welcomePageHandlerMapping;
}
WelcomePageHandlerMapping的构造函数
WelcomePageHandlerMapping(TemplateAvailabilityProviders templateAvailabilityProviders, ApplicationContext applicationContext, Resource welcomePage, String staticPathPattern) {
// 欢迎页的处理
if (welcomePage != null && "/**".equals(staticPathPattern)) {
logger.info("Adding welcome page: " + welcomePage);
this.setRootViewName("forward:index.html");
} else if (this.welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) {
logger.info("Adding welcome page template: index");
this.setRootViewName("index");
}
}
3. 请求参数处理
3.1 rest使用与原理
3.1.1 什么是rest
- 以前:用不同的mapping来区分功能
- /getUser 获取用户
- /deleteUser 删除用户
- /editUser 修改用户
- /saveUser 保存用户
- 现在:使用同一个mapping——/user,不过请求方法不同
- GET请求-获取用户
- DELETE-删除用户
- PUT-修改用户
- POST-保存用户
3.1.2 如何实现rest
- 请求表单:
<!DOCTYPE html>
<html lang="zh-CH">
<head>
<meta charset="UTF-8">
<title>rest</title>
</head>
<body>
<form method="get" action="/user">
<!--
因为表单只能发get和post请求,所以使用一个隐藏域来发送_method请求参数
服务器的filter就会拿到这个请求参数,以此来做一些操作
-->
<input type="hidden" value="GET" name="_method" >
<input type="submit" value="GET">
</form>
<form method="post" action="/user">
<input type="hidden" value="POST" name="_method" >
<input type="submit" value="POST">
</form>
<form method="post" action="/user">
<input type="hidden" value="DELETE" name="_method" >
<input type="submit" value="DELETE">
</form>
<form method="post" action="/user">
<input type="hidden" value="PUT" name="_method" >
<input type="submit" value="PUT">
</form>
</body>
</html>
- controller
@RestController
public class MyRestController {
// 下面两个注解效果一样
@GetMapping("/user")
// @RequestMapping(value = "/user", method = RequestMethod.GET)
public String getUser() {
return "GET-USER";
}
@PostMapping("/user")
// @RequestMapping(value = "/user", method = RequestMethod.POST)
public String postUser() {
return "POST-USER";
}
@DeleteMapping("/user")
@RequestMapping(value = "/user", method = RequestMethod.DELETE)
public String deleteUser() {
return "DELETE-USER";
}
@PutMapping("/user")
@RequestMapping(value = "/user", method = RequestMethod.PUT)
public String putUser() {
return "PUT-USER";
}
}
- 配置属性
上面说到,我们的表单会带着_method参数交给服务器的filter处理。这个filter就是,HiddenHttpMethodFilter
。这个filter在WebMvcAutoConfiguration中有配置。
WebMvcAutoConfiguration 部分代码
@Bean
@ConditionalOnMissingBean(HiddenHttpMethodFilter.class)
// 只要配置 spring.mvc.hiddenmethod.filter.enabled=true,这个filter就可以加入到容器中
@ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled", matchIfMissing = false)
public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
return new OrderedHiddenHttpMethodFilter();
}
application.yaml
spring:
mvc:
hiddenmethod:
filter:
enabled: true
OrderedHiddenHttpMethodFilter 继承自 HiddenHttpMethodFilter,所以直接看后者
HiddenHttpMethodFilter 部分代码
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
HttpServletRequest requestToUse = request;
// 只有post请求才做操作
if ("POST".equals(request.getMethod()) && request.getAttribute("javax.servlet.error.exception") == null) {
// 获得_method参数
String paramValue = request.getParameter(this.methodParam);
if (StringUtils.hasLength(paramValue)) {
// _method参数全部变大写
String method = paramValue.toUpperCase(Locale.ENGLISH);
// ALLOWED_METHODS包括:PUT.DELETE.PATCH
if (ALLOWED_METHODS.contains(method)) {
// 里面重写了getMethod方法,使之返回_method参数
requestToUse = new HiddenHttpMethodFilter.HttpMethodRequestWrapper(request, method);
}
}
}
// 放行
filterChain.doFilter((ServletRequest)requestToUse, response);
}
- 我能换成其他的名字吗?不用_method
自定义filter
net.tiejiankudan.part03_web.config.Myconfig
@Configuration
public class Myconfig {
@Bean
public HiddenHttpMethodFilter hiddenHttpMethodFilter() {
HiddenHttpMethodFilter filter = new HiddenHttpMethodFilter();
// 改成想要的
filter.setMethodParam("_m");
return filter;
}
}
- Rest使用客户端工具,如PostMan直接发送Put、delete等方式请求,无需Filter。直接就可以实现rest,所以filter默认是没有的
3.2 请求映射原理
学过SSM,我们都知道DispatcherServlet负责找到对应路径的Handler,所以只需研究清楚DispatcherServlet即可。
3.2.1 DispatcherServlet的继承树
- Ctrl + N 搜索 DispatcherServlet 类,打开这个类,Ctrl + H 查看继承关系
- 可以看出它继承自 Httpservlet ,这个相比都很熟悉了。它里面的doGet和doPost方法用来处理请求,
- 所以经过一列盘查,最后由 DispatcherServlet 的 doDispatch 方法间接实现了处理请求的方法
- 所以重点看 doDispatch 方法
3.2.2 doDispatche方法
doDispatche部分代码
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// 找到当前请求使用哪个Handler(Controller的方法)处理
mappedHandler = getHandler(processedRequest);
getHandler方法
@Nullable
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (this.handlerMappings != null) {
// handlerMappings 保存了很多handlerMapping
Iterator var2 = this.handlerMappings.iterator();
while(var2.hasNext()) {
HandlerMapping mapping = (HandlerMapping)var2.next();
HandlerExecutionChain handler = mapping.getHandler(request);
if (handler != null) {
return handler;
}
}
}
return null;
}
- 然后程序会在这几个handlerMapping一一匹配
- mappingRegistry里保存了映射关系
至于getHandler处理的细节以及如何自定义HandlerMapping,这次就先略过吧༼ つ ◕_◕ ༽つ
3.3 方法参数与基本注解
3.3.1 注解
- @PathVariable
利用此注解可以拿到请求路径中的占位参数,搭配rest使用
net.tiejiankudan.part03_web.controll.AnnotationController
@RequestMapping("/car/{id}/owner/{name}")
@ResponseBody
public Map<String, Object> getCar(@PathVariable("id") int id,
@PathVariable("name") String name,
// 将所有的参数封装成map,类似“占位符”:"值"的键值对
// 注意键值都得是String
@PathVariable Map<String, String> pv) {
HashMap<String, Object> map = new HashMap<>();
map.put("id", id);
map.put("name", name);
map.put("pv", pv);
return map;
}
/************************************************************
输入:http://localhost:8080/car/1/owner/leo
输出:{"pv":{"id":"1","name":"leo"},"name":"leo","id":1}
思考:
路径变量mapping:/hehe/{temp}
普通路径mapping: /hehe/haha
请求路径:/hehe/haha 访问谁?
访问普通路径
*************************************************************/
- @RequestHeader
利用此注解可以拿到请求头
net.tiejiankudan.part03_web.controll.AnnotationController
@RequestMapping("/header")
@ResponseBody
public Map<String, Object> getHeader(@RequestHeader("User-Agent") String userAgent,
@RequestHeader Map<String, String> pv) {
HashMap<String, Object> map = new HashMap<>();
map.put("User-Agent", userAgent);
map.put("pv", pv);
return map;
}
/************************************************************
输入:http://localhost:8080/header
输出:{
"pv": {
"host": "localhost:8080",
"connection": "keep-alive",
"sec-ch-ua": "\" Not A;Brand\";v=\"99\", \"Chromium\";v=\"90\", \"Microsoft Edge\";v=\"90\"",
"sec-ch-ua-mobile": "?0",
"upgrade-insecure-requests": "1",
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36 Edg/90.0.818.66",
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
"sec-fetch-site": "none",
"sec-fetch-mode": "navigate",
"sec-fetch-user": "?1",
"sec-fetch-dest": "document",
"accept-encoding": "gzip, deflate, br",
"accept-language": "zh-CN,zh;q=0.9,en-GB;q=0.8,en-US;q=0.7,en;q=0.6",
"cookie": "Idea-e0268f82=6f27a1af-cf8d-46bb-aafe-57ecbd8976d3; locale=zh; chii_theme=light"
},
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36 Edg/90.0.818.66"
}
*************************************************************/
- @RequestParam
利用此注解可以拿到请求参数
net.tiejiankudan.part03_web.controll.AnnotationController
@RequestMapping("/request")
@ResponseBody
public Map<String, Object> getRequestPara(@RequestParam("name") String name,
@RequestParam("age") int age,
@RequestParam Map<String, String> pv) {
HashMap<String, Object> map = new HashMap<>();
map.put("name", name);
map.put("age", age);
map.put("pv", pv);
return map;
}
/************************************************************
输入:http://localhost:8080/request?name=leo&age=17
输出:{"pv":{"name":"leo","age":"17"},"name":"leo","age":17}
*************************************************************/
- @CookieValue
利用此注解可以拿到Cookie信息
net.tiejiankudan.part03_web.controll.AnnotationController
@RequestMapping("/cookie")
@ResponseBody
public Map<String, Object> getCookie(@CookieValue("locale") String localeS,
@CookieValue("locale") Cookie locale,
@CookieValue @RequestParam Map<String, String> cookie) {
HashMap<String, Object> map = new HashMap<>();
map.put("locale", locale);
map.put("localeS", localeS);
map.put("cookie", cookie);
return map;
}
/************************************************************
输入:http://localhost:8080/cookie
输出:{
"localeS": "zh",
"cookie": {},
"locale": {
"name": "locale",
"value": "zh",
"version": 0,
"comment": null,
"domain": null,
"maxAge": -1,
"path": null,
"secure": false,
"httpOnly": false
}
}
结论:Cookie貌似不能自动封装成一个map
*************************************************************/
- @RequestBody
post.html
<!DOCTYPE html>
<html lang="zh-CH">
<head>
<meta charset="UTF-8">
<title>post</title>
</head>
<body>
<form method="post" action="/body">
<div><input type="text" name="name" placeholder="用户名"></div>
<div><input type="text" name="password" placeholder="密码"></div>
<div><input type="submit" value="提交"></div>
</form>
</body>
</html>
net.tiejiankudan.part03_web.controll.AnnotationController
@RequestMapping("/body")
@ResponseBody
public Map<String, Object> getRequestBody(@RequestBody String content) {
HashMap<String, Object> map = new HashMap<>();
map.put("content", content);
return map;
}
/************************************************************
输入:http://localhost:8080/post.html
输出:{"content":"name=%E5%BC%A0%E4%B8%89&password=2333"}
*************************************************************/
- @RequestAttribute
利用这个注解可以得到request域中的数据
net.tiejiankudan.part03_web.controll.AnnotationController
@RequestMapping("/set")
public String setAttribute(HttpServletRequest request) {
request.setAttribute("name", "leo");
return "forward:/attribute";
}
@RequestMapping("/attribute")
@ResponseBody
public Map<String, Object> getAttribute(@RequestAttribute("name") String name) {
HashMap<String, Object> map = new HashMap<>();
map.put("name", name);
return map;
}
/************************************************************
输入:http://localhost:8080/set
输出:{"name":"leo"}
*************************************************************/
- @MatrixVariable
利用这个注解可以获得矩阵变量
问:什么是矩阵变量?
答:跟在路径变量后面,形如
;jessionid=xxxx;uid=xxx
就叫矩阵变量,请记住一定要搭配路径变量使用,他们是一个组合
net.tiejiankudan.part03_web.controll.AnnotationController
@RequestMapping("/matrix/{user}")
@ResponseBody
public Map<String, Object> getAttribute(@MatrixVariable("age") String age,
@MatrixVariable("gender") String gender,
@PathVariable("user") String user) {
HashMap<String, Object> map = new HashMap<>();
map.put("age", age);
map.put("gender", gender);
map.put("user", user);
return map;
}
Spring-boot 默认将矩阵变量给忽略了,所以如果不配置的话,就会包找不到矩阵变量的错误。
所以在需要往容器中自定义WebMvcConfigurer
net.tiejiankudan.part03_web.config.Myconfig
@Bean
public WebMvcConfigurer webMvcConfigurer() {
return new WebMvcConfigurer() {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
UrlPathHelper urlPathHelper = new UrlPathHelper();
// 关键
urlPathHelper.setRemoveSemicolonContent(false);
configurer.setUrlPathHelper(urlPathHelper);
}
};
}
再次访问
/************************************************************
输入:http://localhost:8080/matrix/leo;gender=male;age=19
输出:{"gender":"male","user":"leo","age":"19"}
*************************************************************/
如果有多个路径变量,每个路径变量都可以有自己的矩阵变量
net.tiejiankudan.part03_web.controll.AnnotationController
@RequestMapping("/matrix/{user1}/{user2}")
@ResponseBody
// 用pathVar决定取哪个路径变量的矩阵变量
public Map<String, Object> getAttributes(@MatrixVariable(value = "age", pathVar = "user1") String age1,
@MatrixVariable(value = "gender", pathVar = "user1") String gender1,
@MatrixVariable(value = "age", pathVar = "user1") String age2,
@MatrixVariable(value = "gender", pathVar = "user1") String gender2) {
HashMap<String, Object> map = new HashMap<>();
map.put("age1", age1);
map.put("gender1", gender1);
map.put("age2", age2);
map.put("gender2", gender2);
return map;
}
/************************************************************
输入:http://localhost:8080/matrix/leo;gender=male;age=19/difa;gender=female;age=18
输出:{"gender1":"male","gender2":"female","age2":"18","age1":"19"}
*************************************************************/
- @ModelAttribute
此注解标注的方法会在每个controller调用之前调用一次,利用这个注解可以往模型里放数据,同一次请求,不管是从request域中还是model域中都能获得之前存放的数据。如果直接往model里放数据,如果转发到其它controller不一定能获得,不过倒是可以通过request域获得。
3.3.2 Servlet API
WebRequest、ServletRequest、MultipartRequest、 HttpSession、javax.servlet.http.PushBuilder、Principal、InputStream、Reader、HttpMethod、Locale、TimeZone、ZoneId
ServletRequestMethodArgumentResolver 支持以上部分参数解析
@Override
public boolean supportsParameter(MethodParameter parameter) {
Class<?> paramType = parameter.getParameterType();
return (WebRequest.class.isAssignableFrom(paramType) ||
ServletRequest.class.isAssignableFrom(paramType) ||
MultipartRequest.class.isAssignableFrom(paramType) ||
HttpSession.class.isAssignableFrom(paramType) ||
(pushBuilder != null && pushBuilder.isAssignableFrom(paramType)) ||
Principal.class.isAssignableFrom(paramType) ||
InputStream.class.isAssignableFrom(paramType) ||
Reader.class.isAssignableFrom(paramType) ||
HttpMethod.class == paramType ||
Locale.class == paramType ||
TimeZone.class == paramType ||
ZoneId.class == paramType);
}
3.3.2 复杂参数
- Map、**Model(map、model里面的数据在方法返回后,返回值响应前会被放在request的请求域中)、**Errors/BindingResult、RedirectAttributes( 重定向携带数据)、ServletResponse(response)、SessionStatus、UriComponentsBuilder、ServletUriComponentsBuilder
- Map 和 Model 其实参数解析器解析的结果是同一个,都是**mavContainer.getModel()**的返回值——BindingAwareModelMap。
3.3.3 自定义对象参数
自定义对象可以自动完成封装,支持级联封装
person.html
<!DOCTYPE html>
<html lang="zh-CH">
<head>
<meta charset="UTF-8">
<title>person</title>
</head>
<body>
<form action="/person" method="post">
姓名: <input name="userName" type="text"/> <br/>
年龄: <input name="age" type="text"/> <br/>
// 时间格式XXXX/XX/XX XX:XX:XX 类型转换器的问题
生日: <input name="birth" type="text"/> <br/>
宠物姓名:<input name="pet.name" type="text"/><br/>
宠物年龄:<input name="pet.age" type="text"/><br>
<input type="submit" value="提交"/>
</form>
</body>
</html>
Person.java和Pet.java
@Data
public class Person {
private String userName;
private Integer age;
private Date birth;
private Pet pet;
}
@Data
public class Pet {
private String name;
private String age;
}
net.tiejiankudan.part03_web.controll.AnnotationController
@RequestMapping("/person")
@ResponseBody
public Person getPerson(Person person) {
return person;
}
/************************************************************
输入:http://localhost:8080/person.html
输出:{"userName":"张三","age":19,"birth":"2003-12-14T16:00:00.000+00:00","pet":{"name":"汤姆","age":"3"}}
*************************************************************/
3.3.4 自定义类型转换器
net.tiejiankudan.part03_web.config.Myconfig
@Bean
public WebMvcConfigurer webMvcConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new Converter<String, Pet>() {
@Override
public Pet convert(String s) {
String[] split = s.split(";");
Pet pet = new Pet();
pet.setName(split[0]);
pet.setAge(split[1]);
return pet;
}
});
}
};
}
3.4 参数处理原理
- 在HandlerMapping中找到能够处理请求的Handler
mappedHandler = this.getHandler(processedRequest);
- 为当前Handler找到一个适配器HandlerAdapter
HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
- 适配器执行方法并确定方法参数的每一个值
3.4.1 HandlerAdpater
其中,RequestMappingHandlerAdapter 支持标注@RequestMapping 的方法。
3.4.2 执行目标方法
// ha 就是HandlerAdpater
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
- 进入此方法后
// 发现有执行此方法
protected ModelAndView handleInternal(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {}
- 进入发现handleInternal的核心方法是
// mav 就是要返回的ModeAndView
mav = this.invokeHandlerMethod(request, response, handlerMethod);
- 进入上面的方法内观察
// 做了一系列准备工作后,执行此方法
invocableMethod.invokeAndHandle(webRequest, mavContainer, new Object[0]);
- 进入上面的方法,发现执行下面方法就得到返回值了
Object returnValue = this.invokeForRequest(webRequest, mavContainer, providedArgs);
- 所以继续进入上面的方法
// 一上来就调用方法确定了传入参数值
Object[] args = this.getMethodArgumentValues(request, mavContainer, providedArgs);
- 进入这个得到args的方法,查看他是怎么得到的
// parameters 保存了所有参数的详细信息,如变量类型,参数名,注解信息
MethodParameter[] parameters = this.getMethodParameters();
...
// 判断参数解析器是否支持这个参数的解析
if (!this.resolvers.supportsParameter(parameter)) {
throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
}
- 上面提到了参数解析器,那是什么?根据标注的注解和参数类型会有这么多解析器
- 让我们进入
this.resolvers.supportsParameter(parameter)
内部,看一看是怎么判断的
// 原来就是判断是否能由当前参数得到一个解析器
this.getArgumentResolver(parameter) != null;
- 在进入内部,看它是怎么得到解析器的
@Nullable
private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
HandlerMethodArgumentResolver result = (HandlerMethodArgumentResolver)this.argumentResolverCache.get(parameter);
if (result == null) {
Iterator var3 = this.argumentResolvers.iterator();
while(var3.hasNext()) {
// 就是遍历所有的参数解析器,调用各自的supportsParameter方法判断而已,至于怎么判断的略过
HandlerMethodArgumentResolver resolver = (HandlerMethodArgumentResolver)var3.next();
if (resolver.supportsParameter(parameter)) {
result = resolver;
this.argumentResolverCache.put(parameter, resolver);
break;
}
}
}
return result;
}
- 现在我们得到了相应的解析器并加入到了缓存当中,现在我们回到
getMethodArgumentValues
方法,就是上面判断是否存在一个解析器解析参数的方法中
// 用参数解析器解析参数
args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
-
现在我们就得到了所有的参数值了,回到
invokeForRequest
方法,利用反射调用方法得到返回值 -
最后我们回到
invokeAndHandle
方法,就一目了然了
// 经过了这么多步,我们终于得到了返回值
Object returnValue = this.invokeForRequest(webRequest, mavContainer, providedArgs);
- 得到返回值后我们又要调用返回值处理器,来处理返回值。下面列出了所有返回值处理器
- 同样的方式,遍历所有处理器看哪个能处理就给他处理,后续的步骤就略过了o( ̄▽ ̄)d
3.5 POJO封装过程
pojo参数的处理过程,只不过pojo是复杂数据类型所以用到的参数解析器是ServletModelAttributeMethodProcessor,利用这解析器就将我们自定义的对象封装好了,然后传给方法执行。那么这个解析器是怎么工作的呢?
-
由 **WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name)**创建出binder
-
WebDataBinder:web数据绑定器,将请求参数的值绑定到指定的JavaBean里面
-
WebDataBinder 利用它里面的 Converters 将请求数据转成指定的数据类型。再次封装到JavaBean中
-
GenericConversionService:在设置每一个值的时候,找它里面的所有converter哪个可以将这个数据类型(request带来参数的字符串)转换到指定的类型
3.6 处理派发结果
略