
Java异常该不该捕获NullPointerException
绝大多数情况下,NullPointerException不该被显式捕获——它代表逻辑缺陷,不是可恢复的业务异常。捕获它往往掩盖了空值来源没校验、对象未初始化、接口契约被破坏等问题。
真正该做的是:在入参处用Objects.requireNonNull()主动抛出带上下文的NullPointerException,或用@NonNull注解配合编译期检查;对可能为空的返回值,优先用Optional封装,而非留到下游硬扛空指针。
- 日志里看到
NullPointerException堆栈末尾是.get()或.toString()?说明上游没处理Optional,别在这一层try-catch - DTO转VO时字段为null导致NPE?应该在转换层做默认值填充或抛
IllegalArgumentException,而不是让空值穿透到展示层 - Spring Boot中Controller参数用
@Valid后仍出现NPE?检查是否漏了@NotNull在嵌套对象字段上
自定义异常该继承RuntimeException还是Exception
看调用方有没有能力/意愿处理它。如果异常发生后业务必须中断、无法降级、需要人工介入(比如支付签名验签失败、核心配置加载失败),就用受检异常(继承Exception);否则一律用非受检异常(继承RuntimeException)。
强行用受检异常会倒逼调用方写一堆catch(Exception e) { throw new RuntimeException(e); },反而丢失语义。Spring生态默认把所有异常转成HTTP 500,除非你显式用@ResponseStatus或全局异常处理器映射。
立即学习“Java免费学习笔记(深入)”;
- 数据库连接超时、Redis集群不可用这类基础设施故障,适合定义为
InfrastructureException extends RuntimeException,上层统一兜底重试或熔断 - 用户提交了非法邮箱格式?用
ValidationException extends RuntimeException,前端直接提示,不需要强制上层写try-catch - 银行转账余额不足?这是明确的业务规则异常,用
InsufficientBalanceException extends Exception,让服务编排层决定是提示用户、冻结订单,还是走退款流程
全局异常处理器里@ExceptionHandler怎么避免包路径冲突
多个@ControllerAdvice类里写了相同异常类型的@ExceptionHandler,Spring按类名字母序选择,不是按包深度或声明顺序。这会导致预期外的处理器生效,尤其在引入第三方SDK自带异常处理时。
解决办法只有两个:要么统一收口到一个@ControllerAdvice类,要么用@Order明确优先级(数值越小优先级越高)。别依赖IDE自动import的包路径缩写,手写全限定名更安全。
- 自己写的
GlobalExceptionHandler和Swagger的WebMvcConfigurer都处理HttpRequestMethodNotSupportedException?加@Order(Ordered.HIGHEST_PRECEDENCE) - 测试环境启用了
spring-boot-devtools,它的RestartEndpoint会注册自己的异常处理器,上线前务必确认是否被覆盖 - 用Lombok的
@Slf4j时,别在@ExceptionHandler方法里直接log.error("msg", e)——e可能是被包装过的,要先调用e.getCause()或e.getSuppressed()看根因
日志里异常堆栈为什么总在第3行就断了
因为很多中间件(Druid、MyBatis、Feign)默认只打印异常的“根源帧”,省略了无关的框架调用链。看起来像堆栈被截断,其实是做了折叠。真正的根因往往藏在Caused by:之后,或者Suppressed:里。
排查时别只盯着第一段堆栈。用Throwable.printStackTrace(PrintStream s)完整输出,或在IDE调试时展开e.getStackTrace()数组;线上环境则确保日志框架(如Logback)配置了%ex{full}而不是%ex。
- MyBatis执行SQL报
org.apache.ibatis.exceptions.PersistenceException,实际错误在Caused by里的MySQLSyntaxErrorException,但日志默认不显示那行 - CompletableFuture异步任务里抛异常,堆栈首行是
ForkJoinWorkerThread.run,真实业务代码在java.util.concurrent.CompletableFuture$AsyncSupply.exec之后 - Spring Cloud Gateway过滤器里异常被
ServerWebExchange包装过两次,得连看三层Caused by:才能定位到原始TimeoutException
异常分层不是堆砌类名,是让每一层只关心自己能决策的事。下层抛出的异常,上层要么处理、要么转译、要么继续上抛——选错哪一环,健壮性就断在哪。










