前言
Spring-web给我们提供了丰富的参数绑定功能,本文主要说明参数绑定的原理以及使用方式,主要围绕@RequestBody @RequestParam @PathVariable这3种参数解析方式
在介绍三种参数解析方式之前先给出参数解析器的核心接口HandlerMethodArgumentResolver ,主要实现了其中两个方法 supportsParameter和resolveArgument,具体含义见如下码块
public interface HandlerMethodArgumentResolver {
/**
* 根据给定的参数对象是不是支持当前参数解析器
*/
boolean supportsParameter(MethodParameter parameter);
/**
* 返回controller中所需的参数对像并赋值
*/
Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception;
}
@PathVariable
-
原理
PathVariableMethodArgumentResolver是@PathVariable对应的参数解析器,其中resolveArgument()方法是在父类AbstractNamedValueMethodArgumentResolver中实现的,具体的流程如下图代码所示:
@Override
public boolean supportsParameter(MethodParameter parameter) {
//只支持参数带有PathVariable 注解的方式
if (!parameter.hasParameterAnnotation(PathVariable.class)) {
return false;
}
//如果参数是map类型的子类,注解value()必须有值
if (Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) {
String paramName = parameter.getParameterAnnotation(PathVariable.class).value();
return StringUtils.hasText(paramName);
}
return true;
}
@Override
@SuppressWarnings("unchecked")
protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
//从map中取出对应map中的参数值 至于map中的是在UriTemplateVariablesHandlerInterceptor中设置的
Map<String, String> uriTemplateVars = (Map<String, String>) request.getAttribute(
HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST);
return (uriTemplateVars != null ? uriTemplateVars.get(name) : null);
}
/**
* 父类中的方法@see org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver
* @return
* @throws Exception
*/
@Override
public final Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
//获取参数名
NamedValueInfo namedValueInfo = getNamedValueInfo(parameter);
MethodParameter nestedParameter = parameter.nestedIfOptional();
//处理参数名称 有些场景是支持el表达式的
Object resolvedName = resolveStringValue(namedValueInfo.name);
if (resolvedName == null) {
throw new IllegalArgumentException(
"Specified name must not resolve to null: [" + namedValueInfo.name + "]");
}
//获取参对饮的值
Object arg = resolveName(resolvedName.toString(), nestedParameter, webRequest);
if (arg == null) {
if (namedValueInfo.defaultValue != null) {
arg = resolveStringValue(namedValueInfo.defaultValue);
}
else if (namedValueInfo.required && !nestedParameter.isOptional()) {
handleMissingValue(namedValueInfo.name, nestedParameter, webRequest);
}
arg = handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType());
}
else if ("".equals(arg) && namedValueInfo.defaultValue != null) {
arg = resolveStringValue(namedValueInfo.defaultValue);
}
if (binderFactory != null) {
WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name);
try {
arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter);
}
catch (ConversionNotSupportedException ex) {
throw new MethodArgumentConversionNotSupportedException(arg, ex.getRequiredType(),
namedValueInfo.name, parameter, ex.getCause());
}
catch (TypeMismatchException ex) {
throw new MethodArgumentTypeMismatchException(arg, ex.getRequiredType(),
namedValueInfo.name, parameter, ex.getCause());
}
}
//设置值到request的attribute的中
handleResolvedValue(arg, namedValueInfo.name, parameter, mavContainer, webRequest);
return arg;
}
-
使用说明
@PathVariable的使用方法,如下图代码片段所示,当我们使用rest风格时会大量使用到此注解。
@RequestMapping("/path/{param0}/{param1}")
public String pathValueParam(@PathVariable("param0") String param0, @PathVariable String param1) {
System.out.println("param0 = " + param0 + "=====param1 = " + param1);
return "hello";
}
/**
* 当PathVariable中未指定值时采用参数名来匹配
* 所以这种方式也是可以获取到参数的
* @param param0
* @return
*/
@RequestMapping("/path/{param0}/{param1}")
public String pathValueParam(@PathVariable String param0) {
System.out.println("param0 = " + "=====param1 = " + param0);
return "hello";
}
需要保证url{}中的值和@PathVariable中的值保持一致,当@PathVariable中的值在查找不到时调用该方法的时候会抛出MissingPathVariableException异常。
@RequestParam
-
原理
RequestParamMethodArgumentResolver是@RequestParam对应的参数解析器,其中resolveArgument()方法是在父类AbstractNamedValueMethodArgumentResolver中实现的,具体的流程如下图代码所示:
@Override
public boolean supportsParameter(MethodParameter parameter) {
//支持注解为RequestParam
if (parameter.hasParameterAnnotation(RequestParam.class)) {
//如果参数是map的子类 则name()必须有值 否则不支持
if (Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) {
String paramName = parameter.getParameterAnnotation(RequestParam.class).name();
return StringUtils.hasText(paramName);
}
else {
return true;
}
}
else {
if (parameter.hasParameterAnnotation(RequestPart.class)) {
return false;
}
//支持文件MultipartFile 类型的参数也会默认走到这个解析器
parameter = parameter.nestedIfOptional();
if (MultipartResolutionDelegate.isMultipartArgument(parameter)) {
return true;
}
else if (this.useDefaultResolution) {
return BeanUtils.isSimpleProperty(parameter.getNestedParameterType());
}
else {
return false;
}
}
}
@Override
protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class);
MultipartHttpServletRequest multipartRequest =
WebUtils.getNativeRequest(servletRequest, MultipartHttpServletRequest.class);
//处理文件
Object mpArg = MultipartResolutionDelegate.resolveMultipartArgument(name, parameter, servletRequest);
if (mpArg != MultipartResolutionDelegate.UNRESOLVABLE) {
return mpArg;
}
Object arg = null;
if (multipartRequest != null) {
List<MultipartFile> files = multipartRequest.getFiles(name);
if (!files.isEmpty()) {
arg = (files.size() == 1 ? files.get(0) : files);
}
}
//非文件 从parameters中获取
if (arg == null) {
String[] paramValues = request.getParameterValues(name);
if (paramValues != null) {
arg = (paramValues.length == 1 ? paramValues[0] : paramValues);
}
}
return arg;
}
/**
* 父类中的方法@see org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver
* @return
* @throws Exception
*/
@Override
public final Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
//获取参数名
NamedValueInfo namedValueInfo = getNamedValueInfo(parameter);
MethodParameter nestedParameter = parameter.nestedIfOptional();
//处理参数名称 有些场景是支持el表达式的
Object resolvedName = resolveStringValue(namedValueInfo.name);
if (resolvedName == null) {
throw new IllegalArgumentException(
"Specified name must not resolve to null: [" + namedValueInfo.name + "]");
}
//获取参对饮的值
Object arg = resolveName(resolvedName.toString(), nestedParameter, webRequest);
if (arg == null) {
if (namedValueInfo.defaultValue != null) {
arg = resolveStringValue(namedValueInfo.defaultValue);
}
else if (namedValueInfo.required && !nestedParameter.isOptional()) {
handleMissingValue(namedValueInfo.name, nestedParameter, webRequest);
}
arg = handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType());
}
else if ("".equals(arg) && namedValueInfo.defaultValue != null) {
arg = resolveStringValue(namedValueInfo.defaultValue);
}
if (binderFactory != null) {
WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name);
try {
arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter);
}
catch (ConversionNotSupportedException ex) {
throw new MethodArgumentConversionNotSupportedException(arg, ex.getRequiredType(),
namedValueInfo.name, parameter, ex.getCause());
}
catch (TypeMismatchException ex) {
throw new MethodArgumentTypeMismatchException(arg, ex.getRequiredType(),
namedValueInfo.name, parameter, ex.getCause());
}
}
//设置值到request的attribute的中
handleResolvedValue(arg, namedValueInfo.name, parameter, mavContainer, webRequest);
return arg;
}
-
使用说明
因为@RequestParam注解的参数是通过getParameterValues来获取参数,parameters中包含的参数官方给出的说明
For HTTP servlets, parameters are contained in the query string or posted form data.
及form表单(contentType=application/x-www-form-urlencoded)的数据和url中?之后拼接的参数
/**
* get请求 url:http://localhost:8080/request/param?param0=a¶m1=b
*/
@RequestMapping("/request/param")
public String requestParam(@RequestParam(name ="param0") String param0,@RequestParam(name ="param1") String param1) {
System.out.println("param0 = " + param0 + "=====param1 = " + param1);
return "hello";
}
@RequestParam 默认的required是true也就意味着必填,否则会抛出MissingServletRequestParameterException的异常。需要保证name值和实际的传值名称保持一致(name= "param0" 缺省时要保证和参数名一致也是可以的)。
@RequestBody
下面我们着重来讲解一下其中还涉及了MessageConverter,其对应的参数解析器为RequestResponseBodyMethodProcessor,有且仅支持@RequestBody修饰的参数。在解析参数是采用可messageConverters来解析参数并生成对应的对象。我们最常用到的就是MappingJackson2HttpMessageConverter,它左右就是将我们提交的json数据转换成java对象。
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(RequestBody.class);
}
/**
* Throws MethodArgumentNotValidException if validation fails.
* @throws HttpMessageNotReadableException if {@link RequestBody#required()}
* is {@code true} and there is no body content or if there is no suitable
* converter to read the content with.
*/
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
parameter = parameter.nestedIfOptional();
Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
String name = Conventions.getVariableNameForParameter(parameter);
WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
if (arg != null) {
validateIfApplicable(binder, parameter);
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
}
}
mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
return adaptArgumentIfNecessary(arg, parameter);
}
注意:所有messageconverter遍历玩对象仍然未能处理,此时会抛出HttpMediaTypeNotSupportedException,主要是不支持的内容类型。
下面我们在来说一下messageconverter,当我们不想使用MappingJackson2HttpMessageConverter 想使用Fastjson来代替时我们需要自定义一个FastJson的消息转换器来解读数据。
下面是自定义并配置了一个messageConverter,仅需要实现GenericHttpMessageConverter接口定义的方法就行了
@Component("fastjson2HttpMessageConverter")
public class Fastjson2HttpMessageConverter implements GenericHttpMessageConverter<Object> {
@Override
public boolean canRead(Type type, Class<?> contextClass, MediaType mediaType) {
return getSupportedMediaTypes().contains(mediaType);
}
@Override
public Object read(Type type, Class<?> contextClass, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException {
Object obj = JSON.parseObject(inputMessage.getBody(), type);
return obj;
}
@Override
public boolean canWrite(Type type, Class<?> clazz, MediaType mediaType) {
return false;
}
@Override
public void write(Object o, Type type, MediaType contentType, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
}
@Override
public boolean canRead(Class<?> clazz, MediaType mediaType) {
return false;
}
@Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
return false;
}
@Override
public List<MediaType> getSupportedMediaTypes() {
return Arrays.asList(MediaType.APPLICATION_JSON_UTF8, MediaType.APPLICATION_JSON);
}
@Override
public Object read(Class<?> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException {
return null;
}
@Override
public void write(Object o, MediaType contentType, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
}
}
然后在xml中配置一下
<!-- 添加注解驱动 -->
<mvc:annotation-driven>
<mvc:message-converters>
<ref bean="fastjson2HttpMessageConverter"></ref>
</mvc:message-converters>
</mvc:annotation-driven>