杰瑞科技汇

Java Bean 如何高效转换到另一个 Bean?

Of course! The term "Java Bean to Bean" mapping is a very common task in Java development. It refers to the process of copying data from one object (the source bean) to another object (the destination bean) when they have similar but not necessarily identical structures.

Java Bean 如何高效转换到另一个 Bean?-图1
(图片来源网络,侵删)

Let's break down the concept, explore the manual way, and then dive into the best practices using popular libraries.

Why Do We Need Bean-to-Bean Mapping?

In real-world applications, you often work with different representations of the same data:

  1. Separation of Concerns: Your database entity (e.g., JpaUser) should be separate from your data transfer object (DTO) for a REST API (e.g., UserDto). This prevents your database schema from directly exposing your API structure and vice-versa.
  2. Security: You might want to expose only a subset of an entity's fields in a DTO (e.g., not the password hash).
  3. API Stability: You can change your internal database model without breaking your public API by keeping the DTO stable.
  4. Different Data Shapes: The data needed for a form might be different from the data needed to display a summary.

Scenario

Let's use a consistent example throughout. We have a User entity from a database and a UserDto to be sent to a client.

Source Bean: User.java (Entity)

Java Bean 如何高效转换到另一个 Bean?-图2
(图片来源网络,侵删)
import java.time.LocalDateTime;
public class User {
    private Long id;
    private String username;
    private String passwordHash; // We don't want to expose this
    private String email;
    private LocalDateTime createdAt;
    // Constructors, Getters, and Setters (omitted for brevity, but essential for a JavaBean)
    public User() {}
    public User(Long id, String username, String passwordHash, String email, LocalDateTime createdAt) {
        this.id = id;
        this.username = username;
        this.passwordHash = passwordHash;
        this.email = email;
        this.createdAt = createdAt;
    }
    // Getters and Setters...
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getUsername() { return username; }
    public void setUsername(String username) { this.username = username; }
    public String getPasswordHash() { return passwordHash; }
    public void setPasswordHash(String passwordHash) { this.passwordHash = passwordHash; }
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
    public LocalDateTime getCreatedAt() { return createdAt; }
    public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
}

Destination Bean: UserDto.java (Data Transfer Object)

// We'll use a String for the date for simplicity in JSON serialization
public class UserDto {
    private Long id;
    private String username;
    private String email;
    private String creationDate; // Different field name and type
    // Constructors, Getters, and Setters...
    public UserDto() {}
    public UserDto(Long id, String username, String email, String creationDate) {
        this.id = id;
        this.username = username;
        this.email = email;
        this.creationDate = creationDate;
    }
    // Getters and Setters...
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getUsername() { return username; }
    public void setUsername(String username) { this.username = username; }
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
    public String getCreationDate() { return creationDate; }
    public void setCreationDate(String creationDate) { this.creationDate = creationDate; }
}

Method 1: Manual Mapping (The "Old School" Way)

This involves writing the mapping logic by hand. It's straightforward for one-off, simple mappings.

Code:

public class ManualMapper {
    public UserDto mapToDto(User user) {
        if (user == null) {
            return null;
        }
        UserDto dto = new UserDto();
        dto.setId(user.getId());
        dto.setUsername(user.getUsername());
        dto.setEmail(user.getEmail());
        // Type conversion and renaming logic
        if (user.getCreatedAt() != null) {
            dto.setCreationDate(user.getCreatedAt().toString());
        }
        return dto;
    }
    public User mapToEntity(UserDto dto) {
        if (dto == null) {
            return null;
        }
        User user = new User();
        user.setId(dto.getId());
        user.setUsername(dto.getUsername());
        user.setEmail(dto.getEmail());
        // Note: passwordHash would need to be set separately, e.g., during registration.
        return user;
    }
}

Pros:

  • No Dependencies: No external libraries needed.
  • Full Control: You have complete control over the mapping logic.

Cons:

  • Verbose & Repetitive: A lot of boilerplate code.
  • Error-Prone: It's easy to forget to map a new field.
  • Hard to Maintain: As the number of beans grows, the number of mapper classes explodes. This is known as the "Mapper Explosion" problem.

Method 2: Using Mapping Libraries (The Modern Approach)

This is the recommended approach for any non-trivial application. These libraries automate the process, reducing boilerplate and errors.

A. MapStruct

MapStruct is a code generator that creates type-safe and performant bean mappers at compile time. This is often the top choice for production applications.

How it works: You define a mapper interface with @Mapping annotations. During the build, MapStruct generates an implementation class for you.

Setup (Maven):

<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct</artifactId>
    <version>1.5.5.Final</version> <!-- Use the latest version -->
</dependency>
<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct-processor</artifactId>
    <version>1.5.5.Final</version>
    <scope>provided</scope>
</dependency>

Code:

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
@Mapper // MapStruct will generate an implementation for this interface
public interface UserMapper {
    // Singleton instance of the mapper
    UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);
    // Define the mapping for the field with a different name
    @Mapping(target = "creationDate", source = "createdAt")
    // We can ignore a field from the source
    @Mapping(target = "passwordHash", ignore = true)
    UserDto userToUserDto(User user);
    // The reverse mapping is often possible too
    @Mapping(target = "createdAt", source = "creationDate", dateFormat = "yyyy-MM-dd HH:mm:ss")
    @Mapping(target = "passwordHash", ignore = true) // Or default it
    User userDtoToUser(UserDto userDto);
}

Usage:

User user = new User(1L, "john_doe", "a-very-secure-hash", "john.doe@example.com", LocalDateTime.now());
// Use the generated mapper instance
UserDto userDto = UserMapper.INSTANCE.userToUserDto(user);
System.out.println(userDto);
// Output: UserDto{id=1, username='john_doe', email='john.doe@example.com', creationDate='...'}

Pros:

  • Type-Safe: Errors are caught at compile-time.
  • High Performance: No reflection; it's just plain Java method calls.
  • Readability: The mapping definition is clean and declarative in an interface.
  • IDE Support: Excellent autocompletion and refactoring support.

Cons:

  • Build-time Dependency: Requires an annotation processor (e.g., in Maven or Gradle).

B. ModelMapper

ModelMapper is a runtime library that uses reflection to map objects. It's very flexible and easy to set up.

Setup (Maven):

<dependency>
    <groupId>org.modelmapper</groupId>
    <artifactId>modelmapper</artifactId>
    <version>3.2.0</version> <!-- Use the latest version -->
</dependency>

Code:

import org.modelmapper.ModelMapper;
import org.modelmapper.convention.MatchingStrategies;
public class ModelMapperExample {
    public static void main(String[] args) {
        ModelMapper modelMapper = new ModelMapper();
        // Set a stricter matching strategy to avoid unintended mappings
        modelMapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT);
        User user = new User(1L, "jane_doe", "another-secure-hash", "jane.doe@example.com", LocalDateTime.now());
        // Perform the mapping
        UserDto userDto = modelMapper.map(user, UserDto.class);
        System.out.println(userDto);
        // Output: UserDto{id=1, username='jane_doe', email='jane.doe@example.com', creationDate='...'}
    }
}

For more complex mappings (like field renaming), you can configure the mapper:

// To handle field renaming
modelMapper.typeMap(User.class, UserDto.class)
           .addMapping(User::getCreatedAt, UserDto::setCreationDate);

Pros:

  • Easy Setup: No build-time plugins needed.
  • Flexible: Handles complex nested mappings and custom logic well.
  • Convention-based: Can automatically map fields with the same name.

Cons:

  • Runtime Performance: Slower than MapStruct due to reflection.
  • Less Safe: Errors can only be caught at runtime.
  • Debugging: Can be harder to debug as the mapping logic happens at runtime.

Summary and Recommendation

Feature Manual Mapping MapStruct (Compile-Time) ModelMapper (Runtime)
Performance Excellent Excellent (Best) Good (but slower)
Type Safety High (if you write tests) Excellent (Compile-time) Low (Runtime errors)
Boilerplate Very High Very Low Low
Setup None Requires Annotation Processor Simple Dependency
Complexity Low for simple, high for complex Low to Medium Medium
Debugging Easy Easy (generated code) Harder

Final Recommendation:

  • For new projects, especially Spring Boot applications, use MapStruct. It provides the best combination of performance, safety, and maintainability. It's the industry standard for high-performance applications.
  • For small projects, prototypes, or situations where adding a build-time processor is difficult, ModelMapper is a good, flexible alternative.
  • Avoid manual mapping for anything beyond the simplest of cases or when you have only a couple of beans to map. The maintenance cost is not worth it.
分享:
扫描分享到社交APP
上一篇
下一篇