[Java]反射——框架设计的灵魂

本文深入讲解Java反射机制,包括反射的概念、使用场景、获取Class对象的三种方式、Class类对象的功能,以及通过多个案例演示反射在实际编程中的应用,如框架设计、Bean对象工厂和Spring IoC底层实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前沿:
反射——框架设计的灵魂
使用框架,不会反射也没关系,因为框架已经写好了,并不需要用反射;
开发框架,写框架,需要用到反射;
理解反射,掌握反射,在学习框架,使用框架时,也能使用的更好。

1. 反射概念

  • 反射:将类的各个组成部分封装为其他对象,这就是反射机制

    • 将类的成员变量封装为Filed对象;
    • 将类的构造方法封装为Constructor对象;
    • 将类的成员方法封装为Method对象。
  • Java代码在计算机中经历的三个阶段:

    1. Source 源代码阶段
      源代码阶段代码还在硬盘
    2. Class 类对象阶段
    3. Runtime 运行时阶段

字节码文件Person.class要变成Person对象(Person对象在内存中),即需要把字节码文件Person.class加载进内存后才能有Person对象。所以在第二个阶段需要用类加载器 ClassLoader 把字节码文件加载进内存。
在内存中如何描述字节码文件Person.class?在Java中万物皆对象,所以在内存中会有一个对象来描述这个字节码文件,这个对象即为 Class 类对象
所有第三阶段的对象,如Person对象都是通过这些 Class 类对象创建出来的。

反射三个阶段

Java中有一个 Class 类,描述所有字节码文件,这些物理文件的共同特征和行为。
不管是什么字节码,或多或少都有一些:成员变量、构造方法、成员方法。所以Class 类对象有三部分比较重要的东西:成员变量、构造方法、成员方法。这三者是三部分不同的东西,每一部分有每一部分的特征,把这三部分分别封装为不同的对象:将类的成员变量封装为Filed对象;将类的构造方法封装为Constructor对象;将类的成员方法封装为Method对象。

  • 反射好处
    1. 在程序的运行过程中,操作这些对象。
    2. 可以解耦,降低程序的耦合性,降低程序的紧密程度,来提高程序的可扩展性。

2. 2. 获取字节码Class类对象的三种方式

  1. 如果Java代码在第一个阶段,意味着Java只有字节码文件,并没有进内存,我们需要手动将它加载进内存,生成字节码Class类对象。
    Class.forName(“全限定类名”) :将字节码文件加载进内存,返回Class类对象。
    全限定类名=包名.类名
    forName(“全限定类名”) 是静态方法,通过类名 Class 可以直接打点调用。

  2. 如果在第二阶段,已经将字节码文件加载进内存,也就是说这个Class类对象已经有了,那么不需要加载它,只需要获取它即可。
    类名.class:通过类名的属性class获取

  3. 如果在第三阶段,已经有对象,可以通过对象的方法来获取。
    对象.getClass()
    getClass()方法是在Object类中定义的,被所有的对象继承。

  • 结论
    同一个字节码文件(XXX.class)在一次程序运行过程中,只会被加载一次,不论通过以上哪一种方式获取的Class类对象都是同一个。

  • 三种方式适用场合

    1. Class.forName(“全限定类名”)
      多用于配置文件,将类名定义在配置文件中。读取配置文件,加载类。
      因为Class.forName(“全限定类名”)传的是字符串,所以从配置文件中读出来的也是字符串。
    2. 类名.class
      多用于参数的传递。传参,传Class类对象的话,用此方式较为方便。
    3. 对象.getClass()
      多用于对象的获取字节码的方式。有对象了,想获取字节码文件对象,可以用此方式获取。

3. Class类对象功能

  1. 获取功能

    1. 获取成员变量们
      1. Field[] getFields():获取所有public修饰的成员变量(以下同理)
      2. Field getField(String name):获取指定名称public修饰的成员变量(以下同理)
      3. Field[] getDeclaredFields():获取所有成员变量不考虑修饰符(以下同理)
      4. Field getDeclaredField(String name)
    2. 获取构造方法们
      1. Constructor<?>[] getConstructors()
      2. Constructor getConstructor(类<?>… parameterTypes)
      3. Constructor getDeclaredConstructor(类<?>… parameterTypes)
      4. Constructor<?>[] getDeclaredConstructors()
    3. 获取成员方法们
      1. Method[] getMethods()
      2. Method getMethod(String name, 类<?>… parameterTypes)
        • 方法具有三要素:方法名、返回值列表、参数列表
        • 确定一个方法有两要素:方法名、参数列表
          • 因为方法名一样,参数列表不一样
      3. Method[] getDeclaredMethods()
      4. Method getDeclaredMethod(String name, 类<?>… parameterTypes)
    4. 获取全限定类名
      1. String getName()
  2. 成员变量(Field)对象对应的方法
    操作:

    1. 设置值
      void set(Object obj, Object value)
    2. 获取值
      get(Object obj)
    3. 忽略访问权限修饰符的安全检查
      setAccessible(true):暴力反射
      在反射面前没有是隐私的
      Field、Constructor、Method里都有这么一个方法
  3. 构造方法(Constructor)对象对应的方法
    构造器对象用来创建对象:
    * T newInstance(Object… initargs)
    * 如果使用空参数构造方法创建对象,操作可以简化:Class对象的newInstance方法——Class对象.newInstance()(常用此操作)

  4. 成员方法(Method)对象对应的方法

    • 执行方法:
      Object invoke(Object obj, Object… args)
    • 获取方法名称:
      String getName():获取方法名

4. 案例1

  • 需求:写一个"框架",不能改变该类的任何代码的前提下,可以帮我们创建任意类的对象,并且执行其中任意方法
    • 实现:
      1. 配置文件
      2. 反射
    • 步骤:
      1. 将需要创建的对象的全类名和需要执行的方法定义在配置文件中;
      2. 在程序中加载并读取配置文件;
      3. 使用反射技术来加载类文件进内存;
      4. 创建对象;
      5. 获取方法对象;
      6. 执行方法

ReflectTest.java文件:

//1.加载配置文件
//1.1 创建Properties对象
Properties pro = new Properties();
//1.2 加载配置文件,转换为一个集合
//1.2.1 获取class目录下的配置文件
/*
第一步:类名.class:拿到当前类的字节码
第二步:.getClassLoader():获取这个字节码的类加载器
第三步:.getResourceAsStream(filePath):根据类加载器读取指定的配置文件并转换为字节流
*/
ClassLoder classLoder = ReflectTest.class.getClassLoader();//获取了字节码文件对应的类加载器,是由这个类加载器把ReflectTest加载进内存的
/*
URL getResource(String name):获取资源的路径
InputStream getResourceAsStream(String name):获取资源对应的字节流
*/
InputStream is = classLoder.getResourceAsStream("pro.properties");
pro.load(is);

//2.获取配置文件中定义的数据
String className = pro.getProperty("className");//获取配置文件中的加载类的全类名
String methodName = pro.getProperty("methodName");//获取配置文件中的需要执行的方法名称

//3.加载该类进内存(反射)
Class cls = Class.forName(className);

//4.创建对象
Object obj = cls.newInstance();	

//5.获取方法对象
Method method = cls.getMethod(methodName);

//6.执行方法
method.invoke(obj);
  • Q:改代码和改配置文件有啥区别?
    A:庞大的系统,改Java代码,代码一旦改了,需要重新测试,重新编译,重新上线;
    但是仅仅改配置文件的话,配置文件只是一个物理文件,改完之后就完事了。而且改配置文件的方式让程序的扩展性更强一些。

  • 将来只要在配置文件中看到有一个地方配了全限定类名,第一时间应该反应过来:用的是反射机制。

5. 案例2:一个创建Bean对象的工厂

  1. BeanJavaBean
  • Bean:在计算机英语中,有可重用组件的含义

  • JavaBean:用Java语言编写的可重用组件

    • javabean != 实体类
    • javabean范围远大于实体类
  • 一个创建Bean对象的工厂,它就是创建我们的service和dao对象

  1. 工厂模式
  • 工厂模式优势:
    解耦(降低类之间的依赖关系),使我们不用每次重新编译、重新部署,包括重新启动Tomcat。
  1. 解耦思路

    • 第一步:需要一个配置文件配置service和dao
      • 配置文件可以是xml也可以是properties(这里采用properties)
      • 配置文件的内容:唯一标志=全限定类名(key=value)
    • 第二步:通过读取配置文件中配置的内容,反射创建对象
  2. 单例对象多例对象

  • 单例对象:只被创建一次,从而类中的成员也就只会初始化一次。
  • 多例对象:每个对象都是重新初始化的。多例对象被创建多次,执行效率没有单例对象高。
  • 我们需要每次都是同一个对象,即单例对象。
    .newInstance()会调用默认构造函数创建对象
    我们的方法只能.newInstance()一次。如果创建完之后不存起来由于Java的垃圾回收机制,它会在长时间不用时被回收,当下次再用时肯定就没有了。所以.newInstance()用完,对象创建出来之后应该马上存起来。
    此案例中,我们定义一个Map,用于存放我们要创建的对象。(如下代码中所示)

bean.properties文件:

accountService=service.impl.AccountServiceImpl
accountDao=dao.impl.AccountDaoImpl

BeanFactory.java文件:

package factory;

import java.io.InputStream;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

/**
 * 一个创建Bean对象的工厂
 *
 * Bean:在计算机英语中,有可重用组件的含义
 * JavaBean:用Java语言编写的可重用组件
 *          javabean != 实体类
 *          javabean范围远大于实体类
 *
 * 一个创建Bean对象的工厂 它就是创建我们的service和dao对象的。
 *
 * 第一个:需要一个配置文件来配置我们的service和dao
 *          配置文件的内容:唯一标志=全限定类名(key=value)
 * 第二个:通过读取配置文件中配置的内容,反射创建对象
 *
 * 我们的配置文件可以是xml也可以是properties
 */

public class BeanFactory {
    //定义一个Properties对象
    private static Properties props;

    //定义一个Map,用于存放我们要创建的对象。我们把它称之为容器。
    private static Map<String, Object> beans;

    //使用静态代码块为Properties对象赋值
    static {
        try {
            //实例化对象
            props = new Properties();
            //获取properties文件的流对象
            InputStream in = BeanFactory.class.getClassLoader().getResourceAsStream("bean.properties");//读取配置文件
            props.load(in);

            //实例化容器
            beans = new HashMap<String, Object>();
            //取出配置文件中所有的Key
            Enumeration keys = props.keys();//返回一个枚举类型
            //遍历枚举
            while(keys.hasMoreElements()){
                //取出每个Key
                String key = keys.nextElement().toString();
                //根据key获取value
                String beanPath = props.getProperty(key);
                //反射创建对象
                Object value = Class.forName(beanPath).newInstance();
                //把key和value存入容器中
                beans.put(key, value);
            }
//            for (String key : beans.keySet()) {
//                System.out.println(key);
//                System.out.println(beans.get(key));
//            }
        }catch (Exception e){
            throw new ExceptionInInitializerError("初始化properties失败!");
        }
    }

    /**
     * 根据Bean的名称获取bean对象
     * @param beanName
     * @return
     *
    public static Object getBean(String beanName){
        Object bean = null;
        try {
            String beanPath = props.getProperty(beanName);
//            System.out.println("全限定类名:" + beanPath);
            // 用反射的方式创建对象
            bean = Class.forName(beanPath).newInstance();//每次都会调用默认构造函数创建对象
        }catch (Exception e){
            e.printStackTrace();
        }
        return bean;
    }*/

    /**
     * 根据bean的名称获取对象
     * @param beanName
     * @return
     */
    public static Object getBean(String beanName){
        return beans.get(beanName);//从map中获取beanName对应的对象
    }

}

使用:
xxxServlet.java文件中:

IAccountService as = (IAccountService) BeanFactory.getBean("accountService");

AccountServiceImpl.java文件中:

private IAccountDao accountDao = (IAccountDao) BeanFactory.getBean("accountDao");

6. 案例3:Spring IoC 底层实现

  • 核心技术点:XML解析 + 反射
  • 具体思路: 配置XML文件,程序读配置文件,把信息读出来,创建对象,装到Map里,再从Map里把对象取出来。
    1、根据需求编写 XML 文件,配置需要创建的 bean。
    2、编写程序读取 XML 文件(dom4j 解析 XML),获取 bean 相关信息,id、类、属性。
    3、根据第 2 步获取到的信息,结合反射机制动态创建对象,同时完成属性的赋值。
    4、将创建好的 bean 存入 Map 集合,设置 key - value 映射,key 就是 bean 中 id 值,value 就是 bean 对象。
    5、提供方法从 Map 中通过 id 获取到对应的 value。

ApplicationContext 接口

package ioc;

public interface ApplicationContext {
    public Object getBean(String id);
}

ClassPathXmlApplicationContext 类

package ioc;

import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

public class ClassPathXmlApplicationContext implements ApplicationContext{
    private Map<String, Object> ioc = new HashMap<String, Object>();

    public ClassPathXmlApplicationContext(String path) {
        try {
            //1. 将spring.xml转成Document对象
            SAXReader reader = new SAXReader();
            Document document = reader.read("./src/main/resources/" + path);
            //2. dog4j 解析 XML
            //2.1 首先获取根节点对象<beans>
            Element root = document.getRootElement();
            //2.2 迭代获取子节点对象<bean>
            //获取一个迭代器
            Iterator<Element> iterator = root.elementIterator();
            //循环
            while(iterator.hasNext()){//若迭代器中有下一个
                Element element = iterator.next();
                //2.2.1 获取key,即bean中的id属性值
                String id = element.attributeValue("id");//把属性名id传进去
                //2.2.2 获取全类名,即bean中的class属性值
                String className = element.attributeValue("class");//把属性名class传进去

                //2.2.3 获去value,即通过全类名,通过反射机制调用无参构造函数创建对象
                //2.2.3.1 通过反射机制获取class对象
                Class clazz = Class.forName(className);
                /*
                //2.2.3.2 获取无参构造方法对象
                Constructor constructor = clazz.getConstructor();
                //2.2.3.3 创建目标对象
                Object object = constructor.newInstance();
                */
                //2.2.3.2 调用无参构造函数创建目标对象
                //如果使用无参数构造方法创建对象,可简化为:Class对象的newInstance方法,即Class对象.newInstance()(常用此操作)
                Object object = clazz.newInstance();

                //2.2.4 给目标对象赋值
                Iterator<Element> beanIter = element.elementIterator();
                while(beanIter.hasNext()){
                    Element property = beanIter.next();
                    //获取property中name、value、ref的值
                    String name = property.attributeValue("name");
                    String valueStr = property.attributeValue("value");
                    String ref = property.attributeValue("ref");

                    //通过反射调用目标对象的set方法完成对对象的赋值
                    //字符串拼接得到set方法名
                    String methodName = "set" + name.substring(0, 1).toUpperCase() + name.substring(1);
                    //获取成员变量对象
                    Field field = clazz.getDeclaredField(name);// 获取private修饰的成员变量
                    //第一个参数是set方法名,第二个参数是set方法的参数类型
                    Method method = clazz.getDeclaredMethod(methodName, field.getType());
                    if(ref == null){//<property name="" value=""/>
                        //根据成员变量的数据类型将 valueStr 进行转换
                        Object value = null;
                        if(field.getType().getName() == "long"){
                            value = Long.parseLong(valueStr);//包装类方法将String类型转成long类型
                        }
                        if(field.getType().getName() == "java.lang.String"){
                            value = valueStr;
                        }
                        if(field.getType().getName() == "int"){
                            value = Integer.parseInt(valueStr);
                        }
                        //执行set方法
                        method.invoke(object, value);
                    }else{//<property name="" ref=""/> 依赖注入DI
                        Object refValue = ioc.get(ref);
                        //执行set方法
                        method.invoke(object, refValue);
                    }
                }

                //2.2.5 key和value存入map集合
                ioc.put(id, object);
            }
            //System.out.println(ioc);
        } catch (DocumentException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
//        } catch (NoSuchMethodException e) {
//            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }

    }

    public Object getBean(String id) {
        return ioc.get(id);
    }
}

调用 getBean() 方法通过 id 获取到 Map 中对应的 value,也就是对象

package ioc;

import entity.Student;

public class Test {
    public static void main(String[] args) {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
        Student student = (Student) applicationContext.getBean("student");
        System.out.println(student);
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值