Compare commits
1 Commits
dev
...
3980001654
| Author | SHA1 | Date | |
|---|---|---|---|
| 3980001654 |
60
NOTICE
60
NOTICE
@@ -1,60 +0,0 @@
|
||||
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,31 +1,221 @@
|
||||
# Plusone Commons
|
||||
## 1. 简介
|
||||
Plusone Commons 是一个 Java 工具类库,提供了一系列实用的类和方法,用于简化开发。
|
||||
## 一、annotation - 注解
|
||||
### 1. StaticFactoryMethod
|
||||
标识静态工厂方法。 *《Effective Java》* 的 **Item1** 建议考虑用静态工厂方法替换构造器, 因而考虑有一个注解可以标记一下静态工厂方法,以和其它方法进行区分。
|
||||
|
||||
一开始是为了补充日常开发中,guava 认为不需要,而我又用得上的工具,所以需要结合 guava 使用。后面也包含了一些从日常工作与学习中抽离出来可以通用的东西。
|
||||
### 2. ReaderMethod 和 WriterMethod
|
||||
分别标识读方法(如 getter)或写方法(如 setter)。
|
||||
|
||||
Plusone Commons 的工具类不追求“大而全”,而是只提供相对需要的部分功能。
|
||||
最早是写了一个集合类,为了方便判断使用读写锁时,哪些情况下使用读锁,哪些情况下使用写锁。
|
||||
|
||||
> 未来一些不够“通用”的组件会迁移到更合适的模块中。
|
||||
### 3. UnsupportedOperation
|
||||
标识该方法不被支持或没有实现,将抛出 `UnsupportedOperationException`。 为了方便在使用时,不需要点进源码,就能知道该方法没有实现。
|
||||
|
||||
## 2. 安装
|
||||
项目基于 OpenJDK 8 和 maven 构建。
|
||||
### 4. Virtual
|
||||
Java 非 final 的实例方法,对应 C++/C# 中的虚方法,允许被子类覆写。 Virtual 注解旨在设计父类时,强调该方法父类虽然有默认实现,但子类可以根据自己的需要覆写。
|
||||
|
||||
项目目前暂未发布到 maven 中央仓库,使用时需克隆代码到本地,并安装到本地仓库,然后才能在项目中引入依赖。
|
||||
### 5. ValueObject
|
||||
标记一个类,表示其作为值对象,区别于 Entity。
|
||||
|
||||
## 3. 功能
|
||||
详细功能说明请查阅文档:
|
||||
## 二、base - 基础组件
|
||||
### 1. Ref
|
||||
`Ref` 包装了一个值,表示对该值的应用。
|
||||
|
||||
+ [文档地址](/plusone-commons/docs)
|
||||
灵感来自于 C# 的 ref 参数修饰符。C# 允许通过以下方式,将值返回给调用端:
|
||||
```C#
|
||||
void Method(ref int refArgument)
|
||||
{
|
||||
refArgument = refArgument + 44;
|
||||
}
|
||||
|
||||
## 4. 代码仓库
|
||||
项目仓库一共建了三个:
|
||||
int number = 1;
|
||||
Method(ref number);
|
||||
Console.WriteLine(number); // Output: 45
|
||||
```
|
||||
`Ref` 使 Java 可以达到类似的效果,如:
|
||||
```java
|
||||
void method(Ref<Integer> refArgument) {
|
||||
refArgument.transformValue(i -> i + 44);
|
||||
}
|
||||
|
||||
+ [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);
|
||||
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 和 gitee 上通过 issue 反馈使用过程中发现的问题和建议,也接受善意的 PR。
|
||||
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`,便于在需要的地方对这些接口的实现进行处理。
|
||||
|
||||
## 5. 许可
|
||||
项目使用 [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0) 开源,相关声明请参阅 `NOTICE` 文件。
|
||||
## 三、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](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`)及其它实用工具类。
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
"aliyun",
|
||||
"baomidou",
|
||||
"Batis",
|
||||
"buildmetadata",
|
||||
"Consolas",
|
||||
"cspell",
|
||||
"databind",
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
## 1. 注解
|
||||
|注解|说明|
|
||||
|--|--|
|
||||
| **StaticFactoryMethod** | **标识静态工厂方法**。 *《Effective Java》* 的 **Item1** 建议考虑用静态工厂方法替换构造器, 因而考虑有一个注解可以标记一下静态工厂方法,以和其它方法进行区分。|
|
||||
| **ReaderMethod** / **WriterMethod** | **分别标识读方法(如 getter)或写方法(如 setter)**。<br>*最早是写了一个集合类,为了方便判断使用读写锁时,哪些情况下使用读锁,哪些情况下使用写锁。*|
|
||||
| **UnsupportedOperation** | **标识该方法不被支持或没有实现**,将抛出 `UnsupportedOperationException`。 为了方便在使用时,不需要点进源码,就能知道该方法没有实现。|
|
||||
| **Virtual** | Java 非 final 的实例方法,对应 C++/C# 中的虚方法,允许被子类覆写。 **Virtual 注解旨在设计父类时,强调该方法父类虽然有默认实现,但子类可以根据自己的需要覆写**。|
|
||||
| **ValueObject** | 标记一个类,表示其作为**值对象**,区别于 Entity。|
|
||||
@@ -1,39 +0,0 @@
|
||||
## 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);
|
||||
```
|
||||
@@ -1,118 +0,0 @@
|
||||
## 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();
|
||||
```
|
||||
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
## 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) |
|
||||
@@ -1,100 +0,0 @@
|
||||
## 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)
|
||||
@@ -1,24 +0,0 @@
|
||||
## 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`
|
||||
@@ -1,275 +0,0 @@
|
||||
## 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 工具
|
||||
@@ -1,9 +0,0 @@
|
||||
## 8. 其它内容
|
||||
|
||||
### 8.1. IWithCode
|
||||
|
||||
对于类似枚举这样的类型,通常需要设置固定的码值表示对应的含义。 可实现 `IWithCode`、`IWithIntCode`、`IWithLongCode`,便于在需要的地方对这些接口的实现进行处理。
|
||||
|
||||
### 8.2. 正则常量
|
||||
|
||||
`RegexConsts` 包含常见正则表达式;`PatternConsts` 包含对应的 `Pattern` 对象
|
||||
@@ -13,7 +13,7 @@
|
||||
<artifactId>plusone-commons</artifactId>
|
||||
|
||||
<description>
|
||||
Plusone Commons 是一个 Java 工具类库,提供了一系列实用的类和方法,用于简化开发。结合 guava 使用。
|
||||
常见工具集,结合 guava 使用。
|
||||
</description>
|
||||
|
||||
<properties>
|
||||
|
||||
@@ -15,7 +15,44 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* 注解
|
||||
* <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>
|
||||
*/
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package xyz.zhouxy.plusone.commons.util;
|
||||
package xyz.zhouxy.plusone.commons.base;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.function.Consumer;
|
||||
@@ -15,7 +15,56 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* 基础内容
|
||||
* <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>
|
||||
*/
|
||||
@@ -24,5 +73,4 @@
|
||||
package xyz.zhouxy.plusone.commons.base;
|
||||
|
||||
import javax.annotation.ParametersAreNonnullByDefault;
|
||||
|
||||
import javax.annotation.CheckReturnValue;
|
||||
|
||||
@@ -1,254 +0,0 @@
|
||||
/*
|
||||
* 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,7 +15,12 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* 集合相关工具
|
||||
* <h2>集合</h2>
|
||||
*
|
||||
* <h3>
|
||||
* 1. {@link CollectionTools}
|
||||
* </h3>
|
||||
* 集合工具类
|
||||
*
|
||||
* @author ZhouXY108 <luquanlion@outlook.com>
|
||||
*/
|
||||
|
||||
@@ -15,8 +15,115 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* 包含常见的业务异常与系统异常,以及异常相关的工具
|
||||
* <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>
|
||||
*/
|
||||
package xyz.zhouxy.plusone.commons.exception;
|
||||
|
||||
import xyz.zhouxy.plusone.commons.exception.business.*;
|
||||
import xyz.zhouxy.plusone.commons.exception.system.*;
|
||||
|
||||
@@ -1,275 +0,0 @@
|
||||
/*
|
||||
* 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.model;
|
||||
|
||||
|
||||
import static xyz.zhouxy.plusone.commons.util.AssertTools.checkArgument;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Objects;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import com.google.common.base.Splitter;
|
||||
|
||||
import xyz.zhouxy.plusone.commons.util.StringTools;
|
||||
|
||||
/**
|
||||
* SemVer 语义版本号
|
||||
*
|
||||
* @author ZhouXY108 <luquanlion@outlook.com>
|
||||
* @since 1.1.0
|
||||
*
|
||||
* @see <a href="https://semver.org/">Semantic Versioning 2.0.0</a>
|
||||
*/
|
||||
public class SemVer implements Comparable<SemVer>, Serializable {
|
||||
private static final long serialVersionUID = 458265121025514002L;
|
||||
|
||||
private final String value;
|
||||
|
||||
private final int[] versionNumbers;
|
||||
@Nullable
|
||||
private final String preReleaseVersion;
|
||||
@Nullable
|
||||
private final String buildMetadata;
|
||||
|
||||
private static final String VERSION_NUMBERS = "(?<numbers>(?<major>0|[1-9]\\d*)\\.(?<minor>0|[1-9]\\d*)\\.(?<patch>0|[1-9]\\d*)(\\.(0|[1-9]\\d*)){0,2})";
|
||||
private static final String PRE_RELEASE_VERSION = "(?:-(?<prerelease>(?:0|[1-9]\\d{0,41}|\\d{0,18}[a-zA-Z-][0-9a-zA-Z-]{0,18})(?:\\.(?:0|[1-9]\\d{0,41}|\\d{0,18}[a-zA-Z-][0-9a-zA-Z-]{0,18})){0,18}))?";
|
||||
private static final String BUILD_METADATA = "(?:\\+(?<buildmetadata>[0-9a-zA-Z-]{1,18}(?:\\.[0-9a-zA-Z-]{1,18}){0,18}))?";
|
||||
|
||||
private static final Pattern PATTERN = Pattern.compile(
|
||||
"^" + VERSION_NUMBERS + PRE_RELEASE_VERSION + BUILD_METADATA + "$");
|
||||
|
||||
/**
|
||||
* 创建语义化版本号的值对象
|
||||
*
|
||||
* @param value 字符串值
|
||||
* @param versionNumbers 主版本号、次版本号、修订号
|
||||
* @param preReleaseVersion 先行版本号
|
||||
* @param buildMetadata 版本编译信息
|
||||
*/
|
||||
private SemVer(String value,
|
||||
int[] versionNumbers,
|
||||
@Nullable String preReleaseVersion,
|
||||
@Nullable String buildMetadata) {
|
||||
this.value = value;
|
||||
this.versionNumbers = versionNumbers;
|
||||
this.preReleaseVersion = preReleaseVersion;
|
||||
this.buildMetadata = buildMetadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 SemVer 对象
|
||||
*
|
||||
* @param value 语义化版本号
|
||||
* @return SemVer 对象
|
||||
*/
|
||||
public static SemVer of(final String value) {
|
||||
checkArgument(StringTools.isNotBlank(value), "版本号不能为空");
|
||||
final Matcher matcher = PATTERN.matcher(value);
|
||||
checkArgument(matcher.matches(), "版本号格式错误");
|
||||
// 数字版本部分
|
||||
final String versionNumbersPart = matcher.group("numbers");
|
||||
// 先行版本号部分
|
||||
final String preReleaseVersionPart = matcher.group("prerelease");
|
||||
// 版本编译信息部分
|
||||
final String buildMetadataPart = matcher.group("buildmetadata");
|
||||
|
||||
final int[] versionNumbers = Splitter.on('.')
|
||||
.splitToStream(versionNumbersPart)
|
||||
// 必须都是数字
|
||||
.mapToInt(Integer::parseInt)
|
||||
.toArray();
|
||||
return new SemVer(value, versionNumbers, preReleaseVersionPart, buildMetadataPart);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取主版本号
|
||||
*
|
||||
* @return 主版本号
|
||||
*/
|
||||
public int getMajor() {
|
||||
return this.versionNumbers[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取次版本号
|
||||
*
|
||||
* @return 次版本号
|
||||
*/
|
||||
public int getMinor() {
|
||||
return this.versionNumbers[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取修订号
|
||||
*
|
||||
* @return 修订号
|
||||
*/
|
||||
public int getPatch() {
|
||||
return this.versionNumbers[2];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取先行版本号
|
||||
*
|
||||
* @return 先行版本号
|
||||
*/
|
||||
@Nullable
|
||||
public String getPreReleaseVersion() {
|
||||
return this.preReleaseVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取版本编译信息
|
||||
*
|
||||
* @return 版本编译信息
|
||||
*/
|
||||
@Nullable
|
||||
public String getBuildMetadata() {
|
||||
return buildMetadata;
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public int compareTo(@SuppressWarnings("null") SemVer that) {
|
||||
if (this == that) {
|
||||
return 0;
|
||||
}
|
||||
int result = compareVersionNumbers(that);
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
return comparePreReleaseVersion(that);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字符串值
|
||||
*
|
||||
* @return 版本字符串
|
||||
*/
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(value);
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (this == obj)
|
||||
return true;
|
||||
if (!(obj instanceof SemVer))
|
||||
return false;
|
||||
SemVer other = (SemVer) obj;
|
||||
return Objects.equals(value, other.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 SemVer 的字符串表示。如 {@code v1.2.3-alpha.1+build.1234}
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
return 'v' + value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 比较主版本号、次版本号、修订号
|
||||
*/
|
||||
private int compareVersionNumbers(SemVer that) {
|
||||
final int minLength = Integer.min(this.versionNumbers.length, that.versionNumbers.length);
|
||||
|
||||
for (int i = 0; i < minLength; i++) {
|
||||
final int currentVersionNumberOfThis = this.versionNumbers[i];
|
||||
final int currentVersionNumberOfThat = that.versionNumbers[i];
|
||||
if (currentVersionNumberOfThis != currentVersionNumberOfThat) {
|
||||
return currentVersionNumberOfThis - currentVersionNumberOfThat;
|
||||
}
|
||||
}
|
||||
return this.versionNumbers.length - that.versionNumbers.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 比较先行版本号
|
||||
*/
|
||||
private int comparePreReleaseVersion(SemVer that) {
|
||||
int thisWithoutPreReleaseVersionFlag = bool2Int(this.preReleaseVersion == null);
|
||||
int thatWithoutPreReleaseVersionFlag = bool2Int(that.preReleaseVersion == null);
|
||||
if (isTrue(thisWithoutPreReleaseVersionFlag | thatWithoutPreReleaseVersionFlag)) {
|
||||
return thisWithoutPreReleaseVersionFlag - thatWithoutPreReleaseVersionFlag;
|
||||
}
|
||||
|
||||
Splitter splitter = Splitter.on('.');
|
||||
|
||||
final String[] preReleaseVersionOfThis = splitter
|
||||
.splitToStream(this.preReleaseVersion) // NOSONAR
|
||||
.toArray(String[]::new);
|
||||
final String[] preReleaseVersionOfThat = splitter
|
||||
.splitToStream(that.preReleaseVersion) // NOSONAR
|
||||
.toArray(String[]::new);
|
||||
final int minLength = Integer.min(preReleaseVersionOfThis.length, preReleaseVersionOfThat.length);
|
||||
for (int i = 0; i < minLength; i++) {
|
||||
int r = comparePartOfPreReleaseVersion(preReleaseVersionOfThis[i], preReleaseVersionOfThat[i]);
|
||||
if (r != 0) {
|
||||
return r;
|
||||
}
|
||||
}
|
||||
return preReleaseVersionOfThis.length - preReleaseVersionOfThat.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 比较先行版本号的组成部分
|
||||
*/
|
||||
private static int comparePartOfPreReleaseVersion(String p1, String p2) {
|
||||
boolean p1IsNumber = isAllDigits(p1);
|
||||
boolean p2IsNumber = isAllDigits(p2);
|
||||
|
||||
if (p1IsNumber) {
|
||||
return p2IsNumber
|
||||
? Integer.parseInt(p1) - Integer.parseInt(p2) // 都是数字
|
||||
: -1; // p1 是数字,p2 是字符串
|
||||
}
|
||||
// 如果 p1 是字符串,p2 是数字,则返回 1(字符串优先于纯数字)
|
||||
return p2IsNumber ? 1 : p1.compareTo(p2);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断字符串是否全为数字
|
||||
*/
|
||||
private static boolean isAllDigits(String str) {
|
||||
for (int i = 0; i < str.length(); i++) {
|
||||
char c = str.charAt(i);
|
||||
if (c < '0' || c > '9') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int bool2Int(boolean expression) {
|
||||
return expression ? 1 : 0;
|
||||
}
|
||||
|
||||
private static boolean isTrue(int b) {
|
||||
return b != 0;
|
||||
}
|
||||
}
|
||||
@@ -35,64 +35,8 @@ import xyz.zhouxy.plusone.commons.util.StringTools;
|
||||
* 分页排序查询参数
|
||||
*
|
||||
* <p>
|
||||
* 包含三个主要的属性:
|
||||
* <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>
|
||||
* 根据传入的 {@code size} 和 {@code pageNum},
|
||||
* 提供 {@code getOffset} 方法计算 SQL 语句中 {@code offset} 的值。
|
||||
*
|
||||
* @author ZhouXY108 <luquanlion@outlook.com>
|
||||
* @see PagingParams
|
||||
@@ -100,23 +44,10 @@ 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
|
||||
|
||||
/**
|
||||
@@ -157,31 +88,11 @@ public class PagingAndSortingQueryParams {
|
||||
+ "]";
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个分页参数构造器
|
||||
*
|
||||
* @param defaultSize 默认每页大小
|
||||
* @param maxSize 最大每页大小
|
||||
* @param sortableProperties
|
||||
* 可排序属性。
|
||||
* key 是供前端指定用于排序的属性名,value 是对应数据库中的字段名。
|
||||
* 只有在此白名单中的属性名才允许用于排序。
|
||||
* @return 分页参数构造器
|
||||
*/
|
||||
public static PagingParamsBuilder pagingParamsBuilder(
|
||||
protected 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 可排序属性
|
||||
*/
|
||||
@@ -192,7 +103,7 @@ public class PagingAndSortingQueryParams {
|
||||
|
||||
private final String sqlSnippet;
|
||||
|
||||
private SortableProperty(String propertyName, String columnName, String orderType) {
|
||||
SortableProperty(String propertyName, String columnName, String orderType) {
|
||||
this.propertyName = propertyName;
|
||||
this.columnName = columnName;
|
||||
checkArgument("ASC".equalsIgnoreCase(orderType) || "DESC".equalsIgnoreCase(orderType));
|
||||
|
||||
@@ -19,40 +19,7 @@ package xyz.zhouxy.plusone.commons.model.dto;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* {@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>。
|
||||
* UnifiedResponse 工厂
|
||||
*
|
||||
* @author ZhouXY108 <luquanlion@outlook.com>
|
||||
* @since 1.0.0
|
||||
|
||||
@@ -52,11 +52,13 @@
|
||||
* {@link UnifiedResponse} 对返回给前端的数据进行封装,包含 code、message、data。
|
||||
*
|
||||
* <p>
|
||||
* {@link UnifiedResponses} 用于快速构建 {@link UnifiedResponse} 对象,默认的成功代码为 {@code 2000000}。
|
||||
*
|
||||
* <p>
|
||||
* 用户可以继承 {@link UnifiedResponses} 实现自己的工厂类,
|
||||
* 自定义 SUCCESS_CODE 和 DEFAULT_SUCCESS_MSG,以及工厂方法。
|
||||
* 可使用 {@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>。
|
||||
*
|
||||
* @author ZhouXY108 <luquanlion@outlook.com>
|
||||
*/
|
||||
|
||||
@@ -27,11 +27,7 @@ import javax.annotation.Nullable;
|
||||
import xyz.zhouxy.plusone.commons.exception.system.NoAvailableMacFoundException;
|
||||
|
||||
/**
|
||||
* 修改版雪花 ID 生成器
|
||||
*
|
||||
* <p>
|
||||
* 来自 Seata (https://seata.apache.org) 的 {@code org.apache.seata.common.util.IdWorker}
|
||||
*
|
||||
* Seata 提供的修改版雪花ID。
|
||||
* <p>
|
||||
* 大体思路为:
|
||||
* <ol>
|
||||
@@ -47,6 +43,7 @@ import xyz.zhouxy.plusone.commons.exception.system.NoAvailableMacFoundException;
|
||||
* <li><a href="https://juejin.cn/post/7264387737276203065">在开源项目中看到一个改良版的雪花算法,现在它是你的了。</a></li>
|
||||
* <li><a href="https://juejin.cn/post/7265516484029743138">关于若干读者,阅读“改良版雪花算法”后提出的几个共性问题的回复。</a></li>
|
||||
* </ul>
|
||||
*
|
||||
*/
|
||||
public class IdWorker {
|
||||
|
||||
|
||||
@@ -29,6 +29,8 @@ import com.google.common.collect.Range;
|
||||
|
||||
/**
|
||||
* 随机工具类
|
||||
* <p>
|
||||
* 建议调用方自行维护 Random 对象
|
||||
*
|
||||
* @author ZhouXY108 <luquanlion@outlook.com>
|
||||
*/
|
||||
@@ -47,41 +49,18 @@ public final class RandomTools {
|
||||
DEFAULT_SECURE_RANDOM = secureRandom;
|
||||
}
|
||||
|
||||
/**
|
||||
* 大写字母
|
||||
*/
|
||||
public static final String CAPITAL_LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
/**
|
||||
* 小写字母
|
||||
*/
|
||||
public static final String LOWERCASE_LETTERS = "abcdefghijklmnopqrstuvwxyz";
|
||||
/**
|
||||
* 数字
|
||||
*/
|
||||
public static final String NUMBERS = "0123456789";
|
||||
|
||||
/**
|
||||
* 默认的 {@code SecureRandom}
|
||||
*
|
||||
* @return 默认的 {@code SecureRandom}
|
||||
*/
|
||||
public static SecureRandom defaultSecureRandom() {
|
||||
return DEFAULT_SECURE_RANDOM;
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前线程的 {@code ThreadLocalRandom}
|
||||
*
|
||||
* @return 当前线程的 {@code ThreadLocalRandom}
|
||||
*/
|
||||
public static ThreadLocalRandom currentThreadLocalRandom() {
|
||||
return ThreadLocalRandom.current();
|
||||
}
|
||||
|
||||
// ================================
|
||||
// #region - randomStr
|
||||
// ================================
|
||||
|
||||
/**
|
||||
* 使用传入的随机数生成器,生成指定长度的字符串
|
||||
*
|
||||
@@ -99,26 +78,12 @@ public final class RandomTools {
|
||||
return randomStrInternal(random, sourceCharacters, length);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用当前线程的 {@code ThreadLocalRandom},生成指定长度的字符串
|
||||
*
|
||||
* @param sourceCharacters 字符池。字符串的字符将在数组中选,不为空
|
||||
* @param length 字符串长度
|
||||
* @return 随机字符串
|
||||
*/
|
||||
public static String randomStr(char[] sourceCharacters, int length) {
|
||||
checkArgumentNotNull(sourceCharacters, "Source characters cannot be null.");
|
||||
checkArgument(length >= 0, "The length should be greater than or equal to zero.");
|
||||
return randomStrInternal(ThreadLocalRandom.current(), sourceCharacters, length);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用默认的 {@code SecureRandom},生成指定长度的字符串
|
||||
*
|
||||
* @param sourceCharacters 字符池。字符串的字符将在数组中选,不为空
|
||||
* @param length 字符串长度
|
||||
* @return 随机字符串
|
||||
*/
|
||||
public static String secureRandomStr(char[] sourceCharacters, int length) {
|
||||
checkArgumentNotNull(sourceCharacters, "Source characters cannot be null.");
|
||||
checkArgument(length >= 0, "The length should be greater than or equal to zero.");
|
||||
@@ -142,104 +107,46 @@ public final class RandomTools {
|
||||
return randomStrInternal(random, sourceCharacters, length);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用当前线程的 {@code ThreadLocalRandom},生成指定长度的字符串
|
||||
*
|
||||
* @param sourceCharacters 字符池。字符串的字符将在数组中选,不为空
|
||||
* @param length 字符串长度
|
||||
* @return 随机字符串
|
||||
*/
|
||||
public static String randomStr(String sourceCharacters, int length) {
|
||||
checkArgumentNotNull(sourceCharacters, "Source characters cannot be null.");
|
||||
checkArgument(length >= 0, "The length should be greater than or equal to zero.");
|
||||
return randomStrInternal(ThreadLocalRandom.current(), sourceCharacters, length);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用默认的 {@code SecureRandom},生成指定长度的字符串
|
||||
*
|
||||
* @param sourceCharacters 字符池。字符串的字符将在数组中选,不为空
|
||||
* @param length 字符串长度
|
||||
* @return 随机字符串
|
||||
*/
|
||||
public static String secureRandomStr(String sourceCharacters, int length) {
|
||||
checkArgumentNotNull(sourceCharacters, "Source characters cannot be null.");
|
||||
checkArgument(length >= 0, "The length should be greater than or equal to zero.");
|
||||
return randomStrInternal(DEFAULT_SECURE_RANDOM, sourceCharacters, length);
|
||||
}
|
||||
|
||||
// ================================
|
||||
// #endregion - randomStr
|
||||
// ================================
|
||||
|
||||
// ================================
|
||||
// #region - randomInt
|
||||
// ================================
|
||||
/**
|
||||
* 生成随机整数
|
||||
*
|
||||
* @param min 最小值(包含)
|
||||
* @param max 最大值(不包含)
|
||||
* @return 在区间 {@code [min, max)} 内的随机整数
|
||||
*/
|
||||
public static int randomInt(int min, int max) {
|
||||
return randomIntInternal(ThreadLocalRandom.current(), min, max);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用传入的随机数生成器,生成随机整数
|
||||
* 生成随机整数
|
||||
*
|
||||
* @param startInclusive 最小值(包含)
|
||||
* @param endExclusive 最大值(不包含)
|
||||
* @param min 最小值(包含)
|
||||
* @param max 最大值(不包含)
|
||||
* @return 在区间 {@code [min, max)} 内的随机整数
|
||||
*
|
||||
* @since 1.1.0
|
||||
*/
|
||||
public static int randomInt(Random random, int startInclusive, int endExclusive) {
|
||||
public static int randomInt(Random random, int min, int max) {
|
||||
checkArgumentNotNull(random, "Random cannot be null.");
|
||||
checkArgument(startInclusive < endExclusive, "Start value must be less than end value.");
|
||||
return randomIntInternal(random, startInclusive, endExclusive);
|
||||
return randomIntInternal(random, min, max);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用当前线程的 {@code ThreadLocalRandom},生成随机整数
|
||||
*
|
||||
* @param startInclusive 最小值(包含)
|
||||
* @param endExclusive 最大值(不包含)
|
||||
* @return 在区间 {@code [min, max)} 内的随机整数
|
||||
*
|
||||
* @since 1.1.0
|
||||
*/
|
||||
public static int randomInt(int startInclusive, int endExclusive) {
|
||||
checkArgument(startInclusive < endExclusive, "Start value must be less than end value.");
|
||||
return randomIntInternal(ThreadLocalRandom.current(), startInclusive, endExclusive);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用默认的 {@code SecureRandom},生成随机整数
|
||||
*
|
||||
* @param startInclusive 最小值(包含)
|
||||
* @param endExclusive 最大值(不包含)
|
||||
* @return 在区间 {@code [min, max)} 内的随机整数
|
||||
*
|
||||
* @since 1.1.0
|
||||
*/
|
||||
public static int secureRandomInt(int startInclusive, int endExclusive) {
|
||||
checkArgument(startInclusive < endExclusive, "Start value must be less than end value.");
|
||||
return randomIntInternal(DEFAULT_SECURE_RANDOM, startInclusive, endExclusive);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用传入的随机数生成器,生成随机整数
|
||||
* 生成随机整数
|
||||
*
|
||||
* @param range 整数区间
|
||||
* @return 在指定区间内的随机整数
|
||||
*
|
||||
* @since 1.1.0
|
||||
*/
|
||||
public static int randomInt(Random random, Range<Integer> range) {
|
||||
checkArgumentNotNull(random, "Random cannot be null.");
|
||||
checkArgumentNotNull(range, "Range cannot be null.");
|
||||
return randomIntInternal(random, range);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用当前线程的 {@code ThreadLocalRandom},生成随机整数
|
||||
*
|
||||
* @param range 整数区间
|
||||
* @return 在指定区间内的随机整数
|
||||
*
|
||||
* @since 1.1.0
|
||||
*/
|
||||
public static int randomInt(Range<Integer> range) {
|
||||
checkArgumentNotNull(range, "Range cannot be null.");
|
||||
@@ -247,26 +154,17 @@ public final class RandomTools {
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用默认的 {@code SecureRandom},生成随机整数
|
||||
* 生成随机整数
|
||||
*
|
||||
* @param range 整数区间
|
||||
* @return 在指定区间内的随机整数
|
||||
*
|
||||
* @since 1.1.0
|
||||
*/
|
||||
public static int secureRandomInt(Range<Integer> range) {
|
||||
public static int randomInt(Random random, Range<Integer> range) {
|
||||
checkArgumentNotNull(random, "Random cannot be null.");
|
||||
checkArgumentNotNull(range, "Range cannot be null.");
|
||||
return randomIntInternal(DEFAULT_SECURE_RANDOM, range);
|
||||
return randomIntInternal(random, range);
|
||||
}
|
||||
|
||||
// ================================
|
||||
// #endregion - randomInt
|
||||
// ================================
|
||||
|
||||
// ================================
|
||||
// #region - private methods
|
||||
// ================================
|
||||
|
||||
/**
|
||||
* 使用传入的随机数生成器,生成指定长度的字符串
|
||||
*
|
||||
@@ -310,18 +208,18 @@ public final class RandomTools {
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用传入的随机数生成器,生成随机整数
|
||||
* 生成随机整数
|
||||
*
|
||||
* @param startInclusive 最小值(包含)
|
||||
* @param endExclusive 最大值(不包含)
|
||||
* @param min 最小值(包含)
|
||||
* @param max 最大值(不包含)
|
||||
* @return 在区间 {@code [min, max)} 内的随机整数
|
||||
*/
|
||||
private static int randomIntInternal(Random random, int startInclusive, int endExclusive) {
|
||||
return random.nextInt(endExclusive - startInclusive) + startInclusive;
|
||||
private static int randomIntInternal(Random random, int min, int max) {
|
||||
return random.nextInt(max - min) + min;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用传入的随机数生成器,生成随机整数
|
||||
* 生成随机整数
|
||||
*
|
||||
* @param range 整数区间
|
||||
* @return 在指定区间内的随机整数
|
||||
@@ -334,10 +232,6 @@ public final class RandomTools {
|
||||
return random.nextInt(max - min) + min;
|
||||
}
|
||||
|
||||
// ================================
|
||||
// #endregion - private methods
|
||||
// ================================
|
||||
|
||||
private RandomTools() {
|
||||
throw new IllegalStateException("Utility class");
|
||||
}
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
/*
|
||||
* 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 xyz.zhouxy.plusone.commons.util.AssertTools.checkArgument;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.zip.Deflater;
|
||||
import java.util.zip.DeflaterOutputStream;
|
||||
import java.util.zip.Inflater;
|
||||
import java.util.zip.InflaterOutputStream;
|
||||
|
||||
import javax.annotation.CheckForNull;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* zip 工具类
|
||||
*
|
||||
* <p>
|
||||
* 提供最基础的数据压缩/解压方法
|
||||
*
|
||||
* @author ZhouXY108 <luquanlion@outlook.com>
|
||||
*
|
||||
* @see Deflater
|
||||
* @see Inflater
|
||||
*/
|
||||
public class ZipTools {
|
||||
|
||||
/**
|
||||
* 使用默认压缩级别压缩数据
|
||||
*
|
||||
* @param input 输入
|
||||
* @param level 压缩级别
|
||||
* @return 压缩后的数据
|
||||
*
|
||||
* @throws IOException 发生 I/O 错误时抛出
|
||||
*/
|
||||
public static byte[] zip(@Nullable byte[] input) throws IOException {
|
||||
return zipInternal(input, Deflater.DEFAULT_COMPRESSION);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用指定压缩级别压缩数据
|
||||
*
|
||||
* @param input 输入
|
||||
* @param level 压缩级别
|
||||
* @return 压缩后的数据
|
||||
*
|
||||
* @throws IOException 发生 I/O 错误时抛出
|
||||
*/
|
||||
public static byte[] zip(@Nullable byte[] input, int level) throws IOException {
|
||||
checkArgument((level >= 0 && level <= 9) || level == Deflater.DEFAULT_COMPRESSION,
|
||||
"invalid compression level");
|
||||
return zipInternal(input, level);
|
||||
}
|
||||
|
||||
@CheckForNull
|
||||
private static byte[] zipInternal(@Nullable byte[] input, int level) throws IOException {
|
||||
if (input == null) {
|
||||
return null;
|
||||
}
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
try (DeflaterOutputStream dos = new DeflaterOutputStream(out, new Deflater(level))) {
|
||||
dos.write(input);
|
||||
dos.finish();
|
||||
return out.toByteArray();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解压数据
|
||||
*
|
||||
* @param input 输入
|
||||
* @return 解压后的数据
|
||||
*
|
||||
* @throws IOException 发生 I/O 错误时抛出
|
||||
*/
|
||||
@CheckForNull
|
||||
public static byte[] unzip(@Nullable byte[] input) throws IOException {
|
||||
if (input == null) {
|
||||
return null;
|
||||
}
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
try (InflaterOutputStream dos = new InflaterOutputStream(out, new Inflater())) {
|
||||
dos.write(input);
|
||||
dos.finish();
|
||||
return out.toByteArray();
|
||||
}
|
||||
}
|
||||
|
||||
private ZipTools() {
|
||||
throw new IllegalStateException("Utility class");
|
||||
}
|
||||
}
|
||||
@@ -17,8 +17,7 @@
|
||||
/**
|
||||
* <h2>工具类</h2>
|
||||
* <p>
|
||||
* 包含树构建器({@link TreeBuilder})、断言工具({@link AssertTools})、
|
||||
* ID 生成器({@link IdGenerator})及其它实用工具类。
|
||||
* 包含树构建器({@link TreeBuilder})、断言工具({@link AssertTools})、ID 生成器({@link IdGenerator})及其它实用工具类。
|
||||
*
|
||||
* @author ZhouXY108 <luquanlion@outlook.com>
|
||||
*/
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package xyz.zhouxy.plusone.commons.util;
|
||||
package xyz.zhouxy.plusone.commons.base;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@@ -1,316 +0,0 @@
|
||||
/*
|
||||
* 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'");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,251 +0,0 @@
|
||||
/*
|
||||
* 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.model;
|
||||
|
||||
import static org.apache.commons.lang3.ObjectUtils.compare;
|
||||
import static org.junit.jupiter.api.Assertions.assertAll;
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
public class SemVerTests {
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {
|
||||
"25.10.17",
|
||||
"25.10.17.11",
|
||||
"25.10.17.11.38",
|
||||
"25.10.17-RC1",
|
||||
"25.10.17.11-RC1",
|
||||
"25.10.17.11.38-RC1",
|
||||
"25.10.17+build.a20251017.1",
|
||||
"25.10.17.11+build.a20251017.1",
|
||||
"25.10.17.11.38+build.a20251017.1",
|
||||
"25.10.17-RC1+build.a20251017.1",
|
||||
"25.10.17.11-RC1+build.a20251017.1",
|
||||
"25.10.17.11.38-RC1+build.a20251017.1",
|
||||
})
|
||||
void test_of_success(String value) {
|
||||
assertDoesNotThrow(() -> SemVer.of(value));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {
|
||||
"25",
|
||||
"25.10",
|
||||
"x.10.17",
|
||||
"25.x.17",
|
||||
"25.10.x",
|
||||
"25.10.17.11.38.20",
|
||||
"025.10.17",
|
||||
"25.010.17",
|
||||
"25.10.017",
|
||||
})
|
||||
void test_of_wrongValue(String value) {
|
||||
IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> SemVer.of(value));
|
||||
assertEquals("版本号格式错误", e.getMessage());
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {
|
||||
"25.1.1",
|
||||
"25.1.1-RC1",
|
||||
"25.1.1+build.a20250101.1",
|
||||
"25.1.1-RC1+build.a20250101.1",
|
||||
})
|
||||
void compareTo_Major(String version_25_x_x) { // NOSONAR sonarqube(java:S117)
|
||||
assertTrue(compare(SemVer.of(version_25_x_x), SemVer.of("24.12.30")) > 0);
|
||||
assertTrue(compare(SemVer.of(version_25_x_x), SemVer.of("24.12.30.1")) > 0);
|
||||
assertTrue(compare(SemVer.of(version_25_x_x), SemVer.of("24.12.30-RC2")) > 0);
|
||||
assertTrue(compare(SemVer.of(version_25_x_x), SemVer.of("24.12.30+build.z20241230.1")) > 0);
|
||||
assertTrue(compare(SemVer.of(version_25_x_x), SemVer.of("24.12.30-RC2+build.z20241230.1")) > 0);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {
|
||||
"25.10.1",
|
||||
"25.10.1-RC1",
|
||||
"25.10.1+build.a20251001.1",
|
||||
"25.10.1-RC1+build.a20251001.1",
|
||||
})
|
||||
void compareTo_Minor(String version_25_10_x) { // NOSONAR sonarqube(java:S117)
|
||||
assertTrue(compare(SemVer.of(version_25_10_x), SemVer.of("25.9.30")) > 0);
|
||||
assertTrue(compare(SemVer.of(version_25_10_x), SemVer.of("25.9.30.1")) > 0);
|
||||
assertTrue(compare(SemVer.of(version_25_10_x), SemVer.of("25.9.30-RC2")) > 0);
|
||||
assertTrue(compare(SemVer.of(version_25_10_x), SemVer.of("25.9.30+build.z20250930.1")) > 0);
|
||||
assertTrue(compare(SemVer.of(version_25_10_x), SemVer.of("25.9.30-RC2+build.z20250930.1")) > 0);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {
|
||||
"25.10.17",
|
||||
"25.10.17-RC1",
|
||||
"25.10.17+build.a20251017.1",
|
||||
"25.10.17-RC1+build.a20251017.1",
|
||||
})
|
||||
void compareTo_Patch(String version_25_10_17) { // NOSONAR sonarqube(java:S117)
|
||||
|
||||
assertTrue(compare(SemVer.of(version_25_10_17), SemVer.of("25.10.16")) > 0);
|
||||
assertTrue(compare(SemVer.of(version_25_10_17), SemVer.of("25.10.16.1")) > 0);
|
||||
assertTrue(compare(SemVer.of(version_25_10_17), SemVer.of("25.10.16-RC2")) > 0);
|
||||
assertTrue(compare(SemVer.of(version_25_10_17), SemVer.of("25.10.16+build.z20251016.2")) > 0);
|
||||
assertTrue(compare(SemVer.of(version_25_10_17), SemVer.of("25.10.16-RC2+build.z20251016.2")) > 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void compareTo_MoreVersionNumber() {
|
||||
|
||||
assertTrue(compare(SemVer.of("25.10.17.1"), SemVer.of("25.10.17")) > 0);
|
||||
assertTrue(compare(SemVer.of("25.10.17.11"), SemVer.of("25.10.17.1")) > 0);
|
||||
assertEquals(0, compare(SemVer.of("25.10.17.11"), SemVer.of("25.10.17.11")));
|
||||
|
||||
assertTrue(compare(SemVer.of("25.10.17.11.1"), SemVer.of("25.10.17.11")) > 0);
|
||||
assertTrue(compare(SemVer.of("25.10.17.11.38"), SemVer.of("25.10.17.11.1")) > 0);
|
||||
assertEquals(0, compare(SemVer.of("25.10.17.11.38"), SemVer.of("25.10.17.11.38")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void compareTo_PreReleaseVersion() {
|
||||
|
||||
// 先行版的优先级低于相关联的标准版本
|
||||
assertTrue(compare(SemVer.of("25.10.17"), SemVer.of("25.10.17-0")) > 0);
|
||||
assertTrue(compare(SemVer.of("25.10.17"), SemVer.of("25.10.17-RC1")) > 0);
|
||||
|
||||
// 只有数字的标识符以数值高低比较
|
||||
assertAll(
|
||||
() -> assertTrue(compare("25.10.17-RC.11", "25.10.17-RC.2") < 0),
|
||||
() -> assertTrue(compare(SemVer.of("25.10.17-RC.11"), SemVer.of("25.10.17-RC.2")) > 0)
|
||||
);
|
||||
|
||||
// 纯数字优先级低于非数字
|
||||
assertAll(
|
||||
() -> assertTrue(compare("25.10.17-999", "25.10.17-A") < 0),
|
||||
() -> assertTrue(compare(SemVer.of("25.10.17-A.A"), SemVer.of("25.10.17-A.99")) > 0)
|
||||
);
|
||||
|
||||
SemVer[] versions = {
|
||||
SemVer.of("25.10.17-a"),
|
||||
SemVer.of("25.10.17-aa"),
|
||||
SemVer.of("25.10.17-A"),
|
||||
SemVer.of("25.10.17-AA"),
|
||||
|
||||
SemVer.of("25.10.17--"),
|
||||
|
||||
SemVer.of("25.10.17-999"),
|
||||
|
||||
SemVer.of("25.10.17-z"),
|
||||
SemVer.of("25.10.17-zz"),
|
||||
SemVer.of("25.10.17-Z"),
|
||||
SemVer.of("25.10.17-ZZ"),
|
||||
};
|
||||
|
||||
assertArrayEquals(
|
||||
new SemVer[] {
|
||||
// 纯数字优先级低于非数字
|
||||
SemVer.of("25.10.17-999"),
|
||||
|
||||
// 有字母或连接号时逐字符以 ASCII 的排序比较
|
||||
SemVer.of("25.10.17--"),
|
||||
|
||||
SemVer.of("25.10.17-A"),
|
||||
SemVer.of("25.10.17-AA"),
|
||||
SemVer.of("25.10.17-Z"),
|
||||
SemVer.of("25.10.17-ZZ"),
|
||||
SemVer.of("25.10.17-a"),
|
||||
SemVer.of("25.10.17-aa"),
|
||||
SemVer.of("25.10.17-z"),
|
||||
SemVer.of("25.10.17-zz"),
|
||||
},
|
||||
Arrays.stream(versions)
|
||||
.sorted()
|
||||
.toArray(SemVer[]::new));
|
||||
|
||||
// 若开头的标识符都相同时,栏位比较多的先行版本号优先级比较高
|
||||
assertAll(
|
||||
() -> assertTrue(compare(SemVer.of("25.10.17-ABC.DEF.1"), SemVer.of("25.10.17-ABC.DEF")) > 0),
|
||||
() -> assertTrue(compare(SemVer.of("25.10.17-ABC.DEF.G"), SemVer.of("25.10.17-ABC.DEF")) > 0));
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void compareTo_ignoreBuildMeta() {
|
||||
|
||||
assertNotEquals(SemVer.of("25.10.17"), SemVer.of("25.10.17+build.20251017.01"));
|
||||
assertTrue(compare(SemVer.of("25.10.17"), SemVer.of("25.10.17+build.20251017.01")) == 0); // NOSONAR sonarqube(java:S5785)
|
||||
|
||||
assertNotEquals(SemVer.of("25.10.17+build.20251017.02"), SemVer.of("25.10.17+build.20251017.01"));
|
||||
assertTrue(compare(SemVer.of("25.10.17+build.20251017.02"), SemVer.of("25.10.17+build.20251017.01")) == 0); // NOSONAR sonarqube(java:S5785)
|
||||
|
||||
assertNotEquals(SemVer.of("25.10.17-ABC.DEF"), SemVer.of("25.10.17-ABC.DEF+build.20251017.01"));
|
||||
assertTrue(compare(SemVer.of("25.10.17-ABC.DEF"), SemVer.of("25.10.17-ABC.DEF+build.20251017.01")) == 0); // NOSONAR sonarqube(java:S5785)
|
||||
|
||||
assertNotEquals(SemVer.of("25.10.17-ABC.DEF+build.20251017.02"), SemVer.of("25.10.17-ABC.DEF+build.20251017.01"));
|
||||
assertTrue(compare(SemVer.of("25.10.17-ABC.DEF+build.20251017.02"), SemVer.of("25.10.17-ABC.DEF+build.20251017.01")) == 0); // NOSONAR sonarqube(java:S5785)
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {
|
||||
"25.10.17",
|
||||
"25.10.17-ABC.DEF.1",
|
||||
"25.10.17+build.20251017.a2",
|
||||
"25.10.17-ABC.DEF.1+build.20251017.a2",
|
||||
})
|
||||
void test_equals(String value) {
|
||||
final SemVer v1 = SemVer.of(value);
|
||||
final SemVer v2 = SemVer.of(value);
|
||||
|
||||
assertTrue(v1.compareTo(v1) == 0); // NOSONAR sonarqube(java:S5785)
|
||||
assertTrue(v1.compareTo(v2) == 0); // NOSONAR sonarqube(java:S5785)
|
||||
|
||||
assertEquals(v1, v1);
|
||||
assertEquals(v1.hashCode(), v2.hashCode());
|
||||
assertEquals(v1, v2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_equals_null() {
|
||||
assertFalse(SemVer.of("25.10.17").equals(null)); // NOSONAR sonarqube(java:S5785)
|
||||
}
|
||||
|
||||
@Test
|
||||
void details() {
|
||||
final String s = "25.10.17-ABC.DEF.1+build.20251017.02";
|
||||
final SemVer v = SemVer.of(s);
|
||||
assertEquals(25, v.getMajor());
|
||||
assertEquals(10, v.getMinor());
|
||||
assertEquals(17, v.getPatch());
|
||||
|
||||
assertEquals("ABC.DEF.1", v.getPreReleaseVersion());
|
||||
assertEquals(s, v.getValue());
|
||||
assertEquals("v" + s, v.toString());
|
||||
|
||||
assertEquals("build.20251017.02", v.getBuildMetadata());
|
||||
}
|
||||
}
|
||||
@@ -234,10 +234,6 @@ 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;
|
||||
@@ -252,6 +248,10 @@ class AccountQueryParams extends PagingAndSortingQueryParams {
|
||||
}
|
||||
return this.createTimeEnd.plusDays(1);
|
||||
}
|
||||
|
||||
public PagingParams buildPagingParams() {
|
||||
return PAGING_PARAMS_BUILDER.buildPagingParams(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
|
||||
@@ -59,16 +59,13 @@ public class RandomToolsTests {
|
||||
|
||||
@Test
|
||||
public void randomStr_NullSourceCharacters_ThrowsException() {
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> RandomTools.randomStr(random, (char[]) null, 5));
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> RandomTools.randomStr(random, (String) null, 5));
|
||||
assertThrows(IllegalArgumentException.class, () -> RandomTools.randomStr(random, (char[]) null, 5));
|
||||
assertThrows(IllegalArgumentException.class, () -> RandomTools.randomStr(random, (String) null, 5));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void randomStr_NegativeLength_ThrowsException() {
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> RandomTools.randomStr(random, sourceCharactersArray, -1));
|
||||
assertThrows(IllegalArgumentException.class, () -> RandomTools.randomStr(random, sourceCharactersArray, -1));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -115,7 +112,7 @@ public class RandomToolsTests {
|
||||
|
||||
@Test
|
||||
public void randomInt_WithMinAndMax() {
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
for (int i = 0; i < 1000_0000; i++) {
|
||||
int r = RandomTools.randomInt(random, -2, 3);
|
||||
assertTrue(r >= -2 && r < 3);
|
||||
}
|
||||
@@ -124,7 +121,7 @@ public class RandomToolsTests {
|
||||
@Test
|
||||
public void randomInt_WithClosedOpenRange() {
|
||||
Range<Integer> co = Range.closedOpen(-2, 3);
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
for (int i = 0; i < 1000_0000; i++) {
|
||||
int rco = RandomTools.randomInt(random, co);
|
||||
assertTrue(rco >= -2 && rco < 3);
|
||||
}
|
||||
@@ -133,7 +130,7 @@ public class RandomToolsTests {
|
||||
@Test
|
||||
public void randomInt_WithClosedRange() {
|
||||
Range<Integer> cc = Range.closed(-2, 3);
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
for (int i = 0; i < 1000_0000; i++) {
|
||||
int rcc = RandomTools.randomInt(random, cc);
|
||||
assertTrue(rcc >= -2 && rcc <= 3);
|
||||
}
|
||||
@@ -142,7 +139,7 @@ public class RandomToolsTests {
|
||||
@Test
|
||||
public void randomInt_WithOpenClosedRange() {
|
||||
Range<Integer> oc = Range.openClosed(-2, 3);
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
for (int i = 0; i < 1000_0000; i++) {
|
||||
int roc = RandomTools.randomInt(random, oc);
|
||||
assertTrue(roc > -2 && roc <= 3);
|
||||
}
|
||||
@@ -151,7 +148,7 @@ public class RandomToolsTests {
|
||||
@Test
|
||||
public void randomInt_WithOpenRange() {
|
||||
Range<Integer> oo = Range.open(-2, 3);
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
for (int i = 0; i < 1000_0000; i++) {
|
||||
int roo = RandomTools.randomInt(random, oo);
|
||||
assertTrue(roo > -2 && roo < 3);
|
||||
}
|
||||
|
||||
@@ -36,10 +36,7 @@ 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")
|
||||
@@ -64,7 +61,7 @@ class TreeBuilderTests {
|
||||
|
||||
private final TreeBuilder<Menu, MenuList, String> treeBuilder = new TreeBuilder<>(
|
||||
Menu::getMenuCode,
|
||||
menu -> Optional.ofNullable(menu.getParentMenuCode()),
|
||||
menu -> Optional.ofNullable(menu.parentMenuCode),
|
||||
MenuList::addChild,
|
||||
Comparator.comparing(Menu::getOrderNum));
|
||||
|
||||
@@ -137,23 +134,45 @@ class TreeBuilderTests {
|
||||
((MenuList) menuMap.get("C1")).children);
|
||||
}
|
||||
|
||||
@AllArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
@EqualsAndHashCode
|
||||
@ToString
|
||||
@EqualsAndHashCode
|
||||
private abstract static class Menu implements Serializable {
|
||||
protected final @Getter String parentMenuCode;
|
||||
protected final @Getter String menuCode;
|
||||
protected final @Getter String title;
|
||||
protected final @Getter int orderNum;
|
||||
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;
|
||||
}
|
||||
|
||||
private static final long serialVersionUID = 20240917181424L;
|
||||
}
|
||||
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
private static final class MenuItem extends Menu {
|
||||
|
||||
private final @Getter String url;
|
||||
private final String url;
|
||||
|
||||
private MenuItem(String parentMenuCode, String menuCode, String title, String url, int orderNum) {
|
||||
super(parentMenuCode, menuCode, title, orderNum);
|
||||
@@ -168,6 +187,10 @@ class TreeBuilderTests {
|
||||
return new MenuItem(null, menuCode, title, url, orderNum);
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
private static final long serialVersionUID = 20240917181910L;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
/*
|
||||
* 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;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.net.URL;
|
||||
import java.util.Arrays;
|
||||
import java.util.Random;
|
||||
import java.util.zip.DataFormatException;
|
||||
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
import com.google.common.base.Stopwatch;
|
||||
import com.google.common.io.Resources;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
public class ZipToolsTests {
|
||||
|
||||
static byte[] bytes;
|
||||
|
||||
@BeforeAll
|
||||
static void setup() throws IOException {
|
||||
URL resource = Resources.getResource("xyz/zhouxy/plusone/commons/util/LoremIpsum.txt");
|
||||
bytes = Resources.toByteArray(resource);
|
||||
}
|
||||
|
||||
@Test
|
||||
void zip_WithDefaultLevel() throws IOException, DataFormatException {
|
||||
Stopwatch stopwatch = Stopwatch.createStarted();
|
||||
byte[] zip = ZipTools.zip(bytes);
|
||||
stopwatch.stop();
|
||||
log.info("default level, size: {} ({})", zip.length, stopwatch);
|
||||
assertArrayEquals(bytes, ZipTools.unzip(zip));
|
||||
|
||||
assertNull(ZipTools.zip(null));
|
||||
assertNull(ZipTools.unzip(null));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(ints = { -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 })
|
||||
void zip_WithLevel(int level) throws IOException, DataFormatException {
|
||||
Stopwatch stopwatch = Stopwatch.createStarted();
|
||||
byte[] zip = ZipTools.zip(bytes, level);
|
||||
stopwatch.stop();
|
||||
log.info("level: {}, size: {} ({})", level, zip.length, stopwatch);
|
||||
assertArrayEquals(bytes, ZipTools.unzip(zip));
|
||||
|
||||
assertNull(ZipTools.zip(null, level));
|
||||
assertNull(ZipTools.unzip(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void zip_WithWrongLevel() {
|
||||
Random random = new Random();
|
||||
final int levelGtMax = random.nextInt() + 9;
|
||||
assertThrows(IllegalArgumentException.class, () -> ZipTools.zip(bytes, levelGtMax));
|
||||
|
||||
final int levelLtMin = -1 - random.nextInt();
|
||||
assertThrows(IllegalArgumentException.class, () -> ZipTools.zip(bytes, levelLtMin));
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_constructor_isNotAccessible_ThrowsIllegalStateException() {
|
||||
Constructor<?>[] constructors = ZipTools.class.getDeclaredConstructors();
|
||||
Arrays.stream(constructors)
|
||||
.forEach(constructor -> {
|
||||
assertFalse(constructor.isAccessible());
|
||||
constructor.setAccessible(true);
|
||||
Throwable cause = assertThrows(Exception.class, constructor::newInstance)
|
||||
.getCause();
|
||||
assertInstanceOf(IllegalStateException.class, cause);
|
||||
assertEquals("Utility class", cause.getMessage());
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
Heaven much and fluttered sat the that lie chamber heart. Sure fantastic pallas in if chamber vainly i was by so pallas deep late the distant the stood. Now crest bust its and streaming but friends a on that still with master late the she this. Lent head soul this truly and presently surely i off. Grim and linking thy a is me my. Raven its is sir he his both with unmerciful a if evil vainly aptly this.
|
||||
|
||||
Truly mystery unseen that wished nevermore cushioned said bust lamplight. Home respite chamber its nevermore. Hesitating in upon said quoth broken sainted and leave soul floor. Home above still violet more fowl more much oer burning wandering maiden i. Desolate ghost still said from gently door before that rare the still till still only doubtless and the whether. That from pallas is entrance upon my when gilead. More its raven door bust then myself whose fiend swung that floor and quaint has i. Let whom ever followed on or of word name a the was and oer a surcease. Lamplight the sat my him tell darkness grew that distinctly. Some be as. Let chamber lattice of me caught i no me tufted raven songs ghastly quoth here stepped nights have. Heaven and door.
|
||||
|
||||
The angels relevancy radiant the undaunted me morrow though thy be mystery this my. Gently angels flown burning he tapping lining whispered i. Ungainly the broken stronger angels i tell separate what faintly and raven so. And take bird i.
|
||||
|
||||
Me ah was. The above floating madam cushioned lie had i. No as the. I horror with heard the above its said stayed. Me and what my me lenore the when the was. I croaking lie seat what cannot suddenly stood ghost. The of lenore terrors lifted for. Utters thy the desert grim that shrieked this fearing and form meant soon he friends louder. Filled more of of the raven. At above open engaged scarce never still vainly chamber the shore was still. Separate ancient dreary but his thy if nothing. Whose silence ominous at thy napping entrance off bust. Grew expressing on fowl. Decorum and token the door soul. Tapping kind of cushioned. Of nothing have thy evil above velvet a to doubtless memories chamber ungainly. I clasp rustling doubting more sitting for. Into core bird loneliness me lenore undaunted this raven the came mien bird i smiling and.
|
||||
|
||||
Sorrow engaged of what lenore i one agreeing quoth no. Stopped then i from me sculptured and and some in is respite. Lenore and lamplight and the at on the dream beguiling the angels said sad and on. By above the at. Lost above placid the it the of quoth and this in dreams ashore nothing. Wore shore came above raven stately. Of word it i from parting spoke heard. Was or lie prophet we. His into lamplight i. You more felt beating books shall word let above and mystery air.
|
||||
@@ -52,8 +52,6 @@
|
||||
<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>
|
||||
|
||||
@@ -148,27 +146,22 @@
|
||||
<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>
|
||||
@@ -176,7 +169,6 @@
|
||||
<version>${querydsl.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Byte Buddy 字节码工具 -->
|
||||
<dependency>
|
||||
<groupId>net.bytebuddy</groupId>
|
||||
<artifactId>byte-buddy</artifactId>
|
||||
@@ -196,34 +188,22 @@
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user