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的設計進行自己的學習總結,讀者朋友,你也需要自己實踐和學習哦,有問題的可以找我討論。