欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

SpringBoot如何解析参数的深入理解

程序员文章站 2024-02-24 19:17:16
前言 前几天笔者在写rest接口的时候,看到了一种传值方式是以前没有写过的,就萌生了一探究竟的想法。在此之前,有篇文章曾涉及到这个话题,但那篇文章着重于处理流程的分析...

前言

前几天笔者在写rest接口的时候,看到了一种传值方式是以前没有写过的,就萌生了一探究竟的想法。在此之前,有篇文章曾涉及到这个话题,但那篇文章着重于处理流程的分析,并未深入。

本文重点来看几种传参方式,看看它们都是如何被解析并应用到方法参数上的。

一、http请求处理流程

不论在springboot还是springmvc中,一个http请求会被dispatcherservlet类接收,它本质是一个servlet,因为它继承自httpservlet。在这里,spring负责解析请求,匹配到controller类上的方法,解析参数并执行方法,最后处理返回值并渲染视图。

SpringBoot如何解析参数的深入理解

我们今天的重点在于解析参数,对应到上图的目标方法调用这一步骤。既然说到参数解析,那么针对不同类型的参数,肯定有不同的解析器。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。

SpringBoot如何解析参数的深入理解

面对这样一个请求,我们在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做的就是通过注解,尽量适配大部分应用场景。

好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对的支持。