Ribbon & Feign
文章目录
- [Ribbon & Feign](#Ribbon & Feign)
-
- 一:Ribbon负载均衡
- 2:ribbon的五大核心组件
- 二:Feign外部接口访问
-
- 1:Feign概述
- [2:Feign vs OpenFeign](#2:Feign vs OpenFeign)
- 3:使用示例
- 4:其他控制
- 5:请求拦截器RequestInterceptor
- 6:Feign原理(动态代理)
- 三:调用第三方接口的坑
一:Ribbon负载均衡
大佬的你好ribbon:https://blog.csdn.net/xiaomujiang_dafangzi/category_10515853.html
1:介绍
Ribbon其实就是一个软负载均衡 的客户端组件, 他可以和其他所需请求的客户端结合使用,和eureka结合只是其中一个实例.
2:ribbon的五大核心组件
- 服务列表(全部的服务地址信息),过滤列表和更新列表(最新的注册表信息)
- ping心跳检测服务的可用性
- 负载均衡策略(轮询,随机,权重和响应速度)
二:Feign外部接口访问
1:Feign概述
Feign
Feign 的英文表意为"假装,伪装,变形", 是一个http请求调用的轻量级框架,可以以Java接口注解的方式调用Http请求,而不用像Java中通过封装HTTP请求报文的方式直接调用。Feign通过处理注解,将请求模板化,当实际调用的时候,传入参数,根据参数再应用到请求上,进而转化成真正的请求,这种请求相对而言比较直观。
Feign被广泛应用在Spring Cloud 的解决方案中,是学习基于Spring Cloud 微服务架构不可或缺的重要组件。
Feign 是Spring Cloud Netflix
组件中的一量级Restful
的 HTTP 服务客户端,实现了负载均衡和 Rest 调用的开源框架
Feign 封装了Ribbon
和RestTemplate
, 实现了WebService
的面向接口编程,进一步降低了项目的耦合度。
什么是服务调用
服务调用就是服务之间的接口互相调用,在微服务架构中很多功能都需要调用多个服务才能完成某一项功能。
为什么要使用Feign
Feign 旨在使编写 JAVA HTTP 客户端变得更加简单,Feign 简化了RestTemplate
代码,实现了Ribbon
负载均衡,使代码变得更加简洁,也少了客户端调用的代码
使用 Feign 实现负载均衡是首选方案,只需要你创建一个接口,然后在上面添加注解即可。
Feign 是声明式服务调用组件,其核心就是:像调用本地方法一样调用远程方法,无感知远程 HTTP 请求。
2:Feign vs OpenFeign
Feign 内置了Ribbon
,用来做客户端负载均衡调用服务注册中心的服务,使用 Feign 的注解定义接口,然后调用这个接口,就可以调用服务注册中心的服务
Feign 本身并不支持 Spring MVC 的注解,它有一套自己的注解,为了更方便的使用 Spring Cloud 孵化了OpenFeign
。
OpenFeigin还支持了 Spring MVC 的注解,如@RequestMapping
,@PathVariable
等等。
OpenFeign 的@FeignClient
可以解析 Spring MVC 的 @RequestMapping
注解下的接口,通过动态代理方式产生实现类,实现类中做负载均衡调用服务。
3:使用示例
3.1:注解支持
xml
<!--Feign相关-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-feign</artifactId>
<version>1.4.7.RELEASE</version>
</dependency>
<!-- openFeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
3.2:yml配置
声明eureka-server的地址
yaml
server:
port: 80
# eureka相关注解
eureka:
client:
register-with-eureka: false
fetch-registry: true
service-url:
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
3.3:主启动类添加注解
java
package com.atguigu.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableFeignClients // <-
public class OrderFeignMain80 {
public static void main(String[] args) {
SpringApplication.run(OrderFeignMain80.class,args);
}
}
3.4:消费注册中心的内容
微服务调用接口 + @FeignClient
java
package com.atguigu.springcloud.service;
import com.atguigu.springcloud.entities.CommonResult;
import com.atguigu.springcloud.entities.Payment;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
@Component
@FeignClient(value = "CLOUD-PAYMENT-SERVICE")
public interface PaymentFeignService {
/**
* 通过主键查询单条数据
*
* @param id 主键
* @return 单条数据
*/
@GetMapping("payment/selectOne/{id}")
CommonResult<Payment> selectOne(@PathVariable("id") Long id);
@GetMapping("payment/feign/timeout")
String getFeignTimeOut();
}
3.5:自动装配接口并消费
java
package com.atguigu.springcloud.controller;
import com.atguigu.springcloud.entities.CommonResult;
import com.atguigu.springcloud.entities.Payment;
import com.atguigu.springcloud.service.PaymentFeignService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
@RequestMapping("order")
public class OrderFeignController {
@Resource
private PaymentFeignService paymentFeignService;
@GetMapping("selectOne/{id}")
public CommonResult<Payment> selectOne(@PathVariable("id") Long id){
return paymentFeignService.selectOne(id);
}
@GetMapping("/feign/timeout")
public String getFeignTimeOut() {
return paymentFeignService.getFeignTimeOut();
}
}
4:其他控制
4.1:OpenFeign超时控制
默认的,feign客户端只等待1s,如果超过1s,将会直接报错,因此有时需要设置超时控制
要在yml文件中开启配置
yaml
server:
port: 80
eureka:
client:
register-with-eureka: false
fetch-registry: true
service-url:
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
# 设置feign客户端超时时间(OpenFeign默认支持ribbon)
ribbon:
# 指的是建立连接所用的时间,适用于网络状态正常的情况下,两端连接所用的时间
ReadTimeout: 5000
# 指的是建立连接后从服务器读取到可用资源所用的时间
ConnectTimeout: 5000
4.2:OpenFeign日志打印功能
Feign提供了日志打印功能,可以调整对应打印的日志的级别,从而了解对http请求的细节
说白了就是对Feign接口的调用情况进行监控和输出
日志级别
- None -> 默认的,不返回任何的日志
- Basic -> 仅仅记录请求的方法,URL,相应状态码和执行的时间
- Headers -> 除了Basic中记录的信息,还有请求和响应的头信息
- Full -> 除了Headers中定义的信息之外,还有请求和响应的正文及元数据
配置bean
java
package com.atguigu.springcloud.config;
import feign.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* OpenFeignClient配置
**/
@Configuration
public class FeignConfig {
/**
* feignClient配置日志级别
*/
@Bean
public Logger.Level feignLoggerLevel() {
// 请求和响应的头信息,请求和响应的正文及元数据
return Logger.Level.FULL;
}
}
java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import feign.Logger;
// ====== 在客户端接口指定此配置 ======
/**
* 用户服务
*/
@FeignClient(contextId = "remoteUserService",
value = ServiceNameConstants.SYSTEM_SERVICE,
fallbackFactory = RemoteUserFallbackFactory.class, // 失败策略
configuration = FeignConfig.class) // 执行上面的配置
public interface RemoteUserService {
}
YML文件里需要开启日志的Feign客户端
yaml
server:
port: 80
eureka:
client:
register-with-eureka: false
fetch-registry: true
service-url:
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
# 设置feign客户端超时时间(OpenFeign默认支持ribbon)
ribbon:
# 指的是建立连接所用的时间,适用于网络状态正常的情况下,两端连接所用的时间
ReadTimeout: 5000
# 指的是建立连接后从服务器读取到可用资源所用的时间
ConnectTimeout: 5000
logging:
level:
# feign日志以什么级别监控哪个接口
com.atguigu.springcloud.service.PaymentFeignService: debug
4.3:Gzip压缩(了解)
gzip是一种数据格式,采用deflate算法压缩数据。gzip大约可以帮我们减少70%以上的文件大小。
全局配置Gzip
yaml
server:
compression:
# 是否开启压缩
enabled: true
# 配置支持压缩的 MIME TYPE
mime-types: text/html,text/xml,text/plain,application/xml,application/json
局部配置
开启压缩可以有效节约网络资源,但是会增加CPU压力,建议把最小压缩的文档大小适度调大一点。
yaml
feign:
compression:
request:
# 开启请求压缩
enabled: true
# 配置压缩支持的 MIME TYPE
mime-types: text/xml,application/xml,application/json
# 配置压缩数据大小的下限
min-request-size: 2048
response:
# 开启响应压缩
enabled: true
4.4:Http连接池(重要)
两台服务器建立HTTP
连接的过程涉及到多个数据包的交换,很消耗时间。采用HTTP连接池可以节约大量的时间提示吞吐量。
Feign的HTTP客户端支持3种框架:HttpURLConnection
、HttpClient
、OkHttp
。
默认是采用java.net.HttpURLConnection
,每次请求都会建立、关闭连接
为了性能考虑,可以引入httpclient、okhttp作为底层的通信框架。
例如将Feign的HTTP客户端工具修改为
HttpClient
。
1:添加依赖
xml
<!-- feign httpclient -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
2:全局配置
yaml
feign:
httpclient:
# 开启httpclient
enabled: true
3:测试验证
启动后访问http://localhost:8888/user/pojo?userName=ry
,返回正确数据表示测试通过
java
// RemoteUserService FeignClient
@GetMapping("/user/pojo")
public Object selectUser(SysUser user);
// 消费端
@Autowired
private RemoteUserService remoteUserService;
@GetMapping("/user/pojo")
public Object UserInfo(SysUser user) {
return remoteUserService.selectUser(user);
}
// 服务端
@GetMapping("/pojo")
public R<SysUser> selectUser(@RequestBody SysUser user) {
return R.ok(userService.selectUserByUserName(user.getUserName()));
}
例如将Feign的HTTP客户端工具修改为
OkHTTP
xml
<!--OK http 的依赖 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>
开启连接池
yaml
# 在application.yml配置文件中开启Feign的连接池功能
feign:
okhttp:
enabled: true # 开启OKHttp功能
5:请求拦截器RequestInterceptor
在微服务应用中,通过feign
的方式实现http
的调用
可以通过实现feign.RequestInterceptor
接口在feign
执行后进行拦截,对请求头等信息进行修改。
例如项目中利用feign
拦截器将本服务的userId
、userName
、authentication
传递给下游服务
java
package com.ruoyi.common.security.feign;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Component;
import com.ruoyi.common.core.constant.CacheConstants;
import com.ruoyi.common.core.utils.ServletUtils;
import com.ruoyi.common.core.utils.StringUtils;
import feign.RequestInterceptor;
import feign.RequestTemplate;
/**
* feign 请求拦截器,实现feign.RequestInterceptor
*
* @author ruoyi
*/
@Component
public class FeignRequestInterceptor implements RequestInterceptor {
/**
* 要实现apply方法,对请求进行拦截
*/
@Override
public void apply(RequestTemplate requestTemplate) {
// 获取http servlet请求
HttpServletRequest httpServletRequest = ServletUtils.getRequest();
if (StringUtils.isNotNull(httpServletRequest)) {
Map<String, String> headers = ServletUtils.getHeaders(httpServletRequest);
// 传递用户信息请求头,防止丢失
String userId = headers.get(SecurityConstants.DETAILS_USER_ID);
if (StringUtils.isNotEmpty(userId)) {
requestTemplate.header(SecurityConstants.DETAILS_USER_ID, userId);
}
String userKey = headers.get(SecurityConstants.USER_KEY);
if (StringUtils.isNotEmpty(userKey)) {
requestTemplate.header(SecurityConstants.USER_KEY, userKey);
}
String userName = headers.get(SecurityConstants.DETAILS_USERNAME);
if (StringUtils.isNotEmpty(userName)) {
requestTemplate.header(SecurityConstants.DETAILS_USERNAME, userName);
}
String authentication = headers.get(SecurityConstants.AUTHORIZATION_HEADER);
if (StringUtils.isNotEmpty(authentication)) {
requestTemplate.header(SecurityConstants.AUTHORIZATION_HEADER, authentication);
}
// 配置客户端IP
requestTemplate.header("X-Forwarded-For", IpUtils.getIpAddr());
}
}
}
java
// ----------- 客户端工具类 ServletUtils 如下 -----------------
package com.ruoyi.common.core.utils;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.util.LinkedCaseInsensitiveMap;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import com.alibaba.fastjson2.JSON;
import com.ruoyi.common.core.constant.Constants;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.text.Convert;
import reactor.core.publisher.Mono;
/**
* 客户端工具类
*
* @author ruoyi
*/
public class ServletUtils {
/**
* 获取String参数
*/
public static String getParameter(String name) {
return getRequest().getParameter(name);
}
/**
* 获取String参数
*/
public static String getParameter(String name, String defaultValue) {
return Convert.toStr(getRequest().getParameter(name), defaultValue);
}
/**
* 获取Integer参数
*/
public static Integer getParameterToInt(String name) {
return Convert.toInt(getRequest().getParameter(name));
}
/**
* 获取Integer参数
*/
public static Integer getParameterToInt(String name, Integer defaultValue) {
return Convert.toInt(getRequest().getParameter(name), defaultValue);
}
/**
* 获取Boolean参数
*/
public static Boolean getParameterToBool(String name) {
return Convert.toBool(getRequest().getParameter(name));
}
/**
* 获取Boolean参数
*/
public static Boolean getParameterToBool(String name, Boolean defaultValue) {
return Convert.toBool(getRequest().getParameter(name), defaultValue);
}
/**
* 获得所有请求参数
*
* @param request 请求对象{@link ServletRequest}
* @return Map
*/
public static Map<String, String[]> getParams(ServletRequest request) {
final Map<String, String[]> map = request.getParameterMap();
return Collections.unmodifiableMap(map);
}
/**
* 获得所有请求参数
*
* @param request 请求对象{@link ServletRequest}
* @return Map
*/
public static Map<String, String> getParamMap(ServletRequest request) {
Map<String, String> params = new HashMap<>();
for (Map.Entry<String, String[]> entry : getParams(request).entrySet()) {
params.put(entry.getKey(), StringUtils.join(entry.getValue(), ","));
}
return params;
}
/**
* 获取request
*/
public static HttpServletRequest getRequest() {
try {
return getRequestAttributes().getRequest();
} catch (Exception e) {
return null;
}
}
/**
* 获取response
*/
public static HttpServletResponse getResponse() {
try {
return getRequestAttributes().getResponse();
} catch (Exception e) {
return null;
}
}
/**
* 获取session
*/
public static HttpSession getSession() {
return getRequest().getSession();
}
/**
* 获取请求属性
*/
public static ServletRequestAttributes getRequestAttributes() {
try {
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
return (ServletRequestAttributes) attributes;
} catch (Exception e) {
return null;
}
}
/**
* 通过请求信息和key获取指定key的请求头
*/
public static String getHeader(HttpServletRequest request, String name) {
String value = request.getHeader(name);
if (StringUtils.isEmpty(value)) {
return StringUtils.EMPTY;
}
return urlDecode(value);
}
/**
* 通过请求信息获取请求头
*/
public static Map<String, String> getHeaders(HttpServletRequest request) {
Map<String, String> map = new LinkedCaseInsensitiveMap<>();
Enumeration<String> enumeration = request.getHeaderNames();
if (enumeration != null) {
while (enumeration.hasMoreElements()) {
String key = enumeration.nextElement();
String value = request.getHeader(key);
map.put(key, value);
}
}
return map;
}
/**
* 将字符串渲染到客户端
*
* @param response 渲染对象
* @param string 待渲染的字符串
*/
public static void renderString(HttpServletResponse response, String string) {
try {
response.setStatus(200);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().print(string);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 是否是Ajax异步请求
*
* @param request
*/
public static boolean isAjaxRequest(HttpServletRequest request) {
String accept = request.getHeader("accept");
if (accept != null && accept.contains("application/json")) {
return true;
}
String xRequestedWith = request.getHeader("X-Requested-With");
if (xRequestedWith != null && xRequestedWith.contains("XMLHttpRequest")) {
return true;
}
String uri = request.getRequestURI();
if (StringUtils.inStringIgnoreCase(uri, ".json", ".xml")) {
return true;
}
String ajax = request.getParameter("__ajax");
return StringUtils.inStringIgnoreCase(ajax, "json", "xml");
}
/**
* 内容编码
*
* @param str 内容
* @return 编码后的内容
*/
public static String urlEncode(String str) {
try {
return URLEncoder.encode(str, Constants.UTF8);
} catch (UnsupportedEncodingException e) {
return StringUtils.EMPTY;
}
}
/**
* 内容解码
*
* @param str 内容
* @return 解码后的内容
*/
public static String urlDecode(String str) {
try {
return URLDecoder.decode(str, Constants.UTF8);
} catch (UnsupportedEncodingException e) {
return StringUtils.EMPTY;
}
}
/**
* 设置webflux模型响应
*
* @param response ServerHttpResponse
* @param value 响应内容
* @return Mono<Void>
*/
public static Mono<Void> webFluxResponseWriter(ServerHttpResponse response, Object value) {
return webFluxResponseWriter(response, HttpStatus.OK, value, R.FAIL);
}
/**
* 设置webflux模型响应
*
* @param response ServerHttpResponse
* @param code 响应状态码
* @param value 响应内容
* @return Mono<Void>
*/
public static Mono<Void> webFluxResponseWriter(ServerHttpResponse response, Object value, int code) {
return webFluxResponseWriter(response, HttpStatus.OK, value, code);
}
/**
* 设置webflux模型响应
*
* @param response ServerHttpResponse
* @param status http状态码
* @param code 响应状态码
* @param value 响应内容
* @return Mono<Void>
*/
public static Mono<Void> webFluxResponseWriter(ServerHttpResponse response, HttpStatus status, Object value, int code) {
return webFluxResponseWriter(response, MediaType.APPLICATION_JSON_VALUE, status, value, code);
}
/**
* 设置webflux模型响应
*
* @param response ServerHttpResponse
* @param contentType content-type
* @param status http状态码
* @param code 响应状态码
* @param value 响应内容
* @return Mono<Void>
*/
public static Mono<Void> webFluxResponseWriter(ServerHttpResponse response, String contentType, HttpStatus status, Object value, int code) {
response.setStatusCode(status);
response.getHeaders().add(HttpHeaders.CONTENT_TYPE, contentType);
R<?> result = R.fail(code, value.toString());
DataBuffer dataBuffer = response.bufferFactory().wrap(JSON.toJSONString(result).getBytes());
return response.writeWith(Mono.just(dataBuffer));
}
}
6:Feign原理(动态代理)
- 基于面向接口的动态代理方式生成实现类:request bean -> target -> Contract
- 根据接口类的注解声明规则,解析出底层的MethodHandler
- 基于reqeust bean -> encode -> 请求
- 拦截器对请求进行装饰 -> 记录日志
- 基于重试器发送http请求 -> 向服务器发送请求报文