设计 API 接口 实现统一格式返回

返回格式

后端返回给前端我们一般用 JSON 体方式,定义如下:

{
	#返回状态码
	code:integer,		
	#返回信息描述
	message:string,
	#返回值
	data:object
}

CODE 状态码

code 返回状态码,一般小伙伴们是在开发的时候需要什么,就添加什么

如接口要返回用户权限异常,我们加一个状态码为 101 吧,下一次又要加一个数据参数异常,就加一个 102 的状态码。这样虽然能够照常满足业务,但状态码太凌乱了

我们应该可以参考 HTTP 请求返回的状态码

:下面是常见的HTTP状态码:
200 - 请求成功
301 - 资源(网页等)被永久转移到其它URL
404 - 请求的资源(网页等)不存在
500 - 内部服务器错误

HMJ7hW

我们可以参考这样的设计,这样的好处就把错误类型归类到某个区间内,如果区间不够,可以设计成 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 了,简单吧。到此返回的设计思路完成,是不是又简洁,又优雅。

总结

这个方案还有没有别的优化空间,当然是有的。如:每次请求都要反射一下,获取请求的方法是否需要包装,其实可以做个缓存,不需要每次都需要解析

posted @ 2020-12-24 09:48  javaYanglei  阅读(1977)  评论(0编辑  收藏  举报