zl程序教程

您现在的位置是:首页 >  Java

当前栏目

二、Java注解&反射和动态代理

2023-04-18 14:27:50 时间

(持续更新ing… …反射常用API&JDK动态代理)

Java注解

什么是注解?

百度百科对注解的定义如下:注解(Annotation),也叫元数据。一种代码级别的说明。
它是JDK1.5及以后版本引入的一个特性,注解 @interface 与类class、接口 interface、枚举 enum是在同一个层次。
它可以声明在包、类、字段、方法、局部变量、方法参数等的前面,用来对这些元素进行说明,注释。

Annotation接口中有这么一句话 "The common interface extended by all annotation types. "。也就是所有的注解都继承于 java.lang.annotation.Annotation 接口。
这句话有点抽象,但是却说出了注解的本质。我们随便看一个JDK内置注解的定义:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

上面代码是@Override注解的定义,其实他本质上是

public interface Override extends Annotation{    
}

注解的本质就是一个继承了Annotation接口的接口。

而注解在编译完成后,也是字节码,所以,它在JVM中也是Class的实例。如:Class<? extends Annotation>
注解的目的是用来描述数据,更确切地讲,是用代码的方式来描述数据。

注解从使用方法和用途上,可以分为三种?

  • Java自带的标准注解
    包括@Override、@Deprecated、@SuppressWarnings等,使用这些注解后编译器就会进行检查。
  • 元注解
    元注解是用于定义注解的注解,包括@Retention、@Target、@Inherited、@Documented、@Repeatable 等。
    元注解也是Java自带的标准注解,只不过用于修饰注解,比较特殊。
  • 自定义注解
    用户可以根据自己的需求定义注解。

系统注解有哪些?JDK内置注解

标准注解(3+1)

包括@Override、@Deprecated、@SuppressWarnings等,使用这些注解后编译器就会进行检查。

@Override:检查该方法是否重写方法,如果发现其父类,或者是引用的接口中并没有该方法时,会报编译错误。
@Deprecated:标记过时方法,如果使用该方法,会报编译警告。
@SuppressWarnings:指示编译器去忽略注解中声明的警告。
@FunctionalInterface:Java8支持,标识一个匿名函数或者函数式接口。

@Override

如果试图使用 @Override 标记一个实际上并没有覆写父类的方法时,java 编译器会告警。

public class Parent {
    public void test() {
    }
}

public class Child extends Parent{
    @Override
    public void test() {
        super.test();
    }
}

@Deprecated

用于标明被修饰的类或类成员、类方法已经废弃、过时,不建议使用。

@Deprecated
class TestClass {
  // do something
}

@SuppressWarnings

用于关闭对类、方法、成员编译时产生的特定警告。

  • 抑制单类型的警告
    @SuppressWarnings("unchecked")  
    public void addItems(String item){  
      @SuppressWarnings("rawtypes")  
       List items = new ArrayList();  
       items.add(item);  
    }
    
  • 抑制多类型的警告
    @SuppressWarnings(value={"unchecked", "rawtypes"})  
    public void addItems(String item){  
       List items = new ArrayList();  
       items.add(item);  
    }
    
  • 抑制所有类型的警告
    @SuppressWarnings("all")  
    public void addItems(String item){  
       List items = new ArrayList();  
       items.add(item);  
    }
    

@SuppressWarnings 注解的常见参数值的简单说明:

  • deprecation:使用了不赞成使用的类或方法时的警告。
  • unchecked:执行了未检查的转换时的警告,例如当使用集合时没有用泛型 (Generics) 来指定集合保存的类型。
  • fallthrough:当 Switch 程序块直接通往下一种情况而没有 Break 时的警告。
  • path:在类路径、源文件路径等中有不存在的路径时的警告。
  • serial:当在可序列化的类上缺少 serialVersionUID 定义时的警告。
  • finally:任何 finally 子句不能正常完成时的警告。
  • all:关于以上所有情况的警告。

@FunctionalInterface

用于指示被修饰的接口是函数式接口,在JDK8 引入。

@FunctionalInterface
public interface UserService {
    /**
     * 抽象方法
     */
    void getUser(Long userId);
    
    /**
     * 默认方法,可以用多个默认方法
     */
    default void setUser() {
    }

    /**
     * 静态方法
     */
    static void saveUser() {
    }

    /**
     * 覆盖Object中的equals方法
     */
    @Override
    boolean equals(Object obj);
}

函数式接口(Functional Interface)就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口。

元注解(5+2)

元注解是负责对其它注解进行说明的注解,自定义注解时可以使用元注解。
Java5定义了4个注解,分别是@Documented@Target@Retention@Inherited。Java8又增加了@Repeatable@Native两个注解。这些注解都可以在java.lang.annotation包中找到。

@Documented

@Documented 是一个标记注解,没有成员变量。用@Documented注解修饰的注解类会被 JavaDoc工具提取成文档。默认情况下,JavaDoc是不包括注解的,但如果声明注解时指定了@Documented,就会被JavaDoc之类的工具处理,所以注解类型信息就会被包括在生成的帮助文档中。

@Documented
@Target({ ElementType.TYPE, ElementType.METHOD })
public @interface MyDocumented {
    public String value() default "这是@Documented注解";
}

public class DocumentedTest {
    /**
     * 测试document
     */
    @MyDocumented
    public String test() {
        return "Java教程";
    }
}

然后打开Java文件所在的目录,分别输入如下两条命令行:

javac MyDocumented.java DocumentedTest.java
javadoc -d doc MyDocumented.java DocumentedTest.java

运行成功后,打开生成的帮助文档,可以看到在类和方法上都保留了MyDocument的注解信息。如下所示:
在这里插入图片描述
在这里插入图片描述

@Target

@Target 注解用来指定一个注解的使用范围,即被@Target修饰的注解可以用在什么地方。@Target 注解有一个成员变量(value)用来设置适用目标,value是java.lang.annotation.ElementType枚举类型的数组,下表为 ElementType 常用的枚举常量。

TYPE:用于类、接口(包括注解类型)或 enum 声明
FIELD:用于成员变量(包括枚举常量)
METHOD:用于方法
LOCAL_VARIABLE:用于局部变量
PACKAGE:用于包
TYPE_PARAMETER:用于类型参数(JDK1.8新增)
在这里插入图片描述

自定义一个 MyTarget 注解,使用范围为方法,代码如下所示。

@Target({ ElementType.METHOD })
public @interface MyTarget {
}

public class MyTargetTest {
    @MyTarget
    private String name;
}

在这里插入图片描述
如上代码第 8 行会编译错误,错误信息为:'@MyTarget' not applicable to field。提示此位置不允许使用注解@MyTarget,@MyTarget不能修饰成员变量,只能修饰方法。

@Retention

Retention 用于描述注解的生命周期,也就是该注解被保留的时间长短。@Retention 注解中的成员变量(value)用来设置保留策略,value 是java.lang.annotation.RetentionPolicy枚举类型,RetentionPolicy 有 3 个枚举常量,如下所示。

SOURCE:在源文件中有效(即源文件保留)
CLASS:在 class 文件中有效(即 class 保留)
RUNTIME:在运行时有效(即运行时保留)

生命周期大小排序为 SOURCE < CLASS < RUNTIME,前者能使用的地方后者一定也能使用。如果需要在运行时去动态获取注解信息,那只能用 RUNTIME 注解;如果要在编译时进行一些预处理操作,比如生成一些辅助代码(如 ButterKnife),就用 CLASS 注解;如果只是做一些检查性的操作,比如 @Override 和 @SuppressWarnings,则可选用 SOURCE 注解。

@Inherited

@Inherited 是一个标记注解,用来指定该注解可以被继承。使用 @Inherited 注解的 Class 类,表示这个注解可以被用于该 Class 类的子类。就是说如果某个类使用了被 @Inherited 修饰的注解,则其子类将自动具有该注解。

测试代码如下:

@Target({ ElementType.TYPE })
@Inherited
@Retention(RetentionPolicy.RUNTIME)
public @interface MyInherited {
}

@MyInherited
public class MyInheritedTestA {
    public static void main(String[] args) {
        System.out.println(MyInheritedTestA.class.getAnnotation(MyInherited.class));
        System.out.println(MyInheritedTestB.class.getAnnotation(MyInherited.class));
        System.out.println(MyInheritedTestC.class.getAnnotation(MyInherited.class));
    }
}

public class MyInheritedTestB extends MyInheritedTestA{
}

public class MyInheritedTestC extends MyInheritedTestB{
}

运行结果:

@com.aiz.annotation.MyInherited()
@com.aiz.annotation.MyInherited()
@com.aiz.annotation.MyInherited()

@Repeatable

@Repeatable 注解是 Java 8 新增加的,它允许在相同的程序元素中重复注解,在需要对同一种注解多次使用时,往往需要借助 @Repeatable 注解。Java 8 版本以前,同一个程序元素前最多只能有一个相同类型的注解,如果需要在同一个元素前使用多个相同类型的注解,则必须使用注解“容器”。

Java 8 之前的做法:

public @interface Roles {
    Role[] roles();
}
public @interface Roles {
    Role[] value();
}
public class RoleTest {
    @Roles(roles = {@Role(roleName = "程序员"), @Role(roleName = "教师")})
    public String doString(){
        return "Java教程";
    }
}

Java 8 之后增加了重复注解,使用方式如下:

public @interface Roles {
    Role[] value();
}
@Repeatable(Roles.class)
public @interface Role {
    String roleName();
}
public class RoleTest {
    @Role(roleName = "程序员")
    @Role(roleName = "教师")
    public String doString(){
        return "Java教程";
    }
}

不同的地方是,创建重复注解 Role 时加上了 @Repeatable 注解,指向存储注解 Roles,这样在使用时就可以直接重复使用 Role 注解。从上面例子看出,使用 @Repeatable 注解更符合常规思维,可读性强一点。

两种方法获得的效果相同。重复注解只是一种简化写法,这种简化写法是一种假象,多个重复注解其实会被作为“容器”注解的 value 成员的数组元素处理。

@Native

使用 @Native 注解修饰成员变量,则表示这个变量可以被本地代码引用,常常被代码生成工具使用。对于 @Native 注解不常使用,了解即可。

参考:https://blog.csdn.net/Hell_potato777/article/details/126808559

为什么要用注解?

1、检查
2、约束
3、灵活
4、方便简洁
5、让静态语言拥有动态机制

注解和XML的异同:
使用Annotation之前(甚至在使用之后),XML被广泛的应用于描述元数据。不知何时开始一些应用开发人员和架构师发现XML的维护越来越糟糕了。他们希望使用一些和代码紧耦合的东西,而不是像XML那样和代码是松耦合的(在某些情况下甚至是完全分离的)代码描述。如果你在Google中搜索“XML vs. annotations”,会看到许多关于这个问题的辩论。最有趣的是XML配置其实就是为了分离代码和配置而引入的。上述两种观点可能会让你很疑惑,两者观点似乎构成了一种循环,但各有利弊。下面我们通过一个例子来理解这两者的区别。

假如你想为应用设置很多的常量或参数,这种情况下,XML是一个很好的选择,因为它不会同特定的代码相连。如果你想把某个方法声明为服务,那么使用Annotation会更好一些,因为这种情况下需要注解和方法紧密耦合起来,开发人员也必须认识到这点。

另一个很重要的因素是Annotation定义了一种标准的描述元数据的方式。在这之前,开发人员通常使用他们自己的方式定义元数据。例如,使用标记interfaces,注释,transient关键字等等。每个程序员按照自己的方式定义元数据,而不像Annotation这种标准的方式。

目前,许多框架将XML和Annotation两种方式结合使用,平衡两者之间的利弊。

参考:https://www.cnblogs.com/chanshuyi/p/annotation_serial_01_why_annotation.html

注解应用场景?

注解的保留级别不同,对注解的使用自然存在不同场景。

级别1:源码——APT,Annotation Parse/Process Tools
在编译期能够获取注解与注解声明的类,包括类中的所有成员信息,一般用于生成额外的辅助类。

级别2:字节码——字节码增强
在编译出Class后,通过修改Class数据以实现修改代码逻辑目的。对于是否需要修改的区分或者修改为不同逻辑的判断可以使用注解。

级别3:运行时——反射
在程序运行期间,通过反射技术动态获取注解与其元素,从而完成不同的逻辑判定。

自定义注解?

创建自定义注解

使用 @interface 自定义注解时,自动继承了java.lang.annotation.Annotation接口,由编译程序自动完成其他细节。在定义注解时,不能继承其他的注解或接口。@interface 用来声明一个注解,其中的每一个方法实际上是声明了一个配置参数。方法的名称就是参数的名称,返回值类型就是参数的类型(返回值类型只能是 基本类型、Class、String、enum)。可以通过 default 来声明参数的默认值。

自定义注解格式:
public @interface 注解名{注解体}

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Inherited
public @interface HelloAnnotation {
    String value() default "hi,ZhangYao!";
}

开发注解时,元素的类型只能是:所有基本数据类型(int,float,boolean,byte,double,char,long,short)、String类型、Class类型、enum类型、Annotation类、以上所有类型的数组。

使用自定义注解

注解开发出来本身是不会产生功能的,它必需要被其它的元素”使用”,哪些元素可以使用注解呢?类、属性、方法、局部变量、包… …
当然,你开发注解时,是需要事先指定此注解将来要用在哪些元素上的。你需要使用元注解来限定。

没有元素的注解叫标记注解;只有1个元素的注解,一般来说,都会把元素名取为value,叫做单值注解;有多个元素的注解,就是完整注解
根据你要用的注解的API,查看此注解有哪些元素?
根据元素的类型,使用不同的格式,如:

  • String 直接使用 “” 括起来
  • 基本类型 直接使用 字面量
  • 枚举类型 直接使用 枚举常量值
  • 注解类型 使用 @注解名
  • 数组类型 使用 {} 括起来,多个元素之间使用, 号隔开
public class HelloAnnotationClient {

    @HelloAnnotation(value = "Simple Custom Annotation Example")
    public void sayHello() {
        System.out.println("Inside sayHello method..");
    }
}

测试自定义注解

要让注解产生作用,要编写注解的解析/处理程序,这个程序就叫APT,Annotation Parse/Process Tools
如何开发APT?? 基于反射

API:
    java.lang.reflect.AnnotatedElement 接口
    方法:
        Annotation[] getDeclaredAnnotations(); //返回直接存在于此元素上的所有注释。与此接口中的其他方法不同,该方法将忽略继承的注释。(如果没有注释直接存在于此元素上,则返回长度为零的一个数组。)该方法的调用者可以随意修改返回的数组;这不会对其他调用者返回的数组产生任何影响。
        Annotation[] getAnnotations(); //包含从父类继承的
        boolean isAnnotationPresent(Class<? extends Annotation> c);// 判断该程序元素上是否包含指定类型的注解,存在则返回true,否则返回false.
        <T extends Annotation> T getAnnotation(Class<T> c);	// 返回改程序元素上存在的、指定类型的注解,如果该类型注解不存在,则返回null。
    
    java.lang.annotation.Annotation 接口
        Class<? extends Annotaion> annotationType();

测试上面我们自定义的@HelloAnnotation。

/**
 * 自定义注解测试
 */
public class HelloAnnotationTest {
    public static void main(String[] args)
            throws InvocationTargetException, IllegalAccessException, NoSuchMethodException {

        HelloAnnotationClient helloAnnotationClient = new HelloAnnotationClient();
        Method method = helloAnnotationClient.getClass().getMethod("sayHello", null);
        if (method.isAnnotationPresent(HelloAnnotation.class)) {
            HelloAnnotation helloAnnotation = method.getAnnotation(HelloAnnotation.class);
            //Get value of custom annotation
            System.out.println("Value : " + helloAnnotation.value());
            //Invoke sayHello method
            method.invoke(helloAnnotationClient);
        }
    }
}

输出结果:

Value : Simple Custom Annotation Example
Inside sayHello method..

Java反射

什么是反射?

百度百科对反射的定义:Java的反射(reflection)机制是指在程序的运行状态中,可以构造任意一个类的对象,可以了解任意一个对象所属的类,可以了解任意一个类的成员变量和方法,可以调用任意一个对象的属性和方法。这种动态获取程序信息以及动态调用对象的功能称为Java语言的反射机制。反射被视为动态语言的关键。

Java反射的原理

要想解剖一个类,必须先要获取到该类的字节码文件对象。而解剖使用的就是Class类中的方法。所以先要获取到每一个字节码文件对应的Class类型的对象。

反射就是把java类中的各种成分映射成一个个的Java对象。
例如:一个类有:成员变量、方法、构造方法、包等等信息,利用反射技术可以对一个类进行解剖,把一个个组成部分映射成一个个对象。(其实:一个类中这些成员方法、构造方法、在加入类中都有一个类来描述)
加载的时候:Class对象的由来是将 .class 文件读入内存,并为之创建一个Class对象。

Class类
Class 类的实例表示正在运行的 Java 应用程序中的类和接口。也就是jvm中有N多的实例每个类都有该Class对象。(包括基本数据类型)
Class 没有公共构造方法。Class 对象是在加载类时由 Java 虚拟机以及通过调用类加载器中的defineClass方法自动构造的。也就是这不需要我们自己去处理创建,JVM已经帮我们创建好了。

我们知道Spring框架可以帮我们创建和管理对象。需要对象时,我们无需自己手动new对象,直接从Spring提供的容器中的Beans获取即可。Beans底层其实就是一个Map<String,Object>,最终通过getBean(“user”)来获取。而这其中最核心的实现就是利用反射技术。

Bean
1、Java面向对象,对象有方法和属性,那么就需要对象实例来调用方法和属性(即实例化);
2、凡是有方法或属性的类都需要实例化,这样才能具象化去使用这些方法和属性;
3、规律:凡是子类及带有方法或属性的类都要加上注册Bean到Spring IoC的注解;(@Component、@Repository、@ Controller 、@Service、@Configration)
4、把Bean理解为类的代理或代言人(实际上确实是通过反射、代理来实现的),这样它就能代表类拥有该拥有的东西了。
5、在Spring中,你标识一个@符号,那么Spring就会来看看,并且从这里拿到一个Bean(注册)或者给出一个Bean(使用)

参考:https://blog.csdn.net/qq_51515673/article/details/124830558

反射API

获取类对应的字节码的对象

获取某个类型在JVM中的Class实例,有如下三种方法:

  1. 通过类名.class 来获取

    Class<Teacher> c = Teacher.class;
    
  2. 通过对象来获取

    Teacher t1 = new Teacher();
    Class<Teacher> c = t1.getClass();
    
  3. 通过Class中的一个静态方法 forName

    Class<Teacher> c = Class.forName("Teacher全限定名 ");
    

常用API

API:
java.lang
        - Class
        - Package 
java.lang.reflect 包 
        - Array    针对数类型的反射工具类
        - Modifier 修饰符
        - AccessibleObject
                - Constructor 构造
                - Method 方法
                - Field 属性
//获取包名、类名
clazz.getPackage().getName()//包名
clazz.getSimpleName()//类名
clazz.getName()//完整类名
 
//获取成员变量定义信息
getFields()//获取所有公开的成员变量,包括继承变量
getDeclaredFields()//获取本类定义的成员变量,包括私有,但不包括继承的变量
getField(变量名)
getDeclaredField(变量名)
 
//获取构造方法定义信息
getConstructor(参数类型列表)//获取公开的构造方法
getConstructors()//获取所有的公开的构造方法
getDeclaredConstructors()//获取所有的构造方法,包括私有
getDeclaredConstructor(int.class,String.class)
 
//获取方法定义信息
getMethods()//获取所有可见的方法,包括继承的方法
getMethod(方法名,参数类型列表)
getDeclaredMethods()//获取本类定义的的方法,包括私有,不包括继承的方法
getDeclaredMethod(方法名,int.class,String.class)
 
//反射新建实例
clazz.newInstance();//执行无参构造创建对象
clazz.newInstance(222,"Java");//执行有参构造创建对象
clazz.getConstructor(int.class,String.class)//获取构造方法
 
//反射调用成员变量
clazz.getDeclaredField(变量名);//获取变量
clazz.setAccessible(true);//使私有成员允许访问
f.set(实例,);//为指定实例的变量赋值,静态变量,第一参数给null
f.get(实例);//访问指定实例变量的值,静态变量,第一参数给null
 
//反射调用成员方法
Method m = Clazz.getDeclaredMethod(方法名,参数类型列表);
m.setAccessible(true);//使私有方法允许被调用
m.invoke(实例,参数数据);//让指定实例来执行该方法


//例如:
public class A {
    void ma() { ... }
    void ma(int a, int b) { ... }
    void mb(double a, double b) { ... }
}
Class<A> c = A.class;
Method m1 = c.getDeclaredMethod("ma",Integer.class, Integer.TYPE);
Method m2 = c.getDeclaredMethod("ma");

反射应用场景

1、配合注解的使用

使用注解的实现原理它的底层实现就是java反射。

2、编辑基础框架

有一句话这么说来着:反射机制是很多Java框架的基石,经典的就是在xml文件或者properties里面写好了配置,然后在Java类里面解析xml或properties里面的内容,得到一个字符串,然后用反射机制,根据这个字符串获得某个类的Class实例,这样就可以动态配置一些东西,spring,Hibernate底层都有类似的实现。

3、动态加载配置类

其他在编码阶段不知道那个类名,要在运行期从配置文件读取类名配置。
这段代码想必大家肯定都有写过,这个数据库的连接驱动类就是编译的时候不知道你到底是用的mysql,oracle还是其他数据库,而是由运行期动态加载的。

// 1.加载驱动程序
Class.forName("com.mysql.jdbc.Driver");
// 2.获得数据库的连接
Connection conn = DriverManager.getConnection(URL, NAME, PASSWORD);
// 3.通过数据库的连接操作数据库,实现增删改查
Statement stmt = conn.createStatement();
ResultSet rs = stmt
        .executeQuery("select user_name,age from user");
while (rs.next()) {// 如果对象中有数据,就会循环打印出来
    System.out.println(rs.getString("user_name") + ","
            + rs.getInt("age"));
}

以上介绍了反射的应用场景,程序猿开发业务代码中应尽量少用反射,一个是代码可读性不是特别好,第二是反射需要运行期JVM去重新解析性能上也没有直接使用好,唯一比较合理的地方是业务中需要用到AOP可以大大简化业务代码建议使用。

JDK动态代理

我们知道 Spring 主要有两大思想,一个是 IOC,一个就是 AOP。对于 IOC 容器中 bean 的创建就是使用了 Java 的反射技术;而对于 Spring 的核心 AOP 来说,我们知道 AOP 的实现原理之一就是 JDK 的动态代理机制,而 JDK 的动态代理本质就是利用了反射技术来实现的。

JDK 的动态代理机制中,有两个重要的类和接口,一个是 InvocationHandler 接口、一个是 Proxy 类,这一个类和接口是实现我们动态代理所必须用到的。

Proxy 类

Proxy 类来自于 java.lang.reflect 包下,它就是用来动态创建一个代理对象的类,它提供了许多的方法,但是我们用的最多的就是 newProxyInstance() 这个方法。

Object proxy = Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
  • loader:一个 ClassLoader 类加载器对象,定义了由哪个 ClassLoader 对象来对生成的代理对象进行加载
  • interfaces:一个 Interface 对象的数组,表示的是我将要给我需要代理的对象提供一组什么接口,如果我提供了一组接口给它,那么这个代理对象就宣称实现了该接口(多态),这样我代理对象就能调用这组接口中的方法了
  • h:一个 InvocationHandler 对象,表示的是当我这个动态代理对象在调用方法的时候,会关联到哪一个 InvocationHandler 对象上。

通过Proxy.newProxyInstance() 创建的代理对象是在 JVM 运行时动态生成的一个对象 ,它并不是我们的 InvocationHandler 类型,也不是我们定义的那组接口的类型,而是在运行是动态生成的一个对象。

InvocationHandler 接口

InvocationHandler 接口也来自于 java.lang.reflect 包下。每一个动态代理类都必须要实现 InvocationHandler 这个接口,并且每个代理类的实例都关联到了一个 handler,当我们通过代理对象调用一个方法的时候,这个方法的调用就会被转发为由 InvocationHandler 这个接口的 invoke() 方法来进行调用。

package java.lang.reflect;
public interface InvocationHandler {
	public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable;
}
  • proxy:指代我们所代理的那个真实对象
  • method:指代的是我们所要调用真实对象的某个方法的 Method 对象
  • args:指代的是调用真实对象某个方法时接受的参数

参考:https://blog.csdn.net/weixin_38192427/article/details/120347850