1. 概述

本文将探讨一种替代标准 try-catch 块的函数式异常处理方案。我们将使用 Vavr 库中的 Try 类,通过将错误处理嵌入正常程序流程,构建更流畅且可读性更强的 API。

如果想深入了解 Vavr,可参考这篇文章

2. 传统异常处理方式

假设我们有一个包含 call() 方法的简单接口,成功时返回 Response,失败时抛出受检异常 ClientException

public interface HttpClient {
    Response call() throws ClientException;
}

Response 是仅包含 id 字段的简单类:

public class Response {
    public final String id;

    public Response(String id) {
        this.id = id;
    }
}

当服务调用 HttpClient 时,需要用标准 try-catch 处理受检异常:

public Response getResponse() {
    try {
        return httpClient.call();
    } catch (ClientException e) {
        return null;
    }
}

这种方式的痛点:

  • ✅ 每个抛出受检异常的方法都会打断程序流程
  • ❌ 代码中充斥大量 try-catch 块,可读性差
  • ⚠️ 返回 null 可能导致后续 NPE 风险

理想情况下,我们需要一个能封装结果状态(成功/失败)的特殊类,根据状态链式处理后续操作。

3. 使用 Try 处理异常

Vavr 提供的 Try 容器表示可能抛出异常或成功完成的计算。将操作封装在 Try 对象中后,结果只能是 SuccessFailure,我们可以据此执行后续操作。

Try 重写之前的 getResponse() 方法:

public class VavrTry {
    private HttpClient httpClient;

    public Try<Response> getResponse() {
        return Try.of(httpClient::call);
    }

    // 标准构造器
}

关键点在于返回类型 Try<Response>。调用此类方法时必须显式处理两种可能结果:

  • ✅ 成功时返回 Success<Response>
  • ❌ 失败时返回 Failure<ClientException>

3.1. 处理成功场景

httpClient 成功返回结果时,测试用例如下:

@Test
public void givenHttpClient_whenMakeACall_shouldReturnSuccess() {
    // given
    Integer defaultChainedResult = 1;
    String id = "a";
    HttpClient httpClient = () -> new Response(id);

    // when
    Try<Response> response = new VavrTry(httpClient).getResponse();
    Integer chainedResult = response
      .map(this::actionThatTakesResponse)
      .getOrElse(defaultChainedResult);
    Stream<String> stream = response.toStream().map(it -> it.id);

    // then
    assertTrue(!stream.isEmpty());
    assertTrue(response.isSuccess());
    response.onSuccess(r -> assertEquals(id, r.id));
    response.andThen(r -> assertEquals(id, r.id)); 
 
    assertNotEquals(defaultChainedResult, chainedResult);
}

辅助方法 actionThatTakesResponse() 简单返回 id 的哈希值:

public int actionThatTakesResponse(Response response) {
    return response.id.hashCode();
}

核心操作解析:

  1. 链式调用map() 仅在 Success 时执行转换操作
  2. 默认值处理getOrElse() 在失败时返回默认值
  3. 状态检查isSuccess() 判断执行状态
  4. 副作用操作
    • onSuccess():成功时执行 Consumer
    • andThen():成功时执行副作用操作(与 onSuccess 类似)
  5. 流式转换toStream()Try 转为 Stream 进行后续处理

💡 进阶技巧:使用 transform() 可直接操作 Try 对象而无需解包:

public int actionThatTakesTryResponse(Try<Response> response, int defaultTransformation){
    return response.transform(responses -> response.map(it -> it.id.hashCode())
      .getOrElse(defaultTransformation));
}

3.2. 处理失败场景

httpClient 抛出异常时:

@Test
public void givenHttpClientFailure_whenMakeACall_shouldReturnFailure() {
    // given
    Integer defaultChainedResult = 1;
    HttpClient httpClient = () -> {
        throw new ClientException("problem");
    };

    // when
    Try<Response> response = new VavrTry(httpClient).getResponse();
    Integer chainedResult = response
        .map(this::actionThatTakesResponse)
        .getOrElse(defaultChainedResult);
     Option<Response> optionalResponse = response.toOption();

    // then
    assertTrue(optionalResponse.isEmpty());
    assertTrue(response.isFailure());
    response.onFailure(ex -> assertTrue(ex instanceof ClientException));
    assertEquals(defaultChainedResult, chainedResult);
}

失败场景关键操作:

  1. 状态判断isFailure() 返回 true
  2. 异常处理onFailure() 执行异常回调
  3. 类型转换toOption()Failure 转为 Option
    • ✅ 成功时:OptionSome
    • ❌ 失败时:OptionNone

⚠️ 实用技巧:当需要将 Try 传递给只处理 Option 的方法时,toOption() 特别有用。

3.3. 模式匹配实战

通过 recover() 结合模式匹配,可针对不同异常类型执行不同恢复策略:

@Test
public void givenHttpClientThatFailure_whenMakeACall_shouldReturnFailureAndNotRecover() {
    // given
    Response defaultResponse = new Response("b");
    HttpClient httpClient = () -> {
        throw new RuntimeException("critical problem");
    };

    // when
    Try<Response> recovered = new VavrTry(httpClient).getResponse()
      .recover(r -> Match(r).of(
          Case(instanceOf(ClientException.class), defaultResponse)
      ));

    // then
    assertTrue(recovered.isFailure());

关键点:

  • 仅当异常为 ClientException 时恢复为 Success
  • 其他异常(如 RuntimeException)保持 Failure 状态

💡 重新抛出关键异常:使用 getOrElseThrow() 可将关键异常向上抛出:

recovered.getOrElseThrow(throwable -> {
    throw new RuntimeException(throwable);
});

对于可恢复异常,模式匹配能优雅处理多种异常类型:

@Test
public void givenHttpClientThatFailure_whenMakeACall_shouldReturnFailureAndRecover() {
    // given
    Response defaultResponse = new Response("b");
    HttpClient httpClient = () -> {
        throw new ClientException("non critical problem");
    };

    // when
    Try<Response> recovered = new VavrTry(httpClient).getResponse()
      .recover(r -> Match(r).of(
        Case(instanceOf(ClientException.class), defaultResponse),
        Case(instanceOf(IllegalArgumentException.class), defaultResponse)
       ));
    
    // then
    assertTrue(recovered.isSuccess());
}

恢复策略:

  • ClientExceptionIllegalArgumentException 被恢复为 Success
  • 其他异常保持 Failure

4. 总结

本文通过实战案例展示了 Vavr Try 容器的核心用法:

  1. 函数式异常处理:将异常处理融入正常流程
  2. 链式操作:通过 map/recover 构建流畅 API
  3. 状态显式化:编译期强制处理成功/失败两种状态
  4. 模式匹配:精准控制异常恢复策略

使用 Try 能显著提升代码的函数式特性和可读性,特别适合构建健壮的异步/流式处理管道。

完整示例代码见 GitHub 项目(Maven 项目,可直接导入运行)。


原始标题:Guide to Try in Vavr