上一篇博客探究了一下Spring MVC是如何处理一个http请求的Spring MVC处理请求流程。但是遗留了几个问题,这篇博客就深入探究一下Spring MVC是如何处理请求报文和响应报文的,以及如何自己定义一个方法来将请求报文或者响应报文转换成需要的格式。
文章目录
前言
一、Spring MVC请求参数的处理
二、Spring MCV响应参数的处理
三、自定义HttpMessageConverter
总结
前言
创建一个Controller然后定义一个方法,方法上面添加@RequestMapping定义url,然后在方法参数中添加@RequestBody注解,在方法上添加@ResponseBody注解,这样在通过url调用这个接口的时候就会自动将参数转换成对象,并且将响应报文也变成对象。而且在调用接口的时候传的String类型的对象,处理请求的handler方法中参数类型却是Integer,这些都是如何实现的呢。其实这些都是通过Spring的参数装换器实现的,接下来我们就通过源码的形式看一下这种参数装换的细节。
一、Spring MVC请求参数的处理
先定义一个Controller,并编写一个handler方法
这里在请求参数前面添加了@RequestBody注解,并且添加了@RestController注解,所以请求参数会转换成对象,并且响应报文也会是json对象。
上一篇博客说过请求处理流程最终是在DispatchServlet中通过HandlerAdapter调用handler的方法,而HandlerAdapter的主要功能也是使servlet能够调用正确的handler来处理请求,并将参数适配成handler方法需要的格式并转换响应报文格式。所以这里直接断点进入方法,http请求处理流程是如何到达这个方法的上一篇博客已经详细说明了,这里就直接进入方法中。
这里设置了两个关键Resolver,即请求参数解析器-MethodArgumentResolver和响应参数解析器MethodReturnValueHandler,这两个是参数解析的关键。
可以看到这里的解析器一共有27个,根据 这些名称可以猜出来这些处理器都是处理哪些参数的。由于方法中加入了@RequsetBody所以会使用RequestResponseBodyMethodProcessor去处理请求参数。
根据URM类图可以看出 RequestResponseBodyMethodProcessor既有处理请求参数的能力又有处理返回参数的能力。
这两个接口中的第一个方法是支持处理哪种类型的参数,第二个方法则是具体的处理逻辑。再看一下RequestResponseBodyMethodProcessor中实现的这两个接口中的第一个方法。
由此可见 RequestResponseBodyMethodProcessor是处理带有@RequstBody注解的请求参数和带有@ResponseBody注解的响应参数。具体实现的处理参数的逻辑通过源码处理的逻辑后面再看。回到invokeHandlerMehtod方法,设置完参数处理器之后继续往下执行。
这里获取了请求参数,并调用doInvoke方法,doInvoke方法是具体的handler方法,入参的处理在getMethodArgumentValues里面进行,而这次关注的也是这个处理参数的方法,所以现在进入方法。
这里获取了参数以及参数里面的详情信息,MethodParameter中有参数的类型信息,注解信息等。关键点在resolvers.resolveArgument方法,这里是处理参数的主要逻辑,进入该方法中。
这里的这个方法就会找出能够处理参数的resolver,如果没找到能够处理的则会抛出异常。 先看一下这个选取具体处理的resolver的方法逻辑。
遍历所有的resolver找出一个能够处理参数的resolver,主要看resolver的supportsParameter方法。这里前面有说过resolver是否能够处理参数主要是看 supportsParameter。
由于 RequestResponseBodyMethodProcessor的supportsParameter方法中的逻辑正好是支持处理@RequestBody注解的参数,所以这里最终会选择RequestResponseBodyMethodProcessor来处理入参。
最终如我们所想的进入了 RequestResponseBodyMethodProcessor的处理方法中,这里面就是处理请求参数的具体逻辑。这里会通过readWithMessageConverters方法从HttpRequestServlet中读取请求参数,方法处理完成之后返回的arg就是handler中参数的对象。进入方法查看具体的处理逻辑。最终会进入AbstractMessageConverterMethodArgumentResolver#readWithMessageConverts方法中,通过这个方法名是不是有所感悟,什么是MessageConvert。这就引出了参数处理的重要角色MessageConvert,这是真正执行处理参数的类,我们后面需要自定义参数处理也是要创建一个自己的MessageConvert类,然后让Resolver去调用我们自己的MessageConvert去处理参数。
HttpMessageConverter中定义了四个关键的方法canRead、canWrite、read、write。前两个方法是判断是否支持处理参数,后两个是真正处理参数的逻辑。
AbstractMessageConverterMethodArgumentResolver#readWithMessageConverts方法遍历所有的HttpMessageConverter找出一个能够处理请求参数的converter。通过debug可以看到当前拢共有是个HttpMessageConverter
继续执行代码,这里最终选择的是MappingJackson2HttpMessageConverter去处理请求参数
调用read方法就将HttpRequstServlet中的请求数据流转换成了我们定义的handler方法中的参数类型,此处是User对象。到这里为止请求参数的处理流程已经说完了,后面就是doInvoke方法执行handler的方法逻辑并返回结果了。
二、Spring MCV响应参数的处理
经过上面请求参数处理流程的分析,对于响应参数的处理也是同出一辙,所以响应参数的处理应该会简单的多,回到上面的方法中。
断点处就是处理请求结果的方法
与处理请求参数的流程一样,这里通过selectHandler去选择一个Handler去处理响应结果。上面已经说过会通过supportsReturnType来匹配支持处理的Handler。这里就不进入selectHandler方法中了,读者可以自己进入方法查看。通过断点可以看到最终处理响应结果的Handler仍然是RequestResponseBodyMethodProcessor,这是因为添加了@RestController注解。debug继续跟进最终会进入AbstractMessageConverterMethodProcessor#writeWithMessageConverters方法中,由于这个方法逻辑太长,所以这里只看关键的执行代码。
与请求参数处理一样,这里最终处理响应参数的类也是HttpMessageConverter,遍历所有的Converter找出能够处理响应的Converter。
最终会调用write方法将数据写入响应流。到此为止响应数据的处理流程也分析完了,接下来就是定义一个HttpMessageConvert来处理请求参数和响应参数。
三、自定义HttpMessageConverter
接下来就定义一个HttpMessageConverter,实现将客户端传的用“-”横杠拼接的属性转换成对象,并且返回用横拼接的字符串。
请求报文和响应报文类:
public class User {
private int id;
private String name;
public User() {
}
public User(int id, String name) {
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
Controller:
@RestController
public class UserController {
@PostMapping("/v2/users")
public User getAllUsersV2(@RequestBody User user) {
return user;
}
}
接下来就是定义转换器:
public class MyHttpMessageConverter extends AbstractHttpMessageConverter<User> {
public MyHttpMessageConverter() {
// 自定义的媒体类型application/xxx-str
super(new MediaType("application", "xxx-str", Charset.forName("UTF-8")));
}
/**
* 只有User类型才使用这个转换器
* @param clazz
* @return
*/
@Override
protected boolean supports(Class<?> clazz) {
return User.class.isAssignableFrom(clazz);
}
@Override
protected User readInternal(Class<? extends User> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
String temp = StreamUtils.copyToString(inputMessage.getBody(), Charset.forName("UTF-8"));
String[] args = temp.split("-");
return new User(Integer.valueOf(args[0]), args[1]);
}
@Override
protected void writeInternal(User user, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
String resp = user.getId()+"-"+user.getName();
outputMessage.getBody().write(resp.getBytes());
}
}
这里定义了一个媒体类型,因为是字符串转实体,如果使用默认的媒体类型比如application/json会报参数错误等错误,所以自定义了一个类型。
@Configuration
public class MyWebmvcConfiguration implements WebMvcConfigurer {
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(converter());
}
@Bean
public MyHttpMessageConverter converter() {
return new MyHttpMessageConverter();
}
}
现在就可以调用接口了:
总结
通过这篇博客了解了Spring MVC是如何处理请求参数和响应参数。其实依赖的就是参数处理器Resolver,自带的有27种参数处理器,而参数处理器又是调用HttpMessageConverter来处理参数的,所以我们可以自己定义HttpMessageConverter来处理请求参数和响应参数。