前几天搬砖的时候,发现所有接口方法都定义了一样的返回值,不能真正地将业务逻辑表达出来,没有达到“望文生意”的效果。着手改造一下。

虽然标题是Spring Boot,但是这个接口在包spring-webmvc.jar下(请原谅我这个标题党)。ResponseBodyAdvice接口类路径:

org.springframework.web.servlet.mvc.method.annotation

首先,我们需要定义一个统一的状态码ResultCode,来统一工程中异常情况的描述:

/**
 * 统一状态返回码
 * 三级状态码不满足时,可返回二级宏观码,依次类推
 * 状态码参考 alibaba 《JAVA开发手册-泰山版》
 */
public enum ResultCode {

    OK("00000", "成功"),

    /** 一级宏观错误码 */
    CLIENT_ERROR("A0001", "用户端错误 "),

    /** 二级宏观错误码 */
    USER_REGISTER_ERROR("A0100", "用户注册错误"),

    USER_DISAGREE_PRIVACY_PROTOCOL("A0101", "用户未同意隐私协议"),

    REGION_REGISTER_LIMITED("A0102","注册国家或地区受限"),

    VALIDATE_USERNAME_FAILED("A0110","用户名校验失败"),

    USERNAME_EXISTED("A0111","用户名已存在"),

    /* 中间还有好多,鉴于篇幅,就不贴出来了 */
    
    MAIL_NOTICE_FAILED("C0503", "邮件提醒服务失败");

    private String status;
    private String message;

    ResultCode(String status, String message) {
        this.status = status;
        this.message = message;
    }

    public String getStatus() {
        return status;
    }

    public void setStatus(String status) {
        this.status = status;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    @Override
    public String toString() {
        return "ResultCode{" +
                "status='" + status + '\'' +
                ", message='" + message + '\'' +
                '}';
    }
}

枚举的好处我就不多说了,想必大家也经常使用HttpStatus这个枚举。

其次,我们封装一个数据传输对象Result,这个类就用来封装我们统一的返回格式:

import com.jason.enums.ResultCode;
import com.fasterxml.jackson.annotation.JsonView;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import org.springframework.data.domain.Page;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

/**
 * 统一返回DTO
 * 通过 JsonView 尽可能控制返回格式的精简,酌情使用
 */
@ApiModel
public class Result {

    public interface commonResult{};

    public interface standardResult extends commonResult{};

    @ApiModelProperty(value = "status", name = "响应状态码")
    private String status;

    @ApiModelProperty(value = "message", name = "响应信息")
    private String message;

    @ApiModelProperty(value = "body", name = "响应内容")
    private Object body;

    public Result() {
        this.status = ResultCode.OK.getStatus();
        this.message = ResultCode.OK.getMessage();
    }

    public Result(ResultCode resultCode) {
        this.status = resultCode.getStatus();
        this.message = resultCode.getMessage();
    }

    public Result(ResultCode resultCode, String message) {
        this(resultCode);
        this.message = message;
    }

    public Result(Object body) {
        this.status = ResultCode.OK.getStatus();
        this.message = ResultCode.OK.getMessage();
        this.body = body;
    }

    public Result(ResultCode resultCode, Object body) {
        this(body);
        this.status = resultCode.getStatus();
        this.message = resultCode.getMessage();
    }

    public Result(Collection collection) {
        this.status = ResultCode.OK.getStatus();
        this.message = ResultCode.OK.getMessage();
        this.body = collection;
    }

    public Result(ResultCode resultCode, Collection collection) {
        this(collection);
        this.status = resultCode.getStatus();
        this.message = resultCode.getMessage();
    }

    public Result(Page page) {
        this.status = ResultCode.OK.getStatus();
        this.message = ResultCode.OK.getMessage();
        Map<String, Object> info = new HashMap<>(8);
        info.put("totalItem", page.getTotalElements());
        info.put("pageSize", page.getNumber());
        info.put("pageNum", page.getSize());
        info.put("totalPage", page.getTotalPages());
        info.put("item", page.getContent());
        this.body = info;
    }

    public Result(ResultCode resultCode, Page page) {
        this(page);
        this.status = resultCode.getStatus();
        this.message = resultCode.getMessage();
    }

    @JsonView(commonResult.class)
    public String getStatus() {
        return status;
    }

    public void setStatus(String status) {
        this.status = status;
    }

    @JsonView(commonResult.class)
    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    @JsonView(standardResult.class)
    public Object getBody() {
        return body;
    }

    public void setBody(Object body) {
        this.body = body;
    }

    @Override
    public String toString() {
        return "Result{" +
                "status='" + status + '\'' +
                ", message='" + message + '\'' +
                ", body=" + body +
                '}';
    }
}

这个类每个人定义的方式不一样,包括构造方法或者是否用static修饰,大家都可以根据自身情况实现。

最后,我们再实现接口ResponseBody,可以根据一些条件来控制:

import com.jason.dto.Result;
import com.jason.enums.ResultCode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.servlet.mvc.method.annotation.ResponseBodyAdvice;

/**
 * 统一返回对象配置类
 * create by Jason
 */
@ControllerAdvice
public class ResultBodyConfig implements ResponseBodyAdvice<Object> {

    private Logger logger = LoggerFactory.getLogger(this.getClass());

    private final static String PACKAGE_PATH = "com.jason.component";

    /**
     * 针对以下情况 不做 统一包装处理
     * 1.返回值为 void 的方法
     * 2.返回值为 String 类型的方法
     * 3.返回值为 Result 类型的方法
     * 4.在包路径 PACKAGE_PATH 以外的方法
     * @param methodParameter
     * @param aClass
     * @return
     */
    @Override
    public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
        return !methodParameter.getMethod().getReturnType().isAssignableFrom(Void.TYPE)
                && !methodParameter.getMethod().getReturnType().isAssignableFrom(String.class)
                && !methodParameter.getMethod().getReturnType().isAssignableFrom(Result.class)
                && methodParameter.getDeclaringClass().getPackage().getName().contains(PACKAGE_PATH);
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        if (body instanceof Result) {
            return body;
        }
        return new Result(ResultCode.OK, body);
    }
}

方法supports返回true则会执行beforeBodyWrite方法,否则会跳过,继续按照原来接口方法的返回值进行返回。

这里谈一下我过滤这些条件的想法:

  • 返回值为 Void 的方法:返回值为void,嗯。
  • 返回值为 String 的方法: 在SpringMVC中我们会返回字符串来匹配模板,虽然现在都是前后端分离的项目,但是还是按照约定俗成或者是第一反应,将String排除。
  • 返回 Result 的方法:我们已经做了封装,不需要在封装一次了。
  • 不在指定包路径下的方法:用来规避其他组件,比如Swagger

经过以上这种处理,我们在写接口的时候就可以放心大胆的定义业务需要的返回值了,真正的实现了接口方法就可以描述业务的初衷。

但是这样做会出现一个问题,比如Spring Data Jpa实体使用@JsonView时,接口就会返回空,不知道哪位路过的大神可以指导我一下。

除了这种方式,大家还可以通过过滤器来实现这个功能,这里就不做说明了。


虚惊一百场
19 声望7 粉丝

1 + 1 = 2