15 Commits

Author SHA1 Message Date
829a7ed798 docs: 更新项目描述信息 2025-10-24 17:13:36 +08:00
f492d5d62e docs: 重命名文档文件名 2025-10-24 17:10:17 +08:00
159a7769dc docs: 修改文档目录 2025-10-24 17:02:57 +08:00
9ab92ce471 docs: 完善项目文档 [!9 (gitee)]
修改包描述(package-info.java)
修改 README.md
添加 docs 文件夹,包含各部分功能的介绍
2025-10-24 03:18:37 +00:00
8dfb3ff694 refactor!: 将 Ref 类从 base 包移动到 util 2025-10-23 16:59:04 +08:00
264717eb62 docs: 修复 UnifiedResponses 的 javadoc 错误 2025-10-23 16:26:11 +08:00
ba38175d93 refactor!: 优化 PagingAndSortingQueryParams (!7 @Gitee)
`PagingAndSortingQueryParams` 的构造器中必须传入一个 `PagingParamsBuilder`,
构建分页参数时使用此 `PagingParamsBuilder`。

只要子类确保同一场景下共用一个  `PagingParamsBuilder`  实例,就不会导致白名单重复校验。
2025-10-23 08:17:17 +00:00
b8c666a023 test: 使用 lombok 简化 TreeBuilder 的测试代码 2025-10-22 17:50:09 +08:00
255aaf182a feat: 新增 MapModifier 用于链式操作修改 Map 数据 (#71 @Gitea)
1. 添加 `MapModifier` 类,封装了一系列对 Map 数据的修改过程,如 `put`、`putIfAbsent`、`putAll`、`remove`、`clear` 等
2. 支持直接修改指定的 `Map`
3. 支持从 `Supplier` 中获取 `Map` 并进行修改
4. 提供了创建并初始化 unmodifiableMap 的方法
5. 增加了相应的单元测试用例

issue Close #67 @Gitea
2025-10-22 09:37:04 +08:00
468453781e chore: add lincese header 2025-10-21 16:32:29 +08:00
3b241de08c docs: 修改 UnifiedResponses 相关文档 (!5 @Gitee) 2025-10-21 07:16:24 +00:00
fb46def402 update NOTICE.
Signed-off-by: ZhouXY108 <luquanlion@outlook.com>
2025-10-21 05:25:00 +00:00
386ede6afd add NOTICE.
Signed-off-by: ZhouXY108 <luquanlion@outlook.com>
2025-10-21 05:23:42 +00:00
8ec61a84c9 build: 添加 MinIO 依赖版本管理 2025-10-20 23:16:03 +08:00
726a94a1a8 chore: 添加依赖管理注释 2025-10-20 23:15:24 +08:00
27 changed files with 1439 additions and 467 deletions

60
NOTICE Normal file
View File

@@ -0,0 +1,60 @@
Plusone Commons
Copyright 2022-present ZhouXY108
This product includes software developed at
Plusone Commons (http://gitea.zhouxy.xyz/plusone/plusone-commons).
===========================================================================
Third-party components and their licenses:
===========================================================================
This software contains code from the following third-party projects:
1. Apache Seata
- Component: IdWorker class implementation
- Source: org.apache.seata.common.util.IdWorker
- Origin: https://github.com/apache/incubator-seata/blob/2.x/common/src/main/java/org/apache/seata/common/util/IdWorker.java
- License: Apache License 2.0
- License URL: https://www.apache.org/licenses/LICENSE-2.0.txt
- Copyright: The Apache Software Foundation
===========================================================================
Dependencies and their licenses:
===========================================================================
The following dependencies are used in this project:
Required Dependencies:
- guava: Apache License 2.0 (https://www.apache.org/licenses/LICENSE-2.0.txt)
Optional Dependencies:
- gson: Apache License 2.0 (https://www.apache.org/licenses/LICENSE-2.0.txt)
- jsr305: Apache License 2.0 (https://www.apache.org/licenses/LICENSE-2.0.txt)
- joda-time: Apache License 2.0 (https://www.apache.org/licenses/LICENSE-2.0.txt)
Test Dependencies:
- commons-lang3: Apache License 2.0 (https://www.apache.org/licenses/LICENSE-2.0.txt)
- Logback: Eclipse Public License 1.0 (https://www.eclipse.org/org/documents/epl-1.0/EPL-1.0.txt) / LGPL 2.1 (https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html)
- Slf4j: MIT License (https://mit-license.org/)
- JUnit: Eclipse Public License 2.0 (https://www.eclipse.org/org/documents/epl-2.0/EPL-2.0.txt)
- lombok: MIT License (https://mit-license.org/)
- hutool: MulanPSL-2.0 (http://license.coscl.org.cn/MulanPSL2)
- MyBatis: Apache License 2.0 (https://www.apache.org/licenses/LICENSE-2.0.txt)
- h2: MPL 2.0 (https://www.mozilla.org/en-US/MPL/2.0/) / EPL 1.0 (https://www.eclipse.org/org/documents/epl-1.0/EPL-1.0.txt)
- Jackson: Apache License 2.0 (https://www.apache.org/licenses/LICENSE-2.0.txt)
===========================================================================
Apache License 2.0 Notice:
===========================================================================
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

230
README.md
View File

@@ -1,221 +1,31 @@
## 一、annotation - 注解
### 1. StaticFactoryMethod
标识静态工厂方法。 *《Effective Java》***Item1** 建议考虑用静态工厂方法替换构造器, 因而考虑有一个注解可以标记一下静态工厂方法,以和其它方法进行区分
# Plusone Commons
## 1. 简介
Plusone Commons 是一个 Java 工具类库,提供了一系列实用的类和方法,用于简化开发
### 2. ReaderMethod 和 WriterMethod
分别标识读方法(如 getter或写方法如 setter
一开始是为了补充日常开发中guava 认为不需要,而我又用得上的工具,所以需要结合 guava 使用。后面也包含了一些从日常工作与学习中抽离出来可以通用的东西。
最早是写了一个集合类,为了方便判断使用读写锁时,哪些情况下使用读锁,哪些情况下使用写锁
Plusone Commons 的工具类不追求“大而全”,而是只提供相对需要的部分功能
### 3. UnsupportedOperation
标识该方法不被支持或没有实现,将抛出 `UnsupportedOperationException`。 为了方便在使用时,不需要点进源码,就能知道该方法没有实现。
> 未来一些不够“通用”的组件会迁移到更合适的模块中。
### 4. Virtual
Java 非 final 的实例方法,对应 C++/C# 中的虚方法,允许被子类覆写。 Virtual 注解旨在设计父类时,强调该方法父类虽然有默认实现,但子类可以根据自己的需要覆写
## 2. 安装
项目基于 OpenJDK 8 和 maven 构建
### 5. ValueObject
标记一个类,表示其作为值对象,区别于 Entity。
项目目前暂未发布到 maven 中央仓库,使用时需克隆代码到本地,并安装到本地仓库,然后才能在项目中引入依赖。
## 二、base - 基础组件
### 1. Ref
`Ref` 包装了一个值,表示对该值的应用。
## 3. 功能
详细功能说明请查阅文档:
灵感来自于 C# 的 ref 参数修饰符。C# 允许通过以下方式,将值返回给调用端:
```C#
void Method(ref int refArgument)
{
refArgument = refArgument + 44;
}
+ [文档地址](/plusone-commons/docs)
int number = 1;
Method(ref number);
Console.WriteLine(number); // Output: 45
```
`Ref` 使 Java 可以达到类似的效果,如:
```java
void method(Ref<Integer> refArgument) {
refArgument.transformValue(i -> i + 44);
}
## 4. 代码仓库
项目仓库一共建了三个:
Ref<Integer> number = Ref.of(1);
method(number);
System.out.println(number.getValue()); // Output: 45
```
当一个方法需要产生多个结果时,无法有多个返回值,可以使用 `Ref` 作为参数传入,方法内部修改 `Ref` 的值。 调用方在调用方法之后,使用 `getValue()` 获取结果。
```java
String method(Ref<Integer> intRefArgument, Ref<String> strRefArgument) {
intRefArgument.transformValue(i -> i + 44);
strRefArgument.setValue("Hello " + strRefArgument.getValue());
return "Return string";
}
+ [GitHub](https://github.com/ZhouXY108/plusone-commons)
+ [gitee](https://gitee.com/zhouxy108/plusone-commons)
+ [自建 Gitea 仓库](http://gitea.zhouxy.xyz/plusone/plusone-commons)
Ref<Integer> number = Ref.of(1);
Ref<String> str = Ref.of("Java");
String result = method(number, str);
System.out.println(number.getValue()); // Output: 45
System.out.println(str.getValue()); // Output: Hello Java
System.out.println(result); // Output: Return string
```
### 2. IWithCode
类似于枚举这样的类型,通常需要设置固定的码值表示对应的含义。 可实现 `IWithCode`、`IWithIntCode`、`IWithLongCode`,便于在需要的地方对这些接口的实现进行处理。
欢迎在 GitHub 和 gitee 上通过 issue 反馈使用过程中发现的问题和建议,也接受善意的 PR。
## 三、collection - 集合
### 1. CollectionTools
集合工具类
## 四、constant - 常量
### 1. 正则常量
`RegexConsts` 包含常见正则表达式;`PatternConsts` 包含对应的 `Pattern` 对象
## 五、exception - 异常
### 1. IMultiTypesException - 多类型异常
异常在不同场景下被抛出,可以用不同的枚举值,表示不同的场景类型。
异常实现 `IMultiTypesException` 的 `IMultiTypesException#getType` 方法,返回对应的场景类型。
表示场景类型的枚举实现 `IMultiTypesException.IExceptionType`,其中的工厂方法用于创建对应类型的异常。
```java
public final class LoginException
extends RuntimeException
implements IMultiTypesException<LoginException.Type> {
private static final long serialVersionUID = 881293090625085616L;
private final Type type;
private LoginException(@Nonnull Type type, @Nonnull String message) {
super(message);
this.type = type;
}
private LoginException(@Nonnull Type type, @Nonnull Throwable cause) {
super(cause);
this.type = type;
}
private LoginException(@Nonnull Type type,
@Nonnull String message,
@Nonnull Throwable cause) {
super(message, cause);
this.type = type;
}
@Override
public @Nonnull Type getType() {
return this.type;
}
// ...
public enum Type implements IExceptionType<String>, IExceptionFactory<LoginException> {
DEFAULT("00", "当前会话未登录"),
NOT_TOKEN("10", "未提供token"),
INVALID_TOKEN("20", "token无效"),
TOKEN_TIMEOUT("30", "token已过期"),
BE_REPLACED("40", "token已被顶下线"),
KICK_OUT("50", "token已被踢下线"),
;
@Nonnull
private final String code;
@Nonnull
private final String defaultMessage;
Type(@Nonnull String code, @Nonnull String defaultMessage) {
this.code = code;
this.defaultMessage = defaultMessage;
}
@Override
public @Nonnull String getCode() {
return code;
}
@Override
public @Nonnull String getDefaultMessage() {
return defaultMessage;
}
@Override
public @Nonnull LoginException create() {
return new LoginException(this, this.defaultMessage);
}
@Override
public @Nonnull LoginException create(String message) {
return new LoginException(this, message);
}
@Override
public @Nonnull LoginException create(Throwable cause) {
return new LoginException(this, cause);
}
@Override
public @Nonnull LoginException create(String message, Throwable cause) {
return new LoginException(this, message, cause);
}
}
}
```
使用时,可以使用这种方式创建并抛出异常:
```java
throw LoginException.Type.TOKEN_TIMEOUT.create();
```
### 2. 业务异常
预设常见的业务异常。可继承 `BizException` 自定义业务异常。
### 3. 系统异常
预设常见的系统异常。可继承 `SysException` 自定义系统异常。
## 六、function - 函数式编程
### 1. PredicateTools
`PredicateTools` 用于 `Predicate` 的相关操作。
### 2. Functional interfaces
补充可能用得上的函数式接口:
| Group | FunctionalInterface | method |
| ------------- | -------------------- | -------------------------------- |
| UnaryOperator | BoolUnaryOperator | boolean applyAsBool (boolean) |
| UnaryOperator | CharUnaryOperator | char applyAsChar(char) |
| Throwing | Executable | void execute() throws E |
| Throwing | ThrowingConsumer | void accept(T) throws E |
| Throwing | ThrowingFunction | R apply(T) throws E |
| Throwing | ThrowingPredicate | boolean test(T) throws E |
| Throwing | ThrowingSupplier | T get() throws E |
| Optional | OptionalSupplier | Optional<T> get() throws E |
| Optional | ToOptionalBiFunction | Optional<R> apply(T,U) |
| Optional | ToOptionalFunction | Optional<R> apply(T) |
## 七、model - 业务建模组件
包含业务建模可能用到的性别、身份证等元素,也包含数据传输对象,如分页查询参数、响应结果、分页结果等。
### 数据传输对象
#### 1. 分页
分页组件由 `PagingAndSortingQueryParams` 作为入参, 因为分页必须伴随着排序,不然可能出现同一个对象重复出现在不同页,有的对象不被查询到的情况, 所以分页查询的入参必须包含排序条件。
用户可继承 `PagingAndSortingQueryParams` 构建自己的分页查询入参,需在构造器中调用 `PagingAndSortingQueryParams` 的构造器,传入一个 Map 作为白名单, key 是供前端指定用于排序的**属性名**value 是对应数据库中的**字段名**,只有在白名单中指定的属性名才允许作为排序条件。
`PagingAndSortingQueryParams` 包含三个主要的属性:
- **size** - 每页显示的记录数
- **pageNum** - 当前页码
- **orderBy** - 排序条件
其中 `orderBy` 是一个 List可以指定多个排序条件每个排序条件是一个字符串 格式为“**属性名-ASC**”或“**属性名-DESC**”,分别表示升序和降序。
比如前端传入的 orderBy 为 ["name-ASC","age-DESC"],意味着要按 name 进行升序name 相同的情况下则按 age 进行降序。
使用时调用 `PagingAndSortingQueryParams#buildPagingParams()` 方法获取分页参数 `PagingParams`。
分页结果可以存放到 `PageResult` 中,作为出参。
#### 2. UnifiedResponse
UnifiedResponse 对返回给前端的数据进行封装,包含 `code`、`message`、`data。`
可使用 `UnifiedResponses` 快速构建 `UnifiedResponse` 对象。 `UnifiedResponses` 默认的成功代码为 "2000000" 用户按测试类 `CustomUnifiedResponseFactoryTests` 中所示范的,继承 `UnifiedResponses` 实现自己的工厂类, 自定义 `SUCCESS_CODE` 和 `DEFAULT_SUCCESS_MSG` 和工厂方法。 见 [issue#22 @Gitea](http://gitea.zhouxy.xyz/plusone/plusone-commons/issues/22)。
## 八、time - 时间 API
### 1. 季度
模仿 JDK 的 `java.time.Month` 和 `java.time.YearMonth` 实现 `Quarter`、`YearQuarter`,对季度进行建模。
## 九、util - 工具类
包含树构建器(`TreeBuilder`)、断言工具(`AssertTools`、ID 生成器(`IdGenerator`)及其它实用工具类。
## 5. 许可
项目使用 [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0) 开源,相关声明请参阅 `NOTICE` 文件。

View File

@@ -0,0 +1,8 @@
## 1. 注解
|注解|说明|
|--|--|
| **StaticFactoryMethod** | **标识静态工厂方法***《Effective Java》***Item1** 建议考虑用静态工厂方法替换构造器, 因而考虑有一个注解可以标记一下静态工厂方法,以和其它方法进行区分。|
| **ReaderMethod** / **WriterMethod** | **分别标识读方法(如 getter或写方法如 setter**<br>*最早是写了一个集合类,为了方便判断使用读写锁时,哪些情况下使用读锁,哪些情况下使用写锁。*|
| **UnsupportedOperation** | **标识该方法不被支持或没有实现**,将抛出 `UnsupportedOperationException`。 为了方便在使用时,不需要点进源码,就能知道该方法没有实现。|
| **Virtual** | Java 非 final 的实例方法,对应 C++/C# 中的虚方法,允许被子类覆写。 **Virtual 注解旨在设计父类时,强调该方法父类虽然有默认实现,但子类可以根据自己的需要覆写**。|
| **ValueObject** | 标记一个类,表示其作为**值对象**,区别于 Entity。|

View File

@@ -0,0 +1,39 @@
## 2. 集合
### 2.1. CollectionTools
简单的集合工具类,包含判空等常用方法。
### 2.2. MapModifier
Map 修改器。封装一系列对 Map 数据的修改操作,修改 Map 的数据。可以用于 Map 的数据初始化等操作。
```java
// MapModifier
MapModifier<String, Object> modifier = new MapModifier<String, Object>()
.putAll(commonProperties)
.put("username", "Ben")
.put("accountStatus", LOCKED);
// 从 Supplier 中获取 Map并修改数据
Map<String, Object> map = modifier.getAndModify(HashMap::new);
// 可以灵活使用不同 Map 类型的不同构造器
Map<String, Object> map = modifier.getAndModify(() -> new HashMap<>(8));
Map<String, Object> map = modifier.getAndModify(() -> new HashMap<>(anotherMap));
Map<String, Object> map = modifier.getAndModify(TreeMap::new);
Map<String, Object> map = modifier.getAndModify(ConcurrentHashMap::new);
// 修改已有的 Map
modifier.modify(map);
// 创建一个有初始化数据的不可变的 Map
Map<String, Object> map = modifier.getUnmodifiableMap();
// 链式调用创建并初始化数据
Map<String, Object> map = new MapModifier<String, Object>()
.putAll(commonProperties)
.put("username", "Ben")
.put("accountStatus", LOCKED)
.getAndModify(HashMap::new);
```

View File

@@ -0,0 +1,118 @@
## 3. 异常
### 3.1. 业务异常
|异常|描述|
|---|---|
|`BizException`|**业务异常**<br>*用户可继承 `BizException` 自定义业务异常。*|
`RequestParamsException`|**用户请求参数错误**|
|» » `InvalidInputException`|**用户输入内容非法**<br>00 - **DEFAULT** (用户输入内容非法)<br>01 - **CONTAINS_ILLEGAL_AND_MALICIOUS_LINKS** (包含非法恶意跳转链接)<br>02 - **CONTAINS_ILLEGAL_WORDS** (包含违禁敏感词)<br>03 - **PICTURE_CONTAINS_ILLEGAL_INFORMATION** (图片包含违禁信息)<br>04 - **INFRINGE_COPYRIGHT** (文件侵犯版权)|
### 3.2. 系统异常
|异常|描述|
|---|---|
|`SysException`|**系统异常**(表示技术异常)<br>*用户可继承 `SysException` 自定义系统异常。*|
`DataOperationResultException`|**数据操作的结果不符合预期**|
`NoAvailableMacFoundException`|**无法找到可访问的 Mac 地址**|
### 3.3. 其它异常
|异常|描述|
|---|---|
|`DataNotExistsException`|**数据不存在异常**|
|`ParsingFailureException`|**数据解析异常**<br>00 - **DEFAULT** (解析失败)<br>10 - **NUMBER_PARSING_FAILURE** (数字转换失败)<br>20 - **DATE_TIME_PARSING_FAILURE** (时间解析失败)<br>30 - **JSON_PARSING_FAILURE** (JSON 解析失败)<br>40 - **XML_PARSING_FAILURE** (XML 解析失败)|
### 3.4. 多类型异常
异常在不同场景下被抛出,可以用不同的枚举值,表示不同的场景类型。
异常实现 `IMultiTypesException``getType` 方法,返回对应的场景类型。
枚举实现 `IExceptionType` 接口,表示不同的异常场景。也可以实现 `IExceptionFactory`,用于创建对应场景的异常。
```java
public final class LoginException
extends RuntimeException
implements IMultiTypesException<LoginException.Type> {
private static final long serialVersionUID = 881293090625085616L;
private final Type type;
private LoginException(@Nonnull Type type, @Nonnull String message) {
super(message);
this.type = type;
}
private LoginException(@Nonnull Type type, @Nonnull Throwable cause) {
super(cause);
this.type = type;
}
private LoginException(@Nonnull Type type,
@Nonnull String message,
@Nonnull Throwable cause) {
super(message, cause);
this.type = type;
}
@Override
public @Nonnull Type getType() {
return this.type;
}
// ...
public enum Type implements IExceptionType<String>, IExceptionFactory<LoginException> {
DEFAULT("00", "当前会话未登录"),
NOT_TOKEN("10", "未提供token"),
INVALID_TOKEN("20", "token无效"),
TOKEN_TIMEOUT("30", "token已过期"),
BE_REPLACED("40", "token已被顶下线"),
KICK_OUT("50", "token已被踢下线"),
;
@Nonnull
private final String code;
@Nonnull
private final String defaultMessage;
Type(@Nonnull String code, @Nonnull String defaultMessage) {
this.code = code;
this.defaultMessage = defaultMessage;
}
@Override
public @Nonnull String getCode() {
return code;
}
@Override
public @Nonnull String getDefaultMessage() {
return defaultMessage;
}
@Override
public @Nonnull LoginException create() {
return new LoginException(this, this.defaultMessage);
}
@Override
public @Nonnull LoginException create(@Nonnull String message) {
return new LoginException(this, message);
}
@Override
public @Nonnull LoginException create(@Nonnull Throwable cause) {
return new LoginException(this, cause);
}
@Override
public @Nonnull LoginException create(@Nonnull String message, @Nonnull Throwable cause) {
return new LoginException(this, message, cause);
}
}
}
```
使用时,可以使用这种方式创建并抛出异常:
```java
throw LoginException.Type.TOKEN_TIMEOUT.create();
```

View File

@@ -0,0 +1,22 @@
## 4. - 函数式编程
### 4.1. PredicateTools
`PredicateTools` 用于 `Predicate` 的相关操作。
### 4.2. Functional interfaces
补充可能用得上的函数式接口:
| Group | FunctionalInterface | method |
| --- | --- | --- |
| UnaryOperator | **BoolUnaryOperator** | boolean applyAsBool (boolean) |
| UnaryOperator | **CharUnaryOperator** | char applyAsChar(char) |
| Throwing | **Executable** | void execute() throws E |
| Throwing | **ThrowingConsumer** | void accept(T) throws E |
| Throwing | **ThrowingFunction** | R apply(T) throws E |
| Throwing | **ThrowingPredicate** | boolean test(T) throws E |
| Throwing | **ThrowingSupplier** | T get() throws E |
| Optional | **OptionalSupplier** | Optional<T> get() throws E |
| Optional | **ToOptionalBiFunction** | Optional<R> apply(T,U) |
| Optional | **ToOptionalFunction** | Optional<R> apply(T) |

View File

@@ -0,0 +1,100 @@
## 5. 数据模型
### 5.1. 业务模型
| | 类型 | 描述 |
| --- | --- | --- |
| 接口 | `IDCardNumber` | 身份证号 |
| 值对象 | » `Chinese2ndGenIDCardNumber` | 中国二代居民身份证号 |
| 值对象 | `SemVer` | 语义化版本号 |
| 值对象(枚举)| `Gender` | 性别 |
| ~~值对象(抽象类)~~| ~~`ValidatableStringRecord`~~ | ~~带校验的字符串值对象~~ |
### 5.2. 数据传输对象
#### 5.2.1. 分页查询
`PagingAndSortingQueryParams` (分页排序查询参数)
`PagingAndSortingQueryParams` 包含三个主要的属性:
- **size** - 每页显示的记录数
- **pageNum** - 当前页码
- **orderBy** - 排序条件
分页必须伴随着排序,不然可能出现同一个对象重复出现在不同页,有的对象不被查询到的情况。
其中 `orderBy` 是一个 `List<String>`,可以指定多个排序条件,每个排序条件是一个字符串, 格式为“**属性名-ASC**”或“**属性名-DESC**”,分别表示升序和降序。
例如当 `orderBy` 的值为 `["name-ASC","age-DESC"]`,意味着要按 `name` 进行升序排列,`name` 相同的情况下则按 `age` 进行降序排列。
用户可继承 `PagingAndSortingQueryParams` 构建自己的分页查询入参,子类需在构造器中调用 `PagingAndSortingQueryParams` 的构造器,传入一个 `PagingParamsBuilder` 用于构建分页参数。同一场景下,复用一个 {@link PagingParamsBuilder} 实例即可。
构建 `PagingParamsBuilder` 时,需传入一个 `Map` 作为可排序字段的白名单,`key` 是供前端指定用于排序的**属性名**`value` 是对应数据库中的**字段名**,只有在白名单中指定的属性名才允许作为排序条件。
```java
@ToString(callSuper = true)
class AccountQueryParams extends PagingAndSortingQueryParams {
private static final Map<String, String> PROPERTY_COLUMN_MAP = ImmutableMap.<String, String>builder()
.put("id", "id")
.put("username", "username")
.build();
private static final PagingParamsBuilder PAGING_PARAMS_BUILDER = PagingAndSortingQueryParams
.pagingParamsBuilder(20, 100, PROPERTY_COLUMN_MAP);
public AccountQueryParams() {
super(PAGING_PARAMS_BUILDER);
}
private @Getter @Setter Long id;
private @Getter @Setter String username;
private @Getter @Setter String email;
private @Getter @Setter Integer status;
}
```
使用时调用 `PagingAndSortingQueryParams#buildPagingParams()` 方法获取分页参数 `PagingParams`。分页结果可以存放到 `PageResult` 中,作为出参。
```java
public PageResult<AccountVO> queryPage(AccountQueryParams params) {
// 获取分页参数
PagingParams pagingParams = params.buildPagingParams();
// 从 params 获取字段查询条件,从 pagingParams 获取分页条件,查询一页数据
List<AccountVO> list = accountQueries.queryAccountList(params, pagingParams);
// 查询总记录数
long count = accountQueries.countAccount(params);
// 返回分页结果
return PageResult.of(list, count);
}
```
#### 5.2.2. UnifiedResponse
`UnifiedResponse` 对返回给前端的数据进行封装,包含 `code``message``data`
`UnifiedResponses``UnifiedResponse` 的工厂类。用于快速构建 `UnifiedResponse` 对象,默认的成功代码为 `2000000`
用户可以继承 `UnifiedResponses` 实现自己的工厂类,自定义 SUCCESS_CODE 和 DEFAULT_SUCCESS_MSG以及工厂方法。如下所示
```java
// 自定义工厂类
public static class CustomUnifiedResponses extends UnifiedResponses {
public static final String SUCCESS_CODE = "000";
public static final String DEFAULT_SUCCESS_MSG = "成功";
public static <T> UnifiedResponse<T> success() {
return of(SUCCESS_CODE, DEFAULT_SUCCESS_MSG);
}
public static <T> UnifiedResponse<T> success(@Nullable String message) {
return of(SUCCESS_CODE, message);
}
public static <T> UnifiedResponse<T> success(@Nullable String message, @Nullable T data) {
return of(SUCCESS_CODE, message, data);
}
private CustomUnifiedResponses() {
super();
}
}
// 使用自定义工厂类
CustomUnifiedResponses.success("查询成功", userList); // 状态码为 000
```
> 见 [issue#22 @Gitea](http://gitea.zhouxy.xyz/plusone/plusone-commons/issues/22)

View File

@@ -0,0 +1,24 @@
## 6. 时间 API
### 6.1. 季度
模仿 JDK 的 `java.time.Month``java.time.YearMonth` 实现 `xyz.zhouxy.plusone.commons.time.Quarter``xyz.zhouxy.plusone.commons.timeYearQuarter`,对季度进行建模。
*这两个类的代码修改后,也提交给了 **hutool**。见 gitee
上的 [pr#1324](https://gitee.com/chinabugotech/hutool/pulls/1324)*。
### 6.2. DateTimeTools
`xyz.zhouxy.plusone.commons.util.DateTimeTools` 提供了包含 Java 旧的时间 API 和 `java.time` API 在内的日期时间的常用操作。
### 6.3. JodaTimeTools
`xyz.zhouxy.plusone.commons.util.JodaTimeTools` 提供了 JodaTime 和 `java.time` API 相互转换的工具方法:
- `toJodaInstant`
- `toJavaInstant`
- `toJodaDateTime`
- `toZonedDateTime`
- `toJodaLocalDateTime`
- `toJavaLocalDateTime`
- `toJavaZone`
- `toJodaZone`

View File

@@ -0,0 +1,275 @@
## 7. 工具类
### 7.1. 数组工具ArrayTools
| 方法 | 描述 |
| --- | --- |
| `isEmpty` | 判断数组是否为空 |
| `isNotEmpty` | 判断数组是否不为空 |
| `isAllElementsNotNull` | 判断数组中所有元素是否不为空 |
| `concat` | 拼接数组 |
| `repeat` | 重复数组中的元素 |
| `fill` | 填充数组 |
| `indexOf` | 获取元素在数组中的索引 |
| `lastIndexOf` | 获取元素最后出现在数组中的索引 |
| `contains` | 判断数组中是否包含某个元素 |
### 7.2. 断言工具AssertTools
`AssertTools` 不封装过多判断逻辑,鼓励充分使用项目中的工具类对数据进行判断:
```java
AssertTools.checkArgument(StringUtils.hasText(str), "The argument cannot be blank.");
AssertTools.checkState(ArrayUtils.isNotEmpty(result), "The result cannot be empty.");
AssertTools.checkCondition(!CollectionUtils.isEmpty(roles),
() -> new InvalidInputException("The roles cannot be empty."));
AssertTools.checkCondition(RegexTools.matches(email, PatternConsts.EMAIL),
"must be a well-formed email address");
```
### 7.3. 枚举工具
#### ~~7.3.1 枚举类Enumeration已废弃~~
~~`Enumeration` 的实现来自于 .net 社区。因为 C# 本身的枚举不带行为,所以继承自 `Enumeration` 类,以实现带行为的枚举常量。~~
**~~但 Java 的枚举可以带行为,故大多数情况下不需要这种设计。~~**
#### 7.3.2 Enum 工具类EnumTools
用于枚举的 ordinal 和枚举值的转换等操作。
由于不推荐使用枚举的 ordinal**故大多数方法已废弃**。更推荐的实现是枚举实现 `IWithCode` 之类的接口,在枚举中提供枚举值和枚举码的转换。
### 7.4. ID 生成器
#### 7.4.1. ID 生成器IdGenerator
- 提供了 `UUID` 相关的方法
| 方法 | 描述 |
| --- | --- |
| newUuid | 获取新的 `UUID` |
| uuidString | 获取新的 UUID 字符串 |
| simpleUuidString | 获取新的 UUID 字符串(无连接符) |
| toSimpleString | 将 `UUID` 转换为无连接符的字符串 |
- 使用 `IdWorker` *(来自 **Seata** 的雪花算法的变种)* 生成分布式唯一 ID
#### 7.4.2. IdWorker
来自 [Apache Seata](https://seata.apache.org/) 的 [`org.apache.seata.common.util.IdWorker`](https://github.com/apache/incubator-seata/blob/2.x/common/src/main/java/org/apache/seata/common/util/IdWorker.java),是雪花算法的变种。
详细介绍参考以下文章:
- [Seata基于改良版雪花算法的分布式UUID生成器分析](https://seata.apache.org/zh-cn/blog/seata-analysis-UUID-generator)
- [关于新版雪花算法的答疑](https://seata.apache.org/zh-cn/blog/seata-snowflake-explain)
- [在开源项目中看到一个改良版的雪花算法,现在它是你的了。](https://juejin.cn/post/7264387737276203065)
- [关于若干读者,阅读“改良版雪花算法”后提出的几个共性问题的回复。](https://juejin.cn/post/7265516484029743138)
#### 7.4.3. SnowflakeIdGenerator
`SnowflakeIdGenerator` 是原版的雪花算法的实现
### 7.5. 树构建器TreeBuilder
`TreeBuilder` 是一个树构建器,用于将列表数据构建为树结构。
`TreeBuilder` 构造器的入参:
- **identityGetter**: 从节点中获取其标识的逻辑
- **parentIdentityGetter**: 获取父节点标识的逻辑
- **addChild**: 添加子节点的逻辑
- **defaultComparator**: 默认的 Comparator用于排序
> **注意:`TreeBuilder` 的 `buildTree` 方法,会直接更改列表中的节点。设计初衷是将查询到的列表,构建成为树结构之后直接返回给前端,如果需要,请在调用之前做深拷贝,然后再将深拷贝的结果作为入参传入。**
以下示例演示 `TreeBuilder` 的使用:
#### 7.5.1. 处理相对复杂的 entity
假设有如下的实体类:
```java
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@EqualsAndHashCode
@ToString
class Menu implements Serializable {
protected final @Getter String parentMenuCode;
protected final @Getter String menuCode;
protected final @Getter String title;
protected final @Getter int orderNum;
private static final long serialVersionUID = 20240917181424L;
}
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
class MenuItem extends Menu {
private final @Getter String url;
private MenuItem(String parentMenuCode, String menuCode, String title, String url, int orderNum) {
super(parentMenuCode, menuCode, title, orderNum);
this.url = url;
}
static MenuItem of(String parentMenuCode, String menuCode, String title, String url, int orderNum) {
return new MenuItem(parentMenuCode, menuCode, title, url, orderNum);
}
static MenuItem of(String menuCode, String title, String url, int orderNum) {
return new MenuItem(null, menuCode, title, url, orderNum);
}
private static final long serialVersionUID = 20240917181910L;
}
@ToString(callSuper = true)
class MenuList extends Menu {
private List<Menu> children;
private MenuList(String parentMenuCode, String menuCode, String title, int orderNum) {
super(parentMenuCode, menuCode, title, orderNum);
}
static MenuList of(String parentMenuCode, String menuCode, String title, int orderNum) {
return new MenuList(parentMenuCode, menuCode, title, orderNum);
}
static MenuList of(String menuCode, String title, int orderNum) {
return new MenuList(null, menuCode, title, orderNum);
}
@SuppressWarnings("unused")
static MenuList of(String menuCode, String title, Iterable<Menu> children, int orderNum) {
return of(null, menuCode, title, children, orderNum);
}
static MenuList of(String parentMenuCode, String menuCode, String title, Iterable<Menu> children,
int orderNum) {
final MenuList instance = of(parentMenuCode, menuCode, title, orderNum);
children.forEach(instance::addChild);
return instance;
}
public void addChild(Menu child) {
if (this.children == null) {
this.children = Lists.newArrayList();
}
this.children.add(child);
}
private static final long serialVersionUID = 20240917181917L;
}
```
其中,`Menu` 表示菜单节点,其子类 `MenuItem` 表示菜单项,在树中作为叶子节点,另一子类 `MenuList` 表示菜单列表,其子菜单放在 `children` 字段中。`MenuList` 提供了 `addChild` 方法用于将子菜单添加到 `children` 中。
使用以下方式构建并使用 `TreeBuilder`
```java
// 创建 TreeBuilder
TreeBuilder<Menu, MenuList, String> treeBuilder = new TreeBuilder<>(
// getMenuCode 方法获取节点标识
Menu::getMenuCode,
// getParentMenuCode 方法获取父节点标识,如果父节点不存在,返回 Optional.empty()
menu -> Optional.ofNullable(menu.getParentMenuCode()),
// addChild 方法用于将子节点添加到父节点的 children 中
MenuList::addChild,
// 默认的 Comparator使用 orderNum 进行排序
Comparator.comparing(Menu::getOrderNum));
// 需要的话进行深拷贝
List<Menu> clonedMenus = menus.stream().map(ObjectUtil::clone).collect(Collectors.toList());
// 按照创建时设置的逻辑,构建树结构
List<Menu> result = treeBuilder.buildTree(clonedMenus);
```
#### 7.5.2. 处理 POJO
`TreeBuilder` 也可以处理 POJO只需要自定义 `TreeBuilder` 所需的入参即可。
```java
// POJO
@Data
class Menu implements Serializable {
private final String parentMenuCode;
private final String menuCode;
private final String title;
private final int orderNum;
private final String url;
private List<Menu> children;
private static final long serialVersionUID = 1298482252210272617L;
}
```
使用以下方式构建并使用 `TreeBuilder`
```java
// 创建 TreeBuilder
TreeBuilder<Menu, MenuList, String> treeBuilder = new TreeBuilder<>(
// getMenuCode 方法获取节点标识
Menu::getMenuCode,
// getParentMenuCode 方法获取父节点标识,如果父节点不存在,返回 Optional.empty()
menu -> Optional.ofNullable(menu.getParentMenuCode()),
// 自定义 addChild 逻辑
(menuList, child) -> {
List<Menu> children = menuList.getChildren();
if (children == null) {
children = Lists.newArrayList();
menuList.setChildren(children);
}
children.add(child);
},
// 默认的 Comparator使用 orderNum 进行排序
Comparator.comparing(Menu::getOrderNum));
// 按照创建时设置的逻辑,构建树结构
List<Menu> result = treeBuilder.buildTree(clonedMenus);
```
### 7.6. Ref
`Ref` 包装了一个值,表示对该值的应用。
C# 中允许通过 ref 参数修饰符,将值返回给调用端:
```csharp
void Method(ref int refArgument)
{
refArgument = refArgument + 44;
}
int number = 1;
Method(ref number);
Console.WriteLine(number); // Output: 45
```
`Ref` 使 Java 可以达到类似的效果,如:
```java
void method(Ref<Integer> refArgument) {
refArgument.transformValue(i -> i + 44);
}
Ref<Integer> number = Ref.of(1);
method(number);
System.out.println(number.getValue()); // Output: 45
```
当一个方法需要产生多个结果时,无法有多个返回值,可以使用 `Ref` 作为参数传入,方法内部修改 `Ref` 的值。 调用方在调用方法之后,使用 `getValue()` 获取结果。
```java
String method(Ref<Integer> intRefArgument, Ref<String> strRefArgument) {
intRefArgument.transformValue(i -> i + 44);
strRefArgument.setValue("Hello " + strRefArgument.getValue());
return "Return string";
}
Ref<Integer> number = Ref.of(1);
Ref<String> str = Ref.of("Java");
String result = method(number, str);
System.out.println(number.getValue()); // Output: 45
System.out.println(str.getValue()); // Output: Hello Java
System.out.println(result); // Output: Return string
```
### 7.7 其它工具类
- **`BigDecimals`**: BigDecimal 工具
- **`Numbers`**: 数字工具
- **`OptionalTools`**: Optional 工具
- **`RandomTools`**: 随机工具
- **`RegexTools`**: 正则工具
- **`StringTools`**: 字符串工具
- **`ZipTools`**: zip 工具

View File

@@ -0,0 +1,9 @@
## 8. 其它内容
### 8.1. IWithCode
对于类似枚举这样的类型,通常需要设置固定的码值表示对应的含义。 可实现 `IWithCode``IWithIntCode``IWithLongCode`,便于在需要的地方对这些接口的实现进行处理。
### 8.2. 正则常量
`RegexConsts` 包含常见正则表达式;`PatternConsts` 包含对应的 `Pattern` 对象

View File

@@ -13,7 +13,7 @@
<artifactId>plusone-commons</artifactId>
<description>
常见工具集,结合 guava 使用。
Plusone Commons 是一个 Java 工具类库,提供了一系列实用的类和方法,用于简化开发。结合 guava 使用。
</description>
<properties>

View File

@@ -15,44 +15,7 @@
*/
/**
* <h2>注解</h2>
*
* <h3>
* 1. {@link StaticFactoryMethod}
* </h3>
* <p>
* 标识<b>静态工厂方法</b>。
* 《Effective Java》的 Item1 建议考虑用静态工厂方法替换构造器,
* 因而考虑有一个注解可以标记一下静态工厂方法,以和其它方法进行区分。
*
* <h3>
* 2. {@link ReaderMethod} 和 {@link WriterMethod}
* </h3>
* <p>
* 分别标识<b>读方法</b>(如 getter或<b>写方法</b>(如 setter
*
* <p>
* 最早是写了一个集合类,为了方便判断使用读写锁时,哪些情况下使用读锁,哪些情况下使用写锁。
*
* <h3>
* 3. {@link UnsupportedOperation}
* </h3>
* <p>
* 标识该方法不被支持或没有实现,将抛出 {@link UnsupportedOperationException}。
* 为了方便在使用时,不需要点进源码,就能知道该方法没有实现。
*
* <h3>
* 4. {@link Virtual}
* </h3>
* <p>
* Java 非 final 的实例方法,对应 C++/C&num; 中的虚方法,允许被子类覆写。
* {@link Virtual} 注解旨在设计父类时,强调该方法父类虽然有默认实现,但子类可以根据自己的需要覆写。
*
* <h3>
* 5. {@link ValueObject}
* </h3>
* <p>
* 标记一个类,表示其作为值对象,区别于 Entity。
* 注解
*
* @author ZhouXY108 <luquanlion@outlook.com>
*/

View File

@@ -15,56 +15,7 @@
*/
/**
* <h2>基础组件</h2>
*
* <h3>1. Ref</h3>
* <p>
* {@link Ref} 包装了一个值,表示对该值的应用。
*
* <p>灵感来自于 C&num; 的 {@code ref} 参数修饰符。C&num; 允许通过以下方式,将值返回给调用端:</p>
* <pre>
* void Method(ref int refArgument)
* {
* refArgument = refArgument + 44;
* }
*
* int number = 1;
* Method(ref number);
* Console.WriteLine(number); // Output: 45
* </pre>
* {@link Ref} 使 Java 可以达到类似的效果,如:
* <pre>
* void method(Ref&lt;Integer&gt; refArgument) {
* refArgument.transformValue(i -&gt; i + 44);
* }
*
* Ref&lt;Integer&gt; number = Ref.of(1);
* method(number);
* System.out.println(number.getValue()); // Output: 45
* </pre>
* <p>
* 当一个方法需要产生多个结果时,无法有多个返回值,可以使用 {@link Ref} 作为参数传入,方法内部修改 {@link Ref} 的值。
* 调用方在调用方法之后,使用 {@code getValue()} 获取结果。
*
* <pre>
* String method(Ref&lt;Integer&gt; intRefArgument, Ref&lt;String&gt; strRefArgument) {
* intRefArgument.transformValue(i -&gt; i + 44);
* strRefArgument.setValue("Hello " + strRefArgument.getValue());
* return "Return string";
* }
*
* Ref&lt;Integer&gt; number = Ref.of(1);
* Ref&lt;String&gt; str = Ref.of("Java");
* String result = method(number, str);
* System.out.println(number.getValue()); // Output: 45
* System.out.println(str.getValue()); // Output: Hello Java
* System.out.println(result); // Output: Return string
* </pre>
*
* <h3>2. IWithCode</h3>
* <p>
* 类似于枚举这样的类型,通常需要设置固定的码值表示对应的含义。
* 可实现 {@link IWithCode}、{@link IWithIntCode}、{@link IWithLongCode},便于在需要的地方对这些接口的实现进行处理。
* 基础内容
*
* @author ZhouXY108 <luquanlion@outlook.com>
*/
@@ -73,4 +24,5 @@
package xyz.zhouxy.plusone.commons.base;
import javax.annotation.ParametersAreNonnullByDefault;
import javax.annotation.CheckReturnValue;

View File

@@ -0,0 +1,254 @@
/*
* Copyright 2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.plusone.commons.collection;
import static xyz.zhouxy.plusone.commons.util.AssertTools.checkArgumentNotNull;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import com.google.common.annotations.Beta;
/**
* Map 修改器
*
* <p>
* 封装一系列对 Map 数据的修改操作,修改 Map 的数据。可以用于 Map 的数据初始化等操作。
*
* <pre>
* // MapModifier
* MapModifier&lt;String, Object&gt; modifier = new MapModifier&lt;String, Object&gt;()
* .putAll(commonProperties)
* .put("username", "Ben")
* .put("accountStatus", LOCKED);
*
* // 从 Supplier 中获取 Map并修改数据
* Map&lt;String, Object&gt; map = modifier.getAndModify(HashMap::new);
*
* // 可以灵活使用不同 Map 类型的不同构造器
* Map&lt;String, Object&gt; map = modifier.getAndModify(() -&gt; new HashMap&lt;&gt;(8));
* Map&lt;String, Object&gt; map = modifier.getAndModify(() -&gt; new HashMap&lt;&gt;(anotherMap));
* Map&lt;String, Object&gt; map = modifier.getAndModify(TreeMap::new);
* Map&lt;String, Object&gt; map = modifier.getAndModify(ConcurrentHashMap::new);
*
* // 修改已有的 Map
* modifier.modify(map);
*
* // 创建一个有初始化数据的不可变的 Map
* Map&lt;String, Object&gt; map = modifier.getUnmodifiableMap();
*
* // 链式调用创建并初始化数据
* Map&lt;String, Object&gt; map = new MapModifier&lt;String, Object&gt;()
* .putAll(commonProperties)
* .put("username", "Ben")
* .put("accountStatus", LOCKED)
* .getAndModify(HashMap::new);
* </pre>
*
* @author ZhouXY108 <luquanlion@outlook.com>
* @since 1.1.0
*/
@Beta
public class MapModifier<K, V> {
@Nonnull
private Consumer<Map<K, V>> operators;
/**
* 创建一个空的 MapModifier
*/
public MapModifier() {
this.operators = m -> {
// do nothing
};
}
/**
* 添加一个键值对。
*
* <p>
* <b>注意:键值对是否允许为 {@code null},由最终作用的 {@link Map} 类型决定。</b>
*
* @param key 要添加的 {@code key}
* @param value 要添加的 {@code value}
* @return MapModifier
*/
public MapModifier<K, V> put(@Nullable K key, @Nullable V value) {
return addOperationInternal(map -> map.put(key, value));
}
/**
* 添加一个键值对,如果 key 已经存在,则不添加。
*
* <p>
* <b>注意:键值对是否允许为 {@code null},由最终作用的 {@link Map} 类型决定。</b>
*
* @param key 要添加的 {@code key}
* @param value 要添加的 {@code value}
* @return MapModifier
*/
public MapModifier<K, V> putIfAbsent(@Nullable K key, @Nullable V value) {
return addOperationInternal(map -> map.putIfAbsent(key, value));
}
/**
* 添加多个键值对。
*
* <p>
* <b>注意:键值对是否允许为 {@code null},由最终作用的 {@link Map} 类型决定。</b>
*
* @param otherMap 要添加的键值对集合。
* 如果为 {@code null},则什么都不做。
*
* @return MapModifier
*/
public MapModifier<K, V> putAll(@Nullable Map<? extends K, ? extends V> otherMap) {
if (otherMap == null || otherMap.isEmpty()) {
return this;
}
return addOperationInternal(map -> map.putAll(otherMap));
}
/**
* 添加多个键值对。
*
* <p>
* <b>注意:键值对是否允许为 {@code null},由最终作用的 {@link Map} 类型决定。</b>
*
* @param entries 要添加的键值对集合
* @return MapModifier
*/
@SafeVarargs
public final MapModifier<K, V> putAll(Map.Entry<? extends K, ? extends V>... entries) {
if (entries.length == 0) {
return this;
}
return addOperationInternal(map -> {
for (Map.Entry<? extends K, ? extends V> entry : entries) {
map.put(entry.getKey(), entry.getValue());
}
});
}
/**
* 当 {@code key} 不存在时,计算对应的值,并添加到 {@code map} 中。
*
* <p>
* 调用 {@link Map#computeIfAbsent(Object, Function)}。
*
* <p>
* <b>注意:键值对是否允许为 {@code null},由最终作用的 {@link Map} 类型决定。</b>
*
* @param key 要添加的 {@code key}
* @param mappingFunction 计算 {@code key} 对应的值
* @return MapModifier
*/
public MapModifier<K, V> computeIfAbsent(K key,
Function<? super K, ? extends V> mappingFunction) {
return addOperationInternal(map -> map.computeIfAbsent(key, mappingFunction));
}
/**
* 当 {@code key} 存在时,计算对应的值,并添加到 {@code map} 中。
*
* <p>
* 调用 {@link Map#computeIfPresent(Object, BiFunction)}。
*
* <p>
* <b>注意:键值对是否允许为 {@code null},由最终作用的 {@link Map} 类型决定。</b>
*
* @param key 要添加的 {@code key}
* @param remappingFunction 计算 {@code key} 对应的值
* @return MapModifier
*/
public MapModifier<K, V> computeIfPresent(K key,
BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
return addOperationInternal(map -> map.computeIfPresent(key, remappingFunction));
}
/**
* 删除 {@code key}。
*
* <p>
* <b>注意key 是否允许为 {@code null},由最终作用的 {@link Map} 类型决定。</b>
*
* @param key 要删除的 {@code key}
* @return MapModifier
*/
public MapModifier<K, V> remove(K key) {
return addOperationInternal(map -> map.remove(key));
}
/**
* 清空 {@code map}
*
* @return MapModifier
*/
public MapModifier<K, V> clear() {
return addOperationInternal(Map::clear);
}
/**
* 修改 {@code map}
*
* @param map 要修改的 {@code map}
* @return 修改后的 {@code map}。当入参是 {@code null} 时,返回 {@code null}。
*/
public <T extends Map<K, V>> void modify(@Nullable T map) {
if (map != null) {
this.operators.accept(map);
}
}
/**
* 修改 {@code map}
*
* @param mapSupplier {@code map} 的 {@link Supplier}
* @return 修改后的 {@code map}。
* 当从 {@code mapSupplier} 获取的 {@code map} 为 {@code null} 时,返回 {@code null}。
*/
@CheckForNull
public <T extends Map<K, V>> T getAndModify(Supplier<T> mapSupplier) {
checkArgumentNotNull(mapSupplier, "The map supplier cannot be null.");
T map = mapSupplier.get();
modify(map);
return map;
}
/**
* 创建一个有初始化数据的不可变的 {@code Map}
*
* @return 不可变的 {@code Map}
*/
public Map<K, V> getUnmodifiableMap() {
return Collections.unmodifiableMap(getAndModify(HashMap::new));
}
private MapModifier<K, V> addOperationInternal(Consumer<Map<K, V>> operator) {
this.operators = this.operators.andThen(operator);
return this;
}
}

View File

@@ -15,12 +15,7 @@
*/
/**
* <h2>集合</h2>
*
* <h3>
* 1. {@link CollectionTools}
* </h3>
* 集合工具类
* 集合相关工具
*
* @author ZhouXY108 <luquanlion@outlook.com>
*/

View File

@@ -15,115 +15,8 @@
*/
/**
* <h2>异常</h2>
*
* <h3>1. {@link IMultiTypesException} - 多类型异常</h3>
* <p>
* 异常在不同场景下被抛出,可以用不同的枚举值,表示不同的场景类型。
*
* <p>
* 异常实现 {@link IMultiTypesException} 的 {@link IMultiTypesException#getType} 方法,返回对应的场景类型。
*
* <p>
* 表示场景类型的枚举实现 {@link IMultiTypesException.IExceptionType},各个枚举值本身就是该场景的异常的工厂实例,
* 使用其中的工厂方法用于创建对应类型的异常。
*
* <pre>
* public final class LoginException
* extends RuntimeException
* implements IMultiTypesException&lt;LoginException, LoginException.Type, String&gt; {
* private static final long serialVersionUID = 881293090625085616L;
* private final Type type;
* private LoginException(&#64;Nonnull Type type, &#64;Nonnull String message) {
* super(message);
* this.type = type;
* }
*
* private LoginException(&#64;Nonnull Type type, &#64;Nonnull Throwable cause) {
* super(cause);
* this.type = type;
* }
*
* private LoginException(&#64;Nonnull Type type,
* &#64;Nonnull String message,
* &#64;Nonnull Throwable cause) {
* super(message, cause);
* this.type = type;
* }
*
* &#64;Override
* public &#64;Nonnull Type getType() {
* return this.type;
* }
*
* // ...
*
* public enum Type implements IExceptionType&lt;LoginException, String&gt; {
* DEFAULT("00", "当前会话未登录"),
* NOT_TOKEN("10", "未提供token"),
* INVALID_TOKEN("20", "token无效"),
* TOKEN_TIMEOUT("30", "token已过期"),
* BE_REPLACED("40", "token已被顶下线"),
* KICK_OUT("50", "token已被踢下线"),
* ;
*
* &#64;Nonnull
* private final String code;
* &#64;Nonnull
* private final String defaultMessage;
*
* Type(&#64;Nonnull String code, &#64;Nonnull String defaultMessage) {
* this.code = code;
* this.defaultMessage = defaultMessage;
* }
*
* &#64;Override
* public &#64;Nonnull String getCode() {
* return code;
* }
*
* &#64;Override
* public &#64;Nonnull String getDefaultMessage() {
* return defaultMessage;
* }
*
* &#64;Override
* public &#64;Nonnull LoginException create() {
* return new LoginException(this, this.defaultMessage);
* }
*
* &#64;Override
* public &#64;Nonnull LoginException create(String message) {
* return new LoginException(this, message);
* }
*
* &#64;Override
* public &#64;Nonnull LoginException create(Throwable cause) {
* return new LoginException(this, cause);
* }
*
* &#64;Override
* public &#64;Nonnull LoginException create(String message, Throwable cause) {
* return new LoginException(this, message, cause);
* }
* }
* }
* </pre>
*
* 使用时,可以使用这种方式创建并抛出异常:
* <pre>
* throw LoginException.Type.TOKEN_TIMEOUT.create();
* </pre>
*
* <h3>2. 业务异常</h3>
* 预设常见的业务异常。可继承 {@link BizException} 自定义业务异常。
*
* <h3>3. 系统异常</h3>
* 预设常见的系统异常。可继承 {@link SysException} 自定义系统异常。
* 包含常见的业务异常与系统异常,以及异常相关的工具
*
* @author ZhouXY108 <luquanlion@outlook.com>
*/
package xyz.zhouxy.plusone.commons.exception;
import xyz.zhouxy.plusone.commons.exception.business.*;
import xyz.zhouxy.plusone.commons.exception.system.*;

View File

@@ -35,8 +35,64 @@ import xyz.zhouxy.plusone.commons.util.StringTools;
* 分页排序查询参数
*
* <p>
* 根据传入的 {@code size} 和 {@code pageNum}
* 提供 {@code getOffset} 方法计算 SQL 语句中 {@code offset} 的值。
* 包含三个主要的属性:
* <ul>
* <li>size - 每页显示的记录数</li>
* <li>pageNum - 当前页码</li>
* <li>orderBy - 排序条件</li>
* </ul>
*
* <p>
* 分页必须伴随着排序,不然可能出现同一个对象重复出现在不同页,有的对象不被查询到的情况。
*
* <p>
* 其中 {@code orderBy} 是一个 {@code List&lt;String&gt;},可以指定多个排序条件。
* 每个排序条件是一个字符串, 格式为“属性名-ASC”或“属性名-DESC”分别表示升序和降序。
* 例如,当 {@code orderBy} 的值为 {@code ["name-ASC","age-DESC"]}
* 意味着要按 {@code name} 进行升序排列,{@code name} 相同的情况下则按 {@code age} 进行降序排列。
*
* <p>
* 用户可继承 {@link PagingAndSortingQueryParams} 构建自己的分页查询入参,
* 子类需在构造器中调用 {@link PagingAndSortingQueryParams} 的构造器,
* 传入一个 {@link PagingParamsBuilder} 用于构建分页参数。
* 同一场景下,复用一个 {@link PagingParamsBuilder} 实例即可。
*
* <p>
* 构建 {@link PagingParamsBuilder} 时,需传入一个 {@code Map} 作为可排序字段的白名单,
* {@code key} 是供前端指定用于排序的属性名,{@code value} 是对应数据库中的字段名。
* 只有在此白名单中的属性名才允许用于排序。
*
* <pre>
* class AccountQueryParams extends PagingAndSortingQueryParams {
* private static final Map&lt;String, String&gt; PROPERTY_COLUMN_MAP = ImmutableMap.&lt;String, String&gt;builder()
* .put("id", "id")
* .put("username", "username")
* .build();
* private static final PagingParamsBuilder PAGING_PARAMS_BUILDER = PagingAndSortingQueryParams
* .pagingParamsBuilder(20, 100, PROPERTY_COLUMN_MAP);
*
* public AccountQueryParams() {
* // 所有的 AccountQueryParams 复用同一个 PagingParamsBuilder 实例
* super(PAGING_PARAMS_BUILDER);
* }
*
* private @Getter @Setter Long id;
* private @Getter @Setter String username;
* private @Getter @Setter String email;
* private @Getter @Setter Integer status;
* }
*
* public PageResult&lt;AccountVO&gt; queryPage(AccountQueryParams params) {
* // 获取分页参数
* PagingParams pagingParams = params.buildPagingParams();
* // 从 params 获取字段查询条件,从 pagingParams 获取分页条件,查询一页数据
* List&lt;AccountVO&gt; list = accountQueries.queryAccountList(params, pagingParams);
* // 查询总记录数
* long count = accountQueries.countAccount(params);
* // 返回分页结果
* return PageResult.of(list, count);
* }
* </pre>
*
* @author ZhouXY108 <luquanlion@outlook.com>
* @see PagingParams
@@ -44,10 +100,23 @@ import xyz.zhouxy.plusone.commons.util.StringTools;
*/
public class PagingAndSortingQueryParams {
private final PagingParamsBuilder pagingParamsBuilder;
private Integer size;
private Long pageNum;
private List<String> orderBy;
/**
* 创建一个 {@code PagingAndSortingQueryParams} 实例
*
* @param pagingParamsBuilder
* 分页参数构造器。
* 通过 {@link #pagingParamsBuilder(int, int, Map)} 创建,同一场景下只需要共享同一个实例。
*/
public PagingAndSortingQueryParams(PagingParamsBuilder pagingParamsBuilder) {
this.pagingParamsBuilder = pagingParamsBuilder;
}
// Setters
/**
@@ -88,11 +157,31 @@ public class PagingAndSortingQueryParams {
+ "]";
}
protected static PagingParamsBuilder pagingParamsBuilder(
/**
* 创建一个分页参数构造器
*
* @param defaultSize 默认每页大小
* @param maxSize 最大每页大小
* @param sortableProperties
* 可排序属性。
* key 是供前端指定用于排序的属性名value 是对应数据库中的字段名。
* 只有在此白名单中的属性名才允许用于排序。
* @return 分页参数构造器
*/
public static PagingParamsBuilder pagingParamsBuilder(
int defaultSize, int maxSize, Map<String, String> sortableProperties) {
return new PagingParamsBuilder(defaultSize, maxSize, sortableProperties);
}
/**
* 根据当前查询参数,构建分页参数
*
* @return 分页参数
*/
public PagingParams buildPagingParams() {
return this.pagingParamsBuilder.buildPagingParams(this);
}
/**
* 可排序属性
*/
@@ -103,7 +192,7 @@ public class PagingAndSortingQueryParams {
private final String sqlSnippet;
SortableProperty(String propertyName, String columnName, String orderType) {
private SortableProperty(String propertyName, String columnName, String orderType) {
this.propertyName = propertyName;
this.columnName = columnName;
checkArgument("ASC".equalsIgnoreCase(orderType) || "DESC".equalsIgnoreCase(orderType));

View File

@@ -19,7 +19,40 @@ package xyz.zhouxy.plusone.commons.model.dto;
import javax.annotation.Nullable;
/**
* UnifiedResponse 工厂
* {@link UnifiedResponse} 工厂类。
* 用于快速构建 {@link UnifiedResponse} 对象,默认的成功代码为 {@code 2000000}。
*
* <p>
* 用户可以继承 {@link UnifiedResponses} 实现自己的工厂类,
* 自定义 SUCCESS_CODE 和 DEFAULT_SUCCESS_MSG以及工厂方法。
* 如下所示:
* <pre>
* // 自定义工厂类
* public static class CustomUnifiedResponses extends UnifiedResponses {
*
* public static final String SUCCESS_CODE = "000";
* public static final String DEFAULT_SUCCESS_MSG = "成功";
*
* public static &lt;T&gt; UnifiedResponse&lt;T&gt; success() {
* return of(SUCCESS_CODE, DEFAULT_SUCCESS_MSG);
* }
*
* public static &lt;T&gt; UnifiedResponse&lt;T&gt; success(@Nullable String message) {
* return of(SUCCESS_CODE, message);
* }
*
* public static &lt;T&gt; UnifiedResponse&lt;T&gt; success(@Nullable String message, @Nullable T data) {
* return of(SUCCESS_CODE, message, data);
* }
*
* private CustomUnifiedResponses() {
* super();
* }
* }
* // 使用自定义工厂类
* CustomUnifiedResponses.success("查询成功", userList); // 状态码为 000
* </pre>
* 见 <a href="http://zhouxy.xyz:3000/plusone/plusone-commons/issues/22">issue#22</a>。
*
* @author ZhouXY108 <luquanlion@outlook.com>
* @since 1.0.0

View File

@@ -52,13 +52,11 @@
* {@link UnifiedResponse} 对返回给前端的数据进行封装,包含 code、message、data。
*
* <p>
* 可使用 {@link UnifiedResponses} 快速构建 {@link UnifiedResponse} 对象。
* {@link UnifiedResponses} 默认的成功代码为 "2000000"
* 用户按测试类
* <a href="http://zhouxy.xyz:3000/plusone/plusone-commons/src/branch/main/src/test/java/xyz/zhouxy/plusone/commons/model/dto/CustomUnifiedResponseFactoryTests.java">CustomUnifiedResponseFactoryTests</a>
* 中所示范的,继承 {@link UnifiedResponses} 实现自己的工厂类,
* 自定义 SUCCESS_CODE 和 DEFAULT_SUCCESS_MSG 和工厂方法。
* 见 <a href="http://zhouxy.xyz:3000/plusone/plusone-commons/issues/22">issue#22</a>。
* {@link UnifiedResponses} 用于快速构建 {@link UnifiedResponse} 对象,默认的成功代码为 {@code 2000000}
*
* <p>
* 用户可以继承 {@link UnifiedResponses} 实现自己的工厂类,
* 自定义 SUCCESS_CODE 和 DEFAULT_SUCCESS_MSG以及工厂方法。
*
* @author ZhouXY108 <luquanlion@outlook.com>
*/

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package xyz.zhouxy.plusone.commons.base;
package xyz.zhouxy.plusone.commons.util;
import java.util.Objects;
import java.util.function.Consumer;

View File

@@ -17,7 +17,8 @@
/**
* <h2>工具类</h2>
* <p>
* 包含树构建器({@link TreeBuilder})、断言工具({@link AssertTools})、ID 生成器({@link IdGenerator})及其它实用工具类。
* 包含树构建器({@link TreeBuilder})、断言工具({@link AssertTools})、
* ID 生成器({@link IdGenerator})及其它实用工具类。
*
* @author ZhouXY108 <luquanlion@outlook.com>
*/

View File

@@ -0,0 +1,316 @@
/*
* Copyright 2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.plusone.commons.collection;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import javax.annotation.Nullable;
import org.junit.jupiter.api.Test;
import com.google.common.collect.ImmutableMap;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class MapModifierTests {
private static final String APP_START_ID = UUID.randomUUID().toString();
private static final String LOCKED = "LOCKED";
private static final Map<String, String> commonProperties = ImmutableMap.<String, String>builder()
.put("channel", "MOBILE")
.put("appStartId", APP_START_ID)
.build();
@Test
void demo() {
Map<String, String> expected = new HashMap<String, String>() {
{
put("channel", "MOBILE");
put("appStartId", APP_START_ID);
put("username", "Ben");
put("accountStatus", LOCKED);
}
};
// MapModifier
MapModifier<String, String> modifier = new MapModifier<String, String>()
.putAll(commonProperties)
.put("username", "Ben")
.put("accountStatus", LOCKED);
// 从 Supplier 中获取 Map并修改数据
HashMap<String, String> hashMap1 = modifier.getAndModify(HashMap::new);
assertEquals(expected, hashMap1);
// 可以灵活使用不同 Map 类型的不同构造器
HashMap<String, String> hashMap2 = modifier.getAndModify(() -> new HashMap<>(8));
assertEquals(expected, hashMap2);
// HashMap<String, String> hashMap3 = modifier.getAndModify(() -> new HashMap<>(anotherMap));
TreeMap<String, String> treeMap = modifier.getAndModify(TreeMap::new);
assertEquals(expected, treeMap);
ConcurrentHashMap<String, String> concurrentHashMap = modifier.getAndModify(ConcurrentHashMap::new);
assertEquals(expected, concurrentHashMap);
assertNull(modifier.getAndModify(() -> (Map<String, String>) null));
// 修改已有的 Map
Map<String, String> srcMap = new HashMap<>();
srcMap.put("srcKey1", "srcValue1");
srcMap.put("srcKey2", "srcValue2");
modifier.modify(srcMap);
assertEquals(new HashMap<String, String>() {
{
putAll(commonProperties);
put("username", "Ben");
put("accountStatus", LOCKED);
put("srcKey1", "srcValue1");
put("srcKey2", "srcValue2");
}
}, srcMap);
assertDoesNotThrow(() -> modifier.modify((Map<String, String>) null));
// 创建一个有初始化数据的不可变的 {@code Map}
Map<String, String> unmodifiableMap = modifier.getUnmodifiableMap();
assertEquals(expected, unmodifiableMap);
assertThrows(UnsupportedOperationException.class,
() -> unmodifiableMap.put("key", "value"));
}
@Test
void createAndInitData() {
// 链式调用创建并初始化数据
HashMap<String, String> map = new MapModifier<String, String>()
.putAll(commonProperties)
.put("username", "Ben")
.put("accountStatus", LOCKED)
.getAndModify(HashMap::new);
HashMap<String, String> expected = new HashMap<String, String>() {
{
put("channel", "MOBILE");
put("appStartId", APP_START_ID);
put("username", "Ben");
put("accountStatus", LOCKED);
}
};
assertEquals(expected, map);
}
@Test
void put() {
Map<String, String> map = new MapModifier<String, String>()
.put("key1", "value0")
.put("key1", "value1")
.getAndModify(HashMap::new);
assertEquals(new HashMap<String, String>() {
{
put("key1", "value0");
put("key1", "value1");
}
}, map);
new MapModifier<String, String>()
.put("key1", "newValue1")
.put("key2", null)
.modify(map);
assertEquals("newValue1", map.get("key1"));
assertTrue(map.containsKey("key2"));
assertNull(map.get("key2"));
}
@Test
void putIfAbsent() {
Map<String, String> map = new MapModifier<String, String>()
.putIfAbsent("key1", null)
.putIfAbsent("key1", "value1")
.putIfAbsent("key1", "value2")
.getAndModify(HashMap::new);
assertEquals(new HashMap<String, String>() {
{
putIfAbsent("key1", null);
putIfAbsent("key1", "value1");
putIfAbsent("key1", "value2");
}
}, map);
new MapModifier<String, String>()
.putIfAbsent("key1", "newValue1")
.modify(map);
assertTrue(map.containsKey("key1"));
assertEquals("value1", map.get("key1"));
}
@Test
void putAll_map() {
Map<String, String> entries = new HashMap<String, String>() {
{
put("key1", "value1");
put("key2", "value2");
}
};
Map<String, String> map = new MapModifier<String, String>()
.putAll((Map<String, String>) null)
.putAll(Collections.emptyMap())
.putAll(entries)
.getAndModify(HashMap::new);
assertEquals(entries, map);
new MapModifier<String, String>()
.putAll(new HashMap<String, String>() {
{
put("key2", "newValue2");
put("key3", "value3");
}
})
.modify(map);
assertEquals(new HashMap<String, String>() {
{
put("key1", "value1");
put("key2", "value2");
put("key2", "newValue2");
put("key3", "value3");
}
}, map);
}
@Test
void putAll_entries() {
Map<String, String> entries = new HashMap<String, String>() {
{
put("key1", "value1");
put("key2", "value2");
}
};
Map<String, String> map = new MapModifier<String, String>()
.putAll(new SimpleEntry<>("key1", "value1"),
new SimpleEntry<>("key2", "value2"))
.getAndModify(HashMap::new);
assertEquals(entries, map);
new MapModifier<String, String>()
.putAll()
.putAll(new SimpleEntry<>("key2", "newValue2"),
new SimpleEntry<>("key3", "value3"))
.modify(map);
assertEquals(new HashMap<String, String>() {
{
put("key1", "value1");
put("key2", "value2");
put("key2", "newValue2");
put("key3", "value3");
}
}, map);
}
@Test
void computeIfAbsent_keyAndFunction() {
Map<String, String> map = new MapModifier<String, String>()
.computeIfAbsent("key1", k -> null)
.computeIfAbsent("key1", k -> "value1")
.computeIfAbsent("key1", k -> "value2")
.getAndModify(HashMap::new);
assertEquals(new HashMap<String, String>() {
{
computeIfAbsent("key1", k -> null);
computeIfAbsent("key1", k -> "value1");
computeIfAbsent("key1", k -> "value2");
}
}, map);
new MapModifier<String, String>()
.computeIfAbsent("key1", k -> "newValue1")
.modify(map);
assertTrue(map.containsKey("key1"));
assertEquals("value1", map.get("key1"));
}
@Test
void computeIfPresent_keyAndBiFunction() {
Map<String, String> map = new HashMap<String, String>() {{
put("key1", "value1");
}};
new MapModifier<String, String>()
.computeIfPresent("key1", (k, v) -> k + v)
.computeIfPresent("key2", (k, v) -> k + v)
.modify(map);
assertEquals(new HashMap<String, String>() {{
put("key1", "key1value1");
}}, map);
}
@Test
void remove() {
Map<String, String> map = new HashMap<String, String>() {{
put("key1", "value1");
put("key2", "value2");
}};
new MapModifier<String, String>()
.remove("key2")
.modify(map);
assertEquals(new HashMap<String, String>() {{
put("key1", "value1");
}}, map);
}
@Test
void clear() {
Map<String, String> map = new HashMap<String, String>() {{
put("key1", "value1");
put("key2", "value2");
}};
new MapModifier<String, String>()
.clear()
.modify(map);
assertTrue(map.isEmpty());
}
@Getter
static class SimpleEntry<K, V> implements Map.Entry<K, V> {
private final K key;
private final V value;
public SimpleEntry(K key, V value) {
this.key = key;
this.value = value;
}
@Override
public V setValue(@Nullable V value) {
throw new UnsupportedOperationException("Unimplemented method 'setValue'");
}
}
}

View File

@@ -234,6 +234,10 @@ class AccountQueryParams extends PagingAndSortingQueryParams {
private static final PagingParamsBuilder PAGING_PARAMS_BUILDER = PagingAndSortingQueryParams
.pagingParamsBuilder(20, 100, PROPERTY_COLUMN_MAP);
public AccountQueryParams() {
super(PAGING_PARAMS_BUILDER);
}
private @Getter @Setter Long id;
private @Getter @Setter String username;
private @Getter @Setter String email;
@@ -248,10 +252,6 @@ class AccountQueryParams extends PagingAndSortingQueryParams {
}
return this.createTimeEnd.plusDays(1);
}
public PagingParams buildPagingParams() {
return PAGING_PARAMS_BUILDER.buildPagingParams(this);
}
}
@Data

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package xyz.zhouxy.plusone.commons.base;
package xyz.zhouxy.plusone.commons.util;
import static org.junit.jupiter.api.Assertions.*;

View File

@@ -36,7 +36,10 @@ import com.google.common.collect.Lists;
import com.google.gson.Gson;
import cn.hutool.core.util.ObjectUtil;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.ToString;
@SuppressWarnings("null")
@@ -61,7 +64,7 @@ class TreeBuilderTests {
private final TreeBuilder<Menu, MenuList, String> treeBuilder = new TreeBuilder<>(
Menu::getMenuCode,
menu -> Optional.ofNullable(menu.parentMenuCode),
menu -> Optional.ofNullable(menu.getParentMenuCode()),
MenuList::addChild,
Comparator.comparing(Menu::getOrderNum));
@@ -134,45 +137,23 @@ class TreeBuilderTests {
((MenuList) menuMap.get("C1")).children);
}
@ToString
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@EqualsAndHashCode
@ToString
private abstract static class Menu implements Serializable {
protected final String parentMenuCode;
protected final String menuCode;
protected final String title;
protected final int orderNum;
public Menu(String parentMenuCode, String menuCode, String title, int orderNum) {
this.parentMenuCode = parentMenuCode;
this.menuCode = menuCode;
this.title = title;
this.orderNum = orderNum;
}
public String getMenuCode() {
return menuCode;
}
public String getParentMenuCode() {
return parentMenuCode;
}
public String getTitle() {
return title;
}
public int getOrderNum() {
return orderNum;
}
protected final @Getter String parentMenuCode;
protected final @Getter String menuCode;
protected final @Getter String title;
protected final @Getter int orderNum;
private static final long serialVersionUID = 20240917181424L;
}
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
private static final class MenuItem extends Menu {
private final String url;
private final @Getter String url;
private MenuItem(String parentMenuCode, String menuCode, String title, String url, int orderNum) {
super(parentMenuCode, menuCode, title, orderNum);
@@ -187,10 +168,6 @@ class TreeBuilderTests {
return new MenuItem(null, menuCode, title, url, orderNum);
}
public String getUrl() {
return url;
}
private static final long serialVersionUID = 20240917181910L;
}

View File

@@ -1,3 +1,19 @@
/*
* Copyright 2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.plusone.commons.util;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;

View File

@@ -52,6 +52,8 @@
<jasypt.version>1.9.3</jasypt.version>
<jbcrypt.version>0.4</jbcrypt.version>
<minio.version>8.6.0</minio.version>
<lombok.version>1.18.36</lombok.version>
<hutool.version>5.8.37</hutool.version>
@@ -146,22 +148,27 @@
<version>${gson.version}</version>
</dependency>
<!-- MapStruct -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${mapstruct.version}</version>
</dependency>
<!-- H2 测试数据库 -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>${h2.version}</version>
</dependency>
<!-- MyBatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>${mybatis.version}</version>
</dependency>
<!-- Query DSL -->
<dependency>
<groupId>com.querydsl</groupId>
@@ -169,6 +176,7 @@
<version>${querydsl.version}</version>
</dependency>
<!-- Byte Buddy 字节码工具 -->
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
@@ -188,22 +196,34 @@
<version>${poi.version}</version>
</dependency>
<!-- JWT -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>${java-jwt.version}</version>
</dependency>
<!-- jasypt 加密解密 -->
<dependency>
<groupId>org.jasypt</groupId>
<artifactId>jasypt</artifactId>
<version>${jasypt.version}</version>
</dependency>
<!-- Bcrypt是一种用于密码哈希的加密算法基于Blowfish算法 -->
<dependency>
<groupId>org.mindrot</groupId>
<artifactId>jbcrypt</artifactId>
<version>${jbcrypt.version}</version>
</dependency>
<!-- MinIO -->
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>${minio.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>