Spring instantiation source code analysis of ConversionService (10)

Foreword

ConversionService is a core interface in the Spring framework, used for conversion and formatting operations between different types. It provides a unified way to handle type conversions between objects, as well as converting data from one representation to another.

Here are some of the main uses of ConversionService:

  1. Type conversion: ConversionService allows automatic conversion between different types. It provides a set of built-in converters that can handle common type conversions such as string to number, date to string, etc. You can also customize and register your own converters to handle conversions between specific types.

  2. Data formatting: The ConversionService can be used to convert data from one format to another, such as formatting a date object into a specific date string representation. It supports the use of standard formatting modes and custom formatting rules.

  3. Data binding: ConversionService can be used during the data binding process to convert input data into the attribute type of the target object. It helps convert user-entered data into the types required by the application for validation and processing.

  4. Expression evaluation: In Spring Expression Language (SpEL), the ConversionService is used to perform type conversions during expression evaluation. It allows the use of type conversion operations in expressions for data processing and conversion during expression evaluation.

Converter SPI

The SPI (Service Provider Interface) that implements type conversion logic is simple and strongly typed, as shown in the interface definition below:

package org.springframework.core.convert.converter;

public interface Converter<S, T> {<!-- -->

T convert(S source);
}

To create your own converter, simply implement the above interface. The generic parameter S represents the source type you want to convert, and the generic parameter T represents the target type you want to convert. If a collection or array containing elements of type S needs to be converted to an array or collection containing elements of type T, then this converter can also be applied transparently, provided that a converter that delegates the array or collection has been registered (by default will be handled by DefaultConversionService).

For each call to the convert(S) method, the source parameter value must be ensured not to be empty. If the conversion fails, your converter can throw any unchecked exception; specifically, to report an illegal source parameter value, an IllegalArgumentException should be thrown. Also be careful to ensure that your Converter implementation must be thread-safe.

For convenience, the core.convert.support package already provides some converter implementations that cover conversions from strings to numbers and other common types. Consider StringToInteger as a typical Converter implementation example:

package org.springframework.core.convert.support;

final class StringToInteger implements Converter<String, Integer> {<!-- -->

public Integer convert(String source) {<!-- -->
return Integer.valueOf(source);
}
}

ConverterFactory

When centralized management of conversion logic is required for the entire class hierarchy (for example, converting from a string to an enumeration object), a ConverterFactory can be implemented, as shown in the following example:

public interface ConverterFactory<S, R> {<!-- -->

    <T extends R> Converter<S, T> getConverter(Class<T> targetType);

}

ConverterFactory is an interface provided by the Spring framework for implementing type conversion factories. By implementing this interface, we can define our own conversion logic and apply it to the entire class hierarchy.

In the above code, the ConverterFactory interface defines a generic method getConverter, which receives the target type targetType as a parameter and returns a Converter object. The Converter interface is used to define the specific implementation of type conversion.

By implementing a custom ConverterFactory, the corresponding Converter instance can be returned according to the target type to implement the conversion logic from the source type to the target type. This allows unified management of conversion logic throughout the class hierarchy, providing a more flexible and extensible conversion method.

package com.qhyu.cloud.conversion;

import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.ConverterFactory;

/**
 * All rights Reserved, Designed By https://umd.edu/<br>
 * Title: StringToEnumConverterFactory<br>
 * Package: com.qhyu.cloud.conversion<br>
 * Copyright ? 2023 umd.edu. All rights reserved.<br>
 * Company: The University of Maryland<br>
 *
 * @author candidate<br>
 * @date October 10, 2023 10:12<br>
 */
@SuppressWarnings(value = {<!-- -->"rawtypes","unchecked"})
public class StringToEnumConverterFactory implements ConverterFactory<String,Enum> {<!-- -->

@Override
public <T extends Enum> Converter<String, T> getConverter( Class<T> targetType) {<!-- -->
return new StringToEnumConverter<>(targetType);
}

private static final class StringToEnumConverter<T extends Enum> implements Converter<String, T> {<!-- -->

private final Class<T> enumType;

public StringToEnumConverter(Class<T> enumType) {<!-- -->
this.enumType = enumType;
}

public T convert(String source) {<!-- -->
return (T) Enum.valueOf(this.enumType, source.trim());
}
}
}

GenericConverter

When a more complex Converter implementation is required, consider using the GenericConverter interface. Compared with the Converter interface, GenericConverter has a more flexible but less type-constrained signature, and it supports conversion between multiple source types and target types. In addition, GenericConverter provides information about the source and target field contexts, which can be used when implementing conversion logic. Such contexts can drive type conversions through generic information declared on field annotations or field signatures. The following code listing shows the definition of the GenericConverter interface:

public interface GenericConverter {<!-- -->

    Set<ConvertiblePair> getConvertibleTypes();

    Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);

    class ConvertiblePair {<!-- -->
        private final Class<?> sourceType;
        private final Class<?> targetType;

        public ConvertiblePair(Class<?> sourceType, Class<?> targetType) {<!-- -->
            this.sourceType = sourceType;
            this.targetType = targetType;
        }

        // getter methods for sourceType and targetType
    }
}

The GenericConverter interface defines three methods and an internal class ConvertiblePair.

  • The getConvertibleTypes method returns a ConvertiblePair collection, where ConvertiblePair represents a pair of convertible source types and target types.
  • The convert method is used to perform actual type conversion. It receives the source object source, the source type descriptor sourceType, and the target type descriptor targetType as parameters, and returns the converted target object.
  • The ConvertiblePair class represents a pair of convertible source and target types. It contains two attributes: sourceType and targetType, which represent the source type and target type respectively. Through the ConvertiblePair class, you can define conversion relationships between multiple different source types and target types.

By implementing the GenericConverter interface, you can define conversion logic more flexibly and support conversion between multiple source types and target types. Additionally, contextual information, such as field annotations or generic information on field signatures, can be leveraged to drive the transformation process.

When we use a specific example to illustrate the use of GenericConverter, we can consider a simple scenario: convert a date represented by a string into a different date object (such as java.util.Date and java.time.LocalDate).

First, we define a class that implements the GenericConverter interface to perform the conversion of date strings to target date objects:

import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.GenericConverter;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;

public class StringToDateConverter implements GenericConverter {<!-- -->

    @Override
    public Set<ConvertiblePair> getConvertibleTypes() {<!-- -->
        Set<ConvertiblePair> convertiblePairs = new HashSet<>();
        convertiblePairs.add(new ConvertiblePair(String.class, Date.class));
        convertiblePairs.add(new ConvertiblePair(String.class, LocalDate.class));
        return convertiblePairs;
    }

    @Override
    public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {<!-- -->
        if (source == null || !(source instanceof String)) {<!-- -->
            return null;
        }

        String dateString = (String) source;
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");

        try {<!-- -->
            if (targetType.getType().equals(Date.class)) {<!-- -->
                return dateFormat.parse(dateString);
            } else if (targetType.getType().equals(LocalDate.class)) {<!-- -->
                Date date = dateFormat.parse(dateString);
                return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
            }
        } catch (ParseException e) {<!-- -->
            e.printStackTrace();
        }

        return null;
    }
}

In the above code, we implemented the GenericConverter interface and overridden two of its methods: getConvertibleTypes and convert.

In the getConvertibleTypes method, we define the pairing of convertible source types and target types. For our example, we can use the String type and the Date type and the String type and the LocalDate type as Convertible pairings.

In the convert method, we first check if the source object is null or not of string type, if so, return null. Then, we use SimpleDateFormat to parse the string date into a Date object, and perform corresponding conversions depending on the target type. In the example, we convert the Date object into a LocalDate object.

Next, we can use this custom converter to convert date strings to date objects. For example:

import org.springframework.core.convert.support.GenericConversionService;

public class Main {<!-- -->
    public static void main(String[] args) {<!-- -->
        GenericConversionService conversionService = new GenericConversionService();
        conversionService.addConverter(new StringToDateConverter());

        String dateString = "2023-10-10";

        Date date = conversionService.convert(dateString, Date.class);
        System.out.println("Date: " + date);

        LocalDate localDate = conversionService.convert(dateString, LocalDate.class);
        System.out.println("LocalDate: " + localDate);
    }
}

In the above code, we create a GenericConversionService object and add the converter we defined StringToDateConverter. Then, we use the conversionService object to convert the string date into a Date object and a LocalDate object, and print the output results.

This is just a simple example of how to use GenericConverter for conversion. In actual applications, you may need to implement your own GenericConverter based on specific needs and more complex conversion logic.

Please note that the above code only shows the definition of the GenericConverter interface, and the specific implementation and conversion logic will vary according to your needs and business logic. You need to implement the GenericConverter interface according to your actual situation, and define the appropriate conversion relationship between source type and target type based on ConvertiblePair.

Since GenericConverter is a more complex SPI interface, Converter or ConverterFactory is preferred for basic type conversion requirements.

ConditionalGenericConverter

Sometimes, you want the Converter to run only when a specific condition is true. For example, you might want to run the Converter only when a specific annotation exists on the target field, or only when a specific method (such as the static valueOf method) is defined on the target class. ConditionalGenericConverter is a combination of the GenericConverter and ConditionalConverter interfaces, which allows the definition of custom matching conditions like this:

public interface ConditionalGenericConverter extends GenericConverter, ConditionalConverter {<!-- -->

    boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType);

    Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);

}

The ConditionalGenericConverter interface inherits the GenericConverter and ConditionalConverter interfaces and defines two methods: matches and convert.

  • The matches method is used to determine whether the given source type and target type meet specific conditions. Depending on your needs, you can implement custom matching logic in the matches method, such as checking whether annotations on the target field or specific methods on the target class exist.
  • The convert method performs the actual type conversion. It receives the source object source, the source type descriptor sourceType, and the target type descriptor targetType as parameters, and returns the converted target object. Customized conversion logic can be implemented in the convert method to perform different conversion methods based on specific conditions.

By implementing the ConditionalGenericConverter interface, you can decide whether to run the Converter based on specific conditions and define conversion logic suitable for the conditions. In this way, you can control the conversion behavior more flexibly and choose the appropriate conversion method based on conditions.

Please note that the above code is just an example of the interface definition, and the specific implementation and matching conditions will vary according to your needs and business logic. You need to implement the ConditionalGenericConverter interface according to your actual situation, and define suitable conversion logic according to specific conditions.

Next, we will use an example to demonstrate how to use ConditionalGenericConverter to run a converter based on specific conditions.

Suppose we have an annotation @ConvertToUpperCase that indicates the conversion logic to convert a string to uppercase. We want the conversion to be performed only if the annotation exists on the target field.

First, define the annotation @ConvertToUpperCase:

import java.lang.annotation.*;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ConvertToUpperCase {<!-- -->
}

Next, create a converter class StringToUpperCaseConverter that implements the ConditionalGenericConverter interface:

import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.ConditionalGenericConverter;

import java.util.Collections;
import java.util.Set;

public class StringToUpperCaseConverter implements ConditionalGenericConverter {<!-- -->

    @Override
    public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {<!-- -->
        return targetType.hasAnnotation(ConvertToUpperCase.class);
    }

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

    @Override
    public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {<!-- -->
        if (source == null) {<!-- -->
            return null;
        }

        String stringValue = (String) source;
        return stringValue.toUpperCase();
    }
}

In the above code, the matches method checks whether the target type descriptor targetType has the ConvertToUpperCase annotation. If the annotation exists on the target field, the match is successful and the converter performs the conversion operation.

The getConvertibleTypes method returns a pair of convertible source types and target types. Here we only care about string type conversion.

The convert method performs the actual conversion operation, converting the string to uppercase.

Finally, we can use this custom converter to perform the conversion:

import org.springframework.core.convert.support.GenericConversionService;

public class Main {<!-- -->
    public static void main(String[] args) {<!-- -->
        GenericConversionService conversionService = new GenericConversionService();
        conversionService.addConverter(new StringToUpperCaseConverter());

        String lowercaseString = "hello";
        String uppercaseString = conversionService.convert(lowercaseString, String.class);
        System.out.println("Lowercase string: " + lowercaseString);
        System.out.println("Uppercase string: " + uppercaseString);

        // Example with annotation
        class Example {<!-- -->
            @ConvertToUpperCase
            private String annotatedString;

            // Getter and setter for annotatedString
        }

        Example example = new Example();
        example.setAnnotatedString("hello");
        conversionService.convert(example, Example.class);
        System.out.println("Annotated string: " + example.getAnnotatedString());
    }
}

In the above code, we create a GenericConversionService object and add the custom converter StringToUpperCaseConverter to the conversion service.

First, we convert the ordinary string “hello” to uppercase and print the result.

We then created a sample object containing a string field annotated with @ConvertToUpperCase and performed the conversion through the conversion service. In this example, the converter performs conversion operations based on the annotations on the fields.

Note that this is just a simple example of how to use ConditionalGenericConverter to run a converter based on specific conditions. In actual applications, you may need to implement your own converters and matching logic based on specific needs and more complex conditions.

ConversionService API

ConversionService defines a unified API that performs type conversion logic at runtime. Converters typically run behind the following facades:

public interface ConversionService {<!-- -->

    boolean canConvert(Class<?> sourceType, Class<?> targetType);

    boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType);

    <T> T convert(Object source, Class<T> targetType);

    Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);

    // Additional methods for registering and managing converters

}

The ConversionService interface defines a set of methods for performing type conversion operations. These methods include:

  • canConvert(Class sourceType, Class targetType): Checks whether the source type can be converted to the target type.
  • canConvert(TypeDescriptor sourceType, TypeDescriptor targetType): Checks whether the source type descriptor can be converted to the target type descriptor.
  • convert(Object source, Class targetType): Convert the source object to the target type and return the converted target object.
  • convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType): Convert the source object according to the specified source type descriptor and target type descriptor, and return the converted target object.

Through the ConversionService interface, conversion operations between various types can be performed, whether they are basic types or custom types. Conversion operations can be performed based on source and target types, or using type descriptors. ConversionService also provides other methods for registering and managing converters.

Using the ConversionService interface, you can hide specific conversion implementation details and perform type conversion through a unified API. This makes using different converters in your application much simpler and more convenient.

When using Spring Framework, you can use the ConversionService interface to perform type conversion operations. The following is a simple example showing how to use ConversionService for type conversion:

import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.DefaultConversionService;

public class Main {<!-- -->
    public static void main(String[] args) {<!-- -->
        // Create ConversionService object
        ConversionService conversionService = new DefaultConversionService();

        //Perform type conversion
        int intValue = conversionService.convert("123", Integer.class);
        double doubleValue = conversionService.convert("3.14", Double.class);
        String stringValue = conversionService.convert(42, String.class);

        // Output the conversion result
        System.out.println("Integer value: " + intValue);
        System.out.println("Double value: " + doubleValue);
        System.out.println("String value: " + stringValue);
    }
}

In the above code, we use the DefaultConversionService implementation to create a ConversionService object.

Then, we use the conversionService.convert() method to perform the type conversion operation. We convert the string “123” to type Integer, the string “3.14” to type Double, and the integer value 42 to Stringtype.

Finally, we print out the converted results.

Please note that DefaultConversionService is the default ConversionService implementation in Spring Framework, which provides many built-in converters. You can also customize and register your own converters to meet specific conversion needs.

Configuring ConversionService

A ConversionService is a stateless object designed to be instantiated when an application starts and then shared among multiple threads. In a Spring application, one ConversionService instance is usually configured for each Spring container (or ApplicationContext). Spring will recognize this ConversionService and use it when the framework needs to perform type conversion. You can also inject this ConversionService into any bean and call it directly.

The general steps for configuring and using ConversionService in a Spring application are as follows:

  1. Create a class that implements the ConversionService interface, or use the default implementation DefaultConversionService provided by Spring.
  2. Configuring the ConversionService instance can be done through Java configuration, XML configuration or annotation configuration.
  3. Register the ConversionService instance into the Spring container or ApplicationContext.

The following is an example that demonstrates how to configure and use ConversionService in a Spring application:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.DefaultConversionService;

@Configuration
public class AppConfig {<!-- -->

    @Bean
    public ConversionService conversionService() {<!-- -->
        return new DefaultConversionService();
    }

    // You can register a custom converter or configure other ConversionService related settings here

}

In the above example, we created an instance of DefaultConversionService and registered it as a Bean in the Spring container. Through the @Bean annotation, Spring will automatically manage the ConversionService instance and inject it when needed.

You can register custom converters or configure other related settings in the AppConfig class. For example, use the addConverter() method to register a custom converter, use the addFormatter() method to register a formatter, etc.

In other Spring components, you can obtain this ConversionService instance through dependency injection and call it when needed. For example:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class MyBean {<!-- -->

    private final ConversionService conversionService;

    @Autowired
    public MyBean(ConversionService conversionService) {<!-- -->
        this.conversionService = conversionService;
    }

    public void doConversion() {<!-- -->
        int intValue = conversionService.convert("123", Integer.class);
        //Perform other type conversion operations...
    }

    // Other methods...

}

In the above example, we inject the ConversionService instance into the MyBean component and use it in the doConversion() method to perform type conversion operations. Through dependency injection, Spring will automatically pass the instance of ConversionService to the constructor of MyBean.

In this way, the ConversionService can be configured and used in Spring applications to perform various type conversion operations. This keeps type conversions consistent throughout the application and easily customizable and extendable.

Summary

In short, ConversionService provides a general mechanism for handling type conversion and data formatting between objects. It is widely used in the Spring framework, especially in data binding, data conversion and expression evaluation scenarios. By using ConversionService, you can simplify the type conversion code and improve the maintainability and flexibility of the application.