当前位置: 首页 > 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.xdnf.cn/news/1128277.html

相关文章:

  • 前端开发数据缓存方案详解
  • 1.easypan-登录注册
  • git起步
  • Jfinal+SQLite java工具类复制mysql表数据到 *.sqlite
  • 同济医院R语言训练营第三期开讲!上交大张维拓老师主讲
  • 2025最新国产用例管理工具评测:Gitee Test、禅道、蓝凌测试、TestOps 哪家更懂研发协同?
  • 希尔排序:突破传统排序的边界
  • 22.计算指定范围内数字的幂次和
  • StampedLock分析
  • 基于cornerstone3D的dicom影像浏览器 第二章,初始化页面结构
  • 亚矩阵云手机:破解 Yandex 广告平台多账号风控难题的利器
  • 跨平台游戏引擎 Axmol-2.7.1 发布
  • APP端定位实现(uniapp Vue3)(腾讯地图)
  • Ext系列文件系统知识点
  • Linux进程信号--1、信号产生
  • 时间复杂度和空间复杂度是衡量一个算法好坏的标准
  • A*算法详解
  • 9、线程理论1
  • eVTOL分布式电推进(DEP)适航审定探究
  • redisson tryLock
  • Spring MVC2
  • 尚庭公寓-----day1----@MapperScan爆红问题
  • 三十二、【核心功能改造】数据驱动:重构仪表盘与关键指标可视化
  • 【转】Rust: PhantomData,#may_dangle和Drop Check 真真假假
  • 【字节跳动】数据挖掘面试题0019:带货直播间推荐:现在有一个带货的直播间,怎么把它精准地推送给有需要的用户
  • 【C++】神奇的AVL树
  • WebView JSBridge 无响应问题排查实录 全流程定位桥接调用失效
  • 无人机故障响应模块运行与技术难点
  • Ubuntu24 辅助系统-屏幕键盘的back按键在网页文本框删除不正常的问题解决方法
  • RTL编程中常用的几种语言对比