Lombok讓Java變得再次酷炫

201902W5 Review, 這是一篇關于Spring開發(fā)插件Lombok的譯文陈辱。

https://foreti.me/2019/02/15/lombok-make-java-cool-again/


image

原文

在Grauhub瘸右,我們在大多數后端編程中都是用Java纯赎。Java是一門經過20多年實戰(zhàn)考驗的語言痢畜,已經證明了它的速度和可靠性俐填。雖然我們已經使用Java很多年了蝴光,最近叛氨,它開始展現了它的老舊的特性。

盡管Java是最受歡迎的JVM語言之一擂橘,但它不是唯一的晌区。在過去幾年里,它面臨著一些挑戰(zhàn)者通贞,比如Scala朗若,Clojure和Kotlin,它們提供了新的功能和高效的語言特性昌罩。簡而言之哭懈,它們讓你用更少的代碼做更多的事。
是的·
JVM生態(tài)系統(tǒng)中的這一創(chuàng)新令人興奮茎用。更多的競爭意味著Java被迫改變以保持競爭力遣总。從Java 8(Valhalla睬罗,Local-Variable Type Inference,Loom)以來旭斥,新的六個月發(fā)布計劃和幾個JEP(JDK 增強提議)證明了Java在未來幾年將繼續(xù)保持競爭力容达。

但是,Java語言的大小和規(guī)模意味著開發(fā)進度比我們想要的要慢垂券,更不用說Java不惜一切代價保持向后兼容性的強烈意愿花盐。通過任何軟件工程工作,功能都需要優(yōu)先考慮菇爪,因此如果完全使用Java的話算芯,我們想要的功能可能需要很長時間。與此同時娄帖,現在Grubhub利用Lombok項目獲得簡化和改進的Java也祠。Lombok是一個編譯器插件,它為Java添加了新的“關鍵字”近速,并將注釋轉換為Java代碼诈嘿,減少了繁雜的工程工作,并提供了一些額外的功能削葱。

設置Lombok

Grubhub一直在尋求改進我們的軟件生命周期奖亚,但每個新工具和流程都需要在采用之前考慮成本。幸運的是析砸,添加Lombok就像在gradle文件中添加幾行一樣簡單昔字。

Lombok是一個編譯器插件,因為它在編譯器處理它們之前將源代碼中的注釋轉換為Java語句--在運行時不需要提供lombok依賴項首繁,因此添加Lombok不會增加構建工件的大小作郭。因此,您需要下載Lombok并將其添加到您的構建工具中弦疮。要使用Gradle設置Lombok(它也適用于Maven)夹攒,請將此塊添加到build.gradle文件中:

plugins {
    id 'io.franzbecker.gradle-lombok' version '1.14'
    id 'java'
}
repositories {
    jcenter() // or Maven central, required for Lombok dependency
}
lombok {
    version = '1.18.4'
    sha256 = ""
}

由于Lombok是一個編譯器插件,我們?yōu)樗帉懙脑创a實際上并不是有效的Java胁塞。因此咏尝,您還需要為正在使用的IDE安裝插件。幸運的是啸罢,Lombok支持所有主要的Java IDE编检。沒有插件,IDE不知道如何解析代碼扰才。IDE集成是無縫的允懂。諸如“show usages”和“go to implementation”等功能繼續(xù)按預期工作,帶您進入相關字段/類衩匣。

Lombok使用

了解Lombok的最佳方式是看它的使用方法累驮。讓我們深入研究一些如何將Lombok應用于Java應用程序的常見方面的示例酣倾。

為POJO增添趣味

我們使用普通的舊Java對象(POJO)將數據與處理分開,使我們的代碼更易于閱讀并簡化網絡有效負載谤专。一個簡單的POJO有一些私有字段和相應的getter和setter。它們只在寫了很多樣板代碼之后可以完成了工作午绳。

Lombok有助于使POJO更有用置侍,更靈活,更有結構拦焚,而無需編寫更多其他代碼蜡坊。使用Lombok,我們可以使用@Data注釋簡化最基本的POJO :

@Data
public class User {
  private UUID userId;
  private String email;
}

該@Data注釋實際上是包含多個Lombok注釋的便利結合赎败。

  • @ToString生成該toString()方法的實現秕衙,該實現由包含類名和每個字段及其值的對象的“漂亮打印”版本組成。

  • @EqualsAndHashCode生成equals和hashCode方法的實現僵刮,默認情況下据忘,它們使用所有非靜態(tài),非transient字段搞糕,但是可配置勇吊。

  • @Getter/@Setter為私有字段生成getter和setter方法。

  • @RequiredArgsConstructor生成帶參數的構造函數窍仰,其中需要參數是常量字段和帶@NonNull注釋的字段(稍后將詳細介紹)汉规。

這一個注釋簡單而優(yōu)雅地涵蓋了許多常見用例,但POJO并不總是足夠的驹吮。一個注釋@Data的類是完全可變的针史,它一旦被濫用,可能在應用程序增加復雜性和限制并發(fā)量碟狞,這兩點都有害于應用程序的持久性啄枕。

Lombok剛剛修復。讓我們重新審視我們的User類篷就,使其不可變射亏,并添加一些其他有用的Lombok注釋。

@Value
@Builder(toBuilder = true)
public class User {
  @NonNull 
  UUID userId;
  @NonNull 
  String email;
  @Singular
  Set<String> favoriteFoods;
  @NonNull
  @Builder.Default
  String avatar = “default.png”;
}

所需要的只是@Value注釋竭业。@Value類似于@Data智润,除了所有字段都默認為private和final,并且不生成setter未辆。這些特點使注釋@Value的對象有效地不變窟绷。由于字段都是常量的,因此沒有無參數構造函數咐柜。相反兼蜈,Lombok用@AllArgsConstructor生成所有參數構造函數攘残,這產生了一個功能完備,有效不可變的對象为狸。

但是歼郭,如果只能使用all args構造函數創(chuàng)建對象,那么不可變是不太有用的辐棒。Joshua Bloch在《Effective Java》解釋病曾,當面臨著許多構造函數參數時應該使用建造者。這就是Lombok的@Builder的作用漾根,自動生成構建器內部類:

User user = User.builder()
  .userId(UUID.random())
  .email(“grubhub@grubhub.com”)
  .favoriteFood(“burritos”)
  .favoriteFood(“dosas”)
  .build()

使用Lombok生成的構建器可以輕松創(chuàng)建具有多個參數的對象泰涂,并在將來添加新字段。靜態(tài)構建器方法返回構建器實例以設置對象的所有屬性辐怕。設置后逼蒙,在構建器上調用build()方法返回實例。

該@NonNull注釋可被用來在對象被實例化時寄疏,斷言這些字段不為空是牢,在空時拋出一個NullPointerException。請注意頭像字段是如何注釋@NonNull但未設置的赁还。這是因為@Builder.Default注釋表示默認使用“default.png”妖泄。(Grubhub是一個美國外賣公司,這里的頭像指用戶頭像艘策。)

還要注意構建器使用favoriteFood蹈胡,即對象上屬性的單數名稱。當@Singular注釋放在集合屬性上時朋蔫,Lombok會創(chuàng)建特殊的構建器方法來單獨向該集合添加項目罚渐,而不是一次添加整個集合。這對于測試來說特別好驯妄,因為在Java中創(chuàng)建小型集合并不簡潔荷并。

最后,toBuilder = true設置添加了一個實例方法toBuilder()青扔,該方法創(chuàng)建一個使用該實例的所有值填充的構建器對象源织。這樣可以輕松創(chuàng)建一個預先填充原始實例中所有值的新實例,并僅更改所需的字段微猖。這對于@Value類特別有用谈息,因為字段是不可變的。

通過一些注釋凛剥,你可以進一步配置專門的setter功能侠仇。@Wither為每個接受值的屬性創(chuàng)建“withX”方法,并返回實例的克隆,并更新一個字段值逻炊。@Accessors允許您配置自動創(chuàng)建的setter互亮。默認情況下,它允許將setter鏈接起來余素,就像構建器一樣豹休,返回而不是void。它還有一個參數溺森,fluent=true慕爬,它刪除了getter和setter上的“get”和“set”前綴約定。如果用例需要更多自定義屏积,這對于@Builder可能是一個有用的替代品。

如果Lombok實現不適合您的用例(并且您已經查看了注釋的修飾符)磅甩,那么您始終可以手動編寫自己的實現炊林。例如,如果您有一個@Data類但是一個getter需要自定義邏輯卷要,那么只需實現該getter渣聚。Lombok將看到已經提供了一個實現,并且不會使用自動生成的實現重寫它僧叉。

只需幾個簡單的注釋奕枝,最初的User POJO已經獲得了許多豐富的功能,使其更易于使用瓶堕,而不會給我們的工程師帶來太多負擔或增加開發(fā)的時間或成本隘道。

刪除組件樣板代碼

Lombok不僅在POJO中有用 - 它可以應用于應用程序的任何層。Lombok的以下用法在應用程序的組件類中特別有用郎笆,例如Controller谭梗,Service和DAO(數據訪問對象)。

日志是每個軟件的基準需求宛蚓,作為關鍵的調查工具激捏。任何正在做有意義的工作的類都應該記錄日志信息。由于日志記錄是一個貫穿各領域的問題凄吏,因此在每個類中聲明一個private static final logger成為即時模板远舅。Lombok將此樣板簡化為一個注釋,該注釋自動定義并實例化具有正確類名的記錄器痕钢。根據您使用的日志記錄框架图柏,有一些不同的注釋。

@Slf4j // also: @CommonsLog @Flogger @JBossLog @Log @Log4j @Log4j2 @XSlf4j
public class UserService {
  // created automatically
  // private static final org.slf4j.Logger log = 
}

在聲明了logger之后盖喷,接下來讓我們添加我們的依賴項:

@Slf4j
@RequiredArgsConstructor
@FieldDefaults(makeFinal=true, level=AccessLevel.PRIVATE)
public class UserService {
  @NonNull UserDao userDao;
}

該@FieldDefaults注釋增加了final和private修飾符的所有字段爆办。在@RequiredArgsConstructor創(chuàng)建構造器接受并設置一個UserDao實例。該@NonNull注釋在構造函數中增加了一個檢查课梳,如果UserDao實例為null拋出一個NullPointerException距辆。

其他

有很多方法可以使用Lombok余佃。以上兩節(jié)主要針對特定用例,但Lombok可以在許多方面使開發(fā)更容易跨算。以下是一些小例子爆土,展示了如何更有效地利用Lombok。

盡管Java 9引入了var關鍵字诸蚕,var仍可以重新分配步势。Lombok提供了一個val關鍵字,它可以在var不支持的地方生效背犯,提供本地常量推斷變量坏瘩。

// final Map map = new HashMap<Integer, String>();
val map = new HashMap<Integer, String>();

有些類只具有純靜態(tài)函數,而且從不打算初始化漠魏。聲明拋出異常的私有構造函數是阻止它實例化的一種方法倔矾。Lombok在其@UtilityClass注釋中編寫了該模式,該注釋創(chuàng)建了一個私有構造函數柱锹,它拋出異常哪自,使類成為final,并使所有方法都是靜態(tài)的禁熏。

@UtilityClass
// will be made final
public class UtilityClass {
  // will be made static
  private final int GRUBHUB = “ GRUBHUB”;

  // autogenerated by Lombok
  // private UtilityClass() {
  //   throw new java.lang.UnsupportedOperationException("This is a utility class and cannot be instantiated");
  //}

  // will be made static
  public void append(String input) {
    return input + GRUBHUB;
  }
}

對Java的常見批評是創(chuàng)建通過拋出已檢查的異常的冗長壤巷。Lombok有一個注釋,可以刪除那些討厭的關鍵詞:@SneakyThrows瞧毙。正如您所料胧华,實現非常狡猾(sneaky)。它不會吞下甚至將異常包裝成一個RuntimeException升筏。相反撑柔,它依賴于以下事實:在運行時,JVM不會檢查已檢查異常的一致性您访。只有javac這樣做铅忿。因此,Lombok使用字節(jié)碼轉換在編譯時選擇退出此檢查灵汪。這導致代碼順利運行檀训。

public class SneakyThrows {

    @SneakyThrows
    public void sneakyThrow() {
        throw new Exception();
    }

}

并排比較

沒什么能比做并排比較更清楚看到Lombok節(jié)省的代碼。IDE插件提供了一個“de-lombok”函數享言,可將大多數Lombok注釋轉換為近似的本機Java代碼(@NonNull注釋不轉換)峻凫。安裝了Lombok插件的任何IDE都允許你將大多數注釋轉換為本機Java代碼(并再次返回)。讓我們從上面回到我們的User類览露。

@Value
@Builder(toBuilder = true)
public class User {
  @NonNull 
  UUID userId;
  @NonNull 
  String email;
  @Singular
  Set<String> favoriteFoods;
  @NonNull
  @Builder.Default
  String avatar = “default.png”;
}

Lombok類只有13條簡單易讀的描述性代碼行荧琼。但是在運行de-lombok之后,這個課程變成了一百多行的樣板,沒有人愿意看到命锄,但每個人都想要堰乔!

public class User {

   @NonNull
   UUID userId;
   @NonNull
   String email;
   Set<String> favoriteFoods;
   @NonNull
   @Builder.Default
   String avatar = "default.png";

   @java.beans.ConstructorProperties({"userId", "email", "favoriteFoods", "avatar"})
   User(UUID userId, String email, Set<String> favoriteFoods, String avatar) {
       this.userId = userId;
       this.email = email;
       this.favoriteFoods = favoriteFoods;
       this.avatar = avatar;
   }

   public static UserBuilder builder() {
       return new UserBuilder();
   }

   @NonNull
   public UUID getUserId() {
       return this.userId;
   }

   @NonNull
   public String getEmail() {
       return this.email;
   }

   public Set<String> getFavoriteFoods() {
       return this.favoriteFoods;
   }

   @NonNull
   public String getAvatar() {
       return this.avatar;
   }

   public boolean equals(Object o) {
       if (o == this) return true;
       if (!(o instanceof User)) return false;
       final User other = (User) o;
       final Object this$userId = this.getUserId();
       final Object other$userId = other.getUserId();
       if (this$userId == null ? other$userId != null : !this$userId.equals(other$userId)) return false;
       final Object this$email = this.getEmail();
       final Object other$email = other.getEmail();
       if (this$email == null ? other$email != null : !this$email.equals(other$email)) return false;
       final Object this$favoriteFoods = this.getFavoriteFoods();
       final Object other$favoriteFoods = other.getFavoriteFoods();
       if (this$favoriteFoods == null ? other$favoriteFoods != null : !this$favoriteFoods.equals(other$favoriteFoods))
           return false;
       final Object this$avatar = this.getAvatar();
       final Object other$avatar = other.getAvatar();
       if (this$avatar == null ? other$avatar != null : !this$avatar.equals(other$avatar)) return false;
       return true;
   }

   public int hashCode() {
       final int PRIME = 59;
       int result = 1;
       final Object $userId = this.getUserId();
       result = result * PRIME + ($userId == null ? 43 : $userId.hashCode());
       final Object $email = this.getEmail();
       result = result * PRIME + ($email == null ? 43 : $email.hashCode());
       final Object $favoriteFoods = this.getFavoriteFoods();
       result = result * PRIME + ($favoriteFoods == null ? 43 : $favoriteFoods.hashCode());
       final Object $avatar = this.getAvatar();
       result = result * PRIME + ($avatar == null ? 43 : $avatar.hashCode());
       return result;
   }

   public String toString() {
       return "User(userId=" + this.getUserId() + ", email=" + this.getEmail() + ", favoriteFoods=" + this.getFavoriteFoods() + ", avatar=" + this.getAvatar() + ")";
   }

   public UserBuilder toBuilder() {
       return new UserBuilder().userId(this.userId).email(this.email).favoriteFoods(this.favoriteFoods).avatar(this.avatar);
   }

   public static class UserBuilder {
       private UUID userId;
       private String email;
       private ArrayList<String> favoriteFoods;
       private String avatar;

       UserBuilder() {
       }

       public User.UserBuilder userId(UUID userId) {
           this.userId = userId;
           return this;
       }

       public User.UserBuilder email(String email) {
           this.email = email;
           return this;
       }

       public User.UserBuilder favoriteFood(String favoriteFood) {
           if (this.favoriteFoods == null) this.favoriteFoods = new ArrayList<String>();
           this.favoriteFoods.add(favoriteFood);
           return this;
       }

       public User.UserBuilder favoriteFoods(Collection<? extends String> favoriteFoods) {
           if (this.favoriteFoods == null) this.favoriteFoods = new ArrayList<String>();
           this.favoriteFoods.addAll(favoriteFoods);
           return this;
       }

       public User.UserBuilder clearFavoriteFoods() {
           if (this.favoriteFoods != null)
               this.favoriteFoods.clear();

           return this;
       }

       public User.UserBuilder avatar(String avatar) {
           this.avatar = avatar;
           return this;
       }

       public User build() {
           Set<String> favoriteFoods;
           switch (this.favoriteFoods == null ? 0 : this.favoriteFoods.size()) {
               case 0:
                   favoriteFoods = java.util.Collections.emptySet();
                   break;
               case 1:
                   favoriteFoods = java.util.Collections.singleton(this.favoriteFoods.get(0));
                   break;
               default:
                   favoriteFoods = new java.util.LinkedHashSet<String>(this.favoriteFoods.size() < 1073741824 ? 1 + this.favoriteFoods.size() + (this.favoriteFoods.size() - 3) / 3 : Integer.MAX_VALUE);
                   favoriteFoods.addAll(this.favoriteFoods);
                   favoriteFoods = java.util.Collections.unmodifiableSet(favoriteFoods);
           }

           return new User(userId, email, favoriteFoods, avatar);
       }

       public String toString() {
           return "User.UserBuilder(userId=" + this.userId + ", email=" + this.email + ", favoriteFoods=" + this.favoriteFoods + ", avatar=" + this.avatar + ")";
       }
   }
}

我們可以從上面為UserService類做同樣的事情。

@Slf4j
@RequiredArgsConstructor
@FieldDefaults(makeFinal=true, level=AccessLevel.PRIVATE)
public class UserService {
  @NonNull UserDao userDao;
}

將導致大約這個Java代碼脐恩。

public class UserService {
   
   private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(UserService.class);
   
   private final UserDao userDao;
   
   @java.beans.ConstructorProperties({"userDao"})
   public UserService(UserDao userDao) {
       if (userDao == null) {
           throw new NullPointerException("userDao is marked @NonNull but is null")
       }
       this.userDao = userDao;
   }

 }

衡量影響

Grubhub有超過一百種服務來滿足業(yè)務需求镐侯。我們采用了其中一種服務并運行了Lombok IntelliJ插件的“de-lombok”功能,以查看使用Lombok節(jié)省了多少行代碼驶冒。結果是大約180個文件的更改苟翻,導致大約18,000個額外的代碼行和800個Lombok使用的刪除。這是18,000行自動生成骗污,標準化和經過實戰(zhàn)考驗的代碼行崇猫!平均而言,每行Lombok代碼都節(jié)省了23行Java代碼需忿。有了這樣的影響邓尤,很難想象沒有Lombok就使用Java。

總結

Lombok是一種很好的方式贴谎,可以激發(fā)工程師的新語言功能,而無需在整個組織內付出太多努力季稳。將插件應用于項目當然比使用現有代碼訓練所有工程師使用新語言和端口更容易擅这。Lombok可能沒有一切,但它確實提供了足夠的開箱即用景鼠,對工程經驗產生了明顯的影響仲翎。

Lombok的另一個好處是它使我們的代碼庫保持一致。憑借遍布全球的一百多種不同服務和分布式團隊铛漓,使我們的代碼庫保持一致溯香,可以更輕松地擴展團隊并減少啟動新項目時上下文切換的負擔。自Java 6以來浓恶,Lombok與任何版本的Java都相關玫坛,因此我們可以指望它在所有項目中都可用。

Lombok對Grubhub的意義遠遠超過了閃亮的新功能包晰。畢竟湿镀,Lombok做的任何事情都可以手工編寫。如圖所示伐憾,Lombok簡化了代碼庫的無聊部分勉痴,而不會影響業(yè)務邏輯。這使我們專注于為Grubhub提供最大價值的工作树肃,并且是我們工程師最感興趣的工作蒸矛。編寫者,審閱者和維護者讓代碼庫的這么大部分成為單調的樣板代碼是浪費時間。此外雏掠,由于此代碼不再手動編寫斩祭,因此它消除了所有類型的拼寫錯誤。自動生成的好處與強大的功能相結合磁玉,@NonNull減少了漏洞的可能性停忿,并使我們的工程專注于為您提供便利!



<p id="div-border-left-red"><i>DigitalOcean 優(yōu)惠碼蚊伞,注冊充值 5 送100席赂,鏈接一 鏈接二</i></p>
<p id="div-border-left-red"><i>Lastly, welcome to follow me on github</i></p>

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市时迫,隨后出現的幾起案子颅停,更是在濱河造成了極大的恐慌,老刑警劉巖掠拳,帶你破解...
    沈念sama閱讀 211,561評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件癞揉,死亡現場離奇詭異,居然都是意外死亡溺欧,警方通過查閱死者的電腦和手機喊熟,發(fā)現死者居然都...
    沈念sama閱讀 90,218評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來姐刁,“玉大人芥牌,你說我怎么就攤上這事∧羰梗” “怎么了壁拉?”我有些...
    開封第一講書人閱讀 157,162評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長柏靶。 經常有香客問我弃理,道長,這世上最難降的妖魔是什么屎蜓? 我笑而不...
    開封第一講書人閱讀 56,470評論 1 283
  • 正文 為了忘掉前任痘昌,我火速辦了婚禮,結果婚禮上梆靖,老公的妹妹穿的比我還像新娘控汉。我一直安慰自己,他們只是感情好返吻,可當我...
    茶點故事閱讀 65,550評論 6 385
  • 文/花漫 我一把揭開白布姑子。 她就那樣靜靜地躺著,像睡著了一般测僵。 火紅的嫁衣襯著肌膚如雪街佑。 梳的紋絲不亂的頭發(fā)上谢翎,一...
    開封第一講書人閱讀 49,806評論 1 290
  • 那天,我揣著相機與錄音沐旨,去河邊找鬼森逮。 笑死,一個胖子當著我的面吹牛磁携,可吹牛的內容都是我干的褒侧。 我是一名探鬼主播,決...
    沈念sama閱讀 38,951評論 3 407
  • 文/蒼蘭香墨 我猛地睜開眼谊迄,長吁一口氣:“原來是場噩夢啊……” “哼闷供!你這毒婦竟也來了?” 一聲冷哼從身側響起统诺,我...
    開封第一講書人閱讀 37,712評論 0 266
  • 序言:老撾萬榮一對情侶失蹤歪脏,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后粮呢,有當地人在樹林里發(fā)現了一具尸體婿失,經...
    沈念sama閱讀 44,166評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,510評論 2 327
  • 正文 我和宋清朗相戀三年啄寡,在試婚紗的時候發(fā)現自己被綠了豪硅。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,643評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡挺物,死狀恐怖舟误,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情姻乓,我是刑警寧澤,帶...
    沈念sama閱讀 34,306評論 4 330
  • 正文 年R本政府宣布眯牧,位于F島的核電站蹋岩,受9級特大地震影響,放射性物質發(fā)生泄漏学少。R本人自食惡果不足惜剪个,卻給世界環(huán)境...
    茶點故事閱讀 39,930評論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望版确。 院中可真熱鬧扣囊,春花似錦、人聲如沸绒疗。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,745評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽吓蘑。三九已至惕虑,卻和暖如春坟冲,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背溃蔫。 一陣腳步聲響...
    開封第一講書人閱讀 31,983評論 1 266
  • 我被黑心中介騙來泰國打工健提, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人伟叛。 一個月前我還...
    沈念sama閱讀 46,351評論 2 360
  • 正文 我出身青樓私痹,卻偏偏與公主長得像,于是被迫代替她去往敵國和親统刮。 傳聞我的和親對象是個殘疾皇子紊遵,可洞房花燭夜當晚...
    茶點故事閱讀 43,509評論 2 348

推薦閱讀更多精彩內容