easyconfig开发者指南

https://gitee.com/sleepywang1024/easyconfig/blob/master/developer-guide.md

easyconfig开发者指南

面向需要深入了解easyconfig使用的研发人员,请关注本文内容。普通研发用户请你移驾quickstart

接入easyconfig组件

quickstart 提到的接入方法通常已经满足需要,您也可以自定义、更灵活地进行此项工作。根本上讲,easyconfig提供两个类供业务应用系统接入和集成。这两个类分别实现配参的刷新配参的管理能力。

配参的刷新

jm.easyconfig.CfgContextEnvironmentAware用于注册管理系统中需要配参的Spring管理的Bean,以及后续配参更新时的数据更新。CfgContextEnvironmentAware自身必须是一个Spring Bean,且只能有一个。

把CfgContextEnvironmentAware注册为Spring的一个Bean就完成了配参刷新能力的初始化。

配参的管理

jm.easyconfig.CfgAdminServlet是一个标准的Servlet,用于提供管理界面给用户访问。把它注册为一个WEB Servlet则完成了配参管理控制台的初始化。Spring MVC项目通常在web.xml文件中配置。

定置化接入示例

easyconfig源码中有一个类CfgAutoConfiguration,就是负责完成上面刷新Bean和管理Servlet的注册,如下示意:

@ConfigurationpublicclassCfgAutoConfiguration{@Bean@ConditionalOnMissingBean(CfgContextEnvironmentAware.class)CfgContextEnvironmentAwarecfgContextEnvironmentAware(){returnnewCfgContextEnvironmentAware();}@Bean@ConditionalOnMissingBean(CfgServletRegistrationBean.class)publicCfgServletRegistrationBeancfgAdminServlet(){returnnewCfgServletRegistrationBean("/easyconfig-admin/*");}}

SpringBoot项目快速接入

对于SpringBoot项目,可以通过导入CfgAutoConfiguration更快捷地接入,如下所示:

@Import({jm.easyconfig.CfgAutoConfiguration.class})

这样一条指令就完成easyconfig系统集成和使用,Import指令通常放在主应用(@SpringBootApplication)的class标注处,或任意**@Configuration**类的class标注处导入。

注:出于spring版本兼容性考虑,CfgAutoConfiguration代码被注释掉了,需要的话你可以加回去。

通过属性标注访问动态配参

通过标注快速得到使用

请快速浏览quickstart 介绍。

@Value动态刷新支持开关

值得注意的是:并不是所有的@Value标注都被动态刷新支持。@Value在Spring中应用更广泛,例如构造方法的参数,SpEL表达式等,但这些不是配参管理面临的问题,不被easyconfig支持。动态刷新限于属性占位符${...}

出于兼容性、习惯性考虑,easyconfig允许关闭@Value的动态刷新能力,属性easyconfig.value.enabled设置为false即可。

easyconfig.value.enabled=false

注:@Value动态刷新默认是开启的,明确配置为false才可以禁用。

@EasyValue的使用(推荐)

@EasyValue标注提供@Value标注的value()属性外,提供了name()属性,用于为配参指定供人阅读的名字,这为后续配参管理提供很大便利。示例:

@EasyValue(value="${test.port:8080}",name="XXX服务端口")publicvoidsetPort(intport){this.port=port;}

注:在加入到动态配参管理时,name属性会随属性key和value一起自动导入,简化管理和文档工作。

除此name属性之外,@EasyValue在以下方面与@Value有所不同。

@Value引用的属性,如果Spring传统方式(如默认值、配参文件等)没有提供,则Spring会启动失败,@EasyValue则没有这个限制。

@Value支持的数据类型更少,主要是简单数据类型,以及简单数据类型的数组或列表。@EasyValue则不受这个限制,详情请参考支持的数据类型

@EasyValue标注的属性在Spring容器生命周期的postProcessBeforeInitialization阶段赋值,@Value在Spring更早一点就被初始化。

@ValueExt的使用

@ValueExt标注为方便配参管理增加了name()属性,其它行为与@Value完全形同,包括@Value若干限制。

可以标注在什么地方?

简单地讲,就是可以标注在类的field变量setter方法上面,可以是父类子类,可以是任意的可见性,从public公开到private私有。

注意:

在setter方法上添加标注,并不需要提供一个getter方法,但建议你同时提供getter方法,便于easyconfig检测数值是否发生变化,避免不必要的更新。

不支持在接口方法上添加动态刷新的标注,认为不应该对配参这么做。

选择什么样的标注形式?

推荐使用**@EasyValue**标注,可以继续使用@Value。

field上面使用标注更简单,在property的getter方法上使用标注更灵活、强大、健壮。

伴随配参变化需要进行一些处理的情景,如各类数据检查及转换、后台服务等重新初始化等,建议标注在setter上。

而对于更复杂的情景,如果依赖于多个配参的初始化工作,建议使用@PostRefresh标注初始化方法,更多信息参见Bean属性赋值及刷新次序。这种情景可能意味着配参设计不合理,如果不是遗留系统,标注setter方法结合复杂数据类型能更方便地解决这个问题。

通过API访问动态配参

通过前面提到的标注获得配参的数值,必须依附于一个Spring管理的Bean对象(@Bean、@Component、@Service、 @Controller @RestController、 @SpringApplicationBoot等)。但并不是所有的应用都希望这样,下面讨论通过API的形式更灵活的访问动态配参。

配参数值的访问

通过Spring内置的Environment可以访问easyconfig管理的配参,返回联机更新的最新内容。

受限于Environment,这种方式不支持复杂数据类型。需要得到复杂数据类型,可以先获取String类型配参,自行转换为复杂类型。下面示例中就是使用工具类CfgHelper做的Json的文本到Bean的转换。

@AutowiredEnvironmentenvironment;voidaccessByEnvironment(){StringbeanListText=environment.getProperty("complex.beanlist");DataBeanSample[]beanList=CfgHelper.jsonRead(beanListText,DataBeanSample[].class);}

配参对象的注册

对象如果不被Spring管理,但还希望其被@EasyValue等标注的属性联机更新,可以借助CfgContextEnvironmentAware的register方法,示例如下:

@AutowiredCfgContextEnvironmentAwareaware;publicstaticclassPropSample{@EasyValue("${complex.beanlist}")DataBeanSample[]beanList;}voidaccessByBean(){PropSampleprop=newPropSample();aware.register(prop);System.out.println("accessByBean:"+Arrays.asList(prop.beanList));}

如果不是@Autowired注入CfgContextEnvironmentAware,编程可以通过**CfgContextEnvironmentAware.getSharedInstance()**的方式获得。

支持的数据类型

丰富的数据类型支持

easyconfig支持比Spring更为丰富的数据类型,保持兼容和一致,会优先使用Spring内置的类型转换服务,而复杂类型使用JSON解析器进行转换。

原子数据类型及其包装类,如String、boolean(Boolean)、int(Integer)等

常用简单类型,如File、URI、URL、BigDecimal等

原子类型、简单类型的数组,以及字符串的列表,如int[], String[]等,ArrayList (JAVA及Spring限制导致,其它元素类型的集合请参考下面的复杂数据类型)。文本的书写形式是元素之间以","号分割,例如a,b,c表示三个字符串(a/b/c)的数组或List。

复杂数据类型(推荐),如任意类型的数组、任意Bean类型、集合、Map(Key-Value)等。文本的书写格式要符合JSON,数组以[...]包裹,对象以{...}包裹。

注:

前三种类型是Spring配参自身就支持的,easyconfig保持继承和兼容。

第四种类型--任意复杂数据类型,是easyconfig重点扩展支持的内容,简单、强大、易用。

理论上,第四种类型覆盖前三种类型,easyconfig对它们是使用不同的是不同类型转换器。

复杂数据类型使用

复杂数据类型使用起来一点也不复杂,应该说和其它数据类型的使用方式一样,并不需研发人员为此额外做工作。但有一些技巧和注意事项,能帮助研发童鞋更好地使用:

底层转换使用JSON机制,JSON转换器保持与Spring使同一个(Spring可以使用多个Json转换器类库,并且可以定制)。复杂类型具体表现可能会受到JSON转换器的约束(不同担心这个,目前JSON转换很成熟)。

复杂类型是一个集合时,建议使用数组类型替代。这是因为JAVA语言对泛型支持的限制,导致JSON转换器对集合支持不是很好。下面示例中,由于runtime知道listLimited变量是List.class但不知道它的元素是DataBean.class类型,导致赋值给listLimited的数值是一个ArrayList>类型,但arrayStyle变量就没有这个问题,可以正确地赋值。

...publicstaticclassDataBean{publicStringname;publicintage;}@EasyValue("${test.datalist}")publicDataBean[]arrayStyle; @EasyValue("${test.datalist}")publicList<DataBean>listLimited;...

改用List类型为array类型实际上并不会给研发带来什么不便,但却很容易解决JSON转换的困惑。

复杂类型是一个Bean(class)时,最为灵活且强大。Bean拥有的属性也可以使用List类型,不受前一条建议的限制。例如下例中List list作为复杂配参对象的一个属性,这是没有问题的。

...publicstaticclassComplexBean{publicList<DataBean>list;publicintanyvalue=9;Stringanything="hello easyconfig";} @EasyValue("${test.complex}")publicComplexBeanbeanStyle;...

复杂类型的配参设置必须是JSON格式。这点很容易理解,easyconfig的管理控制台,也能很好地支持JSON类型。但如果你也希望在属性文件(properties)文件中配置,请注意不要换行 或者 最后一行外每行以反斜杠 \ 结尾,如下所示。

...test.datalist=[\{"name":"zs","age":21},\{"name":"lis","age":32}]...

注:easyconfig管理控制台提供工具支持把多行数据平铺到一行。

复杂类型的JSON转换技巧。JSON转换过程中,有些数据类型容易出现兼容性问题,如datetime。解决此类问题的方法通常是对JSON转换器进行各类配置,添加定置的对象串行化/反串行化组件,您可以同解决任意WEB项目遇到的问题解决此处复杂类型转换。easyconfig使用spring内置的转换器,你对此转换器的配置会被easyconfig继承。

数据源连接配置及初始化

通常情况下,easyconfig不要你对动态配参的数据源做任何配置,easyconfig会在启动时自动检测并做好相关初始化动作。

自动检测并使用数据源

通常情况下,easyconfig会自动检测系统中已经存在的DataSource,把这个数据源当作储存配参的数据源,并在数据源自动检测,需要时自动创建动态配参需要的两张数据库表。检测数据源的方式是从Spring的ApplicationContext中检查是否存在DataSource.class创建的Bean。

手工配置数据源

easyconfig允许研发同学通过property指定配参使用的数据源,指定数据源优先级高于自动检测。这是考虑到有些多数据源的系统、用户动态配置数据源的系统、以及非SQL数据源的系统等,无论是多DataSource或者没有DataSource,通过配参指定配置数据源准确地解决配参的存储问题。

easyconfig指定了四个property用于配置数据源,含义同spring.datasource下的对应的属性,示例如下:

easyconfig.datasource.url=jdbc:mysql://localhost:3306/testeasyconfig.datasource.username=rooteasyconfig.datasource.password=easyconfig.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

注: 显然这几个参数只能通过Spring工程的传统技术配置:properties或者yml文件进行,虽然easyconfig的许多配参支持动态修改,但数据源连接参数不支持动态配参。

数据源中schema检测及初始化

动态配参存储在数据源数据库的一张表中,另一张表存储配参的变更历史。系统启动时,easyconfig会检测相关数据表是否创建,没有创建时会自动创建,完成数据源的初始化。

如果你不希望每次启动做这种检测,可以通过一个配参关闭此检测,如下所示:

easyconfig.auto-init-schema=false

easyconfig创建的两张数据表的结构都很简单,如下所示。config_item_main存储当前的动态配参,可以手工或任意方式维护,但不建议这样做,直接修改数据库可能需要若干分钟才能在系统中生效。

配参数据存储表

config_item_main

字段代码字段名称说明

item_code配参的唯一代码@Value或@EasyValue引用使用,非空唯一、表的主键

item_name配参的名字(简称)

item_value参数的数值@Value或@EasyValue标注的属性或setter方法,得到的数值

item_schema配参的格式说明扩展json schema方式的配参说明,复杂类型的格式规范

item_status配参的状态说明二进制第一位为1时表示删除,0表示正常,第二位为1表示密文,0表示明文

updated_time配参最后更新时间戳

updated_user配参最后更新用户

updated_history更新历史

config_item_main_history( 配参变更历史记录表)

字段代码字段名称说明

changed_id唯一IDID为自增数字,主键,满足关系数据库要求所加

changed_summary变更汇总信息

changed_content变更的详细内容

changed_time变更时间

changed_user变更用户

changed_status变更状态

注:

easyconfig支持单数据源多套配参,会在数据源中会创建多套数据表,表名字格式为 config_item_main中main被替换为相应名称。

配置的方式是由easyconfig.client属性指定,默认为main。

easyconfig.client=main

配参动态刷新机制

当配参被修改后,如何快速及时地更新到所有使用这些配参的系统中,常用机制无外乎主动轮询 或 被动通知,easyconfig支持这两种方式,用户基于需要选择其中之一。

轮询数据刷新

优点是简单、不需要额外中间件、jar包支持。缺点是时效性差、对数据源造成一些负载。

消息通知机制

优点是时效性、扩展性比较好。缺点是需要rabbitmq中间件和访问jar包。

更新机制配参与选择

系统配参easyconfig.updater用于指定使用那个配参更新器,默认值为auto,自动检测系统中是否存在rabbitmq,存在则使用,不存在则使用轮询方式。

可以明确指定更新器,polling为轮询方式、rabbitmq为采用rabbitmq的消息机制。

easyconfig.updater=auto

详细介绍请参考更新器详情

Bean属性赋值及刷新次序

搞明白此小结需要清楚了解Spring对Bean生命周期管理,供资深用户参考。

系统启动加载时,关键事件的执行次序如下:

在setEnvironment(Environment)期间,获取系统的本地缓存的动态配参,保存到environment对象中。

在setApplicationContext(ApplicationContext)期间,从数据源获取动态配参,覆盖environment对象的动态配参。

在postProcessBeforeInitialization期间,检测Spring管理的每一个bean对象内的标注,如@EasyValue、@ValueExt、@Value等,注册相关信息到内存中,并解析参数数值、做相应的类型转换,对相应属性赋值。

当一个bean的所有属性赋值完成后,对bean进行整体初始化。

配参修改后刷新时,关键事件的执行次序如下:

针对每一个配参,更新所有引用此配参的bean对象的相应属性。

当所有被更新配参的所有bean对象都被更新完毕后,针对实际做出修改的bean对象,依次对bean进行整体初始化。

Bean的整体初始化逻辑

Bean的整体初始化指Bean的各个属性被赋值完成后,被触发的初始化操作。包括

InitializingBean接口的afterPropertiesSet方法

@PostConstruct标注的方法

@PostRefresh标注的方法,这个标注是easyconfig引入并触发的。

基本的规则是:

在系统加载的初始化阶段,afterPropertiesSet 和 @PostConstruct会优先于@PostRefresh。也就是这个阶段只有afterPropertiesSet 和 @PostConstruct都没定义时,才会调用@PostRefresh标注的方法。而没有afterPropertiesSet 和 @PostConstruct时,才会去调用标注@PostRefresh。并且这个阶段对afterPropertiesSet 和 @PostConstruct的调用,不是easyconfig而是由spring完成。

在配参的动态刷新阶段,@PostRefresh会优先于afterPropertiesSet 和 @PostConstruct。也就是这个阶段只要标注了@PostRefresh方法,就只会调用@PostRefresh,而不再会调用afterPropertiesSet 和 @PostConstruct。而没有标注@PostRefresh时,才会去调用afterPropertiesSet 和 @PostConstruct。

访问安全控制

easyconfig内置管理控制台用于配参的查看、修改、恢复等操作,出于安全和审计的需要,对配参的修改与恢复需要得到严格的管理:谁有权限进行什么操作,easyconfig会自动记录谁进行相关操作。

easyconfig内置用户权限控制,也可以集成宿主应用用户权限,此时需要系统告知easyconfig当前操作的用户是谁。

四种安全模式

easyconfig提供四种安全控制方式:none、self、host、mixture,通过配参easyconfig.security.mode控制,默认为self,即:

easyconfig.security.mode=self

none:没有安全控制,easyconfig没有为此做工作;

self: easyconfig控制安全,创建用户、授权、登录登出等;

host: 宿主应用控制安全,提供访问easyconfig的用户及安全信息;

mixture:easyconfig和宿主应用共同控制安全,宿主应用提供系统当前登录用户,由easyconfig控制能对哪些配参做哪些访问。

none是没有认证和授权,self是easyconfig控制认证授权、host是宿主应用控制认证授权,mixture是宿主提供认证,easyconfig提供授权。

另:easyconfig管理控制台是前后端分离应用,宿主应用可以通过应用服务器的filter等机制,自行控制对eayconfig的请求。此种情境下,任何控制自然不受easyconfig限制,或easyconfig.security.mode选项的限制。

宿主定制安全控制

easyconfig.security.mode为host 或 mixture时,需要宿主应用定制安全能力。具体方式为:

注册一个bean对象,实现接口CfgContextUser, 示例如下:

@AutowiredprivateHttpSessionsession; @BeanpublicCfgContextUsercontextUser(){returnnewCfgContextUser(){@OverridepublicStringusername(){HostUseruser=session.getAttribute("user");returnuser==null?"":user.getEmail();}@OverridepublicAdminUseruser(){AdminUseruser=newAdminUser();user.username="hello";user.func=AdminUser.AUTH_ALL;returnuser;}};}

当host模式时,需要实现AdminUser user()方法,返回访问easyconfig的当前用户信息,关键信息是username、func、acl。

username:用户名称,唯一,登记在配参修改日志。

func: 功能权限,以及是否启动数据权限。

acl: 数据权限的访问控制表,形式同easyconfig的数据权限配置内容。

当mixture模式时,需要实现String username()方法,返回访问easyconfig的当前username,权限信息取自easyconfig配置的同名用户的权限。

控制用户访问权限

出于安全考虑,easyconfig.security.mode只能通过属性文件等传统方式配置,不能通过easyconfig自身的动态配参配置。

配置应用profiles

概述

配参环境管理是一项非常重要和有效的工具,支持应用系统同一时间不同实例使用不同配参运行。其设计目标如下:

空间维度的配参管理。相对于“时间维度”:不同时间内使用不同的参数运行系统,“空间维度”在同一时间让不同的应用实例节点采用不同的参数运行。

支持不同研发环境不同配参,如开发、测试、准生产。

简化不同实例节点共同参数、以及特定参数的配置(假设多数相同、少量不同)。

提供的主要能力如下:

为所有实例节点提供名字为default的默认profile,但优先级最低;

基于启动参数指定应用节点生效的profiles,可以多个;

同一个配参在多个profile中存在,后序定义覆盖前序定义;

内置特定名字$node的profile,代表为每个实例节点启动节点专用的profile;

支持自定义动态profile定义。

profiles配置

easyconfig.profiles.active用于指定easyconfig为应用节点启动哪些配置环境。可指定多个,多个profile中有相同配参时,后序profile定义优先于前序profile中定义。

easyconfig.profiles.active=prod,$node

profile指一组配参代表的环境,环境与配参可以任意组合;

default为内置的默认profile,无需指定自动存在且自动生效,优先级最低;也就是说default是默认的第一个active的profile。

$node环境

$node是另一个特殊的profile,指应用实例节点专属profile,每启动一个应用实例,就会基于此节点的ip/port构建一个专属profile。

有了$node,就可以方便地为每一个节点配置不同的参数。

$node并不会自动启用,需要启动应用时,通过easyconfig.profiles.active明确指定才能激活。

客户化环境

easyconfig还提供了类似于 $node环境动态扩展机制。

前端页面访问配参

前后台分离架构,前端也有配参支持的的需求,easyconfig对前端配参提供简单支持。

通过以下属性开启或关闭前端访问,默认为开启。

easyconfig.frontend.enabled=false

访问路径方式:

方法:GET

路径:/${easyconfig-admin}/get-prop

参数1:code为配参属性代码

返回值:JSON结构 {"error":0,"data":"..."}。error不为0则表示错误,为0正常返回,此时data是对应配参的文本数值。

前端访问配参支持相对简单许多,这是考虑到:

使用场景较少,历史负担小;

保障数据安全,不易提供批量访问接口;

复杂配参选用JSON格式支持,无需要访问多次多个配参;

网络访问的延迟、性能问题,不易大规模使用;

配参的加密及访问

配参经常涉及一些敏感信息,如网络地址、用户名、密码等,提高它们的安全必要的,避免不恰当管理造成泄露。easyconfig对此提供安全增强,基于实际情况,选用下面任意三种方式。

管理控制台管理加密

在管理控制台设置配参加密保存,系统自动得到解密后的明文信息。详情请参考用户手册

属性文件管理加密

对于一些基础的,在easyconfig启动之前就需要的配参,例如数据库连接,提供另一种加密设置方式。如下示例:

spring.datasource.url=qTe2SoazP1vp918zK6U7xweHgfJR+OzZhewQfC32LGUST1rNIoCI4pmbeJgW03azWIkTudMU8yS53a03daznZFw++Sc1jcN9nXYWWgQGh4tHhJ47HAlsvz7wXZJ51I4588j/r7x6phnI10hERXgqP9MjU473MeoiIKKny3W28lo1aPCSU4RlW3yNwGGgbludspring.datasource.password=9SZS6s/utwXZtFhOAe3ASg==easyconfig.cipher-keys=spring.datasource.url,spring.datasource.password

说明:

把加密的属性key以逗号分隔的形式设置到easyconfig.cipher-keys。

用密文设置相应属性值,系统得到明文数值,即使Spring使用的参数也是如此。

密文通过管理控制台 或 jar命令行 中的加解密工具生成,请参考用户手册

如果是数字等类型的属性配参,加密之后就变成文本了,编辑器会提示错误,但不影响使用。

程序编码管理加密

easyconfig.CfgHelper类中提供了两个加解密的辅助方法,研发同学可以编码的方式灵活处理加解密。

publicstaticStringencrypt(Stringcontent)...publicstaticStringdecrypt(Stringcontent)...

加密安全与强度考量

easyconfig目的是对配参安全进行增强,达到一种合理水平,而不追求更高安全。实际上由于easyconfig被设计成组件,对于对源码进行反编译的专业破解者来讲,安全性是不够的。

easyconfig采用AES加密算法进行,系统可以指定一个salt来提升及定置化加密,如下:

easyconfig.cipher-salt=myPrivateToken

注意:easyconfig.cipher-salt设置后不能修改,否则加密的配参就不能解密。更换salt需要手工把所有密文改为明后再进行。

系统的鲁棒性保护

默认值、属性文件、本地缓存、配参数据源的多层次支持

@Value标注可以指定默认值,例如${server.port:8080}表示属性server.port没有配置时,默认值为8080。本地的properties文件或yml文件对相关属性进行配置,就会覆盖@Value的默认值。

easyconfig追加了另外两套机制:本地缓存和配参数据源。本地缓存是系统每隔一定时间缓存下来的配参数据源数据,当系统启动时配参数据源不可用时,本地缓存会的优先级会高于本地属性文件。本地缓存默认开启,但可以关闭,如下:

easyconfig.localcache.enabled=false

配参数据源是动态配参管理的数据源,通过它集中管理系统的各个运行实例的配参,动态修改和更新,而不用重新启动服务。

这几套机制结合起来使用,避免环境短期故障导致对用户的服务中断,或者导致研发工作的中断,毕竟配参是系统运行的基础和前提,任何中断都是麻烦。

服务/网络中断及恢复

系统启动时,easyconfig会检查配参数据源,从数据源获取最新的参数。如果失败的话,可以启动成功,在配参会保留到本地缓存状态。

系统运行过程中,数据源或者redis服务中断,easyconfig会继续按照约定周期检查更新,服务一旦恢复,easyconfig会随即恢复。

单记录错误及容忍

easyconfig 努力避免单记录错误对整体服务造成影响,让单条配参记录错误影响限制在单条范围内。

减少人工维护内容以减少出错几率

通过配参的格式推导、数据校验、配参探查能减少出错机会。

### 通过变更日志及恢复减小错误后果

多种变更恢复机制,保障配参维护错误能快速恢复到正确状态上。

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

推荐阅读更多精彩内容