From f00787b71f47e54939627495f4146cb19e4f0475 Mon Sep 17 00:00:00 2001 From: wangkang Date: Fri, 25 Aug 2017 10:52:23 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=8D=9A=E5=AE=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 + md/13.md | 192 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 194 insertions(+) create mode 100644 md/13.md diff --git a/README.md b/README.md index c7815cd..9e56ff6 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,8 @@ ## 项目博客 (点击跳转) +- [spring boot / cloud (十三) 404错误处理进阶](https://my.oschina.net/wangkang80/blog/1523308) + - [spring boot / cloud (十二) 异常统一处理进阶](https://my.oschina.net/wangkang80/blog/1519189) - [spring boot / cloud (十一) 回归-博客配套项目开源](https://my.oschina.net/wangkang80/blog/1517938) diff --git a/md/13.md b/md/13.md new file mode 100644 index 0000000..20e74e6 --- /dev/null +++ b/md/13.md @@ -0,0 +1,192 @@ +# spring boot / cloud (十三) 404错误处理进阶 + +## **前言** + +在上一篇文章中介绍了[spring boot 官方文档](http://docs.spring.io/spring-boot/docs/1.5.6.RELEASE/reference/htmlsingle/#boot-features-error-handling)推荐的异常处理方式.承接上一篇文章,我们来了一下如何更好的处理404错误. + +在[spring boot / cloud (二) 规范响应格式以及统一异常处理](https://my.oschina.net/wangkang80/blog/908919)这篇文章的最后跟大家提到了如下的配置 + +``` java +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官方文档](https://docs.spring.io/spring-boot/docs/1.5.6.RELEASE/reference/htmlsingle/#boot-features-spring-mvc-static-content),来了解如何处理静态资源) + +显然,这样做不太友好,因为不可避免的会有在后端服务中包加入一些静态界面情况.所以说,这里提到一个原则 + +>不管是封装也好,增强也好,不能丢失原有框架原本支持的特性. + +那么.如果去掉上面两行的配置,大家运行项目,请求一个不而存在的地址,错误信息会变成如下情况: + +``` java +{ + "timestamp": 1503626878668, + "status": 404, + "error": "Not Found", + "message": "No message available", + "path": "/a/a/a" +} +``` + +而我们希望的错误输出格式是这样的: + +``` java +{ + "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格式的错误信息,我们看到的默认输出结果,就是这个方法生成的 + +``` java +@RequestMapping(produces = "text/html") +public ModelAndView errorHtml(HttpServletRequest request, + HttpServletResponse response) { + HttpStatus status = getStatus(request); + Map 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> error(HttpServletRequest request) { + Map body = getErrorAttributes(request, + isIncludeStackTrace(request, MediaType.ALL)); + HttpStatus status = getStatus(request); + return new ResponseEntity>(body, status); +} +``` + +我们现在知道了我们要关注的地方,但是我们如何改造它呢? + +细心的spring早就想到了可能存在的客制化需求,所以已经为我们留好了口子. + +在源文件里**ErrorMvcAutoConfiguration**是用来配置**BasicErrorController**的,如下是核心方法, + +这个方法上面标记了@ConditionalOnMissingBean,也就是说我们只需要实现一个ErrorController接口,注入到上下文中就可以了 + +>@ConditionalOnMissingBean的意思 : 当上下文中存在某一个bean,则不初始化当前被标记的bean + +``` java +@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) + +``` java +@ApiIgnore +@Controller +@RequestMapping("${server.error.path:${error.path:/error}}") +@Slf4j +public class ExceptionController extends AbstractErrorController { + ....省略 +} +``` + +编写如下两个方法,其实就是跟默认实现的方法一样,只是内容可以定制 + +然后类中其他的方法,就按照默认的实现照抄就行了. + +``` java +@RequestMapping(produces = "text/html") +public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) { + HttpStatus status = getStatus(request); + Map model = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.TEXT_HTML)); + RestResponse 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> error(HttpServletRequest request) { + Map body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL)); + HttpStatus status = getStatus(request); + //这里构建自己的输出格式,详细代码就不贴出,如有需要可以到代码仓库查看 + return new ResponseEntity<>(getRestResponse(request, status, body), status); +} +``` + +然后在创建ExceptionControllerConfig类,注意上面一堆注解照抄默认实现即可 + +``` java +@Configuration +@ConditionalOnWebApplication +@ConditionalOnClass({Servlet.class, DispatcherServlet.class}) +@AutoConfigureBefore(WebMvcAutoConfiguration.class) +@EnableConfigurationProperties(ResourceProperties.class) +public class ExceptionControllerConfig { + ....省略 +} +``` + +创建exceptionController的bean,然后到这里,我们就已经替换了spring boot默认的错误处理controller,后续的错误处理都会走ExceptionController类 + +``` java +@Bean +public ExceptionController exceptionController(ErrorAttributes errorAttributes) { + return new ExceptionController(errorAttributes, this.serverProperties.getError(), this.errorViewResolvers); +} +``` + +## **结束** + +在上面我们优化了404错误的处理流程,替换掉了原来不是很健壮的处理方式,使得代码更加灵活,也更加合理了. + +在下一篇文章中,我会介绍一下服务间接口调用的认证和鉴权的思考和设计. + +## **代码仓库** (博客配套代码) + +- [udf-starter : 基础项目,脚手架,框架](https://gitee.com/wangkang/udf) + +- [udf-sample : 集成样例](https://gitee.com/wangkang/udf-sample) + +--------- + +想获得最快更新,请关注公众号 + +![想获得最快更新,请关注公众号](https://static.oschina.net/uploads/img/201705/24155414_Pukg.jpg "想获得最快更新,请关注公众号") +