以下內容純屬個人扯淡,僅供參考
目錄
參考:
統一結果返回
題外話:前後端分離是一種設計理念,數據傳輸格式一般都是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、日誌文件收集