远程过程调用(Remote Procedure Call)的缩写形式
Birrell 和 Nelson 在 1984 发表于 ACM Transactions on Computer Systems 的论文《Implementing remote procedure calls》对 RPC 做了经典的诠释。
RPC 是指计算机 A 上的进程,调用另外一台计算机 B 上的进程,其中 A 上的调用进程被挂起,而 B 上的被调用进程开始执行,当值返回给 A 时,A 进程继续执行。
调用方可以通过使用参数将信息传送给被调用方,而后可以通过传回的结果得到信息。而这一过程,对于开发人员来说是透明的。
OpenFeign 是 Spring Cloud 在 Feign 的基础上支持了SpringMVC的注解,如@RequesMapping等等。
OpenFeign 的 @FeignClient 可以解析 SpringMVC 的 @RequestMapping 注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务。
Feign 是Spring Cloud组件中的一个轻量级RESTful的HTTP服务客户端,Feign内置了Ribbon,用来做客户端负载均衡,去调用服务注册中心的服务。
Feign的使用方式是使用Feign的注解定义接口,调用这个接口就可以调用服务注册中心的服务。
OpenFeign 的设计宗旨是简化 Java Http 客户端的开发。
Feign 在 restTemplate 的基础上做了进一步的封装,由其来帮助我们定义和实现依赖服务接口的定义。
在OpenFeign的协助下,我们只需创建一个接口并使用注解的方式进行配置(类似于Dao接口上面的Mapper注解)即可完成对服务提供方的接口绑定,大大简化了Spring cloud Ribbon的开发,自动封装服务调用客户端的开发量。
OpenFeign集成了Ribbon,利用ribbon维护了服务列表,并且通过ribbon实现了客户端的负载均衡。
与 ribbon 不同的是,通过OpenFeign只需要定义服务绑定接口且以申明式的方法,优雅而简单的实现了服务调用。
依赖于springMVC 的 web 服务,或者直接使用 spring-boot 的 spring-boot-starter-web 系列基础包。
web 等基础系列包
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
feign 基本
${openfeign.version} 可以自定替换版本,这里示例用 2.2.2.RELEASE
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
<version>${openfeign.version}version>
dependency>
<dependency>
<groupId>io.github.openfeigngroupId>
<artifactId>feign-okhttpartifactId>
<version>${feign-okhttp.version}version>
dependency>
如果出现引入后出现个别问题可以尝试去除如下
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
<version>${openfeign.version}version>
<exclusions>
<exclusion>
<artifactId>guavaartifactId>
<groupId>com.google.guavagroupId>
exclusion>
exclusions>
dependency>
服务注册中心的依赖,一般场景不需要(使用分布式场景时考虑)
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
spring-boot 需要在启动类上添加注解 @EnableFeignClients
位置与 @SpringBootApplication, @EnableTransactionManagement,@ComponentScan 等在启动类上的注解 位置一致
例如:
eureka:
client:
serviceUrl:
#服务中心地址
defaultZone: http://localhost:8881/eureka/,http://localhost:8882/eureka/
instance:
#显示ip地址
prefer-ip-address: true
默认超时时间为 1s
feign:
client:
config:
default:
readTimeout: ${RPC_TIME_OUT:600000}
connectTimeout: ${RPC_TIME_OUT:600000}
也可以配置
# OpenFeign 默认支持 ribbon
ribbon:
# 建立连接所用的时间,两端连接所用的时间
ReadTimeout: 6000
# 建立连接后从服务器读取到可用资源所用时间
ConnectTimeout: 6000
大致可以参考如下示例:
import com.alibaba.fastjson.JSONObject;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.multipart.MultipartFile;
@FeignClient(name = "XxxxClient", url = "${xxxx.server}")
public interface XxxxClient {
@PostMapping("/test/create")
XxxxxResult createXxxxxxx(@RequestBody XxxxxxxRequest body,
@RequestHeader MultiValueMap<String, String> header);
@PostMapping(value = "/test/check", consumes = "multipart/form-data")
XxxxxResult importXxxxx(@RequestPart("file") MultipartFile file, @RequestParam Long id,
@RequestHeader MultiValueMap<String, String> header);
@PostMapping("/test/config")
XxxxxResult xxxxxxConfig(@RequestParam Long id, @RequestBody JSONObject body,
@RequestHeader MultiValueMap<String, String> header);
@PostMapping("/test/xxxxxx")
XxxxxResult xxxxxXxxxxxxxx(@RequestParam Long id, @RequestBody JSONObject body, @RequestHeader MultiValueMap<String, String> header);
@GetMapping("/test/xxxx/{id}")
XxxxxResult xxxxXxxxx(@PathVariable String id, @RequestHeader MultiValueMap<String, String> header);
}
在上面的请求中添加请求头的方式:
@RequestMapping(value="/xxxxx/xxxxx",method = RequestMethod.POST,
headers = {"Content-Type=application/json;charset=UTF-8"})
List<String> test();
如上方直接在参数中增加,多个使用map ,也可以在单个参数前加 单个注解@RequestHeader(“xxxx”) 指定值
在方法上方使用注解
@RequestMapping(value="/xxxx/xxxxx",method = RequestMethod.POST)
@Headers({"Content-Type: application/json;charset=UTF-8"})
List<String> test();
说明:
1.其中 @FeignClient(name = “XxxxClient”, url = “${xxxxx.server}”)
url 中的 ${xxxxx.server} 为spring-boot 中 yml 或者 properties 中配置的地址,也可以直接写入地址 例如:http://localhost:8081/testapi
2.可以通过 @GetMapping 或 @PostMapping 直接指定将要请求的方法是 GET 或者POST 等请求,同理 @RequestParam 指定普通请求入参, @RequestHeader 指定请求的请求的头内容, @PathVariable 指定路径参数,@RequestBody 指定 请求体内容, @RequestPart 指定文件类型
3.@GetMapping 或 @PostMapping后面的路径 即为实际请求第三方或要请求的其他产品的路径
用法基本与书写普通的请求类似,具体实现通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务,只要我们指定基本的所有关键信息即可,换言之,只要知道我们要携带的信息,后续的实现写法基本固定,故此可以实现高度封装。
spring cloud 项目使用 feign 时会发现一个问题,就是get方式无法解析对象参数。
其实 feign 是支持对象传递的,但是必须是 Map 形式,而且不能为空,与spring在机制上不兼容,因此无法使用。
spring cloud 在 2.1.x版本 中提供了**@SpringQueryMap** 注解,可以传递对象参数,框架自动解析。
例如:
@GetMapping(value = "/test/xxxxxxXxxx")
XxxxxResult xxxxXxxxXxX(@SpringQueryMap BookDto dto);
@FeignClient 相关属性如下:
属性 | 说明 |
---|---|
name | 指定该类的容器名称,类似于@Service(容器名称) |
url | 即要请求的地址,可以更改指定地址 |
decode404 | 当发生http 404错误时,如果该字段位true,会调用decoder进行解码,否则抛出FeignException |
configuration | Feign配置类,可以自定义Feign的Encoder、Decoder、LogLevel、Contract |
fallback | 定义容错的处理类,当调用远程接口失败或超时时,会调用对应接口的容错逻辑,fallback指定的类必须实现@FeignClient标记的接口 |
fallbackFactory | 用于生成fallback类示例,通过这个属性可以实现每个接口通用容错逻辑,减少重复代码 |
path | 定义当前FeignClient的统一前缀,当我们项目中配置了server.context-path,server.servlet-path时使用 |
远程调用接口当中,一般我们称提供接口的服务为提供者,而调用接口的服务为消费者。
OpenFeign一定是用在消费者上。
Feign 提供了日志打印功能,我们可以通过配置不同级别来调整日志输出内容,从而了解 Feign 中 Http 请求的细节。
其实就是多了一个请求日志的输出控制方式。
日志级别:
级别名 | 说明 |
---|---|
NONE | 默认等级,不显示任何日志; |
BASIC | 仅记录请求方法、URL、响应状态码及执行时间; |
HEADERS | 除了 BASIC 中定义的信息之外,还有请求和响应的头信息; |
FULL | 除了 HEADERS 中定义的信息之外,还有请求和响应的正文及元数据。 |
具体配置方式见后面的其他配置说明示例。
可以通过自定义某一个类实现接口 RequestInterceptor(feign.RequestInterceptor) 来完成发送请求前要做的操作,例如添加统一的请求头,拼接统一参数,替换路径等等操作
例如:
该接口只有一个方法,通过接口的入参可以获取当前请求的一些信息,或者修改当前请求的一些信息
@Slf4j
public class FeignBasicRequestInterceptor implements RequestInterceptor {
public FeignBasicRequestInterceptor() {
}
@Override
public void apply(RequestTemplate template) {
// 可以通过获取不同的路径来确定当前请求的是那一个请求或者哪一类请求,来具体确定要执行怎样的操作
// template.feignTarget().url() 获取当前请求基本路,即@FeignClient 中的url 基础路径
// template.url() 获取定义请求方法时 getMapping或postMapping后的地址
// template.method() 请求的类型 GET 还是 POST
// template.body() 请求体,可能为空则可以 new String(template.body()) 或给与默认值
// template.headers() 请求的头信息
// template.header(String name, String... values) 设置添加请求头,具体可以点击类查看源码
// template.query(String name, Iterable values) 设置增加query 参数
}
}
可能用到: 基础认证
template.header(HttpHeaders.AUTHORIZATION, okhttp3.Credentials.basic(ak, sk));
即 上方 @FeignClient 属性中可以指定的配置类
不是必须的但可以指定,内部的几个类也可以按需增加,部分可以不要
MyX509TrustManager 为自定类,用于适配https 信任所有证书或者自定义,在后面提供定义的内容
import feign.Client;
import feign.Logger;
import feign.Retryer;
import feign.codec.Decoder;
import feign.optionals.OptionalDecoder;
import lombok.extern.slf4j.Slf4j;
import okhttp3.OkHttpClient;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.ssl.SSLContexts;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.cloud.openfeign.support.SpringDecoder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import static org.apache.http.conn.ssl.SSLConnectionSocketFactory.TLS;
@Slf4j
@Configuration
class FeignConfiguration {
@Autowired
private ObjectFactory<HttpMessageConverters> messageConverters;
// OkHttpClient 替代原有的client,可以不替代
@Bean
public Client feignClient() throws NoSuchAlgorithmException, KeyStoreException, KeyManagementException {
SSLContext sslContext =
SSLContexts.custom().loadTrustMaterial(null, (chain, authType) -> true).setProtocol(TLS).build();
SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
OkHttpClient okHttpClient = new OkHttpClient.Builder().hostnameVerifier(NoopHostnameVerifier.INSTANCE)
.sslSocketFactory(sslSocketFactory, new MyX509TrustManager()).build();
return new feign.okhttp.OkHttpClient(okHttpClient);
}
@Bean
// 与上方加的配置类名称一致
public FeignBasicRequestInterceptor basicAuthRequestInterceptor() {
return new FeignBasicRequestInterceptor();
}
@Bean
// 日志等级 上方日志等级中提及到的
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
@Bean
// 重试
Retryer feignRetryer() {
return Retryer.NEVER_RETRY;
}
@Bean
// 结果解码类 new FeignResponseDecoder 为自己定义的解码类,见下方定义
public Decoder feignDecoder() {
return new OptionalDecoder(
new FeignResponseDecoder(new SpringDecoder(this.messageConverters)));
}
}
日志配置后的使用,需要在配置文件中指定某一接口或者类的等级
logging:
level:
# feign日志以什么级别监控哪个接口
com.xxx.xxx.xxx.xxx: debug
(可选)需要可以仿照添加
import javax.net.ssl.X509TrustManager;
/**
* Https调用需要使用的TrustManager
*/
public class MyX509TrustManager implements X509TrustManager {
@Override
public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String authType) {
// 什么都不做则信任所有 否则自定义内容
}
@Override
public void checkServerTrusted(java.security.cert.X509Certificate[] chain, String authType) {
// 什么都不做则信任所有 否则自定义内容
}
@Override
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
return new java.security.cert.X509Certificate[]{};
}
}
import feign.FeignException;
import feign.Response;
import feign.codec.Decoder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.openfeign.support.ResponseEntityDecoder;
import java.io.IOException;
import java.lang.reflect.Type;
/**
* feign返回结果自定义解析
**/
@Slf4j
public class FeignResponseDecoder extends ResponseEntityDecoder implements Decoder {
public FeignResponseDecoder(Decoder decoder) {
super(decoder);
}
@Override
public Object decode(Response response, Type type) throws IOException, FeignException {
Object decode = super.decode(response, type);
// type.getTypeName() 最上方创建请求时 定义的返回结果的toString 结果
if (type.getTypeName().contains(XxxxxResult.class.getName())) {
XxxxxResult result = (XxxxxResult) decode;
// 检查返回是否为对方定义的成功码 如果不是抛出异常
if (!"200".equals(result.getCode())) {
// 可以使用自定义异常类 这里示例使用普通异常
throw new Exception(result.getMessage());
}
}
return decode;
}
}