SpringMVC之类型转换Converter

1. 前言

在以往我们需要SpringMVC为我们自动进行类型转换的时候都是用的PropertyEditor。通过PropertyEditorsetAsText()方法我们可以实现字符串向特定类型的转换。但是这里有一个限制是它只支持从String类型转为其他类型。在Spring3中引入了一个Converter接口,它支持从一个Object转为另一个Object。除了Converter接口之外,实现ConverterFactory接口和GenericConverter接口也可以实现我们自己的类型转换逻辑。

2. Converter接口

我们先来看一下Converter接口的定义:

public interface Converter<S, T> {     
     T convert(S source);  
 }  

我们可以看到这个接口是使用了泛型的,第一个类型表示原类型,第二个类型表示目标类型,然后里面定义了一个convert方法,将原类型对象作为参数传入进行转换之后返回目标类型对象。当我们需要建立自己的converter的时候就可以实现该接口。下面假设有这样一个需求,有一个文章实体,在文章中是可以有附件的,而附件我们需要记录它的请求地址、大小和文件名,所以这个时候文章应该是包含一个附件列表的。在实现的时候我们的附件是实时上传的,上传后由服务端返回对应的附件请求地址、大小和文件名,附件信息不直接存放在数据库中,而是作为文章的属性一起存放在Mongodb中。客户端获取到这些信息以后做一个简单的展示,然后把它们封装成特定格式的字符串作为隐藏域跟随文章一起提交到服务端。在服务端我们就需要把这些字符串附件信息转换为对应的List<Attachment>。所以这个时候我们就建立一个String[]到List<Attachment>的Converter。代码如下:

import java.util.ArrayList;  
import java.util.List;  
   
import org.springframework.core.convert.converter.Converter;  
   
import com.tiantian.blog.model.Attachment;  
   
public class StringArrayToAttachmentList implements Converter<String[], List<Attachment>> {  
   
    @Override  
    public List<Attachment> convert(String[] source) {  
       if (source == null)  
           return null;  
       List<Attachment> attachs = new ArrayList<Attachment>(source.length);  
       Attachment attach = null;  
       for (String attachStr : source) {  
           //这里假设我们的Attachment是以“name,requestUrl,size”的形式拼接的。  
           String[] attachInfos = attachStr.split(",");  
           if (attachInfos.length != 3)//当按逗号分隔的数组长度不为3时就抛一个异常,说明非法操作了。  
              throw new RuntimeException();  
           String name = attachInfos[0];  
           String requestUrl = attachInfos[1];  
           int size;  
           try {  
              size = Integer.parseInt(attachInfos[2]);  
           } catch (NumberFormatException e) {  
              throw new RuntimeException();//这里也要抛一个异常。  
           }  
           attach = new Attachment(name, requestUrl, size);  
           attachs.add(attach);  
       }  
       return attachs;  
    }  
   
}  

3. ConversionService接口

在定义好Converter之后,就是使用Converter了。为了统一调用Converter进行类型转换,Spring为我们提供了一个ConversionService接口。通过实现这个接口我们可以实现自己的Converter调用逻辑。我们先来看一下ConversionService接口的定义:

public interface ConversionService {  
   
    boolean canConvert(Class<?> sourceType, Class<?> targetType);  
   
    <T> T convert(Object source, Class<T> targetType);  
     
    boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType);  
   
    Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);  
   
} 

我们可以看到ConversionService接口里面定义了两个canConvert方法和两个convert方法,canConvert方法用于判断当前的ConversionService是否能够对原类型和目标类型进行转换,convert方法则是用于进行类型转换的。上面出现的参数类型TypeDescriptor是对于一种类型的封装,里面包含该种类型的值、实际类型等等信息。
在定义了ConversionService之后我们就可以把它定义为一个bean对象,然后指定<mvn:annotation-driven/>conversion-service属性为我们自己定义的ConversionService bean对象。如:

<mvc:annotation-driven conversion-service="myConversionService"/>  
  
<bean id="myConversionService" class="com.tiantian.blog.web.converter.support.MyConversionService"/> 

这样当SpringMVC需要进行类型转换的时候就会调用ConversionServicecanConvertconvert方法进行类型转换。 一般而言我们在实现ConversionService接口的时候也会实现ConverterRegistry接口。使用ConverterRegistry可以使我们对类型转换器做一个统一的注册。ConverterRegistry接口的定义如下:

 public interface ConverterRegistry {  
         
        void addConverter(Converter<?, ?> converter);  
       
        void addConverter(GenericConverter converter);  
       
        void addConverterFactory(ConverterFactory<?, ?> converterFactory);  
       
        void removeConvertible(Class<?> sourceType, Class<?> targetType);  
       
    }  

正如前言所说的,要实现自己的类型转换逻辑我们可以实现Converter接口、ConverterFactory接口和GenericConverter接口,ConverterRegistry接口就分别为这三种类型提供了对应的注册方法,至于里面的逻辑就可以发挥自己的设计能力进行设计实现了
对于ConversionService,Spring已经为我们提供了一个实现,它就是GenericConversionService,位于org.springframework.core.convert.support包下面,它实现了ConversionService接口和ConverterRegistry接口。但是不能直接把它作为SpringMVC的ConversionService,因为直接使用时不能往里面注册类型转换器。也就是说不能像下面这样使用:

<mvc:annotation-driven conversion-service="conversionService"/>  
  
<bean id="conversionService" class="org.springframework.core.convert.support.GenericConversionService"/> 

为此我们必须对GenericConversionService做一些封装,比如说我们可以在自己的ConversionService里面注入一个GenericConversionService,然后通过自己的ConversionService的属性接收Converter并把它们注入到GenericConversionService中,之后所有关于ConversionService的方法逻辑都可以调用GenericConversionService对应的逻辑。按照这种思想我们的ConversionService大概是这样的:

package com.tiantian.blog.web.converter.support;  
   
import java.util.Set;  
   
import javax.annotation.PostConstruct;  
   
import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.core.convert.ConversionService;  
import org.springframework.core.convert.TypeDescriptor;  
import org.springframework.core.convert.converter.Converter;  
import org.springframework.core.convert.converter.ConverterFactory;  
import org.springframework.core.convert.converter.GenericConverter;  
import org.springframework.core.convert.support.GenericConversionService;  
   
public class MyConversionService implements ConversionService {  
   
    @Autowired  
    private GenericConversionService conversionService;  
    private Set<?> converters;  
     
    @PostConstruct  
    public void afterPropertiesSet() {  
       if (converters != null) {  
           for (Object converter : converters) {  
              if (converter instanceof Converter<?, ?>) {  
                  conversionService.addConverter((Converter<?, ?>)converter);  
              } else if (converter instanceof ConverterFactory<?, ?>) {  
                  conversionService.addConverterFactory((ConverterFactory<?, ?>)converter);  
              } else if (converter instanceof GenericConverter) {  
                  conversionService.addConverter((GenericConverter)converter);  
              }  
           }  
       }  
    }  
     
    @Override  
    public boolean canConvert(Class<?> sourceType, Class<?> targetType) {  
       return conversionService.canConvert(sourceType, targetType);  
    }  
   
    @Override  
    public boolean canConvert(TypeDescriptor sourceType,  
           TypeDescriptor targetType) {  
       return conversionService.canConvert(sourceType, targetType);  
    }  
   
    @Override  
    public <T> T convert(Object source, Class<T> targetType) {  
       return conversionService.convert(source, targetType);  
    }  
   
    @Override  
    public Object convert(Object source, TypeDescriptor sourceType,  
           TypeDescriptor targetType) {  
       return conversionService.convert(source, sourceType, targetType);  
    }  
   
    public Set<?> getConverters() {  
       return converters;  
    }  
   
    public void setConverters(Set<?> converters) {  
       this.converters = converters;  
    }  
   
}  

在上面代码中,通过converters属性我们可以接收需要注册的ConverterConverterFactoryGenericConverter,在converters属性设置完成之后afterPropertiesSet方法会被调用,在这个方法里面我们把接收到的converters都注册到注入的GenericConversionService中了,之后关于ConversionService的其他操作都是通过这个GenericConversionService来完成的。这个时候我们的SpringMVC文件可以这样配置:

 <mvc:annotation-driven conversion-service="conversionService"/>  
      
 <bean id="genericConversionService" class="org.springframework.core.convert.support.GenericConversionService"/>  
      
 <bean id="conversionService" class="com.tiantian.blog.web.converter.support.MyConversionService">  
    <property name="converters">  
        <set>  
           <bean class="com.tiantian.blog.web.converter.StringArrayToAttachmentList"/>  
        </set>  
    </property>  
 </bean>  

除了以上这种使用GenericConversionService的思想之外,Spring已经为我们提供了一个既可以使用GenericConversionService,又可以注入Converter的类,那就是ConversionServiceFactoryBean该类为我们提供了一个可以接收Converterconverters属性,在它的内部有一个GenericConversionService对象的引用,在对象初始化完成之后它会new一个GenericConversionService对象,并往GenericConversionService中注册converters属性指定的Converter和Spring自身已经实现了的默认Converter,之后每次返回的都是这个GenericConversionServic对象。当使用ConversionServiceFactoryBean的时候我们的SpringMVC文件可以这样配置:

<mvc:annotation-driven conversion-service="conversionService"/>  
<bean id="conversionService"  
  class="org.springframework.context.support.ConversionServiceFactoryBean">  
    <property name="converters">  
        <list>  
            <bean class="com.tiantian.blog.web.converter.StringArrayToAttachmentList"/>  
        </list>  
    </property>  
</bean> 

除了ConversionServiceFactoryBean之外,Spring还为我们提供了一个FormattingConversionServiceFactoryBean。当使用FormattingConversionServiceFactoryBean的时候我们的SpringMVC配置文件的定义应该是这样:

<mvc:annotation-driven conversion-service="conversionService"/>  
   
<bean id="conversionService"  
      class="org.springframework.format.support.FormattingConversionServiceFactoryBean">  
    <property name="converters">  
       <set>  
           <bean class="com.tiantian.blog.web.converter.StringArrayToAttachmentList"/>  
       </set>  
    </property>
</bean>  

以上介绍的是SpringMVC自动进行类型转换时需要我们做的操作。如果我们需要在程序里面手动的来进行类型转换的话,我们也可以往我们的程序里面注入一个ConversionService,然后通过ConversionService来进行相应的类型转换操作,也可以把Converter直接注入到我们的程序中。

4. ConverterFactory接口

ConverterFactory的出现可以让我们统一管理一些相关联的Converter。顾名思义,ConverterFactory就是产生Converter的一个工厂,确实ConverterFactory就是用来产生Converter的。我们先来看一下ConverterFactory接口的定义:

public interface ConverterFactory<S, R> {  
    <T extends R> Converter<S, T> getConverter(Class<T> targetType);  
}  

我们可以看到ConverterFactory接口里面就定义了一个产生ConvertergetConverter方法,参数是目标类型的class。我们可以看到ConverterFactory中一共用到了三个泛型,S、R、T,其中S表示原类型,R表示目标类型,T是类型R的一个子类。
考虑这样一种情况,我们有一个表示用户状态的枚举类型UserStatus,如果要定义一个从String转为UserStatus的Converter,根据之前Converter接口的说明,我们的StringToUserStatus大概是这个样子:

 public class StringToUserStatus implements Converter<String, UserStatus> {  
      
       @Override  
       public UserStatus convert(String source) {  
           if (source == null) {  
              return null;  
           }  
           return UserStatus.valueOf(source);  
       }  
        
    }  

如果这个时候有另外一个枚举类型UserType,那么我们就需要定义另外一个从String转为UserType的Converter——StringToUserType,那么我们的StringToUserType大概是这个样子:

public class StringToUserType implements Converter<String, UserType> {  
  
   @Override  
   public UserType convert(String source) {  
       if (source == null) {  
          return null;  
       }  
       return UserType.valueOf(source);  
   }  
    
}

如果还有其他枚举类型需要定义原类型为String的Converter的时候,我们还得像上面那样定义对应的Converter。有了ConverterFactory之后,这一切都变得非常简单,因为UserStatus、UserType等其他枚举类型同属于枚举,所以这个时候我们就可以统一定义一个从String到Enum的ConverterFactory,然后从中获取对应的Converter进行convert操作。Spring官方已经为我们实现了这么一个StringToEnumConverterFactory

 @SuppressWarnings("unchecked")  
 final class StringToEnumConverterFactory implements ConverterFactory<String, Enum> {  
    
     public <T extends Enum> Converter<String, T> getConverter(Class<T> targetType) {  
        return new StringToEnum(targetType);  
     }  
    
     private class StringToEnum<T extends Enum> implements Converter<String, T> {  
    
        private final Class<T> enumType;  
    
        public StringToEnum(Class<T> enumType) {  
            this.enumType = enumType;  
        }  
    
        public T convert(String source) {  
            if (source.length() == 0) {  
               // It's an empty enum identifier: reset the enum value to null.  
               return null;  
            }  
            return (T) Enum.valueOf(this.enumType, source.trim());  
        }  
     }  
    
 }  

这样,如果是要进行String到UserStatus的转换,我们就可以通过StringToEnumConverterFactory实例的getConverter(UserStatus.class).convert(string)获取到对应的UserStatus,如果是要转换为UserType的话就是getConverter(UserType.class).convert(string)。这样就非常方便,可以很好的支持扩展。

对于ConverterFactory我们也可以把它当做ConvertionServiceFactoryBeanconverters属性进行注册,在ConvertionServiceFactoryBean内部进行Converter注入的时候会根据converters属性具体元素的具体类型进行不同的注册,对于FormattingConversionServiceFactoryBean也是同样的方式进行注册。所以如果我们自己定义了一个StringToEnumConverterFactory,我们可以这样来进行注册:

<bean id="conversionService"  
  class="org.springframework.context.support.ConversionServiceFactoryBean">  
    <property name="converters">  
        <list>  
            <bean class="com.tiantian.blog.web.converter.StringArrayToAttachmentList"/>  
            <bean class="com.tiantian.blog.web.converter.StringToEnumConverterFactory"/>  
        </list>  
    </property>  
</bean>

5. GenericConverter接口

5.1 概述

GenericConverter接口是所有的Converter接口中最灵活也是最复杂的一个类型转换接口。像我们之前介绍的Converter接口只支持从一个原类型转换为一个目标类型;ConverterFactory接口只支持从一个原类型转换为一个目标类型对应的子类型;而GenericConverter接口支持在多个不同的原类型和目标类型之间进行转换,这也就是GenericConverter接口灵活和复杂的地方。
我们先来看一下GenericConverter接口的定义:

 public interface GenericConverter {  
         
        Set<ConvertiblePair> getConvertibleTypes();  
       
        Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);  
       
        public static final class ConvertiblePair {  
       
           private final Class<?> sourceType;  
       
           private final Class<?> targetType;  
       
           public ConvertiblePair(Class<?> sourceType, Class<?> targetType) {  
               Assert.notNull(sourceType, "Source type must not be null");  
               Assert.notNull(targetType, "Target type must not be null");  
               this.sourceType = sourceType;  
               this.targetType = targetType;  
           }  
       
           public Class<?> getSourceType() {  
               return this.sourceType;  
           }  
       
           public Class<?> getTargetType() {  
               return this.targetType;  
           }  
        }  
       
    } 

我们可以看到GenericConverter接口中一共定义了两个方法,getConvertibleTypes()convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType)getConvertibleTypes方法用于返回这个GenericConverter能够转换的原类型和目标类型的这么一个组合;convert方法则是用于进行类型转换的,我们可以在这个方法里面实现我们自己的转换逻辑。之所以说GenericConverter是最复杂的是因为它的转换方法convert的参数类型TypeDescriptor是比较复杂的。TypeDescriptor对类型Type进行了一些封装,包括value、Field及其对应的真实类型等等,具体的可以查看API。

关于GenericConverter的使用,这里也举一个例子。假设我们有一项需求是希望能通过user的id或者username直接转换为对应的user对象,那么我们就可以针对于id和username来建立一个GenericConverter。这里假设id是int型,而username是String型的,所以我们的GenericConverter可以这样来写:

public class UserGenericConverter implements GenericConverter {  
   
    @Autowired  
    private UserService userService;  
     
    @Override  
    public Object convert(Object source, TypeDescriptor sourceType,  
           TypeDescriptor targetType) {  
       if (source == null || sourceType == TypeDescriptor.NULL || targetType == TypeDescriptor.NULL) {  
           return null;  
       }  
       User user = null;  
       if (sourceType.getType() == Integer.class) {  
           user = userService.findById((Integer) source);//根据id来查找user  
       } else if (sourceType.getType() == String.class) {  
           user = userService.find((String)source);//根据用户名来查找user  
       }  
       return user;  
    }  
   
    @Override  
    public Set<ConvertiblePair> getConvertibleTypes() {  
       Set<ConvertiblePair> pairs = new HashSet<ConvertiblePair>();  
       pairs.add(new ConvertiblePair(Integer.class, User.class));  
       pairs.add(new ConvertiblePair(String.class, User.class));  
       return pairs;  
    }  
   
}

我们可以看到在上面定义的UserGenericConverter中,我们在getConvertibleTypes方法中添加了两组转换的组合,Integer到User和String到User。然后我们给UserGenericConverter注入了一个UserService,在convert方法
中我们简单的根据原类型是Integer还是String来判断传递的原数据是id还是username,并利用UserService对应的方法返回相应的User对象。

GenericConverter接口实现类的注册方法跟Converter接口和ConverterFactory接口实现类的注册方法是一样的,这里就不再赘述了。

虽然Converter接口、ConverterFactory接口和GenericConverter接口之间没有任何的关系,但是Spring内部在注册Converter实现类ConverterFactory实现类时是先把它们转换为GenericConverter,之后再统一对GenericConverter进行注册的。也就是说Spring内部会把Converter和ConverterFactory全部转换为GenericConverter进行注册,在Spring注册的容器中只存在GenericConverter这一种类型转换器。我想之所以给用户开放Converter接口和ConverterFactory接口是为了让我们能够更方便的实现自己的类型转换器。基于此,Spring官方也提倡我们在进行一些简单类型转换器定义时更多的使用Converter接口和ConverterFactory接口,在非必要的情况下少使用GenericConverter接口。

5.2 ConditionalGenericConverter 接口

对于GenericConverter接口,Spring还为我们提供了一个它的子接口,叫做ConditionalGenericConverter,在这个接口中只定义了一个方法:matches方法。我们一起来看一下ConditionalGenericConverter接口的定义:

public interface ConditionalGenericConverter extends GenericConverter {  
   
    boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType);  
     
} 

顾名思义,从Conditional我们就可以看出来这个接口是用于定义有条件的类型转换器的,也就是说不是简单的满足类型匹配就可以使用该类型转换器进行类型转换了,必须要满足某种条件才能使用该类型转换器。而该类型转换器的条件控制就是通过ConditionalGenericConverter接口的matches方法来实现的。关于ConditionalGenericConverter的使用Spring内部已经实现了很多,这里我们来看一个Spring已经实现了的将String以逗号分割转换为目标类型数组的实现:

final class StringToArrayConverter implements ConditionalGenericConverter {  
   
    private final ConversionService conversionService;  
   
    public StringToArrayConverter(ConversionService conversionService) {  
       this.conversionService = conversionService;  
    }  
   
    public Set<ConvertiblePair> getConvertibleTypes() {  
       return Collections.singleton(new ConvertiblePair(String.class, Object[].class));  
    }  
   
    public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {  
       return this.conversionService.canConvert(sourceType, targetType.getElementTypeDescriptor());  
    }  
   
    public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {  
       if (source == null) {  
           return null;  
       }       
       String string = (String) source;  
       String[] fields = StringUtils.commaDelimitedListToStringArray(string);  
       Object target = Array.newInstance(targetType.getElementType(), fields.length);  
       for (int i = 0; i < fields.length; i++) {  
           Object sourceElement = fields[i];  
           Object targetElement = this.conversionService.convert(sourceElement, sourceType, targetType.getElementTypeDescriptor());  
           Array.set(target, i, targetElement);  
       }  
       return target;  
    }  
   
}  

我们可以看到这个StringToArrayConverter就是实现了ConditionalGenericConverter接口的。根据里面的matches方法的逻辑我们知道当我们要把一个字符串转换为一个数组的时候,只有我们已经定义了一个字符串到这个目标数组元素对应类型的类型转换器时才可以使用StringToArrayConverter进行类型转换。也就是说假如我们已经定义了一个String到User的类型转换器,那么当我们需要将String转换为对应的User数组的时候,我们就可以直接使用Spring为我们提供的StringToArrayConverter了。

欢迎关注我的公众号:程序员L札记

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

推荐阅读更多精彩内容