這篇文章我會總結一些實用的有助於提高程式碼質量的建議,內容較多,建議收藏!
內容概覽:
註解、反射和動態代理是 Java 語言中的利器,使用得當的話,可以大大簡化程式碼編寫,並提高程式碼的可讀性、可維護性和可延伸性。
我們可以利用 註解 + 反射 和 註解+動態代理 來提取類、類屬性或者類方法通用處理邏輯,進而避免重複的程式碼。雖然可能會帶來一些效能損耗,但與其帶來的好處相比還是非常值得的。
通過 註解 + 反射 這種方式,可以在執行時動態地獲取類的資訊、屬性和方法,並對它們進行通用處理。比如說在通過 Spring Boot 中通過註解驗證介面輸入的資料就是這個思想的運用,我們通過註解來標記需要驗證的引數,然後通過反射獲取屬性的值,並進行相應的驗證。
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class PersonRequest {
@NotNull(message = "classId 不能為空")
private String classId;
@Size(max = 33)
@NotNull(message = "name 不能為空")
private String name;
@Pattern(regexp = "(^Man$|^Woman$|^UGM$)", message = "sex 值不在可選範圍")
@NotNull(message = "sex 不能為空")
private String sex;
@Region
private String region;
@PhoneNumber(message = "phoneNumber 格式不正確")
@NotNull(message = "phoneNumber 不能為空")
private String phoneNumber;
}
相關閱讀:一坨一坨的 if/else 引數校驗,終於被 SpringBoot 引數校驗元件整乾淨了! 。
通過 註解 + 動態代理 這種方式,可以在執行時生成代理物件,從而實現通用處理邏輯。比如說 Spring 框架中,AOP 模組正是利用了這種思想,通過在目標類或方法上新增註解,動態生成代理類,並在代理類中加入相應的通用處理邏輯,比如事務管理、紀錄檔記錄、快取處理等。同時,Spring 也提供了兩種代理實現方式,即基於 JDK 動態代理和基於 CGLIB 動態代理(JDK 動態代理底層基於反射,CGLIB 動態代理底層基於位元組碼生成),使用者可以根據具體需求選擇不同的實現方式。
@LogRecord(content = "修改了訂單的配送地址:從「#oldAddress」, 修改到「#request.address」",
bizNo="#request.deliveryOrderNo")
public void modifyAddress(updateDeliveryRequest request){
// 查詢出原來的地址是什麼
LogRecordContext.putVariable("oldAddress", DeliveryService.queryOldAddress(request.getDeliveryOrderNo()));
// 更新派送資訊 電話,收件人、地址
doUpdate(request);
}
相關閱讀:美團技術團隊:如何優雅地記錄操作紀錄檔? 。
程式碼沒必要一味追求「短」,是否易於閱讀和維護也非常重要。像炫技式的單行程式碼就非常難以理解、排查和修改起來都比較麻煩且耗時。
反例:
if (response.getData() != null && CollectionUtils.isNotEmpty(response.getData().getShoppingCartDTOList())) {
cartList = response.getData().getShoppingCartDTOList().stream().map(CartResponseBuilderV2::buildCartList).collect(Collectors.toList());
}
正例:
T data = response.getData();
if (data != null && CollectionUtils.isNotEmpty(data.getShoppingCartDTOList())) {
cartList = StreamUtil.map(data.getShoppingCartDTOList(), CartResponseBuilderV2::buildCartList);
}
相關閱讀:一個較重的程式碼壞味:「炫技式」的單行程式碼 。
基於介面而非實現程式設計是一種常用的程式設計正規化,也是一種非常好的程式設計習慣,一定要牢記於心!
基於介面程式設計可以讓程式碼更加靈活、更易擴充套件和維護,因為介面可以為不同的實現提供相同的方法簽名(方法的名稱、引數型別和順序以及返回值型別)和契約(介面中定義的方法的行為和約束,即方法應該完成的功能和要求),這使得實現類可以相互替換,而不必改變程式碼的其它部分。另外,基於介面程式設計還可以幫助我們避免過度依賴具體實現類,降低程式碼的耦合性,提高程式碼的可測試性和可重用性。
就比如說在編寫簡訊服務、郵箱服務、儲存服務等常用第三方服務的程式碼時,我們可以先先定義一個介面,介面中抽象出具體的方法,然後實現類再去實現這個介面。
public interface SmsSender {
SmsResult send(String phone, String content);
SmsResult sendWithTemplate(String phone, String templateId, String[] params);
}
/*
* 阿里雲簡訊服務
*/
public class AliyunSmsSender implements SmsSender {
...
}
/*
* 騰訊雲簡訊服務
*/
public class TencentSmsSender implements SmsSender {
...
}
拿簡訊服務這個例子來說,如果需要新增一個百度雲簡訊服務,直接實現 SmsSender
即可。如果想要替換專案中使用的簡訊服務也比較簡單,修改的程式碼非常少,甚至說可以直接通過修改設定無需改動程式碼就能輕鬆更改簡訊服務。
儘量不要將運算元據庫、快取、中介軟體的程式碼和業務處理程式碼混合在一起,而是要單獨抽取一個類或者封裝一個介面,這樣程式碼更清晰易懂,更容易維護,一些通用邏輯也方便統一維護。
資料庫:
public interface UserRepository extends JpaRepository<User, Long> {
...
}
快取:
@Repository
public class UserRedis {
@Autowired
private RedisTemplate<String, String> redisTemplate;
public User save(User user) {
}
}
訊息佇列:
// 取消訂單訊息生產者
public class CancelOrderProducer{
...
}
// 取消訂單訊息消費者
public class CancelOrderConsumer{
...
}
這個是老生常談了,最基本的規範。一定不要把業務程式碼應該放在 Controller 中,業務程式碼就是要交給 Service 處理。
業務程式碼放到 Service 的好處 :
@Value
注入值會失敗。錯誤案例:
@RestController
public class UserController {
@Autowired
private UserRepository userRepository;
@GetMapping("/users/{id}")
public Result<UserVO> getUser(@RequestParam(name = "userId", required = true) Long userId) {
User user = repository.findById(id)
.orElseThrow(() -> new UserNotFoundException(id));
UserVO userVO = new UserVO();
BeanUtils.copyProperties(user, userVO);//演示使用
// 可能還有其他業務操作
...
return Result.success(userVO);
}
...
}
靜態函數/方法不屬於某個特定的物件,而是屬於這個類。呼叫靜態函數無需建立物件,直接通過類名即可呼叫。
靜態函數最適合放在工具類中定義,比如檔案操作、格式轉換、網路請求等。
/**
* 檔案工具類
*/
public class FileUtil extends PathUtil {
/**
* 檔案是否為空<br>
* 目錄:裡面沒有檔案時為空 檔案:檔案大小為0時為空
*
* @param file 檔案
* @return 是否為空,當提供非目錄時,返回false
*/
public static boolean isEmpty(File file) {
// 檔案為空或者檔案不存在直接返回 true
if (null == file || false == file.exists()) {
return true;
}
if (file.isDirectory()) {
// 檔案是資料夾的情況
String[] subFiles = file.list();
return ArrayUtil.isEmpty(subFiles);
} else if (file.isFile()) {
// 檔案不是資料夾的情況
return file.length() <= 0;
}
return false;
}
}
Java 的一大優勢就是生態特別好, 包含了許多好用的工具類庫和框架,幾乎覆蓋了所有的需求場景。很多事情我們完全不需要自己從頭開始做,利用現有的穩定可靠的工具類庫可以大大提高開發效率。
比如 Excel 檔案處理,你可以考慮下面這幾個開源的工具類庫:
再比如 PDF 檔案處理:
我的網站上總結了 Java 開發常用的一些工具類庫,可以作為參考:https://javaguide.cn/open-source-project/tool-library.html 。
實際開發專案的過程中,我們應該合理地使用現有的設計模式來優化我們的程式碼。不過,切忌為了使用設計模式而使用。
新來了個同事,設計模式用的是真優雅呀!這篇文章中介紹了 9 種在原始碼中非常常見的設計模式:
策略模式是一種常見的優化條件邏輯的方法。當程式碼中有一個包含大量條件邏輯(即 if 語句)的方法時,你應該考慮使用策略模式對其進行優化,這樣程式碼更加清晰,同時也更容易維護。
假設我們有這樣一段程式碼:
public class IfElseDemo {
public double calculateInsurance(double income) {
if (income <= 10000) {
return income*0.365;
} else if (income <= 30000) {
return (income-10000)*0.2+35600;
} else if (income <= 60000) {
return (income-30000)*0.1+76500;
} else {
return (income-60000)*0.02+105600;
}
}
}
下面是使用策略+工廠模式重構後的程式碼:
首先定義一個介面 InsuranceCalculator
,其中包含一個方法 calculate(double income)
,用於計算保險費用。
public interface InsuranceCalculator {
double calculate(double income);
}
然後,分別建立四個類來實現這個介面,每個類代表一個保險費用計算方式。
public class FirstLevelCalculator implements InsuranceCalculator {
public double calculate(double income) {
return income * 0.365;
}
}
public class SecondLevelCalculator implements InsuranceCalculator {
public double calculate(double income) {
return (income - 10000) * 0.2 + 35600;
}
}
public class ThirdLevelCalculator implements InsuranceCalculator {
public double calculate(double income) {
return (income - 30000) * 0.1 + 76500;
}
}
public class FourthLevelCalculator implements InsuranceCalculator {
public double calculate(double income) {
return (income - 60000) * 0.02 + 105600;
}
}
最後,我們可以為每個策略類新增一個唯一的識別符號,例如字串型別的 name
屬性。然後,在工廠類中建立一個 Map
來儲存策略物件和它們的識別符號之間的對映關係(也可以用 switch 來維護對映關係)。
import java.util.HashMap;
import java.util.Map;
public class InsuranceCalculatorFactory {
private static final Map<String, InsuranceCalculator> CALCULATOR_MAP = new HashMap<>();
static {
CALCULATOR_MAP.put("first", new FirstLevelCalculator());
CALCULATOR_MAP.put("second", new SecondLevelCalculator());
CALCULATOR_MAP.put("third", new ThirdLevelCalculator());
CALCULATOR_MAP.put("fourth", new FourthLevelCalculator());
}
public static InsuranceCalculator getCalculator(double income) {
if (income <= 10000) {
return CALCULATOR_MAP.get("first");
} else if (income <= 30000) {
return CALCULATOR_MAP.get("second");
} else if (income <= 60000) {
return CALCULATOR_MAP.get("third");
} else {
return CALCULATOR_MAP.get("fourth");
}
}
}
這樣,就可以通過 InsuranceCalculatorFactory
類手動獲取相應的策略物件了。
double income = 40000;
// 獲取第三級保險費用計算器
InsuranceCalculator calculator = InsuranceCalculatorFactory.getCalculator(income);
double insurance = calculator.calculate(income);
System.out.println("保險費用為:" + insurance);
這種方式允許我們在執行時根據需要選擇不同的策略,而無需在程式碼中寫死條件語句。
相關閱讀:Replace Conditional Logic with Strategy Pattern - IDEA 。
除了策略模式之外,Map+函數式介面也能實現類似的效果,程式碼一般還要更簡潔一些。
下面是使用Map+函數式介面重構後的程式碼:
首先,在 InsuranceCalculatorFactory
類中,將 getCalculator
方法的返回型別從 InsuranceCalculator
改為 Function<Double, Double>
,表示該方法返回一個將 double
型別的 income
對映到 double
型別的 insurance
的函數。
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
public class InsuranceCalculatorFactory {
private static final Map<String, Function<Double, Double>> CALCULATOR_MAP = new HashMap<>();
static {
CALCULATOR_MAP.put("first", income -> income * 0.365);
CALCULATOR_MAP.put("second", income -> (income - 10000) * 0.2 + 35600);
CALCULATOR_MAP.put("third", income -> (income - 30000) * 0.1 + 76500);
CALCULATOR_MAP.put("fourth", income -> (income - 60000) * 0.02 + 105600);
}
public static Function<Double, Double> getCalculator(double income) {
if (income <= 10000) {
return CALCULATOR_MAP.get("first");
} else if (income <= 30000) {
return CALCULATOR_MAP.get("second");
} else if (income <= 60000) {
return CALCULATOR_MAP.get("third");
} else {
return CALCULATOR_MAP.get("fourth");
}
}
}
然後,在呼叫工廠方法時,可以使用 Lambda 表示式或方法參照來代替實現策略介面的類。
double income = 40000;
Function<Double, Double> calculator = InsuranceCalculatorFactory.getCalculator(income);
double insurance = calculator.apply(income);
System.out.println("保險費用為:" + insurance);;
複雜物件的建立可以使用建造者模式優化。
使用 Caffeine 建立本地快取的程式碼範例:
Caffeine.newBuilder()
// 設定最後一次寫入或存取後經過固定時間過期
.expireAfterWrite(60, TimeUnit.DAYS)
// 初始的快取空間大小
.initialCapacity(100)
// 快取的最大條數
.maximumSize(500)
.build();
責任鏈模式在實際開發中還是挺實用的,像 MyBatis、Netty、OKHttp3、SpringMVC、Sentinel 等知名框架都大量使用了責任鏈模式。
如果一個請求需要進過多個步驟處理的話,可以考慮使用責任鏈模式。
責任鏈模式下,存在多個處理者,這些處理者之間有順序關係,一個請求被依次傳遞給每個處理者(對應的是一個物件)進行處理。處理者可以選擇自己感興趣的請求進行處理,對於不感興趣的請求,轉發給下一個處理者即可。如果滿足了某個條件,也可以在某個處理者處理完之後直接停下來。
責任鏈模式下,如果需要增加新的處理者非常容易,符合開閉原則。
Netty 中的 ChannelPipeline
使用責任鏈模式對資料進行處理。我們可以在 ChannelPipeline
上通過 addLast()
方法新增一個或者多個ChannelHandler
(一個資料或者事件可能會被多個 Handler
處理) 。當一個 ChannelHandler
處理完之後就將資料交給下一個 ChannelHandler
。
ChannelPipeline pipeline = ch.pipeline()
// 新增一個用於對 HTTP 請求和響應報文進行編解碼的 ChannelHandler
.addLast(HTTP_CLIENT_CODEC, new HttpClientCodec())
// 新增一個對 gzip 或者 deflate 格式的編碼進行解碼的 ChannelHandler
.addLast(INFLATER_HANDLER, new HttpContentDecompressor())
// 新增一個用於處理分塊傳輸編碼的 ChannelHandler
.addLast(CHUNKED_WRITER_HANDLER, new ChunkedWriteHandler())
// 新增一個處理 HTTP 請求並響應的 ChannelHandler
.addLast(AHC_HTTP_HANDLER, new HttpHandler);
Tomcat 中的請求處理是通過一系列過濾器(Filter)來完成的,這同樣是責任連模式的運用。每個過濾器都可以對請求進行處理,並將請求傳遞給下一個過濾器,直到最後一個過濾器將請求轉發到相應的 Servlet 或 JSP 頁面。
public class CompressionFilter implements Filter {
// ...
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 檢查是否支援壓縮
if (isCompressable(request, response)) {
// 建立一個自定義的響應物件,用於在壓縮資料時獲取底層輸出流
CompressionServletResponseWrapper wrappedResponse = new CompressionServletResponseWrapper(
(HttpServletResponse) response);
try {
// 將請求轉發給下一個過濾器或目標 Servlet/JSP 頁面
chain.doFilter(request, wrappedResponse);
// 壓縮資料並寫入原始響應物件的輸出流
wrappedResponse.finishResponse();
} catch (IOException e) {
log.warn(sm.getString("compressionFilter.compressFailed"), e); //$NON-NLS-1$
handleIOException(e, wrappedResponse);
}
} else {
// 不支援壓縮,直接將請求轉發給下一個過濾器或目標 Servlet/JSP 頁面
chain.doFilter(request, response);
}
}
// ...
}
相關閱讀:聊一聊責任鏈模式 。
觀察者模式也是解耦的利器。當物件之間存在一對多關係,可以使用觀察者模式,讓多個觀察者物件同時監聽某一個主題物件。當主題物件狀態發生變化時,會通知所有觀察者,觀察者收到通知之後可以根據通知的內容去針對性地做一些事情。
Spring 事件就是基於觀察者模式實現的。
1、定義一個事件。
public class CustomSpringEvent extends ApplicationEvent {
private String message;
public CustomSpringEvent(Object source, String message) {
super(source);
this.message = message;
}
public String getMessage() {
return message;
}
}
2、建立事件釋出者釋出事件。
@Component
public class CustomSpringEventPublisher {
@Autowired
private ApplicationEventPublisher applicationEventPublisher;
public void publishCustomEvent(final String message) {
System.out.println("Publishing custom event. ");
CustomSpringEvent customSpringEvent = new CustomSpringEvent(this, message);
applicationEventPublisher.publishEvent(customSpringEvent);
}
}
3、建立監聽器監聽並處理事件(支援非同步處理事件的方式,需要設定執行緒池)。
@Component
public class CustomSpringEventListener implements ApplicationListener<CustomSpringEvent> {
@Override
public void onApplicationEvent(CustomSpringEvent event) {
System.out.println("Received spring custom event - " + event.getMessage());
}
}
多個並行的類實現相似的程式碼邏輯。我們可以考慮提取相同邏輯在父類別中實現,差異邏輯通過抽象方法留給子類實現。
對於相同的流程和邏輯,我們還可以借鑑模板方法模式將其固定成模板,保留差異的同時儘可能避免程式碼重複。
下面是一個利用模板方法模式定義流程的範例程式碼:
public abstract class AbstractDataImporter {
private final String filePath;
public AbstractDataImporter(String filePath) {
this.filePath = filePath;
}
public void importData() throws IOException {
List<String> data = readDataFromFile();
validateData(data);
saveDataToDatabase(data);
}
protected abstract List<String> readDataFromFile() throws IOException;
protected void validateData(List<String> data) {
// 若子類沒有實現該方法,則不進行資料校驗
}
protected abstract void saveDataToDatabase(List<String> data);
protected String getFilePath() {
return filePath;
}
}
在上面的程式碼中,AbstractDataImporter
是一個抽象類。該類提供了一個 importData()
方法,它定義了匯入資料的整個流程。具體而言,該方法首先從檔案中讀取原始資料,然後對資料進行校驗,最後將資料儲存到資料庫中。
其中,readDataFromFile()
和 saveDataToDatabase()
方法是抽象的,由子類來實現。validateData()
方法是一個預設實現,可以通過覆蓋來客製化校驗邏輯。getFilePath()
方法用於獲取待匯入資料的檔案路徑。
子類繼承 AbstractDataImporter
後,需要實現 readDataFromFile()
和 saveDataToDatabase()
方法,並覆蓋 validateData()
方法(可選)。例如,下面是一個具體的子類 CsvDataImporter
的實現:
public class CsvDataImporter extends AbstractDataImporter {
private final char delimiter;
public CsvDataImporter(String filePath, char delimiter) {
super(filePath);
this.delimiter = delimiter;
}
@Override
protected List<String> readDataFromFile() throws IOException {
List<String> data = new ArrayList<>();
try (BufferedReader reader = new BufferedReader(new FileReader(getFilePath()))) {
String line;
while ((line = reader.readLine()) != null) {
data.add(line);
}
}
return data;
}
@Override
protected void validateData(List<String> data) {
// 對 CSV 格式的資料進行校驗,例如檢查是否每行都有相同數量的欄位等
}
@Override
protected void saveDataToDatabase(List<String> data) {
// 將 CSV 格式的資料儲存到資料庫中,例如將每行解析為一個物件,然後使用 JPA 儲存到資料庫中
}
}
在上面的程式碼中,CsvDataImporter
繼承了 AbstractDataImporter
類,並實現了 readDataFromFile()
和 saveDataToDatabase()
方法。它還覆蓋了 validateData()
方法,以支援對 CSV 格式的資料進行校驗。
通過以上實現,我們可以通過繼承抽象父類別並實現其中的抽象方法,來定義自己的資料匯入流程。另外,由於抽象父類別已經定義了整個流程的結構和大部分預設實現,因此子類只需要關注客製化化的邏輯即可,從而提高了程式碼的可複用性和可維護性。
相關閱讀:21 | 程式碼重複:搞定程式碼重複的三個絕招 - Java 業務開發常見錯誤 100 例 。
Java 版本在更新迭代過程中會增加很多好用的特性,一定要善於使用 Java 新特性來優化自己的程式碼,增加程式碼的可閱讀性和可維護性。
就比如火了這麼多年的 Java 8 在增強程式碼可讀性、簡化程式碼方面,相比 Java 7 增加了很多功能,比如 Lambda、Stream 流操作、並行流(ParallelStream)、Optional 可空型別、新日期時間型別等。
Lambda 優化排序程式碼範例:
// 匿名內部類實現陣列從小到大排序
Integer[] scores = {89, 100, 77, 90, 86};
Arrays.sort(scores,new Comparator<Integer>(){
@Override
public int compare(Integer o1, Integer o2) {
return o1.compareTo(o2);
}
});
for(Integer score:scores){
System.out.print(score);
}
// 使用 Lambda 優化
Arrays.sort(scores,(o1,o2)->o1.compareTo(o2) );
// 還可以像下面這樣寫
Arrays.sort(scores,Comparator.comparing(Integer::intValue));
Optional 優化程式碼範例:
private Double calculateAverageGrade(Map<String, List<Integer>> gradesList, String studentName)
throws Exception {
return Optional.ofNullable(gradesList.get(studentName))// 建立一個Optional物件,傳入引數為空時返回Optional.empty()
.map(list -> list.stream().collect(Collectors.averagingDouble(x -> x)))// 對 Optional 的值進行操作
.orElseThrow(() -> new NotFoundException("Student not found - " + studentName));// 當值為空時,丟擲指定的異常
}
再比如 Java 17 中轉正的密封類(Sealed Classes) ,Java 16 中轉正的記錄型別(record
關鍵字定義)、instanceof 模式匹配等新特性。
record
關鍵字優化程式碼範例:
/**
* 這個類具有兩個特徵
* 1. 所有成員屬性都是final
* 2. 全部方法由構造方法,和兩個成員屬性存取器組成(共三個)
* 那麼這種類就很適合使用record來宣告
*/
final class Rectangle implements Shape {
final double length;
final double width;
public Rectangle(double length, double width) {
this.length = length;
this.width = width;
}
double length() { return length; }
double width() { return width; }
}
/**
* 1. 使用record宣告的類會自動擁有上面類中的三個方法
* 2. 在這基礎上還附贈了equals(),hashCode()方法以及toString()方法
* 3. toString方法中包括所有成員屬性的字串表示形式及其名稱
*/
record Rectangle(float length, float width) { }
我們經常在程式碼中會對一個資料結構封裝成 DO、DTO、VO 等,而這些 Bean 中的大部分屬性都是一樣的,所以使用屬性拷貝類工具可以幫助我們節省大量的 set 和 get 操作。
常用的 Bean 對映工具有:Spring BeanUtils、Apache BeanUtils、MapStruct、ModelMapper、Dozer、Orika、JMapper 。
由於 Apache BeanUtils 、Dozer 、ModelMapper 效能太差,所以不建議使用。MapStruct 效能更好而且使用起來比較靈活,是一個比較不錯的選擇。
這裡以 MapStruct 為例,簡單演示一下轉換效果。
1、定義兩個類 Employee
和 EmployeeDTO
。
public class Employee {
private int id;
private String name;
// getters and setters
}
public class EmployeeDTO {
private int employeeId;
private String employeeName;
// getters and setters
}
2、定義轉換介面讓 Employee
和 EmployeeDTO
互相轉換。
@Mapper
public interface EmployeeMapper {
// Spring 專案可以將 Mapper 注入到 IoC 容器中,這樣就可以像 Spring Bean 一樣呼叫了
EmployeeMapper INSTANT = Mappers.getMapper(EmployeeMapper.class);
@Mapping(target="employeeId", source="entity.id")
@Mapping(target="employeeName", source="entity.name")
EmployeeDTO employeeToEmployeeDTO(Employee entity);
@Mapping(target="id", source="dto.employeeId")
@Mapping(target="name", source="dto.employeeName")
Employee employeeDTOtoEmployee(EmployeeDTO dto);
}
3、實際使用。
// EmployeeDTO 轉 Employee
Employee employee = EmployeeMapper.INSTANT.employeeToEmployeeDTO(employee);
// Employee 轉 EmployeeDTO
EmployeeDTO employeeDTO = EmployeeMapper.INSTANT.employeeDTOtoEmployee(employeeDTO);
相關閱讀:
1、不要隨意列印紀錄檔,確保自己列印的紀錄檔是後面能用到的。
列印太多無用的紀錄檔不光影響問題排查,還會影響效能,加重磁碟負擔。
2、列印紀錄檔中的敏感資料比如身份證號、電話號、密碼需要進行脫敏。相關閱讀:Spring Boot 3 步完成紀錄檔脫敏,簡單實用!!
3、選擇合適的紀錄檔列印級別。最常用的紀錄檔級別有四個: DEBUG、INFO、WARN、ERROR。
4、生產環境禁止輸出 DEBUG 紀錄檔,避免列印的紀錄檔過多(DEBUG 紀錄檔非常多)。
5、應用中不可直接使用紀錄檔系統(Log4j、Logback)中的 API,而應依賴使用紀錄檔框架 SLF4J 中的 API,使用門面模式的紀錄檔框架,有利於維護和各個類的紀錄檔處理方式統一。
Spring Boot 應用程式可以直接使用內建的紀錄檔框架 Logback,Logback 就是按照 SLF4J API 標準實現的。
6、異常紀錄檔需要列印完整的異常資訊。
反例:
try {
//讀檔案操作
readFile();
} catch (IOException e) {
// 只保留了異常訊息,棧沒有記錄
log.error("檔案讀取錯誤, {}", e.getMessage());
}
正例:
try {
//讀檔案操作
readFile();
} catch (IOException e) {
log.error("檔案讀取錯誤", e);
}
7、避免層層列印紀錄檔。
舉個例子:method1 呼叫 method2,method2 出現 error 並列印 error 紀錄檔,method1 也列印了 error 紀錄檔,等同於一個錯誤紀錄檔列印了 2 遍。
8、不要列印紀錄檔後又將異常丟擲。
反例:
try {
...
} catch (IllegalArgumentException e) {
log.error("出現異常啦", e);
throw e;
}
在紀錄檔中會對丟擲的一個異常列印多條錯誤資訊。
正例:
try {
...
} catch (IllegalArgumentException e) {
log.error("出現異常啦", e);
}
// 或者包裝成自定義異常之後丟擲
try {
...
} catch (IllegalArgumentException e) {
throw new MyBusinessException("一段對異常的描述資訊.", e);
}
相關閱讀:15 個紀錄檔列印的實用建議 。
阿里巴巴 Java 例外處理規約如下:
所有的異常都應該由最上層捕獲並處理,這樣程式碼更簡潔,還可以避免重複輸出異常紀錄檔。 如果我們都在業務程式碼中使用try-catch
或者try-catch-finally
處理的話,就會讓業務程式碼中冗餘太多例外處理的邏輯,對於同樣的異常我們還需要重複編寫程式碼處理,還可能會導致重複輸出異常紀錄檔。這樣的話,程式碼可維護性、可閱讀性都非常差。
Spring Boot 應用程式可以藉助 @RestControllerAdvice
和 @ExceptionHandler
實現全域性統一例外處理。
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public Result businessExceptionHandler(HttpServletRequest request, BusinessException e){
...
return Result.faild(e.getCode(), e.getMessage());
}
...
}
java.lang.AutoCloseable
或者 java.io.Closeable
的物件try-with-resources
語句中,任何 catch 或 finally 塊在宣告的資源關閉後執行《Effective Java》中明確指出:
面對必須要關閉的資源,我們總是應該優先使用
try-with-resources
而不是try-finally
。隨之產生的程式碼更簡短,更清晰,產生的異常對我們也更有用。try-with-resources
語句讓我們更容易編寫必須要關閉的資源的程式碼,若採用try-finally
則幾乎做不到這點。
Java 中類似於InputStream
、OutputStream
、Scanner
、PrintWriter
等的資源都需要我們呼叫close()
方法來手動關閉,一般情況下我們都是通過try-catch-finally
語句來實現這個需求,如下:
//讀取文字檔案的內容
Scanner scanner = null;
try {
scanner = new Scanner(new File("D://read.txt"));
while (scanner.hasNext()) {
System.out.println(scanner.nextLine());
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
if (scanner != null) {
scanner.close();
}
}
使用 Java 7 之後的 try-with-resources
語句改造上面的程式碼:
try (Scanner scanner = new Scanner(new File("test.txt"))) {
while (scanner.hasNext()) {
System.out.println(scanner.nextLine());
}
} catch (FileNotFoundException fnfe) {
fnfe.printStackTrace();
}
當然多個資源需要關閉的時候,使用 try-with-resources
實現起來也非常簡單,如果你還是用try-catch-finally
可能會帶來很多問題。
通過使用分號分隔,可以在try-with-resources
塊中宣告多個資源。
try (BufferedInputStream bin = new BufferedInputStream(new FileInputStream(new File("test.txt")));
BufferedOutputStream bout = new BufferedOutputStream(new FileOutputStream(new File("out.txt")))) {
int b;
while ((b = bin.read()) != -1) {
bout.write(b);
}
}
catch (IOException e) {
e.printStackTrace();
}
不要把異常定義為靜態變數,因為這樣會導致異常棧資訊錯亂。每次手動丟擲異常,我們都需要手動 new 一個異常物件丟擲。
// 錯誤做法
public class Exceptions {
public static BusinessException ORDEREXISTS = new BusinessException("訂單已經存在", 3001);
...
}
throw new BIZException(e.getMessage()
這種形式的異常丟擲),儘量自定義異常,而不是直接使用 RuntimeException
或Exception
。介面不要直接返回資料庫物件(也就是 DO),資料庫物件包含類中所有的屬性。
// 錯誤做法
public UserDO getUser(Long userId) {
return userService.getUser(userId);
}
原因:
建議的做法是單獨定義一個類比如 VO(可以看作是介面返回給前端展示的物件資料)來對介面返回的資料進行篩選,甚至是封裝和組合。
public UserVo getUser(Long userId) {
UserDO userDO = userService.getUser(userId);
UserVO userVO = new UserVO();
BeanUtils.copyProperties(userDO, userVO);//演示使用
return userVO;
}
介面返回的資料一定要統一格式,遮掩更方面對接前端開發的同學以及其他呼叫該介面的開發。
通常來說,下面這些資訊是必備的:
public enum ResultEnum implements IResult {
SUCCESS(2001, "介面呼叫成功"),
VALIDATE_FAILED(2002, "引數校驗失敗"),
COMMON_FAILED(2003, "介面呼叫失敗"),
FORBIDDEN(2004, "沒有許可權存取資源");
private Integer code;
private String message;
...
}
public class Result<T> {
private Integer code;
private String message;
private T data;
...
public static <T> Result<T> success(T data) {
return new Result<>(ResultEnum.SUCCESS.getCode(), ResultEnum.SUCCESS.getMessage(), data);
}
public static Result<?> failed() {
return new Result<>(ResultEnum.COMMON_FAILED.getCode(), ResultEnum.COMMON_FAILED.getMessage(), null);
}
...
}
對於 Spring Boot 專案來說,可以使用 @RestControllerAdvice
註解+ ResponseBodyAdvic
介面統一處理介面返回值,實現程式碼無侵入。篇幅問題這裡就不貼具體實現程式碼了,比較簡單,具體實現方式可以參考這篇文章:Spring Boot 無侵入式 實現 API 介面統一 JSON 格式返回 。
需要注意的是,這種方式在 Spring Cloud OpenFeign 的繼承模式下是有侵入性,解決辦法見:SpringBoot 無侵入式 API 介面統一格式返回,在 Spring Cloud OpenFeign 繼承模式具有了侵入性 。
實際專案中,其實使用比較多的還是下面這種比較直接的方式:
public class PostController {
@GetMapping("/list")
public R<List<SysPost>> getPosts() {
...
return R.ok(posts);
}
}
上面介紹的無侵入的方式,一般改造舊專案的時候用的比較多。
開發過程中,第三方介面呼叫、RPC 呼叫以及服務之間的呼叫建議設定一個超時時間。
我們平時接觸到的超時可以簡單分為下面 2 種:
一些連線池使用者端框架中可能還會有獲取連線超時和空閒連線清理超時。
如果沒有設定超時的話,就可能會導致伺服器端連線數爆炸和大量請求堆積的問題。這些堆積的連線和請求會消耗系統資源,影響新收到的請求的處理。嚴重的情況下,甚至會拖垮整個系統或者服務。
我之前在實際專案就遇到過類似的問題,整個網站無法正常處理請求,伺服器負載直接快被拉滿。後面發現原因是專案超時設定錯誤加上使用者端請求處理異常,導致伺服器端連線數直接接近 40w+,這麼多堆積的連線直接把系統幹趴了。
相關閱讀:超時&重試詳解 。
在 10 個執行緒池最佳實踐和坑! 這篇文章中,我總結了 10 個使用執行緒池的注意事項:
ThreadPoolExecutor
的建構函式來宣告,避免使用 Executors
類建立執行緒池,會有 OOM 風險。ThreadLocal
共用,可能會導致執行緒從 ThreadLocal
獲取到的是舊值/髒資料。163****892
。