Exploring Spring's PropertyEditor

Transferred from: Exploring Spring's PropertyEditor - a short book

Introduction to propertyeditor & propertyeditorsupport

java.beans.PropertyEditor is a JDK built-in class and is provided to AWT. What is it used for? It means that the string input by the user in the graphic face converts the value (object) of the corresponding type. Similar to a converter.

public interface PropertyEditor {

    void setValue(Object value);

    Object getValue();

    boolean isPaintable();

    String getJavaInitializationString();

    String getAsText();

    void setAsText(String text) throws java.lang.IllegalArgumentException;

    String[] getTags();

    java.awt.Component getCustomEditor();

    boolean supportsCustomEditor();

    void addPropertyChangeListener(PropertyChangeListener listener);

    void removePropertyChangeListener(PropertyChangeListener listener);

}

There are four main methods

  • void setValue(Object value); Set attribute value
  • Object getValue(); Get property value
  • String getAsText(); Convert property value to string
  • void setAsText(String text); Convert string to property value

Java also provides us with a default implementation class java.beans.PropertyEditorSupport

private Object value;
public void setValue(Object value) {
    this.value = value;
    firePropertyChange();
}

public Object getValue() {
    return value;
}
public void setAsText(String text) throws java.lang.IllegalArgumentException {
    if (value instanceof String) {
        setValue(text);
        return;
    }
    throw new java.lang.IllegalArgumentException(text);
}
public String getAsText() {
    return (this.value != null)
    ? this.value.toString()
    : null;
}

As long as we override setAsText and getAsText methods, we can realize the conversion from String type to specific type

Relationship with Spring

After talking for so long, does this have anything to do with Spring?

Let's think, when you use the xml configuration file to set a Value for an attribute (or use the @ Value annotation to give a default Value), whether we enter a string, but the type of the attribute we correspond to is not necessarily a character string type. In this scenario, is it the same as the AWT scenario. Therefore, Spring's property interpretation is inherited from PropertyEditorSupport, and then rewrites setAsText and getAsText

for instance

public class CustomBooleanEditor extends PropertyEditorSupport {

   public static final String VALUE_TRUE = "true";

   public static final String VALUE_FALSE = "false";

   public static final String VALUE_ON = "on";

   public static final String VALUE_OFF = "off";

   public static final String VALUE_YES = "yes";

   public static final String VALUE_NO = "no";

   public static final String VALUE_1 = "1";

   public static final String VALUE_0 = "0";

    // When it is true, the string is null by default
   @Nullable
   private final String trueString;
    
    // When it is false, the string is null by default
   @Nullable
   private final String falseString;
    // Allow null s 
    // Empty strings are not allowed when the basic type is boolean
    // Null strings are allowed when referencing type Boolean
   private final boolean allowEmpty;

 
   public CustomBooleanEditor(boolean allowEmpty) {
      this(null, null, allowEmpty);
   }
 
   public CustomBooleanEditor(@Nullable String trueString, @Nullable String falseString, boolean allowEmpty) {
      this.trueString = trueString;
      this.falseString = falseString;
      this.allowEmpty = allowEmpty;
   }


   @Override
   public void setAsText(@Nullable String text) throws IllegalArgumentException {
        
      String input = (text != null ? text.trim() : null);

      if (this.allowEmpty && !StringUtils.hasLength(input)) {
         // Treat empty String as null value.
         setValue(null);
      } else if (this.trueString != null && this.trueString.equalsIgnoreCase(input)) {
         setValue(Boolean.TRUE);
      } else if (this.falseString != null && this.falseString.equalsIgnoreCase(input)) {
         setValue(Boolean.FALSE);
      } else if (this.trueString == null &&
            (VALUE_TRUE.equalsIgnoreCase(input) || VALUE_ON.equalsIgnoreCase(input) ||
                  VALUE_YES.equalsIgnoreCase(input) || VALUE_1.equals(input))) {
         setValue(Boolean.TRUE);
      } else if (this.falseString == null &&
            (VALUE_FALSE.equalsIgnoreCase(input) || VALUE_OFF.equalsIgnoreCase(input) ||
                  VALUE_NO.equalsIgnoreCase(input) || VALUE_0.equals(input))) {
         setValue(Boolean.FALSE);
      } else {
         throw new IllegalArgumentException("Invalid boolean value [" + text + "]");
      }
   }

   @Override
   public String getAsText() {
      if (Boolean.TRUE.equals(getValue())) {
         return (this.trueString != null ? this.trueString : VALUE_TRUE);
      } else if (Boolean.FALSE.equals(getValue())) {
         return (this.falseString != null ? this.falseString : VALUE_FALSE);
      } else {
         return "";
      }
   }

}

The method is also very simple. I won't explain it in detail

for instance

public class Job {

   private boolean completed;

   private Boolean started;
   // get and set ...........
}

<bean class="com.demo.property.editor.Job" id="job">
   <property name="completed" value="on" />
   <property name="started" value=""/>
</bean>

Get this bean and print Job{completed=true, started=null}

Introduction to related components

PropertyEditorRegistry

You can see from the name that it is a registered interface

void registerCustomEditor(Class<?> requiredType, PropertyEditor propertyEditor);

void registerCustomEditor(@Nullable Class<?> requiredType, @Nullable String propertyPath, PropertyEditor propertyEditor);

@Nullable
PropertyEditor findCustomEditor(@Nullable Class<?> requiredType, @Nullable String propertyPath);

PropertyEditorRegistrySupport

Implementation Class of PropertyEditorRegistry. When we try to get the corresponding PropertyEditor through the Class object, it will initialize a series of default propertyeditors for us

In the populateBean of doCreateBean, getDefaultEditor will be called to obtain the corresponding PropertyEditor for value type conversion

// The property editor provided by spring by default
@Nullable
private Map<Class<?>, PropertyEditor> defaultEditors;

// To override the default property editor
@Nullable
private Map<Class<?>, PropertyEditor> overriddenDefaultEditors;

// Some custom property editor s
@Nullable
private Map<Class<?>, PropertyEditor> customEditors;

// Path / property name of the property. CustomEditorHolder contains Class and PropertyEditor
@Nullable
private Map<String, CustomEditorHolder> customEditorsForPath;

// If the registered parent class and the child class cannot be found, the parent class will be returned and the relationship will be saved in
// In this map
@Nullable
private Map<Class<?>, PropertyEditor> customEditorCache;

@Nullable
    public PropertyEditor getDefaultEditor(Class<?> requiredType) {
        if (!this.defaultEditorsActive) {
            return null;
        }
        if (this.overriddenDefaultEditors != null) {
            PropertyEditor editor = this.overriddenDefaultEditors.get(requiredType);
            if (editor != null) {
                return editor;
            }
        }
        if (this.defaultEditors == null) {
            createDefaultEditors();
        }
        return this.defaultEditors.get(requiredType);
    }

    private void createDefaultEditors() {
        this.defaultEditors = new HashMap<>(64);

        // Simple editors, without parameterization capabilities.
        // The JDK does not contain a default editor for any of these target types.
        this.defaultEditors.put(Charset.class, new CharsetEditor());
        this.defaultEditors.put(Class.class, new ClassEditor());
        this.defaultEditors.put(Class[].class, new ClassArrayEditor());
        this.defaultEditors.put(Currency.class, new CurrencyEditor());
        this.defaultEditors.put(File.class, new FileEditor());
        this.defaultEditors.put(InputStream.class, new InputStreamEditor());
        this.defaultEditors.put(InputSource.class, new InputSourceEditor());
        this.defaultEditors.put(Locale.class, new LocaleEditor());
        this.defaultEditors.put(Path.class, new PathEditor());
        this.defaultEditors.put(Pattern.class, new PatternEditor());
        this.defaultEditors.put(Properties.class, new PropertiesEditor());
        this.defaultEditors.put(Reader.class, new ReaderEditor());
        this.defaultEditors.put(Resource[].class, new ResourceArrayPropertyEditor());
        this.defaultEditors.put(TimeZone.class, new TimeZoneEditor());
        this.defaultEditors.put(URI.class, new URIEditor());
        this.defaultEditors.put(URL.class, new URLEditor());
        this.defaultEditors.put(UUID.class, new UUIDEditor());
        this.defaultEditors.put(ZoneId.class, new ZoneIdEditor());

        // Default instances of collection editors.
        // Can be overridden by registering custom instances of those as custom editors.
        this.defaultEditors.put(Collection.class, new CustomCollectionEditor(Collection.class));
        this.defaultEditors.put(Set.class, new CustomCollectionEditor(Set.class));
        this.defaultEditors.put(SortedSet.class, new CustomCollectionEditor(SortedSet.class));
        this.defaultEditors.put(List.class, new CustomCollectionEditor(List.class));
        this.defaultEditors.put(SortedMap.class, new CustomMapEditor(SortedMap.class));

        // Default editors for primitive arrays.
        this.defaultEditors.put(byte[].class, new ByteArrayPropertyEditor());
        this.defaultEditors.put(char[].class, new CharArrayPropertyEditor());

        // The JDK does not contain a default editor for char!
        this.defaultEditors.put(char.class, new CharacterEditor(false));
        this.defaultEditors.put(Character.class, new CharacterEditor(true));

        // Spring's CustomBooleanEditor accepts more flag values than the JDK's default editor.
        this.defaultEditors.put(boolean.class, new CustomBooleanEditor(false));
        this.defaultEditors.put(Boolean.class, new CustomBooleanEditor(true));

        // The JDK does not contain default editors for number wrapper types!
        // Override JDK primitive number editors with our own CustomNumberEditor.
        this.defaultEditors.put(byte.class, new CustomNumberEditor(Byte.class, false));
        this.defaultEditors.put(Byte.class, new CustomNumberEditor(Byte.class, true));
        this.defaultEditors.put(short.class, new CustomNumberEditor(Short.class, false));
        this.defaultEditors.put(Short.class, new CustomNumberEditor(Short.class, true));
        this.defaultEditors.put(int.class, new CustomNumberEditor(Integer.class, false));
        this.defaultEditors.put(Integer.class, new CustomNumberEditor(Integer.class, true));
        this.defaultEditors.put(long.class, new CustomNumberEditor(Long.class, false));
        this.defaultEditors.put(Long.class, new CustomNumberEditor(Long.class, true));
        this.defaultEditors.put(float.class, new CustomNumberEditor(Float.class, false));
        this.defaultEditors.put(Float.class, new CustomNumberEditor(Float.class, true));
        this.defaultEditors.put(double.class, new CustomNumberEditor(Double.class, false));
        this.defaultEditors.put(Double.class, new CustomNumberEditor(Double.class, true));
        this.defaultEditors.put(BigDecimal.class, new CustomNumberEditor(BigDecimal.class, true));
        this.defaultEditors.put(BigInteger.class, new CustomNumberEditor(BigInteger.class, true));

        // Only register config value editors if explicitly requested.
        if (this.configValueEditorsActive) {
            StringArrayPropertyEditor sae = new StringArrayPropertyEditor();
            this.defaultEditors.put(String[].class, sae);
            this.defaultEditors.put(short[].class, sae);
            this.defaultEditors.put(int[].class, sae);
            this.defaultEditors.put(long[].class, sae);
        }
    }

BeanWrapper

In Spring, the bean wrapper type is used to encapsulate beans, and it indirectly inherits PropertyEditorRegistry. BeanWrapperImpl is the implementation class of BeanWrapper. Most of the propertyeditorregistrys we see in the system are BeanWrapperImpl objects. BeanWrapperImpl also inherits the implementation class PropertyEditorRegistrySupport

PropertyEditorRegistrar

Registry of property editor

void registerCustomEditors(PropertyEditorRegistry registry);

ResourceEditorRegistrar

The only default implementation class

public class ResourceEditorRegistrar implements PropertyEditorRegistrar {

   private final PropertyResolver propertyResolver;

   private final ResourceLoader resourceLoader;

   public ResourceEditorRegistrar(ResourceLoader resourceLoader, PropertyResolver propertyResolver) {
      this.resourceLoader = resourceLoader;
      this.propertyResolver = propertyResolver;
   }

   @Override
   public void registerCustomEditors(PropertyEditorRegistry registry) {
      ResourceEditor baseEditor = new ResourceEditor(this.resourceLoader, this.propertyResolver);
      doRegisterEditor(registry, Resource.class, baseEditor);
      doRegisterEditor(registry, ContextResource.class, baseEditor);
      doRegisterEditor(registry, InputStream.class, new InputStreamEditor(baseEditor));
      doRegisterEditor(registry, InputSource.class, new InputSourceEditor(baseEditor));
      doRegisterEditor(registry, File.class, new FileEditor(baseEditor));
      doRegisterEditor(registry, Path.class, new PathEditor(baseEditor));
      doRegisterEditor(registry, Reader.class, new ReaderEditor(baseEditor));
      doRegisterEditor(registry, URL.class, new URLEditor(baseEditor));

      ClassLoader classLoader = this.resourceLoader.getClassLoader();
      doRegisterEditor(registry, URI.class, new URIEditor(classLoader));
      doRegisterEditor(registry, Class.class, new ClassEditor(classLoader));
      doRegisterEditor(registry, Class[].class, new ClassArrayEditor(classLoader));

      if (this.resourceLoader instanceof ResourcePatternResolver) {
         doRegisterEditor(registry, Resource[].class,
               new ResourceArrayPropertyEditor((ResourcePatternResolver) this.resourceLoader, this.propertyResolver));
      }
   }

   private void doRegisterEditor(PropertyEditorRegistry registry, Class<?> requiredType, PropertyEditor editor) {
      if (registry instanceof PropertyEditorRegistrySupport) {
         ((PropertyEditorRegistrySupport) registry).overrideDefaultEditor(requiredType, editor);
      }
      else {
         registry.registerCustomEditor(requiredType, editor);
      }
   }

}

First, let's talk about where this class is used. This Registrar will be used only when ApplicationContext is used. The above PropertyEditor will register or override the default value of PropertyEditorRegistry

The calling relationship chain is

ClassPathXmlApplicationContext Constructor -> refresh -> prepareBeanFactory() -> establish ResourceEditorRegistrar increase to Set in

Then it will register the default PropertyEditor of resourceeditorregister in the createbean instance of doCreateBean

image

example

public class Job {

    private boolean completed;

    private Content content;
    // get and set method
}

public class Content {
    private String details;
    private String type;
    private int priority;
    // get and set method
}

<bean class="com.demo.property.editor.Job" id="job" lazy-init="true">
   <property name="completed" value="off" />
   <property name="content" value="Pay attention to me:urgent:100"/>
</bean>

ClassPathXmlApplicationContext classPathXmlApplicationContext =
      new ClassPathXmlApplicationContext("property.editor/coderLi.xml");

classPathXmlApplicationContext.getBeanFactory().addPropertyEditorRegistrar(registry -> {
   if (registry instanceof PropertyEditorRegistrySupport) {
      ((PropertyEditorRegistrySupport) registry).overrideDefaultEditor(Content.class, new ContentPropertyEditor());
      System.out.println("PropertyEditorRegistrySupport");
   } else {
      registry.registerCustomEditor(Content.class, new ContentPropertyEditor());
   }
});


Object job = classPathXmlApplicationContext.getBean("job");
System.out.println(job);

There are many methods to achieve the same effect, such as CustomEditorConfigurer, beanfactoryprocessor interface, and so on

As I implemented in the above code, one thing to note is that this bean must be a delayed instantiation, because ApplicationContext will instantiate all non lazy beans by default. At this time, our PropertyEditor has not been registered, and an error will be reported



Author: official account CoderLi
Link: https://www.jianshu.com/p/a0ce60d05778
 

Tags: Spring

Posted on Tue, 19 Oct 2021 15:03:37 -0400 by Donny Bahama