soul单机部署简单架构
遵循 https://dromara.org/zh-cn/docs/soul/induction.html 教程,尝试本地搭建一个soul网关
一、启动soul-admin服务
soul-admin是soul网关的控制中心,负责网关的管理和配置,并同步给网关服务
-
上一节我们已经本地编译通过了整个soul项目的代码。我们可以直接通过idea引入整个soul项目
-
修改soul-admin项目中resources/application.yml的数据库配置文件
- 运行SoulAdminBootstrap
-
启动完成后,使用浏览器访问http://127.0.0.1:9095地址,进入到登录页面
-
输入默认用户名密码 admin , 123456。进入首页
到此 soul-admin的启动已经完成,至于具体的配置项我们之后在继续深入研究
二、搭建自己的网关
示例代码:https://github.com/wyc192273/soul-learn-project/tree/main/soul-bootstrap-test
- 首先使用idea创建一个spring-boot项目,可以参考soul-bootstrap项目
- 在pom引入如下配置
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<version>2.2.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<version>2.2.2.RELEASE</version>
</dependency>
<!--soul gateway start-->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>soul-spring-boot-starter-gateway</artifactId>
<version>${soul.last.version}</version>
</dependency>
<!--soul data sync start use websocket-->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>soul-spring-boot-starter-sync-data-websocket</artifactId>
<version>${soul.last.version}</version>
</dependency>
- 在application.yaml文件,加上如下配置
spring:
main:
allow-bean-definition-overriding: true
management:
health:
defaults:
enabled: false
soul :
sync:
websocket :
# 设置你的admin的地址
urls: ws://localhost:9095/websocket
- 启动项目后,网关便启动成功了
2021-01-15 22:14:11.116 INFO 68511 --- [ main] com.soul.bootstrap.Application : Starting Application on kuaikandeMacBook-Pro-8.local with PID 68511 (/Users/kuaikan/work/open_source/soul-learn-project/soul-bootstrap-test/target/classes started by kuaikan in /Users/kuaikan/work/open_source/website)
2021-01-15 22:14:11.117 INFO 68511 --- [ main] com.soul.bootstrap.Application : No active profile set, falling back to default profiles: default
2021-01-15 22:14:13.791 INFO 68511 --- [ main] o.d.s.w.configuration.SoulConfiguration : load plugin:[global] [org.dromara.soul.plugin.global.GlobalPlugin]
2021-01-15 22:14:13.963 INFO 68511 --- [ main] b.s.s.d.w.WebsocketSyncDataConfiguration : you use websocket sync soul data.......
2021-01-15 22:14:14.107 INFO 68511 --- [ main] o.d.s.p.s.d.w.WebsocketSyncDataService : websocket connection is successful.....
2021-01-15 22:14:14.197 INFO 68511 --- [ main] o.s.b.a.e.web.EndpointLinksResolver : Exposing 2 endpoint(s) beneath base path '/actuator'
2021-01-15 22:14:14.448 INFO 68511 --- [ main] o.s.b.web.embedded.netty.NettyWebServer : Netty started on port(s): 8080
2021-01-15 22:14:14.452 INFO 68511 --- [ main] com.soul.bootstrap.Application : Started Application in 4.334 seconds (JVM running for 5.909)
三、网关接入Spring Boot 项目
示例代码:
https://github.com/wyc192273/soul-learn-project/tree/main/spring-boot-test
https://github.com/wyc192273/soul-learn-project/tree/main/spring-boot-test2
我们从最简单的spring-boot项目入手
通过前面两步我们已经部署了我们的soul-admin(soul网关控制台),soul-bootstrap(网关),接下来我要接入我们的spring-boot项目
在<u>网关项目spring-bootstrap</u>中,引入pom依赖,来支持http代理
这里要注意,下面的配置是在soul-bootstrap网关项目中!!!不要加到我们自己的spring-boot项目中
- pom文件引入相关依赖
<!--if you use http proxy start this-->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>soul-spring-boot-starter-plugin-divide</artifactId>
<version>${last.version}</version>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>soul-spring-boot-starter-plugin-httpclient</artifactId>
<version>${last.version}</version>
</dependency>
- 重新启动soul-bootstrap
2021-01-15 22:41:43.064 INFO 68666 --- [ main] com.soul.bootstrap.Application : Starting Application on kuaikandeMacBook-Pro-8.local with PID 68666 (/Users/kuaikan/work/open_source/soul-learn-project/soul-bootstrap-test/target/classes started by kuaikan in /Users/kuaikan/work/open_source/website)
2021-01-15 22:41:43.065 INFO 68666 --- [ main] com.soul.bootstrap.Application : No active profile set, falling back to default profiles: default
2021-01-15 22:41:44.656 INFO 68666 --- [ main] o.d.s.w.configuration.SoulConfiguration : load plugin:[global] [org.dromara.soul.plugin.global.GlobalPlugin]
2021-01-15 22:41:44.657 INFO 68666 --- [ main] o.d.s.w.configuration.SoulConfiguration : load plugin:[divide] [org.dromara.soul.plugin.divide.DividePlugin]
2021-01-15 22:41:44.657 INFO 68666 --- [ main] o.d.s.w.configuration.SoulConfiguration : load plugin:[webClient] [org.dromara.soul.plugin.httpclient.WebClientPlugin]
2021-01-15 22:41:44.657 INFO 68666 --- [ main] o.d.s.w.configuration.SoulConfiguration : load plugin:[divide] [org.dromara.soul.plugin.divide.websocket.WebSocketPlugin]
2021-01-15 22:41:44.657 INFO 68666 --- [ main] o.d.s.w.configuration.SoulConfiguration : load plugin:[response] [org.dromara.soul.plugin.httpclient.response.WebClientResponsePlugin]
2021-01-15 22:41:44.793 INFO 68666 --- [ main] b.s.s.d.w.WebsocketSyncDataConfiguration : you use websocket sync soul data.......
2021-01-15 22:41:44.842 INFO 68666 --- [ main] o.d.s.p.s.d.w.WebsocketSyncDataService : websocket connection is successful.....
2021-01-15 22:41:44.929 INFO 68666 --- [ main] o.s.b.a.e.web.EndpointLinksResolver : Exposing 2 endpoint(s) beneath base path '/actuator'
2021-01-15 22:41:45.196 INFO 68666 --- [ main] o.s.b.web.embedded.netty.NettyWebServer : Netty started on port(s): 8080
2021-01-15 22:41:45.208 INFO 68666 --- [ main] com.soul.bootstrap.Application : Started Application in 2.781 seconds (JVM running for 3.73)
会发现日志中增加了额外的几个插件plugin,其中就包括下面要说的divide插件
- 设置divide插件
需要设置divide插件为开启。该插件实现HTTP正向代理的功能,所有HTTP类型相关的请求,都由它进行负载均衡的调用
使用浏览器访问http://127.0.0.1:9095/#/system/plugin,进入「系统管理-插件管理菜单」,保证divide插件是开启状态
- 默认情况下就是开启状态
- 接下来搭建我们自己的Spring Boot项目,并简单支持http请求
- 在pom引入如下依赖。
<dependency>
<!-- Soul对SpringMVC的集成支持 -->
<groupId>org.dromara</groupId>
<artifactId>soul-spring-boot-starter-client-springmvc</artifactId>
<version>${last.version}</version>
</dependency>
- 修改application.yaml文件,添加如下配置项
soul:
# Soul 针对 SpringMVC 的配置项,对应 SoulHttpConfig 配置类
http:
admin-url: http://127.0.0.1:9095 # Soul Admin 地址
context-path: /my-api # 设置在 Soul 网关的路由前缀,例如说 /order、/product 等等。
# 后续,网关会根据该 context-path 来进行路由
app-name: my-service # 应用名。未配置情况下,默认使用 `spring.application.name` 配置项
port: 8081 #你本项目的启动端口
full: false # 设置true 代表代理你的整个服务,false表示代理你其中某几个controller,如果设置了false,我们需要在自己的要拦截controller类或方法上手动添加@SoulSpringMvcClient注解
- 添加@SoulSpringMvcClient注解。这里我修改在UserController类上
@RestController
@RequestMapping("user")
@SoulSpringMvcClient(path = "/user")
public class UserController {
@GetMapping("find_by_id")
public Object findById(@RequestParam("id") int id) {
User user = new User();
user.setId(id);
user.setName("name_" + id);
return user;
}
}
- 启动项目, 发现下面这些信息和soul有关
// 暂时忽略
2021-01-15 23:13:27.625 INFO 68877 --- [ main] trationDelegate$BeanPostProcessorChecker : Bean 'org.dromara.soul.springboot.starter.client.springmvc.SoulSpringMvcClientConfiguration' of type [org.dromara.soul.springboot.starter.client.springmvc.SoulSpringMvcClientConfiguration$$EnhancerBySpringCGLIB$$23a5833d] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2021-01-15 23:13:27.664 INFO 68877 --- [ main] trationDelegate$BeanPostProcessorChecker : Bean 'soulHttpConfig' of type [org.dromara.soul.client.springmvc.config.SoulSpringMvcConfig] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
//同步配置到soul-admin
2021-01-15 23:22:56.723 INFO 68904 --- [pool-1-thread-1] o.d.s.client.common.utils.RegisterUtils : http client register success: {"appName":"my-service","context":"/my-api","path":"/my-api/user/*","pathDesc":"","rpcType":"http","host":"192.168.1.7","port":8081,"ruleName":"/my-api/user/*","enabled":true,"registerMetaData":false}
- 我们访问http://127.0.0.1:9095/#/plug/divide,看插件列表,如下图所示
至于插件的具体作用,我们后面章节在讨论
- 接下来我们使用浏览器请求网关的地址http://localhost:8080/my-api/user/find_by_id?id=1 , 会转发到真正的服务上。
我们的网关并接入Spring Boot项目已经完成
问题
- 讲@SoulSpringMvcClient注解到类上,path没有写*导致没有注册到selector上
通过查看@SoulSpringMvcClient发现,只有SpringMvcClientBeanPostProcessor有用到该注解。那么我们跳到SpringMvcClientBeanPostProcessor类里看下具体逻辑
- 类SpringMvcClientBeanPostProcessor重载了BeanPostProcessor的postProcessAfterInitialization方法
class SpringMvcClientBeanPostProcessor implements BeanPostProcessor
- 构造方法,会将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<>());
}
- 主要是校验了contextPath和adminUrl 和port不可为空,这些是我们之前在application.yaml里面的配置
// org.dromara.soul.client.springmvc.utils.ValidateUtils#validate
/**
* 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);
}
- 重点是 postProcessAfterInitialization 的实现
@Override
public Object postProcessAfterInitialization(@NonNull final Object bean, @NonNull final String beanName) throws BeansException {
//如果我们配置了全部代理,则直接返回bean
if (soulSpringMvcConfig.isFull()) {
return bean;
}
//这里主要是想拦截所有的Controller类
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则代表是SpringMVC的Controller
if (controller != null || restController != null || requestMapping != null) {
//看类上是否有SoulSpringMvcClient注解
SoulSpringMvcClient clazzAnnotation = AnnotationUtils.findAnnotation(bean.getClass(), SoulSpringMvcClient.class);
String prePath = "";
//如果有SoulSpringMvcClient注解
if (Objects.nonNull(clazzAnnotation)) {
//如果path中存在*号
if (clazzAnnotation.path().indexOf("*") > 1) {
//则finalPrePath最后的前缀更新为当前注解中配置的path
String finalPrePath = prePath;
//注册相关配置
executorService.execute(() -> RegisterUtils.doRegister(buildJsonParams(clazzAnnotation, finalPrePath), url,
RpcTypeEnum.HTTP));
return bean;
}
//如果path中不能存在*,则讲当前注解的path更新为prePath
prePath = clazzAnnotation.path();
}
//获取当前controller的所有方法
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;
}
- 我们在看下buildJosnParams具体干了什么
private String buildJsonParams(final SoulSpringMvcClient soulSpringMvcClient, final String prePath) {
String contextPath = soulSpringMvcConfig.getContextPath();
String appName = soulSpringMvcConfig.getAppName();
Integer port = soulSpringMvcConfig.getPort();
//构造paht = application.yaml中的contextPath + 参数的prePath + 注解的path
String path = contextPath + prePath + soulSpringMvcClient.path();
String desc = soulSpringMvcClient.desc();
String configHost = soulSpringMvcConfig.getHost();
String host = StringUtils.isBlank(configHost) ? IpUtils.getHost() : configHost;
String configRuleName = soulSpringMvcClient.ruleName();
String ruleName = StringUtils.isBlank(configRuleName) ? path : configRuleName;
//构造 SpringMVC注册的 model
SpringMvcRegisterDTO registerDTO = SpringMvcRegisterDTO.builder()
.context(contextPath)
.host(host)
.port(port)
.appName(appName)
.path(path)
.pathDesc(desc)
.rpcType(soulSpringMvcClient.rpcType())
.enabled(soulSpringMvcClient.enabled())
.ruleName(ruleName)
.registerMetaData(soulSpringMvcClient.registerMetaData())
.build();
return OkHttpTools.getInstance().getGson().toJson(registerDTO);
}
- 构造了SpringMVC注册json,然后在通过doRegister方法注册相关信息到soul-admin
//这里就是简单的通过OkHttp工具发送post请求注册到soul-admin
public static void doRegister(final String json, final String url, final RpcTypeEnum rpcTypeEnum) {
try {
String result = OkHttpTools.getInstance().post(url, json);
if (AdminConstants.SUCCESS.equals(result)) {
log.info("{} client register success: {} ", rpcTypeEnum.getName(), json);
} else {
log.error("{} client register error: {} ", rpcTypeEnum.getName(), json);
}
} catch (IOException e) {
log.error("cannot register soul admin param, url: {}, request body: {}", url, json, e);
}
}
- 这里注册的url是通过构造方法中构造的。这里会看到就是我们application.yaml配置的adminUrl加上一个固定的请求
url = soulSpringMvcConfig.getAdminUrl() + "/soul-client/springmvc-register";
- 到这里我们就将SpringMVC注册的逻辑梳理清晰了