Spring Cloud-12-Feign入门

前面,我们使用了Ribbon,RestTemplate,Hystrix这些组件,都是spring cloud的一些非常基础的组件,在使用过程中,这些东西都是同时出现的,而且配置也差不多,每次开发都有很多相同的代码,因此Spring Cloud基于Netflix Feign整合了Ribbon和Hystrix,让开发更简化,同时,还提供了一种声明式的Web服务客户端定义方式.

之前我们,为了简化RestTemplate操作,我们把请求都封装在了service中,但同时也发现,service中的方法都是模板式的,写起来很枯燥,Spring Cloud Feign对此进行了进一步的封装,简化了我们的封装操作,接下来就来看一下具体是怎么使用的.

Spring Cloud Feign入门

环境搭建

新建工程

还是在原来项目的基础上再创建一个子项目,名称是consumer-feign.

添加依赖

把项目加到主项目下,然后引入以下几个依赖:

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

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

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

启动类添加注解

启动类添加@EnableFeignClients注解,表示开启Spring Cloud Feign的功能, 如下:

1
2
3
4
5
6
7
8
9
10
11
12
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class App {

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

}

配置修改

修改application.yml,配置服务注册中心,端口号和应用名称. 内容如下:

1
2
3
4
5
6
7
8
9
spring:
application:
name: feign-consumer
server:
port: 2101
eureka:
client:
serviceUrl:
defaultZone: http://peer1:1111/eureka/,http://peer2:1112/eureka/

声明服务

写一个HelloService接口,通过@FeignClient注解来指定服务名绑定服务,然后通过Spring MVC提供的注解来绑定服务提供者提供的接口.

来看一下HelloService的代码:

1
2
3
4
5
6
7
@FeignClient("eureka-client")
public interface HelloService {

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

}

这里要看一下@FeignClient("eureka-client"),这个注解的意思就是,绑定了一个名称是eureka-client的服务提供者,这个eureka-client大小写无所谓, 然后下面有一个hello方法, 就是说绑定了名字是eureka-client的服务提供者的hello接口.

这里绑定了hello接口,也就是说,在服务提供者中,需要有一个hello接口, 所以在服务提供者中新增以下接口.

1
2
3
4
@GetMapping("/hello")
public String hello(){
return "hello";
}

Controller调用

在消费者中,调用刚才HelloService中的接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
@RestController
@Slf4j
public class FeignController {

@Autowired
private HelloService helloService;

@GetMapping("/hello")
public String hello(){
return helloService.hello();
}

}

测试

上面的操作都ok以后,依次启动服务注册中心,服务提供者,和刚刚创建的consumer-feign, 然后访问http://localhost:2101/hello, 可以看到返回了hello

Feign具备了Ribbon和Hystrix的功能,使用起来更加简单方便.

参数传递

上面的这个例子中,只是调用了个接口,没有任何参数的传递,在实际开发过程中,会有各种各样的参数传递,那接下来就来看一下,参数传递要怎么实现:

服务提供者修改

在服务提供者中添加三个接口,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 @GetMapping("/hello1")
public String hello1(@RequestParam String name) {
return "name:" + name;
}

@GetMapping("/hello2")
public User hello2(@RequestHeader String name, @RequestHeader String job, @RequestHeader Long id) throws UnsupportedEncodingException {
return new User(id, URLDecoder.decode(name,"UTF-8"), URLDecoder.decode(job,"UTF-8"));
}

@PostMapping("/hello3")
public String hello3(@RequestBody User user) {
return user.getName();
}

hello1,就是接收一个String类型的参数,然后再返回去.

hello2,是接收请求头中的参数,在请求头中传递中文参数,会乱码,所以就要传过来的时候编码,接收以后再解码.

hello3,是接收了一个body,一个user对象.

消费者修改

再来看一下我们的consumer-feign这个消费者工程需要怎么修改, 具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@FeignClient("eureka-client")
public interface HelloService {

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

@GetMapping("/hello1")
String hello(@RequestParam("name") String name);

@GetMapping("/hello2")
User hello(@RequestHeader("name") String name, @RequestHeader("job") String job, @RequestHeader("id") Long id);

@PostMapping("/hello3")
String hello(@RequestBody User user);
}

这里要注意一下, 在服务提供者的接口中的@RequestParam@RequestHeader是没有指定value的,Spring MVC中,默认会使用参数名作为注解的value, 但是在Feign中,必须指定value,否则会报以下错误:

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
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'feignController': Unsatisfied dependency expressed through field 'helloService'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'com.eureka.consumer.service.HelloService': FactoryBean threw exception on object creation; nested exception is java.lang.IllegalStateException: RequestHeader.value() was empty on parameter 0
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:588) ~[spring-beans-4.3.9.RELEASE.jar:4.3.9.RELEASE]
at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:88) ~[spring-beans-4.3.9.RELEASE.jar:4.3.9.RELEASE]
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessPropertyValues(AutowiredAnnotationBeanPostProcessor.java:366) ~[spring-beans-4.3.9.RELEASE.jar:4.3.9.RELEASE]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1264) ~[spring-beans-4.3.9.RELEASE.jar:4.3.9.RELEASE]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:553) ~[spring-beans-4.3.9.RELEASE.jar:4.3.9.RELEASE]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:483) ~[spring-beans-4.3.9.RELEASE.jar:4.3.9.RELEASE]
at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:306) ~[spring-beans-4.3.9.RELEASE.jar:4.3.9.RELEASE]
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230) ~[spring-beans-4.3.9.RELEASE.jar:4.3.9.RELEASE]
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:302) ~[spring-beans-4.3.9.RELEASE.jar:4.3.9.RELEASE]
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:197) ~[spring-beans-4.3.9.RELEASE.jar:4.3.9.RELEASE]
at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:761) ~[spring-beans-4.3.9.RELEASE.jar:4.3.9.RELEASE]
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:867) ~[spring-context-4.3.9.RELEASE.jar:4.3.9.RELEASE]
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:543) ~[spring-context-4.3.9.RELEASE.jar:4.3.9.RELEASE]
at org.springframework.boot.context.embedded.EmbeddedWebApplicationContext.refresh(EmbeddedWebApplicationContext.java:122) ~[spring-boot-1.5.4.RELEASE.jar:1.5.4.RELEASE]
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:693) ~[spring-boot-1.5.4.RELEASE.jar:1.5.4.RELEASE]
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:360) ~[spring-boot-1.5.4.RELEASE.jar:1.5.4.RELEASE]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:303) ~[spring-boot-1.5.4.RELEASE.jar:1.5.4.RELEASE]
at org.springframework.boot.builder.SpringApplicationBuilder.run(SpringApplicationBuilder.java:134) [spring-boot-1.5.4.RELEASE.jar:1.5.4.RELEASE]
at com.eureka.consumer.App.main(App.java:16) [classes/:na]
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'com.eureka.consumer.service.HelloService': FactoryBean threw exception on object creation; nested exception is java.lang.IllegalStateException: RequestHeader.value() was empty on parameter 0
at org.springframework.beans.factory.support.FactoryBeanRegistrySupport.doGetObjectFromFactoryBean(FactoryBeanRegistrySupport.java:175) ~[spring-beans-4.3.9.RELEASE.jar:4.3.9.RELEASE]
at org.springframework.beans.factory.support.FactoryBeanRegistrySupport.getObjectFromFactoryBean(FactoryBeanRegistrySupport.java:103) ~[spring-beans-4.3.9.RELEASE.jar:4.3.9.RELEASE]
at org.springframework.beans.factory.support.AbstractBeanFactory.getObjectForBeanInstance(AbstractBeanFactory.java:1634) ~[spring-beans-4.3.9.RELEASE.jar:4.3.9.RELEASE]
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:254) ~[spring-beans-4.3.9.RELEASE.jar:4.3.9.RELEASE]
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202) ~[spring-beans-4.3.9.RELEASE.jar:4.3.9.RELEASE]
at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:208) ~[spring-beans-4.3.9.RELEASE.jar:4.3.9.RELEASE]
at org.springframework.beans.factory.support.DefaultListableBeanFactory.addCandidateEntry(DefaultListableBeanFactory.java:1316) ~[spring-beans-4.3.9.RELEASE.jar:4.3.9.RELEASE]
at org.springframework.beans.factory.support.DefaultListableBeanFactory.findAutowireCandidates(DefaultListableBeanFactory.java:1282) ~[spring-beans-4.3.9.RELEASE.jar:4.3.9.RELEASE]
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1101) ~[spring-beans-4.3.9.RELEASE.jar:4.3.9.RELEASE]
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1066) ~[spring-beans-4.3.9.RELEASE.jar:4.3.9.RELEASE]
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:585) ~[spring-beans-4.3.9.RELEASE.jar:4.3.9.RELEASE]
... 18 common frames omitted
Caused by: java.lang.IllegalStateException: RequestHeader.value() was empty on parameter 0
at feign.Util.checkState(Util.java:128) ~[feign-core-9.5.0.jar:na]
at org.springframework.cloud.netflix.feign.annotation.RequestHeaderParameterProcessor.processArgument(RequestHeaderParameterProcessor.java:62) ~[spring-cloud-netflix-core-1.3.1.RELEASE.jar:1.3.1.RELEASE]
at org.springframework.cloud.netflix.feign.support.SpringMvcContract.processAnnotationsOnParameter(SpringMvcContract.java:238) ~[spring-cloud-netflix-core-1.3.1.RELEASE.jar:1.3.1.RELEASE]
at feign.Contract$BaseContract.parseAndValidateMetadata(Contract.java:107) ~[feign-core-9.5.0.jar:na]
at org.springframework.cloud.netflix.feign.support.SpringMvcContract.parseAndValidateMetadata(SpringMvcContract.java:133) ~[spring-cloud-netflix-core-1.3.1.RELEASE.jar:1.3.1.RELEASE]
at feign.Contract$BaseContract.parseAndValidatateMetadata(Contract.java:64) ~[feign-core-9.5.0.jar:na]
at feign.ReflectiveFeign$ParseHandlersByName.apply(ReflectiveFeign.java:146) ~[feign-core-9.5.0.jar:na]
at feign.ReflectiveFeign.newInstance(ReflectiveFeign.java:53) ~[feign-core-9.5.0.jar:na]
at feign.Feign$Builder.target(Feign.java:218) ~[feign-core-9.5.0.jar:na]
at org.springframework.cloud.netflix.feign.HystrixTargeter.target(HystrixTargeter.java:39) ~[spring-cloud-netflix-core-1.3.1.RELEASE.jar:1.3.1.RELEASE]
at org.springframework.cloud.netflix.feign.FeignClientFactoryBean.loadBalance(FeignClientFactoryBean.java:145) ~[spring-cloud-netflix-core-1.3.1.RELEASE.jar:1.3.1.RELEASE]
at org.springframework.cloud.netflix.feign.FeignClientFactoryBean.getObject(FeignClientFactoryBean.java:166) ~[spring-cloud-netflix-core-1.3.1.RELEASE.jar:1.3.1.RELEASE]
at org.springframework.beans.factory.support.FactoryBeanRegistrySupport.doGetObjectFromFactoryBean(FactoryBeanRegistrySupport.java:168) ~[spring-beans-4.3.9.RELEASE.jar:4.3.9.RELEASE]
... 28 common frames omitted

Controller调用

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

@Autowired
private HelloService helloService;

@GetMapping("/hello")
public String hello(){
return helloService.hello();
}

@GetMapping("/hello1")
public String hello1(){
return helloService.hello("张三");
}

@GetMapping("/hello2")
public User hello2() throws UnsupportedEncodingException {
return helloService.hello(URLEncoder.encode("张三", "UTF-8"), URLEncoder.encode("测试", "UTF-8"), 1L);
}

@GetMapping("/hello3")
public String hello3(){
User user = new User(1L, "张三", "开发");
return helloService.hello(user);
}
}

测试

启动服务,分别访问hello1,hello2,hello3. 然后看下结果:
image

image

image