【從零入門系列-4】Spring Boot 之 WEB接口設計實現

【從零入門系列-4】Spring Boot 之 WEB接口設計實現

文章系列


前言

前一章簡述了已經實現了對數據庫的增刪改查以及複雜查詢的功能,這一步將對相應的功能方法封裝成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;
}

考慮到數據庫的操作需要用到BookJpaRepositoryBookService,這裏首先聲明這兩個屬性,並使用@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變量。

爲了接口通用,返回值增加了字段codeinfo分別用來返回錯誤碼和錯誤信息,返回數據放在字段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請求,如果成功返回書籍信息,否則返回錯誤信息。

使用瀏覽器測試結果如下:

1557907999084
刪除不存在的書籍時

[外鏈圖片轉存失敗(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]更新成功"
}

從上述執行結果我們可以看到,在使用JpaRepositorysave更新數據時,只會更新非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;
}

測試執行結果如下:

1557909406789

搜索接口

由於我們之前的搜索接口的入參類型爲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;
}

此處返回rowstotal是爲了後續Web頁面的bootstrap-table需要,該控件根據這兩個數據以表格化的形式展示查詢結果數據。

由於此處使用POST請求類型,測試時依舊使用MockMvcWebApplicationContext,測試代碼如下:

@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接口,下一篇我們將從前端一起整合整個項目,實現數據的展示和管理,敬請期待。

下一篇

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章