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

简介: 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 整合的?


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


相关实践学习
部署高可用架构
本场景主要介绍如何使用云服务器ECS、负载均衡SLB、云数据库RDS和数据传输服务产品来部署多可用区高可用架构。
负载均衡入门与产品使用指南
负载均衡(Server Load Balancer)是对多台云服务器进行流量分发的负载均衡服务,可以通过流量分发扩展应用系统对外的服务能力,通过消除单点故障提升应用系统的可用性。 本课程主要介绍负载均衡的相关技术以及阿里云负载均衡产品的使用方法。
相关文章
|
1月前
|
SpringCloudAlibaba Java 持续交付
【构建一套Spring Cloud项目的大概步骤】&【Springcloud Alibaba微服务分布式架构学习资料】
【构建一套Spring Cloud项目的大概步骤】&【Springcloud Alibaba微服务分布式架构学习资料】
155 0
|
1月前
|
SpringCloudAlibaba Java 网络架构
【Springcloud Alibaba微服务分布式架构 | Spring Cloud】之学习笔记(七)Spring Cloud Gateway服务网关
【Springcloud Alibaba微服务分布式架构 | Spring Cloud】之学习笔记(七)Spring Cloud Gateway服务网关
101 0
|
2天前
|
Java Nacos 开发者
Java从入门到精通:4.2.1学习新技术与框架——以Spring Boot和Spring Cloud Alibaba为例
Java从入门到精通:4.2.1学习新技术与框架——以Spring Boot和Spring Cloud Alibaba为例
|
17天前
|
Nacos
SpringCloud Feign使用
SpringCloud Feign使用
23 1
|
24天前
|
开发框架 负载均衡 Java
Spring boot与Spring cloud之间的关系
总之,Spring Boot和Spring Cloud之间的关系是一种构建和扩展的关系,Spring Boot提供了基础,而Spring Cloud在此基础上提供了分布式系统和微服务架构所需的扩展和工具。
18 4
Spring boot与Spring cloud之间的关系
|
1月前
|
SpringCloudAlibaba 负载均衡 Java
【Springcloud Alibaba微服务分布式架构 | Spring Cloud】之学习笔记(目录大纲)
【Springcloud Alibaba微服务分布式架构 | Spring Cloud】之学习笔记(目录大纲)
65 1
|
1月前
|
Java Nacos Sentinel
【Springcloud Alibaba微服务分布式架构 | Spring Cloud】之学习笔记(九)Nacos+Sentinel+Seata
【Springcloud Alibaba微服务分布式架构 | Spring Cloud】之学习笔记(九)Nacos+Sentinel+Seata
203 0
|
1月前
|
消息中间件 SpringCloudAlibaba Java
【Springcloud Alibaba微服务分布式架构 | Spring Cloud】之学习笔记(八)Config服务配置+bus消息总线+stream消息驱动+Sleuth链路追踪
【Springcloud Alibaba微服务分布式架构 | Spring Cloud】之学习笔记(八)Config服务配置+bus消息总线+stream消息驱动+Sleuth链路追踪
785 0
|
1月前
|
SpringCloudAlibaba Java 测试技术
【Springcloud Alibaba微服务分布式架构 | Spring Cloud】之学习笔记(六)Hystrix(豪猪哥)的使用
【Springcloud Alibaba微服务分布式架构 | Spring Cloud】之学习笔记(六)Hystrix(豪猪哥)的使用
39 1
|
1月前
|
SpringCloudAlibaba 负载均衡 Java
【Springcloud Alibaba微服务分布式架构 | Spring Cloud】之学习笔记(五)OpenFeign的使用
【Springcloud Alibaba微服务分布式架构 | Spring Cloud】之学习笔记(五)OpenFeign的使用
42 0