[Soul 源码之旅] 1.8 Soul插件初体验 (Divid
Divide 插件是 Soul 中最基础的插件之一,主要负责Spring MVC 项目的请求转发,我们这次从这里开始一步步探索 Soul 处理 Spring MVC 转发的整体流程。
1.8.1 插件数据注册流程
1.8.1.1 Spring MVC 项目注册数据
在使用Divide 插件,我们只需要在项目中引入 如下依赖:
<dependency>
<groupId>org.dromara</groupId>
<artifactId>soul-spring-boot-starter-client-springmvc</artifactId>
<version>${soul.version}</version>
</dependency>
和以下 soul 配置信息, 这些功能我们接下来一一解析。
soul:
http:
adminUrl: http://localhost:9095
port: 8188
contextPath: /http
appName: http
full: false
我们还是按照之前的流程,从 soul-spring-boot-starter-client-springmvc 开始,这里只引入了配置类 SoulSpringMvcClientConfiguration ,这个类只引入了三个 Bean ,我们接下来一个个看他们的作用。首先是 soulHttpConfig 这个类主要是读取 soul 的配置信息,然后给接下来两个 bean 使用。
我们先看第一个 bean SpringMvcClientBeanPostProcessor 这里先校验数据是否正确,然后生成一个线程池,主要是为后面注册服务使用。
public SpringMvcClientBeanPostProcessor(final SoulSpringMvcConfig soulSpringMvcConfig) {
ValidateUtils.validate(soulSpringMvcConfig);
this.soulSpringMvcConfig = soulSpringMvcConfig;
url = soulSpringMvcConfig.getAdminUrl() + "/soul-client/springmvc-register";
executorService = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>());
}
它实现了 BeanPostProcessor 接口,按照套路,他就是在 postProcessAfterInitialization 方法做信息初始化流程。他会先检测该bean 是否有 Controller RestController 或者 RequestMapping 三个注解,假如有 再看这个类是否有SoulSpringMvcClient 这个注解,然后检测该注解的 path 属性是否有 /* 的属性,有则注册整个路径 如 /test/** ,然后在 buildJsonParams 中还将会拼接上 上面配置的 context path ,然后向线程池里面建立一个任务,就是向 admin 注册服务信息。假如类上没有 SoulSpringMvcClient 注解,则遍历该类上的所有方法,同样走一遍上面的注册流程。
@Override
public Object postProcessAfterInitialization(@NonNull final Object bean, @NonNull final String beanName) throws BeansException {
if (soulSpringMvcConfig.isFull()) {
return bean;
}
Controller controller = AnnotationUtils.findAnnotation(bean.getClass(), Controller.class);
RestController restController = AnnotationUtils.findAnnotation(bean.getClass(), RestController.class);
RequestMapping requestMapping = AnnotationUtils.findAnnotation(bean.getClass(), RequestMapping.class);
if (controller != null || restController != null || requestMapping != null) {
SoulSpringMvcClient clazzAnnotation = AnnotationUtils.findAnnotation(bean.getClass(), SoulSpringMvcClient.class);
String prePath = "";
if (Objects.nonNull(clazzAnnotation)) {
if (clazzAnnotation.path().indexOf("*") > 1) {
String finalPrePath = prePath;
executorService.execute(() -> RegisterUtils.doRegister(buildJsonParams(clazzAnnotation, finalPrePath), url,
RpcTypeEnum.HTTP));
return bean;
}
prePath = clazzAnnotation.path();
}
final Method[] methods = ReflectionUtils.getUniqueDeclaredMethods(bean.getClass());
for (Method method : methods) {
SoulSpringMvcClient soulSpringMvcClient = AnnotationUtils.findAnnotation(method, SoulSpringMvcClient.class);
if (Objects.nonNull(soulSpringMvcClient)) {
String finalPrePath = prePath;
executorService.execute(() -> RegisterUtils.doRegister(buildJsonParams(soulSpringMvcClient, finalPrePath), url,
RpcTypeEnum.HTTP));
}
}
}
return bean;
}
我们接着看一下 ContextRegisterListener
public ContextRegisterListener(final SoulSpringMvcConfig soulSpringMvcConfig) {
ValidateUtils.validate(soulSpringMvcConfig);
this.soulSpringMvcConfig = soulSpringMvcConfig;
url = soulSpringMvcConfig.getAdminUrl() + "/soul-client/springmvc-register";
}
同时它也实现了接口 ApplicationListener<ContextRefreshedEvent> ,ContextRefreshedEvent 是Spring 内置的事件,但所有Spring bean 分发完成后就会触发 onApplicationEvent 方法。onApplicationEvent 方法是先判断Spring 的配置 isfull 但为true 时执行注册流程。 这里有个关键点,这里会用一个AtomicBoolean 来设置是否已经完成注册,这是因为在 web应用会出现父子容器,这个事件会触发两次, 这里通过这个状态看是否已经注册过该信息。
@Override
public void onApplicationEvent(final ContextRefreshedEvent contextRefreshedEvent) {
if (!registered.compareAndSet(false, true)) {
return;
}
if (soulSpringMvcConfig.isFull()) {
RegisterUtils.doRegister(buildJsonParams(), url, RpcTypeEnum.HTTP);
}
}
那么 isfull 属性是干什么但呢,我们看一下它但 buildJsonParam 有点不一样。这里注册路径是直接注册成 contextPath + /** 这就意味着该服务代理 /contextPath/** 下所有转发请求,我们看到上面 postProcessAfterInitialization 中也做了判断,就是假如是 full ,那么就不扫描各个 bean了, 因为他已经代理了contextPath 下但所有请求,没必要后面一个个注册。
private String buildJsonParams() {
String contextPath = soulSpringMvcConfig.getContextPath();
String appName = soulSpringMvcConfig.getAppName();
Integer port = soulSpringMvcConfig.getPort();
String path = contextPath + "/**";
String configHost = soulSpringMvcConfig.getHost();
String host = StringUtils.isBlank(configHost) ? IpUtils.getHost() : configHost;
SpringMvcRegisterDTO registerDTO = SpringMvcRegisterDTO.builder()
.context(contextPath)
.host(host)
.port(port)
.appName(appName)
.path(path)
.rpcType(RpcTypeEnum.HTTP.getName())
.enabled(true)
.ruleName(path)
.build();
return OkHttpTools.getInstance().getGson().toJson(registerDTO);
}
1.8.1.2 Soul admin
我们可以看到 客户端都是通过调用 /soul-client/springmvc-register 这个 admin 的 Rest 接口进行请求的。我们看看它做了什么。它调用了 soulClientRegisterService 进行信息注册。这里先判断是否是注册元信息,假如是则调用 saveSpringMvcMetaData 进行注册。
@Override
@Transactional
public String registerSpringMvc(final SpringMvcRegisterDTO dto) {
if (dto.isRegisterMetaData()) {
MetaDataDO exist = metaDataMapper.findByPath(dto.getPath());
if (Objects.isNull(exist)) {
saveSpringMvcMetaData(dto);
}
}
String selectorId = handlerSpringMvcSelector(dto);
handlerSpringMvcRule(selectorId, dto);
return SoulResultMessage.SUCCESS;
}
这里 根据selector 的id 更新 rule。
private void handlerSpringMvcRule(final String selectorId, final SpringMvcRegisterDTO dto) {
RuleDO ruleDO = ruleMapper.findByName(dto.getRuleName());
if (Objects.isNull(ruleDO)) {
registerRule(selectorId, dto.getPath(), dto.getRpcType(), dto.getRuleName());
}
}
registe流程如下, 先拼接规则,假如是有 * 则使用 match 规则,假如没有则是 =规则。最终调用 ruleService.register 进行注册信息。
private void registerRule(final String selectorId, final String path, final String rpcType, final String ruleName) {
RuleHandle ruleHandle = RuleHandleFactory.ruleHandle(RpcTypeEnum.acquireByName(rpcType), path);
RuleDTO ruleDTO = RuleDTO.builder()
.selectorId(selectorId)
.name(ruleName)
.matchMode(MatchModeEnum.AND.getCode())
.enabled(Boolean.TRUE)
.loged(Boolean.TRUE)
.sort(1)
.handle(ruleHandle.toJson())
.build();
RuleConditionDTO ruleConditionDTO = RuleConditionDTO.builder()
.paramType(ParamTypeEnum.URI.getName())
.paramName("/")
.paramValue(path)
.build();
if (path.indexOf("*") > 1) {
ruleConditionDTO.setOperator(OperatorEnum.MATCH.getAlias());
} else {
ruleConditionDTO.setOperator(OperatorEnum.EQ.getAlias());
}
ruleDTO.setRuleConditions(Collections.singletonList(ruleConditionDTO));
ruleService.register(ruleDTO);
}
最终调用了 publishEvent 方法
private void publishEvent(final RuleDO ruleDO, final List<RuleConditionDTO> ruleConditions) {
SelectorDO selectorDO = selectorMapper.selectById(ruleDO.getSelectorId());
PluginDO pluginDO = pluginMapper.selectById(selectorDO.getPluginId());
List<ConditionData> conditionDataList =
ruleConditions.stream().map(ConditionTransfer.INSTANCE::mapToRuleDTO).collect(Collectors.toList());
// publish change event.
eventPublisher.publishEvent(new DataChangedEvent(ConfigGroupEnum.RULE, DataEventTypeEnum.UPDATE,
Collections.singletonList(RuleDO.transFrom(ruleDO, pluginDO.getName(), conditionDataList))));
}
这里就又回到了我们之前说的 DataChangedEventDispatcher 方法,和我们之前的流程串起来了。
1.8.1.3 总结
今天主要介绍了 SpringMVC 客户端Divide的注册流程,后面我们有了数据就是转发流程怎么根据这些数据进行流转了,敬请期待。