尽管这是一个较旧的问题,但我想分享我的想法。我希望对你们中的一些人有所帮助。
我目前正在构建一个REST API,它使用Spring Boot 1.5.2.RELEASE和Spring Framework 4.3.7.RELEASE。我使用Java Config方法(而不是XML配置)。此外,我的项目使用
@RestControllerAdvice
注释实现全局异常处理机制(稍后会详细介绍)。
我的项目具有与你的要求相同的要求:当API客户端尝试向不存在的URL发送请求时,我希望我的REST API返回HTTP 404 Not Found,并在HTTP响应中附带JSON有效负载。在我的情况下,JSON有效负载如下所示(顺便说一句,这与Spring Boot默认值明显不同):
{
"code": 1000,
"message": "No handler found for your request.",
"timestamp": "2017-11-20T02:40:57.628Z"
}
我终于让它工作了。以下是你需要简要完成的主要任务:
- 确保如果API客户端调用没有处理程序方法的URL,抛出
NoHandlerFoundException
(请参见下面的步骤1)。
- 创建一个自定义错误类(在我的情况下为
ApiError
),其中包含应返回给API客户端的所有数据(请参见步骤2)。
- 创建一个异常处理程序,该处理程序对
NoHandlerFoundException
进行反应,并向API客户端返回适当的错误消息(请参见步骤3)。
- 编写测试并确保其正常工作(请参见步骤4)。
好的,现在进入详细信息:
步骤1:配置 application.properties
我不得不将以下两个配置设置添加到项目的application.properties
文件中:
spring.mvc.throw-exception-if-no-handler-found=true
spring.resources.add-mappings=false
这样做可以确保当客户端试图访问没有控制器方法能够处理请求的URL时,会抛出
NoHandlerFoundException
异常。
第二步:为API错误创建一个类
我创建了一个类,类似于Eugen Paraschiv博客上
这篇文章中建议的类。这个类代表API错误。在发生错误时,将此信息发送到客户端的HTTP响应主体中。
public class ApiError {
private int code;
private String message;
private Instant timestamp;
public ApiError(int code, String message) {
this.code = code;
this.message = message;
this.timestamp = Instant.now();
}
public ApiError(int code, String message, Instant timestamp) {
this.code = code;
this.message = message;
this.timestamp = timestamp;
}
}
步骤 3:创建 / 配置全局异常处理程序
我使用以下类来处理异常(为简单起见,我已删除了导入语句、日志记录代码和一些其他非相关的代码):
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(NoHandlerFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ApiError noHandlerFoundException(
NoHandlerFoundException ex) {
int code = 1000;
String message = "No handler found for your request.";
return new ApiError(code, message);
}
}
步骤四:编写测试
我希望确保API在出现故障时,始终向调用客户端返回正确的错误信息。因此,我编写了以下测试:
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SprintBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
@ActiveProfiles("dev")
public class GlobalExceptionHandlerIntegrationTest {
public static final String ISO8601_DATE_REGEX =
"^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$";
@Autowired
private MockMvc mockMvc;
@Test
@WithMockUser(roles = "DEVICE_SCAN_HOSTS")
public void invalidUrl_returnsHttp404() throws Exception {
RequestBuilder requestBuilder = getGetRequestBuilder("/does-not-exist");
mockMvc.perform(requestBuilder)
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.code", is(1000)))
.andExpect(jsonPath("$.message", is("No handler found for your request.")))
.andExpect(jsonPath("$.timestamp", RegexMatcher.matchesRegex(ISO8601_DATE_REGEX)));
}
private RequestBuilder getGetRequestBuilder(String url) {
return MockMvcRequestBuilders
.get(url)
.accept(MediaType.APPLICATION_JSON);
}
@ActiveProfiles("dev")
注解可以省略。我只是在使用不同的配置文件时才会用到它。 RegexMatcher
是一个自定义的Hamcrest matcher,我用它来更好地处理时间戳字段。以下是代码(我在这里找到的):
public class RegexMatcher extends TypeSafeMatcher<String> {
private final String regex;
public RegexMatcher(final String regex) {
this.regex = regex;
}
@Override
public void describeTo(final Description description) {
description.appendText("matches regular expression=`" + regex + "`");
}
@Override
public boolean matchesSafely(final String string) {
return string.matches(regex);
}
public static RegexMatcher matchesRegex(final String string) {
return new RegexMatcher(regex);
}
}
我的一些进一步的注释:
- 在StackOverflow上的许多其他帖子中,人们建议设置
@EnableWebMvc
注释。但在我的情况下并不需要。
- 这种方法与MockMvc很好地配合(请参见上面的测试)。