Spring Cloud 入门之Feign&OpenFeign

8/18/2021 Spring Cloud

# Feign & OpenFeign

# Feign简介

OpenFeign是Netflix 开发的声明式、模板化的HTTP请求客户端。可以更加便捷、优雅地调用http api。

OpenFeign会根据带有注解的函数信息构建出网络请求的模板,在发送网络请求之前,OpenFeign会将函数的参数值设置到这些请求模板中。

feign主要是构建微服务消费端。只要使用OpenFeign提供的注解修饰定义网络请求的接口类,就可以使用该接口的实例发送RESTful的网络请求。还可以集成Ribbon和Hystrix,提供负载均衡和断路器。

英文表意为“假装,伪装,变形”, 是一个 Http 请求调用的轻量级框架,可以以 Java 接口注解的方式调用 Http 请求,而不用像 Java 中通过封装 HTTP 请求报文的方式直接调用。通过处理注解,将请求模板化,当实际调用的时候,传入参数,根据参数再应用到请求上,进而转化成真正的请求,这种请求相对而言比较直观。Feign 封装 了HTTP 调用流程,面向接口编程,回想第一节课的SOP。

# Feign和OpenFeign的关系

Feign本身不支持Spring MVC的注解,它有一套自己的注解

OpenFeign是Spring Cloud 在Feign的基础上支持了Spring MVC的注解,如@RequesMapping等等。 OpenFeign的@FeignClient可以解析SpringMVC的@RequestMapping注解下的接口, 并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务。

# 声明式服务调用

provider方提供公用API包,Feign通过SpringMVC的注解来加载URI

# 1. 创建项目User-Provider

# 依赖

  1. Spring Web
  2. Eureka DIscovery Client

# 2. 创建项目User-API

# 依赖

  1. spring-boot-starter-web

# 创建一个接口 UserApi

package xyz.yowei.userapi;

import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;

//@RequestMapping("/User")
public interface UserApi {

    @GetMapping("/User/alive")
    public String alive();

    @GetMapping("/User/getById")
    public String getById(@RequestParam Integer id);

    @PostMapping("/User/postPerson")
    public Person postPerson(@RequestBody Person person);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 3. User-Provider 实现API

# 配置文件

eureka.client.service-url.defaultZone=http://euk1.com:7001/eureka/
server.port=80
spring.application.name=user-provider
1
2
3

# 引入API

User-Provider的Pom.xml添加依赖

		<dependency>
				<groupId>xyz.yowei.User-API</groupId>
				<artifactId>User-API</artifactId>
				<version>0.0.1-SNAPSHOT</version>
		</dependency>
1
2
3
4
5

# 创建UserController

实现Api的接口

package xyz.yowei.UserProvider;

import xyz.yowei.UserAPI.UserApi;
@RestController
public class UserController implements UserApi {

	@Override
	public String isAlive() {
		// TODO Auto-generated method stub
		return "ok";
	}

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 4.创建项目User-Consumer

# 依赖

  1. Spring Web
  2. Eureka Discovery Client
  3. OpenFeign

# 引入API

Pom.xml添加依赖

<dependency>
		<groupId>xyz.yowei.User-API</groupId>
		<artifactId>User-API</artifactId>
		<version>0.0.1-SNAPSHOT</version>
</dependency>
1
2
3
4
5

# 配置文件

eureka.client.service-url.defaultZone=http://euk1.com:7001/eureka/

server.port=90

spring.application.name=consumer
1
2
3
4
5

# 创建Service接口

package xyz.yowei.UserConsumer;

import org.springframework.cloud.openfeign.FeignClient;

import xyz.yowei.userapi.UserApi;

@FeignClient(name = "user-provider")
public interface UserConsumerService extends UserApi {

}

1
2
3
4
5
6
7
8
9
10
11

# 创建Controller

package xyz.yowei.UserConsumer;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ConsumerController {

	@Autowired
	UserConsumerService consumerSrv;

	@GetMapping("/alive")
	public String alive() {

		return consumerSrv.isAlive();
	}

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 修改启动类

package xyz.yowei.UserConsumer;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableFeignClients
public class UserConsumerApplication {

	public static void main(String[] args) {
		SpringApplication.run(UserConsumerApplication.class, args);
	}

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 5.测试

访问 http://localhost:90/alive 即可完成声明式远程服务调用

# Get和Post

Feign默认所有带参数的请求都是Post,想要使用指定的提交方式需引入依赖

<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-httpclient</artifactId>
</dependency>
1
2
3
4

并指明提交方式

@RequestMapping(value = "/alived", method = RequestMethod.POST)
@GetMapping("/findById")
1
2

# 带参请求

	@GetMapping("/findById")
	public Map findById(@RequestParam("id") Integer id);

	@PostMapping("/register")
	public Map<String, String> reg(@RequestBody User user);
1
2
3
4
5

# 权限

feign的默认配置类是:org.springframework.cloud.openfeign.FeignClientsConfiguration。默认定义了feign使用的编码器,解码器等。

允许使用@FeignClient的configuration的属性自定义Feign配置。自定义的配置优先级高于上面的FeignClientsConfiguration。

通过权限的例子,学习feign的自定义配置。

服务提供者。上述例子开放service-valuation的权限 后,访问。

  • 添加依赖
开放权限:
<!-- 安全认证 -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-security</artifactId>
</dependency>
1
2
3
4
5
6
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
      // 关闭csrf
      http.csrf().disable();
      // 表示所有的访问都必须认证,认证处理后才可以正常进行
      http.httpBasic().and().authorizeRequests().anyRequest().fullyAuthenticated();
      // 所有的rest服务一定要设置为无状态,以提升操作效率和性能
      http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
spring:
  security:
    user:
      name: root
      password: root
1
2
3
4
5

继续feign原来访问,报错。401。

有如下两种方式:

  1. 自定义配置类。
  2. 增加拦截器。

自定义配置

// 配置类:
public class FeignAuthConfiguration {

	@Bean
	public BasicAuthRequestInterceptor basicAuthRequestInterceptor() {
		return new BasicAuthRequestInterceptor("root", "root");
	}
}

//在feign上加配置
@FeignClient(name = "service-valuation",configuration = FeignAuthConfiguration.class)


1
2
3
4
5
6
7
8
9
10
11
12
13

OK,可以正常访问了。

小结:如果在配置类上添加了@Configuration注解,并且该类在@ComponentScan所扫描的包中,那么该类中的配置信息就会被所有的@FeignClient共享。最佳实践是:不指定@Configuration注解(或者指定configuration,用注解忽略),而是手动:

@FeignClient(name = "service-valuation",configuration = FeignAuthConfiguration.class)

拦截器

import feign.RequestInterceptor;
import feign.RequestTemplate;

public class MyBasicAuthRequestInterceptor implements RequestInterceptor {

	@Override
	public void apply(RequestTemplate template) {
		// TODO Auto-generated method stub
		template.header("Authorization", "Basic cm9vdDpyb290");
	}
}

feign:
  client:
    config:
      service-valuation:

        request-interceptors:
        - com.online.taxi.passenger.feign.interceptor.MyBasicAuthRequestInterceptor
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

代码中取消上面的配置,访问,报401.用下面的方式。

# 属性定义

  1. 接上面例子,此例子和上面例子实现的功能一样。记得两者取一个即可。说明用属性而不是用属性中的configuration。
定义拦截器
public class MyBasicAuthRequestInterceptor implements RequestInterceptor {

	@Override
	public void apply(RequestTemplate template) {
		// TODO Auto-generated method stub
		template.header("Authorization", "Basic cm9vdDpyb290");
	}
}

配置文件
feign:
  client:
    config:
      service-valuation:
        request-interceptors:
        - com.online.taxi.passenger.feign.interceptor.MyBasicAuthRequestInterceptor


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

再次访问,测试Ok。

  1. 扩展

指定服务名称配置:

   feign:
     client:
       config:
         service-valuation:
           connect-timeout: 5000
           read-timeout: 5000
           logger-level: full

1
2
3
4
5
6
7
8

通用配置

   feign:
     client:
       config:
         default:
           connect-timeout: 5000
           read-timeout: 5000
           logger-level: full
1
2
3
4
5
6
7

属性配置比Java代码优先级高。也可通过配置设置java代码优先级高。

feign:
	client:
		default-to-properties: false
1
2
3

feign在方法上可以设置:@RequestMapping,@ResponseBody。

方法中的参数可以设置:@RequestBody等等,Spring MVC中的注解。

推荐使用yml配置方式,在yml中按 代码提示键,可以看到所有配置。

# 原理

  1. 主程序入口添加@EnableFeignClients注解开启对Feign Client扫描加载处理。根据Feign Client的开发规范,定义接口并加@FeignClient注解。
  2. 当程序启动时,会进行包扫描,扫描所有@FeignClient注解的类,并将这些信息注入Spring IoC容器中。当定义的Feign接口中的方法被调用时,通过JDK的代理方式,来生成具体的RequestTemplate。当生成代理时,Feign会为每个接口方法创建一个RequestTemplate对象,该对象封装了HTTP请求需要的全部信息,如请求参数名、请求方法等信息都在这个过程中确定。
  3. 然后由RequestTemplate生成Request,然后把这个Request交给client处理,这里指的Client可以是JDK原生的URLConnection、Apache的Http Client,也可以是Okhttp。最后Client被封装到LoadBalanceClient类,这个类结合Ribbon负载均衡发起服务之间的调用。

# 压缩

服务端provider配置

#服务端开启压缩
server.compression.enabled=true
1
2

调用方consumer配置

#配置请求GZIP压缩
feign.compression.request.enabled=true
#配置响应GZIP压缩
feign.compression.response.enabled=true
#单位是B
feign.compression.request.min-request-size=100
1
2
3
4
5
6

# 自定义接口

# 请求

# API

@FeignClient(name = "user-provider")
public interface ConsumerApi extends UserApi {

	@GetMapping("/getMap")
	Map<Integer, String> getMap(@RequestParam("id") Integer id);
	@GetMapping("/getMap2")
	Map<Integer, String> getMap2(@RequestParam("id") Integer id,@RequestParam("name") String name);

	@GetMapping("/getMap3")
	Map<Integer, String> getMap3(@RequestParam Map<String, Object> map);

	@PostMapping("/postMap")
	Map<Integer, String> postMap(Map<String, Object> map);

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# Controller

package xyz.yowei.UserConsumer;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import xyz.yowei.UserAPI.UserApi;

@RestController
public class MainController {

	@Autowired
	ConsumerApi api;

	@Autowired
	MashibingApi mapi;

	@GetMapping("/alive")
	public String alive() {
		/**
		 * URL 不能变
		 *
		 * jar
		 * 文档
		 */
		return api.alive();
	}


	@GetMapping("/vip")
	public String vip() {

		return mapi.getVip();
	}

	@GetMapping("/map")
	public Map<Integer, String> map(Integer id) {
		System.out.println(id);
		return api.getMap(id);
	}

	@GetMapping("/map2")
	public Map<Integer, String> map2(Integer id,String name) {
		System.out.println(id);
		return api.getMap2(id,name);
	}


	@GetMapping("/map3")
	public Map<Integer, String> map3(@RequestParam Map<String, Object> map) {
//		System.out.println(id);
//		HashMap<String, Object> map = new HashMap<>(2);
//
//		map.put("id", id);
//		map.put("name", name);
//		syso
		System.out.println(map);
		return api.getMap3(map);
	}


	@GetMapping("/map4")
	public Map<Integer, String> map4(@RequestParam Map<String, Object> map) {
//		System.out.println(id);
//		HashMap<String, Object> map = new HashMap<>(2);
//
//		map.put("id", id);
//		map.put("name", name);
//		syso
		System.out.println(map);
		return api.postMap(map);
	}
}

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

# Provider

package xyz.yowei.UserProvider;

import java.util.Collections;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

import org.springframework.beans.factory.annotation.Value;
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;
import org.springframework.web.bind.annotation.RestController;

import xyz.yowei.UserAPI.UserApi;

@RestController
public class UserController implements UserApi {

	@Value("${server.port}")
	String port;


	private AtomicInteger count = new AtomicInteger();

	@Override
	public String alive() {

		try {
			System.out.println("准备睡");

			Thread.sleep(500);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

		int i = count.getAndIncrement();
		System.out.println("====好的第:" + i + "次调用");
		return "port:" + port;
	}

	@Override
	public String getById(Integer id) {
		// TODO Auto-generated method stub
		return null;
	}

	@GetMapping("/getMap")
	public Map<Integer, String> getMap(@RequestParam("id") Integer id) {
		// TODO Auto-generated method stub
		System.out.println(id);
		return Collections.singletonMap(id, "mmeme");
	}
	@GetMapping("/getMap2")
	public Map<Integer, String> getMap2(Integer id,String name) {
		// TODO Auto-generated method stub
		System.out.println(id);
		return Collections.singletonMap(id, name);
	}

	@GetMapping("/getMap3")
	public Map<Integer, String> getMap3(@RequestParam Map<String, Object> map) {
		// TODO Auto-generated method stub
		System.out.println(map);
		return Collections.singletonMap(Integer.parseInt(map.get("id").toString()), map.get("name").toString());
	}


	@PostMapping("/postMap")
	public Map<Integer, String> postMap(@RequestBody Map<String, Object> map) {
		// TODO Auto-generated method stub
		System.out.println(map);
		return Collections.singletonMap(Integer.parseInt(map.get("id").toString()), map.get("name").toString());
	}
}

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

# 开启日志

# 配置文件

logging.level.xyz.yowei.UserConsumer:debug
1

# 重写日志等级

package xyz.yowei.UserConsumer;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import feign.Logger;

@Configuration
public class FeiginConfig {

	@Bean
	Logger.Level logLevel(){

		return Logger.Level.BASIC;
	}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 超时

Feign默认支持Ribbon;Ribbon的重试机制和Feign的重试机制有冲突,所以源码中默认关闭Feign的重试机制,使用Ribbon的重试机制

#连接超时时间(ms)
ribbon.ConnectTimeout=1000
#业务逻辑超时时间(ms)
ribbon.ReadTimeout=6000
1
2
3
4

# 重试

#同一台实例最大重试次数,不包括首次调用
ribbon.MaxAutoRetries=1
#重试负载均衡其他的实例最大重试次数,不包括首次调用
ribbon.MaxAutoRetriesNextServer=1
#是否所有操作都重试
ribbon.OkToRetryOnAllOperations=false
1
2
3
4
5
6

加了OkToRetryOnAllOperations这个配置上面超时时间不生效

使用ribbon重试机制,请求失败后,每个6秒会重新尝试