2021-08-31自定義枚舉序列化器

源碼 https://gitee.com/eric-tutorial/SpringCloud-multiple-gradle
本文內(nèi)容是:SpringBoot+Mybatisplus中枚舉正反序列化的實際應(yīng)用

本文基于SpringBoot+Mybatisplus 框架就Java枚舉的正反序列化的實際應(yīng)用進(jìn)行一次分析與研究,此外順便帶上DAO層關(guān)于枚舉的操作,使得程序中完全使用枚舉編程。由于SpringBoot內(nèi)置的json處理器是jackson,所以本文的json相關(guān)處理也就是采用默認(rèn)的jackson。

背景

N久之前,leo曾經(jīng)問我枚舉的應(yīng)用,我清楚地記得菜鳥教程(https://www.runoob.com/)上面有這樣一段話。

image.png

當(dāng)時我還找到了給leo看,說這玩意要被取代了.現(xiàn)在看來是我斷章取義了。因為那時候很少會接觸到枚舉,所以我以為這玩意真的沒救了。

現(xiàn)在看來,看多了不去實踐與思考,正應(yīng)了一句話“盡信書不如無書”。

最近《阿里巴巴開發(fā)規(guī)范-嵩山版版》有下面的一句話,可能會與接下來的內(nèi)容相沖。這里為啥不能返回枚舉類型?大概率因為集團內(nèi)部的RPC調(diào)用的時候,版本升級無法正確兼容。

image.png

無論枚舉要怎么使用,我還是按照自己的相關(guān)需求來實踐了一把,由于項目中有很多枚舉,使用和管理起來非常暈乎乎的。需要把枚舉與Integer轉(zhuǎn)來轉(zhuǎn)去,前端傳輸過來了一個Integer,需要手動將Integer轉(zhuǎn)成枚舉,存儲到數(shù)據(jù)庫的時候,又得將枚舉轉(zhuǎn)成Integer保存。如果純粹使用Integer傳值,編碼又不能知道這個數(shù)字代表啥意思,最后找來找去。不光是后端很是暈乎乎的。前端由于也只接受了Integer,需要顯示文字的時候,只能前后端共同定,一旦后端修改了枚舉,那么前端必須同步修改。所以我在網(wǎng)上找了一些解決辦法,但是都不盡人意。最后折騰了jackson源碼并求助于jackson的維護者解決了枚舉正反序列化的問題。

先看下一般工程的基本模型

image.png

本文的重點是枚舉的正反序列化,但是為了讓整個枚舉在工程中的應(yīng)用比較完整,也會描述下枚舉在DAO層的操作。jackson的正反序列化主要應(yīng)用在Controller層的參數(shù)接收與結(jié)果返回。在參數(shù)接收的時候有兩種形式,一種的前端通過表單提交的數(shù)據(jù),另一種是從body提交的json數(shù)據(jù),兩種有很大的區(qū)別,在Controller的方法里面主要體現(xiàn)在body提交的json數(shù)據(jù)需要在對象前面加上@RequestBody.當(dāng)然兩者本質(zhì)上有點區(qū)別,由于表單提交的不是json,所以無法采用json反序列化,但是本文中會順帶描述到表單提交的數(shù)據(jù)如何轉(zhuǎn)換成枚舉。

show you code

工程源代碼

https://gitee.com/eric-tutorial/SpringCloud-multiple-gradle

篇幅有限,只講述重點代碼邏輯,完整的可以參考源代碼。項目基于Gradle構(gòu)建.

定義枚舉

public enum GenderEnum  {
    BOY(100, "男"), GIRL(200, "女"),UNKNOWN(0, "未知");
    private final Integer code;
    private final String description;
    GenderEnum(int code, String description) {
        this.code = code;
        this.description = description;
    }
}

接受參數(shù)的對象

@Data
public class UserParam {
    @NotBlank(message = "name不能為空")
    String name;
    @NotNull(message = "gender為100或者200")
    GenderEnum gender;
    @NotNull(message = "age不能為空")
    Integer age;
}

Controller POST方法

    @PostMapping("add/body")
    public BaseResponseVO saveBody(@Valid @RequestBody UserParam userParam) {
        UserModel userModel = userService.add(userParam);
        return BaseResponseVO.success(userModel);
    }

上面代碼可以看出來框架在接受參數(shù)的時候?qū)⒕W(wǎng)絡(luò)傳輸過來的數(shù)據(jù)進(jìn)行了反序列化,在返回給前端的時候進(jìn)行了正序列化成json返回的。默認(rèn)的jackson是無法直接按照GenderEnum中的code來正反序列化枚舉的,因為jackson有一套自己的枚舉序列化機制,從源代碼中看出來,它是按照name和ordinal來正反序列化的。但是這個不能滿足我自己定義的code和description來正反序列化的需求。因此我在網(wǎng)上搜了下,看看有木有人完成這樣的需求,我想這個需求應(yīng)該比較正常,網(wǎng)上一搜果然有很多。很快就有了下面的代碼(最后發(fā)現(xiàn)都是采用默認(rèn)的jackson枚舉正反序列化器,并不滿足需求)。

自定義的枚舉序列化器

面向接口編程

為我需要正序列化的枚舉統(tǒng)一定義了一個接口.所以需要參與正序列化的枚舉都得實現(xiàn)這個接口.

public interface BaseEnum {
    /**
     * Code integer.
     *
     * @return the integer
     */
    Integer code();
    /**
     * Description string.
     *
     * @return the string
     */
    String description();
}

正序列化器

@Slf4j
public class BaseEnumSerializer extends JsonSerializer<BaseEnum> {
    @Override
    public void serialize(BaseEnum value, JsonGenerator gen, SerializerProvider serializerProvider) throws IOException {
        log.info("\n====>開始序列化[{}]", value);
        gen.writeStartObject();
        gen.writeNumberField("code", value.code());
        gen.writeStringField("description", value.description());
        gen.writeEndObject();
    }
}

效果就是既返回code和description,前端既知道code也知道description.description可以直接顯示,code可以用來返回給后端的操作.前端再也不用同步修改description了,也不需要自己判斷code是啥意思,直接顯示description即可.皆大歡喜.

反序列化器

@Slf4j
public class BaseEnumDeserializer extends JsonDeserializer<BaseEnum> {
    @Override
    public BaseEnum deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
        try {
            //前端輸入的值
            String inputParameter = p.getText();
            if (StringUtils.isBlank(inputParameter)) {
                return null;
            }
            JsonStreamContext parsingContext = p.getParsingContext();
            String currentName = parsingContext.getCurrentName();//字段名
            Object currentValue = parsingContext.getCurrentValue();//前端注入的對象(ResDTO)
            Field field = ReflectionUtils.getField(currentValue.getClass(), currentName);               // 通過對象和屬性名獲取屬性的類型
            // 獲取對應(yīng)得枚舉類
            Class enumClass = field.getType();
            // 根據(jù)對應(yīng)的值和枚舉類獲取相應(yīng)的枚舉值
            BaseEnum anEnum = DefaultInputJsonToEnum.getEnum(inputParameter, enumClass);
            log.info("\n====>測試反序列化枚舉[{}]==>[{}.{}]", inputParameter, anEnum.getClass(), anEnum);
            return anEnum;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

效果就是反序列化器用來解決參數(shù)接受的時候,將前端傳過來的code轉(zhuǎn)成Enum.方便枚舉在程序中的操作,降低程序的復(fù)雜度,使編碼更加簡單,代碼清晰明了.

注入到SpringBoot框架中

    @Bean
    public Jackson2ObjectMapperBuilderCustomizer enumCustomizer() {
//        將枚舉轉(zhuǎn)成json返回給前端
        return jacksonObjectMapperBuilder -> {
//            自定義序列化器注入
            Map<Class<?>, JsonSerializer<?>> serializers = new LinkedHashMap<>();
            serializers.put(BaseEnum.class, new BaseEnumSerializer());
            jacksonObjectMapperBuilder.serializersByType(serializers);
//            自定義反序列化器注入,這里的注入貌似效果不行
            Map<Class<?>, JsonDeserializer<?>> deserializers = new LinkedHashMap<>();
            deserializers.put(BaseEnum.class, new BaseEnumDeserializer());
            jacksonObjectMapperBuilder.deserializersByType(deserializers);
        };
    }

經(jīng)過測試,枚舉序列化后返回到前端的效果如下,與期望的效果一致,這樣的好處就是前端不需要管數(shù)字是啥意思,直接顯示description即可,無論后端枚舉是否修改,前端都不需要關(guān)心了。


image.png

經(jīng)過反復(fù)測試與人分享成果的時候,發(fā)現(xiàn)一個非常嚴(yán)重的問題,雖然前端接收參數(shù)的時候也可以反序列化成枚舉,但是實際上沒有按照code來反序列化。最后只能把jackson的源代碼拉下來調(diào)試,經(jīng)過調(diào)試發(fā)現(xiàn),jackson反序列化的時候一直使用的是默認(rèn)的枚舉反序列化器,并沒有使用自定義枚舉反序列化器。

com.fasterxml.jackson.databind.deser.BasicDeserializerFactory#createEnumDeserializer

  /**
     * Factory method for constructing serializers of {@link Enum} types.
     */
    @Override
    public JsonDeserializer<?> createEnumDeserializer(DeserializationContext ctxt,
            JavaType type, BeanDescription beanDesc)
        throws JsonMappingException
    {
        final DeserializationConfig config = ctxt.getConfig();
        final Class<?> enumClass = type.getRawClass();
        // 23-Nov-2010, tatu: Custom deserializer?
        JsonDeserializer<?> deser = _findCustomEnumDeserializer(enumClass, config, beanDesc);
        if (deser == null) {
            // 12-Feb-2020, tatu: while we can't really create real deserializer for `Enum.class`,
            //    it is necessary to allow it in one specific case: see [databind#2605] for details
            //    but basically it can be used as polymorphic base.
            //    We could check `type.getTypeHandler()` to look for that case but seems like we
            //    may as well simply create placeholder (AbstractDeserializer) regardless
            if (enumClass == Enum.class) {
                return AbstractDeserializer.constructForNonPOJO(beanDesc);
            }
            ValueInstantiator valueInstantiator = _constructDefaultValueInstantiator(ctxt, beanDesc);
            SettableBeanProperty[] creatorProps = (valueInstantiator == null) ? null
                    : valueInstantiator.getFromObjectArguments(ctxt.getConfig());
            // May have @JsonCreator for static factory method:
            for (AnnotatedMethod factory : beanDesc.getFactoryMethods()) {
                if (_hasCreatorAnnotation(ctxt, factory)) {
                    if (factory.getParameterCount() == 0) { // [databind#960]
                        deser = EnumDeserializer.deserializerForNoArgsCreator(config, enumClass, factory);
                        break;
                    }
                    Class<?> returnType = factory.getRawReturnType();
                    // usually should be class, but may be just plain Enum<?> (for Enum.valueOf()?)
                    if (returnType.isAssignableFrom(enumClass)) {
                        deser = EnumDeserializer.deserializerForCreator(config, enumClass, factory, valueInstantiator, creatorProps);
                        break;
                    }
                }
            }
            // Need to consider @JsonValue if one found
            if (deser == null) {
                deser = new EnumDeserializer(constructEnumResolver(enumClass,
                        config, beanDesc.findJsonValueAccessor()),
                        config.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS));
            }
        }
        // and then post-process it too
        if (_factoryConfig.hasDeserializerModifiers()) {
            for (BeanDeserializerModifier mod : _factoryConfig.deserializerModifiers()) {
                deser = mod.modifyEnumDeserializer(config, type, beanDesc, deser);
            }
        }
        return deser;
    }

從上面可以看出來枚舉反系列化器是怎么找到的.仔細(xì)閱讀后發(fā)現(xiàn),上面并沒有按照接口 BaseEnum 來查找反序列化器,這也是為啥自定義的反序列化器沒有生效的原因.

既然我發(fā)現(xiàn)了這個問題,我直接在github拉下來了jackson代碼,然后修改成按照接口查找自定義反序列化器的方式提交了我的代碼.于是下面的代碼就來了

    List<JavaType> interfaces = type.getInterfaces();
    for (JavaType javaType : interfaces) {
        Class<?> rawClass = javaType.getRawClass();
        deser = _findCustomEnumDeserializer(rawClass, config, beanDesc);
        if (deser != null) {
            return deser;
        }
    }

pull request之后,管理者很快給我回復(fù)了。我們來回扯了幾個回合之后,我們得到一個更加合理的解決辦法. 這個問題,這個也是本文的重點。就是重寫查找枚舉反序列化器的方法,把我寫的代碼放在一個重寫類里面即可.

https://github.com/FasterXML/jackson-databind/pull/2842

依據(jù)開閉原則,修改源代碼的事情不太能發(fā)生,管理者說修改違背了原有的思想,所以我的PR最后被我自己關(guān)閉了。

com.fasterxml.jackson.databind.module.SimpleDeserializers#findEnumDeserializer

    @Override
    public JsonDeserializer<?> findEnumDeserializer(Class<?> type,
            DeserializationConfig config, BeanDescription beanDesc)
        throws JsonMappingException
    {
        if (_classMappings == null) {
            return null;
        }
        JsonDeserializer<?> deser = _classMappings.get(new ClassKey(type));
        if (deser == null) {
            // 29-Sep-2019, tatu: Not 100% sure this is workable logic but leaving
            //   as is (wrt [databind#2457]. Probably works ok since this covers direct
            //   sub-classes of `Enum`; but even if custom sub-classes aren't, unlikely
            //   mapping for those ever requested for deserialization
            if (_hasEnumDeserializer && type.isEnum()) {
                deser = _classMappings.get(new ClassKey(Enum.class));
            }
        }
        return deser;
    }

從上面看出來這個就是查找枚舉反序列化器的邏輯,重寫SimpleDeserializers類即可.上面這個代碼是無法按照接口找到反序列化器的,所以重寫它,讓它按照我期望的接口方式找到即可,最后也成功了.

此外還從源碼中分析出來 為啥有的枚舉反序列化就能正常,但是有的不能完成翻序列化。原來默認(rèn)的枚舉反序列化器是按照ordinal來反序列化的,也就是說只有當(dāng)code與ordinal一致的時候就會造成一種假象, 以為是code反序列化來的,其實依舊是ordinal反序列化來的。

從下面代碼中可以看出來,枚舉存儲在數(shù)組中,而ordinal剛好是下標(biāo).

com.fasterxml.jackson.databind.deser.std.EnumDeserializer#deserialize

 @Override
    public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException
    {
        JsonToken curr = p.currentToken();
        // Usually should just get string value:
        if (curr == JsonToken.VALUE_STRING || curr == JsonToken.FIELD_NAME) {
            CompactStringObjectMap lookup = ctxt.isEnabled(DeserializationFeature.READ_ENUMS_USING_TO_STRING)
                    ? _getToStringLookup(ctxt) : _lookupByName;
            final String name = p.getText();
            Object result = lookup.find(name);
            if (result == null) {
                return _deserializeAltString(p, ctxt, lookup, name);
            }
            return result;
        }
        // But let's consider int acceptable as well (if within ordinal range)
        if (curr == JsonToken.VALUE_NUMBER_INT) {
            // ... unless told not to do that
            int index = p.getIntValue();
            if (ctxt.isEnabled(DeserializationFeature.FAIL_ON_NUMBERS_FOR_ENUMS)) {
                return ctxt.handleWeirdNumberValue(_enumClass(), index,
                        "not allowed to deserialize Enum value out of number: disable DeserializationConfig.DeserializationFeature.FAIL_ON_NUMBERS_FOR_ENUMS to allow"
                        );
            }
            if (index >= 0 && index < _enumsByIndex.length) {
                return _enumsByIndex[index];
            }
            if ((_enumDefaultValue != null)
                    && ctxt.isEnabled(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE)) {
                return _enumDefaultValue;
            }
            if (!ctxt.isEnabled(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL)) {
                return ctxt.handleWeirdNumberValue(_enumClass(), index,
                        "index value outside legal index range [0..%s]",
                        _enumsByIndex.length-1);
            }
            return null;
        }
        return _deserializeOther(p, ctxt);
    } 
image.png

image.png
image.png

Java的枚舉本質(zhì)上是java.lang.Enum.class,自帶有ordinal和name兩個屬性。ordinal可以理解成數(shù)組的下標(biāo)。

調(diào)試過程中最讓人百思不得解的是,自定義的正反枚舉序列化器,序列化器是可以按照自己定義的接口來序列化,但是反序列化不行。最后經(jīng)過反復(fù)調(diào)試,發(fā)現(xiàn)正反序列化過程有點區(qū)別,正序列化的時候會找父類找接口,按照父類或者接口定義的序列化器來序列化。而反序列化的時候不會。體會一下,可以理解成一個正序列化的時候,準(zhǔn)確度可以忽略,反正都是丟出去的。但是反序列化的時候必須保證精度,否則無法正確反序列化,那么對應(yīng)的對象無法獲取到正確的值。瞎扯一下.好比,銀行存錢的時候不需要密碼,取錢的時候就需要密碼一樣,看似一個對稱的過程,但是校驗機制還是有點區(qū)別的,可以細(xì)細(xì)體會這種方式的必要性。

重寫SimpleDeserializers的findEnumDeserializer方法

重寫了這個方法之后,把我原本寫在源代碼的邏輯搬出來了,很快就解決了枚舉無法找到自定義反序列化器的問題。

public class SimpleDeserializersWrapper extends SimpleDeserializers {
    static final Logger logger = LoggerFactory.getLogger(SimpleDeserializersWrapper.class);
    @Override
    public JsonDeserializer<?> findEnumDeserializer(Class<?> type, DeserializationConfig config, BeanDescription beanDesc) throws JsonMappingException {
        JsonDeserializer<?> enumDeserializer = super.findEnumDeserializer(type, config, beanDesc);
        if (enumDeserializer != null) {
            return enumDeserializer;
        }
        for (Class<?> typeInterface : type.getInterfaces()) {
            enumDeserializer = this._classMappings.get(new ClassKey(typeInterface));
            if (enumDeserializer != null) {
                logger.info("\n====>重寫枚舉查找邏輯[{}]",enumDeserializer);
                return enumDeserializer;
            }
        }
        return null;
    }
}

換種方式注入到SpringBoot

放棄之前的注入方式,換用新的注入方式向jackson注冊重寫的類SimpleDeserializersWrapper。

  @Bean
    public ObjectMapper objectMapper(Jackson2ObjectMapperBuilder builder) {
        SimpleDeserializersWrapper deserializers = new SimpleDeserializersWrapper();
        deserializers.addDeserializer(BaseEnum.class, new BaseEnumDeserializer());
        SimpleModule simpleModule = new SimpleModule();
        simpleModule.setDeserializers(deserializers);
        simpleModule.addSerializer(BaseEnum.class, new BaseEnumSerializer());
        ObjectMapper objectMapper = builder.createXmlMapper(false).build();
        objectMapper.registerModule(simpleModule);
        return objectMapper;
    }

時間等序列化

一般來說,會在Date上滿加上時間序列化的注解@JsonFormat,但是也可以針對Date自定義正反序列化器,就可以很輕松解決問題。

仔細(xì)閱讀jackson的源代碼你會發(fā)現(xiàn)這個還是里面有很多的默認(rèn)序列化器,用來解決一些常用的類型序列化.

表單提交的數(shù)據(jù)轉(zhuǎn)成枚舉

表單提交的數(shù)據(jù)與jackson沒有關(guān)系,主要與SpringWebMVC有關(guān)系,所以具體可以看工程源代碼,應(yīng)用比較簡單,但是底層原理可以看看Spring源代碼。表單提交的數(shù)據(jù)與jackson沒有關(guān)系,主要與SpringWebMVC有關(guān)系,所以具體可以看工程源代碼,應(yīng)用比較簡單,但是底層原理可以看看Spring源代碼。


image.png

DAO 層處理枚舉存到數(shù)據(jù)庫

具體就是在枚舉的屬性上面上一個注解

   @EnumValue//標(biāo)記數(shù)據(jù)庫存的值是code
   private final Integer code;

此外在yaml配置文件中指定枚舉所在的包。

mybatis-plus:
  type-enums-package: hxy.dream.entity.enums

上面兩步,就是借助mybatis-plus完成了枚舉存儲到數(shù)據(jù)庫,與讀取的時候轉(zhuǎn)換的問題。這個比較簡單,框架也就是做這些事情的,讓開發(fā)者專注于業(yè)務(wù),而不是實現(xiàn)技術(shù)的本身(不是說不要鉆研技術(shù)底層原理)。

參考 mybatis-plus:https://mp.baomidou.com/guide/enum.html

總結(jié)

以上的操作完成了枚舉的從前端接收,反序列化成枚舉對象在程序中表達(dá)。然后再存儲到數(shù)據(jù)庫中。從數(shù)據(jù)庫中取code轉(zhuǎn)成枚舉,在程序中表達(dá),再序列化枚舉后傳輸給前端。一個非常完整的循環(huán),基本上滿足了程序中對枚舉使用的需求。

源碼 https://gitee.com/eric-tutorial/SpringCloud-multiple-gradle

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,546評論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,570評論 3 418
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,505評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,017評論 1 313
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,786評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,219評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,287評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,438評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,971評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,796評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,995評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,540評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,230評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,918評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,697評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,991評論 2 374

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