spring cloud提供2种微服务调用方式:RestTemplate、Feign

RESTful

RESTful网络请求是指RESTful风格的网络请求,其中REST是Resource Representational State Transfer的缩写,直接翻译即“资源表现层状态转移”。

  • Resource代表互联网资源。所谓“资源”是网络上的一个实体,或者说网上的一个具体信息。它可以是一段文本、一首歌曲、一种服务,可以使用一个URI指向它,每种“资源”对应一个URI。
  • Representational是“表现层”意思。“资源”是一种消息实体,它可以有多种外在的表现形式,我们把“资源”具体呈现出来的形式叫作它的“表现层”。比如说文本可以用TXT格式进行表现,也可以使用XML格式、JSON格式和二进制格式;视频可以用MP4格式表现,也可以用AVI格式表现。URI只代表资源的实体,不代表它的形式。它的具体表现形式,应该由HTTP请求的头信息Accept和Content-Type字段指定,这两个字段是对“表现层”的描述。
  • State Transfer是指“状态转移”。客户端访问服务的过程中必然涉及数据和状态的转化。如果客户端想要操作服务端资源,必须通过某种手段,让服务器端资源发生“状态转移”。而这种转化是建立在表现层之上的,所以被称为“表现层状态转移”。客户端通过使用HTTP协议中的四个动词来实现上述操作,它们分别是:获取资源的GET新建或更新资源的POST、更新资源的PUT删除资源的DELETE

RestTemplate是Spring提供的同步HTTP网络客户端接口,可以简化客户端与HTTP服务器的交互,并且强制使用RESTful风格。RestTemplate会处理HTTP连接和关闭,因此用户被只需提供服务器的地址和模板参数即可。

  1. 第一个层次(Level 0)的 Web 服务只是使用 HTTP 作为传输方式,实际上只是远程方法调用(RPC)的一种具体形式。SOAP XML-RPC 都属于此类。
  2. 第二个层次(Level 1)的 Web 服务引入了资源的概念。每个资源有对应的标识符和表达。
  3. 第三个层次(Level 2)的 Web 服务使用不同的 HTTP 方法来进行不同的操作,并且使用 HTTP 状态码来表示不同的结果。如 HTTP GET 方法来获取资源,HTTP DELETE 方法来删除资源。
  4. 第四个层次(Level 3)的 Web 服务使用 HATEOAS。在资源的表达中包含了链接信息。客户端可以根据链接来发现可以执行的动作。

RestTemplate

RestTemplate常用API:

getForObject
getForEntity
postForEntity
postForLocation

@Service
public class ConsumerService {

    @Autowired
    private EurekaDiscoveryClient discoveryClient;

    @Autowired
    private RestTemplate restTemplate;

    public String test() {
        String path = servicePath("test");
        return restTemplate.getForObject(path, String.class);
    }


    public List<User> users() {
        String path = servicePath("users");
        return restTemplate.getForObject(path, List.class);
    }
    // getForObject
    public User aUser(Integer id) {
        String path = servicePath("users/" + id);
        return restTemplate.getForObject(path, User.class);
    }

    //  getForEntity
    public ResponseEntity<User> entity(Integer id) {
        String path = servicePath("users/" + id);
        return restTemplate.getForEntity(path, User.class);
    }

    public User add(String name) {
        String path = servicePath("users/add");
        ResponseEntity<User> userResponseEntity = restTemplate.postForEntity(path, name, User.class);
        User user = null;
        if (userResponseEntity.getStatusCode().value() == 200) {
            user = userResponseEntity.getBody();
        }
        return user;
    }

    public URI search(User user) {
        String path = servicePath("users/search");
        return restTemplate.postForLocation(path, user);
    }


    //----------------以下是对请求地址进行处理----------------------------------

    private URI getProviderServiceUri() {
        List<ServiceInstance> instances = discoveryClient.getInstances("provider");
        if (instances != null && instances.size() > 0) {
            // 随机获取一个实例进行访问
            int i = (int) (Math.random() * instances.size());
            ServiceInstance serviceInstance = instances.get(i);
            return serviceInstance.getUri();
        }
        return null;
    }

    private String servicePath(String url) {
        URI uri = getProviderServiceUri();
        String path = null;
        if (uri != null) {
            try {
                path = uri.toURL().toString().concat("/provider/").concat(url);
            } catch (MalformedURLException e) {
                e.printStackTrace();
            }
        }
        return path;
    }
}

image.png

自定义拦截器

在consumer端自定义拦截远程调用请求

@Component
public class LoggingClientHttpRequestInterceptor implements ClientHttpRequestInterceptor {
    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {

        System.out.println("拦截啦!!!");
        System.out.println(request.getURI());

        ClientHttpResponse response = execution.execute(request, body);

        System.out.println(response.getHeaders());
        return response;
    }
}
@Configuration
public class ComponentConfig {

    @Bean
    public RestTemplate restTemplate(LoggingClientHttpRequestInterceptor interceptor) {
        RestTemplate restTemplate = new RestTemplate();
        restTemplate.getInterceptors().add(interceptor);
        return restTemplate;
    }
}

负载均衡 Ribbon

在搭建服务集群时,需要考虑的问题之一就是负载均衡,通过实现负载使得集群负载保持在稳定高效的状态,从而提高整个服务的效率。
负载均衡策略可分为软件负载均衡和硬件负载均衡,常见如下:

  • 软件负载均衡:ngix、lvs
  • 硬件负载均衡:F5

软件负载均衡分为:服务端(集中式)、客户端

  • 服务端负载均衡:在客户端和服务端中间使用代理,nginx。
  • 客户端负载均衡:根据自身的情况做负载,ribbon。
  • 客户端负载均衡与服务端负载均衡,区别:服务端地址列表的存储位置,以及负载算法在哪里。

    • 客户端负载均衡,所有的客户单都有一份自己要访问的服务端地址列表,这些列表从服务注册中心获取。
    • 服务端负载均衡,客户端只知道单一服务代理的地址,服务代理则知道所有服务端的地址

      手动实现客户端负载均衡

      
      private URI getProviderServiceUri() {
         // 获取服务名为provider的全部服务实例
         List<ServiceInstance> instances = discoveryClient.getInstances("provider");
         //  以下为手动实现负载均衡策略——随机
         if (instances != null && instances.size() > 0) {
             // 随机获取一个实例进行访问
             int i = (int) (Math.random() * instances.size());
             ServiceInstance serviceInstance = instances.get(i);
             return serviceInstance.getUri();
         }
         return null;
      }
      
      private String servicePath(String url) {
         URI uri = getProviderServiceUri();
         String path = null;
         if (uri != null) {
             try {
                 path = uri.toURL().toString().concat("/provider/").concat(url);
             } catch (MalformedURLException e) {
                 e.printStackTrace();
             }
         }
         return path;
      }
      

      实现效果(随机):
      image.png
      image.png

      Ribbon实现客户端负载均衡

      Ribbon是Netflix客户端均衡器,为Ribbon配置服务提供者地址列表后,Ribbon可以基于某种负载均衡策略算法,自动地帮助服务消费者请求服务提供者。

  1. Ribbon可以单独使用,作为一个独立的负载均衡组件,只需手动配置服务地址列表
  2. Ribbon与Eureka配合使用,Ribbon可以自动从Eureka Server获取服务列表,并基于负载均衡算法,请求其中一个服务提供者示例
  3. Ribbon可与OpenFeign、RestTemplate进行无缝衔接,让二者具有负载均衡能力。OpenFeign默认集成了Ribbon

    Ribbon组成

    官网首页https://github.com/Netflix/ribbon
  • ribbon-core: 核心的通用性代码。api一些配置。
  • ribbon-eureka:基于eureka封装的模块,能快速集成eureka。
  • ribbon-examples:学习示例。
  • ribbon-httpclient:基于apache httpClient封装的rest客户端,集成了负载均衡模块,可以直接在项目中使用。
  • ribbon-loadbalancer:负载均衡模块。
  • ribbon-transport:基于netty实现多协议的支持。比如http,tcp,udp等。

    Ribbon负载均衡算法

  • ZoneAvoidanceRule(区域权衡策略):复合判断Server所在区域的性能和Server的可用性,轮询选择服务器。(默认)

  • BestAvailableRule(最低并发策略):会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务。逐个找服务,如果断路器打开,则忽略。
  • RoundRobinRule(轮询策略):以简单轮询选择一个服务器。按顺序循环选择一个server。
  • RandomRule(随机策略):随机选择一个服务器。
  • AvailabilityFilteringRule(可用过滤策略):会先过滤掉多次访问故障而处于断路器跳闸状态的服务和过滤并发的连接数量超过阀值得服务,然后对剩余的服务列表安装轮询策略进行访问。
  • WeightedResponseTimeRule(响应时间加权策略):据平均响应时间计算所有的服务的权重,响应时间越快服务权重越大,容易被选中的概率就越高。刚启动时,如果统计信息不中,则使用RoundRobinRule(轮询)策略,等统计的信息足够了会自动的切换到WeightedResponseTimeRule。响应时间长,权重低,被选择的概率低。反之,同样道理。此策略综合了各种因素(网络,磁盘,IO等),这些因素直接影响响应时间。
  • RetryRule(重试策略):先按照RoundRobinRule(轮询)的策略获取服务,如果获取的服务失败则在指定的时间会进行重试,进行获取可用的服务。如多次获取某个服务失败,就不会再次获取该服务。主要是在一个时间段内,如果选择一个服务不成功,就继续找可用的服务,直到超时。

    使用LoadBalancerClient

    public List<User> users2() {
      // 选择一个provider服务实例
      ServiceInstance serviceInstance = loadBalancerClient.choose("provider");
      String path = null;
      try {
          path = serviceInstance.getUri().toURL().toString().concat("/provider/users");
      } catch (MalformedURLException e) {
          e.printStackTrace();
      }
      return restTemplate.getForObject(path, List.class);
    }
    
      @Bean
      public IRule iRule() {
      return new RoundRobinRule(); // 轮询效果
          return new RandomRule(); // 随机效果
      }
    
    实现效果(轮询):
    image.png
    实现效果(随机):
    image.png

    负载均衡策略配置

    通过注解配置
    @Bean
      public IRule iRule() {
      return new RoundRobinRule(); // 轮询效果
          return new RandomRule(); // 随机效果
      }
    
    通过配置文件配置
    // 针对服务定ribbon策略
    provider.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RandomRule
    // 给所有服务定ribbon策略
    ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RandomRule
    
    注:属性配置方式优先级高于Java代码

    Ribbon 脱离Eureka

    Ribbon可以和服务注册中心Eureka一起工作,从服务注册中心获取服务端的地址信息,也可以在配置文件中使用listOfServers字段来设置服务端地址。
    ribbon.eureka.enabled=false
    ribbon.listOfServers=localhost:80,localhost:81
    

    Feign

    OpenFeign是Netflix 开发的声明式、模板化的HTTP请求客户端。可以更加便捷、优雅地调用http api。
    OpenFeign会根据带有注解的函数信息构建出网络请求的模板,在发送网络请求之前,OpenFeign会将函数的参数值设置到这些请求模板中。
    Feign主要是构建微服务消费端。只要使用OpenFeign提供的注解修饰定义网络请求的接口类,就可以使用该接口的实例发送RESTful的网络请求。还可以集成Ribbon和Hystrix,提供负载均衡和断路器。

Feign 是一个 Http 请求调用的轻量级框架,可以以 Java 接口注解的方式调用 Http 请求,而不用像 Java 中通过封装 HTTP 请求报文的方式直接调用。通过处理注解,将请求模板化,当实际调用的时候,传入参数,根据参数再应用到请求上,进而转化成真正的请求,这种请求相对而言比较直观。Feign 封装 了HTTP 调用流程,面向接口编程,回想第一节课的SOP。

Feign本身不支持Spring MVC的注解,它有一套自己的注解。
OpenFeign是Spring Cloud 在Feign的基础上支持了Spring MVC的注解,如@RequesMapping等等。OpenFeign的@FeignClient可以解析SpringMVC的@RequestMapping注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务。

Feign远程调用使用

UserApi

<dependencies>
    <dependency>
        <groupId>com.ixiaoyu2</groupId>
        <artifactId>common</artifactId>
        <version>1.0.0</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>
@RequestMapping("/user")
public interface UserApi {
    /**
     * 获取全部用户
     *
     * @return 返回用户列表
     */
    @GetMapping("/users")
    List<User> users();
}
@SpringBootApplication
public class UserApiApplication {
    public static void main(String[] args) {
        SpringApplication.run(UserApiApplication.class, args);
    }
}

UserProvider

 <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <dependency>
            <groupId>com.ixiaoyu2</groupId>
            <artifactId>user-api</artifactId>
            <version>1.0.0</version>
        </dependency>
    </dependencies>
server:
  port: 8000
spring:
  application:
    name: provider
eureka:
  instance:
    appname: provider
    lease-renewal-interval-in-seconds: 5
    lease-expiration-duration-in-seconds: 60
    metadata-map:
      microserviceId: 9
    prefer-ip-address: true
    ip-address: 127.0.0.1
  client:
    service-url:
      defaultZone: http://ixiaoyu2:ixiaoyu2@127.0.0.1:7000/eureka
    registry-fetch-interval-seconds: 5

    healthcheck:
      enabled: true

management:
  endpoint:
    shutdown:
      enabled: true #开启远程服务下线management:
  endpoints:
    web:
      exposure:
        include: '*'
@RestController
public class ProviderController implements UserApi {
    @Override
    public List<User> users() {
        List<User> list = new ArrayList<>(10);
        for (int i = 0; i < 10; i++) {
            list.add(new User(i + 1, "黑岩射手:number" + i + 1, 18 + i));
        }
        return list;
    }
}
@SpringBootApplication
@EnableEurekaClient
public class UserProviderApplication {
    public static void main(String[] args) {
        SpringApplication.run(UserProviderApplication.class, args);
    }
}

UserConcumer

<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
  </dependency>
  <dependency>
    <groupId>com.ixiaoyu2</groupId>
    <artifactId>user-api</artifactId>
    <version>1.0.0</version>
  </dependency>
</dependencies>
@FeignClient(name = "provider")
public interface UserService extends UserApi {
}
@RestController
@RequestMapping("consumer")
public class ConsumerController {

    @Autowired
    UserService userService;

    @GetMapping("/users")
    public List<User> users() {
        return userService.users();
    }
}
spring:
  application:
    name: consumer
eureka:
  client:
    service-url:
      defaultZone: http://ixiaoyu2:ixiaoyu2@127.0.0.1:7000/eureka
    healthcheck:
      enabled: true
server:
  port: 8081
@SpringBootApplication
@EnableFeignClients
public class UserConsumerApplication {
    public static void main(String[] args) {
        SpringApplication.run(UserConsumerApplication.class, args);
    }
}

注:Feign请求带参数时,需要api对应方法需要@RequestParm注解

  /**
     * 根据id获取用户
     *
     * @param id id
     * @return 用户
     */
    @GetMapping("/user")
    User oneUser(@RequestParam("id") Integer id);

    /**
     * 添加用户
     *
     * @param map 封装用户信息
     * @return 新创建的用户
     */
    @PostMapping("/user")
    User add(@RequestParam HashMap<String, String> map);
@Override
public User oneUser(Integer id) {
    System.out.println("接收的id: " + id);
    return new User(id, "Black Rock Shooter", id + 1);
}

@Override
public User add(HashMap<String, String> map) {
    System.out.println(map);
    return new User(1, map.get("name"), Integer.parseInt(map.get("age")));
}
@GetMapping("/user")
public User oneUser(Integer id) {
    return userService.oneUser(id);
}

@PostMapping("/user")
public User oneUser2(@RequestParam HashMap<String, String> map) {
    System.out.println(map);
    return userService.add(map);
}

Feign远程调用原理

  1. 主程序入口添加@EnableFeignClients注解开启对Feign Client扫描加载处理。根据Feign Client的开发规范,定义接口并加@FeignClient注解。
  2. 当程序启动时,会进行包扫描,扫描所有@FeignClient注解的类,并将这些信息注入Spring IoC容器中。当定义的Feign接口中的方法被调用时,通过JDK的代理方式,来生成具体的RequestTemplate。当生成代理时,Feign会为每个接口方法创建一个RequestTemplate对象,该对象封装了HTTP请求需要的全部信息,如请求参数名、请求方法等信息都在这个过程中确定。
  3. 然后由RequestTemplate生成Request,然后把这个Request交给client处理,这里指的Client可以是JDK原生的URLConnection、Apache的Http Client,也可以是Okhttp。最后Client被封装到LoadBalanceClient类,这个类结合Ribbon负载均衡发起服务之间的调用。

    超时设置

    Feigh远程调用超时分为远程连接超时,业务逻辑超时
    ribbon:
    # 连接超时时间(ms)
    ConnectTimeout: 1000
    # 业务逻辑超时时间(ms)
    ReadTimeout: 3000
    

    重试设置

    ribbon:
    # 同一台实例最大重试次数,不包括首次调用
    MaxAutoRetries: 2
    # 重试负载均衡其他的实例最大重试次数,不包括首次调用
    MaxAutoRetriesNextServer: 2
    # 是否所有操作都重试
    OkToRetryOnAllOperations: false
    
    使用ribbon重试机制,请求失败后,每个6秒会重新尝试

FeignClient 异常信息:Method %s not annotated with HTTP method type (ex. GET, POST)
FeignClient默认Contract不支持SpringMVC注解规范