【從零入門系列-4】Spring Boot 之 WEB接口設計實現
文章系列
- 【從零入門系列-0】Spring Boot 之 Hello World
- 【從零入門系列-1】Spring Boot 之 程序結構設計說明
- 【從零入門系列-2】Spring Boot 之 數據庫實體類
- 【從零入門系列-3】Spring Boot 之 數據庫操作類
前言
前一章簡述了已經實現了對數據庫的增刪改查以及複雜查詢的功能,這一步將對相應的功能方法封裝成WEB接口,對外提供WEB接口服務。
控制層類設計及測試
控制層的角色是負責對訪問路由到處理過程的關聯映射和封裝,在這裏我們隊Book
新建一個控制類即可,在文件夾Controller
上右鍵,New->Java Class
新建BookController
類。作爲控制類,該類需要使用@Controller
註解使之能夠被識別爲控制類對象,在這裏我們使用@RestController
,該註解包含了@Controller
,相當於@Controller+@ResponseBody兩個註解的結合,適合返回Json格式的控制器使用。
類定義
@RestController
@RequestMapping(path = "/library")
public class BookController {
@Autowired
private BookJpaRepository bookJpaRepository;
@Autowired
private BookService bookService;
}
考慮到數據庫的操作需要用到BookJpaRepository
和BookService
,這裏首先聲明這兩個屬性,並使用@Autowired
註解自動裝配。
在類上使用@RequestMapping(path = "/library")
註解後,定義了該類的路徑都是/library
開始,可以統一接口路徑,避免重複書寫。
新增接口
/**
* 新增書籍
* @param name
* @param author
* @param image
* @return
*/
@PostMapping("/save")
public Map<String, Object> save(@RequestParam String name, @RequestParam String author, @RequestParam String image){
Book book = new Book();
Map<String, Object> rsp = new HashMap<>();
book.setName(name);
book.setAuthor(author);
book.setImage(image);
bookJpaRepository.save(book);
rsp.put("data", book);
rsp.put("code", "0");
rsp.put("info", "成功");
return rsp;
}
使用@PostMapping
表示接口只接受POST請求,WEB接口路徑爲/library/save
,該接口返回的是一個Map類型對象,但是由於類使用@RestController
註解後,使得返回結果會自動轉換成Json字符串格式。
接口參數@RequestParam
的註解用於將指定的請求參數賦值給方法中的形參,默認根據參數名匹配,也可以使用value
指定參數名,支持的參數如下:
- name:形參綁定的請求參數名,與
value
功能一樣,默認與形參名相同自動關聯 - required:指定該參數是否必輸,默認爲True
- defaultValue:指定該參數的默認值
- value:與
name
功能相同
在該接口中,通過形參自動綁定取的入參,然後通過BookJpaRepository
直接save
保存新增數據,save
新增後,該記錄自動生成的id
值已經被設置到book
變量。
爲了接口通用,返回值增加了字段code
和info
分別用來返回錯誤碼和錯誤信息,返回數據放在字段data
。
單元測試代碼
@Autowired
private WebApplicationContext wac;
private MockMvc mockMvc;
@Before
public void setUp (){
mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
}
引入Web的MVC單元測試對象,然後編寫Web新增接口測試用例:
@Test
public void webApi(){
try {
String urlRoot = "/library";
String urlApi = urlRoot + "/view/1";
MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.get(urlApi)
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(MockMvcResultMatchers.status().isOk())
.andReturn();
System.out.println("WEB測試返回[" + urlApi + "]:" + mvcResult.getResponse().getContentAsString());
} catch (Exception e) {
e.printStackTrace();
}
}
執行結果:
[外鏈圖片轉存失敗(img-7L3yNWzP-1567739303665)(https://raw.githubusercontent.com/arbboter/resource/master/segmentfault/image/SpringBoot/20190515-WEB接口設計實現/1557907574221.png)]
刪除接口
根據數據ID刪除書籍,且數據id作爲請求路徑的一部分,不通過@RequestParam
獲取,而是通過@PathVariable("id")
,代碼如下:
/**
* 刪除書籍
* @param id
* @return
*/
@GetMapping("/remove/{id}")
public Map<String, Object> removeById(@PathVariable("id") Integer id){
Map<String, Object> rsp = new HashMap<>();
Optional<Book> book = bookJpaRepository.findById(id);
if(!book.isPresent()) {
rsp.put("code", 1001);
rsp.put("info", "書籍ID[" + id + "]不存在");
}else {
bookJpaRepository.deleteById(id);
rsp.put("code", 0);
rsp.put("info", "書籍ID[" + id + "]刪除成功");
rsp.put("data", book);
}
return rsp;
}
@PathVariable
只支持一個屬性value,類型是爲String,代表綁定的屬性名稱,默認綁定爲同名的形參。
在接口中,我們使用@GetMapping
接收處理GET請求,如果成功返回書籍信息,否則返回錯誤信息。
使用瀏覽器測試結果如下:
刪除不存在的書籍時
[外鏈圖片轉存失敗(img-1EJh5EE3-1567739303667)(https://raw.githubusercontent.com/arbboter/resource/master/segmentfault/image/SpringBoot/20190515-WEB接口設計實現/1557908059948.png)]
正常刪除數據
更新接口
根據書籍ID更新書籍信息,參數信息使用HttpServletRequest
和路徑參數相配合
/**
* 更新書籍
* @param id
* @param request
* @return
*/
@PostMapping("/edit/{id}")
public Map<String, Object> updateById(@PathVariable("id") Integer id, HttpServletRequest request){
Map<String, Object> rsp = new HashMap<>();
Optional<Book> book = bookJpaRepository.findById(id);
if(!book.isPresent()) {
rsp.put("code", 1001);
rsp.put("info", "書籍ID[" + id + "]不存在");
}else {
Book bookUpd = book.get();
if(request.getParameter("name") != null){
bookUpd.setName(request.getParameter("name"));
}
if(request.getParameter("author") != null){
bookUpd.setAuthor(request.getParameter("author"));
}
if(request.getParameter("image") != null){
bookUpd.setImage(request.getParameter("image"));
}
rsp.put("code", 0);
rsp.put("info", "書籍ID[" + id + "]更新成功");
rsp.put("data", bookUpd);
}
return rsp;
}
HttpServletRequest
對象代表客戶端的請求,當客戶端通過HTTP協議訪問服務器時,HTTP請求頭中的所有信息都封裝在這個對象中,通過這個對象提供的方法,可以獲得客戶端請求的所有信息。
單元測試用例:
@Test
public void webBookEdit() throws Exception {
String url = "/library/edit/2";
// 只修改名字
MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.post(url)
.param("name", "webBookEdit1")
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(MockMvcResultMatchers.status().isOk())
.andReturn();
System.out.println("1-WEB測試返回[" + url + "]:" + mvcResult.getResponse().getContentAsString());
// 修改名字和作者
mvcResult = mockMvc.perform(MockMvcRequestBuilders.post(url)
.param("name", "webBookEdit2")
.param("author", "webBookEdit2")
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(MockMvcResultMatchers.status().isOk())
.andReturn();
System.out.println("2-WEB測試返回[" + url + "]:" + mvcResult.getResponse().getContentAsString());
}
執行結果(JSON格式化處理過)
Hibernate: select book0_.id as id1_0_0_, book0_.author as author2_0_0_, book0_.image as image3_0_0_, book0_.name as name4_0_0_ from library_book book0_ where book0_.id=?
1-WEB測試返回[/library/edit / 2]:
{
"code": 0,
"data": {
"id": 2,
"name": "webBookEdit1",
"author": "arbboter",
"image": "https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=2656353677,2997395625&fm=26&gp=0.jpg"
},
"info": "書籍ID[2]更新成功"
}
Hibernate: select book0_.id as id1_0_0_, book0_.author as author2_0_0_, book0_.image as image3_0_0_, book0_.name as name4_0_0_ from library_book book0_ where book0_.id=?
2-WEB測試返回[/library/edit/2]:
{
"code": 0,
"data": {
"id": 2,
"name": "webBookEdit2",
"author": "webBookEdit2",
"image": "https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=2656353677,2997395625&fm=26&gp=0.jpg"
},
"info": "書籍ID[2]更新成功"
}
從上述執行結果我們可以看到,在使用JpaRepository
的save
更新數據時,只會更新非null
字段,且返回結果包括完整的更新後的數據內容,即默認支持按設定的字段更新,而不是每次需要全字段更新。
查詢接口
使用路徑參數根據書籍ID獲取書籍內容信息,代碼如下:
/**
* 查看書籍
* @param id
* @return
*/
@GetMapping("/view/{id}")
public Map<String, Object> findById(@PathVariable("id") Integer id){
Map<String, Object> rsp = new HashMap<>();
Optional<Book> book = bookJpaRepository.findById(id);
if(!book.isPresent()) {
rsp.put("code", 1001);
rsp.put("info", "書籍ID[" + id + "]不存在");
}else {
rsp.put("code", 0);
rsp.put("info", "成功");
rsp.put("data", book);
}
return rsp;
}
測試執行結果如下:
搜索接口
由於我們之前的搜索接口的入參類型爲Map
,但是Web接口的入參信息都是從HttpServletRequest
獲取,因此首先需要將需要的入參信息從HttpServletRequest
轉換到Map
類型,然再使用。考慮到該轉換功能爲通用型,因此可以將該函數封裝到系統的工具包下面,新建Util
包,然後右鍵新建Util
文件,完成數據的轉換函數,代碼如下:
public class Util {
/**
* 把 @HttpServletRequest 轉換成普通的字典
* @param request
* @return
*/
public static Map getParameterMap(HttpServletRequest request) {
// 參數Map
Map properties = request.getParameterMap();
// 返回值Map
Map returnMap = new HashMap();
Iterator entries = properties.entrySet().iterator();
Map.Entry entry;
String name = "";
String value = "";
while (entries.hasNext()) {
entry = (Map.Entry) entries.next();
name = (String) entry.getKey();
Object valueObj = entry.getValue();
if(null == valueObj){
value = "";
}else if(valueObj instanceof String[]){
String[] values = (String[])valueObj;
for(int i=0;i<values.length;i++){
value = values[i] + ",";
}
value = value.substring(0, value.length()-1);
}else{
value = valueObj.toString();
}
returnMap.put(name, value);
}
return returnMap;
}
}
然後使用我們BookService
實現的封裝的複雜查詢接口即可,代碼如下:
/**
* 搜索查詢接口
* @param request
* @return
*/
@PostMapping("/search")
public Map<String, Object> search(HttpServletRequest request){
Map<String, String> map = new HashMap<>();
map = Util.getParameterMap(request);
Page<Book> books = bookService.search(map);
Map<String, Object> rsp = new HashMap<>();
rsp.put("code", 0);
rsp.put("info", "成功");
rsp.put("rows", books.getContent());
rsp.put("total", books.getTotalElements());
return rsp;
}
此處返回rows
和total
是爲了後續Web頁面的bootstrap-table
需要,該控件根據這兩個數據以表格化的形式展示查詢結果數據。
由於此處使用POST請求類型,測試時依舊使用MockMvc
和WebApplicationContext
,測試代碼如下:
@Test
public void webSearch() throws Exception{
String url = "/library/search";
// 1-無條件
MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.post(url)
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(MockMvcResultMatchers.status().isOk())
.andReturn();
System.out.println("1-無條件-WEB測試返回[" + url + "]:" + mvcResult.getResponse().getContentAsString());
// 2-根據作者名查詢
mvcResult = mockMvc.perform(MockMvcRequestBuilders.post(url)
.param("author", "作者_3")
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(MockMvcResultMatchers.status().isOk())
.andReturn();
System.out.println("2-根據作者名(作者_3)查詢-WEB測試返回[" + url + "]:" + mvcResult.getResponse().getContentAsString());
}
測試執行結果如下:
Hibernate: select TOP(?) book0_.id as id1_0_, book0_.author as author2_0_, book0_.image as image3_0_, book0_.name as name4_0_ from library_book book0_ where 1=1 order by book0_.id desc
Hibernate: select count(book0_.id) as col_0_0_ from library_book book0_ where 1=1
1-無條件-WEB測試返回[/library/search]:
{
"total": 22,
"code": 0,
"rows": [{
"id": 26,
"name": "書名_19",
"author": "作者_4",
"image": "img19"
}, {
"id": 25,
"name": "書名_18",
"author": "作者_3",
"image": "img18"
}, {
"id": 24,
"name": "書名_17",
"author": "作者_2",
"image": "img17"
}, {
"id": 23,
"name": "書名_16",
"author": "作者_1",
"image": "img16"
}, {
"id": 22,
"name": "書名_15",
"author": "作者_0",
"image": "img15"
}, {
"id": 21,
"name": "書名_14",
"author": "作者_4",
"image": "img14"
}, {
"id": 20,
"name": "書名_13",
"author": "作者_3",
"image": "img13"
}, {
"id": 19,
"name": "書名_12",
"author": "作者_2",
"image": "img12"
}, {
"id": 18,
"name": "書名_11",
"author": "作者_1",
"image": "img11"
}, {
"id": 17,
"name": "書名_10",
"author": "作者_0",
"image": "img10"
}
],
"info": "成功"
}
Hibernate: select TOP(?) book0_.id as id1_0_, book0_.author as author2_0_, book0_.image as image3_0_, book0_.name as name4_0_ from library_book book0_ where book0_.author=? order by book0_.id desc
2-根據作者名(作者_3)查詢-WEB測試返回[/library/search]:
{
"total": 4,
"code": 0,
"rows": [{
"id": 25,
"name": "書名_18",
"author": "作者_3",
"image": "img18"
}, {
"id": 20,
"name": "書名_13",
"author": "作者_3",
"image": "img13"
}, {
"id": 15,
"name": "書名_8",
"author": "作者_3",
"image": "img8"
}, {
"id": 10,
"name": "書名_3",
"author": "作者_3",
"image": "img3"
}
],
"info": "成功"
}
結束語
到這裏,整個項目的所有服務器後端部分已經完成,已經可以提供給前端使用各種常用的Web接口,下一篇我們將從前端一起整合整個項目,實現數據的展示和管理,敬請期待。