背景:
一天早上睡觉醒来,翻了翻手机,发现7点时一个C端服务出现了短暂的报警,通过日志发现,大量的未知请求,有的返回了404,而/error请求http响应码返回了500,很疑惑,按道理服务里没有这个接口应该返回404啊,这个接口有什么特殊之处?于是就调研了一下。
通过排查:
原来springboot中有个BasicErrorController类,这个类有个默认的/error接口,
当我们调用没有定义的接口时,springboot会自动调用error接口将一个自定义的页面返回给用户,状态码是404
而我们直接调用这个error接口时,由于我们的服务是有默认的error接口的,所以不是404,而由于我们调用时没有传合适的参数,springboot执行逻辑时报错,返回了500状态码。注意:此时我们自定义的ExceptionHandlerAdvice全局异常捕获处理器是捕获不到异常的,因为springboot没有抛出异常,只是返回了500状态码,于是就出现了上边的问题。如何解决呢?如何在这种情况下不返回5xx呢?
解决方案一:
按照BasicErrorController类写一个类似的ServerErrorController,接口名改为/server/error,覆盖BasicErrorController的/error接口,如下:
import org.springframework.boot.autoconfigure.web.ErrorProperties;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.autoconfigure.web.servlet.error.AbstractErrorController;
import org.springframework.boot.web.error.ErrorAttributeOptions;
import org.springframework.boot.web.servlet.error.ErrorAttributes;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Collections;
import java.util.Map;
/**
* 自定义的异常处理控制器,用于覆盖默认的BasicErrorController;
**/
@Controller
@RequestMapping("/server/error")
public class ServerErrorController extends AbstractErrorController {
private final ErrorProperties errorProperties;
public ServerErrorController(ErrorAttributes errorAttributes, ServerProperties serverProperties) {
super(errorAttributes);
this.errorProperties = serverProperties.getError();
}
@RequestMapping(
produces = {"text/html"}
)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = this.getStatus(request);
Map<String, Object> model = Collections.unmodifiableMap(this.getErrorAttributes(request, this.getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
//如果>=500返回400
if (status.value() >= HttpStatus.INTERNAL_SERVER_ERROR.value()) {
status = HttpStatus.BAD_REQUEST;
}
response.setStatus(status.value());
ModelAndView modelAndView = this.resolveErrorView(request, response, status, model);
return modelAndView != null ? modelAndView : new ModelAndView("error", model);
}
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
HttpStatus status = this.getStatus(request);
//如果>=500返回400
if (status.value() >= HttpStatus.INTERNAL_SERVER_ERROR.value()) {
status = HttpStatus.BAD_REQUEST;
}
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity(status);
} else {
Map<String, Object> body = this.getErrorAttributes(request, this.getErrorAttributeOptions(request, MediaType.ALL));
return new ResponseEntity(body, status);
}
}
@ExceptionHandler({HttpMediaTypeNotAcceptableException.class})
public ResponseEntity<String> mediaTypeNotAcceptable(HttpServletRequest request) {
HttpStatus status = this.getStatus(request);
return ResponseEntity.status(status).build();
}
protected ErrorAttributeOptions getErrorAttributeOptions(HttpServletRequest request, MediaType mediaType) {
ErrorAttributeOptions options = ErrorAttributeOptions.defaults();
if (this.errorProperties.isIncludeException()) {
options = options.including(new ErrorAttributeOptions.Include[]{ErrorAttributeOptions.Include.EXCEPTION});
}
if (this.isIncludeStackTrace(request, mediaType)) {
options = options.including(new ErrorAttributeOptions.Include[]{ErrorAttributeOptions.Include.STACK_TRACE});
}
if (this.isIncludeMessage(request, mediaType)) {
options = options.including(new ErrorAttributeOptions.Include[]{ErrorAttributeOptions.Include.MESSAGE});
}
if (this.isIncludeBindingErrors(request, mediaType)) {
options = options.including(new ErrorAttributeOptions.Include[]{ErrorAttributeOptions.Include.BINDING_ERRORS});
}
return options;
}
protected boolean isIncludeStackTrace(HttpServletRequest request, MediaType produces) {
switch(this.getErrorProperties().getIncludeStacktrace()) {
case ALWAYS:
return true;
case ON_PARAM:
return this.getTraceParameter(request);
default:
return false;
}
}
protected boolean isIncludeMessage(HttpServletRequest request, MediaType produces) {
switch(this.getErrorProperties().getIncludeMessage()) {
case ALWAYS:
return true;
case ON_PARAM:
return this.getMessageParameter(request);
default:
return false;
}
}
protected boolean isIncludeBindingErrors(HttpServletRequest request, MediaType produces) {
switch(this.getErrorProperties().getIncludeBindingErrors()) {
case ALWAYS:
return true;
case ON_PARAM:
return this.getErrorsParameter(request);
default:
return false;
}
}
protected ErrorProperties getErrorProperties() {
return this.errorProperties;
}
然后修改application.yml配置:
server:
error:
path: /server/error
这样就覆盖了/error接口,当调用未定义的接口时,springboot会转到我们定义的/server/error接口。并且我们可以自定义接口的逻辑,例如,当状态码是5xx时,返回400.
/@RequestMapping(
produces = {"text/html"}
)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = this.getStatus(request);
Map<String, Object> model = Collections.unmodifiableMap(this.getErrorAttributes(request, this.getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
//如果>=500返回400
if (status.value() >= HttpStatus.INTERNAL_SERVER_ERROR.value()) {
status = HttpStatus.BAD_REQUEST;
}
response.setStatus(status.value());
ModelAndView modelAndView = this.resolveErrorView(request, response, status, model);
return modelAndView != null ? modelAndView : new ModelAndView("error", model);
}
注意:BasicErrorController中其实是有两个接口,
一个是:(这个接口会返回一个html页面。当我们用浏览器调用时会调用这个,因为Content-Type是text/html)
@RequestMapping(produces = {"text/html"})
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response)
{}
另一个是:(这个接口会返回json数据。当我们用postman调用时会调用这个,因为Content-Type是application/json)
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request)
{}
因此两个接口都要改。
这样就解决了调用/error时返回500的问题
解决方案二:
直接在nginx侧改,当调用error时,返回4xx,简单粗暴。
哪个方案更好:
如果可以在不重启服务、不重启nginx的情况在nginx修改,肯定选择方案二。因为方案一虽然返回了4xx,但是请求仍然进了服务,消耗了服务资源。
举一反三:
因此我们的服务接口是不是应该搞个统一的前缀呢?这样就可以直接在nginx侧统一过滤掉错误请求了,使服务更健康。