spring cloud feign学习二:Feign的深入使用

覆盖Feign的默认配置

A central concept in Spring Cloud’s Feign support is that of the named client. Each feign client is part of an ensemble of components that work together to contact a remote server on demand, and the ensemble has a name that you give it as an application developer using the @FeignClient annotation. Spring Cloud creates a new ensemble as an ApplicationContext on demand for each named client using FeignClientsConfiguration. This contains (amongst other things) an feign.Decoder, a feign.Encoder, and a feign.Contract.

Spring Cloud的Feign支持的一个中心概念就是命名客户端。 每个Feign客户端都是组合的组件的一部分,它们一起工作以按需调用远程服务器,并且该集合具有您将其作为使用@FeignClient注释的参数名称。 Spring Cloud使用FeignClientsConfiguration创建一个新的集合作为每个命名客户端的ApplicationContext(应用上下文)。 这包含(除其他外)feign.Decoderfeign.Encoderfeign.Contract

你可以自定义FeignClientsConfiguration以完全控制这一系列的配置。比如我们下面的demo:

定义一个order服务,并加入依赖:

 <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <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>
    </dependencies>

定义主体启动类:

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class OrderApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class,args);
    }
}

定义Controller:

@RestController
@RequestMapping("/order")
public class OrderController {

    private Logger logger = LoggerFactory.getLogger(getClass());


    @Autowired
    UserService userService;

    @RequestMapping("/index")
    public String index(){
        logger.info("index方法");
        return userService.index();
    }
}

定义Feign客户端接口:

@FeignClient(value = "user-service",configuration = FooConfiguration.class)
public interface UserService {

    @RequestLine("GET /user/index")
    String index();

}

使用了配置@Configuration参数,自己定义了FooConfiguration类来自定义FeignClientsConfiguration,并且FeignClientsConfiguration类的类路径不在启动类OrderApplication的扫描路径下,是因为如果在扫描目录下会覆盖该项目所有的Feign接口的默认配置。

FooConfiguration定义:

package com.zhihao.miao.config;

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

@Configuration
public class FooConfiguration {

    //使用Feign自己的注解,使用springmvc的注解就会报错
    @Bean
    public Contract feignContract() {
        return new feign.Contract.Default();
    }
}

因为配置FooConfiguration定义的是new feign.Contract.Default(),所有在UserService接口中只能使用Feign自己的注解url方式。

配置文件:

spring:
  application:
    name: order-service
eureka:
  client:
    service-url:
     defaultZone: http://zhihao.miao:123456@localhost:8761/eureka
  instance:
    instance-id:  ${spring.application.name}:${spring.cloud.client.ipAddress}:${spring.application.instance_id:${server.port}}
server:
  port: 9090

访问http://192.168.5.3:9090/order/index,正常访问到结果。

再定义一个FeignClient接口,使用SpringMvc注解的方式来访问

@FeignClient(value = "eureka-service",url = "http://localhost:8761/",configuration = EurekaConfiguration.class)
public interface EurekaService {

    @RequestMapping(value = "/eureka/apps/{serviceName}")
    String findServiceInfoFromEurekaByServiceName(@PathVariable("serviceName") String serviceName);
}

因为Eureka配置了用户名和密码,所有这个FeignClient也自己定义了FeignClientsConfiguration,也可以用来访问Eureka服务接口。

package com.zhihao.miao.config;

import feign.auth.BasicAuthRequestInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class EurekaConfiguration {

    @Bean
    public BasicAuthRequestInterceptor basicAuthRequestInterceptor() {
        return new BasicAuthRequestInterceptor("zhihao.miao", "123456");
    }
}

通过访问http://192.168.5.3:9090/order/findServiceInfoFromEurekaByServiceName/user-service也能访问成功,通过这个列子我们知道可以为每个Feign客户端都配置了自己的默认配置。

注意

  • @FeignClient注解的serviceId参数不建议被使用。
  • 以前使用@FeignClient注解的时候使用url参数的使用就不需要使用name属性了,现在不然,需要在url属性的基础上也要使用name属性,此时的name属性只是一个标识。

参考资料

springcloud 官网
feign的github地址

参数绑定

在快速入门中,我们使用了spring cloud feign实现的是一个不带参数的REST服务绑定。现实中的各种业务接口要比它复杂的多,我们会在http的各个位置传入各种不同类型的参数,并且在返回请求响应的时候也可能是一个复杂的对象结构。

扩展一下user-servcice服务,增加一些接口定义,其中包含Request参数的请求,带有Header信息的请求,带有RequestBody的请求以及请求响应体中是一个对象的请求,扩展了三个接口分别是hello,hello2,hello3

@RestController
@RequestMapping("/user")
public class UserController {

    private final Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private DiscoveryClient client;

    @RequestMapping(value="/index",method = RequestMethod.GET)
    public String index(){
        ServiceInstance instance = client.getLocalServiceInstance();
        logger.info("/user,host:"+instance.getHost()+",service id:"+instance.getServiceId()+",port:"+instance.getPort());
        return "user index, local time="+ LocalDateTime.now();
    }

    @GetMapping("/hello")
    public String userHello() throws Exception{
        ServiceInstance serviceInstance = client.getLocalServiceInstance();
        //线程阻塞
        int sleeptime = new Random().nextInt(3000);
        logger.info("sleeptime:"+sleeptime);
        Thread.sleep(sleeptime);
        logger.info("/user,host:"+serviceInstance.getHost()+",service id:"+serviceInstance.getServiceId()+",port:"+serviceInstance.getPort());
        return "user hello";
    }

    @RequestMapping(value = "/hello1",method = RequestMethod.GET)
    public String hello(@RequestParam String username){
        return "hello "+username;
    }

    @RequestMapping(value = "hello2",method = RequestMethod.GET)
    public User hello2(@RequestHeader String username,@RequestHeader Integer age){
        return new User(username,age);
    }

    @RequestMapping(value = "hello3",method = RequestMethod.POST)
    public String hello3(@RequestBody User user){
        return "hello "+user.getUsername() +", "+user.getAge()+", "+user.getId();
    }
}

访问:

localhost:8080/user/hello1?username=zhihao.miao

User对象的定义如下,需要注意的是要有User的默认的构造函数,不然,spring cloud feign根据json字符串转换User对象的时候会抛出异常。

public class User {
    private String username;

    private int age;

    private int id;

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public int getAge() {
        return age;
    }

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

    public int getId() {
        return id;
    }

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

    public User(String username, int age) {
        this.username = username;
        this.age = age;
    }

    public User() {

    }

    @Override
    public String toString() {
        return "User{" +
                "username='" + username + '\'' +
                ", age=" + age +
                ", id=" + id +
                '}';
    }
}

完成对user-service的改造之后,我们对pay-service进行改造:

  • user-service中创建与上面一样的User类
  • pay-service中的UserService接口中加入之前的接口定义:
@FeignClient("user-service")
public interface UserService {

    @RequestMapping("/user/index")
    String index();

    @RequestMapping("/user/hello")
    String hello();

    @RequestMapping(value = "/user/hello1",method = RequestMethod.GET)
    String hello1(@RequestParam("username") String username);

    @RequestMapping(value = "/user/hello2",method = RequestMethod.GET)
    User hello2(@RequestHeader("username") String username, @RequestHeader("age") Integer age);

    @RequestMapping(value = "/user/hello3",method = RequestMethod.POST)
    String hello3(@RequestBody User user);
}

注意
在定义各参数绑定的时候,@RequestParam@RequestHeader等可以指定参数名称的注解,它们的value值千万不能少。在spring mvc程序中,这些注解会根据指定参数名来作为默认值,但是在fegin中绑定参数必须通过value属性来指明具体的参数名,不然会抛出IllegalStateException异常,value属性不能为空。

  • 在payservice中增加对UserService中新增接口的调用,来验证feign客户端的调用是否可行:
@RestController
@RequestMapping("/pay")
public class PayController {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    UserService userService;

    @RequestMapping("/index")
    public String index(){
        return userService.index();
    }

    @RequestMapping("/hello")
    public String hello(){
        return userService.hello();
    }

    @RequestMapping(value = "/hello1",method = RequestMethod.GET)
    public String hello1(@RequestParam String username){
        return userService.hello1(username);
    }

    @RequestMapping(value = "/hello2",method = RequestMethod.GET)
    public User hello2(@RequestHeader String username,@RequestHeader Integer age){
        logger.info(age.getClass().getName());
        return userService.hello2(username,age);
    }

    @RequestMapping(value = "/hello3",method = RequestMethod.POST)
    public String hello3(@RequestBody User user){
        return userService.hello3(user);
    }
}

测试,

localhost:7070/pay/hello1?username=zhihao.miao
localhost:7070/pay/hello2
localhost:7070/pay/hello3
图片.png

继承特性

通过上面的快速入门和参数绑定二个demo,当使用springmvc的注解来绑定服务接口时候,我们几乎可以完全从服务提供方(user-service)的Controller中依靠复制操作,构建出相应的服务客户端绑定接口。既然存在这么多复制操作,我们自然需要考虑这部分内容是否可以得到进一步的抽象。spring cloud feign中,针对该问题提供了继承特性来帮助我们解决这些复制操作,以进一步减少编码量。

  • 定义一个maven工程,user-service-api
  • 由于在user-service-api中需要定义可同时复用于服务端与客户端的接口,需要用到spring mvc的注解,所以在pom.xml中引入spring-boot-starter-web依赖,具体的内容如下:
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
  • 将之前的User对象复制到自己的项目中,创建UserService接口,内容如下:
 @RequestMapping("/refactor")
public interface UserService {

    @RequestMapping(value = "/hello4",method = RequestMethod.GET)
    String hello1(@RequestParam("username") String username);

    @RequestMapping(value = "/hello5",method = RequestMethod.GET)
    User hello2(@RequestHeader("username") String username, @RequestHeader("age") Integer age);

    @RequestMapping(value = "/hello6",method = RequestMethod.POST)
    String hello3(@RequestBody User user);
}
  • 对user-service进行重构,在pom依赖中加入user-service-api的依赖:
 <dependency>
       <groupId>com.zhihao.miao</groupId>
       <artifactId>user-service-api</artifactId>
       <version>1.0-SNAPSHOT</version>
</dependency>
  • 创建com.zhihao.miao.user.controller.RefactorUserController继承user-service中的UserService接口,实现如下:
@RestController
public class RefactorUserController implements UserService{

    @Override
    public String hello1(@RequestParam String username) {
        return "user "+username;
    }

    @Override
    public User hello2(@RequestHeader("username")  String username, @RequestHeader("age") Integer age) {
        return new User(username,age);
    }

    @Override
    public String hello3(@RequestBody User user) {
        return "user "+user.getUsername()+", "+user.getAge();
    }
}

我们看到可以通过集成的方式,在Controller中不再包含以往会定义的映射注解@RequestMapping,而参数的注解定义在重写的时候自动带过来了,这个类中,除了要实现接口逻辑之外,只需要增加了@RestController注解使该类成为一个REST接口类。

此时这些restful接口的接口url就是user-service-api中定义的,具体的uri地址是/refactor/hello4/refactor/hello5/refactor/hello6

  • 完成了对服务提供者的重构,在消费端的pay-service中也要进行改造,在pay-service中加入如下依赖:
<dependency>
       <groupId>com.zhihao.miao</groupId>
       <artifactId>user-service-api</artifactId>
       <version>1.0-SNAPSHOT</version>
</dependency>
  • 创建RefactorUserService,继承user-service-apiUserService接口,然后添加@FeignClient来绑定服务。
@FeignClient(value = "user-service")
public interface RefactorUserService extends com.zhihao.miao.service.UserService{
    
}
  • PayController2中注入RefactorUserService实例,新增restful接口进行访问:
@RestController
@RequestMapping("/pay2")
public class PayController2 {

    @Autowired
    RefactorUserService refactorUserService;

    private Logger logger = LoggerFactory.getLogger(getClass());

    @RequestMapping(value = "/hello1",method = RequestMethod.GET)
    public String hello4(@RequestParam String username){
        return refactorUserService.hello1(username);
    }

    @RequestMapping(value = "/hello2",method = RequestMethod.GET)
    public User hello5(@RequestHeader String username, @RequestHeader Integer age){
        logger.info(age.getClass().getName());
        return refactorUserService.hello2(username,age);
    }

    @RequestMapping(value = "/hello3",method = RequestMethod.POST)
    public String hello6(@RequestBody User user){
        return refactorUserService.hello3(user);
    }
}
  • 测试:

访问user-service服务:localhost:8080/refactor/hello4?username=zhihao.miao

测试pay-service:

localhost:7070/pay/hello1?username=zhihao.miao
图片.png
图片.png

优点与缺点
使用spring cloud feign的继承特性的优点很明显,可以将接口的定义从Controller中剥离,同时配合maven仓库就可以轻易实现接口定义的共享,实现在构建期的接口绑定,从而有效的减少服务客户端的绑定配置。这么做虽然可以很方便的实现接口定义和依赖的共享,不用在复制粘贴接口进行绑定,但是这样的做法使用不当的话会带来副作用。由于接口在构建期间就建立起了依赖,那么接口变化就会对项目构建造成了影响,可能服务提供方修改一个接口定义,那么会直接导致客户端工程的构建失败。所以,如果开发团队通过此方法来实现接口共享的话,建议在开发评审期间严格遵守面向对象的开闭原则,尽可能低做好前后版本兼容,防止因为版本原因造成接口定义的不一致。

代码地址
代码地址

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,340评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,762评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,329评论 0 329
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,678评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,583评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,995评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,493评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,145评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,293评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,250评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,267评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,973评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,556评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,648评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,873评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,257评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,809评论 2 339

推荐阅读更多精彩内容