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

文章系列


前言

前一章簡述了已經實現了對數據庫的增刪改查以及複雜查詢的功能,這一步將對相應的功能方法封裝成WEB接口,對外提供WEB接口服務。前端


控制層類設計及測試

控制層的角色是負責對訪問路由處處理過程的關聯映射和封裝,在這裏咱們隊Book新建一個控制類便可,在文件夾Controller上右鍵,New->Java Class新建BookController類。做爲控制類,該類須要使用@Controller註解使之可以被識別爲控制類對象,在這裏咱們使用@RestController,該註解包含了@Controller,至關於@Controller+@ResponseBody兩個註解的結合,適合返回Json格式的控制器使用。java

類定義

@RestController
@RequestMapping(path = "/library")
public class BookController {
    @Autowired
    private BookJpaRepository bookJpaRepository;

    @Autowired
    private BookService bookService;
}

考慮到數據庫的操做須要用到BookJpaRepositoryBookService,這裏首先聲明這兩個屬性,並使用@Autowired註解自動裝配。git

在類上使用@RequestMapping(path = "/library")註解後,定義了該類的路徑都是/library開始,能夠統一接口路徑,避免重複書寫。github

新增接口

/**
* 新增書籍
* @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字符串格式。web

接口參數@RequestParam的註解用於將指定的請求參數賦值給方法中的形參,默認根據參數名匹配,也可使用value指定參數名,支持的參數以下:spring

  • name:形參綁定的請求參數名,與value功能同樣,默認與形參名相同自動關聯
  • required:指定該參數是否必輸,默認爲True
  • defaultValue:指定該參數的默認值
  • value:與name功能相同

在該接口中,經過形參自動綁定取的入參,而後經過BookJpaRepository直接save保存新增數據,save新增後,該記錄自動生成的id值已經被設置到book變量。sql

爲了接口通用,返回值增長了字段codeinfo分別用來返回錯誤碼和錯誤信息,返回數據放在字段data數據庫

單元測試代碼json

@Autowired
private WebApplicationContext wac;

private MockMvc mockMvc;

@Before
public void setUp (){
    mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
}

引入Web的MVC單元測試對象,而後編寫Web新增接口測試用例:bootstrap

@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();
    }
}

執行結果:

1557907574221

刪除接口

根據數據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
刪除不存在的書籍時

1557908059948
正常刪除數據

更新接口

根據書籍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接口,下一篇咱們將從前端一塊兒整合整個項目,實現數據的展現和管理,敬請期待。

[下一篇]()

相關文章
相關標籤/搜索