diff --git a/README.md b/README.md index 5ecf730..14a3b50 100644 --- a/README.md +++ b/README.md @@ -1,246 +1,31 @@ -## 一、annotation - 注解 -### 1. StaticFactoryMethod -标识静态工厂方法。 *《Effective Java》* 的 **Item1** 建议考虑用静态工厂方法替换构造器, 因而考虑有一个注解可以标记一下静态工厂方法,以和其它方法进行区分。 +# Plusone Commons +## 1. 简介 +Plusone Commons 是一个 Java 工具类库,提供了一系列实用的类和方法,用于简化开发。 -### 2. ReaderMethod 和 WriterMethod -分别标识读方法(如 getter)或写方法(如 setter)。 +一开始是为了补充日常开发中,guava 认为不需要,而我又用得上的工具,所以需要结合 guava 使用。后面也包含了一些从日常工作与学习中抽离出来可以通用的东西。 -最早是写了一个集合类,为了方便判断使用读写锁时,哪些情况下使用读锁,哪些情况下使用写锁。 +Plusone Commons 的工具类不追求“大而全”,而是只提供相对需要的部分功能。 -### 3. UnsupportedOperation -标识该方法不被支持或没有实现,将抛出 `UnsupportedOperationException`。 为了方便在使用时,不需要点进源码,就能知道该方法没有实现。 +> 未来一些不够“通用”的组件会迁移到更合适的模块中。 -### 4. Virtual -Java 非 final 的实例方法,对应 C++/C# 中的虚方法,允许被子类覆写。 Virtual 注解旨在设计父类时,强调该方法父类虽然有默认实现,但子类可以根据自己的需要覆写。 +## 2. 安装 +项目基于 OpenJDK 8 和 maven 构建。 -### 5. ValueObject -标记一个类,表示其作为值对象,区别于 Entity。 +项目目前暂未发布到 maven 中央仓库,使用时需克隆代码到本地,并安装到本地仓库,然后才能在项目中引入依赖。 -## 二、base - 基础组件 -### 1. Ref -`Ref` 包装了一个值,表示对该值的应用。 +## 3. 功能 +详细功能说明请查阅文档: -灵感来自于 C# 的 ref 参数修饰符。C# 允许通过以下方式,将值返回给调用端: -```C# -void Method(ref int refArgument) -{ - refArgument = refArgument + 44; -} ++ [gitee 文档地址](https://gitee.com/zhouxy108/plusone-commons/tree/dev/docs) -int number = 1; -Method(ref number); -Console.WriteLine(number); // Output: 45 -``` -`Ref` 使 Java 可以达到类似的效果,如: -```java -void method(Ref refArgument) { - refArgument.transformValue(i -> i + 44); -} +## 4. 代码仓库 +项目仓库一共建了三个: -Ref number = Ref.of(1); -method(number); -System.out.println(number.getValue()); // Output: 45 -``` -当一个方法需要产生多个结果时,无法有多个返回值,可以使用 `Ref` 作为参数传入,方法内部修改 `Ref` 的值。 调用方在调用方法之后,使用 `getValue()` 获取结果。 -```java -String method(Ref intRefArgument, Ref strRefArgument) { - intRefArgument.transformValue(i -> i + 44); - strRefArgument.setValue("Hello " + strRefArgument.getValue()); - return "Return string"; -} ++ [GitHub](https://github.com/ZhouXY108/plusone-commons) ++ [gitee](https://gitee.com/zhouxy108/plusone-commons) ++ [自建 Gitea 仓库](http://gitea.zhouxy.xyz/plusone/plusone-commons) -Ref number = Ref.of(1); -Ref str = Ref.of("Java"); -String result = method(number, str); -System.out.println(number.getValue()); // Output: 45 -System.out.println(str.getValue()); // Output: Hello Java -System.out.println(result); // Output: Return string -``` -### 2. IWithCode -类似于枚举这样的类型,通常需要设置固定的码值表示对应的含义。 可实现 `IWithCode`、`IWithIntCode`、`IWithLongCode`,便于在需要的地方对这些接口的实现进行处理。 +欢迎在 GitHub 和 gitee 上通过 issue 反馈使用过程中发现的问题和建议,也接受善意的 PR。 -## 三、collection - 集合 -### 1. CollectionTools -集合工具类 - -## 四、constant - 常量 -### 1. 正则常量 -`RegexConsts` 包含常见正则表达式;`PatternConsts` 包含对应的 `Pattern` 对象 - -## 五、exception - 异常 -### 1. IMultiTypesException - 多类型异常 -异常在不同场景下被抛出,可以用不同的枚举值,表示不同的场景类型。 - -异常实现 `IMultiTypesException` 的 `IMultiTypesException#getType` 方法,返回对应的场景类型。 - -表示场景类型的枚举实现 `IMultiTypesException.IExceptionType`,其中的工厂方法用于创建对应类型的异常。 -```java -public final class LoginException - extends RuntimeException - implements IMultiTypesException { - 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, IExceptionFactory { - 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 get() throws E | - | Optional | ToOptionalBiFunction | Optional apply(T,U) | - | Optional | ToOptionalFunction | Optional 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` 的工厂类。用于快速构建 `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 UnifiedResponse success() { - return of(SUCCESS_CODE, DEFAULT_SUCCESS_MSG); - } - public static UnifiedResponse success(@Nullable String message) { - return of(SUCCESS_CODE, message); - } - public static UnifiedResponse 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) - -## 八、time - 时间 API -### 1. 季度 -模仿 JDK 的 `java.time.Month` 和 `java.time.YearMonth`, 实现 `Quarter`、`YearQuarter`,对季度进行建模。 - -## 九、util - 工具类 -包含树构建器(`TreeBuilder`)、断言工具(`AssertTools`)、ID 生成器(`IdGenerator`)及其它实用工具类。 +## 5. 许可 +项目使用 [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0) 开源,相关声明请参阅 `NOTICE` 文件。 diff --git a/docs/1_annotation.md b/docs/1_annotation.md new file mode 100644 index 0000000..5e9105e --- /dev/null +++ b/docs/1_annotation.md @@ -0,0 +1,8 @@ +## 1. 注解 +|注解|说明| +|--|--| +| **StaticFactoryMethod** | **标识静态工厂方法**。 *《Effective Java》* 的 **Item1** 建议考虑用静态工厂方法替换构造器, 因而考虑有一个注解可以标记一下静态工厂方法,以和其它方法进行区分。| +| **ReaderMethod** / **WriterMethod** | **分别标识读方法(如 getter)或写方法(如 setter)**。
*最早是写了一个集合类,为了方便判断使用读写锁时,哪些情况下使用读锁,哪些情况下使用写锁。*| +| **UnsupportedOperation** | **标识该方法不被支持或没有实现**,将抛出 `UnsupportedOperationException`。 为了方便在使用时,不需要点进源码,就能知道该方法没有实现。| +| **Virtual** | Java 非 final 的实例方法,对应 C++/C# 中的虚方法,允许被子类覆写。 **Virtual 注解旨在设计父类时,强调该方法父类虽然有默认实现,但子类可以根据自己的需要覆写**。| +| **ValueObject** | 标记一个类,表示其作为**值对象**,区别于 Entity。| diff --git a/docs/2_collection.md b/docs/2_collection.md new file mode 100644 index 0000000..bc32d16 --- /dev/null +++ b/docs/2_collection.md @@ -0,0 +1,39 @@ +## 2. 集合 + +### 2.1. CollectionTools + +简单的集合工具类,包含判空等常用方法。 + +### 2.2. MapModifier + +Map 修改器。封装一系列对 Map 数据的修改操作,修改 Map 的数据。可以用于 Map 的数据初始化等操作。 + +```java +// MapModifier +MapModifier modifier = new MapModifier() + .putAll(commonProperties) + .put("username", "Ben") + .put("accountStatus", LOCKED); + +// 从 Supplier 中获取 Map,并修改数据 +Map map = modifier.getAndModify(HashMap::new); + +// 可以灵活使用不同 Map 类型的不同构造器 +Map map = modifier.getAndModify(() -> new HashMap<>(8)); +Map map = modifier.getAndModify(() -> new HashMap<>(anotherMap)); +Map map = modifier.getAndModify(TreeMap::new); +Map map = modifier.getAndModify(ConcurrentHashMap::new); + +// 修改已有的 Map +modifier.modify(map); + +// 创建一个有初始化数据的不可变的 Map +Map map = modifier.getUnmodifiableMap(); + +// 链式调用创建并初始化数据 +Map map = new MapModifier() + .putAll(commonProperties) + .put("username", "Ben") + .put("accountStatus", LOCKED) + .getAndModify(HashMap::new); +``` diff --git a/docs/3_exception.md b/docs/3_exception.md new file mode 100644 index 0000000..905ce16 --- /dev/null +++ b/docs/3_exception.md @@ -0,0 +1,118 @@ +## 3. 异常 + +### 3.1. 业务异常 +|异常|描述| +|---|---| +|`BizException`|**业务异常**
*用户可继承 `BizException` 自定义业务异常。*| +|» `RequestParamsException`|**用户请求参数错误**| +|» » `InvalidInputException`|**用户输入内容非法**
00 - **DEFAULT** (用户输入内容非法)
01 - **CONTAINS_ILLEGAL_AND_MALICIOUS_LINKS** (包含非法恶意跳转链接)
02 - **CONTAINS_ILLEGAL_WORDS** (包含违禁敏感词)
03 - **PICTURE_CONTAINS_ILLEGAL_INFORMATION** (图片包含违禁信息)
04 - **INFRINGE_COPYRIGHT** (文件侵犯版权)| + +### 3.2. 系统异常 +|异常|描述| +|---|---| +|`SysException`|**系统异常**(表示技术异常)
*用户可继承 `SysException` 自定义系统异常。*| +|» `DataOperationResultException`|**数据操作的结果不符合预期**| +|» `NoAvailableMacFoundException`|**无法找到可访问的 Mac 地址**| + +### 3.3. 其它异常 +|异常|描述| +|---|---| +|`DataNotExistsException`|**数据不存在异常**| +|`ParsingFailureException`|**数据解析异常**
00 - **DEFAULT** (解析失败)
10 - **NUMBER_PARSING_FAILURE** (数字转换失败)
20 - **DATE_TIME_PARSING_FAILURE** (时间解析失败)
30 - **JSON_PARSING_FAILURE** (JSON 解析失败)
40 - **XML_PARSING_FAILURE** (XML 解析失败)| + +### 3.4. 多类型异常 + +异常在不同场景下被抛出,可以用不同的枚举值,表示不同的场景类型。 + +异常实现 `IMultiTypesException` 的 `getType` 方法,返回对应的场景类型。 + +枚举实现 `IExceptionType` 接口,表示不同的异常场景。也可以实现 `IExceptionFactory`,用于创建对应场景的异常。 + +```java +public final class LoginException + extends RuntimeException + implements IMultiTypesException { + 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, IExceptionFactory { + 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(); +``` + + diff --git a/docs/4_function.md b/docs/4_function.md new file mode 100644 index 0000000..ecc1920 --- /dev/null +++ b/docs/4_function.md @@ -0,0 +1,22 @@ +## 4. - 函数式编程 + +### 4.1. PredicateTools + +`PredicateTools` 用于 `Predicate` 的相关操作。 + +### 4.2. Functional interfaces + +补充可能用得上的函数式接口: + +| Group | FunctionalInterface | method | +| --- | --- | --- | +| UnaryOperator | **BoolUnaryOperator** | boolean applyAsBool (boolean) | +| UnaryOperator | **CharUnaryOperator** | char applyAsChar(char) | +| Throwing | **Executable** | void execute() throws E | +| Throwing | **ThrowingConsumer** | void accept(T) throws E | +| Throwing | **ThrowingFunction** | R apply(T) throws E | +| Throwing | **ThrowingPredicate** | boolean test(T) throws E | +| Throwing | **ThrowingSupplier** | T get() throws E | +| Optional | **OptionalSupplier** | Optional get() throws E | +| Optional | **ToOptionalBiFunction** | Optional apply(T,U) | +| Optional | **ToOptionalFunction** | Optional apply(T) | diff --git a/docs/5_model.md b/docs/5_model.md new file mode 100644 index 0000000..6761209 --- /dev/null +++ b/docs/5_model.md @@ -0,0 +1,100 @@ +## 5. 数据模型 + +### 5.1. 业务模型 + +| | 类型 | 描述 | +| --- | --- | --- | +| 接口 | `IDCardNumber` | 身份证号 | +| 值对象 | » `Chinese2ndGenIDCardNumber` | 中国二代居民身份证号 | +| 值对象 | `SemVer` | 语义化版本号 | +| 值对象(枚举)| `Gender` | 性别 | +| ~~值对象(抽象类)~~| ~~`ValidatableStringRecord`~~ | ~~带校验的字符串值对象~~ | + +### 5.2. 数据传输对象 + +#### 5.2.1. 分页查询 + +`PagingAndSortingQueryParams` (分页排序查询参数) + +`PagingAndSortingQueryParams` 包含三个主要的属性: +- **size** - 每页显示的记录数 +- **pageNum** - 当前页码 +- **orderBy** - 排序条件 + +分页必须伴随着排序,不然可能出现同一个对象重复出现在不同页,有的对象不被查询到的情况。 + +其中 `orderBy` 是一个 `List`,可以指定多个排序条件,每个排序条件是一个字符串, 格式为“**属性名-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 PROPERTY_COLUMN_MAP = ImmutableMap.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 queryPage(AccountQueryParams params) { + // 获取分页参数 + PagingParams pagingParams = params.buildPagingParams(); + // 从 params 获取字段查询条件,从 pagingParams 获取分页条件,查询一页数据 + List 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 UnifiedResponse success() { + return of(SUCCESS_CODE, DEFAULT_SUCCESS_MSG); + } + public static UnifiedResponse success(@Nullable String message) { + return of(SUCCESS_CODE, message); + } + public static UnifiedResponse 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) diff --git a/docs/6_time.md b/docs/6_time.md new file mode 100644 index 0000000..25ef325 --- /dev/null +++ b/docs/6_time.md @@ -0,0 +1,24 @@ +## 6. 时间 API + +### 6.1. 季度 + +模仿 JDK 的 `java.time.Month` 和 `java.time.YearMonth`, 实现 `xyz.zhouxy.plusone.commons.time.Quarter`、`xyz.zhouxy.plusone.commons.timeYearQuarter`,对季度进行建模。 + +*这两个类的代码修改后,也提交给了 **hutool**。见 gitee + 上的 [pr#1324](https://gitee.com/chinabugotech/hutool/pulls/1324)*。 + +### 6.2. DateTimeTools + +`xyz.zhouxy.plusone.commons.util.DateTimeTools` 提供了包含 Java 旧的时间 API 和 `java.time` API 在内的日期时间的常用操作。 + +### 6.3. JodaTimeTools + +`xyz.zhouxy.plusone.commons.util.JodaTimeTools` 提供了 JodaTime 和 `java.time` API 相互转换的工具方法: +- `toJodaInstant` +- `toJavaInstant` +- `toJodaDateTime` +- `toZonedDateTime` +- `toJodaLocalDateTime` +- `toJavaLocalDateTime` +- `toJavaZone` +- `toJodaZone` diff --git a/docs/7_other_tools.md b/docs/7_other_tools.md new file mode 100644 index 0000000..2046b73 --- /dev/null +++ b/docs/7_other_tools.md @@ -0,0 +1,275 @@ +## 7. 其它工具类 + +### 7.1. 数组工具类(ArrayTools) + +| 方法 | 描述 | +| --- | --- | +| `isEmpty` | 判断数组是否为空 | +| `isNotEmpty` | 判断数组是否不为空 | +| `isAllElementsNotNull` | 判断数组中所有元素是否不为空 | +| `concat` | 拼接数组 | +| `repeat` | 重复数组中的元素 | +| `fill` | 填充数组 | +| `indexOf` | 获取元素在数组中的索引 | +| `lastIndexOf` | 获取元素最后出现在数组中的索引 | +| `contains` | 判断数组中是否包含某个元素 | + +### 7.2. 断言工具类(AssertTools) + +`AssertTools` 不封装过多判断逻辑,鼓励充分使用项目中的工具类对数据进行判断: + +```java +AssertTools.checkArgument(StringUtils.hasText(str), "The argument cannot be blank."); +AssertTools.checkState(ArrayUtils.isNotEmpty(result), "The result cannot be empty."); +AssertTools.checkCondition(!CollectionUtils.isEmpty(roles), + () -> new InvalidInputException("The roles cannot be empty.")); +AssertTools.checkCondition(RegexTools.matches(email, PatternConsts.EMAIL), + "must be a well-formed email address"); +``` + +### 7.3. 枚举工具 + +#### ~~7.3.1 枚举类(Enumeration)(已废弃)~~ + +~~`Enumeration` 的实现来自于 .net 社区。因为 C# 本身的枚举不带行为,所以继承自 `Enumeration` 类,以实现带行为的枚举常量。~~ + +**~~但 Java 的枚举可以带行为,故大多数情况下不需要这种设计。~~** + +#### 7.3.2 Enum 工具类(EnumTools) + +用于枚举的 ordinal 和枚举值的转换等操作。 + +由于不推荐使用枚举的 ordinal,**故大多数方法已废弃**。更推荐的实现是枚举实现 `IWithCode` 之类的接口,在枚举中提供枚举值和枚举码的转换。 + +### 7.4. ID 生成器 + +#### 7.4.1. ID 生成器(IdGenerator) + +- 提供了 `UUID` 相关的方法 + | 方法 | 描述 | + | --- | --- | + | newUuid | 获取新的 `UUID` | + | uuidString | 获取新的 UUID 字符串 | + | simpleUuidString | 获取新的 UUID 字符串(无连接符) | + | toSimpleString | 将 `UUID` 转换为无连接符的字符串 | +- 使用 `IdWorker` *(来自 **Seata** 的雪花算法的变种)* 生成分布式唯一 ID + +#### 7.4.2. IdWorker + +来自 [Apache Seata](https://seata.apache.org/) 的 [`org.apache.seata.common.util.IdWorker`](https://github.com/apache/incubator-seata/blob/2.x/common/src/main/java/org/apache/seata/common/util/IdWorker.java),是雪花算法的变种。 + +详细介绍参考以下文章: +- [Seata基于改良版雪花算法的分布式UUID生成器分析](https://seata.apache.org/zh-cn/blog/seata-analysis-UUID-generator) +- [关于新版雪花算法的答疑](https://seata.apache.org/zh-cn/blog/seata-snowflake-explain) +- [在开源项目中看到一个改良版的雪花算法,现在它是你的了。](https://juejin.cn/post/7264387737276203065) +- [关于若干读者,阅读“改良版雪花算法”后提出的几个共性问题的回复。](https://juejin.cn/post/7265516484029743138) + +#### 7.4.3. SnowflakeIdGenerator + +`SnowflakeIdGenerator` 是原版的雪花算法的实现 + +### 7.5. 树构建器(TreeBuilder) + +`TreeBuilder` 是一个树构建器,用于将列表数据构建为树结构。 + +`TreeBuilder` 构造器的入参: +- **identityGetter**: 从节点中获取其标识的逻辑 +- **parentIdentityGetter**: 获取父节点标识的逻辑 +- **addChild**: 添加子节点的逻辑 +- **defaultComparator**: 默认的 Comparator,用于排序 + +> **注意:`TreeBuilder` 的 `buildTree` 方法,会直接更改列表中的节点。设计初衷是将查询到的列表,构建成为树结构之后直接返回给前端,如果需要,请在调用之前做深拷贝,然后再将深拷贝的结果作为入参传入。** + +以下示例演示 `TreeBuilder` 的使用: + +#### 7.5.1. 处理相对复杂的 entity + +假设有如下的实体类: +```java +@AllArgsConstructor(access = AccessLevel.PROTECTED) +@EqualsAndHashCode +@ToString +class Menu implements Serializable { + protected final @Getter String parentMenuCode; + protected final @Getter String menuCode; + protected final @Getter String title; + protected final @Getter int orderNum; + + private static final long serialVersionUID = 20240917181424L; +} + +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +class MenuItem extends Menu { + + private final @Getter String url; + + private MenuItem(String parentMenuCode, String menuCode, String title, String url, int orderNum) { + super(parentMenuCode, menuCode, title, orderNum); + this.url = url; + } + + static MenuItem of(String parentMenuCode, String menuCode, String title, String url, int orderNum) { + return new MenuItem(parentMenuCode, menuCode, title, url, orderNum); + } + + static MenuItem of(String menuCode, String title, String url, int orderNum) { + return new MenuItem(null, menuCode, title, url, orderNum); + } + + private static final long serialVersionUID = 20240917181910L; +} + +@ToString(callSuper = true) +class MenuList extends Menu { + + private List 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 children, int orderNum) { + return of(null, menuCode, title, children, orderNum); + } + + static MenuList of(String parentMenuCode, String menuCode, String title, Iterable 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 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 clonedMenus = menus.stream().map(ObjectUtil::clone).collect(Collectors.toList()); +// 按照创建时设置的逻辑,构建树结构 +List 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 children; + + private static final long serialVersionUID = 1298482252210272617L; +} +``` + +使用以下方式构建并使用 `TreeBuilder`: +```java +// 创建 TreeBuilder +TreeBuilder treeBuilder = new TreeBuilder<>( + // getMenuCode 方法获取节点标识 + Menu::getMenuCode, + // getParentMenuCode 方法获取父节点标识,如果父节点不存在,返回 Optional.empty() + menu -> Optional.ofNullable(menu.getParentMenuCode()), + // 自定义 addChild 逻辑 + (menuList, child) -> { + List children = menuList.getChildren(); + if (children == null) { + children = Lists.newArrayList(); + menuList.setChildren(children); + } + children.add(child); + }, + // 默认的 Comparator,使用 orderNum 进行排序 + Comparator.comparing(Menu::getOrderNum)); + +// 按照创建时设置的逻辑,构建树结构 +List 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 refArgument) { + refArgument.transformValue(i -> i + 44); +} + +Ref number = Ref.of(1); +method(number); +System.out.println(number.getValue()); // Output: 45 +``` +当一个方法需要产生多个结果时,无法有多个返回值,可以使用 `Ref` 作为参数传入,方法内部修改 `Ref` 的值。 调用方在调用方法之后,使用 `getValue()` 获取结果。 +```java +String method(Ref intRefArgument, Ref strRefArgument) { + intRefArgument.transformValue(i -> i + 44); + strRefArgument.setValue("Hello " + strRefArgument.getValue()); + return "Return string"; +} + +Ref number = Ref.of(1); +Ref 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 工具 diff --git a/docs/8_others.md b/docs/8_others.md new file mode 100644 index 0000000..5ab7211 --- /dev/null +++ b/docs/8_others.md @@ -0,0 +1,9 @@ +## 8. 其它内容 + +### 8.1. IWithCode + +对于类似枚举这样的类型,通常需要设置固定的码值表示对应的含义。 可实现 `IWithCode`、`IWithIntCode`、`IWithLongCode`,便于在需要的地方对这些接口的实现进行处理。 + +### 8.2. 正则常量 + +`RegexConsts` 包含常见正则表达式;`PatternConsts` 包含对应的 `Pattern` 对象 diff --git a/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/annotation/package-info.java b/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/annotation/package-info.java index e80df4e..d439941 100644 --- a/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/annotation/package-info.java +++ b/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/annotation/package-info.java @@ -15,44 +15,7 @@ */ /** - *

注解

- * - *

- * 1. {@link StaticFactoryMethod} - *

- *

- * 标识静态工厂方法。 - * 《Effective Java》的 Item1 建议考虑用静态工厂方法替换构造器, - * 因而考虑有一个注解可以标记一下静态工厂方法,以和其它方法进行区分。 - * - *

- * 2. {@link ReaderMethod} 和 {@link WriterMethod} - *

- *

- * 分别标识读方法(如 getter)或写方法(如 setter)。 - * - *

- * 最早是写了一个集合类,为了方便判断使用读写锁时,哪些情况下使用读锁,哪些情况下使用写锁。 - * - *

- * 3. {@link UnsupportedOperation} - *

- *

- * 标识该方法不被支持或没有实现,将抛出 {@link UnsupportedOperationException}。 - * 为了方便在使用时,不需要点进源码,就能知道该方法没有实现。 - * - *

- * 4. {@link Virtual} - *

- *

- * Java 非 final 的实例方法,对应 C++/C# 中的虚方法,允许被子类覆写。 - * {@link Virtual} 注解旨在设计父类时,强调该方法父类虽然有默认实现,但子类可以根据自己的需要覆写。 - * - *

- * 5. {@link ValueObject} - *

- *

- * 标记一个类,表示其作为值对象,区别于 Entity。 + * 注解 * * @author ZhouXY108 */ diff --git a/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/collection/package-info.java b/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/collection/package-info.java index 7f8573c..15f396b 100644 --- a/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/collection/package-info.java +++ b/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/collection/package-info.java @@ -15,12 +15,7 @@ */ /** - *

集合

- * - *

- * 1. {@link CollectionTools} - *

- * 集合工具类 + * 集合相关工具 * * @author ZhouXY108 */ diff --git a/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/exception/package-info.java b/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/exception/package-info.java index 21f54d3..77f18e3 100644 --- a/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/exception/package-info.java +++ b/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/exception/package-info.java @@ -15,115 +15,8 @@ */ /** - *

异常

- * - *

1. {@link IMultiTypesException} - 多类型异常

- *

- * 异常在不同场景下被抛出,可以用不同的枚举值,表示不同的场景类型。 - * - *

- * 异常实现 {@link IMultiTypesException} 的 {@link IMultiTypesException#getType} 方法,返回对应的场景类型。 - * - *

- * 表示场景类型的枚举实现 {@link IMultiTypesException.IExceptionType},各个枚举值本身就是该场景的异常的工厂实例, - * 使用其中的工厂方法用于创建对应类型的异常。 - * - *

- * 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);
- *         }
- *     }
- * }
- * 
- * - * 使用时,可以使用这种方式创建并抛出异常: - *
- * throw LoginException.Type.TOKEN_TIMEOUT.create();
- * 
- * - *

2. 业务异常

- * 预设常见的业务异常。可继承 {@link BizException} 自定义业务异常。 - * - *

3. 系统异常

- * 预设常见的系统异常。可继承 {@link SysException} 自定义系统异常。 + * 包含常见的业务异常与系统异常,以及异常相关的工具 * * @author ZhouXY108 */ package xyz.zhouxy.plusone.commons.exception; - -import xyz.zhouxy.plusone.commons.exception.business.*; -import xyz.zhouxy.plusone.commons.exception.system.*; diff --git a/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/util/package-info.java b/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/util/package-info.java index b118a3a..56008bc 100644 --- a/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/util/package-info.java +++ b/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/util/package-info.java @@ -17,7 +17,8 @@ /** *

工具类

*

- * 包含树构建器({@link TreeBuilder})、断言工具({@link AssertTools})、ID 生成器({@link IdGenerator})及其它实用工具类。 + * 包含树构建器({@link TreeBuilder})、断言工具({@link AssertTools})、 + * ID 生成器({@link IdGenerator})及其它实用工具类。 * * @author ZhouXY108 */