Spring Cloud 之 Feign (Finchley版)

Feign是轻量级、声明式的Http请求客户端,它吸收了来自的Retrofit JAXRS-2.0和WebSocket的灵感,为了使写Http请求变得更容易而诞生

Feign一开始作为Eureka的子项目,用于简化Http请求。但由于其不断完善,目前作为一个轻量级、声明式的Http请求客户端项目,独立维护。在Spring Cloud中,其引入了Feign,并提供了一系列默认的配置与Spring MVC注解的支持。因此,Feign一直被作为首先的Http请求客户端。

准备工作

需要Eureka Server,这部分看之前的文档部署,这篇以及以后的文章不在多提

搭建基础的Eureka Client,给Feign Clinet调用

引入web相关依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!-- 为了方便引入了lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>

编写Curl Controller

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
@Slf4j
@RestController
public class CurlController {

private static ObjectMapper objectMapper;

@GetMapping("/curl/{id}")
public Mono<String> get(@PathVariable Long id){
return Mono.just(id).map(item -> "Your id is " + item);
}

@GetMapping("/curl")
public Mono<String> get(@RequestParam MultiValueMap<String,String> queryParams){
return Mono.just(queryParams).map(item -> "Your QueryParams is " + item);
}

@PostMapping("/curl")
@ResponseStatus(HttpStatus.CREATED)
public Mono<String> post(@RequestBody Mono<User> user){
return user.map(CurlController::toJson).map(item -> "Your Body is " + item);
}

@PutMapping("/curl")
public Mono<String> put(@RequestBody Mono<User> user){
return post(user);
}

@DeleteMapping("/curl")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete() {
throw new RuntimeException("Fail");
}

private static ObjectMapper getObjectMapper(){
if (objectMapper == null) {
objectMapper = new ObjectMapper();
}
return objectMapper;
}
private static String toJson(Object object) {
ObjectMapper mapper = getObjectMapper();
try {
return mapper.writeValueAsString(object);
} catch (Exception e) {
log.error(e.getMessage());
}
return null;
}
}

编写测试用例

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
@RunWith(SpringRunner.class)
@SpringBootTest
public class CurlControllerTest {

@Autowired
private ApplicationContext context;

private WebTestClient client;

@Before
public void setUp() {
client = WebTestClient.bindToApplicationContext(context).build();
}

@Test
public void get() {

client.get().uri("/curl/1")
.exchange()
.expectStatus().isOk()
.expectBody(String.class).isEqualTo("Your id is 1");

client.get().uri("/curl?name=Jone Test")
.exchange()
.expectStatus().isOk()
.expectBody(String.class).isEqualTo("Your QueryParams is {name=[Jone Test]}");

}

@Test
public void post() {
client.post().uri("/curl")
.contentType(MediaType.APPLICATION_JSON)
.syncBody(User.of("Jone Po",25,1))
.exchange()
.expectStatus().isCreated()
.expectBody(String.class).isEqualTo("Your Body is {\"name\":\"Jone Po\",\"age\":25,\"sex\":1}");
}

@Test
public void put() {
client.put().uri("/curl")
.contentType(MediaType.APPLICATION_JSON)
.syncBody(User.of("Jone Po",25,1))
.exchange()
.expectStatus().isOk()
.expectBody(String.class).isEqualTo("Your Body is {\"name\":\"Jone Po\",\"age\":25,\"sex\":1}");
}

@Test
public void delete() {
client.delete().uri("/curl").exchange().expectStatus().isNoContent();
}
}

测试接口

1
2
[ERROR] Tests run: 4, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 0.204 s <<< FAILURE! - in com.jtj.cloud.baseclient.CurlControllerTest
[ERROR] delete(com.jtj.cloud.baseclient.CurlControllerTest) Time elapsed: 0.077 s <<< FAILURE!

测试接口如预期,成功3个,失败1个(删除接口)

Feign服务

相对于基础的客户端,多引入Feign依赖,并启用Feign

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
1
2
3
4
5
@EnableFeignClients
@SpringCloudApplication
public class FeignClientApplication {
//...
}

编写声明式的Feign接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@FeignClient(value = "base-client")
public interface BaseClient {

@GetMapping("/")
String getBaseClientData();

@GetMapping("/curl/{id}")
String getUser(@PathVariable("id") Long id);

@GetMapping("/curl")
String getUser(@RequestParam Map<String,String> query);

@PostMapping("/curl")
String postUser(@RequestBody User user);

@PutMapping("/curl")
String putUser(@RequestBody User user);

@DeleteMapping("/curl")
void deleteUser();

}

该接口注解与Spring MVC的基本一致(@PathVariable不能省略value值)
其中@FeignClient定义该接口实例化为Feign服务并注入到Spring中,由Spring管理,value值为配置文件中微服务的名称(spring.application.name的值)

调用Feign服务

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

@Resource
private BaseClient baseClient;


@GetMapping("/base/curl")
public Map<String,String> getBaseClientCurl(){
Map<String,String> result = new HashMap<>();

result.put("ID 1",baseClient.getUser(1L));
Map<String,String> query = new HashMap<>();

query.put("name","Jone Taki");
result.put("Query Jone",baseClient.getUser(query));

result.put("Post",baseClient.postUser(User.of("Jone Tiki",20,1)));
result.put("Put",baseClient.postUser(User.of("Jone Kolo",30,1)));

try {
baseClient.deleteUser();
result.put("Delete","success");
} catch (RuntimeException e) {
result.put("Delete","fail: " + e.getMessage());
}

return result;
}

访问接口测试

我们能得到如下结果,其中Delete是失败的

1
2
3
4
5
6
7
{
"Delete":"fail: BaseClient#deleteUser() failed and no fallback available.",
"Query Jone":"Your QueryParams is {name=[Jone Taki]}",
"Post":"Your Body is {\"name\":\"Jone Tiki\",\"age\":20,\"sex\":1}",
"ID 1":"Your id is 1",
"Put":"Your Body is {\"name\":\"Jone Kolo\",\"age\":30,\"sex\":1}"
}

上述这些例子包含了基本的REST操作,也就是Feign的基本使用

参考