• Java反射


    一、动态语言与静态语言

    1、动态语言

    是一类在运行时可以改变其结构的语言:例如新的函数、对象、甚至代码可以被引进,已有的函数可以被删除或是其他结构上的变化。通俗点说就是在运行时代码可以根据某些条件改变自身结构。
    主要动态语言::Object-C、C#、JavaScript、PHP、Python等。

    2、静态语言

    与动态语言相对应的,运行时结构不可变的语言就是静态语言。如Java、C、C++。
    Java虽然不是动态语言,但Java可以称之为 “准动态语言” ;即Java有一定的动态性我们可以利用反射机制获得类似动态语言的特性;Java的动态性让编程的时候更加灵活。

    二、Java反射

    Reflection (反射) 是Java被视为动态语言的关键,反射机制允许程序在运行期借助于Reflection API取得任何类的内部信息,并能直接操作任意对象的内部属性及方法。
    类加载器加载完类之后,在堆内存中就产生了这个类的Class类型的对象(一个类只有一个Class对象),这个对象就包含了完整的类的结构信息。我们可以通过这个对象看到类的结构。这个对象就像一面镜子,透过这个镜子看到类的结构,所以我们形象的称之为:反射。
    比如:Class c = Class.forName(“类的全限定名称(包名+类名)”) 可以获取类的Class对象

    1、Java反射机制提供的功能

    在运行时判断任意一个对象所属的类
    在运行时构造任意一个类的对象
    在运行时判断任意一个类所具有的成员变量和方法
    在运行时获取泛型信息
    在运行时调用任意一个对象的成员变量和方法
    在运行时处理注解
    生成动态代理

    2、反射相关的主要对象

    位于 java.lang.reflect 包下面相关的类等;比如:
    java.lang.Class:代表一个类,Class对象表示某个类加载后在堆中的对象
    java.lang.reflect.Method:代表类的方法,Method对象表示某个类的方法
    java.lang.reflect.Field:代表类的成员变量,Field对象表示某个类的成员变量
    java.lang.reflect.Constructor:代表类的构造器,Constructor对象表示某个类的构造器

    3、反射机制优缺点

    优点:可以实现动态创建使用对象,这也是框架的底层支撑。
    缺点:使用反射基本是解释执行,对性能有影响;我们可以告诉JVM,我们希望做什么并且它满足我们的要求。这类操作总是慢于直接执行相同的操作。

    4、提升反射操作性能方式

    Method、Field、Constructor对象都有setAccessible()方法(因为他们都继承了AccessibleObject类,这个类中有setAccessible()方法);setAccessible作用是启动和禁用访问检查的开关。
    参数值为true则指示反射的对象在使用时应该取消Java语言访问检查(好处是:第一:提高反射的效率,如果代码中必须用反射,那么可以考虑设置为true(这里说的效率提升指的是对于反射来说如果不关闭访问检查耗时会比关闭访问检查耗时多,但性能提升和服务器配置有关,而不论是否关闭访问检查只要是反射方式都比正常对象调用方法这种方式慢得多至少是上百倍的性能差距);第二:使得原本无法访问的私有成员也可以访问)。
    参数值为false则指示反射的对象应该实施Java语言访问检查。
    在这里插入图片描述

    5、Class类介绍

    Class类是Java反射的源头,实际上所谓反射从程序的运行结果来看也很好理解,即:可以通过对象反射求出类的信息(正常是通过类创建对象,而反射则通过对象获取类信息),针对任何想动态加载、运行的类,唯有先获得相应的Class对象。
    对象照镜子后可以得到的信息:某个类的属性、方法和构造器、某个类到底实现了哪些接口。对于每个类而言,JRE 都为其保留一个不变的 Class 类型的对象。一个 Class 对象包含了特定某个结构(class/interface/enum/annotation/primitive type/void/[])的有关信息。
    a、Class 本身也是一个类,顶级超类也是Object类
    b、Class 对象不是new出来的,而是由系统类加载时创建的
    c、通过类的Class对象可以得到一个类的完整结构
    d、一个类的Class对象在JVM 中只有一份,因为类加载只会加载一次(也就是在堆中存在这个类的Class对象就不会进行类加载,否则会调用ClassLoader(类加载器)的loadClass(String name)方法进行类加载)
    e、Class对象存放在堆中的
    f、每个类的实例都会记得自己是由哪个 Class 实例所生成
    g、类加载除了会在堆中生成一个Class对象,还会将类的class内容转为字节码二进制数据保存在方法区中,Class对象会引用方法区的这个二进制数据

    6、获取类的Class对象方式

    1、若已知具体的类,通过类的class属性获取,该方法最为安全可靠,程序性能最高(常用于参数传递,如:通过反射得到对应构造器对象)
    Class clazz = 类名.class;
    2、已知某个类的实例,调用该实例的getClass()方法获取Class对象(即通过创建好的对象获取类的Class对象;Object类中定义该方法,而Object类是所有类的超类,因此所有对象都有该方法)
    Class clazz = 对象名.getClass();
    3、已知一个类的全类名,可通过Class类的静态方法forName()获取可能抛出ClassNotFoundException异常(常用于通过读取配置文件读取全类名进行类加载)
    Class clazz = Class.forName(“类的全限定名称(包名+类名)”):
    4、利用ClassLoader的loadClass(String name)方法获取;也就是先通过某个Class对象的.getClassLoader()方法获取类加载器,然后通过类加载器的loadClass方法传入类的全类名获取Class对象(由于都需要知道类的全类名,相比之下第三种方式更简便)

                        //这种方式也需要先得到类的全类名,相比之下 Class.forName("类的全限定名称(包名+类名)") 方式更简便
                        try {
                            //获取某个类的Class对象
                            Class<User> userClass = User.class;
                            //获取类加载器
                            ClassLoader classLoader = userClass.getClassLoader();
                            //通过类加载器的loadClass方法获取某个类的Class对象
                            Class loadClass = classLoader.loadClass("com.database.pool.testpool.entity.TestIdcardEntity");
                            System.out.println(loadClass);
                        } catch (ClassNotFoundException e) {
                            e.printStackTrace();
                        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    5、八种基本数据类型可以直接 数据类型.class 获取(比如:int.class)
    6、八种基本数据类型的包装类可以直接用 类名.Type 获取(比如:Integer.TYPE)

    7、哪些类型有Class对象:

    a、Class:包括 外部类、成员内部类,静态内部类、局部内部类、匿名内部类(Class类本身也是类,所以Class类也有Class对象)
    b、interface:接口
    c、[]:数组
    d、enum:枚举
    e、annotation:注解(@interface)
    f、primitive type: 基本数据类型(如int、long)
    g、void

    8、Class类常用方法

    方法名功能说明
    static Class forName(String name)获取指定类名name的Class对象
    T newInstance()调用无参构造函数,返回Class对象的一个实例
    String getName()返回此Class对象所表示的实体 (类,接口,数组类或void) 的名称(包名+类名)
    native Class getSuperclass()返回当前Class对象的父类的Class对象
    Class[] getInterfaces()获取当前Class对象的接口(即这个Class对象所代表的类实现的接口)
    ClassLoader getClassLoader()获取类的类加载器
    Constructor[] getConstructors()获取类中所有的构造器(只获取public的构造器、只有本类的)
    Constructor[] getDeclaredConstructors()获取类中所有的构造器(只有本类的)
    Method[] getMethods()获取类中所有的方法(只获取public的方法、包含本类和父类的)
    Method[] getDeclaredMethods()获取类中所有的方法(只有本类的)
    Field[] getFields()获取类中所有的字段(只获取public的字段、包含本类和父类的)
    Field[] getDeclaredFields()获取类中所有的字段(只有本类的)
    (A extends Annotation) A getAnnotation(Class(A) annotationClass)获取某个资源(如:类、方法、字段)的某个类型注解
    Annotation[] getAnnotations()获取某个资源(如:类、方法、字段)的所有注解

    Class对象可以获取类的所有信息(比如:Field、Method、Constructor、 Superclass、Interface、Annotation等等),带有 Declared 修饰的方法是获取对应的所有资源,没有 Declared 修饰的方法则是获取对应的public的资源。
    在这里插入图片描述

    通过Class对象的 newInstance() 方法创建对象时,需要这个类有一个public的无参构造器,否则会报错;如果该类没有public的无参构造器,则可以通过 getDeclaredConstructors() 方法获取类的所有构造器,然后通过调用某个构造器的 newInstance(Object … initargs) 方法进行创建对象。(如果知道类的构造器的参数则可以通过 getDeclaredConstructor(Class… parameterTypes) 方法直接获取具体的某个构造器,然后调用这个构造器的 newInstance(Object … initargs) 方法,传入参数进行创建对象)如:
    在这里插入图片描述
    在这里插入图片描述

        public static void main(String[] args) {
            //获取类的Class对象
            Class<TestIdcardEntity> testIdcardEntityClass = TestIdcardEntity.class;
            try {
                //根据形参类型获取某个构造器
                Constructor<TestIdcardEntity> declaredConstructor = testIdcardEntityClass.getDeclaredConstructor(String.class);
                try {
                    //通过构造器创建对象
                    TestIdcardEntity testIdcardEntity = declaredConstructor.newInstance("张三");
                    //获取类的某个类型的注解信息
                    Test1Annotation annotation = testIdcardEntityClass.getAnnotation(Test1Annotation.class);
                    //打印注解的属性信息
                    System.out.println("类的注解Test1Annotation的paramStr属性的值:"+annotation.paramStr());
                    System.out.println("构造器创建对象:"+testIdcardEntity);
                    //通过Class对象及方法名称、形参获取方法
                    Method setName = testIdcardEntityClass.getDeclaredMethod("setName", String.class);
                    //获取setName方法的所有注解信息
                    Annotation[] annotations = setName.getAnnotations();
                    //反射执行方法,需要传入某个具体的对象参数(这里的testIdcardEntity),如果是静态方法,那么这个对象参数可以写为null更方便;反射执行方法统一用Object来接收方法的返回值,但运行时这个返回值的类型和方法的实际返回值类型一致,只是编译时统一Object类型
                    setName.invoke(testIdcardEntity,"李四");
                    System.out.println("调用方法设置名称后对象:"+testIdcardEntity);
                    try {
                        //通过Class对象及字段名称获取某个字段
                        Field phone = testIdcardEntityClass.getDeclaredField("phone");
                        //获取phone字段的所有注解信息
                        Annotation[] annotations1 = phone.getAnnotations();
                        //关闭安全检查,对于private修饰的字段、方法、构造器等只能在类里面可以调用,不能在类外面进行调用,所以反射调用private修饰的资源时会由于安全检测机制报错
                        //所以要通过反射机制调用private的资源需要关闭这个资源的安全检查机制即调用这个资源的 .setAccessible(true) 方法
                        phone.setAccessible(true);
                        //反射设置字段的值
                        phone.set(testIdcardEntity,"12345678901");
                        //反射读取字段的值,对于set和get方法来说需要指定某个对象(这里的testIdcardEntity),假如是静态变量,那么可以将这个参数传为null更方便,那么传为null修改值之后由于这个值是类级别的,那么所有对象的这个值都被设置了,如果传入具体的某个对象修改同样也是所有对象的这个值都被修改
                        System.out.println(phone.get(testIdcardEntity));
                    } catch (NoSuchFieldException e) {
                        e.printStackTrace();
                    }
                } catch (InstantiationException e) {
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                } catch (InvocationTargetException e) {
                    e.printStackTrace();
                }
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            }
        }
    
    
    • 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

    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

    9、字段 java.lang.reflect.Field 类常用方法

    a、getModifiers():以int形式返回字段修饰符(说明:默认修饰符 是0,public 是1,private 是 2,protected 是 4,static 是 8 ,final 是 16);如果是多个修饰符则是多个修饰符的值的和;比如:public(1) + static (8) = 9
    b、getType():以Class类型返回字段类型(格式:class 类型的全类名,如:class java.lang.String、boolean)
    c、getName():返回字段名

    10、方法 java.lang.reflect.Method 类常用方法

    a、getModifiers():以int形式返回方法修饰符(说明:默认修饰符 是0,public 是1 ,private 是2 ,protected 是4,static 是8,final 是 16)和上面的字段 getModifiers() 的值一致,多个修饰符组合也一样
    b、getReturnType():以Class类型返回方法的返回类型(格式:class 类型的全类名,如:class java.lang.String、void)
    c、getName():返回方法名
    d、getParameterTypes():以Class[]格式返回形参类型数组

    11、构造器 java.lang.reflect.Constructor 类常用方法

    a、getModifiers():以int形式返回构造器修饰符,和上面的字段、方法的一样
    b、getName():返回构造器名称(实际值是全类名)
    c、getParameterTypes():以Class[]格式返回形参类型数组

    12、反射操作泛型

    Java采用泛型擦除的机制来引入泛型,Java中的泛型仅仅是给编译期(javac)使用的,确保数据的安全性和免去强制类型转换问题,但是一旦编译完成,所有和泛型有关的类型全部擦除。
    为了通过反射操作这些类型,Java新增了 ParameterizedType,GenericArrayType,TypeVariable 和 WildcardType 几种类型来代表不能被归到Class类中的类型但是又和原始类型齐名的类型。

    ParameterizedType:表示一种参数化类型,比如Collection
    GenericArrayType:表示一种元素类型是参数化类型或者类型变量的数组类型
    TypeVariable:是各种类型变量的公共父接口
    WildcardType:代表一种通配符类型表达式

    package com.database.pool.testpool;
    
    import com.database.pool.testpool.entity.TestIdcardEntity;
    
    import java.lang.reflect.Method;
    import java.lang.reflect.ParameterizedType;
    import java.lang.reflect.Type;
    import java.util.List;
    import java.util.Map;
    
    public class TestColl {
    
        /**
        * 集合数据类型(如:List、Set、Map)他们都是泛型类,也就是这些集合存入的元素都是泛型定义的,只有在使用这些集合时才能确定真实的元素数据类型;所以反射操作泛型其中一种运用场景就是获取这些泛型类的具体数据类型
        */
        public void setPar(Map<String, TestIdcardEntity> map, List<TestIdcardEntity> list){
    
        }
    
        private Map<String,TestIdcardEntity> getPar(){
            return null;
        }
    
        /**
         * 反射操作泛型
         * @param args
         * @throws NoSuchMethodException
         */
        public static void main(String[] args) throws NoSuchMethodException {
            Class<TestColl> testCollClass = TestColl.class;
            Method setPar = testCollClass.getDeclaredMethod("setPar", Map.class, List.class);
            Method getPar = testCollClass.getDeclaredMethod("getPar",null);
            //获取方法形参信息
            Type[] genericParameterTypes = setPar.getGenericParameterTypes();
            for (Type genericParameterType : genericParameterTypes) {
                System.out.println("方法某个参数信息:"+genericParameterType);
                if (genericParameterType instanceof ParameterizedType){
                    ParameterizedType parameterizedType = (ParameterizedType)genericParameterType;
                    Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
                    for (Type actualTypeArgument : actualTypeArguments) {
                        System.out.println("某个参数具体类型信息:"+actualTypeArgument);
                    }
                }
            }
            //获取方法返回参数信息
            Type genericReturnType = getPar.getGenericReturnType();
            if (genericReturnType instanceof ParameterizedType){
                ParameterizedType genericReturnType1 = (ParameterizedType) genericReturnType;
                Type[] actualTypeArguments = genericReturnType1.getActualTypeArguments();
                for (Type actualTypeArgument : actualTypeArguments) {
                    System.out.println("方法返回参数具体类型:"+actualTypeArgument);
                }
            }
        }
    
    }
    
    
    • 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

    三、类加载

    1、静态加载、动态加载

    反射机制是java实现动态语言的关键,也就是通过反射实现类动态加载。
    a、静态加载:编译时加载相关的类如果没有则报错,依赖性太强(也就是javac编译生成class文件的时候就会进行加载相关类,如果某个类没有就会编译不通过报错;比如代码中写了某个类但这个类没有,这种情况编码软件就会自动提示报错了)。
    b、动态加载:运行时加载需要的类,如果运行时不用该类,不论该类是否存在都不会报错,降低了依赖性(比如通过Class.forName()方式获取类的Class对象(在编译时不会因为类不存在报错编译不通过或编码软件提示报错),此时如果类的Class对象在堆中不存在会进行类加载,在运行到这个代码时且不存在这个类才会报错,如果不会执行这个代码则不会因为类不存在在编译时或运行中报错)。

    2、类加载时机(即什么方式可以触发类加载)

    a、当创建对象时 (比如:new)
    b、当子类被加载时,父类也加载
    c、调用类中的静态成员时
    d、通过反射(只有反射是动态加载、其余都是静态加载)

    3、类加载步骤

    当程序使用某个类时,如果该类还未被加载到内存中,则触发类加载机制;程序运行时才会发生类加载,因此在类加载时已经有了源代码.java文件通过javac编译后的.class文件了:

    1、加载:JVM 在该阶段的主要目的是将字节码从不同的数据源(如:class 文件、jar 包、网络等)转化为二进制字节流加载保存到方法区中,并将这些静态数据转换成JVM的运行时数据结构然后生成一个代表这个类的java.lang.Class对象,由类加载器完成此部分工作

    2、链接:将Java类的二进制合并到JVM的运行状态之中的过程
    2.1、验证:确保加载的类信息符合JVM规范,没有安全方面的问题(如:文件格式验证(是否以魔数 oxcafebabe开头)、元数据验证、字节码验证和符号引用验证等;可以考虑使用 -Xverify:none 参数来关闭大部分的类验证措施,缩短虚拟机类加载的时间)
    2.2、准备:正式为类变量(static修饰的变量)分配内存并设置类变量默认初始值(如:0、0L、null、false)的阶段,这些内存都将在方法区中进行分配(如果是static final修饰的变量那么就是静态常量,它也属于类级别的属性,因此也会分配内存;但final修饰的就是常量一旦赋值就不可以修改值,因此static final修饰的静态常量赋值时不是赋值为默认初始值而是赋值为代码写的值)
    2.3、解析:虚拟机将常量池内的符号引用 (常量名) 替换为直接引用(地址)的过程(比如A类引用了B类,那么在A、B类没有被加载分配内存之前只能记录为有这样一种引用关系,而加载分配内存之后就有了具体的内存地址了,就可以把这个引用关系设置为具体的内存地址)

    3、初始化:JVM负责对类进行初始化(这里是对类的初始化而不是对象的初始化不是new这个初始化)
    到初始化阶段,才真正开始执行类中定义的 Java 程序代码,此阶段是执行类构造器 clinit(){} 方法的过程。
    3.1、clinit0{} 方法是由编译器按语句在源文件中出现的顺序,依次自动收集类中的所有静态变量的赋值动作和静态代码块中的语句,并进行合并(也就是clinit0{}方法的方法体是按照代码的先后顺序将静态变量的赋值动作、静态代码块等类级别的信息进行收集合并到clinit0{}方法体中(合并是指比如静态变量赋值动作和静态代码块(这两种都是类级别的信息没有优先顺序只根据代码的先后顺序收集合并)都进行了某个静态变量的赋值动作,那么后面的代码的赋值会覆盖前面的赋值,因此合并为后面赋值的代码而不是多次赋值都存在))
    3.2、虚拟机会保证一个类的clinit0{}方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的 clinit0{} 方法,其他线程都需要阻塞等待,直到活动线程执行 clinit0{} 方法完毕(前面第二部分第5个的d提到 一个类的Class对象在JVM 中只有一份,因为类加载只会加载一次;就是因为执行 clinit0{} 方法时有加锁的机制在所以只执行一次类加载只有一份Class对象,加锁的方法是synchronized (getClassLoadingLock(name)) {})
    3.3、当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化
    3.4、什么情况发生类的初始化:
    3.4.1:类的主动引用(一定会发生类的初始化),如以下情况:
    当虚拟机启动,先初始化main方法所在的类
    new一个类的对象
    调用类的静态成员(除了final常量)和静态方法
    使用java.lang.reflect包的方法对类进行反射调用
    当初始化一个类,如果其父类没有被初始化,则先会初始化它的父类
    3.4.2:类的被动引用 (不会发生类的初始化),如以下情况:
    当访问一个静态域时,只有真正声明这个域的类才会被初始化。如:当通过子类引用父类的静态变量,不会导致子类初始化
    通过数组定义类引用,不会触发此类的初始化
    引用常量不会触发此类的初始化(常量在链接阶段就存入调用类的常量池中了)

    在这里插入图片描述

    4、类加载器

    类加载的作用:将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后在堆中生成一个代表这个类的java.lang.Class对象,作为方法区中类数据的访问入口。
    类缓存:标准的JavaSE类加载器可以按要求查找类,但一旦某个类被加载到JVM方法区中,它将维持加载(缓存)一段时间。不过JVM垃圾回收机制可以回收这些Class对象。
    在这里插入图片描述
    获取某个类是哪个类加载器加载的:

    clazz.getClassLoader();  //clazz代表某个类的Class对象
    
    • 1

    获取系统的类加载器可加载的类路径(结果包含自己写的代码的路径,因为不论是rt.jar还是自己写的代码都可以运行就说明都会被类加载器加载):

    System.getProperty("java.class.path");
    
    • 1

    获取某个类加载器的父类加载器:

    classLoader.getParent();  //classLoader代表某个类加载器
    
    • 1

    在这里插入图片描述

    5、双亲委派

    类加载器在加载一个类时不会直接去加载这个类,会先去访问父类加载器,父类加载器再去访问父类加载器,直到访问到最顶层的根类加载器之后,如果根类加载器可以加载某个类,那么就根类加载器进行加载这个类,其他的类加载器不进行该类的加载;如果根类加载器不能加载这个类,那么再一层一层交给子类加载器进行加载,一旦某个子类加载器可以加载这个类之后则其余类加载器不进行该类的加载。这样设计的目的保证了系统级别的一些类不会被用户自定义的某些同路径名称的类将系统中该类进行替换导致一些不可预料的错误。比如系统级别的java.lang.String这个类按照双亲委派机制会被根类加载器进行加载,那么正常情况下任何时候使用这个类都是rt.jar包中的java.lang.String类;如果用户在代码中也写了这样一个类,在没有双亲委派机制的情况下如果类加载器直接加载,那么用户自定义的java.lang.String类会被AppClassLoader进行加载,从而不会加载系统级别的java.lang.String类,导致一些错误。

    Java泛型

  • 相关阅读:
    传奇GOM引擎时装功能如何添加
    java基于springboot的民宿预约管理平台系统
    CEC2013(MATLAB):螳螂搜索算法(Mantis Search Algorithm,MSA)求解CEC2013
    python Flask与微信小程序 统计管理
    求助!什么软件可以人声分离?手机上可以进行人声分离操作吗?
    电脑桌面便签工具选择哪一款?
    AJAX概念及入门案例
    宋明的结局揭示了什么
    C 标准库 - <stdio.h> 详解
    推荐一款专用低代码工具,一天开发一个系统不是梦
  • 原文地址:https://blog.csdn.net/doubiy/article/details/133465586