搭建 Restful Web 服務(wù)

1. 理解 REST

??REST 全稱是 Representational State Transfer,中文意思是表征性狀態(tài)轉(zhuǎn)移俯逾。它首次出現(xiàn)在2000年Roy Fielding的博士論文中滴劲,Roy Fielding是HTTP規(guī)范的主要編寫者之一。值得注意的是REST并沒有一個(gè)明確的標(biāo)準(zhǔn),而更像是一種設(shè)計(jì)的風(fēng)格暑椰。如果一個(gè)架構(gòu)符合REST的約束條件和原則,我們就稱它為RESTful架構(gòu)荐绝。
??理論上REST架構(gòu)風(fēng)格并不是綁定在HTTP上一汽,只不過目前HTTP是唯一與REST相關(guān)的實(shí)例。

1.1. REST 原則

  • 資源 可通過目錄結(jié)構(gòu)樣式的 URIs 暴露
  • 表述 可以通過 JSON 或 XML 表達(dá)的數(shù)據(jù)對象或?qū)傩詠韨鬟f
  • 消息 使用統(tǒng)一的 HTTP 方法(例如:GET低滩、POST召夹、PUT 和 DELETE)
  • 無狀態(tài) 客戶端與服務(wù)端之間的交互在請求之間是無狀態(tài)的,從客戶端到服務(wù)端的每個(gè)請求都必須包含理解請求所必需的信息

1.2. HTTP 方法

??使用 HTTP 將 CRUD(create, retrieve, update, delete <創(chuàng)建恕沫、獲取监憎、更新、刪除—增刪改查>)操作映射為 HTTP 請求婶溯。如果按照HTTP方法的語義來暴露資源鲸阔,那么接口將會(huì)擁有安全性和冪等性的特性偷霉,例如GET和HEAD請求都是安全的, 無論請求多少次褐筛,都不會(huì)改變服務(wù)器狀態(tài)类少。而GET、HEAD渔扎、PUT和DELETE請求都是冪等的瞒滴,無論對資源操作多少次, 結(jié)果總是一樣的赞警,后面的請求并不會(huì)產(chǎn)生比第一次更多的影響妓忍。

1.2.1. GET

  • 安全且冪等
  • 獲取信息

1.2.2. POST

  • 不安全且不冪等
  • 使用請求中提供的實(shí)體執(zhí)行操作,可用于創(chuàng)建資源或更新資源

1.2.3. PUT

  • 不安全但冪等
  • 使用請求中提供的實(shí)體執(zhí)行操作愧旦,可用于創(chuàng)建資源或更新資源

1.2.4. DELETE

  • 不安全但冪等
  • 刪除資源
    ??POST和PUT在創(chuàng)建資源的區(qū)別在于世剖,所創(chuàng)建的資源的名稱(URI)是否由客戶端決定。 例如為我的博文增加一個(gè)java的分類笤虫,生成的路徑就是分類名/categories/java旁瘫,那么就可以采用PUT方法。不過很多人直接把POST琼蚯、GET酬凳、PUT、DELETE直接對應(yīng)上CRUD遭庶,例如在一個(gè)典型的rails實(shí)現(xiàn)的RESTful應(yīng)用中就是這么做的宁仔。

1.3. HTTP 狀態(tài)碼

??狀態(tài)碼指示 HTTP 請求的結(jié)果:

  • 1XX:信息
  • 2XX:成功
  • 3XX:轉(zhuǎn)發(fā)
  • 4XX:客戶端錯(cuò)誤
  • 5XX:服務(wù)端錯(cuò)誤

1.4. 媒體類型

??HTTP頭中的 Accept 和 Content-Type 可用于描述HTTP請求中發(fā)送或請求的內(nèi)容。如果客戶端請求JSON響應(yīng)峦睡,那么可以將 Accept 設(shè)為 application/json翎苫。相應(yīng)地,如果發(fā)送的內(nèi)容是XML榨了,那么可以設(shè)置 Content-Type 為 application/xml 煎谍。

2. REST API 設(shè)計(jì)最佳實(shí)踐

??這里介紹一些設(shè)計(jì) REST API 的最佳實(shí)踐,大家先記住下面這句話:

URL 是個(gè)句子龙屉,其中資源是名詞呐粘、HTTP 方法是動(dòng)詞。

2.1. 使用名詞來表示資源

??下面是一些例子:

  • GET - /users:返回用戶列表
  • GET - /users/100:返回一個(gè)特定用戶
  • POST - /users:創(chuàng)建一個(gè)新用戶
  • PUT - /users/200:更新一個(gè)特定用戶
  • DELETE - /users/711:刪除一個(gè)特定用戶
    ??不要使用動(dòng)詞:
  • /getAllsers
  • /getUserById
  • /createNewUser
  • /updateUser
  • /deleteUser

2.2 在 HTTP 頭中使用適當(dāng)?shù)男蛄谢袷?/h2>

??客戶端和服務(wù)端都需要知道通信所用的格式转捕,這個(gè)格式要在 HTTP 頭中指定:

  • Content-Type 定義請求格式
  • Accept 定義一個(gè)可接受的響應(yīng)格式列表

2.3 Get 方法和查詢參數(shù)不應(yīng)當(dāng)改變狀態(tài)

??使用 PUT, POST 和 DELETE 方法來改變狀態(tài)作岖,不要使用 GET 方法來改變狀態(tài):

  • GET /users/711?activate
  • GET /users/711/activate

2.4. 使用子資源表示關(guān)聯(lián)

??如果一個(gè)資源與另一個(gè)資源關(guān)聯(lián),使用子資源:

  • GET /cars/711/drivers: 返回711號(hào)汽車的駕駛員列表
  • GET /cars/711/drivers/4: 返回711號(hào)汽車的第4號(hào)駕駛員

2.5. 使用適當(dāng)?shù)?HTTP 方法 (動(dòng)詞)

??再回顧一下這句話:

URL 是個(gè)句子瓜富,其中資源是名詞鳍咱、HTTP 方法是動(dòng)詞降盹。

  • GET:獲取在URI資源中指定的表述与柑,響應(yīng)消息體包含所請求資源的細(xì)節(jié)谤辜。
  • POST:創(chuàng)建一個(gè)URI指定的新資源,請求消息體提供新資源的細(xì)節(jié)价捧。注意丑念,POST也可以觸發(fā)一些操作,而不一定是要?jiǎng)?chuàng)建新資源结蟋。
  • PUT:創(chuàng)建或替代指定URI的資源脯倚。請求消息體指定要?jiǎng)?chuàng)建或更新的資源。
  • DELETE:移除指定URI的資源嵌屎。

2.6. HTTP 響應(yīng)狀態(tài)碼

??當(dāng)客戶端通過API向服務(wù)端發(fā)起一個(gè)請求時(shí)推正,客戶端應(yīng)當(dāng)知道反饋:是否失敗、通過或者請求錯(cuò)誤宝惰。HTTP 狀態(tài)碼是一批標(biāo)準(zhǔn)化代碼植榕,在不同的場景下有不同的解釋。服務(wù)器應(yīng)當(dāng)總是返回正確的狀態(tài)碼尼夺。
??下面是重要的HTTP代碼分類:

  • 2xx (成功分類):這些狀態(tài)碼代碼請求動(dòng)作被接收且被服務(wù)器成功處理尊残。
    • 200:Ok 表示 GET、PUT 或 POST 請求的標(biāo)準(zhǔn)狀態(tài)碼淤堵。
    • 201:Created(已創(chuàng)建)表示實(shí)例已被創(chuàng)建寝衫,多用于 POST 方法。
    • 204:No Content(無內(nèi)容)表示請求已被成功處理但沒有返回任何內(nèi)容拐邪。常用于 DELETE 方法返回慰毅。
  • 3xx (轉(zhuǎn)發(fā)分類)
    • 304:Not Modified(無修改)表示客戶端已經(jīng)緩存此響應(yīng),無須再次傳輸相同內(nèi)容扎阶。
  • 4xx (客戶端錯(cuò)誤分類):這些狀態(tài)碼代表客戶端提交了一個(gè)錯(cuò)誤請求事富。
    • 400:Bad Request(錯(cuò)誤請求)表示客戶端請求沒被處理,因?yàn)榉?wù)端無法理解客戶端請求乘陪。
    • 401:Unauthorized(無授權(quán))表示客戶端無權(quán)訪問資源统台,應(yīng)當(dāng)加上所需的認(rèn)證信息后再次請求。
    • 403:Forbidden(禁止訪問)表示請求有效且客戶端已獲授權(quán)啡邑,但客戶端無權(quán)訪問該資源贱勃。
    • 404:Not Found(沒發(fā)現(xiàn))表示所請求的資源現(xiàn)在不可用。
    • 410:Gone(移除)表示所請求的資源已被移除谤逼。
  • 5xx (服務(wù)端錯(cuò)誤分類)
    • 500:Internal Server Error(內(nèi)部服務(wù)器錯(cuò)誤)表示請求有效贵扰,但是服務(wù)端發(fā)生了異常。
    • 503:Service Unavailable(服務(wù)不可用)表示服務(wù)器關(guān)閉或不可用流部,通常是指服務(wù)器處于維護(hù)狀態(tài)戚绕。

2.7. 名稱規(guī)約

??你可以遵循任何名稱規(guī)約,只要保持跨應(yīng)用一致性即可枝冀。如果請求體和響應(yīng)體是 JSON 類型舞丛,那么請遵循駝峰名稱規(guī)約耘子。

2.8. 搜索、排序球切、過濾與分頁

??上面一些示例都是在一個(gè)數(shù)據(jù)集上的簡單查詢谷誓,對于復(fù)雜的數(shù)據(jù),我們需要在 GET 方法 API 上加一些參數(shù)來處理吨凑。下面是一些示例:

  • 排序:這個(gè)例子中捍歪,客戶想獲取排序的公司列表,GET /companies 應(yīng)當(dāng)在查詢時(shí)接受多種排序參數(shù)鸵钝。譬如 GET /companies?sort=rank_asc 將以等級升序的方式對公司進(jìn)行排序糙臼。
  • 過濾:要過濾數(shù)據(jù)集,我們可以通過查詢參數(shù)傳遞不同的選項(xiàng)恩商。比如 GET /companies?category=banking&location=india 將過濾分類為銀行且位于印度的公司弓摘。
  • 搜索:在公司列表中搜索公司名的 API 端點(diǎn)應(yīng)當(dāng)是 GET /companies?search=Digital。
  • 分頁:當(dāng)數(shù)據(jù)集太大時(shí)痕届,我們應(yīng)當(dāng)將數(shù)據(jù)集分割成小的數(shù)據(jù)塊韧献,這樣有利于提升服務(wù)端性能,也方便客戶端處理響應(yīng)研叫。如 GET /companies?page=23 意味著獲取公司列表的第 23 頁數(shù)據(jù)锤窑。

2.9. Restful API 版本

??一般使用不帶點(diǎn)的簡單數(shù)字表示版本,數(shù)字前加字母v代表版本號(hào)嚷炉,如下所示:

2.10. 處理 JSON 錯(cuò)誤體

??API 錯(cuò)誤處理機(jī)制是很重要的渊啰,而且要好好規(guī)劃。極力推薦總是在返回字段中包含錯(cuò)誤消息申屹。一個(gè) JSON 錯(cuò)誤體應(yīng)當(dāng)為開發(fā)者提供一些有用的信息:錯(cuò)誤消息绘证、錯(cuò)誤代碼以及詳細(xì)描述。下面是一個(gè)較好的示例:

{
    "code": 1234,
    "message": "Something bad happened :(",
    "description": "More details about the error here"
}

2.11. 如何創(chuàng)建 Rest API URL

??推薦使用下面格式的 URL:

3. 開發(fā)基于 Spring Boot 的 Restful Web 服務(wù)

??Spring Boot 提供了構(gòu)建企業(yè)應(yīng)用中 RESTful Web 服務(wù)的極佳支持哗讥。

3.1. 引入依賴

??要構(gòu)建 RESTful Web 服務(wù)嚷那,我們需要在構(gòu)建配置文件中加上 Spring Boot Starter Web 依賴。
??對于 Maven 用戶杆煞,使用以下的代碼在 pom.xml 文件中加入依賴:

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

??對于 Gradle 用戶魏宽,使用以下的代碼在 build.gradle 文件中加入依賴:

compile('org.springframework.boot:spring-boot-starter-web')

3.2. Rest 相關(guān)注解

??在繼續(xù)構(gòu)建 RESTful web 服務(wù)前,建議你先要熟悉下面的注解:

Rest Controller

??@RestController 注解用于定義 RESTful web 服務(wù)决乎。它提供 JSON队询、XML 和自定義響應(yīng)。語法如下所示:

@RestController
public class ProductServiceController {
}

Request Mapping

??@RequestMapping 注解用于定義請求 URI 以訪問 REST 端點(diǎn)构诚。我們可以定義 Request 方法來消費(fèi) produce 對象蚌斩。默認(rèn)的請求方法是 GET:

@RequestMapping(value = "/products")
public ResponseEntity<Object> getProducts() { }
Request Body
   @RequestBody 注解用于定義請求體內(nèi)容類型。
public ResponseEntity<Object> createProduct(@RequestBody Product product) {
}

Path Variable

??@PathVariable 注解被用于定義自定義或動(dòng)態(tài)的請求 URI范嘱,Path variable 被放在請求 URI 中的大括號(hào)內(nèi)送膳,如下所示:

public ResponseEntity<Object> updateProduct(@PathVariable("id") String id) {
}

Request Parameter

??@RequestParam 注解被用于從請求 URL 中讀取請求參數(shù)员魏。缺少情況下這是一個(gè)必需的參數(shù),也可以為請求參數(shù)設(shè)置默認(rèn)值肠缨,如下所示:
public ResponseEntity<Object> getProduct(
@RequestParam(value = "name", required = false, defaultValue = "honey") String name) {
}

3.3. 編寫 REST API

GET API

??下面的示例代碼定義了 HTTP GET 請求方法逆趋。在這個(gè)例子里盏阶,我們使用 HashMap 來在存儲(chǔ) Product晒奕。注意我們使用了 POJO 類來存儲(chǔ)產(chǎn)品。
??在這里名斟,請求 URI 是 /products脑慧,它會(huì)從 HashMap 倉儲(chǔ)中返回產(chǎn)品列表。下面的控制器類文件包含了 GET 方法的 REST 端點(diǎn):

package com.tutorialspoint.demo.controller;

import java.util.HashMap;
import java.util.Map;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.tutorialspoint.demo.model.Product;

@RestController
public class ProductServiceController {
   private static Map<String, Product> productRepo = new HashMap<>();
   static {
      Product honey = new Product();
      honey.setId("1");
      honey.setName("Honey");
      productRepo.put(honey.getId(), honey);
      
      Product almond = new Product();
      almond.setId("2");
      almond.setName("Almond");
      productRepo.put(almond.getId(), almond);
   }
   @RequestMapping(value = "/products")
   public ResponseEntity<Object> getProduct() {
      return new ResponseEntity<>(productRepo.values(), HttpStatus.OK);
   }
}

POST API

??HTTP POST 請求用于創(chuàng)建資源砰盐。這個(gè)方法包含請求體闷袒。我們可以通過發(fā)送請求參數(shù)和路徑變量來定義自定義或動(dòng)態(tài) URL。
??下面的示例代碼定義了 HTTP POST 請求方法岩梳。在這個(gè)例子中囊骤,我們使用 HashMap 來存儲(chǔ) Product,這里產(chǎn)品是一個(gè) POJO 類冀值。
??這里也物,請求 URI 是 /products,在產(chǎn)品被存入 HashMap 倉儲(chǔ)后列疗,它會(huì)返回字符串滑蚯。

package com.tutorialspoint.demo.controller;

import java.util.HashMap;
import java.util.Map;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import com.tutorialspoint.demo.model.Product;

@RestController
public class ProductServiceController {
   private static Map<String, Product> productRepo = new HashMap<>();
   
   @RequestMapping(value = "/products", method = RequestMethod.POST)
   public ResponseEntity<Object> createProduct(@RequestBody Product product) {
      productRepo.put(product.getId(), product);
      return new ResponseEntity<>("Product is created successfully", HttpStatus.CREATED);
   }
}

PUT API

??HTTP PUT 請求用于更新已有的資源。這個(gè)方法包含請求體抵栈。我們可以通過發(fā)送請求參數(shù)和路徑變量來定義自定義或動(dòng)態(tài) URL告材。
??下面的例子展示了如何定義 HTTP PUT 請求方法。在這個(gè)例子中古劲,我們使用 HashMap 更新現(xiàn)存的產(chǎn)品斥赋。此處,產(chǎn)品是一個(gè) POJO 類产艾。
??這里灿渴,請求 URI 是 /products/{id},在產(chǎn)品被存入 HashMap 倉儲(chǔ)后胰舆,它會(huì)返回字符串骚露。注意我們使用路徑變量 {id} 定義需要更新的產(chǎn)品 ID:

package com.tutorialspoint.demo.controller;

import java.util.HashMap;
import java.util.Map;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import com.tutorialspoint.demo.model.Product;

@RestController
public class ProductServiceController {
   private static Map<String, Product> productRepo = new HashMap<>();
   
   @RequestMapping(value = "/products/{id}", method = RequestMethod.PUT)
   public ResponseEntity<Object> updateProduct(@PathVariable("id") String id, @RequestBody Product product) {
      productRepo.remove(id);
      product.setId(id);
      productRepo.put(id, product);
      return new ResponseEntity<>("Product is updated successsfully", HttpStatus.OK);
   }   
}

DELETE API

??HTTP Delete 請求用于刪除存在的資源。這個(gè)方法不包含任何請求體缚窿。我們可以通過發(fā)送請求參數(shù)和路徑變量來定義自定義或動(dòng)態(tài) URL棘幸。
??下面的例子展示如何定義 HTTP DELETE 請求方法。這個(gè)例子中倦零,我們使用 HashMap 來移除現(xiàn)存的產(chǎn)品误续,用 POJO 來表示吨悍。
??請求 URI 是 /products/{id} 在產(chǎn)品被從 HashMap 倉儲(chǔ)中刪除后,它會(huì)返回字符串蹋嵌。 我們使用路徑變量 {id} 來定義要被刪除的產(chǎn)品 ID育瓜。

package com.tutorialspoint.demo.controller;

import java.util.HashMap;
import java.util.Map;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import com.tutorialspoint.demo.model.Product;

@RestController
public class ProductServiceController {
   private static Map<String, Product> productRepo = new HashMap<>();
   
   @RequestMapping(value = "/products/{id}", method = RequestMethod.DELETE)
   public ResponseEntity<Object> delete(@PathVariable("id") String id) {
      productRepo.remove(id);
      return new ResponseEntity<>("Product is deleted successsfully", HttpStatus.OK);
   }
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市栽烂,隨后出現(xiàn)的幾起案子躏仇,更是在濱河造成了極大的恐慌,老刑警劉巖腺办,帶你破解...
    沈念sama閱讀 221,695評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件焰手,死亡現(xiàn)場離奇詭異,居然都是意外死亡怀喉,警方通過查閱死者的電腦和手機(jī)书妻,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,569評論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來躬拢,“玉大人躲履,你說我怎么就攤上這事×拇常” “怎么了工猜?”我有些...
    開封第一講書人閱讀 168,130評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長馅袁。 經(jīng)常有香客問我域慷,道長,這世上最難降的妖魔是什么汗销? 我笑而不...
    開封第一講書人閱讀 59,648評論 1 297
  • 正文 為了忘掉前任犹褒,我火速辦了婚禮,結(jié)果婚禮上弛针,老公的妹妹穿的比我還像新娘叠骑。我一直安慰自己,他們只是感情好削茁,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,655評論 6 397
  • 文/花漫 我一把揭開白布宙枷。 她就那樣靜靜地躺著,像睡著了一般茧跋。 火紅的嫁衣襯著肌膚如雪慰丛。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,268評論 1 309
  • 那天瘾杭,我揣著相機(jī)與錄音诅病,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛贤笆,可吹牛的內(nèi)容都是我干的蝇棉。 我是一名探鬼主播,決...
    沈念sama閱讀 40,835評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼芥永,長吁一口氣:“原來是場噩夢啊……” “哼篡殷!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起埋涧,我...
    開封第一講書人閱讀 39,740評論 0 276
  • 序言:老撾萬榮一對情侶失蹤板辽,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后飞袋,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體戳气,經(jīng)...
    沈念sama閱讀 46,286評論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡链患,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,375評論 3 340
  • 正文 我和宋清朗相戀三年巧鸭,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片麻捻。...
    茶點(diǎn)故事閱讀 40,505評論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡纲仍,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出贸毕,到底是詐尸還是另有隱情郑叠,我是刑警寧澤,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布明棍,位于F島的核電站乡革,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏摊腋。R本人自食惡果不足惜沸版,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,873評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望兴蒸。 院中可真熱鬧视粮,春花似錦、人聲如沸橙凳。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,357評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽岛啸。三九已至钓觉,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間坚踩,已是汗流浹背荡灾。 一陣腳步聲響...
    開封第一講書人閱讀 33,466評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人卧晓。 一個(gè)月前我還...
    沈念sama閱讀 48,921評論 3 376
  • 正文 我出身青樓芬首,卻偏偏與公主長得像,于是被迫代替她去往敵國和親逼裆。 傳聞我的和親對象是個(gè)殘疾皇子郁稍,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,515評論 2 359