
后端API设计【免费下载链接】feignFeign makes writing java http clients easier项目地址https://gitcode.com/gh_mirrors/fe/feign点击查看免费下载本文是 OpenFeign 生态中feign-annotation-error-decoder模块的完整使用指南。该模块允许开发者在 Feign 客户端接口interface上通过ErrorHandling系列注解按 HTTP 状态码声明式地生成对应的业务异常从而替代手写ErrorDecoder的繁琐分支逻辑。读完本文你将掌握如何在 Feign 构建器中接入AnnotationErrorDecoder、类级/方法级注解的优先级规则、如何构造携带 Request / ResponseBody / ResponseHeaders 的复杂异常、如何复用接口继承与元注解meta-annotation来组织错误处理配置以及底层源码的实现原理。一、模块定位与快速接入1.1 模块解决的问题在 Feign 中默认的ErrorDecoderDefaultErrorDecoder只会把非 2xx 响应包装成通用的FeignException调用方无法直接区分 401、403、404 等不同错误语义。传统做法是实现自定义ErrorDecoder在decode()方法里写大量if (response.status() 401) ...的分支。这种代码既分散又难以维护。feign-annotation-error-decoder模块源码位于 annotation-error-decoder提供了另一条路径直接在 Feign 接口上声明注解把「状态码 → 异常类型」的映射关系以声明式方式表达由模块在构建期自动解析并生成对应异常。1.2 添加依赖该模块的 Maven 坐标定义在 annotation-error-decoder/pom.xml 中dependency groupIdio.github.openfeign/groupId artifactIdfeign-annotation-error-decoder/artifactId /dependency其唯一的核心依赖是feign-core测试时依赖feign-core的 test-jar、mockwebserver 与 JUnit Jupiter因此接入成本很低只要项目里已有 Feign 核心即可。1.3 在 Feign 构建器中启用将AnnotationErrorDecoder作为errorDecoder注入即可GitHub github Feign.builder() .errorDecoder( AnnotationErrorDecoder.builderFor(GitHub.class).build() ) .target(GitHub.class, https://api.github.com);关键 API 是静态工厂方法AnnotationErrorDecoder.builderFor(Class? apiType)其声明位于 AnnotationErrorDecoder.java。Builder 在build()阶段会调用generateErrorHandlerMapFromApi(apiType)扫描接口上的注解生成methodKey - MethodErrorHandler的映射表AnnotationErrorDecoder.java。从源码看运行时解码非常轻量AnnotationErrorDecoder.javaOverride public Exception decode(String methodKey, Response response) { if (errorHandlerMap.containsKey(methodKey)) { return errorHandlerMap.get(methodKey).decode(response); } return defaultDecoder.decode(methodKey, response); }也就是说扫描注解的开销只发生在构建期请求出错时仅是一次Map查找命中后交给MethodErrorHandler按状态码路由。二、注解模型与优先级规则2.1 两个核心注解ErrorHandling可标注在类接口或方法上Target({ElementType.TYPE, ElementType.METHOD})运行时保留ErrorHandling.java。它有两个属性codeSpecificErrorCodes[]数组声明「状态码 → 异常」映射defaultException该作用域的兜底异常类型默认值是内部占位类ErrorHandling.NO_DEFAULT.class。ErrorCodes声明一组状态码与一个目标异常codes()为int[]generate()为Class? extends ExceptionErrorCodes.java。2.2 匹配优先级从最具体到最不具体注解在类级与方法级均可使用解析顺序从「最具体」到「最不具体」为方法上声明的、针对具体状态码的异常method code-specific类上声明的、针对具体状态码的异常class code-specific方法的默认异常method default类的默认异常class default。这一顺序与源码实现完全对应MethodErrorHandler.getConstructorDefinition()依次检查方法级状态码映射、类级状态码映射最后落到默认异常MethodErrorHandler.java而「方法未定义 defaultException 时回落到类级默认」的逻辑在 AnnotationErrorDecoder.java。2.3 完整示例与映射表以下面的 GitHub 客户端为例ErrorHandling(codeSpecific { ErrorCodes( codes {401}, generate UnAuthorizedException.class), ErrorCodes( codes {403}, generate ForbiddenException.class), ErrorCodes( codes {404}, generate UnknownItemException.class), }, defaultException ClassLevelDefaultException.class ) interface GitHub { ErrorHandling(codeSpecific { ErrorCodes( codes {404}, generate NonExistentRepoException.class), ErrorCodes( codes {502, 503, 504}, generate RetryAfterCertainTimeException.class), }, defaultException FailedToGetContributorsException.class ) RequestLine(GET /repos/{owner}/{repo}/contributors) ListContributor contributors(Param(owner) String owner, Param(repo) String repo); }对contributors方法而言各状态码的最终归属为CodeException来源说明401UnAuthorizedException类级定义方法未覆盖403ForbiddenException类级定义方法未覆盖404NonExistentRepoException方法级定义覆盖类级 404 映射类级默认不会命中502、503、504RetryAfterCertainTimeException方法级定义多个状态码可共享同一异常其他任意码FailedToGetContributorsException方法默认异常注意两点边界行为类级默认异常只有在方法未声明defaultException、且状态码在方法级与类级都无映射时才会被抛出。若状态码无处可映射、且任何层级都没有默认异常则落到默认的ErrorDecoder默认即 Feign 自带的DefaultErrorDecoder。2.4 自定义兜底解码器可以通过withDefaultDecoder(...)替换未命中时的兜底解码器GitHub github Feign.builder() .errorDecoder( AnnotationErrorDecoder.builderFor(GitHub.class) .withDefaultDecoder(new MyOtherErrorDecoder()) .build() ) .target(GitHub.class, https://api.github.com);对应源码为Builder.withDefaultDecoder(ErrorDecoder)默认值为new DefaultErrorDecoder()AnnotationErrorDecoder.java。从测试 AnnotationErrorDecoderPriorityTest.java 可以看到该优先级被参数化测试逐条验证例如 404 在method1Test上命中Method1NotFoundException而同样的 404 在method3Test无方法级映射上则命中类级的ClassLevelNotFoundException500/503 命中类级映射的ServeErrorException未被任何映射覆盖的 504 分别落到各方法的默认异常与类级默认异常。三、重复状态码定义会在构建期报错源码中有一个容易踩的坑同一个作用域内如果多个ErrorCodes声明了相同的状态码readAnnotation()会直接抛出IllegalStateExceptionAnnotationErrorDecoder.javathrow new IllegalStateException( Status Code [ statusCode ] has already been declared to throw [ ... ] and [ ... ] - dupe definition);因此同一个类或同一个方法内每个状态码只能映射一次若确实需要「同一状态码在不同方法上抛不同异常」应把映射放在各自的方法级注解上而不是在类级重复声明。四、复杂异常把 Request、响应体、响应头注入异常构造器4.1 默认构造器任何拥有**公开默认构造器public 无参构造器**的异常类型都可以直接使用class DefaultConstructorException extends Exception {}4.2 通过 FeignExceptionConstructor 标注构造器如果想在异常中携带feign.Request、响应体body或响应头headers就需要在构造器上标注FeignExceptionConstructorFeignExceptionConstructor.javaTarget(CONSTRUCTOR)。body 参数的标注ResponseBody是可选的——前提是参数类型不会与其他参数产生歧义。以下所有写法都是合法的源码示例摘自 README.mdclass JustBody extends Exception { FeignExceptionConstructor public JustBody(String body) { } } class JustRequest extends Exception { FeignExceptionConstructor public JustRequest(Request request) { } } class RequestAndResponseBody extends Exception { FeignExceptionConstructor public RequestAndResponseBody(Request request, String body) { } } // 响应头必须是 MapString, CollectionString class BodyAndHeaders extends Exception { FeignExceptionConstructor public BodyAndHeaders(ResponseBody String body, ResponseHeaders MapString, CollectionString headers) { } } class RequestAndResponseBodyAndHeaders extends Exception { FeignExceptionConstructor public RequestAndResponseBodyAndHeaders(Request request, ResponseBody String body, ResponseHeaders MapString, CollectionString headers) { } } class JustHeaders extends Exception { FeignExceptionConstructor public JustHeaders(ResponseHeaders MapString, CollectionString headers) { } }4.3 构造器的解析规则源码级说明ExceptionGenerator.Builder.build()通过反射解析被标注构造器的参数ExceptionGenerator.java参数上带ResponseHeaders的必须是Map类型且一个构造器只能有一个否则checkState报错不带标注、类型为feign.Request的参数被识别为请求对象同样只能有一个其余不带标注或带ResponseBody的参数被识别为 body只能有一个构造器必须至少有一个参数才允许被FeignExceptionConstructor标注若被标注的构造器超过一个同样抛出IllegalStateExceptionToo many constructors marked with FeignExceptionConstructor。4.4 构造器选择与启动期校验getConstructor()的查找顺序是优先寻找带FeignExceptionConstructor且参数非空的构造器若没有则退回查找公开无参构造器两者都没有就抛出IllegalStateExceptionExceptionGenerator.java。另一个重要行为构建期会用一个测试响应TEST_RESPONSE状态码 500、空 body、含 TestHeader实际调用一次异常构造器来做校验validateGeneratorCanBeUsedToGenerateExceptionsExceptionGenerator.java。因此如果构造器内没有对 body 做 null 检查启动/构建时会抛出 NPE 导致初始化失败构造器参数类型不合法例如响应头不是MapString, CollectionString也会在启动期被提前发现而不是等到运行时。这也是 README 强调「at setup/startup time, the generators are checked with a null value of the body」的原因。五、让响应体变成复杂 POJOwithResponseBodyDecoder如果希望异常构造器接收解码后的 POJO 而非原始字符串 body需要像普通响应解码一样传入DecoderGitHub github Feign.builder() .errorDecoder( AnnotationErrorDecoder.builderFor(GitHub.class) .withResponseBodyDecoder(new JacksonDecoder()) .build() ) .target(GitHub.class, https://api.github.com);withResponseBodyDecoder(Decoder)对应的默认值是new DefaultDecoder()AnnotationErrorDecoder.java。传入如 Jackson、Gson 等解码器后就可以写出这样的异常class ComplexPojoException extends Exception { FeignExceptionConstructor public ComplexPojoException(GithubExceptionResponse body) { if (body ! null) { // extract data } else { // fallback code } } } // POJO 可以是任意结构只要解码器能处理 class GithubExceptionResponse { public String message; public int githubCode; public ListString urlsForHelp; }底层解码发生在ExceptionGenerator.resolveBody()若 body 类型本身就是feign.Response则直接返回原响应否则调用bodyDecoder.decode(response, bodyType)解码过程中抛出的IOException或DecodeException会被吞掉并返回nullExceptionGenerator.java——这也是上面构造器里必须写 null 分支的另一层原因。六、接口继承interface inheritance的注意事项子接口可以继承父接口的ErrorHandling配置但这不是 Java 注解的自然继承注解在接口上并不天然继承而是模块自己的查找逻辑AnnotationErrorDecoder.java 的实现如下先检查当前接口自身是否有ErrorHandling有则直接使用否则遍历apiType.getInterfaces()递归查找父接口查找顺序完全由 Java API 返回无法保证稳定找到第一个带注解的父接口即停止不会继续合并其他父接口的定义若类型不是接口还会沿getSuperclass()向上检查跳过Object。由此得到几条实践结论如果多个被extends的父接口都声明了ErrorHandling无法保证选中哪一个应在子接口显式处理只要只从一个基础接口继承例如统一把所有 404 定义为NotFoundException就是安全的一旦陷入复杂多态多父接口、多层继承行为不可控——README 的忠告是「dont go crazy!」。继承示例ErrorHandling(codeSpecific { ErrorCodes( codes {401}, generate UnAuthorizedException.class), ErrorCodes( codes {403}, generate ForbiddenException.class), ErrorCodes( codes {404}, generate UnknownItemException.class), }, defaultException ClassLevelDefaultException.class ) interface FeignClientBase {} interface GitHub1 extends FeignClientBase { ErrorHandling(codeSpecific { ErrorCodes( codes {404}, generate NonExistentRepoException.class), ErrorCodes( codes {502, 503, 504}, generate RetryAfterCertainTimeException.class), }, defaultException FailedToGetContributorsException.class ) RequestLine(GET /repos/{owner}/{repo}/contributors) ListContributor contributors(Param(owner) String owner, Param(repo) String repo); } interface GitHub2 extends FeignClientBase { ErrorHandling(codeSpecific { ErrorCodes( codes {404}, generate NonExistentRepoException.class), ErrorCodes( codes {502, 503, 504}, generate RetryAfterCertainTimeException.class), }, defaultException FailedToGetContributorsException.class ) RequestLine(GET /repos/{owner}/{repo}/contributors) ListContributor contributors(Param(owner) String owner, Param(repo) String repo); } ErrorHandling(codeSpecific { ErrorCodes( codes {401}, generate UnAuthorizedException.class) }, defaultException ClassLevelDefaultException.class ) interface GitHub3 extends FeignClientBase { ErrorHandling(codeSpecific { ErrorCodes( codes {404}, generate NonExistentRepoException.class), ErrorCodes( codes {502, 503, 504}, generate RetryAfterCertainTimeException.class), }, defaultException FailedToGetContributorsException.class ) RequestLine(GET /repos/{owner}/{repo}/contributors) ListContributor contributors(Param(owner) String owner, Param(repo) String repo); }行为分析GitHub1、GitHub2继承父接口的类级错误处理任何 401/403/404 都能被正确处理只要方法没有声明更具体的异常GitHub3因为自己声明了ErrorHandling只处理了 401父接口的 403/404 映射不会与子接口合并因此 403/404 不再被父接口定义覆盖。七、元注解Meta-annotations消除重复注解当多个方法需要以相同方式处理某个状态码但与类级配置不同时可以把ErrorHandling提炼成元注解减少重复代码。元注解就是包含ErrorHandling的注解也可以顺带包含其他注解例如 Spring REST 注解。ErrorHandling( codeSpecific { ErrorCodes(codes {404}, generate NoDataFoundException.class), }, defaultException GithubRemoteException.class) Retention(RetentionPolicy.RUNTIME) interface NoDataErrorHandling { }使用时把元注解贴到方法上即可。下面「Before / After」两段代码行为完全一致Before每个方法重复写完整注解ErrorHandling(codeSpecific { ErrorCodes( codes {404}, generate UnknownItemException.class) }, defaultException ClassLevelDefaultException.class ) interface GitHub { ErrorHandling(codeSpecific { ErrorCodes( codes {404}, generate NoDataFoundException.class) }, defaultException GithubRemoteException.class ) RequestLine(GET /repos/{owner}/{repo}/contributors) ListContributor contributors(Param(owner) String owner, Param(repo) String repo); ErrorHandling(codeSpecific { ErrorCodes( codes {404}, generate NoDataFoundException.class) }, defaultException GithubRemoteException.class ) RequestLine(GET /repos/{owner}/{repo}/languages) MapString, Integer languages(Param(owner) String owner, Param(repo) String repo); ErrorHandling RequestLine(GET /repos/{owner}/{repo}/team) ListTeam teams(Param(owner) String owner, Param(repo) String repo); }After使用元注解精简ErrorHandling(codeSpecific { ErrorCodes( codes {404}, generate UnknownItemException.class) }, defaultException ClassLevelDefaultException.class ) interface GitHub { NoDataErrorHandling RequestLine(GET /repos/{owner}/{repo}/contributors) ListContributor contributors(Param(owner) String owner, Param(repo) String repo); NoDataErrorHandling RequestLine(GET /repos/{owner}/{repo}/languages) MapString, Integer languages(Param(owner) String owner, Param(repo) String repo); ErrorHandling RequestLine(GET /repos/{owner}/{repo}/team) ListTeam teams(Param(owner) String owner, Param(repo) String repo); }最终行为contributors/languages404 抛NoDataFoundException方法级其余状态码抛GithubRemoteException方法默认teams404 抛UnknownItemException类级其余状态码抛ClassLevelDefaultException类默认。元注解的查找逻辑在getErrorHandlingAnnotation()中优先取元素上的ErrorHandling若没有则遍历元素上的注解检查每个注解的annotationType()上是否带ErrorHandlingAnnotationErrorDecoder.java。元注解的使用规则与限制接口继承场景下的元注解继承遵循与接口继承相同的规则ErrorHandling优先于元注解同一类/方法上同时存在两者时以ErrorHandling为准子接口方法或类上的元注解优先于父接口中定义的错误处理元注解之上再套元注解不被支持——只会检查类型上直接声明的注解是否带ErrorHandling若同一类/方法上存在多个带ErrorHandling的元注解只会使用 Java API 返回的第一个其余被忽略因此建议每个方法或类上只放一个元注解顺序同样不保证不支持配置合并merge例如多个元注解、或元注解与ErrorHandling同时出现都不会被合并。八、总结何时使用 AnnotationErrorDecoderfeign-annotation-error-decoder适合以下场景客户端接口方法较多、错误语义丰富希望把「状态码 → 异常」映射收敛到接口声明处保持代码自文档化需要把响应体解码成 POJO 后塞进异常、需要访问原始Request或响应头又不想手写解码逻辑希望复用统一错误约定基础接口 元注解并让启动期尽早暴露配置错误重复状态码、构造器参数类型非法、body 缺失 null 检查等。同时也要注意它的边界不做多父接口配置合并、多个元注解共存时顺序不确定、启动期会真实调用一次构造器做校验构造器必须能容忍 null body。理解这些规则后配合 AnnotationErrorDecoder.java 与 MethodErrorHandler.java 的实现就能在自己的 Feign 客户端中稳定、高效地落地声明式错误处理。赞分享后端API设计【免费下载链接】feignFeign makes writing java http clients easier项目地址https://gitcode.com/gh_mirrors/fe/feign点击查看免费下载相关推荐FastAPI 响应状态码详解声明、使用与动态修改 HTTP 状态码FastAPI 响应状态码详解声明、使用与动态修改 HTTP 状态码 本篇技术指南围绕 FastAPI 中 status_code 参数展开讲解如何在路径操后端Web框架API设计深入Feign注解系统声明式HTTP API开发深入Feign注解系统声明式HTTP API开发 本文深入解析Feign框架的核心注解系统重点探讨RequestLine、Param、Headers、后端API设计Feign自定义异常映射HTTP状态码到业务异常的完美实践Feign自定义异常映射HTTP状态码到业务异常的完美实践 引言告别混乱的异常处理 你是否还在为Feign调用返回的各种HTTP状态码头疼404、403、后端API设计上一篇【亲测免费】 粒子背景动画插件 particles.js —— 轻量级的创意JavaScript库下一篇开源项目particles.js常见问题解决方案创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考