博主是在2018年中就接觸了 RuoYi 專案 這個專案,對於當時國內的開源後臺管理系統來說,RuoYi 算是一個完成度較高,易讀易懂、介面簡潔美觀的前後端不分離專案。
對於當時剛入行還在寫 jsp 模板的博主來說,RuoYi 專案在後臺基礎功能、模組劃分、易用性和頁面美觀度上,對比同期用 Java 開源的前後端不分離後臺專案整體上是高了一個等級的。並且專案 commit 頻繁,程式碼質量不斷提高、bug不斷修復,使得這個專案在今天來說任然是具有學習價值的。
本文博主儘量用一個理性視角帶領大家由淺入深看 RuoYi 專案v4.7.6版本的優秀設計。
RuoYi 專案是一個基於 SpringBoot + Mybatis + Shiro
開發的輕量級 Java 快速開發框架,它包含基礎的後臺管理功能以及許可權控制。專案作者對於 RuoYi 專案的定調是這樣的:
RuoYi是一款基於SpringBoot+Bootstrap的極速後臺開發框架。
RuoYi 是一個 Java EE 企業級快速開發平臺,基於經典技術組合(Spring Boot、Apache Shiro、MyBatis、Thymeleaf、Bootstrap)。內建模組如:部門管理、角色使用者、選單及按鈕授權、資料許可權、系統引數、紀錄檔管理、通知公告等。線上定時任務設定;支援叢集,支援多資料來源,支援分散式事務。
如果想快速瞭解一個專案的設計理念那直接下載這個專案,檢視專案結構即可略知一二。這裡參考官網給出的專案結構:
com.ruoyi
├── ruoyi-common // 工具類
│ └── annotation // 自定義註解
│ └── config // 全域性設定
│ └── constant // 通用常數
│ └── core // 核心控制
│ └── enums // 通用列舉
│ └── exception // 通用異常
│ └── json // JSON資料處理
│ └── utils // 通用類處理
│ └── xss // XSS過濾處理
├── ruoyi-framework // 框架核心
│ └── aspectj // 註解實現
│ └── config // 系統設定
│ └── datasource // 資料許可權
│ └── interceptor // 攔截器
│ └── manager // 非同步處理
│ └── shiro // 許可權控制
│ └── web // 前端控制
├── ruoyi-generator // 程式碼生成(不用可移除)
├── ruoyi-quartz // 定時任務(不用可移除)
├── ruoyi-system // 系統程式碼
├── ruoyi-admin // 後臺服務
├── ruoyi-xxxxxx // 其他模組
由上可知,RuoYi 前後端不分離專案按照模組劃分成了七個模組
ShiroConfig
是最核心的設定,整合了 shiro 框架,給專案提供了許可權管理功能contrller、domain、mapper、service、util、config
等包。如果新增 Spring Boot
啟動類就可以直接作為獨立專案啟動。作為 ruoyi-admin 模組的外掛存在,通過增添 pom 依賴來控制外掛是否開啟mapper、service
層功能程式碼controlelr
層程式碼以及組態檔。也是整個 RuoYi 專案後臺的啟動入口最後再列出專案 ruoyi-admin 的模組依賴圖,簡單講解下各個模組間的依賴關係
看完了 RuoYi 的專案結構與模組依賴關係,大家可以看看自己日常開發業務後臺的專案結構。或多或少,大家都可能遇到過那種一把梭所以程式碼都全部放在同一個 Maven 模組的專案。對比 RuoYi 的專案結構,相信大家都會覺得多模組設計是比單模組更優的設計。
拆分出ruoyi-common模組後,其他外掛模組可以只參照ruoyi-common的通用程式碼就能完成外掛功能開發。拆分出ruoyi-framework模組後,專案中的核心設定程式碼全部放在ruoyi-framework中與ruoyi-admin分離,防止對ruoyi-admin的修改影響到專案核心設定。博主認為合理的模組拆分可以減少模組間的耦合與改動模組所帶來的影響範圍。
通過多模組設計將專案劃分成 common -> system -> framework -> admin
由低到高的核心模組以及外掛形式的 common -> ruoyi-generator|ruoyi-quartz
模組。模組之間儘量鬆耦合,方便模組升級、增減模組。
在 RuoYi 專案中通過 com.ruoyi.framework.aspectj.LogAspect
紀錄檔切面,以自定義紀錄檔註解作為切點來記錄紀錄檔資訊,這樣可以避免在介面中進行重複的操作紀錄檔記錄程式碼編寫,以及紀錄檔記錄發生異常也不影響介面返回。
自定義紀錄檔註解如下:
/**
* 自定義操作紀錄檔記錄註解
*
* @author ruoyi
*/
@Target({ ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {
/**
* 模組
*/
public String title() default "";
/**
* 功能
*/
public BusinessType businessType() default BusinessType.OTHER;
/**
* 操作人類別
*/
public OperatorType operatorType() default OperatorType.MANAGE;
/**
* 是否儲存請求的引數
*/
public boolean isSaveRequestData() default true;
/**
* 是否儲存響應的引數
*/
public boolean isSaveResponseData() default true;
/**
* 排除指定的請求引數
*/
public String[] excludeParamNames() default {};
}
可以看到 LogAspect
註解類中定義了模組名稱、業務操作型別(新增、修改、刪除、匯出等業務操作)、操作人類別(其他、後臺、手機等)、是否儲存請求的引數、是否儲存響應的引數、排除指定的請求引數等六個屬性。我們在使用自定義註解時,通常只用根據介面作用指定模組名稱和業務操作型別就可以,紀錄檔註解使用如下:
@Log(title = "引數管理", businessType = BusinessType.INSERT)
@PostMapping("/add")
@ResponseBody
public AjaxResult addSave(@Validated SysConfig config) {...}
自定義紀錄檔註解切面程式碼如下:
/**
* 操作紀錄檔記錄處理
*
* @author ruoyi
*/
@Aspect
@Component
public class LogAspect {
private static final Logger log = LoggerFactory.getLogger(LogAspect.class);
/** 排除敏感屬性欄位 */
public static final String[] EXCLUDE_PROPERTIES = { "password", "oldPassword" ... };
/** 計算操作消耗時間 */
private static final ThreadLocal<Long> TIME_THREADLOCAL = new NamedThreadLocal<Long>("Cost Time");
/**
* 處理請求前執行
*/
@Before(value = "@annotation(controllerLog)")
public void boBefore(JoinPoint joinPoint, Log controllerLog) {
TIME_THREADLOCAL.set(System.currentTimeMillis());
}
/**
* 處理完請求後執行
*
* @param joinPoint 切點
*/
@AfterReturning(pointcut = "@annotation(controllerLog)", returning = "jsonResult")
public void doAfterReturning(JoinPoint joinPoint, Log controllerLog, Object jsonResult) {
handleLog(joinPoint, controllerLog, null, jsonResult);
}
/**
* 攔截異常操作
*
* @param joinPoint 切點
* @param e 異常
*/
@AfterThrowing(value = "@annotation(controllerLog)", throwing = "e")
public void doAfterThrowing(JoinPoint joinPoint, Log controllerLog, Exception e) {
handleLog(joinPoint, controllerLog, e, null);
}
protected void handleLog(final JoinPoint joinPoint, Log controllerLog,
final Exception e, Object jsonResult)
...
}
}
通過 aop 切面對使用了紀錄檔註解的方法進行三個方面的切入:
@Before(value = "@annotation(controllerLog)")
處理請求前執行記錄紀錄檔記錄開始時間。@AfterReturning(pointcut = "@annotation(controllerLog)", returning = "jsonResult")
處理完請求後執行記錄紀錄檔結束時間,填充操作紀錄檔最後非同步插入。@AfterThrowing(value = "@annotation(controllerLog)", throwing = "e")
以及處理完請求發生異常後執行記錄紀錄檔結束時間,填充操作紀錄檔、異常原因最後非同步插入紀錄檔。在使用了紀錄檔切面後,操作紀錄檔記錄的邏輯與後臺各功能介面的業務邏輯相分離,減少了紀錄檔記錄程式碼的的重複編寫,後期修改紀錄檔記錄邏輯只用修改切面程式碼,提高了操作紀錄檔記錄的可維護性,也避免了紀錄檔記錄發生異常時影響業務介面,使用執行緒池插入紀錄檔記錄還可以縮短介面響應時長。可以看到通過切面完成紀錄檔記錄有這麼多好處。
其實 RuoYi 中不僅僅只有紀錄檔記錄使用了切面處理,像是日常開發中資料過濾許可權、多資料來源切換等也都使用了切面處理。使用切面可以讓我們集中處理單一邏輯、方便增添關注點、減少重複程式碼、對控制層零侵入性以及提高可維護性。
本文目前從模組設計、操作紀錄檔記錄等兩個方面對 RuoYi 專案進行了講解。如果大家也使用過 RuoYi 專案,歡迎大家討論發言給出想法,最後希望本文對大家日常專案開發有所幫助,喜歡的朋友們可以點贊加關注