一种避免写大量CRUD方法的新思路
先看看业务——租赁平台领域图:
0x1
思路分析
要做到所有表共用一个Controller,就需要复用公共的CRUD方法。我们需要满足以下5个条件:
-
不同的表需要通过模型名称进行隔离
-
通过模型名称能找到对应的模型类
-
通过模型类能找到对应的仓库,从而操作数据库
-
对于查询方法,请求参数能转化为查询条件,模型作为查询返回类
-
对于操作方法,请求参数能转化为模型
老规矩——设计先行:
好吧,我承认这张图是刚临时画的,代码早就已经实现了,正如你的产品经理告诉你:
开发小哥哥,客户说后天要上线这个新功能,能不能拜托你今天把这个小需求开发完,晚上测试完就能发布上线了呗。你不得不用脑子先画个蓝图,边写代码边小步迭代,做完后再补设计。
0x2
先造轮子
首先来个聚合控制器接口AggregateController:
/*** 聚合控制器,实现该控制器的Controller,自带CRUD方法** @author Jensen* @公众号 架构师修行录*/public interface AggregateController {// 公共POST分页@PostMapping("/{modelName}/page")default Page<Model> postPage(@PathVariable("modelName") String modelName, @RequestBody Map<String, Object> query) {return convertQuery(getModelClass(modelName), query).page();}// 公共GET分页@GetMapping("/{modelName}/page")default Page<Model> getPage(@PathVariable("modelName") String modelName, Map<String, Object> query) {return convertQuery(getModelClass(modelName), query).page();}// 公共POST列表@PostMapping("/{modelName}/list")default List<Model> postList(@PathVariable("modelName") String modelName, @RequestBody Map<String, Object> query) {return convertQuery(getModelClass(modelName), query).list();}// 公共GET列表@GetMapping("/{modelName}/list")default List<Model> getList(@PathVariable("modelName") String modelName, Map<String, Object> query) {return convertQuery(getModelClass(modelName), query).list();}// 公共详情,通过其他条件查第一条@GetMapping("/{modelName}/detail")default Model detail(@PathVariable("modelName") String modelName, Map<String, Object> 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<String, Object> query) {Model model = convertModel(getModelClass(modelName), query);model.save();return model;}// 公共批量创建@PostMapping("/{modelName}/saveBatch")default void saveBatch(@PathVariable("modelName") String modelName, @RequestBody List<Map<String, Object>> params) {Class<Model> modelClass = getModelClass(modelName);BaseRepository.of(modelClass).save(convertModels(modelClass, params));}// 公共修改@PostMapping({"/{modelName}/update", "/{modelName}/modify"})default void update(@PathVariable("modelName") String modelName, @RequestBody Map<String, Object> 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<Model> getModelClass(String modelName) {Class<Model> modelClass = MappingKit.get("MODEL_NAME", modelName);BizAssert.notNull(modelClass, "Model: {} not found", modelName);return modelClass;}// 通过模型类找到查询类,并把Map参数转换为查询参数static Query convertQuery(Class<Model> modelClass, Map<String, Object> queryMap) {Class<Query> queryClass = MappingKit.get("MODEL_QUERY", modelClass);BizAssert.notNull(queryClass, "Query not found");return BeanKit.ofMap(queryMap, queryClass);}// 通过Map参数转换为模型static Model convertModel(Class<Model> modelClass, Map<String, Object> modelMap) {return BeanKit.ofMap(modelMap, modelClass);}}
路径参数{modelName}就是模型名,比如建了个表user_info,对应的模型是UserInfo,对应的模型名叫userInfo。
下一步,我们需要 通过这个动态的模型名路由到对应的模型上 ,怎么做呢?这时候,我们需要在应用启动时,在初始化仓库实现类中获取到模型后,注入到一个容器。
这里我们先定义一个基础仓库接口:
/*** 基础仓库接口* 针对CRUD进行封装,业务仓库需要实现当前接口** @author Jensen* @公众号 架构师修行录*/public interface BaseRepository<M extends Model, Q extends Query> {// 定义一个存放模型类/查询类-仓库实现类映射的容器Map<Class<?>, Class<?>> REPOSITORY_MAPPINGS = new ConcurrentHashMap<>();/*** 注入仓库类** @param mappingClass Model类/Query类* @param repositoryClass 仓库类*/static <R extends BaseRepository> void inject(Class<?> mappingClass, Class<R> repositoryClass) {REPOSITORY_MAPPINGS.put(mappingClass, repositoryClass);}// TODO 封装的CRUD方法暂且略过}
上面这种使用ConcurrentHashMap作为容器的技术,在各个框架里随处可见,还是挺实用的,大家可以学一学。
/*** 基础仓库实现类* 针对CRUD进行封装,业务仓库实现需要继承当前类** @author Jensen* @公众号 架构师修行录*/public abstract class BaseRepositoryImpl<MP extends BaseMapper<P>, M extends Model, P, Q extends Query> implements BaseRepository<M, Q>, Serializable {// 在仓库实现类构造器中初始化各种映射信息public BaseRepositoryImpl() {// 通过反射工具,拿到具体的模型类final Class<M> modelClass = (Class<M>) ReflectionKit.getSuperClassGenericType(this.getClass(), 1);// 通过反射工具,拿到具体的持久化实体类final Class<P> poClass = (Class<P>) ReflectionKit.getSuperClassGenericType(this.getClass(), 2);// 通过反射工具,拿到具体的查询类final Class<Q> queryClass = (Class<Q>) 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方法暂且略过}
上面的MappingKit是封装好的用于Bean映射的容器工具类:
/*** 用于任意对象映射,按biz隔离(为了复用)*/@UtilityClasspublic final class MappingKit {// Bean容器private final Map<String, Map<Object, Object>> BEAN_MAPPINGS = new ConcurrentHashMap<>();public <K, V> void map(String biz, K key, V value) {Map<Object, Object> mappings = BEAN_MAPPINGS.get(biz);if (mappings == null) {mappings = new ConcurrentHashMap<>();BEAN_MAPPINGS.put(biz, mappings);}mappings.put(key, value);}public <K, V> V get(String field, K source) {Map<Object, Object> mappings = BEAN_MAPPINGS.get(field);if (mappings == null) return null;return (V) mappings.get(source);}}
注入的逻辑比较简单,就是建一个Map<String, Class>,key放模型名,Class放Model类,这样就可以通过模型名找到对应的Model类了。
在仓库实现类初始化时,我们把其他必要信息先进行映射,如通过Model类找RepositoryImpl仓库实现、通过Model类找Query类等等。
至此,我们把映射的工作完成了,大家可以回过头看看AggregateController,就是实际通过模型名modelName从容器中取出模型类、模型类取出仓库的过程了。
还看不懂没关系,结合上面的AC架构图,重新理解几遍~
0x3
造完轮子,开车!
打开方式很简单,比如我按端隔离定义了下面两个控制器,仅仅用几行代码就代替了288个API接口的编写:
/*** 客户端控制器*/@RestController@RequestMapping("/client")public class ClientController implements AggregateController {}
/*** 管理端控制器*/@RestController@RequestMapping("/admin")public class AdminController implements AggregateController {}
0x4
写在最后
希望今天分享的AC架构能提高大家CRUD的效率,也让系统重构不再可怕。
对了,我把它的完整版集成到了我的D3Boot(DDD快速启动)开源基础框架内,也有适用于部分场景使用的CRUDController,大家需要的话可以移步Gitee抄作业。
Gitee源码地址 (差1个ʷ到10k Stars) : https://gitee.com/jensvn/d3boot CrudBoy的最高境界,就是不写CRUD。0x-1
题外话
P.S. 上一篇文章不小心爆了2w5阅读量,粉丝量也终于破2k了
在此真心
感谢大家一直以来的默默支持,
你的每个点赞在看都让我年欣百万
EOF
Java老兵一枚, 曾涉猎 航空/电信/IoT/电商/AI 等多领域SaaS+PaaS产品研发。 D3Boot开源基础框架 作者,现任职SaaS软件架构师。 关注回复“ DDD ”,免费领取 DDD学习大礼包 。 关注回复“ 进群 ”,我拉你进 架构技术交流群 。 点赞在看ᴸᵉᵗᴹᵉ年薪百万