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