高性能和隐私是大多数成功软件系统的核心。没有人愿意使用一个加载时间荒谬的软件服务——也没有公司希望他们的用户数据在最小的漏洞下暴露出来。这就是为什么DTOs是软件工程师需要理解的一个关键话题。
在构建包含敏感数据(如财务或健康记录)的应用程序时,使用DTOs是有帮助的。当正确使用时,DTOs可以防止敏感字段暴露给客户端。在关键系统中,它们可以通过确保只接受有效且必需的字段来进一步加固安全并减少故障条件。
在本文中,您将了解什么是DTO,为什么它们很重要,以及如何为您的基于Spring的应用程序创建它们的最佳方法。
先决条件
这是一个稍微高级一点的教程。所以为了更好地理解它,你应该对Java概念有深入的了解,比如对象、getter和setter,以及Spring/Spring Boot。你还应该对软件工作的一般原理有扎实的理解。
目录
什么是DTO?
如何为基于Spring的应用程序创建DTO
如何从两个或多个对象创建DTO
结论
什么是DTO?
DTO代表数据传输对象。它是一种软件设计模式,确保在软件系统的不同层之间传输定制化的/简化的数据对象。

图像来源 | Fabio Ribeiro
数据传输的方向是双向的,在软件的不同层之间使用DTO。DTO要么用于将外部客户端/用户的数据传入软件,要么用于构建并携带软件的数据传出。
数据传输对象(DTO)只包含字段数据、构造函数以及必要的getter和setter方法。因此它们是普通的Java对象(POJO)。
您可以在下面的图像中看到双向流动:

图像来源 | Fabio Ribeiro
为什么要使用DTO?
1. 数据隐私
在Spring Boot中,实体类作为创建数据对象的蓝图。这些实体类是带有@Entity注解的类,并映射到数据库表。实体类的实例代表数据库的一行或记录,而实体类中的字段代表数据库的列。
在注册软件服务或产品时,用户可能需要提供敏感和非敏感数据,以便应用程序正常运行。这些数据作为字段存储在实体类中,并最终映射并持久化到数据库中。
当我们需要从数据库中检索数据并通过基于查询的API端点暴露它时——比如说,检索用户记录或实体的查询,Jackson(在基于Spring的应用程序中常用的序列化器依赖)会序列化检索到的用户实体中包含的所有数据字段。现在,想象你有一个User实体,其中包含密码、信用卡详情、出生日期、家庭地址等敏感数据字段,这些数据在用户实体被序列化时你不希望泄露。那么,这就是DTOs发挥作用的地方了。
有了DTO,你可以从数据库中检索完整的实体(包含敏感和不敏感数据),创建一个自定义类(比如UserDTO.java)只包含你认为可以安全暴露的不敏感字段,最后将数据库检索到的实体映射到可以安全暴露的UserDTO对象上。这样,UserDTO就是被序列化并通过API端点暴露的,而不是完整的实体——保持敏感数据的安全。
2. 改进软件性能
数据传输对象(DTO)可以通过减少API调用来提高软件应用程序的性能。使用DTO,您可以在一个API调用中返回来自多个实体的序列化数据。
假设在您的Spring Boot应用程序中,有用户(User)和关注者(Follower)实体,您希望返回用户数据以及他们的关注者。通常情况下,Jackson一次只能序列化一个实体,要么是用户(User),要么是关注者(Follower)。但是通过使用DTO,您可以将这两个实体组合成一个,并最终在单个请求中序列化和返回所有数据,而不是构建两个端点来分别返回用户和关注者数据。
在下一节中,我将向您展示如何使用代码实现为您的Spring Boot项目创建DTOs的各种方法。
如何为基于Spring的应用程序创建DTO
在Spring/Spring Boot中创建DTO有两种主要方法:
1. 创建自定义对象并手动处理映射
这种方法要求您自己处理将现有实体映射/转换到自定义对象(DTO)的过程——也就是说,您需要编写代码来创建DTO并将DTO字段设置为现有实体中存在的值。这对于喜欢细粒度控制的开发者来说很常见,但对于大规模项目来说可能会很繁琐。
按照下面的步骤,从User实体创建一个UserDTO:
步骤1:创建DTO类
创建一个名为UserDTO.java的新文件,并将下面的代码写入其中:
public class UserDTO {
private Long id;
private String firstName;
private String lastName;
private String email;
// No-args Constructor
public UserDTO() {}
// All-args constructor
public UserDTO(Long id, String firstName, String lastName, String email) {
this.id = id;
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
}
// Getters and Setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}}定义的UserDTO类只能包含四个(4)字段:id、firstName、lastName和email。它无法暴露或接收超过此数量的字段。该类还包含用于检索和分配数据到字段的getter和setter方法。
步骤2:在工具类中创建映射器方法
创建一个名为UserMapper.java的新文件,并将以下代码放入其中:
public class UserMapper {
// Convert Entity to DTO
public static UserDTO toDTO(UserEntity user) {
if (user == null) return null;
UserDTO dto = new UserDTO();
dto.setId(user.getId());
dto.setFirstName(user.getFirstName());
dto.setLastName(user.getLastName());
dto.setEmail(user.getEmail());
return dto;
}
// Convert DTO to Entity
public static UserEntity toEntity(UserDTO dto) {
if (dto == null) return null;
UserEntity user = new UserEntity();
user.setFirstName(dto.getFirstName());
user.setLastName(dto.getLastName());
user.setEmail(dto.getEmail());
return user;
}UserMapper类是一个实用类,它将UserEntity映射到DTO,将DTO映射回实体。这就是我之前提到的双向数据传输发挥作用的地方。首先,UserEntity到DTO的方向涉及从数据库检索完整记录,并将其转换为一个精简的对象(去除不必要的信息),然后通过API端点将其序列化并暴露给客户端。
DTO-用户实体方向涉及将对象从客户端作为输入带入系统,但这次是为了限制客户端传递给系统的数据字段数量。这个对象被接收、映射到实体,并保存在系统中。当你不希望客户端访问某些关键字段(这会使你的应用程序变得脆弱)时,这一点很重要。这就是为什么软件工程师总是说,“不要信任用户”。
让我给你看看我们的UserEntity长什么样:
import jakarta.persistence.*;import java.time.LocalDate;@Entity@Table(name = "users")public class UserEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String firstName;
private String lastName;
@Column(unique = true)
private String email;
private String password;
private String phoneNumber;
private String gender;
private LocalDate dateOfBirth;
private String address;
private String city;
private String state;
private String country;
private String profilePictureUrl;
private boolean isVerified;
private LocalDate createdAt;
private LocalDate updatedAt;
// Constructors
public UserEntity() {}
// Getters and Setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getPhoneNumber() {
return phoneNumber;
}
public void setPhoneNumber(String phoneNumber) {
this.phoneNumber = phoneNumber;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
public LocalDate getDateOfBirth() {
return dateOfBirth;
}
public void setDateOfBirth(LocalDate dateOfBirth) {
this.dateOfBirth = dateOfBirth;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
public String getCountry() {
return country;
}
public void setCountry(String country) {
this.country = country;
}
public String getProfilePictureUrl() {
return profilePictureUrl;
}
public void setProfilePictureUrl(String profilePictureUrl) {
this.profilePictureUrl = profilePictureUrl;
}
public boolean isVerified() {
return isVerified;
}
public void setVerified(boolean verified) {
isVerified = verified;
}
public LocalDate getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDate createdAt) {
this.createdAt = createdAt;
}
public LocalDate getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(LocalDate updatedAt) {
this.updatedAt = updatedAt;
}}在上面的代码片段中,你可以看到UserDTO只包含四个(4)字段,这些字段在序列化时是不敏感且安全的。这些字段是id、firstName、lastName和email——与UserEntity不同,后者包含敏感和非敏感字段。因此,不安全的UserEntity映射到安全的UserDTO。通过这样做,UserDTO对象可以被序列化并通过端点返回。现在你可以看到为什么DTO帮助我们防止暴露机密信息。
2. 通过外部库创建自定义对象和处理映射
使用外部库意味着在映射过程中增加了一层抽象。该库为你处理了任务中压力最大的部分,它通常是大规模项目的首选。在本文中,我们使用MapStruct是因为它流行且易于使用。Maven将作为我们的构建工具。
步骤1:将依赖项添加到您的项目中
既然你正在使用Maven作为你的构建工具,打开你的pom.xml文件并添加以下代码:
org.mapstruct mapstruct 1.5.5.Final org.apache.maven.plugins maven-compiler-plugin 3.10.1 org.mapstruct mapstruct-processor 1.5.5.Final
这将在项目构建过程中帮助下载依赖项。
步骤2:定义你的DTO
使用第一种方法的第1步中给出的UserDTO.java文件。
步骤 3:创建 MapStruct 映射器接口
创建一个文件,命名为UserMapper.java,并将下面的代码添加到文件中:
import org.mapstruct.Mapper;import org.mapstruct.factory.Mappers;@Mapper(componentModel = "spring")public interface UserMapper {
UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);
UserDTO toDTO(UserEntity user);
UserEntity toEntity(UserDTO userDTO);}UserMapper接口包含一个INSTANCE字段和两个方法,分别是toDTO和toEntity,它们分别接受UserEntity和UserDTO类型的对象作为参数。这些方法的实现是抽象的,由库为我们处理。
现在你可以在你的Service或Controller中使用mapper方法(toDTO和toEntity)。
如何从两个或多个对象创建DTO
这是使用DTO的最重要方法之一:从多个实体创建DTO,并将它们组合成一个,以便可以在一个API调用或请求中返回。
有很多方法可以应用这种技术并创建复杂的响应DTO,这取决于你的项目需求。你的API响应对象的形式或结构可能与本教程中给出的示例不同——但适用的原则是相同的,即简单地创建单独的DTO并将它们组合成一个DTO,最终作为响应DTO。
下面的示例不是很复杂,但它足以帮助您理解它是如何工作的,以便您可以在创建更复杂的API响应对象时利用这项技术。这个示例将结合医生及其预约的DTOs。
步骤1:创建DTO类
创建一个名为DoctorDto.java的文件,并向其中添加以下代码:
public class DoctorProfileDTO {
private String doctorId;
private String fullName;
private String email;
private String specialization;
// No-args constructor
public DoctorProfileDTO(){
}
// Getter and Setter for doctorId
public String getDoctorId() {
return doctorId;
}
public void setDoctorId(String doctorId) {
this.doctorId = doctorId;
}
// Getter and Setter for fullName
public String getFullName() {
return fullName;
}
public void setFullName(String fullName) {
this.fullName = fullName;
}
// Getter and Setter for email
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
// Getter and Setter for specialization
public String getSpecialization() {
return specialization;
}
public void setSpecialization(String specialization) {
this.specialization = specialization;
}}创建另一个名为AppointmentDto.java的文件,并包含以下代码:
public class AppointmentDTO {
private String appointmentId;
private String appointmentDate;
private String status;
private String patientName;
private String patientEmail;
// No-args constructor
public AppointmentDTO(){
}
// Getter and Setter for appointmentId
public String getAppointmentId() {
return appointmentId;
}
public void setAppointmentId(String appointmentId) {
this.appointmentId = appointmentId;
}
// Getter and Setter for appointmentDate
public String getAppointmentDate() {
return appointmentDate;
}
public void setAppointmentDate(String appointmentDate) {
this.appointmentDate = appointmentDate;
}
// Getter and Setter for status
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
// Getter and Setter for patientName
public String getPatientName() {
return patientName;
}
public void setPatientName(String patientName) {
this.patientName = patientName;
}
// Getter and Setter for patientEmail
public String getPatientEmail() {
return patientEmail;
}
public void setPatientEmail(String patientEmail) {
this.patientEmail = patientEmail;
}}步骤2:创建一个结合两个实体的复合DTO
创建一个名为DoctorWithAppointmentsDTO.java的文件:
import java.util.List;public class DoctorWithAppointmentsDTO {
private DoctorProfileDTO doctorProfile;
private List appointments;
// No-args constructor
public DoctorWithAppointmentsDTO() {
}
// Getter and Setter for doctorProfile
public DoctorProfileDTO getDoctorProfile() {
return doctorProfile;
}
public void setDoctorProfile(DoctorProfileDTO doctorProfile) {
this.doctorProfile = doctorProfile;
}
// Getter and Setter for appointments
public List getAppointments() {
return appointments;
}
public void setAppointments(List appointments) {
this.appointments = appointments;
}} 步骤3:创建一个映射器类
创建一个映射器类 DoctorMapper.java,包含将 DoctorWithAppointmentsDTO 类进行映射的逻辑:
public class MapperClass {
public DoctorWithAppointmentsDTO toDTO(Doctor doctor, List appointments) {
DoctorProfileDTO doctorProfile = new DoctorProfileDTO();
doctorProfile.setDoctorId(doctor.getId());
doctorProfile.setFullName(doctor.getFullName());
doctorProfile.setEmail(doctor.getEmail());
doctorProfile.setSpecialization(doctor.getSpecialization());
List appointmentDTOs = appointments.stream().map(appt -> {
AppointmentDTO dto = new AppointmentDTO();
dto.setAppointmentId(appt.getId());
dto.setAppointmentDate(appt.getDate().toString());
dto.setStatus(appt.getStatus().name());
dto.setPatientName(appt.getPatient().getName());
dto.setPatientEmail(appt.getPatient().getEmail());
return dto;
}).toList();
DoctorWithAppointmentsDTO doctorWithAppointment = new DoctorWithAppointmentsDTO();
doctorWithAppointment.setDoctorProfile(doctorProfile);
doctorWithAppointment.setAppointments(appointmentDTOs);
return doctorWithAppointment;
}} 从上面的例子中,您可以看到在创建复合DTO(DoctorWithAppointmentsDTO)之前,创建了两个单独的DTO(AppointmentDTO和DoctorProfileDTO)。复合DTO类(DoctorWithAppointmentsDTO)包含字段,用于存储Appointment和DoctorProfile DTO实例。映射器类接受Doctor和一组Appointment实体作为参数,分别将它们映射到DoctorProfileDTO和AppointmentDTO。最后,使用从实体映射得到的DTO对象来设置复合DTO类的字段。
DoctorWithAppointmentsDTO在通过端点序列化并返回时,应该给你一个这样的输出:
{
"doctorProfile": {
"doctorId": "abc123",
"fullName": "Dr. Susan Emeka",
"email": "suzan.emeka@example.com",
"specialisation": "Cardiology"
},
"appointments": [
{
"appointmentId": "appt001",
"appointmentDate": "2025-07-10T09:00:00",
"status": "CONFIRMED",
"patientName": "James Agaji",
"patientEmail": "james.agaji@example.com"
},
{
"appointmentId": "appt002",
"appointmentDate": "2025-08-12T07:05:08",
"status": "CONFIRMED",
"patientName": "Jane Augustine",
"patientEmail": "jane.augustine@example.com"
}
]}结论
如果你是一名关心隐私和效率的软件工程师,那么在你的应用程序中使用DTO是必须的。
在本文中,您已经了解了DTO是什么以及创建和使用它们的主要方法。花些时间阅读本文中给出的代码片段,并练习使用它们,直到您能够自己实施它们为止。感谢您的阅读。