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

时间:2021-10-29 20:33:56

前言

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)