04、SpringCloud之Feign组件学习笔记(二)

本文涉及的产品
应用型负载均衡 ALB,每月750个小时 15LCU
网络型负载均衡 NLB,每月750个小时 15LCU
传统型负载均衡 CLB,每月750个小时 15LCU
简介: 04、SpringCloud之Feign组件学习笔记(二)

三、手写feign简易实现


本质:feign本质就是进行发送http请求,并且再此基础上具备负载均衡的功能,我们来对其进行复现一下。


我们来接着二实战中User服务中UserOrderFeign接口,来对该接口中的doOrder方法进行代理:



①首先我们来增强RestTemplate方法,另起能够具备负载均衡的效果。


在对应的启动器类UserserviceApplication中添加该bean:


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


②之后我们在test测试包中来进行代理类编写


package com.changlu.userservice;
import com.changlu.userservice.controller.UserController;
import com.changlu.userservice.feign.UserOrderFeign;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.client.RestTemplate;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
@SpringBootTest
class UserserviceApplicationTests {
    @Autowired
    private RestTemplate restTemplate;
    @Test
    void contextLoads() {
        UserOrderFeign feign = (UserOrderFeign)Proxy.newProxyInstance(UserController.class.getClassLoader(), new Class[]{UserOrderFeign.class}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                String res = null;
                if (method.getName().equals("doOrder")) {
                    //1、获取到对应的path路径
                    // 能去拿到对方的ip和port 并且拿到这个方法上面的注解里面的值 那么就完事了
                    GetMapping annotation = method.getAnnotation(GetMapping.class);
                    String[] paths = annotation.value();
                    String path = paths[0];
                    //2、根据对应的的方法来获取到相应的class字节码
                    Class<?> aclass = method.getDeclaringClass();
                    //3、获取到对应feign类的注解,取得对应的服务名
                    FeignClient fannoation = aclass.getAnnotation(FeignClient.class);
                    String serviceName = fannoation.value();
                    //4、拼接服务地址
                    String url = "http://" + serviceName + path;
                    //5、发送请求(使用resttemplate来进行发送请求)
                    res = restTemplate.getForObject(url, String.class);
                }
                return res;
            }
        });
        System.out.println(feign.doOrder());
    }
}



测试成功!


四、feign的源码分析


4.1、OpenFeign的原理分析


使用动态代理jdk (invoke) 及cglib 子类继承的 :


源码简述:


1、给接口创建代理对象(启动扫描)


2、代理对象执行进入 invoke 方法


3、在 invoke 方法里面做远程调用


具体流程:


1、通过注解扫描来获取到调用服务名称以及对应的接口url。



2、拼接:``http://服务名/doOrder,首先会通过ribbon从注册中心中根据服务名获取对应的访问地址,最终根据负载均衡策略确定得到一个服务,最终得到的对应的url地址:http://ip:port/doOrder`


3、最终发起请求,来进行远程调用。


4.2、如何扫描注解@FeignClient?


在启动器上我们添加了一个开启Feign扫描的注解:


@EnableFeignClients //开启feign客户端扫描


//看下对应的注解源码
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import({FeignClientsRegistrar.class})  //FeignClientsRegistrar是Feign的注册类
public @interface EnableFeignClients {
    String[] value() default {};
    String[] basePackages() default {};
    Class<?>[] basePackageClasses() default {};
    Class<?>[] defaultConfiguration() default {};
    Class<?>[] clients() default {};
}


进入 FeignClientsRegistrar 这个类 去查看里面的东西



public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
    this.registerDefaultConfiguration(metadata, registry);
    //扫描注解来进行一个注册(@FeignClient)
    this.registerFeignClients(metadata, registry);
}
//AnnotationMetadata即注解的元信息
public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
    LinkedHashSet<BeanDefinition> candidateComponents = new LinkedHashSet();
    //获取到启动类上的feign注解
    Map<String, Object> attrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName());
    Class<?>[] clients = attrs == null ? null : (Class[])((Class[])attrs.get("clients"));
    if (clients != null && clients.length != 0) {
        Class[] var12 = clients;
        int var14 = clients.length;
        for(int var16 = 0; var16 < var14; ++var16) {
            Class<?> clazz = var12[var16];
            candidateComponents.add(new AnnotatedGenericBeanDefinition(clazz));
        }
    } else {
        ClassPathScanningCandidateComponentProvider scanner = this.getScanner();
        scanner.setResourceLoader(this.resourceLoader);
        scanner.addIncludeFilter(new AnnotationTypeFilter(FeignClient.class));
        Set<String> basePackages = this.getBasePackages(metadata);
        Iterator var8 = basePackages.iterator();
        //循环遍历得到对应的包名
        while(var8.hasNext()) {
            String basePackage = (String)var8.next();
            candidateComponents.addAll(scanner.findCandidateComponents(basePackage));
        }
    }
    Iterator var13 = candidateComponents.iterator();
    while(var13.hasNext()) {
        BeanDefinition candidateComponent = (BeanDefinition)var13.next();
        if (candidateComponent instanceof AnnotatedBeanDefinition) {
            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 = this.getClientName(attributes);
            this.registerClientConfiguration(registry, name, attributes.get("configuration"));
            //将创建的代理对象registry交给spring
            this.registerFeignClient(registry, annotationMetadata, attributes);
        }
    }
}



4.3、如何创建代理对象去执行调用?


在启动时,在ReflectiveFeign的newInstance方法中,给接口创建了代理对象


public class ReflectiveFeign extends Feign {
  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 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;
    }
}



ReflectiveFeign 类中的 invoke 方法帮我们完成调用:



当我们去进行远程调用的时候可以debug到对应的断点:



SynchronousMethodHandler 的invoke中给每一个请求创建了一个requestTemplate 对 象,去执行请求:


final class SynchronousMethodHandler implements MethodHandler {
    //执行请求走对应的invoke方法
    public Object invoke(Object[] argv) throws Throwable {
        //可以看到构建了一个RequestTemplate
        RequestTemplate template = this.buildTemplateFromArgs.create(argv);
        Options options = this.findOptions(argv);
        Retryer retryer = this.retryer.clone();
        while(true) {
            try {
                //交给ribbon来进行请求调用
                return this.executeAndDecode(template, options);
            } catch (RetryableException var9) {
                ...
            }
        }
    }
}
Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {
        Request request = this.targetRequest(template);
        if (this.logLevel != Level.NONE) {
            this.logger.logRequest(this.metadata.configKey(), this.logLevel, request);
        }
        long start = System.nanoTime();
        Response response;
        try {
            //这里client指的是LoadBalancerFeignClient,就是对应的负载均衡客户端请求工具
            response = this.client.execute(request, options);
            response = response.toBuilder().request(request).requestTemplate(template).build();
        } catch (IOException var12) {
            if (this.logLevel != Level.NONE) {
                this.logger.logIOException(this.metadata.configKey(), this.logLevel, var12, this.elapsedTime(start));
            }
            throw FeignException.errorExecuting(request, var12);
        }
}



此时就会走LoadBalancerFeignClient中的execute():


public class LoadBalancerFeignClient implements Client {
    public Response execute(Request request, Options options) throws IOException {
        try {
            //整理得到对应的url
            URI asUri = URI.create(request.url());
            String clientName = asUri.getHost();
            URI uriWithoutHost = cleanUrl(request.url(), clientName);
            RibbonRequest ribbonRequest = new RibbonRequest(this.delegate, request, uriWithoutHost);
            IClientConfig requestConfig = this.getClientConfig(options, clientName);
            //执行处理根据负载均衡处理器
            return ((RibbonResponse)this.lbClient(clientName).executeWithLoadBalancer(ribbonRequest, requestConfig)).toResponse();
        } catch (ClientException var8) {
            IOException io = this.findIOException(var8);
            if (io != null) {
                throw io;
            } else {
                throw new RuntimeException(var8);
            }
        }
    }
}



接着会走AbstractLoadBalancerAwareClient的executeWithLoadBalancer:


public T executeWithLoadBalancer(final S request, final IClientConfig requestConfig) throws ClientException {
        LoadBalancerCommand<T> command = buildLoadBalancerCommand(request, requestConfig);
        try {
            return command.submit(
                new ServerOperation<T>() {
                    //此时Server对象会取得最终的ip地址,例如:ip地址:port
                    @Override
                    public Observable<T> call(Server server) {
                        URI finalUri = reconstructURIWithServer(server, request.getUri());
                        S requestForServer = (S) request.replaceUri(finalUri);
                        try {
                            //这里才是真正进行发送请求操作
                            return Observable.just(AbstractLoadBalancerAwareClient.this.execute(requestForServer, requestConfig));
                        } 
                        catch (Exception e) {
                            return Observable.error(e);
                        }
                    }
                })
                .toBlocking()
                .single();
        } catch (Exception e) {
            Throwable t = e.getCause();
            if (t instanceof ClientException) {
                throw (ClientException) t;
            } else {
                throw new ClientException(e);
            }
        }
    }



最终在FeignLoadBalancer中的execute方法中完成远程方法访问:


public FeignLoadBalancer.RibbonResponse execute(FeignLoadBalancer.RibbonRequest request, IClientConfig configOverride) throws IOException {
    Options options;
    if (configOverride != null) {
        RibbonProperties override = RibbonProperties.from(configOverride);
        options = new Options((long)override.connectTimeout(this.connectTimeout), TimeUnit.MILLISECONDS, (long)override.readTimeout(this.readTimeout), TimeUnit.MILLISECONDS, override.isFollowRedirects(this.followRedirects));
    } else {
        options = new Options((long)this.connectTimeout, TimeUnit.MILLISECONDS, (long)this.readTimeout, TimeUnit.MILLISECONDS, this.followRedirects);
    }
  //最终这里完成了远程方法调用
    Response response = request.client().execute(request.toRequest(), options);
    return new FeignLoadBalancer.RibbonResponse(request.getUri(), response);
}


其实本质就是发送的HttpURLConnection



public Response execute(Request request, Options options) throws IOException {
    //发起HttpURLConnection请求
    HttpURLConnection connection = this.convertAndSend(request, options);
    //转转响应体
    return this.convertResponse(connection, request);
}
Response convertResponse(HttpURLConnection connection, Request request) throws IOException {
    //获取到状态码
    int status = connection.getResponseCode();
    String reason = connection.getResponseMessage();
    if (status < 0) {
        throw new IOException(String.format("Invalid status(%s) executing %s %s", status, connection.getRequestMethod(), connection.getURL()));
    } else {
        Map<String, Collection<String>> headers = new LinkedHashMap();
        Iterator var6 = connection.getHeaderFields().entrySet().iterator();
        while(var6.hasNext()) {
            Entry<String, List<String>> field = (Entry)var6.next();
            if (field.getKey() != null) {
                headers.put(field.getKey(), field.getValue());
            }
        }
        Integer length = connection.getContentLength();
        if (length == -1) {
            length = null;
        }
        InputStream stream;
        if (status >= 400) {
            stream = connection.getErrorStream();
        } else {
            stream = connection.getInputStream();
        }
        return Response.builder().status(status).reason(reason).headers(headers).request(request).body(stream, length).build();
    }
}



4.4、feign调用问题快速定位


只要是 feign 调用出了问题,看 feign 包下面的 Client 接口下面的 108 行。



我们可以根据对应响应的状态码来进行定位:


200:成功         400:请求参数错误   401:没有权限 
403:权限不够     404:路径不匹配     405:方法不允许 
500:提供者报错了  302:资源重定向


五、OpenFeign总结


OpenFeign 主要基于接口和注解实现了远程调用。


源码总结:面试


1、OpenFeign 用过吗?它是如何运作的?


在主启动类上加上@EnableFeignClients 注解后,启动会进行包扫描,把所有加了 @FeignClient(value=”xxx-service”)注解的接口进行创建代理对象通过代理对象,使用 ribbon 做了负载均衡和远程调用


2、如何创建的代理对象?


当 项 目 在 启 动 时 , 先 扫 描 , 然 后 拿 到 标 记 了 @FeignClient 注 解 的 接 口 信 息 , 由 ReflectiveFeign 类的 newInstance 方法创建了代理对象 JDK 代理


3、OpenFeign 到底是用什么做的远程调用?


使用的是 HttpURLConnection (java.net)


OpenFeign 怎么和 ribbon 整合的?


在代理对象执行调用的时候


相关实践学习
SLB负载均衡实践
本场景通过使用阿里云负载均衡 SLB 以及对负载均衡 SLB 后端服务器 ECS 的权重进行修改,快速解决服务器响应速度慢的问题
负载均衡入门与产品使用指南
负载均衡(Server Load Balancer)是对多台云服务器进行流量分发的负载均衡服务,可以通过流量分发扩展应用系统对外的服务能力,通过消除单点故障提升应用系统的可用性。 本课程主要介绍负载均衡的相关技术以及阿里云负载均衡产品的使用方法。
相关文章
|
1月前
|
监控 负载均衡 Java
5 大 SpringCloud 核心组件详解,8 张图彻底弄懂
本文图文详解 Spring Cloud 的五大核心组件,帮助深入理解和掌握微服务架构。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
5 大 SpringCloud 核心组件详解,8 张图彻底弄懂
|
19天前
|
负载均衡 Java 开发者
深入探索Spring Cloud与Spring Boot:构建微服务架构的实践经验
深入探索Spring Cloud与Spring Boot:构建微服务架构的实践经验
62 5
|
1月前
|
负载均衡 算法 Java
除了 Ribbon,Spring Cloud 中还有哪些负载均衡组件?
这些负载均衡组件各有特点,在不同的场景和需求下,可以根据项目的具体情况选择合适的负载均衡组件来实现高效、稳定的服务调用。
80 5
|
2月前
|
JSON Java 数据格式
【微服务】SpringCloud之Feign远程调用
本文介绍了使用Feign作为HTTP客户端替代RestTemplate进行远程调用的优势及具体使用方法。Feign通过声明式接口简化了HTTP请求的发送,提高了代码的可读性和维护性。文章详细描述了Feign的搭建步骤,包括引入依赖、添加注解、编写FeignClient接口和调用代码,并提供了自定义配置的示例,如修改日志级别等。
127 1
|
3月前
|
负载均衡 Java Nacos
SpringCloud基础2——Nacos配置、Feign、Gateway
nacos配置管理、Feign远程调用、Gateway服务网关
SpringCloud基础2——Nacos配置、Feign、Gateway
|
3月前
|
前端开发 API 微服务
SpringCloud微服务之间使用Feign调用不通情况举例
SpringCloud微服务之间使用Feign调用不通情况举例
660 2
|
2月前
|
负载均衡 Java API
【Spring Cloud生态】Spring Cloud Gateway基本配置
【Spring Cloud生态】Spring Cloud Gateway基本配置
54 0
|
3月前
|
Java API 开发者
【已解决】Spring Cloud Feign 上传文件,提示:the request was rejected because no multipart boundary was found的问题
【已解决】Spring Cloud Feign 上传文件,提示:the request was rejected because no multipart boundary was found的问题
660 0
|
4月前
|
Java Spring
【Azure Spring Cloud】Spring Cloud Azure 4.0 调用Key Vault遇见认证错误 AADSTS90002: Tenant not found.
【Azure Spring Cloud】Spring Cloud Azure 4.0 调用Key Vault遇见认证错误 AADSTS90002: Tenant not found.
|
4月前
|
Java Spring 容器
【Azure Spring Cloud】在Azure Spring Apps上看见 App Memory Usage 和 jvm.menory.use 的指标的疑问及OOM
【Azure Spring Cloud】在Azure Spring Apps上看见 App Memory Usage 和 jvm.menory.use 的指标的疑问及OOM
下一篇
DataWorks