当前位置: 首页 > news >正文

Spring应用抛出NoHandlerFoundException、全局异常处理、日志级别

本文记录在基于Spring(Boot)框架(使用Java语言)和Grails框架(使用Groovy语言)下,开发Controller接口,对不存在的URL请求,接口返回404 not found,而不是抛出NoHandlerFoundException异常的问题,以及排查过程。

对于Grails框架,请参考Grails。

Spring

代码省略,常规的Spring应用(Spring Boot基于Spring),/sso/logout接口。在Postman里请求明显不存在的路径/sso/logout1,服务响应如下图:
在这里插入图片描述
乍看之下,好像没什么问题,/sso/logout1本来就不存在,请求一个不存在的路径,服务返回404报错等各种详情,一目了然。

深入思考下,真的没有问题吗?

在业务开发过程中,经常会有新接口的产生,同时还有旧接口的废弃。因此,接口的调用者请求某个接口时,势必会出现遇到404 Not Found异常报错。接口的请求者主要有两类:

  • 微服务(或分布式)体系下的请求发起方:此时发起方会做好异常状态码处理,大概率不会出现问题;
  • 大前端(包括Web,Android,iOS,小程序,H5等):前端页面会对404错误进行统一处理,然后给用户展示一个友好的页面。

好像也没啥问题。真的吗?

实际上,这里存在的问题是,应用里并没有记录任何有效信息,更谈何WARN或ERROR日志:

2025-01-11 17:50:12.465  INFO 24532 --- [nio-8880-exec-2] com.tesla.security.filter.LogFilter       : ==== request url: GET /sso/logout1  ====
2025-01-11 17:50:12.485  INFO 24532 --- [nio-8880-exec-2] i.j.internal.reporters.LoggingReporter   : Span reported - GET
2025-01-11 17:50:12.512  INFO 24532 --- [nio-8880-exec-2] i.j.internal.reporters.LoggingReporter   : Span reported - error

日志分析:第一行日志是一个统一的:

@Slf4j
@Order(2)
@WebFilter(filterName = "logFilter", urlPatterns = "/*", asyncSupported = true)
public class LogFilter implements Filter {@Overridepublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {HttpServletRequest request = (HttpServletRequest) request;// 获取请求路径String path = CommonUtil.getRequestPath(request);if ("/health".equals(path)) {chain.doFilter(request, response);return;}String queryString = request.getQueryString();if (StringUtils.isNotBlank(queryString)) {queryString = "?" + queryString;} else {queryString = "";}log.info("==== request url: {} {} {} ====", request.getMethod(), path, queryString);chain.doFilter(request, response);}
}

第2~3行日志,是在应用接入到Jaeger后才会打印的日志。也就是说,应用接收到一个不存在的请求,没有记录任何有效日志。

系统应用或组件越来越多后,即变成所谓的复杂系统。复杂系统的可观测性,是一门学问,诞生(或催生)出可观测性工程。

404 Not Found,主要有两个场景:

  • 前端(或其他后端通过RPC或HTTP方式)请求一个不存在的接口:接口确实不存在,或接口存在但调用方写错
  • Spring Cloud Gateway等网关配置路由转发规则有误:接口存在,配置有误

NoHandlerFoundException

那如何配置才能让服务抛出异常呢?在Spring体系里,404对应的异常是NoHandlerFoundException。经过搜索,应用的application.yaml文件做如下配置后:

spring:mvc:throw-exception-if-no-handler-found: trueweb:resources:add-mappings: false

应用会抛NoHandlerFoundException异常。

全局异常处理

非常经典的需求。此处直接给出实现代码片段。

NoHandlerFoundException也需要纳入全局统一异常处理:

@Slf4j
@RestControllerAdvice
@RequiredArgsConstructor
public class AdviceConfiguration implements ResponseBodyAdvice<Object> {protected static final MediaType MEDIA_TYPE = new MediaType("application", "json", StandardCharsets.UTF_8);@ExceptionHandler(NoHandlerFoundException.class)@ResponseStatus(HttpStatus.NOT_FOUND)public ResponseEntity<R<?>> handleNoHandlerFound(HttpServletRequest request, NoHandlerFoundException e) {// 前端请求不存在的URI,或网关配置错误,日志级别为ERRORthis.logError(e, request, "方法不存在");return createResponseEntity(HttpStatus.NOT_FOUND, R.error(NOT_FOUND.value(), e.getMessage()));}private void logError(Exception e, HttpServletRequest request, String... msg) {String template = "[Web][有异常被抛出] >> 异常类=[%s], URI=[%s], 消息=[%s], 异常=[%s]";log.error(String.format(template, e.getClass().getName(), request.getRequestURI(),ArrayUtil.isEmpty(msg) ? e.getMessage() : ArrayUtil.join(msg, ","), ExceptionUtil.stacktraceToString(e)));}protected static ResponseEntity<R<?>> createResponseEntity(HttpStatus httpStatus, R<?> body) {return ResponseEntity.status(httpStatus.value()).contentType(MEDIA_TYPE).body(body);}
}

日志级别

对于404 Not Found异常,不建议使用WARN级别来记录日志,应使用ERROR级别。

几种适合使用WARN日志级别的异常:

  • 自定义业务异常:业务代码里的校验,如用户ID非法,抛出BizException,然后交由全局统一异常处理类处理时,打印warn日志;
  • HttpRequestMethodNotSupportedException:请求方法不支持,接口标记为POST请求,被PUT调用;
  • com.fasterxml.jackson.core.JsonParseException
  • HttpMessageNotReadableException:HTTP消息不可读异常;
  • IllegalArgumentException:
  • BindException:
  • MethodArgumentNotValidException:
  • MissingServletRequestParameterException:
  • 还有更多
@ResponseStatus(OK)
@ExceptionHandler(BizException.class)
public ResponseEntity<R<?>> handleBaseException(HttpServletRequest request, BizException e) {this.logWarn(e, request, "自定义业务异常");return createResponseEntity(OK, R.error(e.getCode(), e.getMessage()));
}@ResponseStatus(BAD_REQUEST)
@ExceptionHandler({JsonParseException.class, HttpMessageNotReadableException.class, BindException.class,IllegalArgumentException.class, MethodArgumentNotValidException.class, MissingServletRequestParameterException.class})
public ResponseEntity<R<?>> handleJsonParseException(Exception e, HttpServletRequest request) {this.logWarn(e, request, "不合法的参数异常");this.countInc(request, e, CODE_400);return createResponseEntity(BAD_REQUEST, R.error(BAD_REQUEST.value(), e.getMessage()));
}@ResponseStatus(METHOD_NOT_ALLOWED)
@ExceptionHandler({HttpRequestMethodNotSupportedException.class})
public ResponseEntity<R<?>> handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e, HttpServletRequest request) {this.logWarn(e, request, "不支持的请求方式");this.countInc(request, e, "405");return createResponseEntity(METHOD_NOT_ALLOWED, R.error(METHOD_NOT_ALLOWED.value(), "不支持的请求方式"));
}private void logWarn(Exception e, HttpServletRequest request, String... msg) {String template = "[Web][有Warn被抛出] >> Warn类=[%s], URI=[%s], 消息=[%s], Warn=[%s]";log.warn(String.format(template, e.getClass().getName(), request.getRequestURI(),ArrayUtil.isEmpty(msg) ? e.getMessage() : ArrayUtil.join(msg, ","), ExceptionUtil.stacktraceToString(e)));
}

HttpMessageNotReadableException

产生此异常的场景有很多:

  • POST请求传入非法JSON;

IllegalArgumentException

产生此异常的场景:

  • 1

BindException

MethodArgumentNotValidException

产生此异常的场景:

  • POST请求里对实体类加@RequestBody、@Valid等注解,在实体类里对(部分)字段加Validation注解。请求接口时,实体类不满足校验条件,比如字段不存在,为空,长度超过20个字符等,就会抛此异常;

MissingServletRequestParameterException

产生此异常的场景:

  • GET请求里有个@RequestParam(value = "app_id") String appId,但是请求时并没有带上此Query Params参数,就会抛此异常;
http://www.lryc.cn/news/589248.html

相关文章:

  • 游戏加速器核心技术:动态超发
  • Postman + Newman + Jenkins 接口自动化测试
  • 【PTA数据结构 | C语言版】二叉树层序序列化
  • MYSQL练习2
  • UVM(1)—配置环境
  • 3分钟搞定!用ChatGPT+工具生成流程图超简单(附提示词)
  • 基于 AI 的大前端安全态势感知与应急响应体系建设
  • 证明在赋范线性空间中,如果一个闭子空间内的点列弱收敛于空间中的一个点,那么这个点也必然属于该闭子空间
  • 稳定细胞系构建|蛋白表达细胞株|高表达细胞株
  • 备忘录设计模式
  • Python+Selenium自动化爬取携程动态加载游记
  • MIPI DSI(四) video 和 command 模式
  • MySQL数学函数
  • 【STM32项目】环境监测设计
  • QML视图与代理控件
  • Spring Boot全局异常处理:打造坚如磐石的应用防线
  • 【Java代码审计(2)】MyBatis XML 注入审计
  • Datawhale AI夏令营 机器学习2.1
  • AWS中国区资源成本优化全面指南:从理论到实践
  • 从零开始的python学习(八)P115+P116+P117+P118+P119+P120+P121+P122
  • 第十三讲 | map和set的使用
  • Windows内核对象
  • 【AutoCAD保姆级安装教程】AutoCAD 2025 版详细图文下载安装教程
  • wkhtmltopdf导出pdf调试参数
  • 【08】MFC入门到精通——MFC模态对话框 和 非模态对话框 解析 及 实例演示
  • 农村养老模式:乡土智慧与时代创新的共生之路
  • Gitlab跑CICD的时候,maven镜像和pom.xml使用的maven版本冲突导致没办法build成功的解决方法
  • 【C#地图显示教程:实现鼠标绘制图形操作】
  • jmeter之随机读取csv文件
  • jmeter提取接口返回值,对比去重