Ribbon & Feign
文章目录
- Ribbon & Feign
- 一:Ribbon负载均衡
- 1:介绍
- 2:ribbon的五大核心组件
- 二:Feign外部接口访问
- 1:Feign概述
- 2:Feign vs OpenFeign
- 3:使用示例
- 3.1:注解支持
- 3.2:yml配置
- 3.3:主启动类添加注解
- 3.4:消费注册中心的内容
- 3.5:自动装配接口并消费
- 4:其他控制
- 4.1:OpenFeign超时控制
- 4.2:OpenFeign日志打印功能
- 4.3:Gzip压缩(了解)
- 4.4:Http连接池(重要)
- 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:注解支持
<!--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的地址
server:port: 80# eureka相关注解
eureka:client:register-with-eureka: falsefetch-registry: trueservice-url:defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
3.3:主启动类添加注解
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
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:自动装配接口并消费
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 {@Resourceprivate 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文件中开启配置
server:port: 80
eureka:client:register-with-eureka: falsefetch-registry: trueservice-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
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配置日志级别*/@Beanpublic Logger.Level feignLoggerLevel() {// 请求和响应的头信息,请求和响应的正文及元数据return Logger.Level.FULL;}
}
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客户端
server:port: 80
eureka:client:register-with-eureka: falsefetch-registry: trueservice-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
server:compression:# 是否开启压缩enabled: true# 配置支持压缩的 MIME TYPEmime-types: text/html,text/xml,text/plain,application/xml,application/json
局部配置
开启压缩可以有效节约网络资源,但是会增加CPU压力,建议把最小压缩的文档大小适度调大一点。
feign:compression:request:# 开启请求压缩enabled: true# 配置压缩支持的 MIME TYPEmime-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:添加依赖
<!-- feign httpclient -->
<dependency><groupId>io.github.openfeign</groupId><artifactId>feign-httpclient</artifactId>
</dependency>
2:全局配置
feign:httpclient:# 开启httpclientenabled: true
3:测试验证
启动后访问http://localhost:8888/user/pojo?userName=ry
,返回正确数据表示测试通过
// 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
<!--OK http 的依赖 -->
<dependency><groupId>io.github.openfeign</groupId><artifactId>feign-okhttp</artifactId>
</dependency>
开启连接池
# 在application.yml配置文件中开启Feign的连接池功能
feign:okhttp:enabled: true # 开启OKHttp功能
5:请求拦截器RequestInterceptor
在微服务应用中,通过feign
的方式实现http
的调用
可以通过实现feign.RequestInterceptor
接口在feign
执行后进行拦截,对请求头等信息进行修改。
例如项目中利用feign
拦截器将本服务的userId
、userName
、authentication
传递给下游服务
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方法,对请求进行拦截*/@Overridepublic 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);}// 配置客户端IPrequestTemplate.header("X-Forwarded-For", IpUtils.getIpAddr());}}
}
// ----------- 客户端工具类 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请求 -> 向服务器发送请求报文