Convenient and efficient JAVA object conversion tool

1. Introduction

lamia is a high-performance Java entity mapping tool. Using simple annotations can help you generate the corresponding conversion code at compile time

Project address:

1.1 advantages

  • Convenient and flexible compile time to quickly generate conversion code
  • Supports the simultaneous use of annotation processor frameworks such as lombok
  • Support conversion between collection types such as Map/List
  • Independent of idea plug-ins
  • Support incremental compilation of idea

Differences between 1.1 and MapStruct

Similar to MapStruct, this framework generates the corresponding conversion code at compile time. Why should the author repeat this wheel?

The author is also a user of MapStruct, but there are several problems with MapStruct

  • The conversion method is defined in the form of an interface. You need to manually call mappers.getmapper (conversion interface. class) to obtain the true value
    The conversion implementation class has a certain coupling to the code
  • For very complex object conversion, it is necessary to define the corresponding expression in the annotation. In fact, many times, I have written the expression myself and completed the set conversion
    , it's uncomfortable for coders like me to write expressions without any prompts

Based on the above points, the author wrote Lamia, which provides higher flexibility while generating conversion code quickly

2. Quick use

Support environment JDK8 and above

1. Introduce maven coordinates

<dependency>
    <groupId>io.github.cao2068959</groupId>
    <artifactId>lamia</artifactId>
    <version>1.2.0</version>
</dependency>

If you need to compile manually, first clone the warehouse, and then execute the maven command

mvn clean install

Note: only jdk8 can be used in the compilation environment. Jdk9 or above modularizes the package of sun.tools.jar, which may lead to compilation failure
However, the version compiled with JDK8 can be compatible with JDK9 and above

2. Write a conversion method and annotate @ Mapping

3. Call the static method of generating assignment statement in the conversion method

MyType varName = (Strong conversion to the type you want to return)Lamia.convert(Converted parameters);

The corresponding simplest demo is shown below

public class Test {

    public static void main(String[] args) {
        User user = User.builder().address("dewdewd").name("name").old(312).chy("ffw").build();
        Test main = new Test();
        System.out.println(main.toVO(user));
    }

    @Mapping
    UserVO toVO(User user) {
        return (UserVO) Lamia.convert(user);
    }
}

@Data
@Builder
public class User {
  String name;
  Integer old;
  String address;
}

@Data
public class UserVO {
  String name;
  Integer old;
  String address;
}

The corresponding compiled code is as follows:

Note here:

  • The generated entity must have a corresponding constructor / setter method. If a field does not exist in the constructor / setter method, this field will not be set
    • If there are multiple construction methods, the one that best matches the converted object will be selected
  • The corresponding getter method is required in the converted object

3. Advanced tutorial

3.1 @Mapping

For the method annotated with @ Mapping, each line of code in the method will be scanned at compile time until the statement is scanned
Lamia.convert, and then generate the corresponding conversion code according to the parameters configured in lamia.convert, and then replace the statement with the generated code
Lamia.convert

Note: after the corresponding conversion code is generated here, the entire method is not replaced, but only the Lamia.convert statement is replaced

As shown in the following code, there is a corresponding print code before calling Lamia.convert

    @Mapping
    void printlnUserVO(User user) {
        System.out.println("Before transcoding--------------------->");
        UserVO userVO = (UserVO) Lamia.convert(user);
        System.out.println("After code transfer<---------------------");
        System.out.println(userVO);
    }

The corresponding generated code is as follows:

Note: multiple Lamia.convert statements can exist in a method at the same time

As shown in the following code, there are multiple conversion statements in a method marked with @ Mapping

    @Mapping
    void printlnUserVO(User user) {
        //Convert to map
        Map<String, Object> map = (HashMap<String, Object>) Lamia.convert(user);
        System.out.println(map);
    
        //Convert to userVO
        UserVO userVO = (UserVO) Lamia.convert(user);
        System.out.println(userVO);
    }

The generated code is as follows

3.2 Lamia.convert statement

In the class Lamia of this framework, a static method convert(Object... param) is provided for applications
This method doesn't make any sense because it doesn't do anything, but it is very important for this framework during compilation
The core engine of this framework will replace this method with real conversion code, which needs to be used in conjunction with the @ Mapping annotation

When calling Lamia.convert method, it should be noted that when calling this method, three parts must be set: conversion input parameter, forced conversion type and receiving attribute

3.2.1 forced rotor type

The forced conversion type here represents the actual generated instance type, and the received parameter can be the corresponding parent class, as shown below

Map<String, Object> map = (HashMap<String, Object>) Lamia.convert(user);

Here, the user object is converted to Map type, and the HashMap used in the type is forcibly converted. Then the real instance generated is HashMap

  • QA:
    • Q: why do you use strong conversion instead of directly putting the type to be converted into the first parameter of the method, such as public static t convert (class < T > Tclass, object... Param)
      Form of
    • A: because if you put the type to be converted into the first parameter, you will lose generics. For example, it is illegal to use it below

3.2.2 conversion into parameters

Note that the static method uses variable parameters, so you can put an infinite number of parameters into it to convert an aggregated object
As follows:

    @Mapping
    UserVO toVO(User user) {
        String name = "myName";
        //It should be noted here that the UserVO object has a field called name and whose type is also String, and has a corresponding setter or constructor
        return (UserVO) Lamia.convert(user, name);
    }

The generated code is as follows:

There is a corresponding priority for input parameters. By default: separate fields > fields contained in the object, so here name > user.name

Note: priority can be modified. See 3.3 @MapMember for details

3.3 @MapMember

When the Lamia.convert(Object... param) method is called, the original data to be converted can be passed into the parameters of the method,
Lamia maps the fields corresponding to the generated object by passing in the field name of the parameter. If the field name I pass in is different from the field name to be mapped
Then you can mark the @ MapMember annotation on the corresponding incoming field, as shown below

    @Mapping
    UserVO toVO(User user) {
        @MapMember("name") String what = "what";
        return (UserVO) Lamia.convert(user, what);
    }

The generated code is as follows:

3.3.1 input priority

Lamia.convert(Object... param) if there are multiple input parameters, you can set the priority of the input parameters. Without setting the priority manually, follow two principles

  • Separate fields > fields contained in the object. This has been demonstrated above, but there is no more explanation
  • If both fields are separate fields or fields contained in the object, the higher the priority in the input parameter of the method Lamia.convert(Object... param), the higher the priority
    The specific formula is as follows:
    @Mapping
    UserVO toVO(User user) {
        @MapMember("name") String what = "what";
        @MapMember("name") String why = "why";
        return (UserVO) Lamia.convert(user, what, why);
    }

As can also be seen above, the two input parameters with the same priority take effect after the previous one, so what is finally set in the above code

If the default priority is identified by a number, it is

  • Individual fields: 10
  • Fields contained in object: 5

The higher the number, the higher the priority

Of course, the priority can also be controlled manually. There is a method priority() in the annotation @ MapMember, which is used to set the priority, as shown below

@Mapping
    UserVO priorityTest(User user) {
        @MapMember(value = "name", priority = 4) String what = "what";
        return (UserVO) Lamia.convert(user, what);
    }

user is the field in the object used, so the default priority is 5, and I manually set the priority of the name variable to 4 (the default is 10), so the compiled code is as follows

At the same time, priority() can also act on the object to make all mapped fields have priority, as shown below:

    @Mapping
    UserVO priorityTest(User user) {
        @MapMember(spread = true, priority = 2) DetailedUser detailedUser = DetailedUser.builder()
                .address("DetailedUser_address").name("DetailedUser_name")
                .email("704188931@qq.com").old(1).build();
        return (UserVO) Lamia.convert(detailedUser, user);
    }

    @Data
    @Builder
    public class User {
      String name;
      Integer old;
      String address;
    }

    @Data
    @Builder
    public class DetailedUser {
      String name;
      Integer old;
      String address;

      String email;
    }

    @Data
    public class UserVO {
      String name;
      Integer old;
      String address;

      String email;
    }

Here, only one email field is added to the DetailedUser object, and other fields are exactly the same, so I think name/old/address
The three common fields use the User's opposite, while the email field uses the one in DetailedUser, so you just need to make DetailedUser take precedence over User

Therefore, the compiled code is as follows:

3.3.2 spread

When an object is passed into the Lamia.convert(Object... param) method, do you map all fields in the object to the fields of the result object,
Or put this object into the result object as a field?

Because of this difference, there is spread. Here, spread means expansion and diffusion. Here, the fields in the object are diffused

There is a spread() method in the @ MapMember annotation to identify whether the object needs a spread. Of course, there is a default value of spread when the @ Mapping annotation is not written

  • When the object comes from the method input parameter: spread=true
  • When the object comes from a variable defined inside the method: spread=false

As follows:

    @Mapping
    UserVO spreadTest(User childUser) {
            return (UserVO) Lamia.convert(childUser);
    }

Because the childUser parameter comes from the method input parameter, the default spread=true, then all attributes in childUser will be mapped to UserVO
The compilation results are as follows:

If using method internal parameters

    @Mapping
    UserVO spreadTest() {
        User childUser = User.builder().address("dewdewd").name("name").old(312).build();
        return (UserVO) Lamia.convert(childUser);
    }

Then the method variable childUser will be set as a field into UserVO.childUser. The compilation results are as follows:

Of course, you can use the @ MapMember annotation to change the value of spread, as shown below

    @Mapping
    UserVO spreadTest(@MapMember(value = "childUser", spread = false) User user) {
        @MapMember(spread = true) User childUser = User.builder().address("dewdewd").name("name").old(312).build();
        return (UserVO) Lamia.convert(childUser, user);
    }

The compilation results are as follows

4. Conversion of common data structures

Lamia also supports the transformation of some common data structures

4.1 Map

Support mutual conversion between objects and maps. Generic types will be erased here. The generated map type is HashMap < string, Object >. Please ensure the unity of types

    @Mapping
    Map<String, Object> toMap(User user) {
        return (HashMap) Lamia.convert(user);
    }

    @Mapping
    UserVO mapToUser(Map<String, Object> map) {
        return (UserVO) Lamia.convert(map);
    }

The compiled codes are:

4.2 conversion between lists

It also supports the conversion between two collection objects, such as list < user > -- > List < uservo >

    @Mapping
    void listTest(List<User> users) {
        List<UserVO> result = (List<UserVO>) Lamia.convert(users);
        //Add another custom object to the converted result
        result.add(new UserVO());
    }

At the same time, when two sets are converted to each other, the value of a field can be overwritten as follows:

    @Mapping
    void listTest(List<User> users) {
        @MapMember(value = "name", priority = 10) String test = "listTest";
        List<UserVO> result = (List<UserVO>) Lamia.convert(users, test);
        //Add another custom object to the converted result
        result.add(new UserVO());
    }

4.3 Optional support

When the mapped field names can correspond to each other, but the types are different, if it is found that it is an Optional packaging type, it will be unpacked / packed automatically,
Optional < string > name and String name can be exchanged

Define an entity with Optional fields as follows:

@Data
public class Anonymous {

  Optional<String> name;
  Optional<Integer> old;
  Optional<Integer> address;
}

Then it is converted to the normal User object

    @Mapping
    Anonymous toAnonymous(User user) {
        return (Anonymous) Lamia.convert(user);
    }

    @Mapping
    UserVO toUser(Anonymous anonymous) {
        return (UserVO) Lamia.convert(anonymous);
    }

The compiled code is as follows

Tags: Java

Posted on Wed, 29 Sep 2021 22:25:25 -0400 by Sindarin