How does SpringBoot copy objects? Old birds play like this!

Keywords: Java Spring Spring Boot mvc

Hello, I'm misty.

Today, we bring the fourth article of the old bird series of SpringBoot to talk about how to gracefully implement object replication in daily development.

First, let's see why object replication is needed?

Why do I need object replication

As mentioned above, it is the most common three-tier MVC architecture model in our usual development. During editing, the Controller layer receives the DTO object from the front end, and the DTO needs to be converted into DO in the Service layer, and then saved in the database. During the query operation, after the Service layer queries the DO object, it needs to convert the DO object into VO object, and then return it to the front end through the Controller layer for rendering.

This involves a lot of object conversion. Obviously, we can't directly use getter/setter to copy object properties, which seems too low. Imagine that your business logic is full of getters & setters. How will old birds laugh at you during code review?

So we must find a third-party tool to help us realize object transformation.

Seeing this, some students may ask, why can't you use DO objects uniformly at the front and back ends? So there is no object conversion?

Imagine that if we don't want to define DTO and VO, we can directly use DO to the data access layer, service layer, control layer and external access interface. At this time, if a field is deleted or modified in the table, the DO must be modified synchronously. This modification will affect all layers, which does not comply with the principle of high cohesion and low coupling. By defining different DTOs, you can control the exposure of different attributes to different systems, and hide specific field names through attribute mapping. Different businesses use different models. When a business changes and needs to modify fields, it does not need to consider the impact on other businesses. If the same object is used, many inelegant compatibility behaviors may occur because it "dare not change arbitrarily".

Object replication tool class recommendation

There are many class library tools for object replication, in addition to the common Apache BeanUtils, Spring BeanUtils, Cglib BeanCopier, and heavyweight components MapStruct, Orika, Dozer, ModelMapper, etc.

If there are no special requirements, these tool classes can be used directly, except for Apache's BeanUtils. The reason is that in order to pursue perfection, the underlying source code of Apache beautils adds too much packaging, uses a lot of reflection and makes a lot of verification, which leads to poor performance. It is mandatory to avoid using Apache beautils in Alibaba's development manual.

As for the remaining heavyweight components, considering their performance and ease of use, I recommend Orika here. At the bottom of Orika, javassist class library is used to generate the bytecode of Bean mapping, and then the bytecode file generated by execution is directly loaded, which is much faster than using reflection for assignment.

Foreign great God baeldung has tested the performance of common components in detail, and you can pass it https://www.baeldung.com/java-performance-mapping-frameworks see.

Orika basic use

To use Orika is simple, just four simple steps:

  1. Introduce dependency
<dependency>
  <groupId>ma.glasnost.orika</groupId>
  <artifactId>orika-core</artifactId>
  <version>1.5.4</version>
</dependency>
  1. Construct a MapperFactory
MapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();  
  1. Register field mapping
mapperFactory.classMap(SourceClass.class, TargetClass.class)  
   .field("firstName", "givenName")
   .field("lastName", "sirName")
   .byDefault()
   .register();

When the field names of two entities are inconsistent, they can be mapped through the. field() method. If the field names are the same, they can be omitted. The byDefault() method is used to register attributes with the same name. If you don't want a field to participate in the mapping, you can use the exclude method.

  1. Mapping
MapperFacade mapper = mapperFactory.getMapperFacade();

SourceClass source = new SourceClass();  
// set some field values
...
// map the fields of 'source' onto a new instance of PersonDest
TargetClass target = mapper.map(source, TargetClass.class);  

After the above four steps, we have completed the conversion from SourceClass to TargetClass. As for other ways of using Orika, you can refer to it http://orika-mapper.github.io/orika-docs/index.html

After seeing this, some fans will say: what do you recommend? Orika is not easy to use. Every time, you must create MapperFactory and establish field mapping relationship before mapping transformation.

Don't worry, I've prepared a tool class OrikaUtils for you, which you can get through the github warehouse at the end of the text.

It provides five common methods:

Corresponding to:

  1. Field consistent entity conversion
  2. Field inconsistent entity transformation (field mapping required)
  3. Field consistent set conversion
  4. Field inconsistent set conversion (field mapping required)
  5. Field property conversion registration

Next, we focus on the use of this tool class through unit test cases.

Orika tool class usage document

First prepare two basic entity classes, Student and Teacher.

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student {
    private String id;
    private String name;
    private String email;
}

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Teacher {
    private String id;
    private String name;
    private String emailAddress;
}

TC1, basic entity mapping

/**
 * Copy only the same attributes
 */
@Test
public void convertObject(){
  Student student = new Student("1","javadaily","jianzh5@163.com");
  Teacher teacher = OrikaUtils.convert(student, Teacher.class);
  System.out.println(teacher);
}

Output results:

Teacher(id=1, name=javadaily, emailAddress=null)

At this time, the field email cannot be mapped because the attribute names are inconsistent.

TC2, entity mapping - field conversion

/**
 * Copy different attributes
 */
@Test
public void convertRefObject(){
  Student student = new Student("1","javadaily","jianzh5@163.com");

  Map<String,String> refMap = new HashMap<>(1);
  //map key places the source attribute and value places the target attribute
  refMap.put("email","emailAddress");
  Teacher teacher = OrikaUtils.convert(student, Teacher.class, refMap);
  System.out.println(teacher);
}

Output results:

Teacher(id=1, name=javadaily, emailAddress=jianzh5@163.com)

At this point, because the fields are mapped, you can map email to emailAddress. Note that in the refMap here, the key places the attributes of the source entity, while the value places the attributes of the target entity. Don't reverse it.

TC3, base set mapping

/**
  * Copy only the same set of attributes
  */
@Test
public void convertList(){
  Student student1 = new Student("1","javadaily","jianzh5@163.com");
  Student student2 = new Student("2","JAVA Rizhilu","jianzh5@xxx.com");
  List<Student> studentList = Lists.newArrayList(student1,student2);

  List<Teacher> teacherList = OrikaUtils.convertList(studentList, Teacher.class);

  System.out.println(teacherList);
}

Output results:

[Teacher(id=1, name=javadaily, emailAddress=null), Teacher(id=2, name=JAVA Rizhilu, emailAddress=null)]

At this time, due to inconsistent attribute names, the field email cannot be mapped in the collection.

TC4, set mapping - field mapping

/**
 * Map a collection of different attributes
 */
@Test
public void convertRefList(){
  Student student1 = new Student("1","javadaily","jianzh5@163.com");
  Student student2 = new Student("2","JAVA Rizhilu","jianzh5@xxx.com");
  List<Student> studentList = Lists.newArrayList(student1,student2);

  Map<String,String> refMap = new HashMap<>(2);
  //map key places the source attribute and value places the target attribute
  refMap.put("email","emailAddress");

  List<Teacher> teacherList = OrikaUtils.convertList(studentList, Teacher.class,refMap);

  System.out.println(teacherList);
}

Output results:

[Teacher(id=1, name=javadaily, emailAddress=jianzh5@163.com), Teacher(id=2, name=JAVA Rizhilu, emailAddress=jianzh5@xxx.com)]

You can also map by:

Map<String,String> refMap = new HashMap<>(2);
refMap.put("email","emailAddress");
List<Teacher> teacherList = OrikaUtils.classMap(Student.class,Teacher.class,refMap)
        .mapAsList(studentList,Teacher.class);

TC5, set and entity mapping

Sometimes we need to map collection data to entities, such as the Person class

@Data
public class Person {
    private List<String> nameParts;
}

Now you need to map the value of the Person class nameParts to the Student, which you can do

/**
 * Array and List mapping
 */
@Test
public void convertListObject(){
   Person person = new Person();
   person.setNameParts(Lists.newArrayList("1","javadaily","jianzh5@163.com"));

    Map<String,String> refMap = new HashMap<>(2);
    //map key places the source attribute and value places the target attribute
    refMap.put("nameParts[0]","id");
    refMap.put("nameParts[1]","name");
    refMap.put("nameParts[2]","email");

    Student student = OrikaUtils.convert(person, Student.class,refMap);
    System.out.println(student);
}

Output results:

Student(id=1, name=javadaily, email=jianzh5@163.com)

TC6, class type mapping

Sometimes we need class type object mapping, such as the BasicPerson class

@Data
public class BasicPerson {
    private Student student;
}

Now you need to map the BasicPerson to the Teacher

/**
 * Class type mapping
 */
@Test
public void convertClassObject(){
    BasicPerson basicPerson = new BasicPerson();
    Student student = new Student("1","javadaily","jianzh5@163.com");
    basicPerson.setStudent(student);

    Map<String,String> refMap = new HashMap<>(2);
    //map key places the source attribute and value places the target attribute
    refMap.put("student.id","id");
    refMap.put("student.name","name");
    refMap.put("student.email","emailAddress");

    Teacher teacher = OrikaUtils.convert(basicPerson, Teacher.class,refMap);
    System.out.println(teacher);
}

Output results:

Teacher(id=1, name=javadaily, emailAddress=jianzh5@163.com)

TC7, multiple mapping

Sometimes we encounter multiple mappings, such as mapping StudentGrade to TeacherGrade

@Data
public class StudentGrade {
    private String studentGradeName;
    private List<Student> studentList;
}

@Data
public class TeacherGrade {
    private String teacherGradeName;
    private List<Teacher> teacherList;
}

This scenario is a little complicated. The email fields of Student and Teacher attributes are different, so conversion mapping is required; The attributes in StudentGrade and TeacherGrade also need to be mapped.

/**
 * One to many mapping
 */
@Test
public void convertComplexObject(){
  Student student1 = new Student("1","javadaily","jianzh5@163.com");
  Student student2 = new Student("2","JAVA Rizhilu","jianzh5@xxx.com");
  List<Student> studentList = Lists.newArrayList(student1,student2);

  StudentGrade studentGrade = new StudentGrade();
  studentGrade.setStudentGradeName("master");
  studentGrade.setStudentList(studentList);

  Map<String,String> refMap1 = new HashMap<>(1);
  //map key places the source attribute and value places the target attribute
  refMap1.put("email","emailAddress");
  OrikaUtils.register(Student.class,Teacher.class,refMap1);


  Map<String,String> refMap2 = new HashMap<>(2);
  //map key places the source attribute and value places the target attribute
  refMap2.put("studentGradeName", "teacherGradeName");
  refMap2.put("studentList", "teacherList");


  TeacherGrade teacherGrade = OrikaUtils.convert(studentGrade,TeacherGrade.class,refMap2);
  System.out.println(teacherGrade);
}

The scenario of multiple mapping needs to call OrikaUtils.register() to register the field mapping according to the situation.

Output results:

TeacherGrade(teacherGradeName=master, teacherList=[Teacher(id=1, name=javadaily, emailAddress=jianzh5@163.com), Teacher(id=2, name=JAVA Rizhilu, emailAddress=jianzh5@xxx.com)])

TC8, MyBaits plus page mapping

If you are using the paging component of mybatis, you can convert it like this

public IPage<UserDTO> selectPage(UserDTO userDTO, Integer pageNo, Integer pageSize) {
  Page page = new Page<>(pageNo, pageSize);
  LambdaQueryWrapper<User> query = new LambdaQueryWrapper();
  if (StringUtils.isNotBlank(userDTO.getName())) {
    query.like(User::getKindName,userDTO.getName());
  }
  IPage<User> pageList = page(page,query);
  // Entity conversion SysKind to SysKindDto
  Map<String,String> refMap = new HashMap<>(3);
  refMap.put("kindName","name");
  refMap.put("createBy","createUserName");
  refMap.put("createTime","createDate");
  return pageList.convert(item -> OrikaUtils.convert(item, UserDTO.class, refMap));
}

Summary

In MVC architecture, the functions of object replication and attribute conversion are certainly necessary. These functions can be easily realized by borrowing Orika components. This paper encapsulates the tool class on the basis of Orika, which further simplifies the operation of Orika. I hope it will be helpful to you.

Finally, I am misty Jam, an architect who writes code and an architecture programmer. I look forward to your forwarding and attention. Of course, I can also add my personal wechat jianzh5. Let's talk about technology together!

The source code of the old bird series has been uploaded to GitHub. If necessary, click the business card below, follow the public account and reply to the keyword 0923

Posted by amma on Wed, 22 Sep 2021 22:53:43 -0700