竟然还有人不知道 Java 的内省机制

文摘   2025-01-29 13:41   山东  

“见贤思齐焉,见不贤而内自省也”。新的一年,我们从内省开始。

在传统儒家思想中,人们应在日常生活中不断反思自己的行为,向优秀的人学习,同时审视自身的不足之处,才能实现个人的成长与进步。

新年伊始,我们也都会停下匆忙的脚步,去回顾过去一年的经历,思考哪些事情做得好,哪些地方还有待改进。这种自我反省与复盘,其实就是“内省”的体现。通过不断地“内省”,我们可以更好地理解自己,制定更加明确的目标和计划,找到前进的方向,迎接未来的挑战。

在 Java 中,也有“内省”的概念。它虽与儒家提倡的精神内核不同,但在功能上却有着异曲同工之妙 😂。

什么是内省?

在计算机科学中,内省是指程序运行时检查对象类型的能力。在 Java 中,内省机制是一种用于处理符合 JavaBean 规范的类的方法。它提供了一套 API,允许我们通过程序化的方式访问和操作类的属性、方法及事件。通过这种方式,我们可以在不知道具体实现细节的情况下,动态地访问或修改对象的状态。

内省与反射

  • 反射是一种更为通用的技术,指程序在运行时通过检查或“自检”其结构,动态修改自身行为的能力。通过反射,我们可以动态地加载类、创建对象实例、调用方法以及访问字段等,并且反射是可以应用于任何类的,无论该类是否遵循了 JavaBean 规范;
  • 内省其实是基于反射实现的,只是提供了更高层次的抽象,它更专注于对 JavaBean 的操作,使得操作 JavaBean 更加方便高效。例如,当我们想要获取某个类的所有公共字段时,可以使用反射直接获取 Field 对象数组;但如果操作的目标符合 JavaBean 规范,则应该优先考虑使用内省机制。

什么是 JavaBean

JavaBean 是指符合特定规范的 Java 类,主要用于封装数据。成为一个 JavaBean 需要满足以下这几个条件:

  • 无参构造方法:必须有一个无参构造方法,确保可以通过默认构造器创建实例。
  • 私有化属性:所有属性须使用 private 访问修饰符封装,防止外部随意更改类的内部状态。
  • 提供公共的 getter 和 setter 方法:为每个属性定义相应的 public 读写方法,并遵循命名约定(如 getName() 和 setName(String name))。
  • 序列化支持:实现 Serializable 接口,以保证对象能够序列化为字节流便于存储或者网络传输。

内省机制的工作原理

核心组件

Java 内省机制主要依赖于 java.beans 包下的几个关键类和接口。以下是其中几个重要的组成部分:

  • Introspector 类:内省机制的核心类,提供了静态方法 getBeanInfo(Class<?> beanClass) 来获取给定类型的 JavaBean 信息。该方法返回一个 BeanInfo 对象,对象中包含了关于此 JavaBean 的全面描述,包括所有属性、方法和事件等信息。
  • BeanInfo 接口:JavaBean 信息的顶层抽象,定义了如何描述一个 JavaBean 的行为,用于提供 JavaBean 的元数据。通过 BeanInfo,我们可以获取 Bean 的属性描述符(PropertyDescriptor[] getPropertyDescriptors())、方法描述符(MethodDescriptor[] getMethodDescriptors())以及其他相关信息。
  • PropertyDescriptor 类:对于 JavaBean 的每一个属性,都会有一个对应的 PropertyDescriptor 实例。该类不仅封装了属性的基本信息(如名称、类型),还提供了对 getter 和 setter 方法的访问途径。获取到 PropertyDescriptor 实例后,就可以实现 JavaBean 的属性值读写了。

基本用法

下面,我们通过具体的例子来演示如何使用 Java 内省机制进行基本的操作。

public class IntrospectorBasicUsage {
    public static void main(String[] args) throws Exception {
        User user = new User("张三"25);

        // 获取 BeanInfo 对象
        BeanInfo beanInfo = Introspector.getBeanInfo(User.class);

        // 获取所有属性描述符
        PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();

        for (PropertyDescriptor pd : propertyDescriptors) {
            if (!"class".equals(pd.getName())) {
                // 获取属性的 get 方法
                Method readMethod = pd.getReadMethod();
                if (readMethod != null) {
                    // 调用 get 方法获取属性值
                    Object value = readMethod.invoke(user);
                    System.out.println("属性名: " + pd.getName() + ", 属性值: " + value);
                }
            }
        }

        // 修改 name 属性的值
        PropertyDescriptor pd = new PropertyDescriptor("name", User.class);
        Method writeMethod = pd.getWriteMethod();
        if (writeMethod != null) {
            writeMethod.invoke(user, "李四");
        }

        // 验证修改是否成功
        Method readMethod = pd.getReadMethod();
        if (readMethod != null) {
            Object newValue = readMethod.invoke(user);
            System.out.println("'name' 的新值为: " + newValue);
        }
    }
}

class User {
    private String name;
    privateint age;

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

    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; }
}

示例展示了如何利用 Introspector 类获取 User 类的所有属性,并通过调用 getter 方法读取它们的值。这里的关键在于 getPropertyDescriptors() 方法返回了一个 PropertyDescriptor 数组,数组中的每个元素代表了类中的一个属性。我们可以通过 getReadMethod() 来获得相应的 getter 方法,getWriteMethod() 来获取其 setter 方法,并使用反射机制调用它以获取属性的实际值。

内省在 Spring 框架中的应用

  • 依赖注入:Spring 利用了 Java 的内省机制来自动发现和管理 Spring Bean 之间的依赖关系。BeanWrapper 接口是 Spring 用来操作 Java Bean 属性的核心类。通过其子类,Spring 可以在运行时动态地获取和设置 Spring Bean 的属性值。

org.springframework.beans.BeanWrapperImpl#getPropertyDescriptor

public PropertyDescriptor getPropertyDescriptor(String propertyName) throws InvalidPropertyException {
    BeanWrapperImpl nestedBw = (BeanWrapperImpl) getPropertyAccessorForPropertyPath(propertyName);
    String finalPath = getFinalPath(nestedBw, propertyName);
    PropertyDescriptor pd = nestedBw.getCachedIntrospectionResults().getPropertyDescriptor(finalPath);
    if (pd == null) {
        throw new InvalidPropertyException(getRootClass(), getNestedPath() + propertyName,
                "No property '" + propertyName + "' found");
    }
    return pd;
}
  • BeanUtils:Spring BeanUtils 中的 copyProperties() 这个方法也是依赖于 Java 的内省机制实现,从而可以实现动态地获取源对象和目标对象的属性描述符,并执行相应的 getter 和 setter 方法进行属性值的转移,实现了 Java 对象之间属性值的便捷复制。

org.springframework.beans.BeanUtils#copyProperties(java.lang.Object, java.lang.Object, java.lang.Class<?>, java.lang.String...)

private static void copyProperties(Object source, Object target, @Nullable Class<?> editable,
   @Nullable String... ignoreProperties)
 throws BeansException 
{

    Assert.notNull(source, "Source must not be null");
    Assert.notNull(target, "Target must not be null");

    Class<?> actualEditable = target.getClass();
    if (editable != null) {
        if (!editable.isInstance(target)) {
            thrownew IllegalArgumentException("Target class [" + target.getClass().getName() +
                    "] not assignable to editable class [" + editable.getName() + "]");
        }
        actualEditable = editable;
    }
    PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);
    Set<String> ignoredProps = (ignoreProperties != null ? new HashSet<>(Arrays.asList(ignoreProperties)) : null);
    CachedIntrospectionResults sourceResults = (actualEditable != source.getClass() ?
            CachedIntrospectionResults.forClass(source.getClass()) : null);

    for (PropertyDescriptor targetPd : targetPds) {
        Method writeMethod = targetPd.getWriteMethod();
        if (writeMethod != null && (ignoredProps == null || !ignoredProps.contains(targetPd.getName()))) {
            PropertyDescriptor sourcePd = (sourceResults != null ?
                    sourceResults.getPropertyDescriptor(targetPd.getName()) : targetPd);
            if (sourcePd != null) {
                Method readMethod = sourcePd.getReadMethod();
                if (readMethod != null) {
                    if (isAssignable(writeMethod, readMethod)) {
                        try {
                            ReflectionUtils.makeAccessible(readMethod);
                            Object value = readMethod.invoke(source);
                            ReflectionUtils.makeAccessible(writeMethod);
                            writeMethod.invoke(target, value);
                        }
                        catch (Throwable ex) {
                            thrownew FatalBeanException(
                                    "Could not copy property '" + targetPd.getName() + "' from source to target", ex);
                        }
                    }
                }
            }
        }
    }
}

内省机制最佳实践

  • 遵循 JavaBean 规范:务必正确实现 getter/setter 方法,并保持属性名的一致性,这样可以保证内省机制能够准确无误地识别并操作对象。
  • 最小化反射调用:尽可能减少对 getDeclaredMethods()getDeclaredFields() 等方法的调用次数,因为每次调用都会带来额外的性能成本。
  • 使用缓存机制:如果多次访问相同的类信息,可以将其结果缓存起来以供后续重用。

好了,Java 内省机制的介绍到这里就结束了。其实,无论是个人成长还是精进技术,内省都是至关重要的。

在新的一年里,让我们不仅在生活上践行内省的价值观,在技术探索的路上也同样秉持着这一精神,不断地学习新的知识和技术,迎接每一个挑战,抓住每一次机遇,共同成长,共创辉煌。

再一次祝福大家:新年快乐!!!

您的鼓励对我持续创作非常关键,如果本文对您有帮助,请记得点赞、分享、在看哦~~~谢谢!

Java驿站
这里是【Java驿站】,一个Java编程学习与交流平台。
 最新文章