DTO、DO、VO这些东西看着复杂,核心就俩字:省事。
先说说DTO、DO、VO到底解决啥问题
刚开始写代码,总是一个实体类用到底:查数据库用它,接口入参用它,返回给前端还⽤它。直到有一次,产品突然说要大改接口返回字段,改着改着发现不对劲——数据库实体(比如UserDO)里有密码、创建时间这些敏感字段,之前直接返回给前端了!更坑的是,因为实体类被Service、Controller、前端共用,改一个字段得全链路检查,差点没改崩。
DTO、DO、VO分层,本质是给不同层划清界限:
DO(Data Object):和数据库表一一对应,只在Dao层和Service层之间用,里面全是数据库字段(比如user_id、password、create_time)。
DTO(Data Transfer Object):前端传给后端的参数载体,比如用户登录时传的username和password,只包含接口需要的字段,多余的一概不要。
VO(View Object):后端返回给前端的结果,会根据前端需求“裁剪”DO里的字段,比如隐藏密码,只返回username、nickname这些前端需要展示的。
这么一分层,好处立马就显出来了。就像我之前遇到的产品大改:Service层逻辑全重写了,查的表都换了,但因为Controller层只认DTO和VO,我只要保证入参DTO和返回VO的格式不变,前端完全不用改,Swagger文档也没动——这就是解耦的威力。
下面我将为你提供一个集成了 MyBatis-Plus 代码生成器、MapStruct 和 SpringDoc(OpenAPI/Swagger)的完整方案,使用模板配置方式实现。
<!-- MyBatis-Plus 相关依赖 --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-generator</artifactId> <version>3.5.3.1</version> </dependency> <!-- 模板引擎 --> <dependency> <groupId>org.apache.velocity</groupId> <artifactId>velocity-engine-core</artifactId> <version>2.3</version> </dependency> <!-- MapStruct --> <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct</artifactId> <version>1.5.3.Final</version> </dependency> <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version>1.5.3.Final</version> <scope>provided</scope> </dependency> <!-- SpringDoc (OpenAPI/Swagger) --> <dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> <version>2.1.0</version> </dependency> <!-- 数据库驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> <scope>runtime</scope> </dependency> <!-- Lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.24</version> <optional>true</optional> </dependency>
import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.info.Contact; import io.swagger.v3.oas.models.info.Info; import io.swagger.v3.oas.models.info.License; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class SpringDocConfig { @Bean public OpenAPI customOpenAPI() { return new OpenAPI() .info(new Info() .title("API文档") .version("1.0.0") .description("系统API接口文档") .contact(new Contact().name("开发团队").email("dev@example.com")) .license(new License().name("Apache 2.0").url("https://www.apache.org/licenses/LICENSE-2.0"))); } }
import com.baomidou.mybatisplus.generator.FastAutoGenerator; import com.baomidou.mybatisplus.generator.config.OutputFile; import com.baomidou.mybatisplus.generator.engine.VelocityTemplateEngine; import java.util.ArrayList; import java.util.Collections; import java.util.List; public class CodeGenerator { // 数据库连接信息 private static final String URL = "jdbc:mysql://localhost:3306/your_database?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai"; private static final String USERNAME = "root"; private static final String PASSWORD = "password"; // 父包名 private static final String PARENT_PACKAGE = "com.example.demo"; // 模块名 private static final String MODULE_NAME = "system"; // 作者 private static final String AUTHOR = "your name"; // 需要生成的表名 private static final List<String> TABLE_NAMES = Collections.singletonList("sys_user"); // XML文件输出路径 private static final String XML_OUTPUT_DIR = System.getProperty("user.dir") + "/src/main/resources/mapper/" + MODULE_NAME; public static void main(String[] args) { generateCode(); } public static void generateCode() { FastAutoGenerator.create(URL, USERNAME, PASSWORD) // 全局配置 .globalConfig(builder -> { builder.author(AUTHOR) // 作者 .outputDir(System.getProperty("user.dir") + "/src/main/java") // 输出路径 .commentDate("yyyy-MM-dd") // 注释日期 .disableOpenDir() // 生成后不打开文件夹 .fileOverride(); // 覆盖已生成文件 }) // 包配置 .packageConfig(builder -> { builder.parent(PARENT_PACKAGE) // 父包名 .moduleName(MODULE_NAME) // 模块名 .entity("entity") // 实体类包名 .service("service") // service包名 .serviceImpl("service.impl") // serviceImpl包名 .mapper("mapper") // mapper包名 .xml("mapper.xml") // mapper.xml包名 .controller("controller") // controller包名 .other("convert") // MapStruct转换器包名 .pathInfo(Collections.singletonMap(OutputFile.xml, XML_OUTPUT_DIR)); // XML路径 }) // 策略配置 .strategyConfig(builder -> { builder.addInclude(TABLE_NAMES) // 生成的表 .addTablePrefix("sys_") // 表前缀,生成实体时去掉前缀 // 实体类策略 .entityBuilder() .enableLombok() // 开启lombok .enableChainModel() // 开启链式模式 .enableTableFieldAnnotation() // 开启字段注解 // Controller策略 .controllerBuilder() .enableRestStyle() // 开启restful风格 .enableHyphenStyle() // 开启连字符风格 // Service策略 .serviceBuilder() .formatServiceFileName("%sService") .formatServiceImplFileName("%sServiceImpl"); }) // 自定义模板配置,生成DTO、VO、MapStruct转换器 .templateEngine(new VelocityTemplateEngine()) .injectionConfig(consumer -> { List<com.baomidou.mybatisplus.generator.config.po.TableInfo> tableInfos = consumer.getInjectionConfig().getTableInfoList(); List<com.baomidou.mybatisplus.generator.config.FileOutConfig> fileOutConfigs = new ArrayList<>(); // 生成DTO fileOutConfigs.add(new com.baomidou.mybatisplus.generator.config.FileOutConfig("/templates/dto.java.vm") { @Override public String outputFile(com.baomidou.mybatisplus.generator.config.po.TableInfo tableInfo) { return System.getProperty("user.dir") + "/src/main/java/" + PARENT_PACKAGE.replace(".", "/") + "/" + MODULE_NAME + "/dto/" + tableInfo.getEntityName() + "DTO.java"; } }); // 生成VO fileOutConfigs.add(new com.baomidou.mybatisplus.generator.config.FileOutConfig("/templates/vo.java.vm") { @Override public String outputFile(com.baomidou.mybatisplus.generator.config.po.TableInfo tableInfo) { return System.getProperty("user.dir") + "/src/main/java/" + PARENT_PACKAGE.replace(".", "/") + "/" + MODULE_NAME + "/vo/" + tableInfo.getEntityName() + "VO.java"; } }); // 生成MapStruct转换器 fileOutConfigs.add(new com.baomidou.mybatisplus.generator.config.FileOutConfig("/templates/convert.java.vm") { @Override public String outputFile(com.baomidou.mybatisplus.generator.config.po.TableInfo tableInfo) { return System.getProperty("user.dir") + "/src/main/java/" + PARENT_PACKAGE.replace(".", "/") + "/" + MODULE_NAME + "/convert/" + tableInfo.getEntityName() + "Convert.java"; } }); consumer.getInjectionConfig().setFileOutConfigList(fileOutConfigs); }) // 模板配置 .templateConfig(builder -> { builder.mapperXml("/templates/mapper.xml.vm") .entity("/templates/entity.java.vm") .service("/templates/service.java.vm") .serviceImpl("/templates/serviceImpl.java.vm") .mapper("/templates/mapper.java.vm") .controller("/templates/controller.java.vm"); }) .execute(); } }
在src/main/resources/templates目录下创建以下模板文件:
package ${package.Entity}; #foreach($pkg in ${table.importPackages}) import ${pkg}; #end import lombok.Data; import io.swagger.v3.oas.annotations.media.Schema; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableName; /** * ${table.comment!} * * @author ${author} * @date ${date} */ @Data @TableName("${table.name}") @Schema(description = "${table.comment!}实体类") public class ${entity} { #foreach($field in ${table.fields}) #if(${field.keyFlag}) #set($keyPropertyName = ${field.propertyName}) #end @TableId(value = "${field.name}", type = ${field.idType}) @Schema(description = "${field.comment!}") private ${field.propertyType} ${field.propertyName}; #end #if(${entityLombokModel}) #else #foreach($field in ${table.fields}) public ${field.propertyType} get${field.capitalName}() { return ${field.propertyName}; } public void set${field.capitalName}(${field.propertyType} ${field.propertyName}) { this.${field.propertyName} = ${field.propertyName}; } #end #end }
package ${package.Parent}.${moduleName}.dto; import lombok.Data; import java.io.Serializable; import io.swagger.v3.oas.annotations.media.Schema; #foreach($field in $table.fields) import ${field.type}; #end /** * ${table.comment!}数据传输对象 * * @author ${author} * @date ${date} */ @Data @Schema(description = "${table.comment!}数据传输对象") public class ${table.entityName}DTO implements Serializable { private static final long serialVersionUID = 1L; #foreach($field in $table.fields) @Schema(description = "${field.comment!}") private ${field.propertyType} ${field.propertyName}; #end }
package ${package.Parent}.${moduleName}.vo; import lombok.Data; import java.io.Serializable; import io.swagger.v3.oas.annotations.media.Schema; #foreach($field in $table.fields) import ${field.type}; #end /** * ${table.comment!}视图对象 * * @author ${author} * @date ${date} */ @Data @Schema(description = "${table.comment!}视图对象") public class ${table.entityName}VO implements Serializable { private static final long serialVersionUID = 1L; #foreach($field in $table.fields) @Schema(description = "${field.comment!}") private ${field.propertyType} ${field.propertyName}; #end }
package ${package.Controller}; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import java.util.List; import ${package.Service}.${table.serviceName}; import ${package.Entity}.${entity}; import ${package.Parent}.${moduleName}.vo.${table.entityName}VO; import ${package.Parent}.${moduleName}.dto.${table.entityName}DTO; import ${package.Parent}.${moduleName}.convert.${table.entityName}Convert; /** * ${table.comment!}控制器 * * @author ${author} * @date ${date} */ @RestController @RequestMapping("/${moduleName}/${table.entityPath}") @Tag(name = "${table.comment!}接口", description = "${table.comment!}相关操作") public class ${table.controllerName} { @Resource private ${table.serviceName} ${table.entityName?uncapitalize}Service; /** * 查询详情 */ @GetMapping("/{id}") @Operation(summary = "查询详情", description = "根据ID查询${table.comment!}详情") public ${table.entityName}VO getById( @Parameter(description = "主键ID", required = true) @PathVariable Long id) { ${entity} entity = ${table.entityName?uncapitalize}Service.getById(id); return ${table.entityName}Convert.INSTANCE.toVO(entity); } /** * 列表查询 */ @GetMapping("/list") @Operation(summary = "列表查询", description = "查询${table.comment!}列表") public List<${table.entityName}VO> list() { List<${entity}> entityList = ${table.entityName?uncapitalize}Service.list(); return ${table.entityName}Convert.INSTANCE.toVOList(entityList); } /** * 新增 */ @PostMapping @Operation(summary = "新增", description = "新增${table.comment!}") public boolean save( @Parameter(description = "${table.comment!}信息", required = true) @RequestBody ${table.entityName}DTO dto) { ${entity} entity = ${table.entityName}Convert.INSTANCE.toEntity(dto); return ${table.entityName?uncapitalize}Service.save(entity); } /** * 修改 */ @PutMapping @Operation(summary = "修改", description = "修改${table.comment!}") public boolean updateById( @Parameter(description = "${table.comment!}信息", required = true) @RequestBody ${table.entityName}DTO dto) { ${entity} entity = ${table.entityName}Convert.INSTANCE.toEntity(dto); return ${table.entityName?uncapitalize}Service.updateById(entity); } /** * 删除 */ @DeleteMapping("/{id}") @Operation(summary = "删除", description = "根据ID删除${table.comment!}") public boolean removeById( @Parameter(description = "主键ID", required = true) @PathVariable Long id) { return ${table.entityName?uncapitalize}Service.removeById(id); } }
package ${package.Parent}.${moduleName}.convert; import org.mapstruct.Mapper; import org.mapstruct.factory.Mappers; import ${package.Entity}.${table.entityName}; import ${package.Parent}.${moduleName}.dto.${table.entityName}DTO; import ${package.Parent}.${moduleName}.vo.${table.entityName}VO; import java.util.List; /** * ${table.entityName}对象转换器 * * @author ${author} * @date ${date} */ @Mapper public interface ${table.entityName}Convert { ${table.entityName}Convert INSTANCE = Mappers.getMapper(${table.entityName}Convert.class); /** * DO转DTO */ ${table.entityName}DTO toDTO(${table.entityName} entity); /** * DTO转DO */ ${table.entityName} toEntity(${table.entityName}DTO dto); /** * DO转VO */ ${table.entityName}VO toVO(${table.entityName} entity); /** * DO列表转DTO列表 */ List<${table.entityName}DTO> toDTOList(List<${table.entityName}> entityList); /** * DO列表转VO列表 */ List<${table.entityName}VO> toVOList(List<${table.entityName}> entityList); }
修改CodeGenerator中的数据库连接信息、包名等配置 运行CodeGenerator的main方法,生成所有代码 启动项目后,访问 SpringDoc 的 Swagger UI 界面:http://localhost:8080/swagger-ui/index.html
生成的代码将包含:
带有@Schema注解的实体类、DTO 和 VO
带有@Tag、@Operation、@Parameter等注解的 Controller
自动生成的 MapStruct 转换器
完整的 MyBatis-Plus 服务层和数据访问层代码
这种集成方式实现了:
数据库表到实体类的自动映射
DO、DTO、VO 之间的类型安全转换
API 文档的自动生成和可视化
标准 CRUD 接口的快速开发
通过一次配置,即可生成符合规范的代码架构,极大提高开发效率。
本文作者:Allen Tang
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!