# cloud2standalone **Repository Path**: tca/cloud2standalone ## Basic Information - **Project Name**: cloud2standalone - **Description**: 微服务转成单服务 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2021-11-22 - **Last Updated**: 2022-01-21 ## Categories & Tags **Categories**: Uncategorized **Tags**: 微服务转成单服务 ## README # spring-cloud 微服务转成 standalone单服务 ## 构建standalone单服务 pom.xml文件 ```xml tca equipment-biz 1.0.0 tca person-biz 1.0.0 ``` 启动类 ```java @SpringBootApplication @ComponentScan(basePackages = "com.tca") public class StandaloneApplication { public static void main(String[] args) { SpringApplication.run(StandaloneApplication.class, args); } } ``` ## 问题:如果直接启动, 报错 PersonFeign无法注入到EquipmentController中 ``` Field personFeign in com.tca.cloud.standalone.equipment.biz.controller.EquipmentController required a bean of type 'com.tca.cloud.standalone.person.api.service.PersonFeign' that could not be found. The injection point has the following annotations: - @org.springframework.beans.factory.annotation.Autowired(required=true) ``` ## 解决方案 ### step1 pom处理 ``` 将cloud相关组件都排除掉, 但是这里api中的@FeignClient会报错, 所以我们再将@FeignClient源码复制到项目里 ``` pom文件 ```xml tca equipment-biz 1.0.0 spring-cloud-starter-netflix-ribbon org.springframework.cloud tca person-biz 1.0.0 spring-cloud-starter-netflix-ribbon org.springframework.cloud ``` ### step2 拓展spring, 生成feign的代理 #### 自定义@EnableStandalone注解 ```java @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Documented @Import(FeignRegistrar.class) public @interface EnableStandalone { String[] value() default {}; } ``` 注解中引用了FeignRegistrar.class, 并且有一个value属性作为basePackage #### 自定义Feign扫描器 ```java /** * @author zhouan * @Date 2021/7/29 */ public class FeignRegistrar implements ImportBeanDefinitionRegistrar { @Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { AnnotationAttributes annoAttrs = AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(EnableStandalone.class.getName())); String[] basePackage = annoAttrs.getStringArray("value"); ClassPathFeignScanner classPathFeignScanner = new ClassPathFeignScanner(registry); classPathFeignScanner.scan(basePackage); } } ``` spring初始化时, 会调用ImportBeanDefinitionRegistrar对象的registerBeanDefinitions方法, 参数中一个是注解, 另一个是beanFactory 这个方法的作用是扫描到所有的@FeignClient的接口, 将它们封装成BeanDefinition, 并修改BeanDefinition的BeanClass属性为我们自定义的 FeignFactoryBean, 它实现了FactoryBean接口, 用于创建Feign接口的代理实现对象, 这里的扫描参考了spring-mybatis整合中的设计, 自定义 ClassPathFeignScanner扫描类, 继承了ClassPathBeanDefinitionScanner扫描类, 但是重写了相关方法 #### ClassPathFeignScanner ```java /** * @author zhouan * @Date 2021/7/31 */ @Slf4j public class ClassPathFeignScanner extends ClassPathBeanDefinitionScanner { /** * 后缀判断 */ private static final String PROXY_CLASS_SUFFIX = "Feign"; public ClassPathFeignScanner(BeanDefinitionRegistry registry) { super(registry); registerFilters(); } private void registerFilters() { // 添加过滤器, 判断是否需要扫描 addIncludeFilter((metadataReader, metadataReaderFactory) -> metadataReader.getAnnotationMetadata().hasAnnotation(FeignClient.class.getName()) ); } @Override public Set doScan(String... basePackages) { Set beanDefinitions = super.doScan(basePackages); if (!beanDefinitions.isEmpty()) { try { processBeanDefinitions(beanDefinitions); } catch (Exception e) { log.error("scan error ", e); throw new RuntimeException(e); } } return beanDefinitions; } private void processBeanDefinitions(Set beanDefinitions) throws ClassNotFoundException { GenericBeanDefinition definition; for (BeanDefinitionHolder holder : beanDefinitions) { definition = (GenericBeanDefinition) holder.getBeanDefinition(); // 这里使用的是: ScannedGenericBeanDefinition, 其beanClass目前还是字符串, 并不是Class对象, // 但是这里我们要用的是对象, 因此使用反射获取Class对象 definition.getPropertyValues().add("feignClass", Class.forName(definition.getBeanClassName())); definition.getPropertyValues().add("beanFactory", getRegistry()); // 添加相关属性 // 获取注解属性, 这里又添加了一个prefix属性, 用于获取当前FeignClient的contextId, 拼接url AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) definition; AnnotationMetadata annotationMetadata = beanDefinition.getMetadata(); Map attributes = annotationMetadata .getAnnotationAttributes( FeignClient.class.getCanonicalName()); definition.getPropertyValues().add("prefix", getPrefix(attributes)); definition.setBeanClass(FeignFactoryBean.class); } } /** * 从FeignClient中获取value作为prefix * @param attributes * @return */ private String getPrefix(Map attributes) { String path = (String) attributes.get("path"); return StringUtils.isEmpty(path)? "": path.endsWith("/")? path.substring(0, path.length() - 1): path; } /** * 重写方法: 根据Class判断是否需要扫描, 因为父类ClassPathScanningCandidateComponentProvider做了两层校验, * 所以需要配合上面的重载方法一起, 才会被扫描 * 这里必须是接口, 且命名以Feign结尾 * @param beanDefinition * @return */ @Override protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) { return beanDefinition.getMetadata().isInterface() && beanDefinition.getBeanClassName().endsWith(PROXY_CLASS_SUFFIX); } } ``` #### FeignFactoryBean ```java @Data public class FeignFactoryBean implements FactoryBean { private Class feignClass; private BeanFactory beanFactory; // 添加prefix属性, 用于拼接请求全路径 private String prefix; @Override public T getObject() throws Exception { InvocationHandler handler = new FeignProxyHandler<>(feignClass, beanFactory, prefix); return (T) Proxy.newProxyInstance(feignClass.getClassLoader(), new Class[]{feignClass}, handler); } @Override public Class getObjectType() { return feignClass; } @Override public boolean isSingleton() { return true; } } ``` #### FeignProxyHandler ```java 注意: 这个是4.0.0版本最大的地方, 原先是通过name找到feign对应的controller, 这里是依赖springmvc, 通过url和method找到匹配的方法!!! ``` ```java public class FeignProxyHandler implements InvocationHandler { private Class clazz; private BeanFactory beanFactory; private String prefix; private RequestMappingHandlerMapping handlerMapping; public FeignProxyHandler(Class clazz, BeanFactory beanFactory, String prefix) { this.clazz = clazz; this.beanFactory = beanFactory; this.prefix = prefix; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (handlerMapping == null) { synchronized (this) { if (handlerMapping == null) { handlerMapping = beanFactory.getBean(RequestMappingHandlerMapping.class); } } } if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this, args); } // 获取RequestMapping相关属性, 使用 AnnotatedElementUtils#findMergedAnnotation 方法, 可以获取到GetMapping、PostMapping等 RequestMapping methodMapping = AnnotatedElementUtils.findMergedAnnotation(method, RequestMapping.class); String path = prefix + methodMapping.value()[0]; RequestMethod[] requestMethods = methodMapping.method(); String methodType = requestMethods.length == 0? "": requestMethods[0].name(); // 组装HttpServletRequest, 通过HttpServletRequest获取对应的HandlerMethod!!! HandlerExecutionChain handler = this.handlerMapping.getHandler(new HttpServletRequestStandalone(path, methodType)); HandlerMethod handlerMethod = (HandlerMethod) handler.getHandler(); return handlerMethod.getMethod().invoke(handlerMethod.getBean(), args); } } ``` ## 总结 ### 原理 ``` 这里原理类似于 spring-mybatis 整合 (4.x版本) 1.自定义@EnableStandalone注解, 注解上使用 @Import 标签导入 FeignRegistrar, FeignRegistrar 实现了 ImportBeanDefinitionRegistrar 接口, spring在refresh()过程中, 会实例化所有 ImportBeanDefinitionRegistrar 实现类, 并回调其核心方法 registerBeanDefinitions, 该方法 用于向spring容器中注入 BeanDefinition, 用于后期bean的创建 2.我们在自定义 FeignRegistrar 的 registerBeanDefinitions 方法中, 自定了类扫描器 ClassPathBeanDefinitionScanner 的子类 ClassPathFeignScanner , (类扫描器 ClassPathBeanDefinitionScanner 是用于spring内置的用于解析 @ComponentScan 的类扫描), ClassPathFeignScanner使用父类的scan方法和doScan方法完成扫描, 并重写了 isCandidateComponent(AnnotatedBeanDefinition beanDefinition) 方法, 但是没有直接重写 isCandidateComponent(MetadataReader metadataReader)方法, 而是同时参考 spring-mybatis 源码中 ClassPathMapperScanner , 向容器中注册 TypeFilter, 因为父类 ClassPathBeanDefinitionScanner 的 isCandidateComponent(MetadataReader metadataReader) 方法中会使用注册的 TypeFilter 3.对于每一个@FeignClient的Class, 创建对应的BeanDefinition, BeanDefinition的name为Class的简单名, beanClass为自定义的FactoryBean 的实现类, FeignFactoryBean, (当beanClass为FactoryBean时, 创建的实际bean为FactoryBean#getObject方法获取的bean), 这里使用 FactoryBean, 并通过jdk动态代理的方式, 创建@FeignClient的Class的实现对象 4.这里仅仅有 FactoryBean 是不够的, 还需要有三个属性: Class对象, BeanFactory对象, prefix, 所以使用: definition.getPropertyValues().add("feignClass", feignClass); definition.getPropertyValues().add("beanFactory", registry); definition.getPropertyValues().add("prefix", prefix); 方法向BeanDefinition对应的beanClass中添加了 feignClass 和 beanFactory, prefix三个属性! 5. 4.x版本新增:如何通过url和method获取对应的controller!!! 5.1 在springmvc源码中我们知道: 我们是通过 RequestMappingHandlerMapping#getHandler(HttpServletRequest)来获取对应的 HandlerExecutionChain, HandlerExecutionChain 是对对应controller对应method的封装, 但是我们是通过接口调用的, 所以没有HttpServletRequest对象, 所以这里我们构造出HttpServletRequest对象, 用于获取HandlerExecutionChain 5.2 或者, 我们继续看源代码: RequestMappingHandlerMapping的父类的父类中有个重要的属性, 这个属性中封装了url和controller#method的对应关系: private final MappingRegistry mappingRegistry = new MappingRegistry(); 继续看 mappingRegistry, 其中有几个重要的属性: private final MultiValueMap urlLookup = new LinkedMultiValueMap<>(); // 存储url和List的关联关系 private final Map mappingLookup = new LinkedHashMap<>(); // 存储RequestMappingInfo和HandlerMethod的关联关系 但是这里的属性和类都没有对外开放, 所以全部需要通过反射来获取, 且我们根据url可以获取到多个 RequestMappingInfo, 仍然需要借助其他组件来选择最 匹配的 RequestMappingInfo, 再根据 RequestMappingInfo 获取 HandlerMethod, 过程较为繁琐, 所以这里采用了第一种方式, 直接构造HttpServletRequest 再通过RequestMappingHandlerMapping#getHandler方法来获取 ``` ### 演进 ``` 演进过程参考 release 分支 ```