使用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接口使用不同的账号登录,并调用用户微服务的接口。
修改用户微服务
- 复制项目microservice-provider-user,将ArtufactId修改为microservice-provider-user-with-auth。
- 为项目添加以下依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
- 创建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(10):使用Feign实现声明式REST调用-构造多参数请求