Spring Cloud中国社区博客

spring boot / cloud 404错误处理进阶

前言

在上一篇文章中介绍了spring boot 官方文档推荐的异常处理方式.承接上一篇文章,我们来了一下如何更好的处理404错误.

spring boot / cloud (二) 规范响应格式以及统一异常处理这篇文章的最后跟大家提到了如下的配置

1
2
spring.mvc.throw-exception-if-no-handler-found=true
spring.resources.add-mappings=false

这两行配置的主要意思是告诉spring,如果你请求的地址,没有找到对应的handler,则抛出异常,默认配置是fase,同时也要关掉静态资源的映射,不然并不会起作用.

所以我们知道了,在spring中,404并不是是一个异常,而是一个错误,所走的处理方式也是不一样的.

场景分析

按照以上的配置,我们可以轻易的将404错误也变成一个异常,然后走统一的异常处理方式,但是这样做是有代价的,就是也会关闭掉spring boot的经验资源mapping功能.

(大家可查阅spring boot官方文档,来了解如何处理静态资源)

显然,这样做不太友好,因为不可避免的会有在后端服务中包加入一些静态界面情况.所以说,这里提到一个原则

不管是封装也好,增强也好,不能丢失原有框架原本支持的特性.

那么.如果去掉上面两行的配置,大家运行项目,请求一个不而存在的地址,错误信息会变成如下情况:

1
2
3
4
5
6
7
{
"timestamp": 1503626878668,
"status": 404,
"error": "Not Found",
"message": "No message available",
"path": "/a/a/a"
}

而我们希望的错误输出格式是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"id": "e7593f09-2898-478e-a3bd-0280b93ac77f",
"code": "404",
"message": "Not Found",
"result": null,
"error": {
"date": 1503626920897,
"type": "......",
"message": ".........",
"stackTrace": null,
"child": null
}
}

如何调整呢?

源码解读

在spring中,专门处理error的类是BasicErrorController,如下是这个类的两个核心方法,

一个方法处理html请求的返回,会默认返回一个错误页面

另外一个则是处理json请求的,会返回json格式的错误信息,我们看到的默认输出结果,就是这个方法生成的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@RequestMapping(produces = "text/html")
public ModelAndView errorHtml(HttpServletRequest request,
HttpServletResponse response) {
HttpStatus status = getStatus(request);
Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes(
request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
return (modelAndView == null ? new ModelAndView("error", model) : modelAndView);
}
@RequestMapping
@ResponseBody
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
Map<String, Object> body = getErrorAttributes(request,
isIncludeStackTrace(request, MediaType.ALL));
HttpStatus status = getStatus(request);
return new ResponseEntity<Map<String, Object>>(body, status);
}

我们现在知道了我们要关注的地方,但是我们如何改造它呢?

细心的spring早就想到了可能存在的客制化需求,所以已经为我们留好了口子.

在源文件里ErrorMvcAutoConfiguration是用来配置BasicErrorController的,如下是核心方法,

这个方法上面标记了@ConditionalOnMissingBean,也就是说我们只需要实现一个ErrorController接口,注入到上下文中就可以了

@ConditionalOnMissingBean的意思 : 当上下文中存在某一个bean,则不初始化当前被标记的bean

1
2
3
4
5
6
@Bean
@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
public BasicErrorController basicErrorController(ErrorAttributes errorAttributes) {
return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
this.errorViewResolvers);
}

知道了方法,下面就开干吧!

实现

创建一个类ExceptionController继承至AbstractErrorController(此基类实现了ErrorController)

1
2
3
4
5
6
7
@ApiIgnore
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
@Slf4j
public class ExceptionController extends AbstractErrorController {
....省略
}

编写如下两个方法,其实就是跟默认实现的方法一样,只是内容可以定制

然后类中其他的方法,就按照默认的实现照抄就行了.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@RequestMapping(produces = "text/html")
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = getStatus(request);
Map<String, Object> model = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.TEXT_HTML));
RestResponse<String> restResponse = this.getRestResponse(request, status, model);
model.put("restResponse", restResponse);
model.put(KEY_EXCEPTION, restResponse.getError().getType());
model.put(KEY_MESSAGE, restResponse.getError().getMessage());
response.setStatus(status.value());
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
return (modelAndView == null ? new ModelAndView("error", model) : modelAndView);
}
@RequestMapping
@ResponseBody
public ResponseEntity<RestResponse<String>> error(HttpServletRequest request) {
Map<String, Object> body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL));
HttpStatus status = getStatus(request);
//这里构建自己的输出格式,详细代码就不贴出,如有需要可以到代码仓库查看
return new ResponseEntity<>(getRestResponse(request, status, body), status);
}

然后在创建ExceptionControllerConfig类,注意上面一堆注解照抄默认实现即可

1
2
3
4
5
6
7
8
@Configuration
@ConditionalOnWebApplication
@ConditionalOnClass({Servlet.class, DispatcherServlet.class})
@AutoConfigureBefore(WebMvcAutoConfiguration.class)
@EnableConfigurationProperties(ResourceProperties.class)
public class ExceptionControllerConfig {
....省略
}

创建exceptionController的bean,然后到这里,我们就已经替换了spring boot默认的错误处理controller,后续的错误处理都会走ExceptionController类

1
2
3
4
@Bean
public ExceptionController exceptionController(ErrorAttributes errorAttributes) {
return new ExceptionController(errorAttributes, this.serverProperties.getError(), this.errorViewResolvers);
}

结束

在上面我们优化了404错误的处理流程,替换掉了原来不是很健壮的处理方式,使得代码更加灵活,也更加合理了.

在下一篇文章中,我会介绍一下服务间接口调用的认证和鉴权的思考和设计.

代码仓库 (博客配套代码)


想获得最快更新,请关注公众号

想获得最快更新,请关注公众号