# tiny **Repository Path**: xnat/tiny ## Basic Information - **Project Name**: tiny - **Description**: 小巧的java应用微内核通用框架, 可用于构建小工具,web,各种大大小小的应用 - **Primary Language**: Java - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 68 - **Forks**: 15 - **Created**: 2020-12-10 - **Last Updated**: 2026-06-20 ## Categories & Tags **Categories**: Uncategorized **Tags**: 异步, 轻量级, 事件驱动, 多线程, 微内核 ## README # Tiny > 小巧的 Java 应用微内核框架,基于事件驱动架构 ## 前置条件 - JDK 8+ - Maven 3.x - 支持 MySQL / MariaDB / OceanBase / PostgreSQL / SQLite / Oracle / DB2 / H2 等主流数据库(XQL 模块) ## 目录 - [特性概览](#特性概览) - [安装](#安装) - [快速开始](#快速开始) - [统一事件驱动 EP](#统一事件驱动-ep) - [配置](#配置) - [X-ap 同名节点数据同步](#x-ap-同名节点数据同步) - [定时任务调度 Sched](#定时任务调度-sched) - [服务基础类 BeanTpl](#服务基础类-beantpl) - [队列执行器 XQueue](#_队列执行器_-xqueue) - [数据库工具 XQL](#数据库工具-xql) - [HTTP 客户端](#http-客户端) - [对象拷贝器](#对象拷贝器) - [缓存 Cacher](#简单缓存-cacher) - [其他工具](#其他工具) - [应用示例](#应用示例) - [参与贡献](#参与贡献) ## 特性概览 | 模块 | 说明 | |------|------| | **EP (事件驱动)** | 统一的协议空间+路径事件模型,解耦控制层/服务层/DAO层 | | **XQueue** | 轻量级队列执行器(伪协程),控制并发、速度、暂停/恢复 | | **XQL** | 轻量 ORM,注解建表、链式查询、事务支持 | | **Sched** | 五种调度模式:Cron、时间点、延迟、固定间隔、动态调度 | | **HTTP** | 基于事件驱动的 HTTP 服务 + HTTP 客户端工具 | | **Cacher** | 简单内存缓存,支持 Hash 结构和过期策略 | | **X-ap** | 同名节点间数据变更同步 | ## 介绍 目前大部分高级语言都解决了编程中的内存自动管理问题(垃圾回收), 但并没有解决 CPU 资源自动管理的问题。 基本上都是程序员主动创建线程或线程池,这些线程是否被充分利用了?在应用不同的地方创建是否有必要? 太多的线程是否造成竞争性能损耗?毕竟线程再多,决定并发的是 CPU 的个数。 所以需要框架来实现一个智能执行器(线程池):根据应用的忙碌程度自动创建和销毁线程, 即线程池会自己根据排队的任务数和当前池中线程数的比例判断是否需要新创建线程(与传统固定大小线程池不同)。 会 catch 所有异常,不会因业务异常而终止(常驻运行)。 程序只需配置最大最小资源即可自动适配。这样既能充分利用线程资源, 也减少线程的空转和线程过多调度的性能浪费,代码不用直接和线程交互。 > 所以系统性能只由线程池大小属性 sys.exec.corePoolSize=8, sys.exec.maximumPoolSize=16 和 jvm内存参数 -Xmx1g 控制 框架设计一种轻量级执行器(伪协程): __XQueue__ 来控制执行模式(并发,暂停/恢复,速度等) 上层服务应该只关注怎么组装执行任务,然后提交给 __XQueue__ 或直接给执行线程池。如下图: ![Image text](https://www.plantuml.com/plantuml/png/dLHTRnf757tVNp5o7x9ODevv1AcgMlMjBsqYDLBJXqqPcfNNmtO_f5fL9SUj0pW6ktPiZC5oY72CK0b96r4CTlWpUsVPVz61kuPbKIEQVK3CpBtddBdtt37NLAOhXh9ElaYpf6gfQVObIzwpDZHA7cigev8KvvKyx5JXxTRdL67tTIqXIQQkgPP4nBVyALcD4kUcxpwlG-iAiaMUknnKTw5KbnzyRLERHXAZmFXFmuKyFeNzl3WP7Ga1reYQBFm7n0BvHKJYD-R63pNmmYLast3LXhq_4bE7yUZmVff6DPcXb60UMtwqiSputhxJhIMcAOZoewAoS8AtUGxTLqWN9Z9rIfFoGkpkYhamImLd6TZTXEnJwFm6bJflxRcN1yZyaF7A9jHEOtUTJiFfzKIvvnB3JxVWIML6XYs4iAJCYssl-6w5drMTNdjM3t_o4chvSB91JNkTof7vYd5oBherrBvVGDcyakD7sJ2eRTAl5DEIBSEciq86cGRir7bdsolL415oUU_ifNlwEu8-PB3Nm4gXFvnE4qd78HDGSNFV8XAIfrCfQiWOvfF_AabtWlSIgv0F7znNpBKu9ddbfolupxq-veK3w1uAFs8ENd0O_AsXw3er4e5zbhOeBiSVuk3cM6ue3jNZEb0c70hUlF0QDMIPQhf5zTlXojpmepUHG4puhg4kPlB221-6D-Yz8G7Qr4_uiofebaY0LAmWM-F7JNxIWlrN6FT_vU0G8seWAJpCGThLfU0gjXIDHf5lnXDtfYhwZsZ6Yq6_d0WtKpo65ltPcgkPlYbUNq6fxll4LtDzn0yB7-erDawuJaQvQCrWTNobiIXno-zuuSAVWkijqQYWy-EAkS5-cBXS2eo15F2r-tVFxPsFtzSLjzjtwy_usnUtB-peHqqH7qh-ncPCUQJHI8HOZoaZibVyAuAKedQmVU4sS_1-6-SMjQJInfeKABEUrZGI7jCuCfFLIGvUUUxCs4TdCh65xPXefPf99m3VVMBGvFS21_gbE5bSX9j3oENngeWgV0O7kxYFi697F4UyzTnv_spGtXxqCqvd4_8xijSx6RokHHON0qeWMuPUbn-_mTon83M5jRsrlx33PsBRNjV-1G00) ## 安装 ```xml cn.xnatural tiny 3.0.test7 ``` ## 快速开始 ```java App.source(new XQL("db.test")); // 应用启动(会自动扫描并创建当前包下所有@bean注解的类, 依次触发系统事件) App.start("需要扫描的包"); ``` ### 启动流程 ``` App.source(服务实例...) ← 启动前手动注册服务(可选) App.start("扫描包路径") ← 入口 │ ├─ 1. 注册 JVM 关闭钩子 (kill 时触发 sys/stopping) │ ├─ 2. 扫描包, 自动创建 @Bean 类并加入容器 │ └─ 先扫框架包 cn.xnatural, 再扫用户包 │ ├─ 3. 解析所有 bean 的 @EL 方法, 注册为事件监听器 │ ├─ 4. @Inject 字段自动注入 │ └─ 5. 依次触发系统事件: sys/inited → 环境配置、线程池、事件中心就绪 sys/starting → 各服务执行自己的初始化逻辑 sys/started → 各服务开始运行, 应用启动完成 EP 事件驱动 │ ├─ resolve: 扫描 @EL 方法 → 按 ns(协议空间) + path(路径) 注册监听器 │ ├─ fire: 触发事件 → 匹配监听器 → 执行 │ └─ 执行策略: ├─ 同步/异步 (async) ├─ 并发控制 (parallel) └─ 排队顺序 (order) ``` ## 统一事件驱动 EP EP(Event Protocol)是 Tiny 的核心通信机制。事件由 **协议空间(ns)+ 路径(path)** 唯一标识,任何组件都可以发布或订阅事件,无需直接依赖。 ``` 发布者 ──fire("user/login")──→ EP 事件中心 ──匹配──→ 所有监听器执行 ``` ### @EL 注解 ```java @EL(ns = "协议空间", path = "事件路径") void handler(参数...) { ... } ``` | 属性 | 说明 | 默认值 | |------|------|--------| | `ns` | 协议空间,用于分类事件 | 空 | | `path` | 事件路径,`/` 分隔,支持 `{变量}` | 必填 | | `async` | 是否异步执行(不排队) | `false` | | `parallel` | 并发数(同一事件多个监听器同时执行) | `1` | | `order` | 执行顺序(数字越小越先) | `0` | | `exclusive` | 排他模式(优先于其他监听器执行) | `false` | ### 发布事件 ```java // 简单触发 ep.fire("order/created"); // 带参数 ep.fire("order/created", orderId, userId); // 带协议空间(需构造 EC) ep.fire(new EC("sys", "started")); ``` ### 监听事件 ```java @Bean public class OrderService extends BeanTpl { // 监听订单创建 @EL(path = "order/created") void onOrderCreated(String orderId, String userId) { log.info("订单已创建: {}", orderId); } // 同一事件可以有多个监听器,按 order 排序执行 @EL(path = "order/created", order = 10) void sendNotification(String orderId, String userId) { // 发通知... } } ``` ### 系统生命周期事件 App 启动后依次触发,服务在这些事件中完成初始化: ``` sys/inited → 基础设施就绪(配置、线程池、事件中心) sys/starting → 各服务执行初始化逻辑 sys/started → 应用启动完成,各服务开始运行 sys/stopping → 应用关闭(kill 时触发) ``` ```java @Bean public class CacheService extends BeanTpl { @EL(ns = "sys", path = "starting") void init() { // 加载缓存数据... } @EL(ns = "sys", path = "stopping") void destroy() { // 持久化缓存... } } ``` ### HTTP 事件 HTTP 请求也是事件,`ns = "HTTP"`,`path` = URL 路径。类级 `@EL` 需加 `exclusive = true`,确保请求只由匹配的控制器处理,不会穿透到其他监听器。 ```properties ### app.properties xnet.hp=:8080 # xnet.webDir=web # 静态资源目录(相对运行目录) # cluster.detect=http://localhost:7070 # 集群探测地址 ``` ```java @Bean @EL(ns = "HTTP", path = "api/user", exclusive = true) public class UserCtrl extends BeanTpl { @EL(path = "list", op = "get") R> list(Integer limit) { return R.ok(userService.list(limit)); } @EL(path = "{id}", op = "get") R get(String id) { return R.ok(userService.get(id)); } @EL(path = "save") R save(User user) { return R.ok(userService.save(user)); } // 异步响应:返回 CompletableFuture @EL(path = "async") CompletableFuture async() { CompletableFuture f = new CompletableFuture<>(); App.exec(() -> f.complete("done")); return f; } // 文件下载 @EL(path = "download/{fName}", op = "get") File download(String fName, XResponse response) { response.contentDisposition("attachment;filename=" + fName); return new File("/tmp/" + fName); } } ``` #### HTTP 扩展点 通过特殊协议空间拦截 HTTP 处理流程: ```java // 自定义错误处理 @EL(ns = "http:error", path = "{_}", exclusive = true) R onError(XContext ctx, Throwable ex) { log.error(ctx.ep.logStr(ctx), ex); return R.fail(ex == null ? null : ex.getMessage()); } // 自定义 JSON 序列化 @EL(ns = "http:response:json", path = "{_}", exclusive = true) Object toJson(XResponse resp) { Object r = resp.ctx.result(); if (r instanceof String) return r; return JSON.toJSONString(r); } // 自定义会话管理 @EL(ns = "http:session", path = "{_}", exclusive = true) Map session(XContext ctx) { // 从 cookie 获取 sessionId,返回会话 Map // ... } ``` ### 自定义事件 任意定义协议空间和路径,用于业务解耦: ```java // 发布 ep.fire("order/paid", orderId); // 监听 @EL(path = "order/paid") void onPaid(String orderId) { ... } // 数据库就绪事件(无协议空间) @EL(path = "/db/test/started", async = true) void onDbReady() { ... } ``` ## 配置 > 配置文件加载顺序(优先级从低到高): * classpath: app.properties, classpath: app-[profile].properties * file: ./app.properties, file: ./app-[profile].properties * configDir: app.properties, configDir: app-[profile].properties * 自定义环境属性配置(重写方法): App#customEnv * System.getProperties() >+ 系统属性(-DconfigName): configName 指定配置文件名. 默认: app >+ 系统属性(-Dprofile): profile 指定启用特定的配置 >+ 系统属性(-DconfigDir): configDir 指定额外配置文件目录 * 只读取properties文件. 按顺序读取app.properties, app-[profile].properties 两个配置文件 * 配置文件支持简单的 ${} 属性替换 ## X-ap: 同名节点数据同步 > 扩展协议 X-Upgrade: X-ap 一般用于同名多节点有内存缓存,缓存更改后和其他节点之间的数据同步通知 ### 变更监听器 ```java @EL(path = "user", ns = AP.DATA_VERSION) void ap_user(String dataKey, Long version) { // 处理同名应用其他节点的用户数据更改通知 } ``` ### 变更通知 ```java ap.update("user", "user1", System.currentTimeMillis()); ``` ## 定时任务调度 Sched ### Cron 时间表达式 ```java sched.cron("0 0/5 * * * ? ", () -> { System.out.println("每隔5分钟执行"); }) ``` ```java sched.cron("0 0/5 8-11:20,13:10-17:50 * * ? ", () -> { System.out.println("在(8-11:20,13:10-17:50)范围内每隔5分钟执行"); }) ``` ### 在将来的某个时间点执行 ```java sched.time( new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse("2020-12-12 11:55:33"), () -> System.out.println("2020-12-12 11:55:33 执行") ); ``` ### 一段时间之后执行 ```java sched.after(Duration.ofMinutes(3), () -> { System.out.println("3分钟之后执行") }); ``` ### 任务间隔执行 #### fixedDelay ```java sched.fixedDelay(Duration.ofSeconds(10), Duration.ofSeconds(5), () -> { System.out.println("fixedDelay====第一次执行延时5秒执行"); }); ``` ```java sched.fixedDelay(Duration.ofSeconds(10), () -> { System.out.println("fixedDelay===="); }); ``` #### fixedRate ```java sched.fixedRate(Duration.ofSeconds(10), Duration.ofSeconds(5), () -> { System.out.println("fixedRate====第一次执行延时5秒执行"); }); ``` ```java sched.fixedRate(Duration.ofSeconds(10), () -> { System.out.println("fixedRate===="); }); ``` ### 动态任务调度执行. 自定义下次执行时间 #### dynRate ```java sched.dynRate(() -> { //每次执行完任务函数,会获取一次下次执行时间 if (new Random().nextInt(100) == 70) return null; // 返回null任务停止 Calendar cal = Calendar.getInstance(); cal.add(Calendar.MINUTE, new Random().nextInt(30) + 10); return cal.getTime(); }, () -> System.out.println("动态任务执行")); ``` #### dynDelay ```java sched.dynDelay(() -> { //每次执行完任务函数,会获取一次下次执行时间 if (new Random().nextInt(100) == 70) return null; // 返回null任务停止 Calendar cal = Calendar.getInstance(); cal.add(Calendar.MINUTE, new Random().nextInt(30) + 10); return cal.getTime(); }, () -> System.out.println("动态任务执行")); ``` ## 动态按需添加服务 ```java @EL(ns = "sys", name = "inited") void inited() { if (!app.attrs("redis").isEmpty()) { //根据配置是否有redis,创建redis客户端工具 app.source(new RedisClient()); } } ``` ## 服务基础类 BeanTpl > 推荐所有被加入到App中的服务都是BeanTpl的子类 ```properties ### app.properties 服务名.prop=1 ``` ```java app.source(new BeanTpl("服务名") { @EL(ns = "sys", path = "starting") void start() { // 初始化服务 } }) ``` ### bean注入 @Inject(name = "beanName") > 注入匹配规则: (已经存在值则不需要再注入) > > 1. 如果 @Inject name 没配置 > > > 先按 字段类型 和 字段名 匹配, 如无匹配 再按 字段类型 匹配 > > 2. 则按 字段类型 和 @Inject(name = "beanName") beanName 匹配 ```java app.source(new BeanTpl() { @Inject XQL repo; //自动注入 @EL(ns = "sys", path = "started") void init() { List rows = xql.rows("select * from test") log.info("========= {}", rows); } }); ``` ### 动态bean获取: 方法 bean(Class bean类型, String bean名字) ```java app.source(new BeanTpl() { @EL(ns = "sys", path = "started") void start() { Integer cnt = app.bean(XQL.class).single("select count(1) as total from test", Integer.class); log.info("=========" + cnt); } }); ``` ### bean依赖注入原理 > 两种bean容器: App是全局bean容器, 每个服务(BeanTpl)都是一个bean容器 > > 获取bean对象: 先从全局查找, 再从每个服务中获取 * 暴露全局bean ```java app.source(new TestService()); ``` * 服务(BeanTpl)里面暴露自己的bean ```java TestService repo = new TestService(); expose(repo); // 加入到bean容器,暴露给外部使用 ``` ### 属性直通车 > 服务(BeanTpl)提供便捷方法获取配置.包含: getLong, getInteger, getDouble, getBoolean等 ```properties ## app.properties testSrv.prop1=1 testSrv.prop2=2.2 ``` ```java app.source(new BeanTpl("testSrv") { @EL(ns = "sys", path = "starting") void init() { log.info("print prop1: {}, prop2: {}", getInteger("prop1"), getDouble("prop2")); } }) ``` ### 对应上图的两种任务执行 #### 异步任务 ```java App.exec(() -> { // 异步执行任务 }) ``` #### 创建任务队列 ```java queue("队列名", () -> { // 执行任务 }) ``` ## _队列执行器_: XQueue 会自旋执行完队列中所有任务 当需要控制任务最多 一个一个, 两个两个... 的执行时 ### 添加任务到队列 ```java // 方法1 app.queue("save", () -> { // 执行任务 }); // 方法2 app.queue("save").offer(() -> { // 执行任务 }); ``` ### 队列特性 #### 并发控制 最多同时执行任务数, 默认1(one-by-one) ```java app.queue("save").parallel(2) ``` > 注: parallel 最好小于 系统最大线程数(sys.exec.maximumPoolSize), 即不能让某一个执行队列占用所有可用的线程 #### 执行速度控制 把任务按速度均匀分配在时间线上执行 支持: 每秒(10/s), 每分(10/m), 每小时(10/h), 每天(10/d) ```java // 例: 按每分钟执行30个任务的频率 queue("save").speed("30/m") ``` ```java // 清除速度控制(立即执行) queue("save").speed(null) ``` #### 队列 暂停/恢复 ```java // 暂停执行, 一般用于发生错误时 // 注: 必须有新的任务入队, 重新触发继续执行. 或者resume方法手动恢复执行 queue("save") .errorHandle {ex, me -> // 发生错误时, 让队列暂停执行(不影响新任务入队) // 1. 暂停一段时间 me.suspend(Duration.ofSeconds(180)); // 2. 条件暂停(每个新任务入队都会重新验证条件) // me.suspend(queue -> true); }; // 手动恢复执行 // queue("save").resume() ``` #### 队列最后任务有效 是否只使用队列最后一个, 清除队列前面的任务 适合: 入队的频率比出队高, 前面的任务可有可无 ```java // 例: increment数据库的一个字段的值 XQueue q = queue("increment").useLast(true); for (int i = 0; i < 20; i++) { // 入队快, 任务执行慢, 中间的可以不用执行 q.offer(() -> xql.execute("update test set count=?", i)); } ``` ```java // 例: 从服务端获取最新的数据 XQueue q = queue("newer").useLast(true); // 用户不停的点击刷新 q.offer(() -> { util.http().get("http://localhost:8080/data/newer").execute(); }) ``` #### 原理: 并发流量控制锁 Latcher 当被执行代码块需要控制同时线程执行的个数时 ```java final Latcher lock = new Latcher(); lock.limit(3); // 设置并发限制. 默认为1 if (lock.tryLock()) { // 尝试获取一个锁 try { // 被执行的代码块 } finally { lock.release(); // 释放一个锁 } } ``` ## 数据库工具 XQL 轻量 ORM:注解建表、自动类型推断、链式查询、关联查询、事务支持。自动适配多数据库方言(MySQL/MariaDB/OceanBase、PostgreSQL、SQLite、Oracle、DB2、H2)。 ### 1. 配置与初始化 ```properties ### app.properties db.test.url=jdbc:mysql://localhost:3306/test?useSSL=false&useUnicode=true&characterEncoding=utf-8&allowPublicKeyRetrieval=true&createDatabaseIfNotExist=true db.test.username=root db.test.password=root db.test.minimumIdle=1 db.test.maximumPoolSize=2 # 自动建表/加列(update=自动更新) db.test.ddl=update ``` ```java XQL xql = new XQL("db.test"); App.source(xql); App.start("your.package"); ``` ### 2. 定义实体 ```java @Table public class User { @Column(primary = true, autoIncrement = true) Long id; @Column(indexed = true, comment = "姓名") String name; @Column Integer age; @Column(nullable = false, defaultValue = "1") Boolean enabled; @Column Status status; // 枚举自动推断为 varchar(30) @Column Date ctime; // Date 自动推断为 datetime(3) @Column Role role; // 关联实体:自动存 role.id,自动 JOIN 查询 @Column User ref; // 自引用也支持 public static enum Status { Active, Inactive } } @Table public class Role { @Column(primary = true, autoIncrement = true) Long id; @Column String name; } ``` ### 3. 查询 ```java User u = xql.byPk(User.class, 1L); // 按主键 User u = xql.row(User.class, "age > ? order by ctime desc", 18); // 按条件取单条 List users = xql.rows(User.class, "age > ? limit 10", 18); // 按条件取多条 Integer cnt = xql.single("select count(1) from user", Integer.class); // 单值查询 ``` #### 自动 JOIN(核心特性) 只需在实体中声明关联字段,XQL 自动处理 JOIN 和对象映射,无需手写 SQL: ```java // WHERE 中引用关联字段 → 自动 JOIN User u = xql.row(User.class, "role.name = ?", "admin"); // SQL: SELECT t.* FROM user t LEFT JOIN role t_2 ON t.role = t_2.id WHERE t_2.name = ? // 结果:u 有值,但 u.role 只有 id(未查询 role 的其他字段) ``` #### 控制返回列 + 关联填充(attrs) 通过 attrs 列表控制查询哪些列,以及是否填充关联对象: ```java // 1. 默认:只查本表字段 xql.byPk(User.class, 1L); // SQL: SELECT t.* FROM user t WHERE t.id = ? // 2. 填充一级关联:role 对象完整填充 xql.rows(User.class, Arrays.asList("*", "role.*"), "age > ?", 18); // SQL: SELECT t.*, t_2.id t$role, t_2.name t_2$name FROM user t LEFT JOIN role t_2 ON t.role = t_2.id WHERE t.age > ? // 结果:user.role.name 有值 // 3. 多级关联:role → dept 链式 JOIN xql.rows(User.class, Arrays.asList("*", "role.*", "role.dept.*"), null); // SQL: SELECT t.*, t_2.id t$role, t_2.name t_2$name, t_3.id t_2$dept, t_3.name t_3$name // FROM user t LEFT JOIN role t_2 ON t.role = t_2.id LEFT JOIN dept t_3 ON t_2.dept = t_3.id // 结果:user.role.dept.name 有值 // 4. 只取关联表部分字段(减少 JOIN 开销) xql.rows(User.class, Arrays.asList("name", "role.name"), null); // SQL: SELECT t.name, t_2.name t_2$name FROM user t LEFT JOIN role t_2 ON t.role = t_2.id // 结果:user.name 有值,user.role.name 有值,user.role.id 为 null // 5. 指定列 + SQL 表达式 xql.byPk(User.class, Arrays.asList("name", "age", "now(3) dbTime"), 1L); ``` > **JOIN 规则**: > - `"*"` = 本表所有列 > - `"role.*"` = JOIN role 表并查所有列,填充到 `user.role` 对象 > - `"role.dept.*"` = 链式 JOIN,填充到 `user.role.dept` 对象 > - `"role.name"` = JOIN role 表但只取 name 字段 > - 关联对象自动嵌套映射,无需手动组装 > - WHERE 中的字段名自动添加 `t.` 前缀,避免 JOIN 时的列名歧义 #### 子查询支持 WHERE 条件中可以使用子查询,框架会自动识别并保留子查询中的表名: ```java // 子查询中的 role 是表名,不会被替换 List users = xql.rows(User.class, "role IN (SELECT id FROM role WHERE name = ?)", "admin"); // SQL: SELECT t.* FROM user t WHERE t.role IN (SELECT id FROM role WHERE name = ?) // 嵌套子查询也支持 List users = xql.rows(User.class, "role.id IN (SELECT id FROM role WHERE dept IN (SELECT id FROM dept WHERE active = ?))", true); ``` > 框架通过识别 `SELECT` 关键字来区分子查询和普通括号表达式,子查询内的表名不会被误替换。 #### 统计与判断 ```java long total = xql.count(User.class); // 总数 long cnt = xql.count(User.class, "age > ?", 18); // 条件计数 boolean exists = xql.exist(User.class, 1L); // 主键是否存在 boolean exists = xql.exist(User.class, "age > ?", 18); // 条件是否存在 ``` #### 分页 ```java Page page = xql.paging(User.class, 1, 10, "age > ?", null, 18); // 第1页,每页10条 // page.getData() → 当前页数据 // page.getTotal() → 总记录数 // page.getPageNo() → 当前页码 // page.getLimit() → 每页条数 ``` #### 遍历 ```java xql.iterate(User.class, "age > ?", user -> { // 逐条处理,返回 false 停止遍历 System.out.println(user.name); return true; }, 18); ``` ### 4. 增删改 ```java // 保存(INSERT) xql.save(user); // 更新(UPDATE 非空字段) xql.update(user); // 删除 xql.delete(User.class, 1L); xql.delete(User.class, "age < ?", 0); // 执行原生 SQL xql.execute("update user set age = ? where id = ?", 10, 1L); ``` ### 5. 事务 ```java xql.trans(() -> { xql.save(user1); xql.save(user2); xql.delete(User.class, 3L); return null; // 自动提交;抛异常自动回滚 }); ``` > 事务支持嵌套:内层 `trans()` 自动加入外层事务,不会重复提交。 ### 6. 连接复用 同一线程内,嵌套的数据库操作自动复用同一个连接,避免连接池耗尽: ```java xql.conn(conn -> { // 这里面的 xql 操作复用同一个 conn xql.rows(User.class, "age > ?", 18); xql.single("select count(1) from user", Integer.class); return null; }); ``` ### 附:自动类型推断 `@Column` 的 `type` 属性可省略,框架根据数据库方言自动转换: | Java 类型 | MySQL | PostgreSQL | SQLite | Oracle | DB2 | H2 | |-----------|-------|------------|--------|--------|-----|-----| | `String` | `varchar(255)` | `varchar(255)` | `text` | `VARCHAR2(255)` | `VARCHAR(255)` | `VARCHAR(255)` | | `int` / `Integer` | `int`(PK: `int unsigned`) | `integer` | `integer` | `NUMBER(10)` | `INTEGER` | `integer` | | `long` / `Long` | `bigint`(PK: `bigint unsigned`) | `bigint` | `integer` | `NUMBER(19)` | `BIGINT` | `bigint` | | `boolean` / `Boolean` | `tinyint(1)` | `boolean` | `integer` | `NUMBER(1)` | `SMALLINT` | `boolean` | | `double` / `Double` | `double` | `double precision` | `real` | `BINARY_DOUBLE` | `DOUBLE` | `double precision` | | `float` / `Float` | `float` | `real` | `real` | `BINARY_FLOAT` | `REAL` | `real` | | `Date` | `datetime(3)` | `timestamp(3)` | `integer` | `TIMESTAMP(3)` | `TIMESTAMP(3)` | `TIMESTAMP(3)` | | 枚举 | `varchar(30)` | `varchar(30)` | `text` | `VARCHAR2(30)` | `VARCHAR(30)` | `VARCHAR(30)` | | `@Table` 实体 | 对方主键类型 | 对方主键类型 | 对方主键类型 | 对方主键类型 | 对方主键类型 | 对方主键类型 | | `Collection` | `text` | `text` | `text` | `CLOB` | `CLOB` | `CLOB` | > **关联实体**:字段类型是 `@Table` 实体时,自动存储对方主键值,查询时自动 JOIN。无需手写 `Converter`,自引用和循环引用自动处理。 #### 自定义类型转换器 当 Java 字段类型无法直接映射数据库字段时,手写 `Converter` 实现双向转换: ```java @Table public class User { @Column(primary = true, autoIncrement = true) Long id; // 自定义对象存为 JSON 字符串 @Column(converter = AddressConverter.class) Address address; } // 转换器: Address(属性类型) ↔ String(数据库字段类型) public class AddressConverter extends Column.Converter { @Override public String toCol(Address addr) { return addr == null ? null : JSON.toJSONString(addr); } @Override public Address toProp(String s) { return s == null || s.isEmpty() ? null : JSON.parseObject(s, Address.class); } } ``` > 内置转换器:`JSONObject`、`Map`、`List`、`Set`、`Duration` 自动转换为字符串存储,无需手写。 ## HTTP 客户端 ```java // get util.http("http://xnatural.cn:9090/test/cus?p2=2") .header("test", "test") // 自定义header .cookie("sessionId", "xx") // 自定义 cookie .connectTimeout(5000) // 设置连接超时 5秒 .readTimeout(15000) // 设置读结果超时 15秒 .param("p1", 1) // 添加参数 .debug().get(); ``` ```java // post util.http("http://xnatural.cn:9090/test/cus") .debug().post(); ``` ```java // post 表单 util.http("http://xnatural.cn:9090/test/form") .param("p1", "p1") .debug().post(); ``` ```java // post 上传文件 util.http("http://xnatural.cn:9090/test/upload") .param("p1", "xxx") .param("file", new File("/tmp/1.txt")) .debug().post(); ``` ```java // post 上传文件+json util.http("http://xnatural.cn:9090/test/upload") .param("p1", new JSONObject().fluentPut("a", "qqq").fluentPut("b", 1), "application/json") .param("file", new File("/tmp/1.txt")) .debug().post(); ``` ```java // post json util.http("http://xnatural.cn:9090/test/json") .jsonBody(new JSONObject().fluentPut("p1", 1).toString()) .debug().post(); ``` ```java // post 普通文本 util.http("http://xnatural.cn:9090/test/string") .textBody("xxxxxxxxxxxxxxxx") .debug().post(); ``` ```java // 文件下载 util.http("http://localhost:5000/test/download/79975d5cfce74fceb4138932f3873a2d.png").fileHandle(filename -> { try { return Files.newOutputStream(Paths.get("/tmp/" + (filename == null || filename.isEmpty() ? "tmp" : filename))); } catch (IOException e) { throw new RuntimeException(e); } }) // .perSecondKb(10) // 每秒10kb读取限速 .get(); ``` ## 对象拷贝器 ### javabean 拷贝到 javabean ```java util.copier( new Object() { public String name = "徐言"; }, new Object() { private String name; public void setName(String name) { this.name = name; } public String getName() { return name; } } ).build(); ``` ### 对象 转换成 map ```java util.copier( new Object() { public String name = "方羽"; public String getAge() { return 5000; } }, new HashMap() ).build(); ``` ### 添加额外属性源 ```java util.copier( new Object() { public String name = "云仙君"; }, new Object() { private String name; public Integer age; public void setName(String name) { this.name = name; } public String getName() { return name; } } ).add("age", () -> 1).build(); ``` ### 忽略属性 ```java util.copier( new Object() { public String name = "徐言"; public Integer age = 22; }, new Object() { private String name; public Integer age = 33; public void setName(String name) { this.name = name; } public String getName() { return name; } } ).ignore("age").build(); // 最后 age 为33 ``` ### 属性值转换 ```java util.copier( new Object() { public long time = System.currentTimeMillis(); }, new Object() { private String time; public void setTime(String time) { this.time = time; } public String getTime() { return time; } } ).addConverter("time", o -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date((long) o))) .build(); ``` ### 忽略空属性 ```java util.copier( new Object() { public String name; }, new Object() { private String name = "方羽"; public void setName(String name) { this.name = name; } public String getName() { return name; } } ).ignoreNull(true).build(); // 最后 name 为 方羽 ``` ### 属性名映射 ```java util.copier( new Object() { public String p1 = "徐言"; }, new Object() { private String pp1 = "方羽"; public void setPp1(String pp1) { this.pp1 = pp1; } public String getPp1() { return pp1; } } ).mapProp( "p1", "pp1").build(); // 最后 name 为 徐言 ``` ## 文件内容监控器 (类 linux tail) ```java util.tailer().tail("d:/tmp/tmp.json", 5); ``` ## 相对时间 * 昨天凌晨: 0s:0m:0h:-1d * 当天凌晨: 0s:0m:0h * 3天前: -3d * 1分钟后: +1m * 两小时前: -2h ```java SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); System.out.println(sdf.format(Util.timeRelative("0s:0m:0h:-2d"))); ``` ## OGNL 对象表达式 通过属性路径对对象进行取值或设值,支持多级属性、List/数组索引访问,中间节点为 null 时会尝试自动实例化。 ### 支持的 path 格式 | 示例 | 说明 | |------|------| | `o2.o3.i` | 多级属性访问 | | `list[0]` | List/数组索引访问 | | `arr[1].name` | 多级混合(数组元素再取属性) | ### 设置属性值 ```java // 普通多级属性 Util.ognl(o1, "o2.o3.i", 1); // 对 List 元素设值 Util.ognl(holder, "list[0]", obj); // 对数组元素属性设值 Util.ognl(holder, "arr[0].name", "xxx"); // 设为 null(字段会被清空) Util.ognl(o1, "o2.o3.name", null); ``` ### 获取属性值 ```java // 普通多级属性 Object v = Util.ognl(o1, "o2.o3.i"); // 取 List 第 0 个元素 Object e = Util.ognl(holder, "list[0]"); // 取数组第 1 个元素的属性 Object n = Util.ognl(holder, "arr[1].name"); ``` ## 通用本地命令执行 ```java Util.cmd("ffmpeg -i 视频文件 -vcodec h264 -max_muxing_queue_size 5120 -b:v 500k -r 25 -y out.mp4", Duration.ofSeconds(120)); ``` ## 简单缓存 Cacher ```properties ## app.properties 缓存最多保存100条数据 cacher.limit=100 ``` ### 缓存操作 ```java // 1. 设置缓存 bean(Cacher).set("缓存key", "缓存值", Duration.ofMinutes(30)); // 2. 过期函数 bean(Cacher).set("缓存key", "缓存值", record -> { // 缓存值: record.value // 缓存更新时间: record.getUpdateTime() return 函数返回过期时间点(时间缀), 返回null(不过期,除非达到缓存限制被删除); }); // 3. 获取缓存 bean(Cacher).get("缓存key"); // 4. 获取缓存值, 并更新缓存时间(即从现在开始重新计算过期时间) bean(Cacher).getAndUpdate("缓存key"); // 5. 手动删除 bean(Cacher).remove("缓存key"); ``` ### hash缓存操作 ```java // 1. 设置缓存 bean(Cacher).hset("缓存key", "数据key", "缓存值", Duration.ofMinutes(30)); // 2. 过期函数 bean(Cacher).hset("缓存key", "数据key", "缓存值", record -> { // 缓存值: record.value // 缓存更新时间: record.getUpdateTime() return 函数返回过期时间点(时间缀), 返回null(不过期,除非达到缓存限制被删除); }); // 3. 获取缓存 bean(Cacher).hget("缓存key", "数据key"); // 4. 获取缓存值, 并更新缓存时间(即从现在开始重新计算过期时间) bean(Cacher).hgetAndUpdate("缓存key", "数据key"); // 5. 手动删除 bean(Cacher).hremove("缓存key", "数据key"); ``` ## 无限递归优化 Recursion > 解决java无尾递归替换方案. 例: ```java System.out.println(factorialTailRecursion(1, 10_000_000).invoke()); ``` ```java /** * 阶乘计算 * @param factorial 当前递归栈的结果值 * @param number 下一个递归需要计算的值 * @return 尾递归接口,调用invoke启动及早求值获得结果 */ Recursion factorialTailRecursion(final long factorial, final long number) { if (number == 1) { // new Exception().printStackTrace(); return Recursion.done(factorial); } else { return Recursion.call(() -> factorialTailRecursion(factorial + number, number - 1)); } } ``` > 备忘录模式:提升递归效率. 例: ```java System.out.println(fibonacciMemo(47)); ``` ```java /** * 使用同一封装的备忘录模式 执行斐波那契策略 * @param n 第n个斐波那契数 * @return 第n个斐波那契数 */ long fibonacciMemo(long n) { return Recursion.memo((fib, number) -> { if (number == 0 || number == 1) return 1L; return fib.apply(number-1) + fib.apply(number-2); }, n); } ``` ## 延迟对象 Lazier > 封装是一个延迟计算值(只计算一次) ```java final Lazier _id = new Lazier<>(() -> { String id = getHeader("X-Request-ID"); if (id != null && !id.isEmpty()) return id; return UUID.randomUUID().toString().replace("-", ""); }); ``` * 延迟获取属性值 ```java final Lazier _name = new Lazier<>(() -> getAttr("sys.name", String.class, "app")); ``` * 重新计算 ```java final Lazier _num = new Lazier(() -> new Random().nextInt(10)); _num.get(); _num.clear(); // 清除重新计算 _num.get(); ``` ## 时间段统计 Counter ```java // 按小时统计 Counter counter = new Counter("MM-dd HH", (time, count) -> { // time: 指具体某个小时 // count: 指具体某个小时的统计个数 }); counter.increment(); // 当前小时统计加一 ``` ## 应用示例 最佳实践: [Demo(java)](https://gitee.com/xnat/appdemo) , [GRule(groovy)](https://gitee.com/xnat/grule) ## 更新日志 - [x] feat: 重构事件中心 - [ ] feat: 支持 @PreDestroy @PostConstruct - [ ] feat: Httper 代理 - [ ] feat: 增加日志级别配置 - [ ] feat: Httper 工具支持 websocket - [ ] feat: 自定义注解 ## 参与贡献 xnatural@msn.cn