原创

Java内省

概念

在计算机科学中,内省是指计算机程序在运行时(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 * The Introspector class provides a standard way for tools to learn about
2 * the properties, events, and methods supported by a target Java Bean.

操作范围主要包括但不局限于 Java Beans 的属性,事件和方法,具体是基于以下几个类实现:

  • BeanInfo:Java Bean 信息类
  • PropertyDescriptor:属性描述类
  • MethodDescriptor:方法描述类
  • EventSetDescriptor:事件描述集合

先看一个示例:
定义一个 Java Bean:

import java.util.Date;

public class User {
    private int id;
    private String username;
    private Date birthday;
    private String sex;
    private String address;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public Date getBirthday() {
        return birthday;
    }

    public void setBirthday(Date birthday) {
        this.birthday = birthday;
    }

    public String getSex() {
        return sex;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }
}

测试代码:

@Test
public void test2() throws IntrospectionException {
    BeanInfo userInfo = Introspector.getBeanInfo(User.class);
    System.out.println(userInfo);
}

这里断点情况如下:
Java内省示例

可以看出通过内省机制可以获取 Java Bean 的属性、方法描述,这里事件描述是空的(关于事件相关会在后面介绍)。由于 Java 类都会继承 Object 类,可以看到这里将 Object 类相关的属性和方法描述也输出了,如果想将某个类的描述信息排除可以使用 java.beans.Introspector#getBeanInfo(java.lang.Class<?>, java.lang.Class<?>)这个方法。

属性处理

配置绑定

通过PropertyDescriptor可以基于字段名为可写属性设置值。
比如我们经常会使用这样的配置文件:

user:
  username: zhangsan
  age: 1

配置文件会与对象进行数据绑定。测试代码:

@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();
    //获取 User Bean 信息,排除 Object
    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);
}

输出结果:

User{username='zhangsan', age=1}

在 Spring 中的使用

在传统的 Spring 开发中我们需要在 web.xml 中指定一些配置参数,比如:

<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类中的一个属性进行绑定:

public abstract class FrameworkServlet extends HttpServletBean implements ApplicationContextAware {
  private String contextConfigLocation;
}

那么 Spring 是如何将 web.xml 中的配置项与属性进行绑定的呢,可以参数org.springframework.web.servlet.HttpServletBean#init()方法:

@Override
public final void init() throws ServletException {

  // Set bean properties from init parameters.
  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;
    }
  }

  // Let subclasses do whatever initialization they like.
  initServletBean();
}

可以看到 Spring 是通过BeanWrapper完成对属性的绑定:

public interface BeanWrapper extends ConfigurablePropertyAccessor {
    // 获取属性描述器
    PropertyDescriptor[] getPropertyDescriptors();

    PropertyDescriptor getPropertyDescriptor(String var1) throws InvalidPropertyException;
}

BeanWrapper又继承了PropertyAccessor接口:

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对象的属性赋值:

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

输出结果:

User{username='zhangsan', age=1}

类型转换

有属性赋值,必然就会有类型转换。说白了我们从配置文件读取的数据是字符串,与属性进行参数绑定的过程中势必会有类型转换java.beans中提供了相应的 API:

  • PropertyEditor
    • 属性编辑器顶层接口
  • PropertyEditorSupport
    • 属性编辑器实现类
  • PropertyEditorManager
    • 属性编辑器管理器
    • 在 Spring 中提供了一个PropertyEditorRegistrar

先看一个例子:
User类增加Date属性:

public class User {

    private String username;

    private Integer age;

    private Date createTime;

    // getter/setter
    // toString

}

日期转换器:

/**
 * 日期属性编辑器
 */
public class DatePropertyEditor 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方法触发属性属性修改事件:

public class PropertyEditorSupport implements PropertyEditor {
    public void setValue(Object value) {
        this.value = value;
        firePropertyChange();
    }
}

要注意这里的value实际上是临时存储在PropertyEditorSupport中,PropertyEditorSupport则作为事件源,从而得到类型转换后的value,再通过PropertyDescriptor获取属性的写方法通过反射去赋值。

@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();
    //获取 User Bean 信息,排除 Object
    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(DatePropertyEditor.class);
            //创建属性编辑器
            PropertyEditor propertyEditor = propertyDescriptor.createPropertyEditor(user);
            //添加监听器
            propertyEditor.addPropertyChangeListener(evt -> {
                //获取转换后的value
                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) {
    }
}

事件监听

java.beans包中事件监听:

  • PropertyChangeEvent
    • 属性变化事件
  • PropertyChangeListener
    • 属性(生效)变化监听器
  • PropertyChangeSupport
    • 属性(生效)变化监听器管理器
  • VetoableChangeListener
    • 属性(否决)变化监听器
  • VetoableChangeSupport
    • 属性(否决)变化监听器管理器

PropertyChangeEvent的构造方法:

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中增加属性(生效)变化监听:

public class User {

    private String username;

    private Integer age;

    /**
     * 属性(生效)变化监听器管理器
     */
    private PropertyChangeSupport propertyChangeSupport = new PropertyChangeSupport(this);

    /**
     * 启动属性(生效)变化
     * @param propertyName 
     * @param oldValue 
     * @param newValue
     */
    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);
    }

   // getter/setter
   // toString
}

测试代码:

@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");
}

输出:

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中添加监听器:

/**
 * 属性(否决)变化监听器
 */
private VetoableChangeSupport vetoableChangeSupport = new VetoableChangeSupport(this);

/**
 * 启动属性(否决)变化
 * @param propertyName
 * @param oldValue
 * @param newValue
 */
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);
}

测试代码:

@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");
}

运行时发现一直无法抛出异常。查看源码发现PropertyChangeSupportVetoableChangeSupport当新旧值相等时不会触发监听,于是修改测试代码:

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

运行结果:

lisi,,null
java.beans.PropertyChangeEvent[propertyName=username; oldValue=null; newValue=lisi; propagationId=null; source=User{username='lisi', age=1}]
null,,lisi

java.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
Java自省增加事件

正文到此结束
相关文章