# 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 分支
```