Java高级工程师面试:Java中的反射机制的理解!反射机制的使

2021-06-20  本文已影响0人  攻城狮Chova
655dc10bbf2d4a058f8c45663a33f604.jpg

反射的概念

反射的原理

Class clazz = ClassLoader.getSystemClassLoader().loadClass("com.mypackage.MyClass");

ClassLoader类根据类的完全限定名加载类并返回一个Class对象

// 标记是否使用缓存,可以通过外部的sun.reflect.noCaches配置是否禁用缓存
private static boolean useCaches = true;

static class ReflectionData<T> {
    volatile Field[] declaredFields;
    volatile Field[] publicFields;
    volatile Method[] declaredMethods;
    volatile Method[] publicMethods;
    volatile Constructor<T>[] declaredConstructors;
    volatile Constructors<T>[] publicConstructors;
    volatile Field[] declaredPublicFields;
    volatile Method[] declaredPublicMethods;
    final int redefinedCount;

    ReflectionData(int redefinedCount) {
        this.redefinedCount = redefinedCount;
    }
}
    
    // 这个是SoftReference,在内存资源紧张的时候可能会被回收
    // volatile保证多线程环境下读写的正确性
     private volatile transient SoftReference<RefelectionData<T>> reflectionData;

    // 这个主要用于和ReflectionData中的redefinedCount进行比较
    // 如果两个值不相等,说明ReflectionData缓存的数据已经过期了
    private volatile transient classRedefinedCount = 0;

反射的主要用途

      <action name="login"
               class="org.ScZyhSoft.test.action.SimpleLoginAction"
               method="execute">
           <result>/shop/shop-index.jsp</result>
           <result name="error">login.jsp</result>
       </action>

反射的运用

获得Class对象

public static Class<?> forName(String className);



/* 在JDBC中使用这个方法加载数据库驱动 */
Class.forName(driver);
Class<?> klass=int.class;
Class<?> classInt=Integer.TYPE;
StringBuilder str=new StringBuilder("A");
Class<?> klass=str.getClass();

判断是否是某个类的实例

public native boolean isInstance(Object obj);

创建实例

通过反射生成对象的实例主要有两种方式:

Class<?> c = String.class;
Object str = c.newInstance();
/* 获取String所对应的Class对象 */
Class<?> c=String.class;

/* 获取String类带一个String参数的构造器 */
Constructor constructor=c.getConstructor(String.class);

/* 根据构造器创建实例 */
Object obj=constructor.newInstance("abc");
System.out.println(obj);

获取方法

获取Class对象的方法集合,主要有三种方法:

public Method[] getDeclaredMethods() throws SecurityException {}
public Method[] getMethods() throws SecurityException {}
public Method getMethod(String name,Class<?>... parameterType) {}
public class MethodTest {
    public static void methodTest() throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
        Class<?> c = methodClass.class;
        Object object = c.newInstance();
        Method[] methods = c.getMethods();
        Method[] declaredMethods = c.getDeclaredMethods();
        // 获取methodClass类中的add方法
        Method method = c.getMethod("add", int.class, int.class);
        // getMethods()方法获取的所有方法
        System.out.println("getMethods获取的方法:");
        for (Method m:methods)
            System.out.println(m);
        // getDeclaredMethods()方法获取的所有方法
        System.out.println("getDeclaredMethods获取的方法:");
        for (Method m:declaredMethods)
            System.out.println(m);
    }
}

class methodClass {
    public final int n = 3;
    
    public int add(int a, int b) {
        return a + b;
    }
    
    public int sub(int a, int b) {
        return a * b;
    }
}

程序运行结果:

getMethods获取的方法:
public int org.ScZyhSoft.common.methodClass.add(int,int)
public int org.ScZyhSoft.common.methodClass.sub(int,int)
public final void java.lang.Object.wait() throws java.lang.InterruptedException
public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException
public boolean java.lang.Object.equals(java.lang.Object)
public java.lang.String java.lang.Object.toString()
public native int java.lang.Object.hashCode()
public final native java.lang.Class java.lang.Object.getClass()
public final native void java.lang.Object.notify()
public final native void java.lang.Object.notifyAll()
getDeclaredMethods获取的方法:
public int org.ScZyhSoft.common.methodClass.add(int,int)
public int org.ScZyhSoft.common.methodClass.sub(int,int)

通过getMethods() 获取的方法可以获取到父类的方法

获取构造器信息

public T newInstance(Objec ... initargs)

newInstance方法可以根据传入的参数来调用对应的Constructor创建对象的实例

获取类的成员变量信息

调用方法

public Object invoke(Object obj, Object ... args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {}
public class InvokeTest {
    public static void main(String[] args) throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
        Class<?> klass = method.class;
        // 创建methodClass的实例
        Object obj = klass.newInstance();
        // 获取methodClass的add方法
        Method method = klass.getMethod("add", int.class, int.class);
        // 调用method对应的方法,实现add(1,4)
        Object result = method.invoke(obj, 1, 4);
        System.out.println(result);
    }
}

class methodClass {
    public final int n = 3;
    public int add(int a, int b) {
        return a + b;
    }
    public int sub(int a,int b) {
        return a * b;
    }
}

利用反射创建数组

public static void ArrayTest() throws ClassNotFoundException {
    Class<?> cls = class.forName("java.lang.String");
    Object array = Array.newInstance(cls, 25);
    // 在数组中添加数据
    Array.set(array, 0, "C");
    Array.set(array, 1, "Java");
    Array.set(array, 2, "Python");
    Array.set(array, 3, "Scala");
    Array.set(array, 4, "Docker");
    // 获取数据中的某一项内容
    System.out.println(Array.get(array, 3));
}

Array类是java.lang.reflect.Array类,通过Array.newInstance() 创建数组对象:

public static Object newInstance(Class<?> componentType, int length) throws NegativeArraySizeException {
    return newArray(componentType, length);
}

newArray方法是一个native方法,具体实现在HotSpot JVM中,源码如下:

private static native Object newArray(Class<?> componentType, int length) throws NegativeArraySizeException;


------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


- newArray源码目录: openjdk\hotspot\src\share\vm\runtime\reflection.cpp

arrayOop Reflection::reflect_new_array(oop element_mirror, jint length, TRAPS) {
  if (element_mirror == NULL) {
    THROW_0(vmSymbols::java_lang_NullPointerException());
  }
  if (length < 0) {
    THROW_0(vmSymbols::java_lang_NegativeArraySizeException());
  }
  if (java_lang_Class::is_primitive(element_mirror)) {
    Klass* tak = basic_type_mirror_to_arrayklass(element_mirror, CHECK_NULL);
    return TypeArrayKlass::cast(tak)->allocate(length, THREAD);
  } else {
    Klass* k = java_lang_Class::as_Klass(element_mirror);
    if (k->oop_is_array() && ArrayKlass::cast(k)->dimension() >= MAX_DIM) {
      THROW_0(vmSymbols::java_lang_IllegalArgumentException());
    }
    return oopFactory::new_objArray(k, length, THREAD);
  }
}

invoke方法

java.lang.NullPointerException
  at ......
  at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
  at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
  at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
  at java.lang.reflect.Method.invoke(Method.java:497)

invoke执行过程

@CallSensitive
public Object invoke(Object obj, Object ... args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
    if (!override) {
        if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
            Class<?> caller = Refelection.getCallerClass();
            checkAccess(caller, clazz, obj, modifiers);
        }
    }
    MethodAccessor ma = methodAccessor;
    if (ma == null) {
        ma = acquireMethodAccessor();
    }
    return ma.invoke(obj, args);
}

权限检查

  1. 首先用Reflection.quickCheckMemberAccess(clazz, modifiers) 方法检查方法是否为public
    1.1 如果是public方法的话,就跳过本步
    1.2 如果不是public方法的话,就用Reflection.getCallerClass()方法获取调用这个方法的Class对象,这是一个native方法
@CallerSensitive
    public static native Class<?> getCallerClass();

-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


- 在OpenJDK中可以找到getCallerClass方法的JNI入口-Reflection.c

JNIEXPORT jclass JNICALL Java_sun_reflect_Reflection_getCallerClass__
(JNIEnv *env, jclass unused)
{
    return JVM_GetCallerClass(env, JVM_CALLER_DEPTH);
}

---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------



- JVM_GetCallerClass的源码位于jvm.cpp中

VM_ENTRY(jclass, JVM_GetCallerClass(JNIEnv* env, int depth))
  JVMWrapper("JVM_GetCallerClass");
  // Pre-JDK 8 and early builds of JDK 8 don't have a CallerSensitive annotation; or
  // sun.reflect.Reflection.getCallerClass with a depth parameter is provided
  // temporarily for existing code to use until a replacement API is defined.
  if (SystemDictionary::reflect_CallerSensitive_klass() == NULL || depth != JVM_CALLER_DEPTH) {
    Klass* k = thread->security_get_caller_class(depth);
    return (k == NULL) ? NULL : (jclass) JNIHandles::make_local(env, k->java_mirror());
  }
  // Getting the class of the caller frame.
  //
  // The call stack at this point looks something like this:
  //
  // [0] [ @CallerSensitive public sun.reflect.Reflection.getCallerClass ]
  // [1] [ @CallerSensitive API.method                                   ]
  // [.] [ (skipped intermediate frames)                                 ]
  // [n] [ caller                                                        ]
  vframeStream vfst(thread);
  // Cf. LibraryCallKit::inline_native_Reflection_getCallerClass
  for (int n = 0; !vfst.at_end(); vfst.security_next(), n++) {
    Method* m = vfst.method();
    assert(m != NULL, "sanity");
    switch (n) {
    case 0:
      // This must only be called from Reflection.getCallerClass
      if (m->intrinsic_id() != vmIntrinsics::_getCallerClass) {
        THROW_MSG_NULL(vmSymbols::java_lang_InternalError(), "JVM_GetCallerClass must only be called from Reflection.getCallerClass");
      }
      // fall-through
    case 1:
      // Frame 0 and 1 must be caller sensitive.
      if (!m->caller_sensitive()) {
        THROW_MSG_NULL(vmSymbols::java_lang_InternalError(), err_msg("CallerSensitive annotation expected at frame %d", n));
      }
      break;
    default:
      if (!m->is_ignored_by_security_stack_walk()) {
        // We have reached the desired frame; return the holder class.
        return (jclass) JNIHandles::make_local(env, m->method_holder()->java_mirror());
      }
      break;
    }
  }
  return NULL;
JVM_END
  1. 获取Class对象caller后使用checkAccess方法进行一次快速的权限校验 ,checkAccess方法实现如下:
volatile Object securityCheckCache;

    void checkAccess(Class<?> caller, Class<?> clazz, Object obj, int modifiers) throws IllegalAccessException {
        if(caller == clazz){    // 快速校验
            return;             // 权限通过校验
        }
        Object cache = securityCheckCache;  // 读取volatile
        Class<?> targetClass = clazz;
        if (obj != null && Modifier.isProtected(modifiers) && ((targetClass = obj.getClass()) != clazz)) {  // 必须匹配caller,targetClass中的一个
            if (cache instanceof Class[]) {
                Class<?>[] cache2 = (Class<?>[]) cache;
                if (cache2[1] == targetClass && cache[0] == caller) {
                    return;     // 校验通过
                }
            }
        } else if (cache == caller) {
            return;             // 校验通过
        }
        slowCheckMemberAccess(caller, clazz, obj, modifiers, targetClass);
    }

首先先执行一次快速校验,一旦Class正确则权限检查通过;如果未通过,则创建一个缓存,中间再进行检查

void slowCheckMemberAccess(Class<?> caller, Class<?> clazz, Object obj, int modifiers, Class<?> targetClass) throws IllegalAccessException {
    Refelection.ensureMemberAccess(caller, clazz, obj, modifiers);
    // 如果成功,就更新缓存
    Object cache = ((targetClass == clazz) ? caller : new Class<?>[] {caller, targetClass});
    securityCheckCache = cache;
}

Reflection.ensureMemberAccess方法继续检查权限.若检查通过就更新缓存,这样下一次同一个类调用同一个方法时就不用执行权限检查了,这是一种简单的缓存机制
由于JMMhappens-before规则能够保证缓存初始化能够在写缓存之间发生,因此两个cache不需要声明为volatile

调用MethodAccessor的invoke方法

private volatile MethodAccessor methodAccessor;
/**
 * For sharing of MethodAccessors. This branching structure is
 * currently only two levels deep (i.e., one root Method and
 * potentially many Method objects pointing to it.)
 * 
 * If this branching structure would ever contain cycles, deadlocks can
 * occur in annotation code.
 */
private Method  root;
/**
 * This interface provides the declaration for
 * java.lang.reflect.Method.invoke(). Each Method object is
 * configured with a (possibly dynamically-generated) class which
 * implements this interface
 */
 public interface MethodAccessor {
    // Matches specification in {@link java.lang.reflect.Method}
    public Object invoke(Object obj, Object[] args) throws IllegalArgumentException, InvocationTargetException;
 }

MethodAccessor是一个接口,定义了invoke() 方法,通过Usage可以看出MethodAccessor的具体实现类:

  1. sun.reflect.DelegatingMethodAccessorImpl
  2. sun.reflect.MethodAccessorImpl
  3. sun.reflect.NativeMethodAccessorImpl
/**
 * NOTE that there is no synchronization used here. 
 * It is correct(though not efficient) to generate more than one MethodAccessor for a given Method.
 * However, avoiding synchronization will probably make the implementation more scalable.
 */

private MethodAccessor acquireMethodAccessor() {
    // First check to see if one has been created yet, and take it if so
    MethodAccessor tmp = null;
    if (root != null)
        tmp = root.getMethodAccessor();
    if (tmp != null) {
        methodAccessor = tmp;
    } else {
        tmp = reflectionFactory.newMethodAccessor(this);
        setMethodAccessor(tmp);
    } 
    return tmp;
} 
/**
 * Reflection factory used by subclasses for creating field,
 * method, and constructor accessors. Note that this is called very early in the bootstrapping process.
 */
static final ReflectionFactory reflectionFactory = AccessController.doPrivileged(
                                                    new sun.reflect.ReflectionFactory.GetReflectionFactoryAction());
public class ReflectionFactory {
    private static boolean initted = false;
    private static Permission reflectionFactoryAccessPerm = new RuntimePermission("reflectionFactoryAccess");
    private static ReflectionFactory soleInstance = new ReflectionFactory();
    // Provides access to package-private mechanisms in java.lang.reflect
    private static volatile LangReflectAccess langReflectAccess;

    /**
     * "Inflation" mechanism. Loading bytecodes to implement Method.invoke() and Constructor.
     * newInstance() currently costs 3-4x more than an invocation via native code for the first invocation (though subsequent invocations have been benchmarked to be over 20x faster)
     * Unfortunately this cost increases startup time for certain applications that use reflection intensively (but only once per class) to bootstrap themselves
     * To avoid this penalty we reuse the existing JVM entry points for the first few invocations of Methods and Constructors and then switch to the bytecode-based implementations
     */

    // Package-private to be accessible to NativeMethodAccessorImpl and NativeConstructorAccessorImpl
    private static noInflation = false;
    private static int inflationThreshold = 15;

    // 生成MethodAccessor
    public MethodAccessor newMethodAccessor(Method method) {
        checkInitted();

        if (noInflation && !ReflectUtil.isVMAnonymousClass(method.getDeclaringClass())) {
            return new MethodAccessorGenerator().generateMethod(method.getDeclaringClass(),
                                         method.getName(),
                                         method.getParameterTypes(),
                                         method.getReturnType(),
                                         method.getExceptionTypes(),
                                         method.getModifiers());
        } else {
            NativeMethodAccessorImpl acc = new NativeMethodAccessorImpl(method);
            DelegatingMethodAccessorImpl res = new DelegatingMethodAccessorImpl(acc);
            acc.setParent(res);
            return res;
        }
    }

    /**
     * We have to defer full initialization of this class until after the static initializer is run since java.lang.reflect
     * Method's static initializer (more properly, that for java.lang.reflect.AccessibleObject) causes this class's to be run, before the system properties are set up
     */
     private static void checkInitted() {
        if (initted) return;
        AccessController.doPrivileged(
            new PrivilegedAction<Void>() {
                public Void run() {
                /**
                 * Tests to ensure the system properties table is fully initialized
                 * This is needed because reflection code is called very early in the initialization process (before command-line arguments have been parsed and therefore these user-settable properties installed
                 * We assume that if System.out is non-null then the System class has been fully initialized and that the bulk of the startup code has been run
                 */
                 if (System.out == null) {
                        // java.lang.System not yet fully initialized
                        return null;
                    }
                    String val = System.getProperty("sun.reflect.noInflation");
                    if (val != null && val.equals("true")) {
                        noInflation = true;
                    }
                    val = System.getProperty("sun.reflect.inflationThreshold");
                    if (val != null) {
                        try {
                            inflationThreshold = Integer.parseInt(val);
                        } catch (NumberFormatException e) {
                            throw new RuntimeException("Unable to parse property sun.reflect.inflationThreshold", e);
                        }
                    }
                    initted = true;
                    return null;
                }
            });
    }
}
/* Delegates its invocation to another MethodAccessorImpl and can change its delegate at run time */
class DelegatingMethodAccessorImpl extends MethodAccessorImpl {
    private MethodAccessorImpl delegate;
    DelegatingMethodAccessorImpl(MethodAccessorImpl delegate) {
        setDelegate(delegate);
    }
    public Object invoke(Object obj, Object[] args)
        throws IllegalArgumentException, InvocationTargetException
    {
        return delegate.invoke(obj, args);
    }
    void setDelegate(MethodAccessorImpl delegate) {
        this.delegate = delegate;
    }
}

DelegatingMethodAccessorImpl对象是一个中间层,方便在native版与Java版的MethodAccessor之间进行切换

/* Used only for the first few invocations of a Method; afterward,switches to bytecode-based implementation */
class NativeMethodAccessorImpl extends MethodAccessorImpl {
    private Method method;
    private DelegatingMethodAccessorImpl parent;
    private int numInvocations;
    NativeMethodAccessorImpl(Method method) {
        this.method = method;
    }
    public Object invoke(Object obj, Object[] args)
        throws IllegalArgumentException, InvocationTargetException
    {
        /* We can't inflate methods belonging to vm-anonymous classes because that kind of class can't be referred to by name, hence can't be found from the generated bytecode */
        if (++numInvocations > ReflectionFactory.inflationThreshold()
                && !ReflectUtil.isVMAnonymousClass(method.getDeclaringClass())) {
            MethodAccessorImpl acc = (MethodAccessorImpl)
                new MethodAccessorGenerator().
                    generateMethod(method.getDeclaringClass(),
                                   method.getName(),
                                   method.getParameterTypes(),
                                   method.getReturnType(),
                                   method.getExceptionTypes(),
                                   method.getModifiers());
            parent.setDelegate(acc);
        }
        return invoke0(method, obj, args);
    }
    void setParent(DelegatingMethodAccessorImpl parent) {
        this.parent = parent;
    }
    private static native Object invoke0(Method m, Object obj, Object[] args);
}

JVM层invoke0方法

JNIEXPORT jobject JNICALL Java_sun_reflect_NativeMethodAccessorImpl_invoke0
(JNIEnv *env, jclass unused, jobject m, jobject obj, jobjectArray args)
{
    return JVM_InvokeMethod(env, m, obj, args);
}
JVM_ENTRY(jobject, JVM_InvokeMethod(JNIEnv *env, jobject method, jobject obj, jobjectArray args0))
  JVMWrapper("JVM_InvokeMethod");
  Handle method_handle;
  if (thread->stack_available((address) &method_handle) >= JVMInvokeMethodSlack) {
    method_handle = Handle(THREAD, JNIHandles::resolve(method));
    Handle receiver(THREAD, JNIHandles::resolve(obj));
    objArrayHandle args(THREAD, objArrayOop(JNIHandles::resolve(args0)));
    oop result = Reflection::invoke_method(method_handle(), receiver, args, CHECK_NULL);
    jobject res = JNIHandles::make_local(env, result);
    if (JvmtiExport::should_post_vm_object_alloc()) {
      oop ret_type = java_lang_reflect_Method::return_type(method_handle());
      assert(ret_type != NULL, "sanity check: ret_type oop must not be NULL!");
      if (java_lang_Class::is_primitive(ret_type)) {
        // Only for primitive type vm allocates memory for java object.
        // See box() method.
        JvmtiExport::post_vm_object_alloc(JavaThread::current(), result);
      }
    }
    return res;
  } else {
    THROW_0(vmSymbols::java_lang_StackOverflowError());
  }
JVM_END
oop Reflection::invoke_method(oop method_mirror, Handle receiver, objArrayHandle args, TRAPS) {
  oop mirror             = java_lang_reflect_Method::clazz(method_mirror);
  int slot               = java_lang_reflect_Method::slot(method_mirror);
  bool override          = java_lang_reflect_Method::override(method_mirror) != 0;
  objArrayHandle ptypes(THREAD, objArrayOop(java_lang_reflect_Method::parameter_types(method_mirror)));
  oop return_type_mirror = java_lang_reflect_Method::return_type(method_mirror);
  BasicType rtype;
  if (java_lang_Class::is_primitive(return_type_mirror)) {
    rtype = basic_type_mirror_to_basic_type(return_type_mirror, CHECK_NULL);
  } else {
    rtype = T_OBJECT;
  }
  instanceKlassHandle klass(THREAD, java_lang_Class::as_Klass(mirror));
  Method* m = klass->method_with_idnum(slot);
  if (m == NULL) {
    THROW_MSG_0(vmSymbols::java_lang_InternalError(), "invoke");
  }
  methodHandle method(THREAD, m);
  return invoke(klass, method, receiver, override, ptypes, rtype, args, true, THREAD);
}

Java的对象模型 :klassoop

Java版的实现

    Generator for sun.reflect.MethodAccessor and
    sun.reflect.ConstructorAccessor objects using bytecodes to
    implement reflection. A java.lang.reflect.Method or
    java.lang.reflect.Constructor object can delegate its invoke or
    newInstance method to an accessor using native code or to one
    generated by this class. (Methods and Constructors were merged
    together in this class to ensure maximum code sharing.)

运用了asm动态生成字节码技术 - sun.reflect.ClassFileAssembler

invoke总结

/** <P> MagicAccessorImpl (named for parity with FieldAccessorImpl and
    others, not because it actually implements an interface) is a
    marker class in the hierarchy. All subclasses of this class are
    "magically" granted access by the VM to otherwise inaccessible
    fields and methods of other classes. It is used to hold the code
    for dynamically-generated FieldAccessorImpl and MethodAccessorImpl
    subclasses. (Use of the word "unsafe" was avoided in this class's
    name to avoid confusion with {@link sun.misc.Unsafe}.) </P>
    <P> The bug fix for 4486457 also necessitated disabling
    verification for this class and all subclasses, as opposed to just
    SerializationConstructorAccessorImpl and subclasses, to avoid
    having to indicate to the VM which of these dynamically-generated
    stub classes were known to be able to pass the verifier. </P>
    <P> Do not change the name of this class without also changing the
    VM's code. </P> */
class MagicAccessorImpl {
}
Summary: Improve the security of the JDK’s method-handle implementation by replacing the existing
 hand-maintained list of caller-sensitive methods with a mechanism that accurately identifies
  such methods and allows their callers to be discovered reliably.
/**
 * A method annotated @CallerSensitive is sensitive to its calling class,
 * via {@link sun.reflect.Reflection#getCallerClass Reflection.getCallerClass},
 * or via some equivalent.
 *
 * @author John R. Rose
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({METHOD})
public @interface CallerSensitive {
}

反射注意点

<br />
<br />
<br />
<br />
<br />
<br />

萌新小号主在线求关注同名公众号!分享技术干货,面试题和攻城狮故事。
您的关注支持是我持续进步的最大动力!一起学习,共同进步。
[图片上传失败...(image-128021-1624200793135)]

上一篇 下一篇

猜你喜欢

热点阅读