目錄
一,問題探索
在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 ,有了它能夠自動生成接口的相關信息,想測試時直接在網頁上測試就行了,十分方便。。。那時候你會覺得這篇文章其實沒什麼用。