所有分类
  • 所有分类
  • 未分类

SpringBoot-全局响应处理

简介

本文用实例介绍SpringBoot如何进行全局响应处理。

为什么进行全局响应处理?

在实际开发中,后端给前端的响应值必须要有一层封装,封装里边有状态码、错误信息、接口数据等。前端会先判断状态码,如果是成功,则获取数据;如果是失败,直接将错误信息提示给用户。

  1. 方便前后端交互
    • 有了这个公共的响应值,前后端交互会变得统一,这样会很便捷高效,会减少很多问题。
  2. 方便feign调用
    • 这个公共响应值也会作为feign的公共响应,便于调用方统一解析。

而这个封装响应值的操作,最优雅的做法是全局处理,因为这个公共响应体是个业务无关的东西。如果不全局处理,会有如下问题

  1. 代码繁琐
    • Controller都要这么写:ResultWrapper.success(data)。
  2. 会有人会直接返回数据,不封装
    • 会有人直接将数据在不封装的情况下给前端,或提供给别人!导致项目不统一,难以维护
    • 如果有报错,无法将错误提示给调用方。因为没有封装的话,无法放到错误信息里!

上边这些问题会导致:项目问题很多、bug很多、无法维护

最终效果

Controller

package com.knife.example.business.product.controller;
 
import com.knife.example.business.product.vo.ProductVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Api(tags = "商品")
@RestController
@RequestMapping("product")
public class ProductController {

    @ApiOperation("查询详情")
    @GetMapping("detail")
    public ProductVO detail(Long id) {
        //省略查数据库等逻辑

        ProductVO productVO = new ProductVO();
        productVO.setId(1L);
        productVO.setName("鼠标");
        productVO.setStockQuantity(1200);

        return productVO;
    }
}

测试

这里我用的knife4j,用postman也是一样的。

绿色箭头所指是响应值,可以发现,自动包装到了公共封装类里。

代码

方案简述

全局响应处理(处理返回值):@ControllerAdvice+ 实现ResponseBodyAdvice接口。

@ControllerAdvice所在类的beforBodyWrite方法将会在Controller方法执行之后执行。

下载源码

此隐藏内容仅限VIP查看升级VIP

代码结构

业务类

Controller

package com.knife.example.business.product.controller;
 
import com.knife.example.business.product.vo.ProductVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Api(tags = "商品")
@RestController
@RequestMapping("product")
public class ProductController {

    @ApiOperation("查询详情")
    @GetMapping("detail")
    public ProductVO detail(Long id) {
        //省略查数据库等逻辑

        ProductVO productVO = new ProductVO();
        productVO.setId(1L);
        productVO.setName("鼠标");
        productVO.setStockQuantity(1200);

        return productVO;
    }
}

VO

package com.knife.example.business.product.vo;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

@Data
@ApiModel("商品响应类")
public class ProductVO {
    @ApiModelProperty("id")
    private Long id;

    @ApiModelProperty("名字")
    private String name;

    @ApiModelProperty("库存数量")
    private Integer stockQuantity;
}

全局响应处理类(核心)

package com.knife.example.common.advice;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.knife.example.common.constant.WrapperIgnoreUrl;
import com.knife.example.common.entity.ResultWrapper;
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.bind.annotation.ResponseBody;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

import java.lang.reflect.Executable;
import java.lang.reflect.Method;

@Slf4j
@ControllerAdvice
public class GlobalResponseBodyAdvice implements ResponseBodyAdvice<Object> {
    /**
     * 返回值的含义:是否要处理
     */
    @Override
    public boolean supports(MethodParameter methodParameter,
                            Class<? extends HttpMessageConverter<?>> converterType) {
        // 若接口返回的类型本身就是ResultWrapper,则无需操作,返回false
        // return !methodParameter.getParameterType().equals(ResultWrapper.class);
        // 本处全部都放过,在后边进行处理
        return true;
    }

    @Override
    @ResponseBody
    public Object beforeBodyWrite(Object body, MethodParameter methodParameter,
                                  MediaType selectedContentType,
                                  Class<? extends HttpMessageConverter<?>> selectedConverterType,
                                  ServerHttpRequest request, ServerHttpResponse response) {
        if (WrapperIgnoreUrl.isInWrapperIgnoreUrl(request.getURI().getPath())) {
            // 如果不需要处理,直接跳过
            return body;
        }

        Executable executable = methodParameter.getExecutable();
        if (!(executable instanceof Method)) {
            return body;
        }

        Method method = (Method) executable;
        Class<?> returnType = method.getReturnType();
        if (returnType == String.class) {
            // 若返回值为String类型,需要包装为String类型返回。否则会报错
            try {
                ObjectMapper objectMapper = new ObjectMapper();
                ResultWrapper<Object> resultWrapper = ResultWrapper.success().data(body);
                return objectMapper.writeValueAsString(resultWrapper);
            } catch (JsonProcessingException e) {
                throw new RuntimeException("序列化String错误");
            }
        } else if (returnType == ResultWrapper.class) {
            // 如果已经封装过了,不再封装
            return body;
        }
        return ResultWrapper.success().data(body);
    }
}

其他类

公共响应类

package com.knife.example.common.entity;

import com.knife.example.common.constant.ResultCode;
import lombok.Data;

@Data
public class ResultWrapper<T> {
    private Boolean success = true;

    private Integer code;

    private T data;

    private String message;

    private ResultWrapper() {
    }

    public static <T> ResultWrapper<T> success() {
        return success(null);
    }

    public static <T> ResultWrapper<T> success(T data) {
        return assemble(ResultCode.SUCCESS.getCode(), true, data);
    }

    public static <T> ResultWrapper<T> error() {
        return error(null);
    }

    public static <T> ResultWrapper<T> error(T data) {
        return assemble(ResultCode.SYSTEM_FAILURE.getCode(), false, data);
    }

    public ResultWrapper<T> data(T data) {
        this.setData(data);
        return this;
    }

    public ResultWrapper<T> message(String message) {
        this.setMessage(message);
        return this;
    }

    public ResultWrapper<T> code(int code) {
        this.setCode(code);
        return this;
    }

    public static <T> ResultWrapper<T> assemble(int code, boolean success, T data) {
        ResultWrapper<T> resultWrapper = new ResultWrapper<>();
        resultWrapper.setCode(code);
        resultWrapper.setSuccess(success);
        resultWrapper.setData(data);

        return resultWrapper;
    }
}

返回结果码

package com.knife.example.common.constant;

public enum ResultCode {
    SUCCESS(1000, "访问成功"),
    SYSTEM_FAILURE(1001, "系统异常"),
    ;

    private final int code;
    private final String description;

    ResultCode(int code, String description) {
        this.code = code;
        this.description = description;
    }

    public int getCode() {
        return code;
    }

    public String getDescription() {
        return description;
    }
}

不自动封装的URL

package com.knife.example.common.constant;

import org.springframework.util.AntPathMatcher;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * 全局响应中不包装的url列表
 */
public interface WrapperIgnoreUrl {
    List<String> KNIFE4J = Arrays.asList(
            "/doc.html",
            "/swagger-resources",
            "/swagger-resources/configuration",
            "/v3/api-docs",
            "/v2/api-docs",
            "/webjars/**");

    List<String> ALL = new ArrayList<>(KNIFE4J);

    static boolean isInWrapperIgnoreUrl(String uri) {
        AntPathMatcher pathMatcher = new AntPathMatcher();
        for (String s : WrapperIgnoreUrl.ALL) {
            if (pathMatcher.match(s, uri)) {
                return true;
            }
        }
        return false;
    }
}
0

评论2

请先

  1. 为什么这上面的全局响应处理类的beforeBodyWrite()方法,和源码的不一样?
    JavaLee 2024-07-24 0
    • 是一样的呀,你是哪个SpringBoot哪个版本?可能是版本问题,文中这个是2.0.0~2.5.0,这个范围都可以,新版本没检查
      自学精灵 2024-07-24 0
显示验证码
没有账号?注册  忘记密码?

社交账号快速登录