使用Feign实现声明式REST调用

Posted shi_zi_183

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了使用Feign实现声明式REST调用相关的知识,希望对你有一定的参考价值。

使用Feign实现声明式REST调用

之前示例中是使用RestTemplate实现REST API调用的,代码大致如下:

	public User findById(@PathVariable Long id){
        return this.restTemplate.getForObject("http://microservice-provider-user/"+id,User.class);
    }

由代码可知,我们是使用拼接字符串的方式构造URL的,该URL只有一个参数。然而在现实中,URL中往往有多个参数。如果这时还使用这种方式构造URL,那么就会变得很低效,并且难以维护。
举个例子,想要请求这样的URL

http://localhost:8010/search?name=张三&username=account1&age=20

若使用拼接字符串的方式构建请求URL,那么代码可编写为

    public  User[] findById(String name,String username,Integer age){
        Map<String,Object> paramMap= Maps.newHashMap();
        paramMap.put("name",name);
        paramMap.put("username",username);
        paramMap.put("age",age);
        return this.restTemplate.getForObject("http://microservice-provider-user/search?name={name}&username={username}&age={age}",User[].class,paramMap);
    }

在这里,URL仅包含3个参数。如果URL更加复杂,例如有10个以上的参数,那么代码会变得难以维护。

Feign简介

Feign是Netflix开发的声明式、模板化的HTTP客户端,其灵感来自Retrofit、JAXRS-2.0以及WebSocket。Feign可帮助我们更加便捷、优雅地调用HTTP API。
在Spring Cloud中,使用Feign非常简单——创建一个接口,并在接口上添加一些注解,代码就完成了。Feign支持多种注解,例如Feign自带的注解或者JAX-RS注解等。
Spring Cloud对Feign进行了增强,使Feign支持了Spring MVC注解,并整合了Ribbon和Eureka,从而让Feign的使用更加方便。

为服务消费者整合Feign

之前的电影微服务是使用RestTemplate(通过整合Ribbon实现负载均衡)调用 RESTful API的,这里让电影微服务使用Feign,实现声明式的RESTful API调用。
1)复制项目microservice-consumer-movie,将ArtifactId修改为microservice-consumer-movie-feign。
2)添加Feign的依赖。

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

3)创建一个Feign接口,并添加@FeignClient注解

package com.example.feign;

import com.example.entity.User;
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@FeignClient(name = "microservice-provider-user")
public interface UserFeignClient {
    @RequestMapping(value = "/{id}",method = RequestMethod.GET)
    public User findById(@PathVariable("id") Long id);
}

@FeignClient注解中的microservice-provider-user是一个任意的客户端名称,用于创建Riddon负载均衡器。在本例中,由于使用了Eureka,所以Ribbon会把microservice-provider-user解析成Eureka Server服务注册表中的服务。当然,如果不想使用Eureka,可使用service.ribbon.listofServers属性配置服务器列表。
还可使用url属性指定请求的URL(URL可以是完整的URL或者主机名),例如@FeignClient(name=“microservice-provider-user”,url=“http://localhost:8000/”)。
4)修改Controller代码,让其调用Feign接口

    @Autowired
    private UserFeignClient userFeignClient;
    @GetMapping("/user/{id}")
    public User findById(@PathVariable Long id){
        return this.userFeignClient.findById(id);
    }

5)修改启动类,为其添加@EnableFeignClient注解

package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.netflix.feign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

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

这样,电影微服务就可以用Feign去调用用户微服务的API了
微服务
1)启动microservice-discovery-eureka
2)启动2个或更多microservice-provider-user实例。
3)启动microservice-consumer-movie-feign。
4)多次访问http://localhost:8010/user/1,返回如下结果

{"id":1,"username":"account1","name":"张三","age":20,"balance":100.00}

两个用户微服务实例都会打印类似如下的日志

Hibernate: select user0_.id as id1_0_0_, user0_.age as age2_0_0_, user0_.balance as balance3_0_0_, user0_.name as name4_0_0_, user0_.username as username5_0_0_ from user user0_ where user0_.id=?

自定义Feign配置

很多场景下,我们需要自定义Feign的配置,例如配置日志级别、定义拦截器等。Spring Cloud Edgware允许使用Java代码或属性自定义Feign的配置,两种方式是等价的。

使用Java代码自定义Feign配置

在Spring Cloud中,Feign的默认配置类是FeignClientsConfiguration,该类定义了Fergin默认使用的编码器、解码器、所使用的契约等。
Spring Cloud允许通过注解@FeignClient的configuration属性自定义Feign的配置,自定义配置的优先级比FeignClientConfiguration要高。
在Spring Cloud文档中可看到以下段落,描述了Spring Cloud提供的默认配置。另外,有的配置尽管没有提供默认值,但是Spring也会扫描其中列出的类型(也就是说,这部分配置也能自定义)。

配置指定名称的Feign Client

由此可知,在Spring Cloud中,Feign默认使用的契约是SpringMvcContract,因此它可以使用Spring MVC的注解。下面来自定义Feign的配置,让它使用Feign自带的注解进行工作。
1)复制项目microservice-consumer-movie-feign,将ArtifactId修改为microservice-consumer-movie-feign-customizing.
2)创建Feign的配置类。

package com.example.config;

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

public class FeignConfiguration {
    @Bean
    public Contract feignContract(){
        return new feign.Contract.Default();
    }
}

注:该类可以不写@Configuration注解;如果加了@Configuration注解,那么该类不能放在主程序上下文@ComponentScan所扫描的包中。
将契约改为feign原生的默认契约。这样就可以使用feign自带的注解了。@return默认的feign契约。
3)Feign接口修改为如下,使用@FeignClient的configuration属性指定配置类,同时,将findById上的Spring MVC注解修改为Feign自带的注解。

package com.example.feign;

import com.example.config.FeignConfiguration;
import com.example.entity.User;
import feign.Param;
import feign.RequestLine;
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@FeignClient(name = "microservice-provider-user",configuration = FeignConfiguration.class)
public interface UserFeignClient {
//    @RequestMapping(value = "/{id}",method = RequestMethod.GET)
    @RequestLine("GET /{id}")
    public User findById(@Param("id") Long id);
}

类似地,还自定义Feign地编码器、解码器、日志打印,甚至为Feign添加拦截器。例如,一些接口需要进行基于HttpBasic地认证后才能调用,配置类可以这样写。

package com.example.config;

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

public class FooConfiguration {
    @Bean
    public BasicAuthRequestInterceptor basicAuthRequestInterceptor(){
        return new BasicAuthRequestInterceptor("user","password");
    }
}

1)启动microservice-discovery-eureka
2)启动2个或更多microservice-provider-user实例。
3)启动microservice-consumer-movie-feign-customizing。
4)访问http://localhost:8010/user/1,返回如下结果

在Spring Cloud Edgware中,Feign的配置类(例如本例中的FeignConfiguration类)无须添加@Configuration注解;如果加了@Configuration注解,那么该类不能存放在主应用程序上下文@ComponentScan所扫描的包中。否则,该类中的配置feign.Decoder、feign.Contract等配置就会被所有的@FeignClient共享。
为避免造成问题,最佳实践是不再指定名称的Feign配置类上添加@Configuration注解。

全局配置

注解@EnableFeignClients为我们提供了defaultConfiguration属性,用来指定默认的配置类,例如:

@EnableFeignClients(defaultConfiguration = DefaultRibbonConfig.class)

使用属性自定义Feign配置

从Spring Cloud Netflix 1.4.0开始(即从Spring Cloud Edgware)开始,Feign支持属性自定义。这种方式比使用Java代码配置的方式更加方便。

配置指定名称的Feign Client

对于指定名称的Feign Client,配置如下。

feign:
  client:
    config:
      feignName:
        #相当于Request.Options
        connectTimeout: 5000
        #相当于Request.Options
        readTimeout: 5000
        #配置Feign的日志级别,相当于代码配置方式中的Logger
        loggerLevel: full
        #Feign的错误解码器,相当于代码配置方式中的ErrorDecoder
        errorDecoder: com.example.SimpleErrorDecoder
        #配置重试,相当于代码配置方式中的Retryer
        retryer: com.example.SimpleRetryer
        #配置拦截器,相当于代码配置方式的RequestInterceptor
        requestInterceptors:
          - com.example.FooRequestInterceptor
          - com.example.BarRequestInterceptor
        decode404: false

通用配置

上面讨论了如何配置指定名称的Feign Client,如果想配置所有的Feign Client,只需做如下配置

feign:
  client:
    config:
      default:
        #相当于Request.Options
        connectTimeout: 5000
        #相当于Request.Options
        readTimeout: 5000
        #配置Feign的日志级别,相当于代码配置方式中的Logger
        loggerLevel: full
        #Feign的错误解码器,相当于代码配置方式中的ErrorDecoder
        errorDecoder: com.example.SimpleErrorDecoder
        #配置重试,相当于代码配置方式中的Retryer
        retryer: com.example.SimpleRetryer
        #配置拦截器,相当于代码配置方式的RequestInterceptor
        requestInterceptors:
          - com.example.FooRequestInterceptor
          - com.example.BarRequestInterceptor
        decode404: false

注:属性配置的方式比Java代码配置的方式优先级更高。如果你想让Java代码配置方法优先级更高,可使用这个属性:feign.client.default-to-properties=false。

手动创建Feign

在某些场景下,前文自定义Feign的方式满足不了需求,此时可使用Feign Builder API手动创建Feign

  • 用户微服务的接口需要登陆后才能调用,并且对于相同的API,不同角色的用户有不同的行为。
  • 让电影微服务中的同一个Feign接口使用不同的账号登录,并调用用户微服务的接口。

修改用户微服务

  1. 复制项目microservice-provider-user,将ArtufactId修改为microservice-provider-user-with-auth。
  2. 为项目添加以下依赖
		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
  1. 创建Spring Security的配置类
package com.example.entity;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.ArrayList;
import java.util.Collection;

public class SecurityUser implements UserDetails {
    private static final long serialVersionUID = 1L;
    private Long id;
    private String username;
    private String password;
    private String role;

    public SecurityUser( String username, String password, String role) {
        super();
        this.username = username;
        this.password = password;
        this.role = role;
    }

    public SecurityUser() {
    }

    public Long getId() {
        return id;
    }

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

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

    public void setPassword(String password) {
        this.password = password;
    }

    public String getRole() {
        return role;
    }

    @Override
    public String getUsername() {
        return username;
    }

    @Override
    public String getPassword() {
        return password;
    }

    public void setRole(String role) {
        this.role = role;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> authorities=new ArrayList<GrantedAuthority>();
        SimpleGrantedAuthority authority=new SimpleGrantedAuthority(this.role);
        authorities.add(authority);
        return authorities;
    }
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

package com.example.component;

import com.example.entity.SecurityUser;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.Collection;

@Component
public class CustomUserDetailsService implements UserDetailsService {

    /*
     * 模拟两个账户:
     * 1、账号是user,密码是password1,角色是user-role
     * 2、账号是admin,密码是password2,角色是admin-role
     * */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Collection<GrantedAuthority> authorities=new ArrayList<GrantedAuthority>();
        if ("user".equals(username)) {
            return new SecurityUser("user","password1","user-role");
        } else if ("admin".equals(username)) {
            return new SecurityUser("admin","password2","admin-role");
        } else {
            return null;
        }
    }
}
package com.example.config;

import com.example.component.CustomUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

@Configurati

以上是关于使用Feign实现声明式REST调用的主要内容,如果未能解决你的问题,请参考以下文章

springCloud:使用Feign实现声明式REST调用

SpringCloud系列十:使用Feign实现声明式REST调用

SpringCloud-声明式Rest调用Feign

springCloud(10):使用Feign实现声明式REST调用-构造多参数请求

Spring Cloud 入门教程: 用声明式REST客户端Feign调用远端HTTP服务

Spring Cloud 入门教程: 用声明式REST客户端Feign调用远端HTTP服务