Ohhnews

分类导航

$ cd ..
Baeldung原文

Solon 框架入门:架构、依赖注入与 REST API 实践

#solon#java框架#依赖注入#rest api#mybatis-flex

[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-java17solon-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

$ xml
<parent>
    <groupId>com.baeldung</groupId>
    <artifactId>parent-modules</artifactId>
    <version>1.0.0-SNAPSHOT</version>
</parent>
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.noear</groupId>
            <artifactId>solon-parent</artifactId>
            <version>4.1.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>
<dependencies>
    <dependency>
        <groupId>org.noear</groupId>
        <artifactId>solon-web</artifactId>
    </dependency>
</dependencies>
<properties>
    <java.version>21</java.version>
    <maven.compiler.parameters>true</maven.compiler.parameters>
</properties>

BOM 管理匹配的 Solon 依赖版本。编译器会保留方法参数名,以便 Solon 能够按名称绑定请求参数。Web 捆绑包会引入 solon-libsolon-server-smarthttpsolon-serialization-snack4 等插件。

Solon 声明支持 Java 8 到 Java 26,这也使其可能与遗留应用相关。然而,各个集成可能要求更新的 Java 版本。本项目专门面向 Java 21。

3.2. Hello World

让我们添加一个入口点,用于启动应用并发现其包和子包中的组件:

$ java
public class App {
    public static void main(String[] args) {
        Solon.start(App.class, args);
    }
}

Spring Boot 通常将 SpringApplication.run()@SpringBootApplication 结合使用。在这里,Solon.start() 无需该注解即可初始化容器和可用插件。

接下来,让我们暴露一个接受可选查询参数的问候端点:

$ java
@Controller
public class DemoController {
    @Get
    @Mapping("/hello")
    public String hello(@Param(defaultValue = "World") String name) {
        return "Hello, " + name + "!";
    }
}

@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

$ xml
<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>exec-maven-plugin</artifactId>
    <version>3.6.4</version>
    <configuration>
        <mainClass>com.baeldung.solon.App</mainClass>
    </configuration>
</plugin>

插件就位后,我们可以编译并启动应用:

$ bash
mvn compile exec:java

从另一个终端,让我们调用该端点:

$ bash
curl 'http://localhost:8080/hello?name=Baeldung'

因此,响应确认查询参数已到达控制器:

$ plaintext
Hello, Baeldung!

不带参数调用 /hello 会返回 Hello, World!。## 4. RESTful API 示例

我们正在构建的 API 可以创建、列出、更新和删除任务。具体来说,每个任务都有一个生成的 ID、一个标题和一个完成标志。

4.1. 依赖

让我们将 MyBatis-Flex Solon 插件 1.11.8HikariCP 7.1.0H2 2.5.250 添加到依赖中:

$ xml
<dependency>
    <groupId>com.mybatis-flex</groupId>
    <artifactId>mybatis-flex-solon-plugin</artifactId>
    <version>1.11.8</version>
</dependency>
<dependency>
    <groupId>com.zaxxer</groupId>
    <artifactId>HikariCP</artifactId>
    <version>7.1.0</version>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>2.5.250</version>
    <scope>runtime</scope>
</dependency>

接下来,我们继续配置。

4.2. 配置属性

Solon 从 src/main/resources/app.yml 读取应用程序设置。因此,让我们命名应用程序、选择其端口,并配置 HikariCP 连接池 和 mapper 发现:

$ config
solon.app:
  name: task-api
  group: examples
server.port: 8080
solon.dataSources:
  tasks!:
    class: com.zaxxer.hikari.HikariDataSource
    jdbcUrl: jdbc:h2:mem:tasks
    username: sa
    password: ""
    maximumPoolSize: 4
mybatisFlex:
  mapperLocations:
    - com.baeldung.solon.persistence

! 后缀按类型以及名称 tasks 注册数据源。在这里,我们在注入时使用该名称。mapperLocations 列表标识包含 mapper 接口的包。

来自后续配置层的设置会覆盖同一键的较早值六层配置模型 将应用程序文件置于最低优先级:

[LOADING...]

例如,启动参数会覆盖 app.yml 中的端口:

$ bash
mvn compile exec:java -Dexec.args="--server.port=8081"

云层仅在配置了相关插件时才适用。虽然 Spring Boot 用户熟悉 外部化配置,但 Solon 使用 app.propertiesapp.yml,而不是 Spring Boot 的 application.propertiesapplication.yml

关键的是,H2 数据库仅存在于内存中。当应用程序停止时,其内容会消失。

4.3. 架构分层

让我们围绕三个职责来组织代码:

  • TaskController 处理 HTTP 请求和响应
  • TaskService 验证标题并协调数据库操作
  • TaskMapper 通过 MyBatis-Flex 执行持久化操作

Solon 的 @Component 注解将服务注册为托管 bean。Spring 提供了额外的 角色特定注解,例如 @Service@Repository。然而,两个框架都不要求这种分层组织

此外,我们可以使用 @Inject 连接对象,其作用类似于 Spring 的 @Autowired

4.4. 表现层

record 定义请求体中接受的字段:

$ java
public record TaskRequest(String title, Boolean completed) {
}

使用 Boolean 可以使完成标志为 null。相反,创建任务时始终将其设置为 false。另一方面,更新任务时将缺失或 null 的标志视为 false

让我们在 /tasks 下注册一个控制器并注入服务。其创建端点使用 @Body 绑定 JSON:

$ java
@Controller
@Mapping("/tasks")
public class TaskController {
    @Inject
    private TaskService taskService;
    @Post
    @Mapping
    public Task create(@Body TaskRequest request, Context context) {
        Task task = taskService.create(request.title());
        context.status(201);
        context.headerSet("Location", "/tasks/" + task.getId());
        return task;
    }
}

返回 Task 可以让 Snack4 插件将响应序列化为 JSON。此外,该端点设置状态 201 和指向新资源的 Location 头。

在同一个控制器中,更新方法将路径参数与 JSON 体结合:

$ java
@Put
@Mapping("/{id}")
public Task update(long id, @Body TaskRequest request) {
    return taskService.update(id, request.title(),
      Boolean.TRUE.equals(request.completed()));
}

Solon 将 {id} 绑定到名为 id 的参数。完整的控制器还提供了几个映射

  • GET /tasks 返回所有任务,按 ID 排序
  • GET /tasks/{id} 返回一个任务
  • DELETE /tasks/{id} 删除一个任务并返回 204 和空正文

这些是相当标准的端点,因此结构和框架仍然是重点,而不是实现。

4.5. 业务逻辑层

让我们用 @Component 注解 TaskService。它接收一个 mapper 并在事务内创建任务:

$ java
@Inject
TaskMapper taskMapper;
@Transaction
public Task create(String title) {
    Task task = new Task();
    task.setTitle(normalizeTitle(title));
    task.setCompleted(false);
    taskMapper.insert(task);
    return task;
}

这里,@Transaction 来自 org.noear.solon.data.annotation持久化插件将 mapper 操作与 Solon 事务管理集成在一起

normalizeTitle() 辅助方法去除周围空白,并拒绝空白标题或长度超过 200 个字符的标题。无效输入会引发 IllegalArgumentException

更新首先加载现有任务,保留其 ID。缺失的任务会引发 NoSuchElementException。删除会检查受影响的行数,因此删除未知 ID 会产生相同的错误。

应用程序的 ApiErrorFilter 将这些异常转换为状态为 400404 的 JSON 响应。这将 HTTP 状态处理排除在服务之外。

4.6. 持久层

Task 实体是一个可变的 POJO,具有标准的 getter 和 setter。此外,其 MyBatis-Flex 映射使用自动生成的键:

$ java
@Table("tasks")
public class Task {
    @Id(keyType = KeyType.Auto)
    private Long id;
    private String title;
    private boolean completed;
    // getters and setters
}

MyBatis-Flex 在插入后将生成的 ID 写回实体。因此,控制器拥有响应及其 Location 头所需的 ID。

mapper 继承标准数据库操作:

$ java
public interface TaskMapper extends BaseMapper<Task> {
}

插件通过 mybatisFlex.mapperLocations 配置发现此接口,并使其可用于注入。我们不需要实现其 insert、update 或 delete 方法。

4.7. 初始化数据库

让我们将表定义保存在 src/main/resources/schema.sql 中:

$ query
CREATE TABLE IF NOT EXISTS tasks (
    id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    title VARCHAR(200) NOT NULL,
    completed BOOLEAN NOT NULL DEFAULT FALSE
);

DatabaseInitializer 是另一个 @Component。在这种情况下,它接收命名数据源并在其 @Init 方法中执行脚本:

$ java
@Inject("tasks")
private DataSource dataSource;
@Init
public void initialize() throws SQLException, IOException {
    String schema = ResourceUtil.getResourceAsString("schema.sql");
    try (Connection connection = dataSource.getConnection();
      Statement statement = connection.createStatement()) {
        statement.execute(schema);
    }
}

Solon 在依赖注入后调用初始化方法。模式由该组件显式创建,随后 try-with-resources 关闭 JDBC 资源。

4.8. 试用 API

在端口 8080 上重启完成的应用程序后,让我们创建一个任务:

$ bash
curl -i -X POST http://localhost:8080/tasks \
  -H 'Content-Type: application/json' \
  -d '{"title":"Learn Solon"}'

在全新的数据库上,响应具有状态 201 CreatedLocation: /tasks/1 头以及以下 JSON 正文:

$ cat
{"id":1,"title":"Learn Solon","completed":false}

使用返回的 ID,让我们读取、更新和删除任务:

$ bash
curl http://localhost:8080/tasks/1
curl -X PUT http://localhost:8080/tasks/1 \
  -H 'Content-Type: application/json' \
  -d '{"title":"Learn Solon REST APIs","completed":true}'
curl -i -X DELETE http://localhost:8080/tasks/1

更新返回更改后的任务。删除返回 204 No Content,随后的读取返回 404。空白标题会产生 400 和 JSON 错误消息。

4.9. API 测试

对于自动化测试,我们添加 solon-test,它在此 Solon 版本中包含 JUnit 5

$ xml
<dependency>
    <groupId>org.noear</groupId>
    <artifactId>solon-test</artifactId>
    <scope>test</scope>
</dependency>

示例模块使用 JUnit 5.14.4 和 Surefire 3.5.5,并从 Baeldung 共享父 POM 继承测试选择规则。

接下来,让我们启用 HTTP 服务器并选择测试环境:

$ java
@SolonTest(value = App.class, env = "test", enableHttp = true,
  delay = 0, debug = false)
public class TaskApiIntegrationTest {
    @Inject("${server.port}")
    private int port;
    @Inject
    private TaskService taskService;
}

Solon 在此要求一个公共测试类。在仓库中,app-test.yml 选择一个单独的 H2 数据库。Maven 的 integration profile 保留一个可用端口并将其作为 server.port 传递。

request() 辅助方法使用 JDK HttpClient 联系该端口。因此,此测试检查实际的 HTTP 响应和数据库状态:

$ java
@Test
void givenBlankTitle_whenCreatingTask_thenReturnBadRequestWithoutPersisting() throws Exception {
    HttpResponse<String> response = request("POST", "/tasks", "{\"title\":\" \"}");
    assertEquals(400, response.statusCode());
    assertEquals("Title must not be blank",
      ONode.ofJson(response.body()).get("message").getString());
    assertTrue(taskService.findAll().isEmpty());
}

集成测试套件还检查问候语、缺失的 ID 以及完整的创建-读取-更新-删除序列。让我们从模块目录分别运行单元测试和集成测试:

$ bash
mvn clean install -Pdefault
mvn clean install -Pintegration

让我们查看集成报告:

$ plaintext
Tests run: 5, Failures: 0, Errors: 0, Skipped: 0

因此,所有测试均无问题通过。

4.10. 事务回滚

最后,让我们通过一个单独的服务级测试演示 @Rollback

$ java
@Test
@Rollback
public void whenCreatingTaskWithinTransaction_thenReadUncommittedTask() {
    Task task = taskService.create("Temporary task");
    assertEquals("Temporary task", taskService.findById(task.getId()).getTitle());
}

被注解的方法是公共的,因此 Solon 代理可以拦截它。@AfterEach 钩子在清理之前检查数据库是否为空,从而验证回滚。

值得注意的是,我们不会将 @Rollback 应用于跨多个 HTTP 请求演练 CRUD 的测试。当 @Rollback 处于活动状态时,Solon 安装一个拦截器,分别回滚每个 HTTP 请求的事务。因此,由一个请求创建的任务对下一个请求不可用。

因此,HTTP 工作流测试在没有 @Rollback 的情况下运行,并在每个测试前后使用显式的行清理。在测试中,更改跨请求持久化,而清理使测试彼此隔离。

5. 结论

在本文中,我们使用 MyBatis-Flex 和 H2 构建并测试了一个 Solon REST API。此外,我们了解了它的注解、配置层和插件如何支持请求处理和持久化,包括测试 HTTP 工作流与验证事务回滚之间的区别。

与往常一样,完整源代码可在 GitHub 上获取

帖子 Introduction to Solon 首次出现在 Baeldung 上。