SpringCloud之服务发现

1.服务提供者接口

  • 以获取一个订单接口为例,order服务对user服务进行远程调用获取user基本信息
  • user服务提供的接口如下:
package com.acx.controller;

import com.acx.pojo.vo.ActorInfoVO;
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;

@RestController
@RequestMapping("user")
public class UserController {

    @GetMapping("getUser/{id}")
    public ActorInfoVO getActor(@PathVariable("id") int id) {
        if ((id % 2) == 1) {
            ActorInfoVO actorInfoVO = new ActorInfoVO();
            actorInfoVO.setAge(34);
            actorInfoVO.setGender("男");
            actorInfoVO.setHead("http:localhost:8080/head/");
            actorInfoVO.setNickname("别名:杀马特");
            actorInfoVO.setUsername("张三");
            return actorInfoVO;
        }
        return null;
    }
    
}

2.RestTemplate

2.1.配置RestTemplate实例
package com.acx.config;

import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class RestTemplateConfig {

    @Bean
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }

}
2.2.远程调用业务接口
package com.acx.controller;

import com.acx.pojo.vo.ActorInfoVO;
import com.acx.pojo.vo.OrderInfoVO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
@RequestMapping("order")
public class OrderController {

    private static final Logger logger = LoggerFactory.getLogger(OrderController.class);

    @Autowired
    private RestTemplate restTemplate;

    @GetMapping("getOne")
    public OrderInfoVO getOne() {
        logger.info("开始查询订单");
        OrderInfoVO orderInfoVO = new OrderInfoVO();
        orderInfoVO.setOrderName("订单123");
        orderInfoVO.setOrderSn("046b399937ad4271bcd5ed275f2b4682");
        orderInfoVO.setProductName("商品123");
        orderInfoVO.setProductNum(23);
        int userId = 1;
        String getUserUrl = "http://127.0.0.1:8083/user/getUser/" + userId;
        //服务发现
        ActorInfoVO actor = restTemplate.getForObject(getUserUrl, ActorInfoVO.class);
        orderInfoVO.setUser(actor);
        return orderInfoVO;
    }
    
}
2.3.前端调用获取订单接口
  • http://localhost:8081/order/getOne
{
	"orderSn": "046b399937ad4271bcd5ed275f2b4682",
	"orderName": "订单123",
	"productNum": 23,
	"productName": "商品123",
	"user": {
		"username": "张三",
		"nickname": "别名:杀马特",
		"head": "http:localhost:8080/head/",
		"age": 34,
		"gender": "男"
	}
}

3.RestTemplate优化

  • 前面我们order服务远程调用user接口时使用的是http://127.0.0.1:8083这种硬编码的方式进行调用的。
  • 硬编码远程调用会存在不灵活的问题,比如user服务部署了多个节点,如user8083、user8084、user8085。如果仍然使用http://127.0.0.1:8083的方式来进行远程调用,则请求就只会命中到user8083这个服务器上面。
  • 解决方案:将http://127.0.0.1:8083替换成http://user-service/ 并且配置远程调用负载均衡策略
3.1.RestTemplate实例加上@LoadBalanced标签配置
@Configuration
public class RestTemplateConfig {

    @Bean
    @LoadBalanced //开启ribbon负载均衡策略
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }

}
3.2.更改order服务远程调用接口
package com.acx.controller;

import com.acx.pojo.vo.ActorInfoVO;
import com.acx.pojo.vo.OrderInfoVO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
@RequestMapping("order")
public class OrderController {

    private static final Logger logger = LoggerFactory.getLogger(OrderController.class);

    @Autowired
    private RestTemplate restTemplate;

    @GetMapping("getOne")
    public OrderInfoVO getOne() {
        logger.info("开始查询订单");
        OrderInfoVO orderInfoVO = new OrderInfoVO();
        orderInfoVO.setOrderName("订单123");
        orderInfoVO.setOrderSn("046b399937ad4271bcd5ed275f2b4682");
        orderInfoVO.setProductName("商品123");
        orderInfoVO.setProductNum(23);
        int userId = 1;
//        String getUserUrl = "http://127.0.0.1:8083/user/getUser/" + userId;
        String getUserUrl = "http://user-service/user/getUser/" + userId;
        //服务发现
        ActorInfoVO actor = restTemplate.getForObject(getUserUrl, ActorInfoVO.class);
        orderInfoVO.setUser(actor);
        return orderInfoVO;
    }

}
3.3.user服务开启多实例测试
  • 使用idea开始多个user服务实例以后,如user8083、user8084、user8085,然后我们多请求几次order接口,发现order接口调用user服务器的请求均匀的路由到了每个节点之上。测试请自己实现。
3.4.微服务调用流程
  • 远程调用发起方会向eureka注册中心拉取当前注册服务列表,然后根据服务名找到目标服务的所有服务节点
  • 找到目标服务所有节点以后,服务发起方会通过ribbon负载均衡策略选择具体调用哪一个目标服务节点。

在这里插入图片描述

4.Ribbon负载均衡

4.1.Ribbon核心类:LoadBalancerInterceptor
  • 最重要的是loadBalancer.execute方法
public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {

	private LoadBalancerClient loadBalancer;

	private LoadBalancerRequestFactory requestFactory;

	public LoadBalancerInterceptor(LoadBalancerClient loadBalancer,
			LoadBalancerRequestFactory requestFactory) {
		this.loadBalancer = loadBalancer;
		this.requestFactory = requestFactory;
	}

	public LoadBalancerInterceptor(LoadBalancerClient loadBalancer) {
		// for backwards compatibility
		this(loadBalancer, new LoadBalancerRequestFactory(loadBalancer));
	}

	@Override
	public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
			final ClientHttpRequestExecution execution) throws IOException {
		final URI originalUri = request.getURI();
		String serviceName = originalUri.getHost();
		Assert.state(serviceName != null,
				"Request URI does not contain a valid hostname: " + originalUri);
		//执行负载均衡核心方法
        return this.loadBalancer.execute(serviceName,
				this.requestFactory.createRequest(request, body, execution));
	}

}
4.2. LoadBalancerClient.execute方法
  • 最核心的方法就是:getServer方法,实际上就是调用的ILoadBalancer.chooseServer方法,而执行这个方法实际上是执行其子类BaseLoadBalancer.chooseServer方法。
public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
        return this.execute(serviceId, (LoadBalancerRequest)request, (Object)null);
    }

    public <T> T execute(String serviceId, LoadBalancerRequest<T> request, Object hint) throws IOException {
        ILoadBalancer loadBalancer = this.getLoadBalancer(serviceId);
        //根据服务名称获取具体的服务节点对象
        Server server = this.getServer(loadBalancer, hint);
        if (server == null) {
            throw new IllegalStateException("No instances available for " + serviceId);
        } else {
            RibbonLoadBalancerClient.RibbonServer ribbonServer = new RibbonLoadBalancerClient.RibbonServer(serviceId, server, this.isSecure(server, serviceId), this.serverIntrospector(serviceId).getMetadata(server));
            return this.execute(serviceId, (ServiceInstance)ribbonServer, (LoadBalancerRequest)request);
        }
    }

    protected Server getServer(ILoadBalancer loadBalancer, Object hint) {
        return loadBalancer == null ? null : loadBalancer.chooseServer(hint != null ? hint : "default");
    }

4.3.BaseLoadBalancer.chooseServer方法
  • 最重要的方法就是rule.choose(key)方法,即IRule类,这里springboot默认给的是轮询RoundRobinRule。
  • 备注:在更高版本的spring cloud和springboot里面,ribbon默认的负载均衡策略是ZoneAvoidanceRule。即使用Zone对服务器分类,这个Zone可以理解为一个机房/区域,会先选择一个Zone,然后对这个Zone内的服务器进行轮询。
 protected IRule rule = DEFAULT_RULE;
 private final static IRule DEFAULT_RULE = new RoundRobinRule(); //轮询

public Server chooseServer(Object key) { 
        if (counter == null) {
            counter = createCounter();
        }
        counter.increment();
        if (rule == null) {
            return null;
        } else {
            try {
                //通过路由规则选择节点
                return rule.choose(key);
            } catch (Exception e) {
                logger.warn("LoadBalancer [{}]:  Error choosing server for key {}", name, key, e);
                return null;
            }
        }
    }

4.4.IRule类
  • 它有一下几个子类:
ZoneAvoidanceRule: 按区域轮询策略
RoundRobinRule: 轮询
RandomRule: 随机轮询
RetryRule: 重试轮询

4.5.配置负载均衡策略
  • 因为我们现在使用的这个版本的springboot和springcloud的负载均衡策略默认就是轮询的,如果我们想配置其余的负载均衡策略,也可以自定义配置文件就行了
  • 全局配置:即调用者调用服务A、B、C等提供者接口都采用此负载均衡策略
//方式一
@Bean
public IRule retryRule(){
    return new RetryRule();
}

ribbon: # 方式二
  NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RetryRule

  • 局部配置:即调用者服务给单独的服务配置负载均衡的策略,如下只对user-service服务有效:
userservice:
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RetryRule

4.6.Ribbon饥饿加载
  • Ribbon默认采用的是懒加载,即第一次访问时才会去创建LoadBalancerClient对象,第一次请求时间会很长
  • 饥饿加载:项目启动时就会创建LoadBalancerClient对象
  • 开启饥饿加载:
ribbon:
	eager-load:
		enable: true # 开启饥饿加载
		clients: 
		    - user-service # 指明主要饥饿加载的服务

5.OpenFeign

5.1.为什么要用OpenFeign替换RestTemplate

OpenFeign是一种声明式的HTTP客户端,而RestTemplate具有代码可读性差、调用参数复杂难维护等缺点。故选择OpenFeign更适合我们的微服务远程调用场景。

5.2.OpenFeign实践

步骤一:Order服务调用方引入OpenFeign的依赖

 <dependency>
     <groupId>org.springframework.cloud</groupId>
     <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

步骤二:在启动类上加上@EnableFeignClients标签开启Feign功能

package com.acx;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableFeignClients
public class OrderApplication {

    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class, args);
    }

}

步骤三:编写Feign客户端接口代码:使用@FeignClient()指明调用那个服务,接口按照spring mvc写就行。

package com.acx.client;

import com.acx.pojo.vo.ActorInfoVO;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@FeignClient("USER-SERVICE")
public interface UserClient {

    @GetMapping("/user/getUser/{id}")
    ActorInfoVO getUser(@PathVariable("id") int id);

}

最后:在业务代码里面使用Feign接口进行User服务调用

package com.acx.controller;

import com.acx.client.UserClient;
import com.acx.pojo.vo.ActorInfoVO;
import com.acx.pojo.vo.OrderInfoVO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
@RequestMapping("order")
public class OrderController {

    private static final Logger logger = LoggerFactory.getLogger(OrderController.class);

    @Autowired
    private RestTemplate restTemplate;

    @Autowired
    private UserClient userClient;

    @GetMapping("getOne")
    public OrderInfoVO getOne() {
        logger.info("开始查询订单");
        OrderInfoVO orderInfoVO = new OrderInfoVO();
        orderInfoVO.setOrderName("订单123");
        orderInfoVO.setOrderSn("046b399937ad4271bcd5ed275f2b4682");
        orderInfoVO.setProductName("商品123");
        orderInfoVO.setProductNum(23);
        int userId = 1;
//        String getUserUrl = "http://127.0.0.1:8083/user/getUser/" + userId;
//        String getUserUrl = "http://user-service/user/getUser/" + userId;
        //服务发现
//        ActorInfoVO actor = restTemplate.getForObject(getUserUrl, ActorInfoVO.class);
        ActorInfoVO actor = userClient.getUser(userId);
        orderInfoVO.setUser(actor);
        return orderInfoVO;
    }

}

5.3.Feign的自定义配置

主要的可配置项

配置项说明
Level:日志级别有4中级别:NONE(不输出远程调用日志) BASIC(只输出请求URL和响应状态码及请求 时间) HEADERS(将BASIC信息和请求头信息输出), FULL(输出完成的请求)
Decoder:响应结果解析器默认使用SpringDecoder解码器,会调用Spring MVC 中的消息转换器HttpMessageConverter进行解码。
Encoder:请求参数编码器默认使用SpringEncoder编码器,它会调用Spring MVC 中的消息转换器(HttpMessageConverter)进行编码
Contract:契约配置OpenFeign中默认使用的是springmvc的注解
Retryer:失败重试机制默认没有重试机制,可以使用Ribbon配置重置机制

文件方式配置

  • 以设置level等级为例:一般采用basic或者none即可,这样日志少,OpenFeign的性能就越高
  • 配置全局生效:即所有被调用的服务都生效
feign:
  client:
    config:
      default: # 全局
        loggerLevel: FULL

  • 配置局部生效:
feign:
  client:
    config:
      userservice: # 局部生效 userservice
        loggerLevel: FULL

代码方式配置

  • 编写配置代码
package com.acx.config;

import feign.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FeignConfig {

    @Bean
    public Logger.Level feignLevel() {
        return Logger.Level.FULL;
    }

}

  • 配置全局生效:@EnableFeignClients更换默认配置;这样配置是全局生效的,即不管Order服务调用那个服务的接口,日志打印规则都按照FeignConfig配置的打印。
package com.acx;

import com.acx.config.FeignConfig;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableFeignClients(defaultConfiguration = FeignConfig.class)
public class OrderApplication {

    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class, args);
    }

}

  • 配置局部生效:如下@FeignClient标签里面配置user-service和feign配置类,这样这个日志配置就只对调用user-service接口时生效。
package com.acx.client;

import com.acx.config.FeignConfig;
import com.acx.pojo.vo.ActorInfoVO;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@FeignClient(value = "USER-SERVICE",configuration = FeignConfig.class)
public interface UserClient {

    @GetMapping("/user/getUser/{id}")
    ActorInfoVO getUser(@PathVariable("id") int id);

}

5.4.Feign的性能优化

Feign的底层HTTP客户端选型:

URLConnection:feign默认集成的HTTP客户端,JDK自带,不支持连接池,性能不好

Apache HttpClient:支持连接池

OKHttp:支持连接池

Feign性能优化

  • 思路:使用支持连接池的客户端替换URLConnection

  • Order服务引入依赖:这个jar包包括了Apache HttpClient和OKHttp两大客户端,选择一种即可

<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-httpclient</artifactId>
</dependency>

  • 编写Apache HttpClient配置
feign:
  httpclient:
    enabled: true # 开启feign对Apache HttpClient的支持
    max-connections: 200 # 最大的连接数
    max-connections-per-route: 50 # 每个路径的最大连接数

  • 编写OKHttp配置
feign:
  okhttp:
    enabled: true # 开启feign对Apache HttpClient的支持

5.5.Feign的最佳实践

方式一(耦合):服务提供者定义一个API接口,写一个Feign接口和Controller接口都集成此API接口,然后服务消费者引入服务提供者的依赖来调用Feign客户端。

缺点:服务提供方和服务消费方紧耦合了。参数列表中的注解映射并不会被集成,所以我们再Controller接口里面还要再次声明方法、参数列表、注解。故此方式不推荐使用。

方式二(抽取):将 FeignClient 抽取为独立模块,并且把接口有关的 POJO、默认的 Feign 配置都放到这个模块中,提供给所有消费者使用。

在这里插入图片描述

步骤一:新建feign-api包,并将前面的UserClient转移到这个包中,pom.xml配置如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>cloud-demo</artifactId>
        <groupId>com.acx</groupId>
        <version>1.0.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.acx</groupId>
    <artifactId>feign-api</artifactId>
    <version>1.0.0</version>

    <dependencies>
        <dependency>
            <groupId>com.acx</groupId>
            <artifactId>cloud-common</artifactId>
            <version>1.0.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>io.github.openfeign</groupId>
            <artifactId>feign-httpclient</artifactId>
        </dependency>
    </dependencies>

</project>

步骤二:Order项目引入feign-api包进行微服务调用

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>cloud-demo</artifactId>
        <groupId>com.acx</groupId>
        <version>1.0.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.acx</groupId>
    <artifactId>order-service</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>

    <dependencies>
        <dependency>
            <groupId>com.acx</groupId>
            <artifactId>cloud-common</artifactId>
            <version>1.0.0</version>
        </dependency>
        <dependency>
            <groupId>com.acx</groupId>
            <artifactId>feign-api</artifactId>
            <version>1.0.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
    </dependencies>
</project>

5.6.FeignClient接口扫描报错
  • 当我们写的FeignClient接口不在SpringBootApplication的扫描包范围内时,一般有两种解决方案:
  • 方案1(全局生效):在@EnableFeignClients指明basePackages为FeignClient接口所在的包,这样就能够扫描到了。此种方式全局生效的,即不管是调用哪个微服务都按照这个配置进行FeignClient接口扫描。
package com.acx;

import com.acx.config.FeignConfig;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableFeignClients(defaultConfiguration = FeignConfig.class,basePackages = "com.acx.client")
public class OrderApplication {

    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class, args);
    }

}

  • 方案2(局部生效): 在@EnableFeignClients指明具体的FeignClient字节码(clients)。这样就能扫描到UserClient接口了,但是这个不是全局的,只对UserClient包扫描不到生效。
package com.acx;

import com.acx.client.UserClient;
import com.acx.config.FeignConfig;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableFeignClients(defaultConfiguration = FeignConfig.class,clients = {UserClient.class})
public class OrderApplication {

    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class, args);
    }

}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值