CrazyAirhead

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

0%

创建项目

使用Solon Initializr(https://solon.noear.org/start/),创建基础项目。也可以使用 IDEA 的 Solon 插件,https://plugins.jetbrains.com/plugin/21380-solon,功能类似,用其中一种方式就可以了。

img

这里选择Gradle-Groovey,Java, 3.0.6,jar,jdk17,Solon Web。

运行

解压下载的demo.zip,用 IDEA 打开目录的 build.gradle 文件,弹窗选择open as project。

生成的代码已经包含了一个服务接口,无须额外编写代码。

等 IDEA 加载依赖完毕,即可直接运行 App.java的main函数。

打开浏览器,输入http://localhost:8080/hello

你的第一个 Solon 的 web 应用就完成了。

img

测试

默认生成的Demo 已经提供测试,可以点击后看到如下效果。

img

img

如果提示不存在任何测试的错误,调整 IDEA 的配置。

1
2
3
No matching tests found in any candidate test task.
Requested tests:
Test pattern features.HelloTest.hello in task :demo:test

img

项目结构

img

代码

默认生成的代码可以从下面的分支获取。

https://gitee.com/CrazyAirhead/porpoise-demo/tree/start-up/

性能

Solon 作者针对的提供了多个性能的测试对比,具体看链接 https://solon.noear.org/article/737 的文章。这里不逻辑所有的性能对比,只例出Spring VS Javalin VS Solon的对比,另外测试只是做个参考。不同的环境、场景,效果不同。

测试记录

项目 SpringBoot2 SpringBoot3 Javalin Solon
运行时 java 17 java 17 java 17 java 17
测试前状态/内存 101.1Mb 112.9Mb 66.1Mb 45.6Mb
测试后状态/内存 996.3Mb 326.9Mb 457.3Mb 369.2Mb
测试后状态/并发 2万 2.6万 12万 17万

测试视频

https://www.bilibili.com/video/BV1nJ4m1h79P/

小结

通过 Solon Initializr 可以直接创建基础可运行的应用。

说明

本部分内容建议直接看官网 https://solon.noear.org,以便能看到最新的内容。这里只是简单的罗列。

Solon 体系

img

Solon Core

Solon 核心库,主要分成,容器、插件、上下文处理。

  • Ioc / Aop 容器,提供基于注入依赖的自动装配体系
  • 插件扩展机制,提供“编码风格”的扩展体系
  • 通用上下文处理接口,提供开放式网络协议对接适配体系(俗称,三元合一)

Solon Boot

服务启动器,处理网络协议,三元合一。

Solon Serialization

序列化体系及相关适配插件。

Solon View

Solon Web 的后端视图体系及相关适配插件。

Solon Data

Solon 数据处理,包括事务、缓存、Orm 框架、NoSQL相关的插件。

Solon Doc

Solon 文档相关的插件。

Solon Log

Solon 日志相关的插件。

Solon Test

Solon 单元测试等开发辅助相关的插件。

Solon Security

Solon 安全,鉴权、校验、加密等安全相关的插件。

Solon I18n

Solon 国际化相关的插件。

Solon Native

Solon Native 是可以让 Solon 应用程序以 GraalVM 原生镜像的方式运行的技术方案。

Solon AI

Solon 使用 AI 的相关插件。

Solon Remoting

Solon 远程服务相关的插件及其应用。

Solon Web

Solon Web 一个虚拟的项目,是相关依赖项目的组合。

Solon Scheduling

Solon 任务调度相关的插件。

Solon FaaS

Solon 即时运行函数(一个文件,即为一个函数)的技术方案。

Solon Flow

Solon 流程引擎、规则引擎 等相关的插件。

Solon Cloud

Solon 分布式与微服务开发相关的插件,主要包括:

  • 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 分布式锁插件

技术趋势

我们关注 Spring Boot3 的话,当前已经只支持 JDK17 以上版本,对 WebFlux,异步编程的支持越来越好。而且因为云原生的发展,Native 项目。随着大语言模型的发展,对构建 AI 应用的需求也会增加。在这些方面 Solon 都是有完整支持的,跟进 Solon 也能保持自己在技术方面的更新迭代。

说明

一次在 Jfinal 的群聊中,无意中发现 Solon 的,说「 Solon只需要几M」。于是搜索到官网,看了官网内容。抱着试一试的态度,于是按着官方文档写例子,发现挺好用的,生态也很完整,自己想要的东西基本都有。

目前自己在尝试构建 Solon 版的快速开发框架,算是学习环境。在公司使用 Solon 和 Solon Cloud 开发安全运营平台,算是实践环境。

在学习实践 Solon 的过程中,也是会碰到问题的,我就会把问题和处理过程记录下来。本着「教是最好的学」的原则,于是整理本教程《Solon 实用教程》,以便自己能更好的理解和使用 Solon。

由于我还在学习和实践 Solon 的过程中,因此本教程会持续更新。同时,由于我的学习可能不够深入或使用不当,教程中难免会出现错误,欢迎大家指正。

Solon 是什么

以下是Solon在开放原子开源基金会的介绍https://www.openatom.cn/project/YNjOOt50Swn2)。

项目简介

Solon 一个是新的Java “生态型”应用开发框架。相对目前主流解决方案。Solon并发高 2~ 3 倍;内存省 50%;启动快 5 ~ 10 倍;打包小 50% ~ 90%;同时兼容 java8 ~ java22 运行时。

项目特点

技术价值

Solon 提供了更高的计算性价比;更好的软件开发效率;快的生产与部署体验;更灵活的兼容选择。

业务价值

Solon解决了Spring 的历史包袱重,架构臃肿,计算资源浪费,学习曲线复杂,以及不再支持java17之前版本等问题。

生态价值

Solon 给Java 生态整合提供了更先进的底座支持。灵活的架构,即可适配传统的java-ee体系,又可以支持最新的技术发展成果。

img

为什么要学 Solon

  • 更快、更小、更简单。Solon并发高 2~ 3 倍;启动快 5 ~ 10 倍;打包小 50% ~ 90%;内存省 50%;最小 Web 完整开发单位 1Mb。除了注解模式之外,还可以按需手动,生态丰富,选择自由。同时兼容 java8 ~ java22 运行时。
  • 国产框架,生态完整。随着国产信创的推荐,使用国产框架将更有优势。
  • 交流方便,容易理解。有官方的沟通交流群和使用 Gitee,沟通交流方面,文档使用中文编写,容易理解。

教程内容

在学习和实践 Solon 的过程中,会发现自己一开始更关注怎么用?接着才会关注为什么要这么用?之后才会想这样设计的好处?如何用 Solon 进行 Web 开发是本教程的主线,本教程不会讲解 Solon 的源码。本教程的主要来源为 Solon 官网Solon 源码和自己的实践(https://gitee.com/CrazyAirhead/projects?sort=&scope=&state=public&search=solonhttps://gitee.com/CrazyAirhead/porpoise-demo)。

本教程主要以下几个部分:

  • 初识 Solon
  • 数据操作
  • Web 开发
  • Solon Boot
  • Solon Cloud

本教程使用 IDEA、JDK17 和 Gradle,需要有这些基础知识。本教程于官网资料的主要不同在于,官网面向的读者更宽泛,会涉及各种不同版本 JDK,Maven 和 Gradle 的配置,不同的编程习惯,比如注解或手动获取 Bean 等,不同的技术偏好,比如多种 ORM 组件等。本教程则更面向实践,并限定了选择,这样可以比较快速简单的入门,实际使用 Solon 进行开发工作。

什么是 syslog

syslog 是一种被广泛使用的用于日志记录的标准。许多设备(如打印机、路由器)以及跨平台的系统都使用 syslog 标准。它可以将生成日志的软件、存储日志的系统以及分析日志的应用分离开来。这使得来自不同类型系统的日志数据可以集中存储和分析。syslog 可以用于系统管理和安全审计,以及记录一般信息、分析和调试消息等。

syslog 由 Eric Allman 在 1980 年代开发,最初是作为 Sendmail 项目 的一部分。它很快被其他应用程序采用,并逐渐成为类 Unix 系统上的标准日志记录解决方案。在其他操作系统上也有多种实现,并且它常见于网络设备中,例如路由器。syslog 最初作为一种事实上的标准运行,没有发布任何权威的规范,因此存在许多实现,其中一些实现并不兼容。互联网工程任务组(IETF) 在 2001 年 8 月的 RFC 3164 中记录了当时的现状。随后,Syslog 在 2009 年 3 月通过 RFC 5424 被标准化。

syslog 采用客户端-服务器架构,其中 syslog 服务器监听并记录来自客户端的消息。Syslog消息可以通过多种协议传输,包括UDP、TCP和TLS。

  • syslog 发送方(Sender):生成日志消息的设备或应用程序。
  • syslog 接收方(Receiver):接收并存储日志消息的服务器。
  • syslog 协议:定义日志消息的格式和传输方式。

syslog 消息通常包含以下几个部分(不同的 RFC 标准略有不同):

  • 优先级(Priority):表示消息的严重程度和设施类型。
  • 时间戳(Timestamp):记录消息生成的时间。
  • 主机名(Hostname):生成消息的设备或应用程序的主机名。
  • 标签(Tag):标识生成消息的进程或应用程序。
  • 消息内容(Content):实际的日志信息。

syslog 的 RFC 文档:

实现 syslog 服务端

严格来说,本文并没有实现 syslog 服务端,只是 syslog 服务端的一个类库(gsyslog),该类库实现重点参考了 https://github.com/cnaude/go-syslog ,并使用了 gnet 网络库(https://github.com/panjf2000/gnet)重写。该类库主要实现了网络监听(支持 UDP 和 TCP 协议)和消息解析,并对外提供消息处理接口,实现自己的消息处理接口即可实现完整的 syslog 服务端。公司的 syslog 服务基于该类库已经完整实现消息处理接口、性能监测,并支持编码格式自动转化(虽然 syslog 是 UTF8 格式,但不少客户端可能发松的是 GBK 格式 )。

以下的代码只展示重点部分,具体源码,请自行下载。

网络监听

gnet 需要实现 OnBootOnTraffic,通过 OnTraffic来实现不同协议的处理。

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
type Server struct {
gnet.BuiltinEventEngine
eng gnet.Engine
addr string
network string

bufferSize int
workerPool *goroutine.Pool

codec codec.Codec
handler Handler
}

// NewServer returns a new Server
func NewServer() *Server {
return &Server{
handler: NewDefaultHandler(),
codec: AutomaticCodec,
workerPool: goroutine.Default(),
}
}

// SetHandler Sets the handler, this handler with receive every syslog entry
func (s *Server) SetHandler(handler Handler) {
s.handler = handler
}

func (s *Server) Boot() error {
err := gnet.Run(s, s.addr,
gnet.WithMulticore(true),
gnet.WithSocketRecvBuffer(s.bufferSize))
if err != nil {
return err
}

return nil
}

func (s *Server) Stop() error {
_ = s.eng.Stop(context.Background())
s.workerPool.Release()

return nil
}

func (s *Server) OnBoot(eng gnet.Engine) gnet.Action {
s.eng = eng

logging.Infof("syslog server is listening on %s\n", s.addr)

return gnet.None
}

func (s *Server) OnTraffic(conn gnet.Conn) (action gnet.Action) {
if s.network == "udp" {
return s.handleUdp(conn)
}

if s.network == "tcp" {
return s.handleTcp(conn)
}

if s.network == "unix" {
return s.handleUdp(conn)
}

return gnet.None
}

UDP

UDP 直接读可以缓存,而后通过 GetParser 获取到实际的解析器,之后调用解析器的 Parse进行解析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func (s *Server) handleUdp(conn gnet.Conn) (action gnet.Action) {
data, err := conn.Next(-1)
if err != nil {
logging.Errorf("syslog read buff, something wrong, error:%v", err)
return gnet.None
}

client := conn.RemoteAddr().String()
copyData := make([]byte, len(data))
copy(copyData, data)
_ = s.workerPool.Submit(func() {
p := s.codec.GetParser(copyData)
log, _ := p.Parse(copyData, client)
s.handler.Handle(log)
})

return gnet.None
}

TCP

TCP 通过编码器的 Decode 接口拆包,并获取实际的解析器,之后调用解析器的 Parse进行解析。

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
func (s *Server) handleTcp(conn gnet.Conn) (action gnet.Action) {
for {
data, p, err := s.codec.Decode(conn)
if err != nil {
break
}

client := conn.RemoteAddr().String()
_ = s.workerPool.Submit(func() {
log, _ := p.Parse(data, client)
s.handler.Handle(log)
})

return gnet.None
}

if conn.InboundBuffered() > 0 {
if err := conn.Wake(nil); err != nil { // wake up the connection manually to avoid missing the leftover data
logging.Errorf("failed to wake up the connection, %v", err)
return gnet.Close
}
}

return gnet.None
}

消息解析

编码器

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
70
71
72
type Codec interface {
// Decode 用于 TCP 拆包
Decode(conn gnet.Conn) ([]byte, parser.Parser, error)
// GetParser 获取解析器,用于格式解析
GetParser([]byte) parser.Parser
}

// rfc3164
type RFC3164Codec struct{}

func (f *RFC3164Codec) GetParser(data []byte) parser.Parser {
return rfc3164Parser
}

func (f *RFC3164Codec) Decode(conn gnet.Conn) ([]byte, parser.Parser, error) {
buf, _ := conn.Next(-1)

length := len(buf)
body := make([]byte, length)
copy(body, buf)

_, _ = conn.Discard(length)

return body, rfc3164Parser, nil
}

// rfc5424
type RFC5424Codec struct{}

func (f *RFC5424Codec) GetParser(data []byte) parser.Parser {
return rfc5424Parser
}

func (f *RFC5424Codec) Decode(conn gnet.Conn) ([]byte, parser.Parser, error) {
buf, _ := conn.Next(-1)

length := len(buf)
body := make([]byte, length)
copy(body, buf)

_, _ = conn.Discard(length)

return body, rfc5424Parser, nil
}


// rfc6587
type RFC6587Codec struct{}

func (f *RFC6587Codec) GetParser(data []byte) parser.Parser {
return rfc6587Parser
}

func (f *RFC6587Codec) Decode(conn gnet.Conn) ([]byte, parser.Parser, error) {
if conn.InboundBuffered() < 4 {
// 如果没有找到换行符,说明数据不完整,等待更多数据
return nil, rfc6587Parser, ErrIncompletePacket
}

lenBuf, _ := conn.Peek(4)
length := binary.BigEndian.Uint32(lenBuf)

_, _ = conn.Discard(4)

buf, _ := conn.Next(int(length))
body := make([]byte, length)
copy(body, buf)

_, _ = conn.Discard(int(length))

return nil, rfc6587Parser, nil
}

解析器

主要实现 rfc3164 和 rfc5424 的解析,解析的步骤比较繁琐,这里就不单独列出代码了,具体内容可以参看源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type Parser interface {
Parse(data []byte, client string) (*Log, error)
Location(*time.Location)
}

type Log struct {
// 结构化的数据
Header map[string]interface{} `json:"header"`
// 原始数据
Body []byte `json:"body"`

// 是否有错,有错时结构化数据不可能
Err error

// 辅助解析用,解析的过程中cursor会发生变化
cursor int
// 辅助解析用
len int
// 是否忽略标签
skipTag bool
}

处理接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Handler The handler receive every syslog entry at Handle method
type Handler interface {
Handle(log *parser.Log)
}

type DefaultHandler struct {
counter *int64
}

func NewDefaultHandler() *DefaultHandler {
return &DefaultHandler{
counter: new(int64),
}
}

// Handle entry receiver
func (h *DefaultHandler) Handle(log *parser.Log) {
atomic.AddInt64(h.counter, 1)

logging.Infof("number %d, header:%v, body:%v,", *h.counter, log.Header, log.GetString(parser.LogBody))
}

简易服务端

使用默认的 DefaultHandler 实现简易服务端

1
2
3
4
5
6
7
8
9
10
func Test_udp_server(t *testing.T) {
server := NewServer()
server.SetCodec(rfc3164Codec)
server.SetAddr("udp://0.0.0.0:514")
defer func(server *Server) {
_ = server.Stop()
}(server)

_ = server.Boot()
}

总结

Syslog 是一种强大的日志记录协议,适用于各种系统和应用程序的日志管理。通过实现一个简单的 syslog 服务端,可以更好地理解和掌握 syslog 的工作原理。本文介绍了syslog的基本概念、协议格式,实现 syslog 的服务端类库,并提供了简易服务端的实现。希望本文能为读者在日志管理和系统监控方面提供有价值的参考。

说明

在「使用国产化框架 Solon 的一些开发经验」中提到,我们只是在平台的一个应用开始使用 Solon 框架,并非一次性的完全替换。但随着 solon cloud gateway 官方版本发布,替换Spring Gateway 也成为可能,于是开始相关的替换工作。

我们的网关主要提供了统一授权和鉴权的功能,及最基础的服务路由能力。另外系统中个问卷功能是对外提供服务的,当时为了提供对外服务和减少Gateway的路由判断逻辑,增加一个api-gateway。

此次使用 solon gateway 进行替换需要完整的实现旧有Spring Gateway的功能,同时想利用 Solon 的本地 gateway 的路由分组功能合并api-gateway。

一样的,在开发前,需要完整阅读,Solon Gateway 的官网文档 (https://solon.noear.org/article/804)。一样的,文档说明简单,集成也不难。

Solon 版本:3.0.5

实现过程

引入依赖包

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

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

注意:为了简单起见,已经省去其他依赖;使用ficus-parent进行统一的版本管理,如果没有使用版本管理,添加对应依赖时需要设置版本号。

通用Web授权

管理端授权

提供用户密码授权和基于Token的切换租户。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Api("管理端授权服务")
@Component(tag = "adminApi")
@Mapping("/auth")
public class AdminAuthController {
@Inject private AdminAuthService service;

@Post
@Mapping("login")
@ApiOperation("login")
public CommonResult<UserLoginRes> login(@Body LoginReq loginReq) {
return CommonResult.success(service.login(loginReq));
}

@Post
@Mapping("changeLoginOrg")
@ApiOperation("changeLoginOrg")
public CommonResult<UserLoginRes> changeLoginOrg(@Body ChangeOrgReq changeOrgReq) {
return null;
}
}

应用端授权

提供基于域名的租户识别,用户密码授权及手机授权和用户注册等功能。

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
@Api("应用端授权服务")
@Component(tag = "appApi")
@Mapping("/auth")
public class AppAuthController {
@Inject private AppAuthService authService;

@Post
@Mapping("/getOrgByDomain")
@ApiOperation("getOrgByDomain")
public CommonResult<SimpleOrgRes> getOrgByDomain(@Body GetOrgReq getOrgReq) {
return CommonResult.success(authService.getOrgByDomain(getOrgReq));
}

@Post
@Mapping("/login")
@ApiOperation("login")
public CommonResult<MemberLoginRes> login(Context context, @Body LoginReq loginReq) {
Ret ret = checkLoginReq(loginReq);
if (ret.isFail()) {
ErrorCode error = ret.getAs("error");
return CommonResult.error(error);
}

return authService.login(context, loginReq);
}

@Post
@Mapping("/smsLogin")
@ApiOperation("smsLogin")
public CommonResult<MemberLoginRes> smsLogin(Context context, @Body SmsLoginReq smsLoginReq) {
Ret ret = checkSmsLoginReq(smsLoginReq);
if (ret.isFail()) {
ErrorCode error = ret.getAs("error");
return CommonResult.error(error);
}

return authService.smsLogin(context, smsLoginReq);
}

@Post
@Mapping("/register")
@ApiOperation("register")
public CommonResult<MemberLoginRes> register(Context context, @Body RegisterReq registerReq) {
return authService.register(context, registerReq);
}

@Post
@Mapping("/sendSms")
@ApiOperation("sendSms")
public CommonResult<Boolean> sendSms(Context context, @Body SendSmsReq sendSmsReq) {
return authService.sendSms(context, sendSmsReq);
}
}

路由分组

  • 管理端路由分组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @author airhead
*/
@Mapping("/**")
@Component
public class AdminGateway extends Gateway {
@Override
protected void register() {
// admin的异常抛出,可能包含SQL,起码更详细
filter(-1, new AdminExceptionFilter());
filter(0, new TenantFilter());

addBeans(bw -> "adminApi".equals(bw.tag()));
}
}
  • 应用端路由分组
1
2
3
4
5
6
7
8
9
10
11
12
@Mapping("/app-api/**")
@Component
public class AppGateway extends Gateway {
@Override
protected void register() {
// app的异常抛出不应该暴露SQL等
filter(-1, new AppExceptionFilter());
filter(0, new TenantFilter());

addBeans(bw -> "appApi".equals(bw.tag()));
}
}

响应式路由

实现

通过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
63
64
65
66
@Component
@Slf4j
public class AuthFilterFactory implements RouteFilterFactory {
public static final String ADMIN = "admin";
public static final String APP = "app";
public static final String OPEN = "open";

@Inject private JwtConfig jwtConfig;
@Inject private AdminAuthConfig adminAuthConfig;
@Inject private AdminAuthService adminAuthService;

@Inject private AppAuthConfig appAuthConfig;
@Inject private AppAuthService appAuthService;

@Inject private OpenAuthConfig openAuthConfig;
@Inject private OpenAuthService openAuthService;

public static Completable error(ExContext ctx, ErrorCode errorCode) {
String resultStr = ONode.stringify(CommonResult.error(errorCode));
ctx.newResponse().header("Content-Type", "application/json;charset=UTF-8");
ctx.newResponse().body(Buffer.buffer(resultStr));
if (errorCode.getCode() < HTTP_STATUS_MAX) {
// http status的正常范围的错误
ctx.newResponse().status(errorCode.getCode());
} else {
ctx.newResponse().status(HTTP_INTERNAL_SERVER_ERROR);
}

return Completable.complete();
}

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

@Override
public ExFilter create(String config) {
if (ADMIN.equals(config)) {
return new AdminAuthFilter(jwtConfig, adminAuthConfig, adminAuthService);
} else if (APP.equals(config)) {
return new AppAuthFilter(jwtConfig, appAuthConfig, appAuthService);
} else if (OPEN.equals(config)) {
return new OpenAuthFilter(jwtConfig, openAuthConfig, openAuthService);
} else {
return new EmptyAuthFilter();
}
}

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

@Override
public Completable doFilter(ExContext ctx, ExFilterChain chain) {
log.info("empty filter");
return error(
ctx,
ErrorCode.builder()
.code(HTTP_NOT_FOUND)
.error(NOT_FOUND_ERROR)
.msg(NOT_FOUND_ERROR)
.build());
}
}
}

注意:这里AdminAuthFilter、AppAuthFilter和OpenAuthFilter未提供实现,可参考EmptyAuthFilter实现具体业务逻辑,或者参考官网例子。

配置

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
solon.cloud:
nacos:
server: ${NACOS_SERVER:127.0.0.1:8848}
gateway:
httpClient:
responseTimeout: 180 #单位:秒
routes:
# app-api 用index=-1
- id: topcloud-oa-api
target: lb://topcloud-oa
index: -1
predicates:
- Path=/OA/app-api/**
filters:
- Auth=app

# open-api 用index=-1
- id: topcloud-uac-open
target: lb://topcloud-uac
index: -1
predicates:
- Path=/UAC/open-api/**
filters:
- Auth=open

# admin-api 用index=0
- id: topcloud-uac-admin
target: lb://topcloud-uac
index: 0
predicates:
- Path=/UAC/**
filters:
- Auth=admin

注意:3.0.5 版本如果路由存在包含关系,必须指定index,越具体的地址优先级要越高优先级(也就是index越小)。

可能碰到的问题

端口冲突

如果使用较低版本的 solon 时,提示端口冲突时,注意排除 solon-boot-xxx。

1
2
3
  implementation("org.noear:solon-web") {
exclude group: "org.noear", module: "solon-boot-smarthttp"
}

路由匹配未按预期

3.0.5 版本如果路由存在包含关系,必须指定index,越具体的地址优先级要越高优先级(也就是index越小)。

配置多个 predicates 的 Path导致路由无法匹配

3.0.5 版本不支持配置多个路由的匹配,3.0.6版本将支持,需要匹配多个Path时,配置多个路由。

效果

新版的 Solon Gateway 已经在开发线稳定运行一个多月,另外内存使用方面比Spring Gateway 少了五百兆左右。

  • Spring-gateway

  • Solon-gateway

「定投搜索」更新内容:

  • 增加首页

    • 提供基于标题的模糊搜索,返回匹配的 3 条课程记录。

    • 最新课程,显示最近整理的课程,可以直观看到课程的同步情况。

  • 课程内容

    • 使用 Qwen 大模型校对课程内容。
    • 人工校对课程内容。
  • 服务重构

    • 「定投搜索」和「类比宝库」的机器人合并成一个服务进行。
    • 优化校对和课程索引流程。

特别感谢小熊老师帮忙校对课程内容。

如果发现工具的问题,可以添加我的微信「Crazy_Airhead」,或 Mixin 号「1091586」,进行反馈,谢谢。

说明

本文主要介绍使用 Gradle 工具发布 Jar 包到中央仓库的方法及可能碰到的问题的处理。本文假定读者已经熟悉使用 Gradle,IDEA 等工具,并对中央仓库有所了解。本文使用 macOS,在安装gpg工具时可能有所不同,如果使用 Windows 系统需要自行查阅相关安装方法。

阅读全文 »

问题

系统升级 macOS Sequoia 的 15.1 后,今天要安装安装 gpg 结果提示异常:_error: gnupg: unknown or unsupported macos version: :dunno

分析

macOS 系统版本的升级可能导致 Homebrew 识别新系统版本时出现兼容性问题,具体表现为“unknown or unsupported macOS version: :dunno”错误。通过查看 Github 的讨论(来源),发现该问题可以通过重置 Homebrew 解决。

处理

执行brew docker,检查brew是否有问题。我自己执行此命令提示如下问题:

1
2
3
4
5
6
7
8
9
10
11
Please note that these warnings are just used to help the Homebrew maintainers
with debugging if you file an issue. If everything you use Homebrew for is
working fine: please don't worry or file an issue; just ignore this. Thanks!

Warning: Suspicious https://github.com/Homebrew/brew git origin remote found.
The current git origin is:
https://mirrors.aliyun.com/homebrew/brew.git

With a non-standard origin, Homebrew won't update properly.
You can solve this by setting the origin remote:
git -C "/opt/homebrew" remote set-url origin https://github.com/Homebrew/brew

此时继续执行提示的命令,因为墙的原因,之前设置了国内的镜像。

1
git -C "/opt/homebrew" remote set-url origin https://github.com/Homebrew/brew

再次执行brew docker,提示正常。

执行brew update-reset,将 Homebrew 重置到官方的稳定版本:

1
brew update-reset

接着就可以执行brew update,更新 Homebrew 及所有组件到最新版本。

1
brew update

参考

macOS系统升级Homebrew报错“unknown or unsupported macOS version: :dunno (MacOSVersionError) ”解决方法

一开始发现飞书的菜单栏里显示的推送消息是乱码的,而且在飞书界面中才侧边栏,显示的的已打开的标题也是乱码。一开始以为是飞书的问题,记得是前几天前有升级过,简单找了下飞书的语言相关的设置,没有找到可以配置的地方。于是就将就着用着,影响也不大,虽然标题乱码了,但内容显示是正常的。昨天 Mixin Messenger 推送了新的桌面版本,也做了升级。打开后发现会话列表都是乱码的。突然就想这个应该不是单个软件的问题了,应该是操作系统的配置。

我升级过 MacOS 的最新推送,也就是 macOS Sequoia 的 15.1 版本。首先想到的就是看看设置里的「地区与语言」,我个人的习惯是首选语言是英语,地区是大陆,多少会有点关系。既然中文乱码,那我就先把首选语言修改成中文,然后重启,飞书和 Mixin 的乱码问题都消失了,更加确定就是MacOS升级后引起的配置的问题。于是带着碰碰运气的方式,将首选语言设置成英语,重启后,还是正常显示。于是困扰了几天的问题处理好了。

因为这个问题是一次性的问题,也没有截图。当时想如果没解决,是想具体看看 mac 的系统编码的,现在的系统编码如下:

1
2
3
4
5
6
7
8
9
10
% locale

LANG="zh_CN.UTF-8"
LC_COLLATE="zh_CN.UTF-8"
LC_CTYPE="UTF-8"
LC_MESSAGES="zh_CN.UTF-8"
LC_MONETARY="zh_CN.UTF-8"
LC_NUMERIC="zh_CN.UTF-8"
LC_TIME="zh_CN.UTF-8"
LC_ALL=

总之,如果你和我一样使用的是英语作为首选项,恰好也碰上部分软件突然就显示乱码问题,可以尝试调整下首选语言。

说明

在“「定投搜索」工具的一些分享”中有说到,通过 whsiper.cpp 识别的文本不准确的问题,包括存在繁体中文,错误的同音字,没有标点符号,突然增加的一些版权信息和告别语。

已经尝试使用 GLM4 进行文本的修改,修改效果不错。

以下分享此次使用 GLM4-9B 修改语音识别文本错误的过程。

阅读全文 »