- 前言
- 六、自定義註解
- 6.1定義註解
- 6.2切面實現
- 6.3業務使用
- 七、抽象類和介面
- 7.1隔離業務層與 ORM 層
- 7.2隔離子系統的業務實現
- 7.3選擇對比
- 文章小結
前言
筆者目前從事一線 Java 開發今年是第 3 個年頭了,從 0-1 的 SaaS、PaaS 的專案做過,基於多租戶的標準化開發專案也做過,專案的 PM 也做過...
在實際的開發中積累了一些技巧和經驗,包括線上 bug 處理、日常業務開發、團隊開發規範等等。現在在這裡分享出來,作為成長的記錄和知識的更新,希望與大家共勉。
免責宣告:以下所有demo、程式碼和測試都是出自筆者本人的構思和實踐,不涉及企業隱私和商業機密,屬於個人的知識積累分享。
六、自定義註解
Spring 中的自定義註解可以靈活地定製專案開發時需要的切面 AOP 操作,一般來說在介面處設定的自定義註解是使用的最多的。下面筆者以一個專案全域性通用的介面請求操作日誌持久化為例子,分享一下自定義註解開發的一些小技巧。
6.1定義註解
這一步先定義出具體的註解狀態和屬性:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Inherited
public @interface OperateLog {
/**
* 線索Id
*/
String trackId() default "";
/**
* 具體操作行為
*/
OperationEnum operation();
}
其中的具體行為操作列舉需要提前準備好,方便後續切面內的日誌操作持久化:
@Getter
@RequiredArgsConstructor
public enum OperationEnum {
XX_MODULE_ADD("xx模組","新增xx"),
XX_MODULE_UPDATE("xx模組","修改xx");
private final String module;
private final String detail;
}
6.2切面實現
這一步是具體的切面實現,切面實現的關鍵在於:切面在註解宣告方法的哪種順序執行,即選擇 5 種通知的哪一種。
對於日誌記錄這種型別的,一般來說切面會在方法返回結果之後執行(@AfterReturning),即操作有結果後再記錄日誌;而像使用者登入或者介面許可權校驗的自定義註解,一般來說切面會在方法呼叫前(@Before)就執行。具體切面裡的邏輯如下:
@Aspect
@Component
public class OperateLogAOP {
@Resource
private OperationLogService operationLogService;
/**
* 切面在方法返回結果之後執行,即操作有結果後再記錄日誌
* @param joinPoint
* @param operateLog
*/
@AfterReturning(value = "@annotation(operateLog)")
public void operateLogAopMethod(JoinPoint joinPoint, OperateLog operateLog){
//從自定義註解中取出引數
String trackId = operateLog.trackId();
Assert.hasText(trackId, "trackId param error!");
//處理引數的值,即輸入的業務id值
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Object[] args = joinPoint.getArgs();
String businessLogId = (String) AopUtils.getFieldValue(args, methodSignature, trackId);
//操作描述
String module = operateLog.operation().getModule();
String detail = operateLog.operation().getDetail();
//獲取請求 http request
HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
//持久化入庫
OperationLog operationLog = OperationLog.builder()
.trackId(businessLogId).module(module).detail(detail)
.ip(IpUtil.getUserIp(request)).createTime(new Date())
.operatorUuid(UserDataBuilder.get().getUserUuid())
.operatorName(UserDataBuilder.get().getUserName())
.build();
operationLogService.save(operationLog);
}
}
6.3業務使用
前面兩步完成後,就到最後的業務使用了。一般來說日誌型別的自定義註解會放在 Controller 層的介面前,具體示例如下:
/**
* 編輯
* @return 是否成功
*/
@PostMapping("update")
@OperateLog(trackId = "studyDTO.id", operation = OperationEnum.XX_MODULE_UPDATE)
public BaseResponse<Boolean> updateStudy(@RequestBody StudyDTO studyDTO) {
return ResultUtils.success(studyService.updateStudy(studyDTO));
}
七、抽象類和介面
為什麼在業務設計的時候需要注意抽象類和介面的運用呢?如果只是依靠類的單一範圍原則,那麼業務的實現會擰成一大坨,並且程式碼的耦合會變緊。
抽象類非常適合多個子類共享共同特徵和屬性,但也相容自己獨有的行為情況,同時為子類的定製實現留出空間。
而介面則是解耦的最基本工具,介面允許將方法的定義與其實現分開,這種分離使得多個不相關的類能夠實現同一組方法,從而保證了專案中不同部分之間的相互通訊。
7.1隔離業務層與 ORM 層
-
Mongo 示例
抽象類的繼承關係如下:
@Service public class WorkerServiceImpl extends AbstractWorkerServiceImpl implements WorkerService {}
public abstract class AbstractWorkerServiceImpl extends BaseServiceImpl<Worker, String> implements IWorkerService {}
介面的繼承關係如下:
public interface WorkerService extends IWorkerService {}
public interface IWorkerService extends BaseService<Worker, String> {}
底層的繼承和實現:
/** * 以下抽象類和介面中還有自定義的一些資料庫方法,與 MongoTemplate 和 MongoRepository 形成互補 */ public abstract class BaseServiceImpl<T, ID> implements BaseService<T, ID> {}
-
MySQL 示例
至於 MySQL 可以直接引用 mybaitisplus 的包,裡面有現成的實現,都是一些資料庫語句的 Java 實現。必要的情況下還可以同時引入 mybaitis 包來處理一些複雜的 sql 語句。
抽象類的繼承關係如下:
@Service public class StudyServiceImpl extends ServiceImpl<StudyMapper, Study> implements StudyService {}
介面的繼承關係如下:
public interface StudyService extends IService<Study> {}
底層的繼承和實現:
/** * 以下抽象類和介面都來源於 com.baomidou.mybatisplus 包 */ public class ServiceImpl<M extends BaseMapper<T>, T> implements IService<T> {}
7.2隔離子系統的業務實現
-
facade模式
facade 稱為外觀模式:為子系統中的各類(或方法)提供簡潔一致的入口,隱藏子系統的複雜性。facade 層也通常充當一箇中介的角色,為上層的呼叫者提供統一介面的同時,不直接暴露底層的實現細節。
例如在遠端呼叫時,facade 層可以提供一個顆粒度比較粗的介面,它負責將外部請求轉發給合適的服務進行處理。
service層,只關心資料,在 service 內直接注入mapper
/** * 只關心資料,本質上是資料庫的一些操作 */ @Service public class PersonService extends ServiceImpl<PersonMapper, Person> { @Resource private PersonMapper mapper; //其它資料庫語句 ... }
facade 層,只關心業務,在 facade內直接注入 service
/** * 只關心業務,不繼承也不實現,被 controller 層引用 */ @Service public class PersonFacade { @Resource private PersonService service; //業務具體方法邏輯 ... }
上述模式的優點是將資料處理和業務處理明確地分開,業務、資料與檢視層的通訊靠的是 Bean 注入的方式,並不是強依賴於類的繼承和介面實現,對於外部來說很好地遮蔽了具體的實現邏輯。
但是可能潛在的缺點也有:當業務簡單的時候,facade 與 service 之間的邊界會比較模糊,即 facade 層的存在可能是沒有必要的。
7.3選擇對比
如果在實際專案裡的話,這兩者只能選其一。
筆者對於兩者在不同的專案中都使用過,實踐下來的建議是:選擇抽象類和介面做業務與資料的隔離。
原因無它:抽象類和介面的搭配使用從本質上詮釋了 Java 的繼承、封裝和多型,與物件導向的思想一脈相承。
文章小結
作為開發技巧系列文章的第二篇,本文的內容不多但貴在實用。在之後的文章中我會分享一些關於真實專案中處理高併發、快取的使用、非同步/解耦等內容,敬請期待。
那麼今天的分享到這裡就暫時結束了,如有不足和錯誤,還請大家指正。或者你有其它想說的,也歡迎大家在評論區交流!