目录
一,问题探索
在spring boot开发中,面向前端的接口大多是restful类型的,这样就出了一个问题,如果我在测试时通过浏览器测试接口,get类型的自然不在话下,传值也是直接拼接在url后,但是面对其他的http方式时就需要通过工具了,那么各种诸如加了@RequestParam注解的参数,不加注解的参数,@RequestBody的参数你真的明白了与后端是怎样的对应关系了吗?我反正是很懵逼,于是便有了这篇文章。
二,问题解决
1,事前准备
1.1 前端调试工具准备
在chrome浏览器中可以通过安装postman插件的方式,使浏览器可以发送其他的用户定义请求;在Firefox中可以通过安装RESTClient来完成,本文便是使用了后者,因为后者直接选择“附加组件”,然后就可以搜索相应的组件进行安装,十分方便,而前者由于无法连接google应用商店,所以在安装的时候多半是需要自己在网上找到相应的插件,然后在开发者模式中进行源文件安装的方式。
安装好RESTClient中,在浏览器右上角可以直接找到它,单击点开,它是长这个样子的:
可以添加相应的http头字段,选择请求方式,编辑正文内容,正文也就是RequestBody的内容,十分方便用于测试
1.2 后端接口准备
定义一个User类,来模仿实际业务中的增删查改,User类如下:
@Data
public class User {
private Integer id;
private String name;
private Integer age;
}
即简单的包含用户的id,名字和年龄信息,@Data注解可以省去自己手动添加getter,setter,toString方法,十分方便,idea可以通过安装Lomback插件来实现。
2,开始测试
2.1 简单测试
首先测试最简单的情况,便是通过get方式来提交用户信息,最后再返回其信息,代码如下:
@RequestMapping(value = "/user/add1",method = RequestMethod.GET)
public User userAdd2(String name,Integer age){
User user = new User();
user.setId(1);
user.setAge(age);
user.setName(name);
return user;
}
使用RESTClient测试接口:
返回的结果:
通过上面的返回值就代表我们成功了,在实际开发中其实使用更多的是加上@RequestParm注解,如下:
@RequestMapping(value = "/user/add",method = RequestMethod.GET)
public User userAdd(@RequestParam("name")String name,@RequestParam("age")Integer age){
User user = new User();
user.setId(1);
user.setAge(age);
user.setName(name);
return user;
}
修改url使其映射到该接口其他参数保持不变,经过测试发现依旧返回了结果,此时可能有小伙伴疑惑,加该参数和不加都能成功,两者又有什么差别呢?
修改url为:
http://localhost:8090/user/add?name=123
再测试userAdd接口,此时会发现报错了,结果为:
简单点说就是我需要age这个参数,但是你没有,所以你这个请求是有问题的,那么对于不加@RequestParam注解的方法来说有这种情况吗?感兴趣的童鞋可以测试,发现即使不传值也没有问题,只是返回的结果中age为null。这也就是为什么我们在开发中将实体类的成员基本类型都定义为其包装对象的原因,即使没传值也不会出现异常,也就是返回时直接为null了。
实际上,@RequestParam注解的功能远远不止这些,在实际中往往接口是这样定义的
@RequestMapping(value="/user/add2",method = RequestMethod.GET)
public User userAdd3(@RequestParam("name")String name,
@RequestParam(value = "age",required = false,defaultValue = "0")Integer userAge){
User user = new User();
user.setId(1);
user.setAge(userAge);
user.setName(name);
return user;
}
该接口与上面接口有两个显著不同,第一,age参数我们给它取了给别名为userAge,二,其required参数为false,defaultValue置为0,从字面意思我们也能够明白加上的作用,现在我们以上一个接口的参数进行测试,发现,没有报错,而且不传age,其值也不为null,而为0了。此时我们总结一下@RequestParam注解加与不加的区别:
(1)添加了@RequestParam注解的参数在前端传递的时候相应的key不能没有,值不传递是可以的,而不加该注解则没有这种约束。
(2)将@RequestParam注解的required参数置为false,在传递的时候不传值也不会报错(细心的童鞋查看该注解定义会发现该属性默认为true,这也就是为啥不传递就报错了),
同时可以指定defaultValue,这样对于要求不严格的值我们就可以在前端没有传递时指定它的默认值了,
这样也就不会有在读取时会返回null的尴尬了,毕竟用户不知道null是个什么意思。
(3)@RequestParam(value="") 我们可以指定参数的名字,这个值指定的是前端传递过来的参数key值,在后端接收的时候可以用另一个变量值来接收,例如此例中用userAge来接受age的值
2.2 post方式传值
2.2.1 发送表单数据
好奇的童鞋可能会问在实际中我们添加用户信息的时候都是用的post方式来传递,那又该怎么传递呢?添加新的接口,如下:
@RequestMapping(value="/user/add3",method = RequestMethod.POST)
public User userAdd4(@RequestParam("name")String name,
@RequestParam(value = "age",required = false,defaultValue = "0")Integer userAge){
User user = new User();
user.setId(1);
user.setAge(userAge);
user.setName(name);
return user;
}
向新的接口发送请求:
此时会发现能得到正确的结果,但是参数是拼接在url中的,这和post请求是不相符的,我们知道post在传递值时为了安全,数据是放在请求体中,因此修改请求如下:
但是却报错了,为什么参数没有传递成功呢?回想在前端上传表单内容时,需要使用form的标签;在上传文件的时候还需要显式的定义enctype为“multipart/form-data”,实际上,普通表单的enctype默认使用了
"application/x-www-form-urlencoded",而enctype对应的就为请求头中的content-type,对于不同的content-type,浏览器会对接收的内容有不同的解释。
常用的content-type类型如下:
application/x-www-form-urlencoded //传统表单
application/json //JSON数据格式
text/html //html格式
text/plain //纯文本格式
于是,修改请求添加请求头信息,告诉后端我们传递的是表单内容,数据是放在请求体内的。
接着,以同样的参数信息发送请求,可以看到后端成功的返回了数据。
2.2.2 发送json数据
趁热打铁,spring mvc中有@RequestBody的注解,它的作用呢就是接收前端传递过来的json数据,然后根据字段映射到相应的实体类上。提到json呢就得介绍一下他的格式:
//json主要分为三种类型,以user为说明对象
(1)简单类型
{"id":1,“name”:"张三","age":17}
(2)数组类型,主要对应于Java中的list
eg:
[ {"id":1,“name”:"张三1","age":17},
{"id":2,“name”:"张三2","age":17},
{"id":3,“name”:"张三3","age":17}
]
(3)嵌套类型,如map中的key为String,value为List
{
"张三一家": [
{
"id": 1,
"name": "张三1",
"age": 17
},
{
"id": 2,
"name": "张三2",
"age": 17
},
{
"id": 3,
"name": "张三3",
"age": 17
}
],
"公司a": [
{
"id": 1,
"name": "yuangongA",
"age": 22
},
{
"id": 2,
"name": "yuangongB",
"age": 22
}
]
}
json之所以这么受欢迎,一是因为json就像xml一样它们是与特定平台无关的,所以能够跨平台进行数据传递;二是因为json是JavaScript的一种子集,所以可以在js里面直接解析后台返回的json数据。
后端新增相应的接口:
@RequestMapping(value="/user/add4",method = RequestMethod.POST)
public User userAdd5(@RequestBody User user){
if(null == user)
return new User();
return user;
}
由于现在后端接收的是json数据,所以如果你不指定相应的content-type为json就会报错,如下:
从上图可以得出,RESTClient在不指定Content-Type时默认为text/plain ;之后添加请求头信息为json。
发送请求,发现后台正常解析,返回了正确的结果。需要注意的是,指定了请求体内容为json,就需要在request body中输入正确的json格式的数据,否则会json解析报错。而在用ajax进行后台请求时就需要如下设置,下面的contentType指定了发送的数据内容为json,而dataType指定了后端请求成功后返回的数据为json。
$.ajax({
url:'http://127.0.0.1:8090/user/add4',
type:'post',
contentType:'application/json',
dataType:'json',
data:JSON.stringify({"id":1,"name":"张三","age":17}),
success:function(data){
console.log('成功');
},
error:function(){
console.log('失败');
}
});
js提供了两个api对json进行处理:
JSON.stringify( ) //将一个json对象转为JSON字符串
JSON.parse() //将接收的json字符串转化为js对象
注意:json字符串为请求体内容,但是对于get请求,即使将内容放到请求体中,后端也无法接收,大概是因为get请求是定义为没有请求体的,所以浏览器将多余的请求体数据自动丢弃了。
2.2.3 后端接收对象
此时有童鞋可能要说,json内容多难拼啊,还容易出错,那么有没有一种方式可以将前端传送的数据直接映射到Java实体类上?
是通过@RequestParam吗?经过测试,无论是将参数放在url中还是请求体中都会报参数名user没有找到的错误,那么不加任何注解修饰呢?
@RequestMapping(value="/user/add5",method = RequestMethod.POST)
public User userAdd6(@RequestParam User user){
return user;
}
@RequestMapping(value="/user/add6",method = RequestMethod.POST)
public User userAdd7(User user){
return user;
}
图上的userAdd6方法就是相应的失败例子,当将@RequestParam去掉后,经测试,无论将数据放到url后拼接,还是请求体内,后端都能成功接收到,这种情况下即使前端不传值也是可以的,所以在代码中需要进行相应的判空操作。
2.2.4 多值传递问题
在接收对象参数时,spring会拦截相应的请求,将前端key-value的参数生成我们想要的对象类型,然后注入到方法的参数上,传递一个对象可以,那么传递多个呢?
增加相应的接口,如下:
@RequestMapping(value = "/user/add7",method = RequestMethod.POST)
public String userAdd8(User user,String address){
log.info("user is " + user);
log.info("address is " + address);
return user+"---address is " + address;
}
我们首先测试的是一个User和一个Address的情况,修改RESTClient的请求参数:
最后对于额外的String对象得到了正确的结果,那要是再传递一个user对象呢?添加新的接口:
@RequestMapping(value = "/user/add8",method = RequestMethod.POST)
public List<User> userAdd8(User user1, User user2){
log.info("user1 is {}",user1);
log.info("user2 is {}",user2);
List<User> lists = new ArrayList<>();
if(user1 != null)
lists.add(user1);
if(user2 != null)
lists.add(user2);
return lists;
}
测试:
果然出了问题,对于相同类型,会出现映射错误的情况,其实不难理解,多个同名参数就不知道哪个是哪个的了,那么就没有办法了吗?其实对于多值传递的情况,完全可以用一个map来进行接收,这样不管你有多少参数我都可以放到一个map里,并通过key来进行访问,此时map多与@RequestBody注解配合使用,注意,@RequestBody只对一个参数有效,即一个方法中参数有两个@RequestBody是不合理的。增加接口如下:
@RequestMapping(value = "/user/add9",method = RequestMethod.POST)
public List<User> userAdd10(@RequestBody Map<String,User> maps){
Assert.assertNotNull(maps);
User user1 = (User)maps.get("user1");
User user2 = (User)maps.get("user2");
List<User> lists = new ArrayList<>();
lists.add(user1);
lists.add(user2);
return lists;
}
进行调用:
此时可以发现我们获得了正确的结果,这里还有一个坑就是,当你定义map类型为Map<String,Object>时,解析会失败,报无法转换到user的错误,这是因为你的id会解析为一个Object,name会解析为一个Object,age也会,因此你传递的json会被解析为Map<String,List<String>>的类型,所以会报错,解决方式就是显式地定义value为User类对象,这样转换就不会有错了。
2.2.5 多值传递的其他情况
在开发中有一个场景,需要根据前台传递过来的id进行批量删除操作,那么怎么把id传过来呢?你当然可以将id封装到map中,但是还是太过麻烦,其实还有更简单的方式。
(1)使用@PathVariable注解
//@PathVariable注解可以将url中的请求路径映射到方法形参上
@RequestMapping(value = "/user/delete/{id1}/{id2}/{id3}",method = RequestMethod.DELETE)
public int deleteUserByIds(
@PathVariable("id1")Integer id1,
@PathVariable("id2")Integer id2,
@PathVariable("id3")Integer id3){
return 0;
}
如上,你可以充分利用请求路径,获取相应的参数,但是当参数很多时,这样肯定是不靠谱的,因为url会变得特别长,所以适合参数较少的情况下使用。
(2)使用String参数
我们可以用一个String参数来获取前端传递过来的id值,比较常见的作法是,让不同的id通过逗号进行连接,拼接为一个参数,这样在后端进行处理,如此便很好的控制了参数个数
@RequestMapping(value = "/user/delete",method = RequestMethod.DELETE)
public int deleteByIds1(@RequestParam("ids")String ids){
//不同id之间通过逗号分隔
//ids=1,2,3,4,5,6
String[] temp = ids.split(",");
//do something
return 0;
}
(3)传递list
前端完全可以传递一个Array过来,我在后端直接用List或数组进行接收,增加新的接口如下:
@RequestMapping(value = "/user/delete/ids",method = RequestMethod.DELETE)
public List<String> deleteByIds2(@RequestParam("ids")List<String> ids){
if(ids == null)
return new ArrayList<>();
return ids;
}
进行测试:
发送请求可以获得正确的结果,可是眼尖的小伙伴一眼就发现传递的参数有些别扭;因为定义的key值为ids,所以如果要传多个就重复了多次,那么有没有别的办法?
(1)其实熟悉前端的小伙伴肯定知道:
var a = [1,2,3,4,5];
var data = {ids:a};
然后在传递的时候将对象data转换为json串,后端修改接口将注解参数更改为@RequestBody.
(2)不单纯的只接受字符串类型的ids,我们可以封装一个dto类,如下:
@Data
public class IdDto {
public Integer id;
}
增加相应的后台接口:
@RequestMapping(value = "/user/delete/ids1",method = RequestMethod.DELETE)
public List<IdDto> deleteByIds3(@RequestBody List<IdDto> ids){
if(ids == null)
return new ArrayList<>();
return ids;
}
测试:
测试成功!
三,总结
终于完了,本文主要介绍了针对不同的后端参数组合,在使用工具测试时该如何传值,特此记录下来。
其实在实际开发中还有很多方便的测试接口工具,如Linux与Windows上都有的curl工具,它就是用来测试一个接口的,用法如下,十分方便,-X指定不同的http请求方式,-v 显示请求的详细过程,另外,市面上大多数的测试工具都是对curl进行了一层包装。使用如下(对应本文最后的一个例子):
curl -X DELETE -H 'Content-Type: application/json' -i http://localhost:8090/user/delete/ids1
--data '[{"id":1},{"id":2},{"id":3}]'
用过spring boot的同学可以尝试使用swagger-ui ,有了它能够自动生成接口的相关信息,想测试时直接在网页上测试就行了,十分方便。。。那时候你会觉得这篇文章其实没什么用。