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

本文涉及的产品
传统型负载均衡 CLB,每月750个小时 15LCU
应用型负载均衡 ALB,每月750个小时 15LCU
网络型负载均衡 NLB,每月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)是对多台云服务器进行流量分发的负载均衡服务,可以通过流量分发扩展应用系统对外的服务能力,通过消除单点故障提升应用系统的可用性。 本课程主要介绍负载均衡的相关技术以及阿里云负载均衡产品的使用方法。
相关文章
|
2月前
|
应用服务中间件 nginx 微服务
SpringCloud解决feign调用token丢失问题
【5月更文挑战第2天】在feign调用中可能会遇到如下问题: * 同步调用中,token丢失,这种可以通过创建一个拦截器,将token做透传来解决 * 异步调用中,token丢失,这种就无法直接透传了,因为子线程并没有**token**,这种需要先将token从父线程传递到子线程,再进行透传
235 3
|
11天前
|
Java API 开发工具
Spring Boot与Spring Cloud Config的集成
Spring Boot与Spring Cloud Config的集成
|
24天前
|
Java API 数据格式
Spring三兄弟:Spring、Spring Boot、Spring Cloud的100个常用注解大盘点
Spring三兄弟:Spring、Spring Boot、Spring Cloud的100个常用注解大盘点
|
15天前
|
监控 Java 应用服务中间件
SpringCloud面试之流量控制组件Sentinel详解
SpringCloud面试之流量控制组件Sentinel详解
37 0
|
1月前
|
负载均衡 前端开发 Java
OpenFeign:Spring Cloud声明式服务调用组件
该文本是关于OpenFeign在Spring Cloud中的使用的问答总结。涉及的问题包括:OpenFeign是什么,Feign与OpenFeign的区别,如何使用OpenFeign进行远程服务调用,OpenFeign的超时控制以及日志增强。OpenFeign被描述为Spring官方的声明式服务调用和负载均衡组件,它支持使用注解进行接口定义和服务调用,如@FeignClient和@EnableFeignClients。OpenFeign与Feign的主要区别在于OpenFeign支持Spring MVC注解。超时控制通过Ribbon进行设置,默认超时时间为1秒。
|
18天前
|
Java API 网络架构
Spring Boot与Spring Cloud Gateway的集成
Spring Boot与Spring Cloud Gateway的集成
|
19天前
|
负载均衡 监控 Java
深入理解Spring Boot与Spring Cloud的整合方式
深入理解Spring Boot与Spring Cloud的整合方式
|
24天前
|
XML JSON Java
经验大分享:SpringCloud之Feign
经验大分享:SpringCloud之Feign
15 0
|
1月前
|
消息中间件 Dubbo Java
SpringClou、SpringBoot、SpringCloud-Alibaba各个组件版本对应关系
SpringClou、SpringBoot、SpringCloud-Alibaba各个组件版本对应关系
|
2月前
|
JSON Java Apache
Spring Cloud Feign 使用Apache的HTTP Client替换Feign原生httpclient
Spring Cloud Feign 使用Apache的HTTP Client替换Feign原生httpclient