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
相关产品推荐
相关产品推荐

