五、http客户端Feign
5.1 初识Feign
前面我们使用远程调用一直用的都是RestTemplate对象来发送请求,源码如下:
String url = "http://user/user/"+order.getUserId();
User user = restTemplate.getForObject(url, User.class);
这种写法,有两个比较明显的缺点啊。
1、可读性差
2、维护困难
为了解决这种不优雅的写法,我们可以使用Feign来代替我们的远程调用。那么Feign是什么东西呢?
Feign是一个声明式的http客户端,他能很好地帮助我们实现http请求的发送,解决使用RestTemplate带来的两个弊端。官网地址:https://github.com/OpenFeign/feign,或者也可以使用该镜像链接,速度会快一些。
废话少说,接下来我们就来说一下怎么用我们的Feign完美替代我们的RestTemplate,实现更优雅的代码编写。对Feign的简单使用,分为以下四个步骤:
- 导入Feign依赖
- 给启动类添加**@EnableFeignClients**注解
- 编写Feign客户端(写接口)
- 发送请求(实现远程调用)
首先,第一步我们肯定是导入Feign的依赖啦。(在我们的order服务中导入)
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
第二步,我们在需要做远程调用的服务的启动类上方添加**@EnableFeignClients注解。(OrderApplication的类上方添加@EnableFeignClients**注解)
@EnableFeignClients
@SpringBootApplication
@MapperScan("com.example.order.mapper")
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
第三步,编写Feign的客户端。这个客户端主要是基于SpringMVC的注解来声明远程调用的信息,比如:
- 服务名称:user
- 请求方式:GET
- 请求路径:/user/{id}
- 请求参数:Long id
- 返回值类型:User
这样,Feign就可以帮助我们发送http请求,无需自己使用RestTemplate来发送了。
package com.example.order.client;
import com.example.order.po.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
//请求的服务名
@FeignClient("user")
public interface UserClient {
//Get请求,路径为/user/{id}
@GetMapping("/user/{id}")
User findById(@PathVariable("id") Long id);
}
第四步,发送请求。此时,只需要将我们的远程调用代码修改为:
package com.example.order.service;
import com.example.order.client.UserClient;
import com.example.order.mapper.OrderMapper;
import com.example.order.po.Order;
import com.example.order.po.User;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;
@Service
public class OrderService {
@Resource
OrderMapper orderMapper;
@Resource
RestTemplate restTemplate;
@Resource
UserClient userClient;
public Order selectById(Long id) {
//1、获取订单信息
Order order = orderMapper.selectById(id);
//2、获取http请求结果
User user = userClient.findById(order.getUserId());
//3、封装到order
order.setUser(user);
return order;
// //1、获取订单信息
// Order order = orderMapper.selectById(id);
// //2、利用RestTemplate获取http请求
// String url = "http://user/user/"+order.getUserId();
// /**
// * getForObject方法
// * 第一个参数是请求的url
// * 第二个参数是预封装的实体类
// */
// User user = restTemplate.getForObject(url, User.class);
// //3、封装到order
// order.setUser(user);
// return order;
}
}
重启服务,访问 http://localhost:8080/order/102 运行结果如下:
5.2 自定义Feign的配置
Feign可以支持很多的自定义配置,如下表所示:
类型 | 作用 | 说明 |
---|---|---|
feign.Logger.Level | 修改日志级别 | 包含四种不同的级别:NONE、BASIC、HEADERS、FULL |
feign.codec.Decoder | 响应结果的解析器 | http远程调用的结果做解析,例如解析json字符串为java对象 |
feign.codec.Encoder | 请求参数编码 | 将请求参数编码,便于通过http请求发送 |
feign. Contract | 支持的注解格式 | 默认是SpringMVC的注解 |
feign. Retryer | 失败重试机制 | 请求失败的重试机制,默认是没有,不过会使用Ribbon的重试 |
注意:一般情况下,默认值就能满足我们使用,如果要自定义时,只需要创建自定义的@Bean覆盖默认Bean即可。
一般我们需要配置的其实也就是日志级别,而日志的这四种级别分别会输出的信息也各有不同:
- NONE:不记录任何日志信息,这是默认值。
- BASIC:仅记录请求的方法,URL以及响应状态码和执行时间
- HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息
- FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据。
下面以修改Feign的日志级别为例,默认情况下Feign的日志级别为NONE,这里,我们直接实战将其修改为BASIC。
方法一、在配置文件中进行配置
配置信息如下:
情景一、假设我们希望所有的服务都采用BASIC日志级别,那么配置信息如下:
feign:
client:
config:
#default表示修改所有的服务的配置信息
default:
loggerLevel: BASIC
此时发送请求,结果如下,多了两行数据,记录了请求的方法,URL以及响应状态码和执行时间:
情景二、假设我们只针对某个服务要修改其日志级别为BASIC,那么配置信息如下:
feign:
client:
config:
#user表示只针对远程user服务时候的配置
user:
loggerLevel: BASIC
方法二、基于Java源码进行配置
如果是学习的话,记得先将yaml文件中对日志级别的配置信息先注释掉。
基于Java源码进行配置,我们需要写一个配置类如下:
package com.example.order.config;
import feign.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FeignConfig {
@Bean
public Logger.Level feignLogLevel(){
//设置日志级别为HEADERS
return Logger.Level.HEADERS;
}
}
情景一、假设我们希望所有的服务都采用HEADERS日志级别
那么我们需要修改我们启动类中,@EnableFeignClients注解如下:
@EnableFeignClients(defaultConfiguration = FeignConfig.class)
@SpringBootApplication
@MapperScan("com.example.order.mapper")
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
情景二、假设我们只针对某个服务要修改其日志级别为HEADERS
那么我们需要修改我们的Feign客户端接口中,@FeignClient注解如下:(多一个value属性指定针对的服务)
@FeignClient(value = "user", configuration = FeignConfig.class)
public interface UserClient {
@GetMapping("/user/{id}")
User findById(@PathVariable("id") Long id);
}
5.3 Feign的性能优化
Feign作为声明式的http客户端,底层发起http请求,依赖于其它的框架。其底层客户端实现包括:
-
URLConnection: 不支持连接池(部分版本的Feign默认实现)
-
Apache HttpClient :支持连接池(部分版本的Feign默认实现)
-
OKHttp:支持连接池
要想看自己的Feign默认实现的是那一个框架,我们可以在FeignClientFactoryBean文件中的loadBalance打断点。
然后查看获取到的client对象的delegate属性对应的类名。(我的Feign客户端默认使用的是ApacheHttpClient)
而对于部分默认使用URLConnection的Feign客户端,由于其不支持连接池,在性能上说实话还是有点不太好的,毕竟频繁地触发三次握手和四次挥手也是需要消耗资源的。所以,我们在性能的优化上就可以从这里下手,使用支持连接池的底层客户端实现代替我们 JDK 的 URLConnection。
这里以Spring底层的Apache HttpClient为例,只需要以下两个步骤即可:
步骤一、导入feign-httpclient依赖
<!--HttpClient-->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
步骤二、配置文件配置Feign
feign:
client:
config:
default:
loggerLevel: BASIC # 日志级别,BASIC就是基本的请求和响应信息
httpclient:
enabled: true #开启feign对httpclient的支持
max-connections: 200 #最大的连接数
max-connections-per-route: 50 #每个路径的最大连接数
此时,在FeignClientFactoryBean文件中的loadBalance打断点
然后运行我们的user和order服务,发送请求后我们可以看到,client对象他的delegate属性对应的类名是ApacheHttpClient。
5.4 Feign的最佳实践
方法一、继承,抽离远程调用的方法成一个接口(interface),让消费者的FeignClient继承该接口,并让提供者的Controller实现该接口。
这种做法的优点是:
1、简单
2、实现了代码共享。
缺点:(Spring不建议使用这种写法的原因)
1、高藕合
2、参数列表中的注解映射并不会继承,因此Controller中必须再次声明方法、参数列表、注解
方法二、抽取,将FeignClient抽取为独立的模块,并且把接口有关的POJO、默认的Feign配置都放到这个模块中,提供给消费者使用。
具体的代码实现请看:
步骤一、创建一个module,命名为feign-api。
步骤二、在feign-api中引入openfeign场景的jar包。
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
</dependencies>
步骤三、编写我们请求需要用到的实体类(例如:User),编写我们Feign的Client接口(例如:UserClient)。可能还需要Feign的配置文件(前面写过的FeignConfig)。
注意:这里,我们只需要把之前Order服务下的User.java,UserClient.java还有FeignConfig.java直接拿到feign-api这个module下就可以了。 包结构大致如下:(由于这里我写的包结构和order服务中的包结构有差别,所以,复制过来后,可能还得修改一下UserClient类导入其他类时候的包名)
在消费者服务中引入feign-api的jar包。
<dependency>
<groupId>org.example</groupId>
<artifactId>feign-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
针对本项目,我们首先得要把Order服务下导入的UserClient还有User类,改成导入com.example.api下的类(feign-api这个module的类),完事后还得删除order服务中的UserClient、User、FeignConfig这几个类或接口。
步骤五、在启动文件Application.java指定扫描包的路径。
方法一、扫描feign-api下的client包下所有的类。
@EnableFeignClients(basePackages = "com.example.api.client")
方法二、扫描feign-api下所需要的client类。
@EnableFeignClients(clients = {UserClient.class})
然后我们重启order服务,访问localhost:8080/order/103结果如下: