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