反射概述及使用(扩展类路径下文件的绝对路径)

1.反射的作用是什么?

通过反射机制可以操作字节码文件。

相关类:

  • java.lang.Class:字节码文件类;
  • java.lang.reflect.Field:属性字节码类;
  • java.lang.reflect.Constructor:构造方法字节码;
  • java.lang.reflect.Method:方法字节码。

要操作字节码文件首先就要获取到字节码文件。

2.获取Class的三种方式

(1)通过类的全称限定名

语法:

Class c = Class.forName("类的全称限定名");

(2)通过对象的getClass()方法

语法:

Class c = 对象.getClass();

(3)通过.class属性

Java语言中,任何一种类型,包括基本数据类型,都有class属性。

语法:

类.class;

(4)三种获取Class方式的实例

package com.dh.reflect;

public class Reflect01 {

    public static void main(String[] args) {
        Class c2 = null;
        try {
            //1.Class.forName()方法
            Class c = Class.forName("com.dh.reflect.User");
            c2 = Class.forName("java.lang.String");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

        //2.对象.getClass()
        String s = "abc";
        Class c3 = s.getClass();

        //不同方式获取到的字节码是一样的
        System.out.println(c2 == c3);	//true

        //3.通过class属性
        Class c4 = String.class;
        System.out.println(c3 == c4);	//true
    }
}

3.Class类的常用方法

当获取到了一个字节码文件时,即Class对象,接下来就是通过Class类中的方法来操作字节码文件了。

我们先来看看,直接输出获取到的字节码文件的话是什么呢?

String s = "abc";
Class c3 = s.getClass();
System.out.println(c3);

结果:

class java.lang.String

由结果可知:直接输出通过反射获取到的字节码文件,输出的是:class+类的全程限定名。

如果我们只想要基础类简单的名称呢,即只要String。

(1)获取字节码文件的简单名称

  • String getSimpleName()

例:

String s = "abc";
Class c3 = s.getClass();
String c3SimpleName = c3.getSimpleName();
System.out.println(c3SimpleName);

结果:

String

(2)构造对象

  • Object newInstance():通过要构造对象的类的空的构造方法构造对象

所以必须存在空的构造方法!!!

User类:

package com.dh.reflect;

public class User {

    public User() {
        System.out.println("构造方法执行了。。。");
    }
}

测试类:

package com.dh.reflect;

public class Reflect02 {

    public static void main(String[] args) {
        Class c = null;
        try {
            //获取字节码文件
            c = Class.forName("com.dh.reflect.User");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        try {
            //构造对象
            Object o = c.newInstance(); //被弃用,有替代方法,后续使用
            System.out.println(o);
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

结果:

构造方法执行了。。。
com.dh.reflect.User@49e4cb85

可以看到,无参构造方法被执行了。

如果没有提供无参构造,会出现实例化异常:

java.lang.InstantiationException: com.dh.reflect.User
	at java.base/java.lang.Class.newInstance(Class.java:571)
	at com.dh.reflect.Reflect02.main(Reflect02.java:15)
Caused by: java.lang.NoSuchMethodException: com.dh.reflect.User.<init>()
	at java.base/java.lang.Class.getConstructor0(Class.java:3349)
	at java.base/java.lang.Class.newInstance(Class.java:556)
	... 1 more
为什么要使用反射机制构造对象呢?

看到这,不知道你会不会有一个疑问呢?为什么要使用反射机制这种这么麻烦的方式去构造对象呢?直接new它不香吗?

如果是new的话,想要改变new的对象时,就得修改源代码;

但是如果是反射机制,还记得属性配置文件吗?

我们可以将类的全称限定名书写在属性配置文件中,要改变new的对象时,只需要修改属性配置文件,而不需要修改源代码。

更加灵活。

例:

属性配置文件:

className = com.dh.reflect.User

测试代码:

package com.dh.reflect;

import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.Properties;

public class Reflect03 {

    public static void main(String[] args) {
        FileReader reader = null;
        try {
            reader = new FileReader("Reflect/src/class.properties");
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
        Properties properties = new Properties();
        try {
            properties.load(reader);
            if(reader != null){
                reader.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        String className = properties.getProperty("className");
        //获取Class
        try {
            Class c = Class.forName(className);
            System.out.println(c);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

(高级框架底层实现原理都采用了反射机制)

扩展一:如何实现只执行一个类的静态代码块

Class.forName()会导致类加载,会把类加载到JVM中,所以会执行静态代码块;

所以如果只想让一个类的静态代码块执行的话,就可以使用Class.forName()。

例:

package com.dh.reflect;

public class Reflect04 {
    public static void main(String[] args) {
        try {
            Class c = Class.forName("com.dh.reflect.Test");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

class Test {
    static {
        System.out.println("静态代码块"); 
    }
}

结果:

静态代码块

扩展二:类加载器(双亲委派模型)

上述提到了类加载器,Java中有三种类加载器:

  • 启动类加载器;(父加载器)
  • 扩展类加载器;(母加载器)
  • 应用类加载器。

假设有String s = "abc";

代码在开始执行之前,会将所需的类全部加载到JVM中,通过类加载器加载;

首先通过启动类加载器加载,在jdk\jre\lib\rt.jar(JDK中最核心的类库)中找;

找不到,就去扩展类加载器中找,在jdk\jre\lib\ext里找,

再找不到,就去应用类加载器中找(专门加载classpath下的jar包)

这种就是双亲委派模型:优先从启动类加载器(父加载器)中找,找不到再找扩展类加载器(母加载器),最后再去应用类加载器中找。

为什么需要双亲委派模型呢?

因为如果自己写了一个String类,sun公司也有一个写好的String类,自己写的植入了后门程序,如果在类似于银行的项目中使用了很多String,就很不安全了!

扩展三:获取类路径下文件的绝对路径

以上的参数都是相对路径,代码一换位置,就会找不到文件了,此时需要使用文件的绝对路径,但是又不能直接写死绝对路径,因为如果写死了绝对路径,也不能移植到Linux环境下,所以需要动态的去获取绝对路径。

前提:文件放在src下。

类文件的根目录是项目的out文件夹,out文件夹下直接是IDEA项目中src下的内容,所以我们可以当作类文件的根目录就是src。


死记硬背下来!!!

String absolutePath = Thread.currentThread().getContextClassLoader().getResource("文件相对于类文件根路径下的相对路径").getPath();
//路径不能有中文,否则会乱码
//再将absolutePath放于io中

上述是以String的形式返回,也可以直接返回流

InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream("文件相对于类文件根路径下的相对路径");
//此时不再需要构建新的io对象了
资源绑定器

Java还帮我们写好了一个工具类,java.util.ResourceBundle,可以直接获取属性配置文件中的内容。

但是有限制:

  1. 文件必须放在类路径下;
  2. 必须是properties文件。
ResourceBundle bundle = ResourceBundle.getBundle("属性文件相对于类文件根路径下的相对路径(注意:属性配置文件只要名字不要后缀.properties,写了会报错)");
bundle.getString("属性配置文件中的key");//获取属性配置文件中指定的值

为防止文章篇幅太长,使用Class类中操作类的属性、构造器和方法,以及父类和接口在下一篇文章介绍。

上一篇:ES6 Proxy与Reflect实现观察者模式


下一篇:GO学习-(17) Go语言基础之反射