寫出漂亮代碼的 45 個小技巧

大家好,我是三友~~

不知道大家有沒有經歷過維護一個已經離職的人的代碼的痛苦,一個方法寫老長,還有很多的 if else ,根本無法閱讀,更不知道代碼背後的含義,最重要的是沒有人可以問,此時只能心裏默默地問候這個留坑的兄弟。。

其實造成這些原因的很大一部分原因是由於代碼規範的問題,如果寫的規範,註釋好,其實很多問題也就解決了。所以本文我就從代碼的編寫規範,格式的優化,設計原則和一些常見的代碼優化的技巧等方面總結了了 45 個小技巧分享給大家,如果不足,歡迎指正。

1、規範命名

命名是寫代碼中最頻繁的操作,比如類、屬性、方法、參數等。好的名字應當能遵循以下幾點:

見名知意

比如需要定義一個變量需要來計數

int i = 0;

名稱 i 沒有任何的實際意義,沒有體現出數量的意思,所以我們應當指明數量的名稱

int count = 0;
能夠讀的出來

如下代碼:

private String sfzh;
private String dhhm;

這些變量的名稱,根本讀不出來,更別說實際意義了。

所以我們可以使用正確的可以讀出來的英文來命名

private String idCardNo;
private String phone;

2、規範代碼格式

好的代碼格式能夠讓人感覺看起來代碼更加舒適。

好的代碼格式應當遵守以下幾點:

好在現在開發工具支持一鍵格式化,可以幫助美化代碼格式。

3、寫好代碼註釋

在《代碼整潔之道》這本書中作者提到了一個觀點,註釋的恰當用法是用來彌補我們在用代碼表達意圖時的失敗。換句話說,當無法通過讀代碼來了解代碼所表達的意思的時候,就需要用註釋來說明。

作者之所以這麼說,是因爲作者覺得隨着時間的推移,代碼可能會變動,如果不及時更新註釋,那麼註釋就容易產生誤導,偏離代碼的實際意義。而不及時更新註釋的原因是,程序員不喜歡寫註釋。(作者很懂啊)

但是這不意味着可以不寫註釋,當通過代碼如果無法表達意思的時候,就需要註釋,比如如下代碼

for (Integer id : ids) {
    if (id == 0) {
        continue;
    }
    //做其他事
}

爲什麼 id == 0 需要跳過,代碼是無法看出來了,就需要註釋了。

好的註釋應當滿足一下幾點:

4、try catch 內部代碼抽成一個方法

try catch 代碼有時會干擾我們閱讀核心的代碼邏輯,這時就可以把 try catch 內部主邏輯抽離成一個單獨的方法

如下圖是 Eureka 服務端源碼中服務下線的實現中的一段代碼

整個方法非常長,try 中代碼是真正的服務下線的代碼實現,finally 可以保證讀鎖最終一定可以釋放。

所以這段代碼其實就可以對核心的邏輯進行抽取。

protected boolean internalCancel(String appName, String id, boolean isReplication) {
    try {
        read.lock();
        doInternalCancel(appName, id, isReplication);
    } finally {
        read.unlock();
    }

    // 剩餘代碼
}

private boolean doInternalCancel(String appName, String id, boolean isReplication) {
    //真正處理下線的邏輯
}

5、方法別太長

方法別太長就是字面的意思。一旦代碼太長,給人的第一眼感覺就很複雜,讓人不想讀下去;同時方法太長的代碼可能讀起來容易讓人摸不着頭腦,不知道哪一些代碼是同一個業務的功能。

我曾經就遇到過一個方法寫了 2000 + 行,各種 if else 判斷,我光理清代碼思路就用了很久,最終理清之後,就用策略模式給重構了。

所以一旦方法過長,可以嘗試將相同業務功能的代碼單獨抽取一個方法,最後在主方法中調用即可。

6、抽取重複代碼

當一份代碼重複出現在程序的多處地方,就會造成程序又臭又長,當這份代碼的結構要修改時,每一處出現這份代碼的地方都得修改,導致程序的擴展性很差。

所以一般遇到這種情況,可以抽取成一個工具類,還可以抽成一個公共的父類。

7、多用 return

在有時我們平時寫代碼的情況可能會出現 if 條件套 if 的情況,當 if 條件過多的時候可能會出現如下情況:

if (條件1) {
    if (條件2) {
        if (條件3) {
            if (條件4) {
                if (條件5) {
                    System.out.println("三友的java日記");
                }
            }
        }
    }
}

面對這種情況,可以換種思路,使用 return 來優化

if (!條件1) {
    return;
}
if (!條件2) {
    return;
}
if (!條件3) {
    return;
}
if (!條件4) {
    return;
}
if (!條件5) {
    return;
}

System.out.println("三友的java日記");

這樣優化就感覺看起來更加直觀

8、if 條件表達式不要太複雜

比如在如下代碼:

if (((StringUtils.isBlank(person.getName())
        || "三友的java日記".equals(person.getName()))
        && (person.getAge() != null && person.getAge() > 10))
        && "漢".equals(person.getNational())) {
    // 處理邏輯
}

這段邏輯,這種條件表達式乍一看不知道是什麼,仔細一看還是不知道是什麼,這時就可以這麼優化

boolean sanyouOrBlank = StringUtils.isBlank(person.getName()) || "三友的java日記".equals(person.getName());
boolean ageGreaterThanTen = person.getAge() != null && person.getAge() > 10;
boolean isHanNational = "漢".equals(person.getNational());

if (sanyouOrBlank
    && ageGreaterThanTen
    && isHanNational) {
    // 處理邏輯
}

此時就很容易看懂 if 的邏輯了

9、優雅地參數校驗

當前端傳遞給後端參數的時候,通常需要對參數進場檢驗,一般可能會這麼寫

@PostMapping
public void addPerson(@RequestBody AddPersonRequest addPersonRequest) {
    if (StringUtils.isBlank(addPersonRequest.getName())) {
        throw new BizException("人員姓名不能爲空");
    }

    if (StringUtils.isBlank(addPersonRequest.getIdCardNo())) {
        throw new BizException("身份證號不能爲空");
    }

    // 處理新增邏輯
}

這種寫雖然可以,但是當字段的多的時候,光校驗就佔據了很長的代碼,不夠優雅。

針對參數校驗這個問題,有第三方庫已經封裝好了,比如 hibernate-validator 框架,只需要拿來用即可。

所以就在實體類上加 @NotBlank、@NotNull 註解來進行校驗

@Data
@ToString
private class AddPersonRequest {

    @NotBlank(message = "人員姓名不能爲空")
    private String name;
    @NotBlank(message = "身份證號不能爲空")
    private String idCardNo;
        
    //忽略
}

此時 Controller 接口就需要方法上就需要加上 @Valid 註解

@PostMapping
public void addPerson(@RequestBody @Valid AddPersonRequest addPersonRequest) {
    // 處理新增邏輯
}

10、統一返回值

後端在設計接口的時候,需要統一返回值

{  
    "code":0,
    "message":"成功",
    "data":"返回數據"
}

不僅是給前端參數,也包括提供給第三方的接口等,這樣接口調用方法可以按照固定的格式解析代碼,不用進行判斷。如果不一樣,相信我,前端半夜都一定會來找你。

Spring 中很多方法可以做到統一返回值,而不用每個方法都返回,比如基於 AOP,或者可以自定義 HandlerMethodReturnValueHandler 來實現統一返回值。

11、統一異常處理

當你沒有統一異常處理的時候,那麼所有的接口避免不了 try catch 操作。

@GetMapping("/{id}")
public Result<T> selectPerson(@PathVariable("id") Long personId) {
    try {
        PersonVO vo = personService.selectById(personId);
        return Result.success(vo);
    } catch (Exception e) {
        //打印日誌
        return Result.error("系統異常");
    }
}

每個接口都得這麼玩,那不得滿屏的 try catch。

所以可以基於 Spring 提供的統一異常處理機制來完成。

12、儘量不傳遞 null 值

這個很好理解,不傳 null 值可以避免方法不支持爲 null 入參時產生的空指針問題。

當然爲了更好的表明該方法是不是可以傳 null 值,可以通過 @NonNull 和 @Nullable 註解來標記。@NonNull 就表示不能傳 null 值,@Nullable 就是可以傳 null 值。

//示例1
public void updatePerson(@Nullable Person person) {
    if (person == null) {
        return;
    }
    personService.updateById(person);
}

//示例2
public void updatePerson(@NonNull Person person) {
    personService.updateById(person);
}

13、儘量不返回 null 值

儘量不返回 null 值是爲了減少調用者對返回值的爲 null 判斷,如果無法避免返回 null 值,可以通過返回 Optional 來代替 null 值。

public Optional<Person> getPersonById(Long personId) {
    return Optional.ofNullable(personService.selectById(personId));
}

如果不想這麼寫,也可以通過 @NonNull 和 @Nullable 表示方法會不會返回 null 值。

14、日誌打印規範

好的日誌打印能幫助我們快速定位問題

好的日誌應該遵循以下幾點:

15、統一類庫

在一個項目中,可能會由於引入的依賴不同導致引入了很多相似功能的類庫,比如常見的 json 類庫,又或者是一些常用的工具類,當遇到這種情況下,應當規範在項目中到底應該使用什麼類庫,而不是一會用 Fastjson,一會使用 Gson。

16、儘量使用工具類

比如在對集合判空的時候,可以這麼寫

public void updatePersons(List<Person> persons) {
    if (persons != null && persons.size() > 0) {
           
    }
}

但是一般不推薦這麼寫,可以通過一些判斷的工具類來寫

public void updatePersons(List<Person> persons) {
    if (!CollectionUtils.isEmpty(persons)) {

    }
}

不僅集合,比如字符串的判斷等等,就使用工具類,不要手動判斷。

17、儘量不要重複造輪子

就拿格式化日期來來說,我們一般封裝成一個工具類來調用,比如如下代碼

private static final SimpleDateFormat DATE_TIME_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

public static String formatDateTime(Date date) {
    return DATE_TIME_FORMAT.format(date);
}

這段代碼看似沒啥問題,但是卻忽略了 SimpleDateFormat 是個線程不安全的類,所以這就會引起坑。

一般對於這種已經有開源的項目並且已經做得很好的時候,比如 Hutool,就可以把輪子直接拿過來用了。

18、類和方法單一職責

單一職責原則是設計模式的七大設計原則之一,它的核心意思就是字面的意思,一個類或者一個方法只做單一的功能。

就拿 Nacos 來說,在 Nacos1.x 的版本中,有這麼一個接口 HttpAgent

這個類只幹了一件事,那就是封裝 http 請求參數,向 Nacos 服務端發送請求,接收響應,這其實就是單一職責原則的體現。

當其它的地方需要向 Nacos 服務端發送請求時,只需要通過這個接口的實現,傳入參數就可以發送請求了,而不需要關心如何攜帶服務端鑑權參數、http 請求參數如何組裝等問題。

19、儘量使用聚合 / 組合代替繼承

繼承的弊端:

所以一般推薦使用聚合 / 組合代替繼承。

聚合 / 組合的意思就是通過成員變量的方式來使用類。

比如說,OrderService 需要使用 UserService,可以注入一個 UserService 而非通過繼承 UserService。

聚合和組合的區別就是,組合是當對象一創建的時候,就直接給屬性賦值,而聚合的方式可以通過 set 方式來設置。

組合:

public class OrderService {

    private UserService userService = new UserService();

}

聚合:

public class OrderService {
    
    private UserService userService;

    public void setUserService(UserService userService) {
        this.userService = userService;
    }
}

20、使用設計模式優化代碼

在平時開發中,使用設計模式可以增加代碼的擴展性。

比如說,當你需要做一個可以根據不同的平臺做不同消息推送的功能時,就可以使用策略模式的方式來優化。

設計一個接口:

public interface MessageNotifier {

    /**
     * 是否支持改類型的通知的方式
     *
     * @param type 0:短信 1:app
     * @return
     */
    boolean support(int type);

    /**
     * 通知
     *
     * @param user
     * @param content
     */
    void notify(User user, String content);

}

短信通知實現:

@Component
public class SMSMessageNotifier implements MessageNotifier {
    @Override
    public boolean support(int type) {
        return type == 0;
    }

    @Override
    public void notify(User user, String content) {
        //調用短信通知的api發送短信
    }
}

app 通知實現:

public class AppMessageNotifier implements MessageNotifier {
    @Override
    public boolean support(int type) {
        return type == 1;
    }

    @Override
    public void notify(User user, String content) {
       //調用通知app通知的api
    }
}

最後提供一個方法,當需要進行消息通知時,調用 notifyMessage,傳入相應的參數就行。

@Resource
private List<MessageNotifier> messageNotifiers;

public void notifyMessage(User user, String content, int notifyType) {
    for (MessageNotifier messageNotifier : messageNotifiers) {
        if (messageNotifier.support(notifyType)) {
            messageNotifier.notify(user, content);
        }
    }
}

假設此時需要支持通過郵件通知,只需要有對應實現就行。

21、不濫用設計模式

用好設計模式可以增加代碼的擴展性,但是濫用設計模式確是不可取的。

public void printPerson(Person person) {
    StringBuilder sb = new StringBuilder();
    if (StringUtils.isNotBlank(person.getName())) {
        sb.append("姓名:").append(person.getName());
    }
    if (StringUtils.isNotBlank(person.getIdCardNo())) {
        sb.append("身份證號:").append(person.getIdCardNo());
    }

    // 省略
    System.out.println(sb.toString());
}

比如上面打印 Person 信息的代碼,用 if 判斷就能夠做到效果,你說我要不用責任鏈或者什麼設計模式來優化一下吧,沒必要。

22、面向接口編程

在一些可替換的場景中,應該引用父類或者抽象,而非實現。

舉個例子,在實際項目中可能需要對一些圖片進行存儲,但是存儲的方式很多,比如可以選擇阿里雲的 OSS,又或者是七牛雲,存儲服務器等等。所以對於存儲圖片這個功能來說,這些具體的實現是可以相互替換的。

所以在項目中,我們不應當在代碼中耦合一個具體的實現,而是可以提供一個存儲接口

public interface FileStorage {
    
    String store(String fileName, byte[] bytes);

}

如果選擇了阿里雲 OSS 作爲存儲服務器,那麼就可以基於 OSS 實現一個 FileStorage,在項目中哪裏需要存儲的時候,只要實現注入這個接口就可以了。

@Autowired
private FileStorage fileStorage;

假設用了一段時間之後,發現阿里雲的 OSS 比較貴,此時想換成七牛雲的,那麼此時只需要基於七牛雲的接口實現 FileStorage 接口,然後注入到 IOC,那麼原有代碼用到 FileStorage 根本不需要動,實現輕鬆的替換。

23、經常重構舊的代碼

隨着時間的推移,業務的增長,有的代碼可能不再適用,或者有了更好的設計方式,那麼可以及時的重構業務代碼。

就拿上面的消息通知爲例,在業務剛開始的時候可能只支持短信通知,於是在代碼中就直接耦合了短信通知的代碼。但是隨着業務的增長,逐漸需要支持 app、郵件之類的通知,那麼此時就可以重構以前的代碼,抽出一個策略接口,進行代碼優化。

24、null 值判斷

空指針是代碼開發中的一個難題,作爲程序員的基本修改,應該要防止空指針。

可能產生空指針的原因:

所以在需要這些的時候,需要強制判斷是否爲 null。前面也提到可以使用 Optional 來優雅地進行 null 值判斷。

25、pojo 類重寫 toString 方法

pojo 一般內部都有很多屬性,重寫 toString 方法可以方便在打印或者測試的時候查看內部的屬性。

26、魔法值用常量表示

public void sayHello(String province) {
    if ("廣東省".equals(province)) {
        System.out.println("靚仔~~");
    } else {
        System.out.println("帥哥~~");
    }
}

代碼裏,廣東省就是一個魔法值,那麼就可以將用一個常量來保存

private static final String GUANG_DONG_PROVINCE = "廣東省";

public void sayHello(String province) {
    if (GUANG_DONG_PROVINCE.equals(province)) {
        System.out.println("靚仔~~");
    } else {
        System.out.println("帥哥~~");
    }
}

27、資源釋放寫到 finally

比如在使用一個 api 類鎖或者進行 IO 操作的時候,需要主動寫代碼需釋放資源,爲了能夠保證資源能夠被真正釋放,那麼就需要在 finally 中寫代碼保證資源釋放。

如圖所示,就是 CopyOnWriteArrayList 的 add 方法的實現,最終是在 finally 中進行鎖的釋放。

28、使用線程池代替手動創建線程

使用線程池還有以下好處:

所以爲了達到更好的利用資源,提高響應速度,就可以使用線程池的方式來代替手動創建線程。

如果對線程池不清楚的同學,可以看一下這篇文章:7000 字 + 24 張圖帶你徹底弄懂線程池

29、線程設置名稱

在日誌打印的時候,日誌是可以把線程的名字給打印出來。

如上圖,日誌打印出來的就是 tom 貓的線程。

所以,設置線程的名稱可以幫助我們更好的知道代碼是通過哪個線程執行的,更容易排查問題。

30、涉及線程間可見性加 volatile

在 RocketMQ 源碼中有這麼一段代碼

在消費者在從服務端拉取消息的時候,會單獨開一個線程,執行 while 循環,只要 stopped 狀態一直爲 false,那麼就會一直循環下去,線程就一直會運行下去,拉取消息。

當消費者客戶端關閉的時候,就會將 stopped 狀態設置爲 true,告訴拉取消息的線程需要停止了。但是由於併發編程中存在可見性的問題,所以雖然客戶端關閉線程將 stopped 狀態設置爲 true,但是拉取消息的線程可能看不見,不能及時感知到數據的修改,還是認爲 stopped 狀態設置爲 false,那麼就還會運行下去。

針對這種可見性的問題,java 提供了一個 volatile 關鍵字來保證線程間的可見性。

所以,源碼中就加了 volatile 關鍵字。

加了 volatile 關鍵字之後,一旦客戶端的線程將 stopped 狀態設置爲 true 時候,拉取消息的線程就能立馬知道 stopped 已經是 false 了,那麼再次執行 while 條件判斷的時候,就不成立,線程就運行結束了,然後退出。

31、考慮線程安全問題

在平時開發中,有時需要考慮併發安全的問題。

舉個例子來說,一般在調用第三方接口的時候,可能會有一個鑑權的機制,一般會攜帶一個請求頭 token 參數過去,而 token 也是調用第三方接口返回的,一般這種 token 都會有個過期時間,比如 24 小時。

我們一般會將 token 緩存到 Redis 中,設置一個過期時間。向第三方發送請求時,會直接從緩存中查找,但是當從 Redis 中獲取不到 token 的時候,我們都會重新請求 token 接口,獲取 token,然後再設置到緩存中。

整個過程看起來是沒什麼問題,但是實則隱藏線程安全問題。

假設當出現併發的時候,同時來兩個線程 AB 從緩存查找,發現沒有,那麼 AB 此時就會同時調用 token 獲取接口。假設 A 先獲取到 token,B 後獲取到 token,但是由於 CPU 調度問題,線程 B 雖然後獲取到 token,但是先往 Redis 存數據,而線程 A 後存,覆蓋了 B 請求的 token。

這下就會出現大問題,最新的 token 被覆蓋了,那麼之後一定時間內 token 都是無效的,接口就請求不通。

針對這種問題,可以使用 double check 機制來優化獲取 token 的問題。

所以,在實際中,需要多考慮考慮業務是否有線程安全問題,有集合讀寫安全問題,那麼就用線程安全的集合,業務有安全的問題,那麼就可以通過加鎖的手段來解決。

32、慎用異步

雖然在使用多線程可以幫助我們提高接口的響應速度,但是也會帶來很多問題。

事務問題

一旦使用了異步,就會導致兩個線程不是同一個事務的,導致異常之後無法正常回滾數據。

cpu 負載過高

之前有個小夥伴遇到需要同時處理幾萬調數據的需求,每條數據都需要調用很多次接口,爲了達到老闆期望的時間要求,使用了多線程跑,開了很多線程,此時會發現系統的 cpu 會飆升

意想不到的異常

還是上面的提到的例子,在測試的時候就發現,由於併發量激增,在請求第三方接口的時候,返回了很多錯誤信息,導致有的數據沒有處理成功。

雖然說慎用異步,但不代表不用,如果可以保證事務的問題,或是 CPU 負載不會高的話,那麼還是可以使用的。

33、減小鎖的範圍

減小鎖的範圍就是給需要加鎖的代碼加鎖,不需要加鎖的代碼不要加鎖。這樣就能減少加鎖的時間,從而可以較少鎖互斥的時間,提高效率。

比如 CopyOnWriteArrayList 的 addAll 方法的實現,lock.lock(); 代碼完全可以放到代碼的第一行,但是作者並沒有,因爲前面判斷的代碼不會有線程安全的問題,不放到加鎖代碼中可以減少鎖搶佔和佔有的時間。

34、有類型區分時定義好枚舉

比如在項目中不同的類型的業務可能需要上傳各種各樣的附件,此時就可以定義好不同的一個附件的枚舉,來區分不同業務的附件。

不要在代碼中直接寫死,不定義枚舉,代碼閱讀起來非常困難,直接看到數字都是懵逼的。。

35、遠程接口調用設置超時時間

比如在進行微服務之間進行 rpc 調用的時候,又或者在調用第三方提供的接口的時候,需要設置超時時間,防止因爲各種原因,導致線程” 卡死 “在那。

我以前就遇到過線上就遇到過這種問題。當時的業務是訂閱 kafka 的消息,然後向第三方上傳數據。在某個週末,突然就接到電話,說數據無法上傳了,通過排查線上的服務器才發現所有的線程都線程” 卡死 “了,最後定位到代碼才發現原來是沒有設置超時時間。

36、集合使用應當指明初始化大小

比如在寫代碼的時候,經常會用到 List、Map 來臨時存儲數據,其中最常用的就是 ArrayList 和 HashMap。但是用不好可能也會導致性能的問題。

比如說,在 ArrayList 中,底層是基於數組來存儲的,數組是一旦確定大小是無法再改變容量的。但不斷的往 ArrayList 中存儲數據的時候,總有那麼一刻會導致數組的容量滿了,無法再存儲其它元素,此時就需要對數組擴容。所謂的擴容就是新創建一個容量是原來 1.5 倍的數組,將原有的數據給拷貝到新的數組上,然後用新的數組替代原來的數組。

在擴容的過程中,由於涉及到數組的拷貝,就會導致性能消耗;同時 HashMap 也會由於擴容的問題,消耗性能。所以在使用這類集合時可以在構造的時候指定集合的容量大小。

37、儘量不要使用 BeanUtils 來拷貝屬性

在開發中經常需要對 JavaBean 進行轉換,但是又不想一個一個手動 set,比較麻煩,所以一般會使用屬性拷貝的一些工具,比如說 Spring 提供的 BeanUtils 來拷貝。不得不說,使用 BeanUtils 來拷貝屬性是真的舒服,使用一行代碼可以代替幾行甚至十幾行代碼,我也喜歡用。

但是喜歡歸喜歡,但是會帶來性能問題,因爲底層是通過反射來的拷貝屬性的,所以儘量不要用 BeanUtils 來拷貝屬性。

比如你可以裝個 JavaBean 轉換的插件,幫你自動生成轉換代碼;又或者可以使用性能更高的 MapStruct 來進行 JavaBean 轉換,MapStruct 底層是通過調用(settter/getter)來實現的,而不是反射來快速執行。

38、使用 StringBuilder 進行字符串拼接

如下代碼:

String str1 = "123";
String str2 = "456";
String str3 = "789";
String str4 = str1 + str2 + str3;

使用 + 拼接字符串的時候,會創建一個 StringBuilder,然後將要拼接的字符串追加到 StringBuilder,再 toString,這樣如果多次拼接就會執行很多次的創建 StringBuilder,z 執行 toString 的操作。

所以可以手動通過 StringBuilder 拼接,這樣只會創建一次 StringBuilder,效率更高。

StringBuilder sb = new StringBuilder();
String str = sb.append("123").append("456").append("789").toString();

39、@Transactional 應指定回滾的異常類型

平時在寫代碼的時候需要通過 rollbackFor 顯示指定需要對什麼異常回滾,原因在這:

默認是隻能回滾 RuntimeException 和 Error 異常,所以需要手動指定,比如指定成 Expection 等。

40、謹慎方法內部調用動態代理的方法

如下事務代碼

@Service
public class PersonService {
    
    public void update(Person person) {
        // 處理
        updatePerson(person);
    }

    @Transactional(rollbackFor = Exception.class)
    public void updatePerson(Person person) {
        // 處理
    }

}

update 調用了加了 @Transactional 註解的 updatePerson 方法,那麼此時 updatePerson 的事務就是失效。

其實失效的原因不是事務的鍋,是由 AOP 機制決定的,因爲事務是基於 AOP 實現的。AOP 是基於對象的代理,當內部方法調用時,走的不是動態代理對象的方法,而是原有對象的方法調用,如此就走不到動態代理的代碼,就會失效了。

如果實在需要讓動態代理生效,可以注入自己的代理對象

@Service
public class PersonService {

    @Autowired
    private PersonService personService;

    public void update(Person person) {
        // 處理
        personService.updatePerson(person);
    }

    @Transactional(rollbackFor = Exception.class)
    public void updatePerson(Person person) {
        // 處理
    }

}

41、需要什麼字段 select 什麼字段

查詢全字段有以下幾點壞處:

增加不必要的字段的網絡傳輸

比如有些文本的字段,存儲的數據非常長,但是本次業務使用不到,但是如果查了就會把這個數據返回給客戶端,增加了網絡傳輸的負擔

會導致無法使用到覆蓋索引

比如說,現在有身份證號和姓名做了聯合索引,現在只需要根據身份證號查詢姓名,如果直接 select name 的話,那麼在遍歷索引的時候,發現要查詢的字段在索引中已經存在,那麼此時就會直接從索引中將 name 字段的數據查出來,返回,而不會繼續去查找聚簇索引,減少回表的操作。

所以建議是需要使用什麼字段查詢什麼字段。比如 mp 也支持在構建查詢條件的時候,查詢某個具體的字段。

 Wrappers.query().select("name");

42、不循環調用數據庫

不要在循環中訪問數據庫,這樣會嚴重影響數據庫性能。

比如需要查詢一批人員的信息,人員的信息存在基本信息表和擴展表中,錯誤的代碼如下:

public List<PersonVO> selectPersons(List<Long> personIds) {
    List<PersonVO> persons = new ArrayList<>(personIds.size());
    List<Person> personList = personMapper.selectByIds(personIds);
    for (Person person : personList) {
        PersonVO vo = new PersonVO();
        PersonExt personExt = personExtMapper.selectById(person.getId());
        // 組裝數據
        persons.add(vo);
    }
    return persons;
}

遍歷每個人員的基本信息,去數據庫查找。

正確的方法應該先批量查出來,然後轉成 map:

public List<PersonVO> selectPersons(List<Long> personIds) {
    List<PersonVO> persons = new ArrayList<>(personIds.size());
    List<Person> personList = personMapper.selectByIds(personIds);
        //批量查詢,轉換成Map
    List<PersonExt> personExtList = personExtMapper.selectByIds(person.getId());
    Map<String, PersonExt> personExtMap = personExtList.stream().collect(Collectors.toMap(PersonExt::getPersonId, Function.identity()));
    for (Person person : personList) {
        PersonVO vo = new PersonVO();
        //直接從Map中查找
        PersonExt personExt = personExtMap.get(person.getId());
        // 組裝數據
        persons.add(vo);
    }
    return persons;
}

43、用業務代碼代替多表 join

如上面代碼所示,原本也可以將兩張表根據人員的 id 進行關聯查詢。但是不推薦這麼,阿里也禁止多表 join 的操作

而之所以會禁用,是因爲 join 的效率比較低。

MySQL 是使用了嵌套循環的方式來實現關聯查詢的,也就是 for 循環會套 for 循環的意思。用第一張表做外循環,第二張表做內循環,外循環的每一條記錄跟內循環中的記錄作比較,符合條件的就輸出,這種效率肯定低。

44、裝上阿里代碼檢查插件

我們平時寫代碼由於各種因爲,比如什麼領導啊,項目經理啊,會一直催進度,導致寫代碼都來不及思考,怎麼快怎麼來,cv 大法上線,雖然有心想寫好代碼,但是手確不聽使喚。所以我建議裝一個阿里的代碼規範插件,如果有代碼不規範,會有提醒,這樣就可以知道哪些是可以優化的了。

如果你有強迫症,相信我,裝了這款插件,你的代碼會寫的很漂亮。

45、及時跟同事溝通

寫代碼的時候不能閉門造車,及時跟同事溝通,比如剛進入一個新的項目的,對項目工程不熟悉,一些技術方案不瞭解,如果上來就直接寫代碼,很有可能就會踩坑。

參考資料:

《代碼整潔之道》

《阿里巴巴 Java 開發手冊》

如何寫出讓人抓狂的代碼?

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/0kqDues5Uv4kICTin1i_0Q