【進階篇】Java 實際開發中積累的幾個小技巧(二)

CodeBlogMan發表於2024-04-16

目錄
  • 前言
  • 六、自定義註解
    • 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 的繼承、封裝和多型,與物件導向的思想一脈相承。


文章小結

作為開發技巧系列文章的第二篇,本文的內容不多但貴在實用。在之後的文章中我會分享一些關於真實專案中處理高併發、快取的使用、非同步/解耦等內容,敬請期待。

那麼今天的分享到這裡就暫時結束了,如有不足和錯誤,還請大家指正。或者你有其它想說的,也歡迎大家在評論區交流!

相關文章