三、手写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 整合的?
在代理对象执行调用的时候