基于Apache Zookeeper手寫實(shí)現(xiàn)動態(tài)配置中心(純代碼實(shí)踐)

相信大家都知道送漠,每個項(xiàng)目中會有一些配置信息放在一個獨(dú)立的properties文件中喧兄,比如application.properties。這個文件中會放一些常量的配置宅粥,比如數(shù)據(jù)庫連接信息参袱、線程池大小、限流參數(shù)秽梅。

在傳統(tǒng)的開發(fā)模式下抹蚀,這種方式很方便,一方面能夠?qū)ε渲眠M(jìn)行統(tǒng)一管理企垦,另一方面环壤,我們在維護(hù)的時(shí)候很方便。

但是隨著業(yè)務(wù)的發(fā)展以及架構(gòu)的升級钞诡,在微服務(wù)架構(gòu)中郑现,服務(wù)的數(shù)量以及每個服務(wù)涉及到的配置會越來越多,并且對于配置管理的需求越來越高臭增,比如要求實(shí)時(shí)性、獨(dú)立性竹习。

另外誊抛,在微服務(wù)架構(gòu)下,會涉及到不同的環(huán)境下的配置管理整陌、灰度發(fā)布拗窃、動態(tài)限流瞎领、動態(tài)降級等需求,包括對于配置內(nèi)容的安全與權(quán)限随夸,所以傳統(tǒng)的配置維護(hù)方式很難達(dá)到需求九默。

因此,就產(chǎn)生了分布式配置中心宾毒。

  • 傳統(tǒng)的配置方式不方便維護(hù)
  • 配置內(nèi)容的安全和訪問權(quán)限驼修,在傳統(tǒng)的配置方式中很難實(shí)現(xiàn)
  • 更新配置內(nèi)容時(shí),需要重啟

配置中心的工作流程

image-20200709192446173

<center>圖11-1</center>

Spring Boot的外部化配置

在本次課程中诈铛,我們會Zookeeper集成到Spring Boot的外部化配置中乙各,讓用戶無感知的使用配置中心上的數(shù)據(jù)作為數(shù)據(jù)源,所以我們需要先了解Spring Boot中的外部化配置幢竹。

Spring Boot的外部化配置是基于Environment來實(shí)現(xiàn)的耳峦,它表示Spring Boot應(yīng)用運(yùn)行時(shí)的環(huán)境信息,先來看基本使用

Environment的使用

  • 在spring boot應(yīng)用中焕毫,修改aplication.properties配置

    key=value
    
  • 創(chuàng)建一個Controller進(jìn)行測試

    @RestController
    public class EnvironementController {
    
        @Autowired
        Environment environment;
    
        @GetMapping("/env")
        public String env(){
            return environment.getProperty("key");
        }
    }
    

@Value注解使用

在properties文件中定義的屬性蹲坷,除了可以通過environment的getProperty方法獲取之外,spring還提供了@Value注解邑飒,

@RestController
public class EnvironementController {

    @Value("${env}")
    private String env;

    @GetMapping("/env")
    public String env(){
        return env;
    }
}

spring容器在加載一個bean時(shí)循签,當(dāng)發(fā)現(xiàn)這個Bean中有@Value注解時(shí),那么它可以從Environment中將屬性值進(jìn)行注入幸乒,如果Environment中沒有這個屬性懦底,則會報(bào)錯。

Environment設(shè)計(jì)猜想

Spring Boot的外部化配置罕扎,不僅僅只是appliation.properties聚唐,包括命令行參數(shù)、系統(tǒng)屬性腔召、操作系統(tǒng)環(huán)境變量等杆查,都可以作為Environment的數(shù)據(jù)來源。

  • @Value("${java.version}") 獲取System.getProperties 臀蛛, 獲取系統(tǒng)屬性
  • 配置command的jvm參數(shù)亲桦, -Denvtest=command,然后通過@Value("${envtest}")
image-20210818164156459

<center>圖11-2</center>

  • 第一部分是屬性定義浊仆,這個屬性定義可以來自于很多地方客峭,比如application.properties、或者系統(tǒng)環(huán)境變量等抡柿。
  • 然后根據(jù)約定的方式去指定路徑或者指定范圍去加載這些配置舔琅,保存到內(nèi)存中。
  • 最后洲劣,我們可以根據(jù)指定的key從緩存中去查找這個值备蚓。

擴(kuò)展Environment

我們可以自己擴(kuò)展Environment中的數(shù)據(jù)源课蔬,代碼如下;

其中,EnvironmentPostProcessor:它可以在spring上下文構(gòu)建之前可以設(shè)置一些系統(tǒng)配置郊尝。

CusEnvironmentPostProcessor

public class CusEnvironmentPostProcessor implements EnvironmentPostProcessor {
    private final Properties properties=new Properties();
    private String propertiesFile="custom.properties";

    @Override
    public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
        Resource resource=new ClassPathResource(propertiesFile);
        environment.getPropertySources().addLast(loadProperties(resource));
    }

    private PropertySource<?> loadProperties(Resource resource){
        if(!resource.exists()){
            throw new IllegalArgumentException("file:{"+resource+"} not exist");
        }
        try {
            properties.load(resource.getInputStream());
            return new PropertiesPropertySource(resource.getFilename(),properties);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

custom.properties

在classpath目錄下創(chuàng)建custom.properties文件

name=mic
age=18

spring.factories

在META-INF目錄下創(chuàng)建spring.factories文件二跋,因?yàn)镋nvironmentPostProcessor的擴(kuò)展實(shí)現(xiàn)是基于SPI機(jī)制完成的。

org.springframework.boot.env.EnvironmentPostProcessor=\
  com.example.springbootzookeeper.CusEnvironmentPostProcessor

TestController

創(chuàng)建測試類流昏,演示自定義配置加載的功能扎即。

@RestController
public class TestController {

    @Value("${name}")
    public String val;

    @GetMapping("/")
    public String say(){
        return val;
    }
}

總結(jié)

通過上面的例子我們發(fā)現(xiàn),在Environment中横缔,我們可以通過指定PropertySources來增加Environment外部化配置信息铺遂,使得在Spring Boot運(yùn)行期間自由訪問到這些配置。

那么我們要實(shí)現(xiàn)動態(tài)配置中心茎刚,無非就是要在啟動的時(shí)候襟锐,從遠(yuǎn)程服務(wù)器上獲取到數(shù)據(jù)保存到PropertySource中,并且添加到Environment膛锭。

下面我們就開始來實(shí)現(xiàn)這個過程粮坞。

Zookeeper實(shí)現(xiàn)配置中心

在本小節(jié)中,主要基于Spring的Environment擴(kuò)展實(shí)現(xiàn)自己的動態(tài)配置中心初狰,代碼結(jié)構(gòu)如圖11-3所示莫杈。

image-20210805232800966

<center>圖11-3</center>

自定義配置中心的相關(guān)說明

在本次案例中,我們并沒有完全使用EnvironmentPostProcessor這個擴(kuò)展點(diǎn)奢入,而是基于SpringFactoriesLoader自定義了一個擴(kuò)展點(diǎn)筝闹,主要目的是讓大家知道EnvironmentPostProcessor擴(kuò)展點(diǎn)的工作原理,以及我們以后自己也可以定義擴(kuò)展點(diǎn)腥光。

代碼實(shí)現(xiàn)

以下是所有代碼的實(shí)現(xiàn)過程关顷,按照下面這個步驟去開發(fā)即可完成動態(tài)配置中心。

ZookeeperApplicationContextInitializer

ApplicationContextInitializer擴(kuò)展武福,它是在ConfigurableApplicationContext通過調(diào)用refresh函數(shù)來初始化Spring容器之前會進(jìn)行回調(diào)的一個擴(kuò)展方法议双,我們可以在這個擴(kuò)展中實(shí)現(xiàn)Environment的擴(kuò)展。

所以這個類的主要作用就是在ApplicationContext完成refresh之前捉片,擴(kuò)展Environment平痰,增加外部化配置注入。

public class ZookeeperApplicationContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext>{
    //PropertySourceLocator接口支持?jǐn)U展自定義配置加載到spring Environment中伍纫。
    private final List<PropertySourceLocator> propertySourceLocators;

    public ZookeeperApplicationContextInitializer(){
        //基于SPI機(jī)制加載所有的外部化屬性擴(kuò)展點(diǎn)
        ClassLoader classLoader=ClassUtils.getDefaultClassLoader();
        //這部分的代碼是SPI機(jī)制
        propertySourceLocators=new ArrayList<>(SpringFactoriesLoader.loadFactories(PropertySourceLocator.class,classLoader));
    }
    @Override
    public void initialize(ConfigurableApplicationContext applicationContext) {
        //獲取運(yùn)行的環(huán)境上下文
        ConfigurableEnvironment environment=applicationContext.getEnvironment();
        //MutablePropertySources它包含了一個CopyOnWriteArrayList集合宗雇,用來包含多個PropertySource。
        MutablePropertySources mutablePropertySources = environment.getPropertySources();
        for (PropertySourceLocator locator : this.propertySourceLocators) {
            //回調(diào)所有實(shí)現(xiàn)PropertySourceLocator接口實(shí)例的locate方法莹规,收集所有擴(kuò)展屬性配置保存到Environment中
            Collection<PropertySource<?>> source = locator.locateCollection(environment,applicationContext);
            if (source == null || source.size() == 0) {
                continue;
            }
            //把PropertySource屬性源添加到environment中赔蒲。
            for (PropertySource<?> p : source) {
                //addFirst或者Last決定了配置的優(yōu)先級
                mutablePropertySources.addFirst(p);
            }
        }
    }
}

創(chuàng)建classpath:/META-INF/spring.factories

org.springframework.context.ApplicationContextInitializer=\
  com.gupaoedu.example.zookeepercuratordemo.config.ZookeeperApplicationContextInitializer

PropertySourceLocator

PropertySourceLocator接口支持?jǐn)U展自定義配置加載到spring Environment中。

public interface PropertySourceLocator {

    PropertySource<?> locate(Environment environment,ConfigurableApplicationContext applicationContext);
    //Environment表示環(huán)境變量信息
    //applicationContext表示應(yīng)用上下文
    default Collection<PropertySource<?>> locateCollection(Environment environment, ConfigurableApplicationContext applicationContext) {
        return locateCollection(this, environment,applicationContext);
    }

    static Collection<PropertySource<?>> locateCollection(PropertySourceLocator locator,
                                                          Environment environment,ConfigurableApplicationContext applicationContext) {
        PropertySource<?> propertySource = locator.locate(environment,applicationContext);
        if (propertySource == null) {
            return Collections.emptyList();
        }
        return Arrays.asList(propertySource);
    }
}

ZookeeperPropertySourceLocator

ZookeeperPropertySourceLocator用來實(shí)現(xiàn)基于Zookeeper屬性配置的擴(kuò)展點(diǎn),它會訪問zookeeper獲取遠(yuǎn)程服務(wù)器數(shù)據(jù)嘹履。

public class ZookeeperPropertySourceLocator implements PropertySourceLocator{

    private final CuratorFramework curatorFramework;

    private final String DATA_NODE="/data";  //僅僅為了演示,所以寫死目標(biāo)數(shù)據(jù)節(jié)點(diǎn)

    public ZookeeperPropertySourceLocator() {
        curatorFramework= CuratorFrameworkFactory.builder()
                .connectString("192.168.221.128:2181")
                .sessionTimeoutMs(20000).connectionTimeoutMs(20000)
                .retryPolicy(new ExponentialBackoffRetry(1000,3))
                .namespace("config").build();
        curatorFramework.start();
    }

    @Override
    public PropertySource<?> locate(Environment environment, ConfigurableApplicationContext applicationContext) {
        System.out.println("開始加載遠(yuǎn)程配置到Environment中");
        CompositePropertySource composite = new CompositePropertySource("configService");
        try {
            Map<String,Object> dataMap=getRemoteEnvironment();
            //基于Map結(jié)構(gòu)的屬性源
            MapPropertySource mapPropertySource=new MapPropertySource("configService",dataMap);
            composite.addPropertySource(mapPropertySource);
            addListener(environment,applicationContext);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return composite;
    }

    private Map<String,Object> getRemoteEnvironment() throws Exception {
        String data=new String(curatorFramework.getData().forPath(DATA_NODE));
        //暫時(shí)支持json格式
        ObjectMapper objectMapper=new ObjectMapper();
        Map<String,Object> map=objectMapper.readValue(data, Map.class);
        return map;
    }
    //添加節(jié)點(diǎn)變更事件
    private void addListener(Environment environment, ConfigurableApplicationContext applicationContext){
        NodeDataCuratorCacheListener curatorCacheListener=new NodeDataCuratorCacheListener(environment,applicationContext);
        CuratorCache curatorCache=CuratorCache.build(curatorFramework,DATA_NODE,CuratorCache.Options.SINGLE_NODE_CACHE);
        CuratorCacheListener listener=CuratorCacheListener
                .builder()
                .forChanges(curatorCacheListener).build();
        curatorCache.listenable().addListener(listener);
        curatorCache.start();
    }
}

配置擴(kuò)展點(diǎn): classpath:/META-INF/spring.factories

com.gupaoedu.example.zookeepercuratordemo.config.PropertySourceLocator=\
  com.gupaoedu.example.zookeepercuratordemo.config.ZookeeperPropertySourceLocator

配置動態(tài)變更邏輯

NodeDataCuratorCacheListener

NodeDataCuratorCacheListener用來實(shí)現(xiàn)持久化訂閱機(jī)制债热,當(dāng)目標(biāo)節(jié)點(diǎn)數(shù)據(jù)發(fā)生變更時(shí)砾嫉,需要收到變更并且應(yīng)用。

public class NodeDataCuratorCacheListener implements CuratorCacheListenerBuilder.ChangeListener {
    private Environment environment;
    private ConfigurableApplicationContext applicationContext;
    public NodeDataCuratorCacheListener(Environment environment, ConfigurableApplicationContext applicationContext) {
        this.environment = environment;
        this.applicationContext=applicationContext;
    }
    @Override
    public void event(ChildData oldNode, ChildData node) {
        System.out.println("數(shù)據(jù)發(fā)生變更");
        String resultData=new String(node.getData());
        ObjectMapper objectMapper=new ObjectMapper();
        try {
            Map<String,Object> map=objectMapper.readValue(resultData, Map.class);
            ConfigurableEnvironment cfe=(ConfigurableEnvironment)environment;
            MapPropertySource mapPropertySource=new MapPropertySource("configService",map);
            cfe.getPropertySources().replace("configService",mapPropertySource);
            //發(fā)布事件窒篱,用來更新@Value注解對應(yīng)的值(事件機(jī)制可以分兩步演示)
            applicationContext.publishEvent(new EnvironmentChangeEvent(this));
            System.out.println("數(shù)據(jù)更新完成");
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
    }
}

EnvironmentChangeEvent

定義一個環(huán)境變量變更事件焕刮。

public class EnvironmentChangeEvent extends ApplicationEvent {
    
    public EnvironmentChangeEvent(Object source) {
        super(source);
    }
}

ConfigurationPropertiesRebinder

ConfigurationPropertiesRebinder接收事件,并重新綁定@Value注解的數(shù)據(jù)墙杯,使得數(shù)據(jù)能夠動態(tài)改變

@Component
public class ConfigurationPropertiesRebinder implements ApplicationListener<EnvironmentChangeEvent> {
    private ConfigurationPropertiesBeans beans;
    private Environment environment;
    public ConfigurationPropertiesRebinder(ConfigurationPropertiesBeans beans,Environment environment) {
        this.beans = beans;
        this.environment=environment;
    }

    @Override
    public void onApplicationEvent(EnvironmentChangeEvent event) {
        rebind();
    }
    public void rebind(){
        this.beans.getFieldMapper().forEach((k,v)->{
            v.forEach(f->f.resetValue(environment));
        });
    }
}

ConfigurationPropertiesBeans

ConfigurationPropertiesBeans實(shí)現(xiàn)了BeanPostPorocessor接口配并,該接口我們也叫后置處理器,作用是在Bean對象在實(shí)例化和依賴注入完畢后高镐,在顯示調(diào)用初始化方法的前后添加我們自己的邏輯溉旋。注意是Bean實(shí)例化完畢后及依賴注入完成后觸發(fā)的。

我們可以在這個后置處理器的回調(diào)方法中嫉髓,掃描指定注解的bean观腊,收集這些屬性,用來觸發(fā)事件變更算行。

@Component
public class ConfigurationPropertiesBeans implements BeanPostProcessor {

    private Map<String,List<FieldPair>> fieldMapper=new HashMap<>();

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName)
        throws BeansException {
        Class clz=bean.getClass();
        if(clz.isAnnotationPresent(RefreshScope.class)){ //如果某個bean聲明了RefreshScope注解梧油,說明需要進(jìn)行動態(tài)更新
            for(Field field:clz.getDeclaredFields()){
                Value value=field.getAnnotation(Value.class);
                List<String> keyList=getPropertyKey(value.value(),0);
                for(String key:keyList){
                    //使用List<FieldPair>存儲的目的是,如果在多個bean中存在相同的key州邢,則全部進(jìn)行替換
                    fieldMapper.computeIfAbsent(key,(k)->new ArrayList()).add(new FieldPair(bean,field,value.value()));
                }
            }
        }
        return bean;
    }
    //獲取key信息儡陨,也就是${value}中解析出value這個屬性
    private List<String> getPropertyKey(String value,int begin){
        int start=value.indexOf("${",begin)+2;
        if(start<2){
            return new ArrayList<>();
        }
        int middle=value.indexOf(":",start);
        int end=value.indexOf("}",start);
        String key;
        if(middle>0&&middle<end){
            key=value.substring(start,middle);
        }else{
            key=value.substring(start,end);
        }
        //如果是這種用法,就需要遞歸量淌,@Value("${swagger2.host:127.0.0.1:${server.port:8080}}")
        List<String> keys=getPropertyKey(value,end);
        keys.add(key);
        return keys;
    }

    public Map<String, List<FieldPair>> getFieldMapper() {
        return fieldMapper;
    }
}

RefreshScope

定義注解來實(shí)現(xiàn)指定需要動態(tài)刷新類的識別骗村。

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RefreshScope {

}

FieldPair

這個類中主要通過PropertyPlaceholderHelper將字符串里的占位符內(nèi)容,用我們配置的properties里的替換类少。

public class FieldPair {
    private static PropertyPlaceholderHelper propertyPlaceholderHelper=new PropertyPlaceholderHelper("${","}",":",true);
    private Object bean;
    private Field field;
    private String value;

    public FieldPair(Object bean, Field field, String value) {
        this.bean = bean;
        this.field = field;
        this.value = value;
    }

    public void resetValue(Environment environment){
        boolean access=field.isAccessible();
        if(!access){
            field.setAccessible(true);
        }
        //從新從environment中將占位符替換為新的值
        String resetValue=propertyPlaceholderHelper.replacePlaceholders(value,((ConfigurableEnvironment) environment)::getProperty);
        try {
           //通過反射更新
            field.set(bean,resetValue);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

訪問測試ConfigController

@RefreshScope
@RestController
public class ConfigController {

    @Value("${name}")
    private String name;

    @Value("${job}")
    private String job;

    @GetMapping
    public String get(){
        return name+":"+job;
    }
}

基于自定義PropertySourceLocator擴(kuò)展

由于在上述代碼中叙身,我們創(chuàng)建了一個PropertySourceLocator接口,并且在整個配置加載過程中硫狞,我們都是基于PropertySourceLocator擴(kuò)展點(diǎn)來進(jìn)行加載的信轿,所以也就是意味著除了上述使用的Zookeeper作為遠(yuǎn)程配置裝載以外,我們還可以通過擴(kuò)展PropertySourceLocator來實(shí)現(xiàn)其他的擴(kuò)展残吩,具體實(shí)現(xiàn)如下

CustomPropertySourceLocator

創(chuàng)建一個MapPropertySource作為Environment的屬性源财忽。

public class CustomPropertySourceLocator implements PropertySourceLocator{

    @Override
    public PropertySource<?> locate(Environment environment, ConfigurableApplicationContext applicationContext) {
        Map<String, Object> source = new HashMap<>();
        source.put("age","18");
        MapPropertySource propertiesPropertySource = new MapPropertySource("configCenter",source);
        return propertiesPropertySource;
    }
}

spring.factories

由于CustomPropertySourceLocator是自定義擴(kuò)展點(diǎn),所以我們需要在spring.factories文件中定義它的擴(kuò)展實(shí)現(xiàn)泣侮,修改如下

com.gupaoedu.example.zookeepercuratordemo.config.PropertySourceLocator=\
  com.gupaoedu.example.zookeepercuratordemo.config.ZookeeperPropertySourceLocator,\
  com.gupaoedu.example.zookeepercuratordemo.config.CustomPropertySourceLocator

ConfigController

接下來即彪,我們通過下面的代碼進(jìn)行測試,從結(jié)果可以看到,我們自己定義的propertySource被加載到Environment中了隶校。

@RefreshScope
@RestController
public class ConfigController {

    @Value("${name}")
    private String name;

    @Value("${job}")
    private String job;

    @Value("${age}")
    private String age;

    @GetMapping
    public String get(){
        return name+":"+job+":"+age;
    }
}

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末漏益,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子深胳,更是在濱河造成了極大的恐慌绰疤,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,843評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件舞终,死亡現(xiàn)場離奇詭異轻庆,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)敛劝,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,538評論 3 392
  • 文/潘曉璐 我一進(jìn)店門余爆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人夸盟,你說我怎么就攤上這事蛾方。” “怎么了上陕?”我有些...
    開封第一講書人閱讀 163,187評論 0 353
  • 文/不壞的土叔 我叫張陵转捕,是天一觀的道長。 經(jīng)常有香客問我唆垃,道長五芝,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,264評論 1 292
  • 正文 為了忘掉前任辕万,我火速辦了婚禮枢步,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘渐尿。我一直安慰自己醉途,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,289評論 6 390
  • 文/花漫 我一把揭開白布砖茸。 她就那樣靜靜地躺著隘擎,像睡著了一般。 火紅的嫁衣襯著肌膚如雪凉夯。 梳的紋絲不亂的頭發(fā)上货葬,一...
    開封第一講書人閱讀 51,231評論 1 299
  • 那天,我揣著相機(jī)與錄音劲够,去河邊找鬼震桶。 笑死,一個胖子當(dāng)著我的面吹牛征绎,可吹牛的內(nèi)容都是我干的蹲姐。 我是一名探鬼主播,決...
    沈念sama閱讀 40,116評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼柴墩!你這毒婦竟也來了忙厌?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,945評論 0 275
  • 序言:老撾萬榮一對情侶失蹤江咳,失蹤者是張志新(化名)和其女友劉穎慰毅,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體扎阶,經(jīng)...
    沈念sama閱讀 45,367評論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,581評論 2 333
  • 正文 我和宋清朗相戀三年婶芭,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了东臀。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,754評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡犀农,死狀恐怖惰赋,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情呵哨,我是刑警寧澤赁濒,帶...
    沈念sama閱讀 35,458評論 5 344
  • 正文 年R本政府宣布,位于F島的核電站孟害,受9級特大地震影響拒炎,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜挨务,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,068評論 3 327
  • 文/蒙蒙 一击你、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧谎柄,春花似錦丁侄、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,692評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至劈猿,卻和暖如春拙吉,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背揪荣。 一陣腳步聲響...
    開封第一講書人閱讀 32,842評論 1 269
  • 我被黑心中介騙來泰國打工庐镐, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人变逃。 一個月前我還...
    沈念sama閱讀 47,797評論 2 369
  • 正文 我出身青樓必逆,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子名眉,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,654評論 2 354

推薦閱讀更多精彩內(nèi)容