Backend Development 12 min read

Implementing a Generic Aggregate CRUD Controller with Spring MVC and MyBatisPlus

The article demonstrates how to refactor a large PHP API suite into a Java Spring MVC application by creating a generic AggregateController that, through model‑name mapping, reflection‑based repository registration and a thread‑safe MappingKit, provides CRUD endpoints for any table, collapsing hundreds of endpoints into just two controllers for client and admin.

Sohu Tech Products
Sohu Tech Products
Sohu Tech Products
Implementing a Generic Aggregate CRUD Controller with Spring MVC and MyBatisPlus

Hello, code warriors! The author shares a practical technique for refactoring a large PHP project (hundreds of APIs) into a Java project using a single generic controller to handle CRUD operations for many tables.

The business domain is a rental platform with 36 tables, which would normally require 288 API endpoints for client and admin sides. The goal is to reduce this to just two controllers.

Technical points covered: Spring MVC, MyBatisPlus.

Idea analysis : To let one controller serve multiple tables, five conditions must be satisfied:

Different tables must be isolated by model name.

The model name must map to the corresponding model class.

The model class must map to its repository for database operations.

Query parameters must be convertible to query conditions, with the model serving as the query result type.

For modification operations, request parameters must be convertible to a model instance.

By meeting these conditions, a single controller can handle all CRUD needs.

Design first : The author sketches an architecture diagram (omitted) and proceeds to code.

Below is the core interface AggregateController that provides default CRUD methods for any model identified by {modelName} :

/**
 * 聚合控制器,实现该控制器的Controller,自带CRUD方法
 * @author Jensen
 * @公众号 架构师修行录
 */
public interface AggregateController {
    // 公共POST分页
    @PostMapping("/{modelName}/page")
    default Page
postPage(@PathVariable("modelName") String modelName, @RequestBody Map
query) {
        return convertQuery(getModelClass(modelName), query).page();
    }
    // 公共GET分页
    @GetMapping("/{modelName}/page")
    default Page
getPage(@PathVariable("modelName") String modelName, Map
query) {
        return convertQuery(getModelClass(modelName), query).page();
    }
    // 公共POST列表
    @PostMapping("/{modelName}/list")
    default List
postList(@PathVariable("modelName") String modelName, @RequestBody Map
query) {
        return convertQuery(getModelClass(modelName), query).list();
    }
    // 公共GET列表
    @GetMapping("/{modelName}/list")
    default List
getList(@PathVariable("modelName") String modelName, Map
query) {
        return convertQuery(getModelClass(modelName), query).list();
    }
    // 公共详情,通过其他条件查第一条
    @GetMapping("/{modelName}/detail")
    default Model detail(@PathVariable("modelName") String modelName, Map
query) {
        return convertQuery(getModelClass(modelName), query).first();
    }
    // 公共详情,通过ID查
    @GetMapping("/{modelName}/detail/{id}")
    default Model detail(@PathVariable("modelName") String modelName, @PathVariable("id") String id) {
        return BaseRepository.of(getModelClass(modelName)).get(id);
    }
    // 公共创建
    @PostMapping({"/{modelName}/save", "/{modelName}/create"})
    default Model save(@PathVariable("modelName") String modelName, @RequestBody Map
query) {
        Model model = convertModel(getModelClass(modelName), query);
        model.save();
        return model;
    }
    // 公共批量创建
    @PostMapping("/{modelName}/saveBatch")
    default void saveBatch(@PathVariable("modelName") String modelName, @RequestBody List
> params) {
        Class
modelClass = getModelClass(modelName);
        BaseRepository.of(modelClass).save(convertModels(modelClass, params));
    }
    // 公共修改
    @PostMapping({"/{modelName}/update", "/{modelName}/modify"})
    default void update(@PathVariable("modelName") String modelName, @RequestBody Map
query) {
        convertModel(getModelClass(modelName), query).update();
    }
    // 公共删除
    @PostMapping({"/{modelName}/delete/{id}", "/{modelName}/remove/{id}"})
    default void delete(@PathVariable("modelName") String modelName, @PathVariable("id") String id) {
        BaseRepository.of(getModelClass(modelName)).delete(id);
    }
    // 通过模型名找到模型类
    static Class
getModelClass(String modelName) {
        Class
modelClass = MappingKit.get("MODEL_NAME", modelName);
        BizAssert.notNull(modelClass, "Model: {} not found", modelName);
        return modelClass;
    }
    // 通过模型类找到查询类,并把Map参数转换为查询参数
    static Query convertQuery(Class
modelClass, Map
queryMap) {
        Class
queryClass = MappingKit.get("MODEL_QUERY", modelClass);
        BizAssert.notNull(queryClass, "Query not found");
        return BeanKit.ofMap(queryMap, queryClass);
    }
    // 通过Map参数转换为模型
    static Model convertModel(Class
modelClass, Map
modelMap) {
        return BeanKit.ofMap(modelMap, modelClass);
    }
}

The {modelName} path variable corresponds to the model name (e.g., table user_info maps to model UserInfo with name userInfo ).

A base repository interface is defined to hold a concurrent map of model‑to‑repository mappings:

/**
 * 基础仓库接口
 * 针对CRUD进行封装,业务仓库需要实现当前接口
 */
public interface BaseRepository
{
    Map
, Class
> REPOSITORY_MAPPINGS = new ConcurrentHashMap<>();
    /** 注入仓库类 */
    static
void inject(Class
mappingClass, Class
repositoryClass) {
        REPOSITORY_MAPPINGS.put(mappingClass, repositoryClass);
    }
    // TODO 封装的CRUD方法暂且略过
}

An abstract implementation BaseRepositoryImpl uses reflection to discover generic types, registers mappings in BaseRepository , and populates MappingKit for model‑PO‑query relationships:

public abstract class BaseRepositoryImpl
, M extends Model, P, Q extends Query>
        implements BaseRepository
, Serializable {
    public BaseRepositoryImpl() {
        final Class
modelClass = (Class
) ReflectionKit.getSuperClassGenericType(this.getClass(), 1);
        final Class
poClass = (Class
) ReflectionKit.getSuperClassGenericType(this.getClass(), 2);
        final Class
queryClass = (Class
) ReflectionKit.getSuperClassGenericType(this.getClass(), 3);
        BaseRepository.inject(modelClass, this.getClass());
        BaseRepository.inject(queryClass, this.getClass());
        MappingKit.map("MODEL_PO", modelClass, poClass);
        MappingKit.map("MODEL_PO", poClass, modelClass);
        MappingKit.map("MODEL_QUERY", modelClass, queryClass);
        MappingKit.map("MODEL_QUERY", queryClass, modelClass);
        String modelClassName = modelClass.getSimpleName().toLowerCase().substring(0, 1) + modelClass.getSimpleName().substring(1);
        MappingKit.map("MODEL_NAME", modelClassName, modelClass);
    }
    // TODO 封装的CRUD方法暂且略过
}

The utility MappingKit provides a thread‑safe bean container for arbitrary object mappings:

@UtilityClass
public final class MappingKit {
    private final Map
> BEAN_MAPPINGS = new ConcurrentHashMap<>();
    public
void map(String biz, K key, V value) {
        Map
mappings = BEAN_MAPPINGS.computeIfAbsent(biz, k -> new ConcurrentHashMap<>());
        mappings.put(key, value);
    }
    public
V get(String field, K source) {
        Map
mappings = BEAN_MAPPINGS.get(field);
        if (mappings == null) return null;
        return (V) mappings.get(source);
    }
}

Finally, two concrete controllers implement the generic interface—one for the client side and one for the admin side—requiring only a few lines of code:

@RestController
@RequestMapping("/client")
public class ClientController implements AggregateController {
}
@RestController
@RequestMapping("/admin")
public class AdminController implements AggregateController {
}

This approach reduces hundreds of repetitive CRUD endpoints to just two controllers, dramatically improving development efficiency.

The author also mentions that the full implementation is part of the open‑source D3Boot framework (DDD rapid start) and provides a Gitee repository link: https://gitee.com/jensvn/d3boot .

JavaBackend DevelopmentCRUDSpring MVCGeneric ControllerMyBatisPlus
Sohu Tech Products
Written by

Sohu Tech Products

A knowledge-sharing platform for Sohu's technology products. As a leading Chinese internet brand with media, video, search, and gaming services and over 700 million users, Sohu continuously drives tech innovation and practice. We’ll share practical insights and tech news here.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.