有关微服务中,服务与服务如何通信,我已经给大家介绍了Ribbon远程调用的相关知识,不知道大家有没有发现Ribbon的问题呢?
Ribbon的问题
在Ribbon中,如果我们想要发起一个调用,是这样的:
@Resource
private RestTemplate restTemplate
String result = restTemplate.getForObject("http://my-goods/goods/get", String.class);
Goods goods = JSONObject.parseObject(result, Goods.class);
这就像一个普通的http请求一样,需要对入参和出参进行手动处理。
打一眼看上去好像没什么问题,但仔细一想就不对劲了:这个被调用的接口都是我们自己写的,入参和出参都是确定的,甚至写被调用的接口的人都是同一个...有没有一种更好的方式,比如像调用本地方法一样直接调用它呢?
比如这样:
Goods goods = goodsServer.get();
这个术语叫:远程方法调用
今天的主角
Feign
就为我们实现了这样的功能,下面有请~
什么是Feign
官方是这样介绍的:Feign 是受Retrofit、JAXRS-2.0和WebSocket启发的 Java 到 HTTP 客户端binder(我也不知道这里怎么翻译)。
为什么说是binder?在看Feign所实现的功能及出发点来说,Feign本身并未实现HTPP客户端,而且在其他组件的客户端的上层进行了增强,我们可以先来感受一下Feign所带给我们的功能
客户端
- java.net.URL
- Apache HTTP
- OK Http
- Ribbon
- Java 11 http2
- ....
规范
- Feign
- JAX-RS
- JAX-RS 2
- SOAP
- Spring 4
- ....
编解码
- GSON
- JAXB
- Jackson
- ....
其他
- Hystrix
- SLF4J
- Mock
更多内容查看github: https://github.com/OpenFeign/feign
基本使用
1. 在商品服务中编写crud
小伙伴可以自己随便找个项目写,主要就是搞几个接口来调用
@RestController
@RequestMapping("/goods")
public class GoodsController {
@GetMapping("/get-goods")
public Goods getGoods(){
return new Goods().setName("苹果")
.setPrice(1.1)
.setNumber(2);
}
@GetMapping("/list")
public List<Goods> list(){
ArrayList<Goods> goodsList = new ArrayList<>();
Goods apple = new Goods().setName("苹果")
.setPrice(1.1)
.setNumber(2);
goodsList.add(apple);
Goods lemon = new Goods().setName("柠檬")
.setPrice(5.1)
.setNumber(3);
goodsList.add(lemon);
return goodsList;
}
@PostMapping("save")
public void save(@RequestBody Goods goods){
System.out.println(goods);
}
@DeleteMapping
public void delete(String id){
System.out.println(id);
}
}
2. 建立一个demo,引入依赖
<dependencies>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-core</artifactId>
</dependency>
<!-- 用于编解码 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-jackson</artifactId>
</dependency>
</dependencies>
3.编写feign规范的接口
public interface GoodsApi {
@RequestLine("GET /goods/get-goods")
Goods getGoods();
@RequestLine("GET /goods/list")
List<Goods> list();
@RequestLine("POST /goods/save")
@Headers("Content-Type: application/json")
void save(Goods goods);
@RequestLine("DELETE /goods?id={id}")
@Headers("Content-Type: application/json")
void delete(@Param("id") String id);
}
Feign规范是什么相信小伙伴一看例子就懂了
4.测试
public class FeignDemo {
public static void main(String[] args) {
// 构建feign接口
GoodsApi goodsApi = Feign.builder()
.encoder(new JacksonEncoder())
.decoder(new JacksonDecoder())
.target(GoodsApi.class, "http://localhost:8082");
// 调用测试
System.out.println(goodsApi.getGoods());
System.out.println(goodsApi.list());
goodsApi.save(new Goods().setName("banana"));
goodsApi.delete("1");
}
}
灵光一闪
看到这里,不知道大家有没有灵光一闪呢?
我要是把构建的代码写这样
@Bean
public GoodsApi goodsApi(){
return Feign.builder()
.encoder(new JacksonEncoder())
.decoder(new JacksonDecoder())
.target(GoodsApi.class, "http://localhost:8082");
}
然后使用时直接注入,woc,这不是爽翻天?
比较常用的使用方式
回顾什么是Feign章节,除了上面基本使用之外,feign还支持Spring 4的规范,以及各种http客户端(如okHttp),重试超时,日志等,我给大家介绍一个比较常用的方式
增加依赖
<!-- spring4规范 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-spring4</artifactId>
<version>10.10.1</version>
</dependency>
<!-- ribbon客户端 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-ribbon</artifactId>
</dependency>
<!-- okhttp客户端 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>
<!-- 日志 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-slf4j</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
编写接口
public interface Spring4GoodsApi {
@GetMapping("/goods/get-goods")
Goods getGoods();
@GetMapping("/goods/list")
List<Goods> list();
@PostMapping(value = "/goods/save", consumes = MediaType.APPLICATION_JSON_VALUE)
void save(Goods goods);
@DeleteMapping("/goods")
void delete(@RequestParam(value = "id") String id);
}
测试
public class Spring4FeignDemo {
public static void main(String[] args) {
Spring4GoodsApi goodsApi = Feign.builder()
// 使用spring4规范
.contract(new SpringContract())
// 使用jackson编解码
.encoder(new JacksonEncoder())
.decoder(new JacksonDecoder())
// okhttp客户端
.client(new OkHttpClient())
// 请求失败重试,默认最大5次
.retryer(new Retryer.Default())
// 请求超时配置
.options(new Request.Options(10, TimeUnit.SECONDS, 60, TimeUnit.SECONDS, true))
// 日志配置,将在请求前后打印日志
.logger(new Slf4jLogger())
// 日志等级配置,BASIC:只打印请求路径和响应状态码基本信息
.logLevel(Logger.Level.BASIC)
.target(Spring4GoodsApi.class, "http://localhost:8082");
System.out.println(goodsApi.getGoods());
System.out.println(goodsApi.list());
goodsApi.save(new Goods().setName("banana"));
goodsApi.delete("1");
}
}
拦截器
是个http客户端就会有拦截器机制,用于在请求前统一做一些操作:比如添加请求头
feign的拦截器使用方式如下:
public class AuthFeignInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
System.out.println("进入拦截器");
HttpServletRequest request =
((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
template.header("token", request.getHeader("token"));
}
}
实现RequestInterceptor即可
在build时加入
Feign.builder()
.requestInterceptor(new AuthFeignInterceptor())
.target(Spring4GoodsApi.class, "http://localhost:8082");
以上就是Feign的常用方式了,学会之后,整合到Spring Cloud也是手到擒来。
整合Spring Cloud
官网文档:https://docs.spring.io/spring-cloud-openfeign/docs/current/reference/html/index.html
该整合样例为订单服务调用商品服务
直接三板斧走起
1. 加入依赖
在order-server中引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
2. 增加注解
在Application类中加上注解EnableFeignClients
@EnableFeignClients
@SpringBootApplication
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(GoodsApplication.class, args);
}
}
任何一个配置类都可以,但是推荐在启动类上加,因为注解默认扫描该注解类路径下的所有包,然后启动类又是在最顶端的,所以这样就可以扫描到所有的包了。
当然,你也可以直接扫描某个包,毕竟一般feign接口都放在一起
3. 编写配置
见配置章节
三板斧结束,开始编写样例
4. 编写样例
@FeignClient(name = "my-goods", path = "/goods", contextId = "goods")
public interface GoodsApi {
@GetMapping("/get-goods")
Goods getGoods();
/**
* get 方式传参加上需@SpringQueryMap注解
*/
@GetMapping("/goods")
Goods getGoods(@SpringQueryMap Goods goods);
@GetMapping("/list")
List<Goods> list();
@PostMapping(value = "/save")
void save(Goods goods);
@DeleteMapping
void delete(String id);
}
FeignClient:
name: 调用的服务名称
path: 路径前缀,该类下的所有接口都会继承该路径
contextId: 用于区分不同的feign接口,因为一般来说一个服务不止一个feign接口,比如还有个GoodsDetailApi(商品详情), 但是他们的name属性是相同的,都是商品服务,所有需要一个contextId来区分不同的业务场景
其他的与常用方式相同
5. 测试
@RestController
@RequestMapping("/order")
public class OrderController {
@Resource
private GoodsApi goodsApi;
@GetMapping("/get-goods")
public Goods getGoods(){
return goodsApi.getGoods();
}
}
配置
在常用方式中,我们构建一个feign接口的各种属性,是通过硬编码完成的,整合到spring之后,可以通过配置的方式完成了,更加的灵活。
日志
feign:
client:
config:
# 全局配置, 配置类里面的属性名叫defaultConfig, 值却是default, 注意不要搞错
default:
loggerLevel: FULL
# 单独服务配置 对应的是contextId 优先级更高
goods:
loggerLevel: BASIC
全局配置这里特别坑,不看源码根本不知道怎么配,小伙伴一定要注意
客户端
feign默认使用的HttpURLConnection
作为客户端,小伙伴也可以替换成其他的客户端
前言:所有的客户端都为Client
接口的实现类,想要知道是否替换成功只需在对应的实现类中打个断点
使用httpclient
引入依赖
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
<version>10.10.1</version>
</dependency>
这样就可以了,不用修改任何配置,这是因为当服务中包含ApacheHttpClient
的class时,httpClient的feign自动配置类就会生效,并且比默认的HttpURLConnection
自动配置类优先级更高。此时服务就将注入HttpClient作为客户端。
源码如下:
@Import 导入的顺序是HttpClient, OkHttp, Default(HttpURLConnection)
该配置类的生效条件就是存在ApacheHttpClient类,而
feign.httpclient.enabled
配置默认不配也是生效。
使用OkHttp
引入依赖
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>
由于我们在上一步引入了httpclient依赖,而httpclient的优先级比okhttp高,并且是默认生效,所以想要okhttp生效有两种方式:
- 删除掉httpclient的依赖
- 显示关闭httpclient
这里我使用第二种方式
feign:
# 将httpclient关闭
httpclient:
enabled: false
# 将okhttp开启
okhttp:
enabled: true
GZIP压缩
有时当请求数据过大时,进行压缩数据可以有效的提高请求性能,feign也提供这样的配置方式
注意:压缩只在非okhttp客户端时生效
feign:
httpclient:
enabled: true
# 配置压缩
compression:
request:
enabled: true
# 压缩的类型,默认就是这些
mime-types: text/xml, application/xml, application/json
# 压缩的字节最小阈值, 超过该大小才进行压缩,默认1024
min-request-size: 10
这里我使用httpclient,不写也可以,我只是为了让大家知道我没用okhttp
容错重试
互联网应用无法解决的问题之一:网络分区。当被服务提供者出现这种情况,一直无法响应情况,我们也不可能让服务消费者一直傻傻的等着,所以我们可以给服务配置一个超时时间,超过一定的时间被调用的服务未响应,就把请求掐断。
feign的配置:
feign:
client:
config:
# 全局配置
default:
# 连接超时时间 单位毫秒 默认10秒
connectTimeout: 1000
# 请求超时时间 单位毫秒 默认60秒
readTimeout: 5000
互联网应用无法解决的问题之二:网络抖动。当经历这种情况时,我们只能让服务进行重试。
feign配置:
feign:
client:
config:
# 全局配置
default:
# 重试 默认重试5次
retryer: feign.Retryer.Default
拦截器
拦截器与常用方式相同,实现RequestInterceptor
接口即可
@Component
public class AuthFeignInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
System.out.println("进入拦截器");
HttpServletRequest request =
((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
template.header("token", request.getHeader("token"));
}
}
加上@Component注解放到spring容器中,服务启动时会自动加入到feign的拦截器链
也可以使用配置的方式
feign:
client:
config:
# 全局配置
default:
requestInterceptors:
- com.my.micro.service.order.interceptor.AuthFeignInterceptor
小结
本篇详细介绍了一种远程方法调用方式:Feign,现在,带大家来简单的回顾一下。
什么是Feign?
一种远程方法调用客户端,整合了ribbon,httpclient, okhttp, 支持各种各样的规范,如Spring4
Feign的基本使用方式?
编辑接口,加上Feign支持的规范注解,使用Feign.Builder构建出代理类,发起调用。
如何整合SpringCloud?
引入依赖,加上@EnableFeignClients注解,根据需求增加配置
本篇内容基本涵盖了Feign的常用方式,希望大家有所收获,我们下期再见~