小议 Java 内省机制 [作者简介] 魏民,信息部售后组研发工程师
基本概念 Wiki 中是这样描述内省的:
在计算机科学中,内省是指计算机程序在运行时(Run time)检查对象(Object)类型的一种能力,通常也可以称作运行时类型检查。
这个描述非常宽泛,但有三个关键词:
Java 官方对 Java Beans 内省的定义:
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.
从 Java Bean 的角度来看,这里的对象就是 Bean 对象,主要关注点是属性、方法和事件等,也就是说在运行时可以获取相应的信息进行一些处理,这就是 Java Beans 的内省机制。
与反射的区别 Java Beans 内省其实就是对反射的一种封装,这个从源码中或者官方文档中都能看到:
By default we will use a low level reflection mechanism to study the methods supported by a target bean and then apply simple design patterns to deduce from those methods what properties, events, and public methods are supported.
Java Beans 内省机制 核心类库 Java Beans 内省机制的核心类是 Introspector
:
1 2 * The Introspector class provides a standard way for tools to learn about * the properties , events , and methods supported by a target Java Bean .
操作范围主要包括但不局限于 Java Beans 的属性,事件和方法,具体是基于以下几个类实现:
BeanInfo
PropertyDescriptor
MethodDescriptor
EventSetDescriptor
先看一个示例:
定义一个 Java Bean:
1 2 3 4 5 6 7 8 9 10 public class User { private String username; private Integer age; }
测试代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Test public void test1 () throws IntrospectionException { BeanInfo userBeanInfo = Introspector.getBeanInfo(User.class); PropertyDescriptor[] propertyDescriptors = userBeanInfo.getPropertyDescriptors(); System.out.println("属性描述:" ); Stream.of(propertyDescriptors).forEach(System.out::println); System.out.println("方法描述:" ); MethodDescriptor[] methodDescriptors = userBeanInfo.getMethodDescriptors(); Stream.of(methodDescriptors).forEach(System.out::println); System.out.println("事件描述:" ); EventSetDescriptor[] eventSetDescriptors = userBeanInfo.getEventSetDescriptors(); Stream.of(eventSetDescriptors).forEach(System.out::println); }
输出结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 属性描述: java.beans.PropertyDescriptor[name=age; propertyType=class java .lang .Integer ; readMethod=public java.lang.Integer introspector.bean.User.getAge(); writeMethod=public void introspector.bean.User.setAge(java.lang.Integer)] java.beans.PropertyDescriptor[name=class ; propertyType=class java .lang .Class ; readMethod=public final native java.lang.Class java.lang.Object.getClass()] java.beans.PropertyDescriptor[name=username; propertyType=class java .lang .String ; readMethod=public java.lang.String introspector.bean.User.getUsername(); writeMethod=public void introspector.bean.User.setUsername(java.lang.String)] 方法描述: java.beans.MethodDescriptor[name=getClass; method=public final native java.lang.Class java.lang.Object.getClass()] java.beans.MethodDescriptor[name=setAge; method=public void introspector.bean.User.setAge(java.lang.Integer)] java.beans.MethodDescriptor[name=getAge; method=public java.lang.Integer introspector.bean.User.getAge()] java.beans.MethodDescriptor[name=wait; method=public final void java.lang.Object.wait(long ,int ) throws java.lang.InterruptedException] java.beans.MethodDescriptor[name=notifyAll; method=public final native void java.lang.Object.notifyAll()] java.beans.MethodDescriptor[name=notify; method=public final native void java.lang.Object.notify()] java.beans.MethodDescriptor[name=getUsername; method=public java.lang.String introspector.bean.User.getUsername()] java.beans.MethodDescriptor[name=wait; method=public final void java.lang.Object.wait() throws java.lang.InterruptedException] java.beans.MethodDescriptor[name=hashCode; method=public native int java.lang.Object.hashCode()] java.beans.MethodDescriptor[name=setUsername; method=public void introspector.bean.User.setUsername(java.lang.String)] java.beans.MethodDescriptor[name=wait; method=public final native void java.lang.Object.wait(long ) throws java.lang.InterruptedException] java.beans.MethodDescriptor[name=equals; method=public boolean java.lang.Object.equals(java.lang.Object)] java.beans.MethodDescriptor[name=toString; method=public java.lang.String introspector.bean.User.toString()] 事件描述:
可以看出通过内省机制可以获取 Java Bean 的属性、方法描述,这里事件描述是空的(关于事件相关会在后面介绍)。由于 Java 类都会继承 Object
类,可以看到这里将 Object
类相关的属性和方法描述也输出了,如果想将某个类的描述信息排除可以使用 java.beans.Introspector#getBeanInfo(java.lang.Class<?>, java.lang.Class<?>)
这个方法。
属性处理 配置绑定 通过 PropertyDescriptor
可以基于字段名为可写属性设置值。
比如我们经常会使用这样的配置文件:
1 2 3 user: username: zhangsan age: 1
配置文件会与对象进行数据绑定。测试代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Test public void test2 () throws IntrospectionException { YamlPropertiesFactoryBean yaml = new YamlPropertiesFactoryBean(); yaml.setResources(new ClassPathResource("application.yml" )); String path = "user." ; Properties properties = yaml.getObject(); System.out.println(properties); User user = new User(); BeanInfo userBeanInfo = Introspector.getBeanInfo(User.class, Object.class); PropertyDescriptor[] propertyDescriptors = userBeanInfo.getPropertyDescriptors(); Stream.of(propertyDescriptors).forEach(propertyDescriptor -> { String property = propertyDescriptor.getName(); try { propertyDescriptor.getWriteMethod().invoke(user,properties.get(path+property)); } catch (IllegalAccessException | InvocationTargetException ignored) { } }); System.out.println(user); }
输出结果:
1 User{username='zhangsan' , age=1 }
在 Spring 中的使用 在传统的 Spring 开发中我们需要在 web.xml 中指定一些配置参数,比如:
1 2 3 4 5 6 7 8 9 <servlet > <servlet-name > app</servlet-name > <servlet-class > org.springframework.web.servlet.DispatcherServlet</servlet-class > <init-param > <param-name > contextConfigLocation</param-name > <param-value > </param-value > </init-param > <load-on-startup > 1</load-on-startup > </servlet >
这里有一个 contextConfigLocation
参数,这个参数最终是与 FrameworkServlet
类中的一个属性进行绑定:
1 2 3 public abstract class FrameworkServlet extends HttpServletBean implements ApplicationContextAware { private String contextConfigLocation; }
那么 Spring 是如何将 web.xml 中的配置项与属性进行绑定的呢,可以参数看org.springframework.web.servlet.HttpServletBean#init()
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Override public final void init () throws ServletException { PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this .requiredProperties); if (!pvs.isEmpty()) { try { BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this ); ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext()); bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment())); initBeanWrapper(bw); bw.setPropertyValues(pvs, true ); } catch (BeansException ex) { if (logger.isErrorEnabled()) { logger.error("Failed to set bean properties on servlet '" + getServletName() + "'" , ex); } throw ex; } } initServletBean(); }
可以看到 Spring 是通过 BeanWrapper
完成对属性的绑定:
1 2 3 4 5 6 public interface BeanWrapper extends ConfigurablePropertyAccessor { PropertyDescriptor[] getPropertyDescriptors(); PropertyDescriptor getPropertyDescriptor (String var1) throws InvalidPropertyException ; }
而 BeanWrapper
又继承了 PropertyAccessor
接口:
1 2 3 4 5 6 7 8 9 10 11 12 public interface PropertyAccessor { boolean isReadableProperty (String var1) ; boolean isWritableProperty (String var1) ; @Nullable Class<?> getPropertyType(String var1) throws BeansException; @Nullable TypeDescriptor getPropertyTypeDescriptor (String var1) throws BeansException ; }
也就是说 Spring 中 BeanWrapper
基于 Java 的内省机制实现了对属性的赋值工作,但是 Spring 并未局限于 Java 提供的 API,而是也进行了扩展和进一步的封装,如 TypeDescriptor
。
可以参考 org.springframework.web.servlet.HttpServletBean#init()
中 BeanWrapper
的使用来实现对 User
对象的属性赋值:
1 2 3 4 5 6 7 8 9 10 @Test public void test5 () { User user = new User(); BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(user); MutablePropertyValues pvs = new MutablePropertyValues(); pvs.add("username" ,"zhangsan" ); pvs.add("age" ,1 ); bw.setPropertyValues(pvs); System.out.println(user); }
输出结果:
1 User{username='zhangsan' , age=1 }
类型转换 有属性赋值,必然就会有类型转换。说白了我们从配置文件读取的数据是字符串,与属性进行参数绑定的过程中势必会有类型转换,java.beans
中提供了相应的 API:
PropertyEditor
PropertyEditorSupport
PropertyEditorManager
属性编辑器管理器
在 Spring 中提供了一个 PropertyEditorRegistrar
先看一个例子:
User 类增加 Date 属性:
1 2 3 4 5 6 7 8 9 10 11 12 public class User { private String username; private Integer age; private Date createTime; }
日期转换器:
1 2 3 4 5 6 7 8 9 10 11 12 13 public class DatPropertyEditor extends PropertyEditorSupport { @Override public void setAsText (String text) { try { setValue((text == null ) ? null : new SimpleDateFormat("yyyy-MM-dd" ).parse(text)); } catch (ParseException e) { e.printStackTrace(); } } }
在之前的例子中内省设置属性值都是直接通过 PropertyDescriptor
获取属性的写方法通过反射去赋值,而如果需要对值进行类型转换,则需要通过 PropertyEditorSupport#setAsText
调用 setValue
方法,然后 setValue
方法触发属性属性修改事件:
1 2 3 4 5 6 7 public class PropertyEditorSupport implements PropertyEditor { public void setValue (Object value) { this .value = value; firePropertyChange(); } }
要注意这里的 value
实际上是临时存储在 PropertyEditorSupport
中,PropertyEditorSupport
则作为事件源,从而得到类型转换后的 value
,再通过 PropertyDescriptor
获取属性的写方法通过反射去赋值。
测试代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 @Test public void test6 () throws IntrospectionException, FileNotFoundException { Map<String,Object> properties = ImmutableMap.of("age" ,1 ,"username" ,"zhangsan" ,"createTime" ,"2020-01-01" ); User user = new User(); BeanInfo userBeanInfo = Introspector.getBeanInfo(User.class, Object.class); PropertyDescriptor[] propertyDescriptors = userBeanInfo.getPropertyDescriptors(); Stream.of(propertyDescriptors).forEach(propertyDescriptor -> { String property = propertyDescriptor.getName(); Object value = properties.get(property); if (Objects.equals("createTime" , property)) { propertyDescriptor.setPropertyEditorClass(DatPropertyEditor.class); PropertyEditor propertyEditor = propertyDescriptor.createPropertyEditor(user); propertyEditor.addPropertyChangeListener(evt -> { Object value1 = propertyEditor.getValue(); setPropertyValue(user, propertyDescriptor, value1); }); propertyEditor.setAsText(String.valueOf(value)); return ; } setPropertyValue(user, propertyDescriptor, value); }); System.out.println(user); } private void setPropertyValue (User user, PropertyDescriptor propertyDescriptor, Object value1) { try { propertyDescriptor.getWriteMethod().invoke(user, value1); } catch (IllegalAccessException | InvocationTargetException ignored) { } }
输出结果:
1 User{username='zhangsan' , age=1 , createTime=2020 -1 -1 0 :00 :00 }
事件监听 先举一个的例子,在 Mac 中设置飞书通知方式的时候,当界面右侧选择“提示”的时候,那么左侧也会相应的显示为“提示”:
如果将右侧看成一个 Java Bean,那么这中间势必存在一个属性变化监听。java.beans
包中也提供了相应实现:
PropertyChangeEvent
PropertyChangeListener
PropertyChangeSupport
VetoableChangeListener
VetoableChangeSupport
PropertyChangeEvent
的构造方法:
1 2 3 4 5 6 7 public PropertyChangeEvent (Object source, String propertyName, Object oldValue, Object newValue) { super (source); this .propertyName = propertyName; this .newValue = newValue; this .oldValue = oldValue; }
通过这个构造方法可以看出属性变化监听的关注点:
source
propertyName
oldValue
newValue
示例代码:
在 User
中增加属性(生效)变化监听:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 public class User { private String username; private Integer age; private PropertyChangeSupport propertyChangeSupport = new PropertyChangeSupport(this ); private void firePropertyChange (String propertyName, String oldValue, String newValue) { PropertyChangeEvent event = new PropertyChangeEvent(this , propertyName, oldValue, newValue); propertyChangeSupport.firePropertyChange(event); } public void addPropertyChangeListener (PropertyChangeListener listener) { propertyChangeSupport.addPropertyChangeListener(listener); } public void removePropertyChangeListener (PropertyChangeListener listener) { propertyChangeSupport.removePropertyChangeListener(listener); } public PropertyChangeListener[] getPropertyChangeListeners() { return propertyChangeSupport.getPropertyChangeListeners(); } public void setUsername (String username) { String oldValue = this .username; this .username = username; firePropertyChange("username" , oldValue, username); } }
测试代码:
1 2 3 4 5 6 7 8 9 @Test public void test3 () { User user = new User(); user.setAge(1 ); user.setUsername("zhangsan" ); user.addPropertyChangeListener(System.out::println); user.setUsername("lisi" ); user.setUsername("wangwu" ); }
输出结果:
1 2 java.beans.PropertyChangeEvent[propertyName=name; oldValue=zhangsan; newValue=lisi; propagationId=null ; source=User{username='lisi' , age=1 }] java.beans.PropertyChangeEvent[propertyName=name; oldValue=lisi; newValue=wangwu; propagationId=null ; source=User{username='wangwu' , age=1 }]
可以看到在添加了监听器后,当 username 属性发生变化的时候会出发监听事件。
再看看另外一种监听器 VetoableChangeListener
。在 User
中添加监听器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 private VetoableChangeSupport vetoableChangeSupport = new VetoableChangeSupport(this );private void fireVetoableChange (String propertyName, String oldValue, String newValue) throws PropertyVetoException { PropertyChangeEvent event = new PropertyChangeEvent(this , propertyName, oldValue, newValue); vetoableChangeSupport.fireVetoableChange(event); } public void addVetoableChangeListener (VetoableChangeListener listener) { vetoableChangeSupport.addVetoableChangeListener(listener); } public void removeVetoableChangeListener (VetoableChangeListener listener) { vetoableChangeSupport.removeVetoableChangeListener(listener); } public void setUsername (String username) throws PropertyVetoException { String oldValue = this .username; fireVetoableChange("username" ,oldValue,username); this .username = username; firePropertyChange("username" , oldValue, username); }
测试代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Test public void test3 () throws PropertyVetoException { User user = new User(); user.setAge(1 ); user.addVetoableChangeListener(evt -> { System.out.println(evt.getNewValue()+",," +evt.getOldValue()); if (Objects.equals(evt.getNewValue(), evt.getOldValue())) { throw new PropertyVetoException("当前属性值未发生任何变化" , evt); } }); user.addPropertyChangeListener(System.out::println); user.setUsername("lisi" ); user.setUsername("zhangsan" ); user.setUsername("zhangsan" ); }
运行时发现一直无法抛出异常。查看源码发现 PropertyChangeSupport
和 VetoableChangeSupport
当新旧值相等时不会触发监听,于是修改测试代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Test public void test3 () throws PropertyVetoException { User user = new User(); user.setAge(1 ); user.addVetoableChangeListener(evt -> { System.out.println(evt.getNewValue()+",," +evt.getOldValue()); if (Objects.isNull(evt.getNewValue())) { throw new PropertyVetoException("username 不能为null" , evt); } }); user.addPropertyChangeListener(System.out::println); user.setUsername("lisi" ); user.setUsername(null ); }
运行结果:
1 2 3 4 5 6 7 8 lisi,,null java.beans.PropertyChangeEvent[propertyName=username; oldValue=null ; newValue=lisi; propagationId=null ; source=User{username='lisi' , age=1 }] null ,,lisijava.beans.PropertyVetoException: username 不能为null at introspector.test.IntrospectorTest.lambda$test3$1 (IntrospectorTest.java:78 ) at java.beans.VetoableChangeSupport.fireVetoableChange(VetoableChangeSupport.java:375 )
可以发现当符合“否决”属性变化的条件时,会抛出 PropertyVetoException
异常阻断属性的变化。
在之前的示例中 userBeanInfo
输出的 EventSetDescriptor
为空,这是因为并未到 User
类中增加事件。现在再测试一下获取 EventSetDescriptor
:
1 2 3 4 5 6 @Test public void test1 () throws IntrospectionException { BeanInfo userBeanInfo = Introspector.getBeanInfo(User.class, Object.class); EventSetDescriptor[] eventSetDescriptors = userBeanInfo.getEventSetDescriptors(); Stream.of(eventSetDescriptors).forEach(System.out::println); }
输出结果:
1 2 java.beans.EventSetDescriptor[name=propertyChange; inDefaultEventSet; listenerType=interface java .beans .PropertyChangeListener ; getListenerMethod=public java.beans.PropertyChangeListener[] introspector.bean.User.getPropertyChangeListeners(); addListenerMethod=public void introspector.bean.User.addPropertyChangeListener(java.beans.PropertyChangeListener); removeListenerMethod=public void introspector.bean.User.removePropertyChangeListener(java.beans.PropertyChangeListener)] java.beans.EventSetDescriptor[name=vetoableChange; inDefaultEventSet; listenerType=interface java .beans .VetoableChangeListener ; addListenerMethod=public void introspector.bean.User.addVetoableChangeListener(java.beans.VetoableChangeListener); removeListenerMethod=public void introspector.bean.User.removeVetoableChangeListener(java.beans.VetoableChangeListener)]
其他 在 Java 生态飞速发展的今天,很多底层技术细节都被高级框架所屏蔽,而 Java Beans 就是其中一种。也许平时根本就用不到,但是其代码设计和思想理念不应该被忽视。Dubbo 2.7 之后提出了“服务自省”的概念,其灵感就来源于 Java Beans 内省机制。
References
作者
魏民,信息部售后组
招聘
信息部是小米公司整体系统规划建设的核心部门,支撑公司国内外的线上线下销售服务体系、供应链体系、ERP 体系、内网 OA 体系、数据决策体系等精细化管控的执行落地工作,服务小米内部所有的业务部门以及 40 家生态链公司。
同时部门承担大数据基础平台研发和微服务体系建设落,语言涉及 Java、Go,长年虚位以待对大数据处理、大型电商后端系统、微服务落地有深入理解和实践的各路英雄。
欢迎投递简历:jin.zhang(a)xiaomi.com(武汉)