主頁 > 後端開發 > 求求你們了,別再寫滿屏的 try catch 了!!

求求你們了,別再寫滿屏的 try catch 了!!

2020-10-12 00:10:57 後端開發

作者:巨人大哥
cnblogs.com/jurendage/p/11255197.html

背景

軟體開發程序中,不可避免的是需要處理各種例外,就我自己來說,至少有一半以上的時間都是在處理各種例外情況,所以代碼中就會出現大量的try {...} catch {...} finally {...} 代碼塊,不僅有大量的冗余代碼,而且還影響代碼的可讀性,

比較下面兩張圖,看看您現在撰寫的代碼屬于哪一種風格?然后哪種編碼風格您更喜歡?

丑陋的 try catch 代碼塊

優雅的Controller

上面的示例,還只是在Controller層,如果是在Service層,可能會有更多的try catch代碼塊,這將會嚴重影響代碼的可讀性、“美觀性”,

所以如果是我的話,我肯定偏向于第二種,我可以把更多的精力放在業務代碼的開發,同時代碼也會變得更加簡潔,

既然業務代碼不顯式地對例外進行捕獲、處理,而例外肯定還是處理的,不然系統豈不是動不動就崩潰了,所以必須得有其他地方捕獲并處理這些例外,

那么問題來了,如何優雅的處理各種例外?

什么是統一例外處理

Spring在3.2版本增加了一個注解@ControllerAdvice,可以與@ExceptionHandler@InitBinder@ModelAttribute 等注解注解配套使用,

對于這幾個注解的作用,這里不做過多贅述,若有不了解的,可以參考Spring3.2新注解@ControllerAdvice,先大概有個了解,或者關注微信公眾號:Java技術堆疊,在后臺回復:spring,可以獲取我整理的 N 篇 Spring 教程,都是干貨,

不過跟例外處理相關的只有注解@ExceptionHandler,從字面上看,就是 例外處理器 的意思,其實際作用也是:若在某個Controller類定義一個例外處理方法,并在方法上添加該注解,那么當出現指定的例外時,會執行該處理例外的方法,其可以使用springmvc提供的資料系結,比如注入HttpServletRequest等,還可以接受一個當前拋出的Throwable物件,

但是,這樣一來,就必須在每一個Controller類都定義一套這樣的例外處理方法,因為例外可以是各種各樣,這樣一來,就會造成大量的冗余代碼,而且若需要新增一種例外的處理邏輯,就必須修改所有Controller類了,很不優雅,

當然你可能會說,那就定義個類似BaseController的基類,這樣總行了吧,

這種做法雖然沒錯,但仍不盡善盡美,因為這樣的代碼有一定的侵入性和耦合性,簡簡單單的Controller,我為啥非得繼承這樣一個類呢,萬一已經繼承其他基類了呢,大家都知道Java只能繼承一個類,

那有沒有一種方案,既不需要跟Controller耦合,也可以將定義的 例外處理器 應用到所有控制器呢?所以注解@ControllerAdvice出現了,簡單的說,該注解可以把例外處理器應用到所有控制器,而不是單個控制器,

借助該注解,我們可以實作:在獨立的某個地方,比如單獨一個類,定義一套對各種例外的處理機制,然后在類的簽名加上注解@ControllerAdvice,統一對 不同階段的不同例外 進行處理,這就是統一例外處理的原理,

注意到上面對例外按階段進行分類,大體可以分成:進入Controller前的例外 和 Service 層例外,具體可以參考下圖:

不同階段的例外

目標

消滅95%以上的 try catch 代碼塊,以優雅的 Assert(斷言) 方式來校驗業務的例外情況,只關注業務邏輯,而不用花費大量精力寫冗余的 try catch 代碼塊,

統一例外處理實戰

在定義統一例外處理類之前,先來介紹一下如何優雅的判定例外情況并拋例外,

用 Assert(斷言) 替換 throw exception

想必 Assert(斷言) 大家都很熟悉,比如 Spring 家族的 org.springframework.util.Assert,在我們寫測驗用例的時候經常會用到,使用斷言能讓我們編碼的時候有一種非一般絲滑的感覺,比如:

@Test
public void test1() {
	...
	User user = userDao.selectById(userId);
	Assert.notNull(user, "用戶不存在.");
	...
}

@Test
public void test2() {
	// 另一種寫法
	User user = userDao.selectById(userId);
	if (user == 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 {...} 封裝了一下,是不是很神奇,雖然很簡單,但不可否認的是編碼體驗至少提升了一個檔次,那么我們能不能模仿org.springframework.util.Assert,也寫一個斷言類,不過斷言失敗后拋出的例外不是IllegalArgumentException 這些內置例外,而是我們自己定義的例外,下面讓我們來嘗試一下,

public interface Assert {
    /**
     * 創建例外
     * @param args
     * @return
     */
    BaseException newException(Object... args);

    /**
     * 創建例外
     * @param t
     * @param args
     * @return
     */
    BaseException newException(Throwable t, Object... args);

    /**
     * <p>斷言物件<code>obj</code>非空,如果物件<code>obj</code>為空,則拋出例外
     *
     * @param obj 待判斷物件
     */
    default void assertNotNull(Object obj) {
        if (obj == null) {
            throw newException(obj);
        }
    }

    /**
     * <p>斷言物件<code>obj</code>非空,如果物件<code>obj</code>為空,則拋出例外
     * <p>例外資訊<code>message</code>支持傳遞引數方式,避免在判斷之前進行字串拼接操作
     *
     * @param obj 待判斷物件
     * @param args message占位符對應的引數串列
     */
    default void assertNotNull(Object obj, Object... args) {
        if (obj == null) {
            throw newException(args);
        }
    }
}

上面的Assert斷言方法是使用介面的默認方法定義的,然后有沒有發現當斷言失敗后,拋出的例外不是具體的某個例外,而是交由2個newException介面方法提供,

因為業務邏輯中出現的例外基本都是對應特定的場景,比如根據用戶id獲取用戶資訊,查詢結果為null,此時拋出的例外可能為UserNotFoundException,并且有特定的例外碼(比如7001)和例外信息“用戶不存在”,所以具體拋出什么例外,有Assert的實作類決定,

看到這里,您可能會有這樣的疑問,按照上面的說法,那豈不是有多少例外情況,就得有定義等量的斷言類和例外類,這顯然是反人類的,這也沒想象中高明嘛,別急,且聽我細細道來,

善解人意的Enum

自定義例外BaseException有2個屬性,即codemessage,這樣一對屬性,有沒有想到什么類一般也會定義這2個屬性?沒錯,就是列舉類,且看我如何將 EnumAssert 結合起來,相信我一定會讓你眼前一亮,如下:

public interface IResponseEnum {
    int getCode();
    String getMessage();
}

/**
 * <p>業務例外</p>
 * <p>業務處理時,出現例外,可以拋出該例外</p>
 */
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);
    }
}
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 ResponseEnum implements BusinessExceptionAssert {

    /**
     * Bad licence type
     */
    BAD\_LICENCE\_TYPE(7001, "Bad licence type."),
    /**
     * Licence not found
     */
    LICENCE\_NOT\_FOUND(7002, "Licence not found.")
    ;

    /**
     * 回傳碼
     */
    private int code;
    /**
     * 回傳訊息
     */
    private String message;
}

看到這里,有沒有眼前一亮的感覺,代碼示例中定義了兩個列舉實體:BAD_LICENCE_TYPELICENCE_NOT_FOUND,分別對應了BadLicenceTypeExceptionLicenceNotFoundException兩種例外,

以后每增加一種例外情況,只需增加一個列舉實體即可,再也不用每一種例外都定義一個例外類了,然后再來看下如何使用,假設LicenceService有校驗Licence是否存在的方法,如下:

/**
 * 校驗{@link Licence}存在
 * @param licence
 */
private void checkNotNull(Licence licence) {
	ResponseEnum.LICENCE\_NOT\_FOUND.assertNotNull(licence);
}

若不使用斷言,代碼可能如下:

private void checkNotNull(Licence licence) {
	if (licence == null) {
		throw new LicenceNotFoundException();
		// 或者這樣
		throw new BusinessException(7001, "Bad licence type.");
	}
}

使用列舉類結合(繼承)Assert,只需根據特定的例外情況定義不同的列舉實體,如上面的BAD_LICENCE_TYPELICENCE_NOT_FOUND,就能夠針對不同情況拋出特定的例外(這里指攜帶特定的例外碼和例外訊息),這樣既不用定義大量的例外類,同時還具備了斷言的良好可讀性,當然這種方案的好處遠不止這些,請繼續閱讀后文,慢慢體會,

注:上面舉的例子是針對特定的業務,而有部分例外情況是通用的,比如:服務器繁忙、網路例外、服務器例外、引數校驗例外、404等,所以有CommonResponseEnumArgumentResponseEnumServletResponseEnum,其中 ServletResponseEnum 會在后文詳細說明,

定義統一例外處理器類

@Slf4j
@Component
@ControllerAdvice
@ConditionalOnWebApplication
@ConditionalOnMissingBean(UnifiedExceptionHandler.class)
public class UnifiedExceptionHandler {
    /**
     * 生產環境
     */
    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 = https://www.cnblogs.com/javastack/p/BusinessException.class)
    @ResponseBody
    public ErrorResponse handleBusinessException(BaseException e) {
        log.error(e.getMessage(), e);

        return new ErrorResponse(e.getResponseEnum().getCode(), getMessage(e));
    }

    /**
     * 自定義例外
     *
     * @param e 例外
     * @return 例外結果
     */
    @ExceptionHandler(value = BaseException.class)
    @ResponseBody
    public ErrorResponse handleBaseException(BaseException e) {
        log.error(e.getMessage(), e);

        return new ErrorResponse(e.getResponseEnum().getCode(), getMessage(e));
    }

    /**
     * Controller上一層相關例外
     *
     * @param e 例外
     * @return 例外結果
     */
    @ExceptionHandler({
            NoHandlerFoundException.class,
            HttpRequestMethodNotSupportedException.class,
            HttpMediaTypeNotSupportedException.class,
            MissingPathVariableException.class,
            MissingServletRequestParameterException.class,
            TypeMismatchException.class,
            HttpMessageNotReadableException.class,
            HttpMessageNotWritableException.class,
            // BindException.class,
            // MethodArgumentNotValidException.class
            HttpMediaTypeNotAcceptableException.class,
            ServletRequestBindingException.class,
            ConversionNotSupportedException.class,
            MissingServletRequestPartException.class,
            AsyncRequestTimeoutException.class
    })
    @ResponseBody
    public ErrorResponse handleServletException(Exception e) {
        log.error(e.getMessage(), e);
        int code = CommonResponseEnum.SERVER_ERROR.getCode();
        try {
            ServletResponseEnum servletExceptionEnum = ServletResponseEnum.valueOf(e.getClass().getSimpleName());
            code = servletExceptionEnum.getCode();
        } catch (IllegalArgumentException e1) {
            log.error("class \[{}\] not defined in enum {}", e.getClass().getName(), ServletResponseEnum.class.getName());
        }

        if (ENV_PROD.equals(profile)) {
            // 當為生產環境, 不適合把具體的例外資訊展示給用戶, 比如404.
            code = CommonResponseEnum.SERVER_ERROR.getCode();
            BaseException baseException = new BaseException(CommonResponseEnum.SERVER_ERROR);
            String message = getMessage(baseException);
            return new ErrorResponse(code, message);
        }

        return new ErrorResponse(code, e.getMessage());
    }

    /**
     * 引數系結例外
     *
     * @param e 例外
     * @return 例外結果
     */
    @ExceptionHandler(value = https://www.cnblogs.com/javastack/p/BindException.class)
    @ResponseBody
    public ErrorResponse handleBindException(BindException e) {
        log.error("引數系結校驗例外", e);

        return wrapperBindingResult(e.getBindingResult());
    }

    /**
     * 引數校驗例外,將校驗失敗的所有例外組合成一條錯誤資訊
     *
     * @param e 例外
     * @return 例外結果
     */
    @ExceptionHandler(value = https://www.cnblogs.com/javastack/p/MethodArgumentNotValidException.class)
    @ResponseBody
    public ErrorResponse handleValidException(MethodArgumentNotValidException e) {
        log.error("引數系結校驗例外", e);

        return wrapperBindingResult(e.getBindingResult());
    }

    /**
     * 包裝系結例外結果
     *
     * @param bindingResult 系結結果
     * @return 例外結果
     */
    private ErrorResponse wrapperBindingResult(BindingResult bindingResult) {
        StringBuilder msg = new StringBuilder();

        for (ObjectError error : bindingResult.getAllErrors()) {
            msg.append(", ");
            if (error instanceof FieldError) {
                msg.append(((FieldError) error).getField()).append(": ");
            }
            msg.append(error.getDefaultMessage() == null ? "" : error.getDefaultMessage());

        }

        return new ErrorResponse(ArgumentResponseEnum.VALID_ERROR.getCode(), msg.substring(2));
    }

    /**
     * 未定義例外
     *
     * @param e 例外
     * @return 例外結果
     */
    @ExceptionHandler(value = https://www.cnblogs.com/javastack/p/Exception.class)
    @ResponseBody
    public ErrorResponse 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 new ErrorResponse(code, message);
        }

        return new ErrorResponse(CommonResponseEnum.SERVER_ERROR.getCode(), e.getMessage());
    }

}

可以看到,上面將例外分成幾類,實際上只有兩大類,一類是ServletExceptionServiceException,還記得上文提到的 按階段分類 嗎,即對應 進入Controller前的例外 和 Service 層例外;然后 ServiceException 再分成自定義例外、未知例外,對應關系如下:

  • 進入Controller前的例外: handleServletException、handleBindException、handleValidException

  • 自定義例外: handleBusinessException、handleBaseException

  • 未知例外: handleException

接下來分別對這幾種例外處理器做詳細說明,

例外處理器說明

handleServletException

一個http請求,在到達Controller前,會對該請求的請求資訊與目標控制器資訊做一系列校驗,這里簡單說一下:

NoHandlerFoundException:首先根據請求Url查找有沒有對應的控制器,若沒有則會拋該例外,也就是大家非常熟悉的404例外;

HttpRequestMethodNotSupportedException:若匹配到了(匹配結果是一個串列,不同的是http方法不同,如:Get、Post等),則嘗試將請求的http方法與串列的控制器做匹配,若沒有對應http方法的控制器,則拋該例外;

HttpMediaTypeNotSupportedException:然后再對請求頭與控制器支持的做比較,比如content-type請求頭,若控制器的引數簽名包含注解@RequestBody,但是請求的content-type請求頭的值沒有包含application/json,那么會拋該例外(當然,不止這種情況會拋這個例外);

MissingPathVariableException:未檢測到路徑引數,比如url為:/licence/{licenceId},引數簽名包含@PathVariable("licenceId"),當請求的url為/licence,在沒有明確定義url為/licence的情況下,會被判定為:缺少路徑引數;

MissingServletRequestParameterException:缺少請求引數,比如定義了引數@RequestParam("licenceId") String licenceId,但發起請求時,未攜帶該引數,則會拋該例外;

TypeMismatchException: 引數型別匹配失敗,比如:接收引數為Long型,但傳入的值確是一個字串,那么將會出現型別轉換失敗的情況,這時會拋該例外;

HttpMessageNotReadableException:與上面的HttpMediaTypeNotSupportedException舉的例子完全相反,即請求頭攜帶了"content-type: application/json;charset=UTF-8",但接收引數卻沒有添加注解@RequestBody,或者請求體攜帶的 json 串反序列化成 pojo 的程序中失敗了,也會拋該例外;

HttpMessageNotWritableException:回傳的 pojo 在序列化成 json 程序失敗了,那么拋該例外;

推薦閱讀:關于序列化的 10 幾個問題,你頂得住不?

handleBindException

引數校驗例外,后文詳細說明,

handleValidException

引數校驗例外,后文詳細說明,

handleBusinessException、handleBaseException

處理自定義的業務例外,只是handleBaseException處理的是除了 BusinessException 意外的所有業務例外,就目前來看,這2個是可以合并成一個的,

handleException

處理所有未知的例外,比如操作資料庫失敗的例外,

注:上面的handleServletExceptionhandleException 這兩個處理器,回傳的例外資訊,不同環境回傳的可能不一樣,以為這些例外資訊都是框架自帶的例外資訊,一般都是英文的,不太好直接展示給用戶看,所以統一回傳SERVER_ERROR代表的例外資訊,

異于常人的404

上文提到,當請求沒有匹配到控制器的情況下,會拋出NoHandlerFoundException例外,但其實默認情況下不是這樣,默認情況下會出現類似如下頁面:

Whitelabel Error Page

這個頁面是如何出現的呢?實際上,當出現404的時候,默認是不拋例外的,而是 forward跳轉到/error控制器,spring也提供了默認的error控制器,如下:

那么,如何讓404也拋出例外呢,只需在properties檔案中加入如下配置即可:

spring.mvc.throw-exception-if-no-handler-found=true
spring.resources.add-mappings=false

如此,就可以例外處理器中捕獲它了,然后前端只要捕獲到特定的狀態碼,立即跳轉到404頁面即可

捕獲404對應的例外

統一回傳結果

在驗證統一例外處理器之前,順便說一下統一回傳結果,說白了,其實是統一一下回傳結果的資料結構,codemessage 是所有回傳結果中必有的欄位,而當需要回傳資料時,則需要另一個欄位 data 來表示,

所以首先定義一個 BaseResponse 來作為所有回傳結果的基類;

然后定義一個通用回傳結果類CommonResponse,繼承 BaseResponse,而且多了欄位 data

為了區分成功和失敗回傳結果,于是再定義一個 ErrorResponse

最后還有一種常見的回傳結果,即回傳的資料帶有分頁資訊,因為這種介面比較常見,所以有必要單獨定義一個回傳結果類 QueryDataResponse,該類繼承自 CommonResponse,只是把 data 欄位的型別限制為 QueryDdataQueryDdata中定義了分頁資訊相應的欄位,即totalCountpageNopageSizerecords

其中比較常用的只有 CommonResponseQueryDataResponse,但是名字又賊鬼死長,何不定義2個名字超簡單的類來替代呢?于是 RQR 誕生了,以后回傳結果的時候只需這樣寫:new R<>(data)new QR<>(queryData)

所有的回傳結果類的定義這里就不貼出來了

驗證統一例外處理

因為這一套統一例外處理可以說是通用的,所有可以設計成一個 common包,以后每一個新專案/模塊只需引入該包即可,所以為了驗證,需要新建一個專案,并引入該 common包,

主要代碼

下面是用于驗證的主要原始碼:

@Service
public class LicenceService extends ServiceImpl<LicenceMapper, Licence> {

    @Autowired
    private OrganizationClient organizationClient;

    /**
     * 查詢{@link Licence} 詳情
     * @param licenceId
     * @return
     */
    public LicenceDTO queryDetail(Long licenceId) {
        Licence licence = this.getById(licenceId);
        checkNotNull(licence);

        OrganizationDTO org = ClientUtil.execute(() -> organizationClient.getOrganization(licence.getOrganizationId()));
        return toLicenceDTO(licence, org);
    }

    /**
     * 分頁獲取
     * @param licenceParam 分頁查詢引數
     * @return
     */
    public QueryData<SimpleLicenceDTO> getLicences(LicenceParam licenceParam) {
        String licenceType = licenceParam.getLicenceType();
        LicenceTypeEnum licenceTypeEnum = LicenceTypeEnum.parseOfNullable(licenceType);
        // 斷言, 非空
        ResponseEnum.BAD\_LICENCE\_TYPE.assertNotNull(licenceTypeEnum);

        LambdaQueryWrapper<Licence> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(Licence::getLicenceType, licenceType);
        IPage<Licence> page = this.page(new QueryPage<>(licenceParam), wrapper);
        return new QueryData<>(page, this::toSimpleLicenceDTO);
    }

    /**
     * 新增{@link Licence}
     * @param request 請求體
     * @return
     */
    @Transactional(rollbackFor = Throwable.class)
    public LicenceAddRespData addLicence(LicenceAddRequest request) {
        Licence licence = new Licence();
        licence.setOrganizationId(request.getOrganizationId());
        licence.setLicenceType(request.getLicenceType());
        licence.setProductName(request.getProductName());
        licence.setLicenceMax(request.getLicenceMax());
        licence.setLicenceAllocated(request.getLicenceAllocated());
        licence.setComment(request.getComment());
        this.save(licence);

        return new LicenceAddRespData(licence.getLicenceId());
    }

    /**
     * entity -> simple dto
     * @param licence {@link Licence} entity
     * @return {@link SimpleLicenceDTO}
     */
    private SimpleLicenceDTO toSimpleLicenceDTO(Licence licence) {
        // 省略
    }

    /**
     * entity -> dto
     * @param licence {@link Licence} entity
     * @param org {@link OrganizationDTO}
     * @return {@link LicenceDTO}
     */
    private LicenceDTO toLicenceDTO(Licence licence, OrganizationDTO org) {
        // 省略
    }

    /**
     * 校驗{@link Licence}存在
     * @param licence
     */
    private void checkNotNull(Licence licence) {
        ResponseEnum.LICENCE\_NOT\_FOUND.assertNotNull(licence);
    }

}

PS: 這里使用的DAO框架是mybatis-plus,啟動時,自動插入的資料為:

-- licence
INSERT INTO licence (licence\_id, organization\_id, licence\_type, product\_name, licence\_max, licence\_allocated)
VALUES (1, 1, 'user','CustomerPro', 100,5);
INSERT INTO licence (licence\_id, organization\_id, licence\_type, product\_name, licence\_max, licence\_allocated)
VALUES (2, 1, 'user','suitability-plus', 200,189);
INSERT INTO licence (licence\_id, organization\_id, licence\_type, product\_name, licence\_max, licence\_allocated)
VALUES (3, 2, 'user','HR-PowerSuite', 100,4);
INSERT INTO licence (licence\_id, organization\_id, licence\_type, product\_name, licence\_max, licence\_allocated)
VALUES (4, 2, 'core-prod','WildCat Application Gateway', 16,16);

-- organizations
INSERT INTO organization (id, name, contact\_name, contact\_email, contact_phone)
VALUES (1, 'customer-crm-co', 'Mark Balster', '[email protected]', '823-555-1212');
INSERT INTO organization (id, name, contact\_name, contact\_email, contact_phone)
VALUES (2, 'HR-PowerSuite', 'Doug Drewry','[email protected]', '920-555-1212');
開始驗證
捕獲自定義例外

1. 獲取不存在的 licence 詳情:http://localhost:10000/licence/5,成功回應的請求:licenceId=1

檢驗非空

捕獲 Licence not found 例外

Licence not found

2. 根據不存在的 licence type 獲取 licence 串列:http://localhost:10000/licence/list?licenceType=ddd,可選的 licence type 為:user、core-prod ,

校驗非空

捕獲 Bad licence type 例外

Bad licence type

捕獲進入 Controller 前的例外

1. 訪問不存在的介面:http://localhost:10000/licence/list/ddd

捕獲404例外

2. http 方法不支持:http://localhost:10000/licence

PostMapping

捕獲 Request method not supported 例外

Request method not supported

3. 校驗例外1:http://localhost:10000/licence/list?licenceType=

getLicences

LicenceParam

捕獲引數系結校驗例外

licence type cannot be empty

4. 校驗例外2:post 請求,這里使用postman模擬,

addLicence

LicenceAddRequest

請求url即結果

捕獲引數系結校驗例外

注:因為引數系結校驗例外的例外資訊的獲取方式與其它例外不一樣,所以才把這2種情況的例外從 進入 Controller 前的例外 單獨拆出來,下面是例外資訊的收集邏輯:

例外資訊的收集

捕獲未知例外

假設我們現在隨便對 Licence 新增一個欄位 test,但不修改資料庫表結構,然后訪問:http://localhost:10000/licence/1,

推薦閱讀:關于例外處理的 10 個最佳實踐

增加test欄位

捕獲資料庫例外

Error querying database

小結

可以看到,測驗的例外都能夠被捕獲,然后以 codemessage 的形式回傳,每一個專案/模塊,在定義業務例外的時候,只需定義一個列舉類,然后實作介面 BusinessExceptionAssert,最后為每一種業務例外定義對應的列舉實體即可,而不用定義許多例外類,使用的時候也很方便,用法類似斷言,

擴展

在生產環境,若捕獲到 未知例外 或者 ServletException,因為都是一長串的例外資訊,若直接展示給用戶看,顯得不夠專業,于是,我們可以這樣做:當檢測到當前環境是生產環境,那么直接回傳 "網路例外",

生產環境回傳“網路例外”

可以通過以下方式修改當前環境:

修改當前環境為生產環境

總結

使用 斷言列舉類 相結合的方式,再配合統一例外處理,基本大部分的例外都能夠被捕獲,

為什么說大部分例外,因為當引入 spring cloud security 后,還會有認證/授權例外,網關的服務降級例外、跨模塊呼叫例外、遠程呼叫第三方服務例外等,這些例外的捕獲方式與本文介紹的不太一樣,不過限于篇幅,這里不做詳細說明,以后會有單獨的文章介紹,

另外,當需要考慮國際化的時候,捕獲例外后的例外資訊一般不能直接回傳,需要轉換成對應的語言,不過本文已考慮到了這個,獲取訊息的時候已經做了國際化映射,邏輯如下:

獲取國際化訊息

最后總結,全域例外屬于老生長談的話題,希望這次通過手機的專案對大家有點指導性的學習,大家根據實際情況自行修改,

也可以采用以下的jsonResult物件的方式進行處理,也貼出來代碼.

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    /**
     * 沒有登錄
     * @param request
     * @param response
     * @param e
     * @return
     */
    @ExceptionHandler(NoLoginException.class)
    public Object noLoginExceptionHandler(HttpServletRequest request,HttpServletResponse response,Exception e)
    {
        log.error("\[GlobalExceptionHandler\]\[noLoginExceptionHandler\] exception",e);
        JsonResult jsonResult = new JsonResult();
        jsonResult.setCode(JsonResultCode.NO_LOGIN);
        jsonResult.setMessage("用戶登錄失效或者登錄超時,請先登錄");
        return jsonResult;
    }

    /**
     * 業務例外
     * @param request
     * @param response
     * @param e
     * @return
     */
    @ExceptionHandler(ServiceException.class)
    public Object businessExceptionHandler(HttpServletRequest request,HttpServletResponse response,Exception e)
    {
        log.error("\[GlobalExceptionHandler\]\[businessExceptionHandler\] exception",e);
        JsonResult jsonResult = new JsonResult();
        jsonResult.setCode(JsonResultCode.FAILURE);
        jsonResult.setMessage("業務例外,請聯系管理員");
        return jsonResult;
    }

    /**
     * 全域例外處理
     * @param request
     * @param response
     * @param e
     * @return
     */
    @ExceptionHandler(Exception.class)
    public Object exceptionHandler(HttpServletRequest request,HttpServletResponse response,Exception e)
    {
        log.error("\[GlobalExceptionHandler\]\[exceptionHandler\] exception",e);
        JsonResult jsonResult = new JsonResult();
        jsonResult.setCode(JsonResultCode.FAILURE);
        jsonResult.setMessage("系統錯誤,請聯系管理員");
        return jsonResult;
    }
}

推薦去我的博客閱讀更多:

1.Java JVM、集合、多執行緒、新特性系列教程

2.Spring MVC、Spring Boot、Spring Cloud 系列教程

3.Maven、Git、Eclipse、Intellij IDEA 系列工具教程

4.Java、后端、架構、阿里巴巴等大廠最新面試題

覺得不錯,別忘了點贊+轉發哦!

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/168197.html

標籤:Java

上一篇:廣義表深度

下一篇:圖鄰接矩陣洗掉頂點

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more