Spring整合Feign

Spring整合Feign

Spring Cloud对Feign进行了封装,我们通过一个例子,说明下如何在Spring中整合Feign。

测试案例

测试案例主要分为三个部分:

  1. spring-feign-server:Eureka服务器端项目,端口为8761,提供服务注册,查询功能
  2. spring-feign-provider:服务提供者。此项目支持通过指定不同的端口号启动多个不同的实例
  3. spring-feign-invoker:服务调用者。它会通过spring-feign-server提供的接口查询到spring-feign-provider提供的服务列表,发起调用,对外提供9000端口服务。

spring-feign-server

本项目的依赖为:

1
2
3
4
5
6
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka-server</artifactId>
</dependency>
</dependencies>

仅仅作为一个Eureka服务器运行,因此,不需要太复杂的逻辑,启动类如下所示:

1
2
3
4
5
6
7
8
9
@SpringBootApplication
@EnableEurekaServer
public class ServerApplication {

public static void main(String[] args) {
new SpringApplicationBuilder(ServerApplication.class).run(args);
}
}

配置文件如下:

1
2
3
4
5
6
server:
port: 8761
eureka:
client:
registerWithEureka: false
fetchRegistry: false

spring-feign-provider

项目的依赖为:

1
2
3
4
5
6
7
8
9
10
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
</dependencies>

对外提供了/hello和/person/{personId}两个REST服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@RestController
public class FirstController {

@RequestMapping(value = "/person/{personId}", method = RequestMethod.GET)
public Person findPerson(@PathVariable("personId") Integer personId, HttpServletRequest request) {
Person person = new Person(personId, "Crazyit", 30);
// 为了查看结果,将请求的URL设置到Person实例中
person.setMessage(request.getRequestURL().toString());
return person;
}

@RequestMapping(value = "/hello", method = RequestMethod.GET)
@ResponseBody
public String hello() {
return "Hello World";
}
}

Person类如下:

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
public class Person {

private Integer id;

private String name;

private Integer age;

private String message;

public Person() {
super();
}

public Person(Integer id, String name, Integer age) {
super();
this.id = id;
this.name = name;
this.age = age;
}

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Integer getAge() {
return age;
}

public void setAge(Integer age) {
this.age = age;
}

public String getMessage() {
return message;
}

public void setMessage(String message) {
this.message = message;
}

}

启动类如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
@SpringBootApplication
@EnableEurekaClient
public class ProviderApplication {

public static void main(String[] args) {
// 读取控制台输入的端口,避免端口冲突
Scanner scan = new Scanner(System.in);
String port = scan.nextLine();
new SpringApplicationBuilder(ProviderApplication.class).properties(
"server.port=" + port).run(args);
}
}

配置文件如下:

1
2
3
4
5
6
7
8
9
spring:
application:
name: spring-feign-provider
eureka:
instance:
hostname: localhost
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/

spring-feign-invoker

Invoker项目的依赖为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-ribbon</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-feign</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.18</version>
</dependency>
</dependencies>

它内部封装了调用provider提供的两个REST服务的逻辑,分别在HelloClient和PersonClient中

HelloClient

1
2
3
4
5
6
7
8
9
@FeignClient(name = "spring-feign-provider")
public interface HelloClient {

@MyUrl(method = "GET", url = "/hello")
String myHello();

@RequestMapping(method = RequestMethod.GET, value = "/hello")
String springHello();
}

PersonClient

1
2
3
4
5
6
7
8
9
10
@FeignClient("spring-feign-provider") //声明调用的服务名称
public interface PersonClient {

@RequestMapping(method = RequestMethod.GET, value = "/hello")
String hello();

@RequestMapping(method = RequestMethod.GET, value = "/person/{personId}")
Person getPerson(@PathVariable("personId") Integer personId);
}

Person

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
public class Person {

Integer id;
String name;
Integer age;
String message;

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Integer getAge() {
return age;
}

public void setAge(Integer age) {
this.age = age;
}

public String getMessage() {
return message;
}

public void setMessage(String message) {
this.message = message;
}
}

在两个XXXClient中,我们使用了注解FeignClient,并且声明了需要调用的服务名称。另外,我们还使用了Spring的注解RequestMapping,这意味着,我们需要一个翻译器Contract,让Feign知道Spring的这个注解的含义。

最后,除了@RequestMapping注解外,默认还支持@RequestParam、@RequestHeader和@PathVariable这三个参数注解。

需要注意的是,使用了Spring Cloud的“翻译器”后,将不能再使用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
public class MyContract extends SpringMvcContract {

/**
* 用于处理方法级的注解
*/
protected void processAnnotationOnMethod(MethodMetadata data,
Annotation annotation, Method method) {
// 调用父类的方法,吗时支持 @RequestMapping 注解
super.processAnnotationOnMethod(data, annotation, method);
// 是MyUrl注解才进行处理
if(MyUrl.class.isInstance(annotation)) {
// 获取注解的实例
MyUrl myUrlAnn = method.getAnnotation(MyUrl.class);
// 获取配置的HTTP方法
String httpMethod = myUrlAnn.method();
// 获取服务的url
String url = myUrlAnn.url();
// 将值设置到模板中
data.template().method(httpMethod);
data.template().append(url);
}
}
}

这边的翻译器继承了SpringMVCContract,并且重载了processAnnotationOnMethod方法,但是,在方法的最前面又调用了父类方法。这就意味着,它不仅支持翻译Spring的注解,也支持翻译自定义的MyUrl注解。

MyUrl

1
2
3
4
5
6
7
8
9
@Target(METHOD)
@Retention(RUNTIME)
public @interface MyUrl {

// 定义url与method属性
String url();
String method();
}

接着,我们过一下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
@RestController
@Configuration
public class InvokerController {

@Autowired
private PersonClient personClient;

@RequestMapping(value = "/invokeHello", method = RequestMethod.GET)
public String invokeHello() {
return personClient.hello();
}

@RequestMapping(value = "/router", method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public String router() {
// 调用服务提供者的接口
Person p = personClient.getPerson(2);
return p.getMessage();
}

@Autowired
private HelloClient helloClient;

@RequestMapping(value = "/testContract", method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public String testContract() {
String springResult = helloClient.springHello();
System.out.println("使用 @RequestMapping 注解的接口返回结果:" + springResult);
String myResult = helloClient.myHello();
System.out.println("使用 @MyUrl 注解的接口返回结果:" + myResult);
return "";
}

/**
* 测试请求拦截器
*/
@RequestMapping(value = "/testInterceptors", method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public String testInterceptors() {
String springResult = helloClient.springHello();
return springResult;
}
}

上面使用了@Autowired注解,注入了HelloClient和PersonClient。

配置文件

1
2
3
4
5
6
7
8
9
10
11
server:
port: 9000
spring:
application:
name: spring-feign-invoker
eureka:
instance:
hostname: localhost
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/

启动类

1
2
3
4
5
6
7
8
9
10
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
public class InvokerApplication {

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

启动

我们按照下面的顺序启动三个组件

  1. spring-feign-server
  2. spring-feign-provider
  3. spring-feign-invoker

启动完毕后,可以在Eureka的管理界面看到注册的三个服务

测试

可以在浏览器中输入http://localhost:9000/invokeHello或者http://localhost:9000/router

我们可以得到下面的结果

可以看到,SpringCloud提供的Feign客户端是具有负载均衡功能的。Spring Cloud实现的Feign客户端,类名为LoadBalancerFeignClient,在该类中维护者与SpringClientFactory相关的实例。通过SpringClientFactory可以获取负载均衡器,负载均衡器会根据一定的规则来选取处理请求的服务器,最终实现负载均衡功能。

我们还可以通过Spring的@Bean注解实现自定义配置

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
@Configuration
public class MyConfig {

/**
* 返回一个自定义的注解翻译器
*/
@Bean
public Contract feignContract() {
return new MyContract();
}

@Bean
public RequestInterceptor getRequestInterceptorsA() {
return new RequestInterceptor() {

public void apply(RequestTemplate template) {
System.out.println("这是第一个请求拦截器");
}
};
}

@Bean
public RequestInterceptor getRequestInterceptorsB() {
return new RequestInterceptor() {
public void apply(RequestTemplate template) {
System.out.println("这是第二个请求拦截器");
}
};
}
}

要实现自定义的“翻译器”或者拦截器,只要实现一个返回对应类型的方法,加上注解@Bean即可。比如,实现一个返回Contact的方法,用@Bean标记,就可以自定义“翻译器”了。拦截器也同样道理,具体可参见上面的代码。

参考资料

疯狂Spring Cloud微服务架构实战

0%