一、OpenFeign介绍

  前面在使用Ribbon+RestTemplate时,利用RestTemplate对http请求的封装处理,形成了一套模版化的调用方法。但是在实际开发中,对于服务依赖的调用可能不止一处,往往一个接口会被多处调用。所有Feign在此基础上做了进一步封装,由他来帮助我们定义和实现依赖服务接口的定义。在Feign的实现下,我们只需创建一个接口并使用注解的方式来配置它(以前是Dao接口上面标注Mapper注解,现在是一个微服务接口上面标注一个Feign注解即可),集可完成对服务提供方的接口绑定,简化了使用Spring Cloud Ribbon时,自动封装服务调用客户端的开发量。

  Feign旨在式编写Java Http客户端变得更容易。

  Feign集成了Ribbon,利用Ribbon维护了服务列表信息,并且通过轮询实现了客户端的负载均衡。而与Ribbon不同的是,通过feign只需要定义服务绑定接口且以声明式的方法,优雅而简单的实现了服务调用

  git地址:https://github.com/spring-cloud/spring-cloud-openfeign

  Feign与OpenFeign的区别

  1)Feign是Spring Cloud组件中一个轻量级RESTful的HTTP服务客户端,Feign内置了Ribbon,用来做客户端负载均衡,去调用服务注册中心的服务。Feign的使用方式是:使用Feign的注解定义接口,调用接口,就可以调用服务注册中心的服务

二、OpenFeign使用

  • cloud-consumer-order8002

    pom.xml

    <dependencies>

        <!--open feign-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

        <!-- eureka client -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

    </dependencies>

    application.yml

# 端口
server:
  port: 8002
spring:
  application:
    name: cloud-order
  #安全认证
  security:
    user:
      name: root
      password: root
eureka:
  client:
    # 表示将自己注册进Eureka Server默认为true
    register-with-eureka: true
    # 是否从Eureka Server抓去已有的注册信息,默认是true
    fetch-registry: true
    # 设置与Eureka Server交互的地址查询服务和注册服务都需要依赖这个地址
    service-url:
      #设置同时向三个eureka注册
      defaultZone: http://${spring.security.user.name}:${spring.security.user.password}@eureka1.com:8887/eureka
  instance:
    #instance.getUri()获取到的就是这个值(默认为主机名称+端口 / 这里修改为IP地址+端口)
    instance-id: ${spring.cloud.client.ip-address}:${server.port}
    #访问路径是否显示IP地址
    prefer-ip-address: true
#开启优雅停服
#(必须通过POST请求向Eureka Client发起一个shutdown请求。请求路径为:http://ip:port/shutdown。可以通过任意技术实现,如:HTTPClient,AJAX等。)
management:
  endpoint:
    shutdown:
      enabled: true

    App

 1 package com.sdkj;
 2 
 3 import com.sdkj.myrule.rule2.MyBalancedRule;
 4 import org.springframework.boot.SpringApplication;
 5 import org.springframework.boot.autoconfigure.SpringBootApplication;
 6 import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
 7 import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
 8 import org.springframework.cloud.netflix.ribbon.RibbonClient;
 9 import org.springframework.cloud.openfeign.EnableFeignClients;
10 
11 /**
12  * @Author wangshuo
13  * @Date 2022/5/19, 19:46
14  * Please add a comment
15  */
16 //标识为eureka client端
17 @EnableEurekaClient/*注意这个是client和提供者server不一样*/
18 @SpringBootApplication(scanBasePackages = {"com.sdkj"})
19 //开启服务发现
20 @EnableDiscoveryClient
21 //开启feign客户端
22 @EnableFeignClients
23 //使用自定义的Ribbon规则
24 @RibbonClient(name = "CLOUD-PAYMENT-SERVICE", configuration = MyBalancedRule.class)
25 public class App {
26 
27     public static void main(String[] args) {
28         SpringApplication.run(App.class);
29     }
30 }

    PaymentFeignService

 1 package com.sdkj.service;
 2 
 3 import com.sdkj.response.CommonReturnType;
 4 import org.springframework.cloud.openfeign.FeignClient;
 5 import org.springframework.stereotype.Component;
 6 import org.springframework.web.bind.annotation.GetMapping;
 7 import org.springframework.web.bind.annotation.PathVariable;
 8 import org.springframework.web.bind.annotation.RequestMapping;
 9 import org.springframework.web.bind.annotation.ResponseBody;
10 
11 /**
12  * @Author wangshuo
13  * @Date 2022/5/23, 9:35
14  * 使用open feign调用消费者
15  */
16 @Component
17 @FeignClient("CLOUD-PAYMENT-SERVICE")
18 public interface PaymentFeignService {
19 
20     @GetMapping(value = "/payment/findById?id={id}")
21     public CommonReturnType findPaymentById(@PathVariable Long id);
22 }

    OrderController

 1 package com.sdkj.controller;
 2 
 3 import com.sdkj.myrule.rule3.LoadBalancer;
 4 import com.sdkj.response.CommonReturnType;
 5 import com.sdkj.service.PaymentFeignService;
 6 import org.springframework.beans.factory.annotation.Autowired;
 7 import org.springframework.cloud.client.ServiceInstance;
 8 import org.springframework.cloud.client.discovery.DiscoveryClient;
 9 import org.springframework.stereotype.Controller;
10 import org.springframework.web.bind.annotation.*;
11 import org.springframework.web.client.RestTemplate;
12 
13 import java.util.List;
14 
15 /**
16  * @Author wangshuo
17  * @Date 2022/5/19, 19:52
18  * Please add a comment
19  */
20 @Controller
21 @RequestMapping(value = "/order")
22 @CrossOrigin(origins = {"*"}, allowCredentials = "true")
23 public class OrderController {
24 
25     private static final String url = "http://CLOUD-PAYMENT-SERVICE";
26     @Autowired
27     private RestTemplate restTemplate;
28     @Autowired
29     private LoadBalancer loadBalancer;
30     @Autowired
31     private DiscoveryClient discoveryClient;
32     @Autowired
33     private PaymentFeignService paymentFeignService;
34 
35     @RequestMapping("/getPaymentById")
36     @ResponseBody
37     public CommonReturnType getById(Long id) {
38         //不要忘记把参数传过去
39         return restTemplate.getForObject(url + "/payment/findById/?id=" + id, CommonReturnType.class);
40     }
41 
42     @RequestMapping("/getPort")
43     @ResponseBody
44     public String getPort() {
45 
46         return restTemplate.getForObject(url + "/payment/getPort", String.class);
47     }
48 
49     @RequestMapping("/getLBPort")
50     @ResponseBody
51     public String getLbPort() {
52 
53         List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
54         if (instances == null || instances.size() == 0)
55             return null;
56         ServiceInstance instance = loadBalancer.getInstance(instances);
57         return restTemplate.getForObject(instance.getUri() + "/payment/getPort", String.class);
58     }
59 
60     //使用open feign
61     @GetMapping("/findPaymentById/{id}")
62     @ResponseBody
63     public CommonReturnType findPaymentById(@PathVariable Long id){
64 
65         return paymentFeignService.findPaymentById(id);
66     }
67 }
  • cloud-provider-payment8001 / cloud-provider-payment9001

    PaymentController

 1 package com.sdkj.controller;
 2 
 3 import com.sdkj.dataobject.PaymentDO;
 4 import com.sdkj.error.BusinessException;
 5 import com.sdkj.error.EnumBusinessError;
 6 import com.sdkj.response.CommonReturnType;
 7 import com.sdkj.service.PaymentService;
 8 import lombok.extern.slf4j.Slf4j;
 9 import org.apache.commons.lang.StringUtils;
10 import org.springframework.beans.factory.annotation.Autowired;
11 import org.springframework.beans.factory.annotation.Value;
12 import org.springframework.cloud.client.ServiceInstance;
13 import org.springframework.cloud.client.discovery.DiscoveryClient;
14 import org.springframework.stereotype.Controller;
15 import org.springframework.web.bind.annotation.*;
16 
17 /**
18  * @Author wangshuo
19  * @Date 2022/5/19, 9:08
20  * Please add a comment
21  */
22 @Controller
23 @RequestMapping(value = "/payment")
24 @CrossOrigin(origins = {"*"}, allowCredentials = "true")
25 @Slf4j
26 public class PaymentController extends BaseController {
27 
28     @Autowired
29     PaymentService paymentService;
30     //注入服务发现
31     @Autowired
32     DiscoveryClient discoveryClient;
33     @Value("${server.port}")
34     private String port;
35 
36     @RequestMapping("/findById")
37     @ResponseBody
38     public CommonReturnType findById(Long id) throws BusinessException {
39 
40         if (StringUtils.isEmpty(id.toString()))
41             throw new BusinessException(EnumBusinessError.REGISTER_OTP_ERROR);//参数不合法
42         PaymentDO payment = paymentService.getPaymentById(id);
43         return CommonReturnType.create(payment);
44     }
45 
46     @RequestMapping(value = "/getDiscovery")
47     @ResponseBody
48     public Object getDiscovery() {
49 
50         //获取服务列表
51         for (String service : discoveryClient.getServices()) {
52             log.info("发现service:" + service);
53         }
54         //获取服务实例集合
55         for (ServiceInstance instance : discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE")) {
56             log.info("发现服务CLOUD-PAYMENT-SERVICE:" +
57                     " serviceId = " + instance.getServiceId() +
58                     " host = " + instance.getHost() +
59                     " port = " + instance.getPort() +
60                     " uri = " + instance.getUri());
61         }
62         return this.discoveryClient;
63     }
64 
65     @RequestMapping("/getPort")
66     @ResponseBody
67     public String getPort() {
68         return "server port = " + port;
69     }
70 }

 三、设置logback日志与feign日志

  • application.yml

# 端口
server:
  port: 8002
spring:
  application:
    name: cloud-order
  #安全认证
  security:
    user:
      name: root
      password: root
eureka:
  client:
    # 表示将自己注册进Eureka Server默认为true
    register-with-eureka: true
    # 是否从Eureka Server抓去已有的注册信息,默认是true
    fetch-registry: true
    # 设置与Eureka Server交互的地址查询服务和注册服务都需要依赖这个地址
    service-url:
      #设置同时向三个eureka注册
      defaultZone: http://${spring.security.user.name}:${spring.security.user.password}@eureka1.com:8887/eureka
  instance:
    #instance.getUri()获取到的就是这个值(默认为主机名称+端口 / 这里修改为IP地址+端口)
    instance-id: ${spring.cloud.client.ip-address}:${server.port}
    #访问路径是否显示IP地址
    prefer-ip-address: true
#开启优雅停服
#(必须通过POST请求向Eureka Client发起一个shutdown请求。请求路径为:http://ip:port/shutdown。可以通过任意技术实现,如:HTTPClient,AJAX等。)
management:
  endpoint:
    shutdown:
      enabled: true
#logback
logging:
  config: classpath:log/logback.xml
  • logback.xml

<configuration>
    <!--本文主要输出日志为控制台日志,系统日志,sql日志,异常日志-->
    <!-- %m输出的信息,%p日志级别,%t线程名,%d日期,%c类的全名,,,, -->
    <!--控制台-->
    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d %p (%file:%line\)- %m%n</pattern>
            <charset>UTF-8</charset>
        </encoder>
    </appender>

    <!--系统info级别日志-->
    <!--<File> 日志目录,没有会自动创建-->
    <!--<rollingPolicy>日志策略,每天简历一个日志文件,或者当天日志文件超过64MB时-->
    <!--encoder 日志编码及输出格式-->
    <appender name="fileLog"
              class="ch.qos.logback.core.rolling.RollingFileAppender">
        <File>log/file/fileLog.log</File>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>log/file/fileLog.log.%d.%i</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <!-- or whenever the file size reaches 64 MB -->
                <maxFileSize>64 MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
        </rollingPolicy>
        <encoder>
            <pattern>
                %d %p (%file:%line\)- %m%n
            </pattern>
            <charset>UTF-8</charset>
            <!-- 此处设置字符集 -->
        </encoder>
    </appender>

    <!--sql日志-->
    <appender name="sqlFile"
              class="ch.qos.logback.core.rolling.RollingFileAppender">
        <File>log/sql/sqlFile.log</File>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>log/sql/sqlFile.log.%d.%i</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <!-- or whenever the file size reaches 64 MB -->
                <maxFileSize>64 MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
        </rollingPolicy>
        <!--对记录事件进行格式化。负责两件事,一是把日志信息转换成字节数组,二是把字节数组写入到输出流。-->
        <encoder>
            <!--用来设置日志的输入格式-->
            <pattern>
                %d %p (%file:%line\)- %m%n
            </pattern>
            <charset>UTF-8</charset>
            <!-- 此处设置字符集 -->
        </encoder>
    </appender>


    <!--异常日志-->
    <appender name="errorFile"
              class="ch.qos.logback.core.rolling.RollingFileAppender">
        <File>log/error/errorFile.log</File>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>log/error/errorFile.%d.log.%i</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <!-- or whenever the file size reaches 64 MB -->
                <maxFileSize>64 MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
        </rollingPolicy>
        <!--对记录事件进行格式化。负责两件事,一是把日志信息转换成字节数组,二是把字节数组写入到输出流。-->
        <encoder>
            <!--用来设置日志的输入格式-->
            <pattern>
                %d %p (%file:%line\)- %m%n
            </pattern>
            <charset>UTF-8</charset>
            <!-- 此处设置字符集 -->
        </encoder>
        <!--
            日志都在这里 过滤出 error
            使用 try {}catch (Exception e){} 的话异常无法写入日志,可以在catch里用logger.error()方法手动写入日志
            -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>ERROR</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>
    <!--  日志输出级别 -->
    <!--All\DEBUG\INFO\WARN\ERROR\FATAL\OFF-->
    <!--打印info级别日志,分别在控制台,fileLog,errorFile输出
        异常日志在上面由过滤器过滤出ERROR日志打印
    -->
    <root level="INFO">
        <appender-ref ref="fileLog" />
        <appender-ref ref="console" />
        <appender-ref ref="errorFile" />
    </root>

    <!--打印sql至sqlFile文件日志-->
    <logger name="com.dolphin.mapper" level="DEBUG" additivity="false">
        <appender-ref ref="console" />
        <appender-ref ref="sqlFile" />
    </logger>
</configuration>
  • FeignConfig

 1 package com.sdkj.config;
 2 
 3 import feign.Logger;
 4 import org.springframework.context.annotation.Bean;
 5 import org.springframework.context.annotation.Configuration;
 6 
 7 /**
 8  * @Author wangshuo
 9  * @Date 2022/5/23, 19:08
10  * 配置feign日志级别
11  */
12 @Configuration
13 public class FeignConfig {
14 
15     @Bean
16     public Logger.Level feignLoggerLevel() {
17         /*
18             Feign有一下日志级别:
19             NONE:默认的,不显示任何日志
20             BASIC:仅记录请求方法、URL、响应状态码及执行时间
21             HEADERS:出了BASIC中定义的信息之外,还有请求和响应的头信息
22             FULL:除了HEADERS中定义的信息之外,还有请求和响应的正文及元素
23          */
24         return Logger.Level.BASIC;
25     }
26 }