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


    前言

    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) + "
    ");
            }
            // 获取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)

  • 相关阅读:
    《仔仔细细分析Ext》 第N2章 GridPanel的小难点 第一节 每条数据后面跟随几个操作按钮
    TextField输入验证
    Ext.FormPanel 及控件横排显示
    备份
    重写
    this关键字
    TestCircle程序分析
    java方法重载
    static关键字
    super关键字
  • 原文地址:https://www.cnblogs.com/lxyit/p/10282975.html
Copyright © 2020-2023  润新知