摘要:在Java企业级开发中单表CRUD是最常见也是最枯燥的代码。每新增一个业务实体就要写一套几乎相同的Controller、Service——结构雷同却又不得不写。《鹿鲸项目管理工具》仅用三个文件——IOneEntityService接口、OneEntityServiceImpl抽象类、OneEntityController控制器基类——构建了一套泛型模板让所有表新增改的业务实体共享同一套CRUD骨架新增简单实体只需声明泛型Controller零代码。本文深入解析这三个文件的设计细节与背后的工程思考。一、三个文件的全貌先看三个文件各自的角色定位文件层次泛型数量核心职责IOneEntityServiceService接口5个定义DTO进VO出的业务契约OneEntityServiceImplService实现6个继承MyBatis-Flex实现通用逻辑OneEntityControllerController5个定义8个RESTful端点它们通过相同的泛型参数串联成一条完整的CRUD链路从HTTP请求到数据库操作类型安全一以贯之。二、逐文件深度解析2.1 IOneEntityServiceService接口——定义业务契约public interface IOneEntityServicePO, VO, SearchDTO, AddDTO, UpdateDTO extends IServicePO { VO getById(String id); boolean insertInfo(AddDTO addDTO); boolean updateInfo(UpdateDTO updateDTO); ListVO getList(SearchDTO dto); PageVO getPage(SearchDTO dto, PageDTO pageDTO); int getNextSortNo(String sortExpress); }5个泛型参数每个都对应一种对象类型泛型角色说明PO持久化对象数据库表映射Service内部使用VO视图对象返回给前端的展示数据SearchDTO查询参数列表/分页查询的筛选条件AddDTO新增参数前端提交的新增数据UpdateDTO修改参数前端提交的修改数据关键设计决策所有方法入参都是DTO所有返回值都是VOPO从不出现在接口签名中。这意味着PO被完全封装在Service内部。Controller层拿不到PO前端更拿不到PO。从接口契约层面就杜绝了数据库结构泄露的安全隐患。继承IServicePO这是 MyBatis-Flex 提供的基础服务接口自带save()、updateById()、removeById()、removeByIds()、list()、getById()等方法。继承它之后IOneEntityService不需要重复定义删除方法——Controller直接调用removeById和removeByIds即可。6个方法分工方法入参返回值用途getByIdString idVO主键查询单条insertInfoAddDTOboolean新增updateInfoUpdateDTOboolean修改getListSearchDTOListVO列表查询getPageSearchDTO PageDTOPageVO分页查询getNextSortNoStringint获取下一个排序号注意没有delete方法。删除操作直接使用IService继承来的removeById和removeByIds因为删除只需要ID不需要额外定义。2.2 OneEntityServiceImpl抽象实现——通用逻辑上提public abstract class OneEntityServiceImplM extends BaseMapperPO, PO, VO, SearchDTO, AddDTO, UpdateDTO extends ServiceImplM, PO implements IOneEntityServicePO, VO, SearchDTO, AddDTO, UpdateDTO { Override public int getNextSortNo(String sortExpress) { QueryWrapper queryWrapper new QueryWrapper(); queryWrapper.select(QueryMethods.max(”sort_no”).as(”sort_no”)); PO t this.getMapper().selectOneByQuery(queryWrapper); JSONObject jsonObject JSONObject.parseObject(JSONObject.toJSONString(t)); return jsonObject.getInteger(”sortNo”); } }比接口多了一个泛型参数MMapper类型约束为M extends BaseMapperPO。这个参数用于继承 MyBatis-Flex 的ServiceImplM, PO从而自动获得Mapper注入和基础数据库操作能力。为什么只实现了getNextSortNo一个方法这是整个设计最精妙的地方。6个接口方法中方法能否通用实现原因getNextSortNo✅ 可以所有实体都是查max(sort_no)逻辑完全相同getById❌ 不能每个实体需要查询的字段不同如User要排除密码insertInfo❌ 不能每个实体的业务逻辑不同如User要设置默认密码updateInfo❌ 不能每个实体的修改逻辑不同getList❌ 不能每个实体的搜索条件不同getPage❌ 不能同getListgetNextSortNo的实现逻辑查询当前表sort_no字段的最大值。这个操作对所有继承MasterPO含sortNo字段的实体都完全相同所以放在基类实现一次45个子类共享。声明为abstract class强制子类必须实现其余5个方法。编译期就能发现遗漏而不是等到运行时抛出异常。继承ServiceImplM, PO的收益——子类可以直接调用this.save(po); // 新增 this.updateById(po); // 修改 this.removeById(id); // 删除 this.removeByIds(idList); // 批量删除 queryChain().select(...) // 链式查询2.3 OneEntityController控制器基类——8个REST端点public abstract class OneEntityControllerPO, VO, SearchDTO, AddDTO extends InsertDTO, ModifyDTO extends UpdateDTO { Autowired protected IOneEntityServicePO, VO, SearchDTO, AddDTO, ModifyDTO oneEntityService; GetMapping(”/{id}”) public VO getById(PathVariable String id) { ... } PutMapping public VO updateInfo(RequestBody Validated ModifyDTO dto) { ... } PostMapping public VO insertInfo(RequestBody Validated AddDTO dto) { ... } DeleteMapping(”/{id}”) public boolean deleteById(PathVariable String id) { ... } DeleteMapping(”/list”) public boolean deleteByIdList(RequestBody ListString idList) { ... } GetMapping(”/list”) public ListVO getList(Valid SearchDTO dto) { ... } GetMapping(”/page/list”) public PageVO getPage(Valid SearchDTO dto, PageDTO pageDTO) { ... } GetMapping(”/next/sort”) public int getNextSort(RequestParam String sortExpress) { ... } }泛型参数有边界约束AddDTO extends InsertDTO, ModifyDTO extends UpdateDTO这两个约束不是摆设它们确保了AddDTO必须继承InsertDTO从而拥有getId()方法自动生成NanoIDModifyDTO必须继承UpdateDTO从而拥有id字段修改必须指定ID8个REST端点全景HTTP方法路径方法名返回值说明GET/{id}getByIdVO查询单条POST/insertInfoVO新增返回完整数据PUT/updateInfoVO修改返回更新后数据DELETE/{id}deleteByIdboolean删除单条DELETE/listdeleteByIdListboolean批量删除GET/listgetListListVO列表查询GET/page/listgetPagePageVO分页查询GET/next/sortgetNextSortint获取下一个排序号三个值得注意的设计细节细节一新增和修改都返回VOPostMapping public VO insertInfo(RequestBody Validated AddDTO dto) { this.oneEntityService.insertInfo(dto); // 先保存 return this.oneEntityService.getById(dto.getId()); // 再查询返回 }不是返回boolean而是返回保存后的完整VO。这样前端新增成功后直接拿到完整数据含系统自动填充的创建时间、ID等不需要再发一次查询请求。dto.getId()能正常工作是因为InsertDTO的getId()方法会自动生成 NanoID。细节二删除方法不需要定义在IOneEntityService中DeleteMapping(”/{id}”) public boolean deleteById(PathVariable String id) { return this.oneEntityService.removeById(id); // 直接调用IService的方法 }删除只需要ID不需要DTO转换直接使用IService继承来的removeById和removeByIds。细节三注解驱动的横切关注点PutMapping ResultBodySuccessMsg(msg ”修改数据成功”) // 统一成功提示 OperationLog(operationType OperationTypeEnum.UPDATE, targetKey ”update”) // 操作日志 Operation(summary ”修改数据信息”) // Swagger文档 public VO updateInfo(RequestBody Validated ModifyDTO dto) { ... }三个注解分别处理统一响应消息、操作日志记录、API文档生成。子类继承后自动获得这些能力不需要每个Controller重复添加。三、DTO基类体系泛型边界的设计基础Controller的泛型约束AddDTO extends InsertDTO, ModifyDTO extends UpdateDTO背后是一套精心设计的DTO基类体系InsertDTO——新增DTO基类public class InsertDTO { private String id; public String getId() { if (StringUtils.isEmpty(id)) { this.id IdUtil.nanoId(); // 自动生成NanoID } return this.id; } }getId()时自动生成NanoID这就是Controller中insertInfo能在保存后用dto.getId()查询的原因——ID在DTO层面就已经确定了。UpdateDTO——修改DTO基类public class UpdateDTO { private String id; // 修改必须指定ID }SearchDTO——查询DTO基类public class SearchDTO { private String keyword; // 通用关键词搜索 }所有查询DTO都自带keyword字段子类可以扩展更多筛选条件。PageDTO——分页参数public class PageDTO { private Integer pageSize; private Integer currentPage; public E PageE toPage() { return Page.of(this.currentPage, this.getPageSize()); } }toPage()方法直接转换为 MyBatis-Flex 的Page对象Controller中的getPage方法直接使用。这套基类体系是泛型约束的根基——正是因为InsertDTO提供了getId()方法Controller才敢在insertInfo中调用dto.getId()来返回新增后的数据。四、三个文件如何串联完整链路追踪以新增用户为例追踪一次请求的完整流转HTTP POST /userBody: {”userName”:”张三”,”loginCode”:”zhangsan”}│▼┌─────────────────────────────────────────────────────────┐│ OneEntityController.insertInfo(AddDTO dto) ││ AddDTO AddUserDTO (extends InsertDTO) ││ ├── Validated 触发参数校验 ││ ├── dto.getId() → InsertDTO自动生成NanoID ││ ├── 调用 oneEntityService.insertInfo(dto) ││ │ │ ││ │ ▼ ││ │ UserServiceImpl.insertInfo(AddUserDTO dto) ││ │ ├── 业务逻辑设置默认密码、有效标志 ││ │ ├── userConverter.toPO(dto) → UserPO ││ │ └── this.save(userPO) → ServiceImpl.save() ││ │ └── UserMapper.insert(userPO) → 写入数据库 ││ │ ││ ├── 调用 oneEntityService.getById(dto.getId()) ││ │ │ ││ │ ▼ ││ │ UserServiceImpl.getById(String id) ││ │ └── queryChain().where(ID.eq(id)).oneAs(UserVO.class)││ │ ││ └── return UserVO (不含密码等敏感字段) ││ │ │▼HTTP 200Body: {”id”:”V1StGXR8_Z5”,”userName”:”张三”,”loginCode”:”zhangsan”,”validFlag”:1,”createdTime”:”2026-06-17T10:30:00”,...}类型安全贯穿全链路Controller接收的AddUserDTO由泛型AddDTO约束Service的insertInfo(AddUserDTO)由接口泛型约束返回的UserVO由泛型VO约束全程不需要强制类型转换编译期保证类型正确五、设计模式解析这套设计融合了三种经典模式1. 模板方法模式OneEntityServiceImpl是模板类getNextSortNo()—— 已实现的具体方法所有子类共享getById()、insertInfo()等 —— 抽象方法子类按需实现子类在不改变整体结构的前提下重新定义某些步骤。2. 泛型参数化5~6个泛型参数将操作什么类型这个问题参数化。所有Service的结构完全一致只是类型不同。编译器在编译期检查类型匹配比运行时反射更安全、更高效。3. 依赖注入 控制反转Controller通过Autowired注入IOneEntityServiceSpring根据泛型类型自动找到对应的Service实现。子类不需要关心Controller层的存在只需要实现业务逻辑。六、实战效果三个层次的子类《鹿鲸项目管理工具》中45个Service继承OneEntityServiceImpl按复杂度分三个层次层次一零代码ControllerBusinessDomainController——只需声明泛型自动获得8个REST端点RestController RequestMapping(value ”/modeling/business/domain”) public class BusinessDomainController extends OneEntityController BusinessDomainPO, BusinessDomainVO, SearchBusinessDomainDTO, AddBusinessDomainDTO, ModifyBusinessDomainDTO { // 空的8个REST接口全部由基类提供 }层次二标准实现ProjectServiceImpl——覆写CRUD方法定制查询字段和搜索条件Override public ProjectVO getById(String id) { return this.getColumnQueryChain() .where(PROJECT_PO.ID.eq(id)) .oneAs(ProjectVO.class); } Override public ListProjectVO getList(SearchProjectDTO searchProjectDTO) { return getListQueryChain(searchProjectDTO).listAs(ProjectVO.class); } // 私有方法指定查询字段 private QueryChainProjectPO getColumnQueryChain() { return queryChain().select( PROJECT_PO.ID, PROJECT_PO.PROJECT_NAME, PROJECT_PO.PRINCIPAL_ID, // ...指定需要的字段 PROJECT_PO.CREATED_TIME, PROJECT_PO.MODIFIED_TIME ).from(PROJECT_PO); }层次三复杂业务逻辑UserServiceImpl——在标准实现基础上加入业务逻辑Override public boolean insertInfo(AddUserDTO saveUserDTO) { // 业务逻辑设置系统默认密码SHA512加密 Object defaultPassword parameterService.getParameterValue(USER_DEFAULT_PASSWORD); saveUserDTO.setLoginPassword(SaSecureUtil.sha512(defaultPassword.toString())); saveUserDTO.setValidFlag(ValidFlagEnum.VALID.getValue()); // MapStruct转换DTO→PO后持久化 UserPO userPO userConverter.toPO(saveUserDTO); return this.save(userPO); }三个层次的代码量对比层次Controller代码Service代码适用场景零代码0行0行简单配置表标准实现0行~80行普通业务表复杂逻辑0行~110行核心业务表无论哪个层次Controller永远是0行业务代码——这就是泛型模板的威力。七、设计权衡与局限性为什么不把所有CRUD逻辑都在基类实现因为每个实体的查询字段和业务逻辑不同。强行通用化要么用反射性能差、类型不安全要么用默认实现不符合大多数场景。声明为抽象方法强制子类思考每个操作的正确实现比提供一个错误的默认实现更好。为什么用6个泛型参数而不是更少每个操作的数据结构不同是现实需求。新增不需要ID和修改时间修改必须有ID但不需要创建时间查询只需要筛选条件。如果用同一个DTO会导致前端能传入不该传的字段安全漏洞和字段冗余传输。6个泛型参数确保每个操作的数据结构都是精确的。局限性只适用于单表操作多表关联查询仍需在子类手写子类仍有部分样板代码getColumnQueryChain、dtoToPO等私有方法仍有重复泛型参数较多6个参数在阅读时有一定认知负担不适用于非CRUD场景批量导入导出、复杂报表等需单独设计八、总结《鹿鲸项目管理工具》用三个文件构建了一套完整的泛型CRUD架构文件核心贡献IOneEntityService定义DTO进VO出契约PO不外泄OneEntityServiceImpl通用逻辑getNextSortNo上提特化逻辑下沉OneEntityController8个REST端点注解驱动横切关注点配合InsertDTO、UpdateDTO、SearchDTO、PageDTO四个DTO基类这套设计实现了类型安全泛型约束贯穿Controller→Service→DAO全链路安全封装PO永不外泄DTO按操作类型精确裁剪字段代码复用整个项目所有表增删改的实体共享同一套骨架简单实体零代码渐进式定制从零代码到复杂业务逻辑子类按需选择覆写粒度横切关注点统一参数校验、操作日志、统一响应、API文档由注解自动处理核心设计思想用编译期类型约束替代运行时检查用继承复用替代复制粘贴让开发者把精力集中在业务差异上而不是重复的CRUD样板代码上。如果觉得这篇文章对你有启发欢迎点赞、收藏、分享~
架构实战第3篇:三个文件消灭90%重复代码-泛型CRUD架构解析
摘要:在Java企业级开发中单表CRUD是最常见也是最枯燥的代码。每新增一个业务实体就要写一套几乎相同的Controller、Service——结构雷同却又不得不写。《鹿鲸项目管理工具》仅用三个文件——IOneEntityService接口、OneEntityServiceImpl抽象类、OneEntityController控制器基类——构建了一套泛型模板让所有表新增改的业务实体共享同一套CRUD骨架新增简单实体只需声明泛型Controller零代码。本文深入解析这三个文件的设计细节与背后的工程思考。一、三个文件的全貌先看三个文件各自的角色定位文件层次泛型数量核心职责IOneEntityServiceService接口5个定义DTO进VO出的业务契约OneEntityServiceImplService实现6个继承MyBatis-Flex实现通用逻辑OneEntityControllerController5个定义8个RESTful端点它们通过相同的泛型参数串联成一条完整的CRUD链路从HTTP请求到数据库操作类型安全一以贯之。二、逐文件深度解析2.1 IOneEntityServiceService接口——定义业务契约public interface IOneEntityServicePO, VO, SearchDTO, AddDTO, UpdateDTO extends IServicePO { VO getById(String id); boolean insertInfo(AddDTO addDTO); boolean updateInfo(UpdateDTO updateDTO); ListVO getList(SearchDTO dto); PageVO getPage(SearchDTO dto, PageDTO pageDTO); int getNextSortNo(String sortExpress); }5个泛型参数每个都对应一种对象类型泛型角色说明PO持久化对象数据库表映射Service内部使用VO视图对象返回给前端的展示数据SearchDTO查询参数列表/分页查询的筛选条件AddDTO新增参数前端提交的新增数据UpdateDTO修改参数前端提交的修改数据关键设计决策所有方法入参都是DTO所有返回值都是VOPO从不出现在接口签名中。这意味着PO被完全封装在Service内部。Controller层拿不到PO前端更拿不到PO。从接口契约层面就杜绝了数据库结构泄露的安全隐患。继承IServicePO这是 MyBatis-Flex 提供的基础服务接口自带save()、updateById()、removeById()、removeByIds()、list()、getById()等方法。继承它之后IOneEntityService不需要重复定义删除方法——Controller直接调用removeById和removeByIds即可。6个方法分工方法入参返回值用途getByIdString idVO主键查询单条insertInfoAddDTOboolean新增updateInfoUpdateDTOboolean修改getListSearchDTOListVO列表查询getPageSearchDTO PageDTOPageVO分页查询getNextSortNoStringint获取下一个排序号注意没有delete方法。删除操作直接使用IService继承来的removeById和removeByIds因为删除只需要ID不需要额外定义。2.2 OneEntityServiceImpl抽象实现——通用逻辑上提public abstract class OneEntityServiceImplM extends BaseMapperPO, PO, VO, SearchDTO, AddDTO, UpdateDTO extends ServiceImplM, PO implements IOneEntityServicePO, VO, SearchDTO, AddDTO, UpdateDTO { Override public int getNextSortNo(String sortExpress) { QueryWrapper queryWrapper new QueryWrapper(); queryWrapper.select(QueryMethods.max(”sort_no”).as(”sort_no”)); PO t this.getMapper().selectOneByQuery(queryWrapper); JSONObject jsonObject JSONObject.parseObject(JSONObject.toJSONString(t)); return jsonObject.getInteger(”sortNo”); } }比接口多了一个泛型参数MMapper类型约束为M extends BaseMapperPO。这个参数用于继承 MyBatis-Flex 的ServiceImplM, PO从而自动获得Mapper注入和基础数据库操作能力。为什么只实现了getNextSortNo一个方法这是整个设计最精妙的地方。6个接口方法中方法能否通用实现原因getNextSortNo✅ 可以所有实体都是查max(sort_no)逻辑完全相同getById❌ 不能每个实体需要查询的字段不同如User要排除密码insertInfo❌ 不能每个实体的业务逻辑不同如User要设置默认密码updateInfo❌ 不能每个实体的修改逻辑不同getList❌ 不能每个实体的搜索条件不同getPage❌ 不能同getListgetNextSortNo的实现逻辑查询当前表sort_no字段的最大值。这个操作对所有继承MasterPO含sortNo字段的实体都完全相同所以放在基类实现一次45个子类共享。声明为abstract class强制子类必须实现其余5个方法。编译期就能发现遗漏而不是等到运行时抛出异常。继承ServiceImplM, PO的收益——子类可以直接调用this.save(po); // 新增 this.updateById(po); // 修改 this.removeById(id); // 删除 this.removeByIds(idList); // 批量删除 queryChain().select(...) // 链式查询2.3 OneEntityController控制器基类——8个REST端点public abstract class OneEntityControllerPO, VO, SearchDTO, AddDTO extends InsertDTO, ModifyDTO extends UpdateDTO { Autowired protected IOneEntityServicePO, VO, SearchDTO, AddDTO, ModifyDTO oneEntityService; GetMapping(”/{id}”) public VO getById(PathVariable String id) { ... } PutMapping public VO updateInfo(RequestBody Validated ModifyDTO dto) { ... } PostMapping public VO insertInfo(RequestBody Validated AddDTO dto) { ... } DeleteMapping(”/{id}”) public boolean deleteById(PathVariable String id) { ... } DeleteMapping(”/list”) public boolean deleteByIdList(RequestBody ListString idList) { ... } GetMapping(”/list”) public ListVO getList(Valid SearchDTO dto) { ... } GetMapping(”/page/list”) public PageVO getPage(Valid SearchDTO dto, PageDTO pageDTO) { ... } GetMapping(”/next/sort”) public int getNextSort(RequestParam String sortExpress) { ... } }泛型参数有边界约束AddDTO extends InsertDTO, ModifyDTO extends UpdateDTO这两个约束不是摆设它们确保了AddDTO必须继承InsertDTO从而拥有getId()方法自动生成NanoIDModifyDTO必须继承UpdateDTO从而拥有id字段修改必须指定ID8个REST端点全景HTTP方法路径方法名返回值说明GET/{id}getByIdVO查询单条POST/insertInfoVO新增返回完整数据PUT/updateInfoVO修改返回更新后数据DELETE/{id}deleteByIdboolean删除单条DELETE/listdeleteByIdListboolean批量删除GET/listgetListListVO列表查询GET/page/listgetPagePageVO分页查询GET/next/sortgetNextSortint获取下一个排序号三个值得注意的设计细节细节一新增和修改都返回VOPostMapping public VO insertInfo(RequestBody Validated AddDTO dto) { this.oneEntityService.insertInfo(dto); // 先保存 return this.oneEntityService.getById(dto.getId()); // 再查询返回 }不是返回boolean而是返回保存后的完整VO。这样前端新增成功后直接拿到完整数据含系统自动填充的创建时间、ID等不需要再发一次查询请求。dto.getId()能正常工作是因为InsertDTO的getId()方法会自动生成 NanoID。细节二删除方法不需要定义在IOneEntityService中DeleteMapping(”/{id}”) public boolean deleteById(PathVariable String id) { return this.oneEntityService.removeById(id); // 直接调用IService的方法 }删除只需要ID不需要DTO转换直接使用IService继承来的removeById和removeByIds。细节三注解驱动的横切关注点PutMapping ResultBodySuccessMsg(msg ”修改数据成功”) // 统一成功提示 OperationLog(operationType OperationTypeEnum.UPDATE, targetKey ”update”) // 操作日志 Operation(summary ”修改数据信息”) // Swagger文档 public VO updateInfo(RequestBody Validated ModifyDTO dto) { ... }三个注解分别处理统一响应消息、操作日志记录、API文档生成。子类继承后自动获得这些能力不需要每个Controller重复添加。三、DTO基类体系泛型边界的设计基础Controller的泛型约束AddDTO extends InsertDTO, ModifyDTO extends UpdateDTO背后是一套精心设计的DTO基类体系InsertDTO——新增DTO基类public class InsertDTO { private String id; public String getId() { if (StringUtils.isEmpty(id)) { this.id IdUtil.nanoId(); // 自动生成NanoID } return this.id; } }getId()时自动生成NanoID这就是Controller中insertInfo能在保存后用dto.getId()查询的原因——ID在DTO层面就已经确定了。UpdateDTO——修改DTO基类public class UpdateDTO { private String id; // 修改必须指定ID }SearchDTO——查询DTO基类public class SearchDTO { private String keyword; // 通用关键词搜索 }所有查询DTO都自带keyword字段子类可以扩展更多筛选条件。PageDTO——分页参数public class PageDTO { private Integer pageSize; private Integer currentPage; public E PageE toPage() { return Page.of(this.currentPage, this.getPageSize()); } }toPage()方法直接转换为 MyBatis-Flex 的Page对象Controller中的getPage方法直接使用。这套基类体系是泛型约束的根基——正是因为InsertDTO提供了getId()方法Controller才敢在insertInfo中调用dto.getId()来返回新增后的数据。四、三个文件如何串联完整链路追踪以新增用户为例追踪一次请求的完整流转HTTP POST /userBody: {”userName”:”张三”,”loginCode”:”zhangsan”}│▼┌─────────────────────────────────────────────────────────┐│ OneEntityController.insertInfo(AddDTO dto) ││ AddDTO AddUserDTO (extends InsertDTO) ││ ├── Validated 触发参数校验 ││ ├── dto.getId() → InsertDTO自动生成NanoID ││ ├── 调用 oneEntityService.insertInfo(dto) ││ │ │ ││ │ ▼ ││ │ UserServiceImpl.insertInfo(AddUserDTO dto) ││ │ ├── 业务逻辑设置默认密码、有效标志 ││ │ ├── userConverter.toPO(dto) → UserPO ││ │ └── this.save(userPO) → ServiceImpl.save() ││ │ └── UserMapper.insert(userPO) → 写入数据库 ││ │ ││ ├── 调用 oneEntityService.getById(dto.getId()) ││ │ │ ││ │ ▼ ││ │ UserServiceImpl.getById(String id) ││ │ └── queryChain().where(ID.eq(id)).oneAs(UserVO.class)││ │ ││ └── return UserVO (不含密码等敏感字段) ││ │ │▼HTTP 200Body: {”id”:”V1StGXR8_Z5”,”userName”:”张三”,”loginCode”:”zhangsan”,”validFlag”:1,”createdTime”:”2026-06-17T10:30:00”,...}类型安全贯穿全链路Controller接收的AddUserDTO由泛型AddDTO约束Service的insertInfo(AddUserDTO)由接口泛型约束返回的UserVO由泛型VO约束全程不需要强制类型转换编译期保证类型正确五、设计模式解析这套设计融合了三种经典模式1. 模板方法模式OneEntityServiceImpl是模板类getNextSortNo()—— 已实现的具体方法所有子类共享getById()、insertInfo()等 —— 抽象方法子类按需实现子类在不改变整体结构的前提下重新定义某些步骤。2. 泛型参数化5~6个泛型参数将操作什么类型这个问题参数化。所有Service的结构完全一致只是类型不同。编译器在编译期检查类型匹配比运行时反射更安全、更高效。3. 依赖注入 控制反转Controller通过Autowired注入IOneEntityServiceSpring根据泛型类型自动找到对应的Service实现。子类不需要关心Controller层的存在只需要实现业务逻辑。六、实战效果三个层次的子类《鹿鲸项目管理工具》中45个Service继承OneEntityServiceImpl按复杂度分三个层次层次一零代码ControllerBusinessDomainController——只需声明泛型自动获得8个REST端点RestController RequestMapping(value ”/modeling/business/domain”) public class BusinessDomainController extends OneEntityController BusinessDomainPO, BusinessDomainVO, SearchBusinessDomainDTO, AddBusinessDomainDTO, ModifyBusinessDomainDTO { // 空的8个REST接口全部由基类提供 }层次二标准实现ProjectServiceImpl——覆写CRUD方法定制查询字段和搜索条件Override public ProjectVO getById(String id) { return this.getColumnQueryChain() .where(PROJECT_PO.ID.eq(id)) .oneAs(ProjectVO.class); } Override public ListProjectVO getList(SearchProjectDTO searchProjectDTO) { return getListQueryChain(searchProjectDTO).listAs(ProjectVO.class); } // 私有方法指定查询字段 private QueryChainProjectPO getColumnQueryChain() { return queryChain().select( PROJECT_PO.ID, PROJECT_PO.PROJECT_NAME, PROJECT_PO.PRINCIPAL_ID, // ...指定需要的字段 PROJECT_PO.CREATED_TIME, PROJECT_PO.MODIFIED_TIME ).from(PROJECT_PO); }层次三复杂业务逻辑UserServiceImpl——在标准实现基础上加入业务逻辑Override public boolean insertInfo(AddUserDTO saveUserDTO) { // 业务逻辑设置系统默认密码SHA512加密 Object defaultPassword parameterService.getParameterValue(USER_DEFAULT_PASSWORD); saveUserDTO.setLoginPassword(SaSecureUtil.sha512(defaultPassword.toString())); saveUserDTO.setValidFlag(ValidFlagEnum.VALID.getValue()); // MapStruct转换DTO→PO后持久化 UserPO userPO userConverter.toPO(saveUserDTO); return this.save(userPO); }三个层次的代码量对比层次Controller代码Service代码适用场景零代码0行0行简单配置表标准实现0行~80行普通业务表复杂逻辑0行~110行核心业务表无论哪个层次Controller永远是0行业务代码——这就是泛型模板的威力。七、设计权衡与局限性为什么不把所有CRUD逻辑都在基类实现因为每个实体的查询字段和业务逻辑不同。强行通用化要么用反射性能差、类型不安全要么用默认实现不符合大多数场景。声明为抽象方法强制子类思考每个操作的正确实现比提供一个错误的默认实现更好。为什么用6个泛型参数而不是更少每个操作的数据结构不同是现实需求。新增不需要ID和修改时间修改必须有ID但不需要创建时间查询只需要筛选条件。如果用同一个DTO会导致前端能传入不该传的字段安全漏洞和字段冗余传输。6个泛型参数确保每个操作的数据结构都是精确的。局限性只适用于单表操作多表关联查询仍需在子类手写子类仍有部分样板代码getColumnQueryChain、dtoToPO等私有方法仍有重复泛型参数较多6个参数在阅读时有一定认知负担不适用于非CRUD场景批量导入导出、复杂报表等需单独设计八、总结《鹿鲸项目管理工具》用三个文件构建了一套完整的泛型CRUD架构文件核心贡献IOneEntityService定义DTO进VO出契约PO不外泄OneEntityServiceImpl通用逻辑getNextSortNo上提特化逻辑下沉OneEntityController8个REST端点注解驱动横切关注点配合InsertDTO、UpdateDTO、SearchDTO、PageDTO四个DTO基类这套设计实现了类型安全泛型约束贯穿Controller→Service→DAO全链路安全封装PO永不外泄DTO按操作类型精确裁剪字段代码复用整个项目所有表增删改的实体共享同一套骨架简单实体零代码渐进式定制从零代码到复杂业务逻辑子类按需选择覆写粒度横切关注点统一参数校验、操作日志、统一响应、API文档由注解自动处理核心设计思想用编译期类型约束替代运行时检查用继承复用替代复制粘贴让开发者把精力集中在业务差异上而不是重复的CRUD样板代码上。如果觉得这篇文章对你有启发欢迎点赞、收藏、分享~