深入了解Java反射
类加载器
类加载器结构

BootstrapClassLoader:启动类类加载器由C++实现,在代码中获取使用它加载的类的类加载器(即执行getClassLoader()方法)会返回null。它用来加载<JAVA_HOME>/jre/lib路径,-Xbootclasspath参数指定的路径以<JAVA_HOME>/jre/classes中的类。ExtClassLoader:拓展类类加载器,它用来加载<JAVA_HOME>/jre/lib/ext路径以及java.ext.dirs系统变量指定的类路径下的类。AppClassLoader:应用程序类类加载器,它主要加载应用程序ClassPath下的类(包含jar包中的类)。它是java应用程序默认的类加载器。- 用户自定义类加载器:用户根据自定义需求,自由的定制加载的逻辑,继承
AppClassLoader,仅仅覆盖findClass()时将继续遵守双亲委派模型,也可以同时覆盖loadClass()方法破坏双亲委派模型。 ThreadContextClassLoader:线程上下文加载器,它不是一个新的类型,更像一个类加载器的角色,ThreadContextClassLoader可以是上述类加载器的任意一种,但往往是AppClassLoader,作用我们后面再说。
双亲委派机制
概述原理
双亲委派模型:在使用子级类加载器时,子级会首先调用父级的
loadClass()方法来尝试加载类,加载失败后才会使用子级的类加载器进行加载,由此每当加载类时,都会使用最顶级的类加载器来首先尝试加载类。

那么,为什么要使用双亲委派机制呢?
在加载一个类时优先委派给父类加载器,这样保证不会出现类被重复加载,也保证了java一些基础类可以稳定的存在,不会被用户自定义类顶替掉。
双亲委派机制的缺陷
双亲委派模型并不是完美的,在一些场景下会出现一些比较难解决的问题,举个例子,在使用 SPI 的时候, ServiceLoader 是通过 BootStrap 类加载器加载的,在执行到加载用户编写的扩展类的时候,如果使用当前类的类加载器,是肯定无法加载到用户编写的类的,这个时候就无法继续执行了,所以这个时候就需要使用 Thread 的上下文类加载器,查看源码的时候我们就发现,在用户不主动传递 ClassLoader 的时候,会获取当前上下文类加载器,这样应用程序才能正常的执行。
- 为了理解上面这段话,首先先用代码测试一下这个
ServiceLoader类到底使用什么类加载器加载的。

ServiceLoader 类确实是由 BootStrap 类加载器加载的,上面所说的上下文加载器则在ServiceLoader 源码中可以找到。
public static <S> ServiceLoader<S> load(Class<S> service) {
// 获取上下文加载器,使用此加载器来加载指定类
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
- 为什么需要使用获取到的上下文类加载器来加载指定的类呢?
程序运行过程中要用到的类,通过当前类加载器的自动加载,加载不到(不在当前类加载器的类资源管辖范围),如果要使用这个类,必须指定一个能够加载这个类的加载器去加载,而怎么获取这个加载器是个问题。
程序都是在线程中执行,那么从线程的上下文中去拿最合理,所以就诞生了线程上下文类加载器,这个加载器的是非自动加载,即通过 forName 或者 loadClass 的方式去加载类。
当前 ServiceLoader 类是由 BootStrap 类加载器进行加载的,在上方描述中已有解释,此类加载器只能加载<JAVA_HOME>/jre/lib 路径,-Xbootclasspath参数指定的路径以<JAVA_HOME>/jre/classes中的类。因此要想在高层中加载类(或实例化)低层的类时,我们就需要获取上下文类加载器或者使用指定的类加载器来加载当前类。
引用—— SPI与线程上下文类加载器
Launcher 类
在虚拟机启动的时候会初始化BootstrapClassLoader,然后在Launcher类中去加载ExtClassLoader、AppClassLoader,并将AppClassLoader的parent设置为ExtClassLoader,并设置线程上下文类加载器。
sun.misc.Laucher 类是 java 的入口,在启动 java 应用的时候会首先创建 Launcher 类,创建 Launcher 类的时候会准备应用程序运行中需要的类加载器。
Launcher 作为 JAVA 应用的入口,根据双亲委派模型,Laucher是由JVM创建的,它类加载器应该是BootStrapClassLoader 。

因为
BootStrapClassLoader是由c++实现,在java中无法获取到对象,所以返回null。
- 因此我们查看一个普通类的类加载及其父类加载器分别是什么

由此可以看到类加载器的层级关系为 AppClassLoader < ExtClassLoader < BoorStrapClassLoader
- 那我们在
ServiceLoader获取的上下文类加载器到底是这三个加载器中的哪一个呢?
那就需要看一下加载 main 方法的 Launcher 类中的构造方法了。
public Launcher() {
Launcher.ExtClassLoader var1;
try {
// 获取到ext类加载器
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
// 将ext类加载器作为父加载器获取到app类加载器
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
// 将当前线程的上下文加载器设置为 AppClassLoader
Thread.currentThread().setContextClassLoader(this.loader);
// other code ...
}
可以看到先获取 ExtClassLoader 然后作为父加载器获取到 AppClassLoader 最后将 AppClassLoader 设置为当前线程的上下文加载器。由此可知,在 ServiceLoader 类中获取到的上下文类加载器即为 AppClassLoader 。
现在又引出一个问题——
SPI和ServiceLoader到底是什么?用来做什么的?
SPI
为什么使用 SPI
面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候不用在程序里动态指明,这就需要一种服务发现机制。 java spi 就是提供这样的一个机制:为某个接口寻找服务实现的机制。这有点类似 IOC 的思想,将装配的控制权移到了程序之外。
SPI 概述
现在我们的程序分为实现方(服务)和使用方(客户),在这两方中引入“接口”。此时这个接口有三种情况,一种即是接口归实现方,接口是实现方中的一部分;另一种则是接口归使用方,接口是使用方的一部分;最后即是接口处于独立的包中。
- 当接口归实现方

看起来很眼熟,这不就是 API (Application Programming Interface) 。而 API 有以下特点:
- 概念上接近实现方
- 位于实现方所在的包中
- 实现和接口在一个包中
- 当接口归使用方

对于类似这种情况的接口将其称为 SPI (service provider interface) , SPI 的规则如下:
- 概念上更依赖调用方。
- 组织上位于调用方所在的包中。
- 实现位于独立的包中 (也可以认为在实现方包中)。
- 常见的例子是:插件模式的插件。
- 接口是独立的包
如果一个“接口”在一个上下文是 API ,在另一个上下文是 SPI ,就可以成为一个独立的包,不管是 SPI 或 API ,接口都是可以组织到独立的“包”中。
SPI 实现
概念通常都十分晦涩,手写一个 SPI 接口用于加深记忆。
需求:实现一个搜索功能,分别可以从文件和数据库中搜索文件。
- 实现方
接口类 Search
public interface Search {
public List<String> searchDoc(String keyword);
}
实现类 FileSearch
public class FileSearch implements Search{
@Override
public List<String> searchDoc(String keyword) {
System.out.println("file search by keyword :"+keyword);
return null;
}
}
实现类 DatabaseSearch
public class DatabaseSearch implements Search {
@Override
public List<String> searchDoc(String keyword) {
System.out.println("database search by keyword :" + keyword);
return null;
}
}
resources 文件夹下创建文件 META-INF/services ,在创建好的文件夹创建一个名为搜索接口全限定类名的文件,在其中写入具体我们要使用的实现类的全限定类名。

文件内容
com.mochen.advance.spi.FileSearch
至此,实现方的类和方法已经完成
- 调用方
直接使用测试方法进行测试
public static void main(String[] args) {
// 加载指定接口类
ServiceLoader<Search> service = ServiceLoader.load(Search.class);
Iterator<Search> iterator = service.iterator();
// 遍历实现的类
while (iterator.hasNext()){
Search next = iterator.next();
// 指定接口类中的方法
next.searchDoc("MoChen");
}
}
- 测试结果

当使用在
resources下的配置文件中加入多个实现类,此处也会循环执行多次。
- 为什么要在
resources下创建指定文件夹?

此路径是直接写死在
ServiceLoader中的。
DriverManager 自动注册驱动
说到 SPI 的实现,最常见的就是 MySql JDBC 的自动注册驱动了,自动注册是如何实现的?
打开 java.sql.DriverManager

可以看到静态方法中执行了, loadInitialDrivers() ,这个方法就是用于注册驱动使用。
private static void loadInitialDrivers() {
String drivers;
// 在参数中获取驱动类名
try {
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
// 使用ServiceLoader自动加载驱动
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {}
return null;
}
});
println("DriverManager.initialize: jdbc.drivers = " + drivers);
if (drivers == null || drivers.equals("")) {
return;
}
// 当获取到的驱动不为空时,初始化驱动类
String[] driversList = drivers.split(":");
println("number of Drivers:" + driversList.length);
for (String aDriver : driversList) {
try {
println("DriverManager.Initialize: loading " + aDriver);
Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}
}
中间的 ServiceLoader 即是自动加载驱动的核心代码,自动加载驱动的类名可以在 classpath 下找到。

com.mysql.cj.jdbc.Driver
Security Manager
在上面的很多源码中都出现了 SecurityManager 这个类,这个类是用来做什么的,有什么作用?
什么是 SecurityManager ?
SecurityManager 在 Java 中被用来检查应用程序是否能访问一些有限的资源,例如文件、套接字 (socket) 等等。它可以用在那些具有高安全性要求的应用程序中。通过打开这个功能, 我们的系统资源可以只允许进行安全的操作。
在 Java 中有很多操作都是有风险的操作,而为了限定这些风险并实现最小权限安全原则 (least privilege security principle) ,需要使用 SercurityManager 进行配置。
常见风险操作:
Thread.stop()Socket.bind()System.getProperty()- 等等.
如何使用 SecurityManager
默认情况下,SecurityManager 是关闭的,可以使用以下方法用来打开 SecurityManager 。
JVM启动参数:-Djava.security.manager <class_name>
此参数的 <class_name> 可以留空或者使用 default 用来启动默认的安全管理器,或者直接指定类来启动。既然指定了启动参数,那这个 SecurityManager 究竟是在哪里被启动的?
还记得 Launcher 类吗?那个用来启动 main 方法的类。
public Launcher() {
Launcher.ExtClassLoader var1;
try {
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
Thread.currentThread().setContextClassLoader(this.loader);
// 获取参数内容
String var2 = System.getProperty("java.security.manager");
// 获取到的值不为空,也就是配置了 java.security.manager 参数
if (var2 != null) {
SecurityManager var3 = null;
// 当配置的值不为空字符串或者default时,则会使用ClassLoader加载配置的类,并实例化
if (!"".equals(var2) && !"default".equals(var2)) {
try {
var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
} catch (IllegalAccessException var5) {
} catch (InstantiationException var6) {
} catch (ClassNotFoundException var7) {
} catch (ClassCastException var8) {
}
} else {
// 使用默认的安全管理器
var3 = new SecurityManager();
}
if (var3 == null) {
throw new InternalError("Could not create SecurityManager: " + var2);
}
// 设置安全管理器
System.setSecurityManager(var3);
}
}
可以从上面的源码看出,
Launcher在初始化完成ClassLoader之后尝试获取我们配置的java.security.manager的内容,当获取到的参数不为空时则会开启SecurityManager。
获取到的值为default或者空字符串是会使用系统默认的SercurityManager,否侧则会实例化指定的类作为安全管理器使用。
- 在程序中打开
SecurityManager
其实经过我们上面的源码分析可以得知,直接配置参数其实也是通过在程序中执行相关的方法来打开安全管理器。将上面代码中的主要部分提取出来,我们就可以得到打开 SecurityManager 的方法。
// 打开
SecurityManager sm=new SecurityManager();
System.setSecurityManager(sm);
// 关闭
SecurityManager sm=System.getSecurityManager();
if(sm!=null){
System.setSecurityManager(null);
}
以上就是代码开关 SecurityManager 的方法。
注意:使用在程序中开关
SercurityManager只有在配置文件java.policy中添加permission java.lang.RuntimePermission "setSecurityManager";才可以生效。
Apache Ant™配置
这种方式只有在使用 Ant 进行构建时才可以生效,在 ant 配置文件中写入以下配置项。
<sysproperty key="java.security.manager" value="" />
SecurityManager 配置文件
在上面提到了 SercurityManager 的配置文件, SercurityManager 默认配置文件位于 <JAVA_HOME>/jre/lib/security/java.policy ,当然,这个配置文件也是可以使用参数指定的。
- 在默认配置文件上添加一个配置文件,用于增加更多权限配置
-Djava.security.policy=<file_path>
- 替换默认的配置文件
1 1-Djava.security.policy==<file_path>
单等号是添加,双等号是替换
java.policy 简述
可以看到在配置文件开头有以下配置
grant codeBase "file:${{java.ext.dirs}}/*" {
permission java.security.AllPermission;
};
grant codeBase 用来配置那些代码可以被执行
grant {
// Allows any thread to stop itself using the java.lang.Thread.stop()
// method that takes no argument.
// Note that this permission is granted by default only to remain
// backwards compatible.
// It is strongly recommended that you either remove this permission
// from this policy file or further restrict it to code sources
// that you specify, because Thread.stop() is potentially unsafe.
// See the API specification of java.lang.Thread.stop() for more
// information.
permission java.lang.RuntimePermission "stopThread";
// allows anyone to listen on dynamic ports
permission java.net.SocketPermission "localhost:0", "listen";
// "standard" properies that can be read by anyone
permission java.util.PropertyPermission "java.version", "read";
// ...
}
grant 用来配置详细的权限配置
实践
开启 SecurityManager

当开启 SecurityManager 之后,测试代码会运行错误。
@Test
public void testServiceLoader(){
ClassLoader classLoader = ServiceLoader.class.getClassLoader();
System.out.println(classLoader);
}
在开启之后,安全权限检查就已经生效,在一些风险操作时就需要进行权限检查,当没有指定的权限,就会报错。

此时,我们在配置文件中添加报错的权限,再次运行,以此直到无报错,就实现了最小权限安全原则 (least privilege security principle)

运行成功

由此可见, SecurityManager 可以大幅度提高 Java 代码的安全性,尤其是当前反射等技术大幅度使用导致安全性问题大大增加的情况下。但是这个功能在默认状态下是不会启用的,配置也较为繁琐,导致它并不常用。
- 引用
A Java geek—The Java Security Manager: why and how?
AccessController.doPrivileged的作用
反射
- 一段简单的示例代码
try {
Class<TargetClass> tc = TargetClass.class;
// 获取无参构造函数
Constructor<TargetClass> c = tc.getDeclaredConstructor();
// 关闭访问权限检查
c.setAccessible(true);
// 使用私有无参构造实例化类
TargetClass targetClass = c.newInstance();
// 获取方法
Method pm = tc.getMethod("publicMethod", String.class);
// 执行方法并传值
String result = (String) pm.invoke(targetClass, "test");
System.out.println(result);
// 获取属性
Field publicParam = tc.getField("publicParam");
// 获取属性内容
String param = (String) publicParam.get(targetClass);
System.out.println(param);
} catch (Exception e) {
e.printStackTrace();
}
Class 类
一个 .java 文件在经过编译后会成为一个 .class 文件,在这个字节码文件中会储存有关这个类的所有信息,在使用反射时我们会将字节码文件赋值给 Class 类,因此 Class 类中应该储存着指定类的所有信息。

上面的代码就是 Class 类中储存的类的所有内容,包括 declared (全部内容)和 public (公开内容)。属性、方法和构造方法分别在 Field/Method/Constructor 类中储存。
Class 类的 newInstance 方法
@CallerSensitive
public T newInstance() throws InstantiationException, IllegalAccessException
{
// Constructor lookup
if (cachedConstructor == null) {
// 当调用 newInstance 的类是 Class 时,抛出错误
if (this == Class.class) {
throw new IllegalAccessException(
"Can not call newInstance() on the Class for java.lang.Class"
);
}
try {
Class<?>[] empty = {};
// 获取所有的构造方法
final Constructor<T> c = getConstructor0(empty, Member.DECLARED);
// 关闭权限检测
java.security.AccessController.doPrivileged(
new java.security.PrivilegedAction<Void>() {
public Void run() {
c.setAccessible(true);
return null;
}
});
cachedConstructor = c;
} catch (NoSuchMethodException e) {
throw (InstantiationException)
new InstantiationException(getName()).initCause(e);
}
}
Constructor<T> tmpConstructor = cachedConstructor;
// 检测权限,只可以调用公开无参构造
int modifiers = tmpConstructor.getModifiers();
if (!Reflection.quickCheckMemberAccess(this, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
if (newInstanceCallerCache != caller) {
Reflection.ensureMemberAccess(caller, this, null, modifiers);
newInstanceCallerCache = caller;
}
}
// Run constructor
try {
// 调用 Constructor 类中的 newInstance 方法
return tmpConstructor.newInstance((Object[])null);
} catch (InvocationTargetException e) {
Unsafe.getUnsafe().throwException(e.getTargetException());
// Not reached
return null;
}
}
private volatile transient Constructor<T> cachedConstructor;
private volatile transient Class<?> newInstanceCallerCache;
根据 Class 类 newInstance 方法实例化对象的源码,其实本质上调用的还是 Constructor 类中的 newInstance ,最后调用 native 方法实现实例化。
获取构造函数方法的逻辑。
private Constructor<T> getConstructor0(Class<?>[] parameterTypes, int which)
throws NoSuchMethodException {
// 获取构造方法 传入true为只获取公共构造
Constructor<T>[] constructors = privateGetDeclaredConstructors((which == Member.PUBLIC));
// 遍历获取到的构造方法
for (Constructor<T> constructor : constructors) {
// 对比需要的构造函数的参数类型和获取到的参数类型
if (arrayContentsEq(parameterTypes, constructor.getParameterTypes())) {
// 本质执行的是 Constructor 类的 copy 方法
return getReflectionFactory().copyConstructor(constructor);
}
}
// 没有获取到指定参数类型的构造方法
throw new NoSuchMethodException(getName() + ".<init>" + argumentTypesToString(parameterTypes));
}
这个方法其实就是获取到所有的构造方法,并逐一遍历直到找到相应参数类型的构造函数并返回,再查看一下 privateGetDeclaredConstructors 方法是如何拿到构造函数对象的。
private Constructor<T>[] privateGetDeclaredConstructors(boolean publicOnly) {
checkInitted();
Constructor<T>[] res;
// 获取反射数据
ReflectionData<T> rd = reflectionData();
if (rd != null) {
// 只获取公开构造或者获取所有构造
res = publicOnly ? rd.publicConstructors : rd.declaredConstructors;
if (res != null) return res;
}
// No cached value available; request value from VM
if (isInterface()) { // 当类为接口类时
@SuppressWarnings("unchecked")
// 直接创建一个构造对象返回
Constructor<T>[] temporaryRes = (Constructor<T>[]) new Constructor<?>[0];
res = temporaryRes;
} else {
// 获取所有构造 native 方法
res = getDeclaredConstructors0(publicOnly);
}
if (rd != null) {
// 将获取到的构造对象赋值到rd中
if (publicOnly) {
rd.publicConstructors = res;
} else {
rd.declaredConstructors = res;
}
}
return res;
}
reflectionData() 方法是获取缓存的已有数据,当获取到数据时直接获取其中的指定构造对象直接返回。如果没有获取到 ReflectionData 或者指定的构造,程序会先判断当前类是否是接口类,如果为接口类则直接 new 一个构造后返回,否则则会使用 native 方法获取构造函数。
由此可知, Class 对象的 newInstance 方法只可以用来调用公共无参构造来进行实例化。若想使用私有无参构造实例化则必须直接使用 Constructor 对象的 newInstance 方法,并将访问权限检测关闭。
Class<TargetClass> tc = TargetClass.class;
// 获取无参构造函数
Constructor<TargetClass> c = tc.getDeclaredConstructor();
// 关闭访问权限检查
c.setAccessible(true);
// 使用私有无参构造实例化类
TargetClass targetClass = c.newInstance();
setAccessible() 方法
public void setAccessible(boolean flag) throws SecurityException {
SecurityManager sm = System.getSecurityManager();
if (sm != null) sm.checkPermission(ACCESS_PERMISSION);
setAccessible0(this, flag);
}
private static void setAccessible0(AccessibleObject obj, boolean flag)
throws SecurityException
{
if (obj instanceof Constructor && flag == true) {
Constructor<?> c = (Constructor<?>)obj;
if (c.getDeclaringClass() == Class.class) {
throw new SecurityException("Cannot make a java.lang.Class" +
" constructor accessible");
}
}
obj.override = flag;
}
其实就是将 override 属性改为 true 。
Constructor 对象 newInstance() 方法
@CallerSensitive
public T newInstance(Object ... initargs)
throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException
{
// 访问检查,当 override 为 true 时跳过
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, null, modifiers);
}
}
// other code ...
}
反射执行方法
在上方示例代码中以下两行就是用来获取方法并执行的。
// 获取方法
Method pm = tc.getMethod("publicMethod", String.class);
// 执行方法并传值
String result = (String) pm.invoke(targetClass, "test");
getMethod 方法源码
```java
@CallerSensitive
public Method getMethod(String name, Class<?>... parameterTypes)
throws NoSuchMethodException, SecurityException {
// 检查是否有访问权限
checkMemberAccess(Member.PUBLIC, Reflection.getCallerClass(), true);
// 使用名称和参数类型获取到指定的方法对象
Method method = getMethod0(name, parameterTypes, true);
if (method == null) {
// 获取到的方法对象为空,抛出异常
throw new NoSuchMethodException(getName() + "." + name + argumentTypesToString(parameterTypes));
}
// 返回获取到的方法对象
return method;
}
```
首先分析 `getMethod` 方法,此方法需要两个参数,一个是方法名另一个是方法参数的对象类型。使用名称可以定位到指定的方法,但是方法有可能会有重载,所以需要使用参数对象来定位到准确的方法。
而获取方法的本质其实是遍历 `ReflectionData` 中的 `declaredPublicMethods` 或 `declaredMethods` 属性。

执行方法使用的是方法对象的 `invoke` 方法,为什么在执行此方法时需要传入一个已经实例化的对象,这个问题和 `JVM` 对象实例化有关。

在实例化对象时,我们可能会创建一个类的多个实例化对象,而每个实例化的对象中的属性是独立的部分,在调用方法时必须告诉方法需要使用哪一个实例化对象的属性,所以需要在 `invoke` 方法中传入已经实例化对象的引用。当然,我们在使用非反射的普通方式去调用对象的方法时也会隐形的传递当前实例化的对象。
所以,最好把Method理解为方法执行指令吧,它更像是一个方法执行器,必须告诉它要执行的对象(数据)。当然,如果是invoke一个静态方法,不需要传入具体的对象,因为静态方法是属于类的不属于对象。
引用:[能解答一切的答案——反射](https://www.yuque.com/books/share/2b434c74-ed3a-470e-b148-b4c94ba14535/xswa26#czOZ0)

浙公网安备 33010602011771号