Spring Cloud 04 - 负载均衡和外部服务访问

Ribbon & 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 封装了RibbonRestTemplate, 实现了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种框架:HttpURLConnectionHttpClientOkHttp

默认是采用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拦截器将本服务的userIduserNameauthentication传递给下游服务

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原理(动态代理)

  1. 基于面向接口的动态代理方式生成实现类:request bean -> target -> Contract
  2. 根据接口类的注解声明规则,解析出底层的MethodHandler
  3. 基于reqeust bean -> encode -> 请求
  4. 拦截器对请求进行装饰 -> 记录日志
  5. 基于重试器发送http请求 -> 向服务器发送请求报文

三:调用第三方接口的坑

相关推荐
励碼1 小时前
解决 Sentinel 控制台无法显示 OpenFeign 资源的问题
spring boot·spring cloud·sentinel·bug·openfeign
卓怡学长12 小时前
w200基于spring boot的个人博客系统的设计与实现
java·数据库·spring boot·后端·spring·intellij-idea
zhyhgx12 小时前
【Spring】Spring MVC入门(一)
java·spring·mvc
唐青枫12 小时前
Spring 的 ResponseEntity 包装器使用详解
java·spring boot·spring
莫问alicia13 小时前
苍穹外卖 项目记录 day11 Spring Task订单定时处理-来单提醒-客户催单
java·数据库·spring boot·python·spring·mybatis
安清h16 小时前
【基于SprintBoot+Mybatis+Mysql】电脑商城项目之上传头像和新增收货地址
数据库·后端·mysql·spring·mybatis
星星点点洲19 小时前
【Kubernetes的SpringCloud最佳实践】有Service是否还需要Eureka?
spring cloud·eureka·kubernetes
_周游20 小时前
【Spring】_SpringBoot的yml配置文件的读取
java·spring boot·spring
楠枬1 天前
WebSocket
websocket·网络协议·spring