Bootstrap

spring boot 统一响应三步曲

spring boot 统一响应三步曲

spring boot 统一响应三步曲:

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

统一响应结构

@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

@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 响应中文乱码问题

@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));
    }
}

统一异常返回

@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时,包装了一层

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

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

@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;
    }

}

结果返回就变成:

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

spring mvc是如何定位到 ErrorController的?

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

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

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

	@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:

// 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;
    }
}
;