Spring 学习笔记(三):Spring 中三种装配 bean 的方式

关于为什么要装配 bean 可以先阅读我之前写的两篇:

1.简化 Java 开发

2.Spring 容器以及 bean 的生命周期

关于为什么,这里简述下:Spring 容器负责创建应用程序中的 bean 并通过 DI 来协调这些对象对象之间的关系。但是你需要告诉 Spring 要创建哪些 bean,并且这些 bean 需要装配在哪里。

Spring 提供了三种装配 bean 的方式:

  1. 隐式的 bean 发现机制和自动装配
  2. 在 Java 中进行显式装配
  3. 在 XML 中进行显式装配

接下来就分别介绍这三种装配方式,而且在实际项目中吗,我们不是只使用一种方式,而是配合使用,那么我们就需要他们都有什么特点。

1.自动化装配 bean

自动化装配在实际项目中是最常见的,因为如果我们一个个为 bean 手动装配,那确实太麻烦了。所以我们会自动扫描包中所有 bean 并且将其装配,其他有特殊需求的我们在显示装配。

Spring 从两个角度去实现自动化装配:

  • 组件扫描:Spring 会自动发现应用上下文中所创建的 bean
  • 自动装配:Spring 会自动满足 bean 之间的依赖

这两者需要配合才能达到自动化装配的目的,组件扫描去发现包中的 bean,自动装配则是相当于常见的 new 操作。

1.1 创建可被发现的 bean

先定义一个似乎没什么卵用的接口,接口主要为了降低耦合。

package apple;

public interface AppleDevices {
    void useIcloud();
}

接着创建实现类

package apple;

import org.springframework.stereotype.Component;

@Component
public class IPhone implements AppleDevices {

    String message = "iCloud Synchronization Completed";

    @Override
    public void useIcloud() {
        System.out.println("iPhone " + message);
    }
}

注意,我们在类上加了 @Component注解,这个注解表明IPhone 这个类已经被作为组件类了,并且告知 Spring 要为这个类创建 bean。但Spring 不会默认就去扫描组件,而是需要我们自己去开启,这里我们先使用 Java 代码去完成开启组件扫描,很简单。

package apple;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan
public class SpringBeanConfig {
}

@Configuration 注解等会再说明,但其实看语义也能猜到,这里关键的是 @ComponentScan 注解,看语义也能知道,它就是开启了组件扫描,如果就这样配置的话,它默认是扫描 SpringBeanConfig 这个类同包类中的 bean。

开启组件扫描还有另一种方式,使用 XML 配置。

在 spring 的配置文件中加入

<context:component-scan base-package="apple"/>

这里我们先使用 java 代码配置的方式。

测试一下是否成功,测试代码如下:

import apple.AppleDevices;
import apple.SpringBeanConfig;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import static junit.framework.TestCase.assertNotNull;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringBeanConfig.class)
public class SpringBeanTest {

    @Autowired
    private AppleDevices appleDevices;

    @Test
    public void iPhoneIsExistTest(){
        assertNotNull(appleDevices);
    }
}

@RunWith(SpringJUnit4ClassRunner.class) 是为了在测试开始的时候自动创建 Spring 的上下文,@ContextConfiguration(classes = SpringBeanConfig.class)则是为了告诉测试类加载哪边的配置。@Autowired 则是为了将 AppleDevices 注入,实际注入的是 AppleDevices 的实现类 IPhone,所以上面的代码其实有个问题,因为我们现在的代码中只有一个 AppleDevices 的实现类,如果有两个或者更多,很明显这样是不对的,idea 也会报错。

image-20181124143721073

这个问题等会我们会去解决。

assertNotNull(appleDevices) 是使用了断言,这是 junit 自带的工具类,使用 jdk 自带的断言则是写成这样:assert appleDevices != null ,效果是一样的。

1.2 为组件扫描的 bean 命名

在上面我们遇到一个问题,就是有两个类 IPhone 和 IPad 都继承了 AppleDevices,当我们在AppleDevices 上加 @Autowired 注解后,spring 并不知道我们要注入的到底是哪个,这时候我们可以给 bean 命名,让 spring 知道我们要注入哪个实现类。

如果我们没有自己去命名,Spring 会给应用上下文中所有的 bean 一个迷人的 ID,ID 的规则就是把类名的第一个字母小写,其他不变,比如 IPhone 类的 ID 就是 iPhone , IPad 类的 ID 就是 iPad。如果要自己去命名,可以这样写。

package apple;

import org.springframework.stereotype.Component;

@Component("smartPhone")
public class IPhone implements AppleDevices {
    ...
}

这里我们将 IPhone 这个 bean 在 Spring 上下文中重命名为 smartPhone,这时候我们想要注入这个 bean 的时候可以这样写。

@Autowired()
@Qualifier("smartPhone")
private AppleDevices appleDevices;

@Qualifier 这个注解就告诉了 spring 我要注入的是 AppleDevices 的哪个实现类。

@Named

这里还要提到一下的就是这个 @Named ,这是 Java 依赖注入规范中提供的注解来为 bean 设置 Id,它和Component 基本相同,但 Component 在 spring 中更加语义化一点,因为我们是要让他成为了一个组件供我们使用,而不仅仅是命名。而且在 spring 框架中使用 named 还需要额外导包,太麻烦了。还不如用 spring 提供的Component。

1.3 设置组件扫描的基础包

@ComponentScan 现在默认是扫描配置类的同包下的类,我们也可以为其设置要扫描哪个包。

image-20181124154625450

现在把配置包扫描的类 SpringBeanConfig 放到另一个文件夹下面。里面添加 @ComponentScan 的属性,扫描的包为 apple 包下的。测试依然通过。

package config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan("apple")
public class SpringBeanConfig {
}

也可以写成这样 @ComponentScan(basePackages = "apple"),而且 basePackages 是个复数,很明显我们可以同时写多个包,比如 :@ComponentScan(basePackages = {"apple", "google"})

除了指定包,还可以直接指定类:@ComponentScan(basePackageClasses = {IPhone.class, IPad.class})。这样更不容易出差错,但一个个添加太麻烦了。

除此之外,我们也可以在 set 的时候注入,如下就把 AppleDevices 注入进来了:

@Autowired
public void setAppleDevices(AppleDevices appleDevices) {
    this.appleDevices = appleDevices;
}

或者在构造函数的时候注入,如下:

@Autowired
public User(AppleDevices appleDevices) {
    this.appleDevices = appleDevices;
}

但实际中我发现如果不加注解也能把 AppleDevices 注入进来,为了看的清楚点,我把这个类的代码贴上来

package apple;

import org.springframework.stereotype.Component;

@Component
public class User {
    private AppleDevices C;
    
    public User(AppleDevices appleDevices) {
        this.appleDevices = appleDevices;
    }

    public void use() {
        appleDevices.useICloud();
    }
}

按理说,我们并没有给appleDevices赋值,但实际测试中它确实已经注入进来了,而上面的 set 就不行,这个问题等我解决了再更新,暂时反正都加上就行了。


更新于半天之后。。。

我理解是这样的:

测试类中会注入 User,如下:

@Autowired
private User user;

在 spring 为User 注入的时候,可能就是这样:

@Autowired
private User user = new User(new IPhone());

这个@Autowired 是作用于下面整句话的,所以 IPhone 类也一起被注入进来的。这也是为什么 set 不能少了@Autowired 的原因,因为User 在注入的时候是调用的无参构造,不会吧 IPhone 带进来,所以 IPhone 必须在 set 的时候注入进来,所以必须要注解。

这问题纠结了我一下午,这是我自己理解的,也不一定对,如果觉得有问题的欢迎指出,我们一起讨论

补充:别人给的解释,可能更易懂准确一点:在spring项目启动的时候会去扫描那些注解对应的类,初始化这些类的对象,如果发现类里面有依赖的对象也是spring管理的类,就会先去初始化那些依赖的类的对象,注入到当前的类


还有一点,@AutoWired 的属性:required。默认 required 是 true,如果没有注入进来,会抛异常,如果设为 false,spring 在尝试装配失败会让这个 bean 处于未装配的状态,实际对象也就是 null。

@Inject 功能和 @AutoWired 类似,只不过和 @Named 一样,来源于 Java 依赖注入规范。

2.通过有 Java 代码装配 bean

很多时候组件扫描和自动装配都能帮我们解决问题,但如果想把一些第三方类库里的组件装配到应用中,就必须显式的去装配 bean 了。

这是一个不带注解的正常 java 类:

package apple;

public class IPhone{

    public void useICloud() {
        System.out.println("iPhone iCloud Synchronization Completed");
    }
}

很明显,我们去掉了之前的@Component 注解,配置文件中的包扫描注解 @ComponentScan 也去掉,因为我们暂时不需要是扫描发现应用中的 bean,而是手动去装配 bean。我们需要需要在配置文件中声明 bean,如下:

package apple;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SpringBeanConfig {

    @Bean
    public IPhone IPhone() {
        return new IPhone();
    }
}

@Bean 注解会告诉 Spring 这个方法将会返回一个对象,并且把这个对象注册为 Spring 应用上下文中 Bean。@Bean 注解后面也可以加 name 属性来为这个 bean 命名。

这时候,测试代码依然是通过的:

import apple.IPhone;
import apple.SpringBeanConfig;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import static junit.framework.TestCase.assertNotNull;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringBeanConfig.class)
public class SpringBeanTest {

    @Autowired
    private IPhone iPhone;
    @Test
    public void iPhoneIsExistTest(){
        assertNotNull(iPhone);
    }
}

3.通过 XML 装配 bean

虽然 xml 配置是最麻烦的,但在项目中是最常见的,但在 springboot 的项目中已经见不到它了。xml 装配可以与前两者配合使用。

3.1 声明一个简单的 bean

这时候我们的 IPhone 类还是不变,没有加任何注解:

package apple;

public class IPhone{

    public void useICloud() {
        System.out.println("iPhone iCloud Synchronization Completed");
    }
}

xml 配置文件如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    
    <bean id="iPhone" class="apple.IPhone"></bean>
    
</beans>

配置文件中首先得有个配置规范,也是根命名空间,然后声明一个 bean,通过 class 属性来指定,并给他一个 ID,没有没有定义 ID,那么 ID 会默认为 apple.IPhone#0 ,这在 ide 中可能会报错,但实际测试是通过的,可能因为用的人实在太少了。

image-20181125102857744
image-20181125102915586

所以我们一般都会给它一个 ID,这里为iPhone 。当 Spring 发现这个 bean 元素时,会调用 IPhone 的默认构造器来创建 bean。xml 还有一个缺点就是不能保证 class 属性中的类一定存在,但 java config 中基本不会存在这个问题。

3.2 借助构造器注入初始化 bean

3.2.1 将引用注入构造器

也就是说 bean 的构造函数中有一个引用类型的变量,如下:

package apple;

public class User {
    private AppleDevices appleDevices;

    public User(AppleDevices appleDevices) {
        this.appleDevices = appleDevices;
    }

    public void use() {
        appleDevices.useICloud();
    }
}

User的构造函数中引用了AppleDevices(IPhone 实现的接口),这时候我们在 xml 配置中需要这么写:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    
    <bean id="iPhone" class="apple.IPhone"></bean>
    
    <bean id="user" class="apple.User">
        <constructor-arg ref="iPhone"></constructor-arg>
    </bean>
    
</beans>

需要加一个constructor-arg子节点,ref 后面的值是引用已经申明的 bean。

在有了 c-命名空间和模式声明后,我们还可以这样写:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:c="http://www.springframework.org/schema/c"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="iPhone" class="apple.IPhone"></bean>

    <bean id="user" class="apple.User" c:appleDevices-ref="iPhone"/>

</beans>

c:appleDevices-ref="iPhone" 分解一下,c 是命名空间前缀,appleDevices 是构造器中的参数名,必须一样,ref 代表引用。如果觉得写参数名不好,还可以这样写:

<bean id="user" class="apple.User" c:_0-ref="iPhone"/>

_0 表示第一个参数,如果有更多的参数,可以写成 _1, _2

如果只有一个参数,可以直接把0省略,写这样:

<bean id="user" class="apple.User" c:_-ref="iPhone"/>
image-20181125105556832

ide 会报错,但实际是没问题的。

3.2.2 将字面量注入到构造器中

先把 IPhone 类修改一下,加入一个变量:

package apple;

public class IPhone implements AppleDevices{

    private String name;

    public IPhone(String name) {
        this.name = name;
    }

    public void useICloud() {
        System.out.println(name +":iCloud Synchronization Completed");
    }
}

xml 配置文件也要随之修改:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:c="http://www.springframework.org/schema/c"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="iPhone" class="apple.IPhone">
        <constructor-arg value="iphone"/>
    </bean>

</beans>

与引用不同的是,ref 被换成 value,这可能是值传递引用传递导致的,这里先不讨论。

当然字面量注入也有 c 命名空间的写法:

// 使用构造器参数名称
<bean id="iPhone" class="apple.IPhone" c:name="iphone"/>
// 使用构造器参数的位置
<bean id="iPhone" class="apple.IPhone" c:_0="iphone"/>

上面两种写法都可以,同样,如果只有一个参数,可以写成这样:

<bean id="iPhone" class="apple.IPhone" c:_="iphone"/>

3.2.3 将集合注入到构造器中

这时候 c 命名空间就不行了,只能使用 constructor-arg

把 IPhone 类再改造下:

package apple;

import java.util.List;

public class IPhone implements AppleDevices{

    private String name;
    private String os;
    private List<String> photos;

    public IPhone(String name,String os, List<String> photos) {
        this.name = name;
        this.os = os;
        this.photos = photos;
    }

    public void useICloud() {
        System.out.println(name + "-" + os +":iCloud Synchronization Completed");
    }
}

加入 os 这个属性我仅仅是为了看下属性的顺序,实践证明,构造函数中的变量是什么顺序,在 xml 配置的时候就应该是什么顺序,必须严格遵守。

我们在这个类中加入了一个集合属性,xml 配置中就应该这样写:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:c="http://www.springframework.org/schema/c"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="iPhone" class="apple.IPhone">
        <constructor-arg value="iphone"/>
        <constructor-arg value="ios12"/>
        <constructor-arg>
            <list>
                <value>1.jpg</value>
                <value>2.jpg</value>
                <value>3.jpg</value>
                <value>4.jpg</value>
            </list>
        </constructor-arg>
    </bean>
</beans>

如果是 set 集合就把 list 改成 set。

3.3 设置属性

上面一节都是采用构造器注入,但我们还可以使用属性注入。一般来说,如果是强依赖采用构造器注入,如果可选性的依赖使用属性注入。

首先需要在类中加 set 方法

package apple;

public class User {
    private AppleDevices appleDevices;
    
    public void setAppleDevices(AppleDevices appleDevices) {
        this.appleDevices = appleDevices;
    }

    public void use() {
        appleDevices.useICloud();
    }
}

xml 配置也不再使用 constructor-arg ,而是 property。

<bean id="user" class="apple.User">
    <property name="appleDevices" ref="iPhone"/>
</bean>

前面有 c 命名空间,这里使用的是 p 命名空间,c 是 constructor-arg 的首字母,p 是 property 的首字母,很好记。

<bean id="user" class="apple.User" p:appleDevices-ref="iPhone"/>

上面是引用,字面量则是:

<bean id="iPhone" class="apple.IPhone">
    <property name="name" value="iphone"/>
    <property name="os" value="123"/>
    <property name="photos">
        <list>
            <value>1.jpg</value>
            <value>2.jpg</value>
            <value>3.jpg</value>
            <value>4.jpg</value>
        </list>
    </property>
</bean>

可以指定 name,然后 value 赋值,list 和上面类似。p 命名和 c 命名类似,就不多说了,没有 ref 就是字面量。还有个 util 命名空间可以装配集合,可以这样写:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:c="http://www.springframework.org/schema/c"
       xmlns:p="http://www.springframework.org/schema/p"
       xmlns:util="http://www.springframework.org/schema/util"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/util
        http://www.springframework.org/schema/util/spring-util.xsd">

    <bean id="iPhone" class="apple.IPhone" p:name="ip" p:os="ios" p:photos-ref="photoList"/>

    <util:list id="photoList">
        <value>1.jpg</value>
        <value>2.jpg</value>
        <value>3.jpg</value>
        <value>4.jpg</value>
    </util:list>

    <bean id="user" class="apple.User" p:appleDevices-ref="iPhone"/>

</beans>
image-20181125125834169

测试也是可以的。注意声明命名空间。

4. 如何混合使用这三种方式

上面三种方式我们可以在实际项目中混合使用,而不是说我们只能同时使用一种,所以怎么选择还是得看实际情况。

关于第二种 Java config,如果我们全写在一个 Java config 文件中,太冗余了,我们可以在每个 package 下面都写一个自己的,然后使用 @Import 注解来导入到总的那个 Java config 中。如果是 xml 配置的话就使用 @ImportResource 注解来导入到总的那个 Java config 中。

package config;

import apple.AppleConfig;
import apple.IPhone;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.ImportResource;

@Configuration
@Import(AppleConfig.class)
@ImportResource("classpath:apple/spring.xml")
public class SpringBeanConfig {

    @Bean
    public IPhone IPhone() {
        return new IPhone();
    }
}

相应的,我们可以使用下面方式吧 java config 或者 xml 配置引入同一个 xml 配置中。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!--  引入 xml 配置 -->
    <import resource="spring-base.xml"/>

    <!--引入 java config-->
    <bean class="config.SpringBeanConfig"/>

</beans>

总结

Spring 容器是 Spring 框架的核心,而容器中装的就是这些 bean,因此如何把这些 bean 注入也变得十分关键。一般我们会采用自动化配置为主加上显示配置为辅,而显示配置的两种方式,基于 Java 的配置比基于 xml 的配置更加强大、类型安全和易于重构,所以在以后我会尽量选择基于 Java 的配置,当然具体还要根据实际来选择。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容