登录
注册
写文章
发现
工具
手写Feign
_3t3lfz KEKfID
编辑文章
手写Feign
asfx站长
2023.04.20 14:21:48
阅读
564
###什么是RPC RPC:远程过程调用,如何理解?其实就是服务之间调用不用再写繁琐的代码,有个东西把这件事包装起来,像调用普通方法一样调用远程服务。 从网络分层角度说:Feign不属于RPC,Dubbo是真正的RPC,Feign底层是HTTP(应用层)或7层调用,而RPC是4层(传输层)调用。 ###Feign注入原理 使用Feign需要在启动类上加@EnableFeignClients注解并写上包名,我们在类上加@FeignClient注解、方法加@GetMapping等注解并配置相应属性Feign架构就起作用,那么从这里思考这几个注解起了什么作用?我们倒着分析。 1.GetMapping、PostMapping 这几个注解同Controller一样这里不再描述; 2.FeignClient 是对我们类名、请求地址进行统一配置这里不再描述; 3.EnableFeignClients 这个注解相当重要且它是核心注解,以上两个注解是否起作用,怎么发起HTTP调用都是该注解在工作; @EnableFeignClients注解上有一个@Import(FeignClientsRegistrar.class)核心代码在FeignClientsRegistrar类中。 该类实现接口ImportBeanDefinitionRegistrar它是Spring扩展点之一,支持我们写的代码封装成BeanDefinition俗称bean,bean的有功能它都有例如:@Autowired注入该bean、postProcessAfterInitializationbean的后置初始化等功能。 注册类FeignClientsRegistrar中 的 registerBeanDefinitions 方法中有两个方法:registerDefaultConfiguration注册默认配置和registerFeignClients注册Feign的配置,我们注入Feign的配置核心在registerFeignClients方法中。 ![](//www.asfx.xyz/statics/images/bg/202304/20/7f92bb2bd59d44a187df57eb61468a6a.png) scanner是项目启动扫描所有class文件,并设置过滤条件(类上有FeignClient注解),且通过metadata(启动类上EnableFeignClients注解配置包名)并获得注解所扫描对应包名。 拿到启动类上所有包类,过滤上一步设置好的条件(带FeignClient注解的)类,找到我们定义FeignClient接口类。 ![](//www.asfx.xyz/statics/images/bg/202304/20/a08dfc5aec82438497eb2927cbee555f.png) 上图四步Feign帮我们做了,其实第三步对于我们手写框架来说可以忽略,重点看第四步。 ![](//www.asfx.xyz/statics/images/bg/202304/20/7f2e2ff1bef2484c86b60f99d61fcf98.png) 第一步:构建FeignClientFactoryBean并通过构造函数设置值(生成代理核心)。 第二步:获得bean别名、并设置bean是否是主要bean。 第三步:向Spring中注册bean。 至此我们写的带FeignClient注解的接口注册到spring中,可以通过@Autowired或@Resource等方式注入到代码里开始使用。那实现调用逻辑在哪里呢? ###Feign调用入口 Java动态代理分为两种:JDK和CGLIB,它两本质区别是有没有实现接口,JDK必须是实现接口,CGLIB可以不用实现接口。Feign用的是JDK动态代理所以我们写代码时FeignClient注解要放在接口上,刚才注入bean过程也进行了判断,非接口注解会报错(第二张图有分析)。 我们最后一张图有说:构建FeignClientFactoryBean并通过构造函数设置值,这里是产生代理的核心。该类主要实现了FactoryBean,返回值就是我们注册到spring中bean的类型,在该类中可以注入其它bean,感觉兴趣的小伙伴可以从源一直点下去,在getObject()中生成代理对象。 ------------ ###手写Feign 如果我们要写一个简单版本的Feign该怎么做呢? 1.我们需要把接口类注入到spring中。 2.在动态代理类中编写实现方法。 定义启动类注解 EnableFeignClient,并配置扫描包路径,默认为EnableFeignClient注解所在的包。 ``` /** * @Description: TODO * @Author: whm * @CreateTime: 2023-04-18 14:44 * @Version: 1.0 */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Import(FeignClientRegistrar.class) public @interface EnableFeignClient { String[] basePackages() default {}; } ``` 定义接口类注解 FeignClient,该注解配置url,超时时间等信息。 ``` /** * @Description: TODO * @Author: whm * @CreateTime: 2023-04-18 14:45 * @Version: 1.0 */ @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface FeignClient { /** * 注册bean名称,默认类名 * * @return */ String name() default ""; /** * 调用url * * @return */ String url(); /** * 读超时时间 * * @return */ String readTimeout() default "5000"; /** * 连接超时时间 * * @return */ String connectTimeout() default "5000"; } ``` 定义注册类 FeignClientRegistrar,启动时扫描包并注册到spring中 ``` /** * @Description: 定义注册类,启动时扫描包并注册到Spring中 * @Author: whm * @CreateTime: 2023-04-18 14:46 * @Version: 1.0 */ @Slf4j public class FeignClientRegistrar implements ImportBeanDefinitionRegistrar, EnvironmentAware, BeanClassLoaderAware, ResourceLoaderAware { private Environment environment; private ClassLoader classLoader; private ResourceLoader resourceLoader; @Override public void setEnvironment(Environment environment) { this.environment = environment; } @Override public void setBeanClassLoader(ClassLoader classLoader) { this.classLoader = classLoader; } @Override public void setResourceLoader(ResourceLoader resourceLoader) { this.resourceLoader = resourceLoader; } @Override public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { ClassPathScanningCandidateComponentProvider scanner = getScanner(); scanner.setResourceLoader(this.resourceLoader); AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(FeignClient.class); scanner.addIncludeFilter(annotationTypeFilter); Map<String, Object> attributes = metadata.getAnnotationAttributes(EnableFeignClient.class.getCanonicalName()); if (CollectionUtils.isEmpty(attributes)) { return; } Object basePackagesObj = attributes.get("basePackages"); String[] basePackagesArr = (String[]) basePackagesObj; Set<String> basePackages; if(basePackagesArr.length == 0){ basePackages = getBasePackages(metadata); }else{ basePackages = new HashSet<>(); for (String pkg : basePackagesArr) { if (StringUtils.hasText(pkg)) { basePackages.add(pkg); } } } 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> feignClientAttributeMap = annotationMetadata.getAnnotationAttributes(FeignClient.class.getCanonicalName()); if (CollectionUtils.isEmpty(feignClientAttributeMap)) { return; } String className = annotationMetadata.getClassName(); Class<?> clazz = null; try { clazz = Class.forName(className); } catch (ClassNotFoundException e) { log.error("FeignClient start up fail:", e); } String beanName = className.substring(className.lastIndexOf(".") + 1); String alias = beanName.substring(0, 1).toLowerCase().concat(beanName.substring(1)).concat("FeignClient"); String name = String.valueOf(feignClientAttributeMap.get("name")); String url = String.valueOf(feignClientAttributeMap.get("url")); if (StringUtils.hasText(name)) { alias = name; } String readTimeout = String.valueOf(feignClientAttributeMap.get("readTimeout")); String connectTimeout = String.valueOf(feignClientAttributeMap.get("connectTimeout")); BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class); definition.addConstructorArgValue(clazz); definition.addConstructorArgValue(url); definition.addConstructorArgValue(readTimeout); definition.addConstructorArgValue(connectTimeout); definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE); AbstractBeanDefinition handleDefinition = definition.getBeanDefinition(); handleDefinition.setPrimary(true); // 向Spring的上下文中注册bean组件 BeanDefinitionHolder holder = new BeanDefinitionHolder(handleDefinition, className, new String[]{alias}); BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry); } } } } protected Set<String> getBasePackages(AnnotationMetadata importingClassMetadata) { Map<String, Object> attributes = importingClassMetadata.getAnnotationAttributes(EnableFeignClient.class.getCanonicalName()); Set<String> basePackages = new HashSet(); String[] var4 = (String[]) attributes.get("basePackages"); int var5 = var4.length; int var6; String pkg; for(var6 = 0; var6 < var5; ++var6) { pkg = var4[var6]; if (StringUtils.hasText(pkg)) { basePackages.add(pkg); } } if (basePackages.isEmpty()) { /** * 若没有配置包路径,则获取 @EnableFeignClient 所在包路径 */ basePackages.add(ClassUtils.getPackageName(importingClassMetadata.getClassName())); } return basePackages; } protected ClassPathScanningCandidateComponentProvider getScanner() { return new ClassPathScanningCandidateComponentProvider(false, this.environment) { @Override protected boolean isCandidateComponent( AnnotatedBeanDefinition beanDefinition) { if (beanDefinition.getMetadata().isIndependent()) { if (beanDefinition.getMetadata().isInterface() && beanDefinition.getMetadata().getInterfaceNames().length == 1 && Annotation.class.getName().equals(beanDefinition.getMetadata().getInterfaceNames()[0])) { try { Class<?> target = ClassUtils.forName(beanDefinition.getMetadata().getClassName(), FeignClientRegistrar.this.classLoader); return !target.isAnnotation(); } catch (Exception ex) { this.logger.error("Could not load target class: " + beanDefinition.getMetadata().getClassName(), ex); } } return true; } return false; } }; } } ``` 定义工厂类 FeignClientFactoryBean 生成动态代理 ``` /** * @Description: 定义工厂类,生成动态代理 * @Author: whm * @CreateTime: 2023-04-18 14:48 * @Version: 1.0 */ public class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean { private Class<?> clazz; private String url; private Integer readTimeout; private Integer connectTimeout; public FeignClientFactoryBean(Class<?> clazz, String url, Integer readTimeout, Integer connectTimeout) { this.clazz = clazz; this.url = url; this.readTimeout = readTimeout; this.connectTimeout = connectTimeout; } @Override public Object getObject() throws Exception { return Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz}, new FeignClientHandler(url, readTimeout, connectTimeout)); } @Override public Class<?> getObjectType() { return clazz; } @Override public void afterPropertiesSet() throws Exception { if (Objects.isNull(clazz)) { throw new RuntimeException("FeignClient start up param error, class is null"); } if (!StringUtils.hasText(url)) { throw new RuntimeException("FeignClient start up param error, url is null"); } if (Objects.isNull(readTimeout)) { throw new RuntimeException("FeignClient start up param error, readTimeout is null"); } if (Objects.isNull(connectTimeout)) { throw new RuntimeException("FeignClient start up param error, connectTimeout is null"); } } } ``` 定义代理类 FeignClientHandler,这是最重要的一步!!我们接口实现逻辑全在这里面。 ``` /** * @Description: 定义代理类,接口实现逻辑全在这里。 * @Author: whm * @CreateTime: 2023-04-18 14:51 * @Version: 1.0 * */ @Slf4j public class FeignClientHandler implements InvocationHandler { private final String url; private final Integer readTimeout; private final Integer connectTimeout; public FeignClientHandler(String url, Integer readTimeout, Integer connectTimeout) { this.url = url; this.readTimeout = readTimeout; this.connectTimeout = connectTimeout; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { RequestMapping requestMapping = AnnotationUtils.findAnnotation(method, RequestMapping.class); RequestMethod[] requestMethods = requestMapping.method(); if(requestMethods.length == 0){ throw new RuntimeException("@RequestMapping is not allowed"); } // log.info("{}", requestMapping); switch (requestMapping.method()[0].name()){ case "GET": String queryString = getQueryString(method, args); String reqUrl = url; if (method.getAnnotation(GetMapping.class).value().length > 0) { reqUrl += method.getAnnotation(GetMapping.class).value()[0]; } if(StringUtils.hasText(queryString)){ reqUrl += queryString; } return HttpUtil.get(reqUrl, connectTimeout); case "POST": String postUrl = url; if (method.getAnnotation(PostMapping.class).value().length > 0) { postUrl += method.getAnnotation(PostMapping.class).value()[0]; } return HttpUtil.post(postUrl, JSON.toJSONString(args[0]), connectTimeout); case "DELETE": String deleteUrl = url; if (method.getAnnotation(DeleteMapping.class).value().length > 0) { deleteUrl += method.getAnnotation(DeleteMapping.class).value()[0]; } return HttpUtil.post(deleteUrl, JSON.toJSONString(args[0]), connectTimeout); case "PUT": String putUrl = url; if (method.getAnnotation(PutMapping.class).value().length > 0) { putUrl += method.getAnnotation(PutMapping.class).value()[0]; } return HttpUtil.post(putUrl, JSON.toJSONString(args[0]), connectTimeout); default: throw new RuntimeException("暂不支持的请求方式:" + requestMapping.method()[0].name()); } } private String getQueryString(Method method, Object[] args){ // 获取方法参数的数据 int count = method.getParameterCount(); Class<?>[] parameterTypes = method.getParameterTypes(); Parameter[] parameters = method.getParameters(); StringBuilder queryString = new StringBuilder(); for (int i = 0; i < count; i++) { Class<?> parameterType = parameterTypes[i]; //获取方法参数上面的注解 Annotation[][] parameterAnnotations = method.getParameterAnnotations(); Annotation annotation = parameterAnnotations[i][0]; RequestParam requestParam = (RequestParam) annotation; if (requestParam != null) { String value = requestParam.value(); String requestParamName = StringUtils.hasText(value) ? value : parameters[i].getName(); String requestParamValue = String.class.equals(parameterType) ? (String) args[i] : String.valueOf(args[i]); if(StringUtils.hasText(requestParamValue)){ queryString.append("&").append(requestParamName).append("=").append(requestParamValue); } } } return queryString.toString().replaceFirst("&", "?"); } } ``` 至此,我们的自研框架Feign核心代码就算完成了,结合spring就可以使用声明式注解去调用方法了。 上面发送的http请求是用的hutool的工具类,下面是测试: ``` #请求 http://127.0.0.1:8080/test?id=123 #结果 123 ``` [demo地址](https://github.com/657758014/demo/tree/main/demo-feign "demo地址")
我的主页
退出