昨天只是极简入门,关于网关是怎么感知到我们的应用的,相信小伙伴们一定有疑问,今天先来看下 HTTP 用户如何接入 Soul,以及接入的流程是怎样的。
这是官网对于 HTTP 用户的文档,https://dromara.org/zh-cn/docs/soul/user-http.html。
官网文档明确指出,Soul 网关使用 divide 插件来处理http请求,我们在 soul-admin 页面中看到,第一个就是 divide 插件,而且是默认开启的。
Http请求接入网关,分2种情况,1种是 springMvc 体系用户,1种是非 springMvc 体系。
对于非 springMvc 体系接入,是通过 soul-admin 的页面操作来完成的,这个流程我们后边再分析。
对于 springMvc 体系用户,通过注解和配置,就自动同步到 divide 插件下的选择器规则列表,我们今天就来一探究竟。
无论是 Spring Boot 用户,还是 Spring MVC 用户,都需要一段配置(以 Spring Boot 为例)
soul:
http:
adminUrl: http://localhost:9095
port: 你本项目的启动端口
contextPath: /http
appName: http
full: false
# adminUrl: 为你启动的soul-admin 项目的ip + 端口,注意要加http://
# port: 你本项目的启动端口
# contextPath: 为你的这个mvc项目在soul网关的路由前缀,这个你应该懂意思把? 比如/order ,/product 等等,网关会根据你的这个前缀来进行路由.
# appName:你的应用名称,不配置的话,会默认取 `spring.application.name` 的值
# full: 设置true 代表代理你的整个服务,false表示代理你其中某几个controller
回到我们的测试项目 soul-examples-http 中,查看其配置文件 application.yml
soul:
http:
adminUrl: http://localhost:9095
port: 8188
contextPath: /http
appName: http
full: false
在 http 层级下任一属性上,Ctrl+B,自动跳转到 Bean 类 SoulSpringMvcConfig。
@Data
public class SoulSpringMvcConfig {
private String adminUrl;
private String contextPath;
private String appName;
/**
* Set true means providing proxy for your entire service, or only a few controller.
*/
private boolean full;
private String host;
private Integer port;
}
不得不佩服 IDEA 的开发团队,好多功能简直太强大太方便了,在 SoulSpringMvcConfig 类左边,直接导航到 Spring Bean 的定义。
直接就跳转到这里了,SoulSpringMvcClientConfiguration 这个类,上代码
/**
* Soul http config soul http config.
*
* @return the soul http config
*/
@Bean
@ConfigurationProperties(prefix = "soul.http")
public SoulSpringMvcConfig soulHttpConfig() {
return new SoulSpringMvcConfig();
}
看下这个类的坐标,又看到了 spring.factories 和 spring.providers,是不是想起了什么?对的,聪明的你一定想到了,那就是 Spring Boot Starter。
# spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.dromara.soul.springboot.starter.client.springmvc.SoulSpringMvcClientConfiguration
# spring.provides
provides: soul-spring-boot-starter-client-springmvc
关于 Spring Boot Starter 这里就不多介绍了,感兴趣的小伙伴,可以自行学习。
可以看到 SoulSpringMvcClient 注解定义,既可以加在类上,也可以加在方法上。
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface SoulSpringMvcClient{
String path();
String ruleName() default "";
String desc() default "";
String rpcType() default "http";
boolean enabled() default true;
boolean registerMetaData() default false;
}
查找该注解在哪里引用了,除了测试用例及 import 导包,可以看到有2个地方引用了,点进去一探究竟。
首先看下这个类的定义,实现了 BeanPostProcessor 接口,重写了 postProcessAfterInitialization 方法。
public class SpringMvcClientBeanPostProcessor implements BeanPostProcessor
再次看到这熟悉的图标 ,看看这个类在哪里注入到 Spring IoC 容器里的。再次跳转到了这个类 SoulSpringMvcClientConfiguration,跟上面 SoulSpringMvcConfig 类的注入都是这个类,并且把 SoulSpringMvcConfig 作为构造参数传进去了。
@Configuration
public class SoulSpringMvcClientConfiguration {
/**
* Spring http client bean post processor spring http client bean post processor.
*
* @param soulSpringMvcConfig the soul http config
* @return the spring http client bean post processor
*/
@Bean
public SpringMvcClientBeanPostProcessor springHttpClientBeanPostProcessor(final SoulSpringMvcConfig soulSpringMvcConfig) {
return new SpringMvcClientBeanPostProcessor(soulSpringMvcConfig);
}
...
}
好吧,到这里,我们再回到 SpringMvcClientBeanPostProcessor,看看里面具体的逻辑是怎样的。
先来看下类属性和构造方法。
private final ThreadPoolExecutor executorService;
private final String url;
private final SoulSpringMvcConfig soulSpringMvcConfig;
/**
* Instantiates a new Soul client bean post processor.
*
* @param soulSpringMvcConfig the soul spring mvc config
*/
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<>());
}
构造方法第1行,对 soulSpringMvcConfig 做了个参数校验,保证 contextPath、adminUrl、port 参数都有值,否则报错。
/**
* validate SoulSpringMvcConfig.
*
* @param soulSpringMvcConfig the soulSpringMvcConfig
* @throws RuntimeException the RuntimeException
*/
public static void validate(final SoulSpringMvcConfig soulSpringMvcConfig) {
String contextPath = soulSpringMvcConfig.getContextPath();
String adminUrl = soulSpringMvcConfig.getAdminUrl();
Integer port = soulSpringMvcConfig.getPort();
if (StringUtils.isNotBlank(contextPath) && StringUtils.isNotBlank(adminUrl) && port != null) {
return;
}
String errorMsg = "spring mvc param must config contextPath, adminUrl and port";
log.error(errorMsg);
throw new RuntimeException(errorMsg);
}
构造方法把前面在配置文件中配置的属性传了进来,第3行,把 adminUrl 拼接成了 URL,可以预见,这个就是要调用 soul-admin 接口,实现自动注册的关键,我们拭目以待。第4行,启动了一个固定线程为1的线程池。
好了,终于来到了最核心的逻辑(postProcessAfterInitialization 方法),就是如何把前面这些内容整合利用起来,上代码。
- @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;
- }
第3行,soulSpringMvcConfig.isFull(),我的理解是,如果配置成 true,整个系统都被网关代理,就无需每个Controller 上都加注解了。(不知道对不对,后续遇到为 true 的情况再补充,存疑!)
6~9行,这里只处理 SpringMVC Controller。
10~20行,处理在类上加注解的情况。而22~29行,处理在方法上加注解的情况。
处理逻辑类似,都是根据注解拿到注解具体内容,将注解的内容加上配置文件内容封装,通过 RegisterUtils 这个工具类的 doRegister 方法,调用 soul-admin 方法,将需要网关拦截的接口信息发送过去并持久化。
好的,到这里,我们就把 SpringMVC 接入 Soul 网关的流程打通了。
回顾一下,用户通过 配置文件 + 注解,将需要接入 Soul 网关的接口信息呈现出来,网关使用 Spring Boot Starter,将信息封装,通过调用 soul-admin 接口的方式注册。
今天就先到这里吧,明天见。