Java基础:反射

反射注解动态代理相关阅读

1. 反射概述

Java反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。

Java 反射机制主要提供了以下功能: 在运行时判断任意一个对象所属的类;在运行时构造任意一个类的对象;在运行时判断任意一个类所具有的成员变量和方法;在运行时调用任意一个对象的方法;生成动态代理。

Java中,反射是一种强大的工具。它使您能够创建灵活的代码,这些代码可以在运行时装配,无需在组件之间进行源代表链接。反射允许我们在编写与执行时,使我们的程序代码能够接入装载到JVM中的类的内部信息,而不是源代码中选定的类协作的代码。这使反射成为构建灵活的应用的主要工具。但需注意的是:如果使用不当,反射的成本很高。

2. 反射的应用场景

反射是Java中的高级特性,在各种Java框架中都需要使用反射。所以,就算你将来很长一段时间不使用反射,但你使用的框架都大量使用了反射,所以想深入学习框架,那么就一定要学习反射。

框架通常通过反射来识别一个对象的“类型信息”。当你传递给框架一个对象时,框架会通过反射来了解对象的真实类型(对象实体的类型,而不是引用的类型),这个类型有几个构造器,有什么样的属性,有什么样的方法。还可以通过反射调用构造器,调用方法,对属性进行读写操作。

你可能觉得这没有什么神奇的,那是你还没了解我说的是什么!你需要再想一想,写一个方法,参数是Object obj,然后你的方法需要创建一个与参数类型相同的对象出来,还要调用这个对象上的方法。需要注意,参数是Object类型,但用户调用这个方法时,可能传递的不是Object实体对象,它的真实类型有可能是任何类型。

目前好多框架都会用到java的反射机制。比如struts2,sping,hibernate。 如果我们不用struts2,自己写一个类似的功能也是可以实现的,比如浏览器通过HTTP发送数据,而这些数据都是字符串,我们接受到这些字符串时, 可以通过反射去构造一个对象(通过拦截器做成框架的功能),这样就可以用对象的get和set方法了,而不用原始的getPeremter()方法。事实上, 在struts2出来之前,我们又不想用struts1的ActionForm就做过这样项目。

Java反射机制主要提供了以下功能:

  • 在运行时判断任意一个对象所属的类
  • 在运行时构造任意一个类的对象
  • 在运行时判断任意一个类所具有的成员变量和方法
  • 在运行时调用任意一个对象的方法
  • 生成动态代理

3. 反射

JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。

要想解剖一个类,必须先要获取到该类的字节码文件对象。而解剖使用的就是Class类中的方法,所以先要获取到每一个字节码文件对应的Class类型的对象。

一句话概括:反射就是把java类中的各种成分映射成相应的java类(Class,Field,Method,Constructor),在程序运行的过程中,动态的访问java类中的成分,反射还可以实现框架的功能

例如:一个Java类中用一个Class类的对象来表示,一个类中的组成部分:成员变量,方法,构造方法,包等信息也用一个个的Java类来表示,就像汽车是一个类,汽车中的发动机,变速箱等等也是一个个的类。表示java类的Class类显然要提供一系列的方法,来获得其中的变量,方法,构造方法,修饰符,包等信息,这些信息就是用相应类的实例对象来表示,它们是Field、Method、Contructor、Package等。

3.1 反射的主要作用

  • 运行时取得类的方法和字段的相关信息。
  • 创建某个类的新实例(newInstance())
  • 取得字段引用直接获取和设置对象字段,无论访问修饰符是什么
  • 观察或操作应用程序的运行时行为
  • 调试或测试程序,因为可以直接访问方法、构造函数和成员字段
  • 通过名字调用不知道的方法并使用该信息来创建对象和调用方法

4. 反射从Class类开始

要想使用反射,首先你需要得到Class对象,然后才能通过Class对象获取Constructor、Field、Method等对象。所有的反射对象都不可能自己来new,说白一点,这些反射对象对应的是class文件上的信息,你怎么可能自己去new呢?如果可以自己去new一个Class类的对象,那么是不是就不用我们再去编写.java文件,然后再通过编译器去编译成.class文件了呢?当然这是不可能的!

我们需要思考,Class除了可以返回当前对应类型的所有属性、方法、构造器的反射对象外,还有什么功能呢?例如对应类型的类名是什么?对应类型的父类是谁?对应类型是不是public类,是不是final类。对应类型有没有可能是个数组类型?有没有可能是接口类型?有没有可能是基本类型等等!如果你学会了这样思考,那么你今后学习新类是就方便多了!

4.1 三种获取Class对象的方式

  • Object类的getClass()方法
 Person p = new Person();
 Class c = p.getClass();
  • 数据类型的静态属性class
Class c2 =Person.class;

任意数据类型都具备一个class静态属性,看上去要比第一种方式简单

  • 将类名作为字符串传递给Class类中的静态方法forName即可
 Class c3 = Class.forName("Person");
package cn.itcast;

class Person {
    private String name;
    int age;
    public String address;

    public Person() {
    }

    private Person(String name) {
        this.name = name;
    }

    Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public Person(String name, int age, String address) {
        this.name = name;
        this.age = age;
        this.address = address;
    }

    public void show() {
        System.out.println("show");
    }

    public void method(String s) {
        System.out.println("method " + s);
    }

    public String getString(String s, int i) {
        return s + "---" + i;
    }

    private void function() {
        System.out.println("function");
    }

    public String toString() {
        return "Person [name=" + name + ", age=" + age + ", address=" + address
                + "]";
    }
}

/*
 * 反射:就是通过class文件对象,去使用该文件中的成员变量,构造方法,成员方法。
 * 
 * Person p = new Person(); p.使用
 * 
 * 要想这样使用,首先你必须得到class文件对象,其实也就是得到Class类的对象。 Class类: 成员变量 Field 构造方法 Constructor
 * 成员方法 Method
 * 
 * 获取class文件对象的方式: A:Object类的getClass()方法 B:数据类型的静态属性class C:Class类中的静态方法 public
 * static Class forName(String className)
 * 
 * 一般我们到底使用谁呢? 第三种:因为第三种是一个字符串,而不是一个具体的类名。这样我们就可以把这样的字符串配置到配置文件中。
 */
public class ReflectDemo {
    public static void main(String[] args) throws ClassNotFoundException {
        // 方式1
        Person p = new Person();
        Class c = p.getClass();

        Person p2 = new Person();
        Class c2 = p2.getClass();

        System.out.println(p == p2);// false
        System.out.println(c == c2);// true

        // 方式2
        Class c3 = Person.class;
        // int.class;
        // String.class;
        System.out.println(c == c3);

        // 方式3
        // ClassNotFoundException
        Class c4 = Class.forName("cn.itcast_01.Person");
        System.out.println(c == c4);
    }
}

4.2 第三种和前两种的区别

前两种你必须明确Person类型;后面是你我这种类型的字符串就行.这种扩展更强.我不需要知道你的类.我只提供字符串,按照配置文件加载就可以了。

PS:所谓的框架就是对外提供一些接口,也就是功能扩展的标准,由实现类按照这个接口标准去实现。框架内部如果需要操纵这些实现类的对象完成某些操作,那么只需要把这些实现类的全名(包名+类名)写在某个配置文件中,框架代码只需要读取这个配置文件,就可以获取这个实现类的字节码文件,然后利用反射技术创建这个实现类的对象并且调用相应的方法完成一些操作。

用于描述字节码的类就是Class类,创建对象,可以提取字节码文件中的内容,如字段、构造函数、一般函数。该类就可以获取字节码文件中的所有内容,那么反射就是依靠该类完成的。想要对一个类文件进行解剖,只要获取到该类的字节码文件对象即可。

5. 加载类

我们已经知道,main()方法是程序的入口。那是不是在main()方法开始执行之前,所有的class文件都已经加载到方法区中了呢?答案是:NO!通常只有需要执行到使用某个类的代码时,才会去CLASSPATH中加载class文件,如果程序从头到尾都没有使用某个类,那么这个类对应的class文件就不会被加载到内存。

可以导致一个类被加载可能有:

  • 使用一个类的静态方法;
  • 使用一个类的静态属性;
  • 创建这个类的对象;
  • 使用Class.forName()方法加载类;
  • 反序列化一个类的对象;
  • 加载一个类的子类时,也会加载其父类;
  • 加载一个类时,也会加载与该类相关的类。

上面给出的几个可能也只是可能而已,如果当前类没有被加载过,才会去加载,如果已经加载到方法区中了,那么就不可能再去加载。

6. Class 字节码

Class 类的实例表示正在运行的 Java 应用程序中的类和接口。

方法 说明
forName() 通过类名获取类的字节码
getClassLoader() 获取该类的类加载器
getInterfaces() 获取所实现的接口
getSuperclass() 获取父类
getGenericSuperclass() 获取传递给父类参数化类型
newInstance() 创建实例
getName() 获取类名,接口名
getPackage() 获取包名
isPrimitive() 判定指定的 Class 对象是否表示一个基本类型
isArray() 判定此 Class 对象是否表示一个数组类
getResourceAsStream() 查找具有给定名称的资源

6.1 获取注解

方法 说明
getAnnotation() 获取指定类型的注解
getAnnotations() 获取所有的注解
getDeclaredAnnotations() 获取除了继承得到的所有注解

6.2 获取构造方法

方法 说明
getConstructor() 获取指定的非私有的构造方法
getDeclaredConstructor() 获取指定的构造方法
getConstructors() 获取公有的构造方法
getDeclaredConstructors() 获取所有的构造方法

6.3 获取成员方法

方法 说明
getMethod() 获取指定的非私有方法
getDeclaredMethod() 获取指定的方法
getMethods() 获取公有的方法
getDeclaredMethods() 获取所有的方法

6.4 获取成员变量

方法 说明
getField() 获取指定名称的字段
getFields() 获取公有的字段
getDeclaredField(String name) 获取指定名称的字段
getDeclaredFields() 获取所有的字段

7. AccessibleObject

AccessibleObject 类是 Field、Method 和 Constructor 对象的基类

方法 说明
getAnnotation() 获取指定类型的注解
getAnnotations() 获取所有的注解
getDeclaredAnnotations() 获取除了继承得到的所有注解
setAccessible(true) 暴力反射,取消访问检查

8. Constructor

Constructor 提供关于类的单个构造方法的信息以及对它的访问权限

方法 说明
newInstance() 通过构造方法创建实例
getParameterTypes() 获取构造器的所有参数的类型
getExceptionTypes() 获取构造器上声明的所有异常类型
getDeclaringClass() 获取构造器所属的类型
getModifiers() 获取构造器上的所有修饰符信息

9. Method

表示一个类中的成员方法

方法 说明
invoke(Object obj, Object… args) 通过方法反射对象调用方法,如果当前方法是实例方法,那么当前对象就是obj,如果当前方法是static方法,那么可以给obj传递null。args表示是方法的参数
setAccessible(true) 暴力反射,取消访问检查
getAnnotation() 获取方法上指定类型的注解
getAnnotations() 获取所有的注解
getDeclaredAnnotations() 获取方法上说所有的注解
getGenericParameterTypes() 获取泛型的参数化类型

10. Field

表示一个类中的成员变量

方法 说明
getAnnotation() 获取字段上指定类型的注解
getAnnotations() 获取所有的注解
getDeclaredAnnotations() 获取字段所有的注解
set() 给指定字段设置新值
get() 获取字段值
setAccessible(true) 暴力反射,取消访问检查
getType() 获取字段的类型
getXXX(Object obj) 如果当前属性为基本类型,可以使用getXXX()系列方法获取基本类型属性值
setXXX(Object obj, XXX value) 如果当前属性为基本类型,可以使用setXXX()系统方法基本类型属性值

11. Type

Type 是 Java 编程语言中所有类型的公共高级接口

11.1 ParameterizedType

ParameterizedType 表示参数化类型,如 Collection<String>

方法 说明
Type[ ] getActualTypeArguments() 获取真实参数


public abstract class BaseProtocol<T> {

    ...

    /**泛型解析*/
    protected T parsejson(String jsonString){
        ParameterizedType genericSuperclass = (ParameterizedType) getClass().getGenericSuperclass();
        Type[] args = genericSuperclass.getActualTypeArguments();
        Type type = args[0];
        return GsonUtil.changeGsonToBean(jsonString,type);
    }
}

12. Array

Array 类提供了动态创建和访问 Java 数组的方法

方法 说明
Array.getLenght() 获取数组的长度
Array.get() 获取数组中指定索引的值

13. Modifier

Modifier类有一系列的static方法用来解析其他getModifiers()方法返回的int值

Method m = …
int m = m.getModifiers();
boolean b1 = Modifier.isAbstract(m);//解析m中是否包含abstract修饰
boolean b2 = Modifier.isStatic(m);//解析m中是否包含static修饰
String s = Modifiers.toString(m);//把所有修饰都转换成字符串

14. 反射的应用

通过反射获取构造方法并使用

package cn.itcast_02;

import java.lang.reflect.Constructor;

/*
 * 需求:通过反射获取私有构造方法并使用
 * private Person(String name){}
 * 
 * Person p = new Person("风清扬");
 * System.out.println(p);
 */
public class ReflectDemo3 {
    public static void main(String[] args) throws Exception {
        // 获取字节码文件对象
        Class c = Class.forName("cn.itcast_01.Person");

        // 获取私有构造方法对象
        // NoSuchMethodException:每个这个方法异常
        // 原因是一开始我们使用的方法只能获取公共的,下面这种方式就可以了。
        Constructor con = c.getDeclaredConstructor(String.class);

        // 用该私有构造方法创建对象
        // IllegalAccessException:非法的访问异常。
        // 暴力访问
        con.setAccessible(true);// 值为true则指示反射的对象在使用时应该取消Java语言访问检查。
        Object obj = con.newInstance("风清扬");

        System.out.println(obj);
    }
}

通过反射获取成员变量并使用

package cn.itcast_03;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;

/*
 * 通过发生获取成员变量并使用
 */
public class ReflectDemo {
    public static void main(String[] args) throws Exception {
        // 获取字节码文件对象
        Class c = Class.forName("cn.itcast_01.Person");

        // 获取所有的成员变量
        // Field[] fields = c.getFields();
        // Field[] fields = c.getDeclaredFields();
        // for (Field field : fields) {
        // System.out.println(field);
        // }

        /*
         * Person p = new Person(); p.address = "北京"; System.out.println(p);
         */

        // 通过无参构造方法创建对象
        Constructor con = c.getConstructor();
        Object obj = con.newInstance();
        System.out.println(obj);

        // 获取单个的成员变量
        // 获取address并对其赋值
        Field addressField = c.getField("address");
        // public void set(Object obj,Object value)
        // 将指定对象变量上此 Field 对象表示的字段设置为指定的新值。
        addressField.set(obj, "北京"); // 给obj对象的addressField字段设置值为"北京"
        System.out.println(obj);

        // 获取name并对其赋值
        // NoSuchFieldException
        Field nameField = c.getDeclaredField("name");
        // IllegalAccessException
        nameField.setAccessible(true);
        nameField.set(obj, "林青霞");
        System.out.println(obj);

        // 获取age并对其赋值
        Field ageField = c.getDeclaredField("age");
        ageField.setAccessible(true);
        ageField.set(obj, 27);
        System.out.println(obj);
    }
}

通过反射获取成员方法并使用

package cn.itcast_04;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

public class ReflectDemo {
    public static void main(String[] args) throws Exception {
        // 获取字节码文件对象
        Class c = Class.forName("cn.itcast_01.Person");

        // 获取所有的方法
        // Method[] methods = c.getMethods(); // 获取自己的包括父亲的公共方法
        // Method[] methods = c.getDeclaredMethods(); // 获取自己的所有的方法
        // for (Method method : methods) {
        // System.out.println(method);
        // }

        Constructor con = c.getConstructor();
        Object obj = con.newInstance();

        /*
         * Person p = new Person(); p.show();
         */

        // 获取单个方法并使用
        // public void show()
        // public Method getMethod(String name,Class<?>... parameterTypes)
        // 第一个参数表示的方法名,第二个参数表示的是方法的参数的class类型
        Method m1 = c.getMethod("show");
        // obj.m1(); // 错误
        // public Object invoke(Object obj,Object... args)
        // 返回值是Object接收,第一个参数表示对象是谁,第二参数表示调用该方法的实际参数
        m1.invoke(obj); // 调用obj对象的m1方法

        System.out.println("----------");
        // public void method(String s)
        Method m2 = c.getMethod("method", String.class);
        m2.invoke(obj, "hello");
        System.out.println("----------");

        // public String getString(String s, int i)
        Method m3 = c.getMethod("getString", String.class, int.class);
        Object objString = m3.invoke(obj, "hello", 100);
        System.out.println(objString);
        // String s = (String)m3.invoke(obj, "hello",100);
        // System.out.println(s);
        System.out.println("----------");

        // private void function()
        Method m4 = c.getDeclaredMethod("function");
        m4.setAccessible(true);
        m4.invoke(obj);
    }
}

14.1 反射应用举例

给ArrayList<Integer>的一个对象,在这个集合中添加一个字符串数据,如何实现呢?

package cn.itcast.test;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;

/*
 * 我给你ArrayList<Integer>的一个对象,我想在这个集合中添加一个字符串数据,如何实现呢?
 */
public class ArrayListDemo {
    public static void main(String[] args) throws NoSuchMethodException,
            SecurityException, IllegalAccessException,
            IllegalArgumentException, InvocationTargetException {
        // 创建集合对象
        ArrayList<Integer> array = new ArrayList<Integer>();

        // array.add("hello");
        // array.add(10);

        Class c = array.getClass(); // 集合ArrayList的class文件对象
        Method m = c.getMethod("add", Object.class);

        m.invoke(array, "hello"); // 调用array的add方法,传入的值是hello
        m.invoke(array, "world");
        m.invoke(array, "java");

        System.out.println(array);
    }
}

通过配置文件运行类中的方法

package cn.itcast.test;

import java.io.FileReader;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.Properties;

/*
 * 通过配置文件运行类中的方法
 * 
 * 反射:
 *      需要有配置文件配合使用。
 *      用class.txt代替。
 *      并且你知道有两个键。
 *          className
 *          methodName
 */
public class Test {
    public static void main(String[] args) throws Exception {
        // 反射前的做法
        // Student s = new Student();
        // s.love();
        // Teacher t = new Teacher();
        // t.love();
        // Worker w = new Worker();
        // w.love();
        // 反射后的做法

        // 加载键值对数据
        Properties prop = new Properties();
        FileReader fr = new FileReader("class.txt");
        prop.load(fr);
        fr.close();

        // 获取数据
        String className = prop.getProperty("className");
        String methodName = prop.getProperty("methodName");

        // 反射
        Class c = Class.forName(className);

        Constructor con = c.getConstructor();
        Object obj = con.newInstance();

        // 调用方法
        Method m = c.getMethod(methodName);
        m.invoke(obj);
    }
}

写一个方法:public void setProperty(Object obj, String propertyName, Object value){},此方法可将obj对象中名为propertyName的属性的值设置为value

package cn.itcast.test;

import java.lang.reflect.Field;

public class Tool {
    public void setProperty(Object obj, String propertyName, Object value)
            throws NoSuchFieldException, SecurityException,
            IllegalArgumentException, IllegalAccessException {
        // 根据对象获取字节码文件对象
        Class c = obj.getClass();
        // 获取该对象的propertyName成员变量
        Field field = c.getDeclaredField(propertyName);
        // 取消访问检查
        field.setAccessible(true);
        // 给对象的成员变量赋值为指定的值
        field.set(obj, value);
    }
}
package cn.itcast.test;

public class ToolDemo {
    public static void main(String[] args) throws NoSuchFieldException,
            SecurityException, IllegalArgumentException, IllegalAccessException {
        Person p = new Person();
        Tool t = new Tool();
        t.setProperty(p, "name", "林青霞");
        t.setProperty(p, "age", 27);
        System.out.println(p);
        System.out.println("-----------");

        Dog d = new Dog();

        t.setProperty(d, "sex", '男');
        t.setProperty(d, "price", 12.34f);

        System.out.println(d);
    }
}

class Dog {
    char sex;
    float price;

    @Override
    public String toString() {
        return sex + "---" + price;
    }
}

class Person {
    private String name;
    public int age;

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

推荐阅读更多精彩内容