• Java的反射机制和动态代理


    介绍Java注解的时候,多次提到了Java的反射API。与javax.lang.model不同的是,通过反射API可以获取程序在运行时刻的内部结构。反射API中提供的动态代理也是非常强大的功能,可以原生实现AOP中 的方法拦截功能。正如英文单词reflection的含义一样,使用反射API的时候就好像在看一个Java类在水中的倒影一样。知道了Java类的内部 结构之后,就可以与它进行交互,包括创建新的对象和调用对象中的方法等。这种交互方式与直接在源代码中使用的效果是相同的,但是又额外提供了运行时刻的灵活性。使用反射的一个最大的弊端是性能比较差。相同的操作,用反射API所需的时间大概比直接的使用要慢一两个数量级。不过现在的JVM实现中,反射操作的性能已经有了很大的提升。在灵活性与性能之间,总是需要进行权衡的。应用可以在适当的时机来使用反射API。

    基本用法

    Java 反射API的第一个主要作用是获取程序在运行时刻的内部结构。这对于程序的检查工具和调试器来说,是非常实用的功能。只需要短短的十几行代码,就可以遍历出来一个Java类的内部结构,包括其中的构造方法、声明的域和定义的方法等。这不得不说是一个很强大的能力。只要有了java.lang.Class类 的对象,就可以通过其中的方法来获取到该类中的构造方法、域和方法。对应的方法分别是getConstructorgetFieldgetMethod。这三个方法还有相应的getDeclaredXXX版本,区别在于getDeclaredXXX版本的方法只会获取该类自身所声明的元素,而不会考虑继承下来的。ConstructorFieldMethod这三个类分别表示类中的构造方法、域和方法。这些类中的方法可以获取到所对应结构的元数据。

    反射API的另外一个作用是在运行时刻对一个Java对象进行操作。 这些操作包括动态创建一个Java类的对象,获取某个域的值以及调用某个方法。在Java源代码中编写的对类和对象的操作,都可以在运行时刻通过反射API来实现。考虑下面一个简单的Java类。

     
    class MyClass {
        public int count;
        public MyClass(int start) {
            count = start;
        }
        public void increase(int step) {
            count = count + step;
        }
    } 

    使用一般做法和反射API都非常简单。

    MyClass myClass = new MyClass(0); //一般做法
    myClass.increase(2);
    System.out.println("Normal -> " + myClass.count);
    try {
        Constructor constructor = MyClass.class.getConstructor(int.class); //获取构造方法
    MyClass myClassReflect = constructor.newInstance(10); //创建对象
    Method method = MyClass.class.getMethod("increase", int.class); //获取方法
    method.invoke(myClassReflect, 5); //调用方法
    Field field = MyClass.class.getField("count"); //获取域
    System.out.println("Reflect -> " + field.getInt(myClassReflect)); //获取域的值
    } catch (Exception e) {
    e.printStackTrace();
    }

    由于数组的特殊性,Array类提供了一系列的静态方法用来创建数组和对数组中的元素进行访问和操作。

    Object array = Array.newInstance(String.class, 10); //等价于 new String[10]
    Array.set(array, 0, "Hello");  //等价于array[0] = "Hello"
    Array.set(array, 1, "World");  //等价于array[1] = "World"
    System.out.println(Array.get(array, 0));  //等价于array[0]
    

    使用Java反射API的时候可以绕过Java默认的访问控制检查,比如可以直接获取到对象的私有域的值或是调用私有方法。只需要在获取到Constructor、Field和Method类的对象之后,调用setAccessible方法并设为true即可。有了这种机制,就可以很方便的在运行时刻获取到程序的内部状态。

    处理泛型

    Java 5中引入了泛型的概念之后,Java反射API也做了相应的修改,以提供对泛型的支持。由于类型擦除机制的存在,泛型类中的类型参数等信息,在运行时刻是不存在的。JVM看到的都是原始类型。对此,Java 5对Java类文件的格式做了修订,添加了Signature属性,用来包含不在JVM类型系统中的类型信息。比如以java.util.List接口为例,在其类文件中的Signature属性的声明是<E:Ljava/lang/Object;>Ljava/lang/Object;Ljava/util/Collection<TE;>;; ,这就说明List接口有一个类型参数E。在运行时刻,JVM会读取Signature属性的内容并提供给反射API来使用。

    比如在代码中声明了一个域是List<String>类型的,虽然在运行时刻其类型会变成原始类型List,但是仍然可以通过反射来获取到所用的实际的类型参数。

    Field field = Pair.class.getDeclaredField("myList"); //myList的类型是List 
    Type type = field.getGenericType(); 
    if (type instanceof ParameterizedType) {     
        ParameterizedType paramType = (ParameterizedType) type;     
        Type[] actualTypes = paramType.getActualTypeArguments();     
        for (Type aType : actualTypes) {         
            if (aType instanceof Class) {         
                Class clz = (Class) aType;             
                System.out.println(clz.getName()); //输出java.lang.String         
            }     
        } 
    }  

    动态代理

    熟悉设计模式的人对于代理模式可 能都不陌生。 代理对象和被代理对象一般实现相同的接口,调用者与代理对象进行交互。代理的存在对于调用者来说是透明的,调用者看到的只是接口。代理对象则可以封装一些内部的处理逻辑,如访问控制、远程通信、日志、缓存等。比如一个对象访问代理就可以在普通的访问机制之上添加缓存的支持。这种模式在RMIEJB中都得到了广泛的使用。传统的代理模式的实现,需要在源代码中添加一些附加的类。这些类一般是手写或是通过工具来自动生成。JDK 5引入的动态代理机制,允许开发人员在运行时刻动态的创建出代理类及其对象。在运行时刻,可以动态创建出一个实现了多个接口的代理类。每个代理类的对象都会关联一个表示内部处理逻辑的InvocationHandler接 口的实现。当使用者调用了代理对象所代理的接口中的方法的时候,这个调用的信息会被传递给InvocationHandler的invoke方法。在 invoke方法的参数中可以获取到代理对象、方法对应的Method对象和调用的实际参数。invoke方法的返回值被返回给使用者。这种做法实际上相 当于对方法调用进行了拦截。熟悉AOP的人对这种使用模式应该不陌生。但是这种方式不需要依赖AspectJ等AOP框架。

    下面的代码用来代理一个实现了List接口的对象。所实现的功能也非常简单,那就是禁止使用List接口中的add方法。如果在getList中传入一个实现List接口的对象,那么返回的实际就是一个代理对象,尝试在该对象上调用add方法就会抛出来异常。

    public List getList(final List list) {
        return (List) Proxy.newProxyInstance(DummyProxy.class.getClassLoader(), new Class[] { List.class },
            new InvocationHandler() {
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    if ("add".equals(method.getName())) {
                        throw new UnsupportedOperationException();
                    }
                    else {
                        return method.invoke(list, args);
                    }
                }
            });
     } 
    

    这里的实际流程是,当代理对象的add方法被调用的时候,InvocationHandler中的invoke方法会被调用。参数method就包含了调用的基本信息。因为方法名称是add,所以会抛出相关的异常。如果调用的是其它方法的话,则执行原来的逻辑。

    使用案例

    Java 反射API的存在,为Java语言添加了一定程度上的动态性,可以实现某些动态语言中的功能。比如在JavaScript的代码中,可以通过 obj["set" + propName]()来根据变量propName的值找到对应的方法进行调用。虽然在Java源代码中不能这么写,但是通过反射API同样可以实现类似 的功能。这对于处理某些遗留代码来说是有帮助的。比如所需要使用的类有多个版本,每个版本所提供的方法名称和参数不尽相同。而调用代码又必须与这些不同的版本都能协同工作,就可以通过反射API来依次检查实际的类中是否包含某个方法来选择性的调用。

    Java 反射API实际上定义了一种相对于编译时刻而言更加松散的契约。如果被调用的Java对象中并不包含某个方法,而在调用者代码中进行引用的话,在编译时刻就会出现错误。而反射API则可以把这样的检查推迟到运行时刻来完成。通过把Java中的字节代码增强、类加载器和反射API结合起来,可以处理一些对灵 活性要求很高的场景。

    在 有些情况下,可能会需要从远端加载一个Java类来执行。比如一个客户端Java程序可以通过网络从服务器端下载Java类来执行,从而可以实现自动更新 的机制。当代码逻辑需要更新的时候,只需要部署一个新的Java类到服务器端即可。一般的做法是通过自定义类加载器下载了类字节代码之后,定义出 Class类的对象,再通过newInstance方法就可以创建出实例了。不过这种做法要求客户端和服务器端都具有某个接口的定义,从服务器端下载的是 这个接口的实现。这样的话才能在客户端进行所需的类型转换,并通过接口来使用这个对象实例。如果希望客户端和服务器端采用更加松散的契约的话,使用反射API就可以了。两者之间的契约只需要在方法的名称和参数这个级别就足够了。服务器端Java类并不需要实现特定的接口,可以是一般的Java类。

    动态代理的使用场景就更加广泛了。需要使用AOP中的方法拦截功能的地方都可以用到动态代理。Spring框架的AOP实现默认也使用动态代理。不过JDK中的动态代理只支持对接口的代理,不能对一个普通的Java类提供代理。不过这种实现在大部分的时候已经够用了。

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

    反射:在程序运行的时候,动态的获取某个类中的属性和方法,并且能够调用(很多框架能自动识别你写的类,然后调用一些共同的方法,靠的就是它)。

    代理:给类包装上一层壳,通过这个壳去操作这个类,使得你在操作这个类之前之后可以做一些你想做的事情。

    反射介绍

    反射比较简单,主要使用Class类,Class类提供了运行时获取或调用某个类具体内容的方法。如下代码作实例:

    待调用的类MyClass:

    [java] view plaincopy
     
    1. public class MyClass {  
    2.     private int myInt;  
    3.     private String myString;  
    4.   
    5.     public MyClass(){  
    6.   
    7.     }  
    8.   
    9.     public MyClass(int a){  
    10.         this.myInt = a;  
    11.     }  
    12.   
    13.     public void Method2Void(){  
    14.   
    15.     }  
    16.   
    17.     public int Method2Int(){  
    18.         System.out.println("Method2Int has run");  
    19.         return 0;  
    20.     }  
    21.   
    22.     public String Method2String(){  
    23.         return "";  
    24.     }  
    25.   
    26.     public Object Method2Object() {  
    27.         return new Date();  
    28.     }  
    29.   
    30.     public void Method3Param(int a, String b){  
    31.         System.out.println("Method3Param has run with param-a:" + a + " and param-b:" + b);  
    32.     }  
    33. }  


    Main函数

    [java] view plaincopy
     
    1. public static void main(String[] args){  
    2.     MyClass myClass = new MyClass();  
    3.     Class cls = myClass.getClass();//获取一个类的Class  
    4.   
    5.     System.out.println("1:" + cls.getName());//名字  
    6.     System.out.println("2:" + cls.getSimpleName());  
    7.     System.out.println("3:" + cls.getPackage());  
    8.     try{  
    9.         Method m = cls.getMethod("Method2Int", new Class[]{});//获取一个类的方法  
    10.         m.invoke(myClass, new Object[]{});//精髓所在,调用这个类的方法  
    11.   
    12.         Method m2 = cls.getMethod("Method3Param", new Class[]{int.class, String.class});  
    13.         m2.invoke(myClass, new Object[]{5, "fake"});//调用这个类的方法,带参数的  
    14.     }catch(Exception e){  
    15.         e.printStackTrace();  
    16.     }  
    17.     Method[] ms = cls.getMethods();  
    18.   
    19.     for(Method m:ms){  
    20.         System.out.println(m.getName());  
    21.     }  
    22.   
    23.   
    24.     try{  
    25.         Class cls2 = Class.forName("MyClass");//这种方法也能获取一个类的Class,同时能动态载入这个类  
    26.     }catch(Exception e){  
    27.         e.printStackTrace();  
    28.     }  
    29. }  

    下面详细介绍下代理

          代理这种现象在生活中是非常常见的,比如你要买火车票,可以让你的朋友帮你买,也可以托代售点帮你买。你把钱给了他们,他们可能会做任何事情。也许你朋友忘记;或者代售点把你黑了。当然,程序是你的,你可以控制他们的行为。

          代理有普通代理和动态代理。上面说的你的朋友,可以看成是普通代理,代售点可以看成是动态代理。区别在于,你的朋友并不会帮每个人都买票,仅仅帮他认识少部分人买,而代售点是来者不拒的,具有通用性。

          使用代理模式,需要区分真实对象和代理对象。代理对象中含有真实对象的引用,可以对真实对象进行操作,调用真实对象的方法。可以通过调用代理对象的方法,间接的去调用真实对象的方法。

    下面还是举例来说说这两种代理吧。

    以下是这两种代理所需的类:

    行为PersonAct.java

    [java] view plaincopy
     
    1. package proxy;  
    2.   
    3. public interface PersonAct {  
    4.     void buyTicket();//买票  
    5.     void checkProperty();//查看财产  
    6. }  

    抽象类Person.java

    [java] view plaincopy
     
    1. import java.util.Map.Entry;  
    2.   
    3. public abstract class Person implements PersonAct{  
    4.     //全部家当放在这个HashMap里  
    5.     HashMap<String, Object> property = new HashMap<String, Object>();  
    6.       
    7.     String name;  
    8.       
    9.     public Person(String name, int money){  
    10.         this.name= name;  
    11.         this.property.put("money", money);  
    12.     }  
    13.       
    14.     //打印目前家当  
    15.     public void checkProperty(){  
    16.         System.out.println(this.name + "的家当如下:");  
    17.         Iterator<Entry<String, Object>> iter = property.entrySet().iterator();  
    18.         while(iter.hasNext()){  
    19.             Entry<String, Object> entry = (Entry<String, Object>)iter.next();  
    20.             System.out.println(entry.getKey() + "---" + entry.getValue());  
    21.         }          
    22.     }             
    23. }  

    真实类Boy.java

    [java] view plaincopy
     
    1. package proxy;  
    2.   
    3. public class Boy extends Person{  
    4.       
    5.     public Boy(String name, int money){  
    6.         super(name, money);  
    7.     }  
    8.       
    9.     //买票  
    10.     @Override  
    11.     public void buyTicket() {  
    12.         int money = (Integer)property.get("money");  
    13.           
    14.         property.put("money", money - 38);//扣钱  
    15.         property.put("ticket", "北京到上海机票");//拿票          
    16.     }  
    17. }  

    真实类Girl.java

    [java] view plaincopy
     
    1. package proxy;  
    2.   
    3. public class Girl extends Person{  
    4.       
    5.     public Girl(String name, int money){  
    6.         super(name, money);  
    7.     }      
    8.       
    9.     @Override  
    10.     public void buyTicket() {  
    11.         int money = (Integer)property.get("money");  
    12.           
    13.         property.put("money", money - 46);//扣钱  
    14.         property.put("ticket", "北京到深圳机票");//拿票      
    15.     }          
    16. }  

    上面的类Boy和Girl代表现实生活中的两个人,Boy自己去买票的过程如下。

    [java] view plaincopy
     
    1. Boy boy = new Boy("西门庆", 100);          
    2. boy.checkProperty();  
    3. boy.buyTicket();  
    4. boy.checkProperty();  

    结果将会打印出boy购票前和投票后的家当。

    1,普通代理

    类FatherOfBoy.java

    [java] view plaincopy
     
    1. package proxy;  
    2.   
    3. public class FatherOfBoy {  
    4.     private Boy boy = new Boy("西门庆", 100);  
    5.       
    6.     public void buyTicket(){  
    7.         boy.buyTicket();  
    8.     }  
    9.       
    10.     public void checkProperty(){  
    11.         boy.checkProperty();  
    12.     }  
    13. }  

    概念很好理解,普通代理买票可以这样测试。

    [java] view plaincopy
     
    1. FatherOfBoy father = new FatherOfBoy();  
    2. father.checkProperty();  
    3. father.buyTicket();  
    4. father.checkProperty();  

    2,  动态代理

    动态代理主要通过继承InvocationHandler接口来实现。

    ProxyAgency.java如下:

    [java] view plaincopy
     
    1. package proxy;  
    2.   
    3. import java.lang.reflect.InvocationHandler;  
    4. import java.lang.reflect.Method;  
    5.   
    6. public class ProxyAgency implements InvocationHandler{  
    7.     private Person target;//真实对象的引用  
    8.       
    9.     public void setTarget(Person target) {  
    10.         this.target = target;  
    11.     }  
    12.   
    13.     public ProxyAgency(){  
    14.           
    15.     }  
    16.       
    17.     public ProxyAgency(Person obj){  
    18.         this.setTarget(obj);  
    19.     }  
    20.       
    21.     //obj,这个是代理对象  
    22.     //method,具体调用的方法  
    23.     //args,调用方法的时候传进来的参数,一般都直接传给真实对象即可  
    24.     @Override  
    25.     public Object invoke(Object obj, Method method, Object[] args) throws Throwable {  
    26.         //target,真实对象,收取手续费10元  
    27.         if(method.getName().equalsIgnoreCase("buyTicket")){  
    28.             int money = Integer.parseInt(target.property.get("money").toString());  
    29.             target.property.put("money", money - 10);  
    30.         }  
    31.         //target,真实对象,下面一句属于java反射机制的东西,用反射机制获取真实对象的方法          
    32.         return method.invoke(target, args);//调用真实对象的方法  
    33.     }  
    34. }  

    调用实例:

    [java] view plaincopy
     
    1.         //代理对象构建器  
    2.         ProxyAgency proxyAgency = new ProxyAgency();  
    3.           
    4.         //真实对象  
    5.         Boy b = new Boy("西门庆", 100);  
    6.         Girl g = new Girl("潘金莲", 200);          
    7.           
    8.         //代理对象,获取方法1  
    9.         PersonAct p1 = (PersonAct) Proxy.newProxyInstance(Person.class.getClassLoader(), new Class[]{ PersonAct.class }, proxyAgency);  
    10.           
    11.         //真实对象注入构建器,也可以在构建器的构造函数中注入  
    12.         proxyAgency.setTarget(b);          
    13.           
    14.           
    15.         //代理对象获取方法2  
    16. //        Class<?> cls = g.getClass();  
    17. //        PersonAct p1 = (PersonAct) Proxy.newProxyInstance(cls.getClassLoader(), cls.getInterfaces(), proxyAgency);  
    18.           
    19.         p1.checkProperty();  
    20.         p1.buyTicket();  
    21.         p1.checkProperty();  
    22.           
    23.         proxyAgency.setTarget(g);  
    24.           
    25.         p1.checkProperty();  
    26.         p1.buyTicket();  
    27.         p1.checkProperty();  


    输出如下:

    西门庆的家当如下:
    money---100
    西门庆的家当如下:
    ticket---北京到上海机票
    money---52
    潘金莲的家当如下:
    money---200
    潘金莲的家当如下:
    ticket---北京到深圳机票
    money---144

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

     运行时信息(RunTime Type Information,RTTI)使得你可以在程序运行时发现和使用类型信息。RTTI主要用来运行时获取向上转型之后的对象到底是什么具体的类型。
    Class对象 
          JAVA使用Class对象来执行RTTI。每个类都有一个Class对象,它用来创建这个类的所有对象,反过来说,每个类的所有对象都会关联同一个Class对象(对于数组来说,维数、类型一致的数组的Class对象才是相同的),每个对象的创建都依赖于Class对象的是否创建,Class对象的创建发生在类加载(java.lang.ClassLoader)的时候。
          Java.lang.Class类实现了Serializable、GenericDeclaration、Type、AnnotatedElement四个接口,分别实现了可序列化、泛型定义、类型、元数据(注解)的功能。可以把Class对象理解为一个类在内存中的接口代理(它代理了这个类的类型信息、方法签名、属性),JVM加载一个类的时候首先创建Class对象,然后创建这个类的每个实例的时候都使用这个Class对象。
          Class只有一个私有的无参构造方法,也就是说Class的对象创建只有JVM可以完成。如何验证同一个类的多个对象的Class对象是一个呢?我们知道==用来比较应用是否相等(也就是同一个引用),应为Class对象在JVM中只有一个,所以Class对象是否相等是JAVA对象中唯一可以使用==判断的。
    如何获取Class对象
          所有的引用数据类型(l类-类型)的类名、基本数据类型都可以通过.class方式获取其Class对象,通过这种方式获取class对象就做类的字面常量;
          Class的forName(String name)传入一个类的完整路径也可以获得Class对象,但由于使用的是字符串,必须强制转换才可以获取泛型的Class<T>的Class对象,并且你可以获取这个方法可能抛出的ClassNotreFoundException异常。
    对于应用数据类的引用(必须初始化),可以通过Object类继承的getClass()方法获取这个引用的Class对象,由于引用已经被初始化,所以这种方式也不会初始化静态域,因为静态域已经被初始化过。另外,前面两种方式如果说是创建Class对象,那么这种方式应该是取得Class对象,因为类的实例已经被创建,那么Class对象也一定早就被创建。
    JAVA的反射机制
          前面说过RTTI获取某个对象的确切类型,要求在这个对象在编译时已知,也就是必须已经在你的代码中存在完整的声明,但是如果是运行时才会知晓的对象(例如,网络中传递过来的字节),RTTI就没有办法工作了。
           java.lang.class与java.lang.reflect.*包中的类提供了有别于RTTI(编译器在编译时打开和检查*.class文件)的反射机制(运行时打开和检查*.class文件)。JAVA的反射机制功能相当强大。
    Java的动态代理
          JAVA自带的动态代理是基于java.lang.reflect.Proxy、java.lang.reflect.InvocationHandler两个类来完成的,使用JAVA反射机制。
    通常使用下面方法创建代理对象:
          Object proxy = Proxy.newProxyInstance(定义代理对象的类加载器,要代理的目标对象的归属接口数组,回调接口InvocationHandler);
           JDK的动态代理会动态的创建一个$Proxy0的类,这个类继承了Proxy并且实现了要代理的目标对象的接口,但是在JDK中是找不到这个类的,因为它是动态生成的。
    public final class $Proxy0 extends Proxy implements 目标对象的接口1,接口2,…{
    //构造方法
    Public $Proxy0(InvocationHandler h){
    … …
    }
    }
    我们用下面的图说明上面的执行过程:
     Java的反射机制和动态代理
          我们看到目标对象的方法调用被Proxy拦截,在InvocationHandler中的回调方法中通过反射调用。这种动态代理的方法实现了对类的方法的运行时修改。
    JDK的动态代理有个缺点,那就是不能对类进行代理,只能对接口进行代理,想象一下我们的类如果没有实现任何接口,那么就无法使用这种方式进行动态代理(实际上是因为$Proxy0这个类继承了Proxy,JAVA的继承不允许出现多个父类)。但准确的说这个问题不应该是缺点,因为良好的系统,每一个类都是应该有一个接口的。
          下面介绍以下InvocationHandler这个接口,它只有一个方法invoke()需要实现,这个方法会在目标对象的方法调用的时候被激活,你可以在这里控制目标对象的方法的调用,在调用前后插入一些其它操作(譬如:权限控制、日志记录、事物管理等)。
          Invoke()方法的后两个参数很好理解,一个是调用的方法的Method对象,另一个是方法的参数,第一个参数有些需要注意的地方,这个proxy参数就是我们使用Proxy的静态对象创建的动态代理对象,也就是$Proxy0的实例。由于$Proxy0在JDK中不是静态存在的,因此不可以把第一个参数Object proxy强制装换为$proxy0类型,因为你根本就无法从ClassPath中导入$Proxy0。那么我们可以把$Proxy0转为目标对象的接口吗?因为$Proxy0是实现了目标对象的所有的接口的,答案是可以的。但实际上这样做的意义不大,因为你发发现转换为目标对象的接口之后,你调用接口中的任何一个方法,都会导致Invoke的调用陷入死循环而导致堆栈溢出。这是因为目标对象的大部分的方法都被代理了,你在invoke()通过代理对象转换之后的接口嗲用目标对象的方法,依然是走的代理对象。
          那么invoke()方法的第一个参数到底是干什么用的呢?其实一般情况下这个参数是用不到,除非你想获得代理对象的类信息描述,因为它的getClass()方法的调用不会陷入死循环。
          这里还要注意一个问题,那就是从Object身上继承的方法hashcode()等的调用也会导致陷入死循环,为什么getClass()不会呢?因为getClass()方法是final的,不可以被覆盖,所以也就不会被Proxy代理。但不要认为Proxy不可以对final的方法进行动态代理,因为Proxy面向的是接口,即使在实现类中把接口的方法都变成了final的,也不会影响到Proxy的动态代理。
  • 相关阅读:
    jsp 认知(2)
    jsp 认知
    Java 匿名类
    Usage of readonly and const
    Javascript 原型继承(续)—从函数到构造器的角色转换
    JavaScript 原型继承开端
    JS 函数调用
    Javascript Basic Operation Extraction
    JS单词形式的运算符
    git问题收集
  • 原文地址:https://www.cnblogs.com/wnlja/p/3914234.html
Copyright © 2020-2023  润新知