大家好,我是飘渺。今天,我们将继续探讨DDD与微服务架构的系列主题。
在前一篇文章 DailyMart13:优雅远程调用,微服务中ACL与OpenFeign的绝佳配合!,一位粉丝提到在DailyMart中,关于Feign的error解码器对404错误处理不够友好,会返回空对象而不报错。
实际上,这个问题并不是Feign解码器的问题。
众所周知,在Spring Boot中,当我们访问一个不存在的接口时,会触发404异常,接口调用会返回如下JSON格式的错误消息:
JSON
{
"timestamp": "2023-09-23T03:36:50.503+00:00",
"status": 404,
"error": "Not Found",
"path": "/api/inventory/xxxx"
}
但是在DailyMart中,这个错误消息会被全局包装类GlobalResponseBodyAdvice
处理,因此出现了以下奇怪的错误信息:code码为OK表示操作正常,但data消息体却表示404异常。由于这种情况,OpenFeign的异常解码器无法正确处理。
json
{
"code": "OK",
"message": null,
"data": {
"timestamp": "2023-09-23T03:36:50.503+00:00",
"status": 404,
"error": "Not Found",
"path": "/api/inventory/xxxx"
},
"timestamp": 1695440210514
}
因此,问题的关键在于解决Spring Boot中404异常处理的问题,以便能够返回正确的响应结果。
1. SpringBoot 的默认错误处理机制
Spring Boot默认为我们提供了BasicErrorController来处理全局/error
请求。BasicErrorController提供两种错误响应方式,一种是针对页面请求的错误响应,另一种是针对JSON请求的错误响应。
2. 自定义错误响应
在SpringBoot中,BasicErrorController是由自动配置类ErrorMvcAutoConfiguration
负责加载。
java
@AutoConfiguration(before = WebMvcAutoConfiguration.class)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class })
@EnableConfigurationProperties({ ServerProperties.class, WebMvcProperties.class })
public class ErrorMvcAutoConfiguration {
...
@Bean
@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
public BasicErrorController basicErrorController(ErrorAttributes errorAttributes,
ObjectProvider<ErrorViewResolver> errorViewResolvers) {
return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
errorViewResolvers.orderedStream().toList());
}
}
根据上述配置,只要我们自己配置一个ErrorController,就可以覆盖掉BasicErrorController的默认行为。下面的代码示例展示了如何通过继承AbstractErrorController
并重写error方法来自定义错误响应,以使其与项目整体风格一致:
java
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class CustomErrorController extends AbstractErrorController {
...
@RequestMapping
public Result<Void> error(HttpServletRequest request) {
return ResultFactory.fail(String.valueOf(status),message);
}
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
...
}
}
通过这种方式,当我们访问不存在的方法时,将会返回符合预期的JSON响应结果:
json
{
"code": "404 NOT_FOUND",
"message": "No message available: /api/inventory/verify",
"data": null,
"timestamp": "1696598118098"
}
3. 微服务中的解决方案
上述方案在单服务中表现良好,但在微服务体系中,每个服务都需要创建一个独立的CustomErrorController
,这显然违反了DRY(Don't Repeat Yourself)原则。
在DailyMart中,我们封装了一个公共模块dailymart-web-spring-boot-starter
,专门处理Web请求的逻辑,包括全局响应包装和异常处理。我们可以将CustomErrorController
放置在此模块中,并在配置类中加载该Controller。
JAVA
/**
* 注册SpringBoot默认异常处理器
*/
@Bean
public CusotmErrorController globalErrorController(ErrorAttributes errorAttributes){
return new CusotmErrorController(errorAttributes);
}
然而,这种做法在启动服务时可能会引发以下异常:
JAVA
Caused by: java.lang.IllegalStateException: Ambiguous mapping. Cannot map 'customerErrorController' method
com.jianzh5.dailymart.springboot.starter.web.CustomerErrorController#errorHtml(HttpServletRequest, HttpServletResponse)
to { [/error], produces [text/html]}: There is already 'basicErrorController' bean method
这是因为Spring Boot本身已经有一个处理/error
请求的控制器BasicErrorController,而我们又定义了一个CustomErrorController
来处理/error
请求。
3.1 解决方案
这个问题有两种解决方案。
第一种解决方案是确保系统在启动时能够优先加载自定义的CustomErrorController
。
根据前文提到的BasicErrorController配置的条件注解,一旦定义了ErrorController就不会再加载BasicErrorController,所以只需要在自动配置类上加上@AutoConfiguration(before = ErrorMvcAutoConfiguration.class)
注解,确保CustomErrorController在BasicErrorController之前加载。
第二种解决方案是在加载CustomErrorController
时修改默认的BasicErrorController的拦截路径。
如前所述,BasicErrorController的拦截路径为@RequestMapping("${server.error.path:${error.path:/error}}")
,如果我们定义了error.path的值,它就会优先使用。因此,只需设置error.path的值即可解决问题。
java
@PostConstruct
public void customizeErrorPath() {
// 修改 server.error.path 的值为自定义值
System.setProperty("error.path", "/deprecated/error");
}
小结
通过本文,我们探讨了Spring Boot中的404异常处理问题,以及如何通过自定义ErrorController来解决这一问题。特别是在微服务架构中,我们介绍了如何利用共享模块的方法来更有效地管理异常处理。希望这些实践经验对您有所帮助,提高了您在微服务开发中处理异常情况的能力。我们非常欢迎各位读者提出宝贵的意见和建议,以进一步完善这个系列文章。
DailyMart是一个基于领域驱动设计(DDD)和Spring Cloud Alibaba的微服务商城系统。我们将在该系统中整合博主其他专栏文章的核心内容。如果你对这两大技术栈感兴趣,可以在公众号Java日知录回复关键词 DDD 以获取相关源码。