hibernate Validator 使用介绍

hibernate Validator 使用介绍

1.hibernate Validator 简介

平时项目中,难免需要对参数 进行一些参数正确性的校验,这些校验出现在业务代码中,让我们的业务代码显得臃肿,而且,频繁的编写这类参数校验代码很无聊。鉴于此,觉得 Hibernate Validator 框架刚好解决了这些问题,可以很优雅的方式实现参数的校验,让业务代码 和 校验逻辑 分开,不再编写重复的校验逻辑。
Hibernate Validator 是 Bean Validation 的参考实现 . Hibernate Validator 提供了 JSR 303 规范中所有内置 constraint 的实现,除此之外还有一些附加的 constraint。
Bean Validation 为 JavaBean 验证定义了相应的元数据模型和API。缺省的元数据是 Java Annotations,通过使用 XML 可以对原有的元数据信息进行覆盖和扩展。Bean Validation 是一个运行时的数据验证框架,在验证之后验证的错误信息会被马上返回。

2.Hibernate Validator 的作用

  • 验证逻辑与业务逻辑之间进行了分离,降低了程序耦合度;
  • 统一且规范的验证方式,无需你再次编写重复的验证代码;
  • 你将更专注于你的业务,将这些繁琐的事情统统丢在一边。

3.Hibernate Validator 校验Demo

hibernate validator(官方文档)提供了一套比较完善、便捷的验证实现方式。

spring-boot-starter-web包里面有hibernate-validator包,不需要引用hibernate validator依赖。

package top.zjzcn.model;

import lombok.Data;

import javax.validation.constraints.AssertFalse;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;

/**
 * @author zhang.junze
 **/
@Data
public class DemoModel1 {
    @NotBlank(message = "用户名不能为空")
    private String userName;

    @NotBlank(message = "年龄不能为空")
    @Pattern(regexp = "^[0-9]{1,2}$", message = "年龄不正确")
    private String age;

    @AssertFalse(message = "必须为false")
    private Boolean isFalse;
    /**
     * 如果是空,则不校验,如果不为空,则校验
     */
    @Pattern(regexp = "^[0-9]{4}-[0-9]{2}-[0-9]{2}$", message = "出生日期格式不正确")
    private String birthday;
}

package top.zjzcn.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import top.zjzcn.model.DemoModel1;

import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;

/**
 * @author zhang.junze
 **/
@RestController
@RequestMapping("validate")
@Slf4j
public class ValidateController {

    //如果成功返回success,不成功返回错误信息
    @GetMapping("/demo1")
    public void testDemo1(@RequestBody @Valid DemoModel1 model1, BindingResult result){
       if(result.hasErrors()){
            for (ObjectError error : result.getAllErrors()) {
                System.out.println(error.getDefaultMessage());
            }
        }
    }
}

POST请求传入的参数{}

[用户名不能为空, 年龄不能为空]

POST请求传入的参数{"userName":"zhangjz","age":120,"isFalse":true,"birthday":"21010-21-12"}

[出生日期格式不正确, 年龄不正确, 必须为false]

4.Hibernate Validator 的校验模式

Hibernate Validator有以下两种验证模式:

  1. 普通模式(默认是这个模式)

普通模式(会校验完所有的属性,然后返回所有的验证失败信息)

  1. 快速失败返回模式

快速失败返回模式(只要有一个验证失败,则返回)

两种验证模式配置方式:(参考官方文档

failFast:true 快速失败返回模式 false 普通模式

ValidatorFactory factory = Validation.byProvider(HibernateValidator.class)
                .configure()
                .addProperty("hibernate.validator.fail_fast", "false")
                .buildValidatorFactory();
        return factory.getValidator();

和 (hibernate.validator.fail_fast:true 快速失败返回模式 false 普通模式)

ValidatorFactory factory = Validation.byProvider(HibernateValidator.class)
               .configure()
               .addProperty("hibernate.validator.fail_fast", "true")
               .buildValidatorFactory();
       return factory.getValidator();

配置hibernate Validator为快速失败返回模式:

package top.zjzcn.config;


import org.hibernate.validator.HibernateValidator;
import org.springframework.context.annotation.Bean;

import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;


/**
 * @author zhang.junze
 **/
public class ValidatorConfig {

    @Bean
    public Validator validator() {

        ValidatorFactory factory = Validation.byProvider(HibernateValidator.class)
                .configure()
                // 将fail_fast设置为true即可,如果想验证全部,则设置为false或者取消配置即可
                .addProperty("hibernate.validator.fail_fast", "true")
                .buildValidatorFactory();
        return factory.getValidator();

    }
}

5.Hibernate Validator简单实践

以下为最简使用方式

  1. 全局异常类
package top.zjzcn.config;

@Slf4j
@RestControllerAdvice
@Order(value = Ordered.HIGHEST_PRECEDENCE)
public class GlobalExceptionHandler {
    /**
     * 统一处理请求参数校验(实体对象传参)
     *
     * @param e BindException
     * @return FebsResponse
     */
    @ExceptionHandler(BindException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public String validExceptionHandler(BindException e) {
      
        List<String> errorInformation = e.getBindingResult().getAllErrors()
                .stream()
                .map(ObjectError::getDefaultMessage).collect(Collectors.toList());
        return errorInformation.toString();

    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public String validExceptionHandler(MethodArgumentNotValidException e) {

        List<String> errorInformation = e.getBindingResult().getAllErrors()
                .stream()
                .map(ObjectError::getDefaultMessage).collect(Collectors.toList());
        return errorInformation.toString();

    }

    /**
     * 统一处理请求参数校验(普通传参)
     *
     * @param e ConstraintViolationException
     * @return FebsResponse
     */
    @ExceptionHandler(value = ConstraintViolationException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public String handleConstraintViolationException(ConstraintViolationException e) {

        List<String> errorInformation = e.getConstraintViolations()
                .stream()
                .map(ConstraintViolation::getMessage)
                .collect(Collectors.toList());
        return errorInformation.toString();
    }
}


  1. 校验模式配置类
@Configuration
public class ValidatorConfig {

    @Bean
    public Validator validator() {

        ValidatorFactory factory = Validation.byProvider(HibernateValidator.class)
                .configure()
                // 将fail_fast设置为true即可,如果想验证全部,则设置为false或者取消配置即可
                .addProperty("hibernate.validator.fail_fast", "true")
                .buildValidatorFactory();
        return factory.getValidator();

    }
}

  1. Model类
@Data
public class DemoModel1 {
    @NotBlank(message = "用户名不能为空")
    private String userName;

    @NotBlank(message = "年龄不能为空")
    @Pattern(regexp = "^[0-9]{1,2}$", message = "年龄不正确")
    private String age;

    @AssertFalse(message = "必须为false")
    private Boolean isFalse;
    /**
     * 如果是空,则不校验,如果不为空,则校验
     */
    @Pattern(regexp = "^[0-9]{4}-[0-9]{2}-[0-9]{2}$", message = "出生日期格式不正确")
    private String birthday;
}

  1. Controller类
@RestController
@RequestMapping("validate")
@Validated
@Slf4j
public class ValidateController {

    //如果成功返回success,不成功返回错误信息
    @PostMapping("/demo1")
    public String testDemo1(@RequestBody @Valid DemoModel1 model1, BindingResult result){
        if(result.hasErrors()){
            for (ObjectError error : result.getAllErrors()) {
                System.out.println(error.getDefaultMessage());
            }
        }
        return "success";
    }

    @GetMapping()
    public String getName(@NotBlank(message = "不能为空") String name, @Size(min=5,max = 10,message = "有效长度{min}到{max}个字符")String password){
        log.info("进来了==="+name+"=="+password);
        return name+"=="+password;
    }
}

6.自定义Constraint的实现

有时候框架提供的校验注解(constraint)并不能满足我们的需求,所以需要自定义自己的校验规则。满足自己的校验需求。

比如我希望的时间格式是:yyyy-MM-dd HH:mm:ss,就需要字符串格式能匹配这个格式。于是自己就实现了一个自定义注解来实现校验。

6.1 编写constraint

  1. 实现类必须实现接口ConstraintValidator
  2. 注解必须有@Constraint(validatedBy = {******.class}) 注解标注,validateBy 的值就是校验逻辑的实现类
  3. 自定义注解必须包含message ,groups,payload 属性

import org.apache.commons.lang3.time.DateUtils;

import javax.validation.Constraint;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import javax.validation.Payload;
import java.lang.annotation.*;
import java.text.ParseException;
import java.util.Date;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = {DateValidator.DateValidatorInner.class})
public @interface DateValidator {
    /**
     * 必须的属性
     * 显示 校验信息
     * 利用 {} 获取 属性值,参考了官方的message编写方式
     *
     * @see org.hibernate.validator 静态资源包里面 message 编写方式
     */
    String message() default "日期格式不匹配{dateFormat}";

    /**
     * 必须的属性
     * 用于分组校验
     */
    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    /**
     * 非必须
     */
    String dateFormat() default "yyyy-MM-dd HH:mm:ss";

    /**
     * 必须要实现的接口
     */
    class DateValidatorInner implements ConstraintValidator<DateValidator, String> {
        private String dateFormat;

        /**
         * 初始化内容
         * @param dateValidator
         */
        @Override
        public void initialize(DateValidator dateValidator) {
            this.dateFormat = dateValidator.dateFormat();
        }

        /**
         * 校验逻辑的实现
         *
         * @param value 需要校验的 值
         * @return 布尔值结果
         */
        @Override
        public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
            if (value == null) {
                return true;
            }
            if ("".equals(value)) {
                return true;
            }
            try {
                Date date = DateUtils.parseDate(value, dateFormat);
                return date != null;
            } catch (ParseException e) {
                return false;
            }
        }
    }
}

4.2 测试自定义注解

  • 在类DemoModel1中新增验证属性
import lombok.Data;
import top.zjzcn.common.constraint.DateValidator;

import javax.validation.constraints.AssertFalse;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;

/**
 * @author zhang.junze
 **/
@Data
public class DemoModel1 {
    @NotBlank(message = "{required}")
    private String userName;

    @NotBlank(message = "{required}")
    @Pattern(regexp = "^[0-9]{1,2}$", message = "{age}")
    private String age;

    @AssertFalse(message = "必须为false")
    private Boolean isFalse;
    /**
     * 如果是空,则不校验,如果不为空,则校验
     */
    @DateValidator //此为新增
    @Pattern(regexp = "^[0-9]{4}-[0-9]{2}-[0-9]{2}$", message = "{date}")
    private String birthday;
}
  • 测试代码
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import top.zjzcn.model.DemoModel1;
import top.zjzcn.utils.ValidationUtil;

@SpringBootTest
class HibernateValidatorDemoApplicationTests {

    @Test
    void contextLoads() {
        DemoModel1 demoModel1 = new DemoModel1();
        demoModel1.setUserName("zhangjz");
        demoModel1.setAge("120");
        demoModel1.setIsFalse(true);
        demoModel1.setBirthday("201010-21-12");
        ValidationUtil.ValidResult validResult = ValidationUtil.validateBean(demoModel1);
        if(validResult.hasErrors()){
            String errors = validResult.getErrors();
            System.out.println(errors);
        }
    }

}

  • 测试结果

    age:年龄不正确 isFalse:必须为false birthday:日期格式不正确 birthday:日期格式不匹配yyyy-MM-dd HH:mm:ss 
    

7.分组

添加校验注解的方式固然很方便,但是如果同一验证对象(eg:DemoModel) 在不同的业务中校验规则不同的话,难道我们需要编写两个验证对象么?答案肯定不是,我们可以使用分组校验,对不同的校验规则进行隔离校验,互相不受影响。

7.1 DemoModel添加多套校验规则

可以自己定义空的class对象当作group。如果不指定groups 那么就是默认的即为Default.class 分组。

import lombok.Data;
import top.zjzcn.common.constraint.DateValidator;
import top.zjzcn.common.groups.AddGroup;

import javax.validation.constraints.AssertFalse;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;

/**
 * @author zhang.junze
 **/
@Data
public class DemoModel1 {
    @NotBlank(message = "{required}")
    private String userName;

    @NotBlank(message = "{required}")
    @Pattern(regexp = "^[0-9]{1,2}$", message = "{age}",groups = {AddGroup.class})
    private String age;

    @AssertFalse(message = "必须为false",groups = {AddGroup.class})
    private Boolean isFalse;
    /**
     * 如果是空,则不校验,如果不为空,则校验
     */
    @DateValidator
    @Pattern(regexp = "^[0-9]{4}-[0-9]{2}-[0-9]{2}$", message = "{date}")
    private String birthday;
}

7.2 测试分组校验

  • 测试代码
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import top.zjzcn.common.groups.AddGroup;
import top.zjzcn.model.DemoModel1;
import top.zjzcn.utils.ValidationUtil;

@SpringBootTest
class HibernateValidatorDemoApplicationTests {

    @Test
    void contextLoads() {
        DemoModel1 demoModel1 = new DemoModel1();
        demoModel1.setUserName("zhangjz");
        demoModel1.setAge("120");
        demoModel1.setIsFalse(true);
        demoModel1.setBirthday("201010-21-12");
        ValidationUtil.ValidResult validResult = ValidationUtil.validateBean(demoModel1, AddGroup.class);
        if(validResult.hasErrors()){
            String errors = validResult.getErrors();
            System.out.println(errors);
        }
    }

}

  • 测试结果

由于age与isFalse指定分组,所以只会校验该属性

isFalse:必须为false age:年龄不正确 

8. ValidationMessages.properties属性文件使用

Spring boot 表单验证 @Validated 的 message 国际化资源文件默认必须放在resources/ValidationMessages.properties

如果需要更改资源文件地址,需要进行如下配置

@Configuration
public class ValidationConfig {

    @Bean
    public Validator getValidator() {
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        messageSource.setBasename("资源文件所在位置");

        LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
        validator.setValidationMessageSource(messageSource);

        return validator;
    }

}

9.使用封装工具类代码中校验

  • validationUtil 工具类
package top.zjzcn.utils;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;

import lombok.Data;
import org.hibernate.validator.HibernateValidator;

/**
 * @author zhang.junze
 * @description 校验工具类
 * @date 2020/3/10
 **/
public class ValidationUtil {

    /**
     * 开启快速结束模式 failFast (true)
     */
    private static Validator validator = Validation.byProvider(HibernateValidator.class).configure().failFast(false).buildValidatorFactory().getValidator();

    /**
     * 校验对象
     *
     * @param t      bean
     * @param groups 校验组
     * @return ValidResult
     */
    public static <T> ValidResult validateBean(T t, Class<?>... groups) {
        ValidResult result = new ValidationUtil().new ValidResult();
        Set<ConstraintViolation<T>> violationSet = validator.validate(t, groups);
        boolean hasError = violationSet != null && violationSet.size() > 0;
        result.setHasErrors(hasError);
        if (hasError) {
            for (ConstraintViolation<T> violation : violationSet) {
                result.addError(violation.getPropertyPath().toString(), violation.getMessage());
            }
        }
        return result;
    }

    /**
     * 校验bean的某一个属性
     *
     * @param obj          bean
     * @param propertyName 属性名称
     * @return ValidResult
     */
    public static <T> ValidResult validateProperty(T obj, String propertyName) {
        ValidResult result = new ValidationUtil().new ValidResult();
        Set<ConstraintViolation<T>> violationSet = validator.validateProperty(obj, propertyName);
        boolean hasError = violationSet != null && violationSet.size() > 0;
        result.setHasErrors(hasError);
        if (hasError) {
            for (ConstraintViolation<T> violation : violationSet) {
                result.addError(propertyName, violation.getMessage());
            }
        }
        return result;
    }

    /**
     * 校验结果类
     */
    @Data
    public class ValidResult {

        /**
         * 是否有错误
         */
        private boolean hasErrors;

        /**
         * 错误信息
         */
        private List<ErrorMessage> errors;

        public ValidResult() {
            this.errors = new ArrayList<>();
        }

        public boolean hasErrors() {
            return hasErrors;
        }

        public void setHasErrors(boolean hasErrors) {
            this.hasErrors = hasErrors;
        }

        /**
         * 获取所有验证信息
         *
         * @return 集合形式
         */
        public List<ErrorMessage> getAllErrors() {
            return errors;
        }

        /**
         * 获取所有验证信息
         *
         * @return 字符串形式
         */
        public String getErrors() {
            StringBuilder sb = new StringBuilder();
            for (ErrorMessage error : errors) {
                sb.append(error.getPropertyPath()).append(":").append(error.getMessage()).append(" ");
            }
            return sb.toString();
        }

        public void addError(String propertyName, String message) {
            this.errors.add(new ErrorMessage(propertyName, message));
        }
    }

    @Data
    public class ErrorMessage {

        private String propertyPath;

        private String message;

        public ErrorMessage() {
        }

        public ErrorMessage(String propertyPath, String message) {
            this.propertyPath = propertyPath;
            this.message = message;
        }
    }
}

  • 测试代码
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import top.zjzcn.model.DemoModel1;
import top.zjzcn.utils.ValidationUtil;

@SpringBootTest
class HibernateValidatorDemoApplicationTests {

    @Test
    void contextLoads() {
        DemoModel1 demoModel1 = new DemoModel1();
        demoModel1.setUserName("zhangjz");
        demoModel1.setAge("120");
        demoModel1.setIsFalse(true);
        demoModel1.setBirthday("201010-21-12");
        ValidationUtil.ValidResult validResult = ValidationUtil.validateBean(demoModel1);
        if(validResult.hasErrors()){
            String errors = validResult.getErrors();
            System.out.println(errors);
        }
    }

}

10.常用的注解

Bean Validation 中内置的 constraint
@Null 被注释的元素必须为 null
@NotNull 被注释的元素必须不为 null
@AssertTrue 被注释的元素必须为 true
@AssertFalse 被注释的元素必须为 false
@Min(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@Max(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@DecimalMin(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@DecimalMax(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@Size(max=, min=) 被注释的元素的大小必须在指定的范围内
@Digits (integer, fraction) 被注释的元素必须是一个数字,其值必须在可接受的范围内
@Past 被注释的元素必须是一个过去的日期
@Future 被注释的元素必须是一个将来的日期
@Pattern(regex=,flag=) 被注释的元素必须符合指定的正则表达式
Hibernate Validator 附加的 constraint
@NotBlank(message =) 验证字符串非null,且长度必须大于0
@Email 被注释的元素必须是电子邮箱地址
@Length(min=,max=) 被注释的字符串的大小必须在指定的范围内
@NotEmpty 被注释的字符串的必须非空
@Range(min=,max=,message=) 被注释的元素必须在合适的范围内

//大于0.01,不包含0.01
@NotNull
@DecimalMin(value = "0.01", inclusive = false)
private Integer greaterThan;

//大于等于0.01
@NotNull
@DecimalMin(value = "0.01", inclusive = true)
private BigDecimal greatOrEqualThan;

@Length(min = 1, max = 20, message = "message不能为空")
//不能将Length错用成Range
//@Range(min = 1, max = 20, message = "message不能为空")
private String message;

参考:

https://www.cnblogs.com/mr-yang-localhost/p/7812038.html#_label4

https://www.jianshu.com/p/0bfe2318814f

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

推荐阅读更多精彩内容