CrazyAirhead

疯狂的傻瓜,傻瓜也疯狂——傻方能执著,疯狂才专注!

0%

说明

限流

限流是控制系统的的并发流量,通过限制请求流量的手段防止过度的流量导致系统崩溃。限流的指标有TPS, HPS、QPS等,限流的方法有流量计数器,滑动时间窗口,漏桶算法,令牌桶算法等。

熔断

熔断的概念来自电路保护,提供了断路开关,主要有三种状态,关闭状态,打开状态和半开状态。熔断可以防止应用程序不断地尝试可能超时和失败的服务,能达到应用程序执行而不必等待下游服务修正错误服务。

降级

降级是指当自身服务压力增大时,采取一些手段,增强自身服务的处理能力,以保障服务的持续可用。比如,下线非核心服务以保证核心服务的稳定、降低实时性、降低数据一致性等。

三者关系

限流、熔断和服务降级是系统容错的重要设计模式,从一定意义上讲限流和熔断也是一种服务降级的手段。

措施 产生原因 针对服务
熔断 下游服务不可用 下游服务
降级 自身服务的处理能力不够 自身服务
限流 上游服务请求增多 上游服务

Sentinel

Sentinel 是面向分布式、多语言异构化服务架构的流量治理组件,主要以流量为切入点,从流量路由、流量控制、流量整形、熔断降级、系统自适应过载保护、热点流量防护等多个维度来帮助开发者保障微服务的稳定性。

之前使用的是Spring Cloud 默认带的是 Hystrix,之前也没有用过Sentinel,这次按理是尝试使用Setinel。

依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
dependencies {
implementation platform(project(":demo-parent"))

implementation("org.noear:solon-web")
implementation("org.noear:solon-logging-logback")
implementation("org.noear:solon-openapi2-knife4j")
implementation("org.noear:sentinel-solon-cloud-plugin")
implementation("com.alibaba.csp:sentinel-transport-simple-http:1.8.8")

annotationProcessor("org.mapstruct:mapstruct-processor:${mapstructVersion}")

testImplementation("org.noear:solon-test-junit5")
}

配置

1
2
3
4
solon.cloud.local:
breaker:
root: 1000
hello: 10

Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.example.demo.solon.controller;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.noear.solon.annotation.Controller;
import org.noear.solon.annotation.Mapping;
import org.noear.solon.annotation.Param;
import org.noear.solon.cloud.annotation.CloudBreaker;

/**
* @author airhead
*/
@Api("Demo")
@Controller
@Mapping
public class DemoController {
@ApiOperation("hello")
@Mapping("/hello")
@CloudBreaker("hello")
public String hello(@Param(defaultValue = "world") String name) {
return String.format("Hello %s!", name);
}
}

验证

需要通过多线程的方式来测试,写个单元测试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package com.example.demo.solon.controller;

import static org.junit.jupiter.api.Assertions.*;

import com.example.demo.solon.DemoCloudBreakerApp;
import java.util.concurrent.CountDownLatch;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.noear.solon.Utils;
import org.noear.solon.test.HttpTester;
import org.noear.solon.test.SolonTest;

@Slf4j
@SolonTest(DemoCloudBreakerApp.class)
public class DemoControllerTest extends HttpTester {
@Test
public void test() throws Exception {
CountDownLatch count = new CountDownLatch(100);
for (int i = 0; i < 100; i++) {
log.info("get hello start. i:{}", i);
int finalI = i;
Utils.async(
() -> {
try {
path("/hello").get();
} catch (Exception e) {
log.error("", e);
}

log.info("get hello end. i:{}", finalI);
count.countDown();
});
}

count.await();
}
}

当超过限流时,提示错误。

img

问题

  1. 当前使用的版本3.0.9及 3.1.0-M3版本暂时不能把 CloudBreaker 注解在类上。
  2. 当前版本无法注册到 sentinel 的 dashboard 中,属于本地配置。
  3. 当前支持限流,没有熔断的支持。

小结

从测试使用的情况来看,Solon 目前只支持本地的限流处理,暂时不支持熔断,对 sentinel 的支持还不够完善,无法注册到 sentinel 的 dashboard 中。如果需要用于生产,需要自己扩展下对应的插件,或者再等一等官方的更新。

说明

关于 Solon Cloud 目前正在准备「熔断与限流」 的部分,想使用 sentinel 的插件,但我之前只用过 Spring 集成的 Hystrix,还需要先学习下 sentinel,目前还在准备当中,不会那么快更新。Solon AI 是 《Solon 实用教程》的第六部分,算是新增章节,恰好之前有使用过 agents-flex,准备起来更容易些,因此先开始更新 Solon AI 部分。

Feishu Docs - Image

Solon 将在 3.1.0 版本引入 AI 相关插件,现在已经发布 SNAPSHOT 版本,现在接口和功能还不稳定,只建议尝鲜。

从官网的介绍中(https://solon.noear.org/article/learn-solon-ai),可以看到Solon AI 对大模型的支持是比较完整的,聊天模型接口支持同步调用,流式调用,Function Call,记忆功能,多种消息角色和多种消息格式,提供 RAG 支持和流程编排。

在初体验中,我测试的是聊天模型的同步调用,流式调用,Function Call这几个功能。

前置

本地测试使用了 ollama,需要安装好 ollama (https://ollama.com/download),并下载好模型,我这里演示用的两个模型一个是deepseek-r1:7b(基于 qwen2.5 的蒸馏的一个推理模型)和 qwen2.5:7b(支持tools,在ollama 可以通过点击 tools 标签查看哪些模型支持 tools)。

1
2
ollama run deepseek-r1:7b
ollama run qwen2.5:7b

如果无法调用 ollama 接口时,可做如下配置,主要处理跨域或者局域网调用。

1
2
3
4
5
6
vi .zshrc

export OLLAMA_ORIGINS="*"
export OLLAMA_HOST=0.0.0.0:11434

source .zshrc

依赖

增加 solon-ai 的依赖,solon-web-rx 和 solon-web-sse 用于支持流式调用。

1
2
3
4
5
6
7
8
9
10
11
12
dependencies {
implementation platform(project(":demo-parent"))

implementation("org.noear:solon-web")
implementation("org.noear:solon-ai")
implementation("org.noear:solon-logging-logback")
implementation("org.noear:solon-openapi2-knife4j")
implementation("org.noear:solon-web-rx")
implementation("org.noear:solon-web-sse")

testImplementation("org.noear:solon-test")
}

配置

我这里使用 ollama 服务时,需要配置 provider。如果使用云服务时,设置 apiKey。

这里配置 timeout 是避免使用推理模型时,接口调用时间过长报错问题,可以根据自己的模型和机器的配置情况进行调整。

1
2
3
4
5
6
7
8
demo:
llm:
apiUrl: "http://127.0.0.1:11434/api/chat" # 使用完整地址(而不是 api_base)
# apiKey: "xxxx"
provider: "ollama"
# model: "deepseek-r1:7b"
model: "qwen2.5:7b"
timeout: 600s

实现

LlmConfig

通过注入的方式获取配置,并初始化ChatModel,这里可以根据自在的需求进一步配置ChatModel。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.example.demo.ai.llm.config;

import org.noear.solon.ai.chat.ChatConfig;
import org.noear.solon.ai.chat.ChatModel;
import org.noear.solon.annotation.Bean;
import org.noear.solon.annotation.Configuration;
import org.noear.solon.annotation.Inject;

/**
* @author airhead
*/
@Configuration
public class LlmConfig {
@Bean
public ChatModel build(@Inject("${demo.llm}") ChatConfig config) {
return ChatModel.of(config).build();
}
}

Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package com.example.demo.ai.llm.controller;

import com.example.demo.ai.llm.service.LlmService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import java.io.IOException;
import org.noear.solon.annotation.*;
import org.noear.solon.core.util.MimeType;
import reactor.core.publisher.Flux;

/**
* @author airhead
*/
@Controller
@Mapping("/llm")
@Api("聊天")
public class LlmController {
@Inject private LlmService service;

@ApiOperation("chat")
@Post
@Mapping("chat")
public String chat(String prompt) {
return service.chat(prompt);
}

@Produces(MimeType.TEXT_EVENT_STREAM_UTF8_VALUE)
@Mapping("stream")
public Flux<String> stream(String prompt) throws IOException {
return service.stream(prompt);
}

@ApiOperation("functionCall")
@Post
@Mapping("functionCall")
public String functionCall(String prompt) {
return service.functionCall(prompt);
}
}

Service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package com.example.demo.ai.llm.service;

import java.io.IOException;
import org.noear.solon.ai.chat.ChatModel;
import org.noear.solon.ai.chat.ChatResponse;
import org.noear.solon.annotation.Component;
import org.noear.solon.annotation.Inject;
import reactor.core.publisher.Flux;

/**
* @author airhead
*/
@Component
public class LlmService {
@Inject private ChatModel chatModel;

public String chat(String prompt) {
try {
ChatResponse response = chatModel.prompt(prompt).call();

return response.getMessage().getContent();
} catch (IOException e) {
throw new RuntimeException(e);
}
}

public Flux<String> stream(String prompt) throws IOException {
return Flux.from(chatModel.prompt(prompt).stream())
.filter(ChatResponse::hasChoices)
.map(resp -> resp.getMessage().getContent());
}

public String functionCall(String prompt) {
try {
ChatResponse response =
chatModel.prompt(prompt).options(o -> o.functionAdd(new Tools())).call();

return response.getMessage().getContent();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

Tools

Solon 提供了多种设置 Fuction Call 的方式,这里只是从官网拿过来的一个例子,更多内容看这里 https://solon.noear.org/article/921

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.example.demo.ai.llm.service;

import org.noear.solon.ai.chat.annotation.FunctionMapping;
import org.noear.solon.ai.chat.annotation.FunctionParam;

public class Tools {
@FunctionMapping(description = "获取指定城市的天气情况")
public String get_weather(
@FunctionParam(name = "location", description = "根据用户提到的地点推测城市") String location) {
if (location == null) {
throw new IllegalStateException("arguments location is null (Assistant recognition failure)");
}

return "晴,24度"; // 可使用 “数据库” 或 “网络” 接口根据 location 查询合适数据;
}
}

验证

同步调用

img

流式调用

可以在浏览器中直接进行测试,目前的情况是,如果使用了推理模型 think 可能不显示,如果推理时间过长,可能出现超时的情况。作者在最新版本的SNAPSHOT中已处理,但我暂时还拉取不到最新版本。

img

Function Call

我们可能看到这例子里面只简单返回了 “晴,24度” 的天气情况。经过大模型的处理,内容就更完整丰富了。

img

小结

这里我只测试了 Solon AI 的基础功能,可以说是非常容易上手,通过简单的配置就能调用本地的服务了,如果是云服务也是一样的,增加配置 apiKey 就可以了。后续我将继续测试 Solon AI 的 RAG 和 Flow 的功能。

广告

在 AI 来临的时候,越来越多的新奇的东西会占有我们的时间,我们的注意力,不少人甚至认为有了 AI 甚至可以不用学习了,然而现实有了 AI 能学的人越能学,不学的人越不会学,就像现在面对同样的 AI 有的人能做出精美的图片,有的人能写出生动的故事,但一些人面对 AI 还是不知所措。那么更关键的是即使没有 AI,自己也应该是一台学习机器、生产机器。不论学习还是生产都是需要占用时间的,或者更准确的说是注意力,这个注意力就好像是时间的利用率,注意力越高,时间利用率就越高,越能学习,越能生产。

说明

Solon Cloud Gateway 是 一个可 Java 编程的分布式网关,提供了服务路由的能力和各种拦截的支持,只要是 http 服务(不需要关心实现的语言)都可以通过 Solon Cloud Gateway 进行代理转发,代理转发的服务也不一定要注册到服务注册中心。

虽然 Solon Cloud 提供了网关的实现,但在其官网出于性能及资源等原因的考虑,推荐优先使用专业网关(ngix,apisix,kong, k8s ingress controller),其次才是 Java 实现的网关 Spring Cloud Gateway 和 Solon Cloud Gateway。因此,在实际的项目中需要结合自己的项目情况来选择。

以下是微服务框架配合网关的架构图,图片来自 Solon 官网。https://solon.noear.org/article/328

img

依赖

1
2
3
4
5
6
7
8
9
10
dependencies {
implementation platform(project(":demo-parent"))

implementation("org.noear:nacos2-solon-cloud-plugin")
implementation("org.noear:solon.cloud.gateway")

annotationProcessor("org.mapstruct:mapstruct-processor:${mapstructVersion}")

testImplementation("org.noear:solon-test-junit5")
}

实现

响应式

Solon Cloud Gateway 使用响应式接口,由 Solon-Rx 来实现,是基于 reactive-streams 封装的 RxJava 极简版。目前仅一个接口 Completable,意为:可完成的发布者。

接口 说明
Completable 作为返回类型
Completable::complete() 构建完成发布者
Completable::error(cause) 构建异常发布者
Completable::create((emitter)->{…}) 构建发射器发布者

路由检测器

内置路由检测器

匹配检测器工厂 本置前缀 说明与示例
AfterPredicateFactory After= After 时间检测器,ZonedDateTime 格式 (After=2017-01-20T17:42:47.789-07:00[America/Denver])
BeforePredicateFactory Before= After 时间检测器,ZonedDateTime 格式 (Before=2017-01-20T17:42:47.789-07:00[America/Denver])
CookiePredicateFactory Cookie= Cookie 检测器 (Cookie=token)(Cookie=token, ^user.)
HeaderPredicateFactory Header= Header 检测器 (Header=token)(Header=token, ^user.)
MethodPredicateFactory Method= Method 检测器 (Method=GET,POST)
PathPredicateFactory Path= Path 检测器(支持多路径匹配,以”,”号隔开) (Path=/demo/) ,(Path=/demo/,/hello/**)

通常情况下,我们使用 PathPredicateFactory 就够用,不需要额外的配置。

路由过滤器

内置过滤器

Solon 提供了一些内置的路由过滤器,可以通过直接的配置就可以使用。

过滤器工厂 本置前缀 说明与示例
AddRequestHeaderFilterFactory AddRequestHeader= 添加请求头 (AddRequestHeader=Demo-Ver,1.0)
AddResponseHeaderFilterFactory AddResponseHeader= 添加响应头 (AddResponseHeader=Demo-Ver,1.0)
PrefixPathFilterFactory PrefixPath= 附加路径前缀 (PrefixPath=/app)
RedirectToFilterFactory RedirectTo= 跳转到 (RedirectTo=302,http://demo.org/a,true)
RemoveRequestHeaderFilterFactory RemoveRequestHeader= 移除请求头 (RemoveRequestHeader=Demo-Ver,1.0)
RemoveResponseHeaderFilterFactory RemoveResponseHeader= 移除响应头 (RemoveResponseHeader=Demo-Ver,1.0)
StripPrefixFilterFactory StripPrefix= 移除路径前缀段数 (StripPrefix=1)

定制

通过RouteFilterFactory定制路由过滤规则,实现不同类型的接口过滤。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
package com.example.demo.solon.filter;

import io.vertx.core.buffer.Buffer;
import lombok.extern.slf4j.Slf4j;
import org.dromara.hutool.core.text.StrUtil;
import org.noear.solon.annotation.Component;
import org.noear.solon.cloud.gateway.exchange.ExContext;
import org.noear.solon.cloud.gateway.exchange.ExFilter;
import org.noear.solon.cloud.gateway.exchange.ExFilterChain;
import org.noear.solon.cloud.gateway.route.RouteFilterFactory;
import org.noear.solon.rx.Completable;

/**
* @author airhead
*/
@Component
@Slf4j
public class AuthFilterFactory implements RouteFilterFactory {
public static final String TOKEN = "token";
public static final String EMPTY = "empty";

@Override
public String prefix() {
return "Auth";
}

@Override
public ExFilter create(String config) {
if (TOKEN.equals(config)) {
return new TokenFilter();
} else {
return new EmptyAuthFilter();
}
}

/** 默认的空鉴权 */
public static class EmptyAuthFilter implements ExFilter {
public EmptyAuthFilter() {}

@Override
public Completable doFilter(ExContext ctx, ExFilterChain chain) {
log.info("empty filter, do nothing");

return chain.doFilter(ctx);
}
}

/** token 鉴权 ,如果鉴权逻辑比较复杂的,用独立类 */
public static class TokenFilter implements ExFilter {
@Override
public Completable doFilter(ExContext ctx, ExFilterChain chain) {
String token = ctx.rawHeader("token");
if (StrUtil.isBlank(token)) {
ctx.newResponse().status(401);
ctx.newResponse().body(Buffer.buffer("token is empty"));
return Completable.complete();
}

return chain.doFilter(ctx);
}
}
}

其中 EmptyFilter 只是简单的记录日志,而 TokenFilter 会从头中获取 token 信息判断是否为空,为空是返回错误信息,不为空时正常情况。

配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
server.port: 8000

solon.app:
name: 'demo-cloud-gateway'
# group: "DEFAULT_GROUP" # 默认值为 DEFAULT_GROUP
# namespace: "public" # 默认值为 public

knife4j.enable: true

# 运行样例时,屏蔽,或者配置app-local.yml
solon.env: local

solon.cloud.nacos:
server: "localhost:8848" #nacos2 服务地址
config:
load: "demo-cloud-gateway.yml"

solon.cloud.gateway:
discover:
enabled: false
excludedServices:
- "self-service"
httpClient:
responseTimeout: 1800 #单位:秒
routes:
- id: demo
index: 0 #默认为0
target: "http://127.0.0.1:8081"
predicates:
- "Path=/demo/**"
filters:
- "Auth=empty"
- "StripPrefix=1"
timeout:
responseTimeout: 1800 #单位:秒
- id: service01
index: 0 #默认为0
target: lb://demo-cloud-service01
predicates:
- "Path=/service01/**"
filters:
- "Auth=token"
- "StripPrefix=1"
timeout:
responseTimeout: 1800 #单位:秒

这里使用 service01 作为上游服务,配置直接地址和使用服务注册的地址来分别测试。

使用直接地址时,使用了一个空的鉴权逻辑,然后利用 Solon 内置的StripPrefix 来移除url 中的第一段,然后请求实际的服务。

使用注册服务时,使用了一个简单的Token鉴权逻辑,然后利用 Solon 内置的StripPrefix 来移除url 中的第一段,然后请求实际的服务。

验证

启动 demo-cloud-gateway 和 demo-cloud-service01。

通过service01来调用,也就是通过注册服务来调用。

img

通过demo来调用,也就是通过直接地址来调用。

img

如果 servic01 没有启动的情况返回的是404的错误。

小结

Solon Cloud Gateway 通过配置的方式就可以对服务进行代理转发,在实际的业务使用中通常会与服务发现一起,实现负载均衡,也可以通过编程的方式进一步的定制自己的业务逻辑,实现统一鉴权,限流熔断,灰度发布等功能。

说明

服务注册与发现是一种动态管理服务实例的机制,其目的是让各个服务能够及时了解其他服务的状态与位置,以便能进行相互的通讯与协作。

依赖

添加 nacos2 的依赖 org.noear:nacos2-solon-cloud-plugin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
dependencies {
implementation platform(project(":demo-parent"))

implementation("org.noear:solon-web")
implementation("org.noear:solon-logging-logback")
implementation("org.noear:solon-openapi2-knife4j")
implementation("org.noear:nacos2-solon-cloud-plugin")
// 使用 httputils
implementation("org.noear:solon-net-httputils")

// 使用 NamiClient
implementation("org.noear:nami-coder-snack3")
implementation("org.noear:nami-channel-http")

annotationProcessor("org.mapstruct:mapstruct-processor:${mapstructVersion}")

testImplementation("org.noear:solon-test-junit5")
}

配置

Nacos2 插件已经做了些默认值,所以我不需要过多的配置即可连接到 nacos 发现服务,如果使用了server 节点,意思是配置服务和发现服务使用同一个配置,或者单独使用solon.cloud.nacos.discovery.server配置。

1
2
3
4
5
6
7
8
9
10
11
solon.app:
name: "demo-cloud-config"
# group: "DEFAULT_GROUP" # 默认值为 DEFAULT_GROUP
# namespace: "public" # 默认值为 public

solon.cloud.nacos:
server: "localhost:8848"
config:
load: "demo-cloud-config.yml"
# discovery:
# server: "localhost:8848"

服务调用

测试服务

测试服务中我们提供一个hello接口,并增加表示是从 Service01 的标识。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
 package com.example.demo.solon.controller;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.noear.solon.annotation.Controller;
import org.noear.solon.annotation.Mapping;
import org.noear.solon.annotation.Param;

/**
* @author airhead
*/
@Api("Demo")
@Controller
@Mapping
public class DemoController {
@ApiOperation("hello")
@Mapping("/hello")
public String hello(@Param(defaultValue = "world") String name) {
return String.format("Hello %s!, By Service01", name);
}
}

DemoController

Solon 的 NamiClient 或者 HttpUtils 可以通过服务名来调用服务,其中NamiClient可以通过类似FeignClient的注解的方式进行使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package com.example.demo.solon.controller;

import com.example.demo.solon.client.DemoClient;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import java.io.IOException;
import org.noear.nami.annotation.NamiClient;
import org.noear.solon.annotation.Controller;
import org.noear.solon.annotation.Mapping;
import org.noear.solon.annotation.Param;
import org.noear.solon.net.http.HttpUtils;

/**
* @author airhead
*/
@Api("Demo")
@Controller
@Mapping
public class DemoController {
@NamiClient private DemoClient demoClient;

@ApiOperation("hello")
@Mapping("/hello")
public String hello(@Param(defaultValue = "world") String name) {
try {
return HttpUtils.http("demo-cloud-service01", "/hello").data("name", name).get();
} catch (IOException e) {
throw new RuntimeException(e);
}
}

@ApiOperation("hello2")
@Mapping("/hello2")
public String hello2(@Param(defaultValue = "world") String name) {
return demoClient.hello(name);
}
}

DemoClient

1
2
3
4
5
6
7
8
9
10
11
package com.example.demo.solon.client;

import org.noear.nami.annotation.NamiClient;

/**
* @author airhead
*/
@NamiClient(name = "demo-cloud-service01")
public interface DemoClient {
String hello(String name);
}

验证

运行 demo-cloud-discovery 和 demo-cloud-service01,我们可以通过 nacos 看到服务到已经注册上来了。

img

接下来通过 hello 和 hello2 接口测试 两种不同方式的调用。

通过 httputils 调用服务

img

通过 NamiClient

img

小结

通过简单的配置,我们就可以把服务注册的 Nacos 中,并可以通过服务名的方式调用其他服务。当我们在普通的服务中增加服务配置和服务注册与发现功能配置后,就可以算作系统中的一个分布式微服务了,可以关注于业务的开发了。而 Solon Cloud 的接下来的更多的面向系统的可用性,可维护性等。

说明

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service的首字母简称,一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。

接下来的服务配置和服务注册,我们使用的是 Nacos2。 需要自己部署好 Nacos2 ,具体内容可以参看 Nacos 的官网 https://nacos.io/docs/latest/quickstart/quick-start。

依赖

添加 nacos2 的依赖 org.noear:nacos2-solon-cloud-plugin

1
2
3
4
5
6
7
8
9
10
11
12
dependencies {
implementation platform(project(":demo-parent"))

implementation("org.noear:solon-web")
implementation("org.noear:solon-logging-logback")
implementation("org.noear:solon-openapi2-knife4j")
implementation("org.noear:nacos2-solon-cloud-plugin")

annotationProcessor("org.mapstruct:mapstruct-processor:${mapstructVersion}")

testImplementation("org.noear:solon-test-junit5")
}

配置

Nacos2 插件已经做了些默认值,所以我不需要过多的配置即可连接到 nacos 配置中心。

1
2
3
4
5
6
7
8
9
solon.app:
name: "demo-cloud-config"
# group: "DEFAULT_GROUP" # 默认值为 DEFAULT_GROUP
# namespace: "public" # 默认值为 public

solon.cloud.nacos:
server: "localhost:8848"
config:
load: "demo-cloud-config.yml"

load 使用group:dataId的格式加载配置配置文件,当加载的配置文件与 solon.app.group 配置的 group 一致时可以不用配置 group 的部分,也就是只需要 dataId 的部分即可。如果需要加载多个配置文件,使用逗号(,)分隔,加载多个配置文件通用配置特别有用,避免了在多个地方管理配置文件。

通过 load 加载的配置文件会进入 Solon.cfg() 成为应用配置,这样就可以通过 @Injtect 的方式进行注入。

以下是一份更详细的配置,内容来自官网 https://solon.noear.org/article/400 ,可以根据实际的需要进行配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
solon.app:
name: "demoapp"
group: "demo"
meta: #添加应用元信息(可选)
version: "v1.0.2"
author: "noear"
tags: "aaa,bbb,ccc" #添加应用标签(可选)

solon.cloud.nacos:
server: "localhost:8848,localhost:8847" #nacos 服务地址
namespace: "3887EBC8-CD24-4BF7-BACF-58643397C138" #nacos 命名空间
contextPath: "nacosx" #nacos 服务的上下文路径(可选)
username: "aaa"
password: "bbb"
config:
load: "demoapp.yml,group:test.yml" #加载配置到应用属性(多个以","隔开)
discovery:
clusterName: "DEFAULT"

获取配置

使用 nacos 的配置是期望获取统一的,动态的配置,所以这里开启自动刷新 autoRefreshed = true

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.example.demo.solon.config;

import java.io.Serializable;
import lombok.Data;
import org.noear.solon.annotation.Configuration;
import org.noear.solon.annotation.Inject;

/**
* @author airhead
*/
@Configuration
@Inject(value = "${app}", autoRefreshed = true)
@Data
public class AppConfig implements Serializable {
private String appId;
private String appKey;
}

接下来就是通过 @Injtect 的方式进行注入使用对应的配置。

1
2
3
4
5
6
7
@Api("配置管理")
@Controller
@Mapping("/config")
public class DemoController {
@Inject private AppConfig appConfig;

}

获取配置文件

获取配置文件根据 dataId 获取配置,通过 @CloudConfig 进行注入。CloudConfig 暂时不支持注解的类上,也就是不能使用如下的方法配合 @Inject 进行注入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.example.demo.solon.config;

import java.io.Serializable;
import lombok.Data;
import org.noear.solon.annotation.Configuration;
import org.noear.solon.cloud.annotation.CloudConfig;

/**
* 通过类获取dataId的配置,暂不支持的。
*
* @author airhead
*/
@Configuration
@CloudConfig(value = "demo-db", autoRefreshed = true)
@Data
public class DemoDbConfig implements Serializable {
private String url;
private String username;
private String password;
}

@Inject 暂时暂时不到值的。

1
2
3
4
5
6
7
@Api("配置管理")
@Controller
@Mapping("/config")
public class DemoController {
/** 获取dataId的配置,获取不到值 */
@Inject private DemoDbConfig demoDbConfig;
}

正确的方式是,把 CloudConfig 注解在属性上。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class DemoController {
/** 获取dataId的配置,获取不到值 */
@Inject private DemoDbConfig demoDbConfig;

@Inject private AppConfig appConfig;

@CloudConfig(value = "demo-db", autoRefreshed = true)
private Properties properties;

@CloudConfig(value = "demo-db", autoRefreshed = true)
private DemoDbConfig demoDbConfig2;

}

验证

通过 Swagger 测试可以正常获取到配置,并且修改 Nacos 的配置后,再次调用接口配置会更新(这里没有截图)。

img

小结

在 Solon Cloud 中通过简单的配置就可以使用 Nacos 配置中心了。在实际的使用过程中注意区分@Inject 和@CloudConfig 的不同,一个是针对key来的,一个是针对dataId来的。为了减少自己在开发过程中的混乱(有时过多的选择不是一件好事情),可以约定使用 load 的方式加载多个配置文件,使用 @Inject 的方式进行注入后使用。

说明

前面的章节,我们讲解了 Solon 的开发应用,接下来准备讲解 Solon Cloud 的的开发。Solon Cloud 是为微服务和云原生准备的分布式开发套件。

微服务

就像 MVC 一样,对于微服务的理解也是有不同的。微服务是一组协调工作的小而自治的服务。微服务是一组分布式的架构框架。

微服务有支持异构,弹性,易扩展,容易替换等优点,但也增加了开发、测试、部署、运维的复杂性。

云原生

为了解决微服务(或者系统)的开发、部署、运维的复杂性,CNCF (Cloud Native Computing Foudation)提出了云原生的概念,就是利用各组织在共有云、私有云和混合云等新型动态环境中,构建和运行可弹性扩展的应用。主要涉及DevOps,CI/CD,Micro Service,Contianer等四个大的方面。

Solon Cloud

img

从上图,我们可以看到 Solon Cloud 的主要组成部分:

  • Solon Cloud Gateway 分布式网关相关插件
  • Solon Cloud Config 分布式配置相关插件
  • Solon Cloud Discovery 分布式注册与发现相关插件
  • Solon Cloud Event 分布式时间总线相关插件
  • Solon Cloud Job 分布式任务调度插件
  • Solon Cloud File 分布式文件插件
  • Solon Cloud Log 分布式日志插件
  • Solon Cloud Trace 分布式跟踪插件
  • Solon Cloud Metrics 分布式监控插件
  • Solon Cloud Breaker 分布式熔断插件
  • Solon Cloud Id 分布式 ID 插件
  • Solon Cloud I18n 分布式国际化配置插件
  • Solon Cloud List 分布式名单,白名单、黑名单等
  • Solon Cloud Lock 分布式锁插件

我们可以看到Solon Cloud 其实是定义了一组接口规范,在这个接口规范的基础上实现不同分布式组件的插件。

在 Solon 的官网中也对分布式设计做了引导,我这里列出重点的部分,详细内容可查看官网 https://solon.noear.org/article/638。

  1. 构建可水平扩展的计算能力
    1. 服务无状态
    2. 服务透明化
    3. 容器弹性伸缩
  2. 构建可水平扩展的业务能力
    1. 基于业务领域拆分微服务
    2. 拆分业务的主线与辅线
    3. 基于实现总线交互

书籍推荐

《微服务架构设计模式》

img

《微服务设计》

img

The Twelve-factor App

https://12factor.net/

技术栈

在本教程的介绍中说到,本教程更面向使用,因此会限制技术栈,对于Solon Cloud 的使用也是一样的,基础的服务:

  • Nacos2 配置管理、服务发现。
  • Redis,分布式 ID,分布式锁等。
  • Skywalking,链路跟踪。
  • Kafka 消息服务。
  • Sentinel 熔断

说明

Solon 是典型的微内核架构,通过框架核心提供了插件的核心接口和生命周期的管理。在之前的应用的生命周期中讲到过插件的三个生命时机点。通过这三个时机点,插件开发人员可以管理插件的资源初始化与资源的销毁。

Solon 的插件支持三种形式的运行方式:

  1. Spi 方式。是基于插件和配置声明的扩展方式,类似Spring Factories,主应用需要增加对插件的依赖。
  2. E-Spi 方式,也叫体外扩展。是基于插件和动态配置的扩展方式,类似Springboot-plugin-fraework,主程序不需要增加对插件的依赖。
  3. H-Spi 方式,也叫热插拔扩展。是基于插件和手动管理的扩展方式,类似Springboot-plugin-fraework,主程序不需要增加对插件的依赖。

这三种扩展方式对于开发插件本身没有什么区别,主要的区别是主应用的依赖和配置等,和实现的效果的不同,具体的特性对比可以参看官网 https://solon.noear.org/article/441 相关的文章。

插件涉及的示例代码包含多个模块,具体参看 https://gitee.com/CrazyAirhead/porpoise-demo:

  • demo-solon03-plugin Solon 插件
  • demo-solon03-main01 Solon 主应用 Spi
  • demo-solon03-main02 Solon 主应用 E-Spi
  • demo-solon03-main03 Solon 主应用 H-Spi

插件 demo-solon03-plugin

实现 hello 的基础功能。

实现 Plugin 接口

这里为了简单,直接扫描了插件包的 Bean 对象,在实际的开发过程中可根据实际的实现使用Conetext的其他方法构建 Bean,或者订阅 Bean,或者加载配置文件等,来完成实际的插件初始化操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.example.demo.solon.plugin;

import org.noear.solon.core.AppContext;
import org.noear.solon.core.Plugin;

/**
* @author airhead
*/
public class DemoSolonPlugin implements Plugin {
@Override
public void start(AppContext context) throws Throwable {
// 简单起见,直接扫描插件所在包,可以根据实际的需要进行定制化的加载
context.beanScan(DemoSolonPlugin.class);
}

@Override
public void prestop() throws Throwable {
Plugin.super.prestop();
}

@Override
public void stop() throws Throwable {
// 移除http处理。在热插拔的插件中特别
Solon.app().router().remove("/hello");
}
}

实现 Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.example.demo.solon.plugin.controller;

import org.noear.solon.annotation.Controller;
import org.noear.solon.annotation.Mapping;
import org.noear.solon.annotation.Param;

/**
* @author airhead
*/
@Controller
public class DemoController {
@Mapping("/hello")
public String hello(@Param(defaultValue = "world") String name) {
return String.format("Hello %s!", name);
}
}

配置插件文件

resources/META-INF/solon/ 目录下创建插件配置文件,文件名要在应用范围内全局唯一,为了避免冲突可以使用插件的包名做了文件名。

这里使用是META-INF/solon/com-example-demo-solon-plugin.properties,文件内容如下:

1
2
3
4
# 插件实现类
solon.plugin=com.jishunetwork.ficus.framework.server.XPluginImp
# 插件加载优先级,越大越优先,默认为0
solon.plugin.priority=1

这里常见的问题:文件目录错误,注解检查是二级目录,目录名是/META-INF/solon/;插件的实现类写错,可以通过点击的方式看下能否正常跳转到对应的类上来验证配置是否正确。

Spi demo-solon03-main01

只要添加对应的依赖即可不需要其他额外的配置。

添加依赖

1
2
3
4
5
6
7
8
9
10
11
12
dependencies {
implementation platform(project(":demo-parent"))

implementation("org.noear:solon-web")
implementation("org.noear:solon-logging-logback")
implementation("org.noear:solon-openapi2-knife4j")
implementation(project(":demo-solon03-plugin"))

annotationProcessor("org.mapstruct:mapstruct-processor:${mapstructVersion}")

testImplementation("org.noear:solon-test-junit5")
}

验证

调用插件实现的接口,http://127.0.0.1:8080/hello,返回hello world!

E-Spi demo-solon03-main02

只要添加修改配置,把插件放入指定文件夹路径即可。

配置

此时把插件目录放在jar相对路径的plugin下。

1
solon.extend: "!plugin"

注意:在idea中可能一下不能区分plugin再那个目录,可以先运行一次让 Solon 把目录创建出来,然后再把插件放到 plugin 目录下,此时我们可以看到是在 build/classes/java/main 目录下。把demo-solon03-plugin的插件放入plugin 目录后重新启动程序(注意此时不能再clean了,否则 刚放入的文件又被清理了)。

img

验证

调用插件实现的接口,http://127.0.0.1:8080/hello,返回hello world!

H-Spi demo-solon03-main03

热插拔的就是允许应用可以更灵活的管理插件的生命周期,因为热插拔是完全隔离的,因此需要自己更严格的管理资源的,更独立,避免与别的组件交互,避免不可拔的情况。

依赖

1
2
3
4
5
6
7
8
9
10
11
12
dependencies {
implementation platform(project(":demo-parent"))

implementation("org.noear:solon-web")
implementation("org.noear:solon-logging-logback")
implementation("org.noear:solon-openapi2-knife4j")
implementation("org.noear:solon-hotplug")

annotationProcessor("org.mapstruct:mapstruct-processor:${mapstructVersion}")

testImplementation("org.noear:solon-test-junit5")
}

配置

这里使用 Solon 默认提供的配置来管理,在加载的插件的过程中,只要能找到插件就可以了,不一定需要使用这个配置,注意路径是绝对路径。

1
2
solon.hotplug:
demo: /plugin/demo-solon03-plugin-1.0.0.jar

实现管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.example.demo.solon.controller;

import lombok.extern.slf4j.Slf4j;
import org.noear.solon.annotation.Controller;
import org.noear.solon.annotation.Mapping;
import org.noear.solon.hotplug.PluginManager;

/**
* @author airhead
*/
@Controller
@Slf4j
public class PluginController {
@Mapping("start")
public String start() {
PluginManager.start("demo");
return "ok";
}

@Mapping("stop")
public String stop() {
PluginManager.stop("demo");
return "ok";
}
}

验证

  1. 调用插件实现的接口,http://127.0.0.1:8080/hello,此时不可访问
  2. 调用启动插件接口,http://127.0.0.1:8080/start,返回ok
  3. 调用插件实现的接口,http://127.0.0.1:8080/hello,此时返回 hello world
  4. 调用停止插件接口,http://127.0.0.1:8080/stop,返回ok
  5. 调用插件实现的接口,http://127.0.0.1:8080/hello,此时不可访问

小结

通过 Solon 提供的插件机制能简单、弹性、自由的实现功能扩展,既可以做框架型的插件开发,也可以做业务性的插件开发。

说明

Solon 的核心概念有 IoC、AOP 和本地事件总线。有人常常有误解以为 IoC 和 AOP 是 Spring 提出的,其实这两种思想在Spring 之前就已经有了,但 Spring 把这两个思想在技术上落地和推广做得很好,让 Ioc 和 AOP 广为人知。

核心概念

IoC

Ioc 的全称是 Inversion of Control,是控制反转或者反转控制的意思。它是一种思想,主要解决的是对象创建和管理的问题,用于解耦依赖。Ioc有时也被称为 DI (Dependency Injection),依赖注入。在使用 IoC 的过程中,我们是通过容器来创建和管理对象的,我们也是从容器中获取对象,所以,有时我们会听到 IoC 就是容器的说法,可能只是简化的一种说法。

AOP

AOP, 全称 Aspect Oriented Programming,是面向切面编程。AOP 的目的是将横切关注点(如日志记录、事务管理、权限控制、接口限流、接口幂等等)从核心业务逻辑中分离出来,通过动态代理、字节码操作等技术,实现代码的复用和解耦,提高代码的可维护性和可扩展性。其中涉及的核心有代理,切点,切面。

本地事件总线

本地事件总线是 Solon 在应用生命周期提供的扩展机制,应用可以根据需要的事件时机点,订阅响应的事件。本地事件总线支持自定义的事件。

注意:

  1. 本地事件总线是基于应用生命周期的(应用的启停,插件的启停,bean 创建等),而不是基于业务,如果是涉及业务,可以使用使用作者的另一个作品 DamiBus。
  2. 要在事件发生前订阅事件,否则会错过时机,无法接收到事件消息。

应用生命周期

应用生命周期,是应用程序从 启动到最后停止的整个过程。应用生命周期当中存在一些关键点,可称为时机点。

SolonApp 的应用生命周期如下图所示,其中时机点包括有:一个初始化函数时机点 + 六个应用事件时机点 + 三个插件生命时机点 + 两个容器生命时机点。

作者的这张图画的很细致,一步步的把这个图走完,能加深对 Solon 的理解。

img

一个初始化函数时机点

应用开发时可扩展的时机。

1
2
3
4
5
6
7
8
@SolonMain
public class App {
public static void main(String[] args) {
Solon.start(App.class, args, (app) -> {
//应用初始化时机点
});
}
}

六个应用事件时机点

应用开发时可扩展的时机。

事件 说明 备注
6.AppInitEndEvent 应用初始化完成事件 只支持手动订阅
8.AppPluginLoadEndEvent 应用插件加载完成事件 只支持手动订阅
b.AppBeanLoadEndEvent 应用Bean加载完成事件(即扫描完成)
e.AppLoadEndEvent 应用加载完成事件(即启动完成)
::运行
g.AppPrestopEndEvent 应用预停止事件
j.AppStopEndEvent 应用停止事件

三个插件生命时机点

插件开发时可扩展的时机。

接口 执行时机 说明
7.start 在应用初始化完成后执行 启动
f.prestop 在 ::stop 前执行 预停止
h.stop 在 Solon::stop 时执行 停止(启用安全停止时,prestop 后等几秒再执行 stop)

两个容器生命时机点

Solon 内部的时机点,应用开发不可扩展。

接口 执行时机 说明
d.start 在扫描完成之后执行 启动
i.stop 在 Solon::stop 时执行,在插件(h.stop)后执行 停止

Bean 生命周期

img

被容器托管的 Bean,它的生命周期只限定在容器内部。

::new()

就是调用构造函数,是在 Bean 被扫描时,且符合条件才会执行。

注意,这个时候,Bean 还注册到容器中,还不能使用注入的字段(还未注入)。如果要初始化,推荐使用@Init 函数。

@Inject

开始执行注入。之后就会注册到容器,并通知订阅者。

start() 或 @Init

执行初始化动作,在 AppContext::start() 开始处被执行。如果使用 start() 需要实现 LifecycleBean 接口。此时 Bean 扫描已完成,理论上所有的 Bean 都已进入容器(某些特殊的 Bean 是在 AppContext.start() 时才生产的),并且所有 Bean 的字段都已完成注入。

postStart()

开始之后,AppContext::start() 结束处被执行,一般用于启动一些任务。如果使用 postStart() 需要实现 LifecycleBean 接口。

preStop()

预停止,AppContext::preStop() 时被执行。一般用来做分布式服务注销之类。如果使用 preStop() 需要实现 LifecycleBean 接口。

stop() 或 @Destroy

停止,AppContext::stop() 时被执行。一般用来做安全停止。如果使用 stop() 需要实现 LifecycleBean 接口。

容器应用

在 Solon 中,我们可以自由的选择用注解的方式,还是手动的方式来实现想要的功能。但在这里我们主要讲解注解的使用方式。

扫描

扫描一般是深度遍历指定“包名”下的 .class 文件获取类名,再通过类名从类加载器里获取元信息。

默认情况下,主类所在包名新的类都会扫描。如果需要修改导入的范围可以使用 @Import 注解,增加扫描的包名,或者导入需要的类名。

构建 / 注入

在 Solon 中,我们通过Singleton,Configuration,Bean,Component,Controller,Remoting等方式产生Bean,在Bean 对象中,我们可以 @Inject 注解注入字段。在构建和注入中需要注意的可能是条件构建和依赖注入的部分了。

注解 说明
@Inject * 注入托管对象(by type)
@Inject(“name”) 注入托管对象(by name)
@Inject(“${name}”) 注入配置(可由基础类型或结构体接收)
@Singleton 单例申明(Solon 默认是单例)
@Singleton(false) 非单例
@Configuration 托管配置组件类(与 @Inject, @Bean 共同完成初始化配置、构建托管对象等)
@Bean 配置托管对象(作用在 @Configuration 类的函数上,才有效)
@Component 托管组件(支持自动代理,v2.5.2 开始支持自动代理)
@Controller 控制器组件类(支持函数拦截)
@Remoting 远程控制器类(有类代理;即RPC服务端)

条件构建

条件构建的意思是满足了指定条件才会构建。通过使用 @Condition 来实现,它包含如下的属性:

属性 说明
onClass 有类(只能一个;其实没必要多个)
onClassName 有类名
onProperty 有属性
onMissingBean 没有 Bean
onMissingBeanName 没有 Bean Name
onBean 有 Bean
onBeanName 有 Bean Name

更多细节,查看官网 https://solon.noear.org/article/434

依赖注入

依赖注入的意思是,在构建时需要使用到其他 Bean 对象。依赖注入通常使用@Configuration,配合@Bean 来实现,其中 @Bean 的函数通过参数注入的方式来获取需要的 Bean 对象。另外就是使用手动模式通过异步订阅的方式获取依赖的 Bean 对象。

以下注入的例子来自官网,更多细节,查看官网 https://solon.noear.org/article/587

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
public class DemoConfig {
@Bean(name="db1", typed=true)
public DataSource db1(@Inject("${demo.db1}") DataSource ds){
return ds;
}

@Bean
public void db1Test(@Db("db1") UserMapper mapper) { //这个注入,依赖“db1”的数据源
return mapper.initUsers();
}
}

说明

Spring Boot 流行起来的一个原因是简化了配置,其中约定优于配置减少了开发者的配置负担,无需配置或者少量的配置就可以启动项目;自动化配置根据项目依赖自动配置应用程序,简化了开发流程。Solon 作为后起之秀也是采用了约定优于配置和自动配置的方式来简化配置。

约定

配置分类

启动参数

启动参数,在应用启动后会被静态化,也就是不能再修改。在Java 启动时指定,通过--key=value的形式指定。

系统属性

系统属性,在应用启动后会被静态化,也就是不能再修改。在Java 启动时指定,通过-Dkey=value的形式指定。

环境变量

操作系统的环境变量。”solon” 开头的环境变量,会被框架同步到系统属性(System::getProperties)与应用属性(Solon::cfg)。

应用配置

应用通过配置文件加载进来的配置。

主配置文件

应用配置文件为 resources/app.yml 或者 app.properties,文件名不能修改也不能配置。

配置加载规则

应用配置的加载主要分了六个层级,其加载规则为:

  • 越静态的越前面
  • 越动态的越后面

因为配置是以“键”为单位,且后面加载的会盖掉前面加载的,所以最终效果就是配置以最后加载为准。

加载顺序为:

  1. 主配置文件,先是主配置文件(app.yml),之后是带环境的主配置(app-{env}.yml),这里不特别区分(yml或者properties)。
  2. 内部配置文件,通过 solon.config.load加载的 classpath 目录下的配置文件。
  3. 外部配置文件,通过 solon.config.add 加载的配置文件。
  4. 动态配置,启动应用时的配置,先是启动参数(–key=value),之后是系统属性(-Dkey=value),最后是环境变量(系统环境设置,或者docker -e 等方式指定)
  5. 启动时加载,通过 app.cfg().loadAdd 或者 app.cfg().loadEnv加载。
  6. 云端配置,通过分布式配置获取,如nacos。

配置引用规则

配置文件(或配置块)解析时,Solon.cfg() 已经存在的变量(或者配置块内的变量),可以被引用。属性之间的引用,使用 ${...}

1
2
3
4
test.demo1: "${db1.url}"                          #引用应用属性
test.demo2: "jdbc:mysql:${db1.server}" #引用应用属性并组合
test.demo3: "jdbc:mysql:${db1.server}/${db1.db}" #引用多个应用属性并组合
test.demo4: "${JAVA_HOME}" #引用环境变量test.demo5: "${.demo4}" #引用本级其它变量(v2.9.0 后支持)

常用配置

配置可能变更,所以直接看官网 https://solon.noear.org/article/174

注入配置

配置注入使用 ${...}的表达式,支持如下具体的配置:

  • ${xxx} 注入字段。
  • ${xxx:def} 注入字段,如果没有则提供 def 默认值,此时只支持单值接收,不支持集合或实体。
  • ${classpath:xxx.yml} 注入资源目录下的配置文件 xxx.xml

注入字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Component
public class DemoService{
//注入值(带默认值:demoApi),并开启自动更新(注意:如果不是单例,请不要开启自动刷新)
@Inject(value="${track.name:demoApi}", autoRefreshed=true)
static String trackName; //v3.0 后支持静态字段注入//注入值(没有时,不覆盖字段初始值)

@Inject("${track.url}")
String trackUrl = "http://x.x.x/track";

//注入配置集合
@Inject("${track.db1}")
Properties trackDbCfg;

//注入Bean(根据对应的配置集合自动生成并注入)
@Inject("${track.db1}")
HikariDataSource trackDs;
}

注入类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Inject("${user.config}")  
@Configuration
public class UserProperties{
public String name;
public List<String> tags;
...
}

@BindProps(prefix="user.config") //或者用绑定属性注解。v3.0.7 后支持
@Configuration
public class UserProperties{
public String name;
public List<String> tags;
...
}

@Configuration
public class DemoConfig {
@BindProps(prefix="user.config") //或者用绑定属性注解。v3.0.7 后支持
@Bean
public UserProperties userProperties(){
return new UserProperties();
}
}

注入参数

当注入到参数的时候,不支持自动刷新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configurationpublic class DemoConfig{
//提示:@Bean 只能与 @Configuration 配合@Bean
public DataSource db1(@Inject("${track.db1}") HikariDataSource ds) {
return ds;
}

@Beanpublic DataSourceWrap db1w(@Inject DataSource ds, @Inject("${wrap}") WrapConfig wc) {
return new DataSourceWrap(ds, wc);
}

//也可以带条件处理@Bean@Condition(onProperty="${cache.enable} = true") //有 "cache.enable" 属性值,且等于truepublic CacheService cache(@Inject("${cache.config}") CacheServiceSupplier supper){
return supper.get();
}
}

自动更新

自动刷新只适合于字段注入,以及单例的类。注意:如果不是单例,请不要开启自动刷新

1
2
3
4
5
6
7
8
9
10
@Componentpublic class DemoService{
//注入值(带默认值:demoApi),并开启自动更新
@Inject(value="${track.name:demoApi}", autoRefreshed=true)
String trackName;

//通过函数时时获取最新的(静态或动态,按需设定)
public static String trackName2(){
return Solon.cfg().get("track.name:demoApi");
}
}

手动配置

通过 Solon.cfg() 获取 SolonProps 后进行操作。这里不做过多的描述,如果需要可以参看官网文档。

示例

配置本身相关简单,记住对应的配置加载规则和注入的方法,基本不会有什么太多的问题。这里不对示例做过多说明,更多需要参看,参看具体的代码 demo-solon01 。

小结

本文主要主要介绍了 Solon 的配置配置规则和自动化配置的方法。在理解了 Solon 的配置后,就可以更动态的做部署的配置。

说明

随着大语言模型的流行,因为使用了 SSE 或者 Websocket的技术进行流式的交换,使得 SSE 和 Websocket也火了起来,今天我们来讲解,如何在 Solon 中实现 Websocket 服务端和 SSE 服务端。

Websocket

WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。它使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

我们这里通过一个简易的 im 的服务做为例子。

依赖

如果使用了solon-web,默认使用的是smart-http,已经集成了 websocket,不需要添加其他的依赖。

插件 适配框架 包大小 信号协议支持 端口
solon-boot-smarthttp smart-http (aio) 0.4Mb http, ws 相同端口
solon-boot-jetty + solon-boot-jetty-add-websocket jetty (nio) 1.9Mb http, ws 相同端口
solon-boot-undertow undertow (nio) 4.3Mb http, ws 相同端口
solon-boot-websocket websocket (nio) 0.4Mb ws 独立端口
solon-boot-websocket-netty netty (nio) ws 独立端口

独立的 WebSocket 插件,会使用独立的端口,且默认为:主端口 + 10000,但都可以通过配置进行修改。

启用

在主类开启 Websocket。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.example.demo.web;

import org.noear.solon.Solon;
import org.noear.solon.annotation.SolonMain;

/**
* @author airhead
*/
@SolonMain
public class DemoWeb05App {
public static void main(String[] args) {
Solon.start(
DemoWeb05App.class,
args,
app -> {
app.enableWebSocket(true);
});
}
}

开放端点

这里集成了 SimpleWebSocketListener,已经提供了基本的实现,可以根据实际的需要实现需要的接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.example.demo.web.im.endpoint;

import com.example.demo.web.im.service.ImService;
import java.io.IOException;
import org.noear.solon.annotation.Inject;
import org.noear.solon.net.annotation.ServerEndpoint;
import org.noear.solon.net.websocket.WebSocket;
import org.noear.solon.net.websocket.listener.SimpleWebSocketListener;

/**
* @author airhead
*/
@ServerEndpoint("/im.ws")
public class ImWebsocket extends SimpleWebSocketListener {
@Inject private ImService service;

@Override
public void onOpen(WebSocket socket) {
service.onOpen(socket);
}

@Override
public void onMessage(WebSocket socket, String text) throws IOException {
service.onMessage(socket, text);
}
}

实现逻辑

此处只是做简单的鉴权,和消息的回复,如果要实现完整的 IM 服务,还需要做会话的管理的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.example.demo.web.im.service;

import java.io.IOException;
import org.dromara.hutool.core.text.StrUtil;
import org.noear.solon.annotation.Component;
import org.noear.solon.net.websocket.WebSocket;

/**
* @author airhead
*/
@Component
public class ImService {
public void onOpen(WebSocket socket) {
String token = socket.param("token");
if (StrUtil.isBlank(token)) {
socket.close();
}

// 省略了管理 socket 的管理
}

public void onMessage(WebSocket socket, String text) throws IOException {
socket.send("> " + text + "\r\n" + "消息已阅");
}
}

验证

img

SSE

SSE 全称是 Server-Sent Event,网页自动获取来自服务器的更新,SSE 是单向消息传递。

我们这里通过一个简易的 LLM 的服务做为例子。

依赖

要实现 SSE 需要引入 solon-web-sse。

1
2
3
4
5
6
7
dependencies {
implementation platform(project(":demo-parent"))

implementation("org.noear:solon-web")
implementation("org.noear:solon-web-sse")

}

开放端点

客户端先要调用 open 方法,初始化 SseEmitter 连接,之后就可以通过 SseEmitter 给客户端发送消息了,为了方便测试这里还提供了 send 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package com.example.demo.web.chat.controller;

import com.example.demo.web.chat.service.ChatService;
import org.noear.solon.annotation.Controller;
import org.noear.solon.annotation.Inject;
import org.noear.solon.annotation.Mapping;
import org.noear.solon.web.sse.SseEmitter;

/**
* @author airhead
*/
@Controller
@Mapping("/chat")
public class ChatController {
@Inject private ChatService service;

@Mapping(value = "/open/{id}")
public SseEmitter open(String id) {
return service.open(id);
}

@Mapping("/send/{id}")
public String send(String id) {
return service.send(id);
}

@Mapping("/close/{id}")
public String close(String id) {
return service.close(id);
}
}

实现逻辑

与 Websocket 不同的是,SSE 是单向连接,请求的过程是不带会话连接的,所以需要自己管理好会话。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
package com.example.demo.web.chat.service;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import org.noear.snack.ONode;
import org.noear.solon.Utils;
import org.noear.solon.annotation.Component;
import org.noear.solon.core.handle.Result;
import org.noear.solon.web.sse.SseEmitter;
import org.noear.solon.web.sse.SseEvent;

/**
* @author airhead
*/
@Component
@Slf4j
public class ChatService {
static Map<String, SseEmitter> emitterMap = new HashMap<>();

public SseEmitter open(String id) {
SseEmitter sseEmitter =
new SseEmitter(60 * 1000L)
.onCompletion(() -> emitterMap.remove(id))
.onError(e -> log.error("初始化 sse 错误", e))
.onInited(s -> emitterMap.put(id, s));

try {
sseEmitter.send("Ok");
} catch (IOException e) {
log.error("发送 sse 错误", e);
throw new RuntimeException(e);
}

// 初始化后,才能使用
return sseEmitter;
}

public String send(String id) {
SseEmitter emitter = emitterMap.get(id);
if (emitter == null) {
return "No user: " + id;
}

String msg = "test msg -> " + System.currentTimeMillis();
System.out.println(msg);
try {
emitter.send(msg);
// reconnectTime 用于提示前端重连时间
emitter.send(new SseEvent().id(Utils.guid()).data(msg).reconnectTime(1000L));
emitter.send(ONode.stringify(Result.succeed(msg)));
} catch (IOException e) {
log.error("发送 sse 错误", e);
throw new RuntimeException(e);
}

return "Ok";
}

public String close(String id) {
SseEmitter emitter = emitterMap.get(id);
if (emitter != null) {
emitter.complete();
}

return "Ok";
}
}

验证

ApiFox 支持测试 SSE,连接成功之后就可以调用 send 方法。

img

也可以直接使用 curl 进行测试

img