您的位置:首页 > 编程语言 > Java开发

老生常谈系列之Aop--JDK动态代理的底层实现原理

2022-01-14 17:54 309 查看

老生常谈系列之Aop--JDK动态代理的底层实现原理

前言

在Aop系列里面有两篇文章,分别是老生常谈系列之Aop--Spring Aop原理浅析老生常谈系列之Aop--Spring Aop源码解析(二)都有涉及JDK动态代理的使用,但是没有详细分析JDK动态代理的实现原理,只是贴出了使用方法。本着知其然更要知其所以然的目标,这一篇文章,我们就来深扒一下JDK动态代理的实现原理。

原理分析

这里的代码分析是基于JDK1.8

Proxy.newProxyInstance()

说到

Proxy.newProxyInstance()
方法,首先我们来回忆一下老生常谈系列之Aop--Spring Aop源码解析(二)文章中调用JDK动态代理的例子,可以看到如下代码。

public Object getProxy(){
return Proxy.newProxyInstance(calculateService.getClass().getClassLoader(),
calculateService.getClass().getInterfaces(),
new MyInvocationHandler
(newCalculateServiceImpl()));
}

我们是通过

Proxy.newProxyInstance()
方法来获取一个代理类的,那么
Proxy.newProxyInstance()
帮我们做了什么呢?

直接跟进源码,这里保留了方法上的英文注释,去除了抛出异常部分的注释,各位看官用心体会。同时这里的代码精简了一部分JDK的权限校验,只留下了核心代码并且添加了注释,逻辑还是比较简单的。

/**
* Returns an instance of a proxy class for the specified interfaces
* that dispatches method invocations to the specified invocation
* handler.
*
* <p>{@code Proxy.newProxyInstance} throws
* {@code IllegalArgumentException} for the same reasons that
* {@code Proxy.getProxyClass} does.
*
* @param   loader the class loader to define the proxy class
* @param   interfaces the list of interfaces for the proxy class
*          to implement
* @param   h the invocation handler to dispatch method invocations to
* @return  a proxy instance with the specified invocation handler of a
*          proxy class that is defined by the specified class loader
*          and that implements the specified interfaces
*/
@CallerSensitive
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
throws IllegalArgumentException
{
Objects.requireNonNull(h);

final Class<?>[] intfs = interfaces.clone();
// 去除一些权限校验

/*
* 查找或生成指定的代理类
* Look up or generate the designated proxy class.
*/
Class<?> cl = getProxyClass0(loader, intfs);

/*
* 获取指定的构造函数,这里指定的构造函数为入参为InvocationHandler的构造函数
* Invoke its constructor with the designated invocation handler.
*/
try {
// 去除一些权限校验
final Constructor<?> cons = cl.getConstructor(constructorParams);final InvocationHandler ih = h;
// 去除一些权限校验
// 调用构造函数生产代理类实例
return cons.newInstance(new Object[]{h});} catch (... e) {
// 省略异常处理
}
}

翻译一下:返回将方法调用分派到指定调用处理器的指定接口的代理类的实例。

显而易见,这里通过了一层转发,实现了在调用方法之前先回调到自定义的

InvocationHandler
invoke()
方法。这一实现原理,在前面的文章是有说到的。但是JDK动态代理是怎么生成了一个可以回调到自定义
InvocationHandler
的代理类的呢?它是怎么将具体实现类的方法和生成的代理类的方法进行关联的呢?下面让我们来逐一解释。

getProxyClass0()

很显然,

getProxyClass0()
是需要重点关注的方法。从这个方法的注释就可以看出来,这个方法是查找或生成指定的代理类,很显然,我们要获取的类对象的结构和内容是在这个方法里完成的。也就是说,这个方法会通过我们传入的
ClassLoader loader
Class<?>[] interfaces
来构造完成一个
Class<?>
对象。

跟进代码

/**
* Generate a proxy class.  Must call the checkProxyAccess method
* to perform permission checks before calling this.
*/
private static Class<?> getProxyClass0(ClassLoader loader,
Class<?>... interfaces) {
if (interfaces.length > 65535) {
throw new IllegalArgumentException("interface limit exceeded");
}

// If the proxy class defined by the given loader implementing
// the given interfaces exists, this will simply return the cached copy;
// otherwise, it will create the proxy class via the ProxyClassFactory
return proxyClassCache.get(loader, interfaces);
}

好家伙,一进来发现这个方法异常简单,可以说就一行代码,只是通过

proxyClassCache
去获取,如果存在缓存直接返回,否则通过
ProxyClassFactory
来生成代理类,那我们来看一下
proxyClassCache
是个什么东东。

/**
* a cache of proxy classes
*/
private static final WeakCache<ClassLoader, Class<?>[], Class<?>>
proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory());

proxyClassCache
是一个
WeakCache
类型的缓存集合,该对象维护了两个
BiFunction<T, U, R>
属性

private final BiFunction<K, P, ?> subKeyFactory;
private final BiFunction<K, P, V> valueFactory;

对应到这里也就是上面代码的

KeyFactory
ProxyClassFactory
,分别对应一个Key生成工厂和value生成工厂,这两个属性会在
new WeakCache
的时候赋值。我们可以重点关注
ProxyClassFactory
里面的
apply(ClassLoader loader, Class<?>[] interfaces)
方法,该方法就是实现了生成代理类Class对象的方法。

ProxyClassFactory

跟进

ProxyClassFactory
代码,JDK的源码很多校验和控制,看起来没那么清晰,但是不要慌,把这些边边角角和异常控制去掉,逻辑还是很清晰的。

/**
* A factory function that generates, defines and returns the proxy class given
* the ClassLoader and array of interfaces.
*/
private static final class ProxyClassFactory
implements BiFunction<ClassLoader, Class<?>[], Class<?>>
{
// prefix for all proxy class names
// 代理类的名称
private static final String proxyClassNamePrefix = "$Proxy";

// next number to use for generation of unique proxy class names
// 生成代理类的序号
private static final AtomicLong nextUniqueNumber = new AtomicLong();

@Override
public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {

Map<Class<?>, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length);
for (Class<?> intf : interfaces) {
/*
* 验证类加载器是否将此接口的名称解析为相同的 Class 对象。
* Verify that the class loader resolves the name of this
* interface to the same Class object.
*/
Class<?> interfaceClass = null;
try {
interfaceClass = Class.forName(intf.getName(), false, loader);
} catch (ClassNotFoundException e) {
}
if (interfaceClass != intf) {
throw new IllegalArgumentException(
intf + " is not visible from class loader");
}
/*
* 验证 Class 对象实际上是不是代表一个接口。
* Verify that the Class object actually represents an
* interface.
*/
if (!interfaceClass.isInterface()) {
throw new IllegalArgumentException(
interfaceClass.getName() + " is not an interface");
}
/*
* 验证此接口不是重复的。
* Verify that this interface is not a duplicate.
*/
if (interfaceSet.put(interfaceClass, Boolean.TRUE) != null) {
throw new IllegalArgumentException(
"repeated interface: " + interfaceClass.getName());
}
}

String proxyPkg = null;     // package to define proxy class in
int accessFlags = Modifier.PUBLIC | Modifier.FINAL;

/*
* 记录一个非公共代理接口的包,以便在同一个包中定义代理类。验证所有非公共代理接口是否在同一个包中
* Record the package of a non-public proxy interface so that the
* proxy class will be defined in the same package.  Verify that
* all non-public proxy interfaces are in the same package.
*/
for (Class<?> intf : interfaces) {
int flags = intf.getModifiers();
if (!Modifier.isPublic(flags)) {
accessFlags = Modifier.FINAL;
String name = intf.getName();
int n = name.lastIndexOf('.');
String pkg = ((n == -1) ? "" : name.substring(0, n + 1));
if (proxyPkg == null) {
proxyPkg = pkg;
} else if (!pkg.equals(proxyPkg)) {
throw new IllegalArgumentException(
"non-public interfaces from different packages");
}
}
}

if (proxyPkg == null) {
// if no non-public proxy interfaces, use com.sun.proxy package
proxyPkg = ReflectUtil.PROXY_PACKAGE + ".";
}

/*
* Choose a name for the proxy class to generate.
*/
long num = nextUniqueNumber.getAndIncrement();
String proxyName = proxyPkg + proxyClassNamePrefix + num;

// 前面的都是校验和生成accessFlags,proxyName等信息

/*
* 生成指定的代理类,可以看到委托给了ProxyGenerator.generateProxyClass()实现
* Generate the specified proxy class.
*/
byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
proxyName, interfaces, accessFlags);
try {
return defineClass0(loader, proxyName,
proxyClassFile, 0, proxyClassFile.length);
} catch (ClassFormatError e) {
/*
* A ClassFormatError here means that (barring bugs in the
* proxy class generation code) there was some other
* invalid aspect of the arguments supplied to the proxy
* class creation (such as virtual machine limitations
* exceeded).
*/
throw new IllegalArgumentException(e.toString());
}
}
}

上面的代码很长?

无所谓的,看到这一句就好了

byte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces, accessFlags)
,这里生产了代理类的
byte[]
对象,然后再调用
defineClass0(loader, proxyName,proxyClassFile, 0, proxyClassFile.length)
生成一个
Class
对象。

ProxyGenerator.generateProxyClass()

跟进

ProxyGenerator.generateProxyClass()
方法。

/**
* Generate a proxy class given a name and a list of proxy interfaces.
*
* @param name        the class name of the proxy class
* @param interfaces  proxy interfaces
* @param accessFlags access flags of the proxy class
*/
public static byte[] generateProxyClass(final String name,
Class<?>[] interfaces,
int accessFlags)
{
ProxyGenerator gen = new ProxyGenerator(name, interfaces, accessFlags);
// 生成代理类的byte[]数组
final byte[] classFile = gen.generateClassFile();

// 是否保存动态代理生成的代码,sun.misc.ProxyGenerator.saveGeneratedFiles
if (saveGeneratedFiles) {
java.security.AccessController.doPrivileged(
new java.security.PrivilegedAction<Void>() {
public Void run() {
try {
int i = name.lastIndexOf('.');
Path path;
if (i > 0) {
Path dir = Paths.get(name.substring(0, i).replace('.', File.separatorChar));
Files.createDirectories(dir);
path = dir.resolve(name.substring(i+1, name.length()) + ".class");
} else {
path = Paths.get(name + ".class");
}
Files.write(path, classFile);
return null;
} catch (IOException e) {
throw new InternalError(
"I/O exception saving generated file: " + e);
}
}
});
}

return classFile;
}

这个属性看着是不是很眼熟,我们通过设置这个属性来保存动态代理生成的字节码,这里也可以看到生效的原理,以及之前用的时候为啥说只对JDK动态代理生效。

/** debugging flag for saving generated class files */
private final static boolean saveGeneratedFiles =
java.security.AccessController.doPrivileged(
new GetBooleanAction(
"sun.misc.ProxyGenerator.saveGeneratedFiles")).booleanValue();

到这里这个

ProxyGenerator
才是真正的生产代理类的文件的
byte[]
数组

/**
* Construct a ProxyGenerator to generate a proxy class with the
* specified name and for the given interfaces.
*
* A ProxyGenerator object contains the state for the ongoing
* generation of a particular proxy class.
*/
private ProxyGenerator(String className, Class<?>[] interfaces, int accessFlags) {
this.className = className;
this.interfaces = interfaces;
this.accessFlags = accessFlags;
}

这个就是生成的具体逻辑了。

  • 为所有方法组装 ProxyMethod 对象以生成代理调度代码。
  • 为我们正在生成的类中的所有字段和方法组装 FieldInfo 和 MethodInfo 结构。
  • 编写最终的类文件。

其中前面第一第二点的逻辑都较好理解,第三点就是在写class文件了,这个按照JVM的字节码结构去拼接一份class字节码。例如

dout.writeInt(0xCAFEBABE)
在文件开头写入魔数,
dout.writeShort(CLASSFILE_MINOR_VERSION)
dout.writeShort(CLASSFILE_MAJOR_VERSION)
分别为写小版本号和大版本号。这些顺序都是JVM字节码规定的顺序,需要严格按照此顺序拼接。关于字节码更多的知识,可以看JVM字节码结构。这个方法的代码很长,但是我不打算删减,这里的步骤缺一不可。

/**
* Generate a class file for the proxy class.  This method drives the
* class file generation process.
*/
private byte[] generateClassFile() {

/* ============================================================
* Step 1: Assemble ProxyMethod objects for all methods to
* generate proxy dispatching code for.
*/

/*
* Record that proxy methods are needed for the hashCode, equals,
* and toString methods of java.lang.Object.  This is done before
* the methods from the proxy interfaces so that the methods from
* java.lang.Object take precedence over duplicate methods in the
* proxy interfaces.
*/
addProxyMethod(hashCodeMethod, Object.class);
addProxyMethod(equalsMethod, Object.class);
addProxyMethod(toStringMethod, Object.class);

/*
* Now record all of the methods from the proxy interfaces, giving
* earlier interfaces precedence over later ones with duplicate
* methods.
*/
for (Class<?> intf : interfaces) {
for (Method m : intf.getMethods()) {
addProxyMethod(m, intf);
}
}

/*
* For each set of proxy methods with the same signature,
* verify that the methods' return types are compatible.
*/
for (List<ProxyMethod> sigmethods : proxyMethods.values()) {
checkReturnTypes(sigmethods);
}

/* ============================================================
* Step 2: Assemble FieldInfo and MethodInfo structs for all of
* fields and methods in the class we are generating.
*/
try {
methods.add(generateConstructor());

for (List<ProxyMethod> sigmethods : proxyMethods.values()) {
for (ProxyMethod pm : sigmethods) {

// add static field for method's Method object
fields.add(new FieldInfo(pm.methodFieldName,
"Ljava/lang/reflect/Method;",
ACC_PRIVATE | ACC_STATIC));

// generate code for proxy method and add it
methods.add(pm.generateMethod());
}
}

methods.add(generateStaticInitializer());

} catch (IOException e) {
throw new InternalError("unexpected I/O Exception", e);
}

if (methods.size() > 65535) {
throw new IllegalArgumentException("method limit exceeded");
}
if (fields.size() > 65535) {
throw new IllegalArgumentException("field limit exceeded");
}

/* ============================================================
* Step 3: Write the final class file.
*/

/*
* Make sure that constant pool indexes are reserved for the
* following items before starting to write the final class file.
*/
cp.getClass(dotToSlash(className));
cp.getClass(superclassName);
for (Class<?> intf: interfaces) {
cp.getClass(dotToSlash(intf.getName()));
}

/*
* Disallow new constant pool additions beyond this point, since
* we are about to write the final constant pool table.
*/
cp.setReadOnly();

ByteArrayOutputStream bout = new ByteArrayOutputStream();
DataOutputStream dout = new DataOutputStream(bout);

try {
/*
* Write all the items of the "ClassFile" structure.
* See JVMS section 4.1.
*/
// u4 magic;
dout.writeInt(0xCAFEBABE);
// u2 minor_version;
dout.writeShort(CLASSFILE_MINOR_VERSION);
// u2 major_version;
dout.writeShort(CLASSFILE_MAJOR_VERSION);

cp.write(dout);             // (write constant pool)

// u2 access_flags;
dout.writeShort(accessFlags);
// u2 this_class;
dout.writeShort(cp.getClass(dotToSlash(className)));
// u2 super_class;
dout.writeShort(cp.getClass(superclassName));

// u2 interfaces_count;
dout.writeShort(interfaces.length);
// u2 interfaces[interfaces_count];
for (Class<?> intf : interfaces) {
dout.writeShort(cp.getClass(
dotToSlash(intf.getName())));
}

// u2 fields_count;
dout.writeShort(fields.size());
// field_info fields[fields_count];
for (FieldInfo f : fields) {
f.write(dout);
}

// u2 methods_count;
dout.writeShort(methods.size());
// method_info methods[methods_count];
for (MethodInfo m : methods) {
m.write(dout);
}

// u2 attributes_count;
dout.writeShort(0); // (no ClassFile attributes for proxy classes)

} catch (IOException e) {
throw new InternalError("unexpected I/O Exception", e);
}

return bout.toByteArray();
}

这个方法最终生成了byte[]数组,也就是说,我们已经拥有了一份类的二进制文件了,可以说到这一步,几乎已经完成了所有工作,只需要把这一份二进制文件加载进JVM,就可以得到一个可运行的

Class<?>
对象了。

最后这一步由

defineClass0(loader, proxyName,proxyClassFile, 0, proxyClassFile.length)
实现,这是一个native方法,由JVM实现,具体代码在
ClassLoader.c

// The existence or signature of this method is not guaranteed since it
// supports a private method.  This method will be changed in 1.7.
JNIEXPORT jclass JNICALL
Java_java_lang_ClassLoader_defineClass0(JNIEnv *env,
jobject loader,
jstring name,
jbyteArray data,
jint offset,
jint length,
jobject pd)
{
return Java_java_lang_ClassLoader_defineClass1(env, loader, name, data, offset,
length, pd, NULL);
}

到这里已经获取了

Class<?>
,让我们回到
Proxy.newProxyInstance()
方法,生成代理类已经完成了。接下来是调用

Constructor<?> cons = cl.getConstructor(constructorParams);

获取入参为

InvocationHandler
的构造函数,最后调用构造函数生产代理对象实例。

cons.newInstance(new Object[]{h});

到这,生成动态代理的逻辑已经全部完成。脑子里有没有一定收获,记不记得发生了什么?没有?那不怪你,我也是乱写的。

手动实现一个JDK动态代理

既然原理都已经清晰了,那我们能不能自己实现一个动态代理。为了简单起见,我这里不会直接去写二进制的byte[]数组,因为我不会字节码的结构。这个例子的实现是拼接字符串,然后调用编译器去编译成class文件,再调用

defineClass0(loader, proxyName,proxyClassFile, 0, proxyClassFile.length)
去构造
Class<?>
对象,然后照葫芦画瓢获取它的构造函数,生成一个代理对象。

还是复用这篇文章老生常谈系列之Aop--Spring Aop原理浅析中JDK动态代理里面的代码。

生成动态代理是这行代码

return Proxy.newProxyInstance(calculateService.getClass().getClassLoader(),
calculateService.getClass().getInterfaces(),
new MyInvocationHandler(new CalculateServiceImpl()));

实现MyProxy类

那么我们自己实现一个

MyProxy
类。

首先那就搞个

MyProxy
类,实现
newProxyInstance()
方法。这里的实现很简单,分为以下几步:

  • 根据传入接口,拼接源码,保存为
    .java
    文件。注意:这里我为了简单,只支持传入一个接口(我太懒了)。
  • 调用
    JavaCompiler
    编译源码
  • loader.findClass("$Proxy0")
    加载编译好的class文件
  • 获取构造器生成
    Class
    对象
package io.codegitz.proxy;
/**
* @author Codegitz
* @date 2022/1/14 16:41
**/
public class MyProxy {

/**
* 生成代理对象
* @param loader
* @param interfaces
* @param h
* @return
* @throws IllegalArgumentException
*/
public static Object newProxyInstance(MyClassLoader loader,
Class<?> interfaces,
InvocationHandler h)
throws IllegalArgumentException, IOException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, ClassNotFoundException {
// 根据传入接口获取源代码
String sourceCode = getSourceCode(interfaces);
String path = MyProxy.class.getResource("").getPath();
File file = new File(path+"$Proxy0.java");

FileWriter fw = new FileWriter(file);
fw.write(sourceCode);
fw.close();

// 获取java编译器
JavaCompiler javaCompiler = ToolProvider.getSystemJavaCompiler();
// 标注java文件管理器,用来获取java字节码文件
StandardJavaFileManager manager = javaCompiler.getStandardFileManager(null,null,null);
Iterable iterable = manager.getJavaFileObjects(file);

// 创建task,通过java字节码文件将类信息加载到JVM中
JavaCompiler.CompilationTask task = javaCompiler.getTask(null,manager,null,null,null,iterable);
// 开始执行task
Boolean call = task.call();
// 关闭管理器
manager.close();
Class proxyClass = loader.findClass("$Proxy0");
// 返回被代理后的代理对象
Constructor c = proxyClass.getConstructor(InvocationHandler.class);
return c.newInstance(h);
}

private static String getSourceCode(Class<?> interfaces){
StringBuilder src = new StringBuilder();
src.append("package io.codegitz.proxy;").append("\n")
.append("import java.lang.reflect.Method;").append("\n")
.append("public class $Proxy0 implements ").append(interfaces.getName()).append("{").append("\n")
.append("private java.lang.reflect.InvocationHandler h;").append("\n")
.append("public $Proxy0(java.lang.reflect.InvocationHandler h){").append("\n")
.append("this.h=h;").append("\n")
.append("}").append("\n");

for(Method method:interfaces.getMethods()){
src.append("public ").append(method.getReturnType()).append(" ").append(method.getName()).append("() {").append("\n")
.append("try {").append("\n")
.append("Method m = ").append(interfaces.getName()).append(".class.getMethod(\"").append(method.getName()).append("\");").append("\n")
.append("this.h.invoke(this, m, new Object[]{});").append("\n")
.append("}catch (Throwable e){").append("\n")
.append("e.printStackTrace();").append("\n")
.append("}").append("\n")
.append("}").append("\n");
}
src.append("}");

return src.toString();

}
}

实现MyClassLoader类

定义

MyClassLoader
对象,实现
findClass()
方法,这里主要是为了定制获取自己编译好的class文件,否则直接使用原有的
ClassLoader
是没问题的。

/**
* @author Codegitz
* @date 2022/1/14 17:24
**/
public class MyClassLoader extends ClassLoader {

private String baseDir;

public MyClassLoader() {
this.baseDir = MyClassLoader.class.getResource("").getPath();
}

/**
* 通过类名称加载类字节码文件到JVM中
*
* @param name 类名
* @return 类的Class独享
* @throws ClassNotFoundException
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 获取类名
String className = MyClassLoader.class.getPackage().getName() + "." + name;
if (null == baseDir) {
throw new ClassNotFoundException();
}

// 获取类文件
File file = new File(baseDir, name + ".class");
if (!file.exists()) {
throw new ClassNotFoundException();
}

// 将类文件转换为字节数组
try (
FileInputStream in = new FileInputStream(file);
ByteArrayOutputStream out = new ByteArrayOutputStream();
) {
byte[] buffer = new byte[1024];
int len;
while ((len = in.read(buffer)) != -1) {
out.write(buffer, 0, len);
}

// 调用父类方法生成class实例
return defineClass(className, out.toByteArray(), 0, out.size());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}

测试运行

最后测试方法改成使用自己的方法

public Object getMyProxy() throws IOException, InvocationTargetException, NoSuchMethodException, ClassNotFoundException, InstantiationException, IllegalAccessException {
return MyProxy.newProxyInstance(new MyClassLoader(),calculateService.getClass().getInterfaces()[0],new MyInvocationHandler(new CalculateServiceImpl()));
}

万事俱备只欠东风,搞个方法测试一把,代码如下:

public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles","true");
CalculateService proxy = (CalculateService) new CalculateServiceProxy(new CalculateServiceImpl()).getMyProxy();
proxy.calculate();
}

如图,这里已经实现了自定义返回动态代理。

运行结果如下,跟我们使用原生的JDK动态代理实现了一样的效果。虽然实现得非常简陋,但是原理就是这么个原理。

总结

好了,到这里文章已经结束。我们首先介绍了JDK动态代理的实现逻辑,随后自己动手实现了一个简陋版的JDK动态代理。

这里再回顾一下思路:

  • 根据传入接口拼接源代码,这一步跟我们平时在IDEA写代码是一样的
  • 调用编译器编译代码,这一步跟我们点一下IDEA的build是一样的
  • 加载编译好的class文件,这一步跟我们在IDEA点击运行是一样的
  • 执行逻辑,输出结果

是不是还是很简单的,跟我们平时的操作是一样的,只不过是手动搞了一遍。又水一篇,简简单单。

如果有人看到这里,那在这里老话重提。与君共勉,路漫漫其修远兮,吾将上下而求索。

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: