前几天笔者在写Rest接口的时候,看到了一种传值方式是之前没有写过的,就萌生了一探究竟的想法。在此以前,有篇文章曾涉及到这个话题,但那篇文章着重于处理流程的分析,并未深刻。前端
本文重点来看几种传参方式,看看它们都是如何被解析并应用到方法参数上的。web
不论在SpringBoot仍是SpringMVC中,一个HTTP请求会被DispatcherServlet
类接收,它本质是一个Servlet
,由于它继承自HttpServlet
。在这里,Spring负责解析请求,匹配到Controller
类上的方法,解析参数并执行方法,最后处理返回值并渲染视图。json
咱们今天的重点在于解析参数,对应到上图的目标方法调用
这一步骤。既然说到参数解析,那么针对不一样类型的参数,确定有不一样的解析器。Spring已经帮咱们注册了一堆这东西。bash
它们有一个共同的接口HandlerMethodArgumentResolver
。supportsParameter
用来判断方法参数是否能够被当前解析器解析,若是能够就调用resolveArgument
去解析。app
public interface HandlerMethodArgumentResolver {
//判断方法参数是否能够被当前解析器解析
boolean supportsParameter(MethodParameter var1);
//解析参数
@Nullable
Object resolveArgument(MethodParameter var1,
@Nullable ModelAndViewContainer var2,
NativeWebRequest var3,
@Nullable WebDataBinderFactory var4)throws Exception;
}
复制代码
在Controller方法中,若是你的参数标注了RequestParam
注解,或者是一个简单数据类型。ide
@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");
ui
在Spring中,这里对应的参数解析器是RequestParamMethodArgumentResolver
。与咱们的想法差很少,就是拿到参数名称后,直接从Request中获取值。this
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;
}
复制代码
若是咱们须要前端传输更多的参数内容,那么经过一个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
负责。整个过程咱们分为三个步骤来看。
在开始以前须要先获取请求的一些辅助信息,好比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;
//.......
}
复制代码
上面获取到的辅助信息是有做用的,就是要肯定一个消息转换器。消息转换器有不少,它们的共同接口是HttpMessageConverter
。在这里,Spring帮咱们注册了不少转换器,因此须要循环它们,来肯定使用哪个来作消息转换。
若是是JSON数据格式的,会选择MappingJackson2HttpMessageConverter
来处理。它的构造函数正是指明了这一点。
public MappingJackson2HttpMessageConverter(ObjectMapper objectMapper) {
super(objectMapper, new MediaType[]{
MediaType.APPLICATION_JSON,
new MediaType("application", "*+json")});
}
复制代码
既然肯定了消息转换器,那么剩下的事就很简单了。经过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
都找到以后调用方法解析便可。
还有一种写法是这样的,在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()
方法。整个过程,咱们也能够分为三个步骤来看。
根据参数类型,先生成一个目标类的构造函数,以供后面绑定数据的时候使用。
WebDataBinder
继承自DataBinder
。而DataBinder
主要的做用,简言之就是利用BeanWrapper
给对象的属性设值。
在这里,又把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作的就是经过注解,尽可能适配大部分应用场景。