You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Java六边形(端口适配器)架构中如何强制适配器抛出指定异常?

要实现端口异常契约的强约束,仅靠Javadoc确实不够,可以通过以下几种技术方案组合落地,甚至可以达到接近入参/返回值修改触发编译错误的强制效果:


1. 优先用受检异常做编译期强制约束

如果团队没有禁止使用受检异常,直接在接口方法签名上声明要抛出的两个业务异常,所有实现类如果抛出其他非运行时异常、或未按要求处理这两个异常,会直接触发编译报错,是成本最低的强约束方案。

public interface RentBookPort {
    /**
     * 根据ISBN租赁图书
     * @param isbn 图书ISBN编号
     * @throws EntityNotFoundException 对应ISBN的图书不存在
     * @throws InvalidInputException 图书已被租赁等业务规则校验失败
     */
    void rentBook(String isbn) throws EntityNotFoundException, InvalidInputException;
}

如果团队规范要求只能用非受检异常,再选择下面的组合方案。


2. 抽象基类封装异常逻辑,从根源限制实现类自定义异常

不要让适配器直接实现Port接口,中间加一层不可修改的抽象基类,把异常转换逻辑全部封装在基类中,适配器只需要实现模板方法返回约定枚举即可,不需要接触异常抛出逻辑。

public abstract class AbstractRentBookAdapter implements RentBookPort {
    // final修饰避免子类重写,强制走统一的异常转换逻辑
    @Override
    public final void rentBook(String isbn) {
        if (isbn == null || isbn.isBlank()) {
            throw new InvalidInputException("ISBN不能为空");
        }
        // 调用子类实现的模板方法获取操作状态
        RentOperationStatus status = doRentBook(isbn);
        // 统一按契约转换为对应异常
        switch (status) {
            case BOOK_NOT_FOUND -> throw new EntityNotFoundException("ISBN对应图书不存在");
            case BOOK_ALREADY_RENTED -> throw new InvalidInputException("图书已被租赁");
            case SUCCESS -> {}
            default -> throw new IllegalStateException("适配器返回未定义状态,违反端口契约");
        }
    }

    // 子类仅需要实现这个方法,返回约定枚举即可
    protected abstract RentOperationStatus doRentBook(String isbn);

    // 统一的操作状态枚举,避免子类自由返回结果
    protected enum RentOperationStatus {
        SUCCESS,
        BOOK_NOT_FOUND,
        BOOK_ALREADY_RENTED
    }
}

这种方案下适配器开发者根本没有自定义异常的空间,自然满足契约要求。


3. 通用端口契约测试套件,运行期强制校验

写一套不依赖任何具体适配器实现的通用测试用例,所有适配器的单元测试必须引入并跑通这套用例,运行期保证异常逻辑符合约定。
以JUnit5为例:

public class RentBookPortContractTest {
    // 所有适配器实现类的测试直接调用这个方法即可完成契约校验
    public static void validate(RentBookPort port, TestDataSupplier dataSupplier) {
        // 用例1:不存在的ISBN必须抛出EntityNotFoundException
        assertThrows(EntityNotFoundException.class, 
            () -> port.rentBook(dataSupplier.getNonExistentIsbn()));
        // 用例2:已被租赁的ISBN必须抛出InvalidInputException
        assertThrows(InvalidInputException.class, 
            () -> port.rentBook(dataSupplier.getRentedIsbn()));
        // 用例3:可租赁的ISBN调用成功不抛异常
        assertDoesNotThrow(() -> port.rentBook(dataSupplier.getAvailableIsbn()));
    }

    // 适配器实现方自行实现该接口,提供适配自身存储的测试数据
    public interface TestDataSupplier {
        String getNonExistentIsbn();
        String getRentedIsbn();
        String getAvailableIsbn();
    }
}

适配器的单元测试只需要少量代码即可完成契约校验:

class JdbcRentBookAdapterTest {
    @Test
    void shouldFollowPortContract() {
        JdbcRentBookAdapter adapter = new JdbcRentBookAdapter(dataSource);
        TestDataSupplier supplier = new JdbcTestDataSupplier(dataSource);
        RentBookPortContractTest.validate(adapter, supplier);
    }
}

4. 可选:ArchUnit静态检查做补充

如果团队规模较大、担心有人绕开上面的约束,可以新增ArchUnit扫描规则,在CI阶段直接阻断不符合要求的代码:

  • 所有RentBookPort的实现类必须继承AbstractRentBookAdapter
  • 所有RentBookPort实现类的rentBook方法只能抛出EntityNotFoundException、InvalidInputException两种运行时异常

落地建议

  • 能接受受检异常的场景:优先用「受检异常签名声明+Javadoc契约说明+通用测试套件」组合,成本最低、约束最强
  • 不能用受检异常的场景:用「抽象基类封装异常逻辑+通用测试套件+可选ArchUnit检查」组合,契约符合率可以达到99%以上,远高于纯文档约束

内容的提问来源于stack exchange,提问作者tscz

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.07 11:36:04