Function + 异常策略链:构建可组合的异常封装工具类
引言:异常到底该谁处理?
在日常 Java 业务开发中,我们常常写出这样的代码:
String result = someRemoteCall();
if (result == null) {throw new RuntimeException("接口返回为空");
}
看起来没问题,但一旦这种模式重复几十次,并在不同的团队成员手中以不同的形式出现(Optional
、if-null-throw
、try-catch 包装、日志输出混杂),你就会发现:异常的处理并不统一,也很难组合。
有没有办法像处理数据一样“组合”异常策略?答案是:有 —— 结合 Function<T, R>
+ 策略链思想,我们可以优雅地构建一套“异常装饰器”工具类。
核心思想:Function 的包装链 + 异常策略分离
我们想要实现的目标是:
- 可以对任何 Function 进行统一的异常封装(比如异常转换、日志打点、返回默认值等)
- 保持函数式接口的组合能力(支持
.andThen()
/.compose()
) - 封装后的 Function 可被透明使用,不需要改业务代码
本质上是一种责任链模式 + 装饰器模式 + 函数式编程的融合。
工具类设计:FunctionChainWrapper
@FunctionalInterface
public interface CheckedFunction<T, R> {R apply(T t) throws Exception;
}
我们定义了一个能抛异常的函数接口,然后封装一个 FunctionWrapper
工具:
public class FunctionChainWrapper<T, R> {private final CheckedFunction<T, R> originalFunction;private final List<Function<Throwable, R>> exceptionHandlers = new ArrayList<>();private Function<Exception, RuntimeException> exceptionTranslator = RuntimeException::new;private Consumer<Throwable> exceptionLogger = e -> {}; // 默认无操作private R fallbackValue = null;private boolean hasFallback = false;public FunctionChainWrapper(CheckedFunction<T, R> originalFunction) {this.originalFunction = originalFunction;}public FunctionChainWrapper<T, R> onException(Function<Throwable, R> handler) {this.exceptionHandlers.add(handler);return this;}public FunctionChainWrapper<T, R> logException(Consumer<Throwable> logger) {this.exceptionLogger = logger;return this;}public FunctionChainWrapper<T, R> translateException(Function<Exception, RuntimeException> translator) {this.exceptionTranslator = translator;return this;}public FunctionChainWrapper<T, R> fallback(R value) {this.fallbackValue = value;this.hasFallback = true;return this;}public Function<T, R> build() {return input -> {try {return originalFunction.apply(input);} catch (Throwable ex) {exceptionLogger.accept(ex);for (Function<Throwable, R> handler : exceptionHandlers) {try {return handler.apply(ex);} catch (Throwable ignore) {}}if (hasFallback) return fallbackValue;if (ex instanceof RuntimeException) throw (RuntimeException) ex;throw exceptionTranslator.apply(new Exception(ex));}};}
}
使用示例:封装远程调用函数
FunctionChainWrapper<String, String> wrapper = new FunctionChainWrapper<>(input -> {if ("bad".equals(input)) throw new IOException("网络失败");return "Result: " + input;
});Function<String, String> safeFunc = wrapper.logException(e -> log.error("调用失败: {}", e.getMessage())).fallback("默认值").build();String val = safeFunc.apply("bad"); // 输出:默认值
可以继续组合使用:
Function<String, String> func = safeFunc.andThen(res -> res.toUpperCase()).andThen(res -> res + " ✅");String result = func.apply("bad"); // "默认值 ✅"
与 BiFunction 的扩展
只需简单改造:
@FunctionalInterface
public interface CheckedBiFunction<T, U, R> {R apply(T t, U u) throws Exception;
}public class BiFunctionChainWrapper<T, U, R> {// 与 FunctionChainWrapper 类似,只是换成 BiFunction
}
你就可以封装两个参数的函数,如数据库查询、组合业务等:
BiFunctionChainWrapper<String, Integer, String> wrapper = new BiFunctionChainWrapper<>((name, age) -> {if (age < 0) throw new IllegalArgumentException("非法年龄");return name + " is " + age + " years old.";}
);BiFunction<String, Integer, String> safeFunc = wrapper.translateException(e -> new BusinessException("年龄不合法", e)).fallback("默认用户").build();
单元测试建议(Function 单测思路)
- 输入正常、输出正常
- 输入异常(会触发 fallback / log)
- 异常链中的处理函数也抛异常(验证链的容错)
- 验证 log 是否触发(可 mock
logger
) - 组合测试(andThen)是否正常传递
@Test
void testFallbackTriggered() {FunctionChainWrapper<String, String> wrapper = new FunctionChainWrapper<>(s -> {throw new RuntimeException("fail");}).fallback("safe");Function<String, String> safeFunc = wrapper.build();Assertions.assertEquals("safe", safeFunc.apply("anything"));
}
避坑提示
- 不要用 try-catch 把所有逻辑包死,应该聚焦于“异常感知点”来封装(I/O、反序列化、远程调用等)
- 日志与 fallback 分离,不要耦合:打日志是一种副作用,fallback 是行为策略
- 组合时注意状态性函数(如带缓存)之间的顺序
延伸思考:函数式异常封装的更大价值
- 能让你的业务逻辑表达更加清晰(只关注 happy path)
- 异常处理逻辑被拆分并复用(通用 fallback、打点策略可组合)
- 可以在策略链中插入熔断器、缓存装饰器等 —— 用法无限扩展
总结
Function + 异常策略链
是一个十分实用且优雅的模式。它不仅解决了代码冗余和异常处理不一致的问题,还让我们重新认识了函数式接口的扩展能力。
推荐阅读文章
-
由 Spring 静态注入引发的一个线上T0级别事故(真的以后得避坑)
-
如何理解 HTTP 是无状态的,以及它与 Cookie 和 Session 之间的联系
-
HTTP、HTTPS、Cookie 和 Session 之间的关系
-
什么是 Cookie?简单介绍与使用方法
-
什么是 Session?如何应用?
-
使用 Spring 框架构建 MVC 应用程序:初学者教程
-
有缺陷的 Java 代码:Java 开发人员最常犯的 10 大错误
-
如何理解应用 Java 多线程与并发编程?
-
把握Java泛型的艺术:协变、逆变与不可变性一网打尽
-
Java Spring 中常用的 @PostConstruct 注解使用总结
-
如何理解线程安全这个概念?
-
理解 Java 桥接方法
-
Spring 整合嵌入式 Tomcat 容器
-
Tomcat 如何加载 SpringMVC 组件
-
“在什么情况下类需要实现 Serializable,什么情况下又不需要(一)?”
-
“避免序列化灾难:掌握实现 Serializable 的真相!(二)”
-
如何自定义一个自己的 Spring Boot Starter 组件(从入门到实践)
-
解密 Redis:如何通过 IO 多路复用征服高并发挑战!
-
线程 vs 虚拟线程:深入理解及区别
-
深度解读 JDK 8、JDK 11、JDK 17 和 JDK 21 的区别
-
10大程序员提升代码优雅度的必杀技,瞬间让你成为团队宠儿!
-
“打破重复代码的魔咒:使用 Function 接口在 Java 8 中实现优雅重构!”
-
Java 中消除 If-else 技巧总结
-
线程池的核心参数配置(仅供参考)
-
【人工智能】聊聊Transformer,深度学习的一股清流(13)
-
Java 枚举的几个常用技巧,你可以试着用用
-
由 Spring 静态注入引发的一个线上T0级别事故(真的以后得避坑)
-
如何理解 HTTP 是无状态的,以及它与 Cookie 和 Session 之间的联系
-
HTTP、HTTPS、Cookie 和 Session 之间的关系
-
使用 Spring 框架构建 MVC 应用程序:初学者教程
-
有缺陷的 Java 代码:Java 开发人员最常犯的 10 大错误
-
Java Spring 中常用的 @PostConstruct 注解使用总结
-
线程 vs 虚拟线程:深入理解及区别
-
深度解读 JDK 8、JDK 11、JDK 17 和 JDK 21 的区别
-
10大程序员提升代码优雅度的必杀技,瞬间让你成为团队宠儿!
-
探索 Lombok 的 @Builder 和 @SuperBuilder:避坑指南(一)
-
为什么用了 @Builder 反而报错?深入理解 Lombok 的“暗坑”与解决方案(二)