Java基础扫盲系列(三)— Java内省技术

梁盛
2023-12-01

前言

Java内省技术属于Java基础体系的的一部分,但是很多人都不甚了解。笔者也是在学习Spring源码的过程中遇到该技术模块的。为了完善技术体系,本文将全面的学习该技术。在提到Java内省技术,就不得不说Java的反射和JavaBeans技术,相信这两点大家应该都非常熟悉。本文将会从以下几个方面学习Java内省:

  • Java内省和JavaBeans技术
  • Java内省和反射技术的关系
  • Java内省的API介绍
  • Java内省实战


Java内省和JavaBeans技术

在JavaBeans 101的规范中对于JavaBeans是这样定义的:

“A Java Bean is a reusable software component that can be manipulated visually in a builder tool.”

JavaBeans是一种可重复使用的软件组件,并且可以通过可视化的工具的方式进行操作。

如在NetBeans IDE中创建一个Button按钮的JavaBeans如下:

以上的Pannel面板中的Button按钮就是一个JavaBean,满足可重复使用的定义。通过点击该按钮将会展示该Button的可视化设计器:

在设计器中可以编辑该Button的Property和Event。这样就完成了一个JavaBean的创建。

Note:
关于NetBeans的创建可视化Button Bean的过程这里不再详细介绍,如果感兴趣可以参考:The Java™ Tutorials

当然这是Java Client的可视化编程,可以这样做。但是对于Java服务端应用而言,却无法这样使用。且站在程序猿的角度,更偏向使用coding方式解决问题。

使用coding方式编写JavaBean组件也非常容易,不需要实现任何借口或者任何特定的工具。但是JavaBean有规范约束,主要从Propeties,Method和Events三个方面约定。只有这样遵循这些规定,Bean的构建工具才可以探测检视JavaBean,再以可视化方式展示JavaBean。

Properties

属性由公共的Setter和Getter方法定义。对于可视化工具(如NetBeans),将能识别方法名称,然后将该属性展示。如下:

public class FaceBean {
    private int mMouthWidth = 90;

    // getter for mMouthWidth
    public int getMouthWidth() {
        return mMouthWidth;
    }
    
    // setter for mMouthWidth
    public void setMouthWidth(int mw) {
        mMouthWidth = mw;
    }
}

关于属性的种类非常多,如:Indexed Properties,Bound Properties等等。如果感兴趣可以参考:The Java™ Tutorials

只需要掌握一点,属性由Setter和Getter标识定义。

Methods

方法即是除了属性的Setter和Getter之外的公共方法。如NetBeans的可视化工具将能检视JavaBeans中的方法。

内省

在大致介绍完JavaBeans后,再来看内省就简单多了。Java内省是JavaBeans技术架构体系中的一部分,这点可以参考:JavaBeans Spec

其中是这样描述内省:

At runtime and in the builder environment we need to be able to figure out which properties, events, and methods a Java Bean supports. We call this process introspection.

即运行时获取JavaBean的properties,events和methods的过程称为Java内省。简而言之,即检视JavaBean内部的信息,如方法,属性,事件。主要用来运行时获取JavaBean的内部信息。


Java内省和反射技术的关系

学习上节关于Java内省技术的介绍,有些读者应该很快联想到Java的反射技术。Java内省和反射技术很类似(反射也具有运行获取Java对象的类型信息)。

文档中是这样描述Java内省和Java反射:

We therefore provide a composite mechanism. By default we will use a low level reflection mechanism to study the methods supported by a target bean and then apply simple design pat- terns to deduce from those methods what properties, events, and public methods are supported. However, if a bean implementor chooses to provide a BeanInfo class describing their bean then this BeanInfo class will be used to programmatically discover the beans behaviour.

为了能够提供JavaBean内部的检视,提供了一组机制用于处理获取JavaBean的内部信息。默认使用低级别的反射机制获取JavaBean的信息。但是对于指定了JavaBean的对应BeanInfo类时,将从BeanInfo中描述的JavaBean信息获取JavaBean的行为,这时将不会再使用反射机制。

所以Java内省和Java反射存在以下关系:

  • Java反射是一组较低级别的API,使用起来比较复杂。而Java内省提供了一套比较简单和有语义的API用于操作JavaBean;
  • Java内省机制更加简单易用;
  • 由于Java内省底层默认使用Java反射机制,所以同样有Java反射机制带来的性能问题;


Java内省的API介绍

1.Introspector

The Introspector class provides a standard way for tools to learn about the properties, events, and methods supported by a target Java Bean

Introspector译名是内省器,它提供了一系列的方法内省获取JavaBean的properties,method和events。

Introspector底层将寻找JavaBean对应的BeanInfo,如果寻找不到,将使用反射机制分析JavaBean。

其中提供getBeanInfo方法可以获取目标JavaBean对应的BeanInfo方法。

2.BeanInfo

BeanInfo是描述JavaBean的信息的一个对象。其中描述了JavaBean的properties,method和event等。但是很多时候是不提供明确的BeanInfo,需要由上述的Introspector使用反射机制获取JavaBean信息生成对应的BeanInfo。

3.BeanDescriptor

A BeanDescriptor provides global information about a "bean", including its Java class, its displayName, etc.

BeanDescriptor是对JavaBean的全局信息描述,描述其Class,名称等。

4.PropertyDescriptor

A PropertyDescriptor describes one property that a Java Bean exports via a pair of accessor methods.

PropertyDescriptor通过一对Setter和Getter方法导出JavaBean的property,是对Property的描述。
PropertyDescriptor提供了获取Setter和Getter方法的接口,获取Property的名称等接口。

5.MethodDescriptor

A MethodDescriptor describes a particular method that a Java Bean supports for external access from other components.

MethodDescriptor用于描述JavaBean中的公共方法,它提供获取该方法对象Method,方法的参数等API。

Java内息机制还提供了一些其他的API,这里只介绍一些常用的重点接口。关于其他的接口,读者有兴趣可以参考:java.beans

下节将通过coding方式学习内息,如何使用这些API避免复杂的反射方式来操作JavaBean。


Java内省实战

首先编写JavaBean类Person:

/**
 * person for java bean.
 *
 * @author huaijin
 */
public class Person {

    private String name;
    private int age;
    private List<String> address = new ArrayList<>();


    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 void setAddress(String[] address) {
        this.address.addAll(Arrays.asList(address));
    }

    public String[] getAddress() {
        String[] strs = new String[this.address.size()];
        return this.address.toArray(strs);
    }

    public void setAddress(int index, String value) {
        this.address.add(index, value);
    }

    public String getAddress(int index) {
        return this.address.get(index);
    }
}

然后再使用上述介绍的API分别来操作JavaBean,内省:

public class IntrospectPerson {

    public static void main(String[] args) throws IntrospectionException, InvocationTargetException, IllegalAccessException {
        Person person = new Person();
        person.setName("huaijin");
        person.setAge(18);
        List<String> address = new ArrayList<>();
        address.add("hangzhou");
        address.add("anhui");
        person.setAddress(address.toArray(new String[address.size()]));

        BeanInfo personBeanInfo = Introspector.getBeanInfo(Person.class, Introspector.USE_ALL_BEANINFO);
        PropertyDescriptor[] propertyDescriptors = personBeanInfo.getPropertyDescriptors();
        // 获取PropertyDescriptor,读写属性
        for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
            System.out.println("property name: " + propertyDescriptor.getName());
            System.out.println("read before writing: " + propertyDescriptor.getReadMethod().invoke(person, null));
            Class<?> propertyClass = propertyDescriptor.getPropertyType();
            if (propertyClass == String.class) {
                propertyDescriptor.getWriteMethod().invoke(person, "lxy");
            } else if (propertyClass == int.class) {
                propertyDescriptor.getWriteMethod().invoke(person, 28);
            }
            if (propertyDescriptor instanceof IndexedPropertyDescriptor) {
                IndexedPropertyDescriptor indexedPropertyDescriptor =
                        (IndexedPropertyDescriptor) propertyDescriptor;
                indexedPropertyDescriptor.getIndexedWriteMethod().invoke(person, 0, "2dfire");
            }
            System.out.println("read after writing: " + propertyDescriptor.getReadMethod().invoke(person, null) + "\n");
        }
        // 获取MethodDescriptor,可以操作JavaBean
        MethodDescriptor[] methodDescriptors = personBeanInfo.getMethodDescriptors();
        for (MethodDescriptor methodDescriptor : methodDescriptors) {
            System.out.println("methodName: " + methodDescriptor.getName());
        }
    }
}

可以看出,利用Introspector、BeanInfo和PropertyDescriptor等比使用反射机制带来更多的简便性。

在日常开发中,或许经常使用Java内省而不知,下面就通过日常开发中使用到的案例进一步熟悉Java内息。读者或许经常使用Spring的BeanUtils工具类拷贝JavaBean的属性到另一个目标对象中。BeanUtils.copyProperties内部实现就使用了Java内省机制。

private static void copyProperties(Object source, Object target, Class<?> editable, 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)) {
            throw new IllegalArgumentException("Target class [" + target.getClass().getName() +
                    "] not assignable to Editable class [" + editable.getName() + "]");
        }
        actualEditable = editable;
    }
    // 获取目标对象的所有Properties
    PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);
    List<String> ignoreList = (ignoreProperties != null ? Arrays.asList(ignoreProperties) : null);
    // 遍历目标对象的Properties
    for (PropertyDescriptor targetPd : targetPds) {
        // 获取Property的Setter方法
        Method writeMethod = targetPd.getWriteMethod();
        // 如果Setter方法不为空且Property没有被忽略
        if (writeMethod != null && (ignoreList == null || !ignoreList.contains(targetPd.getName()))) {
            // 获取源对象对应的Property
            PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());
            // 如果源对象中存在该Property
            if (sourcePd != null) {
                // 获取源对象的Property对应的Getter方法
                Method readMethod = sourcePd.getReadMethod();
                // 如果读方法不为空,且Getter的返回类型和目标对象的Setter方法参数类型匹配
                if (readMethod != null &&
                        ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())) {
                    try {
                        // 如果Getter方法非公共可见,则设置Getter方法可访问
                        if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {
                            readMethod.setAccessible(true);
                        }
                        // 反射调用源对象的Getter方法,获取属性值
                        Object value = readMethod.invoke(source);
                        // 如果目标对象的Setter方法非公共可见,设置其可访问
                        if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
                            writeMethod.setAccessible(true);
                        }
                        // 反射调用目标对象的Setter方法,设置属性值
                        writeMethod.invoke(target, value);
                    }
                    catch (Throwable ex) {
                        throw new FatalBeanException(
                                "Could not copy property '" + targetPd.getName() + "' from source to target", ex);
                    }
                }
            }
        }
    }
}

以上的逻辑便是BeanUtils中利用JavaBeans中的内省技术实现对象的属性拷贝。

从源码中不难返现,其中使用了大量的反射机制。读者应该都知道,大量使用反射必然有一定的性能问题。性能在于:运行时分析类型信息,如Field、Method等,非常消耗性能。这些信息本应该在编译期间解决,在运行时获取虽然带来更大的灵活性,但是也带来不可避免的性能问题。

对于反射的性能问题,常见的手段遍是缓存分析结果信息,如缓存Method,Field等。这种手段在Spring和Java动态代理中都有体现。下面再使用Spring内部的BeanWrapper优化使用JavaBean的内省技术来分析其对性能的优化方式。

@Override
public PropertyDescriptor[] getPropertyDescriptors() {
    // 从缓存中获取PropertyDescriptor
    return getCachedIntrospectionResults().getPropertyDescriptors();
}

private CachedIntrospectionResults getCachedIntrospectionResults() {
    // 断言被包装的目标Bean不为空
    Assert.state(getWrappedInstance() != null, "BeanWrapper does not hold a bean instance");
    // 如果缓存的内省结果为空则进行内省,并缓存
    if (this.cachedIntrospectionResults == null) {
        this.cachedIntrospectionResults = CachedIntrospectionResults.forClass(getWrappedClass());
    }
    return this.cachedIntrospectionResults;
}

Spring中的BeanWrapper对于Java内省的优化,是遵循Java反射的优化的策略的。即缓存PropertyDescriptor:

private final Map<String, PropertyDescriptor> propertyDescriptorCache;


总结

Java内省技术提供一套高级API的机制针对JavaBean操作,底层默认通过Java反射机制获取JavaBean的信息。内省机制在Spring中有很多使用场景,如BeanUtils,BeanWrapper等。在日常开发中也可以使用简化反射机制带来的复杂性。

参考

Java introspection and reflection
java.beans
How to use Reflection, Introspection and Customization in JavaBeans
JavaBeans(TM) Specification 1.01 Final Release
Trail: JavaBeans(TM)

转载于:https://www.cnblogs.com/lxyit/p/10282975.html

 类似资料: