Java Spring中如何为各REST端点定制DTO且避免大量DTO类?
Java Spring REST接口的DTO优化方案探讨
背景
我基于Java & Spring开发了大量REST端点,示例如下:
GET /cars?=> 获取所有车辆GET /cars/<id>=> 获取单辆车辆PATCH /car/<id>=> 更新单辆车辆PATCH /cars-bulk=> 批量更新车辆POST /cars=> 创建单辆车辆POST /cars-bulk=> 批量创建车辆
现有CarDTO类:
public class CarDTO { private String id; private String make; private String model; private String owner; private boolean isAvailable; }
以及CarService接口定义:
public interface CarService { CarDTO getCarById(String id); List<CarDTO> getCars(CarDTO filter); CarDTO updateCar(CarDTO car); CarDTO createCar(CarDTO car); List<BulkUpdateResult> updateCars(List<CarDTO> car); List<BulkUpdateResult> createCars(List<CarDTO> car); }
当前问题
当前DTO在各服务方法间共享,导致接口调用者不清楚每个方法需设置哪些字段:
getCarById和getCars返回除isAvailable外的所有字段updateCar/updateCars仅允许更新owner字段createCar/createCars可设置除id(由服务生成)外的所有字段
虽然可以用校验器和API文档解决,但我希望调用签名自解释,不包含无需生产或消费的字段。
尝试过的方案
- 独立DTO类:为每个端点创建独立DTO(如
CarGetDTO、CarUpdateDTO、CarCreateDTO),但在大型代码库中会导致复杂度提升、代码重复。 - 多态继承:通过抽象基类扩展,但会增加代码复杂度,降低可读性,还存在字段重复问题:
public abstract class CarDTO { private String owner; } public class CarCreateDTO extends CarDTO { private String make; private String model; } public class CarGetDTO extends CarCreateDTO { private String id; // 重复代码 } public class CarUpdateDTO extends CarCreateDTO { private String id; // 重复代码 private boolean isAvailable; }
提问
是否存在更优、可扩展的定制DTO模式,无需逐个定义参数和返回类型?比如通过代码生成实现?
可行的优化方案
1. 基于注解的字段分组 + 代码生成(推荐)
用字段分组注解标记不同场景下的可用字段,配合代码生成工具自动生成对应场景的DTO,既保证调用签名自解释,又避免重复代码。
步骤:
- 定义场景注解:
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface Create {} @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface Update {} @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface Get {}
- 在基础
CarDTO上标记字段所属场景:
public class CarDTO { @Get private String id; @Create @Get private String make; @Create @Get private String model; @Create @Update @Get private String owner; // 仅内部使用,不对外暴露 private boolean isAvailable; }
- 用注解处理器(可基于MapStruct扩展或自定义)自动生成场景DTO:
CarCreateDTO包含make、model、owner字段CarUpdateDTO仅包含owner字段CarGetDTO包含id、make、model、owner字段
- 服务层直接使用生成的DTO:
public interface CarService { CarGetDTO getCarById(String id); List<CarGetDTO> getCars(CarGetDTO filter); CarGetDTO updateCar(CarUpdateDTO car); CarGetDTO createCar(CarCreateDTO car); List<BulkUpdateResult> updateCars(List<CarUpdateDTO> car); List<BulkUpdateResult> createCars(List<CarCreateDTO> car); }
这种方式只需维护基础DTO和注解,生成代码自动同步字段变化,彻底避免重复。
2. 使用@JsonView(快速解决,适合小型项目)
无需额外代码生成,用Spring的@JsonView控制序列化/反序列化字段,配合静态工厂方法强化调用签名的自解释性:
- 定义视图类:
public class Views { public interface Create {} public interface Update {} public interface Get extends Create {} }
- 在
CarDTO上标记字段对应视图:
public class CarDTO { @JsonView(Views.Get.class) private String id; @JsonView(Views.Create.class) private String make; @JsonView(Views.Create.class) private String model; @JsonView({Views.Create.class, Views.Update.class, Views.Get.class}) private String owner; private boolean isAvailable; }
- 服务层指定视图,并提供静态工厂方法:
public interface CarService { @JsonView(Views.Get.class) CarDTO getCarById(String id); @JsonView(Views.Get.class) List<CarDTO> getCars(@JsonView(Views.Get.class) CarDTO filter); @JsonView(Views.Get.class) CarDTO updateCar(@JsonView(Views.Update.class) CarDTO car); @JsonView(Views.Get.class) CarDTO createCar(@JsonView(Views.Create.class) CarDTO car); }
为场景添加静态构造方法,让调用方明确字段要求:
public class CarDTO { // ... 原有字段 public static CarDTO forCreate(String make, String model, String owner) { CarDTO dto = new CarDTO(); dto.make = make; dto.model = model; dto.owner = owner; return dto; } public static CarDTO forUpdate(String owner) { CarDTO dto = new CarDTO(); dto.owner = owner; return dto; } }
这种方案无需额外代码,适合小型项目,但自解释性略逊于独立DTO。
3. Java Record + 映射工具(简洁方案)
利用Java 16+的Record定义轻量不可变DTO,配合MapStruct自动完成与基础DTO的映射,代码量少且调用签名清晰:
- 定义场景Record:
public record CarCreateDTO(String make, String model, String owner) {} public record CarUpdateDTO(String owner) {} public record CarGetDTO(String id, String make, String model, String owner) {}
- 配置MapStruct映射器:
@Mapper(componentModel = "spring") public interface CarMapper { CarMapper INSTANCE = Mappers.getMapper(CarMapper.class); CarDTO toCarDTO(CarCreateDTO createDTO); CarDTO toCarDTO(CarUpdateDTO updateDTO); CarGetDTO toCarGetDTO(CarDTO carDTO); }
- 服务层使用Record作为参数/返回值:
public interface CarService { CarGetDTO getCarById(String id); List<CarGetDTO> getCars(CarGetDTO filter); CarGetDTO updateCar(String id, CarUpdateDTO car); CarGetDTO createCar(CarCreateDTO car); // 批量方法同理 }
Record简洁不可变,MapStruct编译时校验映射正确性,字段变化时容易维护,唯一缺点是需手动同步Record字段。
内容的提问来源于stack exchange,提问作者mitchkman
相关产品推荐
相关产品推荐

