请求映射
什么是请求映射?
网页发来不同的请求,我们在自己的controller做出相互对应的操作,就是请求映射
为什么需要请求映射?
之前我们写不同的请求,都是通过请求不同的路径,后端再次做出判断并且完成对应的操作
现在我们可以只要写同一个路径,通过使用HTTP请求方式动词来表示对资源的操作,从而完成对应的操作(也就是Rest注解方式)
使用Rest风格注解进行请求映射
现在我们的请求路径都是/user,但是现在对zhangsan如何操作如何分辨?
使用method指定请求方式来实现
但是现在有一个问题,我们前端指定相互对应的请求方法只有post和get,这样子可以吗?
测试
对delete和put都是get张三,这就不对了
也就是说我们强制书写的两个请求方式其实并不支持
那现在应该怎么做?
之前我们的springMVC中是配置了一个MVC的http请求拦截器,用来实现相互对应
现在我们的springboot是怎么做的?
其实springboot在源码也给我配置了这个东西,那为什么没有起作用?
也就是我们的Rest功能应该是可以正常使用的
怎么做源码也告诉我们了
我们只需要配一个名称为_method的隐藏参数就可以了
我们还是需要在method里填写post方式,因为源码要求了只有是post请求方式才会拿到this.methodParam(也就是我们在前端写的_method里写的东西),所以正确书写格式应该如下
再次测试
还是不可以
再次分析
查看源码
前两个注解都满足要求,但是第三个的第三个内容
如果我们自己没有配置就不开启,所以我们要把这个过滤器手动开启
在配置文件中自己开启
最终测试
成功了
Rest原理(基于前端表单提交)(因为表单只能写get或者post)
1.表单提交会带上_method=PUT
2.请求过来被HiddenHttpMethodFilter拦截
查看拦截器源码
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
HttpServletRequest requestToUse = request;
if ("POST".equals(request.getMethod()) && request.getAttribute("javax.servlet.error.exception") == null) {
String paramValue = request.getParameter(this.methodParam);
if (StringUtils.hasLength(paramValue)) {
String method = paramValue.toUpperCase(Locale.ENGLISH);
if (ALLOWED_METHODS.contains(method)) {
requestToUse = new HiddenHttpMethodFilter.HttpMethodRequestWrapper(request, method);
}
}
}
HttpServletRequest requestToUse = request;
首先拿到我们最初始的原生请求(POST)
if ("POST".equals(request.getMethod()) && request.getAttribute("javax.servlet.error.exception") == null)
判断我们的原生请求是不是POST(解释了为什么我们的method要一致填写POST)
并且判断请求有没有出错误
String paramValue = request.getParameter(this.methodParam);
获取我们隐藏起来的_method里面写的真正想请求的是什么
String method = paramValue.toUpperCase(Locale.ENGLISH);
不管我们的_method里面写的真正数值是什么,全部转换为英文大写
if (ALLOWED_METHODS.contains(method))
查看源码允许的请求方式包不包括我们_method里面写的真正数值,允许的请求方式有
PUT ,DELETE, PATCH
requestToUse = new HiddenHttpMethodFilter.HttpMethodRequestWrapper(request, method);
调用了HttpMethodRequestWrapper的一个方法,点击查看
3.HttpMethodRequestWrapper的方法
我们发现这个wapper还是继承了基础的httpservlet,在这个wapper里做了
接受了 _method里面写的真正数值传参,并且赋值给了自己的method,最后用自己的执行了httpservlet的getMethod方法
所以我们真正的HttpServlet的方法(请求处理),调用的是wapper里的method的值,但是wapper的值是从前面的_method里传入的,这样就形成了一个闭环,解释了,为什么我们后端写的接收请求当时和_method里写的方式能够对应上
这个wapper也被称作为包装模式
为什么需要这么麻烦还需要手动对这种东西进行开启?
因为表单传达请求的方式只能是post或者get,对于其他第三方可以直接发delete或者put的就不需要这样子了,我们以后前后端分离的项目也不需要这样,所以我们要按需开启
Rest原理(基于第三方提交)
更加简介的书写方式
更改前面的Rest就可以了
测试成功
如何改变_method(换一种写法可以吗)
首先我们一定不能该源码,但是之前我们在分析的时候看到了,如果我们自己配置,就不会使用源码的了,那我们自己写一个配置类,再写一个拦截器组件加到容器中不就可以了吗
@Configuration
public class WebConfig {
@Bean
public HiddenHttpMethodFilter hiddenHttpMethodFilter(){
HiddenHttpMethodFilter hiddenHttpMethodFilter = new HiddenHttpMethodFilter();
hiddenHttpMethodFilter.setMethodParam("_m");
return hiddenHttpMethodFilter;
}
}
测试成功
请求映射的原理(网页发过来请求,是怎么在对应的controller找到对应的方法进行处理的)
还是老话,springboot项目启动springMVC模块的时候,第一步肯定都是要找中央处理器的
但是我们的DispatchServlet说到底还是一个servlet,肯定要重写doget和dopost方法的,但是我们springboot把它写在了哪里呢?
也就是说,我们的doget和dopost最后都被转化为了doDispatch,我们来看看这个 doDispatch到底都做了一些什么事情
SpringMVC功能分析都从 org.springframework.web.servlet.DispatcherServlet---->>>doDispatch
我们找到源码并且发送一个get请求尝试一下
现在如果我们发送一个请求,doDispatch会获取到,现在请求的内容应该是这样子的
mappedHandler = this.getHandler(processedRequest);
if (mappedHandler == null) {
this.noHandlerFound(processedRequest, response);
return;
}
但是如果我们放行了这句代码
mappedHandler = this.getHandler(processedRequest);
mappedHandler就直接帮我们找到了HelloController的getUser方法(正是我们要找到的)
说明问题出在mappedHandler这里,也就是找到当前请求使用哪个Handler(Controller的方法)处理
我们这个代码传入的参数是processedRequest也就是传入的请求路径是哪个(现在是/user)
我们点击这句代码的源码查看
我们发现这里面有handlerMappings,这个里面的内容是
handlerMapping到底是个啥?——是处理器映射
springMVC怎么知道哪个请求要用谁处理就是通过handlerMapping里面的映射规则来知道的(默认handlerMappings里面有五个)
而且现在会对这五个handlerMapping进行循环,一个一个的寻找
里面也有我们比较熟悉的welcomehandlerMapping,我们可以查看
通过第一行和最后一行,我们知道了,如果我们访问/,那么就跳转到index.html
我们/访问哪里,是这个handlerMapping保存的规则而已
但是现在更重要的是第一个默认的
RequestMappingHandlerMapping:保存了所有@RequestMapping 和handler的映射规则
我们之前写在controller上面的注解就是以@RestContrller的方式来书写所有请求的
这里面存放了一个注册中心,内容就是我们自己写的controller与请求的映射规则
但是我们现在的四种请求都是/user,它怎么区分的呢,我们继续点击源码查看
首先确定了当前请求的路径
然后带着现在的路径和请求调用方法
点击查看方法源码
使用路径在注册中心(上文中已有注册中心的内容),中找到和我们路径相同的请求对应的方式
当前路径为/user,但是对应的有四种,也找到了四种
然后会把所有找到的东西放到一个集合当中
并且这个方法内部会检测到当前的请求方式为GET方法并且返回
最后如果这个集合的长度为1,就调用第一个方式
如果长度大于1,会抛出异常(说明此时无论是请求方法还是请求参数有两个方法能够执行)
springboot规定了同样的请求路径,同样的请求方式只能有一个
总结:
关于web请求传参的一些常用注解(也就是前端给后端传参)
常用注解的传参
@PathVariable(路径变量注解)
之前我们前端传参都是这种
如果这样子写就把id给写死了,后端不可能给每个id都写一个方法用来处理
现在即使前端设置的id是动态的,我们后端也可以正常获取,前端说传递的是多少就是多少,然后后端用该注解进行获取
这样子就代表前端传入的参数id和name是动态的
利用注解获取 参数
而且,springboot还帮我们想好了,如果方法内要对前端传入的参数封装为map类型这种情况,我们只需要
这样子我们的pv里就有所有动态请求的值了
测试:
@RequestHeader(获取请求头里的信息注解)
这个注解可以让我们获得请求的时候同时获得请求头里面的信息,用法与之前类似
而且springboot还可以获得请求头里的全部信息,但是需要用Map格式来封装
测试
@RequestParam(获取请求参数注解)
我们的请求可能还会有带参数的情况,我们使用这个注解获取请求中的参数
当然也可以用map封装所有的请求参数
测试
@CookieValue(获取cookie参数的注解)
我们cookie里面有一些值也是可以获取到的
现在只是获取了_ga的数值,我们也可以获得整个_ga的cookie信息
测试
浏览器没有cookie无法测试
@RequestBody(获取请求体参数)
如果现在有一个表单发过来了,我们可以获得表单的参数
注意:表单提交时post请求
测试成功
@RequestAttribute(获取请求域中参数的值)
我们之前的JavaWeb项目中,都是获取请求域参数的值,现在使用该注解同样可以获取
这个方法获得httpservlet,向请求域中放入参数,跳转到success页面
这个方法就是要跳转到的success页面,可以通过注解获得请求域之间的参数
我们使用了两种方式,第一种是注解,第二种由于此时的页面跳转肯定用到的是一个reques,所以也可以引入request,通过getAttribute取出来,但结果应该是一样的
@MatrixVariable(获取矩阵变量里的参数注解)
什么是矩阵变量?
前端发出这种路径请求,意思是在汽车的销售页面找到最低价格34,并且品牌为BYD等的车子
以前的一道面试题,如果cookie被禁止使用了,如何获取到session的内容,就可以用矩阵变量方式来解决
下面这个意思是在boos路径下年龄为20岁的1号老板,并且这个老板手下的年龄为20的2号员工
测试使用注解获取矩阵变量的值
页面说没有找到我们的这个low的矩阵变量
源码里帮我们移除了分号之后的内容,我们不想这样做
相当于是我们要自己定制化springMVC组件的功能
springboot给了我们三种方案
方案一:
也就是在我们的配置类中有一个自己定义的(自己修改的)webMVCconfigurer就可以了,也就是自己定义一个同名的组件
webMVCconfigurer是什么?——是一个接口
而且我们的核心配置类是实现了这个接口的
我们也可以重写这个我们想修改的方法就可以做出改变了
那么我们现在就有两种方法了
(1)在配置类中继承接口并且重写对应方法
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
UrlPathHelper urlPathHelper = new UrlPathHelper();
//设置为不移除分号后的内容
urlPathHelper.setRemoveSemicolonContent(false);
configurer.setUrlPathHelper(urlPathHelper);
}
(2)在配置类中自己写一个想要用的组件
@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
UrlPathHelper urlPathHelper = new UrlPathHelper();
//设置为不移除分号后的内容
urlPathHelper.setRemoveSemicolonContent(false);
configurer.setUrlPathHelper(urlPathHelper);
}
};
}
现在再次测试
还是报错,但是现在是404
这是为什么?——我们需要把我们的路径设置为路径参数的形式才可以获取到
可以看看我们设置的路径变量是带分号还是不带分号的
@GetMapping("/cars/{abc}")
public Map carSell(@MatrixVariable("low") Integer low,
@MatrixVariable("brand") List<String> brand,
@PathVariable("abc") String abc){
Map<String, Object> map = new HashMap<>();
map.put("low",low);
map.put("brand",brand);
map.put("abc",abc);
return map;
}
成功了,而且我们看到,得到的路径是不带分号后面内容的
可能出现的问题(矩阵变量里变量名相同的情况)
这样子一看就有歧义,但是源码里说了,矩阵变量注解的属性还可以设置pathvar,通过不同的var判断当前要找到的是哪个矩阵变量里的值
@GetMapping("/boss/{bossId}/{empId}")
public Map boss(@MatrixVariable(value = "age",pathVar = "bossId") Integer bossAge,
@MatrixVariable(value = "age",pathVar = "empId") Integer empAge){
Map<String, Object> map = new HashMap<>();
map.put("bossId",bossAge);
map.put("empId",empAge);
return map;
}
测试成功
注解实现获取参数的原理
现在我们学会了注解的使用方法,但是springboot底层是如何对这种东西进行实现的呢?
再次声明,处理请求的所有入口一定是DispatchServlet类里的doDispatch方法
我们前端发出用注解获取参数的请求,看看后端的这个doDispatch源码
这边已经为我们找到了请求会找到哪个方法做出对应操作,放在mapperHandler里面
再继续向下走
HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
这行代码的明确作用是:
1:mapperHandler调用方法,找到能处理请求的handler
2:再次调用方法为当前这个handler找一个适配器HandlerAdapter
HandlerAdapter(适配器)是个啥?
这玩意其实是一个接口,作用就是,看看自己支持哪一种handler,如果支持的话就调用真正的方法进行处理
以后我们也可以自己定义handlerAdapter,告诉springMVC我们的适配器支持处理哪一种handler
如何得到对应的适配器的?
点击查看源码
这个方法比较简单,就是封装后再返回对应的handler
点击查看源码
protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
if (this.handlerAdapters != null) {
Iterator var2 = this.handlerAdapters.iterator();
while(var2.hasNext()) {
HandlerAdapter adapter = (HandlerAdapter)var2.next();
if (adapter.supports(handler)) {
return adapter;
}
}
}
我们大体看到里面有一个循环,寻找我们传入的这个handler是否和handlerAdapters里面的东西匹配,但是handlerAdapters这是啥?
里面默认有四个东西
我们现在的请求就是找我们controller里写的呀,所以在找第一个的时候就找到了
if (adapter.supports(handler)) {
return adapter;
}
进行判断,我们传入的这个handler是不是我们适配器能用的,那怎么样才算能用呢?点击查看
我们看到,如果传入的这个handler是HandlerMethod类型的就代表说是可以用的
很凑巧的是符合的,所以现在我们已经成功的返回了适配器,又回到了doDispatch方法
又回到了doDispatch方法,继续向下看(现在获取到的适配器是RequestMappingHandlerAdapter)
我们再继续向下看dodispatch
这个方法使用我们的适配器,把目标handler,目标方法,req,resp都执行了
我们点击查看
我们发现最后会执行这个方法
里面的这一行代码
mav = this.invokeHandlerMethod(request, response, handlerMethod);
这个方法就是对handler进行真正的执行,如何执行的我们还要点击查看
我们看到这个里面有一个argumentResolvers,这个里面的内容是
我们看到这些东西的开头与我们之前写的注解很像,这玩意叫做参数解析器
参数解析器
参数解析器是个啥?
实际上是一个接口,主要是确定将要执行的方法,参数是否支持我们进行使用?
也就是我们标注的注解是什么,比如
这个接口里定义了什么?
returnValueHandlers(返回值处理器)
返回值处理器又是个什么?我们可以查看内容
也即是写了注解的方法返回值设定的类型在源码里也规定好了
我们继续向下看
invocableMethod再次封装
这里面的内容是啥?
这里面就封装了我们的参数解析器和返回值处理器
我们继续向下看
真正的执行handler
我们在这句代码中真正要执行了我们的handler
invocableMethod.invokeAndHandle(webRequest, mavContainer, new Object[0]);
再次点击源码查看
通过调用得知,执行结束这句代码,我们就要执行handler里面的方法了
Object returnValue = this.invokeForRequest(webRequest, mavContainer, providedArgs);
说明这里面还大有文章
现在我们的问题,我们有了参数解析器知道了可能用的什么注解方法,有了返回值处理器知道了可能要返回什么类型的东西,但是我们的注解方法里还有参数,怎么把参数拿到呢?(到目前为止其实都是在准备数据)
我们再次点击查看源码
里面这行代码的args
Object[] args = this.getMethodArgumentValues(request, mavContainer, providedArgs);
我们执行完这一步,就发现args里面已经有我们需要的,从前端获取的参数名称和参数值了
说明这里面还有东西,我们点击查看
只差参数名称和参数,这个代码终于介绍如何获取详细参数的了
获取参数名称(注解名称)中的参数值
protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
MethodParameter[] parameters = this.getMethodParameters();
if (ObjectUtils.isEmpty(parameters)) {
return EMPTY_ARGS;
} else {
Object[] args = new Object[parameters.length];
for(int i = 0; i < parameters.length; ++i) {
MethodParameter parameter = parameters[i];
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
args[i] = findProvidedArgument(parameter, providedArgs);
if (args[i] == null) {
if (!this.resolvers.supportsParameter(parameter)) {
throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
}
try {
args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
} catch (Exception var10) {
if (this.logger.isDebugEnabled()) {
String exMsg = var10.getMessage();
if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) {
this.logger.debug(formatArgumentError(parameter, exMsg));
}
}
throw var10;
}
}
}
return args;
}
}
MethodParameter[] parameters = this.getMethodParameters();
对我们所使用方法的参数获取详细信息,现在已经获取到了参数名称(注解名称)但是此时还没有获取到参数值,并且返回这个列表
if (ObjectUtils.isEmpty(parameters)) {
return EMPTY_ARGS;
}
判断当前列表是不是空的
Object[] args = new Object[parameters.length];
如果不为空,就new一个和我们列表长度相同的args,然后循环遍历我们的列表
args[i] = findProvidedArgument(parameter, providedArgs);
第一次循环,准备给args0赋值
if (!this.resolvers.supportsParameter(parameter)) {
看一下我们的解析器支不支持我们当前的列表
它是如何做的,深入代码
我们看到实际上,我们已经定死了有哪些参数解析器(26种),用for循环来和我们当前获取的参数列表进行比对,如果找到了就break,如何判断的就是if和else了(通过比较类)
现在才是真正的确定了要用哪种参数解析器
args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
真正的解析参数
点击源码查看,是怎么样最终确定的
@Nullable
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
HandlerMethodArgumentResolver resolver = this.getArgumentResolver(parameter);
if (resolver == null) {
throw new IllegalArgumentException("Unsupported parameter type [" + parameter.getParameterType().getName() + "]. supportsParameter should be called first.");
} else {
return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
}
}
HandlerMethodArgumentResolver resolver = this.getArgumentResolver(parameter);
得到当前参数对应的真正需要的那个唯一的解析器
return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
调用对应的resolve方法,查看源码
查看当前要解析的参数的名字
也就是id
解析id的值,如何解析还要点击查看
之前我们说过urlPathHelper,这玩意会把我们前端路径的值给解析好并且放到请求域当中
我们在请求域中获取到id的值就可以了,然后再一步步向上级返回
传入Servlet API
我们之前的代码中就其实已经传入了servlet的api,我们下面将探究为什么可以这样子传入,底层原理是什么
准备工作:
其实之前的准备工作流程都是一样的,但是到了循环遍历当前的参数是不是在参数解析器内的时候就不一样了
进行对当前获取的参数类型(Httprequest)和参数解析器是否支持进行判断:
点击单步执行 :
我们发现对于ServletRequest参数解析器来说,如果参数类型是ServletRequest内的(这其中就包括Httprequest)就判断为可以,返回为真,表示匹配
找到了,返回出来:
之后也是调用args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);这个方法进行解析,但是由于现在参数解析器不同,解析方法也不同
我们发现现在传递的参数里面有一个webRequest,这里面都有什么内容呢?
这个对象相当于把我们原生的request和respone一起组合了
下面执行这个代码进行详细解析
我们点击源码看看到底是怎么解析的
首先判断是不是webrequest,我们不是,向下执行
判断是不是servletrequest,我们是的,执行改行代码
查看源码
我们看到它拿我们传递的参数进行了封装,这个封装最后变成了什么样子呢?
原来这个封装的东西就是我们原生的request,最后返回就可以了
一些复杂参数的原地与原理(Model,Map之类的)
我们在参数中放Map或Model都是相当于给请求域中放入的
测试:
@GetMapping("/params")
public String testParam(Map<String,Object> map,
Model model,
HttpServletResponse response,
HttpServletRequest request){
map.put("111","111");
model.addAttribute("2222","22222");
request.setAttribute("3333","33333");
Cookie cookie = new Cookie("4444","44444");
response.addCookie(cookie);
return "forward:/success";
}
成功了,说明可以这样子放,但是为什么呢?我们需要了解原理
放入复杂参数的原理
准备工作:
其实之前的准备工作流程都是一样的,但是到了循环遍历当前的参数是否和参数解析器内的东西可以相互对应时候就不一样了
找到了Map对应的参数解析器
之前的流程都很像,快进到找到对应的参数解析器进行解析
使用参数解析器进行解析
现在已经找到了Map类型的参数解析器,我们看看做了什么操作
使用mavContainer进行了getModel,这玩意是个啥?
点击mavContainer源码查看
返回了一个defaultModel
这玩意又是个啥,再次点击查看
这玩意其实上是一个BindAwaerModelMap,这玩意又是啥?
保留这个疑问,现在我们先定一个小结论
现在我们的步骤还是在获得参数名称和与之对应的构造器集合,现在Map的相当于找到了,该找Model的了,现在的args0是
找到了Model的对应参数构造器
找到了Map对应的参数解析器
之前的流程都很像,快进到找到对应的参数解析器进行解析
使用参数解析器进行解析
我们发现和之前的Map调用的方法一模一样
拿现在我们可以更新一下我们的结论了
而且现在获取的args1的内容是什么呢?
甚至是类型都一模一样
结束了 获取前端所有的参数名称
现在假设我们已经获取了所有的东西也就是进行到了上文的这一步
最后一行invoke到我们的controller里面执行了
那么再向上返回应该是调用完invokeForRequest方法之后,那么该执行
也即是该跳转到success页面了,但是我们的代码里面并没有说把map和model的内容放到请求域中呀,下面我们就来解决这个问题
源码如何把map和model的内容放到请求域中的
注意:现在我们的mavContainer里已经有了我们map和model的值了
所以我们应该关注这个mavContainer都做了什么事情
那我们就跟着上面的代码来呗
这边设置了我们的相应状态,我们点击查看源码
其他的不重要,但是这句代码要开始处理返回结果了,并且也携带了mavContainer,点击查看
里面有这样一串代码,如果返回值是一个字符串,就把这个字符串保存到 mavContainer里面
现在可以解释这个mavContainer到底是什么了
ModelAndViewContainer——模型(model数据)和视图(View也就是我们要跳转到哪里)处理器
我们之前都只有model,加入了要跳转的页面字符串之后就全面了
返回值处理结束后会有一个获取ModelAndView对象,我们下一步
然后说要更新model,点击下一步
从mavContainer里拿到了defaultModel
然后把mavContainer封装为ModelAndView对象
判断model是不是要重定向,如果要重定向,就把model放回请求上下文当中
但是我们现在不是,最后返回
最后这个mavContainer执行回来了doDispatch当中,并且把model封装成了名称为mv的对象,现在关注这个mv就可以了
再执行这句代码之后点击下一步,经过拦截器,会到这一步
执行派发结果
render(渲染了页面),查看如何渲染
解析视图并且把MV里的model(现在叫result)拿出来
然后视图执行视图解析器流程
进行解析
在这个解析环节已经获得了视图和视图名称,最后用我们的mv中的model数据对视图进行渲染,再进行下一步
把我们的请求响应和model转换为了一个mergeModel,点击进入
我们现在的model不为空,就被放到mergeModel里面了
终于返回出来一次了
我们刚刚把model放到了mergeModel里面,现在又用 mergeModel调用了方法,渲染合并输出的模型数据,点击进入
终于到了核心的方法了,暴露模型在请求域中
点击进入,最后就是用循环把model里面的东西set入到请求域而已
自定义对象参数的获取
一些奇奇怪怪的参数类型我们都尝试过了,那么对象属性可不可以?
测试:
结果:
我们发现是可以的,但是现在有一个问题,我们这个对象类型的属性,源码是如何对这些东西一一对应完成数据绑定的呢?
执行流程与原理
没错,还是和所有的请求流程相同,我们找到哪个参数解析器能解析我们自定义的对象参数
还真就找到了,名称为ServletModeAttributeProcessor这个参数解析器
那么他是如何做出判断的呢?也就是这句代码里发生什么事情了,点击查看
我们发现在这边做出了一个与的判断,首先判断参数是不是ModelAttribyte类型,很显然,我们是自定义的,不满足
再判断,我们现在写的这玩意是不是系统必须的,我们是自定义的也不是,所以判断为真
再次判断,我们这玩意是不是简单属性
但是简单属性是谁规定的呢,再次点击查看
其实在这个方法里已经规定了,只有下面的属性才是简单属性
所以判断结果为true,确定了ServletModeAttributeProcessor这个参数解析器,确定之后向上返回
开始解析
拿到这个参数解析器,并且执行对应的解析方法
点击下一步深入源码,我们发现给我们创建了一个attribute对象
但是这个对象里面装填的是什么?
原来是一个空person对象,方便我们后面把想添加的属性进行数据绑定
这行代码给我们创建了一个WebDataBinder
WebDataBinder是个啥?
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
WebDataBinder :web数据绑定器,将请求参数的值绑定到指定的JavaBean(也即是上面的attribute)里面
WebDataBinder 利用它里面的 Converters 将请求数据转成指定的数据类型。再次封装到JavaBean中(因为我们前端传入的参数都是字符串类型,但是我们的对象里面需要的类型一定千奇百怪,所以会有Converters 把字符串类型转换成我们需要的类型),默认总数为124个
进行数据绑定
这一行代码其实就完成了数据绑定的工作,带着前端所有的请求和绑定器进行绑定
只要这一步结束,我们binder里面person对象的值就都有了![](https://img-blog.csdnimg.cn/ea52444bdf744f3c8ffdd1b7ab1bbcf4.png)
我们可以细致的看一下源码
先获取原生的request
把我们的binder转换一个类型最后再调用.bind方法
深入.bind方法
先拿到request里面所有的KV对
执行绑定值的方法
深入这个方法
应用mpvs的属性到对应值里面
设置所有的属性值
再次查看源码,使用一个for循环,拿到第一个属性值(age),执行方法
利用它对应的反射器进行赋值
但是就在现在,问题出现了,代码说要对age进行转换,因为现在我们获取到的值还是字符串类型,但是对象需要的是整数类型,所以需要转换
对数据类型的转换
我们先接着执行方法,先判断是不是需要转换
获得现在我们需要的类型是整数,说明需要转换
看看谁能把字符串处理成整数
判断能不能进行转换
想要获取所有的转换器
在所有的converters里找能从字符串到整数的,converters内容就是如下,然后在for循环中寻找
找到了
通过字符串转整型的方法我们得知,这转换器这个玩意其实是一个泛型,我们可以自己定义
最后终于是完成了转换,再逐级返回
自定义Converter
现在我们来尝试自定义一下Converter,之前我们前端传输pet的表单,是分开填写了姓名和年龄,现在我们想填写在一张表单里,中间用逗号隔开
但是这不就是springMVC的定制吗,现在我们对于MVC的定制一般都是直接自己写一个组件放到配置类里面
还记得之前我们继承的WebConfig接口吗,里面有一个方法
添加格式化器
看上面的注解,就是源码留给我们添加converter(转换器)用的
我们现在的需求:把表单输入的(狗狗,88)转化为pet类型,那么我们遵照接口来就可以了呀
但是converter也有自己的规定
要清楚的填写原类型
和想转化的类型
书写代码
@Override
public void addFormatters(FormatterRegistry registry) {
//写出想把什么类型转换为什么类型的要求
registry.addConverter(new Converter<String, Pet>() {
//重写convert方法,传入的source就是前端表单内提交的内容
@Override
public Pet convert(String source) {
if (!StringUtils.isEmpty(source)){
Pet pet = new Pet();
//使用数组截取逗号前和逗号后的内容
String[] split = source.split(",");
pet.setName(split[0]);
pet.setAge(Integer.parseInt(split[1]));
}
return null;
}
});
}
测试:
可以成功的原理
还是逐行分析代码,知道获取了binder(数据绑定器),现在我们看一下自己的数据绑定器里面的convert
数量变成了125个,多了一个
说明我们自定义的转换器生效了,其他的流程都是一样的