微服务之路(九)spring cloud feign

前言

Feign是Netflix开发的声明式、模板化的HTTP客户端, Feign可以帮助我们更快捷、优雅地调用HTTP API。

在Spring Cloud中,使用Feign非常简单——创建一个接口,并在接口上添加一些注解,代码就完成了。Feign支持多种注解,例如Feign自带的注解或者JAX-RS注解等。

Spring Cloud对Feign进行了增强,使Feign支持了Spring MVC注解,并整合了Ribbon和Eureka,从而让Feign的使用更加方便。

Spring Cloud Feign是基于Netflix feign实现,整合了Spring Cloud Ribbon和Spring Cloud Hystrix,除了提供这两者的强大功能外,还提供了一种声明式的Web服务客户端定义的方式。

Spring Cloud Feign帮助我们定义和实现依赖服务接口的定义。在Spring Cloud feign的实现下,只需要创建一个接口并用注解方式配置它,即可完成服务提供方的接口绑定,简化了在使用Spring Cloud Ribbon时自行封装服务调用客户端的开发量。

Spring Cloud Feign具备可插拔的注解支持,支持Feign注解、JAX-RS注解和Spring MVC的注解。

主要议题

  • Feign基本使用
  • Ribbon整合
  • Hystrix整合
  • 问题总结

主体内容

一、Feign基本使用、

声明式Web服务客户的-Feign

声明式:接口声明、Annonation驱动

Web服务:HTTP的方式作为通讯协议

客户端:用于服务调用的存根。

Feign:原生并不是Spring Web MVC的实现,基于JAX-RS(Java REST规范)实现。Spring Cloud封装了Feign,使用支持Spring Web MVC。RestTemplate、HttpMessageConverter

RestTemplate以及Spring Web MVC可以显式的自定义HttpMessageConverter实现。

假设有一个Java接口PersonService,Feign可以将其声明它是以HTTP接口来实现接口的。

以前我们的简单架构如下:

那么下面我们将写一个简单的示例,服务配置如下。

需要的服务组件(SOA)

  • 注册中心(Eureka Server):服务发现和注册。
    • a.应用名称:spring-cloud-eureka-server
    • b.服务端口:12345
  • Feign客户端(服务消费)端:调用Feign声明接口。
    • a.应用名称:person-client
  • Feign服务(服务提供)端:不一定强制实现Feign声明接口。
    • a.应用名称:person-service
  • Feign声明接口(契约):定义一种Java强类型接口。
    • a.person-api

我们一步一步来,摒弃之前的eureka server项目,全部重新创建一下。server单独一个工程,Feign客户端+Feign服务+Feign声明接口放另一个工程。

(1)首先,还是去start.spring.io构建Eureka server项目,如下:

(2)然后创建feign工程。

(3)分别导入两个项目,其中先配置eureka server的application.properties.

spring.application.name=spring-cloud-eureka-server
server.port=12345
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false

增加springboot安全依赖

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-security</artifactId>
</dependency>

关闭安全。创建config包,包下类SecurityConfig.java。

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
     
        @Override
        public void configure(WebSecurity web) throws Exception {
            web.ignoring().antMatchers("/**");
        }
}

启动类加上@EnableEurekaServer注解

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@SpringBootApplication
@EnableEurekaServer
public class SpringcloudEurekaServerApplication {

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

启动好eureka server,先丢到一旁。

(4)下面导入feign项目,更改jar为pom,删掉原本的src。创建子模块person-api,创建包com.gupao.feign.api.domain,下面创建Peson类,这里采用lombok的@Data注解简化代码。

import lombok.Data;

/**
 * @ClassName
 * @Describe 人的模型
 * @Author 66477
 * @Date 2020/6/822:14
 * @Version 1.0
 */
@Data
public class Person {
    private Long id;
    private String name;

}

(5)创建service包,包下创建interface接口PersonService。

import com.gupao.feign.api.domain.Person;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import java.util.Collection;

/**
 * @ClassName
 * @Describe {@link Person}服务
 * @Author 66477
 * @Date 2020/6/822:25
 * @Version 1.0
 */
@FeignClient(value="person-service")//服务提供方应用的名称
public interface PersonService {
    /**
     * 保存
     * @param person {@link Person}
     * @return 如果成功,<code>true</code>
     */
    @PostMapping(value = "/person/save")
    boolean save(@RequestBody Person person);

    /**
     * 查找所有的服务
     * @return
     */
    @GetMapping(value="/person/findAll")
    Collection<Person> findAll();
}

(6)然后创建feign客户端,模块名称:person-client。随后在该模块中创建com.gupao.feign.client.controller包,与包同级创建启动类PersonClientApplication.java。内容如下:

import com.gupao.feign.api.service.PersonService;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

/**
 * @ClassName
 * @Describe TODO
 * @Author 66477
 * @Date 2020/6/822:40
 * @Version 1.0
 */
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients(clients= PersonService.class)
public class PersonClientApplication {
    public static void main(String[] args) {
        SpringApplication.run(PersonClientApplication.class,args);
    }
}

然后在controller下创建PersonClientController类,内容如下:

import com.gupao.feign.api.domain.Person;
import com.gupao.feign.api.service.PersonService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.Collection;

/**
 * @ClassName
 * @Describe {@link PersonClientController}实现{@link PersonService}
 * @Author 66477
 * @Date 2020/6/822:42
 * @Version 1.0
 */
@RestController
public class PersonClientController implements PersonService {

    private final PersonService personService;
    @Autowired
    public PersonClientController(PersonService personService) {
        this.personService = personService;
    }

    /**
     * 保存
     * @param person {@link Person}
     * @return 如果成功,<code>true</code>
     */
    public boolean save(@RequestBody Person person){
        return personService.save(person);
    }

    @Override
    public Collection<Person> findAll() {
        return personService.findAll();
    }
}

再定义application.properties文件。

spring.application.name=person-client
server.port=8080
eureka.client.service-url.defaultZone=http://localhost:12345/eureka

pom文件记得依赖person-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>springcloud-feign</artifactId>
        <groupId>com.example</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>person-client</artifactId>
    <dependencies>
        <dependency>
            <groupId>${project.groupId}</groupId>
            <artifactId>person-api</artifactId>
            <version>${project.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
    </dependencies>

</project>

打开spring boot security。同样的,创建config包,然后创建SecurityConfig类。

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
     
        @Override
        public void configure(WebSecurity web) throws Exception {
            web.ignoring().antMatchers("/**");
        }
}

(7)接下来,feign服务端,创建包com.gupao.feign.person.service.provider。包下创建PersonServiceProviderAppplication启动类。

import com.gupao.feign.api.service.PersonService;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

/**
 * @ClassName
 * @Describe {@link PersonService}提供者应用
 * @Author 66477
 * @Date 2020/6/823:00
 * @Version 1.0
 */
@SpringBootApplication
@EnableEurekaClient
public class PersonServiceProviderAppplication {
    public static void main(String[] args) {
        SpringApplication.run(PersonServiceProviderAppplication.class,args);
    }
}

创建包controller,创建服务端控制器PersonServiceProviderController类。

import com.gupao.feign.api.domain.Person;
import com.gupao.feign.api.service.PersonService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @ClassName
 * @Describe {@link PersonService} 提供者控制器(可以实现{@link PersonService}接口 )
 * @Author 66477
 * @Date 2020/6/823:01
 * @Version 1.0
 */
@RestController
public class PersonServiceProviderController {

    private Map<Long,Person> persons = new ConcurrentHashMap<>();

    /**
     * 保存
     * @param person {@link Person}
     * @return 如果成功,<code>true</code>
     */
    @PostMapping(value = "/person/save")
   public boolean save(@RequestBody Person person){
        return persons.put(person.getId(),person) == null;
    }

    /**
     * 查找所有的服务
     * @return
     */
    @GetMapping(value="/person/findAll")
    public  Collection<Person> findAll(){
        return persons.values();
    }
}

引入security依赖

  <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
  </dependency>

关闭springboot security。

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
     
        @Override
        public void configure(WebSecurity web) throws Exception {
            web.ignoring().antMatchers("/**");
        }
}

配置application.properties.

#这个名称需要和@FeignClient(value="person-service")对应
spring.application.name=person-service
server.port=9090
eureka.client.service-url.defaultZone=http://localhost:12345/eureka

(8)最终启动feign服务端,feign客户端。结果:

(9)我们开始用Postman测试peson-client->person-service。

person-api定义了@FeignClients(value="person-service"),person-service实际上是一个服务器提供方的应用名称

person-client和person-service两个应用注册到了Eureka Server。

person-client可以感知person-serivce应用存在的,并且Spring Cloud帮助解析PersonService中声明的应用名称:"person-service",因此person-client在调用person-service服务时,实际就路由到person-service的URL。

浏览器访问http://localhost:8080/person/findAll

二、Ribbon整合

官方文档https://cloud.spring.io/spring-cloud-static/spring-cloud-netflix/2.2.0.RELEASE/reference/html/#spring-cloud-ribbon

1.关闭Eureka注册

(1)调整person-client,关闭Eureka(这个貌似没用)

#整合ribbon-关闭Eureka
ribbon.eureka.enabled=false

我直接采用的下面这个方式直接关闭eureka注册

#整合ribbon-去除eureka注册
eureka.client.register-with-eureka=false
# 不获取注册列表信息, 是否从eureka服务器获取注册信息 , false = 不获取,true = 获取
eureka.client.fetch-registry: false

(2)定义person-client服务ribbon的服务列表(服务名称:person-service)

#整合ribbon-配置“person-service"的负载均衡的服务器列表(写两遍是为了证明它是可以多配置的)
person-service.ribbon.listOfServers:http://localhost:9090《http://localhost:9090

注意:要想完全取消Eureka注册,只需要将person-clien启动类上的@EnableEurekaClient注释掉。(这种视频中讲到,但是我本地没有实现,不是道是不是这个版本是否有升级变动)

2.实现ribbon规则

  • 核心规则接口

    • IRule
      • 随机规则:RandomRule
      • 最可用规则:BestAvailableRule
      • 轮询规则:RoundRobinRule
      • 重试实现:RetryRule
      • 客户端配置:ClientConfigEnabledRoundRobinRule
      • 可用性过滤规则:AvailabilityFilterRule
      • RT权重规则:WeightedResponseTimeRule
      • 规避区域规则:ZoneAvoidanceRule
      • ...

(1)比如,我们举例第一个自定义策略。首先,我们进入RandomRule源码,发现它继承的是 AbstractLoadBalancerRule。那我们也在person-client下创建一个ribbon包,包里创建一个类FirstServerForverRule(意思为永远获取第一台服务器)继承 AbstractLoadBalancerRule抽象类。重写它的方法。

import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.AbstractLoadBalancerRule;
import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.Server;
import java.util.List;

/**
 * @ClassName
 * @Describe 自定义实现 {@link com.netflix.loadbalancer.IRule}
 * @Author 66477
 * @Date 2020/6/922:39
 * @Version 1.0
 */
public class FirstServerForverRule extends AbstractLoadBalancerRule {

    @Override
    public void initWithNiwsConfig(IClientConfig iClientConfig) {

    }

    @Override
    public Server choose(Object o) {
        Server server = null;
        ILoadBalancer loadBalancer = getLoadBalancer();
        List<Server> allServers = loadBalancer.getAllServers();
        return allServers.get(0);
    }
}

(2)person-client启动类中暴露自定义实现为Spring Bean

@Bean
public FirstServerForverRule firstServerForverRule(){
    return new FirstServerForverRule();
}

(3)激活这个配置,启动类上加上@RibbonClient(value = "person-service",configuration = FirstServerForverRule.class)

(4)在下面位置打上断点,debug启动client。

当我postman随便访问两个接口之一,发现确实走到里面的,allServers变量值为

并且两个都是application.properties文件里配置的服务端server。那么显而易见,下面一句代码就是永远获取第一个服务。这就是这个自定义策略的实现。

三、Hystrix整合

1.新增依赖至父项目pom

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

2.调整feign-api接口PersonService.增加fallback

import com.gupao.feign.api.domain.Person;
import com.gupao.feign.api.hystrix.PersonServiceFallback;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import java.util.Collection;

/**
 * @ClassName
 * @Describe {@link Person}服务
 * @Author 66477
 * @Date 2020/6/822:25
 * @Version 1.0
 */
@FeignClient(value="person-service",fallback = PersonServiceFallback.class)//服务提供方应用的名称
public interface PersonService {
    /**
     * 保存
     * @param person {@link Person}
     * @return 如果成功,<code>true</code>
     */
    @PostMapping(value = "/person/save")
    boolean save(@RequestBody  Person person);

    /**
     * 查找所有的服务
     * @return
     */
    @GetMapping(value="/person/findAll")
    Collection<Person> findAll();
}

3.创建一个包hystrix,创建一个类实现PersonService.

import com.gupao.feign.api.service.PersonService;
import org.springframework.cloud.openfeign.FeignClient;
import java.util.Collection;
import java.util.Collections;

/**
 * @ClassName
 * @Describe TODO
 * @Author 66477
 * @Date 2020/6/1020:42
 * @Version 1.0
 */

public class PersonServiceFallback implements PersonService {

    @Override
    public boolean save(Person person) {
        return false;
    }

    @Override
    public Collection<Person> findAll() {
        return Collections.emptyList();
    }
}

4.调整客户端person-client,激活Hystrix:启动类增加@EnableHystrix

四、问题总结

1.能跟dubbo一样,消费端像调用本地接口方法一下调用服务端提供的服务吗?还有就是远程调用方法参数对象不用实现序列化接口吗?

解答:FeignClient类似Dubbo,不过需要增加一下@Annotation,和调用本地接口类似

2.Feign通过注释驱动弱化了调用Serivce细节,但是Feign的API设定会暴露service地址,那么还有实际使用价值吗?

解答:实际价值是存在的,Feign API暴露URI,比如:“/person/save"

3.整合ribbon不是一定要关闭注册中心吧?

解答:Rbbbon对于Eureka是不强依赖,不给过也不排除。

4.生成环境上也都是feign?

解答:据我所知,不少公司在用,需要Spring Cloud更多整合:

Feign作为客户端

Ribbon作为负载均衡

Eureka作为注册中心

Zuul作为网关

Security作为安全OAuth2认证

5.Ribbon直接配置在启动类上是作用所有的Controller,那如果想作用在某个类呢?

解答:Ribbon是控制全局的负载均衡,主要作用于客户端Feign,Controller是调用Feign接口,可能让人感觉直接作用了ontroller。

6.其实Eureka也有ribbon中简单的负载均衡吧?

解答:Eureka也要Ribbon的实现,可以参考com.netflix.ribbon:ribbon-eureka

7.如果服务提供方没有接口,我客户端一般怎么处理?要根据服务信息,自建feign接口?

解答:当然可以,Feign的接口定义要求强制实现。

8.无法连接注册中心的老服务,如何额调用cloud服务?

解答:可以通过域名的配置Ribbon服务白名单。

9.eureka有时监控不到宕机的服务,正确的启动方式是什么?

解答:需要其他设备来监控,PNIG eureka服务器是否活着,活着采用高可用方式管理Eureka。这可以调整心跳检测的频率。

原文地址:https://www.cnblogs.com/xusp/p/13090011.html