内容简介:前几天笔者在写Rest接口的时候,看到了一种传值方式是以前没有写过的,就萌生了一探究竟的想法。在此之前,有篇文章曾涉及到这个话题,但那篇文章着重于处理流程的分析,并未深入。本文重点来看几种传参方式,看看它们都是如何被解析并应用到方法参数上的。不论在SpringBoot还是SpringMVC中,一个HTTP请求会被
前几天笔者在写Rest接口的时候,看到了一种传值方式是以前没有写过的,就萌生了一探究竟的想法。在此之前,有篇文章曾涉及到这个话题,但那篇文章着重于处理流程的分析,并未深入。
本文重点来看几种传参方式,看看它们都是如何被解析并应用到方法参数上的。
一、HTTP请求处理流程
不论在SpringBoot还是SpringMVC中,一个HTTP请求会被 DispatcherServlet 类接收,它本质是一个 Servlet ,因为它继承自 HttpServlet 。在这里,Spring负责解析请求,匹配到 Controller 类上的方法,解析参数并执行方法,最后处理返回值并渲染视图。
我们今天的重点在于解析参数,对应到上图的 目标方法调用 这一步骤。既然说到参数解析,那么针对不同类型的参数,肯定有不同的解析器。Spring已经帮我们注册了一堆这东西。
private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {
List<HandlerMethodArgumentResolver> resolvers = new ArrayList();
resolvers.add(new RequestParamMethodArgumentResolver(this.getBeanFactory(), false));
resolvers.add(new RequestParamMapMethodArgumentResolver());
resolvers.add(new PathVariableMethodArgumentResolver());
resolvers.add(new PathVariableMapMethodArgumentResolver());
resolvers.add(new MatrixVariableMethodArgumentResolver());
resolvers.add(new MatrixVariableMapMethodArgumentResolver());
resolvers.add(new ServletModelAttributeMethodProcessor(false));
resolvers.add(new RequestResponseBodyMethodProcessor(this.getMessageConverters(), this.requestResponseBodyAdvice));
resolvers.add(new RequestPartMethodArgumentResolver(this.getMessageConverters(), this.requestResponseBodyAdvice));
resolvers.add(new RequestHeaderMethodArgumentResolver(this.getBeanFactory()));
resolvers.add(new RequestHeaderMapMethodArgumentResolver());
resolvers.add(new ServletCookieValueMethodArgumentResolver(this.getBeanFactory()));
resolvers.add(new ExpressionValueMethodArgumentResolver(this.getBeanFactory()));
resolvers.add(new SessionAttributeMethodArgumentResolver());
resolvers.add(new RequestAttributeMethodArgumentResolver());
resolvers.add(new ServletRequestMethodArgumentResolver());
resolvers.add(new ServletResponseMethodArgumentResolver());
resolvers.add(new HttpEntityMethodProcessor(this.getMessageConverters(), this.requestResponseBodyAdvice));
resolvers.add(new RedirectAttributesMethodArgumentResolver());
resolvers.add(new ModelMethodProcessor());
resolvers.add(new MapMethodProcessor());
resolvers.add(new ErrorsMethodArgumentResolver());
resolvers.add(new SessionStatusMethodArgumentResolver());
resolvers.add(new UriComponentsBuilderMethodArgumentResolver());
if (this.getCustomArgumentResolvers() != null) {
resolvers.addAll(this.getCustomArgumentResolvers());
}
resolvers.add(new RequestParamMethodArgumentResolver(this.getBeanFactory(), true));
resolvers.add(new ServletModelAttributeMethodProcessor(true));
return resolvers;
}
复制代码
它们有一个共同的接口 HandlerMethodArgumentResolver 。 supportsParameter 用来判断方法参数是否可以被当前解析器解析,如果可以就调用 resolveArgument 去解析。
public interface HandlerMethodArgumentResolver {
//判断方法参数是否可以被当前解析器解析
boolean supportsParameter(MethodParameter var1);
//解析参数
@Nullable
Object resolveArgument(MethodParameter var1,
@Nullable ModelAndViewContainer var2,
NativeWebRequest var3,
@Nullable WebDataBinderFactory var4)throws Exception;
}
复制代码
二、RequestParam
在Controller方法中,如果你的参数标注了 RequestParam 注解,或者是一个简单数据类型。
@RequestMapping("/test1")
@ResponseBody
public String test1(String t1, @RequestParam(name = "t2",required = false) String t2,HttpServletRequest request){
logger.info("参数:{},{}",t1,t2);
return "Java";
}
复制代码
我们的请求路径是这样的: http://localhost:8080/test1?t1=Jack&t2=Java
如果按照以前的写法,我们直接根据参数名称或者 RequestParam 注解的名称从Request对象中获取值就行。比如像这样:
String parameter = request.getParameter("t1");
在Spring中,这里对应的参数解析器是 RequestParamMethodArgumentResolver 。与我们的想法差不多,就是拿到参数名称后,直接从Request中获取值。
protected Object resolveName(String name, MethodParameter parameter,
NativeWebRequest request) throws Exception {
HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class);
//...省略部分代码...
if (arg == null) {
String[] paramValues = request.getParameterValues(name);
if (paramValues != null) {
arg = paramValues.length == 1 ? paramValues[0] : paramValues;
}
}
return arg;
}
复制代码
三、RequestBody
如果我们需要前端传输更多的参数内容,那么通过一个POST请求,将参数放在Body中传输是更好的方式。当然,比较友好的数据格式当属JSON。
面对这样一个请求,我们在Controller方法中可以通过 RequestBody 注解来接收它,并自动转换为合适的Java Bean对象。
@ResponseBody
@RequestMapping("/test2")
public String test2(@RequestBody SysUser user){
logger.info("参数信息:{}",JSONObject.toJSONString(user));
return "Hello";
}
复制代码
在没有Spring的情况下,我们考虑一下如何解决这一问题呢?
首先呢,还是要依靠Request对象。对于Body中的数据,我们可以通过 request.getReader() 方法来获取,然后读取字符串,最后通过JSON工具类再转换为合适的 Java 对象。
比如像下面这样:
@RequestMapping("/test2")
@ResponseBody
public String test2(HttpServletRequest request) throws IOException {
BufferedReader reader = request.getReader();
StringBuilder builder = new StringBuilder();
String line;
while ((line = reader.readLine()) != null){
builder.append(line);
}
logger.info("Body数据:{}",builder.toString());
SysUser sysUser = JSONObject.parseObject(builder.toString(), SysUser.class);
logger.info("转换后的Bean:{}",JSONObject.toJSONString(sysUser));
return "Java";
}
复制代码
当然,在实际场景中,上面的SysUser.class需要动态获取参数类型。
在Spring中, RequestBody 注解的参数会由 RequestResponseBodyMethodProcessor 类来负责解析。
它的解析由父类 AbstractMessageConverterMethodArgumentResolver 负责。整个过程我们分为三个步骤来看。
1、获取请求辅助信息
在开始之前需要先获取请求的一些辅助信息,比如HTTP请求的数据格式,上下文Class信息、参数类型Class、HTTP请求方法类型等。
protected <T> Object readWithMessageConverters(){
boolean noContentType = false;
MediaType contentType;
try {
contentType = inputMessage.getHeaders().getContentType();
} catch (InvalidMediaTypeException var16) {
throw new HttpMediaTypeNotSupportedException(var16.getMessage());
}
if (contentType == null) {
noContentType = true;
contentType = MediaType.APPLICATION_OCTET_STREAM;
}
Class<?> contextClass = parameter.getContainingClass();
Class<T> targetClass = targetType instanceof Class ? (Class)targetType : null;
if (targetClass == null) {
ResolvableType resolvableType = ResolvableType.forMethodParameter(parameter);
targetClass = resolvableType.resolve();
}
HttpMethod httpMethod = inputMessage instanceof HttpRequest ?
((HttpRequest)inputMessage).getMethod() : null;
//.......
}
复制代码
2、确定消息转换器
上面获取到的辅助信息是有作用的,就是要确定一个消息转换器。消息转换器有很多,它们的共同接口是 HttpMessageConverter 。在这里,Spring帮我们注册了很多转换器,所以需要循环它们,来确定使用哪一个来做消息转换。
如果是JSON数据格式的,会选择 MappingJackson2HttpMessageConverter 来处理。它的构造函数正是指明了这一点。
public MappingJackson2HttpMessageConverter(ObjectMapper objectMapper) {
super(objectMapper, new MediaType[]{
MediaType.APPLICATION_JSON,
new MediaType("application", "*+json")});
}
复制代码
3、解析
既然确定了消息转换器,那么剩下的事就很简单了。通过Request获取Body,然后调用转换器解析就好了。
protected <T> Object readWithMessageConverters(){
if (message.hasBody()) {
HttpInputMessage msgToUse = this.getAdvice().beforeBodyRead(message, parameter, targetType, converterType);
body = genericConverter.read(targetType, contextClass, msgToUse);
body = this.getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);
}
}
复制代码
再往下就是Jackson包的内容了,不再深究。虽然写出来的过程比较啰嗦,但实际上主要就是为了寻找两个东西:
方法解析器RequestResponseBodyMethodProcessor
消息转换器MappingJackson2HttpMessageConverter
都找到之后调用方法解析即可。
四、GET请求参数转换Bean
还有一种写法是这样的,在Controller方法上用Java Bean接收。
@RequestMapping("/test3")
@ResponseBody
public String test3(SysUser user){
logger.info("参数:{}",JSONObject.toJSONString(user));
return "Java";
}
复制代码
然后用GET方法请求:
http://localhost:8080/test3?id=1001&name=Jack&password=1234&address=北京市海淀区
URL后面的参数名称对应Bean对象里面的属性名称,也可以自动转换。那么,这里它又是怎么做的呢 ?
笔者首先想到的就是Java的反射机制。从Request对象中获取参数名称,然后和目标类上的方法一一对应设置值进去。
比如像下面这样:
public String test3(SysUser user,HttpServletRequest request)throws Exception {
//从Request中获取所有的参数key 和 value
Map<String, String[]> parameterMap = request.getParameterMap();
Iterator<Map.Entry<String, String[]>> iterator = parameterMap.entrySet().iterator();
//获取目标类的对象
Object target = user.getClass().newInstance();
Field[] fields = target.getClass().getDeclaredFields();
while (iterator.hasNext()){
Map.Entry<String, String[]> next = iterator.next();
String key = next.getKey();
String value = next.getValue()[0];
for (Field field:fields){
String name = field.getName();
if (key.equals(name)){
field.setAccessible(true);
field.set(target,value);
break;
}
}
}
logger.info("userInfo:{}",JSONObject.toJSONString(target));
return "Python";
}
复制代码
除了反射,Java还有一种内省机制可以完成这件事。我们可以获取目标类的属性描述符对象,然后拿到它的Method对象, 通过invoke来设置。
private void setProperty(Object target,String key,String value) {
try {
PropertyDescriptor propDesc = new PropertyDescriptor(key, target.getClass());
Method method = propDesc.getWriteMethod();
method.invoke(target, value);
} catch (Exception e) {
e.printStackTrace();
}
}
复制代码
然后在上面的循环中,我们就可以调用这个方法来实现。
while (iterator.hasNext()){
Map.Entry<String, String[]> next = iterator.next();
String key = next.getKey();
String value = next.getValue()[0];
setProperty(userInfo,key,value);
}
复制代码
为什么要说到内省机制呢?因为Spring在处理这件事的时候,最终也是靠它处理的。
简单来说,它是通过 BeanWrapperImpl 来处理的。关于 BeanWrapperImpl 有个很简单的使用方法:
SysUser user = new SysUser();
BeanWrapper wrapper = new BeanWrapperImpl(user.getClass());
wrapper.setPropertyValue("id","20001");
wrapper.setPropertyValue("name","Jack");
Object instance = wrapper.getWrappedInstance();
System.out.println(instance);
复制代码
wrapper.setPropertyValue 最后就会调用到 BeanWrapperImpl#BeanPropertyHandler.setValue() 方法。
它的 setValue 方法和我们上面的 setProperty 方法大致相同。
private class BeanPropertyHandler extends PropertyHandler {
//属性描述符
private final PropertyDescriptor pd;
public void setValue(@Nullable Object value) throws Exception {
//获取set方法
Method writeMethod = this.pd.getWriteMethod();
ReflectionUtils.makeAccessible(writeMethod);
//设置
writeMethod.invoke(BeanWrapperImpl.this.getWrappedInstance(), value);
}
}
复制代码
通过上面的方式,就完成了GET请求参数到Java Bean对象的自动转换。
回过头来,我们再看Spring。虽然我们上面写的很简单,但真正用起来还需要考虑的很多很多。Spring中处理这种参数的解析器是 ServletModelAttributeMethodProcessor 。
它的解析过程在其父类 ModelAttributeMethodProcessor.resolveArgument() 方法。整个过程,我们也可以分为三个步骤来看。
1、获取目标类的构造函数
根据参数类型,先生成一个目标类的构造函数,以供后面绑定数据的时候使用。
2、创建数据绑定器WebDataBinder
WebDataBinder 继承自 DataBinder 。而 DataBinder 主要的作用,简言之就是利用 BeanWrapper 给对象的属性设值。
3、绑定数据到目标类,并返回
在这里,又把 WebDataBinder 转换成 ServletRequestDataBinder 对象,然后调用它的bind方法。
接下来有个很重要的步骤是,将request中的参数转换为 MutablePropertyValues pvs 对象。
然后接下来就是循环pvs,调用 setPropertyValue 设置属性。当然了,最后调用的其实就是 BeanWrapperImpl#BeanPropertyHandler.setValue() 。
下面有段代码可以更好的理解这一过程,效果是一样的:
//模拟Request参数
Map<String,Object> map = new HashMap();
map.put("id","1001");
map.put("name","Jack");
map.put("password","123456");
map.put("address","北京市海淀区");
//将request对象转换为MutablePropertyValues对象
MutablePropertyValues propertyValues = new MutablePropertyValues(map);
SysUser sysUser = new SysUser();
//创建数据绑定器
ServletRequestDataBinder binder = new ServletRequestDataBinder(sysUser);
//bind数据
binder.bind(propertyValues);
System.out.println(JSONObject.toJSONString(sysUser));
复制代码
五、自定义参数解析器
我们说所有的消息解析器都实现了 HandlerMethodArgumentResolver 接口。我们也可以定义一个参数解析器,让它实现这个接口就好了。
首先,我们可以定义一个 RequestXuner 注解。
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestXuner {
String name() default "";
boolean required() default false;
String defaultValue() default "default";
}
复制代码
然后是实现了 HandlerMethodArgumentResolver 接口的解析器类。
public class XunerArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(RequestXuner.class);
}
@Override
public Object resolveArgument(MethodParameter methodParameter,
ModelAndViewContainer modelAndViewContainer,
NativeWebRequest nativeWebRequest,
WebDataBinderFactory webDataBinderFactory){
//获取参数上的注解
RequestXuner annotation = methodParameter.getParameterAnnotation(RequestXuner.class);
String name = annotation.name();
//从Request中获取参数值
String parameter = nativeWebRequest.getParameter(name);
return "HaHa,"+parameter;
}
}
复制代码
不要忘记需要配置一下。
@Configuration
public class WebMvcConfiguration extends WebMvcConfigurationSupport {
@Override
protected void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new XunerArgumentResolver());
}
}
复制代码
一顿操作后,在Controller中我们可以这样使用它:
@ResponseBody
@RequestMapping("/test4")
public String test4(@RequestXuner(name="xuner") String xuner){
logger.info("参数:{}",xuner);
return "Test4";
}
复制代码
六、总结
本文内容通过相关示例代码展示了Spring中部分解析器解析参数的过程。说到底,无论参数如何变化,参数类型再怎么复杂。
它们都是通过HTTP请求发送过来的,那么就可以通过 HttpServletRequest 来获取到一切。Spring做的就是通过注解,尽量适配大部分应用场景。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- springmvc 请求参数解析细节
- Flink 中如何解析与传递参数
- JWT+SpringBoot+SpringMVC参数解析器
- python模块之getopt(脚本参数解析)
- go语言 从命令行获取参数解析
- php解析url并得到url中的参数
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Mobilizing Web Sites
Layon, Kristofer / 2011-12 / 266.00元
Everyone has been talking about the mobile web in recent years, and more of us are browsing the web on smartphones and similar devices than ever before. But most of what we are viewing has not yet bee......一起来看看 《Mobilizing Web Sites》 这本书的介绍吧!