设计 API 接口 实现统一格式返回
返回格式
后端返回给前端我们一般用 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 了,简单吧。到此返回的设计思路完成,是不是又简洁,又优雅。
总结
这个方案还有没有别的优化空间,当然是有的。如:每次请求都要反射一下,获取请求的方法是否需要包装,其实可以做个缓存,不需要每次都需要解析。