zoukankan      html  css  js  c++  java
  • Java 动态代理

    被代理的接口特点:

    1. 不能有重复的接口,以避免动态代理类代码生成时的编译错误。

    2. 这些接口对于类装载器必须可见,否则类装载器将无法链接它们,将会导致类定义失败。

    3. 需被代理的所有非 public 的接口必须在同一个包中,否则代理类生成也会失败。

    4. 接口的数目不能超过 65535,这是 JVM 设定的限制。

    动态生成的代理类的特点:

    1)包:如果所代理的接口都是 public 的,那么它将被定义在顶层包(即包路径为空),如果所代理的接口中有非 public 的接口(因为接口不能被定义为 protect 或 private,所以除 public 之外就是默认的 package 访问级别),那么它将被定义在该接口所在包(假设代理了 com.ibm.developerworks 包中的某非 public 接口 A,那么新生成的代理类所在的包就是 com.ibm.developerworks),这样设计的目的是为了最大程度的保证动态代理类不会因为包管理的问题而无法被成功定义并访问;

    2)类修饰符:该代理类具有 final 和 public 修饰符,意味着它可以被所有的类访问,但是不能被再度继承;

    3)类名:格式是“$ProxyN”,其中 N 是一个逐一递增的阿拉伯数字,代表 Proxy 类第 N 次生成的动态代理类,值得注意的一点是,并不是每次调用 Proxy 的静态方法创建动态代理类都会使得 N 值增加,原因是如果对同一组接口(包括接口排列的顺序相同)试图重复创建动态代理类,它会很聪明地返回先前已经创建好的代理类的类对象,而不会再尝试去创建一个全新的代理类,这样可以节省不必要的代码重复生成,提高了代理类的创建效率。

    4)类继承关系:该类的继承关系如图:

    图 2. 动态代理类的继承图

    由图可见,Proxy 类是它的父类,这个规则适用于所有由 Proxy 创建的动态代理类。而且该类还实现了其所代理的一组接口,这就是为什么它能够被安全地类型转换到其所代理的某接口的根本原因。

    代理类实例的一些特点:

    1. 每个实例都会关联一个调用处理器对象(InvocationHandler),可以通过 Proxy 提供的静态方法 getInvocationHandler 去获得代理类实例的调用处理器对象。

    2. 当代理的一组接口有重复声明的方法且该方法被调用时,代理类总是从排在最前面的接口中获取方法对象并分派给调用处理器,而无论代理类实例是否正在以该接口(或继承于该接口的某子接口)的形式被外部引用,因为在代理类内部无法区分其当前的被引用类型。

    * 注明: 在代理类实例上调用其代理的接口中所声明的方法时,这些方法最终都会由调用处理器的 invoke 方法执行,此外,代理类的根类 java.lang.Object 中的hashCode,equals 和 toString三个方法也同样会被分派到调用处理器的 invoke 方法执行,可能的原因有:

    1. 因为这些方法为 public 且非 final 类型,能够被代理类覆盖;

    2. 因为这些方法往往呈现出一个类的某种特征属性,具有一定的区分度,所以为了保证代理类与委托类对外的一致性,这三个方法也应该被分派到委托类执行。

    代理:设计模式

    代理是一种常用的设计模式,其目的就是为其他对象提供一个代理以控制对某个对象的访问。代理类负责为委托类预处理消息,过滤消息并转发消息,以及进行消息被委托类执行后的后续处理。

    图 1. 代理模式

    图 1. 代理模式

    为了保持行为的一致性,代理类和委托类通常会实现相同的接口,所以在访问者看来两者没有丝毫的区别。通过代理类这中间一层,能有效控制对委托类对象的直接访问,也可以很好地隐藏和保护委托类对象,同时也为实施不同控制策略预留了空间,从而在设计上获得了更大的灵活性。Java 动态代理机制以巧妙的方式近乎完美地实践了代理模式的设计理念。

    相关的类和接口

    java.lang.reflect.Proxy:这是 Java 动态代理机制的主类,它提供了一组静态方法来为一组接口动态地生成代理类及其对象

    java.lang.reflect.InvocationHandler:这是调用处理器接口,它自定义了一个 invoke 方法,用于集中处理在动态代理类对象上的方法调用,通常在该方法中实现对委托类的代理访问

    java.lang.ClassLoader:这是类装载器类,负责将类的字节码装载到 Java 虚拟机(JVM)中并为其定义类对象,然后该类才能被使用。Proxy 静态方法生成动态代理类同样需要通过类装载器来进行装载才能使用,它与普通类的唯一区别就是其字节码是由 JVM 在运行时动态生成的而非预存在于任何一个 .class 文件中。

    代理机制及其特点

    如何使用 Java 动态代理。具体有如下四步骤:

    1. 1. 通过实现 InvocationHandler 接口创建自己的调用处理器;
    2. 2. 通过为 Proxy 类指定 ClassLoader 对象和一组 interface 来创建动态代理类;
    3. 3. 通过反射机制获得动态代理类的构造函数,其唯一参数类型是调用处理器接口类型;
    4. 4. 通过构造函数创建动态代理类实例,构造时调用处理器对象作为参数被传入。
      /********动态代理过程***********/
      
      // InvocationHandlerImpl 实现了 InvocationHandler 接口,并能实现方法调用从代理类到委托类的分派转发
      // 其内部通常包含指向委托类实例的引用,用于真正执行分派转发过来的方法调用
      InvocationHandler handler = new InvocationHandlerImpl(..); 
      
      // 通过 Proxy 为包括 Interface 接口在内的一组接口动态创建代理类的类对象
      Class clazz = Proxy.getProxyClass(classLoader, new Class[] { Interface.class, ... }); 
      
      // 通过反射从生成的类对象获得构造函数对象
      Constructor constructor = clazz.getConstructor(new Class[] { InvocationHandler.class }); 
      
      // 通过构造函数对象创建动态代理类实例
      Interface Proxy = (Interface)constructor.newInstance(new Object[] { handler });

    实际使用过程更加简单,因为 Proxy 的静态方法 newProxyInstance 已经为我们封装了步骤 2 到步骤 4 的过程,所以简化后的过程如下:

    // InvocationHandlerImpl 实现了 InvocationHandler 接口,并能实现方法调用从代理类到委托类的分派转发
    InvocationHandler handler = new InvocationHandlerImpl(..); 
    
    // 通过 Proxy 直接创建动态代理类实例
    Interface proxy = (Interface)Proxy.newProxyInstance( classLoader, 
         new Class[] { Interface.class }, 
         handler );

    简单的demo:

    Hello接口:

    public interface Hello {  
          void sayHello(String to);  
          void print(String p);   
    }

    Hello接口的实现类:

    public class HelloImpl implements Hello {  
          
        public void sayHello(String to) {  
            System.out.println("Say hello to " + to);  
        }  
          
        public void print(String s) {  
            System.out.println("print : " + s);  
        }  
          
    }

    与代理类(HelloImpl类)相关联的InvocationHandler对象

    public class LogHandler implements InvocationHandler {  
          
        private Object dele;  
          
        public LogHandler(Object obj) {  
            this.dele = obj;  
        }  
          
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {  
            doBefore();  
            //在这里完全可以把下面这句注释掉,而做一些其它的事情  
            Object result = method.invoke(dele, args);  
            after();  
            return result;  
        }  
          
        private void doBefore() {  
            System.out.println("before....");  
        }  
          
        private void after() {  
            System.out.println("after....");  
        }  
    }

    最后测试代码如下:

    public class ProxyTest {  
      
        public static void main(String[] args) {  
            HelloImpl impl = new HelloImpl();  
            LogHandler handler = new LogHandler(impl);  
            //这里把handler与impl新生成的代理类相关联  
            Hello hello = (Hello) Proxy.newProxyInstance(impl.getClass().getClassLoader(), impl.getClass().getInterfaces(), handler);  
              
            //这里无论访问哪个方法,都是会把请求转发到handler.invoke  
            hello.print("All the test");  
            hello.sayHello("Denny");  
        }  
      
    }
    Proxy 静态方法 newProxyInstance
    public static Object newProxyInstance(ClassLoader loader, 
                Class<?>[] interfaces, 
                InvocationHandler h) 
                throws IllegalArgumentException { 
        
        // 检查 h 不为空,否则抛异常
        if (h == null) { 
            throw new NullPointerException(); 
        } 
    
        // 获得与制定类装载器和一组接口相关的代理类类型对象
        Class cl = getProxyClass(loader, interfaces); 
    
        // 通过反射获取构造函数对象并生成代理类实例
        try { 
            Constructor cons = cl.getConstructor(constructorParams); 
            return (Object) cons.newInstance(new Object[] { h }); 
        } catch (NoSuchMethodException e) { throw new InternalError(e.toString()); 
        } catch (IllegalAccessException e) { throw new InternalError(e.toString()); 
        } catch (InstantiationException e) { throw new InternalError(e.toString()); 
        } catch (InvocationTargetException e) { throw new InternalError(e.toString()); 
        } 
    }

    动态代理真正的关键是在 getProxyClass 方法,该方法负责为一组接口动态地生成代理类类型对象。该方法总共可以分为四个步骤:

    1. 对这组接口进行一定程度的安全检查。

    2. 从 loaderToCache 映射表中获取以类装载器对象为关键字所对应的缓存表。

    3. 动态创建代理类的类对象。

    4. 代码生成过程进入结尾部分,更新缓存表,唤醒其他等待进程。

    private static Class<?> getProxyClass0(ClassLoader loader,
                                               Class<?>... interfaces) {
            if (interfaces.length > 65535) {
                throw new IllegalArgumentException("interface limit exceeded");
            }
    
            Class<?> proxyClass = null;
    
            /* 存储interface 名称,用来作为proxy class cache的键值key */
            String[] interfaceNames = new String[interfaces.length];
    
            // 用来确认是否有重复接口
            Set<Class<?>> interfaceSet = new HashSet<>();
    
            for (int i = 0; i < interfaces.length; i++) {
                
                String interfaceName = interfaces[i].getName();
                Class<?> interfaceClass = null;
                try {
                /*
                 *检查接口类对象是否对类装载器可见 
                 */
                    interfaceClass = Class.forName(interfaceName, false, loader);
                } catch (ClassNotFoundException e) {
                }
                /*
                 *检查接口类对象与类装载器所能识别的接口类对象是完全相同的 
                 */
                if (interfaceClass != interfaces[i]) {
                    throw new IllegalArgumentException(
                        interfaces[i] + " is not visible from class loader");
                }
    
                /*
                 * 确保是 interface 类型而不是 class 类型
                 */
                if (!interfaceClass.isInterface()) {
                    throw new IllegalArgumentException(
                        interfaceClass.getName() + " is not an interface");
                }
    
                /*
                 * 确保接口不重复
                 */
                if (interfaceSet.contains(interfaceClass)) {
                    throw new IllegalArgumentException(
                        "repeated interface: " + interfaceClass.getName());
                }
                interfaceSet.add(interfaceClass);
    
                interfaceNames[i] = interfaceName;
            }
    
            /*
             * 从 loaderToCache 映射表中获取以类装载器对象为关键字所对应的缓存表,
             * 如果不存在就创建一个新的缓存表并更新到 loaderToCache。缓存表是一个 HashMap 实例,
             * 正常情况下它将存放键值对(接口名字列表,动态生成的代理类的类对象引用)。
             * 当代理类正在被创建时它会临时保存(接口名字列表,pendingGenerationMarker)。
             * 标记 pendingGenerationMarke 的作用是通知后续的同类请求(接口数组相同且组内接口排列顺序也相同)
             * 代理类正在被创建,请保持等待直至创建完成。
             */
            List<String> key = Arrays.asList(interfaceNames);
    
            /*
             * 获取以类装载器对象为关键字所对应的缓存表
             */
            Map<List<String>, Object> cache;
            synchronized (loaderToCache) {
                cache = loaderToCache.get(loader);
                if (cache == null) {
                    cache = new HashMap<>();
                    loaderToCache.put(loader, cache);
                }
            }
    
            synchronized (cache) {
                
                do {
                // 以接口名字列表作为关键字获得对应 cache 值
                    Object value = cache.get(key);
                    if (value instanceof Reference) {
                        proxyClass = (Class<?>) ((Reference) value).get();
                    }
                    if (proxyClass != null) {
                        // 如果已经创建,直接返回
                        return proxyClass;
                    } else if (value == pendingGenerationMarker) {
                        // 代理类正在被创建,保持等待
                        try {
                            cache.wait();
                        } catch (InterruptedException e) {
                            
                        }
                        // 等待被唤醒,继续循环并通过二次检查以确保创建完成,否则重新等待
                        continue;
                    } else {
                        // 标记代理类正在被创建
                        cache.put(key, pendingGenerationMarker);
                        // break 跳出循环已进入创建过程
                        break;
                    }
                } while (true);
            }
    
            try {
                String proxyPkg = null;     //动态生成的代理类所在的包,见代理类特点1
    
                /*
                 * 记录所有非public接口所在的包,生成的代理类也将被定义在这个包下。
                 * 确认所有非public接口在相同的包下。
                 */
                for (int i = 0; i < interfaces.length; i++) {
                    int flags = interfaces[i].getModifiers();
                    if (!Modifier.isPublic(flags)) {
                        String name = interfaces[i].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");
                        }
                    }
                }
    
                // 如果没有非public接口,则使用com.sun.proxy这个包名
                if (proxyPkg == null) {
                    proxyPkg = ReflectUtil.PROXY_PACKAGE + ".";
                }
    
                {
                    /*
                     * 获取proxy的名字编号。见代理类特点3
                     */
                    long num;
                    synchronized (nextUniqueNumberLock) {
                        num = nextUniqueNumber++;
                    }
                    String proxyName = proxyPkg + proxyClassNamePrefix + num;
    //
    private final static String proxyClassNamePrefix = "$Proxy";
                    
                   
    /*
                     * 动态生成代理类的字节码数组
                    
    */
                    byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
                        proxyName, interfaces);
                   
    try {
                       
    // 动态地定义新生成的代理类
                        proxyClass = defineClass0(loader, proxyName,
                            proxyClassFile,
    0, proxyClassFile.length);
                    }
    catch (ClassFormatError e) {
                       
    throw new IllegalArgumentException(e.toString());
                    }
                }
               
    //把生成的代理类的类对象记录进 proxyClasses 表, isProxyClass方法将用来判断
                proxyClasses.put(proxyClass, null);

            }
    finally {
               
    /*
                 * 根据结果更新缓存表,如果成功则将代理类的类对象引用更新进缓存表,
                 * 否则清楚缓存表中对应关键值,最后唤醒所有可能的正在等待的线程。
                
    */
                synchronized (cache) {
                   
    if (proxyClass != null) {
                        cache.put(key,
    new WeakReference<Class<?>>(proxyClass));
                    }
    else {
                        cache.remove(key);
                    }
                    cache.notifyAll();
                }
            }
           
    return proxyClass;
        }

    给一个样例,最终动态生成的代理类proxyClassFile,写入文件并反编译之后的样子。

    首先接口是Subject,实现类RealSubject,调用处理器ProxyHandler,生成的动态代理类是ProxySubject(实际动态生成的应该是$ProxyN。)

    public interface Subject   
    {   
      public void doSomething();   
    }   
    public class RealSubject implements Subject   
    {   
      public void doSomething()   
      {   
        System.out.println( "call doSomething()" );   
      }   
    }   
    public class ProxyHandler implements InvocationHandler   
    {   
      private Object proxied;   
         
      public ProxyHandler( Object proxied )   
      {   
        this.proxied = proxied;   
      }   
         
      public Object invoke( Object proxy, Method method, Object[] args ) throws Throwable   
      {   
        //在转调具体目标对象之前,可以执行一些功能处理
    
        //转调具体目标对象的方法
        return method.invoke( proxied, args);  
        
        //在转调具体目标对象之后,可以执行一些功能处理
      }    
    }

    代理类的使用和写入ProxySubject.class文件,用以反编译出ProxySubject.java

    import java.lang.reflect.InvocationHandler;   
    import java.lang.reflect.Method;   
    import java.lang.reflect.Proxy;   
    import sun.misc.ProxyGenerator;   
    import java.io.*;   
    public class DynamicProxy   
    {   
      public static void main( String args[] )   
      {   
        RealSubject real = new RealSubject();   
        Subject proxySubject = (Subject)Proxy.newProxyInstance(Subject.class.getClassLoader(), 
         new Class[]{Subject.class}, 
         new ProxyHandler(real));
             
        proxySubject.doSomething();
       
        //write proxySubject class binary data to file   
        createProxyClassFile();   
      }   
         
      public static void createProxyClassFile()   
      {   
        String name = "ProxySubject";   
        byte[] data = ProxyGenerator.generateProxyClass( name, new Class[] { Subject.class } );   
        try  
        {   
          FileOutputStream out = new FileOutputStream( name + ".class" );   
          out.write( data );   
          out.close();   
        }   
        catch( Exception e )   
        {   
          e.printStackTrace();   
        }   
      }   
    }

    编译出的ProxySubject.java

    import java.lang.reflect.*;   
    public final class ProxySubject extends Proxy   
        implements Subject   
    {   
        private static Method m1;   
        private static Method m0;   
        private static Method m3;   
        private static Method m2;   
        public ProxySubject(InvocationHandler invocationhandler)   
        {   
            super(invocationhandler);   
        }   
        public final boolean equals(Object obj)   
        {   
            try  
            {   
                return ((Boolean)super.h.invoke(this, m1, new Object[] {   
                    obj   
                })).booleanValue();   
            }   
            catch(Error _ex) { }   
            catch(Throwable throwable)   
            {   
                throw new UndeclaredThrowableException(throwable);   
            }   
        }   
        public final int hashCode()   
        {   
            try  
            {   
                return ((Integer)super.h.invoke(this, m0, null)).intValue();   
            }   
            catch(Error _ex) { }   
            catch(Throwable throwable)   
            {   
                throw new UndeclaredThrowableException(throwable);   
            }   
        }   
        public final void doSomething()   
        {   
            try  
            {   
                super.h.invoke(this, m3, null);   
                return;   
            }   
            catch(Error _ex) { }   
            catch(Throwable throwable)   
            {   
                throw new UndeclaredThrowableException(throwable);   
            }   
        }   
        public final String toString()   
        {   
            try  
            {   
                return (String)super.h.invoke(this, m2, null);   
            }   
            catch(Error _ex) { }   
            catch(Throwable throwable)   
            {   
                throw new UndeclaredThrowableException(throwable);   
            }   
        }   
        static    
        {   
            try  
            {   
                m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] {   
                    Class.forName("java.lang.Object")   
                });   
                m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);   
                m3 = Class.forName("Subject").getMethod("doSomething", new Class[0]);   
                m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);   
            }   
            catch(NoSuchMethodException nosuchmethodexception)   
            {   
                throw new NoSuchMethodError(nosuchmethodexception.getMessage());   
            }   
            catch(ClassNotFoundException classnotfoundexception)   
            {   
                throw new NoClassDefFoundError(classnotfoundexception.getMessage());   
            }   
        }   
    }

    ProxyGenerator内部是如何生成class二进制数据,可以参考源代码。

    private byte[] generateClassFile() {   
      /*  
       * 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 (int i = 0; i < interfaces.length; i++) {   
          Method[] methods = interfaces[i].getMethods();   
          for (int j = 0; j < methods.length; j++) {   
        addProxyMethod(methods[j], interfaces[i]);   
          }   
      }   
      /*  
       * 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");   
      }   
      /* ============================================================  
       * 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 (int i = 0; i < interfaces.length; i++) {   
          cp.getClass(dotToSlash(interfaces[i].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(ACC_PUBLIC | ACC_FINAL | ACC_SUPER);   
              // 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 (int i = 0; i < interfaces.length; i++) {   
        dout.writeShort(cp.getClass(   
            dotToSlash(interfaces[i].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");   
      }   
      return bout.toByteArray();

    参考:http://www.cnblogs.com/flyoung2008/archive/2013/08/11/3251148.html

    更好的理解,可以参考:https://my.oschina.net/lyp3314/blog/136589

    http://www.360doc.com/content/14/0313/17/16070877_360315087.shtml

  • 相关阅读:
    关于组建“智彩足球技术研究团队”的说明
    2次成功投诉EMS和中国移动的经验
    为什么选择玩足球彩票以及玩彩票的心态?
    【原创】机器学习之PageRank算法应用与C#实现(1)算法介绍
    【原创】开源Math.NET基础数学类库使用(17)C#计算矩阵条件数
    【原创】开源Math.NET基础数学类库使用(16)C#计算矩阵秩
    【文章】本站收集与转载文章目录
    【原创】.NET读写Excel工具Spire.Xls使用(3)单元格控制
    分享一个Visual Studio的背景插件,让堆码更富情趣
    【原创】开源Math.NET基础数学类库使用(15)C#计算矩阵行列式
  • 原文地址:https://www.cnblogs.com/dorothychai/p/4177079.html
Copyright © 2011-2022 走看看