一起來讀官方文檔-----SpringIOC(11)

1.13。Environment

Environment接口是集成在容器中的抽象存在磺平,它表現(xiàn)為應用程序環(huán)境的兩個關鍵方面:profiles和properties练链。

1.13.1。Bean Definition Profiles

Bean Definition Profiles在核心容器中提供了一種機制琢歇,該機制允許在不同environment中注冊不同的Bean。

說白了其實就是判斷 spring.profiles.active 的值
這個值可以有多個中間用 , 隔開就可以

“environment”一詞對不同的用戶而言可能意味著不同的含義,并且此功能可以在許多用例中提供幫助矿微,包括:

  • 在開發(fā)中針對內存中的數(shù)據(jù)源進行工作痕慢,而不是在進行QA或生產時從JNDI查找相同的數(shù)據(jù)源。
  • 僅在將應用程序部署到性能環(huán)境中時注冊監(jiān)視基礎結構涌矢。
  • 為客戶A和客戶B部署注冊bean的自定義實現(xiàn)掖举。

考慮實際應用中需要使用的第一個用例DataSource。
在測試環(huán)境中娜庇,配置可能類似于以下內容:

@Bean
public DataSource dataSource() {
    return new EmbeddedDatabaseBuilder()
        .setType(EmbeddedDatabaseType.HSQL)
        .addScript("my-schema.sql")
        .addScript("my-test-data.sql")
        .build();
}

現(xiàn)在塔次,假設該應用程序的數(shù)據(jù)源已在生產應用程序服務器的JNDI目錄中注冊,請考慮如何將該應用程序部署到QA或生產環(huán)境中名秀。
現(xiàn)在励负,我們的dataSource bean看起來像下面的清單:

@Bean(destroyMethod="")
public DataSource dataSource() throws Exception {
    Context ctx = new InitialContext();
    return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}

問題是如何根據(jù)當前環(huán)境在使用這兩種變體之間進行切換。
隨著時間的流逝匕得,Spring用戶已經設計出許多方法來完成此任務继榆,通常依賴于系統(tǒng)環(huán)境變量和<import/>包含{placeholder}的XML語句的組合,這些{placeholder}根據(jù)環(huán)境變量的值解析為正確的配置文件路徑汁掠。

Bean Definition Profiles是一項核心容器功能略吨,可提供此問題的解決方案。

使用 @Profile

@Profile注解能做到只有在您指定的一個或多個指定的概要文件處于活動狀態(tài)時才對該組件進行注冊考阱。
使用前面的示例翠忠,我們可以重寫數(shù)據(jù)源配置,如下所示:

@Configuration
@Profile("development")
public class StandaloneDataConfig {

    @Bean
    public DataSource dataSource() {
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.HSQL)
            .addScript("classpath:com/bank/config/sql/schema.sql")
            .addScript("classpath:com/bank/config/sql/test-data.sql")
            .build();
    }
}

@Configuration
@Profile("production")
public class JndiDataConfig {

    @Bean(destroyMethod="")
    public DataSource dataSource() throws Exception {
        Context ctx = new InitialContext();
        return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
    }
}
如前所述乞榨,對于@Bean方法秽之,您通常選擇使用編程式JNDI查找,方法是使用Spring的JNDIMplate/JNDilocatorDeleteGate幫助器吃既,
或者使用前面顯示的直接JNDIInitialContext用法考榨,
而不是JndiObjectFactoryBean變量,因為factoryBean方法返回的是FactoryBean類型态秧,而不是DataSource類型董虱。
原理解釋:
1.@Profile注解中指定了@Conditional注解中的ProfileCondition.class

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(ProfileCondition.class)
public @interface Profile {

    /**
     * The set of profiles for which the annotated component should be registered.
     */
    String[] value();

}

2.首先在加載bean的時候發(fā)現(xiàn)有方法判斷是否應該調過當前bean
if (this.conditionEvaluator.shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN)) {
            configClass.skippedBeanMethods.add(methodName);
            return;
}

在shouldSkip中會查詢當前bean的所有的condition
并循環(huán)執(zhí)行每個condition的matches
而@Profile的condition的matches如下所示

class ProfileCondition implements Condition {

    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
        if (attrs != null) {
            for (Object value : attrs.get("value")) {
                //此處 調用了environment的propertySources
                //判斷當前配置中的所有的propertySources是否含有spring.profiles.active屬性
                //有值的話就將它設置到environment的activeProfiles屬性中
                //再判斷當前類的@Profile注解中的值是否被包含在activeProfiles屬性內
                //如果被包含則返回true
                if (context.getEnvironment().acceptsProfiles(Profiles.of((String[]) value))) {
                    return true;
                }
            }
            return false;
        }
        return true;
    }

}

配置文件字符串可以包含簡單的配置文件名稱(例如production)或配置文件表達式扼鞋。
配置文件表達式允許表達更復雜的配置文件邏輯(例如production & us-east)申鱼。
概要文件表達式中支持以下運算符:

  • !:配置文件的邏輯“非”
  • &:配置文件的邏輯“與”
  • |:配置文件的邏輯“或”
您不能在不使用括號的情況下混合使用 & 和 | 運算符。
例如云头, production & us-east | eu-central  不是有效的表達式捐友。
它必須表示為 production & (us-east | eu-central)。

您可以將其@Profile用作元注解溃槐,以創(chuàng)建自定義的組合注解匣砖。

以下示例定義了一個自定義 @Production批注,您可以將其用作@Profile("production")的替代品

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Profile("production")
public @interface Production {
}
如果一個@Configuration類被標記了一個@Profile,則除非一個或多個指定的配置文件處于活動狀態(tài)猴鲫,
否則將忽略與該類關聯(lián)的所有@Bean方法和 @Import注解对人。  

如果一個@Component或@Configuration類標記有@Profile({"p1", "p2"}),
則除非已激活配置文件“ p1”或“p2”拂共,否則不會注冊或處理該類牺弄。  

如果給定的配置文件以NOT運算符(!)為前綴,
則僅在該配置文件未激活時才注冊帶注解的元素宜狐。  
例如势告,給定@Profile({"p1", "!p2"}),
如果配置文件“ p1”處于活動狀態(tài)或配置文件“p2”未處于活動狀態(tài)抚恒,
則會進行注冊咱台。

@Profile 也可以在方法級別聲明,作用范圍僅僅是配置類的一個特定Bean俭驮,
如以下示例所示:

@Configuration
public class AppConfig {

    @Bean("dataSource")
    @Profile("development") 
    public DataSource standaloneDataSource() {
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.HSQL)
            .addScript("classpath:com/bank/config/sql/schema.sql")
            .addScript("classpath:com/bank/config/sql/test-data.sql")
            .build();
    }

    @Bean("dataSource")
    @Profile("production") 
    public DataSource jndiDataSource() throws Exception {
        Context ctx = new InitialContext();
        return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
    }
}
  • 該standaloneDataSource方法僅在development配置文件中可用回溺。
  • 該jndiDataSource方法僅在production配置文件中可用。

對于@Bean方法上的@Profile混萝,可能會應用一個特殊的場景(在同一個配置類中):
-----對于具有相同Java方法名的重載@Bean方法(類似于構造函數(shù)重載)馅而,需要在所有重載方法上聲明相同的@Profile條件,聲明相同的條件并不是因為可以自動選擇重載方法譬圣,是因為這一批重載方法都會因為第一個方法的校驗不合格就全部不通過瓮恭,如果第一個合格才會往下繼續(xù)判斷是否可以用其他的重載方法進行bean的注冊。
-----如果條件不一致厘熟,則只有重載方法中第一個聲明的條件才生效屯蹦。
因此,@Profile不能用于選擇具有特定參數(shù)簽名的重載方法绳姨。
同一bean的所有工廠方法之間的解析在創(chuàng)建時遵循Spring的構造函數(shù)解析算法登澜。

如果您想定義具有不同配置文件條件的替代bean,請使用指向相同bean名稱的不同@Bean方法名飘庄,方法是使用@Bean的name屬性脑蠕,如前面的示例所示。

如果參數(shù)簽名都相同(例如跪削,所有變量都沒有arg工廠方法)谴仙,那么這是在一個有效的Java類中首先表示這種安排的唯一方法(因為只能有一個特定名稱和參數(shù)簽名的方法)。

分析:
// 判斷當前@Bean是否需要跳過 
// 這里的判斷順序就是 Config類中的@Bean代碼的先后順序跟@Order無關
if (this.conditionEvaluator.shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN)) {
    configClass.skippedBeanMethods.add(methodName);
    return;
}
//當同名方法出現(xiàn)并在之前被跳過之后 這里會判斷skippedBeanMethods屬性是否包含并直接跳過
//所以不管同一個配置類中后續(xù)的同名方法是否帶有注解都將不再處理
if (configClass.skippedBeanMethods.contains(methodName)) {
    return;
}

XML Bean定義配置文件XML對應項是元素的profile屬性<beans>碾盐。
我們前面的示例配置可以用兩個XML文件重寫晃跺,如下所示:

<beans profile="development"
    xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:jdbc="http://www.springframework.org/schema/jdbc"
    xsi:schemaLocation="...">

    <jdbc:embedded-database id="dataSource">
        <jdbc:script location="classpath:com/bank/config/sql/schema.sql"/>
        <jdbc:script location="classpath:com/bank/config/sql/test-data.sql"/>
    </jdbc:embedded-database>
</beans>
<beans profile="production"
    xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:jee="http://www.springframework.org/schema/jee"
    xsi:schemaLocation="...">

    <jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
</beans>

也可以避免<beans/>在同一文件中拆分和嵌套元素,如以下示例所示:

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:jdbc="http://www.springframework.org/schema/jdbc"
    xmlns:jee="http://www.springframework.org/schema/jee"
    xsi:schemaLocation="...">

    <!-- other bean definitions -->

    <beans profile="development">
        <jdbc:embedded-database id="dataSource">
            <jdbc:script location="classpath:com/bank/config/sql/schema.sql"/>
            <jdbc:script location="classpath:com/bank/config/sql/test-data.sql"/>
        </jdbc:embedded-database>
    </beans>

    <beans profile="production">
        <jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
    </beans>
</beans>

spring-bean.xsd已經做了限制毫玖,只允許這樣的元素作為文件中的最后一個元素掀虎。這將有助于提供靈活性凌盯,并且不會導致XML文件的混亂。

XML對應項不支持前面描述的配置文件表達式
-----例如:(production & (us-east | eu-central))烹玉。
但是驰怎,可以通過使用!運算符來取消配置文件。
也可以通過嵌套配置文件來應用邏輯“與”二打,如以下示例所示:

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:jdbc="http://www.springframework.org/schema/jdbc"
    xmlns:jee="http://www.springframework.org/schema/jee"
    xsi:schemaLocation="...">

    <!---如果production和 us-east配置文件都處于活動狀態(tài)砸西,則dataSource會被注冊-->

    <beans profile="production">
        <beans profile="us-east">
            <jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
        </beans>
    </beans>
</beans>
Default Profile

默認配置文件表示默認情況下啟用的配置文件≈啡澹考慮以下示例:

@Configuration
@Profile("default")
public class DefaultDataConfig {

    @Bean
    public DataSource dataSource() {
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.HSQL)
            .addScript("classpath:com/bank/config/sql/schema.sql")
            .build();
    }
}

如果沒有配置文件被激活芹枷,dataSource被創(chuàng)建。
您可以看到這是為一個或多個bean提供默認定義的一種方法莲趣。

如果啟用了任何配置文件鸳慈,則默認配置文件不適用。

您可以通過setDefaultProfiles() 或者使用聲明性地使用spring.profiles.default屬性來更改默認配置文件的名稱喧伞。

1.13.2走芋。PropertySource抽象化

Spring的Environment抽象提供了對屬性源可配置層次結構的搜索操作。

ApplicationContext ctx = new GenericApplicationContext();
Environment env = ctx.getEnvironment();
boolean containsMyProperty = env.containsProperty("my-property");
System.out.println("Does my environment contain the 'my-property' property? " + containsMyProperty);

在前面的代碼片段中潘鲫,我們看到了一種方式來詢問Spring是否為當前環(huán)境定義了該my-property屬性翁逞。

為了回答這個問題,Environment對象在一組PropertySource對象上執(zhí)行搜索 溉仑。

PropertySource是對任何鍵-值對源的簡單抽象挖函,Spring的StandardEnvironment配置有兩個PropertySource對象

  • 一個代表JVM系統(tǒng)屬性集(System.getProperties())
  • 一個代表系統(tǒng)環(huán)境變量集(System.getenv())。
這些默認屬性源是為StandardEnvironment提供的浊竟,供獨立應用程序使用怨喘。
StandardServletEnvironment使用了附加的默認屬性源,包括servlet配置和servlet上下文參數(shù)振定。
它可以選擇啟用JndiPropertySource必怜。
所執(zhí)行的搜索是分層的。
默認情況下后频,系統(tǒng)屬性優(yōu)先于環(huán)境變量梳庆。
因此,如果在調用env.getProperty(“my-property”)期間卑惜,恰好在兩個位置都設置了my-property屬性膏执,則系統(tǒng)屬性值“勝出”并被返回。
注意残揉,屬性值沒有被合并胧后,而是被前面的條目完全覆蓋。

對于common StandardServletEnvironment抱环,完整的層次結構如下所示壳快,最高優(yōu)先級的條目位于頂部:

    ServletConfig參數(shù)(如果適用——例如,在DispatcherServlet上下文的情況下)
    ServletContext參數(shù)(web.xml上下文參數(shù)項)
    JNDI環(huán)境變量(java:comp/env/ entries)
    JVM系統(tǒng)屬性(-D命令行參數(shù))
    JVM系統(tǒng)環(huán)境(操作系統(tǒng)環(huán)境變量)

最重要的是镇草,整個機制是可配置的眶痰。
也許您具有要集成到此搜索中的自定義屬性源。
為此梯啤,請實現(xiàn)并實例化自己的實例PropertySource并將其添加到PropertySourcescurrent的集合中Environment竖伯。
以下示例顯示了如何執(zhí)行此操作:

ConfigurableApplicationContext ctx = new GenericApplicationContext();
MutablePropertySources sources = ctx.getEnvironment().getPropertySources();
//MyPropertySource被添加了最高優(yōu)先級。  
sources.addFirst(new MyPropertySource());

該MutablePropertySources API公開了許多方法因宇,
這些方法允許對屬性源集進行精確操作七婴。

1.13.3。使用@PropertySource

@PropertySource注解提供了一種方便的聲明機制察滑,可以將PropertySource添加到Spring的環(huán)境中打厘。

給定一個名為app.properties的文件,
其中包含鍵值對testbean.name=myTestBean贺辰,
下面的@Configuration類使用@PropertySource户盯,
調用env.getProperty("testbean.name")會返回myTestBean:

@Configuration
@PropertySource("classpath:/com/myco/app.properties")
public class AppConfig {

    @Autowired
    Environment env;

    @Bean
    public TestBean testBean() {
        TestBean testBean = new TestBean();
        testBean.setName(env.getProperty("testbean.name"));
        return testBean;
    }
}

任何出現(xiàn)在@PropertySource資源位置的${…}占位符都會根據(jù)已經在環(huán)境中注冊的屬性源進行解析,如下面的示例所示:

//假定my.placeholder存在于已注冊的屬性源之一(例如饲化,系統(tǒng)屬性或環(huán)境變量)中莽鸭,則占位符將解析為相應的值。  
//如果不是吃靠,則default/path用作默認值硫眨。  
//如果未指定默認值并且無法解析屬性, IllegalArgumentException則拋出巢块。

@Configuration
@PropertySource("classpath:/com/${my.placeholder:default/path}/app.properties")
public class AppConfig {

    @Autowired
    Environment env;

    @Bean
    public TestBean testBean() {
        TestBean testBean = new TestBean();
        testBean.setName(env.getProperty("testbean.name"));
        return testBean;
    }
}
根據(jù)Java 8的約定捺球,@PropertySource注解是可重復的。
但是夕冲,所有這樣的@PropertySource注解都需要在同一級別聲明氮兵,要么直接在配置類上聲明,要么作為同一自定義注解中的元注解聲明歹鱼。
不推薦混合使用直接注解和元注解泣栈,因為直接注解有效地覆蓋了元注解。

1.13.4弥姻。聲明中的占位符解析
過去南片,元素中的占位符的值只能根據(jù)JVM系統(tǒng)屬性或環(huán)境變量解析。
現(xiàn)在情況已經不一樣了庭敦。
因為環(huán)境抽象集成在整個容器中疼进,所以很容易通過它來解析占位符。
這意味著您可以以任何您喜歡的方式配置解析過程秧廉。
您可以更改搜索系統(tǒng)屬性和環(huán)境變量的優(yōu)先級伞广,或者完全刪除它們拣帽。
您還可以在適當?shù)那闆r下添加您自己的屬性源。

具體地說嚼锄,無論客戶屬性定義在哪里减拭,只要它在環(huán)境中可用,以下語句都適用:

<beans>
    <import resource="com/bank/service/${customer}-config.xml"/>
</beans>
1.15区丑。ApplicationContext的其他功能

正如在引言中所討論的拧粪,
org.springframework.beans.factory包提供了管理和操作bean的基本功能,包括以編程的方式沧侥。
org.springframework.context 包添加了ApplicationContext接口可霎,
該接口擴展了BeanFactory接口,
此外還擴展了其他接口宴杀,以更面向應用程序框架的風格提供額外的功能癣朗。

許多人以一種完全聲明式的方式使用ApplicationContext,甚至不是通過編程來創(chuàng)建它婴氮,而是依賴于支持類(如ContextLoader)來自動實例化一個ApplicationContext斯棒,作為Java EE web應用程序的正常啟動過程的一部分。

為了以更面向框架的風格增強BeanFactory的功能主经,上下文包還提供了以下功能:

  • 通過MessageSource接口訪問i18n風格的消息荣暮。
  • 通過ResourceLoader接口訪問資源,例如url和文件罩驻。
  • 事件發(fā)布穗酥,即通過使用ApplicationEventPublisher接口發(fā)布到實現(xiàn)ApplicationListener接口的bean。
  • 通過HierarchicalBeanFactory接口加載多個(分層的)上下文惠遏,讓每個上下文都關注于一個特定的層砾跃,比如應用程序的web層。
1.15.1节吮。國際化使用MessageSource

ApplicationContext接口擴展了一個名為MessageSource的接口抽高,因此提供了國際化(“i18n”)功能。
Spring還提供了HierarchicalMessageSource接口透绩,該接口可以分層解析消息翘骂。

這些接口一起提供了Spring實現(xiàn)消息解析的基礎。

在這些接口上定義的方法包括:

  • String getMessage(String code, Object[] args, String default, Locale loc):
    用于從MessageSource檢索消息的基本方法帚豪。
    如果未找到指定語言環(huán)境的消息碳竟,則使用默認消息。
    通過使用標準庫提供的MessageFormat功能狸臣,傳入的任何參數(shù)都將成為替換值莹桅。
  • String getMessage(String code, Object[] args, Locale loc):
    本質上與前面的方法相同,但有一個區(qū)別:不能指定缺省消息烛亦。
    如果找不到消息诈泼,則拋出NoSuchMessageException懂拾。
  • String getMessage(MessageSourceResolvable, Locale Locale):前面方法中使用的所有屬性也包裝在一個名為MessageSourceResolvable類中,可與此方法一起使用厂汗。

加載ApplicationContext時委粉,它會自動搜索上下文中定義的MessageSource bean呜师。
bean的名稱必須是messageSource娶桦。

  • 如果找到這樣一個bean,對前面方法的所有調用都將委托給消息源汁汗。
  • 如果沒有找到消息源衷畦,ApplicationContext將嘗試查找包含同名bean的父消息源。如果是知牌,則使用該bean作為消息源。
  • 如果ApplicationContext找不到任何消息源,則實例化一個空的DelegatingMessageSource榆浓,以便能夠接受對上面定義的方法的調用死相。

Spring提供了兩個消息源實現(xiàn):
ResourceBundleMessageSource和StaticMessageSource。

兩者都實現(xiàn)了HierarchicalMessageSource以執(zhí)行嵌套消息傳遞扁藕。
很少使用StaticMessageSource沮峡,但它提供了將消息添加到源的編程方法。

下面的示例展示ResourceBundleMessageSource:

<beans>
    <bean id="messageSource"
            class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basenames">
            <list>
                <value>format</value>
                <value>exceptions</value>
                <value>windows</value>
            </list>
        </property>
    </bean>
</beans>

這個例子假設你有所謂的三個資源包format.properties亿柑,exceptions.properties邢疙,windows.properties 在類路徑中定義。
解析消息的任何請求均通過JDK標準的通過ResourceBundle對象解析消息的方式來處理望薄。
就本示例而言疟游,假定上述兩個資源束文件的內容如下:

#在format.properties中
message=Alligators rock!



#在exceptions.properties中
argument.required=The {0} argument is required.

下一個示例顯示了運行該MessageSource功能的程序。
請記住痕支,所有ApplicationContext實現(xiàn)也是MessageSource實現(xiàn)颁虐,因此可以強制轉換為MessageSource接口。

public static void main(String[] args) {
    MessageSource resources = new ClassPathXmlApplicationContext("beans.xml");
    String message = resources.getMessage("message", null, "Default", Locale.ENGLISH);
    System.out.println(message);
}

以上程序的結果輸出如下:

Alligators rock!

總之卧须,MessageSource是在一個名為beans.xml的文件中定義的另绩,它存在于classpath中。
MessageSource bean定義通過其basenames屬性引用大量資源包故慈。
在列表中傳遞給basenames屬性的三個文件作為類路徑的根文件存在板熊,它們被稱為format.properties,exceptions.properties察绷,and windows.properties干签。

下一個示例顯示了傳遞給消息查找的參數(shù)。

這些參數(shù)被轉換為字符串對象拆撼,并插入到查找消息中的占位符中容劳。

<beans>

    <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basename" value="exceptions"/>
    </bean>

    <bean id="example" class="com.something.Example">
        <property name="messages" ref="messageSource"/>
    </bean>

</beans>
public class Example {

    private MessageSource messages;

    public void setMessages(MessageSource messages) {
        this.messages = messages;
    }

    public void execute() {
        String message = this.messages.getMessage("argument.required",
            new Object [] {"userDao"}, "Required", Locale.ENGLISH);
        System.out.println(message);
    }
}

execute()方法調用的結果輸出如下:

The userDao argument is required.

關于國際化(“i18n”)喘沿,Spring的各種MessageSource實現(xiàn)遵循與標準JDK ResourceBundle相同的語言環(huán)境解析和回退規(guī)則。
簡而言之竭贩,繼續(xù)前面定義的示例messageSource蚜印,如果您希望根據(jù)英國(en-GB)地區(qū)解析消息,您將創(chuàng)建名為format_en_GB.properties, exceptions_en_GB.properties, and windows_en_GB.properties留量。

通常窄赋,語言環(huán)境解析由應用程序的周圍環(huán)境管理。
在下面的示例中楼熄,手動指定解析(英國)消息所對應的語言環(huán)境:

# in exceptions_en_GB.properties
argument.required=Ebagum lad, the ''{0}'' argument is required, I say, required.


public static void main(final String[] args) {
    MessageSource resources = new ClassPathXmlApplicationContext("beans.xml");
    String message = resources.getMessage("argument.required",
        new Object [] {"userDao"}, "Required", Locale.UK);
    System.out.println(message);
}

運行上述程序的結果輸出如下:

Ebagum lad, the 'userDao' argument is required, I say, required.

您還可以使用MessageSourceAware接口來獲取對已定義的任何消息源的引用忆绰。
當創(chuàng)建和配置bean時,在ApplicationContext中定義的任何bean實現(xiàn)MessageSourceAware接口的都被注入應用上下文的MessageSourceAware接口可岂。

作為ResourceBundleMessageSource的替代方案错敢,Spring提供了一個ReloadableResourceBundleMessageSource類。
這個變體支持相同的bundle文件格式缕粹,但是比基于JDK的標準ResourceBundleMessageSource實現(xiàn)更加靈活稚茅。
特別是,它允許從任何Spring資源位置讀取文件(不僅僅是從類路徑)平斩,并支持bundle屬性文件的熱重新加載(同時有效地緩存它們)亚享。
有關詳細信息,請參見ReloadableResourceBundleMessageSource javadoc双戳。
1.15.2虹蒋。標準和自定義事件

ApplicationContext中的事件處理是通過ApplicationEvent類和ApplicationListener接口提供的。
如果實現(xiàn)ApplicationListener接口的bean被部署到上下文中飒货,那么每當一個ApplicationEvent被發(fā)布到ApplicationContext時魄衅,該bean就會得到通知。
本質上塘辅,這就是標準的觀察者設計模式晃虫。

從Spring 4.2開始,事件基礎設施已經得到了顯著改進扣墩,并提供了一個基于注解的模型哲银,
以及發(fā)布任意事件的能力(也就是說,不需要從ApplicationEvent擴展的對象)呻惕。
當發(fā)布這樣的對象時荆责,我們?yōu)槟鷮⑵浒b在事件中。

表7.內置事件

事件 說明
ContextRefreshedEvent 在初始化或刷新ApplicationContext時發(fā)布
例如亚脆,ConfigurableApplicationContext.refresh()方法

這里做院,初始化意味著加載了所有bean
檢測并激活了后處理器bean
預先實例化了singleton
并且ApplicationContext對象可以使用了。

只要上下文尚未關閉
并且所選的ApplicationContext實際上支持這種“熱”刷新
就可以多次觸發(fā)刷新
例如,XmlWebApplicationContext支持熱刷新键耕,
但GenericApplicationContext不支持寺滚。
ContextStartedEvent 在ConfigurableApplicationContext.start()方法
啟動ApplicationContext時發(fā)布。

這里屈雄,啟動意味著所有生命周期bean都收到一個顯式的啟動信號村视。
通常,這個信號用于在顯式停止后重新啟動bean
酒奶,但是它也可以用于啟動尚未配置為自動啟動的組件
例如蚁孔,尚未在初始化時啟動的組件。
ContextStoppedEvent 在ConfigurableApplicationContext.stop()方法
停止ApplicationContext時發(fā)布讥蟆。

“停止”意味著所有生命周期bean都收到一個顯式的停止信號勒虾。
停止的上下文可以通過start()調用重新啟動纺阔。
ContextClosedEvent 通過使用ConfigurableApplicationContext.close()方法
或通過JVM shutdown hook關閉ApplicationContext時發(fā)布瘸彤。

,“關閉”意味著所有的單例bean將被銷毀笛钝。
一旦上下文關閉质况,
它就會到達生命的終點,無法刷新或重新啟動玻靡。
RequestHandledEvent 一個特定于web的事件结榄,
告訴所有bean一個HTTP請求已經得到服務。
此事件在請求完成后發(fā)布囤捻。
此事件僅適用于使用Spring的DispatcherServlet的web應用程序臼朗。
ServletRequestHandledEvent 該類的子類RequestHandledEvent
添加了Servlet-specific的上下文信息。

您還可以創(chuàng)建和發(fā)布自己的自定義事件蝎土。
下面的示例顯示了一個簡單的類视哑,該類擴展了Spring的ApplicationEvent基類:

public class BlockedListEvent extends ApplicationEvent {

    private final String address;
    private final String content;

    public BlockedListEvent(Object source, String address, String content) {
        super(source);
        this.address = address;
        this.content = content;
    }

    // accessor and other methods...
}

要發(fā)布自定義ApplicationEvent,請在ApplicationEventPublisher上調用publishEvent()方法 誊涯。

通常挡毅,這是通過創(chuàng)建一個實現(xiàn)ApplicationEventPublisherAware并注冊為Spring bean的類來完成的 。

以下示例顯示了此類:

public class EmailService implements ApplicationEventPublisherAware {

    private List<String> blockedList;
    private ApplicationEventPublisher publisher;

    public void setBlockedList(List<String> blockedList) {
        this.blockedList = blockedList;
    }

    public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
        this.publisher = publisher;
    }

    public void sendEmail(String address, String content) {
        if (blockedList.contains(address)) {
            publisher.publishEvent(new BlockedListEvent(this, address, content));
            return;
        }
        // send email...
    }
}

在配置時暴构,Spring容器檢測到該ApplicationEventPublisherAware實現(xiàn)EmailService 并自動調用 setApplicationEventPublisher()跪呈。

實際上,傳入的參數(shù)是Spring容器本身取逾。
您正在通過其ApplicationEventPublisher界面與應用程序上下文進行交互耗绿。

要接收自定義ApplicationEvent,您可以創(chuàng)建一個實現(xiàn) ApplicationListener并注冊為Spring bean的類砾隅。
以下示例顯示了此類:

public class BlockedListNotifier implements ApplicationListener<BlockedListEvent> {

    private String notificationAddress;

    public void setNotificationAddress(String notificationAddress) {
        this.notificationAddress = notificationAddress;
    }

    public void onApplicationEvent(BlockedListEvent event) {
        // notify appropriate parties via notificationAddress...
    }
}

注意误阻,ApplicationListener通常是用自定義事件的類型參數(shù)化的(在前面的示例中是BlockedListEvent)。
這意味著onApplicationEvent()方法可以保持類型安全,避免向下強制轉換堕绩。
您可以注冊任意數(shù)量的事件監(jiān)聽器策幼,但是請注意,默認情況下奴紧,事件監(jiān)聽器同步接收事件特姐。
這意味著publishEvent()方法會阻塞,直到所有監(jiān)聽器都完成了事件的處理黍氮。
這種同步和單線程方法的一個優(yōu)點是唐含,當偵聽器接收到事件時,如果事務上下文可用沫浆,它將在發(fā)布程序的事務上下文內操作捷枯。

下面的例子顯示了用于注冊和配置上面每個類的bean定義:

<!--當調用emailService bean的sendEmail()方法時,  
    如果有任何需要阻止的電子郵件消息专执,
    則發(fā)布類型為BlockedListEvent的自定義事件淮捆。 -->
<bean id="emailService" class="example.EmailService">
    <property name="blockedList">
        <list>
            <value>known.spammer@example.org</value>
            <value>known.hacker@example.org</value>
            <value>john.doe@example.org</value>
        </list>
    </property>
</bean>

<!--blockedListNotifier         
    bean注冊為一個ApplicationListener并接收BlockedListEvent, 
    此時它可以通知適當?shù)姆健?->
<bean id="blockedListNotifier" class="example.BlockedListNotifier">
    <property name="notificationAddress" value="blockedlist@example.org"/>
</bean>
Spring的事件機制是為相同上下文中的Spring bean之間的簡單通信而設計的本股。

然而攀痊,對于更復雜的企業(yè)集成需求,
單獨維護的Spring integration項目提供了構建輕量級拄显、面向模式苟径、事件驅動架構的完整支持,
這些架構構建在眾所周知的Spring編程模型之上躬审。
基于注解的事件偵聽器

從Spring 4.2開始棘街,您可以使用@EventListener注解在托管Bean的任何公共方法上注冊事件偵聽器。

該BlockedListNotifier可改寫如下:

public class BlockedListNotifier {

    private String notificationAddress;

    public void setNotificationAddress(String notificationAddress) {
        this.notificationAddress = notificationAddress;
    }

    @EventListener
    public void processBlockedListEvent(BlockedListEvent event) {
        // notify appropriate parties via notificationAddress...
    }
}

方法簽名再次聲明它偵聽的事件類型承边,但是這次使用了靈活的名稱遭殉,并且沒有實現(xiàn)特定的偵聽器接口。
只要實際事件類型在其實現(xiàn)層次結構中解析泛型參數(shù)炒刁,就可以通過泛型縮小事件類型恩沽。

如果您的方法應該偵聽多個事件,或者您希望在不使用任何參數(shù)的情況下定義它翔始,那么還可以在注解本身上指定事件類型罗心。

下面的例子展示了如何做到這一點:

@EventListener({ContextStartedEvent.class, ContextRefreshedEvent.class})
public void handleContextStart() {
    // ...
}

還可以通過使用定義SpEL表達式的注解的條件屬性來添加額外的運行時過濾,該注解應該與針對特定事件實際調用方法相匹配城瞎。

下面的例子展示了我們的通知程序如何被重寫渤闷,只有在content 屬性等于my-event時才被調用:

@EventListener(condition = "#blEvent.content == 'my-event'")
public void processBlockedListEvent(BlockedListEvent blockedListEvent) {
    // notify appropriate parties via notificationAddress...
}

每個SpEL表達式針對專用上下文進行評估。
下表列出了可用于上下文的項目脖镀,以便您可以將它們用于條件事件處理:

表8. Event SpEL可用的元數(shù)據(jù)

名稱 例子
Event #root.event or event
參數(shù)數(shù)組 #root.args or args;
args[0]訪問第一個參數(shù)等飒箭。
參數(shù)名稱 #blEvent或#a0
(您還可以使用#p0或#p<#arg>參數(shù)表示法作為別名)
請注意,root.event允許您訪問基礎事件,即使您的方法簽名實際上引用了已發(fā)布的任意對象弦蹂。

如果你需要發(fā)布一個事件作為處理另一個事件的結果肩碟,你可以改變方法簽名來返回應該發(fā)布的事件,如下面的例子所示:

@EventListener
public ListUpdateEvent handleBlockedListEvent(BlockedListEvent event) {
    // notify appropriate parties via notificationAddress and
    // then publish a ListUpdateEvent...
}
asynchronous listeners.不支持此功能 凸椿。

此新方法為每處理一個BlockedListEvent事件都會發(fā)布一個新事件ListUpdateEvent削祈。
如果您需要發(fā)布多個事件,則可以返回一個Collection事件脑漫。

asynchronous listeners 異步偵聽器

如果需要一個特定的偵聽器異步處理事件髓抑,可以重用常規(guī)的@Async支持。
下面的例子展示了如何做到這一點:

@EventListener
@Async
public void processBlockedListEvent(BlockedListEvent event) {
    // BlockedListEvent is processed in a separate thread
}

使用異步事件時优幸,請注意以下限制:

  • 如果異步事件偵聽器拋出Exception吨拍,則不會傳播到調用者。
  • 異步事件偵聽器方法無法通過返回值來發(fā)布后續(xù)事件网杆。如果您需要發(fā)布另一個事件作為處理的結果羹饰,請插入一個 ApplicationEventPublisher 以手動發(fā)布事件。
Ordering Listeners

如果需要先調用一個偵聽器跛璧,則可以將@Order注解添加到方法聲明中严里,
如以下示例所示:

@EventListener
@Order(42)
public void processBlockedListEvent(BlockedListEvent event) {
    // notify appropriate parties via notificationAddress...
}
一般事件

還可以使用泛型來進一步定義事件的結構。
考慮使用EntityCreatedEvent<T>追城,其中T是所創(chuàng)建的實際實體的類型。
例如燥撞,您可以創(chuàng)建以下偵聽器定義來只為一個人接收EntityCreatedEvent:

@EventListener
public void onPersonCreated(EntityCreatedEvent<Person> event) {
    // ...
}

由于類型擦除座柱,只有在觸發(fā)的事件解析了事件偵聽器所基于的通用參數(shù)
(即class PersonCreatedEvent extends EntityCreatedEvent<Person> { …? })時,此方法才起作用 物舒。

在某些情況下色洞,如果所有事件都遵循相同的結構(前面示例中的事件也應該如此),那么這可能會變得非常乏味冠胯。
在這種情況下火诸,您可以實現(xiàn)ResolvableTypeProvider來指導運行時環(huán)境所提供的框架。
下面的事件展示了如何做到這一點:

public class EntityCreatedEvent<T> extends ApplicationEvent implements ResolvableTypeProvider {

    public EntityCreatedEvent(T entity) {
        super(entity);
    }

    @Override
    public ResolvableType getResolvableType() {
        return ResolvableType.forClassWithGenerics(getClass(), ResolvableType.forInstance(getSource()));
    }
}
這不僅適用于ApplicationEvent作為事件發(fā)送的任何對象荠察,而且適用于該對象置蜀。
1.15.3。方便地訪問低級資源

為了優(yōu)化使用和理解應用程序上下文悉盆,您應該熟悉Spring的Resource類盯荤,如參考資料中所述。

應用程序上下文是一個ResourceLoader焕盟,可用于加載資源對象秋秤。

Resource本質上是JDK java.net.URL類的功能豐富版本。
事實上,Resource 的實現(xiàn)在適當?shù)臅r候包裝了一個java.net.URL的實例灼卢。

Resource可以以透明的方式從幾乎任何位置獲取底層資源绍哎,包括類路徑、文件系統(tǒng)位置鞋真、可用標準URL描述的任何位置蛇摸,以及其他一些變體。

如果Resource位置字符串是沒有任何特殊前綴的簡單路徑灿巧,那么這些資源的來源是特定的赶袄,并且適合于實際的應用程序上下文類型。

您可以配置部署到應用程序上下文中的bean抠藕,以實現(xiàn)特殊的回調接口ResourceLoaderAware饿肺,在初始化時自動回調,而應用程序上下文本身作為ResourceLoader傳入盾似。
您還可以公開Resource類型的屬性敬辣,以便用于訪問靜態(tài)資源。Resource 像其他屬性一樣可以被注入零院。

您可以將這些資源屬性指定為簡單的字符串路徑溉跃,并在部署bean時依賴于從這些文本字符串到實際資源對象的自動轉換。

提供給ApplicationContext構造函數(shù)的位置路徑或路徑實際上是資源字符串告抄,并且以簡單的形式撰茎,根據(jù)特定的上下文實現(xiàn)進行適當?shù)奶幚怼?br> 例如,ClassPathXmlApplicationContext將簡單的位置路徑視為類路徑位置打洼。
您還可以使用帶有特殊前綴的位置路徑(資源字符串)來強制從類路徑或URL加載定義龄糊,而不管實際上下文類型是什么。

1.15.4募疮。應用程序啟動跟蹤

ApplicationContext管理Spring應用程序的生命周期炫惩,并圍繞組件提供豐富的編程模型。
因此阿浓,復雜的應用程序可能具有同樣復雜的組件圖和啟動階段他嚷。

使用特定的度量來跟蹤應用程序的啟動步驟可以幫助理解啟動階段的時間花費在哪里,它也可以作為一種更好地理解整個上下文生命周期的方法芭毙。

AbstractApplicationContext(及其子類)由ApplicationStartup檢測筋蓖,它收集關于不同啟動階段的StartupStep數(shù)據(jù):

  • 應用程序上下文生命周期(基本包掃描,配置類管理)
  • bean生命周期(實例化稿蹲、智能初始化扭勉、后處理)
  • 應用程序事件處理

下面是AnnotationConfigApplicationContext中的插裝示例:

// 創(chuàng)建并啟動記錄
StartupStep scanPackages = this.getApplicationStartup().start("spring.context.base-packages.scan");
// 向當前步驟添加標記信息
scanPackages.tag("packages", () -> Arrays.toString(basePackages));
// 執(zhí)行我們正在測量的實際階段
this.scanner.scan(basePackages);
// 結束
scanPackages.end();
1.15.5。Web應用程序的便捷ApplicationContext實例化

例如苛聘,可以使用ContextLoader以聲明方式創(chuàng)建ApplicationContext實例涂炎。當然忠聚,也可以通過使用ApplicationContext實現(xiàn)之一以編程方式創(chuàng)建ApplicationContext實例。

您可以使用ContextLoaderListener來注冊一個ApplicationContext唱捣,如以下示例所示:

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/daoContext.xml /WEB-INF/applicationContext.xml</param-value>
</context-param>

<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

監(jiān)聽器檢查contextConfigLocation參數(shù)两蟀。
如果參數(shù)不存在,偵聽器將使用/WEB-INF/applicationContext.xml作為默認值震缭。

當參數(shù)確實存在時赂毯,偵聽器使用預定義的分隔符(逗號、分號和空白)分隔字符串拣宰,并將這些值用作搜索應用程序上下文的位置党涕。

也支持Ant風格的路徑模式。
例如:
/WEB-INF/*Context.xml(對于在WEB-INF目錄中名稱以Context結尾的所有文件)

/WEB-INF/*/Context.xml(對于WEB-INF任何子目錄中的所有此類文件)

1.16.1巡社。BeanFactory or ApplicationContext膛堤?

本節(jié)解釋BeanFactory和ApplicationContext容器級別之間的差異,以及引導的含義晌该。

您應該使用ApplicationContext肥荔,除非您有很好的理由不這樣做,使用GenericApplicationContext和它的子類AnnotationConfigApplicationContext作為自定義引導的通用實現(xiàn)朝群。
這些是用于所有常見目的的Spring核心容器的主要入口點:加載配置文件燕耿、觸發(fā)類路徑掃描、以編程方式注冊bean定義和帶注解的類姜胖,以及(從5.0開始)注冊功能性bean定義誉帅。

因為ApplicationContext包含了BeanFactory的所有功能,所以一般建議它優(yōu)于普通的BeanFactory谭期,除非需要對bean處理進行完全控制的場景除外堵第。

對于許多擴展的容器特性,如注解處理和AOP代理隧出,BeanPostProcessor擴展點是必不可少的。如果只使用普通的DefaultListableBeanFactory阀捅,默認情況下不會檢測到這種后處理器并激活它胀瞪。

下表列出了BeanFactory和ApplicationContext接口和實現(xiàn)提供的特性。
表9.功能矩陣

特征 BeanFactory ApplicationContext
Bean實例化/布線 Yes Yes
集成的生命周期管理 No Yes
自動BeanPostProcessor登記 No Yes
方便的消息源訪問(用于內部化) No Yes
內置ApplicationEvent發(fā)布機制 No Yes

要使用顯式注冊Bean后處理器DefaultListableBeanFactory饲鄙,您需要以編程方式調用addBeanPostProcessor凄诞,如以下示例所示:

DefaultListableBeanFactory factory = new DefaultListableBeanFactory();

factory.addBeanPostProcessor(new AutowiredAnnotationBeanPostProcessor());
factory.addBeanPostProcessor(new MyBeanPostProcessor());

// now start using the factory

要將一個BeanFactoryPostProcessor應用到一個普通的DefaultListableBeanFactory中,你需要調用它的postProcessBeanFactory方法忍级,如下面的例子所示:

DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory);
reader.loadBeanDefinitions(new FileSystemResource("beans.xml"));

// bring in some property values from a Properties file
PropertySourcesPlaceholderConfigurer cfg = new PropertySourcesPlaceholderConfigurer();
cfg.setLocation(new FileSystemResource("jdbc.properties"));

// now actually do the replacement
cfg.postProcessBeanFactory(factory);

如上所見手動登記是十分不方便的帆谍,尤其是依靠BeanFactoryPostProcessor和BeanPostProcessor擴展功能的時候。

一個AnnotationConfigApplicationContext注冊了所有公共注解后處理器轴咱,并可能通過配置注解(如@EnableTransactionManagement)在后臺引入額外的處理器汛蝙。

在Spring的基于注解的配置模型的抽象層上烈涮,bean后處理器的概念僅僅成為容器內部的細節(jié)。
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末窖剑,一起剝皮案震驚了整個濱河市坚洽,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌西土,老刑警劉巖讶舰,帶你破解...
    沈念sama閱讀 217,542評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異需了,居然都是意外死亡跳昼,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評論 3 394
  • 文/潘曉璐 我一進店門肋乍,熙熙樓的掌柜王于貴愁眉苦臉地迎上來鹅颊,“玉大人,你說我怎么就攤上這事住拭∨猜裕” “怎么了?”我有些...
    開封第一講書人閱讀 163,912評論 0 354
  • 文/不壞的土叔 我叫張陵滔岳,是天一觀的道長杠娱。 經常有香客問我,道長谱煤,這世上最難降的妖魔是什么摊求? 我笑而不...
    開封第一講書人閱讀 58,449評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮刘离,結果婚禮上室叉,老公的妹妹穿的比我還像新娘。我一直安慰自己硫惕,他們只是感情好茧痕,可當我...
    茶點故事閱讀 67,500評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著恼除,像睡著了一般踪旷。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上豁辉,一...
    開封第一講書人閱讀 51,370評論 1 302
  • 那天令野,我揣著相機與錄音,去河邊找鬼徽级。 笑死气破,一個胖子當著我的面吹牛,可吹牛的內容都是我干的餐抢。 我是一名探鬼主播现使,決...
    沈念sama閱讀 40,193評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼低匙,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了朴下?” 一聲冷哼從身側響起努咐,我...
    開封第一講書人閱讀 39,074評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎殴胧,沒想到半個月后渗稍,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 45,505評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡团滥,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,722評論 3 335
  • 正文 我和宋清朗相戀三年加匈,在試婚紗的時候發(fā)現(xiàn)自己被綠了茁帽。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,841評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖卦尊,靈堂內的尸體忽然破棺而出肴裙,到底是詐尸還是另有隱情璃俗,我是刑警寧澤死讹,帶...
    沈念sama閱讀 35,569評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站父晶,受9級特大地震影響哮缺,放射性物質發(fā)生泄漏。R本人自食惡果不足惜甲喝,卻給世界環(huán)境...
    茶點故事閱讀 41,168評論 3 328
  • 文/蒙蒙 一尝苇、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧埠胖,春花似錦糠溜、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至谋竖,卻和暖如春汽馋,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背圈盔。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留悄雅,地道東北人驱敲。 一個月前我還...
    沈念sama閱讀 47,962評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像宽闲,于是被迫代替她去往敵國和親众眨。 傳聞我的和親對象是個殘疾皇子握牧,可洞房花燭夜當晚...
    茶點故事閱讀 44,781評論 2 354