问题起源
今天后端与前端同事在讨论对于只有一个参数的接口,能否不将参数当作url的一部分传递,而都通过参数进行传递。 比如说:访问车辆详情页面只需要一个车辆id。
- 第一种方式:通过访问路径传递就是:getDetail/{carId}这种方式。
- 第二种方式:如果以get方式进行请求,那么就可以写成:getDetail?carId=xxxx;
- 第三种方式:如果以post方式,并且访问的content-type:application/json。
对于上述三种方式在springBoot中的解析方案:
- 第一种:@PathVariable(“carId”)
- 第二种:@RequestParam(“carId”)
- 第三种:@RequestBody
但是对于第三方种方案就出现了问题,前端传递的依旧是json字符串:{“carId”:“xxxx”}. 那么我们解析的时候如果入参设置为
getDetail(@RequestBody String carId)
那么carId中的值不是 "xxxx"而是整个json字符串{“carId”:“xxxx”}。 而如果我们想要解析就需要在包装一层对象,类似这样
@Data
public class CarInfo implements Serializable{
private String carId;
}
这样按照json的规范就可以将key与属性值进行映射,然后赋予对应的值。
那么如果像删除车辆之类的POST操作,前端如果不是将参数放到url中当成路径的部分,那么放入body中的数据就不能以json格式进行封装,而只能放入carId的字符串值。 (在进行操作之类的请求将参数放入url中是否存在安全性问题,没有深入思考过有同学了解过的话可以指教下)
问题延伸
那这里就产生了问题,springBoot如何进行判断不同类型? springBoot是如何对于不同类型进行不同的处理? 能否进行自定义的解析方法? 如何实现?
代码实现
思路:当容器收到http请求后,将请求转发给通用处理组件,将请求进行基本处理,然后交给业务进行处理,最后在对业务的返回进行处理,最后返回结果。
容器接受到http请求
前置处理组件
业务流程
后置处理组件
返回结果
其中【前置处理组件】部分就是处理@RequestBody等一些组件的地方,而【业务流程】就是日常我们编写@Controller接收各个请求后处理的业务流程 (前置处理组件内容非常多,本文只关注入参注解相关的处理)
前置准备阶段
请求进行之后会创建一个ServletInvocableHandlerMethod类进行处理
public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
implements BeanFactoryAware, InitializingBean {
protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
......
ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);
if (this.argumentResolvers != null) {
invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
}
......
invocableMethod.invokeAndHandle(webRequest, mavContainer);
......
return getModelAndView(mavContainer, modelFactory, webRequest);
}
}
选择解决方案
在前置的处理过程中,我们会加载一系列实现了HandlerMethodArgumentResolver接口的方法,而这些实现类就是用来处理各种入参类型的解决方案
private Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
MethodParameter[] parameters = getMethodParameters();
Object[] args = new Object[parameters.length];
for (int i = 0; i < parameters.length; i++) {
MethodParameter parameter = parameters[i];
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
args[i] = resolveProvidedArgument(parameter, providedArgs);
if (args[i] != null) {
continue;
}
if (this.argumentResolvers.supportsParameter(parameter)) {
try {
args[i] = this.argumentResolvers.resolveArgument(
parameter, mavContainer, request, this.dataBinderFactory);
continue;
}
catch (Exception ex) {
if (logger.isDebugEnabled()) {
logger.debug(getArgumentResolutionErrorMessage("Failed to resolve", i), ex);
}
throw ex;
}
}
if (args[i] == null) {
throw new IllegalStateException("Could not resolve method parameter at index " +
parameter.getParameterIndex() + " in " + parameter.getExecutable().toGenericString() +
": " + getArgumentResolutionErrorMessage("No suitable resolver for", i));
}
}
return args;
}
在this.argumentResolvers中加载了很多解决方案
- 比如说对@RequetBody注解进行处理的,RequestResponseBodyMethodProcessor类
- 处理@RequestHeader的,RequestHeaderMethodArgumentResolver类
- 处理@RequestParam的,RequestParamMethodArgumentResolver类
如果我们想要自定义一个注解,那么我们就需要实现HandlerMethodArgumentResolver并且将其注册到this.argumentResolvers中。
如何自定义Resolver
1.第一步,我们需要自己实现HandlerMethodArgumentResolver接口
@Component
public class MyTestHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
if(parameter.hasMethodAnnotation(MyTest.class)){
return true;
}
return false;
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
System.out.println("进行处理");
return "返回固定的字符串";
}
}
2.第二步,将实现类加入到spring的解决方案集合中
我们只需要用一个类去实现 WebMvcConfigurer 接口中的
@Configuration
public class XxxConfiguration implements WebMvcConfigurer
{
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(xxxx);
}
}
处理类型
到这里其实只是区分了不同注解的解决方案,但是对于同一种注解不同的入参类型的转换问题还没有解决。 而对于不同类型的转换也定义了一个接口HttpMessageConverter 代码位置:
protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
for (HttpMessageConverter<?> converter : this.messageConverters) {
if (genericConverter != null ? genericConverter.canRead(targetType, contextClass, contentType) :
(targetClass != null && converter.canRead(targetClass, contentType))) {
....
}
break;
}
}
对于不同类型有不同的转换器,这里以@RequestBody为例,其中的转换器有如下一些:
this.messageConverters = {ArrayList@19036} size = 10
0 = {ByteArrayHttpMessageConverter@19806}
1 = {StringHttpMessageConverter@19070}
2 = {StringHttpMessageConverter@19821}
3 = {ResourceHttpMessageConverter@19822}
4 = {ResourceRegionHttpMessageConverter@19823}
5 = {SourceHttpMessageConverter@19824}
6 = {AllEncompassingFormHttpMessageConverter@19825}
7 = {MappingJackson2HttpMessageConverter@19826}
8 = {MappingJackson2HttpMessageConverter@19827}
9 = {Jaxb2RootElementHttpMessageConverter@19828}
这里对于String类型的入参类型,就是使用StringHttpMessageConverter来处理。 而对于对象来说而且content-type:application/json的就使用MappingJackson2HttpMessageConverter来处理。
如何自定义HttpMessageConverter
其实方法与上面的自定义Resolver一致
- 自己实现一个HttpMessageConverter
- 通过WebMvcConfigurer实现,然后将自己定义的添加到其中
void extendMessageConverters(List<HttpMessageConverter<?>> converters)
思考总结
spring框架中定义了非常多的扩展点,在使用过程中如果能深刻理解背后的设计,那么在应用中能避免很多不必要的错误。
|