Solon 框架入门:架构、依赖注入与 REST API 实践
[LOADING...]
1. 概述
Solon 是一个独立于 Spring 构建的企业级 Java 框架。
在本教程中,我们将从理论和实践两方面介绍 Solon。首先,我们会从一个问候端点开始,并将其扩展为一个将任务存储到 H2 数据库中的 REST API。
具体来说,我们将使用 Solon 4.1.0、Java 21 和 Maven 3.9.16,并使用 MyBatis-Flex 1.11.8 进行持久化。在此过程中,我们会将配置、依赖注入和 HTTP 处理与熟悉的 Spring Boot 概念进行比较,然后通过真实的 HTTP 请求测试该 API。
2. 什么是 Solon
Solon 将其核心应用服务与可选集成分离。这使我们能够控制应用加载哪些功能。
2.1. 核心原则与架构
该项目强调克制的设计、高效、开放以及可扩展的生态系统。其核心提供依赖注入、面向切面编程和请求路由。插件则添加数据库访问和 JSON 序列化等能力。
Solon 不需要 Servlet 容器或 Java EE 应用服务器。HTTP 适配器将其请求抽象连接到服务器。此处的示例通过 solon-web 捆绑包使用 Smart-HTTP,不过也可以使用 Servlet 适配器。
2.2. Solon 生态系统
主要的 solon 项目由面向分布式服务的 solon-cloud 和面向 AI 应用的 solon-ai 作为补充。还有其他相关项目:
- 用于工作流的
solon-flow - 用于表达式求值的
solon-expression - 以及用于应用管理的
solon-admin
solon-java17 和 solon-java25 项目承载面向较新 Java 基线的实现。更广泛的插件生态包括 MyBatis-Flex、JPA、Redis、Sa-Token、Nacos 以及网关集成。
2.3. Solon 的适用场景
Solon 的模块化设计使其值得在微服务、无服务器函数以及资源受限的应用(包括嵌入式或 IoT 工作负载)中评估。其 AI 模块也为调用语言模型的应用提供了一个起点。
这些是值得评估的候选场景,而不是性能保证。对于并发要求很高的服务,我们需要使用所需插件对实际工作负载进行基准测试。
当团队已经依赖 Spring Boot 的集成和操作流程时,它可能仍然是更实际的选择。采用 Solon 意味着要学习一套不同的注解、配置约定和扩展点。
3. 第一个应用
让我们创建一个带有标准 src/main/java 目录的 Maven 项目。此外,我们将 App 放在 com.baeldung.solon 中,并将控制器放在其 web 子包中。
3.1. 前提条件
测试环境使用 OpenJDK 21 和 Maven 3.9.16。我们展示的 POM 摘录假定采用 Baeldung 仓库布局及其共享的 parent-modules POM。
因此,让我们将 solon-parent 4.1.0 作为 BOM 导入,并将 solon-web 添加到 pom.xml:
BOM 管理匹配的 Solon 依赖版本。编译器会保留方法参数名,以便 Solon 能够按名称绑定请求参数。Web 捆绑包会引入 solon-lib、solon-server-smarthttp 和 solon-serialization-snack4 等插件。
Solon 声明支持 Java 8 到 Java 26,这也使其可能与遗留应用相关。然而,各个集成可能要求更新的 Java 版本。本项目专门面向 Java 21。
3.2. Hello World
让我们添加一个入口点,用于启动应用并发现其包和子包中的组件:
Spring Boot 通常将 SpringApplication.run() 与 @SpringBootApplication 结合使用。在这里,Solon.start() 无需该注解即可初始化容器和可用插件。
接下来,让我们暴露一个接受可选查询参数的问候端点:
@Mapping 定义路径,而 @Get 限制 HTTP 方法。当 name 缺失时,@Param 提供默认值。具体来说,这些注解来自 org.noear.solon.annotation。
对于此端点,Solon 会将返回的字符串以纯文本形式写入响应体。一个可比的 Spring REST 端点通常使用 @RestController 和 @GetMapping。
3.3. 运行应用
为了从 Maven 运行入口点,让我们在 build/plugins 下配置 exec-maven-plugin 3.6.4:
插件就位后,我们可以编译并启动应用:
从另一个终端,让我们调用该端点:
因此,响应确认查询参数已到达控制器:
不带参数调用 /hello 会返回 Hello, World!。## 4. RESTful API 示例
我们正在构建的 API 可以创建、列出、更新和删除任务。具体来说,每个任务都有一个生成的 ID、一个标题和一个完成标志。
4.1. 依赖
让我们将 MyBatis-Flex Solon 插件 1.11.8、HikariCP 7.1.0 和 H2 2.5.250 添加到依赖中:
接下来,我们继续配置。
4.2. 配置属性
Solon 从 src/main/resources/app.yml 读取应用程序设置。因此,让我们命名应用程序、选择其端口,并配置 HikariCP 连接池 和 mapper 发现:
! 后缀按类型以及名称 tasks 注册数据源。在这里,我们在注入时使用该名称。mapperLocations 列表标识包含 mapper 接口的包。
来自后续配置层的设置会覆盖同一键的较早值。六层配置模型 将应用程序文件置于最低优先级:
例如,启动参数会覆盖 app.yml 中的端口:
云层仅在配置了相关插件时才适用。虽然 Spring Boot 用户熟悉 外部化配置,但 Solon 使用 app.properties 或 app.yml,而不是 Spring Boot 的 application.properties 或 application.yml。
关键的是,H2 数据库仅存在于内存中。当应用程序停止时,其内容会消失。
4.3. 架构分层
让我们围绕三个职责来组织代码:
- TaskController 处理 HTTP 请求和响应
- TaskService 验证标题并协调数据库操作
- TaskMapper 通过 MyBatis-Flex 执行持久化操作
Solon 的 @Component 注解将服务注册为托管 bean。Spring 提供了额外的 角色特定注解,例如 @Service 和 @Repository。然而,两个框架都不要求这种分层组织。
此外,我们可以使用 @Inject 连接对象,其作用类似于 Spring 的 @Autowired。
4.4. 表现层
record 定义请求体中接受的字段:
使用 Boolean 可以使完成标志为 null。相反,创建任务时始终将其设置为 false。另一方面,更新任务时将缺失或 null 的标志视为 false。
让我们在 /tasks 下注册一个控制器并注入服务。其创建端点使用 @Body 绑定 JSON:
返回 Task 可以让 Snack4 插件将响应序列化为 JSON。此外,该端点设置状态 201 和指向新资源的 Location 头。
在同一个控制器中,更新方法将路径参数与 JSON 体结合:
Solon 将 {id} 绑定到名为 id 的参数。完整的控制器还提供了几个映射:
- GET /tasks 返回所有任务,按 ID 排序
- GET /tasks/{id} 返回一个任务
- DELETE /tasks/{id} 删除一个任务并返回 204 和空正文
这些是相当标准的端点,因此结构和框架仍然是重点,而不是实现。
4.5. 业务逻辑层
让我们用 @Component 注解 TaskService。它接收一个 mapper 并在事务内创建任务:
这里,@Transaction 来自 org.noear.solon.data.annotation。持久化插件将 mapper 操作与 Solon 事务管理集成在一起。
normalizeTitle() 辅助方法去除周围空白,并拒绝空白标题或长度超过 200 个字符的标题。无效输入会引发 IllegalArgumentException。
更新首先加载现有任务,保留其 ID。缺失的任务会引发 NoSuchElementException。删除会检查受影响的行数,因此删除未知 ID 会产生相同的错误。
应用程序的 ApiErrorFilter 将这些异常转换为状态为 400 或 404 的 JSON 响应。这将 HTTP 状态处理排除在服务之外。
4.6. 持久层
Task 实体是一个可变的 POJO,具有标准的 getter 和 setter。此外,其 MyBatis-Flex 映射使用自动生成的键:
MyBatis-Flex 在插入后将生成的 ID 写回实体。因此,控制器拥有响应及其 Location 头所需的 ID。
mapper 继承标准数据库操作:
插件通过 mybatisFlex.mapperLocations 配置发现此接口,并使其可用于注入。我们不需要实现其 insert、update 或 delete 方法。
4.7. 初始化数据库
让我们将表定义保存在 src/main/resources/schema.sql 中:
DatabaseInitializer 是另一个 @Component。在这种情况下,它接收命名数据源并在其 @Init 方法中执行脚本:
Solon 在依赖注入后调用初始化方法。模式由该组件显式创建,随后 try-with-resources 关闭 JDBC 资源。
4.8. 试用 API
在端口 8080 上重启完成的应用程序后,让我们创建一个任务:
在全新的数据库上,响应具有状态 201 Created、Location: /tasks/1 头以及以下 JSON 正文:
使用返回的 ID,让我们读取、更新和删除任务:
更新返回更改后的任务。删除返回 204 No Content,随后的读取返回 404。空白标题会产生 400 和 JSON 错误消息。
4.9. API 测试
对于自动化测试,我们添加 solon-test,它在此 Solon 版本中包含 JUnit 5:
示例模块使用 JUnit 5.14.4 和 Surefire 3.5.5,并从 Baeldung 共享父 POM 继承测试选择规则。
接下来,让我们启用 HTTP 服务器并选择测试环境:
Solon 在此要求一个公共测试类。在仓库中,app-test.yml 选择一个单独的 H2 数据库。Maven 的 integration profile 保留一个可用端口并将其作为 server.port 传递。
request() 辅助方法使用 JDK HttpClient 联系该端口。因此,此测试检查实际的 HTTP 响应和数据库状态:
集成测试套件还检查问候语、缺失的 ID 以及完整的创建-读取-更新-删除序列。让我们从模块目录分别运行单元测试和集成测试:
让我们查看集成报告:
因此,所有测试均无问题通过。
4.10. 事务回滚
最后,让我们通过一个单独的服务级测试演示 @Rollback:
被注解的方法是公共的,因此 Solon 代理可以拦截它。@AfterEach 钩子在清理之前检查数据库是否为空,从而验证回滚。
值得注意的是,我们不会将 @Rollback 应用于跨多个 HTTP 请求演练 CRUD 的测试。当 @Rollback 处于活动状态时,Solon 安装一个拦截器,分别回滚每个 HTTP 请求的事务。因此,由一个请求创建的任务对下一个请求不可用。
因此,HTTP 工作流测试在没有 @Rollback 的情况下运行,并在每个测试前后使用显式的行清理。在测试中,更改跨请求持久化,而清理使测试彼此隔离。
5. 结论
在本文中,我们使用 MyBatis-Flex 和 H2 构建并测试了一个 Solon REST API。此外,我们了解了它的注解、配置层和插件如何支持请求处理和持久化,包括测试 HTTP 工作流与验证事务回滚之间的区别。
与往常一样,完整源代码可在 GitHub 上获取。
帖子 Introduction to Solon 首次出现在 Baeldung 上。