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;
    }
}
相关推荐
yuren_xia几秒前
Spring Boot + MyBatis 集成支付宝支付流程
spring boot·tomcat·mybatis
我爱Jack1 小时前
Spring Boot统一功能处理深度解析
java·spring boot·后端
RainbowJie13 小时前
Spring Boot 使用 SLF4J 实现控制台输出与分类日志文件管理
spring boot·后端·单元测试
面朝大海,春不暖,花不开3 小时前
Spring Boot MVC自动配置与Web应用开发详解
前端·spring boot·mvc
发愤图强的羔羊3 小时前
SpringBoot异步导出文件
spring boot·后端
神仙别闹6 小时前
基于Java(SpringBoot、Mybatis、SpringMvc)+MySQL实现(Web)小二结账系统
java·spring boot·mybatis
全职计算机毕业设计7 小时前
SpringBoot+Mysql实现的停车场收费小程序系统+文档
spring boot·mysql·小程序
努力的小郑7 小时前
BeanFactory与ApplicationContext全面指南与实战
spring boot·spring
crud7 小时前
Spring Boot 整合 MyBatis-Plus:从入门到精通,一文搞定高效持久层开发!
java·spring boot·mybatis
alien爱吃蛋挞9 小时前
【JavaEE】Spring Boot项目创建
spring boot·java-ee