spring boot 统一响应三步曲

spring boot 统一响应三步曲

spring boot 统一响应三步曲:

  • 统一响应结构
    • 注意中文乱码问题
  • 统一异常返回
  • 404_状态码处理

统一响应结构

java 复制代码
@Data
public class ResponseResult<T> implements Serializable {
    private static final String SUC = "1";
    private static final String FAIL = "0";
    private String code;
    private String msg;
    private T data;

    public ResponseResult() {
    }

    public ResponseResult(String code, String msg, T data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }

    @JsonIgnore
    public T getCheckedData() {
        if (!this.code.equals(SUC)) {
            throw new RuntimeException("调用异常");
        }
        return data;
    }

    public static <T> ResponseResult<T> success(T data) {
        return new ResponseResult<>(SUC, null, data);
    }

    public static <T> ResponseResult<T> fail(String msg) {
        return new ResponseResult<>(FAIL, msg, null);
    }


}

/**
 * 响应自定义格式
 * 而不是默认数据格式 R
 * @Date: 2024/5/16 14:47
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RawResponse {

}

自定义 ResponseBodyAdvice

java 复制代码
@Component
@ControllerAdvice
public class ResponseBodyWriteAdvice implements ResponseBodyAdvice<Object> {
    private ObjectMapper objectMapper;

    public ResponseBodyWriteAdvice(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }

    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        if (returnType.hasMethodAnnotation(RawResponse.class)) {
            //有些接口需要返回自定义格式
            return body;
        } else if (body instanceof ResponseResult) {
            return body;
        } else if (body instanceof String) {
            // 将 Content-Type 设为 application/json,返回类型是String时,默认 Content-Type = text/plain
            ((ServletServerHttpResponse) response).getServletResponse().setCharacterEncoding(StandardCharsets.UTF_8.name());
            HttpHeaders headers = response.getHeaders();
            headers.setContentType(MediaType.APPLICATION_JSON);
            try {
                return objectMapper.writeValueAsString(ResponseResult.success(body));
            } catch (JsonProcessingException e) {
                throw new RuntimeException(e);
            }
        }
        return ResponseResult.success(body);
    }
}

处理 spring mvc 响应中文乱码问题

java 复制代码
@Configuration
public class FastJsonHttpMessageConverterConfig implements WebMvcConfigurer {

    @Bean
    public HttpMessageConverters messageConverters() {
        //1.需要定义一个convert转换消息的对象;
        MappingJackson2HttpMessageConverter fastJsonHttpMessageConverter = new MappingJackson2HttpMessageConverter();
        //3处理中文乱码问题
        List<MediaType> fastMediaTypes = new ArrayList<>();
        fastMediaTypes.add(MediaType.APPLICATION_JSON_UTF8);
        //4.在convert中添加配置信息.
        fastJsonHttpMessageConverter.setSupportedMediaTypes(fastMediaTypes);
        return new HttpMessageConverters(fastJsonHttpMessageConverter);
    }


    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        //解决@RawResponse 返回 string 类型,且 content-type 为 text/plain 时中文乱码问题
        converters.add(0,new StringHttpMessageConverter(StandardCharsets.UTF_8));
    }
}

统一异常返回

java 复制代码
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {


    @ExceptionHandler(value = Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ResponseResult<String> exceptionHandler(Exception e) {
        log.info("internal error: ", e);
        return ResponseResult.fail(e.getMessage());
    }

    @ExceptionHandler({AccessDeniedException.class})
    @ResponseStatus(HttpStatus.FORBIDDEN)
    public ResponseResult<String> handleAccessDeniedException(AccessDeniedException e) {
        log.info("access error: ", e);
        return ResponseResult.fail(e.getMessage());
    }

}

404_状态码处理

因为我们统一了响应结构, 所以在响应404时,包装了一层

bash 复制代码
{
    "code": "1",
    "msg": null,
    "data": {
        "timestamp": 1723535533933,
        "status": 404,
        "error": "Not Found",
        "path": "/u"
    }
}

那怎么去掉里面的结构呢, 自定义实现ErrorController

java 复制代码
@RestController
@RequestMapping("${server.error.path:/error}")
public class MBasicErrorController extends AbstractErrorController {

    private ServerProperties serverProperties;
    private ErrorProperties errorProperties;

    /**
     * Create a new {@link BasicErrorController} instance.
     * @param errorAttributes the error attributes
     * @param errorProperties configuration properties
     * @param errorViewResolvers error view resolvers
     */
    public MBasicErrorController(ErrorAttributes errorAttributes, ServerProperties serverProperties,
                                List<ErrorViewResolver> errorViewResolvers) {
        super(errorAttributes, errorViewResolvers);
        Assert.notNull(serverProperties, "ErrorProperties must not be null");
        this.errorProperties = serverProperties.getError();
    }


    @RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
    public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
        HttpStatus status = getStatus(request);
        Map<String, Object> model = Collections
                .unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
        response.setStatus(status.value());
        ModelAndView modelAndView = resolveErrorView(request, response, status, model);
        return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
    }

    @RequestMapping
    public ResponseResult<String> error(HttpServletRequest request) {
        HttpStatus status = getStatus(request);
        Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
        return ResponseResult.fail("404_资源不存在");
    }

    protected ErrorAttributeOptions getErrorAttributeOptions(HttpServletRequest request, MediaType mediaType) {
        ErrorAttributeOptions options = ErrorAttributeOptions.defaults();
        if (this.errorProperties.isIncludeException()) {
            options = options.including(ErrorAttributeOptions.Include.EXCEPTION);
        }
        if (isIncludeStackTrace(request, mediaType)) {
            options = options.including(ErrorAttributeOptions.Include.STACK_TRACE);
        }
        if (isIncludeMessage(request, mediaType)) {
            options = options.including(ErrorAttributeOptions.Include.MESSAGE);
        }
        if (isIncludeBindingErrors(request, mediaType)) {
            options = options.including(ErrorAttributeOptions.Include.BINDING_ERRORS);
        }
        return options;
    }

    /**
     * Determine if the stacktrace attribute should be included.
     * @param request the source request
     * @param produces the media type produced (or {@code MediaType.ALL})
     * @return if the stacktrace attribute should be included
     */
    protected boolean isIncludeStackTrace(HttpServletRequest request, MediaType produces) {
        switch (getErrorProperties().getIncludeStacktrace()) {
            case ALWAYS:
                return true;
            case ON_PARAM:
                return getTraceParameter(request);
            default:
                return false;
        }
    }

    /**
     * Determine if the message attribute should be included.
     * @param request the source request
     * @param produces the media type produced (or {@code MediaType.ALL})
     * @return if the message attribute should be included
     */
    protected boolean isIncludeMessage(HttpServletRequest request, MediaType produces) {
        switch (getErrorProperties().getIncludeMessage()) {
            case ALWAYS:
                return true;
            case ON_PARAM:
                return getMessageParameter(request);
            default:
                return false;
        }
    }

    /**
     * Determine if the errors attribute should be included.
     * @param request the source request
     * @param produces the media type produced (or {@code MediaType.ALL})
     * @return if the errors attribute should be included
     */
    protected boolean isIncludeBindingErrors(HttpServletRequest request, MediaType produces) {
        switch (getErrorProperties().getIncludeBindingErrors()) {
            case ALWAYS:
                return true;
            case ON_PARAM:
                return getErrorsParameter(request);
            default:
                return false;
        }
    }

    /**
     * Provide access to the error properties.
     * @return the error properties
     */
    protected ErrorProperties getErrorProperties() {
        return this.errorProperties;
    }

}

结果返回就变成:

java 复制代码
{
    "code": "0",
    "msg": "404_资源不存在",
    "data": null
}
题外话

spring mvc是如何定位到 ErrorController的?

比如请求一个不存在的资源 /u,其实它是经过两次请求

  • 正常请求 /u, 发现不存在, 设置reponse 响应码为404
  • 取到配置的 /error 地址,然后request.forward 到 /error 指定的 Controller

第一次请求到 ResourceHttpRequestHandler.handleRequest 方法:

java 复制代码
	@Override
	public void handleRequest(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {

		// For very general mappings (e.g. "/") we need to check 404 first
		Resource resource = getResource(request);
		if (resource == null) {
			logger.debug("Resource not found");
			response.sendError(HttpServletResponse.SC_NOT_FOUND);
			return;
		}
        // omit...
    }

然后又返回到 tomcat 容器中处理, 即 StandardHostValve.invoke:

java 复制代码
// Look for (and render if found) an application level error page
if (response.isErrorReportRequired()) {
    // If an error has occurred that prevents further I/O, don't waste time
    // producing an error report that will never be read
    AtomicBoolean result = new AtomicBoolean(false);
    response.getCoyoteResponse().action(ActionCode.IS_IO_ALLOWED, result);
    if (result.get()) {
        if (t != null) {
            throwable(request, response, t);
        } else {
            status(request, response);
        }
    }
}

private void status(Request request, Response response) {

    int statusCode = response.getStatus();

    // Handle a custom error page for this status code
    Context context = request.getContext();
    if (context == null) {
        return;
    }

    /*
         * Only look for error pages when isError() is set. isError() is set when response.sendError() is invoked. This
         * allows custom error pages without relying on default from web.xml.
         */
    if (!response.isError()) {
        return;
    }

    //根据响应码查询 配置的 error 页面
    ErrorPage errorPage = context.findErrorPage(statusCode);
    if (errorPage == null) {
        // Look for a default error page
        //如果没有找到,就取第一个
        // 默认配置的一个为 /error
        errorPage = context.findErrorPage(0);
    }
    if (errorPage != null && response.isErrorReportRequired()) {
        response.setAppCommitted(false);
        request.setAttribute(RequestDispatcher.ERROR_STATUS_CODE, Integer.valueOf(statusCode));

        String message = response.getMessage();
        if (message == null) {
            message = "";
        }
        request.setAttribute(RequestDispatcher.ERROR_MESSAGE, message);
        request.setAttribute(Globals.DISPATCHER_REQUEST_PATH_ATTR, errorPage.getLocation());
        request.setAttribute(Globals.DISPATCHER_TYPE_ATTR, DispatcherType.ERROR);


        Wrapper wrapper = request.getWrapper();
        if (wrapper != null) {
            request.setAttribute(RequestDispatcher.ERROR_SERVLET_NAME, wrapper.getName());
        }
        request.setAttribute(RequestDispatcher.ERROR_REQUEST_URI, request.getRequestURI());
        if (custom(request, response, errorPage)) {
            response.setErrorReported();
            try {
                response.finishResponse();
            } catch (ClientAbortException e) {
                // Ignore
            } catch (IOException e) {
                container.getLogger().warn("Exception Processing " + errorPage, e);
            }
        }
    }
}


private boolean custom(Request request, Response response, ErrorPage errorPage) {

    if (container.getLogger().isDebugEnabled()) {
        container.getLogger().debug("Processing " + errorPage);
    }

    try {
        // Forward control to the specified location
        ServletContext servletContext = request.getContext().getServletContext();
        RequestDispatcher rd = servletContext.getRequestDispatcher(errorPage.getLocation());

        if (rd == null) {
            container.getLogger()
                .error(sm.getString("standardHostValue.customStatusFailed", errorPage.getLocation()));
            return false;
        }

        if (response.isCommitted()) {
            // Response is committed - including the error page is the
            // best we can do
            rd.include(request.getRequest(), response.getResponse());

            // Ensure the combined incomplete response and error page is
            // written to the client
            try {
                response.flushBuffer();
            } catch (Throwable t) {
                ExceptionUtils.handleThrowable(t);
            }

            // Now close immediately as an additional signal to the client
            // that something went wrong
            response.getCoyoteResponse().action(ActionCode.CLOSE_NOW,
                                                request.getAttribute(RequestDispatcher.ERROR_EXCEPTION));
        } else {
            // Reset the response (keeping the real error code and message)
            response.resetBuffer(true);
            response.setContentLength(-1);

            rd.forward(request.getRequest(), response.getResponse());

            // If we forward, the response is suspended again
            response.setSuspended(false);
        }

        // Indicate that we have successfully processed this custom page
        return true;

    } catch (Throwable t) {
        ExceptionUtils.handleThrowable(t);
        // Report our failure to process this custom page
        container.getLogger().error("Exception Processing " + errorPage, t);
        return false;
    }
}
相关推荐
qq_12498707535 分钟前
基于SSM的动物保护系统的设计与实现(源码+论文+部署+安装)
java·数据库·spring boot·毕业设计·ssm·计算机毕业设计
Coder_Boy_11 分钟前
基于SpringAI的在线考试系统-考试系统开发流程案例
java·数据库·人工智能·spring boot·后端
2301_8187320623 分钟前
前端调用控制层接口,进不去,报错415,类型不匹配
java·spring boot·spring·tomcat·intellij-idea
汤姆yu4 小时前
基于springboot的尿毒症健康管理系统
java·spring boot·后端
暮色妖娆丶4 小时前
Spring 源码分析 单例 Bean 的创建过程
spring boot·后端·spring
biyezuopinvip5 小时前
基于Spring Boot的企业网盘的设计与实现(任务书)
java·spring boot·后端·vue·ssm·任务书·企业网盘的设计与实现
JavaGuide5 小时前
一款悄然崛起的国产规则引擎,让业务编排效率提升 10 倍!
java·spring boot
figo10tf6 小时前
Spring Boot项目集成Redisson 原始依赖与 Spring Boot Starter 的流程
java·spring boot·后端
zhangyi_viva6 小时前
Spring Boot(七):Swagger 接口文档
java·spring boot·后端
橙露6 小时前
Spring Boot 核心原理:自动配置机制与自定义 Starter 开发
java·数据库·spring boot