鍍金池/ 教程/ Java/ RESTful by Spring Boot with MySQL
通過JMX監(jiān)控Spring Boot應(yīng)用
Spring Boot:定制PropertyEditors
配置是否初始化Bean的方法
Spring Boot的自動配置、Command-line Runner
Spring Boot:定制URL匹配規(guī)則
Spring Boot的自動配置、Command-line Runner
利用Mockito模擬DB
Spring Boot應(yīng)用的打包和部署
了解Spring Boot的自動配置
Spring Boot應(yīng)用的健康監(jiān)控
了解Spring Boot的自動配置
初始化數(shù)據(jù)庫和導(dǎo)入數(shù)據(jù)
Spring Boot應(yīng)用的健康監(jiān)控
Docker with Spring Boot
RESTful by Spring Boot with MySQL
Spring Boot:定制攔截器
Spring Boot:定制static path mappings
Spring Boot with Mysql
Spring Boot:定制自己的starter
在測試中使用內(nèi)存數(shù)據(jù)庫
Restful: Spring Boot with Mongodb
Spring Boot with Redis
Spring Boot:定制HTTP消息轉(zhuǎn)換器
Spring Boot: Data Rest Service
Spring Boot:定制type Formatters
在Spring Boot項目中使用Spock框架
選擇Spring Boot項目的內(nèi)嵌容器
通過EmbeddedServletContainerCustomizer接口調(diào)優(yōu)Tomcat
Spring Boot應(yīng)用的打包和部署
Spring Boot Admin的使用
讓你的Spring Boot工程支持HTTP和HTTPS
Spring Boot:定制servlet filters
Spring Boot:定制URL匹配規(guī)則
Spring Boot應(yīng)用的測試——Mockito
Spring Boot應(yīng)用的測試——Mockito
Spring Boot:定制servlet filters
通過@Enable*注解觸發(fā)Spring Boot配置

RESTful by Spring Boot with MySQL

現(xiàn)在的潮流是前端承擔(dān)越來越多的責(zé)任:MVC中的V和C,后端只需要負責(zé)提供數(shù)據(jù)M,但是后端有更重要的任務(wù):高并發(fā)、提供各個維度的擴展能力(負載均衡、數(shù)據(jù)表切分、服務(wù)分離)、更清晰的API設(shè)計。Spring Boot框架提供的機制便于工程師實現(xiàn)標準的RESTful接口,本文主要討論如何編寫Controller代碼,另外還涉及了MySQL的數(shù)據(jù)庫操作,之前我也寫過一篇關(guān)于Mysql的文章link,但是這篇文章加上了CRUD的操作。

先回顧下之前的文章中我們用到的例子:圖書信息管理系統(tǒng),主要的領(lǐng)域?qū)ο笥衎ook、author、publisher和reviewer。

首先我們要在pom文件中添加對應(yīng)的starter,即spring-boot-starter-web,對應(yīng)的xml代碼示例為:

<dependency>   
     <groupId>org.springframework.boot</groupId>   
     <artifactId>spring-boot-starter-web</artifactId>
</dependency>

然后我們要創(chuàng)建控制器(Controller),先在項目根目錄下創(chuàng)建controller包,一般為每個實體類對象創(chuàng)建一個控制器,例如BookController。

@RestController注解是@Controller和@ResponseBody的合集,表示這是個控制器bean,并且是將函數(shù)的返回值直接填入HTTP響應(yīng)體中,是REST風(fēng)格的控制器。@RequestMapping("/books")表示該控制器處理所有“/books”的URL請求,具體由那個函數(shù)處理,要根據(jù)HTTP的方法來區(qū)分:GET表示查詢、POST表示提交、PUT表示更新、DELETE表示刪除。

  • 查詢所有圖書記錄:利用@Autowired導(dǎo)入BookRepository的Bean,直接調(diào)用bookRepository.findAllBooks()即可。我們的返回值形式如下。關(guān)于RESTful返回值形式的設(shè)計,后續(xù)會有專門的文章討論。
{
    "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": []
        }
    ]
}
  • 根據(jù)isbn查詢圖書記錄:根據(jù)isbn查詢一本書的記錄,調(diào)用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對象,并將它們存入數(shù)據(jù)庫;然后才生成book對象,并調(diào)用bookRepository.save(book)將book記錄存入數(shù)據(jù)庫。該接口的返回值會把剛添加的圖書信息返回給客戶端,形式類似于getBookByIsbn這個接口。
  • 更新圖書書名,這里簡單以這個接口作為更新的例子。主要步驟是先取出對應(yīng)isbn的book對象,然后book.setTitle(title)更新book信息,然后調(diào)用bookRepository.save(book)更新該對象的信息,通過@PathVariable修飾的參數(shù)title與URL中用“{title}”的值對應(yīng)。
  • 刪除圖書記錄;給定圖書的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<Book> getAllBooks() {
        return bookRepository.findAll();
    }

    @RequestMapping(value = "/{isbn}", method = RequestMethod.GET)
    public Map<String, Object> getBook(@PathVariable String isbn) {
        Book book =  bookRepository.findBookByIsbn(isbn);

        Map<String, Object> response = new LinkedHashMap<>();
        response.put("message", "get book with isbn(" + isbn +")");
        response.put("book", book);
        return response;
    }

    @RequestMapping(method = RequestMethod.POST)
    public Map<String, Object> 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<String, Object> response = new LinkedHashMap<>();
        response.put("message", "book add successfully");
        response.put("book", book);
        return response;
    }

    @RequestMapping(value = "/{isbn}", method = RequestMethod.DELETE)
    public Map<String, Object> deleteBook(@PathVariable String isbn) {
        Map<String, Object> 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<String, Object> updateBookTitle(@PathVariable String isbn, @PathVariable String title) {
        Map<String, Object> 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;
    }
}

有三個問題需要補充探討

現(xiàn)在我要說下Controller的角色,大家可以看到,我這里將很多業(yè)務(wù)代碼混淆在Controller的代碼中。實際上,根據(jù)程序員必知之前端演進史一文所述Controller層應(yīng)該做的事是: 處理請求的參數(shù) 渲染和重定向 選擇Model和Service 處理Session和Cookies,我基本上認同這個觀點,最多再加上OAuth驗證(利用攔截器實現(xiàn)即可)。而真正的業(yè)務(wù)邏輯應(yīng)該單獨分處一層來處理,即常見的service層;

今天遇到一個類似參考資料2中的錯誤,我經(jīng)過查找后發(fā)現(xiàn)是Jakson解析我的對象的時候出現(xiàn)了無限遞歸解析,究其原因,是因為外鏈:解析book的時候,需要解析author,但是在author中又有books選項,所以造成死循環(huán),解決的辦法就是在author中的books屬性上加上注解:@JsonBackReference;同樣需要在Publisher類中的books屬性加上@JsonBackReference注解。

上述演示的Controller代碼還有兩個問題:返回值形式不統(tǒng)一;并沒有遵循標準的API設(shè)計(例如update方法實際上應(yīng)該由客戶端返回更新過的完整對象,這樣就可以直接調(diào)用save方法),后續(xù),我會參考RESTful API 設(shè)計指南進行學(xué)習(xí),對API的設(shè)計進行自己的學(xué)習(xí)總結(jié),讀者朋友,你也需要自己實踐和學(xué)習(xí)哦,有問題的可以找我討論。

參考資料

  1. repository中的update方法
  2. 使用spring data創(chuàng)建REST應(yīng)用
  3. 遇到的一個錯誤:at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize
  4. SPRING BOOT: DATA ACCESS WITH JPA, HIBERNATE AND MYSQL