forked from plusone/plusone-commons
Compare commits
15 Commits
3b441e4575
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 829a7ed798 | |||
| f492d5d62e | |||
| 159a7769dc | |||
| 9ab92ce471 | |||
| 8dfb3ff694 | |||
| 264717eb62 | |||
| ba38175d93 | |||
| b8c666a023 | |||
| 255aaf182a | |||
| 468453781e | |||
| 3b241de08c | |||
| fb46def402 | |||
| 386ede6afd | |||
| 8ec61a84c9 | |||
| 726a94a1a8 |
60
NOTICE
Normal file
60
NOTICE
Normal 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
230
README.md
@@ -1,221 +1,31 @@
|
|||||||
## 一、annotation - 注解
|
# Plusone Commons
|
||||||
### 1. StaticFactoryMethod
|
## 1. 简介
|
||||||
标识静态工厂方法。 *《Effective Java》* 的 **Item1** 建议考虑用静态工厂方法替换构造器, 因而考虑有一个注解可以标记一下静态工厂方法,以和其它方法进行区分。
|
Plusone Commons 是一个 Java 工具类库,提供了一系列实用的类和方法,用于简化开发。
|
||||||
|
|
||||||
### 2. ReaderMethod 和 WriterMethod
|
一开始是为了补充日常开发中,guava 认为不需要,而我又用得上的工具,所以需要结合 guava 使用。后面也包含了一些从日常工作与学习中抽离出来可以通用的东西。
|
||||||
分别标识读方法(如 getter)或写方法(如 setter)。
|
|
||||||
|
|
||||||
最早是写了一个集合类,为了方便判断使用读写锁时,哪些情况下使用读锁,哪些情况下使用写锁。
|
Plusone Commons 的工具类不追求“大而全”,而是只提供相对需要的部分功能。
|
||||||
|
|
||||||
### 3. UnsupportedOperation
|
> 未来一些不够“通用”的组件会迁移到更合适的模块中。
|
||||||
标识该方法不被支持或没有实现,将抛出 `UnsupportedOperationException`。 为了方便在使用时,不需要点进源码,就能知道该方法没有实现。
|
|
||||||
|
|
||||||
### 4. Virtual
|
## 2. 安装
|
||||||
Java 非 final 的实例方法,对应 C++/C# 中的虚方法,允许被子类覆写。 Virtual 注解旨在设计父类时,强调该方法父类虽然有默认实现,但子类可以根据自己的需要覆写。
|
项目基于 OpenJDK 8 和 maven 构建。
|
||||||
|
|
||||||
### 5. ValueObject
|
项目目前暂未发布到 maven 中央仓库,使用时需克隆代码到本地,并安装到本地仓库,然后才能在项目中引入依赖。
|
||||||
标记一个类,表示其作为值对象,区别于 Entity。
|
|
||||||
|
|
||||||
## 二、base - 基础组件
|
## 3. 功能
|
||||||
### 1. Ref
|
详细功能说明请查阅文档:
|
||||||
`Ref` 包装了一个值,表示对该值的应用。
|
|
||||||
|
|
||||||
灵感来自于 C# 的 ref 参数修饰符。C# 允许通过以下方式,将值返回给调用端:
|
+ [文档地址](/plusone-commons/docs)
|
||||||
```C#
|
|
||||||
void Method(ref int refArgument)
|
|
||||||
{
|
|
||||||
refArgument = refArgument + 44;
|
|
||||||
}
|
|
||||||
|
|
||||||
int number = 1;
|
## 4. 代码仓库
|
||||||
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);
|
+ [GitHub](https://github.com/ZhouXY108/plusone-commons)
|
||||||
method(number);
|
+ [gitee](https://gitee.com/zhouxy108/plusone-commons)
|
||||||
System.out.println(number.getValue()); // Output: 45
|
+ [自建 Gitea 仓库](http://gitea.zhouxy.xyz/plusone/plusone-commons)
|
||||||
```
|
|
||||||
当一个方法需要产生多个结果时,无法有多个返回值,可以使用 `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);
|
欢迎在 GitHub 和 gitee 上通过 issue 反馈使用过程中发现的问题和建议,也接受善意的 PR。
|
||||||
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`,便于在需要的地方对这些接口的实现进行处理。
|
|
||||||
|
|
||||||
## 三、collection - 集合
|
## 5. 许可
|
||||||
### 1. CollectionTools
|
项目使用 [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0) 开源,相关声明请参阅 `NOTICE` 文件。
|
||||||
集合工具类
|
|
||||||
|
|
||||||
## 四、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`)及其它实用工具类。
|
|
||||||
|
|||||||
8
plusone-commons/docs/1_annotation.md
Normal file
8
plusone-commons/docs/1_annotation.md
Normal 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。|
|
||||||
39
plusone-commons/docs/2_collection.md
Normal file
39
plusone-commons/docs/2_collection.md
Normal 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);
|
||||||
|
```
|
||||||
118
plusone-commons/docs/3_exception.md
Normal file
118
plusone-commons/docs/3_exception.md
Normal 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();
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
22
plusone-commons/docs/4_function.md
Normal file
22
plusone-commons/docs/4_function.md
Normal 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) |
|
||||||
100
plusone-commons/docs/5_model.md
Normal file
100
plusone-commons/docs/5_model.md
Normal 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)
|
||||||
24
plusone-commons/docs/6_time.md
Normal file
24
plusone-commons/docs/6_time.md
Normal 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`
|
||||||
275
plusone-commons/docs/7_tools.md
Normal file
275
plusone-commons/docs/7_tools.md
Normal 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 工具
|
||||||
9
plusone-commons/docs/8_others.md
Normal file
9
plusone-commons/docs/8_others.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
## 8. 其它内容
|
||||||
|
|
||||||
|
### 8.1. IWithCode
|
||||||
|
|
||||||
|
对于类似枚举这样的类型,通常需要设置固定的码值表示对应的含义。 可实现 `IWithCode`、`IWithIntCode`、`IWithLongCode`,便于在需要的地方对这些接口的实现进行处理。
|
||||||
|
|
||||||
|
### 8.2. 正则常量
|
||||||
|
|
||||||
|
`RegexConsts` 包含常见正则表达式;`PatternConsts` 包含对应的 `Pattern` 对象
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
<artifactId>plusone-commons</artifactId>
|
<artifactId>plusone-commons</artifactId>
|
||||||
|
|
||||||
<description>
|
<description>
|
||||||
常见工具集,结合 guava 使用。
|
Plusone Commons 是一个 Java 工具类库,提供了一系列实用的类和方法,用于简化开发。结合 guava 使用。
|
||||||
</description>
|
</description>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
|
|||||||
@@ -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# 中的虚方法,允许被子类覆写。
|
|
||||||
* {@link Virtual} 注解旨在设计父类时,强调该方法父类虽然有默认实现,但子类可以根据自己的需要覆写。
|
|
||||||
*
|
|
||||||
* <h3>
|
|
||||||
* 5. {@link ValueObject}
|
|
||||||
* </h3>
|
|
||||||
* <p>
|
|
||||||
* 标记一个类,表示其作为值对象,区别于 Entity。
|
|
||||||
*
|
*
|
||||||
* @author ZhouXY108 <luquanlion@outlook.com>
|
* @author ZhouXY108 <luquanlion@outlook.com>
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -15,56 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <h2>基础组件</h2>
|
* 基础内容
|
||||||
*
|
|
||||||
* <h3>1. Ref</h3>
|
|
||||||
* <p>
|
|
||||||
* {@link Ref} 包装了一个值,表示对该值的应用。
|
|
||||||
*
|
|
||||||
* <p>灵感来自于 C# 的 {@code ref} 参数修饰符。C# 允许通过以下方式,将值返回给调用端:</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<Integer> refArgument) {
|
|
||||||
* refArgument.transformValue(i -> i + 44);
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* Ref<Integer> 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<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
|
|
||||||
* </pre>
|
|
||||||
*
|
|
||||||
* <h3>2. IWithCode</h3>
|
|
||||||
* <p>
|
|
||||||
* 类似于枚举这样的类型,通常需要设置固定的码值表示对应的含义。
|
|
||||||
* 可实现 {@link IWithCode}、{@link IWithIntCode}、{@link IWithLongCode},便于在需要的地方对这些接口的实现进行处理。
|
|
||||||
*
|
*
|
||||||
* @author ZhouXY108 <luquanlion@outlook.com>
|
* @author ZhouXY108 <luquanlion@outlook.com>
|
||||||
*/
|
*/
|
||||||
@@ -73,4 +24,5 @@
|
|||||||
package xyz.zhouxy.plusone.commons.base;
|
package xyz.zhouxy.plusone.commons.base;
|
||||||
|
|
||||||
import javax.annotation.ParametersAreNonnullByDefault;
|
import javax.annotation.ParametersAreNonnullByDefault;
|
||||||
|
|
||||||
import javax.annotation.CheckReturnValue;
|
import javax.annotation.CheckReturnValue;
|
||||||
|
|||||||
@@ -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<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);
|
||||||
|
* </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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,12 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <h2>集合</h2>
|
* 集合相关工具
|
||||||
*
|
|
||||||
* <h3>
|
|
||||||
* 1. {@link CollectionTools}
|
|
||||||
* </h3>
|
|
||||||
* 集合工具类
|
|
||||||
*
|
*
|
||||||
* @author ZhouXY108 <luquanlion@outlook.com>
|
* @author ZhouXY108 <luquanlion@outlook.com>
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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<LoginException, LoginException.Type, String> {
|
|
||||||
* 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<LoginException, String> {
|
|
||||||
* 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);
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
* </pre>
|
|
||||||
*
|
|
||||||
* 使用时,可以使用这种方式创建并抛出异常:
|
|
||||||
* <pre>
|
|
||||||
* throw LoginException.Type.TOKEN_TIMEOUT.create();
|
|
||||||
* </pre>
|
|
||||||
*
|
|
||||||
* <h3>2. 业务异常</h3>
|
|
||||||
* 预设常见的业务异常。可继承 {@link BizException} 自定义业务异常。
|
|
||||||
*
|
|
||||||
* <h3>3. 系统异常</h3>
|
|
||||||
* 预设常见的系统异常。可继承 {@link SysException} 自定义系统异常。
|
|
||||||
*
|
*
|
||||||
* @author ZhouXY108 <luquanlion@outlook.com>
|
* @author ZhouXY108 <luquanlion@outlook.com>
|
||||||
*/
|
*/
|
||||||
package xyz.zhouxy.plusone.commons.exception;
|
package xyz.zhouxy.plusone.commons.exception;
|
||||||
|
|
||||||
import xyz.zhouxy.plusone.commons.exception.business.*;
|
|
||||||
import xyz.zhouxy.plusone.commons.exception.system.*;
|
|
||||||
|
|||||||
@@ -35,8 +35,64 @@ import xyz.zhouxy.plusone.commons.util.StringTools;
|
|||||||
* 分页排序查询参数
|
* 分页排序查询参数
|
||||||
*
|
*
|
||||||
* <p>
|
* <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<String>},可以指定多个排序条件。
|
||||||
|
* 每个排序条件是一个字符串, 格式为“属性名-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<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() {
|
||||||
|
* // 所有的 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<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);
|
||||||
|
* }
|
||||||
|
* </pre>
|
||||||
*
|
*
|
||||||
* @author ZhouXY108 <luquanlion@outlook.com>
|
* @author ZhouXY108 <luquanlion@outlook.com>
|
||||||
* @see PagingParams
|
* @see PagingParams
|
||||||
@@ -44,10 +100,23 @@ import xyz.zhouxy.plusone.commons.util.StringTools;
|
|||||||
*/
|
*/
|
||||||
public class PagingAndSortingQueryParams {
|
public class PagingAndSortingQueryParams {
|
||||||
|
|
||||||
|
private final PagingParamsBuilder pagingParamsBuilder;
|
||||||
|
|
||||||
private Integer size;
|
private Integer size;
|
||||||
private Long pageNum;
|
private Long pageNum;
|
||||||
private List<String> orderBy;
|
private List<String> orderBy;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建一个 {@code PagingAndSortingQueryParams} 实例
|
||||||
|
*
|
||||||
|
* @param pagingParamsBuilder
|
||||||
|
* 分页参数构造器。
|
||||||
|
* 通过 {@link #pagingParamsBuilder(int, int, Map)} 创建,同一场景下只需要共享同一个实例。
|
||||||
|
*/
|
||||||
|
public PagingAndSortingQueryParams(PagingParamsBuilder pagingParamsBuilder) {
|
||||||
|
this.pagingParamsBuilder = pagingParamsBuilder;
|
||||||
|
}
|
||||||
|
|
||||||
// Setters
|
// 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) {
|
int defaultSize, int maxSize, Map<String, String> sortableProperties) {
|
||||||
return new PagingParamsBuilder(defaultSize, maxSize, 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;
|
private final String sqlSnippet;
|
||||||
|
|
||||||
SortableProperty(String propertyName, String columnName, String orderType) {
|
private SortableProperty(String propertyName, String columnName, String orderType) {
|
||||||
this.propertyName = propertyName;
|
this.propertyName = propertyName;
|
||||||
this.columnName = columnName;
|
this.columnName = columnName;
|
||||||
checkArgument("ASC".equalsIgnoreCase(orderType) || "DESC".equalsIgnoreCase(orderType));
|
checkArgument("ASC".equalsIgnoreCase(orderType) || "DESC".equalsIgnoreCase(orderType));
|
||||||
|
|||||||
@@ -19,7 +19,40 @@ package xyz.zhouxy.plusone.commons.model.dto;
|
|||||||
import javax.annotation.Nullable;
|
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 <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
|
||||||
|
* </pre>
|
||||||
|
* 见 <a href="http://zhouxy.xyz:3000/plusone/plusone-commons/issues/22">issue#22</a>。
|
||||||
*
|
*
|
||||||
* @author ZhouXY108 <luquanlion@outlook.com>
|
* @author ZhouXY108 <luquanlion@outlook.com>
|
||||||
* @since 1.0.0
|
* @since 1.0.0
|
||||||
|
|||||||
@@ -52,13 +52,11 @@
|
|||||||
* {@link UnifiedResponse} 对返回给前端的数据进行封装,包含 code、message、data。
|
* {@link UnifiedResponse} 对返回给前端的数据进行封装,包含 code、message、data。
|
||||||
*
|
*
|
||||||
* <p>
|
* <p>
|
||||||
* 可使用 {@link UnifiedResponses} 快速构建 {@link UnifiedResponse} 对象。
|
* {@link UnifiedResponses} 用于快速构建 {@link UnifiedResponse} 对象,默认的成功代码为 {@code 2000000}。
|
||||||
* {@link UnifiedResponses} 默认的成功代码为 "2000000",
|
*
|
||||||
* 用户按测试类
|
* <p>
|
||||||
* <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} 实现自己的工厂类,
|
||||||
* 中所示范的,继承 {@link UnifiedResponses} 实现自己的工厂类,
|
* 自定义 SUCCESS_CODE 和 DEFAULT_SUCCESS_MSG,以及工厂方法。
|
||||||
* 自定义 SUCCESS_CODE 和 DEFAULT_SUCCESS_MSG 和工厂方法。
|
|
||||||
* 见 <a href="http://zhouxy.xyz:3000/plusone/plusone-commons/issues/22">issue#22</a>。
|
|
||||||
*
|
*
|
||||||
* @author ZhouXY108 <luquanlion@outlook.com>
|
* @author ZhouXY108 <luquanlion@outlook.com>
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package xyz.zhouxy.plusone.commons.base;
|
package xyz.zhouxy.plusone.commons.util;
|
||||||
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
@@ -17,7 +17,8 @@
|
|||||||
/**
|
/**
|
||||||
* <h2>工具类</h2>
|
* <h2>工具类</h2>
|
||||||
* <p>
|
* <p>
|
||||||
* 包含树构建器({@link TreeBuilder})、断言工具({@link AssertTools})、ID 生成器({@link IdGenerator})及其它实用工具类。
|
* 包含树构建器({@link TreeBuilder})、断言工具({@link AssertTools})、
|
||||||
|
* ID 生成器({@link IdGenerator})及其它实用工具类。
|
||||||
*
|
*
|
||||||
* @author ZhouXY108 <luquanlion@outlook.com>
|
* @author ZhouXY108 <luquanlion@outlook.com>
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -234,6 +234,10 @@ class AccountQueryParams extends PagingAndSortingQueryParams {
|
|||||||
private static final PagingParamsBuilder PAGING_PARAMS_BUILDER = PagingAndSortingQueryParams
|
private static final PagingParamsBuilder PAGING_PARAMS_BUILDER = PagingAndSortingQueryParams
|
||||||
.pagingParamsBuilder(20, 100, PROPERTY_COLUMN_MAP);
|
.pagingParamsBuilder(20, 100, PROPERTY_COLUMN_MAP);
|
||||||
|
|
||||||
|
public AccountQueryParams() {
|
||||||
|
super(PAGING_PARAMS_BUILDER);
|
||||||
|
}
|
||||||
|
|
||||||
private @Getter @Setter Long id;
|
private @Getter @Setter Long id;
|
||||||
private @Getter @Setter String username;
|
private @Getter @Setter String username;
|
||||||
private @Getter @Setter String email;
|
private @Getter @Setter String email;
|
||||||
@@ -248,10 +252,6 @@ class AccountQueryParams extends PagingAndSortingQueryParams {
|
|||||||
}
|
}
|
||||||
return this.createTimeEnd.plusDays(1);
|
return this.createTimeEnd.plusDays(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
public PagingParams buildPagingParams() {
|
|
||||||
return PAGING_PARAMS_BUILDER.buildPagingParams(this);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package xyz.zhouxy.plusone.commons.base;
|
package xyz.zhouxy.plusone.commons.util;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
@@ -36,7 +36,10 @@ import com.google.common.collect.Lists;
|
|||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
|
|
||||||
import cn.hutool.core.util.ObjectUtil;
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.Getter;
|
||||||
import lombok.ToString;
|
import lombok.ToString;
|
||||||
|
|
||||||
@SuppressWarnings("null")
|
@SuppressWarnings("null")
|
||||||
@@ -61,7 +64,7 @@ class TreeBuilderTests {
|
|||||||
|
|
||||||
private final TreeBuilder<Menu, MenuList, String> treeBuilder = new TreeBuilder<>(
|
private final TreeBuilder<Menu, MenuList, String> treeBuilder = new TreeBuilder<>(
|
||||||
Menu::getMenuCode,
|
Menu::getMenuCode,
|
||||||
menu -> Optional.ofNullable(menu.parentMenuCode),
|
menu -> Optional.ofNullable(menu.getParentMenuCode()),
|
||||||
MenuList::addChild,
|
MenuList::addChild,
|
||||||
Comparator.comparing(Menu::getOrderNum));
|
Comparator.comparing(Menu::getOrderNum));
|
||||||
|
|
||||||
@@ -134,45 +137,23 @@ class TreeBuilderTests {
|
|||||||
((MenuList) menuMap.get("C1")).children);
|
((MenuList) menuMap.get("C1")).children);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ToString
|
@AllArgsConstructor(access = AccessLevel.PROTECTED)
|
||||||
@EqualsAndHashCode
|
@EqualsAndHashCode
|
||||||
|
@ToString
|
||||||
private abstract static class Menu implements Serializable {
|
private abstract static class Menu implements Serializable {
|
||||||
protected final String parentMenuCode;
|
protected final @Getter String parentMenuCode;
|
||||||
protected final String menuCode;
|
protected final @Getter String menuCode;
|
||||||
protected final String title;
|
protected final @Getter String title;
|
||||||
protected final int orderNum;
|
protected final @Getter 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final long serialVersionUID = 20240917181424L;
|
private static final long serialVersionUID = 20240917181424L;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ToString(callSuper = true)
|
|
||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@ToString(callSuper = true)
|
||||||
private static final class MenuItem extends Menu {
|
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) {
|
private MenuItem(String parentMenuCode, String menuCode, String title, String url, int orderNum) {
|
||||||
super(parentMenuCode, menuCode, title, orderNum);
|
super(parentMenuCode, menuCode, title, orderNum);
|
||||||
@@ -187,10 +168,6 @@ class TreeBuilderTests {
|
|||||||
return new MenuItem(null, menuCode, title, url, orderNum);
|
return new MenuItem(null, menuCode, title, url, orderNum);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getUrl() {
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final long serialVersionUID = 20240917181910L;
|
private static final long serialVersionUID = 20240917181910L;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
package xyz.zhouxy.plusone.commons.util;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||||
|
|||||||
@@ -52,6 +52,8 @@
|
|||||||
<jasypt.version>1.9.3</jasypt.version>
|
<jasypt.version>1.9.3</jasypt.version>
|
||||||
<jbcrypt.version>0.4</jbcrypt.version>
|
<jbcrypt.version>0.4</jbcrypt.version>
|
||||||
|
|
||||||
|
<minio.version>8.6.0</minio.version>
|
||||||
|
|
||||||
<lombok.version>1.18.36</lombok.version>
|
<lombok.version>1.18.36</lombok.version>
|
||||||
<hutool.version>5.8.37</hutool.version>
|
<hutool.version>5.8.37</hutool.version>
|
||||||
|
|
||||||
@@ -146,22 +148,27 @@
|
|||||||
<version>${gson.version}</version>
|
<version>${gson.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- MapStruct -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.mapstruct</groupId>
|
<groupId>org.mapstruct</groupId>
|
||||||
<artifactId>mapstruct</artifactId>
|
<artifactId>mapstruct</artifactId>
|
||||||
<version>${mapstruct.version}</version>
|
<version>${mapstruct.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- H2 测试数据库 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.h2database</groupId>
|
<groupId>com.h2database</groupId>
|
||||||
<artifactId>h2</artifactId>
|
<artifactId>h2</artifactId>
|
||||||
<version>${h2.version}</version>
|
<version>${h2.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- MyBatis -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.mybatis</groupId>
|
<groupId>org.mybatis</groupId>
|
||||||
<artifactId>mybatis</artifactId>
|
<artifactId>mybatis</artifactId>
|
||||||
<version>${mybatis.version}</version>
|
<version>${mybatis.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- Query DSL -->
|
<!-- Query DSL -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.querydsl</groupId>
|
<groupId>com.querydsl</groupId>
|
||||||
@@ -169,6 +176,7 @@
|
|||||||
<version>${querydsl.version}</version>
|
<version>${querydsl.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Byte Buddy 字节码工具 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>net.bytebuddy</groupId>
|
<groupId>net.bytebuddy</groupId>
|
||||||
<artifactId>byte-buddy</artifactId>
|
<artifactId>byte-buddy</artifactId>
|
||||||
@@ -188,22 +196,34 @@
|
|||||||
<version>${poi.version}</version>
|
<version>${poi.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- JWT -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.auth0</groupId>
|
<groupId>com.auth0</groupId>
|
||||||
<artifactId>java-jwt</artifactId>
|
<artifactId>java-jwt</artifactId>
|
||||||
<version>${java-jwt.version}</version>
|
<version>${java-jwt.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- jasypt 加密解密 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.jasypt</groupId>
|
<groupId>org.jasypt</groupId>
|
||||||
<artifactId>jasypt</artifactId>
|
<artifactId>jasypt</artifactId>
|
||||||
<version>${jasypt.version}</version>
|
<version>${jasypt.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Bcrypt是一种用于密码哈希的加密算法,基于Blowfish算法 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.mindrot</groupId>
|
<groupId>org.mindrot</groupId>
|
||||||
<artifactId>jbcrypt</artifactId>
|
<artifactId>jbcrypt</artifactId>
|
||||||
<version>${jbcrypt.version}</version>
|
<version>${jbcrypt.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- MinIO -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.minio</groupId>
|
||||||
|
<artifactId>minio</artifactId>
|
||||||
|
<version>${minio.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.projectlombok</groupId>
|
<groupId>org.projectlombok</groupId>
|
||||||
<artifactId>lombok</artifactId>
|
<artifactId>lombok</artifactId>
|
||||||
|
|||||||
Reference in New Issue
Block a user