Through the crowd, Spring has made a high-level track for you

Share, grow, and refuse to hide. Follow the official account [the Utopia of BAT] and reply to the keyword column. There are small and beautiful original columns such as Spring technology stack and middleware for free learning. This article has been https://www.yourbatman.cn Included.

✍ preface

Hello, I'm YourBatman.

Last article After a long introduction to the new generation of Spring type converters, I have been able to pass the exam at least. Among the many built-in converters introduced to Spring, I deliberately left a tail for this article to explain.

In order to make himself appear less (sudden) in the "crowd", brother A specially prepared these special converters to help you break the game, cross the crowd and set foot on the advanced track that Spring has made for you.

Version Convention

  • Spring Framework: 5.3.1
  • Spring Boot: 2.4.0

✍ text

The focus of this article will be on the four type converters left over above.

  • StreamConverter: converts a Stream stream to a collection / array and, if necessary, to an element type

These three are special and belong to the "last" and "bottom class" type converters:

  • ObjectToObjectConverter: general purpose conversion of original object to target object (through factory method or constructor)
  • IdToEntityConverter: the focus of this article. Give an ID to automatically convert it into an Entity object
  • FallbackObjectToStringConverter: converts any object call toString() to a String type. When no converter can be matched, it is used to cover the bottom

Default converter registration

Spring's new generation of type conversion has many built-in implementations, most of which are registered by default in the initialization phase. The registration point is in a static tool method provided by DefaultConversionService:

Static static methods are instance independent. Personally, I think it would be better to put the static methods in a xxxUtils for unified management. Putting them in a specific component class is likely to be semantically misleading

DefaultConversionService: 

    public static void addDefaultConverters(ConverterRegistry converterRegistry) {
        // 1. Add scalar converter (digital related)
        addScalarConverters(converterRegistry);
        // 2. Add a converter to process a collection
        addCollectionConverters(converterRegistry);

        // 3. Add converter for JSR310 time type support
        converterRegistry.addConverter(new ByteBufferConverter((ConversionService) converterRegistry));
        converterRegistry.addConverter(new StringToTimeZoneConverter());
        converterRegistry.addConverter(new ZoneIdToTimeZoneConverter());
        converterRegistry.addConverter(new ZonedDateTimeToCalendarConverter());

        // 4. Add a bottom-up converter (all the problems that cannot be handled above will be handled by these brothers)
        converterRegistry.addConverter(new ObjectToObjectConverter());
        converterRegistry.addConverter(new IdToEntityConverter((ConversionService) converterRegistry));
        converterRegistry.addConverter(new FallbackObjectToStringConverter());
        converterRegistry.addConverter(new ObjectToOptionalConverter((ConversionService) converterRegistry));
    }

    }

This static method is used to register the global and default converters, so that Spring has the basic conversion ability to complete most of the conversion work. To facilitate the memory of this registration process, I draw it into a diagram for you to save:

Special emphasis: the registration order of the converter is very important, which determines the matching result of the universal converter (who is first, who is first).

You may also have questions about this picture:

  1. The JSR310 converter only sees TimeZone, ZoneId and other conversions. Why don't you see the more commonly used LocalDate, LocalDateTime and other types of conversions? Is Spring not supported by default?

    1. A: of course not. How can Spring not support such a common scenario? However, this is not so much a type conversion as a format. Therefore, the formatting chapters in the next three articles will be described as the top priority
  2. Generally, the name means Converter, but what is the function of StreamConverter? What scenarios will take effect

    1. Answer: This article describes
  3. What is the meaning of the converter with the bottom? What is the role of this versatile converter

    1. Answer: This article describes

StreamConverter

It is used to realize the conversion from set / array type to Stream type. It can also be seen from the set<convertiblepair> set it supports:

@Override
public Set<ConvertiblePair> getConvertibleTypes() {
    Set<ConvertiblePair> convertiblePairs = new HashSet<ConvertiblePair>();
    convertiblePairs.add(new ConvertiblePair(Stream.class, Collection.class));
    convertiblePairs.add(new ConvertiblePair(Stream.class, Object[].class));
    convertiblePairs.add(new ConvertiblePair(Collection.class, Stream.class));
    convertiblePairs.add(new ConvertiblePair(Object[].class, Stream.class));
    return convertiblePairs;
}

It supports two-way matching rules:

Code example

/**
 * {@link StreamConverter}
 */
@Test
public void test2() {
    System.out.println("----------------StreamConverter---------------");
    ConditionalGenericConverter converter = new StreamConverter(new DefaultConversionService());

    TypeDescriptor sourceTypeDesp = TypeDescriptor.valueOf(Set.class);
    TypeDescriptor targetTypeDesp = TypeDescriptor.valueOf(Stream.class);
    boolean matches = converter.matches(sourceTypeDesp, targetTypeDesp);
    System.out.println("Can I convert:" + matches);

    // Perform conversion
    Object convert = converter.convert(Collections.singleton(1), sourceTypeDesp, targetTypeDesp);
    System.out.println(convert);
    System.out.println(Stream.class.isAssignableFrom(convert.getClass()));
}

Run program, output:

----------------StreamConverter---------------
Can I convert: true
java.util.stream.ReferencePipeline$Head@5a01ccaa
true

Concern: the underlying layer still relies on DefaultConversionService to complete the conversion between elements. For example, the actual steps of set - > stream in this example are:

That is to say, any set / array type is a List that is first converted to an intermediate state and finally called List Stream() is converted to stream stream; In case of reverse conversion, call source Collect (collectors.<object>tolist()) converts a stream to a List and then to a specific collection or array type.

Note: if the source is an array type, the ArrayToCollectionConverter is actually used at the bottom layer. Please draw inferences from one instance

Usage scenarios

The access permission of StreamConverter is default, and we cannot use it directly. It can be seen from the above introduction that Spring registers it in the registry by default, so we can directly use the conversion service interface ConversionService for users.

@Test
public void test3() {
    System.out.println("----------------StreamConverter Usage scenarios---------------");
    ConversionService conversionService = new DefaultConversionService();
    Stream<Integer> result = conversionService.convert(Collections.singleton(1), Stream.class);

    // consumption
    result.forEach(System.out::println);
    // result.forEach(System.out::println); //stream has already been operated upon or closed
}

Run program, output:

----------------StreamConverter Usage scenarios---------------
1

Again, it is emphasized that streams can only be read (consumed) once.

With the powerful capabilities provided by ConversionService, we can use it in secondary development based on Spring/Spring Boot to improve the universality and fault tolerance of the system. For example, when the method input parameter is a Stream type, you can pass in either a Stream type, a Collection type, or an array type. Does it force the grid up in an instant.

Bottom cover converter

According to the order of adding converters, Spring adds four general-purpose converters at the end. You may not pay attention to it at ordinary times, but it is playing its role in real time.

ObjectToObjectConverter

Convert the source object to the target type, which is very common: Object - > Object:

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

Although it supports object - > object, there seems to be no restriction, but there are actually agreed conditions:

@Override
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
    return (sourceType.getType() != targetType.getType() &&
            hasConversionMethodOrConstructor(targetType.getType(), sourceType.getType()));
}

The judgment logic of whether it can be processed lies in the hasConversionMethodOrConstructor method, which translates to whether there is a conversion method or constructor. The detailed code processing logic is shown in the following screenshot:

The logic of this part can be divided into two parts:

  • part1: get the Member from the cache, directly judge the availability of the Member, and quickly return if available
  • part2: if part1 does not return, execute the trilogy, try to find a suitable Member and put it into the cache (if not, return null)
part1: quick return process

When it is not the first time to enter the processing, it will go through the quick return process. That is, the isApplicable judgment logic in step 0 has the following concerns:

  1. Member includes Method or Constructor
  2. Method: for static static methods, the first input parameter type of the method must be sourceType; If it is not a static method, the sourceType must be method Subtype of getdeclaringclass() / same type
  3. Constructor: the first input parameter type of the constructor must be the source type sourceType

To create an instance of a target object, this converter supports two ways:

  1. Create an instance through the factory method / instance method (method.invoke(source))
  2. Create an instance through the constructor (ctor.newInstance(source))

For the above case s, code examples will be given below.

part2: trilogy process

For the transformation processed for the first time, you will enter the detailed trilogy logic: try to find the appropriate Member to create the target instance through reflection, that is, steps 1, 2 and 3 in the above figure.

step1:determineToMethod: find the instance method from sourceClass. The method has the following requirements:

  • Method name must be 'to' + targetclass Getsimplename(), such as toPerson()
  • Method access must be public
  • The return value of the method must be the target type or its subtype

step2:determineFactoryMethod, find the static factory method, and have the following requirements for the method:

  • Method name must be valueOf(sourceClass) or of(sourceClass) or from(sourceClass)
  • Method access must be public

step3:determineFactoryConstructor, find the constructor. The constructor has the following requirements:

  • There is a parameter and the parameter type is a constructor of sourceClass type
  • Constructor access must be public

It is worth noting that this converter does not support object The toString () method converts the sourceType to java lang.String. For toString() support, use the more comprehensive FallbackObjectToStringConverter described below.

Code example
  • Instance method
// sourceClass
@Data
public class Customer {
    private Long id;
    private String address;

    public Person toPerson() {
        Person person = new Person();
        person.setId(getId());
        person.setName("YourBatman-".concat(getAddress()));
        return person;
    }

}

// tartgetClass
@Data
public class Person {
    private Long id;
    private String name;
}

Write test cases:

@Test
public void test4() {
    System.out.println("----------------ObjectToObjectConverter---------------");
    ConditionalGenericConverter converter = new ObjectToObjectConverter();

    Customer customer = new Customer();
    customer.setId(1L);
    customer.setAddress("Peking");

    Object convert = converter.convert(customer, TypeDescriptor.forObject(customer), TypeDescriptor.valueOf(Person.class));
    System.out.println(convert);

    // ConversionService mode (actual usage mode)
    ConversionService conversionService = new DefaultConversionService();
    Person person = conversionService.convert(customer, Person.class);
    System.out.println(person);
}

Run program, output:

----------------ObjectToObjectConverter---------------
Person(id=1, name=YourBatman-Peking)
Person(id=1, name=YourBatman-Peking)
  • Static factory method
// sourceClass
@Data
public class Customer {
    private Long id;
    private String address;
}

// targetClass
@Data
public class Person {

    private Long id;
    private String name;

    /**
     * Method names can be valueOf, of, from
     */
    public static Person valueOf(Customer customer) {
        Person person = new Person();
        person.setId(customer.getId());
        person.setName("YourBatman-".concat(customer.getAddress()));
        return person;
    }
}

The test case is completely the same as above, and run the output again:

----------------ObjectToObjectConverter---------------
Person(id=1, name=YourBatman-Peking)
Person(id=1, name=YourBatman-Peking)

Method names can be valueOf, of or from. This naming method is almost an unwritten rule in the industry, so it will be easier to follow. However, it is recommended to write notes to prevent others from renaming and causing the conversion to take effect.

  • constructor

Basically the same as the example of static factory method, omitted

Usage scenarios

Based on this converter, you can complete the conversion from any object to any object. You only need to follow all the default conventions of the method name / constructor. It is very helpful when we are developing and writing the conversion layer. This kind of problem can be solved with the help of ConversionService.

Another way to convert object - > object is to customize the converter<s, t> and register it in the registry. As for which one is appropriate, it depends on the specific application scenario. This article only gives you one more choice

IdToEntityConverter

Id(S) --> Entity(T). Convert an entity ID to an entity object by calling a static lookup method. The search method in entity must meet the following conditions: find[EntityName]([IdType]):

  1. Must be a static static method
  2. Method name must be find + entityName. For the Person class, the method name is findPerson
  3. Method parameter list must be 1
  4. Return value type must be Entity type

Note: this method does not need to be public, but it is recommended to use public. In this way, even if the Security level of the JVM is enabled, it can be accessed normally

The supported conversion pairs are as follows: ID and Entity can be of any type, and can be converted to

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

The conditions for judging whether the quasi transformation can be performed are: there is a find method that meets the conditions, and the source can be converted to the ID type (note that the source can be converted to the ID type, not the target type)

@Override
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
    Method finder = getFinder(targetType.getType());
    return (finder != null 
        && this.conversionService.canConvert(sourceType, TypeDescriptor.valueOf(finder.getParameterTypes()[0])));
}

Locating Entity objects based on ID S is simply too common. Making good use of the capabilities provided by this converter may enable you to get twice the result with half the effort, greatly reduce duplicate code, and write more elegant, concise, and easier to maintain code.

Code example

Entity: prepare the qualified findXXX method

@Data
public class Person {

    private Long id;
    private String name;

    /**
     * Locate a Person instance by ID
     */
    public static Person findPerson(Long id) {
        // Generally, it is checked from the database according to the id. here, it is simulated through new
        Person person = new Person();
        person.setId(id);
        person.setName("YourBatman-byFindPerson");
        return person;
    }

}

Apply IdToEntityConverter and write sample code:

@Test
public void test() {
    System.out.println("----------------IdToEntityConverter---------------");
    ConditionalGenericConverter converter = new IdToEntityConverter(new DefaultConversionService());

    TypeDescriptor sourceTypeDesp = TypeDescriptor.valueOf(String.class);
    TypeDescriptor targetTypeDesp = TypeDescriptor.valueOf(Person.class);
    boolean matches = converter.matches(sourceTypeDesp, targetTypeDesp);
    System.out.println("Can I convert:" + matches);

    // Perform conversion
    Object convert = converter.convert("1", sourceTypeDesp, targetTypeDesp);
    System.out.println(convert);
}

Run program, normal output:

----------------IdToEntityConverter---------------
Can I convert: true
Person(id=1, name=YourBatman-byFindPerson)

The example effect is: if you pass in "1" of string type, you can get a Person instance. As you can see, what we pass in is 1 of string type, while the method input parameter id type is actually Long type. However, because they can complete the string - > Long conversion, they can finally get an Entity instance.

Usage scenarios

There are many usage scenarios, and findById() can be used instead of findById(). For example:

Controller layer:

@GetMapping("/ids/{id}")
public Object getById(@PathVariable Person id) {
    return id;
}

@GetMapping("/ids")
public Object getById(@RequestParam Person id) {
    return id;
}

Tips: I don't recommend writing this in the Controller layer, because there is no semantic alignment, which is bound to bring some trouble in the code writing process.

Service layer:

@Autowired
private ConversionService conversionService;

public Object findById(String id){
    Person person = conversionService.convert(id, Person.class);

    return person;
}

Tips: in the Service layer, I think it is OK. The domain design idea of type transformation replaces the top-down process programming idea.

FallbackObjectToStringConverter

Simply call the object\tostring () method to convert any supported type to String type, which serves as the bottom layer.

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

The converter supports CharSequence/StringWriter and other types, as well as all objecttoobjectconverters Type of hasconversionmethodorconstructor (sourceclass, string.class).

Note: the ObjectToObjectConverter does not handle any conversion of String type. It was left to it

Code example

Omitted.

ObjectToOptionalConverter

Convert any type to an optional<t> type, which serves as the bottom pocket. Just learn about it.

Code example
@Test
public void test5() {
    System.out.println("----------------ObjectToOptionalConverter---------------");
    ConversionService conversionService = new DefaultConversionService();
    Optional<Integer> result = conversionService.convert(Arrays.asList(2), Optional.class);

    System.out.println(result);
}

Run program, output:

----------------ObjectToOptionalConverter---------------
Optional[[2]]
Usage scenarios

A typical application scenario: among the parameters that can be transferred or not in the Controller, we can not only use @RequestParam(required = false) Long id, but also write: @requestparam optional<long> ID.

✍ summary

This article is a supplement to the new generation of Spring type conversion mechanism introduced above. Because there is less attention, there is a chance to break through.

For Spring registered converters, you should pay special attention to the following points:

  1. The order of registration is important. Register first, service first (if supported)
  2. By default, Spring will register a large number of built-in converters to support String/ numeric type conversion and collection type conversion, which can solve most of the conversion problems at the protocol level.

    1. For example, in the Controller layer, the input is a JSON string, which can be automatically encapsulated into numeric types, collection types, and so on
    2. For example, @Value is injected with String type, but can also be received with numeric or collection type

For complex object - > object type conversion, you generally need to customize the converter or complete the conversion according to the standard writing method in this article. In short: the ConversionService provided by Spring focuses on type conversion services. It is a very practical API, especially when you are doing secondary development based on Spring.

Of course, the ConversionService mechanism has not been introduced in detail. How to use it? How does it work? How to extend? With these three questions, I'll see you next.

✔✔✔ Recommended reading ✔✔✔

Spring type conversion series:

[Jackson] series:

[data validation Bean Validation] series:

[new features] series:

[program life] series:

There are also [Spring configuration class], [Spring static], [Spring data binding], [Spring Cloud Netflix], [Feign], [Ribbon], [Hystrix] More original columns can be obtained by following the utopian reply column of BAT. You can also add me fsx1056342982 to make friends.

Some are finished, some are in serials. I'm yourbatman. I'll see you next time

Tags: Spring

Posted by dpacmittal on Fri, 03 Jun 2022 15:40:35 +0530