有關Assert斷言大家並不陌生,我們在做單元測試的時候,看業務事務複合預期,我們可以通過斷言來校驗,斷言常用的方法如下:
public class Assert {
/**
* 結果 = 預期 則正確
*/
static public void assertEquals(Object expected, Object actual);
/**
* 結果 != 預期 則正確
*/
static public void assertNotEquals(Object unexpected, Object actual);
/**
* condition == true 則正確
*/
static public void assertTrue(boolean condition);
/**
* condition == false 則正確
*/
static public void assertFalse(boolean condition);
/**
* 永遠是錯誤
*/
static public void fail();
/**
* 結果不為空 則正確
*/
static public void assertNotNull(Object object);
/**
* 結果為空 則正確
*/
static public void assertNull(Object object);
/**
* 兩個物件引用 相等 則正確(上面相當於equels 這裡類似於使用“==”比較兩個物件)
*/
static public void assertSame(Object expected, Object actual);
/**
* 兩個物件引用 不相等 則正確
*/
static public void assertNotSame(Object unexpected, Object actual);
/**
* 兩個陣列相等 則正確
*/
public static void assertArrayEquals(Object[] expecteds, Object[] actuals);
/**
* 這個單獨介紹 具體參考部落格:https://www.cnblogs.com/qdhxhz/p/13684458.html
*/
public static <T> void assertThat(T actual, Matcher<? super T> matcher);
};
使用斷言能讓我們編碼看去更加的清爽,比如:
@Test
public void test1() {
Order order = orderDao.selectById(orderId);
Assert.notNull(order, "訂單不存在。");
}
@Test
public void test2() {
// 另一種寫法
Order order = orderDao.selectById(orderId);
if (order == null) {
throw new IllegalArgumentException("訂單不存在。");
}
}
這兩種方式一對比,是不是明顯感覺第一種更優雅,第二種寫法則是相對醜陋的 if {...} 程式碼塊。那麼 神奇的 Assert.notNull() 背後到底做了什麼呢?
下面是 Assert 的部分原始碼:
public abstract class Assert {
public Assert() {
}
public static void notNull(@Nullable Object object, String message) {
if (object == null) {
throw new IllegalArgumentException(message);
}
}
}
可以看到,Assert 其實就是幫我們把 if {...} 封裝了一下,是不是很神奇。雖然很簡單,但不可否認的是編碼體驗至少提升了一個檔次。
那麼我們是不是可以模仿Assert,也寫一個自定義斷言類,不過斷言失敗後丟擲的異常不是IllegalArgumentException 這些內建異常,而是我們自己定義的異常。
下面讓我們來嘗試一下。
public interface Assert {
/**
* 建立異常
* @param args
* @return
*/
BaseException newException(Object... args);
/**
* 建立異常
* @param t
* @param args
* @return
*/
BaseException newException(Throwable t, Object... args);
/**
* 斷言物件 obj 非空。如果物件 obj 為空,則丟擲異常
*
* @param obj 待判斷物件
*/
default void assertNotNull(Object obj) {
if (obj == null) {
throw newException(obj);
}
}
/**
* 斷言物件 obj 非空。如果物件 obj 為空,則丟擲異常
* 異常資訊 message 支援傳遞引數方式,避免在判斷之前進行字串拼接操作
*
* @param obj 待判斷物件
* @param args message佔位符對應的引數列表
*/
default void assertNotNull(Object obj, Object... args) {
if (obj == null) {
throw newException(args);
}
}
}
注
:
- 這裡只給出Assert介面的部分原始碼,更多斷言方法請參考專案的原始碼。
- BaseException 是所有自定義異常的基類。
- 在介面中定義預設方法是Java8的新語法。
上面的Assert
斷言方法是使用介面的預設方法定義的,然後有沒有發現當斷言失敗後,丟擲的異常不是具體的某個異常,而是交由2個newException
介面方法提供。
因為業務邏輯中出現的異常基本都是對應特定的場景,比如根據使用者id獲取使用者資訊,查詢結果為null,此時丟擲的異常可能為UserNotFoundException
,並且有特
定的異常碼(比如7001)和異常資訊“使用者不存在”。所以具體丟擲什麼異常,有Assert
的實現類決定。
看到這裡,你可能會有會有疑問,按照上面的做法,那不是有多少個異常情況,我都要去實現Assert介面,然後在寫定義等量的斷言類和異常類,這顯然是反人類的,
這也沒想象中高明嘛。別急,且聽我細細道來。
一、善解人意的Enum
自定義異常BaseException有2個屬性,即code、message,這樣一對屬性,有沒有想到什麼類一般也會定義這2個屬性?
沒錯,就是列舉類。且看我如何將 Enum 和 Assert 結合起來,相信我一定會讓你眼前一亮。如下:
public interface IResponseEnum {
int getCode();
String getMessage();
}
我們自定一個業務異常類
/**
* 業務異常
* 業務處理時,出現異常,可以丟擲該異常
*
*/
public class BusinessException extends BaseException {
private static final long serialVersionUID = 1L;
public BusinessException(IResponseEnum responseEnum, Object[] args, String message) {
super(responseEnum, args, message);
}
public BusinessException(IResponseEnum responseEnum, Object[] args, String message, Throwable cause) {
super(responseEnum, args, message, cause);
}
}
業務異常斷言實現類
,它同時繼承2個介面,一個是Assert介面,一個是IResponseEnum介面
public interface BusinessExceptionAssert extends IResponseEnum, Assert {
@Override
default BaseException newException(Object... args) {
String msg = MessageFormat.format(this.getMessage(), args);
return new BusinessException(this, args, msg);
}
@Override
default BaseException newException(Throwable t, Object... args) {
String msg = MessageFormat.format(this.getMessage(), args);
return new BusinessException(this, args, msg, t);
}
}
業務異常列舉
/**
* 業務異常列舉
*/
@Getter
@AllArgsConstructor
public enum BusinessResponseEnum implements BusinessExceptionAssert {
USER_NOT_FOUND(6001, "未查詢到使用者資訊"),
ORDER_NOT_FOUND(7001, "未查詢到訂單資訊"),
;
/**
* 返回碼
*/
private int code;
/**
* 返回訊息
*/
private String message;
}
這樣如果是業務異常,就都可以在BusinessResponseEnum中定義一個新列舉物件就可以了。以後每增加一種異常情況,只需增加一個列舉例項即可,再也不用每一種異常
都定義一個異常類了。接下來看使用如下:
/**
* 查詢使用者資訊
*/
public User queryDetail(Integer userId) {
final User user = this.getById(userId);
// 校驗非空
BusinessResponseEnum.USER_NOT_FOUND.assertNotNull(user);
return user;
}
若不使用斷言,程式碼可能如下:
/**
* 查詢使用者資訊
*/
public User queryDetail(Integer userId) {
final User user = this.getById(userId);
if (user == null) {
throw new UserNotFoundException();
// 或者這樣
throw new BusinessException(6001, "未查詢到使用者資訊");
}
return user;
}
使用列舉類結合(繼承)Assert,只需根據特定的異常情況定義不同的列舉例項,如上面的USER_NOT_FOUND、ORDER_NOT_FOUND,就能夠針對不同情況丟擲特定的異常
(這裡指攜帶特定的異常碼和異常訊息),這樣既不用定義大量的異常類,同時還具備了斷言的良好可讀性。
二、驗證統一異常處理
在throw丟擲異常後,我們就可以統一去處理異常,如果有必要我們可以加入異常日誌存入資料庫中,有助於後續排查問題。
/**
* 全域性異常處理器
*
* @author xub
* @date 2022/2/28 上午10:52
*/
@Slf4j
@Component
@ControllerAdvice
@ConditionalOnWebApplication
public class ViewExceptionResolver {
/**
* 生產環境
*/
private final static String ENV_PROD = "prod";
@Autowired
private UnifiedMessageSource unifiedMessageSource;
/**
* 當前環境
*/
@Value("${spring.profiles.active}")
private String profile;
/**
* 獲取國際化訊息
*
* @param e 異常
* @return
*/
public String getMessage(BaseException e) {
String code = "response." + e.getResponseEnum().toString();
String message = unifiedMessageSource.getMessage(code, e.getArgs());
if (message == null || message.isEmpty()) {
return e.getMessage();
}
return message;
}
/**
* 業務異常
*
* @param e 異常
* @return 異常結果
*/
@ExceptionHandler(value = BusinessException.class)
@ResponseBody
public CommandResult handleBusinessException(BaseException e) {
log.error(e.getMessage(), e);
return CommandResult.ofFail(e.getResponseEnum().getCode(), getMessage(e));
}
/**
* 自定義異常
*
* @param e 異常
* @return 異常結果
*/
@ExceptionHandler(value = BaseException.class)
@ResponseBody
public CommandResult handleBaseException(BaseException e) {
log.error(e.getMessage(), e);
return CommandResult.ofFail(e.getResponseEnum().getCode(), getMessage(e));
}
/**
* 未定義異常
*
* @param e 異常
* @return 異常結果
*/
@ExceptionHandler(value = Exception.class)
@ResponseBody
public CommandResult handleException(Exception e) {
log.error(e.getMessage(), e);
if (ENV_PROD.equals(profile)) {
// 當為生產環境, 不適合把具體的異常資訊展示給使用者, 比如資料庫異常資訊.
int code = CommonResponseEnum.SERVER_ERROR.getCode();
BaseException baseException = new BaseException(CommonResponseEnum.SERVER_ERROR);
String message = getMessage(baseException);
return CommandResult.ofFail(code, message);
}
return CommandResult.ofFail(CommonResponseEnum.SERVER_ERROR.getCode(), e.getMessage());
}
}
三、測試驗證
這裡請求地址,業務id=10資料庫是不存在這個使用者,所以會報錯。
localhost:8085/user/getUserInfo?userId=10
達到了預期效果。
四、總結
使用 斷言
和 列舉類
相結合的方式,再配合統一異常處理,基本大部分的異常都能夠被捕獲。為什麼說大部分異常,因為當引入 spring cloud security 後,還會
有認證/授權異常,閘道器的服務降級異常、跨模組呼叫異常、遠端呼叫第三方服務異常等,這些異常的捕獲方式與本文介紹的不太一樣,不過限於篇幅,這裡不做詳細說明,
以後會有單獨的文章介紹。
專案地址:用Assert(斷言)封裝異常,讓程式碼更優雅(附專案原始碼)
感謝
這篇文章給自己提供了很好的思路,基本上按照這個思路往下寫的
統一異常處理介紹及實戰:https://www.jianshu.com/p/3f3d9e8d1efa