• 如何系列 如何玩转远程调用之OpenFegin+SpringBoot(非Cloud)


    简介

    市面上都是 Spring Cloud + openfeign

    想搞个Spring(boot) + openfeign

    Github: https://github.com/OpenFeign/feign

    Feign 10.x 及更高版本基于 Java 8 构建,应该适用于 Java 9、10 和 11。对于需要 JDK 6 兼容性的用户,请使用 Feign 9.x

    功能图

    架构图

    img

    依赖

    <dependency>
        <groupId>io.github.openfeigngroupId>
        <artifactId>feign-coreartifactId>
        <version>13.0version>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    原生Fegin示例

    基础

    // 1.定义接口
    interface GitHub {
      @RequestLine("GET /repos/{owner}/{repo}/contributors")
      List<Contributor> contributors(@Param("owner") String owner, @Param("repo") String repo);
    
      @RequestLine("POST /repos/{owner}/{repo}/issues")
      void createIssue(Issue issue, @Param("owner") String owner, @Param("repo") String repo);
    
    }
    
    // 测试
    public class MyApp {
        public static void main(String... args) {
            GitHub github = Feign.builder()
                    .logLevel(Logger.Level.FULL)
                    .options(new Request.Options(Duration.ofSeconds(2), Duration.ofSeconds(5), true))
                    .encoder(new JacksonEncoder())
                    .decoder(new JacksonDecoder())
                    .target(GitHub.class, "https://api.github.com");
            List<Contributor> contributors = github.contributors("OpenFeign", "feign");
            for (Contributor contributor : contributors) {
                System.out.println(contributor.login + " (" + contributor.contributions + ")");
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    原生注解@RequestLine有额外的理解成本,我们一般不会使用

    契约

    从10.5.0版本开始提供了feign-spring4,来适配spring注解。
    使用spring注解需要将contract契约设置为SpringContract。

    <dependency>
       <groupId>io.github.openfeigngroupId>
       <artifactId>feign-spring4artifactId>
       <version>13.0version>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    Feign 仅支持处理 java 接口(不支持抽象类或具体类)

    方法注解

    参数注解

    interface GitHub {
      @GetMapping("/repos/{owner}/{repo}/contributors") //改变
      List<Contributor> contributors(@PathVariable("owner") String owner, @PathVariable("repo") String repo);
      @PostMapping("/repos/{owner}/{repo}/issues") //改变
      void createIssue(Issue issue, @PathVariable("owner") String owner, @PathVariable("repo") String repo);
    }
    public class MyApp {
        public static void main(String... args) {
            GitHub github = Feign.builder()
                    .logLevel(Logger.Level.FULL)
                    .contract(new SpringContract()) // 这里 SpringContract
                    .options(new Request.Options(Duration.ofSeconds(2), Duration.ofSeconds(5), true))
                    .encoder(new JacksonEncoder())
                    .decoder(new JacksonDecoder())
                    .target(GitHub.class, "https://api.github.com");
            List<Contributor> contributors = github.contributors("OpenFeign", "feign");
            for (Contributor contributor : contributors) {
                System.out.println(contributor.login + " (" + contributor.contributions + ")");
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    日志

    <dependency>
       <groupId>io.github.openfeigngroupId>
       <artifactId>feign-sl4jartifactId>
       <version>13.0version>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    SLF4JModule允许将 Feign 的日志记录定向到SLF4J,允许您轻松使用选择的日志记录记录(Logback、Log4J 等)

    相当于 SLF4J 与 Feign 一起使用,满足 SLF4J 模块和您选择的 SLF4J 绑定添加到您的类路径中。然后,配置 Feign 使用 Slf4jLogger:

    public class Example {
      public static void main(String[] args) {
        GitHub github = Feign.builder()
                         .logger(new Slf4jLogger())
                         .logLevel(Level.FULL)
                         .target(GitHub.class, "https://api.github.com");
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    重试

    默认情况下,Feign会自动重试IOExceptions,无论HTTP方法如何,都将其视为临时的与网络相关的异常,并重试从ErrorDecoder中抛出的任何RetryableException。要自定义此行为,请通过构建器注册自定义的Retryer实例。

    以下示例展示了如何在收到401响应时使用ErrorDecoder和Retryer来刷新令牌并进行重试。

    public class Example {
        public static void main(String[] args) {
            var github = Feign.builder()
                    .decoder(new GsonDecoder())
                    .retryer(new MyRetryer(100, 3))
                    .errorDecoder(new MyErrorDecoder())
                    .target(Github.class, "https://api.github.com");
    
            var contributors = github.contributors("foo", "bar", "invalid_token");
            for (var contributor : contributors) {
                System.out.println(contributor.login + " " + contributor.contributions);
            }
        }
    
        static class MyErrorDecoder implements ErrorDecoder {
    
            private final ErrorDecoder defaultErrorDecoder = new Default();
    
            @Override
            public Exception decode(String methodKey, Response response) {
                // wrapper 401 to RetryableException in order to retry
                if (response.status() == 401) {
                    return new RetryableException(response.status(), response.reason(), response.request().httpMethod(), null, response.request());
                }
                return defaultErrorDecoder.decode(methodKey, response);
            }
        }
    
        static class MyRetryer implements Retryer {
    
            private final long period;
            private final int maxAttempts;
            private int attempt = 1;
    
            public MyRetryer(long period, int maxAttempts) {
                this.period = period;
                this.maxAttempts = maxAttempts;
            }
    
            @Override
            public void continueOrPropagate(RetryableException e) {
                if (++attempt > maxAttempts) {
                    throw e;
                }
                if (e.status() == 401) {
                    // remove Authorization first, otherwise Feign will add a new Authorization header
                    // cause github responses a 400 bad request
                    e.request().requestTemplate().removeHeader("Authorization");
                    e.request().requestTemplate().header("Authorization", "Bearer " + getNewToken());
                    try {
                        Thread.sleep(period);
                    } catch (InterruptedException ex) {
                        throw e;
                    }
                } else {
                    throw e;
                }
            }
    
            // Access an external api to obtain new token
            // In this example, we can simply return a fixed token to demonstrate how Retryer works
            private String getNewToken() {
                return "newToken";
            }
    
            @Override
            public Retryer clone() {
                return new MyRetryer(period, maxAttempts);
            }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70

    Retryers负责通过从方法continueOrPropagate(RetryableException e)返回true或false来确定是否应该进行重试;如果需要,将为每个Client执行创建一个Retryer实例,以便在每个请求之间维护状态。

    如果决定重试不成功,将抛出最后一个RetryException。要抛出导致重试不成功的原始原因,请使用exceptionPropagationPolicy()选项构建您的Feign客户端

    编码器/解码器

    • GSON
    • Jackson
    • Moshi
    • SOAP
    <dependency>
        <groupId>io.github.openfeigngroupId>
        <artifactId>feign-jacksonartifactId>
        <version>13.0version>
    dependency>
    
    <dependency>
        <groupId>io.github.openfeigngroupId>
        <artifactId>feign-gsonartifactId>
        <version>13.0version>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    public class Example {
      public static void main(String[] args) {
          GitHub github = Feign.builder()
                         .encoder(new JacksonEncoder())
                         .decoder(new JacksonDecoder())
                         .target(GitHub.class, "https://api.github.com");
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    自定义解码器
    public class MyJacksonDecoder extends JacksonDecoder {
        @Override
        public Object decode(Response response, Type type) throws IOException {
            if (response.body() == null) {
                return null;
            }
            if (type == String.class) {
                return StreamUtils.copyToString(response.body().asInputStream(), StandardCharsets.UTF_8);
            }
            return super.decode(response, type);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    请求拦截器

    当您需要更改所有请求时,无论其目标是什么,您都需要配置一个RequestInterceptor. 例如,如果您充当中介,您可能想要传播标X-Forwarded-For头。

    static class ForwardedForInterceptor implements RequestInterceptor {
      @Override public void apply(RequestTemplate template) {
        template.header("X-Forwarded-For", "origin.host.com");
      }
    }
    
    public class Example {
      public static void main(String[] args) {
        Bank bank = Feign.builder()
                     .decoder(accountDecoder)
                     .requestInterceptor(new ForwardedForInterceptor())
                     .target(Bank.class, "https://api.examplebank.com");
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    响应拦截器

    如果您需要将错误视为成功并返回结果而不是发送异常,那么您可以使用ResponseInterceptor

    例如,Feign 包含一个简单的RedirectionInterceptor可用于从重定向响应中提取位置标头。

    public interface Api {
      // returns a 302 response
      @RequestLine("GET /location")
      String location();
    }
    
    public class MyApp {
      public static void main(String[] args) {
        // Configure the HTTP client to ignore redirection
        Api api = Feign.builder()
                       .options(new Options(10, TimeUnit.SECONDS, 60, TimeUnit.SECONDS, false))
                       .responseInterceptor(new RedirectionInterceptor())
                       .target(Api.class, "https://redirect.example.com");
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    表单文件上传支持

    <dependency>
       <groupId>io.github.openfeign.formgroupId>
       <artifactId>feign-formartifactId>
    dependency>
    
    • 1
    • 2
    • 3
    • 4

    错误解码器

    ErrorDecoder如果您需要更多地控制处理意外响应,Feign 实例可以通过构建器注册自定义。

    public class Example {
      public static void main(String[] args) {
        MyApi myApi = Feign.builder()
                     .errorDecoder(new MyErrorDecoder())
                     .target(MyApi.class, "https://api.hostname.com");
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    所有导致 HTTP 状态不在 2xx 范围内的响应都将触发ErrorDecodersdecode方法,允许您处理响应、将失败包装到自定义异常中或执行任何其他处理。如果您想再次重试请求,请抛出RetryableException. 这将调用注册的 Retryer.

    断路器

    <dependency>
        <groupId>io.github.openfeigngroupId>
        <artifactId>feign-hystrixartifactId>
        <version>13.0version>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    HystrixFeign配置Hystrix提供的断路器支持。

    要将 Hystrix 与 Feign 一起使用,请将 Hystrix 模块添加到类路径中。然后使用HystrixFeign构建器:

    public class Example {
      public static void main(String[] args) {
        MyService api = HystrixFeign.builder()
            .target(MyFeignClient.class, "http://remote-service-url", new MyFeignClientFallbackFactory());
      }
    }
    
    interface GitHub {
      @GetMapping("/repos/{owner}/{repo}/contributors")
      List<Contributor> contributors(@PathVariable("owner") String owner, @PathVariable("repo") String repo);
        
    }    
    
    
    public class MyFeignClientFallbackFactory implements FallbackFactory<GitHub> {
        @Override
        public GitHub create(Throwable cause) {
            return new GitHub() {
                @Override
                public List<Contributor> contributors(String owner, String repo) {
                    return new ArrayList<>(); // 回退逻辑,可以返回默认值或错误消息
                }
            };
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    对于异步或反应式使用,返回HystrixCommandCompletableFuture

    上面配置callback使用

    设置超时等

    1. 熔断器配置:你可以设置熔断器的相关属性,如 circuitBreakerErrorThresholdPercentagecircuitBreakerSleepWindowInMillisecondscircuitBreakerRequestVolumeThreshold 等,以控制熔断器的行为。
    2. 线程池配置:如果你在 Hystrix 命令中使用了线程池隔离,你可以设置线程池的相关属性,如 coreSizemaxQueueSizekeepAliveTimeMinutes 等。
    3. 超时属性:除了设置总的执行超时时间,你还可以设置 executionTimeoutEnabledexecutionIsolationStrategy 等超时相关属性。
    4. 命令名和组名:你可以自定义命令的名称和分组名称,通过 andCommandKeyandCommandGroup 方法来设置。
    5. 并发属性:你可以设置命令执行的并发性相关属性,如 executionIsolationSemaphoreMaxConcurrentRequests
    public class MyApp {
        public static void main(String... args) {
    
    
            GitHub github = HystrixFeign.builder()
                    .logLevel(Logger.Level.FULL)
                    // 设置超时等
                    .setterFactory((target, method) -> HystrixCommand.Setter
                            .withGroupKey(HystrixCommandGroupKey.Factory.asKey(target.name()))
                            .andCommandKey(HystrixCommandKey.Factory.asKey(method.getName()))
                            .andCommandPropertiesDefaults(
                                    HystrixCommandProperties.Setter()
                                            .withExecutionTimeoutInMilliseconds(5000) // 设置执行超时时间
                                            .withCircuitBreakerRequestVolumeThreshold(20) // 设置熔断器请求数阈值
                                            .withCircuitBreakerSleepWindowInMilliseconds(10000) // 设置熔断器休眠窗口
                            )
                            .andThreadPoolPropertiesDefaults(
                                    HystrixThreadPoolProperties.Setter()
                                            .withCoreSize(10) // 设置线程池核心大小
                                            .withMaxQueueSize(100) // 设置线程池队列大小
                            ))
                    .contract(new SpringContract())
                    .options(new Request.Options(Duration.ofSeconds(2), Duration.ofSeconds(5), true))
                    .encoder(new JacksonEncoder())
                    .decoder(new JacksonDecoder())
                    .target(GitHub.class, "https://api.github.com", new MyFeignClientFallbackFactory());
            List<Contributor> contributors = github.contributors("OpenFeign", "feign");
            for (Contributor contributor : contributors) {
                System.out.println(contributor.login + " (" + contributor.contributions + ")");
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    指标metrics

    默认情况下,feign不会收集任何指标。

    但是,可以向任何假客户端添加指标收集功能。

    指标功能提供了一流的指标API,用户可以利用该API来深入了解请求/响应生命周期。

    <dependency>
        <groupId>io.github.openfeigngroupId>
        <artifactId>feign-micrometerartifactId>
        <version>13.0version>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    public class MyApp {
      public static void main(String[] args) {
        GitHub github = Feign.builder()
                             .addCapability(new MicrometerCapability())
                             .target(GitHub.class, "https://api.github.com");
    
        github.contributors("OpenFeign", "feign");
        // metrics will be available from this point onwards
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    Hystrix指标监控

    hystrix.execution{event=failure,group=https://api.github.com,key=contributors,terminal=false} throughput=0.033333/s
    hystrix.execution{event=fallback_missing,group=https://api.github.com,key=contributors,terminal=true} throughput=0.033333/s
    hystrix.execution{event=exception_thrown,group=https://api.github.com,key=contributors,terminal=false} throughput=0.033333/s
    hystrix.execution.terminal{group=https://api.github.com,key=contributors} throughput=0.033333/s
    hystrix.threadpool.tasks.cumulative.count{key=https://api.github.com,type=completed} throughput=0.016667/s
    hystrix.threadpool.tasks.cumulative.count{key=https://api.github.com,type=scheduled} throughput=0.016667/s
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    客户端

    
    <dependency>
        <groupId>io.github.openfeigngroupId>
        <artifactId>feign-okhttpartifactId>
        <version>13.0version>
    dependency>
    
    <dependency>
       <groupId>io.github.openfeigngroupId>
       <artifactId>feign-httpclientartifactId>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    OpenFeign默认Http客户端是HttpURLConnection(JDK自带的Http工具),该工具不能配置连接池,生产中使用时性能较差,故我们配置自己的Apache HttpClient连接池。(当然Open Feign也有OkHttp的适配)

    public class Example {
      public static void main(String[] args) {
        GitHub github = Feign.builder()
                         .client(new OkHttpClient())
                         .target(GitHub.class, "https://api.github.com");
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    配合SpringBoot(阶段一)

    就是把上面的Bean变为Spring Bean去托管,示例代码如下

    @Configuration
    public class FeginConfig {
    
        @Bean
        public Feign.Builder feignBuilder() {
            return Feign.builder()
                    .options(new Request.Options(Duration.ofSeconds(2), Duration.ofSeconds(5), true));
        }
    
        @Bean
        @ConditionalOnMissingBean
        @ConditionalOnProperty(name = "feign.hystrix.enabled", matchIfMissing = false)
        public Feign.Builder feignHystrixBuilder() {
            return HystrixFeign
                    .builder();
        }
    
        @Bean
        @ConditionalOnProperty(value = "feign.okhttp.enabled", matchIfMissing = false)
        public Client feignClient() {
            return new OkHttpClient();
        }
        @Bean
        @ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = true)
        public Client feignClient() {
            return new ApacheHttpClient();
        }
    
    
        @Bean
        public UserClient UserClient(){
            feignBuilder
                    .options(new Request.Options(2000, 5000));
            return feignBuilder.target(UserClient.class, "https://xxxx");
        }
    
    
        @Bean
        public OrgClient OrgClient(){
            feignBuilder
                    .options(new Request.Options(30000, 50000));
            return feignBuilder.target(OrgClient.class, "https://xxxx");
        }
        
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46

    配合SpringBoot(阶段二)

    • https://github.com/spring-cloud/spring-cloud-openfeign

    参考Spring Cloud Fegin流程如下

    1. 项目加载:在项目的启动阶段,EnableFeignClients 注解扮演了“启动开关”的角色,它使用 Spring 框架的 Import 注解导入了 FeignClientsRegistrar 类,开始了OpenFeign 组件的加载过程。
    2. 扫包FeignClientsRegistrar 负责 FeignClient 接口的加载,它会在指定的包路径下扫描所有的 FeignClients 类,并构造 FeignClientFactoryBean 对象来解析FeignClient 接口。
    3. 解析 FeignClient 注解FeignClientFactoryBean 有两个重要的功能,一个是解析FeignClient 接口中的请求路径和降级函数的配置信息;另一个是触发动态代理的构造过程。其中,动态代理构造是由更下一层的 ReflectiveFeign 完成的。
    4. 构建动态代理对象ReflectiveFeign 包含了 OpenFeign 动态代理的核心逻辑,它主要负责创建出 FeignClient 接口的动态代理对象。ReflectiveFeign 在这个过程中有两个重要任务,一个是解析 FeignClient 接口上各个方法级别的注解,将其中的远程接口URL、接口类型(GET、POST 等)、各个请求参数等封装成元数据,并为每一个方法生成一个对应的 MethodHandler 类作为方法级别的代理;另一个重要任务是将这些MethodHandler 方法代理做进一步封装,通过 Java 标准的动态代理协议,构建一个实现了 InvocationHandler 接口的动态代理对象,并将这个动态代理对象绑定到FeignClient 接口上。这样一来,所有发生在 FeignClient 接口上的调用,最终都会由它背后的动态代理对象来承接。

    1.EnableLakerFeignClients

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    @Documented
    @Import(LakerFeignClientsRegistrar.class)
    public @interface EnableLakerFeignClients {
    
        /**
         * Base packages to scan for annotated components.
         * @return base packages
         */
        String[] basePackages() default {};
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    2.LakerFeignClientsRegistrar

    public class LakerFeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware {
    
        private ResourceLoader resourceLoader;
    
    
        @Override
        public void setResourceLoader(ResourceLoader resourceLoader) {
            this.resourceLoader = resourceLoader;
        }
    
    
        @Override
        public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
            registerFeignClients(metadata, registry);
    
        }
    
    
        public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
            Set<BeanDefinition> candidateComponents = new LinkedHashSet<>();
            Map<String, Object> attrs = metadata.getAnnotationAttributes(EnableLakerFeignClients.class.getName());
            ClassPathScanningCandidateComponentProvider scanner = getScanner();
            scanner.setResourceLoader(this.resourceLoader);
            scanner.addIncludeFilter(new AnnotationTypeFilter(LakerFeignClient.class));
            Set<String> basePackages = getBasePackages(metadata);
            for (String basePackage : basePackages) {
                candidateComponents.addAll(scanner.findCandidateComponents(basePackage));
            }
    
            for (BeanDefinition candidateComponent : candidateComponents) {
                if (candidateComponent instanceof AnnotatedBeanDefinition beanDefinition) {
                    // verify annotated class is an interface
                    AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
                    Assert.isTrue(annotationMetadata.isInterface(), "@FeignClient can only be specified on an interface");
                    Map<String, Object> attributes = annotationMetadata
                            .getAnnotationAttributes(LakerFeignClient.class.getCanonicalName());
                    String className = annotationMetadata.getClassName();
                    registerFeignClient(className, attributes, registry);
                }
            }
        }
    
    
        private void registerFeignClient(String className, Map<String, Object> attributes,
                                         BeanDefinitionRegistry registry) {
            BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(LakerFeignClientFactoryBean.class);
            definition.addPropertyValue("url", getUrl(attributes));
            definition.addPropertyValue("type", className);
            definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
            AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
            beanDefinition.setPrimary(false);
            BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, null);
            BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
        }
    
    
        private String resolve(String value) {
            if (StringUtils.hasText(value) && this.resourceLoader instanceof ConfigurableApplicationContext) {
                return ((ConfigurableApplicationContext) this.resourceLoader).getEnvironment()
                        .resolvePlaceholders(value);
            }
            return value;
        }
    
        private String getUrl(Map<String, Object> attributes) {
            String url = resolve((String) attributes.get("url"));
            if (StringUtils.hasText(url)) {
                if (!url.contains("://")) {
                    url = "https://" + url;
                }
                try {
                    new URL(url);
                } catch (MalformedURLException e) {
                    throw new IllegalArgumentException(url + " is malformed", e);
                }
            }
            return url;
        }
    
    
        protected ClassPathScanningCandidateComponentProvider getScanner() {
            return new ClassPathScanningCandidateComponentProvider(false) {
                @Override
                protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
                    boolean isCandidate = false;
                    if (beanDefinition.getMetadata().isIndependent()) {
                        if (!beanDefinition.getMetadata().isAnnotation()) {
                            isCandidate = true;
                        }
                    }
                    return isCandidate;
                }
            };
        }
    
        protected Set<String> getBasePackages(AnnotationMetadata importingClassMetadata) {
            Map<String, Object> attributes = importingClassMetadata
                    .getAnnotationAttributes(EnableLakerFeignClients.class.getCanonicalName());
    
            Set<String> basePackages = new HashSet<>();
            for (String pkg : (String[]) attributes.get("basePackages")) {
                if (StringUtils.hasText(pkg)) {
                    basePackages.add(pkg);
                }
            }
            return basePackages;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108

    3.LakerFeignClientFactoryBean

    @Data
    class LakerFeignClientFactoryBean implements FactoryBean<Object>, InitializingBean, ApplicationContextAware {
        private Class<?> type;
    
        private String url;
    
    
        private ApplicationContext applicationContext;
    
        @Override
        public void afterPropertiesSet() throws Exception {
            Assert.hasText(this.url, "url must be set");
        }
    
        @Override
        public void setApplicationContext(ApplicationContext context) throws BeansException {
            this.applicationContext = context;
        }
    
        protected Feign.Builder feign() {
            Feign.Builder builder = get(Feign.Builder.class)
                    .contract(new SpringContract())
                    // required values
                    .encoder(get(Encoder.class))
                    .decoder(get(Decoder.class));
    
            // optional values
            Client client = getOptional(Client.class);
            if (client != null) {
                builder.client(client);
            }
            Logger.Level level = getOptional(Logger.Level.class);
            if (level != null) {
                builder.logLevel(level);
            }
            Retryer retryer = getOptional(Retryer.class);
            if (retryer != null) {
                builder.retryer(retryer);
            }
            ErrorDecoder errorDecoder = getOptional(ErrorDecoder.class);
            if (errorDecoder != null) {
                builder.errorDecoder(errorDecoder);
            }
            Request.Options options = getOptional(Request.Options.class);
            if (options != null) {
                builder.options(options);
            }
    
            Map<String, RequestInterceptor> requestInterceptors = getOptionals(RequestInterceptor.class);
            if (requestInterceptors != null) {
                builder.requestInterceptors(requestInterceptors.values());
            }
    
            return builder;
        }
    
        protected <T> T get(Class<T> type) {
            if (BeanFactoryUtils.beanNamesForTypeIncludingAncestors(applicationContext, type).length > 0) {
                return BeanFactoryUtils.beanOfTypeIncludingAncestors(applicationContext, type);
            } else {
                throw new IllegalStateException("No bean found of type " + type);
            }
        }
    
        protected <T> T getOptional(Class<T> type) {
            if (BeanFactoryUtils.beanNamesForTypeIncludingAncestors(applicationContext, type).length > 0) {
                return BeanFactoryUtils.beanOfTypeIncludingAncestors(applicationContext, type);
            }
            return null;
        }
    
        protected <T> Map<String, T> getOptionals(Class<T> type) {
            if (BeanFactoryUtils.beanNamesForTypeIncludingAncestors(applicationContext, type).length > 0) {
                return BeanFactoryUtils.beansOfTypeIncludingAncestors(applicationContext, type);
            }
            return null;
        }
    
        @Override
        public Object getObject() throws Exception {
            return feign().target(type, url);
        }
    
        @Override
        public Class<?> getObjectType() {
            return this.type;
        }
    
        @Override
        public boolean isSingleton() {
            return true;
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94

    4.LakerFeignClient

    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface LakerFeignClient {
    
        String url() default "";
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    5.FeginConfig

    @Configuration
    public class FeginConfig {
    
        @Bean
        public Feign.Builder feignBuilder() {
            return Feign.builder()
                    .options(new Request.Options(Duration.ofSeconds(2), Duration.ofSeconds(5), true));
        }
    
        @Bean
        @ConditionalOnMissingBean
        @ConditionalOnProperty(name = "feign.hystrix.enabled", matchIfMissing = false)
        public Feign.Builder feignHystrixBuilder() {
            return HystrixFeign
                    .builder();
        }
    
        @Bean
        @ConditionalOnProperty(value = "feign.okhttp.enabled", matchIfMissing = true)
        public Client feignClient() {
            return new OkHttpClient();
        }
    
        @Bean
        public Encoder encoder() {
            return new JacksonEncoder();
        }
        @Bean
        public Decoder decoder() {
            return new JacksonDecoder();
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    使用示例

    // 1.启用 EnableLakerFeignClients
    @SpringBootApplication
    @EnableLakerFeignClients(basePackages = "com.laker.admin")
    public class EasyAdminApplication {
        public static void main(String[] args) {
            SpringApplication.run(EasyAdminApplication.class, args);
        }
        
    // 或者
    @Configuration
    @EnableLakerFeignClients(basePackages = "com.laker.admin")
    public class FeginConfig {
    }
    // 2.定义接口
    @LakerFeignClient(url ="https://api.github.com")
    public interface GitHub {
      @GetMapping("/repos/{owner}/{repo}/contributors")
      List<Contributor> contributors(@PathVariable("owner") String owner, @PathVariable("repo") String repo);
      @PostMapping("/repos/{owner}/{repo}/issues")
      void createIssue(Issue issue, @PathVariable("owner") String owner, @PathVariable("repo") String repo);
    }
    // 3.调用示例
    @Autowired
    GitHub gitHub;
        
    List<Contributor>  contributors = gitHub.contributors("lakernote","easy-admin");
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    参考

    • https://www.infoq.cn/article/c9rk1mg0erk5mfqps4wz
  • 相关阅读:
    剑指 Offer II 068. 查找插入位置
    Redis核心设计原理(深入底层C源码)
    socket套接字——UDP协议
    实战PyQt5: 140-QChart图表之烛台图
    基于java+android+SQLite的保健型果饮在线销售APP设计
    MATLAB if...else...end 语句
    若依分割拼接图片地址
    JAVA毕业设计Vue网上书籍购买商城登录计算机源码+lw文档+系统+调试部署+数据库
    【Shell】Shell 脚本自动输入密码的三种方式
    Windows下Core Audio APIs的使用简介
  • 原文地址:https://blog.csdn.net/abu935009066/article/details/134016587