Spring Boot 學習筆記(1.3):RESTful by Spring Boot with MySQL
現在的潮流是前端承擔越來越多的責任:MVC中的V和C,后端只需要負責提供數據M,但是后端有更重要的任務:高并發、提供各個維度的擴展能力(負載均衡、數據表切分、服務分離)、更清晰的API設計。Spring Boot框架提供的機制便于工程師實現標準的RESTful接口,本文主要討論如何編寫Controller代碼,另外還涉及了MySQL的數據庫操作, 之前我也寫過一篇關于Mysql的文章 ,但是這篇文章加上了CRUD的操作。
先回顧下之前的文章中我們用到的例子:圖書信息管理系統,主要的領域對象有book、author、publisher和reviewer。
首先我們要在pom文件中添加對應的starter,即 spring-boot-starter-web ,對應的xml代碼示例為:
org.springframework.bootspring-boot-starter-web
然后我們要創建控制器(Controller),先在項目根目錄下創建controller包,一般為每個實體類對象創建一個控制器,例如BookController。
@RestController注解是@Controller和@ResponseBody的合集,表示這是個控制器bean,并且是將函數的返回值直接填入HTTP響應體中,是REST風格的控制器。@RequestMapping(“/books”)表示該控制器處理所有“/books”的URL請求,具體由那個函數處理,要根據HTTP的方法來區分:GET表示查詢、POST表示提交、PUT表示更新、DELETE表示刪除。
- 查詢所有圖書記錄:利用@Autowired導入BookRepository的Bean,直接調用bookRepository.findAllBooks()即可。我們的返回值形式如下。關于RESTful返回值形式的設計,后續會有專門的文章討論。
{ "message": "get all books", "book": [ { "isbn": "9781-1234-5678", "title": "你愁啥", "description": "這是一本奇怪的書", "author": { "firstName": "馮", "lastName": "pp" }, "publisher": { "name": "大錘出版社" }, "reviewers": [] }, { "isbn": "9781-1234-1111", "title": "別吵吵", "description": "哈哈哈", "author": { "firstName": "杜琪", "lastName": "琪" }, "publisher": { "name": "大錘出版社" }, "reviewers": [] } ] }
- 根據isbn查詢圖書記錄:根據isbn查詢一本書的記錄,調用bookRepository.findBookByIsbn()即可。返回值形式如下:
{ "message": "get book with isbn(9781-1234-5678)", "book": { "isbn": "9781-1234-5678", "title": "你愁啥", "description": "這是一本奇怪的書", "author": { "firstName": "馮", "lastName": "pp" }, "publisher": { "name": "大錘出版社" }, "reviewers": [] } }
- 添加圖書記錄,客戶端的圖書信息封裝成json字符串傳遞過來,因此利用@RequestBody獲取POST請求體,由于book記錄中有外鏈記錄,因此要首先解析出author對象和publisher對象,并將它們存入數據庫;然后才生成book對象,并調用bookRepository.save(book)將book記錄存入數據庫。該接口的返回值會把剛添加的圖書信息返回給客戶端,形式類似于getBookByIsbn這個接口。
- 更新圖書書名,這里簡單以這個接口作為更新的例子。主要步驟是先取出對應isbn的book對象,然后 book.setTitle(title) 更新book信息,然后調用bookRepository.save(book)更新該對象的信息,通過@PathVariable修飾的參數title與URL中用“{title}”的值對應。
- 刪除圖書記錄;給定圖書的isbn直接刪除即可。
最后,放上完整的Controller代碼:
package com.test.bookpub.controller; import com.alibaba.fastjson.JSONObject; import com.test.bookpub.domain.Author; import com.test.bookpub.domain.Book; import com.test.bookpub.domain.Publisher; import com.test.bookpub.repository.AuthorRepository; import com.test.bookpub.repository.BookRepository; import com.test.bookpub.repository.PublisherRepository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import java.util.*; /** * @author duqi * @create 2015-12-02 18:18 */ @RestController @RequestMapping("/books") public class BookController { private static final Logger logger = LoggerFactory.getLogger(BookController.class); @Autowired private BookRepository bookRepository; @Autowired public AuthorRepository authorRepository; @Autowired public PublisherRepository publisherRepository; @RequestMapping(method = RequestMethod.GET) public Iterable getAllBooks() { return bookRepository.findAll(); } @RequestMapping(value = "/{isbn}", method = RequestMethod.GET) public Map getBook(@PathVariable String isbn) { Book book = bookRepository.findBookByIsbn(isbn); Map response = new LinkedHashMap(); response.put("message", "get book with isbn(" + isbn +")"); response.put("book", book); return response; } @RequestMapping(method = RequestMethod.POST) public Map addBook(@RequestBody JSONObject bookJson) { JSONObject authorJson = bookJson.getJSONObject("author"); Author author = new Author(authorJson.getString("firstName"), authorJson.getString("lastName")); authorRepository.save(author); String isbn = bookJson.getString("isbn"); JSONObject publisherJson = bookJson.getJSONObject("publisher"); Publisher publisher = new Publisher(publisherJson.getString("name")); publisherRepository.save(publisher); String title = bookJson.getString("title"); String desc = bookJson.getString("desc"); Book book = new Book(author, isbn, publisher, title); book.setDescription(desc); bookRepository.save(book); Map response = new LinkedHashMap(); response.put("message", "book add successfully"); response.put("book", book); return response; } @RequestMapping(value = "/{isbn}", method = RequestMethod.DELETE) public Map deleteBook(@PathVariable String isbn) { Map response = new LinkedHashMap(); try { bookRepository.deleteBookByIsbn(isbn); } catch (NullPointerException e) { logger.error("the book is not in database"); response.put("message", "delete failure"); response.put("code", 0); } response.put("message", "delete successfully"); response.put("code", 1); return response; } @RequestMapping(value = "/{isbn}/{title}", method = RequestMethod.PUT) public Map updateBookTitle(@PathVariable String isbn, @PathVariable String title) { Map response = new LinkedHashMap(); Book book = null; try { book = bookRepository.findBookByIsbn(isbn); book.setTitle(title); bookRepository.save(book); } catch (NullPointerException e) { response.put("message", "can not find the book"); return response; } response.put("message", "book update successfully"); response.put("book", book); return response; } }
有三個問題需要補充探討
現在我要說下Controller的角色,大家可以看到,我這里將很多業務代碼混淆在Controller的代碼中。實際上,根據 程序員必知之前端演進史 一文所述Controller層應該做的事是: 處理請求的參數 渲染和重定向 選擇Model和Service 處理Session和Cookies,我基本上認同這個觀點,最多再加上OAuth驗證(利用攔截器實現即可)。而真正的業務邏輯應該單獨分處一層來處理,即常見的service層;
今天遇到一個類似參考資料2中的錯誤,我經過查找后發現是Jakson解析我的對象的時候出現了無限遞歸解析,究其原因,是因為外鏈:解析book的時候,需要解析author,但是在author中又有books選項,所以造成死循環,解決的辦法就是在author中的books屬性上加上注解:@JsonBackReference;同樣需要在Publisher類中的books屬性加上@JsonBackReference注解。
上述演示的Controller代碼還有兩個問題:返回值形式不統一;并沒有遵循標準的API設計(例如update方法實際上應該由客戶端返回更新過的完整對象,這樣就可以直接調用save方法),后續,我會參考 RESTful API 設計指南 進行學習,對API的設計進行自己的學習總結,讀者朋友,你也需要自己實踐和學習哦,有問題的可以找我討論。