Java学习笔记40

这篇具有很好参考价值的文章主要介绍了Java学习笔记40。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

Java笔记40

创建运行时类的对象

获取运行时类的完整结构

  • 我们可以通过反射来获取运行时类的完整结构,这包括:
    • 实现的全部接口(Interface)
    • 所继承的父类(Superclass)
    • 全部的构造器(Constructor)
    • 全部的方法(Method)
    • 全部的属性/字段(Field)
    • 注解(Annotation)
    • ……
  • 下面我们写一段代码来练习一下获取类的信息:
  • 首先我们创建一个类:

Dog.java

package com.clown.reflection;

public class Dog {

    //属性
    public String name;
    private int age;
    private String ownerName;

    //无参构造
    public Dog() {
    }

    //有参构造
    public Dog(String name, int age, String ownerName) {
        this.name = name;
        this.age = age;
        this.ownerName = ownerName;
    }

    //get() & set()
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getOwnerName() {
        return ownerName;
    }

    public void setOwnerName(String ownerName) {
        this.ownerName = ownerName;
    }

    //重写 toString()
    @Override
    public String toString() {
        return "Dog{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", ownerName='" + ownerName + '\'' +
                '}';
    }

    public void run() {
        System.out.println(this.getName() + "正在奔跑");
    }

    protected void eat() {
        System.out.println(this.getName() + "正在吃东西");
    }

    private void pee() {
        System.out.println(this.getName() + "正在尿尿");
    }
}
  • 然后我们来尝试获取Dog类中的信息:

Test08.java

package com.clown.reflection;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

//获得类的信息
public class Test08 {
    public static void main(String[] args) throws Exception {
        Class c1 = Class.forName("com.clown.reflection.Dog");

        //获得类的名字
        System.out.println("=============================== 获得类的名字 ===============================");

        System.out.println("包名+类名: " + c1.getName());  //getName();  获得包名 + 类名

        System.out.println("--------------------------------------------------------------");

        System.out.println("类名: " + c1.getSimpleName());  //getSimpleName();  获得类名

        //获得类的属性
        System.out.println("=============================== 获得类的属性 ===============================");

        Field[] fields = c1.getFields();  //getFields();  获得本类的所有 public属性
        for (Field field : fields) {
            System.out.println("public属性: " + field);
        }

        System.out.println("--------------------------------------------------------------");

        fields = c1.getDeclaredFields();  //getDeclaredFields();  获得本类的所有属性
        for (Field field : fields) {
            System.out.println("所有属性: " + field);
        }

        System.out.println("--------------------------------------------------------------");

        Field name = c1.getDeclaredField("age");  //getDeclaredField(String name);  获得本类中指定的属性
        System.out.println("指定属性: " + name);

        //获得类的方法
        System.out.println("=============================== 获得类的方法 ===============================");

        Method[] methods = c1.getMethods();  //getMethods();  获得本类及其父类的所有 public方法
        for (Method method : methods) {
            System.out.println("public方法: " + method);
        }

        System.out.println("--------------------------------------------------------------");

        methods = c1.getDeclaredMethods();  //getDeclaredMethods();  获得本类的所有方法
        for (Method method : methods) {
            System.out.println("所有方法: " + method);
        }

        System.out.println("--------------------------------------------------------------");

        Method getName = c1.getMethod("getName", null);  //getMethod("XXX", XXX);  获得本类中指定的方法
        System.out.println("指定方法: " + getName);
        Method setName = c1.getMethod("setName", String.class);
        System.out.println("指定方法: " + setName);
        /*
        getMethod(String name, Class<?>... parameterTypes);  获得本类中指定的方法
        参数:
            name参数是一个 String,它指定了所需方法的简单名称。
            parameterTypes参数是以声明顺序标识方法的形式参数类型的类对象的数组。
            如果 parameterTypes是 null ,它被视为一个空数组。
         */

        //获得类的构造器
        System.out.println("============================== 获得类的构造器 ==============================");

        Constructor[] constructors = c1.getConstructors();  //getConstructors();  获得本类的 public构造器
        for (Constructor constructor : constructors) {
            System.out.println("public构造器: " + constructor);
        }

        System.out.println("--------------------------------------------------------------");

        constructors = c1.getDeclaredConstructors();  //getDeclaredConstructors();  获得本类的所有构造器
        for (Constructor constructor : constructors) {
            System.out.println("所有构造器: " + constructor);
        }

        System.out.println("--------------------------------------------------------------");

        Constructor constructor = c1.getConstructor(String.class, int.class, String.class);  //getConstructor(xxx)  获得本类中指定的构造器
        System.out.println("指定构造器: " + constructor);
        /*
        getConstructor(Class<?>... parameterTypes);  获得本类中指定的构造器
        参数:
            parameterTypes参数是以声明顺序标识构造函数的形式参数类型的类对象的数组
         */
    }
}
  • 运行结果:

Java学习笔记40,Java SE 学习笔记合集,java,学习,笔记

获取了Class对象,通过反射我们能做什么?

一、反射创建类的对象
  • 调用Class对象的newlnstance()方法

    • (1)类必须有一个无参数的构造器。
    • (2)类的构造器的访问权限需要足够
  • 思考:难道没有无参的构造器就不能创建对象了吗?

  • 只要在操作的时候明确的调用类中的构造器,调用构造器对象的newInstance(Object ... initargs)方法,并将参数传递进去,就可以实例化对象了。具体步骤如下:

    • (1)通过Class类的getConstructor(Class<?>... parameterTypes)取得本类的指定形参类型的构造器
    • (2)向构造器的形参中传递一个对象数组进去,里面包含了构造器中所需的各个参数。
    • (3)通过构造器(Constructor)实例化对象
二、反射调用类中的方法
  • 通过Method类完成。

    • 通过Class类的getMethod(String name, Class<?>... parameterTypes)方法取得一个Method对象,并设置此方法操作时所需要的参数类型。
    • 之后使用public Object invoke(Object obj, Object... args)方法进行调用,并向方法中传递要设置的obj对象的参数信息。

Java学习笔记40,Java SE 学习笔记合集,java,学习,笔记

  • Object invoke(Object obj, Object … args)

    • Object对应原方法的返回值,若原方法无返回值,此时返回null
    • 若原方法为静态方法,此时形参Object obj可为null
    • 若原方法形参列表为空,则Object[] argsnull
    • 若原方法声明为private,则需要在调用此invoke()方法前,显式调用方法对象的setAccessible(true)方法,将可访问private的方法。
  • setAccessible(boolean flag)

    • MethodFiedConstructor对象都有setAccessible()方法。
    • setAccessible()的作用是启动和禁用访问安全检查的开关。
    • 参数值为true则指示反射的对象在使用时应该取消 Java 语言访问检查。
      • 提高反射的效率。如果代码中必须用反射,而该句代码需要频繁的被调用,那么请设置为true
      • 使得原本无法访问的私有成员也可以访问。
    • 参数值为false则指示反射的对象应该实施 Java 语言访问检查。
三、反射操作类中的属性
  • 通过Field类完成。

    • 通过Class类的getDeclaredField(String name)方法取得一个Field对象。
    • 之后使用public void set(Object obj, Object value)方法将obj对象参数上的此Field对象表示的属性设置为指定的新值。
  • 下面我们来写一段程序来测试一下通过反射创建类的对象、调用类中的方法以及操作类中的属性。我们使用上面创建的Dog类创建Class对象:

package com.clown.reflection;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;


public class Test09 {
    public static void main(String[] args) throws Exception {
        //获得 Class对象
        Class c1 = Class.forName("com.clown.reflection.Dog");
        
	    //通过反射,动态的创建对象
        //通过调用 Class对象的 newInstance()创建对象
        System.out.println("================== 通过Class对象的newInstance()方法创建对象 ==================");

        //newInstance();  创建由此类对象表示的类的新实例
//        Object obj = c1.newInstance();
//        Dog dog1 = (Dog)obj;
        Dog dog1 = (Dog)c1.newInstance();  //本质上是调用了 Dog类的无参构造器
        System.out.println(dog1);

        //通过构造器对象的 newInstance(Object ... initargs)方法创建对象
        System.out.println("=============== 通过构造器对象的newInstance(参数值)方法创建对象 ===============");

        Constructor constructor = c1.getDeclaredConstructor(String.class, int.class, String.class);

        //newInstance(Object ... initargs);  使用由此 Constructor对象表示的构造函数,用指定的初始化参数创建和初始化构造函数的声明类的新实例
        Dog dog2 = (Dog)constructor.newInstance("旺财", 5, "张三");
        System.out.println(dog2);

        //通过反射调用普通方法
        System.out.println("============================= 通过反射调用方法 ==============================");

        Dog dog3 = (Dog)c1.newInstance();
        Method setName = c1.getDeclaredMethod("setName", String.class);

        //invoke(Object obj, Object... args);  在具有指定参数的 Method对象上调用此 Method对象表示的基础方法。
        //参数: obj - 从底层方法被调用的对象    args - 用于方法调用的参数值
        setName.invoke(dog3, "大黄");
        System.out.println(dog3.getName());

        Method pee = c1.getDeclaredMethod("pee", null);

        //注意: 不能直接操作私有(private)的方法或属性,我们需要先关闭程序的安全检测 - setAccessible(true)

        //setAccessible(boolean flag);  将此对象的 accessible标志设置为指定的布尔值
        //当值为 true时表示反射对象应该在使用时抑制程序的的安全检查。 值为 false时表示反射的对象应该强制执行程序的的安全检查。
        pee.setAccessible(true);
        pee.invoke(dog3, null);

        //通过反射操作属性
        System.out.println("============================= 通过反射操作属性 ==============================");

        Dog dog4 = (Dog)c1.newInstance();
        Field age = c1.getDeclaredField("age");

        age.setAccessible(true);  //关闭程序的安全检测
        //set(Object obj, Object value);  将指定对象参数上的此 Field对象表示的字段设置为指定的新值
        age.set(dog4, 8);
        System.out.println(dog4.getAge());
    }
}
  • 运行结果:

Java学习笔记40,Java SE 学习笔记合集,java,学习,笔记

使用不同方式调用方法的性能测试
  • 在上面我们说过使用setAccessible(boolean flag)方法关闭程序的检测能提高反射的效率,那么是否真是如此呢?又大概提高了多少呢?下面我们就写一段程序来测试一下使用不同的方式调用方法对性能的影响:
package com.clown.reflection;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

//测试性能
public class Test10 {

    public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, IllegalAccessException {
        test01();
        test02();
        test03();
    }

    //普通方式调用
    public static void test01() {
        Dog dog = new Dog();

        long startTime = System.currentTimeMillis();

        for (int i = 0; i < 1000000000; i++) {
            dog.getName();
        }

        long endTime = System.currentTimeMillis();

        System.out.println("普通方式执行10亿次的时间: " + (endTime - startTime) + "毫秒");
    }

    //反射方式调用
    public static void test02() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        Dog dog = new Dog();
        //获取 Class对象
        Class c1 = dog.getClass();
        //获取 Method对象
        Method getName = c1.getDeclaredMethod("getName", null);

        long startTime = System.currentTimeMillis();

        for (int i = 0; i < 1000000000; i++) {
            getName.invoke(dog, null);  //调用方法
        }

        long endTime = System.currentTimeMillis();

        System.out.println("反射方式执行10亿次的时间: " + (endTime - startTime) + "毫秒");
    }

    //反射方式调用,关闭检测
    public static void test03() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        Dog dog = new Dog();
        //获取 Class对象
        Class c1 = dog.getClass();
        //获取 Method对象
        Method getName = c1.getDeclaredMethod("getName", null);

        getName.setAccessible(true);  //关闭检测

        long startTime = System.currentTimeMillis();

        for (int i = 0; i < 1000000000; i++) {
            getName.invoke(dog, null);  //调用方法
        }

        long endTime = System.currentTimeMillis();

        System.out.println("反射方式并关闭检测执行10亿次的时间: " + (endTime - startTime) + "毫秒");
    }
}
  • 运行结果:

Java学习笔记40,Java SE 学习笔记合集,java,学习,笔记

四、拓展:反射操作泛型
  • Java 采用泛型擦除的机制来引入泛型,Java 中的泛型仅仅是给编译器 Javac 使用的,确保数据的安全性和免去强制类型转换问题,但是,一旦编译完成,所有和泛型有关的类型全部擦除。
  • 为了通过反射操作这些类型,Java新增了ParameterizedTypeGenericArrayTypeTypeVariableWildcardType几种类型来代表不能被归一到Class类中的类型但是又和原始类型齐名的类型。
    • ParameterizedType:表示一种参数化类型,比如Collection<String>
    • GenericArrayType:表示一种元素类型是参数化类型或者类型变量的数组类型。
    • TypeVariable:是各种类型变量的公共父接口。
    • WildcardType:代表一种通配符类型表达式。
  • 下面我们编写代码来练习一下如何通过反射来获取泛型:
package com.clown.reflection;

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 Test11 {

    public static void main(String[] args) throws NoSuchMethodException {
        System.out.println("=========================== 获取方法test01()的形式参数类型 ============================");

        Method method1 = Test11.class.getDeclaredMethod("test01", Map.class, List.class);

        //getGenericParameterTypes();  返回一个 Type对象的数组,Type[]以声明顺序表示由该 Method对象表示的方法的形式参数类型
        Type[] genericParameterTypes = method1.getGenericParameterTypes();

        for (Type genericParameterType : genericParameterTypes) {
            System.out.println("test01()方法的形式参数的类型: " + genericParameterType);
            //ParameterizedType接口: 表示一个泛型,如 Collection <String>
            if (genericParameterType instanceof ParameterizedType) {  //判断该形式参数是否为泛型
                //getActualTypeArguments();  返回一个表示此类型的实际类型参数的数组 Type对象
                Type[] actualTypeArguments = ((ParameterizedType) genericParameterType).getActualTypeArguments();
                for (Type actualTypeArgument : actualTypeArguments) {
                    System.out.println("此泛型的实际类型参数类型: " + actualTypeArgument);
                }
            }
        }

        System.out.println("============================ 获取方法test02()的返回值类型 ============================");

        Method method2 = Test11.class.getDeclaredMethod("test02", null);

        //getGenericReturnType();  返回一个 Type对象,它表示由该 Method对象表示的方法的正式返回类型
        Type genericReturnType = method2.getGenericReturnType();

        System.out.println("test02()方法的返回值的类型: " + genericReturnType);

        if (genericReturnType instanceof ParameterizedType) {
            Type[] actualTypeArguments = ((ParameterizedType) genericReturnType).getActualTypeArguments();
            for (Type actualTypeArgument : actualTypeArguments) {
                System.out.println("此泛型的实际类型参数类型: " + actualTypeArgument);
            }
        }

    }

    //定义一个方法,它的形式参数为泛型
    public void test01(Map<String, Dog> map, List<Dog> list) {
        System.out.println("test01");
    }

    //定义一个方法,它的返回值类型类型为泛型
    public Map<String, Dog> test02() {
        System.out.println("test02");
        return null;
    }

}
  • 运行结果:

Java学习笔记40,Java SE 学习笔记合集,java,学习,笔记

了解什么是ORM
  • Object Relationship Mapping --> 对象关系映射
    Java学习笔记40,Java SE 学习笔记合集,java,学习,笔记
    • 类和表结构对应
    • 属性和字段对应
    • 对象和记录对应
  • 要求:利用注解和反射完成类和表结构的映射关系
五、反射操作注解
  • 我们知道通过Class类的getAnnotations()方法可以获取该类的所有注解。
  • 但是怎样才能获取类中指定的注解呢?
  • 例如:我们想获得类中某个属性(Filed)的注解,那我们只需要先获得该属性的Filed对象,然后再使用该Filed对象的getAnnotation(Class<A> annotationClass)方法,并传入我们想要获取的注解的Class对象,即可获得该注解。同理,若我们想获得某个类(Class)/方法(Method)等的指定的注解也是使用getAnnotation(Class<A> annotationClass)方法。
package com.clown.reflection;

import java.lang.annotation.*;
import java.lang.reflect.Field;

//通过反射操作注解
//ORM
public class Test12 {

    public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException {
        //获得 Class对象
        Class c1 = Class.forName("com.clown.reflection.Student2");

        //通过反射获得注解
        Annotation[] annotations = c1.getAnnotations();
        for (Annotation annotation : annotations) {
            System.out.println(annotation);
        }

        //获得注解的 value的值
        TableClown tableClown = (TableClown) c1.getAnnotation(TableClown.class);
        String value = tableClown.value();
        System.out.println(value);

        System.out.println("===================================================");

        //获得类中的指定的注解
        Field f = c1.getDeclaredField("id");
        FiledClown filedClown = f.getAnnotation(FiledClown.class);
        System.out.println(filedClown);

        //获取并打印注解的参数的值
        System.out.println(filedClown.columnName());
        System.out.println(filedClown.type());
        System.out.println(filedClown.length());
    }

}

//创建一个实体类 -->  学生类
@TableClown("db_student")
class Student2 {
    //属性
    @FiledClown(columnName = "bd_id", type = "int", length = 10)
    private int id;
    @FiledClown(columnName = "bd_age", type = "int", length = 3)
    private int age;
    @FiledClown(columnName = "bd_name", type = "varchar", length = 4)  //在数据库中,"String" 一般用 "varchar" 表示
    private String name;

    //无参构造
    public Student2() {
    }

    //有参构造
    public Student2(int id, int age, String name) {
        this.id = id;
        this.age = age;
        this.name = name;
    }

    //get() & set()
    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    //重写 toString()
    @Override
    public String toString() {
        return "Student2{" +
                "id=" + id +
                ", age=" + age +
                ", name='" + name + '\'' +
                '}';
    }
}

//自定义创建一个【类】的注解
@Target(ElementType.TYPE)  //@TableClown可以放在【类】上
@Retention(RetentionPolicy.RUNTIME)  //@TableClown在源码时、编译为 class时以及运行时都有效
@interface TableClown {
    String value();  //参数: 数据库名
}

//自定义创建一个【属性】的注解
@Target(ElementType.FIELD)  //@FiledClown可以放在【属性】上
@Retention(RetentionPolicy.RUNTIME)  //@FiledClown在源码时、编译为 class时以及运行时都有效
@interface FiledClown {
    String columnName();  //参数: 列名
    String type();  //参数: 类型
    int length();  //参数: 长度
}
  • 运行结果:

Java学习笔记40,Java SE 学习笔记合集,java,学习,笔记文章来源地址https://www.toymoban.com/news/detail-668553.html

到了这里,关于Java学习笔记40的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【LinkedHashMap】| 深度剥析Java SE 源码合集Ⅴ

    众所周知,HashMap 提供的访问,是 无序 的。而在一些业务场景下,我们希望能够提供 有序 访问的 HashMap 。那么此时,我们就有两种选择: TreeMap :按照 key 的顺序。 LinkedHashMap :按照 key 的插入和访问的顺序。 LinkedHashMap ,在 HashMap 的基础之上,提供了 顺序 访问的特性。而

    2023年04月19日
    浏览(38)
  • 【TreeSet】| 深度剥析Java SE 源码合集Ⅳ

    TreeSet是基于红黑树实现的Set集合,它具有以下特点: 有序性:TreeSet是有序的,它按照元素的自然排序进行排序,或者按照指定的Comparator进行排序。 不允许重复元素:与HashSet一样,TreeSet也不允许重复元素,如果试图将一个已经存在的元素添加到TreeSet中,那么添加操作将会

    2023年04月17日
    浏览(41)
  • 【HashMap】| 深度剥析Java SE 源码合集Ⅱ | 你会吗?

    HashMap 是 Map 接口的接口实现类,它采用哈希算法实现,是 Map 接口最常用的实现类。 由于底层采用了哈希表存储数据,所以要求键不能重复,如果发生重复,新的值会替换旧的值。 HashMap 在 查找、删除、修改方面都有非常高的效率 。 HashMap 底层实现采用了哈希表,既集合了

    2024年01月22日
    浏览(28)
  • Java核心技术知识点笔记—Java SE 8的流库(二)

    前言:约简是一种终结操作(terminal operation),它们会将流约简为可以在程序中使用的非流值。例如:Stream中的count方法,返回流中元素的数量。 1、简单约简举例: (1)OptionalT max(Comparator? super T comparator):使用给定的comparator规则,产生流的最大元素,如果流为空,则产生一

    2023年04月18日
    浏览(41)
  • 快给自己充点电,Java SE 完整学习专栏【91篇】,赢在开学起跑线!

    作者主页 :Designer 小郑 作者简介 :Java全栈软件工程师一枚,来自浙江宁波,负责开发管理公司OA项目,专注软件前后端开发、系统定制、远程技术指导。CSDN学院、蓝桥云课认证讲师,全栈领域优质创作者。 JavaSE 是 Java 语言的标准平台和核心技术 ,也被称为Java平台,它提

    2024年02月10日
    浏览(26)
  • JAVA SE基础《一》----JAVA入门

    初识Java 1.Java背景知识 java是美国 sun公司(Stanford University Network) 在1995年推出的一门计算机 高级编程语言 。 Java早期称为Oak(橡树),后期改名为Java。 Java之父: 詹姆斯·高斯林(James Gosling) 。 2009年sun公司被 Oracle(甲骨文) 公司收购。 2.Java能做什么? 桌面应用开发:各

    2024年02月09日
    浏览(40)
  • 【Java SE】 详解java访问限定符

    Java中主要通过类和访问权限来实现封装:类可以将数据以及封装数据的方法结合在一起,更符合人类对事物的认知,而访问权限用来控制方法或者字段能否直接在类外使用。Java中提供了四种访问限定符: 实际只有三种访问限定符,default的意思是默认情况下,不加这三种访问

    2024年02月04日
    浏览(33)
  • 【Java SE】继承

    学习完了类之后,我们将继续学习一个Java中的重点内容 “继承” 举例:    在Cat类中和Dog类中我们发现有很多一样的地方,这样写太浪费空间和内存了 我们可以把它相同的地方都用一个类来表示,并且使用它1.2 继承概念  那能否将这些共性抽取呢?面向对象思想中提出了

    2024年02月05日
    浏览(36)
  • Java SE 面试题

    请简要介绍 Java SE。 Java SE (Java Standard Edition) 是 Java 平台的基础版本,提供 Java 开发所需的所有核心功能。它包括 Java 语言规范、Java 虚拟机规范和 Java 类库。 请解释 Java 的垃圾回收机制。 Java 的垃圾回收机制是自动内存管理的一部分,用于在堆内存中自动回收不再使用的对

    2024年02月01日
    浏览(49)
  • 【Java.SE】数组的练习

    作者简介: 辭七七,目前大一,正在学习C/C++,Java,Python等 作者主页: 七七的个人主页 文章收录专栏 :Java.SE,本专栏主要讲解运算符,程序逻辑控制,方法的使用,数组的使用,类和对象,继承和多态,抽象类和接口等内容 欢迎大家点赞 👍 收藏 ⭐ 加关注哦!💖💖 数

    2024年02月11日
    浏览(29)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包