4.SpringCloud学习(四)——Spring Cloud OpenFeign 服务调用

1.简介

1.1 概述

Feign is a declarative web service client. It makes writing web service clients easier. To use Feign create an interface and annotate it. It has pluggable annotation support including Feign annotations and JAX-RS annotations. Feign also supports pluggable encoders and decoders. Spring Cloud adds support for Spring MVC annotations and for using the same HttpMessageConverters used by default in Spring Web. Spring Cloud integrates Ribbon and Eureka, as well as Spring Cloud LoadBalancer to provide a load-balanced http client when using Feign.

Feign是声明性Web服务客户端。它使编写Web服务客户端更加容易。要使用Feign,请创建一个接口并对其进行注释。它具有可插入注释支持,包括Feign注释和JAX-RS注释。 Feign还支持可插拔编码器和解码器。 Spring Cloud添加了对Spring MVC注释的支持,并支持使用Spring Web中默认使用的相同HttpMessageConverters。 Spring Cloud集成了Ribbon和Eureka以及Spring Cloud LoadBalancer,以在使用Feign时提供负载平衡的http客户端。

1.2 特点

在一般的场景中,如果要发送 http 请求,需要根据服务提供方的 ip、端口、url 进行调用,openfeign 提供了一种基于接口的调用方式。

  • 原始的调用方法: client.request("http://ip:port/service");
  • openfeign 调用方法:service.request(args); openfeign 根据服务名进行调用,调用方配置服务提供方的服务名 spring.application.name ;

使用 openfeign 调用远程服务就像 java 代码中接口调用方法一样,不用再编写复杂的 http 请求逻辑;如果集成了 eureka 等注册中心,甚至不用配置服务提供方的 url,只需配置 eureka 即可。

2.演示环境

  1. JDK 1.8.0_201
  2. Spring Boot 2.2.0.RELEASE、Spring Cloud Hoxton.RELEASE
  3. 构建工具(apache maven 3.6.3)
  4. 开发工具(IntelliJ IDEA )

3.演示代码

image-20200821164346658

Feign+Eureka+Hystrix 的混合运用

总体结构说明:

  • ofc-feign-eureka-server: eureka 服务端,提供服务注册功能;
  • ofc-feign-user-api: 公共 api,定义模型和接口,fallback;
  • ofc-feign-user-client: 服务调用方,使用 feign 调用接口,注册到 eureka server;
  • ofc-feign-user-server: 服务提供方,实现 api 中定义的接口,注册到 eureka server。

3.1 ofc-feign-eureka-server

3.1.1 代码说明

eureka 服务端,提供服务注册功能。

3.1.2 maven 依赖

pom.xml

<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
    </dependency>
</dependencies>

3.1.3 配置文件

application.properties

spring.application.name=ofc-feign-eureka-server
# 应用服务web访问端口
server.port=11040

# 服务注册中心主机名
eureka.instance.hostname=localhost
# 是否注册自己
eureka.client.register-with-eureka=false
# 是否检索服务
eureka.client.fetch-registry=false
# eureka server 地址
eureka.client.service-url.defaultZone=http://${eureka.instance.hostname}:${server.port}/eureka/

3.1.4 java代码

OfcFeignEurekaApplication.java

// 开启 eureka server
@EnableEurekaServer
@SpringBootApplication
public class OfcFeignEurekaApplication {

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

3.2 ofc-feign-user-api

3.2.1 代码说明

公共 api,定义了实体模型,公共接口以及 fallback 类。

3.2.2 maven 依赖

pom.xml

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

3.2.3 配置文件

application.properties

spring.application.name=ofc-feign-user-api
# 应用服务web访问端口
server.port=8080

3.2.4 java代码

UserModel.java

public class UserModel {

    private Long id;
    private String name;
    private Integer age;
    private String birthday;
    private String address;
    private String phone;

    public UserModel() {}

    public UserModel(Long id, String name, Integer age, String birthday, String address, String phone) {
        this.id = id;
        this.name = name;
        this.age = age;
        this.birthday = birthday;
        this.address = address;
        this.phone = phone;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public String getBirthday() {
        return birthday;
    }

    public void setBirthday(String birthday) {
        this.birthday = birthday;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }

    @Override
    public String toString() {
        return "UserModel{" + "id=" + id + ", name='" + name + '\'' + ", age=" + age + ", birthday='" + birthday + '\''
                + ", address='" + address + '\'' + ", phone='" + phone + '\'' + '}';
    }
}

UserService.java

// value 中定义服务提供者名称,fallback-降级
@FeignClient(value = "ofc-feign-user-server", fallback = UserServiceFallback.class)
public interface UserService {

    @GetMapping(value = "/user/list")
    List<UserModel> list();

    @PostMapping(value = "/user/save")
    UserModel save(@RequestBody UserModel userModel);
}

UserServiceFallback.java

public class UserServiceFallback implements UserService {
    @Override
    public List<UserModel> list() {
        return Collections.emptyList();
    }

    @Override
    public UserModel save(UserModel userModel) {
        return new UserModel();
    }
}

OfcUserApiApplication.java

@SpringBootApplication
public class OfcUserApiApplication {

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

3.3 ofc-feign-user-server

3.3.1 代码说明

实现 api 中定义的接口,对外提供服务,注册到 eureka server。

集成 hystrix,实现服务降级。

3.3.2 maven 依赖

pom.xml

<dependencies>
    <dependency>
        <groupId>com.soulballad.usage</groupId>
        <artifactId>ofc-feign-user-api</artifactId>
        <version>${project.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
</dependencies>

3.3.3 配置文件

application.properties

spring.application.name=ofc-feign-user-server
# 应用服务web访问端口
server.port=11041

eureka.server.host=localhost
eureka.server.port=11040
eureka.client.service-url.defaultZone=http://${eureka.server.host}:${eureka.server.port}/eureka/

3.3.4 java代码

UserServerController.java

@RestController
public class UserServerController {

    private static final Map<Long, UserModel> USER_MAP = new HashMap<>();
    private static final AtomicLong ID_GENERATOR = new AtomicLong(2);
    private final Random random = new Random();
    private static final Logger LOGGER = LoggerFactory.getLogger(UserServerController.class);

    @GetMapping(value = "/user/list")
    @HystrixCommand(fallbackMethod = "fallBackList", commandProperties = {
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "100")
    })
    public List<UserModel> list() throws InterruptedException {
        int seconds = random.nextInt(200);
        LOGGER.info("user server controller list sleep for {} seconds!", seconds);
        Thread.sleep(seconds);
        return new ArrayList<>(USER_MAP.values());
    }

    @PostMapping(value = "/user/save")
    public UserModel save(@RequestBody UserModel userModel) {
        long id = ID_GENERATOR.incrementAndGet();
        userModel.setId(id);
        USER_MAP.put(id, userModel);
        return userModel;
    }

    public List<UserModel> fallBackList() {
        LOGGER.warn("user server controller list fallback!");
        return Collections.emptyList();
    }

    // 初始化2条数据
    @PostConstruct
    public void init() {
        UserModel user1 = new UserModel(1L, "zhangsan", 20, "2000-01-01", "shenzhen", "13888888888");
        UserModel user2 = new UserModel(2L, "lisi", 21, "1999-01-01", "shanghai", "13777777777");
        USER_MAP.put(user1.getId(), user1);
        USER_MAP.put(user2.getId(), user2);
    }
}

OfcFeignUserServerApplication.java

@EnableHystrix
@EnableEurekaClient
@SpringBootApplication
public class OfcFeignUserServerApplication {

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

3.4 ofc-feign-user-client

3.4.1 代码说明

根据 api 中的 UserService 接口调用服务,同时也需要注册到 eureka server。

3.4.2 maven 依赖

pom.xml

<dependencies>
    <dependency>
        <groupId>com.soulballad.usage</groupId>
        <artifactId>ofc-feign-user-api</artifactId>
        <version>${project.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
</dependencies>

3.2.3 配置文件

application.properties

spring.application.name=ofc-feign-user-client
# 应用服务web访问端口
server.port=11042

eureka.server.host=localhost
eureka.server.port=11040
eureka.client.service-url.defaultZone=http://${eureka.server.host}:${eureka.server.port}/eureka/

# 设置超时时间和日志级别
feign.client.config.default.connect-timeout=5000
feign.client.config.default.read-timeout=5000
feign.client.config.default.logger-level=full

logging.level.com.soulballad.usage.springcloud=debug

3.2.4 java代码

UserClientController.java

@RestController
public class UserClientController implements UserService {

    private final UserService userService;

    @Autowired
    public UserClientController(UserService userService) {
        this.userService = userService;
    }

    @Override
    public List<UserModel> list() {
        return userService.list();
    }

    @Override
    public UserModel save(@RequestBody UserModel userModel) {
        return userService.save(userModel);
    }
}

UserService.java

// 启用feign,接口为UserService;如果不配置默认扫描所有@FeignClient注解修改的类
@EnableEurekaClient
@SpringBootApplication
@EnableFeignClients(clients = UserService.class)
public class OfcFeignUserClientApplication {

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

3.5 git 地址

spring-cloud-ofc-04-feign: Spring Cloud 官方提供的分布式服务调用方案

4.效果展示

启动 SpringBoot03WebApplication.main 方法,在 spring-boot-03-webmvc.http 访问下列地址,观察输出信息是否符合预期。

依次启动 ofc-feign-eureka-server、ofc-feign-user-server、ofc-feign-user-client 服务;

它们分别监听在 11040、11041、11042 端口,启动完成后可在 eureka 管理台上看到:

### GET eureka
GET http://localhost:11040/

image-20200821170927581

spring-cloud-ofc-feign.http 中访问如下地址,查看请求结果是否符合预期

4.1 ofc-feign-user-server

查询用户列表

### GET /user/list
GET http://localhost:11041/user/list

image-20200821171304738

新增用户

### POST /user/save
POST http://localhost:11041/user/save
Accept: application/json
Content-Type: application/json

{
  "name": "wangwu",
  "age": 30,
  "birthday": "1980-03-01",
  "address": "guangzhou",
  "phone": "13666666666"
}

image-20200821171358047

4.2 ofc-feign-user-client

查询用户列表

### GET /user/list
GET http://localhost:11042/user/list

image-20200821172224478

新增用户

### POST /user/save
POST http://localhost:11042/user/save
Accept: application/json
Content-Type: application/json

{
  "name": "zhaoliu",
  "age": 40,
  "birthday": "1970-04-02",
  "address": "wuhan",
  "phone": "13555555555"
}

image-20200821172301295

5.源码分析

5.1 Feign 是如何调用服务的?

当调用 /user/list 时,可以看到这时的 UserService 是一个代理对象,它被 jdk 动态代理。

image-20200821172827151

继续执行会调用 ReflectiveFeign.FeignInvocationHandler#invoke 方法,调用路径如下

5.2 代理对象是如何创建的?

在 OfcFeignUserClientApplication 上添加了 @EnableFeignClients 注解,启用了 feign 功能,@EnableFeignClients 中通过 @Import 引入了 @FeignClientsRegistrar

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {

   String[] value() default {};

   String[] basePackages() default {};

   Class<?>[] basePackageClasses() default {};

   Class<?>[] defaultConfiguration() default {};

   Class<?>[] clients() default {};

}

@FeignClientsRegistrar 实现了 ImportBeanDefinitionRegistrar 接口,可以动态装载 bean

的定义

@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,
                                    BeanDefinitionRegistry registry) {
	// 注册默认配置
    registerDefaultConfiguration(metadata, registry);
    // 注册 FeignClient
    registerFeignClients(metadata, registry);
}

registerFeignClients 扫描所有加了 @FeignClient 的类,然后再调用 registerFeignClient 方法进行注册

public void registerFeignClients(AnnotationMetadata metadata,
                                 BeanDefinitionRegistry registry) {
    // 获取一个scanner扫描对象
    ClassPathScanningCandidateComponentProvider scanner = getScanner();
    // 资源加载器,当前应用上下文
    scanner.setResourceLoader(this.resourceLoader);

    Set<String> basePackages;

    // 获取 @EnableFeignClients 的属性
    Map<String, Object> attrs = metadata
        .getAnnotationAttributes(EnableFeignClients.class.getName());
    AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(
        FeignClient.class);
    // 判断是否配置了 clients 属性
    final Class<?>[] clients = attrs == null ? null
        : (Class<?>[]) attrs.get("clients");
    if (clients == null || clients.length == 0) {
        // 如果没有配置,获取 basePackage
        scanner.addIncludeFilter(annotationTypeFilter);
        basePackages = getBasePackages(metadata);
    }
    else {
        // 否则只扫描 clients 中类所在路径
        final Set<String> clientClasses = new HashSet<>();
        basePackages = new HashSet<>();
        for (Class<?> clazz : clients) {
            basePackages.add(ClassUtils.getPackageName(clazz));
            clientClasses.add(clazz.getCanonicalName());
        }
        AbstractClassTestingTypeFilter filter = new AbstractClassTestingTypeFilter() {
            @Override
            protected boolean match(ClassMetadata metadata) {
                String cleaned = metadata.getClassName().replaceAll("\\$", ".");
                return clientClasses.contains(cleaned);
            }
        };
        scanner.addIncludeFilter(
            new AllTypeFilter(Arrays.asList(filter, annotationTypeFilter)));
    }

    // 遍历 basePackages
    for (String basePackage : basePackages) {
        Set<BeanDefinition> candidateComponents = scanner
            .findCandidateComponents(basePackage);
        for (BeanDefinition candidateComponent : candidateComponents) {
            if (candidateComponent instanceof AnnotatedBeanDefinition) {
                // verify annotated class is an interface
                AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
                AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
                Assert.isTrue(annotationMetadata.isInterface(),
                              "@FeignClient can only be specified on an interface");

                Map<String, Object> attributes = annotationMetadata
                    .getAnnotationAttributes(
                    FeignClient.class.getCanonicalName());

                String name = getClientName(attributes);
                // 注册 FeignClient 上的 configuration 配置
                registerClientConfiguration(registry, name,
                                            attributes.get("configuration"));
				// 注册 FeignClient
                registerFeignClient(registry, annotationMetadata, attributes);
            }
        }
    }
}

registerFeignClient() 中通过 genericBeanDefinition 来构建一个 BeanDefinitionBuilder 对象,它传入一个 FeignClientFactoryBean 参数

private void registerFeignClient(BeanDefinitionRegistry registry,
                                 AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
    String className = annotationMetadata.getClassName();
    // 通过 FeignClientFactoryBean 生成一个 BeanDefinitionBuilder 对象
    BeanDefinitionBuilder definition = BeanDefinitionBuilder
        .genericBeanDefinition(FeignClientFactoryBean.class);
    validate(attributes);
    // 设置一些属性
    definition.addPropertyValue("url", getUrl(attributes));
    definition.addPropertyValue("path", getPath(attributes));
    String name = getName(attributes);
    definition.addPropertyValue("name", name);
    String contextId = getContextId(attributes);
    definition.addPropertyValue("contextId", contextId);
    definition.addPropertyValue("type", className);
    definition.addPropertyValue("decode404", attributes.get("decode404"));
    definition.addPropertyValue("fallback", attributes.get("fallback"));
    definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory"));
    definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);

    String alias = contextId + "FeignClient";
    AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();

    boolean primary = (Boolean) attributes.get("primary"); // has a default, won't be
    // null

    beanDefinition.setPrimary(primary);

    // 判断是否有别名
    String qualifier = getQualifier(attributes);
    if (StringUtils.hasText(qualifier)) {
        alias = qualifier;
    }

    // 使用 BeanDefinitionHolder 进行包装
    BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
                                                           new String[] { alias });
    // 注册到 registry 中
    BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
}

FeignClientFactoryBean 是一个工厂 bean,它实现了 FactoryBean 接口,重写 getObject 方法,自定义 bean 的初始化

@Override
public Object getObject() throws Exception {
   return getTarget();
}

getTarget 判断是否有指定url,如果指定了url,直接调用;否则使用负载均衡选择一个服务提供者。这里没有指定,所以调用 loadBalance

<T> T getTarget() {
    // 从应用上下文中获取 FeignContext
    FeignContext context = this.applicationContext.getBean(FeignContext.class);
	// 初始化 Feign.Builder
    Feign.Builder builder = feign(context);

    if (!StringUtils.hasText(this.url)) {
        // 如果没有指定url,使用负载均衡选择一个服务提供者
        if (!this.name.startsWith("http")) {
            this.url = "http://" + this.name;
        }
        else {
            this.url = this.name;
        }
        this.url += cleanPath();
        // 负载均衡调用
        return (T) loadBalance(builder, context,
                               new HardCodedTarget<>(this.type, this.name, this.url));
    }
    
    // 如果指定了url,直接调用对应的服务
    if (StringUtils.hasText(this.url) && !this.url.startsWith("http")) {
        this.url = "http://" + this.url;
    }
    String url = this.url + cleanPath();
    Client client = getOptional(context, Client.class);
    if (client != null) {
        if (client instanceof LoadBalancerFeignClient) {
            // not load balancing because we have a url,
            // but ribbon is on the classpath, so unwrap
            client = ((LoadBalancerFeignClient) client).getDelegate();
        }
        builder.client(client);
    }
    Targeter targeter = get(context, Targeter.class);
    return (T) targeter.target(this, builder, context,
                               new HardCodedTarget<>(this.type, this.name, url));
}

loadBalance 通过 targeter.target 进行代理

protected <T> T loadBalance(Feign.Builder builder, FeignContext context,
                            HardCodedTarget<T> target) {
    // 获取一个 Client 的实例
    Client client = getOptional(context, Client.class);
    if (client != null) {
        builder.client(client);
        Targeter targeter = get(context, Targeter.class);
        // 代理
        return targeter.target(this, builder, context, target);
    }

    throw new IllegalStateException(
        "No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-netflix-ribbon?");
}

target 方法中,通过 build() 创建 ReflectiveFeign 对象,然后调用它的 newInstance 方法,生成代理对象

@Override
public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign,
      FeignContext context, Target.HardCodedTarget<T> target) {
   return feign.target(target);
}
public <T> T target(Target<T> target) {
    // 调用 newInstance 生成代理对象
    return this.build().newInstance(target);
}

public Feign build() {
    // 创建工厂类
    Factory synchronousMethodHandlerFactory = new Factory(this.client, this.retryer, this.requestInterceptors, this.logger, this.logLevel, this.decode404, this.closeAfterDecode, this.propagationPolicy);
    ParseHandlersByName handlersByName = new ParseHandlersByName(this.contract, this.options, this.encoder, this.decoder, this.queryMapEncoder, this.errorDecoder, synchronousMethodHandlerFactory);
    // 创建 ReflectiveFeign 对象
    return new ReflectiveFeign(handlersByName, this.invocationHandlerFactory, this.queryMapEncoder);
}

调用 ReflectiveFeign#newInstance,生成 jdk 代理对象

public <T> T newInstance(Target<T> target) {
    Map<String, MethodHandler> nameToHandler = this.targetToHandlersByName.apply(target);
    Map<Method, MethodHandler> methodToHandler = new LinkedHashMap();
    List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList();
    Method[] var5 = target.type().getMethods();
    int var6 = var5.length;

    for(int var7 = 0; var7 < var6; ++var7) {
        Method method = var5[var7];
        if (method.getDeclaringClass() != Object.class) {
            if (Util.isDefault(method)) {
                DefaultMethodHandler handler = new DefaultMethodHandler(method);
                defaultMethodHandlers.add(handler);
                methodToHandler.put(method, handler);
            } else {
                methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method)));
            }
        }
    }

    // 创建 InvocationHandler
    InvocationHandler handler = this.factory.create(target, methodToHandler);
    // 生成代理对象
    T proxy = Proxy.newProxyInstance(target.type().getClassLoader(), new Class[]{target.type()}, handler);
    Iterator var12 = defaultMethodHandlers.iterator();

    while(var12.hasNext()) {
        DefaultMethodHandler defaultMethodHandler = (DefaultMethodHandler)var12.next();
        defaultMethodHandler.bindTo(proxy);
    }

    return proxy;
}

这里的 factory 为 InvocationHandlerFactory,调用其 create 方法

public InvocationHandler create(Target target, Map<Method, InvocationHandlerFactory.MethodHandler> dispatch) {
    return new FeignInvocationHandler(target, dispatch);
}

所以最终生成的代理对象为 FeignInvocationHandler

6.参考

  1. 官方文档-Spring Cloud OpenFeign
posted @ 2020-08-23 12:45  Soulballad  阅读(583)  评论(0编辑  收藏  举报