Type conversion of Spring core features (PropertyEditor, ConversionService)

Posted by Mr Chris on Fri, 17 Dec 2021 01:08:28 +0100

preface

Like data binding, type conversion is also one of the core features of Spring. The initial configuration information of Spring mainly exists in the form of XML, which requires Spring to convert string configuration into specific Java types. After the evolution of multiple versions, the type conversion function in Spring is becoming more and more mature.

PropertyEditor

The original purpose of Spring type conversion is to convert a string to the type corresponding to the bean's constructor parameters or properties. A PropertyEditor has been provided in the java bean specification for setting and obtaining properties, so Spring originally directly reused the PropertyEditor for type conversion.

PropertyEditor basic usage

The PropertyEditor interface is defined as follows.

public interface PropertyEditor {
	  // Property setting get method
    void setValue(Object value);
    Object getValue();
    String getAsText();
    void setAsText(String text) throws java.lang.IllegalArgumentException;

	...Omit support GUI Other methods of program
}

PropertyEditor mainly provides support for GUI programs, so there are other methods besides property setting and obtaining methods. So how is this interface used? Suppose we need to convert a string to an integer, the code is as follows.

public class String2IntegerPropertyEditor extends PropertyEditorSupport {
    @Override
    public void setAsText(String text) throws IllegalArgumentException {
        this.setValue(Integer.valueOf(text));
    }   
}

public class App {

    public static void main(String[] args) {
        String text = "520";

        String2IntegerPropertyEditor editor = new String2IntegerPropertyEditor();
        editor.setAsText(text);
        
        Integer number = (Integer) editor.getValue();
        System.out.println(number);
    }
}

When you customize PropertyEditor, you can inherit the PropertyEditorSupport class, then rewrite the #setAsText method, complete the transformation of the string in this method, and call #setValue to store the converted value, then complete the transformation and call the #getValue method to get the value after the transformation type.

Spring default PropertyEditor

Spring's default property editor is located in the package org springframework. beans. In property editors, in the first part Talk about data binding in Spring core features We have mentioned that the properties of XML configuration are set to the object through BeanWrapper. The implementation class used is BeanWrapperImpl. When this class is instantiated, the default PropertyEditor will be activated.

If Spring cannot find the built-in default PropertyEditor during type conversion, Spring will also find the default PropertyEditor according to certain policies. Specifically, find the PropertyEditor with the name of type Editor. If you want to convert String to com zzuhkp. User, the fully qualified name of the default PropertyEditor to be found is com zzuhkp. UserEditor.

Custom PropertyEditor in Spring

Add PropertyEditor to BeanFactory

When Spring parses the property value of the configuration setting bean in XML, instantiating BeanWrapperImpl and activating the default PropertyEditor will initialize the custom PropertyEditor in BeanFactory to BeanWrapperImpl. During type conversion, the custom PropertyEditor will take precedence over the default PropertyEditor.

So how to add a custom PropertyEditor to BeanFactory?

  1. If you can get an instance of ConfigurableBeanFactory, the most direct method is to call the ConfigurableBeanFactory #registercustomeeditor method to set it directly.
  2. Another way is to use the beanfactoryprocessor callback provided by the ApplicationContext lifecycle to obtain the BeanFactory instance and register the custom PropertyEditor.
@Component
public class CustomEditorProcessor implements BeanFactoryPostProcessor {
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory factory) throws BeansException {
        factory.registerCustomEditor(Integer.class, String2IntegerPropertyEditor.class);
    }
}

Add PropertyEditorRegistrar to BeanFactory

In addition to adding the custom PropertyEditor in BeanFactory to BeanWrapperImpl, Spring also uses the propertyeditorregister in BeanFactory to add a new PropertyEditor for BeanWrapperImpl. The PropertyEditorRegistrar interface is defined as follows.

public interface PropertyEditorRegistrar {
	void registerCustomEditors(PropertyEditorRegistry registry);
}

The registry in the interface method parameter is the instance of BeanWrapperImpl. Spring will call back the method in PropertyEditorRegistrar after initializing BeanWrapperImpl. ApplicationContext uses this mechanism to add an instance of propertyeditorregister resourceeditorregister to BeanFactory during refresh, and customize some property editors related to resources.

To add propertyeditorregister to BeanFactory, you can call the configurablebeanfactory #addpropertyeditorregister method. In this way, the custom PropertyEditor code is as follows.

@Component
public class CustomEditorRegistrar implements BeanFactoryPostProcessor {
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory factory) throws BeansException {
        factory.addPropertyEditorRegistrar(new PropertyEditorRegistrar() {
            @Override
            public void registerCustomEditors(PropertyEditorRegistry propertyEditorRegistry) {
                propertyEditorRegistry.registerCustomEditor(Integer.class, new String2IntegerPropertyEditor());
            }
        });
    }
}

Spring built-in class CustomEditorRegistrar

Add custom ProprtyEditor and customeditorregister to BeanFactory. Spring also has a built-in implementation class CustomEditorConfigurer of beanfactoryprocessor, which is used as follows.

@Configuration
public class Config {

   @Bean
   public CustomEditorConfigurer customEditorConfigurer() {
       CustomEditorConfigurer configurer = new CustomEditorConfigurer();

       configurer.setCustomEditors(Collections.singletonMap(Integer.class, String2IntegerPropertyEditor.class));

       configurer.setPropertyEditorRegistrars(new PropertyEditorRegistrar[]{
               new PropertyEditorRegistrar() {
                   @Override
                   public void registerCustomEditors(PropertyEditorRegistry registry) {
                       registry.registerCustomEditor(Integer.class, new String2IntegerPropertyEditor());
                   }
               }
       });

       return configurer;
   }
}

ConversionService

Although PropertyEditor can already support type conversion, due to the insufficient single responsibility of PropertyEditor (including java bean events and GUI support in addition to type conversion) and the limitation of source type (only String), Spring proposed a new type conversion interface ConversionService in version 3.0.

This interface is defined as follows.

public interface ConversionService {
	// Can the source type be converted to the target type
	boolean canConvert(@Nullable Class<?> sourceType, Class<?> targetType);
	boolean canConvert(@Nullable TypeDescriptor sourceType, TypeDescriptor targetType);

	// Type conversion
	<T> T convert(@Nullable Object source, Class<T> targetType);
	Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType);
}

The interface contains two types of methods. One is used to judge whether the source type can be converted to the target type, and the other is used to convert the source type to the target type. TypeDescriptor is used to describe the converted type information to support generics.

Custom ConversionService

In the spring standard environment, ConversionService is not configured by default, but spring provides a default implementation of DefaultConversionService. If you need to use ConversionService to replace PropertyEidtor's conversion from String to bean property types, you can use BeanFactory to directly set ConversionService, You can also configure a ConversionService type bean named ConversionService in the application context. Spring will set this bean to BeanFactory when refreshing the context, and finally to BeanWrapperImpl instance.

// Method 1: directly set ConversionService
@Component
public class ConversionServiceProcessor implements BeanFactoryPostProcessor {
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        beanFactory.setConversionService(new DefaultConversionService());
    }
}

// Method 2: register a ConversionService bean named conversionService
@Configuration
public class Config {

    @Bean
    public ConversionService conversionService(){
        return new DefaultConversionService();
    }
}

Add Converter to ConversionService

Although the purpose of replacing the PropertyEditor can be achieved by configuring ConversionService to BeanFactory, ConversionService is not omnipotent. For example, it is certainly impossible to convert a User class instance into an Order class.

Converter

The default ConversionService implementation does not directly perform type conversion internally, but delegates the type conversion work to the Converter interface. The implementation class of this interface completes the specific type conversion work. Defaultconversionservice has built-in Converter instances and provides some methods to add custom Converter interfaces.

Let's first look at how the Converter is defined.

public interface Converter<S, T> {
	T convert(S source);
}

Converter is a functional interface with only one internal method for type conversion, where generic S represents the source type and generic T represents the target type.

ConverterRegistry

How to add a custom Converter to the ConversionService? The defaultconversionservice implements the interface ConverterRegistry, which defines some methods for registering converters, as follows.

public interface ConverterRegistry {
	void addConverter(Converter<?, ?> converter);
	<S, T> void addConverter(Class<S> sourceType, Class<T> targetType, Converter<? super S, ? extends T> converter);
	void addConverter(GenericConverter converter);
	void addConverterFactory(ConverterFactory<?, ?> factory);
	
	void removeConvertible(Class<?> sourceType, Class<?> targetType);
}

The ConverterRegistry interface provides methods to add converters and remove converters of a given type. Here are two interfaces that we haven't mentioned, namely GenericConverter and ConverterFactory.

GenericConverter

The Converter can only convert one type to another. In order to support multiple types of conversion, GenericConverter appears. This interface is defined as follows.

public interface GenericConverter {

	// Gets the convertible type
	Set<ConvertiblePair> getConvertibleTypes();
	
	// Type conversion
	Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType);

	final class ConvertiblePair {
	
		private final Class<?> sourceType;
		private final Class<?> targetType;
		
		...Omit some codes
	}
}

There is a ConvertiblePair class inside GenericConverter that holds the source type and target type. In addition to the type conversion method, it also provides a method to obtain convertible type pairs.

ConverterFactory

As for ConverterFactory, it is the factory of Converter. With this factory, ConversionService can obtain the Converter. The ConverterFactory interface is defined as follows.

public interface ConverterFactory<S, R> {
	<T extends R> Converter<S, T> getConverter(Class<T> targetType);
}

The generic S in the interface represents the source type and T represents the target type.

ConditionalConverter

At this point, we can add a custom Converter to ConversionService, but we should also mention the ConditionalConverter interface, which is used to determine whether type conversion can be performed before type conversion.

The ConditionalConverter interface is defined as follows.

public interface ConditionalConverter {
	boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType);
}

In addition, Spring also integrates GenericConverter and ConditionalConverter, providing a ConditionalGenericConverter interface.

public interface ConditionalGenericConverter extends GenericConverter, ConditionalConverter {

}

Add Converter to Spring standard environment

After understanding various converters, we can add the custom Converter to ConversionServcie. The example code is as follows.

public class String2IntegerConverter implements ConditionalGenericConverter {

    @Override
    public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
        return sourceType.getType().equals(String.class) && targetType.getType().equals(Integer.class);
    }

    @Override
    public Set<ConvertiblePair> getConvertibleTypes() {
        return Collections.singleton(new ConvertiblePair(String.class, Integer.class));
    }

    @Override
    public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
        return Integer.valueOf(source.toString());
    }
}

@Configuration
public class Config {

    @Bean
    public ConversionService conversionService() {
        DefaultConversionService conversionService = new DefaultConversionService();

        conversionService.addConverter(new String2IntegerConverter());

        return conversionService;
    }
}

Add Converter in Spring Web Environment

In the standard environment, the ConversionService bean named conversionService can only be used to set bean properties after bean instantiation. In the Web environment, Spring needs to convert the information in the request into controller method parameters, which also needs type conversion.

The configuration of the custom Converter in the Web environment is different from that in the standard environment. We need to define a configuration class that implements WebMvcConfigurationSupport, and then override #addFormatters method. The details are as follows.

@Configuration
public class Config extends WebMvcConfigurationSupport {

    @Override
    protected void addFormatters(FormatterRegistry registry) {
        registry.addConverter(new String2IntegerConverter());
    }
}

FormatterRegistry is an interface that inherits the ConverterRegistry interface.

Spring type conversion process

Standard environment bean property setting type conversion process

Some Spring type conversion processes have been summarized above. Here, a diagram is drawn according to the process to intuitively understand the Spring type conversion process, as shown in the figure below.

Web environment controller method parameter type conversion process

The type conversion of Web environment is mainly used to convert the request to the conoller method parameter. The type conversion here only uses ConversionService. However, Spring has configured one for us in the WebMvcConfigurationSupport class and provided an addFormatters method to add a Converter, The trace code can see that the Web environment type conversion has the following process.

Topics: Java Spring