# Abount
URL: https://tio-boot.com/about.html
Source: about.md
# Abount
## About Author
- James 詹波 [github](https://github.com/jfinal/) [原来是这样的 JFinal](https://gitee.com/gitee-stars/2) (jfinal@qq.com)
- Yao Wu Tan 谭耀武 [github](https://github.com/tywo45/) [程序员中最会拉筋的 CTO](https://gitee.com/gitee-stars/10)
- Tong Li 李通 [github](https://github.com/litongjava/) (litongjava001@gmail.com )
## Tong Li 简介
**Tong Li**
邮箱:litongjava@outlook.com
个人网站:https://www.litongjava.com/
GitHub: https://github.com/litongjava (3k stars)
### 全栈开发工程师 | 8 年开发经验
具备扎实的编程基础与丰富的开发经验,熟悉前后端、数据库、运维及微服务架构设计,拥有丰富的算法开发与优化经验。擅长网络编程及分布式系统设计,熟悉人工智能(AI)与大语言模型(LLM)的开发与应用。
#### 核心技能:
- **算法与数据结构**: 熟悉各种算法与数据结构,能够设计和优化高效的解决方案
- **Java**: 扎实的 Java 基础,精通多线程、并发编程,擅长性能调优
- **后端开发**: 深入掌握 Spring Boot, Spring Cloud, MyBatis 等主流 Web 框架及微服务架构
- **设计模式**: 熟练掌握并应用常用设计模式,提升代码可维护性与可扩展性
- **数据库**: 熟悉 MySQL、PostgreSQL 等关系型数据库,具备 SQL 编写与优化能力,能够在开发中高效落地数据库设计
- **缓存与消息系统**: 深入了解 Redis、RocketMQ、ElasticSearch 等技术,具备高并发场景下的缓存与消息队列优化能力
- **网络编程与分布式系统**: 熟悉 TCP/IP、HTTP 协议,掌握 Spring Cloud + Dubbo 分布式架构设计
- **容器化与运维**: 熟练使用 Linux 命令与服务器应用,精通 Docker 与容器化部署,编写过大量高效的 Dockerfile
- **项目经验**: 深度参与大型项目开发,具备人工智能、大模型与智能代理(Agents)开发经验
- **自研框架**: 自主研发基于 Java AIO 的高性能 Web 框架 TioBoot,适用于高并发与低延迟场景
---
### 工作经历
**全栈开发工程师 | Imaginix, Inc**
_2024.02 - 至今_
- 负责后端服务的架构设计与核心功能开发,优化服务器性能,确保系统的高可用与稳定性
- 基于算法的需求分析,设计并实现高效的后端功能,制定开发规范与交互逻辑
- 编写与维护技术文档,提供跨团队技术支持,并指导初级工程师
- 参与网络层优化与数据传输性能的提升
**全栈开发工程师 | 北京合光人工智能机器人技术有限公司**
_2016.09 - 2023.09_
- 设计、开发并维护智能云平台的后端服务,保障服务的高可用性和扩展性
- 负责网络架构的设计与优化,提升系统的吞吐量与响应速度
- 与多团队协作,负责与第三方服务的集成,确保系统整体功能的顺畅协同
- 参与系统的性能测试与优化,针对高并发与大数据场景进行优化调试
---
### 教育经历
**Kapiolani Community College**
_2023.01 - 至今_
---
### 开源项目
**study11**
- Study11.ai 是一个基于 大模型 的智能化学习平台,核心是将学生输入的问题转化为 结构化可视化讲解动画。 平台面向中学理科与大学通识课程,支持 多模态(动效 + 语音 + 字幕)讲解.
- [GitHub](https://github.com/litongjava/study11-backend) |[体验地址](https://www.study11.ai/)
**java-maxkb**
- java-maxkb 是一个基于 Java 开发的智能知识库系统,利用先进的自然语言处理和向量检索技术,为用户提供高效、准确的问答服务
- [GitHub](https://github.com/litongjava/java-maxkb) | [文档](https://www.tio-boot.com/zh/61_knowledge-base/01.html)
**tools-ocr**
- 跨平台离线 OCR 桌面客户端,支持 Windows, Linux, MacOS
- [GitHub](https://github.com/AnyListen/tools-ocr) | [文档](https://tree-hole-ocr-docs.vercel.app/)
**tio-boot**
- 基于 Java AIO 的高性能异步非阻塞 Web 中间件,适用于高并发场景
- [GitHub](https://github.com/litongjava/tio-boot) | [文档](https://tio-boot.litongjava.com/)
**tio-boot-admin**
- 基于 tio-boot 的后端开发脚手架,支持快速构建高性能应用
- [GitHub](https://github.com/litongjava/tio-boot-admin)
**tio-boot-admin-react**
- 基于 React 的前端开发脚手架,支持与 tio-boot 完美集成
- [GitHub](https://litongjava.github.io/tio-boot-admin-react/)
---
# AI 检索与文档数据入口
URL: https://tio-boot.com/ai-retrieval.html
Source: ai-retrieval.md
# AI 检索与文档数据入口
站点在每次构建时从当前 Markdown 正文生成检索资源,章节移动、改名或更新后无需手工维护另一份语料。
## 在线检索与语料下载
使用站内搜索可以快速定位文档;需要在本地建立检索索引时,可以按需下载以下资源:
站内浏览与搜索需要联网,资源按访问需要加载。保存 JSONL、章节正文或完整文档后,本地检索工具可以直接读取文件并离线检索;生成式问答需要另行配置本地模型。
## 资源地址
| 地址 | 用途 |
| --- | --- |
| `/llms.txt` | 按语言和章节组织的目录,链接到可直接读取的 Markdown |
| `/llms-full.txt` | 全站正文,适合离线下载 |
| `/ai/index.json` | 机器可读页面清单,schemaVersion 为 1 |
| `/ai/chunks.jsonl` | 按标题分段的语料,每行一个 JSON 对象 |
| `/ai/chapters/zh/28_redis.txt` | Redis 章节正文;其他章节使用相同路径规则 |
| `/ai/pages/zh/28_redis/01.md` | 单页 Markdown,正文链接已转换为站点绝对地址 |
| `/ai/redirects.json` | 旧 Markdown 源路径到新源路径的迁移映射 |
| `/sitemap.xml` | 可被搜索引擎发现的 HTML 页面清单 |
## 检索流程
1. 读取 llms.txt 或 ai/index.json,按章节、标题、关键词和摘要筛选候选页。
2. 下载 markdownUrl 指向的原文,或将 chunks.jsonl 导入自己的全文/向量索引。分段遵循代码块外的标题,代码示例不会因为其中的 # 字符被切开;单段长度不固定。
3. 将问题、相关段落和对应 url 交给模型,回答时引用 url 指向的正式文档页面。
4. 使用 sha256 判断正文是否变化,按 id 更新或删除索引记录。构建中移除的页面也应从下游索引删除。
页面记录包含 id、source、title、language、chapter、url、markdownUrl、description、keywords、status 和 sha256。分段记录还包含 pageId、heading、content;分段 id 的序号会随标题调整变化,因此应按页面整体更新分段。
status 为 placeholder 表示按长度或仅含标题识别的短内容候选。它不是对所有内容质量的判定;检索时应结合正文中的适用版本、前置条件和功能限制。不要将目录页或占位标题推断为已经实现的功能。
## 构建与部署
运行 `pnpm docs:check` 检查源文档,运行 `pnpm docs:build` 生成页面和语料,再运行 `pnpm docs:check-ai` 验证生成结果。发布整个 `docs/.vuepress/dist`,保留 ai 目录、llms 文件和 robots.txt。
`pnpm docs:test` 启动临时本地站点,用真实浏览器验证在线搜索、语料下载、章节别名与历史地址。测试需安装 Playwright 浏览器(`pnpm exec playwright install chromium`),也可通过 `PLAYWRIGHT_CHANNEL=chrome` 使用已安装的 Chrome。
旧章节 URL 和不带序号的章节别名均生成静态跳转页;Cloudflare Pages 可使用生成的 `_redirects` 获得 301 跳转。例如 `/zh/java-openai` 指向当前 OpenAI 章节,`/zh/java-openai/01.html` 指向对应文章。页面跳转脚本保留查询参数与 hash,但被改写标题的旧锚点不保证存在。
llms.txt 是社区提出的文档发现约定,参见 [llms.txt 提案](https://llmstxt.org/)。本仓库提供可获取、可导入的文本资源,不承诺外部 AI 产品一定抓取或收录,也不包含在线向量数据库或问答服务。
---
# Home
URL: https://tio-boot.com/
Source: index.md
---
# Quick Start
URL: https://tio-boot.com/en/1%20Quick%20Start/1.0%20Quick%20Start.html
Source: en/1 Quick Start/1.0 Quick Start.md
# Quick Start
---
# tio-boot:新一代高性能 Java Web 开发框架
URL: https://tio-boot.com/zh/01_introduction/01.html
Source: zh/01_introduction/01.md
# tio-boot:新一代高性能 Java Web 开发框架
[[toc]]
## 概述
tio-boot:好用、快速、节约时间的高性能 Java Web 开发框架。
**tio-boot** 是一款轻量级、高性能的 Java Web 开发框架,以**更快、更小、更简单**为目标,为开发者带来卓越的性能与开发效率。相比传统框架,tio-boot 提供显著优势:
- **并发性能提升**:快 2 ~ 3 倍
- **内存占用优化**:减少 1/3 ~ 1/2
- **启动速度提高**:快 5 ~ 10 倍
- **打包文件更轻**:缩减至 1/2 ~ 1/10
基于 Java AIO 的设计,tio-boot 在 2 核 4G 的服务器环境下即可稳定支持上万并发连接,是高并发场景的理想选择。
---
## 主要特点
1. **高效架构**
基于 Java AIO 和 t-io 框架,摒弃传统 Servlet 模型,实现非阻塞、高性能的网络通信。
2. **灵活配置**
引入 Spring-Boot 配置理念,支持常见注解,但未引入 Spring 的 IOC 和 AOP,以更简洁的方式满足开发需求。
3. **轻量功能集成**
内置 JFinal 的 AOP 模型、Enjoy 模版引擎和 Active-Row 数据库操作工具,功能强大,简洁易用。
4. **丰富的 Web 组件**
提供拦截器、Handler、Controller 和 WebSocket 支持,满足大部分 Web 开发需求。
5. **多协议支持**
单端口同时支持 UDP、TCP、HTTP 和 WebSocket,多协议场景下表现优异。
---
## 框架优势
### 性能优化
- **启动快,内存占用少**:仅需 300 毫秒即可启动服务,内存需求减少一半。
- **高并发支持**:优化连接处理,轻松支持数万并发连接。
### 资源节省
- **轻量化**:JAR 包仅 3M,大幅降低存储需求。
- **服务器优化**:在相同负载下,所需服务器数量减少一半。
### 极速开发
- **热重载**:结合 hotswap-classloader,实现代码修改 20 毫秒内热重载,无需重启服务。
### 高效运行
- **二进制支持**:配合 GraalVM,可将 JAR 包直接编译为原生二进制文件,运行效率更高。
---
### 设计理念
#### 好用至上
**简洁、好用、开发快、运行快、性能高**,这是 tio-boot 的设计初衷。框架以极简的设计理念为核心,去繁求减,返璞归真,让开发者能够轻装上阵,高效完成项目开发。无论是代码编写还是性能优化,tio-boot 都为开发者提供了卓越的体验。
---
#### 快速开发
**快速开发**是 tio-boot 的核心目标:
- **开发迅速**:提供简单易用的 API 和配置方式,让开发者能专注于业务逻辑。
- **代码量少**:框架通过精简的设计减少冗余代码,开发效率显著提升。
- **学习简单**:兼具强大功能与易上手的特点,帮助开发者快速掌握。
- **功能强大**:支持多协议、多组件,满足复杂业务需求。
- **轻量级与高扩展性**:框架小巧灵活,同时具备强大的扩展能力。
- **高可用与高性能**:为高并发、高吞吐量场景量身打造。
tio-boot 的快速开发特性,让开发者在有限时间内实现更多功能,同时保持项目的高性能与高可用性。
---
#### 节约时间
在保留 Java 语言的所有优势的基础上,tio-boot 以 Ruby、Python 等动态语言的开发效率为目标,让开发更加轻松便捷。
- **开发效率更高**:通过高效的工具链和灵活的架构设计,大幅减少重复劳动和配置成本。
- **节省时间去生活**:tio-boot 的开发效率为您节省更多时间,用于陪伴恋人、家人和朋友,享受工作之外的美好时光
## 实测性能
1. **实测性能一:** 1.9G 内存稳定支持 30 万 TCP 长连接。[详情](https://www.tiocloud.com/61)
2. **实测性能二:** 使用 t-io 跑出每秒 1051 万条聊天消息。[详情](https://www.tiocloud.com/41)
3. **实测性能三:** Netty 和 t-io 对比测试结果。[详情](https://www.tiocloud.com/154)
---
## 不足之处
尽管优势明显,tio-boot 仍有以下不足:
1. **学习曲线较陡**:需要一定的底层知识积累,初学者可能需要更多时间理解框架核心。
2. **基础要求高**:对编程基础有较高要求,适合有经验的开发者。
---
## 总结
tio-boot 是一款简洁高效的 Java Web 开发框架,专为追求高性能和快速开发的开发者而设计。它不仅在并发性能和资源优化上表现卓越,还通过简单的开发流程和强大的扩展能力,提升开发效率,为开发者带来更轻松的开发体验。
如果您需要一个轻量、高效的框架来处理高并发场景,tio-boot 将是您值得信赖的选择!
---
# tio-boot 入门示例
URL: https://tio-boot.com/zh/01_introduction/02.html
Source: zh/01_introduction/02.md
# tio-boot 入门示例
[[toc]]
## 创建新项目
**项目名称:** `tio-boot-web-hello`
**开源仓库地址:**
- [https://github.com/litongjava/tio-boot-web-hello](https://github.com/litongjava/tio-boot-web-hello)
---
## 添加依赖
`TioBoot` 已在 Maven 中央仓库发布。
- [Maven Central 上的 TioBoot](https://central.sonatype.com/artifact/nexus.io/tio-boot)
如果您使用 Java 21 进行开发,请在 `pom.xml` 文件中添加以下依赖项:
```xml
UTF-8
21
${java.version}
${java.version}
1.18.44
2.0.52
1.2.9
2.1.8
1.4.0
web-hello
nexus.io.test.HelloApp
ch.qos.logback
logback-classic
1.3.3
nexus.io
tio-boot
${tio-boot.version}
nexus.io
hotswap-classloader
${hotswap-classloader.version}
nexus.io
jfinal-aop
${jfinal-aop.version}
com.alibaba.fastjson2
fastjson2
${fastjson2.version}
org.projectlombok
lombok
${lombok-version}
true
provided
junit
junit
4.12
test
development
true
org.springframework.boot
spring-boot-maven-plugin
2.7.18
true
${main.class}
org.projectlombok
--mode=dev
production
org.springframework.boot
spring-boot-maven-plugin
2.7.18
${main.class}
org.projectlombok
repackage
```
**说明:**
- **Properties 部分:** 定义了关键的项目和版本属性,便于维护和管理依赖版本。
- **Dependencies 部分:** 列出了所有必要的库和框架,涵盖日志、热加载、AOP、JSON 解析、HTTP 客户端等功能。
- **Profiles 部分:** 配置了开发和生产环境的构建设置,确保在不同环境下应用程序的行为和性能优化。
---
## Controller 和 Handler
**TioBoot** 支持两种类型的接口:**Controller** 和 **Handler**。了解它们的区别有助于您为应用程序选择最合适的方式。
### 区别
- **Controller:**
- 支持自动请求参数封装。
- 支持路由扫描(自动将 URL 映射到方法)。
- 适用于一般用途的接口,开发更加便捷。
- **Handler:**
- 不支持自动请求参数封装,需要手动从 `HttpRequest` 中提取参数。
- 不支持路由扫描,需要手动配置路由。
- 性能更高,适用于对性能有高要求的系统级接口。
---
### Controller 示例
由于 Controller 需要通过扫描来被检测和加载,您需要在主应用程序类上添加 `@AComponentScan` 注解。
**主应用程序类 (`HelloApp.java`):**
```java
package nexus.io.test;
import nexus.io.annotation.AComponentScan;
import nexus.io.tio.boot.TioApplication;
@AComponentScan
public class HelloApp {
public static void main(String[] args) {
long start = System.currentTimeMillis();
//TioApplicationWrapper.run(HelloApp.class, args);
TioApplication.run(HelloApp.class, args);
long end = System.currentTimeMillis();
System.out.println((end - start) + "ms");
}
}
```
**说明:**
- `@AComponentScan` 注解启用了项目中 Controller 的自动扫描。
- `main` 方法初始化 Tio 应用程序并输出启动时间,帮助您了解应用程序的启动性能。
**Controller 类 (`IndexController.java`):**
```java
package nexus.io.test.controller;
import nexus.io.annotation.RequestPath;
@RequestPath("/")
public class IndexController {
@RequestPath()
public String index() {
return "index";
}
}
```
**说明:**
- `@RequestPath("/")` 注解将此控制器映射到根 URL 路径。
- `index()` 方法处理对根路径的 HTTP 请求并返回简单的字符串响应。
---
### Handler 示例
Handler 不需要扫描,因此主应用程序类中不需要 `@AComponentScan` 注解。
**Handler 类 (`HelloHandler.java`):**
```java
package nexus.io.test.handler;
import java.util.HashMap;
import java.util.Map;
import nexus.io.model.body.RespBodyVo;
import nexus.io.tio.boot.http.TioRequestContext;
import nexus.io.tio.http.common.HttpRequest;
import nexus.io.tio.http.common.HttpResponse;
public class HelloHandler {
public HttpResponse hello(HttpRequest request) {
// 如有需要,手动提取参数
// 例如:String param = request.getParam("key");
Map data = new HashMap<>();
RespBodyVo respVo = RespBodyVo.ok(data);
return TioRequestContext.getResponse().setJson(respVo);
}
}
```
**说明:**
- `hello()` 方法手动处理 HTTP 请求并构建响应。
- 由于 Handler 不自动封装请求参数,您需要从 `HttpRequest` 中手动提取。
- 使用 `setJson()` 方法将响应设置为 JSON 格式,确保客户端能够正确解析。
**配置类 (`WebHelloConfig.java`):**
```java
import nexus.io.context.BootConfiguration;
import nexus.io.model.type.TioTypeReference;
import nexus.io.tio.boot.server.TioBootServer;
import nexus.io.tio.http.server.handler.IHttpRequestFunction;
import nexus.io.tio.http.server.router.HttpRequestFunctionRouter;
import nexus.io.tio.http.server.router.HttpRequestRouter;
import nexus.io.tio.web.hello.handler.HelloHandler;
public class WebHelloConfig implements BootConfiguration {
public void config() {
TioBootServer server = TioBootServer.me();
HttpRequestRouter requestRouter = server.getRequestRouter();
HelloHandler helloHandler = new HelloHandler();
requestRouter.add("/hello", helloHandler::hello);
}
}
```
**说明:**
- 实现了 `BootConfiguration` 接口以设置自定义配置。
- 手动添加了 `/hello` 路由并将其与 `HelloHandler` 的 `hello()` 方法关联,确保请求能够正确路由到对应的处理方法。
- WebHelloConfig 不需要添加其他注解,已经在启动时将 WebHelloConfig 实例化并传递到 TioApplication,TioApplication 在启动时会执行 WebHelloConfig 的 config 方法
**主应用程序类 (`HelloApp.java`):**
```java
package nexus.io.test;
import nexus.io.tio.boot.TioApplication;
import nexus.io.test.config.WebHelloConfig;
public class HelloApp {
public static void main(String[] args) {
long start = System.currentTimeMillis();
TioApplication.run(HelloApp.class, new WebHelloConfig(), args);
long end = System.currentTimeMillis();
System.out.println((end - start) + "ms");
}
}
```
**说明:**
- 使用 `WebHelloConfig` 中定义的自定义配置启动 Tio 应用程序。
- 计算并输出启动时间,帮助评估应用程序的启动性能。
---
### Lambda
如果你喜欢lambda,你可以这样写
```java
package nexus.io.test;
import java.util.HashMap;
import java.util.Map;
import nexus.io.context.BootConfiguration;
import nexus.io.model.body.RespBodyVo;
import nexus.io.tio.boot.TioApplication;
import nexus.io.tio.boot.http.TioRequestContext;
import nexus.io.tio.boot.server.TioBootServer;
import nexus.io.tio.http.server.router.HttpRequestRouter;
public class HelloApp {
public static void main(String[] args) {
long start = System.currentTimeMillis();
TioApplication.run(HelloApp.class, new BootConfiguration() {
@Override
public void config() throws Exception {
TioBootServer server = TioBootServer.me();
HttpRequestRouter requestRouter = server.getRequestRouter();
requestRouter.get("/hello", (request) -> {
Map data = new HashMap<>();
RespBodyVo respVo = RespBodyVo.ok(data);
return TioRequestContext.getResponse().setJson(respVo);
});
}
}, args);
long end = System.currentTimeMillis();
System.out.println((end - start) + "ms");
}
}
```
### 输出结果
当您访问 `http://localhost/hello` 时,您将收到以下 JSON 响应:
```json
{
"data": {},
"code": 1,
"msg": null,
"ok": true
}
```
---
## 运行应用程序
### 使用 Spring Boot 插件
使用 Spring Boot Maven 插件,以生产环境配置启动应用程序:
```shell
mvn spring-boot:run -Pproduction
```
**说明:**
- `-Pproduction` 参数激活了 `pom.xml` 中定义的生产环境配置。
- 应用程序将以生产环境的设置启动,优化性能和资源使用。
### 测试应用程序
- 在浏览器中访问 `http://localhost/`,或使用 `curl`、`Postman` 等工具。
- 您应该看到来自 `IndexController` 的输出 `index`。
---
## 打包应用程序
将应用程序打包成可部署的 JAR 文件:
```shell
mvn clean package -DskipTests -Dgpg.skip -Pproduction
```
推荐使用笔者开发的[build工具](https://github.com/litongjava/go-build)
.build.txt
```
[win.env]
set JAVA_HOME=D:\java\jdk-21.0.6
[win.build]
mvn clean install -DskipTests -Dgpg.skip
mvn clean package -DskipTests -Dgpg.skip -Pproduction
[macos.build]
mvn clean install -DskipTests -Dgpg.skip
mvn clean package -DskipTests -Dgpg.skip -Pproduction
[linux.build]
mvn clean install -DskipTests -Dgpg.skip
mvn clean package -DskipTests -Dgpg.skip -Pproduction
```
执行命令.进行构建
```
build
```
**说明:**
- `clean`:在构建前清理 `target` 目录,确保没有旧的构建文件影响新构建。
- `package`:编译代码并打包成 JAR 文件,便于部署和分发。
- `-DskipTests`:在构建过程中跳过测试,提高构建速度(仅在确认代码稳定时使用)。
- `-Pproduction`:使用生产环境配置进行构建,确保生成的 JAR 文件适用于生产环境。
---
## 启动打包后的应用程序
打包完成后,您可以使用生成的 JAR 文件启动应用程序:
```shell
java -jar target/web-hello.jar
```
**注意:** 如果生成的 JAR 文件名称不同,请将 `web-hello.jar` 替换为实际的文件名。
**说明:**
- 该命令使用 Java 运行时环境运行 JAR 文件,启动您的应用程序。
- 应用程序将以生产环境的设置启动,确保在生产环境中的最佳性能和稳定性。
---
## 结论
通过本指南,您已经:
- 创建了一个新的 TioBoot 项目。
- 添加了 Java 8 开发所需的依赖项。
- 了解了 TioBoot 中 Controller 和 Handler 的区别。
- 使用 Controller 和 Handler 实现了示例。
- 为开发和生产环境配置了应用程序。
- 打包并运行了您的应用程序。
**提示:**
- 当您需要自动参数处理和路由扫描时,使用 **Controller**。
- 当您需要更高的性能并且愿意手动处理请求参数时,使用 **Handler**。
- 请在开发和生产模式下测试您的应用程序,以确保行为一致,避免在不同环境中出现意外问题。
祝您开发顺利!
---
# Tio-Boot 配置 : 现代化的配置方案
URL: https://tio-boot.com/zh/01_introduction/03.html
Source: zh/01_introduction/03.md
# Tio-Boot 配置 : 现代化的配置方案
[[toc]]
---
## 简介
Tio-Boot 是一个基于 Java 的轻量级框架,提供了灵活的配置管理功能。它支持多环境配置、从多种来源读取配置,并提供了优先级管理,以满足不同场景下的需求。本指南将详细介绍 Tio-Boot 的配置方法及常见配置项的使用。
## 配置文件概述
Tio-Boot 使用 `nexus.io.tio.utils.environment.EnvUtils` 和 `nexus.io.tio.utils.environment.PropUtils` 来读取配置信息。其主要特点包括:
- **多环境配置**:支持加载 `app.properties` 以及 `app-{dev-name}.properties` 等环境特定的配置文件。
- **多来源配置**:支持从内置 Map、启动命令行、Java 环境属性、系统环境变量、`.env` 文件和配置文件中读取配置。读取优先级依次为内置 Map > 启动命令行 > Java 系统属性 > 系统环境变量 > `PropUtils` 中已加载的文件配置。`.env`、`secrets.txt` 和 properties 文件共同合并到文件配置中,不是独立的读取层级;同名项按实际加载顺序覆盖,详见 [EnvUtils 配置工具]()。
- **系统环境变量命名支持**:支持使用下划线命名法,如配置文件中的 `app.env` 对应系统变量 `APP_ENV`。
### 配置文件示例
主配置文件位于 `src/main/resources/app.properties`,示例如下:
```properties
# HTTP 配置
server.port = 80
```
## 常见配置
### 指定静态文件目录
Tio-Boot 支持两种方式读取静态文件:
1. **从 classpath 下读取静态文件**
```properties
server.resources.static-locations = classpath:/pages
```
2. **从文件系统读取静态文件**
```properties
# 当前目录的 pages 目录下读取配置
server.resources.static-locations = pages
```
指定静态文件目录后,Tio-Boot 会在收到访问请求时自动加载相应的静态文件。
### HTTP 响应头中的 Server 信息
#### 配置项简介
`http.response.header.showServer` 是一个布尔类型的配置项,用于控制是否在 HTTP 响应头中添加服务器信息。
- **true**:在每个 HTTP 响应中添加 `Server` 头部,显示服务器信息。
- **false**(默认值):不添加 `Server` 头部。
#### 配置项意义
该配置项在不同场景下具有不同的用途:
- **开发和调试环境**:启用 `Server` 头部,方便调试工具和开发人员查看服务器信息,有助于定位和解决问题。
- **生产环境**:禁用 `Server` 头部,提高安全性,防止攻击者获取服务器详细信息,减少潜在攻击面。
#### 配置示例
在 Tio-Boot 的配置文件(如 `application.properties`)中,可以通过以下方式设置 `http.response.header.showServer`:
```properties
# 启用 Server 头部
http.response.header.showServer=true
# 或者禁用 Server 头部
http.response.header.showServer=false
```
#### 实际效果
根据配置项的设置,服务器响应头中会有不同的表现:
- **启用 `Server` 头部**:
```http
HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Server: Tio
...
```
- **禁用 `Server` 头部**:
```http
HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
...
```
### 启用全局跨域
要开启全局跨域支持,可以在配置文件中添加以下配置:
```properties
server.http.response.cors.enable=true
```
## 多环境配置
Tio-Boot 支持多环境配置,允许在不同的环境下加载不同的配置文件。以下是配置步骤和注意事项。
### 设置环境键
默认情况下,Tio-Boot 使用 `app.env` 作为环境键。可以通过 `setEnvKey` 方法自定义环境键。如果配置文件中使用的是默认的 `app.env`,则无需进行此步骤。
### 加载主配置文件
Tio-Boot 启动时会加载主配置文件 `app.properties`。该文件应包含指定当前运行环境的键值对,例如:
```properties
app.env=dev
```
可以通过配置文件、环境变量或启动参数来设置 `app.env` 的值。
### 根据环境加载特定配置文件
Tio-Boot 会根据 `app.env` 的值自动加载相应的环境特定配置文件。例如:
- 如果 `app.env=dev`,则加载 `app-dev.properties`。
- 如果 `app.env=prod`,则加载 `app-prod.properties`。
这是通过 `handleEnv` 方法实现的,依据 `app.env` 的值追加相应的环境配置文件。
### 具体配置步骤
假设有三个配置文件:`app.properties`(主配置文件)、`app-dev.properties`(开发环境配置)、`app-prod.properties`(生产环境配置)。按照以下步骤进行配置:
1. **在 `app.properties` 中设置环境**
```properties
# 或者 prod
app.env=dev
```
2. **启动 Tio-Boot**
```java
TioApplication.run(HelloApp.class, args);
```
3. **获取当前环境的键值**
```java
String env = EnvironmentUtils.get(ConfigKeys.appEnv);
```
启动后,Tio-Boot 会根据 `app.properties` 中的 `app.env` 值加载对应的环境文件(如 `app-dev.properties` 或 `app-prod.properties`)。这样,可以根据不同环境自动加载相应的配置文件。
### 注意事项
- 确保 `app.properties`、`app-dev.properties` 和 `app-prod.properties` 文件都位于 CLASSPATH 下或在可访问的文件路径中。
- Tio-Boot 会合并主配置文件和环境特定的配置文件。如果存在重复的键,环境特定配置文件中的值将覆盖主配置文件中的值。
## 通过命令行指定参数
Tio-Boot 支持通过命令行指定参数,参数查找顺序为:命令行参数 > 环境变量 > 配置文件。
### 示例
以下示例展示了如何通过命令行指定 HTTP 端口:
```shell
java -jar paddle-ocr-server-1.0.1.jar --http-port=8080
```
这样,`http-port` 的值将优先于配置文件中的设置。
## 读取配置
Tio-Boot 提供了 `EnvUtils` 工具类,用于在代码中读取配置。
### 使用示例
```java
import nexus.io.tio.utils.environment.EnvUtils;
// 手动加载配置,仅需执行一次。如果使用 Tio-Boot 框架,配置会自动加载
// TioApplication.run(...) 会进入 TioApplicationContext.run(...),并在启动阶段自动调用 EnvUtils.load(args)
// 因此普通 Tio-Boot 应用通常不需要在 main 方法里再手动调用 EnvUtils.load()
EnvUtils.load();
// 读取配置
String host = EnvUtils.get("tdengine.host");
int port = EnvUtils.getInt("tdengine.port");
```
补充说明:标准的 Tio-Boot 启动方式是 `TioApplication.run(...)`,它内部会进入 `TioApplicationContext.run(...)`,并在启动阶段自动执行 `EnvUtils.load(args)`。因此常规的 Tio-Boot 应用一般不需要在 `main` 方法中重复调用 `EnvUtils.load()`;只有脱离 Tio-Boot 启动流程的独立工具类、测试代码或脚本入口,才需要自行调用。
::: warning 不要在 TioApplication.run 之前调用 EnvUtils.load()
`EnvUtils.load()` 只会加载一次。如果在 `main` 方法里先调用它,`TioApplication.run` 启动时会跳过加载,**命令行参数也不再解析**,`--app.env=prod`、`--server.port=8080` 这样的参数会失效。
```java
public static void main(String[] args) {
// 错误:--app.env=prod 不会生效
EnvUtils.load();
TioApplication.run(App.class, args);
}
```
框架会自动加载配置,`main` 方法里不需要调用。确实要在启动前读取配置时,使用 `EnvUtils.load(args)`,它会同时解析命令行参数。
:::
通过 `EnvUtils`,可以方便地在代码中获取配置项的值,支持不同的数据类型转换。
---
通过以上配置指南,可以全面掌握 Tio-Boot 的配置方法,灵活应对不同的开发和生产环境需求,确保应用程序的稳定和安全运行。
---
# tio-boot 整合 hotswap-classloader 实现热加载
URL: https://tio-boot.com/zh/01_introduction/05.html
Source: zh/01_introduction/05.md
# `tio-boot` 整合 `hotswap-classloader` 实现热加载
## 简介
在现代软件开发中,快速迭代和高效测试是提升开发效率的关键。传统的开发流程中,每次代码变更后都需要重启应用程序,这不仅耗时且影响开发体验。为了优化这一过程,本文介绍了如何将 `hotswap-classloader` 与 `tio-boot` 结合使用,实现 Java 应用的热加载(Hot Swapping),从而在不重启 JVM 的情况下动态更新类定义。
### 什么是 `hotswap-classloader`?
[`hotswap-classloader`](https://github.com/litongjava/hotswap-classloader) 是由开发者 litongjava 开发的一款 Java 动态类加载器。其核心功能是在 Java 应用运行时动态更换或更新类定义,无需重启整个 JVM。这种热替换能力在开发过程中尤为有用,因为它显著减少了等待应用重启的时间,加快了开发迭代和测试的效率。
### 为什么将 `hotswap-classloader` 与 `tio-boot` 结合使用?
将 `hotswap-classloader` 与 `tio-boot` 结合使用,为 Java 网络应用开发带来了多项关键优势:
1. **快速迭代和测试**:开发者可以在不重启服务器的情况下实时更新类文件,快速迭代和即时测试,提高开发效率。
2. **提高开发效率**:减少了重启应用程序的时间,使开发者能够更专注于代码的编写和改进。
3. **支持敏捷开发**:在敏捷开发模式下,频繁的代码更改和测试是常态。`hotswap-classloader` 的动态加载能力使这一过程更加流畅和高效。
结合使用 `hotswap-classloader` 和 `tio-boot` 不仅提升了开发效率,还增强了网络应用开发的灵活性和便利性,对于希望快速迭代和改进其网络应用的开发团队而言,这是一个极具价值的组合。
## 整合热加载
### 整合热加载的步骤
以下步骤将指导您如何将 `hotswap-classloader` 与 `tio-boot` 进行整合,实现热加载功能:
#### 1. 添加依赖
在项目的 `pom.xml` 文件中添加 `hotswap-classloader` 的依赖:
```xml
nexus.io
hotswap-classloader
${hotswap-classloader.version}
```
请确保将 `${hotswap-classloader.version}` 替换为实际的版本号。
#### 2. 使用 `TioApplicationWrapper` 启动服务
在启动类中使用 `TioApplicationWrapper` 来启动应用,以启用热加载功能:
```java
import nexus.io.hotswap.wrapper.tio.boot.TioApplicationWrapper;
import nexus.io.annotation.AComponentScan;
@AComponentScan
public class HelloApp {
public static void main(String[] args) {
long start = System.currentTimeMillis();
TioApplicationWrapper.run(HelloApp.class, args);
long end = System.currentTimeMillis();
System.out.println((end - start) + "ms");
}
}
```
#### 3. 启动热加载
- **通过程序参数**:在启动类的程序参数(Program arguments)中添加启动参数 `--mode=dev`。
- **通过配置文件**:在配置文件中添加相应的配置以启用开发模式。
`TioApplicationWrapper` 会自动判断是否启用热加载。判断逻辑为:如果 ClassLoader 的 URL 中包含 `/target/classes/`,则认为是开发环境,开启热加载;否则,认为是生产环境,不启用热加载。
#### 4. 测试热加载效果
- **在 Eclipse IDE 中**:只需保存文件即可触发热加载,实时查看效果。
- **在 IntelliJ IDEA 环境中**:需要在运行时手动编译(`Build` -> `Recompile`)文件才能看到效果。
#### 5. 日志示例
以下是启动和热加载过程中的日志示例:
```
2024-09-15 13:48:09.531 [main] INFO c.l.h.w.t.b.TioApplicationWrapper.runDev:75 - start hotswap watcher:Thread[HotSwapWatcher,10,main]
2024-09-15 13:48:09.540 [main] INFO c.l.h.w.t.b.TioApplicationWrapper.runDev:82 - new hotswap class loader:nexus.io.hotswap.classloader.HotSwapClassLoader@7cf10a6f
2024-09-15 13:48:09.578 [main] INFO c.l.t.u.e.EnvUtils.load:171 - app.env:null
2024-09-15 13:48:09.611 [main] INFO c.l.j.a.s.ComponentScanner.findClasses:72 - resource:file:/E:/code/java/project-litongjava/tio-boot-web-hello/target/classes/com/litongjava/tio/web/hello
2024-09-15 13:48:09.671 [main] INFO c.l.t.u.Threads.getTioExecutor:93 - new worker thread pool:nexus.io.tio.utils.thread.pool.SynThreadPoolExecutor@612679d6[Running, pool size = 1, active threads = 1, queued tasks = 0, completed tasks = 0]
2024-09-15 13:48:09.676 [main] INFO c.l.t.u.Threads.getGroupExecutor:51 - new group thread pool:java.util.concurrent.ThreadPoolExecutor@52f759d7[Running, pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 0]
2024-09-15 13:48:09.698 [main] INFO c.l.t.b.c.TioApplicationContext.run:278 - init:57(ms),scan class:17(ms),config:50(ms),server:13(ms),http route:16(ms)
2024-09-15 13:48:09.698 [main] INFO c.l.t.b.c.TioApplicationContext.printUrl:295 - port:80
http://localhost
363ms
2024-09-15 13:48:46.892 [pool-1-thread-1] INFO c.l.h.w.HotSwapWatcher.process:156 - watch event ENTRY_MODIFY,IndexController.class
2024-09-15 13:48:46.893 [pool-1-thread-1] INFO c.l.h.w.HotSwapWatcher.process:165 - restart server:nexus.io.hotswap.wrapper.tio.boot.TioBootRestartServer@126afc98
loading
2024-09-15 13:48:46.893 [pool-1-thread-1] INFO c.l.t.b.c.TioApplicationContext.close:317 - stop server
2024-09-15 13:48:46.894 [tio-group-2] INFO c.l.t.s.AcceptCompletionHandler.failed:132 - The server will be shut down and no new requests will be accepted:0.0.0.0:80
2024-09-15 13:48:46.895 [pool-1-thread-1] INFO c.l.t.u.Threads.close:177 - shutdown group thread pool:java.util.concurrent.ThreadPoolExecutor@52f759d7[Terminated, pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 7]
2024-09-15 13:48:46.896 [pool-1-thread-1] INFO c.l.t.u.Threads.close:188 - shutdown worker thread pool:nexus.io.tio.utils.thread.pool.SynThreadPoolExecutor@612679d6[Terminated, pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 2]
2024-09-15 13:48:46.896 [pool-1-thread-1] INFO c.l.t.s.TioServer.stop:141 - 0.0.0.0:80 stopped
2024-09-15 13:48:46.898 [pool-1-thread-1] INFO c.l.h.w.t.b.TioBootRestartServer.restart:33 - new classLoader:nexus.io.hotswap.classloader.HotSwapClassLoader@5ff60e5f
2024-09-15 13:48:46.904 [pool-1-thread-1] INFO c.l.t.u.e.EnvUtils.load:171 - app.env:null
2024-09-15 13:48:46.904 [pool-1-thread-1] INFO c.l.j.a.s.ComponentScanner.findClasses:72 - resource:file:/E:/code/java/project-litongjava/tio-boot-web-hello/target/classes/com/litongjava/tio/web/hello
2024-09-15 13:48:46.912 [pool-1-thread-1] INFO c.l.t.u.Threads.getTioExecutor:93 - new worker thread pool:nexus.io.tio.utils.thread.pool.SynThreadPoolExecutor@6bb1b24e[Running, pool size = 1, active threads = 1, queued tasks = 0, completed tasks = 0]
2024-09-15 13:48:46.913 [pool-1-thread-1] INFO c.l.t.u.Threads.getGroupExecutor:51 - new group thread pool:java.util.concurrent.ThreadPoolExecutor@6c0d059c[Running, pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 0]
2024-09-15 13:48:46.917 [pool-1-thread-1] INFO c.l.t.b.c.TioApplicationContext.run:278 - init:5(ms),scan class:3(ms),config:5(ms),server:1(ms),http route:4(ms)
2024-09-15 13:48:46.917 [pool-1-thread-1] INFO c.l.t.b.c.TioApplicationContext.printUrl:295 - port:80
http://localhost
Loading complete in 24 ms (^_^)
```
#### 6. 关闭文件监听(可选)
默认开启 class 文件监听,保存文件后自动重启并热加载。控制台也可以输入 `r` 后回车手动重启。
如果只希望手动重启,可以关闭文件监听:
```properties
hotswap.watch.file.enabled=false
```
说明:
1. 默认值是 `true`(等价于 `EnvUtils.getBoolean("hotswap.watch.file.enabled", true)`)。
2. 设置为 `false` 后,不再监听文件变化。
3. 设置为 `false` 后,控制台仍会提示并支持 `r + Enter` 手动重启。
Tio-boot 启动示例:
```java
import nexus.io.annotation.AComponentScan;
import nexus.io.diamond.boradcast.config.AdminAppConfig;
import nexus.io.hotswap.wrapper.tio.boot.TioApplicationWrapper;
import nexus.io.tio.utils.environment.EnvUtils;
@AComponentScan("nexus.io.diamond.boradcast.controller")
public class BoradcastAdminApp {
public static void main(String[] args) {
EnvUtils.load(args);
long start = System.currentTimeMillis();
AdminAppConfig config = new AdminAppConfig();
TioApplicationWrapper.run(BoradcastAdminApp.class, config, args);
long end = System.currentTimeMillis();
System.out.println((end - start) + "(ms)");
}
}
```
必须先执行 `EnvUtils.load(args)` 读取配置文件,否则加载不到 `hotswap.watch.file.enabled` 配置。
## 热加载的实现原理
热加载功能的实现依赖于以下几个关键组件:
### 1. 动态类加载
`hotswap-classloader` 通过自定义 `ClassLoader` 实现了在运行时动态加载和替换类的功能。当检测到类文件发生变化时,`hotswap-classloader` 会创建一个新的 `ClassLoader` 来加载更新后的类定义,并使用反射机制重新初始化应用上下文。这确保了新版本的类能够在不重启 JVM 的情况下被应用。
### 2. 文件监控
利用 Java NIO 的 `WatchService`实时监控类文件的变动。当检测到文件修改事件时,触发热加载流程,确保类的更新能够及时应用。
### 3. 自定义线程池
由于 AIO 的默认线程池不支持热加载,为避免线程池中的线程持有旧的 `ClassLoader` 导致类无法卸载的问题,`hotswap-classloader` 使用了自定义的线程池 `tio-group`。在热加载时,旧的线程池会被安全地关闭,新的线程池会随新的 `ClassLoader` 一起创建,确保类的正确加载和卸载。
### 4. 生产环境优化
在生产环境中,`TioApplicationWrapper` 会自动检测运行环境,避免加载热加载相关的功能,从而不引入额外的性能损耗。生产环境仍然使用 AIO 的默认线程池,确保应用的性能和稳定性。
## 更多使用说明
有关热加载的更多使用细节和高级配置,请参考官方文档:
[hotswap-classloader 官方文档](https://github.com/litongjava/hotswap-classloader)
### 部署注意事项
在将应用部署到生产环境时,需要注意以下事项,以确保应用的稳定性和性能:
- **禁用热加载**:在生产环境中,**不要**在程序参数中添加 `--mode=dev`。`TioApplicationWrapper` 会调用 `TioApplication` 启动项目,不会启用热加载功能。
- **性能考虑**:理论上,生产环境不会有额外的性能损耗,因为 `TioApplicationWrapper` 仅在启动时判断了运行模式和 classpath 路径。
## 增加对 fastjson2 的支持
在启用了 `TioApplicationWrapper` 后,部分开发者可能会遇到如下错误:
```
java.lang.ClassCastException: nexus.io.maxkb.vo.MaxKbApplicationNoReferencesSetting cannot be cast to nexus.io.maxkb.vo.MaxKbApplicationNoReferencesSetting
at com.alibaba.fastjson2.reader.ORG_9_5_MaxKbDatasetSettingVo.readObject(Unknown Source)
at com.alibaba.fastjson2.reader.ORG_8_18_MaxKbApplicationVo.readObject(Unknown Source)
at com.alibaba.fastjson2.JSON.parseObject(JSON.java:864)
at nexus.io.tio.utils.json.FastJson2.parse(FastJson2.java:42)
at nexus.io.tio.utils.json.MixedJson.parse(MixedJson.java:28)
at nexus.io.tio.utils.json.JsonUtils.parse(JsonUtils.java:20)
```
### 问题描述
上述 `ClassCastException` 异常是由于同一个类 `nexus.io.maxkb.vo.MaxKbApplicationNoReferencesSetting` 被不同的类加载器加载了多次。尽管类的全限定名相同,但如果由不同的类加载器加载,JVM 会将它们视为不同的类,从而导致类型转换失败。
### 原因分析
1. **类重复加载**:
启用了 `hotswap-classloader` 后,每次热加载都会创建一个新的 `ClassLoader` 来加载类定义。这样,同名的类在不同的 `ClassLoader` 下被加载,JVM 认为它们是不同的类,导致 `ClassCastException`。
2. **类加载器的影响**:
类加载器直接影响类的唯一性。Java 遵循双亲委派模型,即子类加载器在加载类时会首先委托给父类加载器。如果同一个类由不同的加载器加载,即使类名相同,JVM 也会将它们视为不同的类。
### 解决方案
为了避免上述问题,需要确保特定的类由系统类加载器(父类加载器)加载,而不是由 `hotswap-classloader` 重复加载。具体步骤如下:
#### 1. 指定系统类加载器加载特定类
在启动类中,配置 `HotSwapResolver` 以指定需要由系统类加载器加载的类的包前缀。例如:
```java
package nexus.io.maxkb;
import nexus.io.annotation.AComponentScan;
import nexus.io.hotswap.watcher.HotSwapResolver;
import nexus.io.hotswap.wrapper.tio.boot.TioApplicationWrapper;
@AComponentScan
public class MaxKbApp {
public static void main(String[] args) {
long start = System.currentTimeMillis();
// 指定需要由系统类加载器加载的类的包名前缀
HotSwapResolver.addSystemClassPrefix("nexus.io.maxkb.vo.");
TioApplicationWrapper.run(MaxKbApp.class, args);
// 如果不需要热加载功能,可以直接使用以下方式启动
// TioApplication.run(MaxKbApp.class, args);
long end = System.currentTimeMillis();
System.out.println((end - start) + "ms");
}
}
```
#### 2. 解释配置
- **`HotSwapResolver.addSystemClassPrefix("nexus.io.maxkb.vo.");`**:
该配置的作用是告知热加载器,对于包名以 `nexus.io.maxkb.vo.` 开头的类,不进行热加载,而是由系统类加载器加载。这样可以确保这些类只被加载一次,避免因重复加载导致的 `ClassCastException`。
### 总结
- **避免类重复加载**:确保关键类仅由一个类加载器加载,尤其是涉及类型转换和序列化的类,避免由多个类加载器加载同名类。
- **合理配置热加载器**:在使用热加载功能时,仔细配置哪些类需要热加载,哪些类不需要。对于不需要热加载的类,尤其是关键类,应该由系统类加载器加载。
- **理解类加载机制**:深入理解 Java 的类加载机制和双亲委派模型,有助于在开发过程中避免类似的问题。
通过以上方法,您可以解决 `ClassCastException` 问题,确保应用程序在启用热加载功能后正常运行。
## 总结
通过整合 `hotswap-classloader`,开发者可以在不重启应用的情况下实时更新代码,大大提高了开发效率。同时,得益于对生产环境的优化处理,热加载机制不会对线上应用的性能和稳定性造成影响。
**使用建议**:
- **在开发环境中**,如 Eclipse IDE,保持文件保存即可测试加载效果。
- **在 IntelliJ IDEA 环境中**,需要在运行时手动编译(`Build` -> `Recompile`)文件才能看到热加载效果。
正确配置和使用 `hotswap-classloader` 与 `tio-boot` 的整合,将为您的 Java 网络应用开发带来显著的效率提升和更好的开发体验。
## 参考资料
- [hotswap-classloader GitHub 仓库](https://github.com/litongjava/hotswap-classloader)
- [tio-boot 官方文档](https://github.com/litongjava/tio-boot)
---
# 自行编译 tio-boot
URL: https://tio-boot.com/zh/01_introduction/06.html
Source: zh/01_introduction/06.md
# 自行编译 tio-boot
[[toc]]
tio-boot 作为 TIO 生态系统中的核心组件,旨在简化 TIO 应用程序的引导过程。虽然 tio-boot 已经相对稳定,并且维护者保持每月发布一次的频率,但有时可能希望体验最新的功能和改进,此时手动编译安装是一个理想的选择。本文将详细介绍如何手动编译和安装 tio-boot 及其周边依赖项。
## 前言
TIO(Tencent IO)是一个高性能、异步的 Java 网络通信框架,广泛应用于构建高并发的网络应用。tio-boot 则基于 TIO 核心库,提供了更为简便的配置和管理方式,使得开发者能够更快速地搭建和部署 TIO 应用程序。
本文旨在指导用户通过手动编译的方式获取最新版本的 tio-boot,涵盖从克隆源码到构建安装的完整步骤,并对每个步骤提供必要的解释和说明。
## 安装前的准备
在开始之前,请确保的开发环境中已安装以下工具:
- **Git**:用于代码的版本控制和管理。
- **Java Development Kit (JDK)**:建议使用 JDK 8 及以上版本。
- **Apache Maven**:用于项目的构建和依赖管理。
可以通过以下命令检查是否已安装这些工具:
```shell
git --version
java -version
mvn -version
```
如果未安装,请根据的操作系统参考相应的安装文档进行安装。
## 安装 Java-Model
java-model 是各个项目的基础依赖
```shell
# 克隆 TIO 工具库源码
git clone https://github.com/litongjava/java-model.git
# 进入项目目录
cd java-model
# 使用 Maven 清理并安装,不运行测试,跳过 GPG 签名
mvn clean install -DskipTests -Dgpg.skip
```
## aio-sockeet
java-model 是各个项目的基础依赖
```shell
# 克隆 TIO 工具库源码
git clone https://github.com/litongjava/aio-socket.git
# 进入项目目录
cd aio-socket
# 使用 Maven 清理并安装,不运行测试,跳过 GPG 签名
mvn clean install -DskipTests -Dgpg.skip
```
## 安装 TIO 依赖项
**步骤:**
```shell
# 克隆 TIO 工具库源码
git clone https://github.com/litongjava/tio-boot.git
# 进入项目目录
cd tio-boot
# 使用 Maven 清理并安装,不运行测试,跳过 GPG 签名
mvn clean install -DskipTests -Dgpg.skip
```
**说明:**
- `git clone` 命令用于从 GitHub 仓库克隆 TIO 工具库的源码。
- `mvn clean install` 命令会清理之前的构建产物,并重新编译项目,生成 JAR 包并安装到本地 Maven 仓库。
- 参数 `-DskipTests` 表示在构建过程中跳过测试阶段,加快构建速度。
- 参数 `-Dgpg.skip` 表示跳过 GPG 签名,避免因未配置 GPG 而导致的构建失败。
## 安装其他库
除了 TIO 生态系统中的核心组件外,tio-boot 还依赖于一些周边工具和框架,以提供更全面的功能支持。以下是这些周边依赖的安装步骤。
### 1. jfinal-aop
**jfinal-aop** 是为 tio-boot 设计的 Bean 容器和 AOP(面向切面编程)框架,旨在简化依赖注入和横切关注点的管理。
**步骤:**
```shell
# 返回上一级目录
cd ..
# 克隆 jfinal-aop 源码
git clone https://github.com/litongjava/jfinal-aop.git
# 进入项目目录
cd jfinal-aop
# 使用 Maven 清理并安装,不运行测试,跳过 GPG 签名
mvn clean install -DskipTests -Dgpg.skip
```
**说明:**
jfinal-aop 提供了灵活的 AOP 功能,使开发者能够更方便地管理和应用横切逻辑,如日志记录、事务管理等。
### 2. java-db
**java-db** 是为 tio-boot 设计的数据库框架,提供了简洁高效的数据库操作接口,支持多种数据库类型。
**步骤:**
```shell
# 返回上一级目录
cd ..
# 克隆 java-db 源码
git clone https://github.com/litongjava/java-db.git
# 进入项目目录
cd java-db
# 使用 Maven 清理并安装,不运行测试,跳过 GPG 签名
mvn clean install -DskipTests -Dgpg.skip
```
**说明:**
java-db 框架封装了常见的数据库操作,简化了数据访问层的开发,支持事务管理、连接池等高级功能。
### 3. api-tble
**ApiTable** 是为 tio-boot 设计的自动化数据库 CRUD(创建、读取、更新、删除)框架,旨在快速生成和管理 API 接口。
**步骤:**
```shell
# 返回上一级目录
cd ..
# 克隆 ApiTable 源码
git clone https://github.com/litongjava/api-table.git
# 进入项目目录
cd api-table
# 使用 Maven 清理并安装,不运行测试,跳过 GPG 签名
mvn clean install -DskipTests -Dgpg.skip
```
**说明:**
ApiTable 通过自动生成 CRUD 接口,大幅提高了开发效率,减少了重复劳动,使开发者能够专注于业务逻辑的实现。
### 4. java-openai
**java-opneai** 是为 tio-boot 设计的调用大模型接口的框架。
**步骤:**
```shell
# 克隆 java-opneai 源码
git clone https://github.com/litongjava/java-openai.git
# 进入项目目录
cd java-openai
# 使用 Maven 清理并安装,不运行测试,跳过 GPG 签名
mvn clean install -DskipTests -Dgpg.skip
```
**说明:**
ApiTable 通过自动生成 CRUD 接口,大幅提高了开发效率,减少了重复劳动,使开发者能够专注于业务逻辑的实现。
### 5.tio-boot-admin
tio-boot-admin 是后台管理系统的基础框架
```sh
git clone https://github.com/litongjava/tio-boot-admin.git
cd /data/apps/tio-boot-admin
git pull
mvn clean install -DskipTests -Dgpg.skip=true -q
```
## 构建脚本
github
```shell
git clone https://github.com/litongjava/jfinal-aop.git
git clone https://github.com/litongjava/java-db.git
git clone https://github.com/litongjava/api-table.git
git clone https://github.com/litongjava/java-openai.git
git clone https://github.com/litongjava/tio-boot-admin.git
```
gitee
```shell
git clone https://github.com/litongjava/jfinal-aop.git
git clone https://github.com/litongjava/java-db.git
git clone https://github.com/litongjava/api-table.git
git clone https://github.com/litongjava/java-openai.git
git clone https://github.com/litongjava/tio-boot-admin.git
```
```shell
cd /data/apps/jfinal-aop
git pull
mvn clean install -DskipTests -Dgpg.skip=true -q
cd /data/apps/java-db
git pull
mvn clean install -DskipTests -Dgpg.skip=true -q
cd /data/apps/api-table
git pull
mvn clean install -DskipTests -Dgpg.skip=true -q
cd /data/apps/java-openai
git pull
mvn clean install -DskipTests -Dgpg.skip=true -q
cd /data/apps/tio-boot-admin
git pull
mvn clean install -DskipTests -Dgpg.skip=true -q
```
## 安装服务库
```shell
git clone https://github.com/litongjava/tio-mail-wing.git
git clone https://gitee.com/ppnt/java-maxkb.git
```
```shell
cd /data/apps/tio-mail-wing
git pull
mvn clean install -DskipTests -Dgpg.skip=true -q
cd /data/apps/java-maxkb
git pull
mvn clean install -DskipTests -Dgpg.skip=true -q
```
## 常见错误
### 1. Maven 构建失败
**问题描述:** 在执行 `mvn clean install` 命令时,出现构建失败的情况,可能伴随错误信息。
**解决方法:**
- **检查网络连接:** Maven 需要从中央仓库下载依赖,确保网络连接正常。
- **更新 Maven 配置:** 如果使用了私有仓库或代理,确保 `settings.xml` 配置正确。
- **清理本地仓库:** 有时本地仓库中的损坏文件会导致构建失败,可以尝试删除本地仓库中的相关依赖,重新构建。
```shell
rm -rf ~/.m2/repository/com/example/dependency
```
- **检查 JDK 版本:** 确保使用的 JDK 版本与项目要求匹配。
### 2. Git 克隆失败
**问题描述:** 在执行 `git clone` 命令时,出现网络错误或权限错误。
**解决方法:**
- **检查网络连接:** 确保可以访问 GitHub,必要时配置代理。
- **验证仓库 URL:** 确保仓库 URL 正确无误。
- **检查权限设置:** 如果仓库为私有,确保具有访问权限,并配置 SSH 密钥或使用 HTTPS 方式进行认证。
### 3. 缺少依赖项
**问题描述:** 构建过程中提示缺少某些依赖项,导致构建失败。
**解决方法:**
- **确保依赖项已正确安装:** 按照本文的顺序,先构建并安装所有 TIO 依赖项。
- **检查本地 Maven 仓库:** 确认依赖项的 JAR 包已正确安装到本地 Maven 仓库 (`~/.m2/repository`)。
- **强制更新 Maven 依赖:** 使用 `-U` 参数强制更新依赖。
```shell
mvn clean install -U -DskipTests -Dgpg.skip
```
## 结语
通过上述步骤,已经成功手动编译和安装了 tio-boot 及其周边依赖项。手动编译的方式不仅能够让体验到最新的功能和改进,还能加深对 TIO 生态系统各个组件的理解。接下来,可以基于 tio-boot 开发高性能的网络应用,充分发挥其强大的功能和灵活性。
如在安装和使用过程中遇到问题,建议参考各项目的[官方文档](https://github.com/litongjava)或在社区中寻求帮助。
祝在 tio-boot 的使用中取得成功!
---
# 最新版本
URL: https://tio-boot.com/zh/01_introduction/07.html
Source: zh/01_introduction/07.md
# 最新版本
最新版本如下
```xml
UTF-8
1.8
${java.version}
${java.version}
23.1.1
1.18.44
2.0.52
1.2.9
1.2.5
2.1.8
1.4.0
1.6.0
1.3.7
web-hello
nexus.io.tio.web.hello.HelloApp
```
```xml
nexus.io
java-model
${java-model.version}
nexus.io
tio-boot
${tio-boot.version}
nexus.io
hotswap-classloader
${hotswap-classloader.version}
nexus.io
java-db
${java-db.version}
nexus.io
api-table
${java-db.version}
nexus.io
jfinal-aop
${jfinal-aop.version}
nexus.io
java-openai
${java-openai.version}
org.projectlombok
lombok
${lombok-version}
true
provided
com.alibaba.fastjson2
fastjson2
${fastjson2.version}
```
---
# 开发规范
URL: https://tio-boot.com/zh/01_introduction/08.html
Source: zh/01_introduction/08.md
# 开发规范
## 模版引擎命名规范
tio-boot 支持多种模版引擎,以 enjoy 模版引擎为例,介绍命名规范
- enjoy-templates: enjoy 模版目录
- enjoy-sql :enjoy sql 目录
- sql-templates :SqlTemplates 目录
## 接口命名规范
### 1. 接口概述
本系统提供三个不同权限级别的页面查询接口,分别适用于不同的访问场景:
1. **Private Page** (`/private/page`): 需要用户认证,过滤 `user_id`。
2. **Public Page** (`/public/page`): 公开访问,不过滤 `user_id`,无需认证。
3. **Admin Page** (`/page`): 需要管理员认证,不过滤 `user_id`。
### 2. 接口详细说明
#### 2.1 Private Page
- **路径**: `/private/page`
- **方法**: `GET`
- **描述**: 查询当前认证用户的页面,返回结果过滤 `user_id`。
- **权限**: 需要用户认证(例如,通过 JWT Token)。
- **请求头**:
- `Authorization: Bearer `
#### 2.2 Public Page
- **路径**: `/public/page`
- **方法**: `GET`
- **描述**: 查询所有公开的页面,不过滤 `user_id`,无需认证。
- **权限**: 公开访问,无需认证。
#### 2.3 Admin Page
- **路径**: `/page`
- **方法**: `GET`
- **描述**: 查询所有页面,不过滤 `user_id`,需要管理员认证。
- **权限**: 需要管理员认证(例如,通过管理员 Token)。
- **请求头**:
- `Authorization: Bearer `
## 数据表
### 数据规范
创建数据表推荐包含下面的字段
```sql
id BIGINT NOT NULL PRIMARY KEY,
remark VARCHAR(500),
creator VARCHAR(64) DEFAULT '',
create_time TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
updater VARCHAR(64) DEFAULT '',
update_time TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted SMALLINT NOT NULL DEFAULT 0,
tenant_id BIGINT NOT NULL DEFAULT 0
```
---
# 01_introduction
URL: https://tio-boot.com/zh/01_introduction/
Source: zh/01_introduction/index.md
# 01_introduction
本目录包含 8 个文件和 0 个子目录,下列摘要基于文件名、一级标题或资源类型整理。
## 文件清单
- [readme.md](): 当前目录的导航与文件摘要。
- [01.md](): 文档《tio-boot:新一代高性能 Java Web 开发框架》。
- [02.md](): 文档《tio-boot 入门示例》。
- [03.md](): 文档《Tio-Boot 配置 : 现代化的配置方案》。
- [05.md](): 文档《`tio-boot` 整合 `hotswap-classloader` 实现热加载》。
- [06.md](): 文档《自行编译 tio-boot》。
- [07.md](): 文档《最新版本》。
- [08.md](): 文档《开发规范》。
完成 Demo 后继续阅读[部署]()与[日志](),完整目录见[文档阅读导航]()。
---
# 使用 Maven Profile 实现分环境打包 tio-boot 项目
URL: https://tio-boot.com/zh/02_deployment/01.html
Source: zh/02_deployment/01.md
# 使用 Maven Profile 实现分环境打包 tio-boot 项目
在软件开发过程中,不同的运行环境(如开发环境、生产环境、测试环境等)通常需要不同的配置和依赖。Maven 提供了 `profiles` 功能,允许开发者根据不同的需求切换构建配置,从而实现分环境打包。本指南将详细介绍如何通过 Maven Profile 配置,实现针对开发、生产、二进制包和 GraalVM 原生镜像的分环境构建。
## 目录
[[toc]]
## Maven Profiles 概述
Maven Profiles 允许在构建过程中动态地修改项目的配置。通过定义不同的 profiles,可以为不同的环境设置不同的依赖、插件配置、构建参数等。这种机制极大地提升了项目构建的灵活性和可维护性。
## 配置 Maven Profiles
以下是一个示例 `pom.xml` 中的 `profiles` 配置,其中定义了四个不同的构建环境:开发环境、生产环境、Assembly 和 Native。
```xml
development
true
org.springframework.boot
spring-boot-maven-plugin
2.7.18
true
${main.class}
org.projectlombok
--mode=dev
production
org.springframework.boot
spring-boot-maven-plugin
2.7.18
${main.class}
org.projectlombok
repackage
assembly
org.apache.maven.plugins
maven-assembly-plugin
3.3.0
false
assembly-${assembly}.xml
make-assembly
package
single
src/main/resources
**/*.*
native
org.slf4j
slf4j-jdk14
1.7.31
org.graalvm.sdk
graal-sdk
${graalvm.version}
provided
${project.artifactId}
org.graalvm.nativeimage
native-image-maven-plugin
21.2.0
native-image
package
false
${project.artifactId}
${main.class}
-H:+RemoveSaturatedTypeFlows
--allow-incomplete-classpath
--no-fallback
```
### 1. 开发环境 Profile
```xml
development
true
ch.qos.logback
logback-classic
1.3.3
```
- **说明**:此 Profile 用于开发环境。通过 `true`,该 Profile 将作为默认激活的配置,除非在构建时显式指定其他 Profile。
- **依赖**:引入了 `logback-classic` 日志框架,方便在开发过程中进行日志记录和调试。
### 2. 生产环境 Profile
```xml
production
ch.qos.logback
logback-classic
1.3.3
org.springframework.boot
spring-boot-maven-plugin
2.7.18
${main.class}
org.projectlombok
repackage
```
- **说明**:此 Profile 专为生产环境设计,包含生产环境所需的依赖和插件配置。
- **依赖**:同样引入 `logback-classic`,确保生产环境的日志记录。
- **构建插件**:配置了 `spring-boot-maven-plugin`,通过 `repackage` 目标,将应用打包为可执行的 Spring Boot JAR 文件。配置中指定了主类 (`${main.class}`) 并排除了 `org.projectlombok` 组的依赖,以减少最终包的体积。
### 3. Assembly Profile
```xml
assembly
ch.qos.logback
logback-classic
1.3.3
org.apache.maven.plugins
maven-jar-plugin
3.2.0
org.apache.maven.plugins
maven-assembly-plugin
3.1.1
${main.class}
jar-with-dependencies
false
make-assembly
package
single
```
- **说明**:此 Profile 用于生成包含所有依赖的可执行 JAR 包,适用于需要将应用打包为单一可分发文件的场景。
- **依赖**:依然引入 `logback-classic`。
- **构建插件**:
- `maven-jar-plugin`:负责标准的 JAR 打包过程。
- `maven-assembly-plugin`:通过 `jar-with-dependencies` 描述符,将所有依赖打包到一个 JAR 文件中。配置中指定了主类,并设置 `appendAssemblyId` 为 `false`,以避免在最终文件名中添加附加标识。
### 4. Native Profile
```xml
native
org.slf4j
slf4j-jdk14
1.7.31
org.graalvm.sdk
graal-sdk
${graalvm.version}
provided
${project.artifactId}
org.graalvm.nativeimage
native-image-maven-plugin
21.2.0
native-image
package
false
${project.artifactId}
${main.class}
-H:+RemoveSaturatedTypeFlows
--allow-incomplete-classpath
--no-fallback
```
- **说明**:此 Profile 用于生成 GraalVM 原生镜像,提高应用的启动速度和性能,适用于生产环境中对性能要求较高的场景。
- **依赖**:
- `slf4j-jdk14`:使用 JDK 自带的日志框架,适配 GraalVM 环境。
- `graal-sdk`:GraalVM 的开发工具包,配置为 `provided` 作用域,表示在编译时需要,但在运行时由 GraalVM 提供。
- **构建插件**:
- `native-image-maven-plugin`:负责将 Java 应用编译为原生镜像。配置中指定了镜像名称、主类以及构建参数,以优化生成的原生镜像。
## 构建命令
根据不同的 Profile,可以使用以下 Maven 命令进行构建:
### 为开发环境构建
```bash
mvn clean package -DskipTests -Pdevelopment
```
- **说明**:清理项目并打包,仅激活 `development` Profile。由于 `development` Profile 被设置为默认激活,因此即使不指定 `-Pdevelopment`,也会使用该配置。
### 为生产环境构建
```bash
mvn clean package -DskipTests -Dgpg.skip -Pproduction
```
- **说明**:清理项目并打包,激活 `production` Profile。该配置会生成适用于生产环境的可执行 JAR 文件,并排除不必要的依赖以优化包的体积。
### 为生产环境构建二进制包
```bash
mvn clean package -DskipTests -Pnative
```
- **说明**:清理项目并打包,激活 `native` Profile。此命令将生成 GraalVM 原生镜像,适用于需要高性能和快速启动的生产环境。
## 详细解释与说明
### 1. Profile 激活机制
- **默认激活**:`development` Profile 通过 `true` 设置为默认激活。这意味着在未指定 Profile 时,Maven 会自动使用该配置。
- **显式激活**:通过 `-P` 参数可以显式指定要激活的 Profile,如 `-Pproduction` 或 `-Pnative`。这会覆盖默认激活的 Profile。
### 2. 依赖管理
不同的 Profile 可以引入不同的依赖,以满足各环境的需求。例如:
- **开发环境**:使用 `logback-classic` 进行日志记录,便于调试。
- **Native Profile**:使用 `slf4j-jdk14` 适配 GraalVM,并引入 `graal-sdk` 进行原生镜像构建。
### 3. 构建插件配置
- **Spring Boot Maven Plugin**(生产环境):
- 通过 `repackage` 目标,将应用打包为可执行的 Spring Boot JAR 文件。
- 配置 `mainClass` 指定应用的主类。
- 排除 `org.projectlombok` 组的依赖,减少最终包的体积。
- **Maven Assembly Plugin**(Assembly Profile):
- 使用 `jar-with-dependencies` 描述符,将所有依赖打包到一个 JAR 文件中,便于分发和部署。
- 配置 `mainClass`,确保可执行 JAR 文件正确运行。
- **Native Image Maven Plugin**(Native Profile):
- 将 Java 应用编译为 GraalVM 原生镜像,提高启动速度和性能。
- 配置 `imageName` 和 `mainClass`,以及优化构建参数,如 `--no-fallback` 以减少镜像体积。
### 4. 变量与属性
- **`${main.class}`**:需在 `pom.xml` 的 `` 中定义,指定应用的主类。例如:
```xml
com.example.MainApplication
21.3.0
```
- **`${project.artifactId}`**:Maven 内置变量,表示项目的 artifactId,用于动态命名生成的文件。
## 总结
通过 Maven Profiles,可以灵活地管理不同环境下的构建配置,简化了项目的构建和部署流程。本指南介绍了如何配置和使用多个 Profile,包括开发、生产、Assembly 和 Native 环境。根据项目的具体需求,可以进一步扩展和定制这些 Profile,以实现更高效的构建过程。
**推荐实践**:
- **统一配置**:将常用的配置和依赖抽取到公共部分,避免在各个 Profile 中重复定义。
- **环境变量**:结合环境变量或 Maven 的 `settings.xml`,实现更动态的 Profile 激活和配置管理。
- **测试与验证**:在引入新的 Profile 时,确保在各环境中进行充分的测试,验证构建结果的正确性和稳定性。
通过合理使用 Maven Profiles,开发者可以显著提升项目的可维护性和构建效率,确保在不同环境下应用的稳定运行。
---
# Maven 项目配置详解:依赖与 Profiles 配置
URL: https://tio-boot.com/zh/02_deployment/02.html
Source: zh/02_deployment/02.md
# Maven 项目配置详解:依赖与 Profiles 配置
本文档详细介绍了一个 Maven 项目的 `pom.xml` 文件配置,包括项目属性、依赖管理以及不同环境下的 Profiles 配置。通过本文档,您将了解如何配置项目的构建参数、依赖库以及针对开发、生产和其他特定环境的定制化设置。
## 目录
[[toc]]
---
## 项目属性配置
在 `pom.xml` 中,`` 标签用于定义项目的全局属性,这些属性可以在整个项目的构建过程中复用,简化配置管理。以下是各属性的详细说明:
```xml
UTF-8
1.8
${java.version}
${java.version}
23.1.1
2.1.8
1.18.44
1.2.9
web-hello
nexus.io.tio.web.hello.HelloApp
```
### 属性说明
- **`project.build.sourceEncoding`**: 设置项目源代码的编码格式为 `UTF-8`,确保在不同环境下的编码一致性。
- **`java.version`**: 定义项目使用的 Java 版本,此处设置为 `1.8`。
- **`maven.compiler.source`** 和 **`maven.compiler.target`**: 指定 Maven 编译器插件使用的 Java 源代码和目标字节码版本,均引用 `java.version` 属性。
- **`graalvm.version`**: 定义 GraalVM 的版本号,为 `23.1.1`。
- **`tio-boot.version`**: 集中管理 TIO Boot 依赖,按项目选用的依赖填写。
- **`lombok-version`**: 集中管理 Lombok 依赖,用于简化 Java 代码中的常见任务。
- **`hotswap-classloader.version`**: 集中管理 Hotswap Classloader 依赖,支持类的热替换功能。
- **`final.name`**: 指定最终构建产物的名称为 `web-hello`。
- **`main.class`**: 定义项目的主类全路径为 `nexus.io.tio.web.hello.HelloApp`。
## 依赖管理
基础库的编译环境与运行验证步骤见 [使用 JDK 8 构建基础库与验证运行环境]()。
项目的依赖库在 `` 标签中进行管理,确保项目在构建和运行时所需的所有库都被正确引入。
```xml
nexus.io
tio-boot
${tio-boot.version}
org.projectlombok
lombok
${lombok-version}
true
provided
nexus.io
hotswap-classloader
${hotswap-classloader.version}
junit
junit
4.12
test
```
### 依赖说明
- **`tio-boot`**: TIO Boot 库,用于简化 TIO 框架的启动和配置。
- **`lombok`**: Lombok 库,通过注解自动生成常用代码(如 getter/setter),提高开发效率。标记为 `optional` 和 `provided`,表示该依赖在编译时需要,但在运行时由外部提供。
- **`hotswap-classloader`**: 支持类的热替换功能,便于在运行时动态更新类定义,无需重启应用。
- **`junit`**: 用于单元测试的 JUnit 框架,作用域为 `test`,仅在测试阶段引入。
## Profiles 配置
Maven Profiles 允许为不同的构建环境定义特定的配置。本文档中的 `pom.xml` 文件定义了四个 Profiles:开发环境、生产环境、自定义环境(assembly)以及 GraalVM 环境。
```xml
development
true
ch.qos.logback
logback-classic
1.3.3
production
ch.qos.logback
logback-classic
1.3.3
org.springframework.boot
spring-boot-maven-plugin
2.7.18
${main.class}
org.projectlombok
repackage
assembly
ch.qos.logback
logback-classic
1.3.3
org.apache.maven.plugins
maven-jar-plugin
3.2.0
org.apache.maven.plugins
maven-assembly-plugin
3.1.1
${main.class}
jar-with-dependencies
false
make-assembly
package
single
native
org.slf4j
slf4j-jdk14
1.7.31
org.graalvm.sdk
graal-sdk
${graalvm.version}
provided
${final.name}
org.graalvm.nativeimage
native-image-maven-plugin
21.2.0
native-image
package
false
${final.name}
${main.class}
-H:+RemoveSaturatedTypeFlows
--allow-incomplete-classpath
--no-fallback
```
### 各 Profile 详解
#### 开发环境 (development)
- **Profile ID**: `development`
- **激活方式**: 默认激活 (`activeByDefault=true`)
- **依赖配置**:
- 引入 `logback-classic` 依赖,用于日志管理,版本为 `1.2.3`。
开发环境下,默认启用该 Profile,无需额外指定。`logback-classic` 提供了强大的日志记录功能,便于开发过程中调试和监控。
#### 生产环境 (production)
- **Profile ID**: `production`
- **依赖配置**:
- 同样引入 `logback-classic` 依赖,用于生产环境的日志管理。
- **构建配置**:
- 使用 `spring-boot-maven-plugin` 插件,版本为 `2.7.4`,用于打包 Spring Boot 应用。
- 配置中指定主类 (`mainClass`) 和排除 `org.projectlombok` 组的依赖。
- 设置执行目标为 `repackage`,确保生成可执行的 Spring Boot Jar 包。
生产环境下,该 Profile 需要显式激活。通过 `spring-boot-maven-plugin`,可以轻松构建和部署 Spring Boot 应用。
#### 自定义环境 (assembly)
- **Profile ID**: `assembly`
- **依赖配置**:
- 引入 `logback-classic` 依赖,与其他环境一致。
- **构建配置**:
- 使用 `maven-jar-plugin` 和 `maven-assembly-plugin` 插件,分别用于生成 Jar 包和包含所有依赖的可执行 Jar 包。
- `maven-assembly-plugin` 配置中,指定主类和依赖打包方式 (`jar-with-dependencies`),并设置 `appendAssemblyId` 为 `false`,以简化最终文件名。
- 在 `package` 阶段执行 `single` 目标,生成包含所有依赖的 Jar 包。
自定义环境适用于需要特定打包方式的场景,如分发包含所有依赖的单一 Jar 文件。
#### GraalVM 环境 (native)
- **Profile ID**: `native`
- **依赖配置**:
- 引入 `slf4j-jdk14` 作为日志实现,版本为 `1.7.31`,适配 GraalVM 环境的日志需求。
- 引入 `graal-sdk` 依赖,版本引用 `graalvm.version`,作用域为 `provided`,表示在运行时由 GraalVM 提供。
- **构建配置**:
- 设置最终文件名为 `web-hello`(由 `final.name` 属性定义)。
- 使用 `native-image-maven-plugin` 插件,版本为 `21.2.0`,用于生成 GraalVM 的本地映像。
- 插件配置中,指定生成的映像名称 (`imageName`)、主类 (`mainClass`) 以及构建参数 (`buildArgs`)。
- 在 `package` 阶段执行 `native-image` 目标,生成原生可执行文件。
GraalVM Profile 适用于需要生成高性能原生可执行文件的场景,通过 Ahead-Of-Time (AOT) 编译优化应用的启动时间和资源占用。
## 总结
本文档通过详细解析 `pom.xml` 文件中的项目属性、依赖管理以及多环境 Profiles 配置,帮助您理解如何在 Maven 项目中进行灵活的构建和依赖管理。根据不同的部署环境(开发、生产、自定义、GraalVM),合理配置 Profiles 可以显著提升项目的可维护性和部署效率。希望本文档能够为您的 Maven 项目配置提供有价值的参考。
---
# tio-boot 打包成 FatJar
URL: https://tio-boot.com/zh/02_deployment/03.html
Source: zh/02_deployment/03.md
# tio-boot 打包成 FatJar
在现代微服务和云原生应用的开发中,快速构建、部署和启动应用显得尤为重要。本文将介绍如何使用 `tio-boot` 将 Java 应用打包成轻量级的 FastJar,并在不同操作系统上进行启动和配置。
[[toc]]
## 1. 项目打包
首先,确保已经正确配置了 Java 环境变量。以 Windows 为例,设置 `JAVA_HOME` 并使用 Maven 进行项目打包:
```java
set JAVA_HOME=D:\java\jdk-21.0.6
mvn clean package -DskipTests -Dgpg.skip -Pproduction
```
上述命令执行了以下操作:
- **设置 Java 环境变量**:确保 Maven 使用正确的 JDK 版本进行编译和打包。
- **清理项目**:`mvn clean` 命令会删除之前的编译产物,确保打包过程的干净整洁。
- **跳过测试**:`-DskipTests` 参数跳过单元测试,加快打包速度,适用于生产环境构建。
- **使用生产环境配置**:`-Pproduction` 指定使用生产环境的 Maven 配置文件。
执行完成后,项目会被打包成一个可执行的 JAR 文件,位于 `target` 目录下,文件名类似于 `tio-boot-web-hello-0.0.1-SNAPSHOT.jar`,大小约为 8.28MB。
## 2. 启动应用
打包完成后,可以在不同操作系统上启动应用。以下是 Windows 和 Linux 下的启动命令:
### Windows 启动
在 Windows 环境下,使用以下命令启动应用:
```java
java -jar target\tio-boot-web-hello-0.0.1-SNAPSHOT.jar
```
### Linux 启动
在 Linux 环境下,启动命令稍有不同,可以通过指定环境变量来加载不同的配置文件:
```java
java -jar tio-boot-web-hello-0.0.1-SNAPSHOT.jar --app.env=prod
```
### 配置环境变量
通过指定 `app.env` 参数,可以自动读取对应的配置文件:
- **指定环境**:例如 `--app.env=prod` 会加载 `app-prod.properties` 中的配置。
- **默认环境**:如果不指定 `app.env`,系统将默认加载 `app.properties`。
这种方式使得在不同环境下运行应用变得更加灵活和便捷。
## 3. 测试应用
应用启动后,可以使用 `curl` 命令进行简单的测试请求,验证应用是否正常运行:
```bash
curl http://localhost/
```
如果应用成功启动并运行,以上命令将返回预期的响应内容。
## 4. 性能表现
经过打包后的 FastJar 文件 `tio-boot-web-hello-0.0.1-SNAPSHOT.jar` 具有以下优异的性能表现:
- **文件大小**:仅 8.28MB,轻量级,适合快速部署和传输。
- **启动时间**:启动时间不到 1 秒,确保服务能够迅速响应请求。
- **内存占用**:在 Windows 环境下,应用启动后仅占用约 70MB 内存,资源消耗低,适合在资源受限的环境中运行。
## 使用build工具
.build.txt
```
[win.env]
set JAVA_HOME=D:\java\jdk1.8.0_121
[win.build]
mvn clean package -DskipTests -Dgpg.skip -Pproduction
```
## 总结
通过上述步骤,您可以轻松地将 `tio-boot` 应用打包成 FastJar,并在不同操作系统上进行高效启动和运行。轻量级的 FastJar 不仅缩短了启动时间,减少了内存占用,还提高了应用的可移植性和部署效率,是现代 Java 应用开发中的一大利器。
希望本文对您在使用 `tio-boot` 进行应用打包和部署有所帮助。如有更多问题,欢迎在评论区交流讨论。
---
# 使用 GraalVM 构建 tio-boot Native 程序
URL: https://tio-boot.com/zh/02_deployment/04.html
Source: zh/02_deployment/04.md
# 使用 GraalVM 构建 tio-boot Native 程序
[toc]
本文将介绍如何使用 GraalVM 构建基于 tio-boot 的原生程序。这对于希望构建快速高效的 Java 服务器应用程序的开发人员尤其有用。我们将从安装必要的依赖开始,逐步构建一个可以独立于 JVM 运行的本地二进制镜像。
代码已提交至 [GitHub 仓库](https://github.com/litongjava/tio-boot-http-request-handler-demo)。
## 编写代码
### `pom.xml` 配置
首先,配置 Maven 项目的 `pom.xml` 文件,指定项目的依赖和构建配置。
```xml
UTF-8
21
${java.version}
${java.version}
23.1.1
2.1.8
web-hello
nexus.io.tio.web.hello.HelloApp
nexus.io
tio-boot
${tio-boot.version}
junit
junit
4.12
test
development
true
ch.qos.logback
logback-classic
1.3.3
production
ch.qos.logback
logback-classic
1.3.3
org.springframework.boot
spring-boot-maven-plugin
2.7.18
${main.class}
org.projectlombok
repackage
assembly
ch.qos.logback
logback-classic
1.3.3
org.apache.maven.plugins
maven-jar-plugin
3.2.0
org.apache.maven.plugins
maven-assembly-plugin
3.1.1
${main.class}
jar-with-dependencies
false
make-assembly
package
single
native
org.slf4j
slf4j-jdk14
1.7.31
org.graalvm.sdk
graal-sdk
${graalvm.version}
provided
${final.name}
org.graalvm.nativeimage
native-image-maven-plugin
21.2.0
native-image
package
false
${final.name}
${main.class}
-H:+RemoveSaturatedTypeFlows
--allow-incomplete-classpath
--no-fallback
```
### 处理器代码
创建一个请求处理器,用于处理 `/hello` 和 `/hi` 两个接口。
```java
package nexus.io.tio.web.hello.handler;
import nexus.io.tio.http.common.HttpRequest;
import nexus.io.tio.http.common.HttpResponse;
import nexus.io.tio.http.server.util.Resps;
public class HelloRequestHandler {
public HttpResponse hello(HttpRequest httpRequest) {
return Resps.txt(httpRequest, "hello");
}
public HttpResponse hi(HttpRequest httpRequest) {
return Resps.txt(httpRequest, "hi");
}
}
```
### 配置类
配置 HTTP 请求路由,将特定路径映射到对应的处理方法。
```java
package nexus.io.tio.web.hello.config;
import nexus.io.tio.boot.server.TioBootServer;
import nexus.io.tio.http.server.router.HttpRequestRouter;
import nexus.io.tio.web.hello.handler.HelloRequestHandler;
public class HttpRequestHandlerConfig {
public void config() {
HttpRequestRouter router = TioBootServer.me().getRequestRouter();
// 实例化处理器
HelloRequestHandler handler = new HelloRequestHandler();
// 添加路由
router.add("/hi", handler::hi);
router.add("/hello", handler::hello);
}
}
```
```java
package nexus.io.tio.web.hello.config;
import nexus.io.context.BootConfiguration;
public class AppConfig implements BootConfiguration {
@Override
public void config() {
new HttpRequestHandlerConfig().config();
}
}
```
### 启动类
定义应用程序的入口点,启动 tio-boot 服务并记录启动时间。
```java
package nexus.io.tio.web.hello;
import nexus.io.tio.boot.TioApplication;
import nexus.io.tio.web.hello.config.AppConfig;
public class HelloApp {
public static void main(String[] args) {
long start = System.currentTimeMillis();
TioApplication.run(HelloApp.class, new AppConfig(), args);
long end = System.currentTimeMillis();
System.out.println((end - start) + "ms");
}
}
```
上述代码已提交至 [GitHub 仓库](https://github.com/litongjava/tio-boot-http-request-handler-demo)。
## 安装 GraalVM
GraalVM 通过将代码提前编译成本地可执行文件来提升 Java 应用的性能。
### 1. 下载并解压 GraalVM
首先,下载最新版本的 GraalVM 并将其解压到指定目录。
```shell
wget https://download.oracle.com/graalvm/21/latest/graalvm-jdk-21_linux-x64_bin.tar.gz
mkdir -p ~/program/
tar -xf graalvm-jdk-21_linux-x64_bin.tar.gz -C ~/program/
```
### 2. 设置环境变量
更新系统的环境变量以包含 GraalVM 的路径,使得可以在全局使用 GraalVM 的 `java` 及其他命令行工具。
```shell
export JAVA_HOME=~/program/graalvm-jdk-21.0.10+8.1
export GRAALVM_HOME=~/program/graalvm-jdk-21.0.10+8.1
export PATH=$JAVA_HOME/bin:$PATH
```
## 安装 Maven
Apache Maven 是 Java 项目的主要构建自动化工具。
### 1. 下载并解压 Maven
下载并解压 Maven 至本地环境。
```shell
wget https://dlcdn.apache.org/maven/maven-3/3.8.8/binaries/apache-maven-3.8.8-bin.zip
unzip apache-maven-3.8.8-bin.zip -d ~/program/
```
### 2. 设置环境变量
确保 Maven 的 `bin` 目录在系统的 PATH 中,以便在任何位置使用 Maven 命令。
```shell
export MVN_HOME=~/program/apache-maven-3.8.8/
export PATH=$MVN_HOME/bin:$PATH
```
## 构建应用程序
所有工具和依赖配置完成后,接下来编译并运行示例应用程序。
### 1. 克隆示例应用程序
克隆内置 HTTP 请求处理器的 TIO-Boot 示例应用程序。
```shell
git clone https://github.com/litongjava/tio-boot-http-request-handler-demo.git
```
### 2. 构建 JAR 文件(可选)
将 Java 应用程序编译成 JAR 文件。如果您打算直接构建本地镜像,此步骤可选。
```shell
cd tio-boot-http-request-handler-demo
mvn clean package -DskipTests -Dgpg.skip -Pproduction
```
### 3. 构建本地二进制镜像
将应用程序编译成本地可执行文件,相较于在 JVM 上运行,可以减少启动时间和资源消耗。
```shell
mvn clean package -DskipTests -Pnative
```
## 完整构建日志
以下是构建过程的完整日志示例:
```
Welcome to Ubuntu 20.04.6 LTS (GNU/Linux 5.15.0-105-generic x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/advantage
* Introducing Expanded Security Maintenance for Applications.
Receive updates to over 25,000 software packages with your
Ubuntu Pro subscription. Free for personal use.
https://ubuntu.com/pro
Expanded Security Maintenance for Applications is not enabled.
80 updates can be applied immediately.
To see these additional updates run: apt list --upgradable
29 additional security updates can be applied with ESM Apps.
Learn more about enabling ESM Apps service at https://ubuntu.com/esm
New release '22.04.3 LTS' available.
Run 'do-release-upgrade' to upgrade to it.
Your Hardware Enablement Stack (HWE) is supported until April 2025.
Last login: Mon May 6 05:50:37 2024 from 192.168.3.8
root@ping-Inspiron-3458:~# export JAVA_HOME=~/program/graalvm-jdk-21.0.10+8.1
root@ping-Inspiron-3458:~# export GRAALVM_HOME=~/program/graalvm-jdk-21.0.10+8.1
root@ping-Inspiron-3458:~# export PATH=$JAVA_HOME/bin:$PATH
root@ping-Inspiron-3458:~# export MVN_HOME=~/program/apache-maven-3.8.8/
root@ping-Inspiron-3458:~# export PATH=$MVN_HOME/bin:$PATH
root@ping-Inspiron-3458:~# cd ~/code/tio-boot-http-request-handler-demo/
root@ping-Inspiron-3458:~/code/tio-boot-http-request-handler-demo# mvn clean package -DskipTests -Pnative
[INFO] Scanning for projects...
[INFO]
[INFO] ---------< nexus.io:tio-boot-http-request-handler-demo >----------
[INFO] Building tio-boot-http-request-handler-demo 1.0.0
[INFO] from pom.xml
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- maven-clean-plugin:2.5:clean (default-clean) @ tio-boot-http-request-handler-demo ---
[INFO] Deleting /root/code/tio-boot-http-request-handler-demo/target
[INFO]
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ tio-boot-http-request-handler-demo ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory /root/code/tio-boot-http-request-handler-demo/src/main/resources
[INFO]
[INFO] --- maven-compiler-plugin:3.1:compile (default-compile) @ tio-boot-http-request-handler-demo ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 4 source files to /root/code/tio-boot-http-request-handler-demo/target/classes
[INFO]
[INFO] --- maven-resources-plugin:2.6:testResources (default-testResources) @ tio-boot-http-request-handler-demo ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory /root/code/tio-boot-http-request-handler-demo/src/test/resources
[INFO]
[INFO] --- maven-compiler-plugin:3.1:testCompile (default-testCompile) @ tio-boot-http-request-handler-demo ---
[INFO] No sources to compile
[INFO]
[INFO] --- maven-surefire-plugin:2.12.4:test (default-test) @ tio-boot-http-request-handler-demo ---
[INFO] Tests are skipped.
[INFO]
[INFO] --- maven-jar-plugin:2.4:jar (default-jar) @ tio-boot-http-request-handler-demo ---
[INFO] Building jar: /root/code/tio-boot-http-request-handler-demo/target/web-hello.jar
[INFO]
[INFO] --- native-image-maven-plugin:21.2.0:native-image (default) @ tio-boot-http-request-handler-demo ---
[INFO] ImageClasspath Entry: nexus.io:tio-boot:jar:1.6.4:compile (file:///root/.m2/repository/com/litongjava/tio-boot/1.6.4/tio-boot-1.6.4.jar)
...
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 08:10 min
[INFO] Finished at: 2024-05-06T06:01:02+08:00
[INFO] ------------------------------------------------------------------------
```
_注意:以上日志仅为示例,实际构建过程可能有所不同。_
## 启动测试
构建完成后,运行生成的本地可执行文件并测试其功能。
```shell
(base) root@DL:/data/apps/tio-boot-http-request-handler-demo# ./target/web-hello --server.port=1024
Jan 06, 2025 4:42:52 PM nexus.io.tio.utils.environment.Prop
INFO: file created successful:app.properties
Jan 06, 2025 4:42:52 PM nexus.io.tio.utils.environment.EnvUtils load
INFO: app.env:null
Jan 06, 2025 4:42:52 PM nexus.io.tio.utils.environment.EnvUtils load
INFO: app.name:null
Jan 06, 2025 4:42:52 PM nexus.io.tio.boot.context.TioApplicationContext run
INFO: AOP class not found: nexus.io.jfinal.aop.Aop
Jan 06, 2025 4:42:52 PM nexus.io.tio.boot.context.TioApplicationContext configureHttp
INFO: Server session enabled: false
Jan 06, 2025 4:42:52 PM nexus.io.tio.boot.context.TioApplicationContext run
INFO: Using cache: class nexus.io.tio.utils.cache.mapcache.ConcurrentMapCacheFactory
Jan 06, 2025 4:42:52 PM nexus.io.tio.boot.context.TioApplicationContext run
INFO: Server heartbeat timeout: 0
Jan 06, 2025 4:42:52 PM nexus.io.tio.utils.Threads getTioExecutor
INFO: new worker thead pool:nexus.io.tio.utils.thread.pool.SynThreadPoolExecutor@bca51ec[Running, pool size = 1, active threads = 1, queued tasks = 0, completed tasks = 0]
Jan 06, 2025 4:42:52 PM nexus.io.tio.boot.context.TioApplicationContext run
INFO: HTTP handler:
{
"/hi": "nexus.io.tio.web.hello.config.HttpRequestHandlerConfig$$Lambda/0xd081350b15bca0456936253d349a9e11adb6c5190@4bb38d70",
"/hello": "nexus.io.tio.web.hello.config.HttpRequestHandlerConfig$$Lambda/0xcb437d3e797185b36b558533d31a5d6958f792c10@299840ed"
}
Jan 06, 2025 4:42:52 PM nexus.io.tio.boot.context.TioApplicationContext run
INFO: Initialization times (ms): Total: 12, Scan Classes: 0, Init Server: 0, Config: 1, Server: 11, Route: 0
Jan 06, 2025 4:42:52 PM nexus.io.tio.boot.context.TioApplicationContext printUrl
INFO: Server port: 1024
Jan 06, 2025 4:42:52 PM nexus.io.tio.boot.context.TioApplicationContext printUrl
INFO: Access URL: http://localhost:1024
17ms
```
启动时间仅为 17 毫秒,极大提升了应用的响应速度。
通过 `curl` 命令进行测试:
```shell
curl http://localhost:1024/hi
curl http://localhost:1024/hello
```
构建后的可执行文件大小约为 34MB。您可以通过以下链接下载构建后的文件:
[下载地址](https://github.com/litongjava/tio-boot-http-request-handler-demo/releases/download/v1.0.0/web-hello)
## 性能测试
使用 ApacheBench (`ab`) 工具进行性能测试,模拟高并发请求。
```shell
ab -c1000 -n10000000 http://localhost:1024/ok
```
此命令将以 1000 个并发连接,总共发送 1000 万个请求到 `http://localhost/ok`,用于评估服务器的处理能力和稳定性。
## 总结
通过上述步骤,我们成功地使用 GraalVM 将基于 tio-boot 的 Java 应用程序编译为原生可执行文件。这不仅显著减少了启动时间,还优化了资源消耗,使应用在生产环境中表现更加高效。GraalVM 的原生镜像技术为 Java 开发者提供了强大的性能优化手段,是构建高性能服务器应用程序的理想选择。
# 参考资料
- [GraalVM 官方文档](https://www.graalvm.org/docs/)
- [tio-boot 官方仓库](https://github.com/litongjava/tio-boot-http-request-handler-demo)
- [Apache Maven 官方网站](https://maven.apache.org/)
---
# 使用 Docker 部署 tio-boot
URL: https://tio-boot.com/zh/02_deployment/05.html
Source: zh/02_deployment/05.md
# 使用 Docker 部署 tio-boot
本文档详细介绍了如何使用 Docker 部署 **tio-boot** 项目,包括安装 Java 和 Maven、克隆项目、使用 Docker 运行和构建镜像等步骤。
[[toc]]
---
## 安装 Java
首先,创建目录并下载 Oracle JDK 8u411:
```bash
mkdir -p /opt/package/java && cd /opt/package/java
wget https://github.com/litongjava/oracle-jdk/releases/download/8u411/jdk-8u411-linux-x64.tar.gz
```
解压并配置 Java 环境变量:
```bash
mkdir -p /usr/java/
tar -xf jdk-8u411-linux-x64.tar.gz -C /usr/java
export JAVA_HOME=/usr/java/jdk1.8.0_411
export PATH=$JAVA_HOME/bin:$PATH
java -version
```
验证 Java 安装:
```bash
java version "1.8.0_411"
Java(TM) SE Runtime Environment (build 1.8.0_411-b09)
Java HotSpot(TM) 64-Bit Server VM (build 25.411-b09, mixed mode)
```
## 安装 Maven
创建目录并下载 Apache Maven 3.8.8:
```bash
mkdir -p /opt/package/maven && cd /opt/package/maven
wget https://dlcdn.apache.org/maven/maven-3/3.8.8/binaries/apache-maven-3.8.8-bin.zip
```
解压并配置 Maven 环境变量:
```bash
mkdir -p /usr/maven
unzip apache-maven-3.8.8-bin.zip -d /usr/maven
export MVN_HOME=/usr/maven/apache-maven-3.8.8
export PATH=$MVN_HOME/bin:$PATH
mvn --version
```
验证 Maven 安装:
```bash
Apache Maven 3.8.8 (4c87b05d9aedce574290d1acc98575ed5eb6cd39)
Maven home: /usr/maven/apache-maven-3.8.8
Java version: 1.8.0_411, vendor: Oracle Corporation, runtime: /usr/java/jdk1.8.0_411/jre
Default locale: en, platform encoding: UTF-8
OS name: "linux", version: "6.1.109-118.189.amzn2023.x86_64", arch: "amd64", family: "unix"
```
## 克隆项目
在目标目录下克隆 **tio-boot** 项目:
```bash
mkdir -p /data/apps && cd /data/apps
git clone https://github.com/litongjava/tio-boot-web-hello.git
```
## 使用 Java 命令启动
### 打包项目
进入项目目录并执行 Maven 打包:
```bash
cd tio-boot-web-hello
mvn clean package -DskipTests -Dgpg.skip -Pproduction
```
### 启动应用
使用 Java 命令运行 JAR 包:
```bash
java -jar target/tio-boot-web-hello-1.0.0.jar
```
## 使用 Docker 启动
### 测试启动
使用预构建的 JDK 镜像运行应用:
**使用 `litongjava/jdk:8u411-stable-slim` 镜像**
```bash
docker run --name web-hello \
-dit \
-v $(pwd)/target:/app \
-p 8080:80 \
-w /app \
litongjava/jdk:8u411-stable-slim \
java -jar tio-boot-web-hello-1.0.0.jar --app.env=prod
```
**使用 `litongjava/jdk:8u211` 镜像**
```bash
docker run --name web-hello \
-dit \
-v $(pwd)/target:/app \
-p 8080:80 \
-w /app \
litongjava/jdk:8u211 \
/usr/java/jdk1.8.0_211/bin/java -jar tio-boot-web-hello-1.0.0.jar --app.env=prod
```
> **注意**: 请将 `$(pwd)/target` 替换为实际的目标目录路径。
### 测试应用
使用 `curl` 命令测试应用是否成功启动:
```bash
curl http://localhost:8080
```
## 封装成镜像
### 编写 Dockerfile
创建 `Dockerfile` 文件,内容如下:
```dockerfile
FROM litongjava/jdk:21_0_6-stable-slim
# 设置工作目录
WORKDIR /app
# 复制 JAR 文件到容器
COPY target/tio-boot-web-hello-1.0.0.jar /app/
# 运行 JAR 文件
CMD ["java", "-jar", "tio-boot-web-hello-1.0.0.jar"]
```
### 构建 Docker 镜像
在项目根目录下执行构建命令:
```bash
docker build -t litongjava/tio-boot-web-hello .
```
### 运行 Docker 容器
启动容器并设置自动重启:
```bash
docker run -dit --restart=always --net=host --name=tio-boot-web-hello litongjava/tio-boot-web-hello
```
### 使用 `ENTRYPOINT`
为了支持 `JAVA_OPTS` 等环境变量,可以修改 `Dockerfile` 为使用 `ENTRYPOINT`:
```dockerfile
FROM litongjava/jdk:8u391-stable-slim
# 设置工作目录
WORKDIR /app
# 复制 JAR 文件到容器
COPY target/tio-boot-web-hello-1.0.0.jar /app/
# 使用 ENTRYPOINT 运行 JAR 文件
ENTRYPOINT ["java", "-jar", "tio-boot-web-hello-1.0.0.jar"]
```
### 指定运行环境
运行容器时,可以通过命令行参数指定应用环境:
```bash
docker run -d --net=host --name tio-boot-web-hello \
-v $(pwd)/logs:/app/logs \
-e TZ=Asia/Shanghai \
-e LANG=C.UTF-8 \
litongjava/tio-boot-web-hello:latest --app.env=prod
```
> **推荐镜像**: 使用 [litongjava/jdk:8u391-stable-slim](https://hub.docker.com/r/litongjava/jdk) 更加稳定高效。其他可选镜像包括:
>
> - `litongjava/jre:8u391-stable-slim` (352MB)
> - `litongjava/jdk:8u391-stable-slim` (437MB)
> - `litongjava/jdk:8u411-stable-slim` (458MB)
> - `litongjava/jdk:8u211` (549MB)
> - `litongjava/jdk:17.0.12-stable-slim`
> - `litongjava/jdk:21_0_6-stable-slim`
> - `litongjava/jdk:25_0_2-stable-slim`
## 使用 Builder 和 Runner
通过分阶段构建(multi-stage build)优化 Docker 镜像大小和构建效率。
### 编写 Dockerfile
```dockerfile
# 构建阶段
FROM litongjava/maven:3.8.8-jdk8u391 AS builder
WORKDIR /app
# 预先下载依赖
COPY pom.xml pom.xml
RUN mvn dependency:go-offline
# 复制源代码并打包
COPY src src
RUN mvn package -Passembly -q
RUN ls target
# 运行阶段
FROM litongjava/jre:8u391-stable-slim
WORKDIR /app
# 从构建阶段复制 JAR 文件
COPY --from=builder /app/target/gpt-translator-backend-1.0.0.jar /app
# 暴露端口
EXPOSE 8080
# 运行应用
CMD [
"java", "-server", "-Xms1G", "-Xmx1G",
"-XX:+UseNUMA", "-XX:+UseParallelGC",
"-Dpacket.handler.mode=queue1",
"-jar", "/app/gpt-translator-backend-1.0.0.jar"
]
```
### 构建和运行镜像
```bash
docker build -t litongjava/tio-boot-web-hello-builder .
docker run -dit --restart=always --net=host --name=tio-boot-web-hello litongjava/tio-boot-web-hello-builder
```
## 中文支持与北京时区
为确保应用支持中文并使用北京时区,启动容器时添加环境变量 `TZ` 和 `LANG`:
```bash
docker run -dit --name tio-boot-web-hello --restart=always --net=host \
-v $(pwd):/app -w /app \
-e TZ=Asia/Shanghai \
-e LANG=C.UTF-8 \
litongjava/jdk:8u391-stable-slim \
java -jar tio-boot-web-hello-1.0.0.jar
```
## 使用二进制文件启动
### 移除 hotswap-classloader 依赖
为了使用二进制文件启动,建议移除 `hotswap-classloader` 依赖,并使用 `TioApplication` 启动应用。示例代码如下:
```java
TioApplication.run(HelloApp.class, args);
```
### 打包成二进制文件
使用 Maven 打包为原生二进制文件:
```bash
mvn clean package -DskipTests -Pnative
```
### 运行二进制文件
**注意**: 目前运行二进制文件可能存在不支持反射的问题,测试阶段可能会失败。
**尝试运行容器**
```bash
docker run --rm -p 8080:80 -v $(pwd)/target:/app debian /app/web-hello
```
若失败,可尝试以下命令:
```bash
docker run --rm -p 8080:80 -v $(pwd)/target:/app \
-e JAVA_HOME=/usr/java/jdk1.8.0_211 \
litongjava/jdk:8u211 /app/web-hello
```
**备注**: 运行二进制文件时可能会遇到未知错误,需进一步排查原因。
---
通过以上步骤,您可以成功使用 Docker 部署 **tio-boot** 项目。如果在部署过程中遇到任何问题,请参考相关日志或联系项目维护者获取帮助。
---
# 部署到 Fly.io
URL: https://tio-boot.com/zh/02_deployment/06.html
Source: zh/02_deployment/06.md
# 部署到 Fly.io
## Fly.io 简介
Fly.io 是一个专注于将全栈应用和数据库部署到用户附近位置的平台。它通过将容器转换为运行在全球 30 多个地区的微型虚拟机(micro-VM),显著降低了延迟并提高了应用的响应速度。
## 部署到 Fly.io
本文将通过 https://github.com/litongjava/tio-boot-web-hello 演示如何将一个 Tio-Boot 项目部署到 Fly.io。
### 1. 打包应用程序
首先,我们需要使用 Maven 命令来打包应用程序:
```bash
mvn clean package -DskipTests -Dgpg.skip -Pproduction
```
这个命令将完成以下任务:
- 清理项目(`clean`)。
- 打包项目(`package`)。
- 跳过测试(`-DskipTests`)。
- 使用生产环境配置(`-Pproduction`)。
### 2. 准备 Docker 镜像
`fly deploy` 命令会使用 `Dockerfile` 来创建 Docker 镜像。以下是一个示例 `Dockerfile`:
```Dockerfile
FROM litongjava/jdk:25_0_2-stable-slim
# 设置工作目录
WORKDIR /app
# 复制 jar 文件到容器中
COPY target/tio-boot-web-hello-1.0.0.jar /app/
# 运行 jar 文件
CMD ["java", "-jar", "tio-boot-web-hello-1.0.0.jar", "--app.env=prod"]
```
这一步骤包括:
- 使用指定的基础镜像。
- 设置容器工作目录。
- 复制打包好的 `jar` 文件到容器。
- 设置容器启动时的命令。
### 3. 配置 Fly.io 应用
Fly.io 使用 `fly.toml` 文件来管理应用配置。以下是一个示例配置:
```toml
app = "tio-boot-web-hello"
primary_region = 'sjc'
kill_signal = "SIGINT"
kill_timeout = 5
processes = []
[[vm]]
cpu_kind = "shared"
cpus = 1
memory_mb = 1024
[http_service]
internal_port = 80
force_https = false
auto_stop_machines = true
auto_start_machines = true
min_machines_running = 0
[checks]
[checks.health_check]
grace_period = "30s"
interval = "15s"
method = "get"
path = "/"
port = 80
timeout = "10s"
type = "http"
[env]
LANG="C.UTF-8"
```
此配置文件包括:
- 定义应用名称和主要区域。
- 设置虚拟机的 CPU 和内存参数。
- 配置 HTTP 服务参数,包括自动启动与停止。
- 配置健康检查,确保服务正常运行。
### 4. 部署到 Fly.io
使用以下命令创建应用并部署:
```bash
fly apps create tio-boot-web-hello
fly deploy
```
此步骤将完成以下任务:
- **构建镜像**:Fly.io 使用 `Dockerfile` 构建 Docker 镜像。
- **推送镜像**:构建好的 Docker 镜像会被推送到 Fly.io 的镜像仓库。
- **启动应用**:Fly.io 会在指定区域启动应用实例。
- **健康检查**:Fly.io 根据配置的健康检查定期监控应用状态,确保服务正常运行。
### 5. 使用 `build` 工具
`build` 是一个自动化部署工具,能够提高部署效率。以下是 `build.txt` 的示例配置:
```text
[win.env]
set JAVA_HOME=D:\java\jdk-25.0.2
[win.build]
mvn clean package -DskipTests -Dgpg.skip -Pproduction
fly deploy
```
运行以下命令即可启动部署:
```bash
build
```
### 6. 添加 Flycast 支持
执行以下命令为应用分配私有 IPv6 地址,启用 Flycast 内网访问:
```bash
fly ips allocate-v6 --private
```
### 7. 设置环境变量
```
fly secrets set OPENAI_API_KEY=<>
fly secrets set JDBC_PSWD=<>
```
### 9. 添加卷
创建
```
fly volume create uni_ai_cache -r sjc -n 1 -s 10
```
扩容
```
fly volumes extend vol_xxx --size 10GB
```
挂载
```
[mounts]
source = "uni_ai_cache"
destination = "/app/cache"
```
在 Fly.io 上,一个存储卷(volume)通常只能同时附加到一个虚拟机实例上。如果的应用部署在多个主机上,需要分别为每个主机创建对应的卷或者使用其他支持多主机共享存储的方案(例如外部数据库或对象存储服务)。这也是为了防止数据竞争和损坏
### 9. 访问应用
- **公网访问**:通过 `flyproxy` 访问应用,访问地址为 `http://app-name.fly.dev`。
- **内网访问**:通过 `flycast` 访问,地址为 `http://app-name.flycast`。
Fly.io 会根据访问情况自动启动或停止应用,以节省资源。
### 使用 Fly.io 进行构建
以下是分两阶段构建并部署应用的 `Dockerfile`:
```Dockerfile
# 第一阶段:构建阶段
FROM litongjava/maven:3.8.8-jdk8u391 AS builder
WORKDIR /src
COPY pom.xml /src/
COPY src /src/src
RUN mvn package -DskipTests -Pproduction
# 第二阶段:运行阶段
FROM litongjava/jdk:8u391-stable-slim
WORKDIR /app
COPY --from=builder /src/target/playwright-server-1.0.0.jar /app/
CMD ["java", "-Xmx900m","-Xms512m","-jar", "playwright-server-1.0.0.jar"]
```
### 修改 Fly.io 服务器配置
如需调整服务器配置,可以参考以下示例:
```toml
[[vm]]
cpu_kind = "performance"
cpus = 2
memory_mb = 4096
```
或
```toml
[[vm]]
size = "shared-cpu-1x"
memory = "2048MB"
```
### 测试数据
shared cpu1 耗时 3.67 s
shared cpu2 耗时 2.60 s
performance cpu1 耗时 3.02 s
performance cpu2 耗时 2.60 s
---
# 部署到 AWS Lambda
URL: https://tio-boot.com/zh/02_deployment/07.html
Source: zh/02_deployment/07.md
# 部署到 AWS Lambda
将应用程序部署到 AWS Lambda 需要进行一些修改和配置。以下是详细步骤:
### 1. 打包应用程序
确保应用程序已经使用 Maven 打包:
```shell
mvn clean package -DskipTests -Dgpg.skip -Pproduction
```
### 2. 创建 Lambda 函数包
创建一个新的目录用于存放 Lambda 函数包,并将打包好的 JAR 文件复制到该目录:
```shell
mkdir lambda-package
cp target/tio-boot-web-hello-1.0.0.jar lambda-package/
```
在 `lambda-package` 目录下创建一个 `bootstrap` 文件,该文件是 Lambda 的入口脚本:
```bash
#!/bin/sh
# Set the java options
JAVA_OPTS="-Djava.awt.headless=true"
# Run the java application
exec java $JAVA_OPTS -jar tio-boot-web-hello-1.0.0.jar --app.env=prod
```
确保 `bootstrap` 文件可执行:
```shell
chmod +x lambda-package/bootstrap
```
### 3. 创建 Lambda 函数
AWS Lambda 使用 AWS CLI 进行部署。首先,确保已经安装并配置好 AWS CLI。
使用以下命令创建 Lambda 函数:
```shell
aws lambda create-function \
--function-name tio-boot-web-hello \
--handler bootstrap \
--runtime provided.al2 \
--role arn:aws:iam::your-account-id:role/your-execution-role \
--zip-file fileb://lambda-package.zip
```
在此之前,需要将 `lambda-package` 目录打包成 ZIP 文件:
```shell
zip -r lambda-package.zip lambda-package
```
### 4. 配置 API Gateway
为了使 Lambda 函数可以通过 HTTP 请求进行访问,需要配置 API Gateway:
1. **创建 API**:在 AWS 管理控制台中,导航到 API Gateway 并创建一个新的 HTTP API。
2. **配置集成**:将 API Gateway 配置为调用 Lambda 函数。确保设置正确的 Lambda 函数 ARN。
3. **部署 API**:创建并部署 API,使其可以通过一个公开的 URL 进行访问。
### 5. 更新 Lambda 函数
在以后更新 Lambda 函数时,只需重新打包并使用以下命令更新函数代码:
```shell
aws lambda update-function-code \
--function-name tio-boot-web-hello \
--zip-file fileb://lambda-package.zip
```
### 6. 配置 AWS Lambda Layers (可选)
如果需要,您可以使用 AWS Lambda Layers 来提供依赖库和运行时环境。创建一个新的 Layer 并将其附加到 Lambda 函数中。
### 总结
通过以上步骤,可以将 `tio-boot` 项目成功部署到 AWS Lambda。AWS Lambda 适用于无服务器架构,能够按需扩展,并且只需为实际使用的资源付费,提供了高效的成本管理和灵活的部署方案。
---
# 到阿里云云函数
URL: https://tio-boot.com/zh/02_deployment/08.html
Source: zh/02_deployment/08.md
# 到阿里云云函数
## 阿里云云函数简介
阿里云的 serverless 产品 https://fcnext.console.aliyun.com/overview
## 部署到阿里云云函数
如果要将应用程序部署到阿里云云函数(Function Compute),需要进行以下步骤:
### 1. 创建函数计算项目
首先,确保已经安装了阿里云函数计算的 CLI 工具 `fun`:
windows
```shell
yarn global add @serverless-devs/s
```
linux or macos
```shell
curl -o- -L http://cli.so/install.sh | bash
```
执行以下命令,验证是否安装成功。
```
s -v
```
### 2. 配置账号
```
>s config add
? Please select a provider:
> Alibaba Cloud (alibaba)
AWS (aws)
Azure (azure)
Baidu Cloud (baidu)
Google Cloud (google)
Huawei Cloud (huawei)
Tencent Cloud (tencent)
```
在项目根目录创建 `Funfile`,定义如何构建和部署应用:
```Funfile
# 使用官方的 Java8 runtime
RUNTIME java8
# 安装依赖
RUN apt-get update && apt-get install -y maven
# 复制项目文件
COPY . /code
# 设置工作目录
WORKDIR /code
# 打包应用程序
RUN mvn clean package -DskipTests -Dgpg.skip -Pproduction
```
### 3. 创建 template.yml
在项目根目录创建 `template.yml` 文件,定义函数计算的服务和函数:
```yaml
ROH: "2018-04-03"
Transform: "Aliyun::Serverless::Transform::Function::Service"
Resources:
TioBootWebHelloService:
Type: "Aliyun::Serverless::Service"
Properties:
Description: "Tio Boot Web Hello Service"
TioBootWebHelloFunction:
Type: "Aliyun::Serverless::Function"
Properties:
Handler: "example.App::handleRequest"
Runtime: "java8"
CodeUri: "./target/tio-boot-web-hello-1.0.0.jar"
MemorySize: 1024
Timeout: 30
Events:
httpTrigger:
Type: "HTTP"
Properties:
AuthType: "ANONYMOUS"
Methods: ["GET", "POST"]
```
### 4. 部署到函数计算
使用 `s deploy -y` 命令将应用部署到阿里云函数计算:
```shell
s deploy -y
```
这一步包括:
- **构建环境**:根据 `Funfile` 构建运行环境。
- **打包应用**:将应用打包并上传到函数计算。
- **创建资源**:根据 `template.yml` 创建服务和函数资源。
- **部署应用**:将打包好的应用部署到阿里云函数计算中。
---
# 使用 Deploy 工具部署
URL: https://tio-boot.com/zh/02_deployment/09.html
Source: zh/02_deployment/09.md
# 使用 Deploy 工具部署
[[toc]]
## 工具简介
为了简化部署流程并提高部署效率,我开发了一款名为 **deploy** 的工具,专用于将 `tio-boot` 工程快速部署到自有服务器上。该工具包含客户端和服务端两部分,用户需先在服务器上安装服务端,之后即可通过客户端进行部署操作。
**工具已开源**,源码地址如下:
- [deploy 工具](https://github.com/litongjava/deploy)
## 服务端安装
有关服务端的详细安装步骤,请参考项目文档,此处不再赘述。
## 部署方法
### Fastjar + Docker
#### 打包配置文件 `.build.txt`
该文件用于配置项目在不同操作系统上的打包命令,示例如下:
```txt
[win.env]
set JAVA_HOME=D:\java\jdk1.8.0_121
[win.build]
mvn clean package -DskipTests -Dgpg.skip -Pproduction
[linux.env]
export JAVA_HOME=/usr/java/jdk1.8.0_121
[linux.build]
mvn clean package -DskipTests -Dgpg.skip -Pproduction
[mac.env]
export JAVA_HOME=~/java/jdk1.8.0_121
[mac.build]
mvn clean package -DskipTests -Dgpg.skip -Pproduction
```
此配置文件定义了在 Windows、Linux 和 macOS 系统上的 Java 环境变量及 Maven 打包命令,确保项目在各平台下能正确构建。
#### 部署配置文件 `.deploy.toml`
该文件用于定义不同环境的部署操作,示例如下:
```toml
[dev.upload-run]
url = "http://192.168.1.2:10405/deploy/file/upload-run/"
p = "123456"
b = ".build.txt"
z = "app.zip target/dynamic-css-1.0.0.jar"
file = "app.zip"
d = "unzip/dynamic-css"
c1 = "mkdir -p /data/apps/dynamic-css"
c2 = "[ -d /data/apps/dynamic-css ] && cp -r /data/apps/dynamic-css backup/dynamic-css-backup-$(date +'%Y%m%d_%H%M%S')"
c3 = "cp unzip/dynamic-css/target/dynamic-css-1.0.0.jar /data/apps/dynamic-css/"
c4 = "docker rm -f dynamic-css 2>/dev/null || true"
c5 = "cd /data/apps/dynamic-css && docker run -dit --name dynamic-css --restart=always --net=host -v $(pwd):/app -w /app -e TZ=Asia/Shanghai -e LANG=C.UTF-8 litongjava/jdk:8u391-stable-slim java -jar dynamic-css-1.0.0.jar"
c = "docker ps | grep dynamic-css"
```
该配置文件定义了 `dev`(开发)、`test`(测试)和 `prod`(生产)三个环境下的部署流程。以 `dev.upload-run` 为例,具体说明如下:
- `url`: 上传文件的服务端地址。
- `p`: 密码,用于身份验证。
- `b`: 指定打包配置文件。
- `z`: 需要压缩的文件列表。
- `file`: 上传的压缩包文件名。
- `d`: 解压路径。
- `c1-c5`: 一系列 Shell 命令,依次执行创建目录、备份旧版本、复制新文件、停止并删除旧容器、启动新容器等操作。
- `c`: 检查容器是否成功启动的命令。
#### 执行部署命令
通过命令行工具 `deploy`,可在不同环境中轻松执行部署操作:
- **开发环境**:
```shell
deploy
```
- **测试环境**:
```shell
deploy -e test
```
- **生产环境**:
```shell
deploy -e prod
```
#### 运行流程
Deploy 工具的运行流程如下:
1. **编译**:根据 `.build.txt` 配置进行项目编译。
2. **压缩打包**:将编译生成的文件压缩为 `.zip` 包。
3. **上传解压**:将压缩包上传至服务器并解压。
4. **备份旧目录**:备份旧版本,以便回滚操作。
5. **复制到指定目录**:将新文件复制到指定的部署目录中。
6. **删除旧容器**:停止并删除旧的 Docker 容器。
7. **启动新容器**:使用新的镜像和配置启动新的 Docker 容器。
8. **检查容器**:最终检查新容器是否成功启动。
#### 部署成功后的反馈信息
部署成功后,Deploy 工具会返回相关的执行信息,例如:
```
response status code: 200
{"success":true,"output":"7636aec41b4b litongjava/jdk:8u391-stable-slim \"java -jar dynamic-c…\" Less than a second ago Up Less than a second dynamic-css\n","time":0}
```
该信息表明新容器已成功启动,运行状态正常。
#### 完整的部署文件示例
```toml
[dev.upload-run]
url = "http://192.168.1.2:10405/deploy/file/upload-run/"
p = "123456"
b = ".build.txt"
z = "app.zip target/dynamic-css-1.0.0.jar"
file = "app.zip"
d = "unzip/dynamic-css"
c1 = "mkdir -p /data/apps/dynamic-css"
c2 = "[ -d /data/apps/dynamic-css ] && cp -r /data/apps/dynamic-css backup/dynamic-css-backup-$(date +'%Y%m%d_%H%M%S')"
c3 = "cp unzip/dynamic-css/target/dynamic-css-1.0.0.jar /data/apps/dynamic-css/"
c4 = "docker rm -f dynamic-css 2>/dev/null || true"
c5 = "cd /data/apps/dynamic-css && docker run -dit --name dynamic-css --restart=always --net=host -v $(pwd):/app -w /app -e TZ=Asia/Shanghai -e LANG=C.UTF-8 litongjava/jdk:8u391-stable-slim java -jar dynamic-css-1.0.0.jar"
c = "docker ps | grep dynamic-css"
[test.upload-run]
url = "http://xxxx:10405/deploy/file/upload-run/"
p = "xxxx"
b = ".build.txt"
z = "app.zip target/dynamic-css-1.0.0.jar"
file = "app.zip"
d = "unzip/dynamic-css"
c1 = "mkdir -p /data/apps/dynamic-css"
c2 = "[ -d /data/apps/dynamic-css ] && cp -r /data/apps/dynamic-css backup/dynamic-css-backup-$(date +'%Y%m%d_%H%M%S')"
c3 = "cp unzip/dynamic-css/target/dynamic-css-1.0.0.jar /data/apps/dynamic-css/"
c4 = "docker rm -f dynamic-css 2>/dev/null || true"
c5 = "cd /data/apps/dynamic-css && docker run -dit --name dynamic-css --restart=always --net=host -v $(pwd):/app -w /app -e TZ=Asia/Shanghai -e LANG=C.UTF-8 litongjava/jdk:8u391-stable-slim java -jar dynamic-css-1.0.0.jar --app.env=test"
c = "docker ps | grep dynamic-css"
[prod.upload-run]
url = "http://xxxx:10405/deploy/file/upload-run/"
p = "xxxx"
b = ".build.txt"
z = "app.zip target/dynamic-css-1.0.0.jar"
file = "app.zip"
d = "unzip/dynamic-css"
c1 = "mkdir -p /data/apps/dynamic-css"
c2 = "[ -d /data/apps/dynamic-css ] && cp -r /data/apps/dynamic-css backup/dynamic-css-backup-$(date +'%Y%m%d_%H%M%S')"
c3 = "cp unzip/dynamic-css/target/dynamic-css-1.0.0.jar /data/apps/dynamic-css/"
c4 = "docker rm -f dynamic-css 2>/dev/null || true"
c5 = "cd /data/apps/dynamic-css && docker run -dit --name dynamic-css --restart=always --net=host -v $(pwd):/app -w /app -e TZ=Asia/Shanghai -e LANG=C.UTF-8 litongjava/jdk:8u391-stable-slim java -jar dynamic-css-1.0.0.jar --app.env=prod"
c = "docker ps | grep dynamic-css"
```
### Fastjar + Systemctl
#### Service 文件配置
在服务器上配置 systemd 服务,以管理 `max-blog-app-backend` 应用。
1. **创建 Service 文件**:
```shell
vi /etc/systemd/system/max-blog-app-backend.service
```
2. **填写以下内容**:
```ini
[Unit]
Description=max-blog-app-backend
After=network.target
[Service]
Type=simple
User=root
Restart=on-failure
RestartSec=5s
WorkingDirectory=/data/apps/max-blog-app-backend
ExecStart=/usr/java/jdk1.8.0_411/bin/java -jar max-blog-app-backend-1.0.0.jar --app.env=test
[Install]
WantedBy=multi-user.target
```
3. **启动并启用服务**:
```shell
systemctl start max-blog-app-backend
systemctl status max-blog-app-backend
systemctl enable max-blog-app-backend
```
#### 打包配置文件 `.build.txt`
该文件用于配置项目在不同操作系统上的打包命令,示例如下:
```txt
[win.env]
set JAVA_HOME=D:\java\jdk1.8.0_121
[win.build]
mvn clean package -DskipTests -Dgpg.skip -Pproduction
[linux.env]
export JAVA_HOME=/usr/java/jdk1.8.0_121
[linux.build]
mvn clean package -DskipTests -Dgpg.skip -Pproduction
[mac.env]
export JAVA_HOME=~/java/jdk1.8.0_121
[mac.build]
mvn clean package -DskipTests -Dgpg.skip -Pproduction
```
此配置文件定义了在 Windows、Linux 和 macOS 系统上的 Java 环境变量及 Maven 打包命令,确保项目在各平台下能正确构建。
#### 部署配置文件 `.deploy.toml`
部署配置文件用于定义不同环境的部署操作,示例如下:
```toml
[test.upload-run]
url = "http://192.168.1.2:10405/deploy/file/upload-run/"
p = "123456"
b = ".build.txt"
z = "app.zip target/max-blog-app-backend-1.0.0.jar"
file = "app.zip"
d = "unzip/max-blog-app-backend"
c1 = "mkdir -p /data/apps/max-blog-app-backend"
c2 = "[ -d /data/apps/max-blog-app-backend ] && cp -r /data/apps/max-blog-app-backend backup/max-blog-app-backend-backup-$(date +'%Y%m%d_%H%M%S')"
c3 = "mkdir -p backup"
c4 = "cp unzip/max-blog-app-backend/target/max-blog-app-backend-1.0.0.jar /data/apps/max-blog-app-backend/"
c = "systemctl restart max-blog-app-backend"
```
该配置文件定义了 `test` 环境下的部署流程,具体说明如下:
- `url`: 上传文件的服务端地址。
- `p`: 密码,用于身份验证。
- `b`: 指定打包配置文件。
- `z`: 需要压缩的文件列表。
- `file`: 上传的压缩包文件名。
- `d`: 解压路径。
- `c1-c4`: 一系列 Shell 命令,依次执行备份旧版本、创建部署目录、复制新文件等操作。
- `c`: 重启 systemd 服务以应用新版本。
## 前端项目部署文档
本文档介绍如何使用 `.build.txt` 和 `deploy` 工具,将前端项目(基于 React 的 `manim-tutor-admin-react`)自动化部署到服务器的 Nginx 网站目录。
---
### 1. 构建配置
在项目根目录的 `.build.txt` 中定义了不同操作系统的构建命令:
```ini
[win.build]
pnpm build
[linux.build]
pnpm build
[mac.build]
pnpm build
```
无论是在 Windows、Linux 还是 macOS 环境中,都会执行 `pnpm build` 来打包前端代码,最终生成 `dist` 目录。
---
### 2. 部署配置
在 `.deploy.toml` 中还定义了生产环境的部署任务:
```ini
[prod.upload-run]
url = "http://127.0.0.1:10405/deploy/file/upload-run/"
p = "123456"
b = ".build.txt"
z = "dist.zip dist"
file = "dist.zip"
d = "unzip/manim-tutor-admin-react"
c = "mkdir -p /opt/1panel/www/sites/admin.jieti.cc/index -p && cp -r unzip/manim-tutor-admin-react/dist/* /opt/1panel/www/sites/admin.jieti.cc/index"
```
参数说明:
* **url**:上传 API 地址
* **p**:认证密码(部署工具使用)
* **b**:构建配置文件(`.build.txt`)
* **z**:将 `dist` 目录打包为 `dist.zip`
* **file**:上传到服务器的压缩文件名
* **d**:服务器解压目录(`unzip/manim-tutor-admin-react`)
* **c**:解压后的文件复制到 Nginx 网站目录 `/opt/1panel/www/sites/admin.jieti.cc/index`
---
### 3. 部署流程
执行以下命令启动部署:
```sh
deploy -e prod
```
运行过程包括以下步骤:
1. **执行构建命令**
读取 `.build.txt`,执行 `pnpm build`,生成 `dist` 目录。
2. **打包前端资源**
将 `dist` 目录压缩为 `dist.zip`。
3. **上传至服务器**
将 `dist.zip` 上传到服务器指定目录 `unzip/manim-tutor-admin-react` 并解压。
4. **复制到网站目录**
在服务器上执行以下命令:
```sh
mkdir -p /opt/1panel/www/sites/admin.jieti.cc/index -p && cp -r unzip/manim-tutor-admin-react/dist/* /opt/1panel/www/sites/admin.jieti.cc/index
```
将构建好的文件复制到 Nginx 的网站目录。
5. **Nginx 提供服务**
最终访问 `https://example.com/` 即可访问前端项目。
## 工具源码
如需了解更多关于 **deploy** 工具的实现细节,欢迎访问以下源码仓库:
- [deploy 工具](https://github.com/litongjava/deploy)
- [max-blog-app-backend 工具](https://github.com/litongjava/max-blog-app-backend)
通过这些工具,您可以实现高效、自动化的项目部署流程,显著提升开发与运维效率。
# 结语
Deploy 工具通过简化复杂的部署流程,使得 `tio-boot` 工程的部署更加高效和可靠。无论是基于 Docker 还是 Systemctl 的部署方式,都能够满足不同环境下的需求。希望本文档能够帮助您快速上手并顺利完成项目部署。
如有任何疑问或建议,欢迎在项目的 GitHub 仓库中提出。
---
# 使用Systemctl启动项目
URL: https://tio-boot.com/zh/02_deployment/10.html
Source: zh/02_deployment/10.md
# 使用Systemctl启动项目
## 一、前提条件
* 操作系统:Linux(内核 ≥ 3.0,支持 systemd)。
* 拥有 `root` 或具有相应权限的用户。
* 已联网,能访问外部下载源。
---
## 二、安装 Java 1.8.0_411
1. 创建下载目录并下载 JDK:
```bash
mkdir -p /opt/package/java && cd /opt/package/java
wget https://github.com/litongjava/oracle-jdk/releases/download/8u411/jdk-8u411-linux-x64.tar.gz
```
2. 解压到系统标准路径:
```bash
mkdir -p /usr/java
tar -xf jdk-8u411-linux-x64.tar.gz -C /usr/java
```
3. 配置环境变量(可添加到 `/etc/profile` 或 `~/.bashrc`):
```bash
export JAVA_HOME=/usr/java/jdk1.8.0_411
export PATH=$JAVA_HOME/bin:$PATH
```
4. 验证安装:
```bash
java -version
```
应输出:
```
java version "1.8.0_411"
Java(TM) SE Runtime Environment (build 1.8.0_411-b09)
Java HotSpot(TM) 64-Bit Server VM (build 25.411-b09, mixed mode)
```
---
## 三、安装 Maven 3.8.8
1. 创建下载目录并获取压缩包:
```bash
mkdir -p /opt/package/maven && cd /opt/package/maven
wget https://dlcdn.apache.org/maven/maven-3/3.8.8/binaries/apache-maven-3.8.8-bin.zip
```
2. 解压并配置:
```bash
mkdir -p /usr/maven
unzip apache-maven-3.8.8-bin.zip -d /usr/maven
```
3. 设置环境变量(同样可写入 `/etc/profile` 或 `~/.bashrc`):
```bash
export MVN_HOME=/usr/maven/apache-maven-3.8.8
export PATH=$MVN_HOME/bin:$PATH
```
4. 验证安装:
```bash
mvn --version
```
应输出类似:
```
Apache Maven 3.8.8 (...)
Java version: 1.8.0_411, vendor: Oracle Corporation
```
---
## 四、下载并编译项目
1. 创建应用存放目录并下载源码:
```bash
mkdir -p /data/apps && cd /data/apps
wget https://gitee.com/ppnt/tio-boot-web-hello/repository/archive/main.zip
yum install -y unzip
unzip main.zip
mv tio-boot-web-hello-main tio-boot-web-hello
```
2. 进入项目目录并执行打包:
```bash
cd /data/apps/tio-boot-web-hello
mvn clean package -Pproduction -DskipTests
```
3. 本地测试启动(可选):
```bash
java -jar target/tio-boot-web-hello-1.0.0.jar
```
---
## 五、配置 systemd 用户级服务
> **说明**:使用 `systemctl --user` 能让非 root 用户在自身会话中管理服务。这里以 `root` 用户为例,若使用其他用户,请确保目录权限和用户一致。
1. 创建用户级服务目录:
```bash
mkdir -p ~/.config/systemd/user
```
2. 新建服务文件 `~/.config/systemd/user/tio-boot-web-hello.service`,内容如下:
```ini
[Unit]
Description=tio-boot-web-hello Java Web Service
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=/data/apps/tio-boot-web-hello
ExecStart=/usr/java/jdk1.8.0_411/bin/java -jar /data/apps/tio-boot-web-hello/target/tio-boot-web-hello-1.0.0.jar
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=default.target
```
* **WorkingDirectory**:服务启动前切换到的目录
* **ExecStart**:完整的 Java 可执行路径与 JAR 包路径
* **WantedBy=default.target**:对应 `systemctl --user enable` 时的目标
3. 重新加载用户级服务配置:
```bash
systemctl --user daemon-reload
```
---
## 六、启动并管理服务
1. 启动服务:
```bash
systemctl --user start tio-boot-web-hello.service
```
2. 查看运行状态:
```bash
systemctl --user status tio-boot-web-hello.service
```
正常输出应类似:
```
● tio-boot-web-hello.service – tio-boot-web-hello Java Web Service
Loaded: loaded (/home/root/.config/systemd/user/tio-boot-web-hello.service; enabled)
Active: active (running) since ...
CGroup: /user.slice/user-0.slice/user@0.service/.../tio-boot-web-hello.service
└─xxxx /usr/java/jdk1.8.0_411/bin/java ...
```
3. 设置开机自动启动:
```bash
systemctl --user enable tio-boot-web-hello.service
```
4. 列出所有用户级服务及其状态:
```bash
systemctl --user list-units --type=service
```
---
## 七、常见问题及优化建议
* **日志输出**
默认日志会打印到控制台;若需重定向到文件,可在服务文件里添加:
```ini
StandardOutput=append:/var/log/tio-boot-web-hello.out
StandardError=append:/var/log/tio-boot-web-hello.err
```
* **环境变量**
若依赖额外环境变量,可在 `[Service]` 段添加:
```ini
Environment="JAVA_HOME=/usr/java/jdk1.8.0_411" "PATH=/usr/java/jdk1.8.0_411/bin:/usr/maven/apache-maven-3.8.8/bin:$PATH"
```
* **安全性**
— 尽量避免以 `root` 运行服务,可创建专用用户并调整文件权限。
— 将 JAR 包及相关配置放置在只读目录或使用 SELinux 进行约束。
---
至此,您已完成从环境准备、源码编译到 systemd 用户级服务配置与管理的全流程。
## 8 添加别名
可以通过在的 shell 配置文件中(如 `~/.bashrc`、`~/.zshrc` 等)添加一个 alias 或者函数,来给 `systemctl --user` 起一个更短的名字。下面以常见的 Bash 为例:
1. 打开(或创建)的 `~/.bashrc`:
```bash
nano ~/.bashrc
```
2. 在文件末尾添加一行 alias,比如:
```bash
# 给 systemctl --user 起别名 scu
alias scu='systemctl --user'
```
这样以后执行 `scu status` 就等同于 `systemctl --user status`。
3. 保存并退出后,应用改动:
```bash
source ~/.bashrc
```
4. 验证 alias 生效:
```bash
type scu
# 输出:scu is aliased to `systemctl --user`
```
这样就能用自己定义的短命令来替代繁琐的 `systemctl --user` 了。
---
# 使用 Jenkins 部署 Tio-Boot 项目
URL: https://tio-boot.com/zh/02_deployment/11.html
Source: zh/02_deployment/11.md
# 使用 Jenkins 部署 Tio-Boot 项目
在这篇博客中,我们将介绍如何使用 Jenkins 自动化部署 Tio-Boot 项目,帮助开发者实现项目的持续集成和部署(CI/CD)。本文详细介绍了从 Jenkins 安装、配置到项目构建、部署的每一步操作,并附带相关的原理说明。
## Jenkins 简介
Jenkins 是一个开源的自动化服务器,广泛用于构建、测试和部署软件项目。它通过流水线的形式自动化处理开发流程,可以极大提高开发效率和部署稳定性。在 Tio-Boot 项目中,Jenkins 可以帮助我们实现代码的自动构建、打包,并通过 Docker 部署项目到生产环境。
### Jenkins Docker 安装
首先,我们使用 Docker 启动 Jenkins 实例。以下命令用于在 Docker 中安装和运行 Jenkins:
```bash
docker run --name jenkins --restart=always -u root --privileged -d \
--dns 8.8.8.8 \
-p 9080:8080 -p 50000:50000 \
-v /data/apps/jenkins/jenkins_home:/var/jenkins_home \
-v /data/apps/jenkins/mvnrepository:/root/.m2/repository \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /usr/bin/docker:/usr/bin/docker \
jenkins/jenkins:lts
```
### 解释说明
1. **--restart=always**:确保 Jenkins 容器在 Docker 重启或宕机后自动重启。
2. **-v** 参数:将宿主机的目录挂载到 Jenkins 容器中,保证 Jenkins 配置数据的持久化以及 Maven 仓库的共享。
3. **-p 9080:8080**:将 Jenkins 的 web 端口映射到宿主机的 9080 端口。
### 常用目录
- **/var/jenkins_home/workspace/**:Jenkins 的工作空间目录,存放项目代码。
## 配置 Jenkins
### 解锁 Jenkins
Jenkins 初次安装时需要通过解锁获取管理员密码:
1. 进入 Jenkins 容器:
```bash
docker exec -it jenkins bash
```
2. 查看密码:
```bash
cat /var/jenkins_home/secrets/initialAdminPassword
```
复制输出的密码,访问 Jenkins 的 Web 界面,解锁后继续配置。
or
```
docker exec -it jenkins cat /var/jenkins_home/secrets/initialAdminPassword
```
### 访问 jenins
http://127.0.0.1:9080
username:jenkins
password:
### 安装插件
**插件选择**:首次启动时建议不要通过网络安装插件,因为网络原因可能导致安装失败,并且下载速度较慢。我们可以选择直接上传所需插件,手动安装。
### 创建管理员用户
按照 Jenkins 的提示,创建管理员用户并登录。
### 导入插件
将从以下地址下载的 `plugins.zip` 上传并解压到 Jenkins 数据目录 `/data/apps/jenkins/jenkins_home`,随后重启 Jenkins 即可生效:
[插件下载链接](https://github.com/litongjava/jenkins-package/releases/tag/v20240909)
```bash
mkdir /opt/package/jenkins -p
cd /opt/package/jenkins
wget https://github.com/litongjava/jenkins-package/releases/download/v20240909/plugins.zip
unzip plugins.zip -d /data/apps/jenkins/jenkins_home/
docker restart jenkins
```
### 导入工具库
安装工具库(JDK、Maven、Node.js),下载并解压到 `/data/apps/jenkins/jenkins_home/tools` 目录下:
- **hudson.model.JDK.zip**
- **hudson.tasks.Maven_MavenInstallation.zip**
- **jenkins.plugins.nodejs.tools.NodeJSInstallation.tar.gz**
```
mkdir /opt/package/jenkins -p
cd /opt/package/jenkins
wget https://github.com/litongjava/jenkins-package/releases/download/v20240909/hudson.model.JDK.zip
wget https://github.com/litongjava/jenkins-package/releases/download/v20240909/hudson.tasks.Maven_MavenInstallation.zip
wget https://github.com/litongjava/jenkins-package/releases/download/v20240909/jenkins.plugins.nodejs.tools.NodeJSInstallation.tar.gz
unzip hudson.model.JDK.zip -d /data/apps/jenkins/jenkins_home/tools
unzip hudson.tasks.Maven_MavenInstallation.zip -d /data/apps/jenkins/jenkins_home/tools
tar -xf jenkins.plugins.nodejs.tools.NodeJSInstallation.tar.gz -C /data/apps/jenkins/jenkins_home/tools
chmod u+x /data/apps/jenkins/jenkins_home/tools/hudson.model.JDK/oracleJDK8/jdk1.8.0_411/bin/*
chmod u+x /data/apps/jenkins/jenkins_home/tools/hudson.tasks.Maven_MavenInstallation/maven3.9.8/apache-maven-3.9.8/bin/*
```
### 系统配置
1. **配置 JDK**
在 Jenkins 中配置 JDK,路径为:
- 名称:oracleJDK8
- 安装目录:`/var/jenkins_home/tools/hudson.model.JDK/oracleJDK8/jdk1.8.0_411`
![JDK 配置]()
2. **配置 Maven**
- 名称:apache-maven-3.9.8
- 安装目录:`/var/jenkins_home/tools/hudson.tasks.Maven_MavenInstallation/maven3.9.8/apache-maven-3.9.8`
![Maven 配置]()
3. **配置 Node.js**
- 名称:node18.20.4
- 安装目录:`/var/jenkins_home/tools/jenkins.plugins.nodejs.tools.NodeJSInstallation/node18.20.4`
![Node.js 配置]()
## 项目构建与部署
### 新建任务
1. 创建任务 `tio-boot-web-hello`:
- 点击【新建任务】,输入任务名称,选择 "构建一个自由风格的软件项目"。
![新建任务]()
2. 配置源码管理:
- 选择 Git,填写项目仓库地址(如 `https://github.com/litongjava/tio-boot-web-hello.git`),并选择正确的分支(如 `main`)。
![源码管理]()
3. 配置构建触发器和步骤:
- 点击【构建触发器】,选择【构建】,添加构建步骤,调用 Maven 构建项目。
- 配置 Maven 命令为 `clean package -DskipTests -Pproduction`,确保能够生成 `.jar` 包。
![构建配置]()
4. 添加 Docker 命令:
构建成功后,执行以下 Docker 命令以启动项目:
```bash
docker stop tio-boot-web-hello || true
docker rm tio-boot-web-hello || true
docker rmi tio-boot-web-hello || true
docker build -t tio-boot-web-hello .
docker run -d -p 1111:1111 --name tio-boot-web-hello \
-v ${pwd}/logs:/app/logs \
-e TZ=Asia/Shanghai -e LANG=C.UTF-8 \
tio-boot-web-hello:latest \
--server.port=1111 --app.env=prod
```
![Docker 命令]()
### 手动触发任务
点击【构建】,手动触发项目构建:
![触发任务]()
2. 查看日志:
- 点击正在构建的任务,进入详情页面,查看 Jenkins 控制台输出,确保项目构建成功。
![查看日志]()
如果控制台显示以下信息,说明项目已经成功构建并运行:
```bash
Successfully built a67d81a888ff
Successfully tagged tio-boot-web-hello:latest
+ docker run -d -p 1111:1111 --name tio-boot-web-hello tio-boot-web-hello:latest --server.port=1111 --app.env=prod
d0dfdc94578c868781182bd5753e6b2bc612e12931a3feb74ade7763dd827e2a
Finished: SUCCESS
```
### 验证构建
使用以下命令查看 Docker 日志,确认项目是否成功启动:
```bash
docker logs -f tio-boot-web-hello
```
## 总结
通过 Jenkins 的持续集成,我们可以自动化管理 Tio-Boot 项目的构建和部署,确保每次代码更新后能够快速、稳定地将项目部署到生产环境中。Docker 结合 Jenkins 的优势让整个过程更灵活、高效。
## 使用笔者封装的镜像启动
```shell
docker run --name jenkins --restart=always -u root --privileged -d \
--dns 8.8.8.8 \
-p 9080:8080 -p 50000:50000 \
-v /data/apps/jenkins/mvnrepository:/root/.m2/repository \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /usr/bin/docker:/usr/bin/docker \
litongjava/jenkins:2.462.2
```
```shell
mkdir -p /data/apps/jenkins/jenkins_home
docker cp jenkins:/var/jenkins_home /data/apps/jenkins/jenkins_home
```
```shell
docker run --name jenkins --restart=always -u root --privileged -d \
--dns 8.8.8.8 \
-p 9080:8080 -p 50000:50000 \
-v /data/apps/jenkins/jenkins_home:/var/jenkins_home \
-v /data/apps/jenkins/mvnrepository:/root/.m2/repository \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /usr/bin/docker:/usr/bin/docker \
litongjava/jenkins:2.462.2
```
---
# 使用 Nginx 反向代理 Tio-Boot
URL: https://tio-boot.com/zh/02_deployment/12.html
Source: zh/02_deployment/12.md
# 使用 Nginx 反向代理 Tio-Boot
## 1. 简介
在高并发场景下,Tio-Boot 作为 Java 的高性能框架,配合 Nginx 反向代理可以进一步提升系统的稳定性和性能。本文将介绍如何通过配置 Nginx 反向代理 Tio-Boot 应用,并着重介绍如何通过 Keep-Alive 长连接来减少连接开销,提升整体性能。
## 2. 什么是 Keep-Alive?
Keep-Alive 是 HTTP 协议中的一种机制,用于在同一个 TCP 连接上发送多个请求/响应对,避免每次请求都重新建立连接的开销。对 Tio-Boot 这类高性能服务器而言,保持长连接可以显著降低网络通信延迟,减少 CPU 和内存的消耗。
## 3. 配置 Nginx 反向代理 Tio-Boot
首先,我们需要确保 Nginx 正确配置反向代理到 Tio-Boot 的服务,同时启用 Keep-Alive 长连接支持。
### Nginx 配置示例:
```nginx
# 根据 upgrade 头的值来设置一个变量
map $http_upgrade $connection_upgrade {
default keep-alive;
websocket upgrade;
}
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_pass_header Set-Cookie;
proxy_set_header Host $host:$server_port;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
# 日志配置
error_log /var/log/nginx/backend.error.log;
access_log /var/log/nginx/backend.access.log;
# 启用Keep-Alive长连接
keepalive_timeout 65;
keepalive_requests 100;
}
}
```
### 配置解读:
- **proxy_pass**:将请求转发到 Tio-Boot 的服务地址。
- **proxy_set_header**:配置相关的请求头信息,确保 Tio-Boot 获取正确的客户端信息。
- **Upgrade 和 Connection**:通过 Upgrade 头来支持 WebSocket 连接,如果是普通 HTTP 请求则使用 Keep-Alive。
- **keepalive_timeout**:设置 Nginx 和客户端之间的 Keep-Alive 超时时间,通常建议设置在 60 秒左右。
- **keepalive_requests**:设置单个 Keep-Alive 连接允许的最大请求数,设置为 100 比较合理,可以避免长时间的单个连接占用。
## 4. 提高性能的关键配置
1. **启用 Keep-Alive**
Keep-Alive 是性能优化的核心,通过减少 TCP 连接的创建开销,可以显著提升性能。配置`keepalive_timeout`和`keepalive_requests`,确保合理的连接超时时间和请求数上限。
2. **WebSocket 支持**
通过在`map`中判断`$http_upgrade`,我们可以为 WebSocket 连接启用`upgrade`机制,而对于普通 HTTP 请求则启用`keep-alive`,实现多协议支持。
3. **代理缓存和连接池**
可以为静态资源和常用数据启用代理缓存。为后端服务配置连接池,例如设置 Nginx 的`proxy_http_version 1.1`和`proxy_set_header Connection keep-alive`,使代理与后端服务的连接也能复用,进一步提升效率。
4. **日志分析和监控**
通过分析 Nginx 日志,可以了解连接的性能瓶颈和优化效果。可以使用`access_log`记录每次请求的处理时间,优化应用性能。
## 5. 总结
通过合理配置 Nginx 的反向代理和 Keep-Alive 机制,Tio-Boot 应用在高并发环境下可以更好地处理大量请求,减少资源消耗和响应延迟。使用本文提供的配置示例,开发者可以轻松集成 Nginx 与 Tio-Boot,并在实践中获得显著的性能提升。
---
# 使用 Supervisor 管理 Java 应用
URL: https://tio-boot.com/zh/02_deployment/13.html
Source: zh/02_deployment/13.md
# 使用 Supervisor 管理 Java 应用
Supervisor 是一个基于 Python 的进程管理工具,可用于在后台启动、监控和管理各类应用程序。本文档详细介绍了如何使用 Supervisor 来管理一个 Java 应用,从安装、配置到启动和监控,帮助构建一个稳定的运行环境。
[[toc]]
## 安装 Supervisor
如果尚未安装 Supervisor,可通过 pip 进行安装(建议使用 Python 3 环境)。在终端中执行以下命令:
```bash
pip install supervisor
```
安装完成后,可使用 `echo_supervisord_conf` 命令生成默认配置文件。将配置文件保存到用户目录下,例如:
```bash
echo_supervisord_conf > ~/supervisord.conf
```
---
## 配置 Supervisor 管理 Java 程序
编辑生成的配置文件(例如 `~/supervisord.conf`),在文件末尾添加如下配置段落,用以管理的 Java 程序:
```ini
[program:javaapp]
command=/Users/ec2-user/java/jdk1.8.0_381.jdk/Contents/Home/bin/java -jar /Users/ec2-user/java-kit-server/java-kit-server-1.0.0.jar
directory=/Users/ec2-user/java-kit-server
autostart=true
autorestart=true
startsecs=5
stdout_logfile=/Users/ec2-user/logs/javaapp.out
stderr_logfile=/Users/ec2-user/logs/javaapp.err
user=ec2-user
```
### 参数说明
- **command**:指定启动 Java 程序的完整命令。
- **directory**:设置工作目录,确保程序能正确加载所需依赖。
- **autostart**:Supervisor 启动时自动启动该程序。
- **autorestart**:程序异常退出后,Supervisor 将自动重启。
- **startsecs**:程序启动后需要等待的秒数,超过此时间后判定为启动成功。
- **stdout_logfile / stderr_logfile**:分别指定标准输出和标准错误的日志文件路径,请确保相应目录存在且具有写权限。
- **user**:指定以哪位用户身份运行程序,确保该用户具备执行该命令的权限。
---
## 启动 Supervisor
配置完成后,即可使用指定的配置文件启动 Supervisor 守护进程。执行以下命令:
```bash
supervisord -c ~/supervisord.conf
```
启动成功后,Supervisor 将自动读取配置文件,并在后台启动所有设置为 autostart 的程序。
---
## 管理与监控
Supervisor 提供了 `supervisorctl` 命令行工具,方便查看状态和管理各个进程。常用操作如下:
- **查看程序状态**
```bash
supervisorctl -c ~/supervisord.conf status
```
- **重启 Java 程序**
```bash
supervisorctl -c ~/supervisord.conf restart javaapp
```
通过这些命令,可以方便地监控进程运行状态,并在需要时进行相应管理。
---
## 自启动设置
如果希望 Supervisor 随系统启动自动运行,可将其加入 macOS 的启动项或使用 launchd 进行管理。具体操作方法请参考相关系统文档,以确保 Supervisor 在系统重启后能自动加载配置并启动应用程序。
---
## 总结
通过上述步骤,已成功配置 Supervisor 来管理 Java 应用程序。Supervisor 不仅能在后台自动启动程序,还能在程序异常退出时进行自动重启,同时提供便捷的管理与监控工具。希望本教程能帮助构建一个稳定可靠的应用运行环境。
---
---
# 历史部署页与替代方案
URL: https://tio-boot.com/zh/02_deployment/14.html
Source: zh/02_deployment/14.md
# 历史部署页与替代方案
本页保留历史路径,不再承载独立部署步骤。按部署目标选择已有完整章节:
- [胖包与瘦包]():依赖如何随应用交付。
- [FatJar 打包]():构建可执行 JAR。
- [Docker]():镜像与容器部署。
- [systemd]():Linux 服务管理。
- [Nginx 反向代理]():外部访问入口。
先确认 Java 与依赖版本,手动完成项目要求的数据库初始化,再启动应用。重启不应自动建表、清空数据或重置管理员。Windows 上重新打包遇到 JAR 占用时,核对并停止本项目进程后重试,不要停止所有 Java 服务。
启动验收应请求实际接口;仅有进程或端口不足以证明控制器、数据库及第三方集成已经就绪。
---
# 胖包与瘦包的打包与部署
URL: https://tio-boot.com/zh/02_deployment/15.html
Source: zh/02_deployment/15.md
# 胖包与瘦包的打包与部署
[[toc]]
在现代软件开发中,高效的打包与部署是确保应用程序稳定运行和快速迭代的关键。尤其是在使用 Java 开发的项目中,选择合适的打包方式不仅影响应用程序的体积和启动速度,还直接关系到部署的便捷性和灵活性。本文将深入探讨 **胖包**(Fat Package)与 **瘦包**(Thin Package)的概念、优势,并详细介绍如何使用 `deploy` 工具将 `tio-boot` 工程进行打包与部署。
---
## 简介
### 胖包与瘦包概述
在 Java 项目的打包过程中,**胖包**(Fat Package)和 **瘦包**(Thin Package)是两种常见的打包方式:
- **胖包**:包含所有的依赖 JAR 包,包括第三方库和自身代码。这种方式生成的可执行文件较大,但独立性强,适合在不同环境下快速部署,无需额外配置依赖环境。
- **瘦包**:仅包含自身代码和资源,不包含第三方库。依赖项需在目标环境中预先安装或单独管理。这种方式的优势在于打包文件体积较小,传输和部署更为快速,但对部署环境的依赖性更高。
### 瘦包的优势
- **体积小**:瘦包只包含应用程序自身的代码和资源,显著减少包体积,节省存储空间。
- **部署快**:由于体积更小,瘦包可以更快速地传输和部署,特别适用于网络传输或持续交付(CI/CD)场景。
### 部署时间
- **胖包**:50M 90 秒(大部分是消耗在了网络传输上)
- **瘦包**:152Kb 10 秒
## 创建工程
本文以 `tio-boot` 工程为例,展示如何创建一个包含底层依赖和代码的 Java 项目。
### 项目结构与依赖
首先,配置 `pom.xml` 文件,定义项目的依赖和构建属性:
```xml
4.0.0
nexus.io.full
thin
1.0.0
UTF-8
1.8
${java.version}
${java.version}
1.18.44
2.1.8
1.2.6
2.0.52
1.2.4
web-hello
nexus.io.full.thin.HelloApp
full
ch.qos.logback
logback-classic
1.3.3
nexus.io
tio-boot
${tio-boot.version}
nexus.io
hotswap-classloader
${hotswap-classloader.version}
nexus.io
jfinal-aop
${jfinal-aop.version}
com.alibaba.fastjson2
fastjson2
${fastjson2.version}
org.projectlombok
lombok
${lombok-version}
true
provided
junit
junit
4.12
test
```
### 启动类与控制器
创建项目的启动类 `HelloApp`:
```java
package nexus.io.full.thin;
import nexus.io.annotation.AComponentScan;
import nexus.io.tio.boot.TioApplication;
@AComponentScan
public class HelloApp {
public static void main(String[] args) {
long start = System.currentTimeMillis();
TioApplication.run(HelloApp.class, args);
long end = System.currentTimeMillis();
System.out.println((end - start) + "ms");
}
}
```
创建一个简单的控制器 `IndexController`:
```java
package nexus.io.full.thin.controller;
import nexus.io.annotation.RequestPath;
@RequestPath("/")
public class IndexController {
@RequestPath()
public String index() {
return "index";
}
}
```
以上代码完成了一个简单的 `tio-boot` 工程的创建,包含必要的依赖和基础代码。
## 打包
### 配置 Maven
为了灵活地管理不同环境下的打包需求,我们在 `pom.xml` 中配置了多个 `profiles`,分别适用于开发和生产环境。并使用 `maven-assembly-plugin` 插件来实现胖包与瘦包的打包。
#### pom.xml 配置
在 `pom.xml` 中定义不同的 `profiles`,如下所示:
```xml
development
true
org.springframework.boot
spring-boot-maven-plugin
2.7.18
true
${main.class}
org.projectlombok
--mode=dev
production
org.springframework.boot
spring-boot-maven-plugin
2.7.18
${main.class}
org.projectlombok
repackage
assembly
org.apache.maven.plugins
maven-assembly-plugin
3.3.0
false
assembly-${assembly}.xml
make-assembly
package
single
src/main/resources
**/*.*
```
**说明:**
- **开发环境 (`development`)**:使用 `spring-boot-maven-plugin` 插件进行打包和运行,支持快速迭代和调试。默认激活。
- **生产环境 (`production`)**:同样使用 `spring-boot-maven-plugin`,但配置略有不同,适用于生成生产环境所需的包。
- **Assembly 配置 (`assembly`)**:使用 `maven-assembly-plugin` 插件,根据不同的 assembly 描述文件(`assembly-full.xml` 或 `assembly-thin.xml`)生成胖包或瘦包。
#### assembly-full.xml 配置
`assembly-full.xml` 用于定义胖包的打包规则。将所有依赖的 JAR 包和必要的脚本文件打包到最终的 ZIP 文件中。
**文件路径**:项目根目录下创建 `assembly-full.xml`
```xml
zipPackage
zip
true
${project.build.directory}/lib
lib
*.jar
${basedir}/src/main/bin
unix
755
*.sh
${basedir}/src/main/bin
unix
service
*.service
${basedir}/src/main/bin
windows
*.bat
lib
```
**主要内容:**
- **格式 (`formats`)**:定义最终生成的包的格式为 ZIP。
- **文件集合 (`fileSets`)**:指定需要打包的文件及其在目标路径中的存放位置。
- `lib` 目录下的所有 JAR 包。
- `src/main/bin` 目录下的 Linux 和 Windows 启动脚本及 Service 文件。
- **依赖集合 (`dependencySets`)**:将项目的所有依赖包复制到 `lib` 目录下,确保运行时所需的所有依赖均已包含在包内。
#### assembly-thin.xml 配置
`assembly-thin.xml` 用于定义瘦包的打包规则。仅包含应用程序自身的代码和资源,不包含第三方依赖。
**文件路径**:项目根目录下创建 `assembly-thin.xml`
```xml
zipPackage
zip
true
${project.build.directory}
lib
*.jar
```
**主要内容:**
- **文件集合 (`fileSets`)**:仅包含项目构建输出目录下的 JAR 包,排除第三方依赖。
### 执行打包命令
根据不同的打包需求,执行以下命令:
- **全量包(胖包)**:
```shell
set JAVA_HOME=D:\java\jdk1.8.0_121
mvn clean package -DskipTests -Passembly -Dassembly=full
```
该命令使用 `assembly-full.xml` 配置文件,生成包含所有依赖的胖包。
- **瘦包**:
```shell
set JAVA_HOME=D:\java\jdk1.8.0_121
mvn clean package -DskipTests -Passembly -Dassembly=thin
```
该命令使用 `assembly-thin.xml` 配置文件,生成仅包含自身代码的瘦包。
### 启动应用
根据不同操作系统,使用相应的命令启动应用程序:
- **Windows 启动**:
```shell
java -Xverify:none -cp config;lib\*;static ${MAIN_CLASS}
```
**示例**:
```shell
java -Xverify:none -cp config;lib\*;static nexus.io.full.thin.HelloApp
```
- **Linux 启动**:
```shell
java -Xverify:none -cp ./config:./lib/*:./static ${MAIN_CLASS}
```
**示例**:
```shell
java -Xverify:none -cp ./config:./lib/*:./static nexus.io.full.thin.HelloApp
```
- **指定启动参数**:
可以通过在命令行中添加参数,指定应用程序的运行配置:
```shell
java -Xverify:none -cp ./config:./lib/*:./static nexus.io.full.thin.HelloApp --server.port=1003
```
## 部署
打包完成后,接下来是将应用程序部署到目标服务器。本文将介绍如何使用 `deploy` 工具实现自动化部署,支持多平台,并通过配置文件定义不同环境的部署步骤。
### 使用 `deploy` 工具 部署到 Docker
`deploy` 工具旨在简化部署流程,提高部署效率。它支持多平台,通过配置文件来定义不同环境的部署步骤。
#### 配置文件说明
部署过程涉及两个主要配置文件:
1. **`.build.txt`**:定义打包时的环境变量和命令。
2. **`.deploy.toml`**:定义部署时的服务器地址、压缩包路径、解压路径等。
##### .build.txt 配置
该文件用于配置项目在不同操作系统上的打包命令。以下是示例配置:
```txt
[win.env]
set JAVA_HOME=D:\java\jdk1.8.0_121
[win.build]
mvn clean package -DskipTests -Passembly -Dassembly=full
[linux.env]
export JAVA_HOME=/usr/java/jdk1.8.0_121
[linux.build]
mvn clean package -DskipTests -Passembly -Dassembly=full
[mac.env]
export JAVA_HOME=~/java/jdk1.8.0_121
[mac.build]
mvn clean package -DskipTests -Passembly -Dassembly=full
```
**说明:**
- **环境变量配置**:分别为 Windows、Linux 和 macOS 系统配置 `JAVA_HOME` 环境变量。
- **打包命令**:在不同系统下执行相应的 Maven 打包命令,生成胖包或瘦包。
##### .deploy.toml 配置
该文件用于定义不同环境(如开发、测试、生产)的部署操作。以下是示例配置:
```toml
[dev.upload-run]
url = "http://192.168.1.2:10405/deploy/file/upload-run/"
p = "123456"
b = ".build.txt"
file = "target/tio-boot-full-thin-demo01-1.0.0.zip"
d = "unzip/tio-boot-full-thin-demo01"
c1 = "mkdir -p /data/apps/tio-boot-full-thin-demo01 && mkdir -p backup/tio-boot-full-thin-demo01"
c2 = "[ -d /data/apps/tio-boot-full-thin-demo01 ] && cp -r /data/apps/tio-boot-full-thin-demo01 backup/tio-boot-full-thin-demo01/$(date +%Y%m%d_%H%M%S)"
c3 = "cp -r unzip/tio-boot-full-thin-demo01/* /data/apps/tio-boot-full-thin-demo01/"
c4 = "if [ $(docker ps -qa -f name=tio-boot-full-thin-demo01) ]; then docker stop tio-boot-full-thin-demo01 && docker rm -f tio-boot-full-thin-demo01; fi"
c5 = "cd /data/apps/tio-boot-full-thin-demo01/tio-boot-full-thin-demo01-1.0.0 && docker run -dit --name tio-boot-full-thin-demo01 --restart=always --net=host -v $(pwd):/app -w /app -e TZ=Asia/Shanghai -e LANG=C.UTF-8 litongjava/jdk:8u391-stable-slim java -Xverify:none -cp ./config:./lib/*:./static nexus.io.full.thin.HelloApp"
c = "docker ps | grep tio-boot-full-thin-demo01"
```
**配置项说明:**
- `url`: 上传文件的服务端地址。
- `p`: 密码,用于身份验证。
- `b`: 指定打包配置文件(即 `.build.txt`)。
- `file`: 上传的压缩包文件名。
- `d`: 解压路径。
- `c1-c5`: 一系列 Shell 命令,依次执行创建目录、备份旧版本、复制新文件、停止并删除旧容器、启动新容器等操作。
- `c`: 检查容器是否成功启动的命令。
#### 部署到 Docker
根据 `.build.txt` 和 `.deploy.toml` 配置文件,执行以下步骤将应用程序部署到 Docker 环境:
1. **胖包部署**:
使用全量包进行部署,执行以下命令:
```shell
deploy
```
该命令将根据 `.deploy.toml` 中 `dev.upload-run` 配置,将胖包上传至服务器,解压并通过 Docker 启动新的容器。
2. **瘦包部署**:
修改 `.build.txt` 中的 `-Dassembly=full` 为 `-Dassembly=thin`,然后执行:
```shell
deploy
```
该命令将生成瘦包并进行相应的部署操作。
### 部署到 systemctl
除了使用 Docker,`deploy` 工具还支持通过 `systemctl` 管理应用程序的部署。以下是具体步骤:
1. **配置 `.deploy.toml`**:
```toml
[test.upload-run]
url = "http://52.9.164.136:10405/deploy/file/upload-run/"
p = "123456"
b = ".build.txt"
file = "target/max-blog-app-backend-1.0.0.zip"
d = "unzip/max-blog-app-backend"
c1 = "mkdir -p backup/max-blog-app-backend"
c2 = "[ -d /data/apps/max-blog-app-backend ] && cp -r /data/apps/max-blog-app-backend backup/max-blog-app-backend/$(date +%Y%m%d_%H%M%S)"
c3 = "mkdir -p /data/apps/max-blog-app-backend"
c4 = "cp -r unzip/max-blog-app-backend/* /data/apps/max-blog-app-backend/"
c = "systemctl restart max-blog-app-backend"
```
2. **执行部署命令**:
```shell
deploy -e test
```
该命令将根据 `test.upload-run` 配置,上传并解压瘦包,然后通过 `systemctl` 重启服务。
#### 配置 Systemd 服务
为确保应用程序能够在系统启动时自动运行,并提供便捷的管理方式,我们需要配置 Systemd 服务。
**创建 Service 文件**
在服务器上创建一个 Systemd Service 文件,以管理 `max-blog-app-backend` 应用。
**创建文件**:
```shell
vi /etc/systemd/system/max-blog-app-backend.service
```
**填写内容**:
```ini
[Unit]
Description=max-blog-app-backend
After=network.target
[Service]
Type=simple
User=root
Restart=on-failure
RestartSec=5s
WorkingDirectory=/data/apps/max-blog-app-backend/max-blog-app-backend-1.0.0
ExecStart=/usr/java/jdk1.8.0_411/bin/java -Xverify:none -cp ./config:./lib/*:./static nexus.io.max.blog.MaxBlogApp --app.env=test
[Install]
WantedBy=multi-user.target
```
**配置说明**:
- **[Unit]**:
- `Description`: 服务描述。
- `After`: 指定服务启动的先后顺序,此处为网络服务启动后再启动本服务。
- **[Service]**:
- `Type`: 服务类型,此处为简单类型。
- `User`: 运行服务的用户。
- `Restart`: 服务失败时的重启策略。
- `RestartSec`: 重启前的等待时间。
- `WorkingDirectory`: 服务的工作目录。
- `ExecStart`: 启动服务的命令。
- **[Install]**:
- `WantedBy`: 指定服务的目标状态,此处为多用户运行级别。
#### 启动并启用服务
执行以下命令以启动并启用服务:
```shell
systemctl daemon-reload
systemctl start max-blog-app-backend
systemctl status max-blog-app-backend
systemctl enable max-blog-app-backend
```
**命令说明**:
- `daemon-reload`:重新加载 Systemd 配置文件。
- `start`:启动服务。
- `status`:查看服务状态。
- `enable`:设置服务开机自启。
#### .deploy.toml 配置
```toml
[test.upload-run]
url = "http://192.168.1.2:10405/deploy/file/upload-run/"
p = "123456"
b = ".build.txt"
file = "target/max-blog-app-backend-1.0.0.zip"
d = "unzip/max-blog-app-backend"
c1 = "mkdir -p backup/max-blog-app-backend"
c2 = "[ -d /data/apps/max-blog-app-backend ] && cp -r /data/apps/max-blog-app-backend backup/max-blog-app-backend/$(date +%Y%m%d_%H%M%S)"
c3 = "mkdir -p /data/apps/max-blog-app-backend"
c4 = "cp -r unzip/max-blog-app-backend/* /data/apps/max-blog-app-backend/"
c = "systemctl restart max-blog-app-backend"
```
### 使用 Shell 脚本管理应用
为了更灵活地管理应用程序的启动、停止和重启,可以使用 Shell 脚本进行控制。
#### 启动脚本 `tio.sh`
创建一个 Shell 脚本 `tio.sh`,用于在 Unix 系统中管理应用程序。该脚本提供了标准的服务管理功能,如启动、停止、重启和查看状态。
**脚本内容**:
```shell
#!/bin/sh
# chkconfig: 345 99 01
# description:tio
##########################
# 获取应用主目录
###########################
PRG="$0"
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`/"$link"
fi
done
##########################
# 自定义变量
###########################
MAIN_CLASS=""
JAVA_HOME=""
if [ -z "$JAVA_HOME" ]; then
JAVA_HOME=${JAVA_HOME:-$(dirname $(readlink -f $(which java)))/..}
fi
APP_HOME=`dirname "$PRG"`
APP_NAME=`basename "$PRG"`
PID_FILE=$APP_HOME/$APP_NAME.pid
CP=$APP_HOME/config:$APP_HOME/lib/*:$APP_HOME/static
# Java 命令行参数,根据需要开启下面的配置,注意等号前后不能有空格
# JAVA_OPTS="-Xms256m -Xmx1024m -Dundertow.port=80 -Dundertow.host=0.0.0.0"
# JAVA_OPTS="-Dundertow.port=80 -Dundertow.host=0.0.0.0"
#########################
# 定义函数
##########################
lock_dir=/var/lock/subsys
lock_file=$lock_dir/$APP_NAME
createLockFile(){
[ -w $lock_dir ] && touch $lock_file
}
start(){
# 检查 Java 命令是否存在
if [ ! -x "$JAVA_HOME/bin/java" ]; then
echo "Error: Java command not found at $JAVA_HOME/bin/java"
exit 1
fi
# 检查 MAIN_CLASS 是否定义
if [ -z "$MAIN_CLASS" ]; then
echo "Error: MAIN_CLASS is not defined. Please provide a valid main class."
exit 1
fi
[ -e $APP_HOME/logs ] || mkdir $APP_HOME/logs -p
if [ -f $PID_FILE ]
then
echo '应用已在运行...'
else
CMD="$JAVA_HOME/bin/java -Xverify:none ${JAVA_OPTS} -cp ${CP} ${MAIN_CLASS}"
echo $CMD
nohup $CMD >> $APP_HOME/logs/$APP_NAME.log 2>&1 &
echo $! > $PID_FILE
createLockFile
echo "[启动成功]"
fi
}
stop(){
if [ -f $PID_FILE ]
then
kill `cat $PID_FILE`
rm -f $PID_FILE
echo "[停止成功]"
else
echo '应用未在运行...'
fi
}
restart(){
stop
start
}
status(){
if [ -f $PID_FILE ]; then
echo "应用正在运行,PID:$(cat $PID_FILE)"
else
echo "应用未运行"
fi
}
##########################
# 执行动作
##########################
ACTION=$1
if [ -n "$2" ]; then
MAIN_CLASS=$2
fi
case $ACTION in
start)
start
;;
stop)
stop
;;
restart)
restart
;;
status)
status
;;
*)
echo "用法: $0 {start|stop|restart|status} [main class]"
;;
esac
```
**脚本说明:**
- **获取应用主目录**:通过解析符号链接,确定脚本的实际位置,从而获取应用的主目录。
- **自定义变量**:
- `MAIN_CLASS`:应用的主类,需要在执行脚本时提供。
- `JAVA_HOME`:Java 安装目录,若未设置则自动检测。
- `APP_HOME`:应用的主目录。
- `APP_NAME`:应用名称。
- `PID_FILE`:存储应用进程 ID 的文件。
- `CP`:类路径,包含配置、库和静态资源目录。
- `JAVA_OPTS`:Java 启动参数,可根据需要进行配置。
- **函数定义**:
- `start`:启动应用程序。
- `stop`:停止应用程序。
- `restart`:重启应用程序。
- `status`:查看应用程序状态。
- **执行动作**:根据传入的参数执行相应的操作(启动、停止、重启或查看状态)。
#### 配置 Service 文件
为方便使用 Systemd 管理应用程序,通过 `tio.sh` 脚本创建 Service 文件。
**创建文件**:
```shell
vi /etc/systemd/system/tio.service
```
**填写内容**:
```ini
[Unit]
Description=tio
After=network.target
[Service]
ExecStart=/opt/tio-hello/tio-hello-1.0/tio.sh start nexus.io.full.thin.HelloApp
ExecStop=/opt/tio-hello/tio-hello-1.0/tio.sh stop
ExecReload=/opt/tio-hello/tio-hello-1.0/tio.sh restart nexus.io.full.thin.HelloApp
Type=forking
PrivateTmp=true
[Install]
WantedBy=multi-user.target
```
**配置说明**:
- **[Unit]**:
- `Description`: 服务描述。
- `After`: 指定服务启动的先后顺序,此处为网络服务启动后再启动本服务。
- **[Service]**:
- `ExecStart`: 启动服务的命令,包含主类参数。
- `ExecStop`: 停止服务的命令。
- `ExecReload`: 重启服务的命令,包含主类参数。
- `Type`: 服务类型,此处为 `forking`,表示服务启动后会派生出子进程。
- `PrivateTmp`: 为服务提供独立的临时目录。
- **[Install]**:
- `WantedBy`: 指定服务的目标状态,此处为多用户运行级别。
**启动并启用服务**:
```shell
systemctl daemon-reload
systemctl start tio.service
systemctl status tio.service
systemctl enable tio.service
```
**命令说明**:
- `daemon-reload`:重新加载 Systemd 配置文件。
- `start`:启动服务。
- `status`:查看服务状态。
- `enable`:设置服务开机自启。
## 工具源码
本文中提到的 **deploy** 工具已开源,欢迎开发者参考和贡献。以下是相关源码仓库的链接:
- [deploy 工具](https://github.com/litongjava/deploy)
- [max-blog-app-backend 工具](https://github.com/litongjava/max-blog-app-backend)
通过这些工具,您可以实现高效、自动化的项目部署流程,显著提升开发与运维效率。
## 结语
本文详细介绍了 **胖包** 与 **瘦包** 的概念及其在 Java 项目中的应用,并通过 `tio-boot` 工程示例,展示了如何使用 Maven 进行不同类型的打包,以及如何使用 `deploy` 工具实现自动化部署。无论是基于 Docker 还是 Systemd 的部署方式,都能够满足不同环境下的需求。希望本文档能够帮助您快速上手并顺利完成项目的打包与部署。
如有任何疑问或建议,欢迎在项目的 GitHub 仓库中提出。
---
# 使用 JDK 8 构建基础库与验证运行环境
URL: https://tio-boot.com/zh/02_deployment/16.html
Source: zh/02_deployment/16.md
# 使用 JDK 8 构建基础库与验证运行环境
aio-socket 和 t-io 基础库使用 JDK 8 编译,可以统一 Java 8 平台 API 与字节码基线,便于应用在 Java 8 和较新的 Java 运行环境中复用构建产物。业务应用可以根据自身依赖选择运行环境,并对最终打包产物进行验证。
## 确认 Maven 使用 JDK 8
编译器由 Maven 实际运行的 Java 环境决定。在 Windows PowerShell 中,按本机安装位置设置当前终端:
```powershell
$env:JAVA_HOME = 'D:\java\jdk-1.8_411'
$env:PATH = "$env:JAVA_HOME\bin;$env:PATH"
java -version
mvn -version
```
两条命令都应显示 Java 1.8。IDE 的 Maven Runner、项目 SDK 和终端环境应分别核对;修改当前终端变量不会自动更新已经运行的 IDE 或服务进程。
`source` 和 `target` 用于约束源码语法与字节码格式。使用 JDK 8 实际编译,还能使编译时解析的平台方法来自 Java 8。仅在较新 JDK 上配置 `source/target=1.8`,并不等于使用 JDK 8 的平台 API。
## 按依赖顺序构建
先进入 aio-socket 源码目录,执行:
```shell
mvn clean install -Dmaven.test.skip=true -Dmaven.javadoc.skip=true -Dgpg.skip=true
```
再进入 t-io 聚合工程根目录,执行同样的命令。`clean` 清除之前的编译结果,`install` 将构建产物安装到本机 Maven 仓库。以上命令用于本地依赖安装,跳过测试编译与执行、文档生成和签名,并不代表回归测试通过。安装后还需执行对应模块的测试和下面的应用验证;发布流程按项目的发布规范执行。
基础库的 Maven 构建在验证阶段检查 Java 环境,要求使用 JDK 8。这样可以在编译开始前明确构建环境,保持产物的一致性。应用自身的编译配置不会重新编译依赖 JAR,因此应先完成基础库构建,再重新打包应用。
## 验证最终应用
1. 重新打包应用,确认使用了本地新构建的依赖。
2. 使用 Java 8 启动应用并访问一个真实 HTTP 接口,验证连接接收、请求解析和响应。
3. 重复访问,并按业务需要覆盖持续连接、大响应等路径。
4. 如果还支持 Java 21,在 Java 21 下运行同一构建产物,重复上述验证。
已运行的 JVM 不会因本地 Maven 仓库更新而自动替换已加载的类。验证新产物时应重新启动对应的应用进程。
更多构建参数见 [Maven 项目配置]()。关于源码、字节码与平台 API 的区别,参见 [Maven Compiler Plugin 官方说明](https://maven.apache.org/plugins/maven-compiler-plugin/examples/set-compiler-source-and-target.html)。
---
# 02_deployment
URL: https://tio-boot.com/zh/02_deployment/
Source: zh/02_deployment/index.md
# 02_deployment
本目录包含 26 个文件和 0 个子目录,下列摘要基于文件名、一级标题或资源类型整理。
## 文件清单
- [readme.md](): 当前目录的导航与文件摘要。
- [01.md](): 文档《使用 Maven Profile 实现分环境打包 tio-boot 项目》。
- [02.md](): 文档《Maven 项目配置详解:依赖与 Profiles 配置》。
- [03.md](): 文档《tio-boot 打包成 FatJar》。
- [04.md](): 文档《使用 GraalVM 构建 tio-boot Native 程序》。
- [05.md](): 文档《使用 Docker 部署 tio-boot》。
- [06.md](): 文档《部署到 Fly.io》。
- [07.md](): 文档《部署到 AWS Lambda》。
- [08.md](): 文档《到阿里云云函数》。
- [09.md](): 文档《使用 Deploy 工具部署》。
- [10.md](): 文档《使用Systemctl启动项目》。
- [11.md](): 文档《使用 Jenkins 部署 Tio-Boot 项目》。
- [12.md](): 文档《使用 Nginx 反向代理 Tio-Boot》。
- [13.md](): 文档《使用 Supervisor 管理 Java 应用》。
- [14.md](): 文档《历史部署页与替代方案》。
- [15.md](): 文档《胖包与瘦包的打包与部署》。
- [16.md](): 文档《使用 JDK 8 构建基础库与验证运行环境》。
- [image-1.png](): 图片资源,用于本目录文档配图或界面示意。
- [image-2.png](): 图片资源,用于本目录文档配图或界面示意。
- [image-3.png](): 图片资源,用于本目录文档配图或界面示意。
- [image-4.png](): 图片资源,用于本目录文档配图或界面示意。
- [image-5.png](): 图片资源,用于本目录文档配图或界面示意。
- [image-6.png](): 图片资源,用于本目录文档配图或界面示意。
- [image-7.png](): 图片资源,用于本目录文档配图或界面示意。
- [image-8.png](): 图片资源,用于本目录文档配图或界面示意。
- [image.png](): 图片资源,用于本目录文档配图或界面示意。
---
# tio-boot 整合 Logback
URL: https://tio-boot.com/zh/03_logging/01-logback.html
Source: zh/03_logging/01-logback.md
# tio-boot 整合 Logback
[[toc]]
`tio-boot` 并未绑定任何特定的日志框架,而是使用 `slf4j` 作为日志门面。本文将介绍如何在开发 `tio-boot` 应用时集成 `Logback` 作为日志实现。
## Logback
建议在开发 `tio-boot` 应用时使用 `Logback` 作为日志实现。以下是集成 `Logback` 的详细步骤:
### 1. 引入 Logback 依赖
在项目的 `pom.xml` 文件中添加 `Logback` 依赖:
java 1.7
```xml
ch.qos.logback
logback-classic
1.3.3
```
java 1.8
```xml
org.slf4j
slf4j-api
2.0.1
ch.qos.logback
logback-classic
1.3.3
```
java 11
```xml
ch.qos.logback
logback-classic
1.4.12
```
### 2. 配置 `logback.xml`
在项目的 `resources` 目录下创建 `logback.xml` 配置文件,内容如下:
```xml
${CONSOLE_LOG_PATTERN}
${CONSOLE_LOG_PATTERN}
${LOG_HOME}/log.%d{yyyyMMddHH}.%i.log
180
100MB
```
### 日志输出示例
控制台输出的日志示例如下:
```
2024-09-10 05:47:00.022 [tio-group-76] INFO DefaultHttpRequestHandler.handler:232 - access:GET:/ok
```
生成的日志文件名称示例:
- `log.2024091006.0.log`
- `log.2024091007.0.log`
### 配置说明
该配置文件基于 Logback 框架,包含了日志的输出格式、日志级别、日志文件的滚动策略以及特定模块的日志级别设置。以下是各部分的详细说明:
#### 1. XML 声明
```xml
```
表示这是一个使用 UTF-8 编码的 XML 文档。
#### 2. `` 标签
这是 Logback 配置文件的根元素,包含所有日志相关的配置。
- `debug="false"`:禁用调试模式。如果设为 `true`,Logback 将输出更多调试信息。
- `xmlns` 和 `xsi:schemaLocation`:定义 Logback 的 XML 命名空间和模式文件的位置。
#### 3. `` 标签
用于定义可复用的变量,便于配置管理和维护。
- `LOG_HOME`:定义日志文件的存储目录为 `logs` 目录。
- `CONSOLE_LOG_PATTERN`:定义日志的输出格式,主要用于控制台和文件输出。
- `%d{yyyy-MM-dd HH:mm:ss.SSS}`:日志的日期时间,精确到毫秒。
- `[%thread]`:输出当前线程的名称。
- `%-6level`:日志级别,左对齐,占 6 个字符宽度。
- `%logger{1}`:输出日志发出者的名字,通常是类名,`{1}` 表示只显示最后一级包名或类名。
- `%M`:输出日志方法的名称。
- `%L`:输出日志所在的行号。
- `%m`:输出日志内容。
- `%n`:换行符。
#### 4. 控制台输出 (`` 标签)
定义了控制台日志输出的配置:
```xml
${CONSOLE_LOG_PATTERN}
```
- `name="STDOUT"`:日志输出器的名称。
- `class="ch.qos.logback.core.ConsoleAppender"`:指定为控制台输出器。
- ``:定义日志的编码器,使用 `PatternLayoutEncoder` 并应用前面定义的日志格式。
#### 5. 文件输出 (`` 标签)
定义了日志文件的输出配置,使用滚动策略:
```xml
${CONSOLE_LOG_PATTERN}
${LOG_HOME}/log.%d{yyyyMMddHH}.%i.log
180
100MB
```
- `name="FILE"`:日志输出器的名称。
- `class="ch.qos.logback.core.rolling.RollingFileAppender"`:指定为文件输出器,并使用滚动策略。
- ``:定义基于时间和文件大小的滚动策略。
- `fileNamePattern`:日志文件的命名规则,格式为 `log.日期小时.序号.log`,每小时生成一个新文件。
- `maxHistory`:日志文件保留的最大天数,此处为 180 天。
- `maxFileSize`:每个日志文件的最大大小,此处为 100MB。
#### 6. Spring 和 Hibernate 特定模块的日志级别设置
针对特定模块调整日志级别,便于调试:
- **Spring 框架**:
```xml
```
设置 Spring 框架的日志级别为 `info`。
- **Hibernate**:
```xml
```
设置 Hibernate 的特定日志类的日志级别,用于显示 SQL 语句和查询参数。
#### 7. MyBatis 和 SQL 相关日志配置
针对 MyBatis 和 SQL 连接相关类设置日志级别:
```xml
```
- `com.apache.ibatis`:MyBatis 框架的日志级别设置为 `TRACE`。
- `java.sql.Connection`、`java.sql.Statement`、`java.sql.PreparedStatement`:数据库连接和 SQL 语句执行的日志级别设置为 `DEBUG`,用于调试 SQL 执行的详细信息。
#### 8. 根日志记录器 (`` 标签)
定义根日志记录器的日志级别和输出目标:
```xml
```
- `level="info"`:设置根日志记录器的默认日志级别为 `info`,即只输出 `info` 及以上级别的日志(如 `warn`、`error`)。
- ``:将日志输出到控制台。
- ``:将日志输出到文件。
### 总结
- **输出方式**:该配置实现了日志的控制台和文件双重输出。
- **滚动策略**:日志文件按小时滚动,每个文件最大为 100MB,日志文件保留 180 天。
- **输出格式**:通过 `CONSOLE_LOG_PATTERN` 统一定义日志输出格式,包括日期、线程、日志级别、类名、方法名、行号和日志内容。
- **模块级别设置**:针对 Spring、Hibernate、MyBatis 等特定模块,调整了日志输出级别,以便更好地调试和监控应用。
通过上述配置,`tio-boot` 应用能够高效、灵活地管理日志输出,满足开发和运维的需求。
---
# 日志
URL: https://tio-boot.com/zh/03_logging/
Source: zh/03_logging/index.md
# 日志
本章介绍 tio-boot 应用的日志配置。完成入门 Demo 和部署后,可以配置日志输出,观察应用运行情况。
## 文章
- [整合 Logback]()
独立项目 tio-log-server 见[项目实战中的 tio-log-server]()。
---
# 配置参数
URL: https://tio-boot.com/zh/04_configuration/01.html
Source: zh/04_configuration/01.md
# 配置参数
## 配置文件
tio-boot 使用 `EnvUtils` 统一读取应用参数与私密配置。应用策略可以随 `app.properties` 发布,密钥保存在不提交的配置文件中,避免散落在业务代码。
文件按以下顺序合并,后加载的同名文件配置覆盖先前值:
1. 用户目录 `.env`、`secrets.txt`。
2. 类路径 `app.properties`、工作目录 `app.properties`。
3. 类路径 `app-环境.properties`、工作目录 `app-环境.properties`。
4. 类路径 `.env`,工作目录 `.env`、`secrets.txt`、`my.txt`。
加载分为环境选择与配置合并两个阶段。先读取基础文件和本地覆盖文件,结合程序设置、命令行、JVM 属性与环境变量确定 `app.env`,再追加对应环境文件。因此工作目录 `secrets.txt` 中的 `app.env=test` 也能选择 `app-test.properties`。环境只选择一次,环境文件中的 `app.env` 不会触发另一个环境文件的加载,合并配置中的 `app.env` 保持为本次选择的环境。环境名称支持字母、数字、下划线和短横线。
环境文件可以省略;存在的文件若解析失败,加载会抛出异常,保留此前已发布的配置,修正文件后可以重试。`EnvUtils.load()` 与 `EnvUtils.load(args)` 在首次成功后不重复加载,也不替换首次加载的启动参数;并发启动调用会串行完成初始化。
用户目录私密配置与类路径应用配置会合并保留;同名值按上述顺序确定。工作目录是启动 Java 进程时所在目录,建议从保存本地 `secrets.txt` 的服务目录启动。类路径中的私密文件会被打包,应优先使用工作目录文件或部署环境变量。
取值优先级为 `EnvUtils.set` 设置值、命令行参数、JVM 系统属性、系统环境变量、合并后的文件配置。点号名称也可使用大写下划线环境变量形式,例如 `server.port` 对应 `SERVER_PORT`。
```java
package com.example.config;
import nexus.io.tio.utils.environment.EnvUtils;
public class LoginConfig {
public String appId() {
return EnvUtils.get("WECHAT_MINI_APP_ID");
}
public int sessionIdleSeconds() {
return EnvUtils.getInt("identity.session.idle.seconds", 7200);
}
}
```
框架启动时自动加载配置;独立工具程序可先调用 `EnvUtils.load()`。不要输出 AppSecret、AccessKey、密码或身份加密密钥。
::: warning 不要在 TioApplication.run 之前调用 EnvUtils.load()
`EnvUtils.load()` 只会加载一次。如果在 `main` 方法里先调用它,`TioApplication.run` 启动时会跳过加载,**命令行参数也不再解析**,`--app.env=prod`、`--server.port=8080` 这样的参数会失效。
```java
public static void main(String[] args) {
// 错误:--app.env=prod 不会生效
EnvUtils.load();
TioApplication.run(App.class, args);
}
```
框架会自动加载配置,`main` 方法里不需要调用。确实要在启动前读取配置时,使用 `EnvUtils.load(args)`,它会同时解析命令行参数。
:::
### 可执行 JAR 的配置与依赖核对
应用同时引入多个业务模块时,各模块应在自己的 POM 中声明经过验证的 `tio-utils` 依赖,并与应用选定的框架依赖保持兼容。Maven 会按依赖路径进行选择,较短路径上的传递依赖可能先于其他模块的依赖被选中。维护这些模块时,应优先更新依赖来源模块的声明,重新构建安装,再构建应用,让其他使用该模块的应用也能获得一致的配置加载行为。应用级依赖管理适合统一约束整个依赖图;暂时覆盖依赖后,仍应同步维护来源模块的依赖声明。
构建前可查看实际依赖选择:
```shell
mvn dependency:tree -Dincludes=nexus.io:tio-utils -Dverbose
```
使用 Spring Boot 打包插件时,应用配置位于 `BOOT-INF/classes/app.properties`,依赖位于 `BOOT-INF/lib/`。更新底层源码后,需要先安装对应构建产物,再重新打包应用,使可执行 JAR 包含已验证的依赖。配置加载日志中的文件名用于识别读取来源;需要确认业务参数时,应检查参数是否存在,不输出其值。
下面的独立诊断程序只加载配置,不初始化数据库或业务服务。把它放在待验证应用的运行类路径中执行,可以核对实际加载的类、配置资源和必要参数:
```java
package com.example.config;
import nexus.io.tio.utils.environment.EnvUtils;
public class ConfigurationCheck {
public static void main(String[] args) {
EnvUtils.load(args);
ClassLoader loader = Thread.currentThread().getContextClassLoader();
if (loader == null) {
loader = EnvUtils.class.getClassLoader();
}
System.out.println("Configuration resource: " + loader.getResource("app.properties"));
System.out.println("Configuration loader: " + EnvUtils.class.getResource("EnvUtils.class"));
String jdbcUrl = EnvUtils.get("jdbc.url");
if (jdbcUrl == null || jdbcUrl.trim().isEmpty()) {
throw new IllegalStateException("Required configuration jdbc.url is missing");
}
System.out.println("Database configuration is present");
}
}
```
数据库初始化与表初始化应按顺序执行:先读取配置、启动数据源与 ActiveRecord 插件,再执行表初始化。需要数据库的应用可以在启动数据源前校验必需参数,便于明确定位配置条件。
## 常用配置
### 配置加载职责与追加
`Prop` 负责单文件读取与属性合并;`PropUtils` 提前创建独立的全局配置对象,后续加载将属性追加到该对象,后追加的同名属性覆盖已有值。单文件缓存与全局配置分离,缓存键包含文件来源和编码,可以区分不同目录的同名文件。
`PropUtils.use(filename)` 只加载并追加指定文件。需要环境文件时使用 `EnvUtils.load(filename)`,或者通过 `EnvUtils.load(env, filename)` 显式指定环境。这两个具名加载方法每次调用都会执行追加,不受默认启动加载的一次性标记限制。旧的 `PropUtils.use(filename, env)` 重载保留兼容入口,环境处理委托给 `EnvUtils`。
```java
package com.example.config;
import nexus.io.tio.utils.environment.EnvUtils;
import nexus.io.tio.utils.environment.PropUtils;
public class ConfigurationExample {
public static void main(String[] args) {
EnvUtils.load(args);
// Append a single optional file after startup configuration.
PropUtils.appendIfExists("feature.properties");
// Append billing.properties and its optional billing-test.properties.
EnvUtils.load("test", "billing.properties");
int timeout = EnvUtils.getInt("billing.timeout.seconds", 30);
System.out.println("Billing timeout: " + timeout);
}
}
```
`appendIfExists` 跳过不存在的文件,存在但格式不正确的文件会报告错误。`PropUtils.useless(filename)` 仅移除单文件缓存;`PropUtils.clear()` 清空全局属性和缓存,保留全局对象本身,不重置 `EnvUtils` 的启动加载标记。业务启动代码通常无需调用这两个清理方法。
### 设置静态文件目录
将 app.properties 中配置
```ini
server.resources.static-locations = classpath:/pages
```
将静态文件放到 pages 目录下即可 DefaultHttpRequestHandler 的 processStatic 类会处理静态文件
### 设置文件上传大小
```ini
# 设置最大请求大小(包含所有文件)单位 字节,这里设置为1G
http.multipart.max-request-size=73741824
# 设置最大文件大小,单位字节,这里设置为1G
http.multipart.max-file-size=73741824
```
默认允许的文件大小是2M,超过2M会出现下面的的错误
```
Request body is too large. Current size: 9114425 bytes (8.69 MB), max allowed: 2097152 bytes (2.00 MB).
```
设置为10M,10 * 1024 * 1024 = 10485760
```ini
http.multipart.max-request-size=10485760
http.multipart.max-file-size=10485760
```
## 配置概览
- `server.address=127.0.0.1`:指定服务器监听的 IP 地址。此处为本地地址,意味着服务器仅在本机上可访问。
- `server.port=8080`:定义服务器监听的端口号。在这里,端口被设置为 8080。
- `server.listening.enable=true`: 是否监听端口
- `server.context-path=/myapp`:设置应用的上下文路径,这里的应用将通过 `/myapp` 路径访问。
- `server.404=/404`:定义 404 错误(页面未找到)时的路由地址。用户将被重定向到 `/404` 路径。
- `server.500=/500`:指定 500 错误(服务器内部错误)时的路由地址。对应的路径是 `/500`。
- `server.resources.static-locations=classpath:/pages`:设定静态页面的位置,此例中静态资源位于类路径下的 `/pages` 目录,默认值也是 classpath:/pages。
- `server.resources.auto.reload=true`: 是否开启静态文件自动加载,开启后在运行是 修改静态文件会立即生效
- `server.heartbeat.timeout`: 设置心跳检测时间,单位毫秒,同时兼容 `server.beartbeat.timeout` 写法
- `server.session.enable`: 开启 session,默认禁用
- `server.read.buffer.size`: 设置读取数据的缓冲区大小
- `server.dev.mode=true`:开启或关闭开发模式。当设为 `true` 时,将启用更详细的日志记录,并激活其他框架的开发模式特性,如热加载功能。
- `server.http.request.printUrl` : 打印请求地址
- `server.enable.session`:是否开启使用 HTTP 会话。
- `server.http.controller.printMapping`:决定是否在启动时打印路由映射信息,有助于调试路由问题。
- `server.http.controller.writeMapping`:选择是否将路由信息写入文件,便于记录和审查。
- `server.http.request.printReport`:设置是否打印请求信息。这通常在开发环境下使用,以便于跟踪和调试。
- `http.max.live.time.of.static.res=0`:设置页面文件的缓存时间。在开发环境中,通常设置为 0 以禁用缓存,而在生产环境中,可以设置为较长的时间(如 3600 秒或 600 秒)以提高性能。
- `http.enable.request.limit`: 是否开启请求限流
- `http.max.requests.per.second` : 开启限流后,每秒请求数量,默认 10
- `http.checkHost`:用于检查和验证 HTTP 请求的主机头。
- `http.multipart.max-request-size`: 设置请求体的大小
- `http.multipart.max-file-size`: 设置上传文件的大小
- `http.response.showExceptionDetails`: 当请求出现异常时,在 HttpResponse 中显示异常信息,默认是是 false
- `http.response.header.showServer`: 用于控制是否在 HTTP 响应头中添加 Server 信息
- `app.env`:定义应用的运行环境。根据 `app.env` 的不同值,可以加载不同的配置文件,以适应不同的开发、测试或生产环境。
- `tcp.core.diagnostic=true`:显示 tio-core 运行过程中的一些调试信息
- `app.tenant=1`
tio-boot 配置参考源码 nexus.io.tio.boot.constatns.ConfigKeys
## 调优配置
### server.protocol
```
server.protocol=http
```
用于指定服务器对外通信时所使用的协议类型,不同取值将直接影响服务器可接受的连接方式以及底层网络实现。
**可选值说明:**
* **auto**
自动模式。服务器会根据客户端的连接方式自动适配,支持 **HTTP、WebSocket 以及 TCP** 三种协议,适合需要兼容多种客户端或部署环境的场景。
* **http**
仅启用 **HTTP 协议**。服务器只接受标准 HTTP 请求,不支持 WebSocket 或 TCP 直连,适用于纯 HTTP 接口服务。
* **websocket**
仅启用 **WebSocket 协议**。服务器只接受 WebSocket 连接,适合需要长连接、实时通信的场景。
* **tcp**
仅启用 **TCP 协议**。服务器通过原生 TCP 方式进行通信,通常用于自定义协议或高性能、低开销的内部服务通信。
**使用建议:**
* 如果不确定客户端类型,或需要同时支持多种接入方式,推荐使用 `auto`。
* 如果服务对外只暴露 REST 接口,选择 `http` 更加清晰、安全。
* 实时推送或双向通信场景,建议使用 `websocket`。
* 对性能或协议控制要求较高的内部系统,可选择 `tcp`。
### server.listening.enable
```
server.listening.enable 默认值为 true。
```
当配置为 false 时,tio-boot 不会执行 HTTP Server 启动逻辑,也不会监听端口。
适用场景包括:
- 只需要启动后台任务
- 只需要初始化配置或插件
- 与其他 Web 容器集成时,不希望 Tio Boot 自带 HTTP Server 占用端口
- 在测试环境中不需要启动 HTTP 服务
---
---
# 服务器监听器
URL: https://tio-boot.com/zh/04_configuration/02.html
Source: zh/04_configuration/02.md
# 服务器监听器
## 1. 实现自定义服务器监听器
为了在 Tio-Boot 项目中创建一个自定义的服务器监听器,我们可以通过实现 `TioBootServerListener` 接口来拦截服务器启动的关键点,并添加一些自定义的启动逻辑。下面是一个实现自定义监听器的示例,`MyServerListener` 类负责在服务器启动后执行额外的操作。
### 示例代码
```java
package nexus.io.tio.web.hello.config;
import nexus.io.hotswap.watcher.HotSwapWatcher;
import nexus.io.hotswap.wrapper.tio.boot.TioBootArgument;
import nexus.io.hotswap.wrapper.tio.boot.TioBootRestartServer;
import nexus.io.jfinal.aop.Aop;
import nexus.io.jfinal.aop.AopManager;
import nexus.io.tio.boot.constatns.ConfigKeys;
import nexus.io.tio.boot.context.Context;
import nexus.io.tio.boot.server.TioBootServerListener;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class MyServerListener implements TioBootServerListener {
protected static volatile HotSwapWatcher hotSwapWatcher;
/**
* 在服务器启动前调用
*/
@Override
public void boforeStart(Class>[] primarySources, String[] args) {
// 可以在此处添加启动前的逻辑,例如参数校验或初始化操作
}
/**
* 服务器启动后调用
*/
@Override
public void afterStarted(Class>[] primarySources, String[] args, Context context) {
}
}
```
### 说明:
1. **`boforeStart()` 方法**:可用于在服务器启动前执行操作,当前未添加任何逻辑。
2. **`afterStarted()` 方法**:在服务器启动后执行,如果当前环境为开发环境 (`dev`),则启动 `HotSwapWatcher` 来监控类文件的变化,从而在开发阶段实现类热加载。
## 2. 注册服务器监听器
为了使自定义的监听器能够生效,我们需要在应用启动前将它注册到 Aop 容器中。通过创建 `TioBootServerListenerConfig` 类,并在启动时将监听器注入 Aop 容器,可以确保 `MyServerListener` 能够在应用启动过程中被正确调用。
### 注册监听器代码
```java
package nexus.io.tio.web.hello.config;
import nexus.io.annotation.ABean;
import nexus.io.annotation.BeforeStartConfiguration;
import nexus.io.tio.boot.server.TioBootServerListener;
@AConfiguration
public class TioBootServerListenerConfig {
@Initialization
public void tioBootServerListener() {
// 注册自定义监听器
TioBootServer.me().setTioBootServerListener(new MyServerListener());
}
}
```
### 说明:
1. **`BeforeStartConfiguration` 注解**:用于在应用启动前进行配置操作,这里用于注册服务器监听器。
2. **`@Initialization` 注解**:用于将 初始化配置,使其能够在启动时被调用。
## 3. 总结
通过实现 `TioBootServerListener` 接口并将自定义监听器注册到 Aop 容器中,可以在 Tio-Boot 项目中定制服务器启动过程中的行为。
这份文档介绍了如何实现和注册服务器监听器的完整流程,并为开发者提供了灵活的方式来控制服务器启动行为。
---
# 内置缓存系统 AbsCache
URL: https://tio-boot.com/zh/04_configuration/03.html
Source: zh/04_configuration/03.md
# 内置缓存系统 AbsCache
在构建高性能的网络应用时,缓存系统起着至关重要的作用。Tio-boot 框架内置了一种轻量级的缓存系统——`AbsCache`。本文将详细介绍如何使用 `AbsCache` 来缓存数据,并通过 `CacheFactory` 实现缓存实例的注册和管理。Tio-boot 框架内部状态都使用 AbsCache 来进行存储
## 简介
### 1. 什么是 `AbsCache`
`AbsCache` 是 Tio-boot 框架中的抽象类,用于定义缓存的核心功能。它提供了基本的缓存操作,例如存储、获取、删除缓存条目,并支持配置缓存的生存时间(TTL, Time to Live)和空闲时间(TTI, Time to Idle)。`AbsCache` 支持各种实现,如基于 `Map` 的缓存。
#### `AbsCache` 核心功能
- **TTL(存活时间)**:缓存条目从创建开始有效,超过设定时间后将自动过期。
- **TTI(空闲时间)**:缓存条目在最后一次访问后有一定的有效期,如果在此期间未再次访问,也会过期。
## 使用
### 1. CacheFactory 和 AbsCache 的使用流程
使用 `AbsCache` 时,主要通过 `CacheFactory` 工厂类来管理缓存实例。它可以注册并返回缓存实例,允许开发者根据需要缓存数据。
#### 核心步骤:
1. **注册缓存实例**:通过 `CacheFactory.register()` 方法,创建或获取一个 `AbsCache` 实例。
2. **缓存数据**:调用缓存实例的 `put()` 方法,将数据存储到缓存中。
3. **获取缓存数据**:使用 `get()` 方法,从缓存中获取数据。
4. **移除缓存数据**:通过 `remove()` 方法,删除缓存中的某个键值对。
### 2. 使用示例
```java
CacheFactory cacheFactory = TioBootServer.me().getServerTioConfig().getCacheFactory();
AbsCache absCache = cacheFactory.register("test001",300L,3000L);
absCache.put("key", "value");
```
下面是一个简单的示例代码,展示如何向 `AbsCache` 缓存系统添加数据
#### 解释:
1. **缓存工厂获取**:通过 `TioBootServer` 获取 `CacheFactory` 实例.
2. **添加缓存名称**:通过`register`,添加缓存名称到缓存系统。
3. **添加缓存数据**:通过 absCache 添加数据。
### 3. 实现类 `ConcurrentMapCache`
`ConcurrentMapCache` 是 `AbsCache` 的一个具体实现,它使用 `ConcurrentHashMap` 来保存缓存数据,并提供 TTL 和 TTI 的过期机制。
#### 主要功能:
- **数据存储**:`put()` 方法用于将数据存储到缓存,并为缓存项设定过期时间。
- **数据获取**:`get()` 方法用于从缓存中获取数据,并更新 TTI 时间。
- **数据删除**:`remove()` 方法用于删除缓存项,并调用删除监听器。
缓存的有效截止时间取 TTL 截止时间与 TTI 截止时间中较早的一个。`get()` 只延长尚未过期条目的 TTI,不突破从本次 `put()` 开始计算的 TTL 上限;读取已经过期的条目返回 `null`,不会续活。
```java
import nexus.io.tio.utils.cache.mapcache.ConcurrentMapCache;
import nexus.io.tio.utils.cache.mapcache.ConcurrentMapCacheFactory;
public class CacheExample {
public void cacheSession() {
ConcurrentMapCache cache = ConcurrentMapCacheFactory.INSTANCE.register("session-example", 3600L, 600L);
cache.put("session-id", "session-data");
String value = cache.get("session-id", String.class);
long remainingMillis = cache.ttl("session-id");
}
}
```
以上条目最多保存一小时,连续十分钟没有读取也会过期。仅需要空闲过期时可将 TTL 传为 `null`;两者都为 `null` 表示不设置过期时间。`ttl()` 返回剩余毫秒数,条目不存在或没有截止时间时返回 `-1`。`ttl()`、`size()` 和键集合查询不会刷新 TTI。
缓存使用共享守护线程安排清理,TTI 延后时会在原任务触发后重新安排清理;覆盖、删除或清空条目会取消旧任务。同名新条目有独立的过期状态,旧任务不会删除新值。读取也会检查截止时间,保证后台清理稍有延迟时不返回过期值。永久条目不安排定时清理。
TTL 和 TTI 到期都以 `RemovalCause.EXPIRED` 通知移除监听器,显式删除使用 `EXPLICIT`。监听器在缓存锁之外执行,应保持简短,避免阻塞清理线程。写入和删除请使用缓存 API;`asMap()` 和键集合保留底层视图,直接修改视图不会完整维护过期元数据。
### 4. 使用场景
`AbsCache` 适用于以下场景:
- **短期缓存**:需要快速访问的数据,例如用户会话信息、临时配置等。
- **限制过期时间**:当需要在一定时间内缓存数据,且希望在不使用后自动过期。
- **轻量级缓存系统**:`AbsCache` 的实现轻便、易于集成,非常适合内存敏感的场景。
## 获取所有缓存数据
IndexController 示例
```java
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import com.jfinal.kit.Kv;
import nexus.io.annotation.RequestPath;
import nexus.io.model.body.RespBodyVo;
import nexus.io.tio.boot.server.TioBootServer;
import nexus.io.tio.utils.cache.AbsCache;
import nexus.io.tio.utils.cache.CacheFactory;
@RequestPath("/internal")
public class InternalController {
@RequestPath()
public RespBodyVo cache() {
CacheFactory cacheFactory = TioBootServer.me().getServerTioConfig().getCacheFactory();
Map map = cacheFactory.getMap();
Kv kv = Kv.by("cacheFactory", cacheFactory);
kv.set("map", map);
for (Map.Entry entry : map.entrySet()) {
String cacheName = entry.getKey();
AbsCache cache = entry.getValue();
Collection keysCollection = cache.keysCollection();
Map cacheMap = new HashMap<>();
for (String key : keysCollection) {
cacheMap.put(key, cache.get(key));
}
kv.set("cache_" + cacheName, cacheMap);
}
return RespBodyVo.ok(kv);
}
}
```
返回数据格式示例
下面是一个返回的 JSON 数据格式示例:
```json
{
"data": {
"cache_TIO_HTTP_STATIC_RES_CONTENT": {},
"cacheFactory": "INSTANCE",
"cache_nexus.io.tio.utils.lock.LockUtilsOBJ": {},
"cache_nexus.io.tio.utils.lock.LockUtilsRW": {
"StrCache:getLowercase-606089688": {
"queueLength": 0,
"fair": false,
"readHoldCount": 0,
"readLockCount": 0,
"writeHoldCount": 0,
"writeLocked": false,
"writeLockedByCurrentThread": false
},
"StrCache:getBytes653303108": {
"queueLength": 0,
"fair": false,
"readHoldCount": 0,
"readLockCount": 0,
"writeHoldCount": 0,
"writeLocked": false,
"writeLockedByCurrentThread": false
},
"StrCache:getLowercase-1844712829": {
"queueLength": 0,
"fair": false,
"readHoldCount": 0,
"readLockCount": 0,
"writeHoldCount": 0,
"writeLocked": false,
"writeLockedByCurrentThread": false
},
"StrCache:getBytes3151881": {
"queueLength": 0,
"fair": false,
"readHoldCount": 0,
"readLockCount": 0,
"writeHoldCount": 0,
"writeLocked": false,
"writeLockedByCurrentThread": false
},
"StrCache:getLowercase1217813246": {
"queueLength": 0,
"fair": false,
"readHoldCount": 0,
"readLockCount": 0,
"writeHoldCount": 0,
"writeLocked": false,
"writeLockedByCurrentThread": false
},
"StrCache:getBytes3178825": {
"queueLength": 0,
"fair": false,
"readHoldCount": 0,
"readLockCount": 0,
"writeHoldCount": 0,
"writeLocked": false,
"writeLockedByCurrentThread": false
},
"StrCache:getLowercase1577755768": {
"queueLength": 0,
"fair": false,
"readHoldCount": 0,
"readLockCount": 0,
"writeHoldCount": 0,
"writeLocked": false,
"writeLockedByCurrentThread": false
},
"StrCache:getBytes1520117783": {
"queueLength": 0,
"fair": false,
"readHoldCount": 0,
"readLockCount": 0,
"writeHoldCount": 0,
"writeLocked": false,
"writeLockedByCurrentThread": false
},
"_tio_ips__0:0:0:0:0:0:0:1": {
"queueLength": 0,
"fair": false,
"readHoldCount": 0,
"readLockCount": 0,
"writeHoldCount": 0,
"writeLocked": false,
"writeLockedByCurrentThread": false
},
"StrCache:getBytes78": {
"queueLength": 0,
"fair": false,
"readHoldCount": 0,
"readLockCount": 0,
"writeHoldCount": 0,
"writeLocked": false,
"writeLockedByCurrentThread": false
},
"StrCache:getBytes-579138113": {
"queueLength": 0,
"fair": false,
"readHoldCount": 0,
"readLockCount": 0,
"writeHoldCount": 0,
"writeLocked": false,
"writeLockedByCurrentThread": false
},
"StrCache:getBytes1381242263": {
"queueLength": 0,
"fair": false,
"readHoldCount": 0,
"readLockCount": 0,
"writeHoldCount": 0,
"writeLocked": false,
"writeLockedByCurrentThread": false
},
"StrCache:getBytes-227623528": {
"queueLength": 0,
"fair": false,
"readHoldCount": 0,
"readLockCount": 0,
"writeHoldCount": 0,
"writeLocked": false,
"writeLockedByCurrentThread": false
},
"StrCache:getLowercase12634714": {
"queueLength": 0,
"fair": false,
"readHoldCount": 0,
"readLockCount": 0,
"writeHoldCount": 0,
"writeLocked": false,
"writeLockedByCurrentThread": false
},
"StrCache:getLowercase12565974": {
"queueLength": 0,
"fair": false,
"readHoldCount": 0,
"readLockCount": 0,
"writeHoldCount": 0,
"writeLocked": false,
"writeLockedByCurrentThread": false
},
"StrCache:getLowercase-1399754748": {
"queueLength": 0,
"fair": false,
"readHoldCount": 0,
"readLockCount": 0,
"writeHoldCount": 0,
"writeLocked": false,
"writeLockedByCurrentThread": false
},
"StrCache:getLowercase-129587587": {
"queueLength": 0,
"fair": false,
"readHoldCount": 0,
"readLockCount": 0,
"writeHoldCount": 0,
"writeLocked": false,
"writeLockedByCurrentThread": false
},
"StrCache:getBytes100245": {
"queueLength": 0,
"fair": false,
"readHoldCount": 0,
"readLockCount": 0,
"writeHoldCount": 0,
"writeLocked": false,
"writeLockedByCurrentThread": false
},
"StrCache:getBytes-1452090263": {
"queueLength": 0,
"fair": false,
"readHoldCount": 0,
"readLockCount": 0,
"writeHoldCount": 0,
"writeLocked": false,
"writeLockedByCurrentThread": false
},
"StrCache:getBytes1519667058": {
"queueLength": 0,
"fair": false,
"readHoldCount": 0,
"readLockCount": 0,
"writeHoldCount": 0,
"writeLocked": false,
"writeLockedByCurrentThread": false
},
"StrCache:getLowercase12115249": {
"queueLength": 0,
"fair": false,
"readHoldCount": 0,
"readLockCount": 0,
"writeHoldCount": 0,
"writeLocked": false,
"writeLockedByCurrentThread": false
},
"StrCache:getLowercase-2040128046": {
"queueLength": 0,
"fair": false,
"readHoldCount": 0,
"readLockCount": 0,
"writeHoldCount": 0,
"writeLocked": false,
"writeLockedByCurrentThread": false
},
"StrCache:getBytes-1610277289": {
"queueLength": 0,
"fair": false,
"readHoldCount": 0,
"readLockCount": 0,
"writeHoldCount": 0,
"writeLocked": false,
"writeLockedByCurrentThread": false
},
"StrCache:getLowercase2024076932": {
"queueLength": 0,
"fair": false,
"readHoldCount": 0,
"readLockCount": 0,
"writeHoldCount": 0,
"writeLocked": false,
"writeLockedByCurrentThread": false
},
"StrCache:getLowercase-1580933225": {
"queueLength": 0,
"fair": false,
"readHoldCount": 0,
"readLockCount": 0,
"writeHoldCount": 0,
"writeLocked": false,
"writeLockedByCurrentThread": false
},
"StrCache:getBytes78355": {
"queueLength": 0,
"fair": false,
"readHoldCount": 0,
"readLockCount": 0,
"writeHoldCount": 0,
"writeLocked": false,
"writeLockedByCurrentThread": false
},
"StrCache:getBytes-1450561559": {
"queueLength": 0,
"fair": false,
"readHoldCount": 0,
"readLockCount": 0,
"writeHoldCount": 0,
"writeLocked": false,
"writeLockedByCurrentThread": false
},
"StrCache:getLowercase2255304": {
"queueLength": 0,
"fair": false,
"readHoldCount": 0,
"readLockCount": 0,
"writeHoldCount": 0,
"writeLocked": false,
"writeLockedByCurrentThread": false
},
"StrCache:getBytes-1383386683": {
"queueLength": 0,
"fair": false,
"readHoldCount": 0,
"readLockCount": 0,
"writeHoldCount": 0,
"writeLocked": false,
"writeLockedByCurrentThread": false
},
"StrCache:getBytes-640121764": {
"queueLength": 0,
"fair": false,
"readHoldCount": 0,
"readLockCount": 0,
"writeHoldCount": 0,
"writeLocked": false,
"writeLockedByCurrentThread": false
},
"StrCache:getLowercase-1099743112": {
"queueLength": 0,
"fair": false,
"readHoldCount": 0,
"readLockCount": 0,
"writeHoldCount": 0,
"writeLocked": false,
"writeLockedByCurrentThread": false
},
"StrCache:getBytes1749280625": {
"queueLength": 0,
"fair": false,
"readHoldCount": 0,
"readLockCount": 0,
"writeHoldCount": 0,
"writeLocked": false,
"writeLockedByCurrentThread": false
},
"StrCache:getBytes1519944307": {
"queueLength": 0,
"fair": false,
"readHoldCount": 0,
"readLockCount": 0,
"writeHoldCount": 0,
"writeLocked": false,
"writeLockedByCurrentThread": false
},
"StrCache:getBytes-1368734941": {
"queueLength": 0,
"fair": false,
"readHoldCount": 0,
"readLockCount": 0,
"writeHoldCount": 0,
"writeLocked": false,
"writeLockedByCurrentThread": false
},
"StrCache:getLowercase1955373352": {
"queueLength": 0,
"fair": false,
"readHoldCount": 0,
"readLockCount": 0,
"writeHoldCount": 0,
"writeLocked": false,
"writeLockedByCurrentThread": false
},
"StrCache:getLowercase12392498": {
"queueLength": 0,
"fair": false,
"readHoldCount": 0,
"readLockCount": 0,
"writeHoldCount": 0,
"writeLocked": false,
"writeLockedByCurrentThread": false
},
"StrCache:getBytes1520186523": {
"queueLength": 0,
"fair": false,
"readHoldCount": 0,
"readLockCount": 0,
"writeHoldCount": 0,
"writeLocked": false,
"writeLockedByCurrentThread": false
}
},
"cache_TIO_HTTP_SESSIONRATELIMITER_CACHENAME": {
"/?d87d6737327c4ee680f1213c67465403": {
"path": "/",
"lastAccessTime": "0",
"accessCount": 0
}
},
"cache_TIO_IP_BLACK_LIST__global__": {},
"cache_TIO_IP_STAT_null_300": {
"0:0:0:0:0:0:0:1": {
"durationType": "300",
"duration": "203",
"ip": "0:0:0:0:0:0:0:1",
"handledPackets": 0,
"handledPacketCosts": 0,
"receivedPackets": 1,
"handledCostsPerPacket": 0,
"packetsPerTcpReceive": 1,
"bytesPerTcpReceive": 1469,
"handledBytes": 0,
"sentPackets": 0,
"receivedTcps": 1,
"receivedBytes": 1469,
"sentBytes": 0,
"requestCount": 2,
"start": "2024-09-08 22:16:41",
"decodeErrorCount": 0,
"formatedDuration": "203毫秒"
}
},
"map": {
"nexus.io.tio.utils.lock.LockUtilsOBJ": {
"cacheName": "nexus.io.tio.utils.lock.LockUtilsOBJ",
"timeToIdleSeconds": "3600",
"timeToLiveSeconds": null
},
"TIO_HTTP_SESSIONRATELIMITER_CACHENAME": {
"cacheName": "TIO_HTTP_SESSIONRATELIMITER_CACHENAME",
"timeToIdleSeconds": null,
"timeToLiveSeconds": "60"
},
"tio-h-s": {
"cacheName": "tio-h-s",
"timeToIdleSeconds": "1800",
"timeToLiveSeconds": null
},
"TIO_IP_STAT_null_300": {
"cacheName": "TIO_IP_STAT_null_300",
"timeToIdleSeconds": null,
"timeToLiveSeconds": "300"
},
"TIO_HTTP_STATIC_RES_CONTENT": {
"cacheName": "TIO_HTTP_STATIC_RES_CONTENT",
"timeToIdleSeconds": null,
"timeToLiveSeconds": "600"
},
"nexus.io.tio.utils.lock.LockUtilsRW": {
"cacheName": "nexus.io.tio.utils.lock.LockUtilsRW",
"timeToIdleSeconds": "3600",
"timeToLiveSeconds": null
},
"TIO_IP_BLACK_LIST__global__": {
"cacheName": "TIO_IP_BLACK_LIST__global__",
"timeToIdleSeconds": null,
"timeToLiveSeconds": "10368000"
}
},
"cache_tio-h-s": {}
},
"ok": true,
"code": 1,
"msg": null
}
```
## 使用 Caffeine 作为内置存储
当配置开启 session 时,tio-boot 会将浏览器的 session 信息存储到默认的 `ConcurrentMapCache` 中。需要更丰富的容量控制和缓存策略时,可以选择 Caffeine。
只需添加以下依赖,tio-boot 在启动时如果检测到 `com.github.benmanes.caffeine.cache.LoadingCache` 存在,会自动启用 `CaffeineCacheFactory`:
```xml
com.github.ben-manes.caffeine
caffeine
2.9.3
```
开启 session 功能
```
server.session.enable=true
```
## 总结
Tio-boot 内置的缓存系统 `AbsCache` 提供了灵活且易用的缓存管理方式。通过 `CacheFactory` 注册并管理缓存实例,我们可以轻松实现高效的缓存机制,减少频繁数据重复计算。对于有 TTL 和 TTI 要求的场景,`AbsCache` 能帮助我们自动管理缓存条目的过期,提升系统性能。
## 按过期策略获取缓存
`CacheUtils.getCaffeineCache(ttl, tti)` 根据完整的 TTL 和 TTI 组合复用缓存。参数完全相同时返回同一实例;TTL 相同但 TTI 不同,或一组未设置 TTI 时,使用不同实例,数据不会因为自动命名相同而混用。
```java
import nexus.io.tio.utils.cache.CacheUtils;
import nexus.io.tio.utils.cache.caffeine.CaffeineCache;
public class CachePolicyExample {
public void store() {
CaffeineCache shortIdle = CacheUtils.getCaffeineCache(3600L, 60L);
CaffeineCache longIdle = CacheUtils.getCaffeineCache(3600L, 600L);
shortIdle.put("status", "short");
longIdle.put("status", "long");
}
}
```
TTL 与 TTI 都为空时,该自动获取接口仍会拒绝调用。单独指定 TTL 或 TTI 的自动命名规则保持兼容;显式使用缓存工厂按名称注册时,同名注册继续复用已有实例,不会用后续参数重新配置它。Caffeine 工厂支持并发注册和查询。
## 缓存空结果
对于查询不到的数据,可以使用 `CacheUtils.get(cache, key, true, creator)` 暂存空结果,减少同一个不存在的键反复访问数据源。内置 `AbsCache` 实现通过临时占位对象区分“未缓存”和“已经查询过但没有结果”;业务调用仍然得到 `null`,不会收到占位对象。
```java
import nexus.io.tio.utils.cache.CacheUtils;
import nexus.io.tio.utils.cache.caffeine.CaffeineCache;
import nexus.io.tio.utils.cache.caffeine.CaffeineCacheFactory;
public class MissingValueCacheExample {
public String demonstrate() {
CaffeineCache cache = CaffeineCacheFactory.INSTANCE.register("lookup-results", 300L, null);
String missing = CacheUtils.get(cache, "item:42", true, () -> null);
// The loader is skipped while the temporary null entry is present.
String stillMissing = CacheUtils.get(cache, "item:42", true, () -> "created");
cache.remove("item:42");
return CacheUtils.get(cache, "item:42", true, () -> "created");
}
}
```
临时条目的有效期由缓存实现的 `putTemporary` 策略决定,不等同于普通条目的 TTL。临时条目失效或被删除后,可以再次加载;明确知道数据已创建时可主动删除对应键。传入 `false` 时,不保存新的空结果。自定义 `ICache` 如果在读取时隐藏空占位对象,需要自行提供相应的空结果命中能力。
## 缓存加载的隔离与异常
缓存加载协调键使用带长度边界的缓存名称与条目键,分别命名的缓存及其条目保持独立。相同缓存、相同键的请求仍共享加载协调机制。
加载函数返回 null 表示无结果;加载失败则向执行加载的调用方传播异常。RuntimeException 保持原异常对象,受检异常通过 IllegalStateException 保留为 cause。失败不写入空结果占位,后续请求可以再次尝试加载。业务可在调用边界统一处理重试、降级或错误响应。
---
# 使用 Redis 作为内部 Cache
URL: https://tio-boot.com/zh/04_configuration/04.html
Source: zh/04_configuration/04.md
# 使用 Redis 作为内部 Cache
[[toc]]
## 内部 Cache 负责存储的信息
在 Tio-boot 运行过程中,内部状态信息会被存入缓存系统 `AbsCache`。以下是 Tio-boot 内部会缓存的一些典型信息:
1. **HTTP 会话(Session)数据**:启用会话管理时,浏览器的会话信息会存储在缓存中,以便在后续请求中快速访问。默认情况下,使用 `ConcurrentMapCache` 存储,但可以通过引入 Caffeine 实现更高效的缓存管理。
2. **IP 统计信息**:缓存保存与 IP 地址相关的统计数据,如请求次数、数据包数量等。这些信息有助于进行访问控制和流量限制。
3. **HTTP 静态资源内容**:为了提升响应速度,静态资源内容会被缓存,避免每次请求都重新读取资源。
4. **IP 黑名单信息**:在安全防护中,黑名单 IP 列表会被缓存,以阻止不良 IP 的访问。
5. **锁定机制状态**:与 `LockUtils` 相关的状态信息会存储在缓存中,以便在并发场景中管理读写锁。
这些缓存信息的管理通过 `CacheFactory` 实现。`CacheFactory` 负责缓存实例的注册、获取以及数据的存取,并支持自定义缓存的 TTL(存活时间)和 TTI(空闲时间)设置。通过适当的配置,可以优化系统性能,减少不必要的数据重复计算和资源消耗。
## 使用 Redis 作为内部 Cache
为了提高缓存的持久性和分布式能力,本指南将介绍如何使用 Redis 作为 Tio-boot 的内部缓存。
### 添加依赖
首先,需要在项目的 `pom.xml` 文件中添加 Redis 客户端依赖。本文使用的是 `Jedis` 作为 Redis 客户端。
```xml
redis.clients
jedis
4.3.1
```
### 添加配置
在 `app.properties` 文件中添加 Redis 相关配置,以及指定使用 Redis 作为缓存存储。
```properties
# 指定缓存存储类型为 Redis
server.cache.store=redis
# 启用会话管理
server.session.enable=true
# Redis 连接配置
redis.host=127.0.0.1
redis.port=6379
redis.password= # 如果 Redis 设置了密码,请在此填写
redis.database=2
redis.timeout=15000
```
**配置说明:**
- `server.cache.store=redis`:指定缓存存储类型为 Redis。默认情况下,Tio-boot 使用 `ConcurrentMapCache`,但通过此配置可以切换为 Redis。
- `server.session.enable=true`:启用会话管理,将会话数据存储在 Redis 中。
- `redis.host`、`redis.port`、`redis.password`、`redis.database`、`redis.timeout`:配置 Redis 连接的主机、端口、密码、数据库编号和连接超时时间。
### 添加配置类
创建一个配置类,用于初始化 Redis 连接池,并将其注入到 `RedisMapCacheFactory` 中。
```java
package nexus.io.website.config;
import nexus.io.annotation.BeforeStartConfiguration;
import nexus.io.annotation.Initialization;
import nexus.io.tio.boot.server.TioBootServer;
import nexus.io.tio.utils.cache.redismap.JedisPoolCan;
import nexus.io.tio.utils.environment.EnvUtils;
import lombok.extern.slf4j.Slf4j;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
@AopBeforeStartConfiguration
@Slf4j
public class BeforeTioStartConfig {
@Initialization
public void config() {
// 从环境变量获取 Redis 配置
String host = EnvUtils.getStr("redis.host");
log.info("host:{}", host);
int port = EnvUtils.getInt("redis.port", 6379);
Integer timeout = EnvUtils.getInt("redis.timeout", 2000);
String password = EnvUtils.getStr("redis.password");
int database = EnvUtils.getInt("redis.database", 0);
// 创建 Jedis 连接池
JedisPool jedisPool = new JedisPool(new JedisPoolConfig(), host, port, timeout, password, database);
try (Jedis resource = jedisPool.getResource()) {
log.info("resource:{}", resource);
}
// 将 Jedis 连接池设置到JedisPoolCan,RedisMapCacheFactory生产的RedisMapCache会自动从JedisPoolCan中读取
JedisPoolCan.jedisPool = jedisPool;
// 注册销毁方法,确保应用关闭时连接池被正确关闭
HookCan.me().addDestroyMethod(jedisPool::close);
}
}
```
**关键点说明:**
- `@AopBeforeStartConfiguration`:该注解标识的配置类会在 Tio-boot 启动前执行,确保 Redis 连接池在缓存系统初始化前就绪。
- `@Initialization`:标识初始化方法,用于配置和初始化 Redis 连接。
- `JedisPoolCan.jedisPool = jedisPool;`:将创建的 Jedis 连接池注入到 `JedisPoolCan` 中,使其能够使用 Redis 进行缓存操作。
- `HookCan.me().addDestroyMethod(jedisPool::close)`:注册应用关闭时的销毁方法,确保 Redis 连接池被正确关闭,释放资源。
### 启动类
启动类无需做任何更改,继续使用现有的 `TioApplication.run` 方法启动应用。
```java
package nexus.io.tio.web.hello;
import nexus.io.annotation.AComponentScan;
import nexus.io.tio.boot.TioApplication;
@AComponentScan
public class HelloApp {
public static void main(String[] args) {
long start = System.currentTimeMillis();
TioApplication.run(HelloApp.class, args);
long end = System.currentTimeMillis();
System.out.println((end - start) + "ms");
}
}
```
### 编写 Controller
编写一个 Controller,用于查看内部缓存存储的数据,验证 Redis 缓存是否正常工作。
```java
package nexus.io.tio.web.hello.controller;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import com.jfinal.kit.Kv;
import nexus.io.annotation.RequestPath;
import nexus.io.model.body.RespBodyVo;
import nexus.io.tio.boot.server.TioBootServer;
import nexus.io.tio.utils.cache.AbsCache;
import nexus.io.tio.utils.cache.CacheFactory;
@RequestPath("/internal")
public class InternalController {
public RespBodyVo cache() {
// 获取 CacheFactory 实例
CacheFactory cacheFactory = TioBootServer.me().getServerTioConfig().getCacheFactory();
Map map = cacheFactory.getMap();
// 创建响应数据
Kv kv = Kv.by("cacheFactory", cacheFactory);
kv.set("map", map);
// 遍历所有缓存,获取缓存中的数据
for (Map.Entry entry : map.entrySet()) {
String cacheName = entry.getKey();
AbsCache cache = entry.getValue();
Collection keysCollection = cache.keysCollection();
Map cacheMap = new HashMap<>();
for (String key : keysCollection) {
cacheMap.put(key, cache.get(key));
}
kv.set("cache_" + cacheName, cacheMap);
}
return RespBodyVo.ok(kv);
}
}
```
**功能说明:**
- 通过 `/internal/cache` 路径访问该 Controller,可以查看当前缓存中存储的所有数据。
- 该方法遍历所有缓存实例,获取每个缓存中的键值对,并将其封装在响应体中返回。
## 关键配置说明
### `server.cache.store=redis`
该配置项指定了 Tio-boot 使用 Redis 作为内部缓存存储。默认情况下,Tio-boot 使用内存中的 `ConcurrentMapCache` 进行缓存管理,这对于单实例应用足够,但在分布式或高可用场景下可能存在限制。通过设置 `server.cache.store=redis`,可以将缓存数据存储在 Redis 中,实现缓存的持久化和跨实例共享。
**优点:**
- **持久化**:缓存数据可以持久化存储,避免因应用重启导致数据丢失。
- **分布式支持**:多实例应用可以共享同一个 Redis 缓存,确保数据一致性。
- **扩展性**:Redis 提供了丰富的数据结构和高级功能,如过期策略、发布订阅等,增强缓存的灵活性。
### `@AopBeforeStartConfiguration` 注解
`@AopBeforeStartConfiguration` 是自定义的注解,用于标识在 Tio-boot 启动前执行的配置类。被该注解标注的类会在应用启动流程的早期阶段被加载和执行,确保关键配置在缓存系统初始化之前完成。
**作用:**
- **初始化顺序控制**:确保 Redis 连接池在 Tio-boot 启动和缓存系统初始化之前就绪,避免在缓存操作时出现连接未建立的情况。
- **统一配置管理**:集中管理与 Redis 相关的配置和初始化逻辑,提高代码的可维护性和可读性。
### `RedisMapCacheFactory`
`RedisMapCacheFactory` 是 Tio-boot 提供的一个缓存工厂类,负责管理 Redis 作为缓存存储的具体实现。通过将 `JedisPool` 注入到 `JedisPoolCan`,它可以使用 Redis 进行缓存的读写操作。
**主要职责:**
- **缓存实例管理**:注册和获取不同类型的缓存实例,如会话缓存、IP 统计缓存等。
- **数据存取**:通过 Redis 客户端与 Redis 服务器交互,实现缓存数据的存取、更新和删除。
- **配置支持**:支持自定义缓存的 TTL(存活时间)和 TTI(空闲时间),通过配置优化缓存行为。
**使用步骤:**
1. **初始化 Redis 连接池**:在配置类中创建 `JedisPool` 并注入到 `JedisPoolCan`。
2. **注册销毁方法**:确保应用关闭时,连接池能够被正确关闭,释放资源。
3. **配置缓存工厂**:通过 `CacheFactory` 获取 `RedisMapCacheFactory`,并管理具体的缓存实例。
## 总结
通过以上步骤,您可以将 Redis 成功集成到 Tio-boot 作为内部缓存系统。这不仅提升了缓存的持久性和分布式能力,还增强了系统的扩展性和稳定性。关键配置如 `server.cache.store=redis` 和 `@AopBeforeStartConfiguration` 注解确保了缓存系统的正确初始化和高效运行。`RedisMapCacheFactory` 则提供了强大的缓存管理功能,使得在不同场景下都能灵活应对。
---
# 静态文件处理器
URL: https://tio-boot.com/zh/04_configuration/05.html
Source: zh/04_configuration/05.md
# 静态文件处理器
在现代 Web 开发中,静态资源的高效管理和分发对提升用户体验至关重要。TioBoot 作为一款高性能的 Java Web 框架,提供了内置的静态文件处理器 `DefaultStaticResourceHandler`,实现了 `StaticResourceHandler` 接口。然而,在某些复杂场景下,我们可能需要根据不同的域名加载不同目录下的静态文件。本文将深入探讨如何自定义 TioBoot 的静态文件处理器,实现基于域名的动态静态资源加载,并结合 Redis 实现高效的缓存机制。
## 背景与目标
在多租户系统或需要对资源进行域名隔离的场景中,我们希望:
- **根据不同的域名**,从对应的目录中加载静态文件;
- **提高静态资源的加载效率**,减少服务器的 I/O 负载;
- **实现缓存机制**,进一步提升响应速度。
## 自定义静态文件处理器
### 设计思路
自定义的静态文件处理器需要解决以下问题:
1. **域名解析**:从请求中获取域名信息,确定静态文件的存储目录。
2. **文件加载**:根据域名和请求路径,加载对应的静态文件。
3. **缓存机制**:引入缓存,减少磁盘读取,提高性能。
4. **响应构建**:根据文件内容和请求头,构建适当的 HTTP 响应。
### 实现代码
以下是自定义静态文件处理器 `MyStaticResourceHandler` 的实现:
```java
package nexus.io.file.handler;
import java.io.File;
import nexus.io.constatns.ServerConfigKeys;
import nexus.io.tio.boot.http.handler.StaticResourceHandler;
import nexus.io.tio.http.common.HeaderName;
import nexus.io.tio.http.common.HeaderValue;
import nexus.io.tio.http.common.HttpConfig;
import nexus.io.tio.http.common.HttpRequest;
import nexus.io.tio.http.common.HttpResource;
import nexus.io.tio.http.common.HttpResponse;
import nexus.io.tio.http.common.HttpResponseStatus;
import nexus.io.tio.http.server.handler.FileCache;
import nexus.io.tio.http.server.util.Resps;
import nexus.io.tio.utils.cache.AbsCache;
import nexus.io.tio.utils.environment.EnvUtils;
import nexus.io.tio.utils.hutool.FileUtil;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class MyStaticResourceHandler implements StaticResourceHandler {
private static final long MAX_CACHE_FILE_SIZE = 5 * 1024 * 1024; // 最大缓存大小5MB
public HttpResponse handle(String path, HttpRequest request, HttpConfig httpConfig, AbsCache staticResCache) {
boolean enable = EnvUtils.getBoolean(ServerConfigKeys.SERVER_RESOURCES_STATIC_FILE_CACHE_ENABLE, false);
String host = request.getHost().replace(':', '_');
String fileKey = host + path;
HttpResponse response = null;
FileCache fileCache = null;
// 从缓存中获取FileCache
if (enable && staticResCache != null) {
fileCache = (FileCache) staticResCache.get(fileKey);
}
if (enable && fileCache != null) {
long lastModified = fileCache.getLastModified();
// 检查是否需要返回304
response = Resps.try304(request, lastModified);
if (response != null) {
response.addHeader(HeaderName.tio_from_cache, HeaderValue.Tio_From_Cache.TRUE);
return response;
}
// 构建HttpResponse
response = new HttpResponse(request);
response.setBody(fileCache.getContent());
response.setLastModified(HeaderValue.from(String.valueOf(lastModified)));
response.setSkipGzipped(fileCache.isHasGzipped());
// 设置必要的响应头
if (fileCache.getContentType() != null) {
response.addHeader(HeaderName.Content_Type, fileCache.getContentType());
}
if (fileCache.getContentEncoding() != null) {
response.addHeader(HeaderName.Content_Encoding, fileCache.getContentEncoding());
}
return response;
} else {
String extension = FileUtil.extName(path);
File file = new File(path);
if (!file.exists()) {
//从默认配置中读取文件
String pageRoot = httpConfig.getPageRoot(request);
if (pageRoot != null) {
HttpResource httpResource;
try {
httpResource = httpConfig.getResource(request, path);
} catch (Exception e) {
e.printStackTrace();
return null;
}
if (httpResource != null) {
file = httpResource.getFile();
} else {
return null;
}
} else {
return null;
}
}
// 文件存在,读取文件内容
long fileLastModified = file.lastModified();
byte[] content = FileUtil.readBytes(file);
HeaderValue lastModified = HeaderValue.from(String.valueOf(fileLastModified));
response = Resps.bytes(request, content, extension);
response.setStaticRes(true);
response.setLastModified(lastModified);
// 缓存文件内容,如果文件大小小于最大缓存大小
if (enable && response.isStaticRes() && staticResCache != null) {
if (response.getBody() != null && response.getStatus() == HttpResponseStatus.C200) {
if (content.length <= MAX_CACHE_FILE_SIZE) {
HeaderValue contentType = response.getHeader(HeaderName.Content_Type);
HeaderValue contentEncoding = response.getHeader(HeaderName.Content_Encoding);
FileCache newFileCache = new FileCache(content, fileLastModified, contentType, contentEncoding, response.hasGzipped());
staticResCache.put(fileKey, newFileCache);
if (log.isInfoEnabled()) {
log.info("add to cache:[{}], {}(B)", fileKey, content.length);
}
} else {
log.info("File size exceeds cache limit, not cached: [{}], {}(B)", fileKey, content.length);
}
}
}
}
return response;
}
}
```
### 代码解析
- **域名处理**:使用 `request.getHost()` 获取请求的主机名,替换其中的冒号为下划线,防止文件系统路径错误。
- **文件键生成**:将主机名与请求路径拼接,形成唯一的文件键,便于文件定位和缓存。
- **缓存机制**:
- **检查缓存**:如果启用了缓存,尝试从 `staticResCache` 中获取 `FileCache` 对象。
- **缓存命中**:如果缓存存在,检查文件的 `Last-Modified`,决定是否返回 `304 Not Modified`。
- **缓存更新**:如果缓存不存在或已过期,读取文件并更新缓存。
- **文件读取**:
- **直接读取**:尝试根据文件键直接读取文件。
- **默认路径**:如果文件不存在,使用 `httpConfig` 获取默认的 `pageRoot`,从默认路径读取文件。
- **响应构建**:根据读取的文件内容,设置响应体、状态码、头信息等。
## 配置自定义处理器到 TioBoot
要使自定义的静态文件处理器生效,需要将其配置到 TioBoot 中。在 `TioBootServerConfig` 类中进行如下设置:
```java
import nexus.io.annotation.AConfiguration;
import nexus.io.annotation.Initialization;
import nexus.io.file.handler.MyStaticResourceHandler;
import nexus.io.tio.boot.server.TioBootServer;
@AConfiguration
public class TioBootServerConfig {
@Initialization
public void config() {
// 设置自定义的静态资源处理器
TioBootServer.me().setStaticResourceHandler(new MyStaticResourceHandler());
}
}
```
## 使用 Redis 作为缓存存储
### 原因分析
- **高性能**:Redis 作为内存数据库,具有高速的读写性能,适合缓存场景。
- **可扩展性**:在分布式环境中,Redis 可以作为集中式的缓存存储,便于扩展和管理。
### 配置实现
```java
package nexus.io.file.config;
import nexus.io.annotation.BeforeStartConfiguration;
import nexus.io.annotation.Initialization;
import nexus.io.tio.boot.server.TioBootServer;
import nexus.io.tio.utils.cache.redismap.RedisMapCacheFactory;
import nexus.io.tio.utils.environment.EnvUtils;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
@AopBeforeStartConfiguration
public class BeforeTioStartConfig {
@Initialization
public void config() {
// 从环境变量获取 Redis 配置
String host = EnvUtils.getStr("redis.host", "localhost");
int port = EnvUtils.getInt("redis.port", 6379);
int timeout = EnvUtils.getInt("redis.timeout", 2000);
String password = EnvUtils.getStr("redis.password", null);
int database = EnvUtils.getInt("redis.database", 0);
// 创建 Jedis 连接池
JedisPoolConfig poolConfig = new JedisPoolConfig();
JedisPool jedisPool = new JedisPool(poolConfig, host, port, timeout, password, database);
// 设置 RedisMapCacheFactory 的 Jedis 连接池
RedisMapCacheFactory.INSTANCE.setJedisPool(jedisPool);
// 注册销毁方法,确保应用关闭时连接池被正确关闭
HookCan.me().addDestroyMethod(jedisPool::close);
}
}
```
### 配置解析
- **环境变量读取**:使用 `EnvUtils` 从环境变量或配置文件中读取 Redis 的连接信息,支持默认值。
- **Jedis 连接池**:配置连接池参数,提升 Redis 访问的性能和稳定性。
- **缓存工厂设置**:将连接池设置到 `RedisMapCacheFactory`,使缓存数据存储在 Redis 中。
- **资源管理**:在应用关闭时,调用 `jedisPool.close()`,确保连接池资源被正确释放。
## 配置文件(可选)
在 `app.properties` 或其他配置文件中,可以添加以下配置:
```properties
server.resources.static-locations=pages
server.resources.static.file.cache.enable=true
```
- `server.resources.static-locations`:指定静态资源的默认目录(可根据实际需求修改)。
- `server.resources.static.file.cache.enable`:启用静态文件缓存。
## 使用示例
完成上述配置后,当您访问 `http://localhost:8123/channel_001/images/439681354499067904.png` 时,服务器将从 `localhost_8123` 目录下加载对应的文件。这是因为自定义的处理器根据请求的主机名(`localhost:8123`)和请求路径,动态确定了文件的存储位置,实现了基于域名的资源隔离。
## 工作原理详解
### 1. 基于域名的资源隔离
- **核心思想**:将请求的主机名作为静态文件目录的标识,结合请求路径,定位到具体的静态文件。
- **实现方式**:在处理请求时,获取 `Host` 头,将其格式化后作为文件路径的一部分。
### 2. 缓存机制的实现
- **目的**:减少磁盘 I/O,提高响应速度,降低服务器负载。
- **策略**:
- **缓存条件**:文件大小小于设定的最大缓存大小(5MB)。
- **缓存存储**:使用 Redis 作为缓存存储,支持高并发访问。
- **缓存校验**:通过 `Last-Modified` 时间,判断缓存是否需要更新。
- **304 响应**:如果文件未修改,返回 HTTP `304 Not Modified` 状态码,减少数据传输。
### 3. Redis 的优势
- **高性能**:内存存储,读写速度快。
- **持久化**:支持数据持久化,防止数据丢失。
- **分布式支持**:方便在集群环境中部署,扩展性强。
### 4. 动态资源加载流程
1. **接收请求**:客户端请求静态资源,包含主机名和请求路径。
2. **处理请求**:自定义处理器解析请求,生成文件键。
3. **检查缓存**:查询 Redis 缓存,判断是否存在对应的文件内容。
- **缓存命中**:校验 `Last-Modified`,决定是否返回缓存内容或 `304` 状态码。
- **缓存未命中**:从文件系统读取文件。
4. **构建响应**:根据文件内容和请求头,构建 `HttpResponse` 对象。
5. **返回响应**:将响应发送给客户端,并根据需要更新缓存。
### 5. 资源管理与优化
- **连接池管理**:使用 Jedis 连接池,优化 Redis 连接的创建和管理。
- **资源释放**:在应用关闭时,确保所有资源(如连接池)被正确释放,防止资源泄漏。
- **日志记录**:在关键操作处添加日志,方便调试和监控。
## 总结
通过自定义 TioBoot 的静态文件处理器,我们成功实现了基于域名的静态资源隔离和高效的缓存机制。这种设计适用于多租户系统、内容分发网络(CDN)等需要对资源进行域名隔离的场景。
利用 Redis 的高速缓存能力,我们大幅提升了静态资源的加载效率,改善了用户体验。同时,代码中充分考虑了可扩展性和可维护性,使用环境变量配置、连接池管理和日志记录等手段,确保系统的稳定运行。
---
# 基于域名的静态资源隔离
URL: https://tio-boot.com/zh/04_configuration/06.html
Source: zh/04_configuration/06.md
# 基于域名的静态资源隔离
在多站点部署或不同环境隔离的场景下,我们可以通过基于域名的配置实现资源隔离。不同的域名对应不同的配置文件,进而映射到服务器文件系统中的不同目录。这样可以在一台服务器上为多个站点提供服务,每个站点的静态资源都存放在独立的目录中。在 tio-boot 中可以使用自定义 StaticResourceHandler 实现
## 数据库表定义
下列 SQL 脚本用于创建保存各个域名对应静态资源目录信息的数据表 `website_page_folder`。其中主要字段包括 `domain` 表示域名,`path` 表示与该域名对应的静态资源根目录。
```sql
drop table if exists website_page_folder;
CREATE TABLE website_page_folder (
id BIGINT primary key,
name varchar(256),
code varchar(256),
path varchar(256),
domain varchar(256),
status int,
"creator" VARCHAR(64) DEFAULT '',
"create_time" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updater" VARCHAR(64) DEFAULT '',
"update_time" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted" SMALLINT DEFAULT 0,
"tenant_id" BIGINT NOT NULL DEFAULT 0
);
```
在初始化时,插入一条记录,用于 `localhost` 域名,其静态资源目录为 `pages/channel_001`。
```sql
INSERT INTO "website_page_folder"("id", "name", "code", "path", "domain", "status", "creator", "create_time", "updater", "update_time", "deleted", "tenant_id")
VALUES (2, 'localhost', 'localhost', 'pages/channel_001', 'localhost', NULL, '', '2025-03-13 09:14:53.140402+08', '', '2025-03-13 09:14:53.140402+08', 0, 0);
```
## 代码实现
系统中主要有两个模块实现资源隔离的逻辑:
### 1. PageFolderService
该服务负责根据请求的域名从数据库中获取对应的资源根目录。
```java
package nexus.io.website.service;
import nexus.io.db.activerecord.Db;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class PageFolderService {
public String getByDomain(String domain) {
log.info("doamin:{}",domain);
String sql = "select path from website_page_folder where domain=?";
return Db.queryStr(sql, domain);
}
}
```
在 `getByDomain` 方法中,根据传入的域名参数查询数据库,返回相应的资源目录。例如,当请求的域名为 `localhost` 时,将返回 `pages/channel_001`。
### 2. MyStaticResourceHandler
```java
package nexus.io.website.handler;
import java.io.File;
import nexus.io.constants.ServerConfigKeys;
import nexus.io.jfinal.aop.Aop;
import nexus.io.tio.boot.http.TioRequestContext;
import nexus.io.tio.boot.http.handler.internal.StaticResourceHandler;
import nexus.io.tio.boot.utils.HttpResourceUtils;
import nexus.io.tio.http.common.HeaderName;
import nexus.io.tio.http.common.HeaderValue;
import nexus.io.tio.http.common.HttpConfig;
import nexus.io.tio.http.common.HttpRequest;
import nexus.io.tio.http.common.HttpResource;
import nexus.io.tio.http.common.HttpResponse;
import nexus.io.tio.http.common.HttpResponseStatus;
import nexus.io.tio.http.server.handler.FileCache;
import nexus.io.tio.http.server.util.Resps;
import nexus.io.tio.utils.cache.AbsCache;
import nexus.io.tio.utils.environment.EnvUtils;
import nexus.io.tio.utils.hutool.StrUtil;
import nexus.io.website.service.PageDomainService;
import nexus.io.website.service.PageFolderService;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class MyStaticResourceHandler implements StaticResourceHandler {
private static final long MAX_CACHE_FILE_SIZE = 5 * 1024 * 1024;
private static final HeaderName TIO_FROM_CACHE = HeaderName.tio_from_cache;
private static final HeaderValue TRUE_CACHE = HeaderValue.Tio_From_Cache.TRUE;
private PageFolderService pageFolderService = Aop.get(PageFolderService.class);
private PageDomainService pageDomainService = Aop.get(PageDomainService.class);
private static final long FILE_SIZE_THRESHOLD = 10 * 1024 * 1024; // 10MB分界点
@Override
public HttpResponse handle(String path, HttpRequest request, HttpConfig httpConfig, AbsCache staticResCache) {
try {
String cacheKey = generateCacheKey(request);
HttpResponse cachedResponse = tryServeFromCache(cacheKey, request, staticResCache);
if (cachedResponse != null) {
return cachedResponse;
}
HttpResource httpResource = locateResource(path, request, httpConfig);
if (httpResource == null || !httpResource.getFile().exists()) {
HttpResponse response = TioRequestContext.getResponse();
response.setStatus(HttpResponseStatus.C404);
return Resps.html(response, "Resource not found");
}
HttpResponse response = TioRequestContext.getResponse();
HttpResourceUtils.buildFileResponse(response, httpResource);
cacheResponseIfNeeded(cacheKey, httpResource.getFile(), response, staticResCache);
return response;
} catch (Exception e) {
log.error("Error handling static resource: {}", path, e);
HttpResponse response = TioRequestContext.getResponse();
response.setStatus(HttpResponseStatus.C500);
return Resps.html(response, "Internal server error");
}
}
private String generateCacheKey(HttpRequest request) {
return request.getHost().replace(':', '_') + request.getRequestURI();
}
private HttpResponse tryServeFromCache(String cacheKey, HttpRequest request, AbsCache staticResCache) {
if (!isCacheEnabled() || staticResCache == null)
return null;
FileCache fileCache = (FileCache) staticResCache.get(cacheKey);
if (fileCache == null)
return null;
HttpResponse notModifiedResponse = checkNotModified(request, fileCache.getLastModified());
if (notModifiedResponse != null)
return notModifiedResponse;
return buildCachedResponse(request, fileCache);
}
private boolean isCacheEnabled() {
return EnvUtils.getBoolean(ServerConfigKeys.SERVER_RESOURCES_STATIC_FILE_CACHE_ENABLE, false);
}
private HttpResponse checkNotModified(HttpRequest request, long lastModified) {
return Resps.try304(request, lastModified);
}
private HttpResponse buildCachedResponse(HttpRequest request, FileCache fileCache) {
HttpResponse response = new HttpResponse(request);
response.setBody(fileCache.getContent());
response.setLastModified(HeaderValue.from(String.valueOf(fileCache.getLastModified())));
response.setSkipGzipped(fileCache.isHasGzipped());
response.addHeader(TIO_FROM_CACHE, TRUE_CACHE);
if (fileCache.getContentType() != null) {
response.addHeader(HeaderName.Content_Type, fileCache.getContentType());
}
if (fileCache.getContentEncoding() != null) {
response.addHeader(HeaderName.Content_Encoding, fileCache.getContentEncoding());
}
return response;
}
private HttpResource locateResource(String path, HttpRequest request, HttpConfig httpConfig) throws Exception {
String domain = request.getDomain();
String pageRoot = pageFolderService.getPathByDomain(domain);
if (StrUtil.isEmpty(pageRoot)) {
pageRoot = pageDomainService.getPathByDomain(domain);
if (StrUtil.isEmpty(pageRoot)) {
pageRoot = httpConfig.getPageRoot();
}
}
return HttpResourceUtils.getResource(pageRoot, path);
}
private void cacheResponseIfNeeded(String cacheKey, File file, HttpResponse response, AbsCache staticResCache) {
// 大文件不进行缓存
if (file.length() > FILE_SIZE_THRESHOLD) {
log.debug("Skipping cache for large file: {}", cacheKey);
return;
}
if (!shouldCache(response))
return;
byte[] content = response.getBody();
if (content.length > MAX_CACHE_FILE_SIZE) {
log.info("File too large for caching: {} ({} bytes)", cacheKey, content.length);
return;
}
FileCache fileCache = createFileCache(file, response, content);
staticResCache.put(cacheKey, fileCache);
log.info("Cached resource: {} ({} bytes)", cacheKey, content.length);
}
private boolean shouldCache(HttpResponse response) {
return isCacheEnabled() && response.isStaticRes() && response.getStatus() == HttpResponseStatus.C200 && response.getBody() != null;
}
private FileCache createFileCache(File file, HttpResponse response, byte[] content) {
return new FileCache(content, file.lastModified(), response.getHeader(HeaderName.Content_Type), response.getHeader(HeaderName.Content_Encoding), response.hasGzipped());
}
}
```
该处理器负责接收静态资源请求,并定位实际文件。关键步骤如下:
1. **生成缓存键**
利用请求的 host 和 URI 生成唯一的缓存键。
2. **从缓存中读取**
如果缓存中有对应的响应,则直接返回缓存内容。
3. **定位资源**
根据请求中获取的域名,通过 `PageFolderService` 获取资源根目录,然后将请求路径与该根目录拼接生成完整的文件路径。例如:
- 请求路径:`/images/439681354499067904.png`
- 返回的资源根目录:`pages/channel_001`
- 拼接后完整的文件路径:`pages/channel_001/images/439681354499067904.png`
```java
private HttpResource locateResource(String path, HttpRequest request, HttpConfig httpConfig) throws Exception {
String domain = request.getDomain();
String pageRoot = pageFolderService.getByDomain(domain);
return getResource(pageRoot, path);
}
public HttpResource getResource(String pageRoot, String path) throws Exception {
HttpResource httpResource = null;
if (pageRoot != null) {
if (StrUtil.endWith(path, "/")) {
path = path + "index.html";
}
String complatePath = pageRoot + path;
File file = new File(complatePath);
if (file.exists()) {
httpResource = new HttpResource(path, null, file);
}
}
return httpResource;
}
```
4. **构建响应**
如果文件存在,读取文件内容后构建 HTTP 响应,同时支持静态文件缓存,进一步提升性能。
## URL 访问与文件路径映射说明
当用户访问 URL:
```
http://localhost:8123/images/439681354499067904.png
```
系统内部的处理过程如下:
1. **提取域名**
请求中包含的域名为 `localhost`。
2. **查询对应的资源根目录**
通过 `PageFolderService.getByDomain("localhost")` 方法查询数据库,返回值为 `pages/channel_001`。
3. **拼接路径**
将返回的资源根目录 `pages/channel_001` 与请求路径 `/images/439681354499067904.png` 进行拼接,最终定位到文件系统中的路径为:
```
pages/channel_001/images/439681354499067904.png
```
这种设计实现了**基于域名的资源隔离**。尽管用户访问的是 `/images/439681354499067904.png`,但是后端会根据域名确定使用哪个资源根目录。这样可以在同一服务器上支持多个不同站点的静态资源,同时保证每个站点的资源不会混淆。
---
总结来说,这套系统通过在数据库中配置域名与对应资源目录的映射关系,实现了基于域名的资源隔离。请求 URL 中的路径经过域名解析后,会映射到对应站点的资源目录下,从而读取正确的静态文件。这不仅使得资源管理更加清晰,也为不同站点提供了灵活的配置能力。
## 清空静态文件缓存
在 tio-boot 框架中,我们可以通过编写一个处理器来清空静态文件缓存。示例代码中,处理器类 StaticResCacheHandler 定义了一个 clear 方法,该方法首先调用 TioBootServer.me().getHttpRequestDispatcher().clearStaticResCache() 来清空静态资源缓存,然后利用 TioRequestContext.getResponse() 获取当前请求的响应对象,并将响应内容设置为 "OK",以此返回一个简单的成功标识。这种方式非常适用于在前端文件更新后希望立即刷新缓存、加载最新资源的场景。
```java
package nexus.io.website.handler;
import nexus.io.tio.boot.http.TioRequestContext;
import nexus.io.tio.boot.server.TioBootServer;
import nexus.io.tio.http.common.HttpRequest;
import nexus.io.tio.http.common.HttpResponse;
public class StaticResCacheHandler {
public HttpResponse clear(HttpRequest request) {
TioBootServer.me().getHttpRequestDispatcher().clearStaticResCache();
HttpResponse response = TioRequestContext.getResponse();
response.setString("OK");
return response;
}
}
```
---
# DecodeExceptionHandler
URL: https://tio-boot.com/zh/04_configuration/07.html
Source: zh/04_configuration/07.md
# DecodeExceptionHandler
`DecodeExceptionHandler` 是 **tio-boot** 的一个核心组件,用于处理消息解码失败的情况。这类失败通常表明可能存在网络攻击。本文档将详细介绍如何配置和实现自定义的 `DecodeExceptionHandler`,以提升您的 tio-boot 服务器的安全性和稳定性。
## 目录
1. [服务器启动前的配置]()
2. [实现自定义的 DecodeExceptionHandler]()
3. [测试 DecodeExceptionHandler]()
4. [增强 Handler:日志记录、数据库存储和报警]()
5. [完整代码示例]()
6. [详细说明]()
---
## 服务器启动前的配置
在初始化服务器之前,需设置自定义的 `DecodeExceptionHandler`。此配置确保服务器启动时,任何解码异常都能被适当地处理。
```java
package nexus.io.tio.web.hello.config;
import nexus.io.annotation.BeforeStartConfiguration;
import nexus.io.annotation.Initialization;
import nexus.io.tio.boot.server.TioBootServer;
import nexus.io.tio.web.hello.handler.MyDecodeExceptionHandler;
@AopBeforeStartConfiguration
public class BeforeServerConfig {
@Initialization
public void config() {
MyDecodeExceptionHandler myDecodeExceptionHandler = new MyDecodeExceptionHandler();
TioBootServer.me().setDecodeExceptionHandler(myDecodeExceptionHandler);
}
}
```
### 说明:
- **包声明**:组织类文件在项目结构中的位置。
- **导入**:引入必要的类和注解。
- **注解**:
- `@AopBeforeStartConfiguration`:标识此配置应在服务器启动前应用。
- `@Initialization`:标记在初始化阶段执行的方法。
- **配置方法**:
- 实例化 `MyDecodeExceptionHandler`。
- 将其设置为 `TioBootServer` 的 `DecodeExceptionHandler`。
---
## 实现自定义的 DecodeExceptionHandler
自定义的处理器通过关闭受影响的通道并记录相关信息来处理解码异常。
```java
package nexus.io.tio.web.hello.handler;
import java.nio.ByteBuffer;
import nexus.io.tio.boot.decode.TioDecodeExceptionHandler;
import nexus.io.tio.core.ChannelContext;
import nexus.io.tio.core.Tio;
import nexus.io.tio.core.exception.TioDecodeException;
import nexus.io.tio.http.common.HttpConfig;
public class MyDecodeExceptionHandler implements TioDecodeExceptionHandler {
@Override
public void handle(ByteBuffer buffer, ChannelContext channelContext, HttpConfig httpConfig, TioDecodeException e) {
Tio.close(channelContext, "MyDecodeExceptionHandler");
// 创建缓冲区的只读副本
ByteBuffer readOnlyBuffer = buffer.asReadOnlyBuffer();
readOnlyBuffer.position(0); // 重置位置,从头开始读取
try {
if (readOnlyBuffer.hasRemaining()) {
byte[] bytes = new byte[readOnlyBuffer.remaining()];
readOnlyBuffer.get(bytes);
System.out.println("Buffer content: " + new String(bytes));
} else {
System.out.println("Buffer is empty.");
}
System.out.println("TioDecodeException: " + e.toString());
} catch (Exception ex) {
System.err.println("读取缓冲区时出错: " + ex.getMessage());
}
}
}
```
### 说明:
- **类声明**:实现 `TioDecodeExceptionHandler` 接口,定义解码失败时的自定义行为。
- **handle 方法**:
- **关闭通道**:确保断开有问题的连接,防止潜在威胁。
- **缓冲区处理**:
- 创建只读缓冲区副本,以安全访问数据而不修改原始缓冲区。
- 重置缓冲区的位置,从头开始读取。
- 尝试提取并打印缓冲区内容以便调试。
- **异常日志记录**:记录解码异常的详细信息。
---
## 测试 DecodeExceptionHandler
为了验证处理器的功能,发送可能导致解码失败的测试请求,例如发送格式错误的查询参数。
### 测试请求:
```
http://localhost/?1==1
```
### 预期输出:
```
Buffer content: GET /?1==1 HTTP/1.1
Host: localhost
Connection: keep-alive
sec-ch-ua: "Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7
Cookie: xxx
TioDecodeException: nexus.io.tio.core.exception.TioDecodeException: 1==1 contain multi=
```
### 说明:
- **格式错误的查询参数**:查询参数 `?1==1` 故意格式错误,以触发解码异常。
- **处理器响应**:`MyDecodeExceptionHandler` 捕获异常,记录缓冲区内容并输出异常详情。
---
## 增强 Handler:日志记录、数据库存储和报警
为了提升 `DecodeExceptionHandler` 的健壮性,建议记录异常详情、将其存储到数据库中以供审计,并设置报警机制以便及时通知相关人员。
### 增强后的 Handler 实现:
```java
import java.nio.ByteBuffer;
import java.util.Date;
import nexus.io.constatns.ServerConfigKeys;
import nexus.io.db.activerecord.Db;
import nexus.io.db.activerecord.Row;
import nexus.io.tio.boot.decode.TioDecodeExceptionHandler;
import nexus.io.tio.core.ChannelContext;
import nexus.io.tio.core.Tio;
import nexus.io.tio.core.exception.TioDecodeException;
import nexus.io.tio.http.common.HttpConfig;
import nexus.io.tio.utils.environment.EnvUtils;
import nexus.io.tio.utils.network.IpUtils;
import nexus.io.tio.utils.notification.NotifactionWarmModel;
import nexus.io.tio.utils.snowflake.SnowflakeIdUtils;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class MyDecodeExceptionHandler implements TioDecodeExceptionHandler {
@Override
public void handle(ByteBuffer buffer, ChannelContext channelContext, HttpConfig httpConfig, TioDecodeException e) {
// 关闭有问题的通道
Tio.close(channelContext, "MyDecodeExceptionHandler");
String exceptionString = e.toString();
log.error("发生 DecodeException: {}", exceptionString);
// 创建缓冲区的只读副本
ByteBuffer readOnlyBuffer = buffer.asReadOnlyBuffer();
readOnlyBuffer.position(0); // 重置位置,从头开始读取
StringBuilder contentBuilder = new StringBuilder();
try {
if (readOnlyBuffer.hasRemaining()) {
byte[] bytes = new byte[readOnlyBuffer.remaining()];
readOnlyBuffer.get(bytes);
contentBuilder.append(new String(bytes));
} else {
log.error("缓冲区为空。");
contentBuilder.append("缓冲区为空。");
}
contentBuilder.append(" 异常: ").append(exceptionString);
} catch (Exception ex) {
log.error("读取缓冲区时出错: {}", ex.getMessage());
}
// 准备报警模型
NotifactionWarmModel model = new NotifactionWarmModel();
String localIp = IpUtils.getLocalIp();
model.setAppGroupName(RedBookDefaultValue.appGroupName);
model.setAppName(EnvUtils.get(ServerConfigKeys.APP_NAME));
model.setWarningName("MyDecodeExceptionHandler");
model.setLevel("II");
model.setAppEnv(EnvUtils.env());
model.setDeviceName(localIp);
model.setTime(ZonedDateTime.now());
model.setContent(contentBuilder.toString());
// 将异常详情保存到数据库
try {
long id = SnowflakeIdUtils.id();
Db.save("tio_boot_admin_system_exception", Row.by("id", id).set("exception", exceptionString));
} catch (Exception dbEx) {
log.error("将异常保存到数据库失败: {}", dbEx.getMessage());
}
// 发送报警通知
try {
AlarmUtils.send(model);
} catch (Exception alarmEx) {
log.error("发送报警通知失败: {}", alarmEx.getMessage());
}
}
}
```
### 说明:
- **日志记录**:
- 使用 `Slf4j` 进行结构化和基于级别的日志记录。
- 记录异常详情以及在处理缓冲区时遇到的问题。
- **数据库存储**:
- 使用 ActiveRecord 模式 (`Db.save` 和 `Row.by`) 将异常详情持久化到 `tio_boot_admin_system_exception` 表中。
- 使用 `SnowflakeIdUtils` 生成每条记录的唯一标识符。
- 优雅地处理潜在的数据库异常,通过日志记录错误信息。
- **报警机制**:
- 构建包含异常和服务器环境相关信息的 `NotifactionWarmModel`。
- 使用 `AlarmUtils.send(model)` 发送通知,以便管理员或监控系统及时获知异常。
- 确保发送报警失败时不会中断处理器的执行,并记录相关错误。
---
## 完整代码示例
### 1. 服务器启动前的配置
```java
package nexus.io.tio.web.hello.config;
import nexus.io.annotation.BeforeStartConfiguration;
import nexus.io.annotation.Initialization;
import nexus.io.tio.boot.server.TioBootServer;
import nexus.io.tio.web.hello.handler.MyDecodeExceptionHandler;
@AopBeforeStartConfiguration
public class BeforeServerConfig {
@Initialization
public void config() {
MyDecodeExceptionHandler myDecodeExceptionHandler = new MyDecodeExceptionHandler();
TioBootServer.me().setDecodeExceptionHandler(myDecodeExceptionHandler);
}
}
```
### 3. 增强的自定义 DecodeExceptionHandler(包含日志记录、数据库存储和报警)
```java
import java.nio.ByteBuffer;
import java.util.Date;
import nexus.io.constatns.ServerConfigKeys;
import nexus.io.db.activerecord.Db;
import nexus.io.db.activerecord.Row;
import nexus.io.tio.boot.decode.TioDecodeExceptionHandler;
import nexus.io.tio.core.ChannelContext;
import nexus.io.tio.core.Tio;
import nexus.io.tio.core.exception.TioDecodeException;
import nexus.io.tio.http.common.HttpConfig;
import nexus.io.tio.utils.environment.EnvUtils;
import nexus.io.tio.utils.network.IpUtils;
import nexus.io.tio.utils.notification.NotifactionWarmModel;
import nexus.io.tio.utils.snowflake.SnowflakeIdUtils;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class MyDecodeExceptionHandler implements TioDecodeExceptionHandler {
@Override
public void handle(ByteBuffer buffer, ChannelContext channelContext, HttpConfig httpConfig, TioDecodeException e) {
// 关闭有问题的通道
Tio.close(channelContext, "MyDecodeExceptionHandler");
String exceptionString = e.toString();
log.error("发生 DecodeException: {}", exceptionString);
// 创建缓冲区的只读副本
ByteBuffer readOnlyBuffer = buffer.asReadOnlyBuffer();
readOnlyBuffer.position(0); // 重置位置,从头开始读取
StringBuilder contentBuilder = new StringBuilder();
try {
if (readOnlyBuffer.hasRemaining()) {
byte[] bytes = new byte[readOnlyBuffer.remaining()];
readOnlyBuffer.get(bytes);
contentBuilder.append(new String(bytes));
} else {
log.error("缓冲区为空。");
contentBuilder.append("缓冲区为空。");
}
contentBuilder.append(" 异常: ").append(exceptionString);
} catch (Exception ex) {
log.error("读取缓冲区时出错: {}", ex.getMessage());
}
// 准备报警模型
NotifactionWarmModel model = new NotifactionWarmModel();
String localIp = IpUtils.getLocalIp();
model.setAppGroupName(RedBookDefaultValue.appGroupName);
model.setAppName(EnvUtils.get(ServerConfigKeys.APP_NAME));
model.setWarningName("MyDecodeExceptionHandler");
model.setLevel("II");
model.setAppEnv(EnvUtils.env());
model.setDeviceName(localIp);
model.setTime(ZonedDateTime.now());
model.setContent(contentBuilder.toString());
// 将异常详情保存到数据库
try {
long id = SnowflakeIdUtils.id();
Db.save("tio_boot_admin_system_exception", Row.by("id", id).set("exception", exceptionString));
} catch (Exception dbEx) {
log.error("将异常保存到数据库失败: {}", dbEx.getMessage());
}
// 发送报警通知
try {
AlarmUtils.send(model);
} catch (Exception alarmEx) {
log.error("发送报警通知失败: {}", alarmEx.getMessage());
}
}
}
```
### 详细说明
#### 1. 配置类 (`BeforeServerConfig`)
- **目的**:在服务器启动前设置自定义的 `DecodeExceptionHandler`。
- **注解**:
- `@AopBeforeStartConfiguration`:确保配置在服务器初始化前应用。
- `@Initialization`:标记执行配置的方法。
- **方法 `config()`**:
- 实例化 `MyDecodeExceptionHandler`。
- 注册到 `TioBootServer` 实例中,以处理解码异常。
#### 2. 基础 Handler (`MyDecodeExceptionHandler`)
- **目的**:通过关闭通道和记录缓冲区内容及异常详情来处理解码异常。
- **关键步骤**:
- **关闭通道**:防止有问题的连接继续通信,提升安全性。
- **缓冲区处理**:
- 创建只读缓冲区副本以安全访问数据。
- 重置缓冲区位置以从头读取。
- 提取并打印缓冲区内容(若有)以供调试。
- **异常日志记录**:将异常详情输出到控制台,便于调试。
#### 3. 增强的 Handler(包含日志记录、数据库存储和报警)
- **目的**:在基础功能上增加日志记录、将异常详情持久化到数据库以及发送报警通知,以提升处理器的全面性和可靠性。
- **关键增强**:
- **日志记录**:
- 使用 `Slf4j` 进行结构化和基于级别的日志记录。
- 记录异常详情以及处理缓冲区时的任何问题。
- **数据库存储**:
- 使用 ActiveRecord 模式 (`Db.save` 和 `Row.by`) 将异常详情保存到 `tio_boot_admin_system_exception` 表中。
- 使用 `SnowflakeIdUtils` 生成每条记录的唯一标识符。
- 处理可能的数据库异常,通过日志记录错误信息,避免影响主流程。
- **报警机制**:
- 构建包含异常和服务器环境信息的 `NotifactionWarmModel`。
- 使用 `AlarmUtils.send(model)` 发送报警通知,确保相关人员及时获知异常。
- 确保在发送报警失败时不会中断处理流程,并记录相关错误信息。
#### 4. 测试 Handler
- **测试场景**:发送格式错误的 HTTP 请求(例如 `http://localhost/?1==1`)以触发解码异常。
- **预期行为**:
- 自定义处理器关闭与请求相关的连接。
- 记录缓冲区内容和异常详情。
- 在增强的处理器中,将异常详情保存到数据库并发送报警通知。
---
通过如上所述的方式实现和增强 `DecodeExceptionHandler`,您将显著提升 tio-boot 服务器的安全性。妥善处理解码异常不仅有助于缓解潜在的攻击风险,还能维护服务器操作的完整性和可靠性。
---
# 开启虚拟线程(Virtual Thread)
URL: https://tio-boot.com/zh/04_configuration/08.html
Source: zh/04_configuration/08.md
# 开启虚拟线程(Virtual Thread)
[[toc]]
## 一、背景说明
自 **Java 21** 起,**虚拟线程(Virtual Thread)** 正式成为稳定特性(JEP 444),为高并发场景提供了一种**低成本、高并发、阻塞友好**的线程模型。
虚拟线程由 JVM 调度,不再与操作系统线程一一对应,在面对大量阻塞型任务(如数据库、RPC、文件 IO)时,能够显著降低线程创建、上下文切换以及内存占用成本。
`t-io / tio-boot` 作为高性能网络通信框架,在 **Java 21 环境下**,可以通过自定义 `ThreadFactory` 或 `ExecutorService` 的方式,让**业务处理逻辑运行在虚拟线程之上**,从而显著提升并发能力和资源利用率。
---
## 二、tio-core 中的线程模型说明
在 `tio-core` 中,线程大致可以分为以下三类(非常重要):
### 1. smart-common 线程(事件循环线程)
* **固定 2 个平台线程**
* 启动并长期运行:
* `commonWorker`
* `writeWorker`
主要职责:
* 处理 **Accept / Connect / Write** 事件
* 触发:
* `doAccept()`
* `doWrite()`
* `connect runnable.run()`
* 少量“同步 read”分支中的 `doRead()`
说明:
* 这是一组 **Selector / Reactor 事件循环线程**
* 长期常驻运行(`while(select)`)
* 对延迟和稳定性要求极高
* **不适合承载业务逻辑**
---
### 2. readWorkers(Read 事件循环线程)
* 数量可配置
* 默认用于:
* 处理 Read 就绪事件
* 数据读取
* 协议解码(ByteBuffer → Packet)
* 在未设置 `bizExecutor` 时,**直接执行 handler**
特点:
* 仍然是 **事件循环线程**
* 不是 per-task 模型
* 数量通常与 CPU 核数相关
* 可通过 `setWorkThreadFactory` + `setWorkThreadNum` 配置
---
### 3. 业务执行线程(BizExecutor)
* **默认未启用**
* 当未设置 `bizExecutor` 时:
* handler 由 `readWorkers` 线程直接执行
* 当设置 `bizExecutor` 后:
* handler 会被投递到业务线程池执行
* readWorkers 只负责 decode + 分发
说明:
* 这是 **最适合使用虚拟线程的一层**
* 可以是:
* 平台线程池(Java 8)
* 虚拟线程 Executor(Java 21)
---
### 补充说明(重要实测结论)
> 实测发现:
> **在返回纯文本、业务逻辑极轻且无阻塞的场景下,不设置 `bizExecutor`,直接由 readWorkers 执行 handler,性能确实非常高。**
但需要注意:
* 该结论 **仅适用于**:
* handler 不阻塞
* handler 极短
* 不访问数据库 / RPC / 文件 IO
* 一旦 handler 出现阻塞或长尾,请务必启用 `bizExecutor`
---
## 三、Java 版本要求
> **必须使用 Java 21 或以上版本**
原因:
* Java 19 / 20 中虚拟线程仍为预览特性
* Java 21 起虚拟线程正式 GA
* `Thread.ofVirtual()`、`Executors.newThreadPerTaskExecutor(...)` 在 Java 21 中稳定可用
可通过以下命令确认版本:
```bash
java -version
```
---
## 四、示例代码(tio-boot 中启用虚拟线程)
下面是一个在 `tio-boot` 中**启用虚拟线程作为业务执行线程**的最小示例。
```java
package example;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import nexus.io.tio.boot.TioApplication;
import nexus.io.tio.boot.server.TioBootServer;
public class Application {
public static void main(String[] args) {
ExecutorService businessExecutor = Executors.newThreadPerTaskExecutor(
Thread.ofVirtual().name("t-biz-v-", 0).factory());
// Configure the executor before starting the server.
TioBootServer server = TioBootServer.me();
server.setBizExecutor(businessExecutor);
Runtime.getRuntime().addShutdownHook(
new Thread(businessExecutor::shutdownNow, "business-executor-shutdown"));
TioApplication.run(Application.class, args);
}
}
```
### 配置时机与执行范围
应在 `TioApplication.run(...)` 之前调用 `setBizExecutor(...)`。框架初始化网络服务时,会将业务执行器传入运行时配置;普通 `BootConfiguration.config()` 用于后续应用配置,不适合放置这项启动设置。已有应用使用带 `BootConfiguration` 参数的 `run` 重载时,也应先在入口中设置业务执行器,再调用该重载。
上述示例仅配置业务执行器,保留网络 I/O 工作线程的默认配置。这样,等待上游 HTTP 响应等阻塞业务可以在虚拟线程中执行,网络工作线程继续接收和解码请求。同一连接内的业务任务仍按框架的顺序执行,不同连接可以并发处理。
可以在业务 Handler 中使用 `Thread.currentThread().isVirtual()` 验证执行线程。并发验证时,应使用多个独立连接,让部分请求等待本地模拟服务,同时检查其他接口是否仍能及时响应。
JDK 21 中,JNI 调用以及部分同步代码可能固定虚拟线程的载体线程。数据库查询仍应配合索引、合理的查询范围和并发控制;虚拟线程可以降低等待的平台线程成本,但不会自动减少 SQL 执行时间。
---
## 五、重要说明(非常关键)
### 1. tio-boot 并非所有线程都是虚拟线程
需要特别注意:
> **tio-boot 在处理“IO 就绪事件(SelectionKey)”时,使用的仍然是平台线程,而不是虚拟线程。**
具体如下:
#### IO / Reactor 事件循环线程
* Selector / Reactor 线程
* smart-common
* readWorkers
* **全部为平台线程**
* 数量有限
* 与 CPU 核数强相关
#### 业务处理线程
* 通过 `setBizExecutor(...)` 注入
* 可以使用 **虚拟线程**
* 数量不再受 OS 线程数限制
这是一个**标准的 Reactor + Worker 架构**,也是当前虚拟线程在网络框架中的**最佳实践模型**。
---
### 2. 为什么不把 IO 线程改成虚拟线程?
原因包括:
* Selector / Reactor 是:
* 常驻事件循环
* 非 per-task 模型
* 虚拟线程最适合:
* 阻塞式业务逻辑
* JDBC / Redis / RPC
* 文件 IO
* Reactor 线程数量本身很少,通常不是性能瓶颈
因此,将 **虚拟线程用于业务层,而不是 IO 事件循环层**,是当前最合理、最稳定的设计。
---
## 六、适用场景分析
启用虚拟线程后,tio-boot 非常适合以下场景:
* 高并发长连接
* WebSocket
* TCP
* 阻塞型业务逻辑
* JDBC
* Redis
* HTTP / RPC 调用
* Benchmark / 压测场景
* 单请求逻辑不复杂,但并发量极高的服务端应用
---
## 七、总结
* Java 21 起,虚拟线程可安全用于生产环境
* tio-boot 支持通过 `ThreadFactory` / `ExecutorService` 注入虚拟线程
* **业务处理线程非常适合使用虚拟线程**
* **IO / Reactor 事件循环线程仍应使用平台线程**
* 这种混合模型在性能、稳定性与可扩展性之间取得了良好平衡
---
---
# 框架级错误通知
URL: https://tio-boot.com/zh/04_configuration/09.html
Source: zh/04_configuration/09.md
# 框架级错误通知
## 一、背景介绍
在实际生产环境中,应用启动阶段的异常(例如数据库连接失败、Redis 连接异常、注解扫描失败、服务启动失败等)如果不能被及时感知,往往会导致问题发现滞后。
**TioBoot 提供了框架级通知能力**,可以在应用启动的关键阶段捕获异常并通过自定义通知通道发送告警信息,从而实现启动即感知、失败即通知。
本文将详细介绍:
* 如何实现自定义框架级通知发送器
* 如何在启动类中正确配置通知发送器
* 启动失败时的通知示例
* 使用过程中的关键注意事项
---
## 二、框架级通知触发时机
TioBoot 在以下阶段发生异常时,会触发框架级通知:
* 数据库(DataSource)初始化失败
* Redis 连接失败
* 注解扫描异常
* 服务启动失败
* TioBoot 配置类执行异常
通知内容通常包含:
* 时间
* 运行环境
* 应用名称
* 异常级别
* 服务器 IP
* 完整堆栈信息
* 错误描述
---
## 三、自定义通知发送器实现
### 3.1 NotificationSender 实现类
下面示例展示了一个基于 **飞书(Lark Suite)Webhook** 的框架级通知发送器实现。
```java
import nexus.io.tio.boot.admin.utils.TioVirtualThreadUtils;
import nexus.io.tio.utils.notification.NotificationSender;
import nexus.io.tio.utils.environment.EnvUtils;
import nexus.io.tio.utils.notification.LarksuiteNotificationUtils;
import nexus.io.tio.utils.notification.NotifactionWarmModel;
import lombok.extern.slf4j.Slf4j;
import okhttp3.Response;
@Slf4j
public class MyNotificationSender implements NotificationSender {
@Override
public boolean send(NotifactionWarmModel model) {
final String webHookUrl = EnvUtils.get("warm.notification.webhook.url");
if (webHookUrl != null) {
try (Response response = LarksuiteNotificationUtils.sendWarm(webHookUrl, model)) {
if (!response.isSuccessful()) {
log.error("Faild to push :{}", response.body().string());
return false;
}
} catch (Exception e) {
log.error(e.getMessage(), e);
return false;
}
}
return true;
}
@Override
public boolean sendAsync(NotifactionWarmModel model) {
TioVirtualThreadUtils.submit(() -> {
send(model);
});
return true;
}
}
```
### 3.2 设计说明
* `send`
同步发送通知,适用于启动阶段的重要错误通知。
* `sendAsync`
使用虚拟线程异步发送,避免阻塞主流程。
* WebHook 地址通过 `EnvUtils` 动态读取,支持多环境配置。
---
## 四、在启动类中启用系统通知
### 4.1 启动类配置示例
**必须在应用启动前设置 NotificationSender**,否则启动阶段无法获取通知实现。
```java
package nexus.io.manim;
import nexus.io.annotation.AComponentScan;
import nexus.io.manim.config.MvAdminAppConfig;
import nexus.io.manim.notification.MyNotificationSender;
import nexus.io.tio.boot.TioApplication;
import nexus.io.tio.boot.server.TioBootServer;
@AComponentScan("nexus.io.manim.controller")
public class CanvasApp {
public static void main(String[] args) {
long start = System.currentTimeMillis();
TioBootServer server = TioBootServer.me();
server.setNotificationSender(new MyNotificationSender());
TioApplication.run(CanvasApp.class, new MvAdminAppConfig(), args);
long end = System.currentTimeMillis();
System.out.println((end - start) + "(ms)");
}
}
```
### 4.2 关键点说明
* `server.setNotificationSender(new MyNotificationSender())`
**必须在 `TioApplication.run` 之前调用**
* 通知发送器在 `TioApplicationContext` 启动阶段就会被使用
---
## 五、启动失败通知示例
当数据库连接失败时,系统会自动推送如下通知信息:
```
- Time : 2026-01-24 18:27:09 +08:00
- App Env : dev
- App Group Name : app.group.name
- Name : TioApplicationContext
- Level : LeveL 0
- Device : 192.168.3.219
- Stack Trace :
com.zaxxer.hikari.pool.HikariPool$PoolInitializationException:
Failed to initialize pool: FATAL: password authentication failed for user "postgres"
at com.zaxxer.hikari.pool.HikariPool.throwPoolInitializationException(HikariPool.java:596)
at com.zaxxer.hikari.pool.HikariPool.checkFailFast(HikariPool.java:582)
at com.zaxxer.hikari.pool.HikariPool.(HikariPool.java:115)
at com.zaxxer.hikari.HikariDataSource.(HikariDataSource.java:81)
at nexus.io.tio.boot.admin.config.TioAdminDbConfiguration.config(TioAdminDbConfiguration.java:61)
at nexus.io.manim.config.MvAdminAppConfig.config(MvAdminAppConfig.java:28)
at nexus.io.tio.boot.context.TioApplicationContext.run(TioApplicationContext.java:282)
at nexus.io.tio.boot.TioApplication.run(TioApplication.java:27)
at nexus.io.manim.CanvasApp.main(CanvasApp.java:27)
Caused by: org.postgresql.util.PSQLException:
FATAL: password authentication failed for user "postgres"
- Content :
Failed to run tioBootConfiguration.config()
```
---
## 六、重要注意事项
### 注意事项一:必须在启动类中设置 NotificationSender
```java
server.setNotificationSender(new MyNotificationSender());
```
原因:
* `TioApplicationContext` 在启动早期即会使用通知能力
* 如果未设置,启动阶段的异常将无法发送通知
---
### 注意事项二:EnvUtils.get 必须在方法内部调用
```java
final String webHookUrl = EnvUtils.get("warm.notification.webhook.url");
```
原因:
* `EnvUtils.load()` 的执行时机 **晚于 `new MyNotificationSender()`**
* 如果在构造函数或成员变量中读取配置,可能会拿到 `null`
* 放在 `send` 方法内部可确保环境变量已加载完成
---
## 七、总结
通过 TioBoot 提供的框架级通知机制,可以实现:
* 启动即告警
* 配置错误秒级感知
* 启动失败无需登录服务器排查
配合企业微信、飞书、钉钉等 Webhook,可以极大提升应用的可运维性和稳定性。
如需扩展通知渠道(短信、邮件、多 Webhook),只需实现 `NotificationSender` 接口即可。
---
---
# 04_configuration
URL: https://tio-boot.com/zh/04_configuration/
Source: zh/04_configuration/index.md
# 04_configuration
本目录包含 10 个文件和 0 个子目录,下列摘要基于文件名、一级标题或资源类型整理。
## 文件清单
- [readme.md](): 当前目录的导航与文件摘要。
- [01.md](): 文档《配置参数》。
- [02.md](): 文档《服务器监听器》。
- [03.md](): 文档《内置缓存系统 AbsCache》。
- [04.md](): 文档《使用 Redis 作为内部 Cache》。
- [05.md](): 文档《静态文件处理器》。
- [06.md](): 文档《基于域名的静态资源隔离》。
- [07.md](): 文档《DecodeExceptionHandler》。
- [08.md](): 文档《开启虚拟线程(Virtual Thread)》。
- [09.md](): 文档《框架级错误通知》。
---
# Json
URL: https://tio-boot.com/zh/05_json/01.html
Source: zh/05_json/01.md
# Json
[[toc]]
## 概述
Tio-Boot 的 JSON 模块以抽象类 `Json` 为核心,方便扩展第三方实现。Tio-Boot 官方提供了五种 `Json` 实现,分别是 `TioJson`、`FastJson2`、`Jackson`、`Gson` 和 `MixedJson`,这些实现均继承自抽象类 `Json`。
抽象类 `Json` 的核心方法如下:
```java
public abstract class Json {
public abstract String toJson(Object object);
public abstract T parse(String jsonString, Class type);
}
```
如上所示,`Json` 抽象类定义了两个核心方法:`toJson(...)` 将任意 Java 对象转换为 JSON 字符串,`parse(...)` 将 JSON 字符串反序列化为指定类型的对象。
## JSON 实现
Tio-Boot 官方默认提供了五种 `Json` 实现,每种实现都有其独特的特性和适用场景。
### 1. TioJson
**TioJson** 是 Tio-Boot 内置的 JSON 实现,位于 `tio-utils` 模块中。它主要用于将 Java 对象序列化为 JSON 字符串,不支持反序列化。
**特点:**
- 体积小,输出速度快。
- 仅支持序列化,不支持反序列化。
**使用示例:**
```java
TioJson.getJson().toJson(object);
```
### 2. FastJson2
**FastJson2** 是基于第三方库 FastJSON2 进行的二次封装。该实现依赖于模型和 Java Bean 的 getter 方法进行转换。
**特点:**
- 支持丰富的 JSON 转换功能。
- 可按照 FastJSON 官方文档配置各种转换参数。
**依赖配置:**
```xml
com.alibaba.fastjson2
fastjson2
${fastjson.version}
```
### 3. Jackson
**Jackson** 实现是基于第三方库 Jackson 进行的二次封装,与 FastJson2 类似。
**特点:**
- 功能强大,支持复杂的 JSON 操作。
- 广泛应用于各种 Java 项目中。
**依赖配置:**
```xml
com.fasterxml.jackson.core
jackson-databind
2.11.0
```
### 4. Gson
**Gson** 实现是基于第三方库 Gson 进行的二次封装,与 FastJson2 和 Jackson 类似。
**特点:**
- 简洁易用,适合快速开发。
- 支持多种自定义序列化和反序列化策略。
**依赖配置:**
```xml
com.google.code.gson
gson
2.10.1
```
### 5. MixedJson
**MixedJson** 是对 `TioJson` 和 `FastJson2` 的再一次封装。在序列化时使用 `TioJson`,反序列化时使用 `FastJson2`。
**特点:**
- 结合了 `TioJson` 和 `FastJson2` 的优势。
- 提供更灵活的 JSON 转换能力。
**依赖配置:**
```xml
com.alibaba.fastjson2
fastjson2
${fastjson.version}
```
## 配置
Tio-Boot 默认使用 `MixedJson`,序列化委托 `TioJson`,反序列化委托 `FastJson2`。可通过 `tio.json.provider` 选择 `fastjson`、`jackson` 或 `gson`,也可在启动时设置自定义工厂。
### 默认配置
未指定 provider 时使用 `MixedJsonFactory`。`tio.json.skipNull=true` 可让当前工厂的默认输出跳过 null 字段;也可以仅在某个调用点使用 `JsonUtils.toSkipNullJson`。
### 自定义配置
可以通过 `JsonManager` 来设置默认的 `JsonFactory`,以切换到其他 JSON 实现。
**示例:切换到 FastJson2**
```java
import nexus.io.tio.utils.json.FastJson2Factory;
import nexus.io.tio.utils.json.JsonManager;
JsonManager.me().setDefaultJsonFactory(new FastJson2Factory());
```
**自定义实现:**
假设用户扩展了 `MyJson` 和 `MyJsonFactory`,可以通过以下方式切换到自定义实现:
```java
import nexus.io.tio.utils.json.IJsonFactory;
import nexus.io.tio.utils.json.JsonManager;
public class JsonFactoryConfig {
public void configure(IJsonFactory factory) {
JsonManager.me().setDefaultJsonFactory(factory);
}
}
```
**使用 MixedJsonFactory:**
```java
import nexus.io.tio.utils.json.JsonManager;
import nexus.io.tio.utils.json.MixedJsonFactory;
JsonManager.me().setDefaultJsonFactory(new MixedJsonFactory());
```
### 日期格式配置
可以自定义 `Date` 类型在序列化后的格式:
```java
import nexus.io.tio.utils.json.JsonManager;
JsonManager.me().setDefaultDatePattern("yyyy-MM-dd");
```
### 配置示例
以下是一个使用 Jackson 作为默认 JSON 实现的配置示例:
```java
import nexus.io.annotation.AConfiguration;
import nexus.io.annotation.Initialization;
import nexus.io.tio.utils.json.JacksonFactory;
import nexus.io.tio.utils.json.JsonManager;
@AConfiguration
public class JsonConfig {
@Initialization
public void config() {
JsonManager.me().setDefaultJsonFactory(new JacksonFactory());
}
}
```
## JSON 转换用法
Tio-Boot 中的 JSON 转换主要有两类用法:使用配置的 JSON 转换器和指定某个实现进行转换。
### 1. 使用配置的 JSON 转换器
可以通过 `Json` 类直接进行 JSON 转换:
```java
Json.getJson().toJson(object);
Json.getJson().parse(jsonString, type);
```
### 2. 使用 `JsonUtils` 工具类
Tio-Boot 提供了 `JsonUtils` 工具类,简化 JSON 转换操作:
```java
import nexus.io.tio.utils.json.JsonUtils;
JsonUtils.toJson(object);
JsonUtils.parse(jsonString, type);
```
### 3. 使用指定的 JSON 实现
可以临时指定使用某个 JSON 实现进行转换,而不依赖全局配置。例如,临时使用 FastJson2 进行转换:
```java
import nexus.io.tio.utils.json.FastJson2;
FastJson2.getJson().toJson(object);
FastJson2.getJson().parse(jsonString, type);
```
这种用法可以在需要时绕过全局配置,使用特定的 JSON 实现。
### 4. 单次调用保留 Long 数值类型
为前端展示 ID 时,默认的 TioJson、MixedJson 和 FastJson2 可按全局 Long 策略输出字符串,避免 JavaScript 数值精度损失。对 JSON Schema、计数或第三方协议报文,可以在本次调用中明确保留 Long 为 JSON number,同时沿用项目选择的 JSON 实现。
| 方法 | Long 输出 | null 字段 |
| --- | --- | --- |
| `JsonUtils.toJson(object)` | 沿用当前实现的默认策略 | 沿用当前工厂 |
| `JsonUtils.toSkipNullJson(object)` | 沿用当前实现的默认策略 | 跳过 |
| `JsonUtils.toJsonWithLongAsNumber(object)` | JSON number | 沿用当前工厂 |
| `JsonUtils.toSkipNullJsonWithLongAsNumber(object)` | JSON number | 跳过 |
新方法分别委托 `Json.getJson().toJson(object, false)` 和 `Json.getSkipNullJson().toJson(object, false)`。TioJson、MixedJson、FastJson2、Jackson、GsonJson 均支持该重载;调用不会切换全局工厂,也不会修改全局 Long 策略,可与普通序列化并发使用。原本的 String 仍输出字符串,数组中的 null 仍保留。
```java
import com.jfinal.kit.Kv;
import nexus.io.tio.utils.json.JsonUtils;
public class NumericSchemaExample {
public static void main(String[] args) {
Kv schema = Kv.create()
.set("type", "integer")
.set("maximum", 9007199254740991L)
.set("label", "9007199254740991")
.set("description", null);
String json = JsonUtils.toSkipNullJsonWithLongAsNumber(schema);
System.out.println(json);
}
}
```
输出中的 `maximum` 没有引号,`label` 保留引号,`description` 被省略:
```json
{"type":"integer","maximum":9007199254740991,"label":"9007199254740991"}
```
字段顺序由所选实现决定。`toJsonWithLongAsNumber` 沿用工厂的 null 设置:如果已启用 `tio.json.skipNull`,该方法也会跳过 null 字段。
自定义 `Json` 实现可覆写 `toJson(Object object, boolean longToString)`,`false` 表示数值,`true` 表示字符串。未实现该重载的自定义 provider 会明确抛出 `UnsupportedOperationException`,便于调用方适配;原有 `toJson(Object)` 调用保持兼容。不要在单个请求中临时修改再恢复 `Json.setLongToString`,避免影响其他线程。
## TioJson 详解
Tio-Boot 默认使用的 JSON 转换器是 `MixedJson`,其中:
- **序列化**(Object 转 JSON)使用 `TioJson`。
- **反序列化**(JSON 转 Object)使用 `FastJson2`。
这样设计的好处是 `TioJson` 更轻量,序列化速度更快,而 `FastJson2` 提供了强大的反序列化能力。
### 1. 使用 TioJson
**序列化示例:**
```java
import java.util.ArrayList;
import java.util.List;
import org.junit.Test;
import com.jfinal.kit.Kv;
import nexus.io.tio.utils.json.TioJson;
public class WebsiteButtonServiceTest {
@Test
public void test() {
Kv kv = Kv.by("name", "01")
.set("link", null)
.set("title", "I am the fan marketing team");
List kvList = new ArrayList<>();
kvList.add(kv);
TioJson tioJson = new TioJson();
String json = tioJson.toJson(kvList);
System.out.println(json);
}
}
```
**输出结果:**
```json
[{ "name": "01", "link": null, "title": "I am the fan marketing team" }]
```
### 2. 常见参数设置
#### 2.1 跳过空值字段 (`SkipNullValueField`)
默认情况下,`SkipNullValueField` 的值为 `false`。如果字段的值为 `null`,则会输出 `"字段名":null`。例如:
```json
[{ "name": "01", "link": null, "title": "I am the fan marketing team" }]
```
为了减少前后端的通讯量,可以将 `SkipNullValueField` 设置为 `true`,此时值为 `null` 的字段将被跳过:
```java
TioJson.setSkipNullValueField(true);
```
**修改后的输出结果:**
```json
[{ "name": "01", "title": "I am the fan marketing team" }]
```
#### 2.2 自定义时间戳格式 (`TimestampPattern`)
`TioJson` 默认将 `java.sql.Timestamp` 输出为毫秒时间戳格式。例如:
```java
package com.sejie.admin.services;
import java.sql.Timestamp;
import org.junit.Test;
import com.jfinal.kit.Kv;
import nexus.io.tio.utils.json.JsonUtils;
public class TimestampFormatTest {
@Test
public void testWithDate() {
long currentTimeSecond = System.currentTimeMillis();
Timestamp timestamp = new Timestamp(currentTimeSecond);
Kv kv = Kv.by("date", timestamp);
String json = JsonUtils.toJson(kv);
System.out.println(json);
}
}
```
**输出结果:**
```json
{ "date": 1720423322746 }
```
可以自定义时间戳的输出格式,例如:
```java
import nexus.io.tio.utils.json.Json;
Json.setTimestampPattern("yyyy-MM-dd HH:mm:ss");
```
**修改后的输出结果:**
```json
{ "date": "2024-07-08 16:30:35" }
```
## 依赖管理
在使用不同的 JSON 实现时,需要添加相应的依赖。以下是各实现的依赖配置示例:
### TioJson
无需额外依赖,`TioJson` 是 Tio-Boot 内置的实现。
### FastJson2
```xml
com.alibaba.fastjson2
fastjson2
${fastjson.version}
```
### Jackson
```xml
com.fasterxml.jackson.core
jackson-databind
2.11.0
```
### Gson
```xml
com.google.code.gson
gson
2.10.1
```
### MixedJson
需要同时添加 `TioJson` 和 `FastJson2` 的依赖。由于 `TioJson` 内置,只需添加 `FastJson2` 依赖:
```xml
com.alibaba.fastjson2
fastjson2
${fastjson.version}
```
---
通过以上配置和使用指南,开发者可以根据项目需求灵活选择和配置 Tio-Boot 的 JSON 实现,充分发挥其在序列化和反序列化过程中的优势。
---
# 接受 JSON 和响应 JSON
URL: https://tio-boot.com/zh/05_json/02.html
Source: zh/05_json/02.md
# 接受 JSON 和响应 JSON
## 手动处理 Json 请求
### 接收 Json 数据
#### 从 http 请求中获取 json 字符串
从 http 请求中获取 json 字符串,接收 JSON 数据有需要手动转为 JavaBean
```java
String bodyString = request.getBodyString();
```
示例代码
```java
package nexus.io.tio.web.hello.controller;
import nexus.io.annotation.AController;
import nexus.io.annotation.RequestPath;
import nexus.io.tio.http.common.HttpRequest;
import nexus.io.tio.http.common.HttpResponse;
import nexus.io.tio.http.server.util.Resps;
import lombok.extern.slf4j.Slf4j;
@AController
@RequestPath("/test/json")
@Slf4j
public class TestJsonController {
@RequestPath(value = "/getBodyString")
public HttpResponse getBodyString(HttpRequest request) throws Exception {
String bodyString = request.getBodyString();
log.info(bodyString);
HttpResponse ret = Resps.txt(request, bodyString);
return ret;
}
}
```
1. **`@RequestPath`**:
@RequestPath("/test/json"),用于声明这个类是一个请求控制器,并指定了访问这个控制器的基础 URL 路径。在这种情况下,任何发送到 `/test/json` 的 HTTP 请求都将由这个控制器处理。
2. **Method `getBodyString`**:
- `@RequestPath(value = "/getBodyString")`: 这个方法注解指定了具体的请求路径。当 HTTP 请求发送到 `/test/json/getBodyString` 时,将调用此方法。
- 方法接收一个 `HttpRequest` 对象作为参数,代表接收到的 HTTP 请求。
- `request.getBodyString()`: 从 HTTP 请求体中获取字符串内容。
- `log.info(bodyString)`: 使用日志记录请求体的内容。
- `HttpResponse ret = Resps.txt(request, bodyString)`: 创建一个 `HttpResponse` 对象来响应请求。`Resps.txt` 方法创建了一个文本响应,内容为请求体中的字符串。
- `return ret;`: 返回构建的响应对象。
请求信息
```
curl --location --request POST 'http://127.0.0.1/test/json/getBodyString' \
--header 'User-Agent: apifox/1.0.0 (https://www.apifox.cn)' \
--header 'Content-Type: application/json' \
--header 'Accept: */*' \
--header 'Host: 127.0.0.1' \
--header 'Connection: keep-alive' \
--data-raw '{
"ip": "49.174.104.160",
"loginName": "引需必装起",
"nick": "邹霞"
}'
```
## 通过实体类接受 JSON 和响应 JSON
### 实体类
```java
package top.ppnt.java.ee.tio.http.server.boot.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@NoArgsConstructor
@AllArgsConstructor
@Data
@Builder
public class User {
private Integer id;
private String loginName;
private String nick;
private String ip;
}
```
#### 自动封装为 java bean
在 Action 的方法签名上添加参数 User user
##### 示例代码
```
import nexus.io.tio.boot.hello.model.User;
import nexus.io.tio.http.common.HttpRequest;
import nexus.io.tio.http.common.HttpResponse;
import nexus.io.annotation.RequestPath;
import nexus.io.tio.http.server.util.Resps;
import lombok.extern.slf4j.Slf4j;
@AController
@RequestPath("/test/json")
@Slf4j
public class TestJsonController {
@RequestPath(value = "/bean")
public User bean(User user, HttpRequest request) throws Exception {
return user;
}
}
```
#### 自动封装为 java bean 支持下面这些格式的请求
##### 支持 POST 请求格式 application/json
```http
POST /demo/bean HTTP/1.1
Host: 127.0.0.1
Content-Type: application/json
{
"ip": "69.134.20.34",
"loginName": "别同器",
"nick": "汪刚"
}
```
##### 支持 POST 请求格式 application/x-www-form-urlencoded
```http
GET /demo/bean HTTP/1.1
Host: 127.0.0.1
Content-Type: application/x-www-form-urlencoded
loginName=Ping%20E%20Lee&nick=%E6%9D%8E%E9%80%9A&ip=127.0.0..1
```
###### 支持 POST 请求格式 multipart/form-data
示例
```http
POST /demo/bean HTTP/1.1
Host: 127.0.0.1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
----WebKitFormBoundary7MA4YWxkTrZu0gW
----WebKitFormBoundary7MA4YWxkTrZu0gW
----WebKitFormBoundary7MA4YWxkTrZu0gW
----WebKitFormBoundary7MA4YWxkTrZu0gW
```
##### 支持 Get 请求 Url 传递参数
示例如下
```http
GET /demo/bean?loginName=Ping%20E%20Lee&nick=%E6%9D%8E%E9%80%9A&ip=127.0.0..1 HTTP/1.1
Host: 127.0.0.1
```
### 返回 Json 数据
直接返回 json 数据
Action 的返回值可以直接是实体类,框架会自动进行转换
```java
package nexus.io.tio.boot.hello.controller;
import nexus.io.tio.boot.hello.model.User;
import nexus.io.tio.http.common.HttpRequest;
import nexus.io.tio.http.common.HttpResponse;
import nexus.io.annotation.RequestPath;
import nexus.io.tio.http.server.util.Resps;
import lombok.extern.slf4j.Slf4j;
@AController
@RequestPath("/test/json")
@Slf4j
public class TestJsonController {
@RequestPath(value = "/responseUser")
public User responseUser(HttpRequest request) throws Exception {
return User.builder().loginName("Ping E Lee").nick("李通").ip("127.0.0.1").build();
}
}
```
@RequestPath(value = "/responseUser"): 方法注解,指定了此方法处理的具体请求路径。当 HTTP 请求发送到 /test/json/responseUser 时,此方法将被调用。
方法返回 User 类型的对象。这里使用了 User 类的 builder 模式创建了一个 User 对象,并设置了相关属性。
该方法无需额外的 HTTP 响应处理,因为 tio-boot 框架将自动处理 User 对象的序列化并返回 JSON 格式的响应。
响应
```json
{
"ip": "127.0.0.1",
"loginName": "Ping E Lee",
"nick": "李通"
}
```
使用 Resps.json(request, user);返回 json 数据
```java
package nexus.io.tio.boot.hello.controller;
import nexus.io.tio.boot.hello.model.User;
import nexus.io.tio.http.common.HttpRequest;
import nexus.io.tio.http.common.HttpResponse;
import nexus.io.annotation.RequestPath;
import nexus.io.tio.http.server.util.Resps;
import lombok.extern.slf4j.Slf4j;
@AController
@RequestPath("/test/json")
@Slf4j
public class TestJsonController {
@RequestPath(value = "/responseJson")
public HttpResponse json(HttpRequest request) throws Exception {
User user = User.builder().loginName("Ping E Lee").nick("李通").ip("127.0.0.1").build();
HttpResponse ret = Resps.json(request, user);
return ret;
}
}
```
**Method `responseJson`**:
- `@RequestPath(value = "/responseJson")`: 方法注解,指定了此方法处理的具体请求路径。当 HTTP 请求发送到 `/test/json/responseJson` 时,此方法将被调用。
- 方法内部创建了一个 `User` 对象,使用了 Builder 模式设置了其属性。
- 使用 `Resps.json(request, user)` 生成了一个包含 JSON 格式用户数据的 `HttpResponse` 对象。这种方式是 tio-boot 框架中处理和返回 JSON 数据的标准做法。
- 最后,方法返回这个 `HttpResponse` 对象。
请求
http://127.0.0.1/demo/responseJson
响应
```json
{
"ip": "127.0.0.1",
"loginName": "Ping E Lee",
"nick": "李通"
}
```
使用 Resp 返回 Json 数据
核心代码
```
return Resps.json(request, Resp.ok(systemInfo));
```
```
package nexus.io.tio.boot.hello.AController;
import nexus.io.tio.boot.hello.model.User;
import nexus.io.tio.http.common.HttpRequest;
import nexus.io.tio.http.common.HttpResponse;
import nexus.io.annotation.RequestPath;
import nexus.io.tio.http.server.util.Resps;
import nexus.io.tio.utils.resp.Resp;
import lombok.extern.slf4j.Slf4j;
@AController
@RequestPath("/test/json")
@Slf4j
public class TestJsonController {
@RequestPath("/responseJsonResp")
public HttpResponse responseJsonResp(HttpRequest request) {
User user = User.builder().loginName("Ping E Lee").nick("李通").ip("127.0.0.1").build();
return Resps.json(request, Resp.ok(user));
}
}
```
**Method `responseJsonResp`**:
- `@RequestPath("/responseJsonResp")`: 方法注解,指定了此方法处理的具体请求路径。当 HTTP 请求发送到 `/test/json/responseJsonResp` 时,此方法将被调用。
- 方法内部创建了一个 `User` 对象,使用了 Builder 模式设置了其属性。
- 使用 `Resps.json(request, Resp.ok(user))` 生成了一个包含 JSON 格式用户数据的 `HttpResponse` 对象。`Resp.ok(user)` 封装了 `User` 对象,并添加了一些额外的响应信息,例如成功状态。
- 最后,方法返回这个 `HttpResponse` 对象。
返回数据如下
```json
{
"data": "数据部分",
"ok": true
}
```
##### 使用 RespBodyVo 返回 Json 数据
核心代码
```
return Resps.json(request, RespBodyVo.ok(data));
```
示例
```java
package nexus.io.tio.boot.hello.AController;
import nexus.io.tio.boot.hello.model.User;
import nexus.io.tio.http.common.HttpRequest;
import nexus.io.tio.http.common.HttpResponse;
import nexus.io.annotation.RequestPath;
import nexus.io.tio.http.server.util.Resps;
import nexus.io.tio.utils.resp.Resp;
import nexus.io.tio.utils.resp.RespBodyVo;
import lombok.extern.slf4j.Slf4j;
@AController
@RequestPath("/test/json")
@Slf4j
public class TestJsonController {
@RequestPath("/responseJsonRespVo")
public HttpResponse responseJsonResps(HttpRequest request) {
User user = User.builder().loginName("Ping E Lee").nick("李通").ip("127.0.0.1").build();
return Resps.json(request, RespBodyVo.ok(user));
}
}
```
**Method `responseJsonResps`**:
- `@RequestPath("/responseJsonRespVo")`: 方法注解,指定了此方法处理的具体请求路径。当 HTTP 请求发送到 `/test/json/responseJsonRespVo` 时,此方法将被调用。
- 方法内部创建了一个 `User` 对象,使用了 Builder 模式设置了其属性。
- 使用 `Resps.json(request, RespBodyVo.ok(user))` 生成了一个包含 JSON 格式用户数据的 `HttpResponse` 对象。`RespBodyVo.ok(user)` 封装了 `User` 对象,并可能添加了一些额外的响应信息,例如成功状态,这是一种标准的响应对象格式。
- 最后,方法返回这个 `HttpResponse` 对象。
返回数据如下
```json
{
"data": "数据部分",
"ok": true
}
```
---
# 响应实体类
URL: https://tio-boot.com/zh/05_json/03.html
Source: zh/05_json/03.md
# 响应实体类
[[toc]]
## RespBodyVo
`RespBodyVo` 是一个自定义的响应体封装类,用于统一 API 接口的响应格式。通过使用 `RespBodyVo`,可以确保所有 API 响应的一致性,便于前后端的协作和错误处理。`RespBodyVo` 提供了多种静态工厂方法和链式调用方式,简化了响应的构建过程。
### 类定义
```java
package nexus.io.model.body;
import nexus.io.model.resp.RespCode;
/**
* 响应实体类
*/
public class RespBodyVo implements java.io.Serializable {
private static final long serialVersionUID = 7492427869347211588L;
/**
* 是否成功,true 表示成功,false 表示失败
*/
private boolean ok;
/**
* 业务编码:一般是在失败情况下会用到这个,以便告知用户失败的原因是什么
*/
private Integer code;
/**
* 业务数据,譬如分页数据,用户信息数据等
*/
private Object data;
/**
* 消息,一般用于显示
*/
private String msg;
// 静态工厂方法和构造方法省略
// ...
// Getter 和 Setter 方法省略
// ...
}
```
### 属性说明
- **ok** (`boolean`): 表示响应的结果状态,`true` 表示成功,`false` 表示失败。
- **code** (`Integer`): 业务编码,通常在失败情况下使用,用于标识具体的错误类型或原因。
- **data** (`Object`): 返回的业务数据,可以是任何类型的对象,如分页数据、用户信息等。
- **msg** (`String`): 消息,用于向用户展示操作结果或错误信息。
### 构造方法
- `RespBodyVo(boolean ok)`: 私有构造方法,根据传入的 `ok` 状态设置 `ok` 属性。
### 静态工厂方法
#### 成功响应 (`ok` 系列方法)
- `static RespBodyVo ok()`: 创建一个默认的成功响应,`ok` 设置为 `true`,`code` 设置为 `1`。
```java
public static RespBodyVo ok() {
RespBodyVo resp = new RespBodyVo(true);
resp.code = 1;
return resp;
}
```
- `static RespBodyVo ok(Object data)`: 创建一个成功响应,并包含业务数据。
```java
public static RespBodyVo ok(Object data) {
return ok().data(data);
}
```
- `static RespBodyVo ok(String msg, Object data)`: 创建一个成功响应,包含自定义消息和业务数据。
```java
public static RespBodyVo ok(String msg, Object data) {
RespBodyVo resp = new RespBodyVo(true);
resp.code = 1;
resp.msg = msg;
resp.data = data;
return resp;
}
```
#### 失败响应 (`fail` 系列方法)
- `static RespBodyVo fail()`: 创建一个默认的失败响应,`ok` 设置为 `false`,`code` 设置为 `0`。
```java
public static RespBodyVo fail() {
RespBodyVo resp = new RespBodyVo(false);
resp.code = 0;
return resp;
}
```
- `static RespBodyVo fail(String msg)`: 创建一个失败响应,并包含错误消息。
```java
public static RespBodyVo fail(String msg) {
return fail().msg(msg);
}
```
- `static RespBodyVo failData(Object data)`: 创建一个失败响应,并包含业务数据。
```java
public static RespBodyVo failData(Object data) {
return fail().setData(data);
}
```
### 链式调用方法
- `RespBodyVo code(Integer code)`: 设置 `code` 属性并返回当前对象。
- `RespBodyVo data(Object data)`: 设置 `data` 属性并返回当前对象。
- `RespBodyVo msg(String msg)`: 设置 `msg` 属性并返回当前对象。
- `RespBodyVo setCode(Integer code)`: 同 `code(Integer code)`。
- `RespBodyVo setData(Object data)`: 同 `data(Object data)`。
- `RespBodyVo setMsg(String msg)`: 同 `msg(String msg)`。
### 方法说明
- `Integer getCode()`: 获取业务编码。
- `Object getData()`: 获取业务数据。
- `String getMsg()`: 获取消息。
- `boolean isOk()`: 判断响应是否为成功状态。
### 使用示例
以下是一个完整的控制器示例,展示如何使用 `RespBodyVo` 进行 API 响应封装:
```java
package com.hami.book.kepping.controller;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import nexus.io.annotation.Get;
import nexus.io.annotation.RequestPath;
import nexus.io.model.body.RespBodyVo;
@RequestPath("/example")
public class ExampleController {
@Get("/success")
public RespBodyVo getSuccess() {
return RespBodyVo.ok();
}
@Get("/data")
public RespBodyVo getData() {
Map data = new HashMap<>();
data.put("key", "value");
return RespBodyVo.ok(data);
}
@Get("/custom-success")
public RespBodyVo getCustomSuccess() {
List list = Arrays.asList("item1", "item2");
return RespBodyVo.ok("Operation successful", list);
}
@Get("/fail")
public RespBodyVo getFail() {
return RespBodyVo.fail();
}
@Get("/fail-message")
public RespBodyVo getFailMessage() {
return RespBodyVo.fail("Invalid request parameters");
}
@Get("/chain")
public RespBodyVo getChain() {
return RespBodyVo.ok().setMsg("Chained response").setData(Arrays.asList(1, 2, 3));
}
}
```
#### 1. 返回默认的成功响应
```java
@Get("/success")
public RespBodyVo getSuccess() {
return RespBodyVo.ok();
}
```
**返回值**
```json
{
"ok": true,
"code": 1,
"data": null,
"msg": null
}
```
#### 2. 返回成功响应并包含数据
```java
@Get("/data")
public RespBodyVo getData() {
Map data = new HashMap<>();
data.put("key", "value");
return RespBodyVo.ok(data);
}
```
**返回值**
```json
{
"ok": true,
"code": 1,
"data": {
"key": "value"
},
"msg": null
}
```
#### 3. 返回成功响应并包含自定义消息和数据
```java
@Get("/custom-success")
public RespBodyVo getCustomSuccess() {
List list = Arrays.asList("item1", "item2");
return RespBodyVo.ok("Operation successful", list);
}
```
**返回值**
```json
{
"ok": true,
"code": 1,
"data": ["item1", "item2"],
"msg": "Operation successful"
}
```
#### 4. 返回默认的失败响应
```java
@Get("/fail")
public RespBodyVo getFail() {
return RespBodyVo.fail();
}
```
**返回值**
```json
{
"ok": false,
"code": 0,
"data": null,
"msg": null
}
```
#### 5. 返回失败响应并包含错误消息
```java
@Get("/fail-message")
public RespBodyVo getFailMessage() {
return RespBodyVo.fail("Invalid request parameters");
}
```
**返回值**
```json
{
"ok": false,
"code": 0,
"data": null,
"msg": "Invalid request parameters"
}
```
#### 6. 使用链式调用设置响应内容
```java
@Get("/chain")
public RespBodyVo getChain() {
return RespBodyVo.ok()
.setMsg("Chained response")
.setData(Arrays.asList(1, 2, 3));
}
```
**返回值**
```json
{
"ok": true,
"code": 1,
"data": [1, 2, 3],
"msg": "Chained response"
}
```
### 注意事项
- **序列化**: `RespBodyVo` 实现了 `Serializable` 接口,确保其可以被序列化为 JSON 或其他格式。
- **结果状态**: `ok` 属性用于表示响应状态,`true` 表示成功,`false` 表示失败。
- **业务编码**: `code` 属性在失败情况下用于标识具体的错误类型或原因,便于前端进行相应的错误处理。
- **链式调用**: 通过链式调用方式设置属性,提升代码的可读性和简洁性。例如:`RespBodyVo.ok().setMsg("...").setData(...)`
- **扩展性**: 可以根据业务需求扩展 `RespBodyVo` 的属性和方法,以满足更复杂的响应需求。
### 总结
`RespBodyVo` 提供了一种简洁且一致的方式来封装 API 响应。通过其丰富的静态工厂方法和灵活的链式调用方式,可以满足各种场景下的响应需求。结合清晰的属性定义和方法设计,使得代码更加可维护和易于理解。
## RsultVo
`ResultVo` 是 `tio-boot` 框架内置的响应体封装类,用于统一 API 接口的响应格式。通过使用 `ResultVo`,可以确保所有 API 响应的一致性,便于前后端的协作和错误处理。
### 类定义
```java
package nexus.io.model.result;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class ResultVo implements java.io.Serializable {
private static final long serialVersionUID = 7295952087858355659L;
public static final int FAIL_CODE = 400;
private int code = 200;
private String message;
private Object data;
// 构造方法
public ResultVo(Object data) { /* ... */ }
public ResultVo(int code, String message) { /* ... */ }
public ResultVo(int code, Object data) { /* ... */ }
public ResultVo(String message, Object data) { /* ... */ }
// 静态工厂方法 - 成功响应
public static ResultVo ok() { /* ... */ }
public static ResultVo ok(Object data) { /* ... */ }
public static ResultVo ok(String message, Object data) { /* ... */ }
public static ResultVo ok(int code, String message, Object data) { /* ... */ }
public static ResultVo ok(int code, Object data) { /* ... */ }
// 静态工厂方法 - 失败响应
public static ResultVo fail() { /* ... */ }
public static ResultVo fail(String message) { /* ... */ }
public static ResultVo fail(int code, String message) { /* ... */ }
public static ResultVo fail(String message, Object data) { /* ... */ }
public static ResultVo fail(int code, String message, Object data) { /* ... */ }
}
```
#### 属性说明
- **code** (`int`): 状态码。默认值为 `200`,表示成功。失败时常用 `400`。
- **message** (`String`): 提示信息,用于描述操作结果或错误信息。
- **data** (`Object`): 返回的数据,可以是任何类型的对象。
#### 构造方法
- `ResultVo()`: 无参构造,默认 `code` 为 `200`。
- `ResultVo(Object data)`: 仅设置 `data`。
- `ResultVo(int code, String message)`: 设置 `code` 和 `message`。
- `ResultVo(int code, Object data)`: 设置 `code` 和 `data`。
- `ResultVo(String message, Object data)`: 设置 `message` 和 `data`。
#### 静态工厂方法
##### 成功响应 (`ok` 系列方法)
- `ResultVo.ok()`: 返回一个默认的成功响应。
- `ResultVo.ok(Object data)`: 返回成功响应,并包含数据。
- `ResultVo.ok(String message, Object data)`: 返回成功响应,包含自定义消息和数据。
- `ResultVo.ok(int code, String message, Object data)`: 返回自定义 `code`、消息和数据的成功响应。
- `ResultVo.ok(int code, Object data)`: 返回自定义 `code` 和数据的成功响应。
##### 失败响应 (`fail` 系列方法)
- `ResultVo.fail()`: 返回一个默认的失败响应,`code` 为 `400`。
- `ResultVo.fail(String message)`: 返回失败响应,包含自定义消息。
- `ResultVo.fail(int code, String message)`: 返回自定义 `code` 和消息的失败响应。
- `ResultVo.fail(String message, Object data)`: 返回失败响应,包含自定义消息和数据。
- `ResultVo.fail(int code, String message, Object data)`: 返回自定义 `code`、消息和数据的失败响应。
### 使用示例
#### 1. 返回默认的成功响应
```java
@GetMapping("/success")
public ResultVo getSuccess() {
return ResultVo.ok();
}
```
**返回值**
```json
{
"code": 200,
"message": null,
"data": null
}
```
#### 2. 返回成功响应并包含数据
```java
@GetMapping("/data")
public ResultVo getData() {
Map data = new HashMap<>();
data.put("key", "value");
return ResultVo.ok(data);
}
```
**返回值**
```json
{
"code": 200,
"message": null,
"data": {
"key": "value"
}
}
```
#### 3. 返回成功响应并包含自定义消息和数据
```java
@GetMapping("/custom-success")
public ResultVo getCustomSuccess() {
List list = Arrays.asList("item1", "item2");
return ResultVo.ok("Operation successful", list);
}
```
**返回值**
```json
{
"code": 200,
"message": "Operation successful",
"data": ["item1", "item2"]
}
```
#### 4. 返回默认的失败响应
```java
@GetMapping("/fail")
public ResultVo getFail() {
return ResultVo.fail();
}
```
**返回值**
```json
{
"code": 400,
"message": null,
"data": null
}
```
#### 5. 返回失败响应并包含错误消息
```java
@GetMapping("/fail-message")
public ResultVo getFailMessage() {
return ResultVo.fail("Invalid request parameters");
}
```
**返回值**
```json
{
"code": 400,
"message": "Invalid request parameters",
"data": null
}
```
#### 6. 使用链式调用设置响应内容
```java
@GetMapping("/chain")
public ResultVo getChain() {
return ResultVo.ok()
.setMessage("Chained response")
.setData(Arrays.asList(1, 2, 3));
}
```
**返回值**
```json
{
"code": 200,
"message": "Chained response",
"data": [1, 2, 3]
}
```
### 注意事项
- **序列化**: `ResultVo` 实现了 `Serializable` 接口,确保其可以被序列化为 JSON 或其他格式。
- **默认值**: 当未设置 `code` 时,默认值为 `200`,表示成功。
- **错误码**: 通过 `FAIL_CODE` 常量统一表示失败的默认状态码为 `400`,可根据需要自定义其他错误码。
- **链式调用**: 由于使用了 Lombok 的 `@Accessors(chain = true)` 注解,可以通过链式调用方式设置属性,提升代码的可读性和简洁性。
### 总结
`ResultVo` 提供了一种简洁且一致的方式来封装 API 响应。通过其丰富的静态工厂方法和灵活的构造函数,可以满足各种场景下的响应需求。结合 Lombok 的简化注解,使得代码更加简洁和易于维护。
### 示例代码
以下是一个完整的 Spring Boot 控制器示例,展示如何使用 `ResultVo` 进行 API 响应封装:
```java
package com.hami.book.kepping.controller;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import nexus.io.annotation.Get;
import nexus.io.annotation.RequestPath;
import nexus.io.model.result.ResultVo;
@RequestPath("/example")
public class ExampleController {
@Get("/success")
public ResultVo getSuccess() {
return ResultVo.ok();
}
@Get("/data")
public ResultVo getData() {
Map data = new HashMap<>();
data.put("key", "value");
return ResultVo.ok(data);
}
@Get("/custom-success")
public ResultVo getCustomSuccess() {
List list = Arrays.asList("item1", "item2");
return ResultVo.ok("Operation successful", list);
}
@Get("/fail")
public ResultVo getFail() {
return ResultVo.fail();
}
@Get("/fail-message")
public ResultVo getFailMessage() {
return ResultVo.fail("Invalid request parameters");
}
@Get("/chain")
public ResultVo getChain() {
return ResultVo.ok().setMessage("Chained response").setData(Arrays.asList(1, 2, 3));
}
}
```
通过上述控制器,可以访问不同的端点来获取各种形式的 `ResultVo` 响应,确保 API 的一致性和易用性。
### 结论
`ResultVo` 作为 `tio-boot` 提供的内置响应体封装类,极大地简化了 API 响应的构建和管理。通过合理使用其提供的方法,可以有效提升代码的可维护性和可读性,同时确保前后端交互的一致性。
---
# 05_json
URL: https://tio-boot.com/zh/05_json/
Source: zh/05_json/index.md
# 05_json
本目录包含 4 个文件和 0 个子目录,下列摘要基于文件名、一级标题或资源类型整理。
## 文件清单
- [readme.md](): 当前目录的导航与文件摘要。
- [01.md](): 文档《Json》。
- [02.md](): 文档《接受 JSON 和响应 JSON》。
- [03.md](): 文档《响应实体类》。
---
# 概述
URL: https://tio-boot.com/zh/06_web/01.html
Source: zh/06_web/01.md
# 概述
## TestControler
在 Web 开发中,常用的类包括 HttpRequest, HttpResponse, Reps 等。以下是一个使用了 tio-boot controller 的 Java 控制器示例:
```java
package nexus.io.tio.web.hello.controller;
import nexus.io.annotation.AController;
import nexus.io.annotation.RequestPath;
@AController
@RequestPath("/test")
public class TestController {
@RequestPath
public String index() {
return "index";
}
}
```
这段代码演示了一些重要的注解的用法:
1. `@AController`: 这个注解标识一个类作为控制器(Controller)。如果一个类已经被 `@RequestPath` 注解标记,那么可以省略 `@AController` 注解。
2. `@RequestPath`: 这个注解用于指明控制器的请求路径。在这个例子中,它将 `TestController` 类关联到了 `/test` 路径。此外,当一个控制器类的成员方法返回值不为 void 时,tio-boot 会自动将这些方法添加到请求路由中。方法名将作为子路径。也可以通过在方法上使用 `@RequestPath` 来手动指定路由路径。
在 TIO HTTP Server 中,一个控制器(Controller)的方法(即 Action 方法)支持注入以下类型的参数:
- `ServerChannelContext`
- `HttpConfig`
- `HttpSession`
- `HttpRequest`
## IndexController
```
import nexus.io.annotation.RequestPath;
@RequestPath("/")
public class IndexController {
@RequestPath("")
public String index() {
return "uh-course";
}
}
```
1. **类声明和注解**:
- `@RequestPath("/")`: 这个注解将整个 `IndexController` 类映射到根路径 `/`。这意味着当请求的 URL 以 `/` 开头时,可能会由这个控制器处理。
2. **方法声明和注解**:
- `@RequestPath("")`: 这个注解将 `index` 方法映射到相对路径为空的请求,即根路径 `/`。当请求 URL 是 `/` 时,这个方法会被调用。
- `public String index()`: 这是一个公开的方法,它返回一个字符串 `"uh-course"`。这个字符串将作为 HTTP 响应的内容返回给客户端。
测试
```
>curl http://localhost/
uh-course
>curl http://localhost
uh-course
```
---
# 接收请求参数
URL: https://tio-boot.com/zh/06_web/02.html
Source: zh/06_web/02.md
# 接收请求参数
### get 获取参数
```
@RequestPath(value = "/get")
public HttpResponse get(String before, String end, HttpRequest request) throws Exception {
HttpResponse ret = Resps.html(request, "before:" + before + "
end:" + end);
return ret;
}
```
这段代码定义了一个名为 `get` 的方法,通过 `@RequestPath` 注解映射到 URL 路径 `/get`。方法接收两个字符串参数 `before` 和 `end`,以及一个 `HttpRequest` 对象 `request`。
### Post 获取参数
```
@RequestPath(value = "/post")
public HttpResponse post(String before, String end, User user, Short shortid, HttpRequest request) throws Exception {
HttpResponse ret = Resps.html(request, "before:" + before + "
end:" + end + "
user:" + Json.toFormatedJson(user) + "
");
return ret;
}
```
这段代码定义了一个名为 `post` 的方法,通过 `@RequestPath` 注解映射到 `/post` URL 路径。这个方法接收几个参数:两个字符串 `before` 和 `end`,一个 `User` 对象 `user`,一个 `Short` 类型的 `shortid`,以及一个 `HttpRequest` 对象 `request`。
这个方法的主要功能是接收 HTTP POST 请求,获取其中的参数和用户对象,并将这些信息以 HTML 格式的响应返回。
### 从请求地址中获取参数
示例代码
```
@RequestPath(value = "/var/{name}/{id}")
public HttpResponse var(String name, String id, HttpRequest request) throws Exception {
HttpResponse ret = Resps.json(request, "name:" + name + "\r\n" + "id:" + id);
return ret;
}
```
这段代码定义了一个处理 HTTP 请求的方法 `var`,该方法映射到一个 URL 路径模式 `/var/{name}/{id}`。这里的 `{name}` 和 `{id}` 是路径变量,它们在 URL 中动态替换成实际的值。方法接收两个字符串参数 `name` 和 `id`,这些参数自动从匹配的 URL 中提取。还有一个 `HttpRequest` 参数,代表接收到的 HTTP 请求。
方法的主体创建并返回一个 `HttpResponse` 对象,其中包含 `name` 和 `id` 的值。通过 `Resps.json` 方法生成响应,它将 `name` 和 `id` 的值格式化为 JSON 格式的字符串。这样,当访问对应的 URL 时,此方法将以 JSON 格式返回提取的 `name` 和 `id` 参数值。
## URL 编码的参数名与重复参数
查询字符串及 `application/x-www-form-urlencoded` 表单由框架统一解码,参数名和参数值都使用请求字符集进行 URL 解码。业务 Handler 直接读取解码后的名称,不必再调用 URLDecoder,避免二次解码。
| 传入片段 | 解码结果 |
| --- | --- |
| `%69d=one&id=two` | 同一个 id 参数的两个值,按请求顺序保留 |
| `first+name=hello+world` | 名称 first name,值 hello world |
| `%E5%90%8D=%E5%80%BC` | 名称“名”,值“值” |
| `x%3Dy=a%3Db` | 名称 x=y,值 a=b |
重复参数可从 `request.getParams().get("id")` 获取数组;不要只读取单值后假定客户端没有提交其他同名值。名称或值中的非法百分号编码会进入请求解码错误处理。
```java
import nexus.io.tio.boot.http.TioRequestContext;
import nexus.io.tio.http.common.HttpRequest;
import nexus.io.tio.http.common.HttpResponse;
import nexus.io.model.body.RespBodyVo;
public class ParameterNameHandler {
public HttpResponse handle(HttpRequest request) {
String value = request.getParam("first name");
return TioRequestContext.getResponse().respond(RespBodyVo.ok(value));
}
}
```
## 程序追加参数与类型保留
`request.addParam(name, value)` 按调用顺序追加同名参数,支持字符串、UploadFile 以及其他 Java 对象。纯字符串参数保留 String[] 类型,纯文件参数保留 UploadFile[] 类型;混合类型使用 Object[],便于完整保留同名字段的不同值。
```java
import nexus.io.tio.http.common.HttpRequest;
public class ParameterEnrichment {
public Object[] addInternalValues(HttpRequest request) {
request.addParam("internal-value", Integer.valueOf(42));
request.addParam("internal-value", "label");
return request.getParamArray("internal-value");
}
}
```
示例结果依次包含 Integer 值 42 和字符串 label。`getObject` 读取第一个对象,`getParamArray` 读取完整数组。字符串字段可用 `getParam` 或 `getParamValues`,文件字段可用 `getUploadFile`;混合字段应通过对象接口读取,再按业务约定检查类型。
程序调用 `addParam(name, null)` 保持不追加的行为。查询字符串和表单的解码追加也保留已有值与顺序,普通对象不会自动转换为字符串。框架按 Content-Type 完成请求解析,Handler 无需另写一套表单解码逻辑。
## Content-Type 的区域无关识别
框架使用固定大小写规则识别 Content-Type,与 JVM 默认语言区域无关。application/json 及其大写形式进入相同的文本处理流程;multipart/form-data 的大小写形式也遵循相同分类,便于在不同部署区域保持一致的请求解析行为。
## HTTP 首部属性
`HttpParseUtils.getSubAttribute` 按完整属性名称匹配,名称不区分大小写,允许等号两侧留空白,属性值保持原始大小写。带引号的值支持分号及反斜杠转义,适合解析 Content-Type 的 boundary、charset 与 Content-Disposition 的 name、filename。缺少属性或引号未闭合时返回 null。
```java
import nexus.io.tio.http.common.utils.HttpParseUtils;
public class HeaderAttributeExample {
public String uploadField() {
String header = "form-data; filename=photo.png; name=upload";
return HttpParseUtils.getSubAttribute(header, "name");
}
public String multipartBoundary() {
return HttpParseUtils.getSubAttribute("multipart/form-data; BOUNDARY = AbCd", "boundary");
}
}
```
示例分别返回 upload 与 AbCd。框架的 multipart 和字符集解析复用该方法,业务处理器通常可以直接使用框架已经解析的请求参数。
---
# 接收日期参数
URL: https://tio-boot.com/zh/06_web/03.html
Source: zh/06_web/03.md
# 接收日期参数
在 Tio-Boot 框架中,处理日期类型参数是开发中的常见场景。通过框架内置的类型转换功能,可以轻松地接收和处理前端传递的日期数据
## Date
### 使用 Tio-Boot 框架处理 `Date` 类型参数
以下是一个简单的示例,演示如何在 Controller 中接收 `Date` 类型的参数。
#### 示例代码
```java
import java.util.Date;
import com.jfinal.kit.Kv;
import nexus.io.annotation.RequestPath;
import nexus.io.model.body.RespBodyVo;
@RequestPath("/base")
public class BaseController {
public RespBodyVo date(Date date) {
Kv kv = Kv.create();
kv.set("date", date);
return RespBodyVo.ok(kv);
}
}
```
在上面的代码中,`BaseController` 通过 `date()` 方法接收一个 `java.util.Date` 类型的参数。这个参数将由前端以特定格式传递,并自动转换为 `Date` 对象。
#### 请求示例
前端请求可以如下发送:
```
date=2014-09-04 08:34:00
```
此请求将会传递一个日期字符串,该字符串将被 Tio-Boot 框架自动解析为 `java.util.Date` 对象,并传递给 `date()` 方法。
#### 响应示例
服务端返回的响应数据可能如下所示:
```json
{
"data": {
"date": "2014-09-04 08:34:00"
},
"ok": true,
"msg": null,
"code": 1
}
```
在响应中,`data` 字段包含了请求传递的日期值。这个日期数据可以用于进一步的业务逻辑处理或直接返回给前端。
## 明确业务时区和日历日期
对于“今天”“明天”等相对日期,先确定业务时区,再按日历日期加减。`LocalDate` 表示日期,`Instant` 表示时间点;将 `Clock` 作为参数便于固定时间测试,也避免依赖服务器默认时区。以下工具可以由 Service 调用,不需要改变框架的请求参数绑定。
```java
import java.time.Clock;
import java.time.LocalDate;
import java.time.ZoneId;
public final class BusinessDateUtils {
private static final ZoneId BUSINESS_ZONE = ZoneId.of("Asia/Shanghai");
private BusinessDateUtils() {
}
public static LocalDate tomorrow(Clock clock) {
return LocalDate.now(clock.withZone(BUSINESS_ZONE)).plusDays(1);
}
}
```
```java
import java.time.Clock;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneOffset;
public class BusinessDateExample {
public static void main(String[] args) {
Clock clock = Clock.fixed(Instant.parse("2026-10-10T16:30:00Z"), ZoneOffset.UTC);
LocalDate date = BusinessDateUtils.tomorrow(clock);
System.out.println(date); // 2026-10-12
}
}
```
业务响应宜返回明确的日期字符串;含出发和到达时间的业务需要分别保留完整日期、时间和时区偏移。价格与日期等外部数据应在应用服务中校验实际响应字段,不能只依据请求 URL 推断结果。
---
# 接收数组参数
URL: https://tio-boot.com/zh/06_web/04.html
Source: zh/06_web/04.md
# 接收数组参数
在 Web 应用程序中,处理包含数组参数的 HTTP 请求是一项常见的需求。本文将介绍两种处理数组参数的方法:
1. **使用框架自带的绑定功能(From)**
2. **使用自定义解析逻辑**
## 1. 使用框架自带的绑定功能(From)
利用框架提供的注解和自动绑定功能,可以简化数组参数的接收和处理。以下是一个示例方法,展示如何通过 `@RequestPath` 注解接收数组参数。
### 示例代码
```java
import nexus.io.annotation.RequestPath;
import nexus.io.http.HttpRequest;
import nexus.io.http.HttpResponse;
import nexus.io.http.Resps;
import nexus.io.json.Json;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ArrayController {
private static final Logger log = LoggerFactory.getLogger(ArrayController.class);
@RequestPath(value = "/array")
public HttpResponse array(String[] names, Integer[] ids, int[] primitiveIds, HttpRequest request) throws Exception {
// 将数组参数转换为格式化的 JSON 字符串,并拼接作为响应内容
String jsonResponse = Json.toFormatedJson(names) + Json.toFormatedJson(ids) + Json.toFormatedJson(primitiveIds);
HttpResponse ret = Resps.json(request, jsonResponse);
// 从请求中获取名为 "names" 的参数数组
Object[] xx = request.getParamArray("names");
log.info("Received names array: {}", Json.toFormatedJson(xx));
return ret;
}
}
```
### 方法说明
- **方法签名**:
- `array` 方法通过 `@RequestPath` 注解映射到 URL 路径 `/array`。
- 接收四个参数:
- `String[] names`:字符串数组。
- `Integer[] ids`:包装类 `Integer` 数组。
- `int[] primitiveIds`:基本类型 `int` 数组。
- `HttpRequest request`:HTTP 请求对象,用于获取额外的请求信息。
- **方法逻辑**:
1. **生成响应内容**:
- 使用 `Json.toFormatedJson` 方法将 `names`、`ids` 和 `primitiveIds` 数组转换为格式化的 JSON 字符串。
- 将这些 JSON 字符串拼接起来,作为响应的内容。
- 通过 `Resps.json` 方法创建一个 `HttpResponse` 对象,包含上述 JSON 内容。
2. **获取并记录参数数组**:
- 使用 `request.getParamArray("names")` 方法从请求中获取名为 `"names"` 的参数数组,并存储在 `Object[] xx` 中。
- 使用日志记录工具(`log.info`)记录 `xx` 数组的格式化 JSON 表示,便于调试和监控。
3. **返回响应**:
- 返回包含 `names`、`ids` 和 `primitiveIds` 数组 JSON 表示的 `HttpResponse` 对象。
### HTML 表单示例
前端可以通过 HTML 表单发送数组参数。以下是一个示例表单,展示如何通过多个 `input` 标签传递数组数据。
```html
```
### 注意事项
- **参数名称一致性**:确保前端 `input` 标签的 `name` 属性与后端方法参数名称一致,以便框架能够正确绑定。
- **数据类型匹配**:前端传递的参数值应与后端接收的数组类型匹配,避免类型转换错误。
## 2. 使用自定义解析逻辑
有时,使用框架自带的绑定功能可能不够灵活或过于繁琐。此时,可以选择自定义解析请求参数,手动处理数组数据。
### 示例代码
```java
import nexus.io.annotation.RequestPath;
import nexus.io.model.body.RespBodyVo;
import nexus.io.tio.http.common.HttpRequest;
@RequestPath("/api/v1/email")
public class ApiV1EmailController {
/**
* 发送邀请邮件
*
* @param request HTTP 请求对象
* @return 响应体,包含处理结果
*/
public RespBodyVo sendInviteEmail(HttpRequest request) {
// 获取名为 "ids" 的参数
String ids = request.getParam("ids");
// 参数校验
if (ids == null || ids.trim().isEmpty()) {
return RespBodyVo.fail("ids 不能为空");
}
// 分割参数字符串,转换为 long 类型数组
String[] split = ids.split(",");
long[] studentIds = new long[split.length];
try {
for (int i = 0; i < split.length; i++) {
studentIds[i] = Long.parseLong(split[i].trim());
}
} catch (NumberFormatException e) {
return RespBodyVo.fail("ids 格式错误,必须为数字");
}
// 返回成功响应,包含转换后的 studentIds 数组
return RespBodyVo.ok(studentIds);
}
}
```
### 方法说明
- **方法签名**:
- `sendInviteEmail` 方法通过 `@RequestPath` 注解映射到 URL 路径 `/api/v1/email/sendInviteEmail`。
- 接收一个参数 `HttpRequest request`,用于获取请求中的参数。
- **方法逻辑**:
1. **获取参数**:
- 使用 `request.getParam("ids")` 方法获取请求中名为 `"ids"` 的参数值。
2. **参数校验**:
- 检查 `ids` 是否为 `null` 或空字符串,若是,则返回失败响应,提示 `"ids 不能为空"`。
3. **参数解析**:
- 使用 `split(",")` 方法将 `ids` 字符串按逗号分割,得到一个字符串数组。
- 创建一个 `long[]` 类型的数组 `studentIds`,用于存储转换后的数字。
- 遍历分割后的字符串数组,使用 `Long.parseLong` 方法将每个字符串转换为 `long` 类型,并存入 `studentIds` 数组。
- 如果转换过程中发生 `NumberFormatException` 异常,说明参数格式错误,返回失败响应,提示 `"ids 格式错误,必须为数字"`。
4. **返回响应**:
- 使用 `RespBodyVo.ok(studentIds)` 返回成功响应,包含转换后的 `studentIds` 数组。
### 前端请求示例
使用 `curl` 命令发送 POST 请求,传递 `ids` 参数。
```bash
curl --location --request POST 'http://localhost/api/v1/email/sendInviteEmail' \
--header 'User-Agent: Apifox/1.0.0 (https://apifox.com)' \
--header 'Authorization: Bearer PAfvyWa268y0on8MbxHhfYY21rvi9Sx8' \
--header 'Accept: */*' \
--header 'Host: localhost' \
--header 'Connection: keep-alive' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'ids=1,2,3,4,5,6,7,8,9'
```
### 后端响应示例
成功处理请求后,后端返回如下 JSON 响应:
```json
{
"data": [1, 2, 3, 4, 5, 6, 7, 8, 9],
"code": 1,
"msg": null,
"ok": true
}
```
### 注意事项
- **参数格式**:确保前端传递的 `ids` 参数为逗号分隔的数字字符串,例如 `"1,2,3,4,5,6,7,8,9"`。
- **异常处理**:在参数解析过程中,捕获可能的格式异常,及时返回有意义的错误信息,提升用户体验。
- **安全性**:对传入的参数进行必要的校验和清理,防止潜在的安全风险,如注入攻击。
## 总结
在 Web 应用程序中,处理数组参数有多种方法。使用框架自带的绑定功能可以简化开发,但在某些情况下,自定义解析逻辑提供了更高的灵活性和控制力。根据具体需求选择合适的方法,确保代码的可维护性和系统的稳定性。
---
# 返回字符串
URL: https://tio-boot.com/zh/06_web/05.html
Source: zh/06_web/05.md
# 返回字符串
```
package nexus.io.tio.boot.hello.AController;
import nexus.io.annotation.RequestPath;
@AController
@RequestPath("/test/string")
public class TestStringController {
@RequestPath()
public String index() {
return "index";
}
}
```
这段代码定义了一个使用 tio-boot 框架的 HTTP 控制器,专门用于处理 /test/string 路径下的 Web 请求,并返回一个字符串。
- `@RequestPath()`: 方法注解,指定了此方法处理的具体请求路径。结合类注解,当 HTTP 请求发送到 `/test/string` 时,此方法将被调用。
- 方法不接收任何参数,这意味着它响应的是不需要任何输入的请求。
- 方法返回一个字符串 `"index"`。在 tio-boot 框架中,会直接将字符串 `"index"` 作为响应内容发送给客户端不会查找名为 `"index"` 的视图模板,并用该模板生成 HTTP 响应。
---
# 返回文本数据
URL: https://tio-boot.com/zh/06_web/06.html
Source: zh/06_web/06.md
# 返回文本数据
使用 `Resps.txt` 返回纯文本,由框架设置 `text/plain` 和对应的 charset,无需手写 Content-Type。
```java
import nexus.io.tio.boot.http.TioRequestContext;
import nexus.io.tio.http.common.HttpRequest;
import nexus.io.tio.http.common.HttpResponse;
import nexus.io.tio.http.server.util.Resps;
public class TextHandler {
public HttpResponse handle(HttpRequest request) {
HttpResponse response = TioRequestContext.getResponse().setStatus(200);
return Resps.txt(response, "服务正常", "UTF-8");
}
}
```
`Resps.txt(response, body, "UTF-8")` 复用传入的响应对象,保留已设置的状态码及其他响应头,使用 UTF-8 编码正文并设置纯文本 Content-Type。第三方协议要求纯文本回执时,可以直接用它返回协议规定的字符串。
| 调用 | 行为 |
| --- | --- |
| `Resps.txt(request, body)` | 创建响应,使用请求字符集 |
| `Resps.txt(response, body)` | 复用响应,使用响应字符集 |
| `Resps.txt(request, body, charset)` | 创建响应,显式指定正文编码与 Content-Type 字符集 |
| `Resps.txt(response, body, charset)` | 复用响应,显式指定正文编码与 Content-Type 字符集 |
普通业务接口需要统一 JSON 响应结构时,继续使用 `RespBodyVo`;纯文本接口根据协议返回原文。
---
# 返回网页
URL: https://tio-boot.com/zh/06_web/07.html
Source: zh/06_web/07.md
# 返回网页
## 概述
本文档介绍如何使用 tio-boot 框架创建 Controller 来直接返回网页内容。通过这种方式,可以将存储在数据库中的 HTML 内容直接渲染到浏览器中。
## 控制器实现
### 完整代码示例
```java
import nexus.io.annotation.RequestPath;
import nexus.io.db.activerecord.Db;
import nexus.io.tio.boot.http.TioRequestContext;
import nexus.io.tio.http.common.HttpResponse;
import nexus.io.tio.http.server.util.Resps;
@RequestPath("/amazon_products")
public class AmazonProductsHtmlController {
@RequestPath("/{id}")
public HttpResponse html(Long id) {
String html = Db.queryStr("select html from amazon_products_html where id=?", id);
HttpResponse response = TioRequestContext.getResponse();
Resps.html(response, html);
return response;
}
}
```
### 代码解析
#### 1. 注解说明
- `@RequestPath("/amazon_products")`: 类级别的注解,定义控制器的基础路径
- `@RequestPath("/{id}")`: 方法级别的注解,定义具体的路由路径,其中 `{id}` 是路径参数
#### 2. 核心组件
- **Db.queryStr()**: 从数据库中查询 HTML 内容
- 执行 SQL 查询:`select html from amazon_products_html where id=?`
- 参数化查询防止 SQL 注入
- **TioRequestContext.getResponse()**: 获取当前的 HTTP 响应对象
- **Resps.html()**: 设置响应内容为 HTML 格式
#### 3. 执行流程
1. 用户访问 `/amazon_products/{id}` 路径
2. 控制器接收 `id` 参数
3. 从数据库查询对应的 HTML 内容
4. 将查询结果设置为 HTTP 响应的 HTML 内容
5. 返回完整的 HTTP 响应
### 路由访问
```
GET /amazon_products/123
```
其中 `123` 是数据库记录的 ID。
### 数据库表结构
```sql
CREATE TABLE amazon_products_html (
id BIGINT PRIMARY KEY,
html TEXT
);
```
---
# 请求和响应字节
URL: https://tio-boot.com/zh/06_web/08.html
Source: zh/06_web/08.md
# 请求和响应字节
### 获取请求字节
```
package nexus.io.tio.boot.hello.AController;
import nexus.io.tio.http.common.HttpRequest;
import nexus.io.annotation.RequestPath;
@AController
@RequestPath("/test/bytes")
public class TestBytesController {
@RequestPath
public String index(HttpRequest reuqest) {
byte[] body = reuqest.getBody();
return "index";
}
}
```
**Method `index`**:
- `@RequestPath`: 方法注解,没有提供具体的路径值,这意味着它将使用类级别的路径 `/test/bytes`。因此,当 HTTP 请求发送到 `/test/bytes` 时,此方法将被调用。
- 方法接收一个 `HttpRequest` 对象作为参数,代表接收到的 HTTP 请求。
- `byte[] body = reuqest.getBody();`: 从 HTTP 请求中获取字节数组格式的请求体。
- 方法返回一个字符串 `"index"`。这个字符串会会作为 Response 返回。
这对深度学习框架很有用,可以它获取客户端发送的 NumPy 的字节数据
### 响应字节
直接响应
```
@RequestPath("/pen/raw/data")
@Slf4j
public class PenRawDataController {
@RequestPath()
public byte[] index() {
byte[] bytes = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 };
HttpResponse response = TioControllerContext.getResponse();
response.setBody(bytes);
return bytes;
}
}
```
封装为 HttpResponse 响应
```
import nexus.io.tio.http.common.HttpResponse;
@RequestPath("/pen/raw/data")
@Slf4j
public class PenRawDataController {
@RequestPath()
public HttpResponse index() {
byte[] bytes = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 };
HttpResponse response = TioControllerContext.getResponse();
response.setBody(bytes);
return response;
}
}
```
---
# 文件上传
URL: https://tio-boot.com/zh/06_web/09.html
Source: zh/06_web/09.md
# 文件上传
## 接收文件
接受文件在 Controller 的方法中添加 UploadFile 类型的参数,在发送 http 请求时,请求的 key 是 UploadFile 类型的参数名.通常是 file,下面的案例是 uploadFile
```java
package nexus.io.tio.web.hello.controller;
import java.io.File;
import nexus.io.annotation.RequestPath;
import nexus.io.tio.boot.http.TioRequestContext;
import nexus.io.tio.http.common.HttpResponse;
import nexus.io.model.upload.UploadFile;
import nexus.io.tio.utils.hutool.FileUtil;
@RequestPath(value = "/simple/upload")
public class SimleUploadController {
@RequestPath(value = "")
public HttpResponse index(UploadFile uploadFile){
if (uploadFile != null) {
byte[] fileData = uploadFile.getData();
File file = new File(uploadFile.getName());
FileUtil.writeBytes(fileData, file);
}
HttpResponse response = TioRequestContext.getResponse();
return response.setString("sucess");
}
}
```
这段代码定义了一个处理 HTTP 请求的方法 `index`。该方法通过 `@RequestPath` 注解映射到一个空的 URL 路径(即默认路径)。它接收两个参数:一个 `UploadFile` 对象 `uploadFile` 和一个 `HttpRequest` 对象 `request`。
- 方法首先检查 `uploadFile` 是否为非空,如果非空,表示有文件上传。
- 然后,从 `uploadFile` 对象中获取文件数据(字节数组)和文件名。
- 使用 `FileUtil.writeBytes` 方法,将文件数据写入到一个新创建的 `File` 对象中,该对象使用上传文件的名称。
- 最后,该方法返回一个 JSON 格式的响应,内容为字符串 "success"。
这个方法的主要功能是处理文件上传的请求,将上传的文件保存在服务器上,并返回一个表示操作成功的响应。
#### 6.3.2.接受文件和包含上传参数
InputType 是 form 中的一个参数,可以省略
InputType 是 from 中的一个参数,可以省略
```
@RequestPath(value = "")
public HttpResponse index(UploadFile uploadFile, String InputType, String outputType, HttpRequest request)
throws Exception {
if (uploadFile != null) {
byte[] fileData = uploadFile.getData();
File file = new File(uploadFile.getName());
FileUtil.writeBytes(fileData, file);
}
return Resps.json(request, "success");
}
```
这段代码定义了一个处理 HTTP 请求的方法 `index`,它处理带有文件上传的请求。该方法接收四个参数:`UploadFile` 对象 `uploadFile`,两个字符串 `InputType` 和 `outputType`,以及一个 `HttpRequest` 对象 `request`。
- 如果 `uploadFile` 不为空,方法读取上传文件的数据(字节),然后创建一个新的 `File` 对象,文件名与上传的文件名相同。
- 使用 `FileUtil.writeBytes` 方法,将上传文件的数据写入到新创建的文件中。
- 最后,方法返回一个 JSON 格式的响应,内容为字符串 "success"。
#### 6.3.3.UploadFile 常用方法
类 `org.tio.http.common.UploadFile` 可能代表在 Web 应用程序上下文中的一个上传文件。下面是这些方法的简要说明:
1. `getName()`: 这个方法返回上传文件的名称。它用于识别文件,可能基于文件名扩展名确定其类型。
2. `getSize()`: 这个方法返回上传文件的大小,以字节为单位。它可以用来检查文件的大小,这对于验证目的(例如,确保文件不太大,应用程序能够处理)是必要的。
3. `getData()`: 这个方法返回文件的实际数据,以字节数组的形式。它用于处理文件的内容,如将其保存到服务器上或执行任何特定于文件的操作。
## 返回文件
```
@RequestPath(value = "/filetest")
public HttpResponse filetest(HttpRequest request) throws Exception {
HttpResponse ret = Resps.file(request, new File("d:/tio.exe"));
return ret;
}
@RequestPath(value = "/test.zip")
public HttpResponse test_zip(HttpRequest request) throws Exception {
String root = request.httpConfig.getPageRoot(request);
HttpResponse ret = Resps.file(request, new File(root, "test/test.zip"));
return ret;
}
```
这两个方法都涉及处理 HTTP 请求并返回文件作为响应。
1. `filetest` 方法:
- 通过 `@RequestPath` 注解映射到 `/filetest` URL 路径。
- 方法使用 `Resps.file` 创建一个 `HttpResponse` 对象,该对象将发送位于 "d:/tio.exe" 的文件作为响应。
2. `test_zip` 方法:
- 映射到 `/test.zip` URL 路径。
- 首先,使用 `request.httpConfig.getPageRoot` 获取服务器的根目录。
- 然后,使用 `Resps.file` 创建 `HttpResponse` 对象,返回位于根目录下 "test/test.zip" 路径的文件。
---
# 文件下载
URL: https://tio-boot.com/zh/06_web/10.html
Source: zh/06_web/10.md
# 文件下载
本文详细介绍了如何在 tio-boot 后端应用程序中实现文件下载功能,涵盖任意文件下载、浏览器触发下载、以及 DOCX/PDF 文件下载等多种场景,同时还展示了如何生成二维码并通过浏览器下载二维码图片。内容包括完整的代码示例和详细的原理解析,适用于希望扩展或自定义文件下载功能的开发者。
---
[[toc]]
## 1. 任意文件下载
在一些场景下,我们可能需要实现一个通用的文件下载接口,该接口能够根据传入的参数动态读取文件并返回给客户端。下面的示例代码展示了如何读取指定文件,并将文件字节流以合适的 MIME 类型返回给浏览器。
```java
package nexus.io.test.controller;
import java.io.File;
import nexus.io.annotation.RequestPath;
import nexus.io.media.NativeMedia;
import nexus.io.tio.boot.http.TioRequestContext;
import nexus.io.tio.http.common.HttpRequest;
import nexus.io.tio.http.common.HttpResponse;
import nexus.io.model.upload.UploadFile;
import nexus.io.tio.http.server.util.Resps;
import nexus.io.tio.utils.http.ContentTypeUtils;
import nexus.io.tio.utils.hutool.FileUtil;
import nexus.io.tio.utils.hutool.FilenameUtils;
@RequestPath("/media")
public class MediaController {
public HttpResponse toMp3(HttpRequest request) {
// 指定要下载的文件名称(这里以 mp3 文件为例)
String result = "a.mp3";
// 根据文件后缀获取正确的 MIME 类型
String contentType = ContentTypeUtils.getContentType(FilenameUtils.getSuffix(result));
// 将文件读取为字节数组
byte[] fileBytes = FileUtil.readBytes(new File(result));
HttpResponse response = TioRequestContext.getResponse();
// 使用工具类将字节流和 MIME 类型设置到响应中
Resps.bytesWithContentType(response, fileBytes, contentType);
return response;
}
}
```
### 原理说明
- **文件读取**:使用 `FileUtil.readBytes()` 将文件转换为字节数组,适用于文件较小的场景。
- **ContentType**:通过 `ContentTypeUtils` 根据文件后缀设置 MIME 类型,确保浏览器正确解析文件内容。
- **响应构建**:利用 `Resps.bytesWithContentType` 工具类,封装文件字节流和 MIME 类型,返回一个 HTTP 响应。
---
## 2. 通过浏览器触发文件下载
在大多数 Web 应用中,我们希望通过浏览器的下载功能直接下载文件。为此,需要在 HTTP 响应中设置特定的响应头,提示浏览器将内容作为附件下载。
### 示例代码
```java
package nexus.io.linux.handler;
import java.io.File;
import nexus.io.tio.boot.http.TioRequestContext;
import nexus.io.tio.http.common.HttpRequest;
import nexus.io.tio.http.common.HttpResponse;
import nexus.io.tio.utils.http.ContentTypeUtils;
import nexus.io.tio.utils.hutool.FileUtil;
import nexus.io.tio.utils.hutool.FilenameUtils;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class DownloadHandler {
public HttpResponse donwload(HttpRequest httpRequest) {
HttpResponse httpResponse = TioRequestContext.getResponse();
String downloadFilename = "readme.md";
File file = new File(downloadFilename);
String suffix = FilenameUtils.getSuffix(downloadFilename);
// 设置响应内容类型,此处使用 Markdown 格式
String contentType = ContentTypeUtils.getContentType(suffix);
log.info("filename:{},{}", downloadFilename, contentType);
// 设置 Content-Type 响应头,确保浏览器以正确格式解析文件内容
httpResponse.setContentType(contentType);
// 设置 Content-Disposition 响应头,告知浏览器将内容作为附件下载,并指定默认文件名
httpResponse.setAttachmentFilename(downloadFilename);
// 设置响应体内容,实际项目中可替换为文件的字节流
httpResponse.setBody(FileUtil.readBytes(file));
return httpResponse;
}
}
```
### 原理说明
- **Content-Disposition**:设置为 `attachment` 告知浏览器将文件作为附件下载,同时 `filename` 参数指定了默认的文件名称。
- **Content-Type**:确保返回的内容格式与文件实际类型相匹配,从而使下载后的文件能够被正确识别。
http响应头
```
content-disposition:attachment; filename="readme.md"
content-type:application/octet-stream
```
---
## 3. 文档下载
针对常见的文档文件(如 DOCX 和 PDF),可以根据文件 ID 动态获取文件内容,并返回给客户端。以下分别介绍 DOCX 和 PDF 文件下载的实现方法。
### 3.1 下载 DOCX 文件
#### 示例代码
```java
@RequestPath("/download/docx/{id}")
public HttpResponse download(String id, HttpRequest request) {
// 根据传入的 id 动态获取 DOCX 文件的输入流
InputStream inputStream = ResourceUtil.getStream(id+".docx");
int available;
try {
available = inputStream.available();
byte[] fileBytes = new byte[available];
// 将文件内容读取到字节数组中
inputStream.read(fileBytes, 0, available);
// 设置 DOCX 文件的 MIME 类型
String contentType = "application/vnd.openxmlformats-officedocument.wordprocessingml.document; charset=utf-8";
HttpResponse response = Resps.bytesWithContentType(request, fileBytes, contentType);
return response;
} catch (IOException e) {
e.printStackTrace();
// 文件读取异常时返回错误 JSON 响应
return Resps.json(request, RespBodyVo.fail("Error generating captcha"));
}
}
```
### 3.2 下载 PDF 文件
#### 示例代码
```java
@RequestPath("/download/pdf/{id}")
public HttpResponse downloadPdf(String id, HttpRequest request) {
log.info("id:{}", id);
// 从资源路径中动态读取 PDF 文件(假设文件存放在 pdf 目录下)
InputStream inputStream = ResourceUtil.getStream("pdf/" + id + ".pdf");
int available;
try {
available = inputStream.available();
byte[] fileBytes = new byte[available];
// 将文件内容读取到字节数组中
inputStream.read(fileBytes, 0, available);
// 设置 PDF 文件的 MIME 类型
HttpResponse response = Resps.bytesWithContentType(request, fileBytes, "application/pdf; charset=utf-8");
return response;
} catch (IOException e) {
e.printStackTrace();
// 文件读取异常时返回错误 JSON 响应
return Resps.json(request, RespBodyVo.fail("Error generating captcha"));
}
}
```
### 原理说明
- **资源获取**:利用 `ResourceUtil.getStream()` 方法根据文件名或路径获取文件的输入流,适用于存储在项目资源中的文件。
- **字节流读取**:调用 `inputStream.available()` 获取可读取的字节数,并一次性读取文件内容。对于大文件,应考虑使用流式传输以避免内存压力。
- **错误处理**:在读取过程中捕获异常,并返回格式化的错误响应,确保接口的健壮性。
### 3.3 用文档类型方法直接下载
`Resps` 为常用文档类型准备了快捷方法:一行写完 Content-Type 与附件文件名,不需要自己拼字符串。文件名里的中文会按 UTF-8 编码写入响应头:
```java
import java.io.InputStream;
import nexus.io.annotation.Post;
import nexus.io.annotation.RequestPath;
import nexus.io.tio.boot.http.TioRequestContext;
import nexus.io.tio.http.common.HttpResponse;
import nexus.io.tio.http.server.util.Resps;
import nexus.io.tio.utils.hutool.ResourceUtil;
@RequestPath("/download")
public class DocumentDownloadController {
@Post("/docx/{id}")
public HttpResponse docx(String id) {
byte[] bytes = read(ResourceUtil.getStream(id + ".docx"));
return Resps.docx(TioRequestContext.getResponse(), bytes, id + ".docx");
}
@Post("/pdf/{id}")
public HttpResponse pdf(String id) {
byte[] bytes = read(ResourceUtil.getStream("pdf/" + id + ".pdf"));
return Resps.pdf(TioRequestContext.getResponse(), bytes, id + ".pdf");
}
private byte[] read(InputStream inputStream) {
try (InputStream in = inputStream) {
return in.readAllBytes();
} catch (java.io.IOException e) {
throw new IllegalStateException("读取文件失败", e);
}
}
}
```
同一组方法还有 `excel`、`excelXls`、`pptx`、`ppt`、`image`,以及通用的 `download(response, bytes, contentType, filename)`;完整清单见 [Resps]()。
---
## 4. 生成二维码并下载
除了常规的文件下载外,有时还需要动态生成图片(例如二维码)并返回给客户端。下面以二维码生成为例,展示如何使用 ZXing 库生成二维码图片,并通过 HTTP 响应返回。
### 前置准备
请确保项目中已添加以下依赖:
#### Maven 依赖示例
```xml
com.google.zxing
core
3.4.1
com.google.zxing
javase
3.4.1
nexus.io
tio-boot
最新版本
```
### QrController 实现
#### 示例代码
```java
package nexus.io.test.controller;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import nexus.io.annotation.AController;
import nexus.io.annotation.RequestPath;
import nexus.io.model.body.RespBodyVo;
import nexus.io.tio.boot.http.TioRequestContext;
import nexus.io.tio.http.common.HttpResponse;
import nexus.io.tio.http.server.util.Resps;
import nexus.io.tio.utils.hutool.StrUtil;
import nexus.io.tio.utils.qrcode.QrCodeUtils;
@AController
@RequestPath("/qr")
public class QrController {
@RequestPath("/gen")
public HttpResponse qr(String content) {
HttpResponse response = TioRequestContext.getResponse();
// 验证二维码内容不能为空
if (StrUtil.isBlank(content)) {
return Resps.json(response, RespBodyVo.fail("No content provided for QR code"));
}
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
// 生成二维码图片,并写入到输出流中,二维码尺寸为 300x300 像素
QrCodeUtils.generateQRCode(content, 300, 300, outputStream);
// 将输出流转换为字节数组
byte[] qrCodeBytes = outputStream.toByteArray();
// 使用工具类返回包含二维码图片字节流的响应,设置 MIME 类型为 image/png
return Resps.bytesWithContentType(response, qrCodeBytes, "image/png");
} catch (IOException e) {
e.printStackTrace();
return Resps.json(response, RespBodyVo.fail("Error generating QR code"));
}
}
}
```
### 原理说明
- **二维码生成**:利用 ZXing 库生成二维码图片,通过 `QrCodeUtils.generateQRCode()` 方法将二维码内容写入输出流中。
- **流转换**:将 `ByteArrayOutputStream` 中的内容转换为字节数组,方便后续通过 HTTP 响应返回给客户端。
- **响应设置**:设置正确的 MIME 类型 `"image/png"`,确保浏览器能够识别并显示图片内容。
---
## 5. 总结
通过以上示例,我们可以看到在 Java Web 应用中实现文件下载功能的关键步骤:
1. **定义请求路径**:通过注解或路由配置定义对应的 URL 接口。
2. **读取文件**:根据实际需求读取服务器中的文件,支持任意文件、DOCX、PDF、二维码图片等。
3. **设置响应头**:重点在于设置 `Content-Type` 和 `Content-Disposition` 响应头,确保浏览器能够正确识别文件类型并触发下载。
4. **返回响应内容**:将读取的字节流写入 HTTP 响应体中,完成文件传输。
5. **错误处理**:合理捕获异常并返回友好的错误提示,提升接口的健壮性和用户体验。
通过正确的设置和处理,开发者可以在项目中灵活扩展文件下载功能,同时确保安全性和性能。希望本文能为您提供清晰的实现思路和实践参考。
---
# 返回视频文件并支持断点续传
URL: https://tio-boot.com/zh/06_web/11.html
Source: zh/06_web/11.md
# 返回视频文件并支持断点续传
本文档介绍如何通过 TioBoot Handler 返回视频文件,支持 HTTP Range 断点续传,以便前端页面使用 `