SpringCloud OpenFeign(服务调用)
1.定义
Feign是一个声明式的Web服务客户端,是面向接口编程的。也就是说使用Feign,只需要创建一个接口并使用注解方式配置它,就可以完成对微服务提供方的接口绑定。
OpenFeign对feign进行进一步的封装,添加了springmvc的一些功能,更加强大。
在使用RestTemplate时,每次调用服务都需要指定服务的具体路径,当在多个地方同时使用时要写多次,显得代码冗余也难以维护,而openfeign就可以避免这种操作。
2.项目实战
源码:https://github.com/zhongyushi-git/cloud-open-feign-demo.git
2.1基础环境搭建
这里使用consul作为服务注册中心。
1)创建一个maven工程名为cloud-open-feign-demo,删除src目录
2)在pom中导入依赖,对SpringBoot和SpringCloud版本进行锁定
 <properties>
        <spring.boot.version>2.2.2.RELEASE</spring.boot.version>
        <spring.cloud.version>Hoxton.SR1</spring.cloud.version>
        <lombok.version>1.16.18</lombok.version>
    </properties>
    <!--  依赖管理,父工程锁定版本-->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring.boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring.cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
           <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>${lombok.version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>
2.2搭建公共模块
1)新建maven子模块(common-api),导入依赖
    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>
2)创建实体对象
package com.zys.cloud.entity; import lombok.Data; @Data public class User { private String name; private String username; private Integer age; }
2.3搭建服务提供者
1)新建maven子模块(cloud-provider8001),导入依赖
     <dependencies>
        <!--web-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-consul-discovery</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
    </dependencies>
2)新建启动类ProviderMain8001并添加注解
package com.zys.cloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient
public class ProviderMain8001 {
    public static void main(String[] args) {
        SpringApplication.run(ProviderMain8001.class, args);
    }
}
3)配置application.yml
server:
  port: 8001
spring:
  application:
    name: cloud-consul-provider
  cloud:
    consul:
      host: localhost
      port: 8500
      discovery:
        service-name: ${spring.application.name}
4)新建controller接口
package com.zys.cloud.controller; import com.zys.cloud.entity.User; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.*; @RestController public class UserController { @Value("${server.port}") private String port; @GetMapping("/user/get") public String get() { return "我是服务提供者,端口:" + port; } @GetMapping("/user/getParam") public String getParam(@RequestParam("name") String name) { return "我是服务提供者,参数:" + name + ",端口:" + port; } @PostMapping("/user/postParam") public String postParam(@RequestParam("username") String username) { return "我是服务提供者,参数:" + username + ",端口:" + port; } @PostMapping("/user/add") public String addUser(@RequestBody User user) { return "我是服务提供者,参数:" + user.toString() + ",端口:" + port; } }
这里的接口并没有在类上使用@RequestMapping注解,而是把接口路径都写在方法上面,那么在服务调用方进行映射时直接复制其方法名即可,不需要方法体。
5)启动服务,可看到已注册到consul。
6)根据服务提供者ProviderMain8001,再创建ProviderMain8002.
2.4搭建服务消费者
1)新建maven子模块(cloud-consumer80),导入依赖
    <dependencies>
        <!--web-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-consul-discovery</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!--feign-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>com.zys.cloud</groupId>
            <artifactId>common-api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>
2)新建启动类ConsumerMain80并添加注解
package com.zys.cloud; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.openfeign.EnableFeignClients; @SpringBootApplication @EnableDiscoveryClient @EnableFeignClients class ConsumerMain80 { public static void main(String[] args) { SpringApplication.run(ConsumerMain80.class, args); } }
要在启动类上启用Feign客户端。
3)配置application.yml
server: port: 80 spring: application: name: cloud-consul-consumer cloud: consul: host: localhost port: 8500 discovery: service-name: ${spring.application.name}
3)创建服务接口UserServiceClient,对于服务提供者接口。需要添加注解@FeiginClient,指定微服务的名称
package com.zys.cloud.service; import com.zys.cloud.entity.User; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; //指定微服务名称 @FeignClient(value = "cloud-consul-provider") public interface UserServiceClient { @GetMapping("/user/get") String get(); @GetMapping("/user/getParam") String getParam(@RequestParam("name") String name); @PostMapping("/user/postParam") String postParam(@RequestParam("username") String username); @PostMapping("/user/add") String addUser(@RequestBody User user); }
注意:
- 必须使用注解@FeignClient指定服务提供方的服务名称
- 在编写接口映射时,可直接复制服务提供方的方法名等信息
- get请求传递参数时,要使用@RequestParam注解,其value是参数的名字,需与服务提供者端保持一致
- post请求传递参数时,当参数是对象时使用@RequestBody,当参数不是对象时需使用@RequestParam注解
4)创建controller接口,将UserServiceClient注入使用
package com.zys.cloud.controller; import com.zys.cloud.entity.User; import com.zys.cloud.service.UserServiceClient; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; @RestController @RequestMapping("/consumer") public class TestController { @Resource private UserServiceClient userServiceClient; @GetMapping("/get") public String get() { return userServiceClient.get(); } @GetMapping("/param") public String getParam(String name) { return userServiceClient.getParam(name); } @PostMapping("/post") public String postParam(String username) { return userServiceClient.postParam(username); } @PostMapping("/add") public String addUser(@RequestBody User user) { return userServiceClient.addUser(user); } }
5)启动测试。先启动服务提供者集群,再启动服务消费者,对四个接口进行测试,服务均可正常调用。
另外对其中一个接口进行多次刷新,会发现服务提供者集群是遵循轮询机制。原因是openfeignl默认已引入了Ribbon,可提供负载均衡策略。
2.5超时控制
为了体现服务快速响应的特点,Feign默认等待1秒,超过后报错。也就是说Feigin客户端只等待一秒,若服务端处理过程超过一秒,会导致客户端会出错,故需设置超时时间,避免出现这样的情况。
1)情景重现
为了看到这种效果,可在服务提供者的接口中设置线程阻塞,让其响应时间超过1S。这里以8001为例,修改UserController的get()方法:
@GetMapping("/user/get") public String get() { try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } return "我是服务提供者,端口:" + port; }
截图:

重启后测试这个接口,消费者控制台报错:

2)配置超时时间
在客户端(服务消费者80)的yml进行配置即可。配置有两种方式:
1)方式一:设置Ribbon的负载超时时间
#设置ribbon的负载超时时间,单位都是ms ribbon: #指的是建立连接所用的时间,适用于网络状况正常的情况下,两端连接所用的时间 ReadTimeout: 5000 #指的是建立连接后从服务器读取到可用资源所用的时间 ConnectTimeout: 5000
2)方式二:设置feign的超时时间
feign: client: config: #指定全局 default: #连接超时时间 connectTimeout: 5000 #服务等待时间 readTimeout: 5000
上述是配置全局的,也可对每个服务设置超时时间:

使用上述任意一种配置,配置后重启客户端后测试这个接口,服务正常调用,没有错误。
2.6日志打印
Feign对日志的处理非常灵活,可为每个Feign客户端指定日志记录策略。每个客户端都会创建一个Logger,默认情况下Feign的日志是Debug级别的,故不会显示。
2.6.1日志级别类型
NONE:默认的,不显示任何日志 BASIC:仅记录请求方法、URL、响应状态码及执行时间 HEADERS:BASIC信息以及请求和响应的头信息 FULL:HEADERS信息以及请求和响应的正文和元数据
2.6.2配置日志
配置日志有两种方式,二选一即可。
1)第一种方式:使用配置类+yml配置
第一步:在客户端(服务消费者80)添加配置类LogConfig,指定日志的级别
package com.zys.cloud.config; import feign.Logger; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; //配置feign日志级别 @Configuration public class LogConfig { @Bean Logger.Level feignLevel(){ return Logger.Level.FULL; } }
第二步:在yml配置指定调用的客户端的日志级别,必须是Debug级别
logging:
  level:
    com.zys.cloud.service.UserServiceClient: debug
2)第二种方式:全部使用yml配置
配置如下:
feign:
client:
config:
#指定全局
default:
loggerLevel: full
logging:
level:
com.zys.cloud.service.UserServiceClient: debug
当然,上述使用default指定了全局的级别,也可以对每个服务指定级别,把default换成服务名即可:

配置完成后重启客户端,调用四个接口的任意一个,在控制台可以看出多了很多的打印信息。

2.7文件上传
在使用openfeign进行文件上传时,需要特别注意,不能使用默认方式。
//import org.springframework.http.MediaType; //文件上传 @PostMapping(value = "/user/fileUpload",consumes = MediaType.MULTIPART_FORM_DATA_VALUE) String fileUpload(@RequestPart("file")MultipartFile file);
调用的关键代码如上。必须在请求中指定consumes的值是MediaType.MULTIPART_FORM_DATA_VALUE,另外传递的参数需要使用@RequestPart注解来修饰MultipartFile.
2.8请求通讯连接优化
OpenFeign底层通信组件默认使用的是JDK自带的 URLConnection 对象进行HTTP请求,而这种方式并没有使用连接池,对于性能来说不太友好。
查看feign的源码,在如下包中

通过对源码(feign.SynchronousMethodHandler#executeAndDecode())debug可以看到,默认的通信组件如下图:

而 OpenFeign支持替换通讯组件为专用通信组件,如Apache HttpClient或OKHttp ,因为这些专用通信组件自带连接池,能更好地对 HTTP 连接对象进行重用与管理,同时能大大的提升 HTTP 请求的效率。
下面以Apache HttpClient为例进行替换:
<dependency>
      <groupId>io.github.openfeign</groupId>
      <artifactId>feign-httpclient</artifactId>
</dependency>


3.面试题-Feign为什么第一次加载很慢?
4.面试题-Feign如何实现认证传递?
@Slf4j @Configuration public class FeignConfiguration implements RequestInterceptor { @Override public void apply(RequestTemplate requestTemplate) { Map<String,String> headers = getHeaders(getHttpServletRequest()); for(String headerName : headers.keySet()){ requestTemplate.header(headerName, getHeaders(getHttpServletRequest()).get(headerName)); } } private HttpServletRequest getHttpServletRequest() { try { return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); } catch (Exception e) { e.printStackTrace(); return null; } } private Map<String, String> getHeaders(HttpServletRequest request) { Map<String, String> map = new LinkedHashMap<>(); Enumeration<String> enumeration = request.getHeaderNames(); while (enumeration.hasMoreElements()) { String key = enumeration.nextElement(); String value = request.getHeader(key); map.put(key, value); } return map; } }

 
                
            
         浙公网安备 33010602011771号
浙公网安备 33010602011771号