Java基础-反射
Table of Contents generated with DocToc (opens new window)
# 反射
# 概述 Java Reflection
- Reflection(反射)被视为动态语言的关键,反射机制允许程序在执行期借助于Reflection API取得任何类的内部信息,并能直接操作任意对象的内部属性及方法。
- 加载完类之后,在堆内存的方法区中就产生了一个class类型的对象(一个类只有一个Class对象),这个对象就包含了完整的类的结构信息。我们可以通过这个对象看到类的结构。这个对象就像一面镜子,透过这个镜子看到类的结构,所以,我们形象的称之为:反射。
- 正常方式:引入需要的"包类"名称 -> 通过new实例化—>取得实例化对象
- 反射方式:实例化对象 -> getClass()方法—>得到完整的“包类”名称
# Java反射机制提供的功能
在运行时判断任意一个对象所属的类
在运行时构造任意一个类的对象
在运行时判断任意一个类所具有的成员变量和方法
在运行时获取泛型信息
在运行时调用任意一个对象的成员变量和方法
在运行时处理注解
生成动态代理
简单示例:
/** * @author zdk * @date 2021/10/7 17:38 */ public class Person { public String name; private Integer age; public Person() { } private Person(String name) { this.name = name; } public Person(String name, Integer age) { this.name = name; this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } @Override public String toString() { return "Person{" + "name='" + name + '\'' + ", age=" + age + '}'; } public void show(){ System.out.println("这是一个public方法"); } private String hidden(String str){ System.out.println("这是一个private方法"); return str+str; } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53/** * @author zdk * @date 2021/10/7 17:40 */ public class ReflectionTest { @Test public void test1() throws Exception { Class<Person> personClass = Person.class; //1.通过反射创建Person类的对象 Constructor<Person> constructor = personClass.getConstructor(String.class, Integer.class); Person person = constructor.newInstance("zdk", 120); System.out.println("person = " + person); //2.通过反射,调用对象指定的属性 Field name = personClass.getDeclaredField("name"); //属性要为public才能进行set name.set(person, "张迪凯"); System.out.println("person = " + person); //3.通过反射,调用对象指定的方法 Method show = personClass.getDeclaredMethod("show"); show.invoke(person); System.out.println("--------------------------"); //4.通过反射调用Person类的私有结构:构造器、方法、属性 //4.1 调用私有构造器 Constructor<Person> constructor1 = personClass.getDeclaredConstructor(String.class); constructor1.setAccessible(true); Person person1 = constructor1.newInstance("啧啧啧"); System.out.println("person1 = " + person1); //4.2 调用私有属性 Field age = personClass.getDeclaredField("age"); age.setAccessible(true); age.set(person1, 200); System.out.println("person1 = " + person1); //4.3 调用私有方法 Method hidden = personClass.getDeclaredMethod("hidden", String.class); hidden.setAccessible(true); Object returnValue = hidden.invoke(person1, "测试"); System.out.println("returnValue = " + returnValue); } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# 关于java.lang.Class类的理解
Class实例对应这加载到内存中的一个运行时类
类的加载过程:
- 程序经过javac.exe命令以后,会生成一个或多个字节码文件(.class)。
- 接着我们使用java.exe命令对某个字节码文件进行解释运行。相当于将某个字节码文件加载到内存中。此过程就称为类的加载。
- 加载到内存中的类,我们就称之为运行时类,此运行时类,就作为Class类的一个实例。
- 加载到内存中的运行时类,会缓存一定的时间。在此时间之内,可以通过不同方式来获取此运行时类
加载:将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后生成一个代表这个类的java.lang.Class对象,作为方法区中类数据的访问入口(即引用地址)。所有需要访问和使用类数据只能通过这个Class对象。这个加载的过程需要类加载器参与。
链接:将Java类的二进制代码合并到JVM的运行状态之中的过程。
- 验证:确保加载的类信息符合JVM规范,例如:以cafe开头,没有安全方面的问题
- 准备:正式为类变量(static)分配内存并设置类变量默认初始值的阶段,这些内存都将在方法区中进行分配。
- 解析:虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程。
初始化:
- 执行类构造器< clinit>()方法的过程。类构造器< clinit>()方法是由编译期自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生的。(类构造器是构造类信息的,不是构造该类对象的构造器)。
- 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化。
- 虚拟机会保证一个类的< clinit>()方法在多线程环境中被正确加锁和同步。
获取Class实例的方式(方式三相对使用得多一些)
/** * 获取Class的实例的四种方式 */ @Test public void test2() throws Exception{ //1:调用运行时类的属性 .class Class<Person> personClass = Person.class; System.out.println("personClass = " + personClass); //2:通过运行时类的对象,调用getClass()方法 Person person = new Person(); Class<? extends Person> personClass1 = person.getClass(); System.out.println("personClass1 = " + personClass1); //3:调用Class类的静态方法:forName(String classPath) Class<?> personClass2 = Class.forName("Person"); System.out.println("personClass2 = " + personClass2); //true System.out.println(personClass == personClass1); //true System.out.println(personClass == personClass2); //4:使用类的加载器:ClassLoader ClassLoader classLoader = ReflectionTest.class.getClassLoader(); Class<?> personClass3 = classLoader.loadClass("Person"); //true System.out.println(personClass == personClass3); }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28哪些类型可以有Class对象
- class:外部类,成员(成员内部类、静态内部类),局部内部类、匿名内部类
- interface接口
- []:数组 (只要数组的元素类型和维度都一样,获得的就是同一个Class)
- enum:枚举类
- annotation:注解@interface
- primitive type:基本数据类型
- void
ClassLoader代码示例
@Test public void test3(){ //对于自定义类,使用系统类加载器进行加载 ClassLoader classLoader = ReflectionTest.class.getClassLoader(); System.out.println(classLoader) ; //调用系统类加载器的getParent():获取扩展类加载器 ClassLoader classLoader1 = classLoader.getParent(); System.out.println(classLoader1); //调用扩展类加载器的getParent():无法获取引导类加载器 //引导类加载器主要负责加载java的核心类库,无法加载自定义类的。 ClassLoader classLoader2 = classLoader1.getParent(); System.out.println(classLoader2); ClassLoader classLoader3 = String.class.getClassLoader(); System.out.println(classLoader3 ); }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 使用ClassLoader加载配置文件
代码示例:
@Test
public void test4() throws Exception{
Properties properties = new Properties();
//读取配置文件的方式一: 此时的文件默认路径在当前的module下
FileInputStream fis = new FileInputStream("jdbc.properties");
properties.load(fis);
String username = properties.getProperty("username");
String password = properties.getProperty("password");
System.out.println("username = " + username);
System.out.println("password = " + password);
ClassLoader classLoader = ReflectionTest.class.getClassLoader();
//使用类加载器读取时,默认路径在当前module的src目录下
//如果是maven项目,则默认在src/resources目录下
InputStream is = classLoader.getResourceAsStream("jdbc1.properties");
Properties properties1 = new Properties();
properties1.load(is);
String username1 = properties1.getProperty("username");
String password1 = properties1.getProperty("password");
System.out.println("username1 = " + username1);
System.out.println("password1 = " + password1);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
注:使用类加载器加载配置文件时,默认路径在module的src目录下开始。如果是maven项目,则默认在src/resources目录下
# 通过反射创建运行时类的对象
@Test
public void test1() throws InstantiationException, IllegalAccessException {
Class<Person> personClass = Person.class;
/*
newInstance()内部调用的是类的空参构造器
且构造器修饰符不能为private
所以要想方法正常运行:1.类必须提供空参构造器;2.空参构造器的访问权限足够,通常为public
在javaBean中通常要求提供一个public的空参构造器。原因:
1.便于通过反射创建运行时类的对象
2.便于子类继承此运行时类时,默认调用super()时,保证父类有此构造器
*/
Person person = personClass.newInstance();
System.out.println("person = " + person);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
# 反射的动态性举例
/**
* 反射的动态性:编译时创建的类未知,运行时才创建对应的对象
* @throws Exception
*/
@Test
public void test2() throws Exception{
for (int i = 0; i < 20; i++) {
int num = new Random().nextInt(3);
String classPath = "";
switch (num){
case 0:
classPath = "java.lang.Object";
break;
case 1:
classPath = "java.util.Date";
break;
case 2:
classPath = "Person";
break;
default:
break;
}
Object instance = getInstance(classPath);
System.out.println(instance);
}
}
public Object getInstance(String classPath) throws Exception{
Class<?> clazz = Class.forName(classPath);
return clazz.newInstance();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 获取运行时类的完整结构
import org.junit.Test;
import org.junit.runners.Parameterized;
import java.lang.annotation.Annotation;
import java.lang.reflect.*;
/**
* @author zdk
* @date 2021/10/13 20:18
* 通过反射获取运行时类的完整结构
*/
public class ClassStruct {
@Test
public void test1(){
Class<Person> clazz = Person.class;
//获取类属性结构
//getFields():获取当前运行时类及其父类中声明为public访问权限的属性
Field[] fields = clazz.getFields();
for (Field field : fields) {
System.out.println(field);
}
//getDeclaredFields():获取当前运行时类中声明的所有属性(不区分权限修饰符、不包含父类)
Field[] declaredFields = clazz.getDeclaredFields();
for (Field declaredField : declaredFields) {
System.out.println(declaredField);
}
}
@Test
public void test2(){
Class<Person> clazz = Person.class;
Field[] declaredFields = clazz.getDeclaredFields();
for (Field declaredField : declaredFields) {
//1.权限修饰符
int modifiers = declaredField.getModifiers();
System.out.println(Modifier.toString(modifiers));
//2.数据类型
Class<?> type = declaredField.getType();
System.out.println("type = " + type.getName());
//3.变量名
String name = declaredField.getName();
System.out.println("name = " + name);
}
}
/**
* 获取运行时类的方法
*/
@Test
public void test3(){
Class<Person> clazz = Person.class;
//getMethods():获取当前运行时类及其父类中声明为public访问权限的方法
Method[] methods = clazz.getMethods();
for (Method method : methods) {
System.out.println(method);
}
System.out.println("==========");
//getDeclaredMethods():获取当前运行时类中声明的所有方法(不区分权限修饰符、不包含父类的)
Method[] declaredMethods = clazz.getDeclaredMethods();
for (Method declaredMethod : declaredMethods) {
System.out.println(declaredMethod);
}
}
/**
* 获取运行时类的方法的结构
* 权限修饰符、返回值类型、方法名(参数类型1 形参名1...)
*/
@Test
public void test4(){
Class<Person> clazz = Person.class;
Method[] declaredMethods = clazz.getDeclaredMethods();
for (Method declaredMethod : declaredMethods) {
//1.获取方法声明的注解
Annotation[] annotations = declaredMethod.getAnnotations();
for (Annotation annotation : annotations) {
System.out.println(annotation);
}
//2.获取方法的权限修饰符
System.out.println("权限修饰符"+Modifier.toString(declaredMethod.getModifiers()));
//3.返回值类型
System.out.println("返回值类型"+declaredMethod.getReturnType().getName());
//4.方法名
System.out.println("方法名"+declaredMethod.getName());
System.out.println();
//5.形参列表
System.out.print("(");
Class<?>[] parameterTypes = declaredMethod.getParameterTypes();
if (!(parameterTypes==null&¶meterTypes.length==0)){
for (int i = 0; i < parameterTypes.length; i++) {
System.out.print(parameterTypes[i].getName()+" arg_"+i);
}
}
System.out.print(")");
//6.抛出的异常
System.out.print("(");
Class<?>[] exceptionTypes = declaredMethod.getExceptionTypes();
if (!(exceptionTypes==null&&exceptionTypes.length==0)){
for (int i = 0; i < exceptionTypes.length; i++) {
System.out.print(exceptionTypes[i].getName());
}
}
System.out.print(")");
System.out.println();
}
}
/**
* 获取运行时类的构造器结构
*/
@Test
public void test5(){
Class<Person> clazz = Person.class;
//getConstructors():获取当前运行时类中声明为public访问权限的构造器
Constructor<?>[] constructors = clazz.getConstructors();
for (Constructor<?> constructor : constructors) {
System.out.println(constructor);
}
//getDeclaredConstructors():获取当前运行时类中所有的构造器
Constructor<?>[] declaredConstructors = clazz.getDeclaredConstructors();
for (Constructor<?> declaredConstructor : declaredConstructors) {
System.out.println(declaredConstructor);
}
}
/**
* 获取运行时类的父类及父类的泛型
*/
@Test
public void test6(){
Class<Person> clazz = Person.class;
//获取父类
Class<? super Person> superclass = clazz.getSuperclass();
System.out.println(superclass);
//获取运行时类的 带泛型的父类
Type genericSuperclass = clazz.getGenericSuperclass();
System.out.println("genericSuperclass = " + genericSuperclass);
//获取运行时类的 带泛型的父类的泛型
ParameterizedType parameterizedType = (ParameterizedType) genericSuperclass;
Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
for (Type actualTypeArgument : actualTypeArguments) {
System.out.println("actualTypeArgument = " + actualTypeArgument);
}
}
/**
* 获取运行时类实现的接口
*/
@Test
public void test7(){
Class<Person> clazz = Person.class;
Class<?>[] interfaces = clazz.getInterfaces();
for (Class<?> anInterface : interfaces) {
System.out.println(anInterface);
}
Class<?>[] interfaces1 = clazz.getSuperclass().getInterfaces();
for (Class<?> aClass : interfaces1) {
System.out.println(aClass);
}
}
/**
* 获取运行时类所在的包
*/
@Test
public void test8(){
Class<Person> clazz = Person.class;
Package pack = clazz.getPackage();
System.out.println(pack);
}
/**
* 获取运行时类声明的注解
*/
@Test
public void test9(){
Class<Person> clazz = Person.class;
Annotation[] annotations = clazz.getAnnotations();
for (Annotation annotation : annotations) {
System.out.println(annotation);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
# 调用运行时类中指定的结构:属性、方法、构造器
import org.junit.Test;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
/**
* @author zdk
* @date 2021/10/13 21:28
* 调用运行时类中指定的结构:属性、方法、构造器
*/
public class InvokeClass {
/**
* 调用指定属性
*/
@Test
public void testField() throws Exception{
Class<Person> clazz = Person.class;
//创建运行时类的对象
Person person = clazz.newInstance();
//获取指定的属性 开发中常用getDeclaredField()
Field name = clazz.getField("name");
Field age = clazz.getDeclaredField("age");
//set方法:参数1:指明设置哪个对象的属性 参数2:将此属性设置为多少
name.set(person, "张迪凯");
//因为age是private的,所以需要将非public的属性设为可访问
age.setAccessible(true);
age.set(person, 20);
//get方法:获取哪个对象的当前属性值
Object o = name.get(person);
Object o1 = age.get(person);
System.out.println(o);
System.out.println(o1);
}
/**
* 调用指定方法
*/
@Test
public void testMethod() throws Exception{
Class<Person> clazz = Person.class;
//创建运行时类的对象
Person person = clazz.newInstance();
Method setName = clazz.getDeclaredMethod("setName", String.class);
//invoke():参数1:方法的调用者 参数2:给方法形参赋值的实参
setName.invoke(person, "张迪凯");
System.out.println("person = " + person);
Method display = clazz.getDeclaredMethod("display", String.class, Integer.class);
//display为私有方法 需要使其可访问
display.setAccessible(true);
display.invoke(person, "张迪凯",2);
//调用类的静态方法
Method showStatic = clazz.getDeclaredMethod("showStatic");
showStatic.setAccessible(true);
//invoke方法的返回值即为其所调用的方法的返回值
//如果调用的方法返回void invoke的返回值为null
Object returnValue = showStatic.invoke(Person.class);
//上下两种方式均可 静态属性与之类似
// Object returnValue = showStatic.invoke(null);
System.out.println("returnValue = " + returnValue);
}
/**
* 调用指定构造器
*/
@Test
public void testConstructor() throws Exception{
Class<Person> clazz = Person.class;
//1.getDeclaredConstructor():参数:指明构造器的参数列表
Constructor<Person> constructor = clazz.getDeclaredConstructor(String.class);
//2.保证此构造器可访问
constructor.setAccessible(true);
//3.调用构造器创建运行时类的对象
Person person = constructor.newInstance("zdk");
System.out.println("person = " + person);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# 动态代理 !!!!!
- 动态代理是指客户通过代理类来调用其它对象的方法,并且是在程序运行时根据需要动态创建目标类的代理对象
- 动态代理使用场景:1.调试 2.远程方法调用
- 动态代理相比于静态代理的优点:抽象角色中(接口)声明的所有方法都被转移到调用处理器一个集中的方法中处理,这样,我们可以更加灵活和统一的处理众多的方法。
示例:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
* 接口
*/
interface Human {
/**
* 方法
* @return
*/
String getBelief();
/**
* 方法
* @param food
*/
void eat(String food);
}
/**
* 被代理类
*/
class SuperMan implements Human {
@Override
public String getBelief() {
return "I believe I can fly";
}
@Override
public void eat(String food) {
System.out.println("我喜欢吃"+food);
}
}
class HumanUtil{
public void method1(){
System.out.println("------通用方法1----------");
}
public void method2(){
System.out.println("---------通用方法2--------");
}
}
/**
* 要想实现动态代理,需要解决的问题?
* 1.如何根据加载到内存中的被代理类,动态的创建一个代理类及其对象
* 2.当通过代理类的对象调用方法时,如何动态的去调用被代理类中的同名方法
*/
class ProxyFactory{
/**
* 调用此方法 返回一个代理类的对象。解决问题1
* @param object 被代理类的对象
* @return
*/
public static Object getProxyInstance(Object object){
MyInvocationHandler handler = new MyInvocationHandler();
handler.bind(object);
return Proxy.newProxyInstance(object.getClass().getClassLoader(), object.getClass().getInterfaces(), handler);
}
}
class MyInvocationHandler implements InvocationHandler{
/**
* 需要使用被代理类的对象进行赋值
*/
private Object object;
public void bind(Object object){
this.object = object;
}
/**
* 当我们通过代理类的对象,调用方法a时,就会自动地调用如下的方法:invoke()
* 将被代理类要执行的方法a的功能就声明在invoke()中
* @param proxy
* @param method
* @param args
* @return
* @throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
HumanUtil humanUtil = new HumanUtil();
humanUtil.method1();
//method:即为代理类对象调用的方法,此方法也就座位了被代理类对象要调用的方法
Object returnValue = method.invoke(object, args);
//上述方法的返回值就作为当前类中的invoke()方法的返回值
humanUtil.method2();
return returnValue;
}
}
/**
* @author zdk
* @date 2021/10/16 9:43
*/
public class DynamicProxyTest {
public static void main(String[] args) {
SuperMan superMan = new SuperMan();
Human proxyInstance = (Human) ProxyFactory.getProxyInstance(superMan);
String belief = proxyInstance.getBelief();
System.out.println("belief = " + belief);
proxyInstance.eat("饭");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
在 GitHub 上编辑此页 (opens new window)
最后更新: 2022/10/04, 16:10:00