从零开始学Java之牛闪闪的“反射”你会用吗?

作者 :孙玉昌,昵称【一一哥 】,另外【壹壹哥】也是我哦

千锋教育高级教研员、CSDN博客专家、万粉博主、阿里云专家博主、掘金优质作者

前言

上一篇文章中,壹哥 给大家讲了注解这个知识点,并给大家提到了"反射"的概念。在Java中,反射是一个介于牛A和牛C之间的技术,玩好了这个,绝对可以让你轻松实现各种变态效果。你是不是让我说的都心痒难耐了?接下来就跟着壹哥来学习反射吧。

------------------------------前戏已做完,精彩即开始----------------------------

全文大约【6500】 字,不说废话,只讲可以让你学到技术、明白原理的纯干货!本文带有丰富的案例及配图视频,让你更好地理解和运用文中的技术概念,并可以给你带来具有足够启迪的思考......

配套开源项目资料

Github: github.com/SunLtd/Lear...

Gitee: gitee.com/sunyiyi/Lea...

一. 反射简介

1. 概念

反射(Reflection)是一种在Java程序运行时动态获取类信息,以及动态操作类的属性、方法和构造方法、注解等元素的技术。

通过这种技术,我们可以更加深入地控制程序的运行过程,在程序运行时动态地获取类的成员变量、方法、构造方法、注解等所有信息,即使这些信息是私有的。拿到这些信息之后,就可以帮助我们更好地了解类的结构和特性,更加灵活地应对不同的业务需求,进而实现一些高级的功能。总的来说,Java的反射机制主要具有以下功能:

  • 在运行时判断任意一个对象所属的类;
  • 在运行时构造任意一个类的对象;
  • 在运行时判断任意一个类所具有的成员变量和方法;
  • 在运行时调用任意一个对象的方法;
  • 生成动态代理。

看到这里,有些小伙伴会很好奇,难道被private修饰的元素也可以被操作吗?正常情况下,private修饰的元素,不是只能被该类自己进行内部访问吗?在正常情况下,确实是这样的!但反射却是一种"不正常"的技术,它能让我们直接操作任何类的私有属性,甚至可以在运行时构造出任意一个类的对象。你可以这么理解,有了反射,任何Java类都不再有隐私可言,你想怎么"蹂躏"这个类都可以。当然,前提条件是我们得先得到这个类的Class字节码才行!

2. 编译期与运行时

另外看到这里,可能有不少小伙伴还是搞不清,运行时是啥意思。在这里,壹哥再跟大家强调一下运行时与编译期这两个概念。

编译期 就是编写好源代码,然后把源码交给编译器编译成计算机可执行文件的过程,也就是把Java代码编成class文件的过程。在Java中,编译期只是做了一些翻译功能,把代码当成文本进行操作,比如检查错误,并没有把代码放在内存中真正地运行起来。
运行期则是把编译后的class字节码文件交给计算机运行,直到程序运行结束。我们把在磁盘中的代码放到内存中执行起来就是运行期。

Java的反射机制就是在运行期工作的,我们可以在运行时获取到类的自身信息。正常情况下,Java源码已经被编译成了字节码,并被加载到了JVM虚拟机中,所以如果没有反射,我们是无法获取到一个正在运行的类信息的。

3. 使用场景

为了让大家更好地理解反射的作用,壹哥给大家举个例子,比如在框架开发中,许多框架都用到了反射来实现一些高级功能。

  • Spring的IoC容器:通过反射,Spring能在运行时动态地创建和管理Java对象,实现松耦合和可配置化的特性。
  • Hibernate的ORM映射:Mybatis使用反射来映射Java对象和数据库表,自动完成对象关系的持久化和查询。
  • MyBatis的Mapper接口:MyBatis使用反射来动态生成Mapper接口的实现类,实现SQL语句的自动映射和执行。

除了框架开发,反射技术还被广泛地应用在其他方面,这些应用可以大大提高我们的开发效率和代码灵活性,尤其是在以下场景中:

  • 框架开发:Java中的许多框架都使用了反射来实现一些高级功能,例如Spring框架的IoC容器、Hibernate框架的ORM映射等。
  • 动态代理:通过反射技术,我们可以在运行时动态地创建一个实现某个接口的代理类,用于处理一些通用的逻辑。
  • 注解处理器:注解处理器通常是在编译期间扫描类的注解信息并生成相应的代码,而通过反射技术,我们就可以在运行时动态地获取类的注解信息,进而实现注解处理器的功能。
  • 插件化开发:插件化开发可以在运行时动态地加载外部类,并动态地创建对象和调用方法等,通过反射我们就可以实现插件化开发。

4. 注意事项

壹哥一直跟自己的学生强调,这个世界上没有十全十美的技术,反射虽然给我们提供了一种强大的动态编程方式,但也有一些需要我们注意的地方:

  • 反射的性能较低,因此在实际使用中应避免过度使用反射技术;
  • 反射操作可能会破坏原有的封装性,因此在使用时应格外谨慎,避免对程序的可维护性造成影响;
  • 反射操作可能会导致代码更加复杂,因此在使用反射时应尽量保持代码的简洁性,避免使用过于复杂的反射操作;
  • 反射操作可能会影响程序的安全性,因此在使用反射时应进行必要的安全检查,避免恶意代码通过反射入侵程序。

了解了以上这些理论内容之后,你可能已经迫不及待地要动手实操一把了,接下来我们就来看看反射该怎么使用吧。

二. 基本使用

1. 常用API

要想实现反射操作,我们需要用到以下几个常用的反射API:

类名 描述
Class 表示一个类的运行时信息,包括类的成员变量、方法、构造函数、注解等信息。
Constructor 表示一个类的构造函数信息。
Field 表示一个类的成员变量信息。
Method 表示一个类的方法信息。
Modifier 表示一个类的修饰符信息,包括public、private、protected、static等修饰符。
Annotation 表示注解类型的信息。

这些API给我们提供了丰富的反射功能,让我们编写出更加灵活和高效的代码,可以在运行时动态地获取到类的信息、调用类的方法、访问类的成员变量等。接下来壹哥就具体给大家讲一下这些API的具体用法。

2. 获取Class字节码

Java反射类位于java.lang.reflect包中,其中的核心类是java.lang.Class,它表示一个类或接口的运行时信息。要想进行反射,首先第一步就是要得到Class字节码对象,只有得到了Class对象,我们才可以进一步获取到类的成员变量、方法、构造函数、注解等信息, 所以Class类是实现反射的关键。Class没有公开的构造方法,Class实例会由JVM在类加载时自动创建。

在Java中,目前主要有以下三种获取字节码的方式:

  • 类名.class: 该方式最为简单,直接在类名后面加上.class即可,该方式可以直接获取已知的类。
  • 对象.getClass(): 该方式比较灵活,可以在运行时获取已知对象的实际类型。
  • Class.forName("类的全路径"): 该方式最为灵活,可以根据类名的字符串动态获取类,适用于需要根据外部条件动态加载类的场景。

2.1 类名.class

这是最简单的一种方式,我们只需要在类名的后面加上.class即可获取到Class对象,例如:

java 复制代码
Class<String> clazz = String.class;

2.2 对象.getClass()

如果我们已经创建出一个对象,那就可以通过该对象的getClass()方法获取到对应的Class对象,例如:

java 复制代码
String s = "hello";
Class<? extends String> clazz = s.getClass();

2.3 Class.forName()

最后一种方式是利用Class.forName()方法,通过类名来获取Class对象,例如:

java 复制代码
Class<?> clazz = Class.forName("java.lang.String");

2.4 Class相关方法

Class也是一个类,在该类中具有以下这些常用的方法,可以供我们得到其他的类型。

类型 访问方法 返回值类型 说明
包路径 getPackage() Package对象 获取该类的存放路径
类名称 getName() String对象 获取该类的名称
继承类 getSuperclass() Class对象 获取该类继承的父类
实现接口 getlnterfaces() Class型数组 获取该类实现的所有接口
构造方法 getConstructors() Constructor型数组 获取所有public的构造方法
getDeclaredContruectors() Constructor对象 获取当前对象的所有构造方法
方法 getMethods() Methods型数组 获取所有public的方法
getDeclaredMethods() Methods对象 获取当前对象的所有方法
成员变量 getFields() Field型数组 获取所有public的成员变量
getDeclareFileds() Field对象 获取当前对象的所有成员变量
内部类 getClasses() Class型数组 获取所有public的内部类
getDeclaredClasses() Class型数组 获取所有内部类
内部类的声明类 getDeclaringClass() Class对象 如果该类为内部类,则返回它的成员类,否则返回null

2.5 小结

以上三种获取字节码的方式都有各自的适用场景,并有其优缺点,壹哥给大家总结如下:

方式 优点 缺点
类名.class 简单易用,性能高 只能获取已知的类
对象.getClass() 可以获取到已知对象的类 必须先有对象才能获取
Class.forName() 可以根据类名动态获取到类对象 可能会抛出ClassNotFoundException异常

总的来说,以上这三种方式各有优缺点,至于选择哪种方式,要取决于具体的应用场景和需求。当我们获取到了类的字节码之后,就可以进行下一步开发了。

3. 创建Person类

为了方便后面的测试,我们先创建一个Person类,代码如下:

java 复制代码
/**
 * @author 一一哥Sun
 * @company 千锋教育
 */
public class Person {
    public String name;
    private int age;
    protected String address;

    public Person() {}

    public Person(String name, int age, String address) {
        this.name=name;
        this.age=age;
        this.address=address;
    }

    private Person(int age) {
        this.age=age;
    }

    protected Person(String name) {
        this.name=name;
    }

    public String getName() {
        return name;
    }

    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 getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    @Override
    public String toString() {
        return "Person [name=" + name + ", age=" + age + ", address=" + address + "]";
    }
}

4. 获取成员变量

4.1 Field相关方法

我们可以通过下列任意方法得到Field类型的对象或数组:

  • getFields()
  • getField(String name)
  • getDeclaredFields()
  • getDeclaredField(String name)

得到了Field类,我们再来看看Field类的一些常用方法,这些方法主要用于获取和操作类的成员变量。各方法的主要功能含义如下表:

方法 描述
public Class<?> getType() 返回该成员变量的类型
public String getName() 返回该成员变量的名称
public int getModifiers() 返回该成员变量的修饰符,以整数方式存储
public void setAccessible(boolean flag) 设置该成员变量的可访问标志
public Object get(Object obj) 返回指定对象上该成员变量的值
public void set(Object obj, Object value) 将指定对象上该成员变量设置为指定的新值
public boolean isAccessible() 判断该成员变量是否可访问
public boolean isEnumConstant() 判断该成员变量是否是枚举常量
public boolean isSynthetic() 判断该成员变量是否是合成成员变量

了解了这些方法的基本含义之后,我们再来看一个具体的实现案例。

4.2 代码实现

在获取到Class对象之后,我们可以通过Class对象的getField()方法或getDeclaredField()方法来获取类的成员变量。其中,getField()方法只能获取道公开的成员变量,而getDeclaredField()方法可以获取到所有的成员变量,包括公有、私有或受保护的成员变量

java 复制代码
import java.lang.reflect.Field;

/**
 * @author 一一哥Sun
 * @company 千锋教育
 */
public class Demo01 {
    public static void main(String[] args) {
        try {
            //给对象赋值
            Person person=new Person();
            person.setName("一一哥");
            person.setAge(18);
            person.setAddress("千锋上海");
			
            // 获取Person类的字节码
            Class<Person> clazz = Person.class;
            // 获取到公有的成员变量name
            Field nameField = clazz.getField("name");
            //获取属性的值。正常情况下是"String name=person.get()",反射时则正好相反,"field.get(对象)"
            Object nameValue = nameField.get(person);
            System.out.println("name = "+nameValue);
			
            // 获取到私有的成员变量age
            Field ageField = clazz.getDeclaredField("age"); 
            //注意:private修饰的元素必须要设置可访问性,否则会出现非法访问的异常"java.lang.IllegalAccessException",
            //设置私有属性的可访问性,属于是"暴力反射"
            ageField.setAccessible(true);
            Object ageValue = ageField.get(person);
            System.out.println("age = "+ageValue);
			
            // 获取到受保护的成员变量address
            Field addressField = clazz.getDeclaredField("address"); 
            Object addressValue = addressField.get(person);
            System.out.println("address = "+addressValue);
			
            //获取所有公开的属性
            Field[] fields = clazz.getFields();
            for(Field field: fields) {
                Object value = field.get(person);
                System.out.println("public value---"+value);
            }
			
            //获取到所有的成员变量,无论公开或私有
            Field[] declaredFields = clazz.getDeclaredFields();
            for(Field field: declaredFields) {
                //设置可访问性
                field.setAccessible(true);
                Object value = field.get(person);
                System.out.println("value---"+value);
            }
        } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) {
            e.printStackTrace();
        } 
    }
}

在上面的代码中,我们用到了一个setAccessible()方法,该方法是反射类java.lang.reflect.AccessibleObject中的一个方法,用于设置成员变量、方法、构造函数等的可访问性 。默认情况下,Java会限制对非公有的成员变量、方法、构造函数的访问,如果我们直接调用这些非公有的成员,会抛出IllegalAccessException异常。我们可以通过调用setAccessible(true)方法来打破这种限制,使得非公有的成员也可以被访问。所以在上面的代码中,壹哥在获取私有的age属性值之前,先setAccessible(true),这样才能得到age的值,否则就会报错。

但需要注意的是,使用setAccessible(true)方法会打破Java的访问控制机制,可能会破坏代码的封装性,导致代码变得更加脆弱和不易维护。因此,我们要谨慎使用setAccessible()方法,尽量避免影响程序的可维护性。

5. 属性的取值与赋值

5.1 取值

如果我们想要获取Java对象的属性值,可以使用反射类java.lang.reflect.Field的get()方法。使用get()方法需要两个参数:要获取属性值的对象和属性对象。例如,假设我们有一个名为person的对象,以及一个名为age的int类型属性,我们可以使用如下代码来获取person对象的age属性值:

java 复制代码
/**
 * @author 一一哥Sun
 * @company 千锋教育
 */
public class Person {
    private int age;
    //省略get和set方法
}

Person person = new Person();
person.age = 20;

Class<Person> personClass = Person.class;
Field ageField = personClass.getDeclaredField("age");
// 私有属性需要打破访问限制
ageField.setAccessible(true); 
// 获取age属性的值
int age = (int) ageField.get(person); 

需要注意的是,我们在使用get()方法获取私有的属性值时,必须首先使用setAccessible(true)方法打破访问限制,否则会抛出IllegalAccessException异常。此外,使用get()方法获取属性值时,需要指定属性的类型,否则可能会发生类型转换错误。

5.2 赋值

而如果我们想要给Java对象的属性赋值,则可以使用反射类java.lang.reflect.Field的set()方法。使用set()方法需要三个参数:要设置属性值的对象、属性对象和属性值。例如,假设我们有一个名为person的对象,以及一个名为age的int类型属性,我们可以使用如下代码将person对象的age属性值设置为20:

java 复制代码
/**
 * @author 一一哥Sun
 * @company 千锋教育
 */
public class Person {
    private int age;
    //省略get和set方法
}

Person person = new Person();

Class<Person> personClass = Person.class;
Field ageField = personClass.getDeclaredField("age");
//赋值时,私有属性也要打破访问限制
ageField.setAccessible(true); 
// 设置age属性的值
ageField.set(person, 20); 

需要注意的是,我们在使用set()方法给私有属性设置属性值时,也必须首先使用setAccessible(true)方法来打破访问限制,否则也会抛出IllegalAccessException异常。此外,使用set()方法设置属性值时,还需要指定正确的属性类型,否则可能会发生类型转换错误。

6. 获取方法

6.1 Method相关方法

我们可以通过以下方法来得到一个Method类或Method数组:

  • getMethods()
  • getMethods(String name,Class<?> ...parameterTypes)
  • getDeclaredMethods()
  • getDeclaredMethods(String name,Class<?>...parameterTypes)

得到Method类之后,我们再来看看它的常用方法有哪些。

方法 描述
public Class<?> getReturnType() 返回该方法的返回值类型
public Class<?>[] getParameterTypes() 返回该方法的参数类型
public int getModifiers() 返回该方法的修饰符,以整数方式存储
public void setAccessible(boolean flag) 设置该方法的可访问标志
getExceptionTypes() 以Class数组的形式,获得该方法可能抛出的异常类型
isVarArgs() 查看该方法是否允许带有可变数量的参数,如果允许则返回 true,否则返回false
public Object invoke(Object obj, Object... args) 调用该方法
public boolean isAccessible() 判断该方法是否可访问
public boolean isBridge() 判断该方法是否是桥方法
public boolean isDefault() 判断该方法是否是默认方法
public boolean isSynthetic() 判断该方法是否是合成方法

6.2 代码实现

其实获取类的方法与获取成员变量类似,同样有getMethod()方法和getDeclaredMethod()方法两种方式。其中,getMethod()方法只能获取公有的方法,而getDeclaredMethod()方法则可以获取所有的方法,包括公开、私有和受保护的方法。

java 复制代码
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * @author 一一哥Sun
 * @company 千锋教育
 */
public class Demo02 {
    public static void main(String[] args) {
        try {
            // 给对象赋值
            Person person = new Person();
            person.setAge(18);
            person.setAddress("千锋上海");

            // 获取Person类的字节码
            Class<Person> clazz = Person.class;
            // 获取到公开方法setName
            // Method getNameMethod = clazz.getMethod("setName");
            // getMethod()方法中的第一个参数是反射的方法名称,第二个参数是该方法中的参数类型
            Method getNameMethod = clazz.getMethod("setName", String.class);
            // 如果是私有的方法,也需要设置可访问性
            // getNameMethod.setAccessible(true);
            // 正常情况下,void setName(String name)--->person.setName("一一哥");
            // 反射时:method.invoke(类对象,参数值),如果没有参数则传递null即可,返回值是方法的执行结果,setName()方法没有返回值
            Object result = getNameMethod.invoke(person, "一一哥");
            System.out.println("name= " + person.getName());

            // 获取私有方法getAge
            Method getAgeMethod = clazz.getDeclaredMethod("getAge");
            // getAge()方法没有参数,所以第二个参数则传入null
            Object ageValue = getAgeMethod.invoke(person, null);
            System.out.println("age= " + ageValue);

            // 获取受保护的方法getAddress
            Method getAddressMethod = clazz.getDeclaredMethod("getAddress");
            Object addressValue = getAddressMethod.invoke(person, null);
            System.out.println("address= " + addressValue);
        } catch (SecurityException | IllegalArgumentException 
        | NoSuchMethodException | IllegalAccessException| InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}

在上面的案例中,我们是通过以下语句来获取类中的方法:

java 复制代码
Method method = clazz.getMethod("methodName", parameterTypes);

其中,methodName是方法名,parameterTypes是方法参数的类型。如果该方法没有参数,则可以省略。找到Method之后,我们可以用以下语句进行方法的调用:

java 复制代码
Object result = method.invoke(object, args);

其中,object是方法所属的对象,如果该方法是静态的,则可以传入null。args是方法的参数列表,如果该方法没有参数,则传入null。result是该方法的返回值,如果没有返回值,则result会为null。

7. 获取构造方法

7.1 Constructor常用方法

下表中是Constructor类的常用方法,主要用于获取和操作类的构造方法。

方法 描述
public Class<?>[] getParameterTypes() 返回构造方法的参数类型
isVarArgs() 查看该构造方法是否允许带可变数量的参数,如果允许,返回 true,否则返回false
getExceptionTypes() 以 Class 数组的形式获取该构造方法可能抛出的异常类型
public int getModifiers() 返回构造方法的修饰符,以整数方式存储
public void setAccessible(boolean flag) 设置该构造方法的可访问标志
public Object newInstance(Object... initargs) 使用指定的初始化参数创建类的新实例
public boolean isAccessible() 判断该构造方法是否可访问
public boolean isSynthetic() 判断该构造方法是否是合成构造方法

7.2 代码实现

我们可以通过以下方法来得到一个Constructor对象或数组:

  • getConstructors()
  • getConstructor(Class<?>...parameterTypes)
  • getDeclaredConstructors()
  • getDeclaredConstructor(Class<?>...parameterTypes)

其中,getConstructor()方法只能获取公开的构造方法,而getDeclaredConstructor()方法则可以获取所有的构造方法,包括公开、私有和受保护的构造方法。

java 复制代码
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

/**
 * @author 一一哥Sun
 * @company 千锋教育
 */
public class Demo03 {
    public static void main(String[] args) {
        try {
            // 获取Person类的构造方法
            Class<Person> clazz = Person.class;
            // 获取公开的构造方法
            Constructor<Person> publicConstructor = clazz.getConstructor(String.class, int.class, String.class);
            //调用构造方法
            Person newInstance = publicConstructor.newInstance("一一哥",18,"千锋上海");
            System.out.println("person="+newInstance.toString());
			
            // 获取私有的构造方法
            Constructor<Person> privateConstructor = clazz.getDeclaredConstructor(int.class);
            //私有方法需要设置可访问性,否则会产生非法访问异常
            privateConstructor.setAccessible(true);
            //调用私有的构造方法
            Person newInstance2 = privateConstructor.newInstance(18);
            System.out.println("person2="+newInstance2.toString());
			
            // 获取受保护的构造方法
            Constructor<Person> protectedConstructor = clazz.getDeclaredConstructor(String.class);
            Person newInstance3 = protectedConstructor.newInstance("壹哥");
            System.out.println("person3="+newInstance3.toString());
        } catch (SecurityException | IllegalArgumentException | NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}

我们拿到了类的Class对象和构造方法之后,就可以在运行时动态地创建对象了,主要是使用Constructor对象的newInstance()方法来创建对象。对于私有的构造方法,同样需要设置setAccessible(true)。

8. getMocMers()方法

壹哥在上面给大家介绍各种API时,多次提到过一个getMocMers()方法。通过该方法,我们可以得到一个 java.lang.reflect.Modifier类,进而可以可以解析出修饰符的信息。这些修饰符信息如下表所示:

静态方法名称 说明
isStatic(int mod) 如果使用static修饰符修饰,则返回 true,否则返回 false
isPublic(int mod) 如果使用public修饰符修饰,则返回 true,否则返回 false
isProtected(int mod) 如果使用protected修饰符修饰,则返回 true,否则返回 false
isPrivate(int mod) 如果使用private修饰符修饰,则返回 true,否则返回 false
isFinal(int mod) 如果使用final修饰符修饰,则返回 true,否则返回 false
toString(int mod) 以字符串形式返回所有的修饰符

对于上述方法,大家仅做了解,开发的时候知道怎么得到修饰符即可。

------------------------------正片已结束,来根事后烟----------------------------

三. 结语

在今天的这篇文章中,壹哥 主要是给大家介绍了反射的基本概念及常规用法,希望大家把今天的代码好好练习。掌握了这些内容之后,对我们在后面深刻理解Java的各种框架底层实现会有着莫大的帮助。在下一篇文章中,壹哥会继续给大家讲解反射的一些高级功能,希望大家继续关注哦。

另外如果你独自学习觉得有很多困难,可以加入壹哥的学习互助群,大家一起交流学习。

相关推荐
_oP_i25 分钟前
Pinpoint 是一个开源的分布式追踪系统
java·分布式·开源
mmsx28 分钟前
android sqlite 数据库简单封装示例(java)
android·java·数据库
武子康1 小时前
大数据-258 离线数仓 - Griffin架构 配置安装 Livy 架构设计 解压配置 Hadoop Hive
java·大数据·数据仓库·hive·hadoop·架构
豪宇刘2 小时前
MyBatis的面试题以及详细解答二
java·servlet·tomcat
秋恬意2 小时前
Mybatis能执行一对一、一对多的关联查询吗?都有哪些实现方式,以及它们之间的区别
java·数据库·mybatis
刘大辉在路上2 小时前
突发!!!GitLab停止为中国大陆、港澳地区提供服务,60天内需迁移账号否则将被删除
git·后端·gitlab·版本管理·源代码管理
FF在路上3 小时前
Knife4j调试实体类传参扁平化模式修改:default-flat-param-object: true
java·开发语言
真的很上进3 小时前
如何借助 Babel+TS+ESLint 构建现代 JS 工程环境?
java·前端·javascript·css·react.js·vue·html
众拾达人3 小时前
Android自动化测试实战 Java篇 主流工具 框架 脚本
android·java·开发语言
皓木.3 小时前
Mybatis-Plus
java·开发语言