java学习springbootfeign

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

2017-09-02  本文已影响7861人  二月_春风

覆盖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客户端都配置了自己的默认配置。

注意

参考资料

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进行改造:

@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属性不能为空。

@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中,针对该问题提供了继承特性来帮助我们解决这些复制操作,以进一步减少编码量。

    <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>
 @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);
}
 <dependency>
       <groupId>com.zhihao.miao</groupId>
       <artifactId>user-service-api</artifactId>
       <version>1.0-SNAPSHOT</version>
</dependency>
@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

<dependency>
       <groupId>com.zhihao.miao</groupId>
       <artifactId>user-service-api</artifactId>
       <version>1.0-SNAPSHOT</version>
</dependency>
@FeignClient(value = "user-service")
public interface RefactorUserService extends com.zhihao.miao.service.UserService{
    
}
@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仓库就可以轻易实现接口定义的共享,实现在构建期的接口绑定,从而有效的减少服务客户端的绑定配置。这么做虽然可以很方便的实现接口定义和依赖的共享,不用在复制粘贴接口进行绑定,但是这样的做法使用不当的话会带来副作用。由于接口在构建期间就建立起了依赖,那么接口变化就会对项目构建造成了影响,可能服务提供方修改一个接口定义,那么会直接导致客户端工程的构建失败。所以,如果开发团队通过此方法来实现接口共享的话,建议在开发评审期间严格遵守面向对象的开闭原则,尽可能低做好前后版本兼容,防止因为版本原因造成接口定义的不一致。

代码地址
代码地址

上一篇下一篇

猜你喜欢

热点阅读