返回格式
后端返回給前端我們一般用 JSON 體方式,定義如下:
{
#返回狀態碼
code:integer,
#返回信息描述
message:string,
#返回值
data:object
}
CODE 狀態碼
code 返回狀態碼,一般小伙伴們是在開發的時候需要什么,就添加什么。
如接口要返回用戶權限異常,我們加一個狀態碼為 101 吧,下一次又要加一個數據參數異常,就加一個 102 的狀態碼。這樣雖然能夠照常滿足業務,但狀態碼太凌亂了
我們應該可以參考 HTTP 請求返回的狀態碼
:下面是常見的HTTP狀態碼:
200 - 請求成功
301 - 資源(網頁等)被永久轉移到其它URL
404 - 請求的資源(網頁等)不存在
500 - 內部服務器錯誤
我們可以參考這樣的設計,這樣的好處就把錯誤類型歸類到某個區間內,如果區間不夠,可以設計成 4 位數。
#1000~1999 區間表示參數錯誤
#2000~2999 區間表示用戶錯誤
#3000~3999 區間表示接口異常
這樣前端開發人員在得到返回值后,根據狀態碼就可以知道,大概什么錯誤,再根據 message 相關的信息描述,可以快速定位。
Message
這個字段相對理解比較簡單,就是發生錯誤時,如何友好的進行提示。一般的設計是和 code 狀態碼一起設計,如
package com.xxtsoft.enumeration;
/**
* 狀態碼枚舉
* <p>
* 常見的 http 狀態碼
* 200 - 請求成功
* 301 - 資源(網頁等)被永久轉移到其它URL
* 404 - 請求的資源(網頁等)不存在
* 500 - 內部服務器錯誤
* <p>
* <p>
* #1000~1999 區間表示參數錯誤
* #2000~2999 區間表示用戶錯誤
* #3000~3999 區間表示接口異常
*
* @author yang
* @version 1.0.0
* @date 2020-11-18 09:22
*/
public enum ResultCode {
private Integer code;
private String message;
ResultCode(Integer code, String message) {
this.code = code;
this.message = message;
}
}
再在枚舉中定義,狀態碼
package com.xxtsoft.enumeration;
/**
* 狀態碼枚舉
* <p>
* 常見的 http 狀態碼
* 200 - 請求成功
* 301 - 資源(網頁等)被永久轉移到其它URL
* 404 - 請求的資源(網頁等)不存在
* 500 - 內部服務器錯誤
* <p>
* <p>
* #1000~1999 區間表示參數錯誤
* #2000~2999 區間表示用戶錯誤
* #3000~3999 區間表示接口異常
*
* @author yang
* @version 1.0.0
* @date 2020-11-18 09:22
*/
public enum ResultCode {
/**
* 成功狀態碼
*/
SUCCESS(1, "成功"),
/**
* 參數錯誤 1001-1999
*/
PARAM_IS_INVALID(1001, "參數無效"),
/**
* 參數錯誤 1001-1999
*/
PARAM_IS_BLANK(1002, "參數為空"),
/**
* 參數錯誤 1001-1999
*/
PARAM_TYPE_BIND_ERROR(1003, "參數類型錯誤"),
/**
* 參數錯誤 1001-1999
*/
PARAM_NOT_COMPLETE(1004, "參數缺失"),
/**
* 用戶錯誤 2001-2999
*/
USER_NOT_LOGGED_IN(2001, "用戶未登錄,訪問的路徑需要驗證,請登錄"),
/**
* 用戶錯誤 2001-2999
*/
USER_L0GIN_ERROR(2002, "賬號不存在或密碼錯誤"),
/**
* 用戶錯誤 2001-2999
*/
USER_ACCOUNT_FORBIDDEN(2003, "賬號已被禁用"),
/**
* 用戶錯誤 2001-2999
*/
USER_NOT_EXIST(2004, "用戶不存在"),
/**
* 用戶錯誤 2001-2999
*/
USER_HAS_EXISTED(2005, "用戶已存在"),
/**
* 服務器錯誤 沒有此ID 3001
*/
SERVER_NO_SUCH_ID(3001, "沒有此ID");
private Integer code;
private String message;
ResultCode() {
}
ResultCode(Integer code, String message) {
this.code = code;
this.message = message;
}
public Integer code() {
return this.code;
}
public String message() {
return this.message;
}
}
狀態碼和信息就會一一對應,比較好維護。
Data
返回數據體,JSON 格式,根據不同的業務又不同的 JSON 體。
我們要設計一個返回體類 Result
package com.xxtsoft.entity;
import com.xxtsoft.enumeration.ResultCode;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.core.serializer.Serializer;
import java.io.Serializable;
/**
* 返回體
*
* @author yang
* @version 1.0.0
* @date 2020-11-18 09:43
*/
@Data
public class Result implements Serializable {
private Integer code;
private String message;
private Object data;
public Result(Integer code, String message, Object data) {
this.code = code;
this.message = message;
this.data = data;
}
public Result() {
}
public Result(ResultCode resultCode, Object data) {
this.code = resultCode.code();
this.message = resultCode.message();
this.data = data;
}
public Result(ResultCode resultCode) {
this.code = resultCode.code();
this.message = resultCode.message();
}
public Result(Integer code, String message) {
this.code = code;
this.message = message;
}
/**
* 返回成功!
*
* @return 成功
*/
public static Result success() {
final Result result = new Result();
result.setResultCode(ResultCode.SUCCESS);
return result;
}
/**
* 返回成功!
*
* @param data 數據
* @return Result,成功!
*/
public static Result success(Object data) {
final Result result = new Result();
result.setResultCode(ResultCode.SUCCESS);
result.setData(data);
return result;
}
/**
* 返回失敗!
*
* @param resultCode 失敗枚舉
* @return 失敗!
*/
public static Result failure(ResultCode resultCode) {
final Result result = new Result();
result.setResultCode(resultCode);
return result;
}
public static Result failure(Integer code, String msg, Object data) {
return new Result(code, msg, data);
}
public static Result failure(Integer code, String msg) {
return new Result(code, msg);
}
/**
* 返回失敗!
*
* @param resultCode 失敗枚舉
* @param data 失敗數據
* @return 失敗!
*/
public static Result failure(ResultCode resultCode, Object data) {
final Result result = new Result();
result.setResultCode(resultCode);
result.setData(data);
return result;
}
public void setResultCode(ResultCode resultCode) {
this.code = resultCode.code();
this.message = resultCode.message();
}
}
優雅優化
上面我們看到在 Result 類中增加了靜態方法,使得業務處理代碼簡潔了。但小伙伴們有沒有發現這樣有幾個問題:
1、每個方法的返回都是 Result 封裝對象,沒有業務含義
2、在業務代碼中,成功的時候我們調用 Result.success,異常錯誤調用 Result.failure。是不是很多余
3、上面的代碼,判斷 id 是否為 null,其實我們可以使用 hibernate validate 做校驗,沒有必要在方法體中做判斷。
我們最好的方式直接返回真實業務對象,最好不要改變之前的業務方式,如下圖
@RestController
@RequestMapping("/RoleManagementController")
@Validated
@Slf4j
public class RoleManagementController {
@Autowired
private ISysUserRoleService iSysUserRoleService;
/**
* 根據 角色 id,獲取擁有所有該角色的用戶
*
* @param id 角色 id,角色 id 必須是數字,且大於 0
* @return 所有用戶
*/
@GetMapping("/listUsersByRoleId/{id}")
public List<SysUser> listUsersByRoleId(@DecimalMin(value = "0", message = "角色 id 必須是數字,且大於 0") @PathVariable("id") Integer id) {
final List<SysUser> sysUsers = iSysUserRoleService.listUsersByRoleId(id);
if (Validator.isNull(sysUsers) || CollUtil.isEmpty(sysUsers)) {
throw new ResultException(ResultCode.SERVER_NO_SUCH_ID);
}
return sysUsers;
}
}
實現方案
1、定義一個注解 @ResponseResult,表示這個接口返回的值需要包裝一下
2、攔截請求,判斷此請求是否需要被 @ResponseResult 注解
3、核心步驟就是實現接口 ResponseBodyAdvice 和 @ControllerAdvice,判斷是否需要包裝返回值,如果需要,就把 Controller 接口的返回值進行重寫。
注解類
用來標記方法的返回值,是否需要包裝
package com.xxtsoft.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.ElementType.*;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* 用來標記方法的返回值,是否需要包裝
*
* @author yang
* @version 1.0.0
* @date 2020-11-18 09:58
*/
@Retention(RUNTIME)
@Target({TYPE, METHOD})
@Documented
public @interface ResponseResult {
}
攔截器
攔截請求,是否此請求返回的值需要包裝,其實就是運行的時候,解析 @ResponseResult 注解
package com.xxtsoft.interceptor;
import cn.hutool.core.lang.Console;
import com.xxtsoft.annotation.ResponseResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
/**
* 攔截器
* 攔截請求,是否此請求返回的值需要包裝,其實就是運行的時候,解析 @ResponseResult 注解
*
* @author yang
* @version 1.0.0
* @date 2020-11-18 10:01
*/
@Slf4j
@Component
public class ResponseResultInterceptor implements HandlerInterceptor {
/**
* 標記名稱
*/
public static final String RESPONSE_RESULT_ANN = "RESPONSE-RESULT-ANN";
/**
* 此代碼核心思想,就是獲取此請求,是否需要返回值包裝,設置一個屬性標記。
*
* @param request request
* @param response response
* @param handler handler
* @return 包裝
* @throws Exception 異常
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.debug("進入 preHandle 方法");
// 請求的方法
if (handler instanceof HandlerMethod) {
final HandlerMethod handlerMethod = (HandlerMethod) handler;
final Class<?> beanType = handlerMethod.getBeanType();
final Method method = handlerMethod.getMethod();
// 判斷是否在類對象上加了注解
if (beanType.isAnnotationPresent(ResponseResult.class)) {
// 設置此請求返回體,需要包裝,往下傳遞,在 ResponseBodyAdvice 接口進行判斷
log.debug("此類有 ResponseResult 注解");
request.setAttribute(RESPONSE_RESULT_ANN, beanType.getAnnotation(ResponseResult.class));
// 方法上是否有注解
} else if (method.isAnnotationPresent(ResponseResult.class)) {
log.debug("此方法有 ResponseResult 注解");
// 設置此請求返回體,需要包裝,往下傳遞,在 ResponseBodyAdvice 接口進行判斷
request.setAttribute(RESPONSE_RESULT_ANN, method.getAnnotation(ResponseResult.class));
}
}
return true;
}
}
此代碼核心思想,就是獲取此請求,是否需要返回值包裝,設置一個屬性標記。
配置攔截器
package com.xxtsoft.config;
import com.xxtsoft.interceptor.ResponseResultInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* web 配置
*
* @author yang
* @version 1.0.0
* @date 2020-11-18 11:13
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private ResponseResultInterceptor responseResultInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 添加攔截器,配置攔截地址
// 其中 /** 表示當前目錄以及所有子目錄(遞歸),/* 表示當前目錄,不包括子目錄。
registry.addInterceptor(responseResultInterceptor).addPathPatterns("/**");
}
}
重寫返回體
package com.xxtsoft.handler;
import com.xxtsoft.annotation.ResponseResult;
import com.xxtsoft.entity.Result;
import com.xxtsoft.exception.ResultException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import javax.servlet.http.HttpServletRequest;
/**
* 重寫返回體
*
* @author yang
* @version 1.0.0
* @date 2020-11-18 10:08
*/
@Slf4j
@ControllerAdvice
public class ResponseResultHandler implements ResponseBodyAdvice<Object> {
/**
* 標記名稱
*/
public static final String RESPONSE_RESULT_ANN = "RESPONSE-RESULT-ANN";
/**
* 是否請求 包含了 包裝注解標記
* ,沒有就直接返回,不需要重寫返回體
*
* @param returnType returnType
* @param converterType converterType
* @return boolean
*/
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
log.debug("進入 supports 方法");
final ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
assert sra != null;
final HttpServletRequest request = sra.getRequest();
// 判斷請求是否有包裝標記
final ResponseResult responseResultAnn = (ResponseResult) request.getAttribute(RESPONSE_RESULT_ANN);
return responseResultAnn != null;
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
log.debug("進入 返回體,重寫格式 ,處理中!!!body {}", body);
if (body instanceof Result) {
// 是 Result 包轉好的,說明是處理過異常的,直接返回
return body;
}
return Result.success(body);
}
}
上面代碼就是判斷是否需要返回值包裝,如果需要就直接包裝。
異常類
package com.xxtsoft.exception;
import com.xxtsoft.enumeration.ResultCode;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 返回體 異常類
*
* @author yang
* @version 1.0.0
* @date 2020-11-18 10:25
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class ResultException extends RuntimeException {
private Integer code;
private String message;
private Object data;
public ResultException(ResultCode resultCode) {
this.code = resultCode.code();
this.message = resultCode.message();
}
public ResultException(ResultCode resultCode, Object data) {
this.code = resultCode.code();
this.message = resultCode.message();
this.data = data;
}
}
全局異常處理
package com.xxtsoft.controller;
import com.xxtsoft.entity.ResponseJson;
import com.xxtsoft.entity.Result;
import com.xxtsoft.enumeration.ResultCode;
import com.xxtsoft.exception.ResultException;
import com.xxtsoft.exception.SysDeptException;
import com.xxtsoft.exception.SysRoleException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.validation.ValidationException;
/**
* 系統級別的異常處理
*
* @author yang
* @version 1.0.0
* @date 2020-11-18 10:55
*/
@RestControllerAdvice
@Slf4j
public class SystemExceptionHandler {
/**
* 捕獲
* ResultException
*
* @param exception ResultException
* @return 對應的信息
*/
@ExceptionHandler(value = {ResultException.class})
public Result sysDeptException(ResultException exception) {
log.debug("code {},message {},data {} ", exception.getCode(), exception.getMessage(), exception.getData());
return Result.failure(exception.getCode(), exception.getMessage(), exception.getData());
}
/**
* 處理 參數不合法異常
*
* @param validator ValidationException
* @return Result
*/
@ExceptionHandler(value = {ValidationException.class})
public Result validationException(Exception validator) {
log.debug("{}", validator.toString());
return Result.failure(ResultCode.PARAM_IS_INVALID, validator.getMessage());
}
}
重寫 Controller
package com.xxtsoft.controller.system.management;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Validator;
import cn.hutool.core.lang.tree.Tree;
import com.xxtsoft.annotation.ResponseResult;
import com.xxtsoft.entity.*;
import com.xxtsoft.enumeration.ResultCode;
import com.xxtsoft.exception.ResultException;
import com.xxtsoft.exception.SysDeptException;
import com.xxtsoft.exception.SysRoleException;
import com.xxtsoft.service.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.annotations.Delete;
import org.hibernate.validator.constraints.Length;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.NotBlank;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
/**
* 角色管理控制器
* <p>
* 獲取權限樹
* <p>
* 獲取所有角色
* <p>
* 添加角色
* 刪除角色
*
* @author yang
* @version 1.0.0
* @date 2020-11-10 10:03
*/
@RestController
@RequestMapping("/RoleManagementController")
@Validated
@Slf4j
public class RoleManagementController {
@Autowired
private ISysMenuService iSysMenuService;
@Autowired
private ISysUserService iSysUserService;
@Autowired
private ISysRoleService iSysRoleService;
@Autowired
private ISysRoleFunctionService iSysRoleFunctionService;
@Autowired
private ISysUserRoleService iSysUserRoleService;
/**
* 獲取 菜單樹
*
* @return 菜單樹
*/
@PostMapping("/treeList")
public ResponseJson treeList() {
final List<Tree<Integer>> trees = iSysMenuService.treeList();
log.debug("trees {}", trees);
return new ResponseJson().ok(trees).message("獲取成功");
}
/**
* 獲取 所有的角色
*
* @return 所有的角色
*/
@GetMapping("/roleList")
public ResponseJson roleListResponseJson() {
final List<SysRole> sysRoleList = iSysRoleService.list();
log.debug("{}", sysRoleList);
return new ResponseJson().ok(sysRoleList).message("獲取成功");
}
/**
* 根據 角色 id 獲取對應的 (功能)權限
*
* @param id 角色 id
* @return 功能(權限)
*/
@GetMapping("/listFunctionByRoleId/{id}")
public ResponseJson getJurisdiction(@DecimalMin(value = "0", message = "角色 id 必須是數字,且大於 0") @PathVariable("id") Integer id) {
final List<SysFunction> sysFunctionList = iSysRoleFunctionService.listFunctionByRoleId(id);
log.debug("id {},list {}", id, sysFunctionList);
if (Validator.isNull(sysFunctionList) || CollUtil.isEmpty(sysFunctionList)) {
throw new SysRoleException("沒有此 id");
}
return new ResponseJson().ok(sysFunctionList).message("獲取成功");
}
/**
* 根據 角色 id,獲取擁有所有該角色的用戶
*
* @param id 角色 id,角色 id 必須是數字,且大於 0
* @return 所有用戶
*/
@ResponseResult
@GetMapping("/listUsersByRoleId/{id}")
public List<SysUser> listUsersByRoleId(@DecimalMin(value = "0", message = "角色 id 必須是數字,且大於 0") @PathVariable("id") Integer id) {
final List<SysUser> sysUsers = iSysUserRoleService.listUsersByRoleId(id);
if (Validator.isNull(sysUsers) || CollUtil.isEmpty(sysUsers)) {
throw new ResultException(ResultCode.SERVER_NO_SUCH_ID);
}
return sysUsers;
}
/**
* 根據 角色 id 和 用戶 ID 取消一條權限
*
* @param role 角色 id
* @param user 用戶 ID
* @return 是否取成功
*/
@ResponseResult
@DeleteMapping("/cancelRoleByRoleIdAndUserId/{role}/{user}")
public Boolean cancelRoleByRoleIdAndUserId(@DecimalMin(value = "0", message = "角色 id 必須是數字,且大於 0") @PathVariable("role") Integer role, @DecimalMin(value = "0", message = "用戶 id 必須是數字,且大於 0") @PathVariable("user") Integer user) {
log.debug(" role {},user {}", role, user);
return iSysUserRoleService.cancelRoleByRoleIdAndUserId(role, user);
}
/**
* 根據 角色 id
* 刪除一個角色
*
* @param roleId 角色 id
* @return 是否成功!
*/
@ResponseResult
@DeleteMapping("/delRoleByRoleId/{roleId}")
public Boolean delRoleByRoleId(@PathVariable("roleId") Integer roleId) {
log.debug("roleId {}", roleId);
// 刪除一個角色
final boolean b = iSysRoleService.removeById(roleId);
// 根據 角色 去 角色功能表 刪除角色對應的功能
iSysRoleFunctionService.deleteByRoleId(roleId);
return b;
}
/**
* 添加一個角色
*
* @param roleName 角色名
* @param roleDes 角色描述
* @return 添加成功后的角色
*/
@ResponseResult
@PutMapping("/addRole/{roleName}/{roleDes}")
public Boolean addRole(@NotBlank(message = "角色名稱不能為空")
@Length(max = 50, min = 1, message = "角色名稱長度限制 1 ~ 50") @PathVariable String roleName, @NotBlank(message = "角色描述不能為空")
@Length(max = 50, min = 1, message = "角色描述長度限制 1 ~ 50") @PathVariable String roleDes) {
log.debug(" 角色名稱 {} 角色描述 {}", roleName, roleDes);
final SysRole sysRole = SysRole.builder()
.froleName(roleName)
.froleDesc(roleDes)
.fcreateLn(iSysUserService.getLoginUserName())
.fcreateDate(LocalDateTime.now())
.flastModifyLn(iSysUserService.getLoginUserName())
.flastModifyDate(LocalDateTime.now())
.fversion(LocalDate.now())
.build();
return iSysRoleService.save(sysRole);
}
/**
* 根據角色 id 變更權限
*
* @param roleId 角色 id
* @param functionList 權限 數組
* @return 是否成功
*/
@ResponseResult
@PutMapping("/AddPermissionByRoleId/{roleId}")
public boolean addPermissionByRoleId(@PathVariable("roleId") Integer roleId, @RequestBody List<Integer> functionList) {
log.debug(" roleId {}, functions {}", roleId, functionList);
return iSysRoleFunctionService.addPermissionByRoleId(roleId, functionList);
}
}
在控制器類上或者方法體上加上 @ResponseResult 注解,這樣就 ok 了,簡單吧。到此返回的設計思路完成,是不是又簡潔,又優雅。
總結
這個方案還有沒有別的優化空間,當然是有的。如:每次請求都要反射一下,獲取請求的方法是否需要包裝,其實可以做個緩存,不需要每次都需要解析。