With 3個Web基礎模塊

以下內容純屬個人扯淡,僅供參考

目錄

統一結果返回

全局異常處理

全局日誌收集


 

參考:

Java項目構建基礎:統一結果,統一異常,統一日誌

統一結果返回

題外話:前後端分離是一種設計理念,數據傳輸格式一般都是json,因此統一一個規範的數據格式有利於雙方代碼約定。因此即便使用了Thymeleaf、Freemarker等框架,也儘量這樣設計:提供單獨的控制器方法僅用於返回視圖,額外提供方法用於數據交互(但這樣設計的話是無法實現純粹的RestFul風格的)

參考:SpringBoot2.2.2.RELEASE+Thymeleaf

(1)統一數據格式

public class CommonResult<T> {
    private long code;
    private String message;
    private T data;
    protected CommonResult() {
    }
    protected CommonResult(long code, String message, T data) {
        this.code = code;
        this.message = message;
        this.data = data;
    }
    public long getCode() {
        return code;
    }
    public void setCode(long code) {
        this.code = code;
    }
    public String getMessage() {
        return message;
    }
    public void setMessage(String message) {
        this.message = message;
    }
    public T getData() {
        return data;
    }
    public void setData(T data) {
        this.data = data;
    }
}

這個CommonResult就是一個領域類,類似DTO。它定義了code、message、data三個屬性,分別表示統一數據格式中的狀態碼、信息、數據,構造器和setter/getter是一個DTO必備的。

定義完上述DTO類後,我們如果要返回數據到前端,那麼就像這樣,示例

@GeiMapping("user/{userId}")
public CommonResult<User> getUser(@ParamVariable String userId){
    
    User user = userService.getOne(userId);
    if(data == null){
         return new CommonResult("500","操作失敗",user);
    }
    return new CommonResult("200","操作成功",user);
}

擴展:泛型T。這裏定義data是泛型,那麼他能接收任意類型數據,這裏用Object類型替換也是能接受任意類型的,但會有強制類型轉換風險問題,這是運行時纔會報出的問題,使用泛型可以在編譯期就能發現類型轉換問題。參考:java 泛型和object比較

(2)狀態碼、消息體枚舉類

就功能而言是沒問題的,我們在任何需要返回給前端數據的地方調用即可,但是就代碼維護而言是有很多問題的:

1.硬編碼
    "200"、"操作成功"等字面量不建議直接寫在業務代碼邏輯裏(IDEA會提示你這是魔法值)
2.表意不夠
    200本身是個無表意的量值,而若定義爲private static final String SUCCESS = "200"才能表達這個200是成功的意思,代碼
並非你自己看得懂就足夠了
3.散亂、重構不友好
    RFC規範規定:第1位數字表示了5種響應狀態:1=消息,2=成功,3=重定向,3=請求錯誤,5=服務器錯誤
    這樣的設計下,最多支持10類大類,每個大類下最多100種具體情況,即共1000種具體響應碼

當系統業務後期擴大到一定規模時,3位狀態碼已不足以支撐所有所有情況,3位數太少表意不夠。
可以設計爲5位,前2位表示大類情況,後3位表示具體情況,就共10000種具體響應碼
那麼,當你要用5位去重構替換3位時,你就不得不修改整個Web每個控制器方法返回處的代碼
我們必須要知曉所有接口的返回值,才能知道整個系統返回給前端的code有哪些類型
如果能定義到一個類中,那麼就顯而易見的了
    

code字段對前端來說至關重要,它的值有限可知的,只能是500、400、401等等,前端的需要根據code的具體值去控制js邏輯,例如:200表示正常,那麼就進行接下來的操作:從data中獲取數據顯示等等;500表示異常,此時需要取出msg值提示用戶失敗。因此code值間接控制着前端的代碼執行方向,必須謹慎對待

msg字段是提示信息,本身也不是很重要。我們可以設計成半有限可知的:既有提供默認的值,如:操作成功、操作失敗等等,也支持根據特定情況傳入特定的提示進行選擇,("用戶名錯誤",這樣的提示信息是特定業務下的返回信息)

因此,可以將code和msg設計到一個枚舉類中,其中每個枚舉實例中保存着那些有限可知的值。

public enum ResultCode {
    /**
     * 操作成功
     */
    SUCCESS(200, "操作成功"),
    /**
     * 操作失敗,服務器內部錯誤
     */
    FAILED(500, "操作失敗"),
    /**
     * 參數檢驗失敗
     */
    VALIDATE_FAILED(400, "請求參數有誤"),
    /**
     * 暫未登錄或token已經過期
     */
    UNAUTHORIZED(401, "登錄失敗"),
    /**
     * 沒有相關權限
     */
    FORBIDDEN(403, "沒有相關權限"),
    /**
     * 未找到資源
     */
    NOT_FOUND(404,"未找到相關資源");

    private long code;

    private String message;

    private ResultCode(long code, String message) {
        this.code = code;
        this.message = message;
    }

    public long getCode() {
        return this.code;
    }

    
    public String getMessage() {
        return this.message;
    }

}

這裏是定義一個普通的枚舉類ResultCode,其中6個實例分別對應返回給前端的6種狀態碼和對應的默認提示信息,如果還有更多的情況,就直接在枚舉類中添加對應實例即可

現在我們可以這樣使用

@GeiMapping("user/{userId}")
public CommonResult<User> getUser(@ParamVariable String userId){
    
    User user = userService.getOne(userId);
    if(data == null){
        //默認提示消息
        return new CommonResult(FAILED.getCode(),SUCCESS.getMessage(),user);
        //如果是要自定義的提示消息
        //return new CommonResult(FAILED.getCode(),"用戶不存在",user);
    }
    return new CommonResult(SUCCESS.getCode(),SUCCESS.getMessage(),user);
}

擴展:如果我們希望枚舉實例擁有某種能力,通過這個能力能完成某件事情,比如:doEat(),每個枚舉實例都需要有doEat()這樣的能力,但是每個枚舉實例執行eat的內容都不一樣,可以用枚舉類實現接口完成

public interface Eat {

    void doEat();
}

這個時候枚舉類就需要有一個Eat類型的成員去完成這樣的事情,並且每個枚舉實例做的事情內容不一樣

public enum ResultCode {
    
    SUCCESS(200, "操作成功",new Eat(){
        
        @Override
        public void doEat(){
            System.out.println("成功喫");
        }
    }),
    
    FAILED(500, "操作失敗",new Eat(){

        @Override
        public void doEat(){
            System.out.println("失敗喫");
        }
    });


    private long code;

    private String message;

    private Eat eat;

    private ResultCode(long code, String message,Eat eat) {
        this.code = code;
        this.message = message;
        this.eat = eat;
    }

    public long getCode() {
        return this.code;
    }

    
    public String getMessage() {
        return this.message;
    }

    public void doEat() {
        eat.doEat();
    }

}

這樣,調用不同枚舉實例去完成doEat事情時,它們都具備doEat()方法能力,但是有各自的實現

ResultCode.SUCCESS.doEat();
ResultCode.FAILED.doEat();

而當每個枚舉實例在這樣的方法內執行的代碼邏輯是一樣時,就可以將方法都抽離到公共接口中

public interface IErrorCode {

    /**
     * 獲取code屬性
     *
     * @return -
     */
    long getCode();

    /**
     * 獲取Message屬性
     *
     * @return -
     */
    String getMessage();
}

因此,最終枚舉類被設計成這樣

public enum ResultCode implements IErrorCode {
    /**
     * 操作成功
     */
    SUCCESS(200, "操作成功"),
    /**
     * 操作失敗,服務器內部錯誤
     */
    FAILED(500, "操作失敗"),
    /**
     * 參數檢驗失敗
     */
    VALIDATE_FAILED(400, "請求參數有誤"),
    /**
     * 暫未登錄或token已經過期
     */
    UNAUTHORIZED(401, "登錄失敗"),
    /**
     * 沒有相關權限
     */
    FORBIDDEN(403, "沒有相關權限"),
    /**
     * 未找到資源
     */
    NOT_FOUND(404,"未找到相關資源");

    private long code;

    private String message;

    private ResultCode(long code, String message) {
        this.code = code;
        this.message = message;
    }

    @Override
    public long getCode() {
        return this.code;
    }

    @Override    
    public String getMessage() {
        return this.message;
    }

}

這實際是"面向接口"思想用在枚舉類上,那麼若有方法需要接收ResultCode枚舉類作爲形參時,我們就可以傳遞一個IErrorCode接口,而不是ResultCode類型

(3)統一結果生成工具類

上面的代碼還是有一點冗餘的。我們每次返回CommonResult對象,Controller控制器方法需要知道太多細節了:它需要知道CommonResult構造器的3個參數具體每個參數細節,例如:當用戶不存在時,它應該傳遞一個null,而不是user=null。

return new CommonResult(FAILED.getCode(),"用戶不存在",user);
return new CommonResult(SUCCESS.getCode(),SUCCESS.getMessage(),user);

因此,我們應該把這種細節交由另外一個類負責,控制器方法只需要選擇方法。它可以選擇不傳遞任何數據給CommonResult對象返回給前端,也可以傳遞數據、特定的提示信息

public class CommonResultUtil {

/**
     * 成功返回結果
     *
     * @param data 獲取的數據
     */
    public static <T> CommonResult<T> success(T data) {
        return new CommonResult<T>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), data);
    }

    /**
     * 成功返回結果
     *
     * @param data 獲取的數據
     * @param  message 提示信息
     */
    public static <T> CommonResult<T> success(T data, String message) {
        return new CommonResult<T>(ResultCode.SUCCESS.getCode(), message, data);
    }

    /**
     * 默認失敗返回結果
     * 使用{@link ResultCode#FAILED}
     */
    public static <T> CommonResult<T> failed() {
        return failed(ResultCode.FAILED);
    }

    /**
     * 失敗返回結果
     * @param message 提示信息
     */
    public static <T> CommonResult<T> failed(String message) {
        return new CommonResult<T>(ResultCode.FAILED.getCode(), message, null);
    }

    /**
     * 通用接口
     * @param errorCode 錯誤碼
     */
    public static <T> CommonResult<T> failed(IErrorCode errorCode) {
        return new CommonResult<T>(errorCode.getCode(), errorCode.getMessage(), null);
    }

    /**
     * 參數驗證失敗返回結果
     */
    public static <T> CommonResult<T> validateFailed() {
        return failed(ResultCode.VALIDATE_FAILED);
    }

    /**
     * 參數驗證失敗返回結果
     * @param message 提示信息
     */
    public static <T> CommonResult<T> validateFailed(String message) {
        return new CommonResult<T>(ResultCode.VALIDATE_FAILED.getCode(), message, null);
    }

    /**
     * 未認證/登錄的返回結果
     */
    public static <T> CommonResult<T> unAuthenticated(T data) {
        return new CommonResult<T>(ResultCode.UNAUTHORIZED.getCode(), ResultCode.UNAUTHORIZED.getMessage(), data);
    }

    /**
     * 未授權的返回結果
     */
    public static <T> CommonResult<T> unAuthorized(T data) {
        return new CommonResult<T>(ResultCode.FORBIDDEN.getCode(), ResultCode.FORBIDDEN.getMessage(), data);
    }
}

那麼,現在調用方法就可以改成

return CommonResultUtil.success(user);
return CommonResultUtil.failed("用戶不存在"); //這個字面量最好也定義一個static final來代替

疑問:分頁對象怎麼傳遞呢?:這裏使用的是MybatisPlus。定義一個分頁對象,再將分頁對象傳遞給CommonResult即可,分頁對象定義如下:

/**
 * 通用分頁
 *
 * @date 17:23 2020/3/27
 * @author 
 **/
@Data
public class CommonPage<T> {

    /**
     * 當前頁
     */
    private long pageNum;

    /**
     * 單頁記錄數
     */
    private long pageSize;

    /**
     * 總頁數
     */
    private long totalPage;

    /**
     * 總記錄數
     */
    private long count;

    /**
     * 數據集
     */
    private List<T> data;

    /**
     * 封裝List數據到
     *
     * @date 17:33 2020/3/27
     * @author 李文龍
     * @param
     * @return
     **/
    public static <T> CommonPage<T> restPage(IPage<T> page) {
        CommonPage<T> result = new CommonPage<T>();
        result.setTotalPage(page.getPages());
        result.setPageNum(page.getCurrent());
        result.setPageSize(page.getSize());
        result.setCount(page.getTotal());
        result.setData(page.getRecords());
        return result;
    }
}

調用示例

@GetMapping("listAll")
public CommonResult<CommonPage<PctMenuVO>> listAll(
            @RequestParam Integer page,
            @RequestParam Integer limit,
            @RequestParam(value = "name", required = false) String name) {
    CommonPage<PctMenuVO> commonPage = pctMenuService.queryPctMenuByNameWithPaging(page, limit, name);
    return CommonResult.success(commonPage);
}

//PctMenuServiceImpl
public CommonPage<PctMenuVO> queryPctMenuByNameWithPaging(Integer current, Integer size, String name) {
    Page<PctMenuVO> page = new Page<>(current, size);
    IPage<PctMenuVO> data = baseMapper.queryPctMenuByNameWithPaging(page, name);
    return CommonPage.restPage(data);
}

//PctMenuMapper
Page<PctMenuVO> queryPctMenuByNameWithPaging(Page<PctMenuVO> page, @Param("name")String name);

//PctMenuMapper.xml
//<select id="queryPctMenuByNameWithPaging" //resultType="com.yihuacomputer.yhcloud.vo.PctMenuVO">
//    SELECT
//        pm.ID,pm.CODE,pm.NAME,pm.URL,
//        pm.TYPE,pm.PARENT_ID,pm.STATUS,pm.LAYOUT_EN,
//        pm.REMARK,pm.OPERATOR,pm.OPERATE_TIME
//    FROM PCT_MENU pm
//    WHERE 1=1
//    AND pm.STATUS != 3
//    <if test="name != null">
//        AND pm.NAME like '%${name}%'
//    </if>
//</select>

 

全局異常處理

1、系統異常設計

1)參數校驗異常

參考:SpringBoot2.2.2.RELEASE+參數校驗

其中Hibernate Validator所提供的參數校驗註解(或者是自定義的參數校驗註解),在參數校驗功能中若校驗失敗,則會拋出

MethodArgumentNotValidException

2)自定義參數校驗異常

註解型參數校驗只是在Controlller層,其在於當校驗失敗時,利用AOP原理攔截控制器方法調用並拋出異常。但這種方式只適用於簡單的參數校驗,當校驗邏輯複雜時、或希望在Service業務層進行更自由化的參數校驗時,可以自定義一個異常類,然後在業務邏輯進行編碼實現參數校驗,主動拋出異常以終止方法執行(注意:這種方式可能很難去複用校驗邏輯,它適合特定的場景)

package com.yihuacomputer.yhcloud.common.exception;

import com.yihuacomputer.yhcloud.common.api.IErrorCode;

/**
 * @ClassName ExcelParseException
 * @Description Excel解析異常
 * @Author 羅新宇
 * @Date 2020/3/31 9:22
**/
public class ExcelParseException extends RuntimeException {

    private IErrorCode errorCode;

    public ExcelParseException(IErrorCode errorCode) {
        super(errorCode.getMessage());
        this.errorCode = errorCode;
    }

    public ExcelParseException(String message) {
        super(message);
    }

    public ExcelParseException(Throwable cause) {
        super(cause);
    }

    public ExcelParseException(IErrorCode errorCode,String message) {
        super(message);
        this.errorCode = errorCode;
    }

    public ExcelParseException(IErrorCode errorCode,String message, Throwable cause) {
        super(message, cause);
        this.errorCode = errorCode;

    }

    public IErrorCode getErrorCode() {
        return errorCode;
    }
}

異常工具類

public class ExcelParseAsserts {

    public static void fail(String message){
        throw new ExcelParseException(message);
    }

    public static void fail(IErrorCode errorCode){
        throw new ExcelParseException(errorCode);
    }

    public static void fail(IErrorCode errorCode,String message) {
        throw new ExcelParseException(errorCode,message);
    }

    public static void fail(IErrorCode errorCode,String message,Throwable throwable){
        throw new ExcelParseException(errorCode,message,throwable);
    }
}

使用示例

 if(cell.getCellTypeEnum() == CellType.NUMERIC){
    //設置Y軸數值
    pctRecord.setYValue(cell.getNumericCellValue());
}else{
    //返回錯誤信息
    ExcelParseAsserts.fail(ResultCode.FAILED,"第"+r+"行第"+c+"列數據格式不正確,應爲數字類型!");
}

2、控制器通知


/**
 * 全局異常處理器
 *
 * @date 16:49 2020/3/19
 * @author 
 **/
@ControllerAdvice
@Order(value = Ordered.HIGHEST_PRECEDENCE)
@Slf4j
public class GlobalExceptionHandler {
    private static final String UNKNOWN_EXCEPTION_MSG = "未知異常";

    /**
     * 其他未捕獲的異常
     *
     * @date 9:47 2020/4/8
     * @author 李文龍
     * @param e:
     * @return
     **/
    @ResponseBody
    @ExceptionHandler(value = Exception.class)
    public CommonResult<String> handleUnKnownException(Exception e) {
        //TODO 目前是直接打印在控制檯以便追蹤問題,後期需要將異常跟蹤信息備份到log文件中
        e.printStackTrace();

        String msg = e.getMessage();

        if (StringUtils.isEmpty(msg)) {
            return CommonResult.failed(msg);
        }

        return CommonResult.failed(UNKNOWN_EXCEPTION_MSG);
    }


    /**
     * 註解型:參數校驗異常
     **/
    @ResponseBody
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public CommonResult<String> handleMethodArgumentNotValidException(
        MethodArgumentNotValidException e) {
        BindingResult result = e.getBindingResult();
        FieldError fieldError = result.getFieldError();

        if (fieldError != null) {
            return CommonResult.validateFailed(fieldError.getDefaultMessage());
        } else {
            return CommonResult.validateFailed();
        }
    }

    /**
     * 捕獲表格解析異常
     */
    @ResponseBody
    @ExceptionHandler(value = ExcelParseException.class)
    public CommonResult<String> handleParamException(ExcelParseException e) {
        if (e.getMessage() != null) {
            return CommonResult.failed(e.getMessage());
        }

        return CommonResult.failed();
    }

}

疑問:多個異常處理器所處理的異常類型是有繼承關係的,那如何判定使用哪個處理器呢?ExceptionHandler的執行順序

通過查看源碼:ExceptionHandlerMethodResolver#getMappedMethod方法可知,首先找到可以匹配的所有ExceptionHandler,然後對其進行排序,利用深度比較器算法(遞歸判斷父類異常是否爲目標異常)取深度最小的那個,也即匹配度最高的那個

結論:定義多個ExceptionHandler時,要注意其所處理的異常類的繼承關係,這決定了處理優先級

全局日誌收集

1、打印web層入口信息

2、日誌文件收集

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章