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的注冊流程,后面我們有了數據就是轉發流程怎么根據這些數據進行流轉了,敬請期待。