3.Spring Boot3 脚手架进阶:通用持久层CRUD架构从零落地

45 阅读21分钟

# Spring Boot3 脚手架进阶:通用持久层CRUD架构从零落地

前期搭建项目演示环境时,曾尝试适配最新的NextDoc4j文档框架,核心原因是Knife4j暂不兼容新版Spring Boot。但实际使用后发现,NextDoc4j的实用性和便捷性远不如Knife4j。因此项目进行版本回退,切换至Spring Boot3.3.10版本,重新接入Knife4j接口文档框架。同时,本次迭代重点完成通用持久层能力的落地封装,搭建一套高复用、低冗余的数据库CRUD通用架构。

1. 持久层框架选型:tk.mybatis

项目基础持久层选用主流的MyBatis,在此基础上引入tk.mybatis作为通用Mapper工具,核心优势如下:

  • 零XML冗余:无需手动编写大量重复的CRUD XML语句,简化持久层开发流程

  • 注解驱动:通过JPA注解完成数据库表与实体类的映射,配置简洁直观

  • 内置通用方法:封装基础增删改查、条件查询、分页查询等通用能力,大幅提升开发效率

2. 架构设计目标

为避免项目中PO、DTO、Form实体类泛滥,减少类文件扩散、降低维护成本,本次采用实体统一复用设计:将数据库映射PO、前端查询Form、数据传输DTO合并为一个实体类。

该设计会让Model实体包含数据库JPA配置、Swagger文档注解、查询条件属性,兼顾数据库映射、接口文档、前端参数接收多重能力。最终实现两大核心能力:

  1. 通用基础CRUD功能(增、删、改、查、批量操作)

  2. 灵活的自定义多条件查询、分页查询、排序查询功能

2.1 统一实体类示例(客户表)

整合数据库映射、接口文档、通用字段的统一实体类,适配数据库持久化与前后端数据交互:

package vip.wayhua.ivy.sys.pojo;

import io.swagger.v3.oas.annotations.media.Schema;
import vip.wayhua.ivy.core.dto.IdDto;

import javax.persistence.*;

import java.time.LocalDateTime;

/****
 * @Author:黄卫华(wayhua@126.com)
 * @Description:<p>客户表</p>实体类
 * @Date 2025年8月21日 上午10:01:52
 *****/
@Schema(description = "客户表")
@Table(name = "custom")
public class Custom extends IdDto {


    @Schema(description = "姓名 姓名")
    @Column(name = "name")
    private String name;//姓名 姓名

    @Schema(description = "手机号 手机号")
    @Column(name = "mobile")
    private String mobile;//手机号 手机号

    @Schema(description = "性别 性别")
    @Column(name = "sex")
    private String sex;//性别 性别

    @Schema(description = "个性签名 个性签名")
    @Column(name = "sign")
    private String sign;//个性签名 个性签名

    @Schema(description = "创建人 创建人")
    @Column(name = "created_by")
    private String createdBy;//创建人 创建人

    @Schema(description = "创建时间 创建时间")
    @Column(name = "created_time")
    private LocalDateTime createdTime;//创建时间 创建时间

    @Schema(description = "更新人 更新人")
    @Column(name = "updated_by")
    private String updatedBy;//更新人 更新人

    @Schema(description = "更新时间 更新时间")
    @Column(name = "updated_time")
    private LocalDateTime updatedTime;//更新时间 更新时间

    //get方法
    public String getName() {
        return name;
    }

    //set方法
    public void setName(String name) {
        this.name = name;
    }

    //get方法
    public String getMobile() {
        return mobile;
    }

    //set方法
    public void setMobile(String mobile) {
        this.mobile = mobile;
    }

    //get方法
    public String getSex() {
        return sex;
    }

    //set方法
    public void setSex(String sex) {
        this.sex = sex;
    }

    //get方法
    public String getSign() {
        return sign;
    }

    //set方法
    public void setSign(String sign) {
        this.sign = sign;
    }

    //get方法
    public String getCreatedBy() {
        return createdBy;
    }

    //set方法
    public void setCreatedBy(String createdBy) {
        this.createdBy = createdBy;
    }

    //get方法
    public LocalDateTime getCreatedTime() {
        return createdTime;
    }

    //set方法
    public void setCreatedTime(LocalDateTime createdTime) {
        this.createdTime = createdTime;
    }

    //get方法
    public String getUpdatedBy() {
        return updatedBy;
    }

    //set方法
    public void setUpdatedBy(String updatedBy) {
        this.updatedBy = updatedBy;
    }

    //get方法
    public LocalDateTime getUpdatedTime() {
        return updatedTime;
    }

    //set方法
    public void setUpdatedTime(LocalDateTime updatedTime) {
        this.updatedTime = updatedTime;
    }
}

3. 通用持久层基类架构设计

针对数据库单主键组合主键两种场景,通过接口分层+泛型封装,设计一套通用CRUD架构,实现业务层零重复代码,所有业务模块可直接复用。

3.1 顶层通用CRUD接口 CommonCrudService

作为所有持久层服务的顶层父接口,定义通用基础能力,包含普通查询、分页查询、保存、复杂条件查询等核心方法,是单主键、组合主键服务的最小能力集。

package vip.wayhua.ivy.core.service;
import vip.wayhua.ivy.core.dto.BaseDto;
import vip.wayhua.ivy.core.dto.Paged;
import vip.wayhua.ivy.core.form.PageForm;
import vip.wayhua.ivy.core.form.QueryCondition;
import vip.wayhua.ivy.core.form.SortForm;
import java.util.List;

public interface CommonCrudService <Dto extends BaseDto> {
    /**
     * 分页查询列表
     *
     * @param query     查询条件
     * @param pageQuery 分页查询条件
     * @return 分页列表信息
     */
    Paged<Dto> queryPaged(Dto query, PageForm pageQuery);

    /**
     * 查询列表
     *
     * @param query     查询条件
     * @param sortQuery 排序查询条件
     * @return 列表信息
     */
    List<Dto> queryList(Dto query, SortForm sortQuery);

    /***
     * 保存数据
     * @param po 实体参数
     */
    Dto save(Dto po);

    /***
     * 复杂条件查询列表
     * @param queryCondition 自定义查询条件
     * @param sortForm 排序条件
     * @return 数据列表
     */
    List<Dto> complexQuery(QueryCondition queryCondition, SortForm sortForm);

    /**
     * 复杂条件分页查询
     *
     * @param queryCondition 自定义查询条件
     * @param pageForm 分页条件
     * @return 分页数据
     */
    Paged<Dto> complexQueryPaged(QueryCondition queryCondition, PageForm pageForm);
}

3.2 单主键服务接口 IdCrudService

继承顶层通用接口,针对单主键数据表扩展专属能力:根据ID查询、新增、修改、单删、批量删除等,适配绝大多数业务表场景。

package vip.wayhua.ivy.core.service;
import vip.wayhua.ivy.core.dto.IdDto;
import java.util.List;

public interface IdCrudService<Dto extends IdDto>
        extends CommonCrudService<Dto> {
    /**
     * 根据ID查询详情
     *
     * @param id 主键ID(兼容多主键格式)
     * @return 实体详情
     */
    Dto queryById(String id);

    /**
     * 新增数据
     *
     * @param form 新增参数
     * @param id 自定义主键
     */
    void create(Dto form, String id);

    /**
     * 修改数据
     *
     * @param form 修改参数
     */
    void update(Dto form);

    /**
     * 根据ID删除单条数据
     *
     * @param id 主键ID
     */
    void delete(String id);

    /**
     * 批量删除数据
     *
     * @param ids 主键ID列表
     */
    void delete(List<String> ids);
}

3.3 组合主键服务接口 BatchCrudService

适配多组合主键数据表,扩展多主键查询、新增、修改、删除能力,通过可变参数接收多个主键,适配特殊业务表场景。

package vip.wayhua.ivy.core.service;
import vip.wayhua.ivy.core.dto.BaseDto;

public interface BatchCrudService<Dto extends BaseDto>
        extends  CommonCrudService<Dto>{
    /**
     * 多主键查询详情
     *
     * @param id 多个主键ID
     * @return 实体详情
     */
    Dto queryById(String... id);

    /**
     * 新增数据
     *
     * @param form 新增参数
     */
    void create(Dto form );

    /**
     * 修改数据
     *
     * @param form 修改参数
     */
    void update(Dto form);

    /**
     * 多主键删除数据
     *
     * @param id 多个主键ID
     */
    void delete(String... id);
}

3.4 通用服务实现类 CommonCrudServiceImpl

顶层通用服务实现类,基于tk.mybatis+PageHelper实现通用查询、分页、复杂条件查询能力,预留钩子方法,支持业务子类自定义扩展,保证通用性和灵活性。

3.4.1 普通分页查询实现

@Override
public Paged<Dto> queryPaged(Dto query, PageForm pageQuery) {
    // 开启分页
    PageHelper.startPage(pageQuery.getPage(), pageQuery.getSize());
    // 构建查询条件
    Example example = new Example(clazz);
    Example.Criteria criteria = example.createCriteria();
    if (query != null) {
        // 根据实体非空字段自动拼接查询条件
        addExample(criteria, query);
    }
    // 预留子类扩展钩子
    doAddExample(example);
    // 无查询条件时默认拼接1=1,避免查询异常
    checkIsNoCondtion(criteria);
    // 解析排序条件
    doSortExample(example, pageQuery);
    // 执行查询
    List<Dto> pos = dao.selectByExample(example);
    // 封装分页结果
    PageInfo<Dto> infos = new PageInfo<>(pos);
    return new Paged<>(pos, infos.getTotal(), pageQuery.getPage(), pageQuery.getSize());
}

3.4.2 复杂条件查询核心实现

复杂条件查询是本持久层架构的核心拓展能力,突破了传统框架仅支持实体非空字段匹配的局限,可满足前端动态自定义筛选需求。该功能采用组件化拆解+统一解析设计,通过枚举、参数转换器、条件实体、核心解析逻辑多层协作,统一管控查询规则。支持模糊查询、区间查询、包含匹配、空值校验及AND/OR逻辑组合,可自动拼接SQL查询语句,无需手动编写SQL,兼顾通用性与业务扩展性。

1. 查询操作符枚举 QueryOperator

统一定义系统支持的所有查询运算规则,包含等值、不等值、区间、模糊、包含、空值判断等全覆盖查询操作。通过统一枚举管控查询语义,避免前端传参混乱,同时提供序列化/反序列化方法,保证前后端操作符一致、可校验、可扩展。

package vip.wayhua.ivy.core.enums;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;

public enum QueryOperator {
    /** 等于 = */
    EQ("="),
    /** 不等于 != */
    NE("!="),
    /** 大于 > */
    GT(">"),
    /** 大于等于 >= */
    GE(">="),
    /** 小于 < */
    LT("<"),
    /** 小于等于 <= */
    LE("<="),
    /** 模糊匹配 LIKE '%value%' */
    LIKE("like"),
    /** 左模糊 LIKE '%value' */
    LEFT_LIKE("left_like"),
    /** 右模糊 LIKE 'value%' */
    RIGHT_LIKE("right_like"),
    /** 包含 IN (...) */
    IN("in"),
    /** 不包含 NOT IN (...) */
    NOT_IN("not_in"),
    /** 区间 BETWEEN ... AND ... */
    BETWEEN("between"),
    /** 为空 IS NULL */
    IS_NULL("is_null"),
    /** 不为空 IS NOT NULL */
    IS_NOT_NULL("is_not_null");

    private final String symbol;

    QueryOperator(String symbol) {
        this.symbol = symbol;
    }

    @JsonValue
    public String getSymbol() {
        return symbol;
    }

    @JsonCreator
    public static QueryOperator fromSymbol(String symbol) {
        if (symbol == null || symbol.trim().isEmpty()) {
            return null;
        }
        String target = symbol.trim().toLowerCase();
        // 优先匹配符号
        for (QueryOperator operator : values()) {
            if (operator.symbol.equals(target)) {
                return operator;
            }
        }
        // 兜底匹配枚举名
        try {
            return QueryOperator.valueOf(symbol.trim().toUpperCase());
        } catch (IllegalArgumentException e) {
            throw new IllegalArgumentException("不支持的查询操作符: " + symbol);
        }
    }
}

2. 前端参数转换器 QueryOperatorConverter

Spring MVC 自定义参数转换器,用于自动将前端传入的字符串操作符转为枚举对象。统一参数解析逻辑,无需业务手动转换,解决前后端参数类型不匹配问题,保证查询操作符解析统一、容错性更强。

package vip.wayhua.ivy.core.converter;
import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;
import vip.wayhua.ivy.core.enums.QueryOperator;

@Component
public class QueryOperatorConverter implements Converter<String, QueryOperator> {
    @Override
    public QueryOperator convert(String source) {
        // 复用枚举转换逻辑,保证前后端一致性
        return QueryOperator.fromSymbol(source);
    }
}

3. 单条查询条件 QueryItem

最小查询条件单元,用于封装单字段、单规则的筛选条件,包含字段名、操作符、单值/多值参数。提供静态构建方法,支持快速构建模糊查询、区间查询、包含查询等条件,是复杂动态查询的基础原子单元。

package vip.wayhua.ivy.core.form;

import io.swagger.v3.oas.annotations.media.Schema;
import vip.wayhua.ivy.core.dto.BaseDto;
import vip.wayhua.ivy.core.enums.QueryOperator;


public class QueryItem extends BaseDto {
    /**
     * DTO 字段名(自动映射数据库列)
     */
    @Schema(description = "DTO字段名", example = "userName")
    private String fieldName;

    /**
     * 查询操作符
     */
    @Schema(description = "查询操作符", example = "=",
            allowableValues = {"=", "!=", ">", ">=", "<", "<=", "like", "in", "not_in", "between"})
    private QueryOperator operator;

    /**
     * 单值场景取值(EQ / GT / LIKE 等)
     */
    private Object value;

    /**
     * 多值场景取值(IN / BETWEEN 等)
     * BETWEEN 时按 [起始值, 结束值] 顺序传入
     */
    private Object[] values;
    public static QueryItem of(String fieldName, QueryOperator operator, Object value) {
        QueryItem item = new QueryItem();
        item.setFieldName(fieldName);
        item.setOperator(operator);
        item.setValue(value);
        return item;
    }

    public static QueryItem ofMulti(String fieldName, QueryOperator operator, Object... values) {
        QueryItem item = new QueryItem();
        item.setFieldName(fieldName);
        item.setOperator(operator);
        item.setValues(values);
        return item;
    }
   // ... setter/getter
}
4. 整体查询条件 QueryCondition

用于封装一组批量查询条件,支持批量传入多个 QueryItem,并支持配置 AND/OR 组合逻辑。实现条件批量组装、链式添加,让前端可以自由组合多字段、多规则、多逻辑的复杂筛选场景。

package vip.wayhua.ivy.core.form;

import io.swagger.v3.oas.annotations.media.Schema;
import vip.wayhua.ivy.core.dto.BaseDto;
import vip.wayhua.ivy.core.enums.LogicType;


import java.util.ArrayList;
import java.util.List;

public class QueryCondition extends BaseDto {
    /**
     * 条件项列表
     */
    @Schema(description = "条件组合" )
    private List<QueryItem> items = new ArrayList<>();

    /**
     * 条件之间的逻辑关系:AND / OR
     */
    @Schema(description = "条件之间的逻辑关系,默认And" )
    private LogicType logic = LogicType.AND;


 
    // 链式便捷方法
    public QueryCondition addItem(QueryItem item) {
        this.items.add(item);
        return this;
    }
   
    //... setter/getter

}

5. 复杂条件查询核心逻辑

架构最终的解析执行层,负责遍历前端传入的所有查询条件,根据不同操作符自动拼接 MyBatis Example 查询语句。同时做参数非空过滤、逻辑符区分、分页排序封装,无需手写 SQL,即可动态生成任意复杂的组合查询,真正实现通用动态查询能力。


    @Override
    public Paged<Dto> complexQueryPaged(QueryCondition queryCondition, PageForm pageQuery) {
        //分页
        PageHelper.startPage(pageQuery.getPage(), pageQuery.getSize());

        //搜索条件构建
        Example example = new Example(clazz);
        Example.Criteria criteria = example.createCriteria();

        doComplexCondtion(criteria, queryCondition);
        checkIsNoCondtion(criteria);
        doSortExample(example, pageQuery);

        List<Dto> pos = dao.selectByExample(example);
        //执行搜索
        PageInfo<Dto> infos = new PageInfo<Dto>(pos);

        return new Paged<>(pos, infos.getTotal(), pageQuery.getPage(), pageQuery.getSize());

    }

    protected void doComplexCondtion(Example.Criteria criteria, QueryCondition condition) {

        if (condition == null || CollUtil.isEmpty(condition.getItems())) {
            return;
        }


        for (QueryItem item : condition.getItems()) {

            QueryOperator operator = item.getOperator();
            Object value = item.getValue();
            Object[] values = item.getValues();

            // 非空判断:除了判空类操作符,其余值为空时跳过该条件
            if (operator != QueryOperator.IS_NULL && operator != QueryOperator.IS_NOT_NULL) {
                if (value == null && (values == null || values.length == 0)) {
                    continue;
                }
            }

            addQueryItem(criteria, condition.getLogic(), item);
        }
    }

    protected void addQueryItem(Example.Criteria criteria, LogicType logic, QueryItem item) {

        String fieldName = item.getFieldName();
        Object value = item.getValue();
        Object[] values = item.getValues();
        QueryOperator operator = item.getOperator();

        switch (operator) {
            case EQ:
                if (logic == LogicType.AND) {
                    criteria.andEqualTo(fieldName, value);
                } else {
                    criteria.orEqualTo(fieldName, value);
                }
                break;
            case NE:
                if (logic == LogicType.AND) {
                    criteria.andNotEqualTo(fieldName, value);
                } else {
                    criteria.orNotEqualTo(fieldName, value);
                }
                break;
            case GT:
                if (logic == LogicType.AND) {
                    criteria.andGreaterThan(fieldName, value);
                } else {
                    criteria.orGreaterThan(fieldName, value);
                }
                break;
            case GE:
                if (logic == LogicType.AND) {
                    criteria.andGreaterThanOrEqualTo(fieldName, value);
                } else {
                    criteria.orGreaterThanOrEqualTo(fieldName, value);
                }
                break;
            case LT:
                if (logic == LogicType.AND) {
                    criteria.andLessThan(fieldName, value);
                } else {
                    criteria.orLessThan(fieldName, value);
                }
                break;
            case LE:
                if (logic == LogicType.AND) {
                    criteria.andLessThanOrEqualTo(fieldName, value);
                } else {
                    criteria.orLessThanOrEqualTo(fieldName, value);
                }
                break;
            case LIKE:
                if (logic == LogicType.AND) {
                    criteria.andLike(fieldName, "%" + value + "%");
                } else {
                    criteria.orLike(fieldName, "%" + value + "%");
                }
                break;
            case LEFT_LIKE:
                if (logic == LogicType.AND) {
                    criteria.andLike(fieldName, "%" + value);
                } else {
                    criteria.orLike(fieldName, "%" + value);
                }
                break;
            case RIGHT_LIKE:
                if (logic == LogicType.AND) {
                    criteria.andLike(fieldName, value + "%");
                } else {
                    criteria.orLike(fieldName, value + "%");
                }
                break;
            case IN:
                if (logic == LogicType.AND) {
                    criteria.andIn(fieldName, Arrays.asList(values));
                } else {
                    criteria.orIn(fieldName, Arrays.asList(values));
                }
                break;
            case NOT_IN:
                if (logic == LogicType.AND) {
                    criteria.andNotIn(fieldName, Arrays.asList(values));
                } else {
                    criteria.orNotIn(fieldName, Arrays.asList(values));
                }
                break;
            case BETWEEN:
                // values[0] 起始值,values[1] 结束值
                if (logic == LogicType.AND) {
                    criteria.andBetween(fieldName, values[0], values[1]);
                } else {
                    criteria.orBetween(fieldName, values[0], values[1]);
                }
                break;
            case IS_NULL:
                if (logic == LogicType.AND) {
                    criteria.andIsNull(fieldName);
                } else {
                    criteria.orIsNull(fieldName);
                }
                break;
            case IS_NOT_NULL:
                if (logic == LogicType.AND) {
                    criteria.andIsNotNull(fieldName);
                } else {
                    criteria.orIsNotNull(fieldName);
                }
                break;
            default:
                throw new IllegalArgumentException("不支持的查询操作符: " + operator);
        }
    }

3.5 单主键服务实现 IdCrudServiceImpl

IdCrudServiceImpl 为单主键业务的通用服务实现类,依托 Java 单类继承、多接口实现的特性搭建架构。该类通过泛型约束适配所有单主键业务实体与数据层对象,类定义继承通用服务基类并实现单主键专属服务接口,统一承载通用CRUD能力与单主键差异化业务能力。

由于架构采用泛型做全局通用适配,运行时会发生泛型类型擦除,Spring 无法通过泛型区分具体的 Mapper 实例,因此不能使用 @Autowired 自动注入DAO。本架构统一采用构造函数传参的方式手动注入DAO实例,解决泛型依赖注入问题。

该实现类的基础数据库操作全部复用父类 CommonCrudServiceImpl 的能力,自身不重复编写通用CRUD逻辑,仅负责拓展单主键专属方法与业务扩展钩子。在主键查询方法中预留 doFillPojoFindById 数据回填钩子,业务模块可按需重写,实现关联数据查询、字段填充等个性化逻辑,在保证代码复用的同时保留业务拓展能力。多主键场景可参照该架构思路自主拓展。

package vip.wayhua.ivy.core.service.impl;

import org.springframework.dao.DuplicateKeyException;
import org.springframework.transaction.annotation.Transactional;
import tk.mybatis.mapper.common.Mapper;
import vip.wayhua.ivy.core.dto.IdDto;
import vip.wayhua.ivy.core.enums.ResultCode;
import vip.wayhua.ivy.core.exception.XException;
import vip.wayhua.ivy.core.idworker.Sid;
import vip.wayhua.ivy.core.service.IdCrudService;
import vip.wayhua.ivy.core.utils.ExceptionExtractUtils;
import vip.wayhua.ivy.core.utils.StringUtils;

import java.util.List;

import static vip.wayhua.ivy.core.enums.ResultCode.DATA_DELETE_ERROR;
import static vip.wayhua.ivy.core.utils.ExceptionExtractUtils.extractDuplicateKeyInfo;


public abstract class IdCrudServiceImpl<Dto extends IdDto, Dao extends Mapper<Dto>>
        extends CommonCrudServiceImpl<Dto, Dao>
        implements IdCrudService<Dto> {

    public IdCrudServiceImpl(Dao dao) {
        super(dao);
    }

    @Override
    public Dto queryById(String id) {
        Dto po = dao.selectByPrimaryKey(id);
        doFillPojoFindById(po);
        return po;
    }

    protected void doFillPojoFindById(Dto form) {

    }

    @Transactional
    @Override
    public void create(Dto form, String id) {

        doBeforeAdd(form, id);
        dao.insert(form);
        doAfterAdd(form);

    }

    @Transactional
    protected void doAfterAdd(Dto form) {

    }

    @Transactional
    protected void doBeforeAdd(Dto form, String id) {

        if (StringUtils.isEmptyEnhance(form.getId())) {
            form.setId(id);
        }
    }

    @Transactional
    @Override
    public void update(Dto form) {
        doBeforeUpdate(form);
        dao.updateByPrimaryKeySelective(form);
        //只更新有值的
        // dao.updateByPrimaryKey(pojo);
        doAfterUpdate(form);
    }

    @Transactional
    protected void doAfterUpdate(Dto form) {

    }

    @Transactional
    protected void doBeforeUpdate(Dto form) {
    }

    @Transactional
    @Override
    public void delete(String id) {
        boolean canDel = doBeforeDelete(id);
        if (canDel) {
            dao.deleteByPrimaryKey(id);
            doAfterDelete(id);
        } else {
            throw new XException(DATA_DELETE_ERROR);
        }
    }

    @Transactional
    protected void doAfterDelete(String id) {

    }

    @Transactional
    protected boolean doBeforeDelete(String id) {
        return true;
    }

    @Transactional
    @Override
    public void delete(List<String> ids) {
        if (ids.isEmpty()) return;
        ids.stream().forEach(po -> delete(po));
    }

    @Transactional
    @Override
    public Dto save(Dto po) {
        try {
            if (StringUtils.isEmptyEnhance(po.getId())) {
                create(po, Sid.nextShort());
            } else {
                update(po);
            }
            return po;
        } catch (DuplicateKeyException ex) {
            String friendlyMsg = extractDuplicateKeyInfo(ex);
            throw new XException(ResultCode.DuplicateKeyException.getValue(), friendlyMsg);
        } catch (Exception e) {
            throw ExceptionExtractUtils.raiseException(e);
        }
    }
}

3.6 通用CRUD控制器 IdCrudController

基于上层通用服务能力,架构进一步封装统一通用CRUD控制器,将分页查询、列表查询、动态条件查询、新增、修改、删除、批量删除等高频接口全部通用化、标准化。所有单主键业务模块只需继承该控制器,即可零代码拥有全套接口能力,统一接口返回格式、统一Swagger文档规范,极大减少控制层重复开发。

package vip.wayhua.ivy.core.controller;

import io.swagger.v3.oas.annotations.Operation;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.*;
import vip.wayhua.ivy.core.dto.IdDto;
import vip.wayhua.ivy.core.dto.Paged;
import vip.wayhua.ivy.core.dto.R;
import vip.wayhua.ivy.core.form.IdsForm;
import vip.wayhua.ivy.core.form.PageForm;
import vip.wayhua.ivy.core.form.QueryCondition;
import vip.wayhua.ivy.core.form.SortForm;
import vip.wayhua.ivy.core.service.IdCrudService;

import java.util.List;

@RestController
public abstract class IdCrudController<S extends IdCrudService<Dto>, Dto extends IdDto> {


    public IdCrudController(S service) {
        this.service = service;
    }

    protected S service;


    /**
     * 分页查询列表
     *
     * @param query 查询条件
     * @return 分页信息
     */
    @Operation(summary = "分页查询列表", description = "分页查询列表")
    @PostMapping("/page/{page}/{size}")
    public R<Paged<Dto>> page(@Valid @RequestBody Dto query,
                              @PathVariable int page,
                              @PathVariable int size,
                              @RequestParam(required = false, defaultValue = "updatedTime,desc") String sort) {

        PageForm pageQuery = new PageForm(page, size, sort);
        Paged<Dto> paged = service.queryPaged(query, pageQuery);
        return R.success(paged);
    }

    /**
     * 查询列表
     *
     * @param query 查询条件
     * @return 列表信息
     */
    @Operation(summary = "查询列表", description = "查询列表")
    @PostMapping("/list")
    public R<List<Dto>> list(@Valid @RequestBody Dto query,
                             @RequestParam(required = false, defaultValue = "updatedTime,desc") String sort
    ) {
        SortForm sortQuery = new SortForm(sort);
        List<Dto> list = service.queryList(query, sortQuery);
        return R.success(list);
    }


    /**
     * 分页查询列表
     *
     * @param query 查询条件
     * @return 分页信息
     */
    @Operation(summary = "按条件分页查询列表", description = "分页查询列表")
    @PostMapping("/page/condition/{page}/{size}")
    public R<Paged<Dto>> pageCondtion(@Valid @RequestBody QueryCondition query,
                                      @PathVariable int page,
                                      @PathVariable int size,
                                      @RequestParam(required = false, defaultValue = "updatedTime,desc") String sort) {

        PageForm pageQuery = new PageForm(page, size, sort);
        Paged<Dto> paged = service.complexQueryPaged(query, pageQuery);
        return R.success(paged);
    }

    /**
     * 查询列表
     *
     * @param query 查询条件
     * @return 列表信息
     */
    @Operation(summary = "按条件查询列表", description = "查询列表")
    @PostMapping("/list/condition")
    public R<List<Dto>> listCondition(@Valid @RequestBody QueryCondition query,
                                      @RequestParam(required = false, defaultValue = "updatedTime,desc") String sort
    ) {
        SortForm sortQuery = new SortForm(sort);
        List<Dto> list = service.complexQuery(query, sortQuery);
        return R.success(list);
    }

    /**
     * 根据id查询详情
     *
     * @param id ID
     * @return 详情信息
     */

    @Operation(summary = "根据id查询详情", description = "根据id查询详情")
    @GetMapping("/{id}")
    public R<Dto> get(@PathVariable("id") String id) {
        Dto dto = service.queryById(id);
        return R.success(dto);
    }


    @Operation(summary = "保存数据", description = "保存数据")
    @PostMapping
    public R<Dto> save(@Valid @RequestBody Dto dto) {
        Dto r = service.save(dto);
        return R.success(r);
    }


    /**
     * 删除
     *
     * @param id ID
     */

    @Operation(summary = "删除数据", description = "删除数据")
    @DeleteMapping("/{id}")
    public R delete(@PathVariable("id") String id) {
        service.delete(id);
        return R.success();
    }

    /**
     * 批量删除
     *
     * @param req 删除请求参数
     */

    @Operation(summary = "批量删除数据", description = "批量删除数据")
    @ResponseBody
    @DeleteMapping
    public R batchDelete(@Valid @RequestBody IdsForm req) {
        service.delete(req.getIds());
        return R.success();
    }


}

4. 业务模块落地实现(客户模块)

为验证整套通用持久层架构的落地可行性,本文以客户管理模块为实战案例,完成数据库表设计、业务实体、DAO层、Service层、Controller层全链路落地实现。本次业务开发严格遵循项目包结构规范,在 ivy-service-sys-model 模块中维护业务模型类。 项目明确区分模型存放规范:数据库映射相关实体类统一放置在pojo 包下,该类实体可完全通过代码生成器批量生成;而Token、登录信息等非数据库映射的传输对象,统一放置在 dto 包中,通过包路径区分数据库实体与业务传输实体,项目结构更加清晰规范。 整套业务模块无需编写任何基础CRUD代码,仅需少量自定义业务拓展逻辑,充分验证了架构的复用性、简洁性与高效性,后续章节将介绍通过PDMan设计数据库表、搭配自定义代码生成器快速生成全套业务代码的实现方案。

4.1 数据库表结构

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for custom
-- ----------------------------
DROP TABLE IF EXISTS `custom`;
CREATE TABLE `custom`  (
  `id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '主键 主键',
  `name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '姓名 姓名',
  `mobile` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '手机号 手机号',
  `sex` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '性别 性别',
  `sign` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '个性签名 个性签名',
  `created_by` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '创建人 创建人',
  `created_time` datetime NOT NULL COMMENT '创建时间 创建时间',
  `updated_by` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '更新人 更新人',
  `updated_time` datetime NOT NULL COMMENT '更新时间 更新时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '客户表 ' ROW_FORMAT = DYNAMIC;

SET FOREIGN_KEY_CHECKS = 1;

4.2 业务实体类(PO)

数据库映射实体单独维护在pojo包下,严格分层区分数据库PO与传输DTO,结构更规范。实体类通过JPA注解完成表字段映射,同时整合Swagger文档注解,兼顾数据库持久化与接口文档自动生成能力,可通过代码生成器一键快速生成。


@Schema(description = "客户表")
@Table(name = "custom")
public class Custom extends IdDto {


    @Schema(description = "姓名 姓名")
    @Column(name = "name")
    private String name;//姓名 姓名

    @Schema(description = "手机号 手机号")
    @Column(name = "mobile")
    private String mobile;//手机号 手机号

    @Schema(description = "性别 性别")
    @Column(name = "sex")
    private String sex;//性别 性别

    @Schema(description = "个性签名 个性签名")
    @Column(name = "sign")
    private String sign;//个性签名 个性签名

    @Schema(description = "创建人 创建人")
    @Column(name = "created_by")
    private String createdBy;//创建人 创建人

    @Schema(description = "创建时间 创建时间")
    @Column(name = "created_time")
    private LocalDateTime createdTime;//创建时间 创建时间

    @Schema(description = "更新人 更新人")
    @Column(name = "updated_by")
    private String updatedBy;//更新人 更新人

    @Schema(description = "更新时间 更新时间")
    @Column(name = "updated_time")
    private LocalDateTime updatedTime;//更新时间 更新时间

    
    // ... setter/getter

4.3 DAO层编写

DAO层直接继承tk.mybatis通用Mapper,无需手动编写任何CRUD方法与XML文件,自动拥有全套数据库操作能力,极简高效。

package vip.wayhua.ivy.sys.dao;

import tk.mybatis.mapper.common.Mapper;
import vip.wayhua.ivy.sys.pojo.Custom;

public interface CustomMapper extends Mapper<Custom> {
}

如果要添加简单的sql,如select,可以通过@Select注解编写。我找一个实例,不是该项目的。

public interface AccountMapper extends Mapper<Account> {

    @Select("""
            <script>
                    SELECT a.id from account a  join auth_account_role ur ON a.id=ur.user_id  
                                where ur.role_id  in  
                     <foreach collection='roleIds' item='item' index='index' open='(' close=')' separator=','>\s
                      #{item}
                     </foreach>
               </script>
            """)
    List<String> getUserIdByRoleId(@Param("roleIds")List<String> roleIds);
}

4.4 Service层编写

业务Service接口直接继承通用单主键服务接口,无需声明基础CRUD方法,自动继承架构所有通用能力,仅专注业务自定义拓展即可。

package vip.wayhua.ivy.sys.service;

import vip.wayhua.ivy.core.service.IdCrudService;
import vip.wayhua.ivy.sys.pojo.Custom;

public interface CustomService  extends IdCrudService<Custom> {
}

业务Service实现类继承通用实现基类,可按需重写查询条件拼接、新增前置处理、修改前置处理等钩子方法,实现业务自定义逻辑拓展,既复用通用能力,又支持业务个性化需求。

package vip.wayhua.ivy.sys.service.impl;


import org.springframework.stereotype.Service;
import tk.mybatis.mapper.entity.Example;
import vip.wayhua.ivy.core.service.impl.IdCrudServiceImpl;
import vip.wayhua.ivy.core.utils.StringUtils;
import vip.wayhua.ivy.sys.dao.CustomMapper;
import vip.wayhua.ivy.sys.pojo.Custom;
import vip.wayhua.ivy.sys.service.CustomService;

import java.time.LocalDateTime;

@Service
public class CustomServiceImpl extends
        IdCrudServiceImpl<Custom, CustomMapper>
        implements CustomService {
    public CustomServiceImpl(CustomMapper customMapper) {
        super(customMapper);
    }

    @Override
    protected void addExample(Example.Criteria criteria, Custom form) {
        if (!StringUtils.isEmpty(form.getId())) {
            criteria.andEqualTo("id", form.getId());
        }

        // 姓名 姓名
        if (!StringUtils.isEmpty(form.getName())) {
            criteria.andLike("name", "%" + form.getName() + "%");
        }

        // 手机号 手机号
        if (!StringUtils.isEmpty(form.getMobile())) {
            criteria.andEqualTo("mobile", form.getMobile());
        }

        // 性别 性别
        if (!StringUtils.isEmpty(form.getSex())) {
            criteria.andEqualTo("sex", form.getSex());
        }

        // 个性签名 个性签名
        if (!StringUtils.isEmpty(form.getSign())) {
            criteria.andEqualTo("sign", form.getSign());
        }

    }

    @Override
    protected void doBeforeAdd(Custom form, String id) {
        super.doBeforeAdd(form, id);
        form.setCreatedTime(LocalDateTime.now());
        form.setUpdatedTime(LocalDateTime.now());
    }

    @Override
    protected void doBeforeUpdate(Custom form) {
        super.doBeforeUpdate(form);
        form.setUpdatedTime(LocalDateTime.now());
    }
}

4.5 Controller层编写

业务Controller直接继承通用CRUD控制器,无需编写任何接口代码,自动拥有全套分页、列表、条件查询、增删改查接口,真正实现业务模块零接口开发

package vip.wayhua.ivy.sys.controller;

import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import vip.wayhua.ivy.core.controller.IdCrudController;

import vip.wayhua.ivy.sys.pojo.Custom;
import vip.wayhua.ivy.sys.service.CustomService;



@Tag(name = "客户Controller")
@RestController
@RequestMapping("/custom")
public class CustomController extends IdCrudController<CustomService, Custom> {
    public CustomController(CustomService service) {
        super(service);
    }


}

4.6 查看接口文档

QQ_1783165167040.png 另外说明,前面说过降级了Springboot,同时将文档界面改回了knife4j。

按id查询:

QQ_1783165241588

{
  "success": true,
  "status": 200,
  "message": "操作成功",
  "data": {
    "id": "1",
    "name": "黄山",
    "mobile": "13866055391",
    "sex": "男",
    "sign": "梦起飞的地方",
    "createdBy": null,
    "createdTime": "2025-08-21 13:52:43",
    "updatedBy": null,
    "updatedTime": "2025-08-21 13:52:46"
  }
}

自定义条件分页查询请求参数

{
  "items": [
    {
      "fieldName": "sign",
      "operator": "like",
      "value": "起飞",
      "values": []
    }
  ] 
}

QQ_1783172687909.png 返回结果:

{
  "success": true,
  "status": 200,
  "message": "操作成功",
  "data": {
    "totalCount": 1,
    "rows": [
      {
        "id": "1",
        "name": "黄山",
        "mobile": "13866155391",
        "sex": "男",
        "sign": "梦起飞的地方",
        "createdBy": null,
        "createdTime": "2025-08-21 13:52:43",
        "updatedBy": null,
        "updatedTime": "2025-08-21 13:52:46"
      }
    ],
    "pageSize": 10,
    "pageIndex": 1,
    "totalPage": 1
  }
}

5. 网关聚合Knife4j文档配置

项目版本回退后,重新适配Knife4j微服务网关聚合方案,通过Nacos统一配置网关路由与文档聚合规则,实现多服务接口文档统一聚合、统一展示、统一调试,完善整体项目接口体系规范化。

5.1在nacos中配置增加Knife4j配置



#knife4j的网关聚合配置 文档地址:http://{gateway.host}:{gateway.port}/doc.html
knife4j:
  # 聚合swagger文档
  gateway:
    enabled: true
    # 排序规则(tag/operation排序自4.2.0版本新增)
    # 取值:alpha-默认排序规则,官方swagger-ui默认实现,order-Knife4j提供的增强排序规则,开发者可扩展x-order,根据数值来自定义排序
    tags-sorter: order
    operations-sorter: order
    # 指定手动配置的模式(默认为该模式)
    strategy: manual
    # discover:
    #   enabled: true
    #   version: openapi3
    routes:
      - name: 系统管理
        url: /sys/v3/api-docs
        service-name: ivy-service-sys
        context-path: /sys
        order: 1

这时knife4j要添加的内容。

5.2 降级后路由配置文件要修改,完成nacos配置


spring:
  cloud:
    gateway:
      # 全局跨域配置
      globalcors:
        cors-configurations:
          '[/**]':
            allowed-origins: "*"
            allowed-methods: "*"
            allowed-headers: "*"
      # 开启路由日志打印,便于排查问题
      httpclient:
        wiretap: true
      httpserver:
        wiretap: true
  
      routes:
        -  id: ivy-service-sys
           uri: lb://ivy-service-sys
           predicates:
             - Path=/sys/**
           filters:
             - StripPrefix=1


#knife4j的网关聚合配置 文档地址:http://{gateway.host}:{gateway.port}/doc.html
knife4j:
  # 聚合swagger文档
  gateway:
    enabled: true
    # 排序规则(tag/operation排序自4.2.0版本新增)
    # 取值:alpha-默认排序规则,官方swagger-ui默认实现,order-Knife4j提供的增强排序规则,开发者可扩展x-order,根据数值来自定义排序
    tags-sorter: order
    operations-sorter: order
    # 指定手动配置的模式(默认为该模式)
    strategy: manual
    # discover:
    #   enabled: true
    #   version: openapi3
    routes:
      - name: 系统管理
        url: /sys/v3/api-docs
        service-name: ivy-service-sys
        context-path: /sys
        order: 1

5.3 网关doc.html

QQ_1783166380323.png 网关会将所有微服务通过combobox列出来选择。

6、小结

本次脚手架迭代围绕通用持久层能力落地完成全套架构搭建与实战验证,基于tk.mybatis、泛型封装、接口分层、动态条件查询的整套设计,彻底解决传统后台开发代码冗余、类文件臃肿、重复造轮子等问题,整体迭代成果总结如下:

  1. 版本优化:舍弃NextDoc4j,回退Spring Boot版本,适配更易用的Knife4j文档框架;

  2. 实体简化:统一PO/DTO/Form实体,减少类冗余,降低维护成本;

  3. 架构通用:分层封装单主键/组合主键通用服务、控制器,业务模块零代码实现基础CRUD;

  4. 查询灵活:支持普通非空查询、多条件组合查询、模糊查询、区间查询、分页排序等全场景查询能力;

  5. 扩展灵活:预留大量钩子方法,支持业务自定义条件拼接、数据回填、前置处理。

整套架构具备极强的通用性与拓展性,后续可结合代码生成器,一键自动生成实体、Mapper、Service、Controller全套代码,进一步提升后台业务开发效率,实现真正意义上的脚手架极速开发。

7. 项目源码地址

完整初始代码已开源,分支:NextDoc4j初始完成

👉 [gitee.com/wavaya88/iv…]: 持久化