哈嘍,各位代碼戰士們,我是Jensen,一個夢想著和大家一起在代碼的海洋里遨游,順便撿起那些散落的知識點的程序員小伙伴。
今天,我繼續給大家帶來一個超級無敵霹靂的編碼新招式,來自我最近的親身實踐,我把公司的PHP工程(兩個端,幾百個接口)重構到Java工程上來,僅僅用了兩天!
先看看業務——租賃平臺領域圖:
乍一看這張領域圖就不簡單(表梳理、核心業務梳理、建模等花了我兩天),順便用腳趾頭數了一下,總共是36張表,只談常規CRUD方法的話,要寫36*4=144個API接口,這里還涉及客戶端和管理端API的隔離,那翻個倍就是288個API接口了唄。
CrudBoy是不可能的,這輩子都不可能的。
你信不信,我只寫兩個Controller,就能把兩個端的CRUD全部搞定!
本文涉及技術點:SpringMVC、MybatisPlus。
問題來了,一個Controller怎么做到多張表的CRUD(增刪查改)呢?
要做到所有表共用一個Controller,就需要復用公共的CRUD方法。我們需要滿足以下5個條件:
只需要解決上述問題,一個Controller即可解決所有表的CRUD需求。
老規矩——設計先行:
好吧,我承認這張圖是剛臨時畫的,代碼早就已經實現了,正如你的產品經理告訴你:
開發小哥哥,客戶說后天要上線這個新功能,能不能拜托你今天把這個小需求開發完,晚上測試完就能發布上線了唄。
你不得不用腦子先畫個藍圖,邊寫代碼邊小步迭代,做完后再補設計。
首先來個聚合控制器接口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作為容器的技術,在各個框架里隨處可見,還是挺實用的,大家可以學一學。
接下來,我們再對MybatisPlus的BaseMapper類進行淺封裝,作為基礎倉庫實現類,針對CRUD進行二次封裝:
/** * 基礎倉庫實現類 * 針對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架構圖,重新理解幾遍~
打開方式很簡單,比如我按端隔離定義了下面兩個控制器,僅僅用幾行代碼就代替了288個API接口的編寫:
/** * 客戶端控制器 */@RestController@RequestMapping("/client")public class ClientController implements AggregateController {}
/** * 管理端控制器 */@RestController@RequestMapping("/admin")public class AdminController implements AggregateController {}
希望今天分享的AC架構能提高大家CRUD的效率,也讓系統重構不再可怕。
對了,我把它的完整版集成到了我的D3Boot(DDD快速啟動)開源基礎框架內,也有適用于部分場景使用的CRUDController,大家需要的話可以移步Gitee抄作業。
本文鏈接:http://www.tebozhan.com/showinfo-26-86684-0.html一種避免寫大量CRUD方法的新思路
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com