diff --git a/CHANGELOG.md b/CHANGELOG.md index 2136dba910841a6bc31ed01538ac104485608a93..e7434e9d6b5e86e46ec7e6aa15012d7000d156f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,35 @@ # 🚀Changelog +------------------------------------------------------------------------------------------------------------- +# 5.7.23 (2022-03-15) + +### 🐣新特性 +* 【http 】 HttpRequest.form采用TableMap方式(issue#I4W427@Gitee) +* 【core 】 AnnotationUtil增加getAnnotationAlias方法(pr#554@Gitee) +* 【core 】 FileUtil.extName增加对tar.gz特殊处理(issue#I4W5FS@Gitee) +* 【crypto 】 增加XXTEA实现(issue#I4WH2X@Gitee) +* 【core 】 增加Table实现(issue#2179@Github) +* 【core 】 增加UniqueKeySet(issue#I4WUWR@Gitee) +* 【core 】 阿拉伯数字转换成中文对发票票面金额转换的扩展(pr#570@Gitee) +* 【core 】 ArrayUtil增加replace方法(pr#570@Gitee) +* 【core 】 CsvReadConfig增加自定义标题行行号(issue#2180@Github) +* 【db 】 增加MongoDB4.x支持(pr#568@Gitee) +* +### 🐞Bug修复 +* 【core 】 修复ObjectUtil.hasNull传入null返回true的问题(pr#555@Gitee) +* 【core 】 修复NumberConverter对数字转换的问题(issue#I4WPF4@Gitee) +* 【core 】 修复ReflectUtil.getMethods获取接口方法问题(issue#I4WUWR@Gitee) +* 【core 】 修复NamingCase中大写转换问题(pr#572@Gitee) +* 【http 】 修复GET重定向时,携带参数问题(issue#2189@Github) +* 【core 】 修复FileUtil、FileCopier相对路径获取父路径错误问题(pr#2188@Github) + ------------------------------------------------------------------------------------------------------------- # 5.7.22 (2022-03-01) ### 🐣新特性 -* 【poi 】 ExcelUtil.readBySax增加对POI-5.2.0的兼容性(issue#I4TJF4@gitee) -* 【extra 】 Ftp增加构造(issue#I4TKXP@gitee) +* 【poi 】 ExcelUtil.readBySax增加对POI-5.2.0的兼容性(issue#I4TJF4@Gitee) +* 【extra 】 Ftp增加构造(issue#I4TKXP@Gitee) * 【core 】 GenericBuilder支持Map构建(pr#540@Github) * 【json 】 新增TemporalAccessorSerializer * 【core 】 使多个xxxBuilder实现Builder接口,扩展CheckedUtil(pr#545@Gitee) @@ -538,7 +561,7 @@ * 【json 】 增加JSONWriter * 【core 】 IdUtil增加getWorkerId和getDataCenterId(issueI3Y5NI@Gitee) * 【core 】 JWTValidator增加leeway重载 -* 【core 】 增加RegexPool(issue#I3W9ZF@gitee) +* 【core 】 增加RegexPool(issue#I3W9ZF@Gitee) ### 🐞Bug修复 * 【json 】 修复XML转义字符的问题(issue#I3XH09@Gitee) diff --git a/README-EN.md b/README-EN.md index 75fcfed1208d82e8f75cbaedcadae898540776c9..7456a08f77dce5d9eeff955d555405332096c860 100644 --- a/README-EN.md +++ b/README-EN.md @@ -111,6 +111,8 @@ Each module can be introduced individually, or all modules can be introduced by [📘Chinese documentation](https://www.hutool.cn/docs/) +[📘Chinese back-up documentation](https://plus.hutool.cn/docs/#/) + [📙API](https://apidoc.gitee.com/dromara/hutool/) [🎬Video](https://www.bilibili.com/video/BV1bQ4y1M7d9?p=2) @@ -142,18 +144,18 @@ We provide the T-Shirt and Sweater with Hutool Logo, please visit the shop: cn.hutool hutool-all - 5.7.22 + 5.7.23 ``` ### 🍐Gradle ``` -implementation 'cn.hutool:hutool-all:5.7.22' +implementation 'cn.hutool:hutool-all:5.7.23' ``` ## 📥Download -- [Maven Repo](https://repo1.maven.org/maven2/cn/hutool/hutool-all/5.7.22/) +- [Maven Repo](https://repo1.maven.org/maven2/cn/hutool/hutool-all/5.7.23/) > 🔔️note: > Hutool 5.x supports JDK8+ and is not tested on Android platforms, and cannot guarantee that all tool classes or tool methods are available. diff --git a/README.md b/README.md index 4516e5e0de10eed8c75e1d9402430be6ae10fb16..409e4e22d9bc15b7458dca3ec5787fc65659709a 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,8 @@ Hutool的存在就是为了减少代码搜索成本,避免网络上参差不 [📘中文文档](https://www.hutool.cn/docs/) +[📘中文备用文档](https://plus.hutool.cn/docs/#/) + [📙参考API](https://apidoc.gitee.com/dromara/hutool/) [🎬视频介绍](https://www.bilibili.com/video/BV1bQ4y1M7d9?p=2) @@ -142,20 +144,20 @@ Hutool的存在就是为了减少代码搜索成本,避免网络上参差不 cn.hutool hutool-all - 5.7.22 + 5.7.23 ``` ### 🍐Gradle ``` -implementation 'cn.hutool:hutool-all:5.7.22' +implementation 'cn.hutool:hutool-all:5.7.23' ``` ### 📥下载jar 点击以下链接,下载`hutool-all-X.X.X.jar`即可: -- [Maven中央库](https://repo1.maven.org/maven2/cn/hutool/hutool-all/5.7.22/) +- [Maven中央库](https://repo1.maven.org/maven2/cn/hutool/hutool-all/5.7.23/) > 🔔️注意 > Hutool 5.x支持JDK8+,对Android平台没有测试,不能保证所有工具类或工具方法可用。 diff --git a/bin/version.txt b/bin/version.txt index e2ff09a282565afc2fd83d3a142516ba537cf703..a5c3e01d2dc7743e5dfdfb2cbb16fb0dcb57c6af 100755 --- a/bin/version.txt +++ b/bin/version.txt @@ -1 +1 @@ -5.7.22 +5.7.23 diff --git a/docs/js/version.js b/docs/js/version.js index e637f5f205ffdcbffca68c58acfa77964334ab54..37b3ee602cac386a8c6cfbb55736ae3287c5b4be 100644 --- a/docs/js/version.js +++ b/docs/js/version.js @@ -1 +1 @@ -var version = '5.7.22' \ No newline at end of file +var version = '5.7.23' \ No newline at end of file diff --git a/hutool-all/pom.xml b/hutool-all/pom.xml index a516862eee3ee79c635de1325e8437c5353d03c0..d99b05ea1d81b20941bb8707faa590873c78d9f7 100644 --- a/hutool-all/pom.xml +++ b/hutool-all/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 5.7.22 + 5.7.23-SNAPSHOT hutool-all diff --git a/hutool-aop/pom.xml b/hutool-aop/pom.xml index f9dc30ef0406c4e161d1a0777545e4bfafdb3a15..9a53b9232e528afe6baad27bebe7e93b4356f345 100644 --- a/hutool-aop/pom.xml +++ b/hutool-aop/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 5.7.22 + 5.7.23-SNAPSHOT hutool-aop diff --git a/hutool-bloomFilter/pom.xml b/hutool-bloomFilter/pom.xml index 4abc55f45a19c2ebbcc3378925233e3ec6378ae7..6820aa9968f5aeebaa182172286238084b311138 100644 --- a/hutool-bloomFilter/pom.xml +++ b/hutool-bloomFilter/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 5.7.22 + 5.7.23-SNAPSHOT hutool-bloomFilter diff --git a/hutool-bom/pom.xml b/hutool-bom/pom.xml index 3490b7ceeda31d2bc4dca473873ac2ae6c135198..35c3ab9aa97c1e458ddeef5c23d6e559903f13d6 100644 --- a/hutool-bom/pom.xml +++ b/hutool-bom/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 5.7.22 + 5.7.23-SNAPSHOT hutool-bom diff --git a/hutool-cache/pom.xml b/hutool-cache/pom.xml index 298a97a6c2a722d4651f4675b41ffe55c1871e57..430d0b0925166f1adaea23f3fa1cc7a3917782c0 100644 --- a/hutool-cache/pom.xml +++ b/hutool-cache/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 5.7.22 + 5.7.23-SNAPSHOT hutool-cache diff --git a/hutool-captcha/pom.xml b/hutool-captcha/pom.xml index 17564bff02b89fcbec1766fffe98240a6ee35a20..b2dbcf27fb2cf1556b672b12226ce0a4acd9144e 100644 --- a/hutool-captcha/pom.xml +++ b/hutool-captcha/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 5.7.22 + 5.7.23-SNAPSHOT hutool-captcha diff --git a/hutool-core/pom.xml b/hutool-core/pom.xml index 9c225e9c7884942f289052cd9030546944c714b0..6ca1dd789492960738e28d87a4ba192caa635c46 100644 --- a/hutool-core/pom.xml +++ b/hutool-core/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 5.7.22 + 5.7.23-SNAPSHOT hutool-core diff --git a/hutool-core/src/main/java/cn/hutool/core/annotation/AnnotationProxy.java b/hutool-core/src/main/java/cn/hutool/core/annotation/AnnotationProxy.java new file mode 100644 index 0000000000000000000000000000000000000000..bcf2ba4b5829e8b74054c3ee227c443c4ff73389 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/annotation/AnnotationProxy.java @@ -0,0 +1,88 @@ +package cn.hutool.core.annotation; + +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; + +import java.io.Serializable; +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; + +/** + * 注解代理
+ * 通过代理指定注解,可以自定义调用注解的方法逻辑,如支持{@link Alias} 注解 + * + * @param 注解类型 + * @since 5.7.23 + */ +public class AnnotationProxy implements Annotation, InvocationHandler, Serializable { + private static final long serialVersionUID = 1L; + + private final T annotation; + private final Class type; + private final Map attributes; + + /** + * 构造 + * + * @param annotation 注解 + */ + public AnnotationProxy(T annotation) { + this.annotation = annotation; + //noinspection unchecked + this.type = (Class) annotation.annotationType(); + this.attributes = initAttributes(); + } + + + @Override + public Class annotationType() { + return type; + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + + // 注解别名 + Alias alias = method.getAnnotation(Alias.class); + if(null != alias){ + final String name = alias.value(); + if(StrUtil.isNotBlank(name)){ + if(false == attributes.containsKey(name)){ + throw new IllegalArgumentException(StrUtil.format("No method for alias: [{}]", name)); + } + return attributes.get(name); + } + } + + final Object value = attributes.get(method.getName()); + if (value != null) { + return value; + } + return method.invoke(this, args); + } + + /** + * 初始化注解的属性
+ * 此方法预先调用所有注解的方法,将注解方法值缓存于attributes中 + * + * @return 属性(方法结果)映射 + */ + private Map initAttributes() { + final Method[] methods = ReflectUtil.getMethods(this.type); + final Map attributes = new HashMap<>(methods.length, 1); + + for (Method method : methods) { + // 跳过匿名内部类自动生成的方法 + if (method.isSynthetic()) { + continue; + } + + attributes.put(method.getName(), ReflectUtil.invoke(this.annotation, method)); + } + + return attributes; + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/annotation/AnnotationUtil.java b/hutool-core/src/main/java/cn/hutool/core/annotation/AnnotationUtil.java index 1049a517ae7ef2fec7a685e58bb60ae79d83856c..be44f080a59adf921fccd1ff6cd796a7ad6df39f 100644 --- a/hutool-core/src/main/java/cn/hutool/core/annotation/AnnotationUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/annotation/AnnotationUtil.java @@ -42,7 +42,7 @@ public class AnnotationUtil { /** * 获取指定注解 * - * @param annotationEle {@link AnnotatedElement},可以是Class、Method、Field、Constructor、ReflectPermission + * @param annotationEle {@link AnnotatedElement},可以是Class、Method、Field、Constructor、ReflectPermission * @param isToCombination 是否为转换为组合注解 * @return 注解对象 */ @@ -205,9 +205,9 @@ public class AnnotationUtil { /** * 设置新的注解的属性(字段)值 * - * @param annotation 注解对象 + * @param annotation 注解对象 * @param annotationField 注解属性(字段)名称 - * @param value 要更新的属性值 + * @param value 要更新的属性值 * @since 5.5.2 */ @SuppressWarnings({"rawtypes", "unchecked"}) @@ -215,4 +215,19 @@ public class AnnotationUtil { final Map memberValues = (Map) ReflectUtil.getFieldValue(Proxy.getInvocationHandler(annotation), "memberValues"); memberValues.put(annotationField, value); } + + /** + * 获取别名支持后的注解 + * + * @param annotationEle 被注解的类 + * @param annotationType 注解类型Class + * @param 注解类型 + * @return 别名支持后的注解 + * @since 5.7.23 + */ + @SuppressWarnings("unchecked") + public static T getAnnotationAlias(AnnotatedElement annotationEle, Class annotationType) { + final T annotation = getAnnotation(annotationEle, annotationType); + return (T) Proxy.newProxyInstance(annotationType.getClassLoader(), new Class[]{annotationType}, new AnnotationProxy<>(annotation)); + } } diff --git a/hutool-core/src/main/java/cn/hutool/core/bean/BeanUtil.java b/hutool-core/src/main/java/cn/hutool/core/bean/BeanUtil.java index b66b15f0df1936a1a2412879fef011196edc374f..84e7c59178f32c2605c37fa07a293bfe0d5d9442 100644 --- a/hutool-core/src/main/java/cn/hutool/core/bean/BeanUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/bean/BeanUtil.java @@ -225,8 +225,8 @@ public class BeanUtil { */ private static Map internalGetPropertyDescriptorMap(Class clazz, boolean ignoreCase) throws BeanException { final PropertyDescriptor[] propertyDescriptors = getPropertyDescriptors(clazz); - final Map map = ignoreCase ? new CaseInsensitiveMap<>(propertyDescriptors.length, 1) - : new HashMap<>((int) (propertyDescriptors.length), 1); + final Map map = ignoreCase ? new CaseInsensitiveMap<>(propertyDescriptors.length, 1f) + : new HashMap<>(propertyDescriptors.length, 1); for (PropertyDescriptor propertyDescriptor : propertyDescriptors) { map.put(propertyDescriptor.getName(), propertyDescriptor); diff --git a/hutool-core/src/main/java/cn/hutool/core/builder/EqualsBuilder.java b/hutool-core/src/main/java/cn/hutool/core/builder/EqualsBuilder.java index bb7c7e46cd2fe188b70de5e856860febbd8597b3..e68ccdf3872d8733790aa9fb50f804655eb61d3b 100644 --- a/hutool-core/src/main/java/cn/hutool/core/builder/EqualsBuilder.java +++ b/hutool-core/src/main/java/cn/hutool/core/builder/EqualsBuilder.java @@ -70,7 +70,7 @@ public class EqualsBuilder implements Builder { * Converters value pair into a register pair. *

* - * @param lhs this object + * @param lhs {@code this} object * @param rhs the other object * @return the pair */ @@ -82,15 +82,15 @@ public class EqualsBuilder implements Builder { /** *

- * Returns true if the registry contains the given object pair. + * Returns {@code true} if the registry contains the given object pair. * Used by the reflection methods to avoid infinite loops. * Objects might be swapped therefore a check is needed if the object pair * is registered in given or swapped order. *

* - * @param lhs this object to lookup in registry + * @param lhs {@code this} object to lookup in registry * @param rhs the other object to lookup on registry - * @return boolean true if the registry contains the given object. + * @return boolean {@code true} if the registry contains the given object. * @since 3.0 */ static boolean isRegistered(final Object lhs, final Object rhs) { @@ -108,7 +108,7 @@ public class EqualsBuilder implements Builder { * Used by the reflection methods to avoid infinite loops. *

* - * @param lhs this object to register + * @param lhs {@code this} object to register * @param rhs the other object to register */ static void register(final Object lhs, final Object rhs) { @@ -131,7 +131,7 @@ public class EqualsBuilder implements Builder { *

* Used by the reflection methods to avoid infinite loops. * - * @param lhs this object to unregister + * @param lhs {@code this} object to unregister * @param rhs the other object to unregister * @since 3.0 */ @@ -170,7 +170,7 @@ public class EqualsBuilder implements Builder { * @param lhs 此对象 * @param rhs 另一个对象 * @param excludeFields 排除的字段集合,如果有不参与计算equals的字段加入此集合即可 - * @return 两个对象是否equals,是返回true + * @return 两个对象是否equals,是返回{@code true} */ public static boolean reflectionEquals(final Object lhs, final Object rhs, final Collection excludeFields) { return reflectionEquals(lhs, rhs, ArrayUtil.toArray(excludeFields, String.class)); @@ -182,62 +182,62 @@ public class EqualsBuilder implements Builder { * @param lhs 此对象 * @param rhs 另一个对象 * @param excludeFields 排除的字段集合,如果有不参与计算equals的字段加入此集合即可 - * @return 两个对象是否equals,是返回true + * @return 两个对象是否equals,是返回{@code true} */ public static boolean reflectionEquals(final Object lhs, final Object rhs, final String... excludeFields) { return reflectionEquals(lhs, rhs, false, null, excludeFields); } /** - *

This method uses reflection to determine if the two Objects + *

This method uses reflection to determine if the two {@code Object}s * are equal.

* - *

It uses AccessibleObject.setAccessible to gain access to private + *

It uses {@code AccessibleObject.setAccessible} to gain access to private * fields. This means that it will throw a security exception if run under * a security manager, if the permissions are not set up correctly. It is also * not as efficient as testing explicitly. Non-primitive fields are compared using - * equals().

+ * {@code equals()}.

* - *

If the TestTransients parameter is set to true, transient + *

If the TestTransients parameter is set to {@code true}, transient * members will be tested, otherwise they are ignored, as they are likely - * derived fields, and not part of the value of the Object.

+ * derived fields, and not part of the value of the {@code Object}.

* *

Static fields will not be tested. Superclass fields will be included.

* - * @param lhs this object + * @param lhs {@code this} object * @param rhs the other object * @param testTransients whether to include transient fields - * @return true if the two Objects have tested equals. + * @return {@code true} if the two Objects have tested equals. */ public static boolean reflectionEquals(final Object lhs, final Object rhs, final boolean testTransients) { return reflectionEquals(lhs, rhs, testTransients, null); } /** - *

This method uses reflection to determine if the two Objects + *

This method uses reflection to determine if the two {@code Object}s * are equal.

* - *

It uses AccessibleObject.setAccessible to gain access to private + *

It uses {@code AccessibleObject.setAccessible} to gain access to private * fields. This means that it will throw a security exception if run under * a security manager, if the permissions are not set up correctly. It is also * not as efficient as testing explicitly. Non-primitive fields are compared using - * equals().

+ * {@code equals()}.

* - *

If the testTransients parameter is set to true, transient + *

If the testTransients parameter is set to {@code true}, transient * members will be tested, otherwise they are ignored, as they are likely - * derived fields, and not part of the value of the Object.

+ * derived fields, and not part of the value of the {@code Object}.

* *

Static fields will not be included. Superclass fields will be appended * up to and including the specified superclass. A null superclass is treated * as java.lang.Object.

* - * @param lhs this object + * @param lhs {@code this} object * @param rhs the other object * @param testTransients whether to include transient fields * @param reflectUpToClass the superclass to reflect up to (inclusive), - * may be null + * may be {@code null} * @param excludeFields array of field names to exclude from testing - * @return true if the two Objects have tested equals. + * @return {@code true} if the two Objects have tested equals. * @since 2.0 */ public static boolean reflectionEquals(final Object lhs, final Object rhs, final boolean testTransients, final Class reflectUpToClass, @@ -343,9 +343,9 @@ public class EqualsBuilder implements Builder { //------------------------------------------------------------------------- /** - *

Adds the result of super.equals() to this builder.

+ *

Adds the result of {@code super.equals()} to this builder.

* - * @param superEquals the result of calling super.equals() + * @param superEquals the result of calling {@code super.equals()} * @return EqualsBuilder - used to chain calls. * @since 2.0 */ @@ -360,8 +360,8 @@ public class EqualsBuilder implements Builder { //------------------------------------------------------------------------- /** - *

Test if two Objects are equal using their - * equals method.

+ *

Test if two {@code Object}s are equal using their + * {@code equals} method.

* * @param lhs the left hand object * @param rhs the right hand object @@ -388,11 +388,11 @@ public class EqualsBuilder implements Builder { /** *

- * Test if two long s are equal. + * Test if two {@code long} s are equal. *

* - * @param lhs the left hand long - * @param rhs the right hand long + * @param lhs the left hand {@code long} + * @param rhs the right hand {@code long} * @return EqualsBuilder - used to chain calls. */ public EqualsBuilder append(final long lhs, final long rhs) { @@ -404,10 +404,10 @@ public class EqualsBuilder implements Builder { } /** - *

Test if two ints are equal.

+ *

Test if two {@code int}s are equal.

* - * @param lhs the left hand int - * @param rhs the right hand int + * @param lhs the left hand {@code int} + * @param rhs the right hand {@code int} * @return EqualsBuilder - used to chain calls. */ public EqualsBuilder append(final int lhs, final int rhs) { @@ -419,10 +419,10 @@ public class EqualsBuilder implements Builder { } /** - *

Test if two shorts are equal.

+ *

Test if two {@code short}s are equal.

* - * @param lhs the left hand short - * @param rhs the right hand short + * @param lhs the left hand {@code short} + * @param rhs the right hand {@code short} * @return EqualsBuilder - used to chain calls. */ public EqualsBuilder append(final short lhs, final short rhs) { @@ -434,10 +434,10 @@ public class EqualsBuilder implements Builder { } /** - *

Test if two chars are equal.

+ *

Test if two {@code char}s are equal.

* - * @param lhs the left hand char - * @param rhs the right hand char + * @param lhs the left hand {@code char} + * @param rhs the right hand {@code char} * @return EqualsBuilder - used to chain calls. */ public EqualsBuilder append(final char lhs, final char rhs) { @@ -449,10 +449,10 @@ public class EqualsBuilder implements Builder { } /** - *

Test if two bytes are equal.

+ *

Test if two {@code byte}s are equal.

* - * @param lhs the left hand byte - * @param rhs the right hand byte + * @param lhs the left hand {@code byte} + * @param rhs the right hand {@code byte} * @return EqualsBuilder - used to chain calls. */ public EqualsBuilder append(final byte lhs, final byte rhs) { @@ -464,16 +464,16 @@ public class EqualsBuilder implements Builder { } /** - *

Test if two doubles are equal by testing that the - * pattern of bits returned by doubleToLong are equal.

+ *

Test if two {@code double}s are equal by testing that the + * pattern of bits returned by {@code doubleToLong} are equal.

* - *

This handles NaNs, Infinities, and -0.0.

+ *

This handles NaNs, Infinities, and {@code -0.0}.

* *

It is compatible with the hash code generated by - * HashCodeBuilder.

+ * {@code HashCodeBuilder}.

* - * @param lhs the left hand double - * @param rhs the right hand double + * @param lhs the left hand {@code double} + * @param rhs the right hand {@code double} * @return EqualsBuilder - used to chain calls. */ public EqualsBuilder append(final double lhs, final double rhs) { @@ -484,16 +484,16 @@ public class EqualsBuilder implements Builder { } /** - *

Test if two floats are equal byt testing that the + *

Test if two {@code float}s are equal byt testing that the * pattern of bits returned by doubleToLong are equal.

* - *

This handles NaNs, Infinities, and -0.0.

+ *

This handles NaNs, Infinities, and {@code -0.0}.

* *

It is compatible with the hash code generated by - * HashCodeBuilder.

+ * {@code HashCodeBuilder}.

* - * @param lhs the left hand float - * @param rhs the right hand float + * @param lhs the left hand {@code float} + * @param rhs the right hand {@code float} * @return EqualsBuilder - used to chain calls. */ public EqualsBuilder append(final float lhs, final float rhs) { @@ -504,10 +504,10 @@ public class EqualsBuilder implements Builder { } /** - *

Test if two booleanss are equal.

+ *

Test if two {@code booleans}s are equal.

* - * @param lhs the left hand boolean - * @param rhs the right hand boolean + * @param lhs the left hand {@code boolean} + * @param rhs the right hand {@code boolean} * @return EqualsBuilder - used to chain calls. */ public EqualsBuilder append(final boolean lhs, final boolean rhs) { @@ -519,7 +519,7 @@ public class EqualsBuilder implements Builder { } /** - *

Returns true if the fields that have been checked + *

Returns {@code true} if the fields that have been checked * are all equal.

* * @return boolean @@ -529,11 +529,11 @@ public class EqualsBuilder implements Builder { } /** - *

Returns true if the fields that have been checked + *

Returns {@code true} if the fields that have been checked * are all equal.

* - * @return true if all of the fields that have been checked - * are equal, false otherwise. + * @return {@code true} if all of the fields that have been checked + * are equal, {@code false} otherwise. * @since 3.0 */ @Override @@ -542,7 +542,7 @@ public class EqualsBuilder implements Builder { } /** - * Sets the isEquals value. + * Sets the {@code isEquals} value. * * @param isEquals The value to set. * @return this diff --git a/hutool-core/src/main/java/cn/hutool/core/collection/CollUtil.java b/hutool-core/src/main/java/cn/hutool/core/collection/CollUtil.java index dd22d5275b4b1ddc1d190879502ca20f0b836c41..21120cd252170e55614716adc8de0335e48b07a2 100644 --- a/hutool-core/src/main/java/cn/hutool/core/collection/CollUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/collection/CollUtil.java @@ -1178,20 +1178,10 @@ public class CollUtil { return collection; } - Collection collection2 = ObjectUtil.clone(collection); - if (null == collection2) { - // 不支持clone - collection2 = create(collection.getClass()); - } - if (isEmpty(collection2)) { + final Collection collection2 = create(collection.getClass()); + if (isEmpty(collection)) { return collection2; } - try { - collection2.clear(); - } catch (UnsupportedOperationException e) { - // 克隆后的对象不支持清空,说明为不可变集合对象,使用默认的ArrayList保存结果 - collection2 = new ArrayList<>(); - } T modified; for (T t : collection) { diff --git a/hutool-core/src/main/java/cn/hutool/core/collection/IterUtil.java b/hutool-core/src/main/java/cn/hutool/core/collection/IterUtil.java index f03451bc3284b33e612c8f03c510c68f0a9ce5a6..2634eefae298917b9f4a5dce793b368199dc2293 100644 --- a/hutool-core/src/main/java/cn/hutool/core/collection/IterUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/collection/IterUtil.java @@ -906,4 +906,19 @@ public class IterUtil { // 当两个Iterable长度不一致时返回false return false == (it1.hasNext() || it2.hasNext()); } + + /** + * 清空指定{@link Iterator},此方法遍历后调用{@link Iterator#remove()}移除每个元素 + * + * @param iterator {@link Iterator} + * @since 5.7.23 + */ + public static void clear(Iterator iterator) { + if (null != iterator) { + while (iterator.hasNext()) { + iterator.next(); + iterator.remove(); + } + } + } } diff --git a/hutool-core/src/main/java/cn/hutool/core/collection/UniqueKeySet.java b/hutool-core/src/main/java/cn/hutool/core/collection/UniqueKeySet.java new file mode 100644 index 0000000000000000000000000000000000000000..f4fbcb15c809f0d253a0430001ca1dd7eb5e2bc5 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/collection/UniqueKeySet.java @@ -0,0 +1,152 @@ +package cn.hutool.core.collection; + +import cn.hutool.core.map.MapBuilder; +import cn.hutool.core.util.ObjectUtil; + +import java.io.Serializable; +import java.util.AbstractSet; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.function.Function; + +/** + * 唯一键的Set
+ * 通过自定义唯一键,通过{@link #uniqueGenerator}生成节点对象对应的键作为Map的key,确定唯一
+ * 此Set与HashSet不同的是,HashSet依赖于{@link Object#equals(Object)}确定唯一
+ * 但是很多时候我们无法对对象进行修改,此时在外部定义一个唯一规则,即可完成去重。 + *
+ * {@code Set set = new UniqueKeySet<>(UniqueTestBean::getId);}
+ * 
+ * + * @param 唯一键类型 + * @param 值对象 + * @author looly + * @since 5.7.23 + */ +public class UniqueKeySet extends AbstractSet implements Serializable { + private static final long serialVersionUID = 1L; + + private Map map; + private final Function uniqueGenerator; + + //region 构造 + + /** + * 构造 + * + * @param uniqueGenerator 唯一键生成规则函数,用于生成对象对应的唯一键 + */ + public UniqueKeySet(Function uniqueGenerator) { + this(false, uniqueGenerator); + } + + /** + * 构造 + * + * @param isLinked 是否保持加入顺序 + * @param uniqueGenerator 唯一键生成规则函数,用于生成对象对应的唯一键 + */ + public UniqueKeySet(boolean isLinked, Function uniqueGenerator) { + this(MapBuilder.create(isLinked), uniqueGenerator); + } + + /** + * 构造 + * + * @param initialCapacity 初始容量 + * @param loadFactor 增长因子 + * @param uniqueGenerator 唯一键生成规则函数,用于生成对象对应的唯一键 + */ + public UniqueKeySet(int initialCapacity, float loadFactor, Function uniqueGenerator) { + this(MapBuilder.create(new HashMap<>(initialCapacity, loadFactor)), uniqueGenerator); + } + + /** + * 构造 + * + * @param builder 初始Map,定义了Map类型 + * @param uniqueGenerator 唯一键生成规则函数,用于生成对象对应的唯一键 + */ + public UniqueKeySet(MapBuilder builder, Function uniqueGenerator) { + this.map = builder.build(); + this.uniqueGenerator = uniqueGenerator; + } + //endregion + + @Override + public Iterator iterator() { + return map.values().iterator(); + } + + @Override + public int size() { + return map.size(); + } + + @Override + public boolean isEmpty() { + return map.isEmpty(); + } + + @Override + public boolean contains(Object o) { + //noinspection unchecked + return map.containsKey(this.uniqueGenerator.apply((V) o)); + } + + @Override + public boolean add(V v) { + return null == map.put(this.uniqueGenerator.apply(v), v); + } + + /** + * 加入值,如果值已经存在,则忽略之 + * + * @param v 值 + * @return 是否成功加入 + */ + public boolean addIfAbsent(V v) { + return null == map.putIfAbsent(this.uniqueGenerator.apply(v), v); + } + + /** + * 加入集合中所有的值,如果值已经存在,则忽略之 + * + * @param c 集合 + * @return 是否有一个或多个被加入成功 + */ + public boolean addAllIfAbsent(Collection c) { + boolean modified = false; + for (V v : c) + if (addIfAbsent(v)) { + modified = true; + } + return modified; + } + + @Override + public boolean remove(Object o) { + //noinspection unchecked + return null != map.remove(this.uniqueGenerator.apply((V) o)); + } + + @Override + public void clear() { + map.clear(); + } + + @Override + @SuppressWarnings("unchecked") + public UniqueKeySet clone() { + try { + UniqueKeySet newSet = (UniqueKeySet) super.clone(); + newSet.map = ObjectUtil.clone(this.map); + return newSet; + } catch (CloneNotSupportedException e) { + throw new InternalError(e); + } + } + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/convert/ConverterRegistry.java b/hutool-core/src/main/java/cn/hutool/core/convert/ConverterRegistry.java index 96b19c52f0d47f3773bba5fb6a92f36627e57007..bbc1fd347045c30cb275d14350dab26d3386052e 100644 --- a/hutool-core/src/main/java/cn/hutool/core/convert/ConverterRegistry.java +++ b/hutool-core/src/main/java/cn/hutool/core/convert/ConverterRegistry.java @@ -77,6 +77,8 @@ import java.util.concurrent.atomic.AtomicIntegerArray; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLongArray; import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.atomic.DoubleAdder; +import java.util.concurrent.atomic.LongAdder; /** * 转换器登记中心 @@ -389,11 +391,13 @@ public class ConverterRegistry implements Serializable { defaultConverterMap.put(Integer.class, new NumberConverter(Integer.class)); defaultConverterMap.put(AtomicInteger.class, new NumberConverter(AtomicInteger.class));// since 3.0.8 defaultConverterMap.put(Long.class, new NumberConverter(Long.class)); + defaultConverterMap.put(LongAdder.class, new NumberConverter(LongAdder.class)); defaultConverterMap.put(AtomicLong.class, new NumberConverter(AtomicLong.class));// since 3.0.8 defaultConverterMap.put(Byte.class, new NumberConverter(Byte.class)); defaultConverterMap.put(Short.class, new NumberConverter(Short.class)); defaultConverterMap.put(Float.class, new NumberConverter(Float.class)); defaultConverterMap.put(Double.class, new NumberConverter(Double.class)); + defaultConverterMap.put(DoubleAdder.class, new NumberConverter(DoubleAdder.class)); defaultConverterMap.put(Character.class, new CharacterConverter()); defaultConverterMap.put(Boolean.class, new BooleanConverter()); defaultConverterMap.put(AtomicBoolean.class, new AtomicBooleanConverter());// since 3.0.8 diff --git a/hutool-core/src/main/java/cn/hutool/core/convert/NumberChineseFormatter.java b/hutool-core/src/main/java/cn/hutool/core/convert/NumberChineseFormatter.java index d6da1d22b578944fd72f2ec6fe446061b9c1b1fb..6ec3f0e1601799a15bfceec6d898f2a6681258e3 100644 --- a/hutool-core/src/main/java/cn/hutool/core/convert/NumberChineseFormatter.java +++ b/hutool-core/src/main/java/cn/hutool/core/convert/NumberChineseFormatter.java @@ -40,7 +40,6 @@ public class NumberChineseFormatter { new ChineseUnit('亿', 1_0000_0000, true), }; - /** * 阿拉伯数字转换成中文,小数点后四舍五入保留两位. 使用于整数、小数的转换. * @@ -53,15 +52,26 @@ public class NumberChineseFormatter { } /** - * 阿拉伯数字转换成中文,小数点后四舍五入保留两位. 使用于整数、小数的转换. + * 阿拉伯数字转换成中文. + * + *

主要是对发票票面金额转换的扩展 + *

如:-12.32 + *

发票票面转换为:(负数)壹拾贰圆叁角贰分 + *

而非:负壹拾贰元叁角贰分 + *

共两点不同:1、(负数) 而非 负;2、圆 而非 元 + * 2022/3/9 * * @param amount 数字 * @param isUseTraditional 是否使用繁体 - * @param isMoneyMode 是否为金额模式 - * @return 中文 + * @param isMoneyMode 是否金额模式 + * @param negativeName 负号转换名称 如:负、(负数) + * @param unitName 单位名称 如:元、圆 + * @return java.lang.String + * @author machuanpeng + * @since 5.7.23 */ - public static String format(double amount, boolean isUseTraditional, boolean isMoneyMode) { - if(0 == amount){ + public static String format(double amount, boolean isUseTraditional, boolean isMoneyMode, String negativeName, String unitName) { + if (0 == amount) { return "零"; } Assert.checkBetween(amount, -99_9999_9999_9999.99, 99_9999_9999_9999.99, @@ -71,7 +81,7 @@ public class NumberChineseFormatter { // 负数 if (amount < 0) { - chineseStr.append("负"); + chineseStr.append(StrUtil.isNullOrUndefined(negativeName) ? "负" : negativeName); amount = -amount; } @@ -82,44 +92,44 @@ public class NumberChineseFormatter { yuan = yuan / 10; // 元 - if(false == isMoneyMode || 0 != yuan){ + if (false == isMoneyMode || 0 != yuan) { // 金额模式下,无需“零元” chineseStr.append(longToChinese(yuan, isUseTraditional)); - if(isMoneyMode){ - chineseStr.append("元"); + if (isMoneyMode) { + chineseStr.append(StrUtil.isNullOrUndefined(unitName) ? "元" : unitName); } } - if(0 == jiao && 0 == fen){ + if (0 == jiao && 0 == fen) { //无小数部分的金额结尾 - if(isMoneyMode){ + if (isMoneyMode) { chineseStr.append("整"); } return chineseStr.toString(); } // 小数部分 - if(false == isMoneyMode){ + if (false == isMoneyMode) { chineseStr.append("点"); } // 角 - if(0 == yuan && 0 == jiao){ + if (0 == yuan && 0 == jiao) { // 元和角都为0时,只有非金额模式下补“零” - if(false == isMoneyMode){ + if (false == isMoneyMode) { chineseStr.append("零"); } - }else{ + } else { chineseStr.append(numberToChinese(jiao, isUseTraditional)); - if(isMoneyMode && 0 != jiao){ + if (isMoneyMode && 0 != jiao) { chineseStr.append("角"); } } // 分 - if(0 != fen){ + if (0 != fen) { chineseStr.append(numberToChinese(fen, isUseTraditional)); - if(isMoneyMode){ + if (isMoneyMode) { chineseStr.append("分"); } } @@ -127,16 +137,28 @@ public class NumberChineseFormatter { return chineseStr.toString(); } + /** + * 阿拉伯数字转换成中文,小数点后四舍五入保留两位. 使用于整数、小数的转换. + * + * @param amount 数字 + * @param isUseTraditional 是否使用繁体 + * @param isMoneyMode 是否为金额模式 + * @return 中文 + */ + public static String format(double amount, boolean isUseTraditional, boolean isMoneyMode) { + return format(amount, isUseTraditional, isMoneyMode, "负", "元"); + } + /** * 阿拉伯数字(支持正负整数)转换成中文 * - * @param amount 数字 + * @param amount 数字 * @param isUseTraditional 是否使用繁体 * @return 中文 * @since 5.7.17 */ - public static String format(long amount, boolean isUseTraditional){ - if(0 == amount){ + public static String format(long amount, boolean isUseTraditional) { + if (0 == amount) { return "零"; } Assert.checkBetween(amount, -99_9999_9999_9999.99, 99_9999_9999_9999.99, @@ -179,16 +201,16 @@ public class NumberChineseFormatter { * 格式化-999~999之间的数字
* 这个方法显示10~19以下的数字时使用"十一"而非"一十一"。 * - * @param amount 数字 + * @param amount 数字 * @param isUseTraditional 是否使用繁体 * @return 中文 * @since 5.7.17 */ - public static String formatThousand(int amount, boolean isUseTraditional){ + public static String formatThousand(int amount, boolean isUseTraditional) { Assert.checkBetween(amount, -999, 999, "Number support only: (-999 ~ 999)!"); final String chinese = thousandToChinese(amount, isUseTraditional); - if(amount < 20 && amount > 10){ + if (amount < 20 && amount >= 10) { // "十一"而非"一十一" return chinese.substring(1); } @@ -218,7 +240,7 @@ public class NumberChineseFormatter { * @return 中文 */ private static String longToChinese(long amount, boolean isUseTraditional) { - if(0 == amount){ + if (0 == amount) { return "零"; } @@ -235,11 +257,11 @@ public class NumberChineseFormatter { // 千 partValue = parts[0]; - if(partValue > 0){ + if (partValue > 0) { partChinese = thousandToChinese(partValue, isUseTraditional); chineseStr.insert(0, partChinese); - if(partValue < 1000){ + if (partValue < 1000) { // 和万位之间空0,则补零,如一万零三百 addPreZero(chineseStr); } @@ -247,26 +269,26 @@ public class NumberChineseFormatter { // 万 partValue = parts[1]; - if(partValue > 0){ - if((partValue % 10 == 0 && parts[0] > 0)){ + if (partValue > 0) { + if ((partValue % 10 == 0 && parts[0] > 0)) { // 如果"万"的个位是0,则补零,如十万零八千 addPreZero(chineseStr); } partChinese = thousandToChinese(partValue, isUseTraditional); chineseStr.insert(0, partChinese + "万"); - if(partValue < 1000){ + if (partValue < 1000) { // 和亿位之间空0,则补零,如一亿零三百万 addPreZero(chineseStr); } - } else{ + } else { addPreZero(chineseStr); } // 亿 partValue = parts[2]; - if(partValue > 0){ - if((partValue % 10 == 0 && parts[1] > 0)){ + if (partValue > 0) { + if ((partValue % 10 == 0 && parts[1] > 0)) { // 如果"万"的个位是0,则补零,如十万零八千 addPreZero(chineseStr); } @@ -274,25 +296,25 @@ public class NumberChineseFormatter { partChinese = thousandToChinese(partValue, isUseTraditional); chineseStr.insert(0, partChinese + "亿"); - if(partValue < 1000){ + if (partValue < 1000) { // 和万亿位之间空0,则补零,如一万亿零三百亿 addPreZero(chineseStr); } - } else{ + } else { addPreZero(chineseStr); } // 万亿 partValue = parts[3]; - if(partValue > 0){ - if(parts[2] == 0){ + if (partValue > 0) { + if (parts[2] == 0) { chineseStr.insert(0, "亿"); } partChinese = thousandToChinese(partValue, isUseTraditional); chineseStr.insert(0, partChinese + "万"); } - if(StrUtil.isNotEmpty(chineseStr) && '零' == chineseStr.charAt(0)){ + if (StrUtil.isNotEmpty(chineseStr) && '零' == chineseStr.charAt(0)) { return chineseStr.substring(1); } @@ -386,7 +408,7 @@ public class NumberChineseFormatter { } else { // 非节单位,和单位前的单数字组合为值 int unitNumber = number; - if(0 == number && 0 == i){ + if (0 == number && 0 == i) { // issue#1726,对于单位开头的数组,默认赋予1 // 十二 -> 一十二 // 百二 -> 一百二 @@ -502,12 +524,12 @@ public class NumberChineseFormatter { } } - private static void addPreZero(StringBuilder chineseStr){ - if(StrUtil.isEmpty(chineseStr)){ + private static void addPreZero(StringBuilder chineseStr) { + if (StrUtil.isEmpty(chineseStr)) { return; } final char c = chineseStr.charAt(0); - if('零' != c){ + if ('零' != c) { chineseStr.insert(0, '零'); } } diff --git a/hutool-core/src/main/java/cn/hutool/core/convert/impl/NumberConverter.java b/hutool-core/src/main/java/cn/hutool/core/convert/impl/NumberConverter.java index 36242cab9e781251660971b72c5af401a06e2b7d..421628c91e8874b3e07e0bf1d8f7346c4403a433 100644 --- a/hutool-core/src/main/java/cn/hutool/core/convert/impl/NumberConverter.java +++ b/hutool-core/src/main/java/cn/hutool/core/convert/impl/NumberConverter.java @@ -186,7 +186,7 @@ public class NumberConverter extends AbstractConverter { return StrUtil.isBlank(valueStr) ? null : NumberUtil.parseFloat(valueStr); } else if (Double.class == targetType) { if (value instanceof Number) { - return ((Number) value).doubleValue(); + return NumberUtil.toDouble((Number) value); } else if (value instanceof Boolean) { return BooleanUtil.toDoubleObj((Boolean) value); } @@ -194,7 +194,7 @@ public class NumberConverter extends AbstractConverter { return StrUtil.isBlank(valueStr) ? null : NumberUtil.parseDouble(valueStr); } else if (DoubleAdder.class == targetType) { //jdk8 新增 - final Number number = convert(value, Long.class, toStrFunc); + final Number number = convert(value, Double.class, toStrFunc); if (null != number) { final DoubleAdder doubleAdder = new DoubleAdder(); doubleAdder.add(number.doubleValue()); diff --git a/hutool-core/src/main/java/cn/hutool/core/io/FileUtil.java b/hutool-core/src/main/java/cn/hutool/core/io/FileUtil.java index e0622f7fc1834ce3eb410d4c319d5e5dd11a8001..8cd3356f490ad1b04af6946799902030c03f7bab 100644 --- a/hutool-core/src/main/java/cn/hutool/core/io/FileUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/io/FileUtil.java @@ -700,7 +700,7 @@ public class FileUtil extends PathUtil { if (null == file) { return null; } - return mkdir(file.getParentFile()); + return mkdir(getParent(file, 1)); } /** diff --git a/hutool-core/src/main/java/cn/hutool/core/io/file/FileCopier.java b/hutool-core/src/main/java/cn/hutool/core/io/file/FileCopier.java index fbdc4938f86f856bc01e4adacfc900659b219e8d..3e7df8220623066e4102dc134785dcc46670d6af 100644 --- a/hutool-core/src/main/java/cn/hutool/core/io/file/FileCopier.java +++ b/hutool-core/src/main/java/cn/hutool/core/io/file/FileCopier.java @@ -267,8 +267,7 @@ public class FileCopier extends SrcToDestCopier{ } }else { //路径不存在则创建父目录 - //noinspection ResultOfMethodCallIgnored - dest.getParentFile().mkdirs(); + FileUtil.mkParentDirs(dest); } final ArrayList optionList = new ArrayList<>(2); diff --git a/hutool-core/src/main/java/cn/hutool/core/io/file/FileNameUtil.java b/hutool-core/src/main/java/cn/hutool/core/io/file/FileNameUtil.java index 6dca23e7d24411b9374c0e35c7264759f28fc17e..5670f4383252f2c267dcf1c000ed47dea57a4415 100644 --- a/hutool-core/src/main/java/cn/hutool/core/io/file/FileNameUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/io/file/FileNameUtil.java @@ -42,6 +42,11 @@ public class FileNameUtil { */ private static final Pattern FILE_NAME_INVALID_PATTERN_WIN = Pattern.compile("[\\\\/:*?\"<>|]"); + /** + * 特殊后缀 + */ + private static final CharSequence[] SPECIAL_SUFFIX = {"tar.bz2", "tar.Z", "tar.gz", "tar.xz"}; + // -------------------------------------------------------------------------------------------- name start @@ -222,6 +227,13 @@ public class FileNameUtil { if (index == -1) { return StrUtil.EMPTY; } else { + // issue#I4W5FS@Gitee + int secondToLastIndex = fileName.substring(0, index).lastIndexOf(StrUtil.DOT); + String substr = fileName.substring(secondToLastIndex == -1 ? index : secondToLastIndex + 1); + if (StrUtil.containsAny(substr, SPECIAL_SUFFIX)) { + return substr; + } + String ext = fileName.substring(index + 1); // 扩展名中不能包含路径相关的符号 return StrUtil.containsAny(ext, UNIX_SEPARATOR, WINDOWS_SEPARATOR) ? StrUtil.EMPTY : ext; diff --git a/hutool-core/src/main/java/cn/hutool/core/lang/ObjectId.java b/hutool-core/src/main/java/cn/hutool/core/lang/ObjectId.java index e4a8c06fa56d894bc5fc1a2d19b07a5dff126dc0..d8c7c695511b17b83c67ea9ad0812ffb79cfc441 100644 --- a/hutool-core/src/main/java/cn/hutool/core/lang/ObjectId.java +++ b/hutool-core/src/main/java/cn/hutool/core/lang/ObjectId.java @@ -22,6 +22,21 @@ import java.util.concurrent.atomic.AtomicInteger; * 4. INC 自增计数器。确保同一秒内产生objectId的唯一性。 * * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
时间戳机器ID进程ID自增计数器
4323
+ * * 参考:http://blog.csdn.net/qxc1281/article/details/54021882 * * @author looly diff --git a/hutool-core/src/main/java/cn/hutool/core/map/AbsEntry.java b/hutool-core/src/main/java/cn/hutool/core/map/AbsEntry.java new file mode 100644 index 0000000000000000000000000000000000000000..a2ab5885fe3ea44678ab3def4195d3d8d6201a21 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/map/AbsEntry.java @@ -0,0 +1,46 @@ +package cn.hutool.core.map; + +import cn.hutool.core.util.ObjectUtil; + +import java.util.Map; + +/** + * 抽象的{@link Map.Entry}实现,来自Guava
+ * 实现了默认的{@link #equals(Object)}、{@link #hashCode()}、{@link #toString()}方法。
+ * 默认{@link #setValue(Object)}抛出异常。 + * + * @param 键类型 + * @param 值类型 + * @author Guava + * @since 5.7.23 + */ +public abstract class AbsEntry implements Map.Entry { + + @Override + public V setValue(V value) { + throw new UnsupportedOperationException("Entry is read only."); + } + + @Override + public boolean equals(Object object) { + if (object instanceof Map.Entry) { + final Map.Entry that = (Map.Entry) object; + return ObjectUtil.equals(this.getKey(), that.getKey()) + && ObjectUtil.equals(this.getValue(), that.getValue()); + } + return false; + } + + @Override + public int hashCode() { + //copy from 1.8 HashMap.Node + K k = getKey(); + V v = getValue(); + return ((k == null) ? 0 : k.hashCode()) ^ ((v == null) ? 0 : v.hashCode()); + } + + @Override + public String toString() { + return getKey() + "=" + getValue(); + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/map/CamelCaseLinkedMap.java b/hutool-core/src/main/java/cn/hutool/core/map/CamelCaseLinkedMap.java index fbd2b776cef2d041bc99b742530731131390a424..639bfbfccd210a51b1240dcf55fd730d97201b45 100644 --- a/hutool-core/src/main/java/cn/hutool/core/map/CamelCaseLinkedMap.java +++ b/hutool-core/src/main/java/cn/hutool/core/map/CamelCaseLinkedMap.java @@ -1,7 +1,5 @@ package cn.hutool.core.map; -import cn.hutool.core.util.StrUtil; - import java.util.LinkedHashMap; import java.util.Map; @@ -15,7 +13,7 @@ import java.util.Map; * @param 值类型 * @since 4.0.7 */ -public class CamelCaseLinkedMap extends CustomKeyMap { +public class CamelCaseLinkedMap extends CamelCaseMap { private static final long serialVersionUID = 4043263744224569870L; // ------------------------------------------------------------------------- Constructor start @@ -48,7 +46,7 @@ public class CamelCaseLinkedMap extends CustomKeyMap { * 构造 * * @param loadFactor 加载因子 - * @param m Map + * @param m Map,数据会被默认拷贝到一个新的LinkedHashMap中 */ public CamelCaseLinkedMap(float loadFactor, Map m) { this(m.size(), loadFactor); @@ -65,18 +63,4 @@ public class CamelCaseLinkedMap extends CustomKeyMap { super(new LinkedHashMap<>(initialCapacity, loadFactor)); } // ------------------------------------------------------------------------- Constructor end - - /** - * 将Key转为驼峰风格,如果key为字符串的话 - * - * @param key KEY - * @return 驼峰Key - */ - @Override - protected Object customKey(Object key) { - if (key instanceof CharSequence) { - key = StrUtil.toCamelCase(key.toString()); - } - return key; - } } diff --git a/hutool-core/src/main/java/cn/hutool/core/map/CamelCaseMap.java b/hutool-core/src/main/java/cn/hutool/core/map/CamelCaseMap.java index ea1642d14198437b963177b95405a7ae6047b1e4..4759b72028131fbf3f2389db00fc008639a84a58 100644 --- a/hutool-core/src/main/java/cn/hutool/core/map/CamelCaseMap.java +++ b/hutool-core/src/main/java/cn/hutool/core/map/CamelCaseMap.java @@ -1,24 +1,24 @@ package cn.hutool.core.map; +import cn.hutool.core.util.StrUtil; + import java.util.HashMap; import java.util.Map; -import cn.hutool.core.util.StrUtil; - /** * 驼峰Key风格的Map
* 对KEY转换为驼峰,get("int_value")和get("intValue")获得的值相同,put进入的值也会被覆盖 * - * @author Looly - * * @param 键类型 * @param 值类型 + * @author Looly * @since 4.0.7 */ -public class CamelCaseMap extends CustomKeyMap { +public class CamelCaseMap extends FuncKeyMap { private static final long serialVersionUID = 4043263744224569870L; // ------------------------------------------------------------------------- Constructor start + /** * 构造 */ @@ -48,7 +48,7 @@ public class CamelCaseMap extends CustomKeyMap { * 构造 * * @param loadFactor 加载因子 - * @param m Map + * @param m 初始Map,数据会被默认拷贝到一个新的HashMap中 */ public CamelCaseMap(float loadFactor, Map m) { this(m.size(), loadFactor); @@ -59,24 +59,26 @@ public class CamelCaseMap extends CustomKeyMap { * 构造 * * @param initialCapacity 初始大小 - * @param loadFactor 加载因子 + * @param loadFactor 加载因子 */ public CamelCaseMap(int initialCapacity, float loadFactor) { - super(new HashMap<>(initialCapacity, loadFactor)); + this(MapBuilder.create(new HashMap<>(initialCapacity, loadFactor))); } - // ------------------------------------------------------------------------- Constructor end /** - * 将Key转为驼峰风格,如果key为字符串的话 + * 构造
+ * 注意此构造将传入的Map作为被包装的Map,针对任何修改,传入的Map都会被同样修改。 * - * @param key KEY - * @return 驼峰Key + * @param emptyMapBuilder Map构造器,必须构造空的Map */ - @Override - protected Object customKey(Object key) { - if (key instanceof CharSequence) { - key = StrUtil.toCamelCase(key.toString()); - } - return key; + CamelCaseMap(MapBuilder emptyMapBuilder) { + super(emptyMapBuilder.build(), (key) -> { + if (key instanceof CharSequence) { + key = StrUtil.toCamelCase(key.toString()); + } + //noinspection unchecked + return (K) key; + }); } + // ------------------------------------------------------------------------- Constructor end } diff --git a/hutool-core/src/main/java/cn/hutool/core/map/CaseInsensitiveLinkedMap.java b/hutool-core/src/main/java/cn/hutool/core/map/CaseInsensitiveLinkedMap.java index 51cdd2561a0032e2b8bb120ebf607f2c92ee1ed9..5be588ef377f4e5e5794fdcd8f2194c0a56ba141 100644 --- a/hutool-core/src/main/java/cn/hutool/core/map/CaseInsensitiveLinkedMap.java +++ b/hutool-core/src/main/java/cn/hutool/core/map/CaseInsensitiveLinkedMap.java @@ -13,7 +13,7 @@ import java.util.Map; * @param 值类型 * @since 3.3.1 */ -public class CaseInsensitiveLinkedMap extends CustomKeyMap { +public class CaseInsensitiveLinkedMap extends CaseInsensitiveMap { private static final long serialVersionUID = 4043263744224569870L; // ------------------------------------------------------------------------- Constructor start @@ -64,18 +64,4 @@ public class CaseInsensitiveLinkedMap extends CustomKeyMap { super(new LinkedHashMap<>(initialCapacity, loadFactor)); } // ------------------------------------------------------------------------- Constructor end - - /** - * 将Key转为小写 - * - * @param key KEY - * @return 小写KEY - */ - @Override - protected Object customKey(Object key) { - if (key instanceof CharSequence) { - key = key.toString().toLowerCase(); - } - return key; - } } diff --git a/hutool-core/src/main/java/cn/hutool/core/map/CaseInsensitiveMap.java b/hutool-core/src/main/java/cn/hutool/core/map/CaseInsensitiveMap.java index 0c892b2680344d97df97b8e92c591d6326ad9b57..40f805ad0d7a3a07619391752d3eac94a47f3c44 100644 --- a/hutool-core/src/main/java/cn/hutool/core/map/CaseInsensitiveMap.java +++ b/hutool-core/src/main/java/cn/hutool/core/map/CaseInsensitiveMap.java @@ -13,7 +13,7 @@ import java.util.Map; * @param 值类型 * @since 3.0.2 */ -public class CaseInsensitiveMap extends CustomKeyMap { +public class CaseInsensitiveMap extends FuncKeyMap { private static final long serialVersionUID = 4043263744224569870L; //------------------------------------------------------------------------- Constructor start @@ -34,9 +34,10 @@ public class CaseInsensitiveMap extends CustomKeyMap { } /** - * 构造 + * 构造
+ * 注意此构造将传入的Map作为被包装的Map,针对任何修改,传入的Map都会被同样修改。 * - * @param m Map + * @param m 被包装的自定义Map创建器 */ public CaseInsensitiveMap(Map m) { this(DEFAULT_LOAD_FACTOR, m); @@ -61,21 +62,23 @@ public class CaseInsensitiveMap extends CustomKeyMap { * @param loadFactor 加载因子 */ public CaseInsensitiveMap(int initialCapacity, float loadFactor) { - super(new HashMap<>(initialCapacity, loadFactor)); + this(MapBuilder.create(new HashMap<>(initialCapacity, loadFactor))); } - //------------------------------------------------------------------------- Constructor end /** - * 将Key转为小写 + * 构造
+ * 注意此构造将传入的Map作为被包装的Map,针对任何修改,传入的Map都会被同样修改。 * - * @param key KEY - * @return 小写KEY + * @param emptyMapBuilder 被包装的自定义Map创建器 */ - @Override - protected Object customKey(Object key) { - if (key instanceof CharSequence) { - key = key.toString().toLowerCase(); - } - return key; + CaseInsensitiveMap(MapBuilder emptyMapBuilder) { + super(emptyMapBuilder.build(), (key)->{ + if (key instanceof CharSequence) { + key = key.toString().toLowerCase(); + } + //noinspection unchecked + return (K) key; + }); } + //------------------------------------------------------------------------- Constructor end } diff --git a/hutool-core/src/main/java/cn/hutool/core/map/CaseInsensitiveTreeMap.java b/hutool-core/src/main/java/cn/hutool/core/map/CaseInsensitiveTreeMap.java index 7f8b065f631bf09fa6ede8ab2673051c2589d8bb..69f3ec538f81d6bc1862d8f3a033f0fd37a065db 100755 --- a/hutool-core/src/main/java/cn/hutool/core/map/CaseInsensitiveTreeMap.java +++ b/hutool-core/src/main/java/cn/hutool/core/map/CaseInsensitiveTreeMap.java @@ -15,7 +15,7 @@ import java.util.TreeMap; * @param 值类型 * @since 3.3.1 */ -public class CaseInsensitiveTreeMap extends CustomKeyMap { +public class CaseInsensitiveTreeMap extends CaseInsensitiveMap { private static final long serialVersionUID = 4043263744224569870L; // ------------------------------------------------------------------------- Constructor start @@ -40,7 +40,7 @@ public class CaseInsensitiveTreeMap extends CustomKeyMap { /** * 构造 * - * @param m Map + * @param m Map,初始Map,键值对会被复制到新的TreeMap中 * @since 3.1.2 */ public CaseInsensitiveTreeMap(SortedMap m) { @@ -56,18 +56,4 @@ public class CaseInsensitiveTreeMap extends CustomKeyMap { super(new TreeMap<>(comparator)); } // ------------------------------------------------------------------------- Constructor end - - /** - * 将Key转为小写 - * - * @param key KEY - * @return 小写KEY - */ - @Override - protected Object customKey(Object key) { - if (key instanceof CharSequence) { - key = key.toString().toLowerCase(); - } - return key; - } } diff --git a/hutool-core/src/main/java/cn/hutool/core/map/CustomKeyMap.java b/hutool-core/src/main/java/cn/hutool/core/map/CustomKeyMap.java index 4584943a5ece9fbd84d40a1523f556bd3d925a02..04ed9d371e961d5f016379578d735549126c44d0 100644 --- a/hutool-core/src/main/java/cn/hutool/core/map/CustomKeyMap.java +++ b/hutool-core/src/main/java/cn/hutool/core/map/CustomKeyMap.java @@ -18,11 +18,11 @@ public abstract class CustomKeyMap extends MapWrapper { * 构造
* 通过传入一个Map从而确定Map的类型,子类需创建一个空的Map,而非传入一个已有Map,否则值可能会被修改 * - * @param m Map 被包装的Map + * @param emptyMap Map 被包装的Map,必须为空Map,否则自定义key会无效 * @since 3.1.2 */ - public CustomKeyMap(Map m) { - super(m); + public CustomKeyMap(Map emptyMap) { + super(emptyMap); } @Override diff --git a/hutool-core/src/main/java/cn/hutool/core/map/FixedLinkedHashMap.java b/hutool-core/src/main/java/cn/hutool/core/map/FixedLinkedHashMap.java index 1559fde7a9cf5eb4d0e560630d1f1b04873ecac4..5ddaaa6f95b795ec58f672001944e3a1003303dd 100644 --- a/hutool-core/src/main/java/cn/hutool/core/map/FixedLinkedHashMap.java +++ b/hutool-core/src/main/java/cn/hutool/core/map/FixedLinkedHashMap.java @@ -3,7 +3,8 @@ package cn.hutool.core.map; import java.util.LinkedHashMap; /** - * 固定大小的{@link LinkedHashMap} 实现 + * 固定大小的{@link LinkedHashMap} 实现
+ * 注意此类非线程安全,由于{@link #get(Object)}操作会修改链表的顺序结构,因此也不可以使用读写锁。 * * @author looly * diff --git a/hutool-core/src/main/java/cn/hutool/core/map/FuncKeyMap.java b/hutool-core/src/main/java/cn/hutool/core/map/FuncKeyMap.java index e5ca6a719a1b68f1a79f91a20dc305ca94dc026d..0cc468d617a4a1b9e1445ef3107b6f8ab6449ce8 100644 --- a/hutool-core/src/main/java/cn/hutool/core/map/FuncKeyMap.java +++ b/hutool-core/src/main/java/cn/hutool/core/map/FuncKeyMap.java @@ -19,13 +19,14 @@ public class FuncKeyMap extends CustomKeyMap { // ------------------------------------------------------------------------- Constructor start /** - * 构造 + * 构造
+ * 注意提供的Map中不能有键值对,否则可能导致自定义key失效 * - * @param m Map + * @param emptyMap Map,提供的空map * @param keyFunc 自定义KEY的函数 */ - public FuncKeyMap(Map m, Function keyFunc) { - super(m); + public FuncKeyMap(Map emptyMap, Function keyFunc) { + super(emptyMap); this.keyFunc = keyFunc; } // ------------------------------------------------------------------------- Constructor end diff --git a/hutool-core/src/main/java/cn/hutool/core/map/MapBuilder.java b/hutool-core/src/main/java/cn/hutool/core/map/MapBuilder.java index 5840d9204273dcc0a2c8e7c08edd36f1843bf5a4..2f605d71e0c31e7304fc7dcc9e8cf209f107f704 100644 --- a/hutool-core/src/main/java/cn/hutool/core/map/MapBuilder.java +++ b/hutool-core/src/main/java/cn/hutool/core/map/MapBuilder.java @@ -13,7 +13,7 @@ import java.util.function.Supplier; * @param Value类型 * @since 3.1.1 */ -public class MapBuilder implements Builder> { +public class MapBuilder implements Builder> { private static final long serialVersionUID = 1L; private final Map map; @@ -120,6 +120,17 @@ public class MapBuilder implements Builder> { return this; } + /** + * 清空Map + * + * @return this + * @since 5.7.23 + */ + public MapBuilder clear() { + this.map.clear(); + return this; + } + /** * 创建后的map * diff --git a/hutool-core/src/main/java/cn/hutool/core/map/MapUtil.java b/hutool-core/src/main/java/cn/hutool/core/map/MapUtil.java index b0fb79250579cd4a982319b972031aec0248652f..194777224c7d752f6df3d4544669cc5269b06b54 100644 --- a/hutool-core/src/main/java/cn/hutool/core/map/MapUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/map/MapUtil.java @@ -7,7 +7,6 @@ import cn.hutool.core.lang.Filter; import cn.hutool.core.lang.Pair; import cn.hutool.core.lang.TypeReference; import cn.hutool.core.util.ArrayUtil; -import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ReflectUtil; import cn.hutool.core.util.StrUtil; @@ -623,25 +622,19 @@ public class MapUtil { * @param editor 编辑器接口 * @return 编辑后的Map */ + @SuppressWarnings("unchecked") public static Map edit(Map map, Editor> editor) { if (null == map || null == editor) { return map; } - Map map2 = ObjectUtil.clone(map); + Map map2 = ReflectUtil.newInstanceIfPossible(map.getClass()); if(null == map2){ - // 不支持clone map2 = new HashMap<>(map.size(), 1f); } - if (isEmpty(map2)) { + if (isEmpty(map)) { return map2; } - try { - map2.clear(); - } catch (UnsupportedOperationException e) { - // 克隆后的对象不支持清空,说明为不可变集合对象,使用默认的ArrayList保存结果 - map2 = new HashMap<>(map.size(), 1f); - } Entry modified; for (Entry entry : map.entrySet()) { @@ -690,20 +683,14 @@ public class MapUtil { if(null == map || null == keys){ return map; } - Map map2 = ObjectUtil.clone(map); + + Map map2 = ReflectUtil.newInstanceIfPossible(map.getClass()); if(null == map2){ - // 不支持clone map2 = new HashMap<>(map.size(), 1f); } - if (isEmpty(map2)) { + if (isEmpty(map)) { return map2; } - try { - map2.clear(); - } catch (UnsupportedOperationException e) { - // 克隆后的对象不支持清空,说明为不可变集合对象,使用默认的ArrayList保存结果 - map2 = new HashMap<>(); - } for (K key : keys) { if (map.containsKey(key)) { diff --git a/hutool-core/src/main/java/cn/hutool/core/map/MapWrapper.java b/hutool-core/src/main/java/cn/hutool/core/map/MapWrapper.java index 000b15ea6f5e8930e63084f0c77d92a3d9b79063..3c1ac5936342a54ec05758ddbe70c9ee7f16422a 100644 --- a/hutool-core/src/main/java/cn/hutool/core/map/MapWrapper.java +++ b/hutool-core/src/main/java/cn/hutool/core/map/MapWrapper.java @@ -1,5 +1,7 @@ package cn.hutool.core.map; +import cn.hutool.core.util.ObjectUtil; + import java.io.Serializable; import java.util.Collection; import java.util.Iterator; @@ -30,7 +32,7 @@ public class MapWrapper implements Map, Iterable>, S */ protected static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 - private final Map raw; + private Map raw; /** * 构造 @@ -86,7 +88,6 @@ public class MapWrapper implements Map, Iterable>, S } @Override - @SuppressWarnings("NullableProblems") public void putAll(Map m) { raw.putAll(m); } @@ -97,25 +98,21 @@ public class MapWrapper implements Map, Iterable>, S } @Override - @SuppressWarnings("NullableProblems") public Collection values() { return raw.values(); } @Override - @SuppressWarnings("NullableProblems") public Set keySet() { return raw.keySet(); } @Override - @SuppressWarnings("NullableProblems") public Set> entrySet() { return raw.entrySet(); } @Override - @SuppressWarnings("NullableProblems") public Iterator> iterator() { return this.entrySet().iterator(); } @@ -199,5 +196,14 @@ public class MapWrapper implements Map, Iterable>, S public V merge(K key, V value, BiFunction remappingFunction) { return raw.merge(key, value, remappingFunction); } + + @Override + public MapWrapper clone() throws CloneNotSupportedException { + @SuppressWarnings("unchecked") + final MapWrapper clone = (MapWrapper) super.clone(); + clone.raw = ObjectUtil.clone(raw); + return clone; + } + //---------------------------------------------------------------------------- Override default methods end } diff --git a/hutool-core/src/main/java/cn/hutool/core/map/SimpleEntry.java b/hutool-core/src/main/java/cn/hutool/core/map/SimpleEntry.java new file mode 100644 index 0000000000000000000000000000000000000000..e414636c0fabc7a22067975105f1ca68a26a89ca --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/map/SimpleEntry.java @@ -0,0 +1,31 @@ +package cn.hutool.core.map; + +/** + * {@link java.util.Map.Entry}简单实现。
+ * 键值对使用不可变字段表示。 + * + * @param 键类型 + * @param 值类型 + * @author looly + * @since 5.7.23 + */ +public class SimpleEntry extends AbsEntry { + + private final K key; + private final V value; + + public SimpleEntry(K key, V value) { + this.key = key; + this.value = value; + } + + @Override + public K getKey() { + return key; + } + + @Override + public V getValue() { + return value; + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/map/TableMap.java b/hutool-core/src/main/java/cn/hutool/core/map/TableMap.java index 596ec51605163d660da1a27cbb801fd982859325..783f1e98a3fca2f4130de0df10be9d774a8789dd 100644 --- a/hutool-core/src/main/java/cn/hutool/core/map/TableMap.java +++ b/hutool-core/src/main/java/cn/hutool/core/map/TableMap.java @@ -13,7 +13,6 @@ import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Set; /** @@ -173,7 +172,7 @@ public class TableMap implements Map, Iterable>, Ser public Set> entrySet() { final Set> hashSet = new LinkedHashSet<>(); for (int i = 0; i < size(); i++) { - hashSet.add(new Entry<>(keys.get(i), values.get(i))); + hashSet.add(new SimpleEntry<>(keys.get(i), values.get(i))); } return hashSet; } @@ -191,7 +190,7 @@ public class TableMap implements Map, Iterable>, Ser @Override public Map.Entry next() { - return new Entry<>(keysIter.next(), valuesIter.next()); + return new SimpleEntry<>(keysIter.next(), valuesIter.next()); } @Override @@ -209,48 +208,4 @@ public class TableMap implements Map, Iterable>, Ser ", values=" + values + '}'; } - - private static class Entry implements Map.Entry { - - private final K key; - private final V value; - - public Entry(K key, V value) { - this.key = key; - this.value = value; - } - - @Override - public K getKey() { - return key; - } - - @Override - public V getValue() { - return value; - } - - @Override - public V setValue(V value) { - throw new UnsupportedOperationException("setValue not supported."); - } - - @Override - public final boolean equals(Object o) { - if (o == this) - return true; - if (o instanceof Map.Entry) { - Map.Entry e = (Map.Entry) o; - return Objects.equals(key, e.getKey()) && - Objects.equals(value, e.getValue()); - } - return false; - } - - @Override - public int hashCode() { - //copy from 1.8 HashMap.Node - return Objects.hashCode(key) ^ Objects.hashCode(value); - } - } } diff --git a/hutool-core/src/main/java/cn/hutool/core/map/multi/AbsTable.java b/hutool-core/src/main/java/cn/hutool/core/map/multi/AbsTable.java new file mode 100644 index 0000000000000000000000000000000000000000..a98e4028677712731d97f283f9ff44207c39718d --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/map/multi/AbsTable.java @@ -0,0 +1,236 @@ +package cn.hutool.core.map.multi; + +import cn.hutool.core.collection.IterUtil; +import cn.hutool.core.collection.TransIter; +import cn.hutool.core.util.ObjectUtil; + +import java.io.Serializable; +import java.util.AbstractCollection; +import java.util.AbstractSet; +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** + * 抽象{@link Table}接口实现
+ * 默认实现了: + *

    + *
  • {@link #equals(Object)}
  • + *
  • {@link #hashCode()}
  • + *
  • {@link #toString()}
  • + *
  • {@link #values()}
  • + *
  • {@link #cellSet()}
  • + *
  • {@link #iterator()}
  • + *
+ * + * @param 行类型 + * @param 列类型 + * @param 值类型 + * @author Guava, Looly + * @since 5.7.23 + */ +public abstract class AbsTable implements Table { + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } else if (obj instanceof Table) { + final Table that = (Table) obj; + return this.cellSet().equals(that.cellSet()); + } else { + return false; + } + } + + @Override + public int hashCode() { + return cellSet().hashCode(); + } + + @Override + public String toString() { + return rowMap().toString(); + } + + //region values + @Override + public Collection values() { + Collection result = values; + return (result == null) ? values = new Values() : result; + } + + private Collection values; + private class Values extends AbstractCollection { + @Override + public Iterator iterator() { + return new TransIter<>(cellSet().iterator(), Cell::getValue); + } + + @Override + public boolean contains(Object o) { + //noinspection unchecked + return containsValue((V) o); + } + + @Override + public void clear() { + AbsTable.this.clear(); + } + + @Override + public int size() { + return AbsTable.this.size(); + } + } + //endregion + + //region cellSet + @Override + public Set> cellSet() { + Set> result = cellSet; + return (result == null) ? cellSet = new CellSet() : result; + } + + private Set> cellSet; + + private class CellSet extends AbstractSet> { + @Override + public boolean contains(Object o) { + if (o instanceof Cell) { + @SuppressWarnings("unchecked") final Cell cell = (Cell) o; + Map row = getRow(cell.getRowKey()); + if (null != row) { + return ObjectUtil.equals(row.get(cell.getColumnKey()), cell.getValue()); + } + } + return false; + } + + @Override + public boolean remove(Object o) { + if (contains(o)) { + @SuppressWarnings("unchecked") final Cell cell = (Cell) o; + AbsTable.this.remove(cell.getRowKey(), cell.getColumnKey()); + } + return false; + } + + @Override + public void clear() { + AbsTable.this.clear(); + } + + @Override + public Iterator> iterator() { + return new AbsTable.CellIterator(); + } + + @Override + public int size() { + return AbsTable.this.size(); + } + } + //endregion + + //region iterator + @Override + public Iterator> iterator() { + return new CellIterator(); + } + + /** + * 基于{@link Cell}的{@link Iterator}实现 + */ + private class CellIterator implements Iterator> { + final Iterator>> rowIterator = rowMap().entrySet().iterator(); + Map.Entry> rowEntry; + Iterator> columnIterator = IterUtil.empty(); + + @Override + public boolean hasNext() { + return rowIterator.hasNext() || columnIterator.hasNext(); + } + + @Override + public Cell next() { + if (false == columnIterator.hasNext()) { + rowEntry = rowIterator.next(); + columnIterator = rowEntry.getValue().entrySet().iterator(); + } + final Map.Entry columnEntry = columnIterator.next(); + return new SimpleCell<>(rowEntry.getKey(), columnEntry.getKey(), columnEntry.getValue()); + } + + @Override + public void remove() { + columnIterator.remove(); + if (rowEntry.getValue().isEmpty()) { + rowIterator.remove(); + } + } + } + //endregion + + /** + * 简单{@link Cell} 实现 + * + * @param 行类型 + * @param 列类型 + * @param 值类型 + */ + private static class SimpleCell implements Cell, Serializable { + private static final long serialVersionUID = 1L; + + private final R rowKey; + private final C columnKey; + private final V value; + + SimpleCell(R rowKey, C columnKey, V value) { + this.rowKey = rowKey; + this.columnKey = columnKey; + this.value = value; + } + + @Override + public R getRowKey() { + return rowKey; + } + + @Override + public C getColumnKey() { + return columnKey; + } + + @Override + public V getValue() { + return value; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj instanceof Cell) { + Cell other = (Cell) obj; + return ObjectUtil.equal(rowKey, other.getRowKey()) + && ObjectUtil.equal(columnKey, other.getColumnKey()) + && ObjectUtil.equal(value, other.getValue()); + } + return false; + } + + @Override + public int hashCode() { + return Objects.hash(rowKey, columnKey, value); + } + + @Override + public String toString() { + return "(" + rowKey + "," + columnKey + ")=" + value; + } + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/map/multi/RowKeyTable.java b/hutool-core/src/main/java/cn/hutool/core/map/multi/RowKeyTable.java new file mode 100644 index 0000000000000000000000000000000000000000..6094300ac200d79b071757e6405aad31e5fe815a --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/map/multi/RowKeyTable.java @@ -0,0 +1,259 @@ +package cn.hutool.core.map.multi; + +import cn.hutool.core.builder.Builder; +import cn.hutool.core.collection.ComputeIter; +import cn.hutool.core.collection.IterUtil; +import cn.hutool.core.collection.TransIter; +import cn.hutool.core.map.AbsEntry; +import cn.hutool.core.map.SimpleEntry; + +import java.util.AbstractMap; +import java.util.AbstractSet; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +/** + * 将行的键作为主键的{@link Table}实现
+ * 此结构为: 行=(列=值) + * + * @param 行类型 + * @param 列类型 + * @param 值类型 + * @author Guava, Looly + * @since 5.7.23 + */ +public class RowKeyTable extends AbsTable { + + final Map> raw; + /** + * 列的Map创建器,用于定义Table中Value对应Map类型 + */ + final Builder> columnBuilder; + + //region 构造 + + /** + * 构造 + */ + public RowKeyTable() { + this(new HashMap<>()); + } + + /** + * 构造 + * + * @param raw 原始Map + */ + public RowKeyTable(Map> raw) { + this(raw, HashMap::new); + } + + /** + * 构造 + * + * @param raw 原始Map + * @param columnMapBuilder 列的map创建器 + */ + public RowKeyTable(Map> raw, Builder> columnMapBuilder) { + this.raw = raw; + this.columnBuilder = null == columnMapBuilder ? HashMap::new : columnMapBuilder; + } + //endregion + + @Override + public Map> rowMap() { + return raw; + } + + @Override + public V put(R rowKey, C columnKey, V value) { + return raw.computeIfAbsent(rowKey, (key) -> columnBuilder.build()).put(columnKey, value); + } + + @Override + public V remove(R rowKey, C columnKey) { + final Map map = getRow(rowKey); + if (null == map) { + return null; + } + final V value = map.remove(columnKey); + if (map.isEmpty()) { + raw.remove(rowKey); + } + return value; + } + + @Override + public boolean isEmpty() { + return raw.isEmpty(); + } + + @Override + public void clear() { + this.raw.clear(); + } + + @Override + public boolean containsColumn(C columnKey) { + if (columnKey == null) { + return false; + } + for (Map map : raw.values()) { + if (null != map && map.containsKey(columnKey)) { + return true; + } + } + return false; + } + + //region columnMap + @Override + public Map> columnMap() { + Map> result = columnMap; + return (result == null) ? columnMap = new ColumnMap() : result; + } + + private Map> columnMap; + + private class ColumnMap extends AbstractMap> { + @Override + public Set>> entrySet() { + return new ColumnMapEntrySet(); + } + } + + private class ColumnMapEntrySet extends AbstractSet>> { + private final Set columnKeySet = columnKeySet(); + + @Override + public Iterator>> iterator() { + return new TransIter<>(columnKeySet.iterator(), + c -> new SimpleEntry<>(c, getColumn(c))); + } + + @Override + public int size() { + return columnKeySet.size(); + } + } + //endregion + + + //region columnKeySet + @Override + public Set columnKeySet() { + Set result = columnKeySet; + return (result == null) ? columnKeySet = new ColumnKeySet() : result; + } + + private Set columnKeySet; + + private class ColumnKeySet extends AbstractSet { + + @Override + public Iterator iterator() { + return new ColumnKeyIterator(); + } + + @Override + public int size() { + return IterUtil.size(iterator()); + } + } + + private class ColumnKeyIterator extends ComputeIter { + final Map seen = columnBuilder.build(); + final Iterator> mapIterator = raw.values().iterator(); + Iterator> entryIterator = IterUtil.empty(); + + @Override + protected C computeNext() { + while (true) { + if (entryIterator.hasNext()) { + Map.Entry entry = entryIterator.next(); + if (false == seen.containsKey(entry.getKey())) { + seen.put(entry.getKey(), entry.getValue()); + return entry.getKey(); + } + } else if (mapIterator.hasNext()) { + entryIterator = mapIterator.next().entrySet().iterator(); + } else { + return null; + } + } + } + } + //endregion + + //region getColumn + + @Override + public Map getColumn(C columnKey) { + return new Column(columnKey); + } + + private class Column extends AbstractMap { + final C columnKey; + + Column(C columnKey) { + this.columnKey = columnKey; + } + + @Override + public Set> entrySet() { + return new EntrySet(); + } + + private class EntrySet extends AbstractSet> { + + @Override + public Iterator> iterator() { + return new EntrySetIterator(); + } + + @Override + public int size() { + int size = 0; + for (Map map : raw.values()) { + if (map.containsKey(columnKey)) { + size++; + } + } + return size; + } + } + + private class EntrySetIterator extends ComputeIter> { + final Iterator>> iterator = raw.entrySet().iterator(); + + @Override + protected Entry computeNext() { + while (iterator.hasNext()) { + final Entry> entry = iterator.next(); + if (entry.getValue().containsKey(columnKey)) { + return new AbsEntry() { + @Override + public R getKey() { + return entry.getKey(); + } + + @Override + public V getValue() { + return entry.getValue().get(columnKey); + } + + @Override + public V setValue(V value) { + return entry.getValue().put(columnKey, value); + } + }; + } + } + return null; + } + } + } + //endregion +} diff --git a/hutool-core/src/main/java/cn/hutool/core/map/multi/Table.java b/hutool-core/src/main/java/cn/hutool/core/map/multi/Table.java new file mode 100644 index 0000000000000000000000000000000000000000..e0fb1c5f4f304ff382eb723bafa461623c66241b --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/map/multi/Table.java @@ -0,0 +1,259 @@ +package cn.hutool.core.map.multi; + +import cn.hutool.core.lang.Opt; +import cn.hutool.core.lang.func.Consumer3; +import cn.hutool.core.map.MapUtil; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; + +/** + * 表格数据结构定义
+ * 此结构类似于Guava的Table接口,使用两个键映射到一个值,类似于表格结构。 + * + * @param 行键类型 + * @param 列键类型 + * @param 值类型 + * @since 5.7.23 + */ +public interface Table extends Iterable> { + + /** + * 是否包含指定行列的映射
+ * 行和列任意一个不存在都会返回{@code false},如果行和列都存在,值为{@code null},也会返回{@code true} + * + * @param rowKey 行键 + * @param columnKey 列键 + * @return 是否包含映射 + */ + default boolean contains(R rowKey, C columnKey) { + return Opt.ofNullable(getRow(rowKey)).map((map) -> map.containsKey(columnKey)).get(); + } + + //region Row + + /** + * 行是否存在 + * + * @param rowKey 行键 + * @return 行是否存在 + */ + default boolean containsRow(R rowKey) { + return Opt.ofNullable(rowMap()).map((map) -> map.containsKey(rowKey)).get(); + } + + /** + * 获取行 + * + * @param rowKey 行键 + * @return 行映射,返回的键为列键,值为表格的值 + */ + default Map getRow(R rowKey) { + return Opt.ofNullable(rowMap()).map((map) -> map.get(rowKey)).get(); + } + + /** + * 返回所有行的key,行的key不可重复 + * + * @return 行键 + */ + default Set rowKeySet() { + return Opt.ofNullable(rowMap()).map(Map::keySet).get(); + } + + /** + * 返回行列对应的Map + * + * @return map,键为行键,值为列和值的对应map + */ + Map> rowMap(); + //endregion + + //region Column + + /** + * 列是否存在 + * + * @param columnKey 列键 + * @return 列是否存在 + */ + default boolean containsColumn(C columnKey) { + return Opt.ofNullable(columnMap()).map((map) -> map.containsKey(columnKey)).get(); + } + + /** + * 获取列 + * + * @param columnKey 列键 + * @return 列映射,返回的键为行键,值为表格的值 + */ + default Map getColumn(C columnKey) { + return Opt.ofNullable(columnMap()).map((map) -> map.get(columnKey)).get(); + } + + /** + * 返回所有列的key,列的key不可重复 + * + * @return 列set + */ + default Set columnKeySet() { + return Opt.ofNullable(columnMap()).map(Map::keySet).get(); + } + + /** + * 返回列-行对应的map + * + * @return map,键为列键,值为行和值的对应map + */ + Map> columnMap(); + //endregion + + //region value + + /** + * 指定值是否存在 + * + * @param value 值 + * @return 值 + */ + default boolean containsValue(V value){ + final Collection> rows = Opt.ofNullable(rowMap()).map(Map::values).get(); + if(null != rows){ + for (Map row : rows) { + if (row.containsValue(value)) { + return true; + } + } + } + return false; + } + + /** + * 获取指定值 + * + * @param rowKey 行键 + * @param columnKey 列键 + * @return 值,如果值不存在,返回{@code null} + */ + default V get(R rowKey, C columnKey) { + return Opt.ofNullable(getRow(rowKey)).map((map) -> map.get(columnKey)).get(); + } + + /** + * 所有行列值的集合 + * + * @return 值的集合 + */ + Collection values(); + //endregion + + /** + * 所有单元格集合 + * + * @return 单元格集合 + */ + Set> cellSet(); + + /** + * 为表格指定行列赋值,如果不存在,创建之,存在则替换之,返回原值 + * + * @param rowKey 行键 + * @param columnKey 列键 + * @param value 值 + * @return 原值,不存在返回{@code null} + */ + V put(R rowKey, C columnKey, V value); + + /** + * 批量加入 + * + * @param table 其他table + */ + default void putAll(Table table){ + if (null != table) { + for (Table.Cell cell : table.cellSet()) { + put(cell.getRowKey(), cell.getColumnKey(), cell.getValue()); + } + } + } + + /** + * 移除指定值 + * + * @param rowKey 行键 + * @param columnKey 列键 + * @return 移除的值,如果值不存在,返回{@code null} + */ + V remove(R rowKey, C columnKey); + + /** + * 表格是否为空 + * + * @return 是否为空 + */ + boolean isEmpty(); + + /** + * 表格大小,一般为单元格的个数 + * + * @return 表格大小 + */ + default int size(){ + final Map> rowMap = rowMap(); + if(MapUtil.isEmpty(rowMap)){ + return 0; + } + int size = 0; + for (Map map : rowMap.values()) { + size += map.size(); + } + return size; + } + + /** + * 清空表格 + */ + void clear(); + + /** + * 遍历表格的单元格,处理值 + * + * @param consumer 单元格值处理器 + */ + default void forEach(Consumer3 consumer) { + for (Cell cell : this) { + consumer.accept(cell.getRowKey(), cell.getColumnKey(), cell.getValue()); + } + } + + /** + * 单元格,用于表示一个单元格的行、列和值 + * + * @param 行键类型 + * @param 列键类型 + * @param 值类型 + */ + interface Cell { + /** + * 获取行键 + * + * @return 行键 + */ + R getRowKey(); + + /** + * 获取列键 + * + * @return 列键 + */ + C getColumnKey(); + + /** + * 获取值 + * + * @return 值 + */ + V getValue(); + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/map/multi/package-info.java b/hutool-core/src/main/java/cn/hutool/core/map/multi/package-info.java index b0a2bcd20c0246bb1b5fe277a720fab39a3491b4..2bf5b685f389fec95bbe65357987b0aa12a99717 100644 --- a/hutool-core/src/main/java/cn/hutool/core/map/multi/package-info.java +++ b/hutool-core/src/main/java/cn/hutool/core/map/multi/package-info.java @@ -1,7 +1,7 @@ /** - * 列表类型值的Map实现 + * 多参数类型的Map实现,包括集合类型值的Map和Table * * @author looly * */ -package cn.hutool.core.map.multi; \ No newline at end of file +package cn.hutool.core.map.multi; diff --git a/hutool-core/src/main/java/cn/hutool/core/text/NamingCase.java b/hutool-core/src/main/java/cn/hutool/core/text/NamingCase.java index a35d78f1ee47d668990dff8832e9f882a5116b76..56eb04c4647a2eed10ec5281649916cd2cbd014e 100644 --- a/hutool-core/src/main/java/cn/hutool/core/text/NamingCase.java +++ b/hutool-core/src/main/java/cn/hutool/core/text/NamingCase.java @@ -97,7 +97,7 @@ public class NamingCase { // 后一个为大写,按照专有名词对待,如aBC -> a_BC } else { //前一个为大写 - if (null == nextChar || Character.isLowerCase(nextChar)) { + if (null != nextChar && Character.isLowerCase(nextChar)) { // 普通首字母大写,如ABcc -> A_bcc sb.append(symbol); c = Character.toLowerCase(c); @@ -157,7 +157,7 @@ public class NamingCase { * 将连接符方式命名的字符串转换为驼峰式。如果转换前的下划线大写方式命名的字符串为空,则返回空字符串。 * * @param name 转换前的自定义方式命名的字符串 - * @param symbol 连接符 + * @param symbol 原字符串中的连接符连接符 * @return 转换后的驼峰式命名的字符串 * @since 5.7.17 */ diff --git a/hutool-core/src/main/java/cn/hutool/core/text/StrBuilder.java b/hutool-core/src/main/java/cn/hutool/core/text/StrBuilder.java index a09dc068107820f1056207adf3a1ce4e4c39e02e..18a18bd92a620ae62a0b793a3bf9bbb2098eb19c 100644 --- a/hutool-core/src/main/java/cn/hutool/core/text/StrBuilder.java +++ b/hutool-core/src/main/java/cn/hutool/core/text/StrBuilder.java @@ -8,7 +8,8 @@ import java.io.Serializable; import java.util.Arrays; /** - * 可复用的字符串生成器,非线程安全 + * 可复用的字符串生成器,非线程安全
+ * TODO 6.x移除此类,java8的StringBuilder非常完善了,无需重写。 * * @author Looly * @since 4.0.0 diff --git a/hutool-core/src/main/java/cn/hutool/core/text/csv/CsvBaseReader.java b/hutool-core/src/main/java/cn/hutool/core/text/csv/CsvBaseReader.java index 84a244e37547852cce5c448cfdadc2a7ffc29ac0..2ac2f918685791f3ef77b472ea723de971d24ff0 100644 --- a/hutool-core/src/main/java/cn/hutool/core/text/csv/CsvBaseReader.java +++ b/hutool-core/src/main/java/cn/hutool/core/text/csv/CsvBaseReader.java @@ -177,7 +177,7 @@ public class CsvBaseReader implements Serializable { final CsvParser csvParser = parse(reader); final List rows = new ArrayList<>(); read(csvParser, rows::add); - final List header = config.containsHeader ? csvParser.getHeader() : null; + final List header = config.headerLineNo > -1 ? csvParser.getHeader() : null; return new CsvData(header, rows); } diff --git a/hutool-core/src/main/java/cn/hutool/core/text/csv/CsvParser.java b/hutool-core/src/main/java/cn/hutool/core/text/csv/CsvParser.java index 21d996039c84d7ae6b178e939c073343f271157c..690127f4fda3d1ecd5509e6d88281f6538ab0d9f 100644 --- a/hutool-core/src/main/java/cn/hutool/core/text/csv/CsvParser.java +++ b/hutool-core/src/main/java/cn/hutool/core/text/csv/CsvParser.java @@ -84,13 +84,13 @@ public final class CsvParser extends ComputeIter implements Closeable, S } /** - * 获取头部字段列表,如果containsHeader设置为false则抛出异常 + * 获取头部字段列表,如果headerLineNo < 0,抛出异常 * * @return 头部列表 * @throws IllegalStateException 如果不解析头部或者没有调用nextRow()方法 */ public List getHeader() { - if (false == config.containsHeader) { + if (config.headerLineNo < 0) { throw new IllegalStateException("No header available - header parsing is disabled"); } if (lineNo < config.beginLineNo) { @@ -152,7 +152,7 @@ public final class CsvParser extends ComputeIter implements Closeable, S } //初始化标题 - if (config.containsHeader && null == header) { + if (lineNo == config.headerLineNo && null == header) { initHeader(currentFields); // 作为标题行后,此行跳过,下一行做为第一行 continue; diff --git a/hutool-core/src/main/java/cn/hutool/core/text/csv/CsvReadConfig.java b/hutool-core/src/main/java/cn/hutool/core/text/csv/CsvReadConfig.java index f9373f8fc09e82b856e5514500924992af3fa7dd..5ad7c7d85490a1ebb7c0c47cc42d157230a69525 100644 --- a/hutool-core/src/main/java/cn/hutool/core/text/csv/CsvReadConfig.java +++ b/hutool-core/src/main/java/cn/hutool/core/text/csv/CsvReadConfig.java @@ -11,8 +11,8 @@ import java.io.Serializable; public class CsvReadConfig extends CsvConfig implements Serializable { private static final long serialVersionUID = 5396453565371560052L; - /** 是否首行做为标题行,默认false */ - protected boolean containsHeader; + /** 指定标题行号,-1表示无标题行 */ + protected long headerLineNo = -1; /** 是否跳过空白行,默认true */ protected boolean skipEmptyRows = true; /** 每行字段个数不同时是否抛出异常,默认false */ @@ -34,13 +34,26 @@ public class CsvReadConfig extends CsvConfig implements Serializa } /** - * 设置是否首行做为标题行,默认false + * 设置是否首行做为标题行,默认false
+ * 当设置为{@code true}时,默认标题行号是{@link #beginLineNo},{@code false}为-1,表示无行号 * * @param containsHeader 是否首行做为标题行,默认false * @return this + * @see #setHeaderLineNo(long) */ public CsvReadConfig setContainsHeader(boolean containsHeader) { - this.containsHeader = containsHeader; + return setHeaderLineNo(containsHeader ? beginLineNo : -1); + } + + /** + * 设置标题行行号,默认-1,表示无标题行
+ * + * @param headerLineNo 标题行行号,-1表示无标题行 + * @return this + * @since 5.7.23 + */ + public CsvReadConfig setHeaderLineNo(long headerLineNo) { + this.headerLineNo = headerLineNo; return this; } diff --git a/hutool-core/src/main/java/cn/hutool/core/util/ArrayUtil.java b/hutool-core/src/main/java/cn/hutool/core/util/ArrayUtil.java index b0c9cbacb0c3f81e0eb007abb6b0e0b736c03688..399fe571dd6875e105ccd04f5a8549b9ca21e616 100644 --- a/hutool-core/src/main/java/cn/hutool/core/util/ArrayUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/util/ArrayUtil.java @@ -122,7 +122,7 @@ public class ArrayUtil extends PrimitiveArrayUtil { } } } - return false; + return array == null; } /** @@ -368,6 +368,48 @@ public class ArrayUtil extends PrimitiveArrayUtil { } } + /** + * 将新元素插入到到已有数组中的某个位置
+ * 添加新元素会生成一个新数组或原有数组
+ * 如果插入位置为为负数,那么生成一个由插入元素顺序加已有数组顺序的新数组 + * + * @param 数组元素类型 + * @param buffer 已有数组 + * @param index 位置,大于长度追加,否则替换,<0表示从头部追加 + * @param values 新值 + * @return 新数组或原有数组 + * @since 5.7.23 + */ + @SuppressWarnings({"unchecked"}) + public static T[] replace(T[] buffer, int index, T... values) { + if(isEmpty(values)){ + return buffer; + } + if(isEmpty(buffer)){ + return values; + } + if (index < 0) { + // 从头部追加 + return insert(buffer, 0, values); + } + if (index >= buffer.length) { + // 超出长度,尾部追加 + return append(buffer, values); + } + + if (buffer.length >= values.length + index) { + System.arraycopy(values, 0, buffer, index, values.length); + return buffer; + } + + // 替换长度大于原数组长度,新建数组 + int newArrayLength = index + values.length; + final T[] result = newArray(buffer.getClass().getComponentType(), newArrayLength); + System.arraycopy(buffer, 0, result, 0, index); + System.arraycopy(values, 0, result, index, values.length); + return result; + } + /** * 将新元素插入到到已有数组中的某个位置
* 添加新元素会生成一个新的数组,不影响原数组
@@ -1540,7 +1582,7 @@ public class ArrayUtil extends PrimitiveArrayUtil { * @since 4.5.18 */ public static boolean isAllEmpty(Object... args) { - for (Object obj: args) { + for (Object obj : args) { if (false == ObjectUtil.isEmpty(obj)) { return false; } @@ -1758,10 +1800,10 @@ public class ArrayUtil extends PrimitiveArrayUtil { /** * 查找最后一个子数组的开始位置 * - * @param array 数组 + * @param array 数组 * @param endInclude 查找结束的位置(包含) - * @param subArray 子数组 - * @param 数组元素类型 + * @param subArray 子数组 + * @param 数组元素类型 * @return 最后一个子数组的开始位置,即子数字第一个元素在数组中的位置 * @since 5.4.8 */ diff --git a/hutool-core/src/main/java/cn/hutool/core/util/ModifierUtil.java b/hutool-core/src/main/java/cn/hutool/core/util/ModifierUtil.java index f2b3f389ffc9ef8e9b3a7e91216ac17c21cd0bd2..dec42bed06c6a818c53bf2ae11789cd19461afdd 100644 --- a/hutool-core/src/main/java/cn/hutool/core/util/ModifierUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/util/ModifierUtil.java @@ -20,35 +20,60 @@ public class ModifierUtil { * @since 4.0.5 */ public enum ModifierType { - /** public修饰符,所有类都能访问 */ + /** + * public修饰符,所有类都能访问 + */ PUBLIC(Modifier.PUBLIC), - /** private修饰符,只能被自己访问和修改 */ + /** + * private修饰符,只能被自己访问和修改 + */ PRIVATE(Modifier.PRIVATE), - /** protected修饰符,自身、子类及同一个包中类可以访问 */ + /** + * protected修饰符,自身、子类及同一个包中类可以访问 + */ PROTECTED(Modifier.PROTECTED), - /** static修饰符,(静态修饰符)指定变量被所有对象共享,即所有实例都可以使用该变量。变量属于这个类 */ + /** + * static修饰符,(静态修饰符)指定变量被所有对象共享,即所有实例都可以使用该变量。变量属于这个类 + */ STATIC(Modifier.STATIC), - /** final修饰符,最终修饰符,指定此变量的值不能变,使用在方法上表示不能被重载 */ + /** + * final修饰符,最终修饰符,指定此变量的值不能变,使用在方法上表示不能被重载 + */ FINAL(Modifier.FINAL), - /** synchronized,同步修饰符,在多个线程中,该修饰符用于在运行前,对他所属的方法加锁,以防止其他线程的访问,运行结束后解锁。 */ + /** + * synchronized,同步修饰符,在多个线程中,该修饰符用于在运行前,对他所属的方法加锁,以防止其他线程的访问,运行结束后解锁。 + */ SYNCHRONIZED(Modifier.SYNCHRONIZED), - /** (易失修饰符)指定该变量可以同时被几个线程控制和修改 */ + /** + * (易失修饰符)指定该变量可以同时被几个线程控制和修改 + */ VOLATILE(Modifier.VOLATILE), - /** (过度修饰符)指定该变量是系统保留,暂无特别作用的临时性变量,序列化时忽略 */ + /** + * (过度修饰符)指定该变量是系统保留,暂无特别作用的临时性变量,序列化时忽略 + */ TRANSIENT(Modifier.TRANSIENT), - /** native,本地修饰符。指定此方法的方法体是用其他语言在程序外部编写的。 */ + /** + * native,本地修饰符。指定此方法的方法体是用其他语言在程序外部编写的。 + */ NATIVE(Modifier.NATIVE), - /** abstract,将一个类声明为抽象类,没有实现的方法,需要子类提供方法实现。 */ + /** + * abstract,将一个类声明为抽象类,没有实现的方法,需要子类提供方法实现。 + */ ABSTRACT(Modifier.ABSTRACT), - /** strictfp,一旦使用了关键字strictfp来声明某个类、接口或者方法时,那么在这个关键字所声明的范围内所有浮点运算都是精确的,符合IEEE-754规范的。 */ + /** + * strictfp,一旦使用了关键字strictfp来声明某个类、接口或者方法时,那么在这个关键字所声明的范围内所有浮点运算都是精确的,符合IEEE-754规范的。 + */ STRICT(Modifier.STRICT); - /** 修饰符枚举对应的int修饰符值 */ + /** + * 修饰符枚举对应的int修饰符值 + */ private final int value; /** * 构造 + * * @param modifier 修饰符int表示,见{@link Modifier} */ ModifierType(int modifier) { @@ -57,6 +82,7 @@ public class ModifierUtil { /** * 获取修饰符枚举对应的int修饰符值,值见{@link Modifier} + * * @return 修饰符枚举对应的int修饰符值 */ public int getValue() { @@ -67,7 +93,7 @@ public class ModifierUtil { /** * 是否同时存在一个或多个修饰符(可能有多个修饰符,如果有指定的修饰符则返回true) * - * @param clazz 类 + * @param clazz 类 * @param modifierTypes 修饰符枚举 * @return 是否有指定修饰符,如果有返回true,否则false,如果提供参数为null返回false */ @@ -81,7 +107,7 @@ public class ModifierUtil { /** * 是否同时存在一个或多个修饰符(可能有多个修饰符,如果有指定的修饰符则返回true) * - * @param constructor 构造方法 + * @param constructor 构造方法 * @param modifierTypes 修饰符枚举 * @return 是否有指定修饰符,如果有返回true,否则false,如果提供参数为null返回false */ @@ -95,7 +121,7 @@ public class ModifierUtil { /** * 是否同时存在一个或多个修饰符(可能有多个修饰符,如果有指定的修饰符则返回true) * - * @param method 方法 + * @param method 方法 * @param modifierTypes 修饰符枚举 * @return 是否有指定修饰符,如果有返回true,否则false,如果提供参数为null返回false */ @@ -109,7 +135,7 @@ public class ModifierUtil { /** * 是否同时存在一个或多个修饰符(可能有多个修饰符,如果有指定的修饰符则返回true) * - * @param field 字段 + * @param field 字段 * @param modifierTypes 修饰符枚举 * @return 是否有指定修饰符,如果有返回true,否则false,如果提供参数为null返回false */ @@ -226,15 +252,27 @@ public class ModifierUtil { return clazz.isSynthetic(); } + /** + * 是否抽象方法 + * + * @param method 方法 + * @return 是否抽象方法 + * @since 5.7.23 + */ + public static boolean isAbstract(Method method) { + return hasModifier(method, ModifierType.ABSTRACT); + } //-------------------------------------------------------------------------------------------------------- Private method start + /** * 多个修饰符做“与”操作,表示同时存在多个修饰符 + * * @param modifierTypes 修饰符列表,元素不能为空 * @return “与”之后的修饰符 */ private static int modifiersToInt(ModifierType... modifierTypes) { int modifier = modifierTypes[0].getValue(); - for(int i = 1; i < modifierTypes.length; i++) { + for (int i = 1; i < modifierTypes.length; i++) { modifier |= modifierTypes[i].getValue(); } return modifier; diff --git a/hutool-core/src/main/java/cn/hutool/core/util/ReflectUtil.java b/hutool-core/src/main/java/cn/hutool/core/util/ReflectUtil.java index 6f31764337b8ed59f678cf1c091adcdecd92e4b9..7b746bc6ec410bdd698883dd9bceda0403741faa 100644 --- a/hutool-core/src/main/java/cn/hutool/core/util/ReflectUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/util/ReflectUtil.java @@ -3,6 +3,7 @@ package cn.hutool.core.util; import cn.hutool.core.annotation.Alias; import cn.hutool.core.bean.NullWrapperBean; import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.UniqueKeySet; import cn.hutool.core.convert.Convert; import cn.hutool.core.exceptions.UtilException; import cn.hutool.core.lang.Assert; @@ -17,6 +18,7 @@ import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.AbstractMap; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -347,11 +349,12 @@ public class ReflectUtil { /** * 是否为父类引用字段
* 当字段所在类是对象子类时(对象中定义的非static的class),会自动生成一个以"this$0"为名称的字段,指向父类对象 + * * @param field 字段 * @return 是否为父类引用字段 * @since 5.7.20 */ - public static boolean isOuterClassField(Field field){ + public static boolean isOuterClassField(Field field) { return "this$0".equals(field.getName()); } @@ -386,7 +389,8 @@ public class ReflectUtil { } /** - * 获得指定类过滤后的Public方法列表 + * 获得指定类过滤后的Public方法列表
+ * TODO 6.x此方法更改返回Method[] * * @param clazz 查找方法的类 * @param filter 过滤器 @@ -647,34 +651,47 @@ public class ReflectUtil { */ public static Method[] getMethods(Class beanClass) throws SecurityException { Assert.notNull(beanClass); - return METHODS_CACHE.get(beanClass, () -> getMethodsDirectly(beanClass, true)); + return METHODS_CACHE.get(beanClass, + () -> getMethodsDirectly(beanClass, true, true)); } /** - * 获得一个类中所有方法列表,直接反射获取,无缓存 + * 获得一个类中所有方法列表,直接反射获取,无缓存
+ * 接口获取方法和默认方法,获取的方法包括: + *
    + *
  • 本类中的所有方法(包括static方法)
  • + *
  • 父类中的所有方法(包括static方法)
  • + *
  • Object中(包括static方法)
  • + *
* - * @param beanClass 类 - * @param withSuperClassMethods 是否包括父类的方法列表 + * @param beanClass 类或接口 + * @param withSupers 是否包括父类或接口的方法列表 + * @param withMethodFromObject 是否包括Object中的方法 * @return 方法列表 * @throws SecurityException 安全检查异常 */ - public static Method[] getMethodsDirectly(Class beanClass, boolean withSuperClassMethods) throws SecurityException { + public static Method[] getMethodsDirectly(Class beanClass, boolean withSupers, boolean withMethodFromObject) throws SecurityException { Assert.notNull(beanClass); - Method[] allMethods = null; + if (beanClass.isInterface()) { + // 对于接口,直接调用Class.getMethods方法获取所有方法,因为接口都是public方法 + return withSupers ? beanClass.getMethods() : beanClass.getDeclaredMethods(); + } + + final UniqueKeySet result = new UniqueKeySet<>(true, ReflectUtil::getUniqueKey); Class searchType = beanClass; - Method[] declaredMethods; while (searchType != null) { - declaredMethods = searchType.getDeclaredMethods(); - if (null == allMethods) { - allMethods = declaredMethods; - } else { - allMethods = ArrayUtil.append(allMethods, declaredMethods); + if (false == withMethodFromObject && Object.class == searchType) { + break; } - searchType = withSuperClassMethods ? searchType.getSuperclass() : null; + result.addAllIfAbsent(Arrays.asList(searchType.getDeclaredMethods())); + result.addAllIfAbsent(getDefaultMethodsFromInterface(searchType)); + + + searchType = (withSupers && false == searchType.isInterface()) ? searchType.getSuperclass() : null; } - return allMethods; + return result.toArray(new Method[0]); } /** @@ -770,10 +787,10 @@ public class ReflectUtil { String name = method.getName(); // 跳过getClass这个特殊方法 - if("getClass".equals(name)){ + if ("getClass".equals(name)) { return false; } - if(ignoreCase){ + if (ignoreCase) { name = name.toLowerCase(); } switch (parameterCount) { @@ -848,7 +865,7 @@ public class ReflectUtil { * * @param 对象类型 * @param beanClass 被构造的类 - * @return 构造后的对象 + * @return 构造后的对象,构造失败返回{@code null} */ @SuppressWarnings("unchecked") public static T newInstanceIfPossible(Class beanClass) { @@ -1037,4 +1054,47 @@ public class ReflectUtil { } return accessibleObject; } + + /** + * 获取方法的唯一键,结构为: + *
+	 *     返回类型#方法名:参数1类型,参数2类型...
+	 * 
+ * + * @param method 方法 + * @return 方法唯一键 + */ + private static String getUniqueKey(Method method) { + final StringBuilder sb = new StringBuilder(); + sb.append(method.getReturnType().getName()).append('#'); + sb.append(method.getName()); + Class[] parameters = method.getParameterTypes(); + for (int i = 0; i < parameters.length; i++) { + if (i == 0) { + sb.append(':'); + } else { + sb.append(','); + } + sb.append(parameters[i].getName()); + } + return sb.toString(); + } + + /** + * 获取类对应接口中的非抽象方法(default方法) + * + * @param clazz 类 + * @return 方法列表 + */ + private static List getDefaultMethodsFromInterface(Class clazz) { + List result = new ArrayList<>(); + for (Class ifc : clazz.getInterfaces()) { + for (Method m : ifc.getMethods()) { + if (false == ModifierUtil.isAbstract(m)) { + result.add(m); + } + } + } + return result; + } } diff --git a/hutool-core/src/test/java/cn/hutool/core/annotation/AnnotationForTest.java b/hutool-core/src/test/java/cn/hutool/core/annotation/AnnotationForTest.java index 6b41de3adb924ac791675c348723038b705aa4fb..83210ae901d010734c0caf46a7a2f6d2fce406e5 100644 --- a/hutool-core/src/test/java/cn/hutool/core/annotation/AnnotationForTest.java +++ b/hutool-core/src/test/java/cn/hutool/core/annotation/AnnotationForTest.java @@ -10,7 +10,6 @@ import java.lang.annotation.Target; * 注解类相关说明见:https://www.cnblogs.com/xdp-gacl/p/3622275.html * * @author looly - * */ // Retention注解决定MyAnnotation注解的生命周期 @Retention(RetentionPolicy.RUNTIME) @@ -23,5 +22,8 @@ public @interface AnnotationForTest { * * @return 属性值 */ - String value(); + String value() default ""; + + @Alias("value") + String retry() default ""; } diff --git a/hutool-core/src/test/java/cn/hutool/core/annotation/AnnotationUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/annotation/AnnotationUtilTest.java index b565cbcb6ca918d2806f67517a64f633ec6e524e..3340dcf37b0ec1720ca89d5bcbd6ec2f0c70a0de 100644 --- a/hutool-core/src/test/java/cn/hutool/core/annotation/AnnotationUtilTest.java +++ b/hutool-core/src/test/java/cn/hutool/core/annotation/AnnotationUtilTest.java @@ -12,6 +12,16 @@ public class AnnotationUtilTest { } + @Test + public void getAnnotationSyncAlias() { + // 直接获取 + Assert.assertEquals("", ClassWithAnnotation.class.getAnnotation(AnnotationForTest.class).retry()); + + // 加别名适配 + AnnotationForTest annotation = AnnotationUtil.getAnnotationAlias(ClassWithAnnotation.class, AnnotationForTest.class); + Assert.assertEquals("测试", annotation.retry()); + } + @AnnotationForTest("测试") static class ClassWithAnnotation{ public void test(){ diff --git a/hutool-core/src/test/java/cn/hutool/core/bean/BeanUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/bean/BeanUtilTest.java index a01dc608be3e87b98be450f5d6f419b1d896520e..f311903132ef5dbdd41c3a44c1f03f4fc75060fb 100644 --- a/hutool-core/src/test/java/cn/hutool/core/bean/BeanUtilTest.java +++ b/hutool-core/src/test/java/cn/hutool/core/bean/BeanUtilTest.java @@ -5,6 +5,7 @@ import cn.hutool.core.bean.copier.CopyOptions; import cn.hutool.core.bean.copier.ValueProvider; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.ListUtil; +import cn.hutool.core.map.MapBuilder; import cn.hutool.core.map.MapUtil; import cn.hutool.core.thread.ThreadUtil; import cn.hutool.core.util.ArrayUtil; @@ -78,14 +79,15 @@ public class BeanUtilTest { @Test public void fillBeanWithMapIgnoreCaseTest() { - HashMap map = MapUtil.newHashMap(); - map.put("Name", "Joe"); - map.put("aGe", 12); - map.put("openId", "DFDFSDFWERWER"); + Map map = MapBuilder.create() + .put("Name", "Joe") + .put("aGe", 12) + .put("openId", "DFDFSDFWERWER") + .build(); SubPerson person = BeanUtil.fillBeanWithMapIgnoreCase(map, new SubPerson(), false); - Assert.assertEquals(person.getName(), "Joe"); - Assert.assertEquals(person.getAge(), 12); - Assert.assertEquals(person.getOpenid(), "DFDFSDFWERWER"); + Assert.assertEquals("Joe", person.getName()); + Assert.assertEquals(12, person.getAge()); + Assert.assertEquals("DFDFSDFWERWER", person.getOpenid()); } @Test diff --git a/hutool-core/src/test/java/cn/hutool/core/collection/UniqueKeySetTest.java b/hutool-core/src/test/java/cn/hutool/core/collection/UniqueKeySetTest.java new file mode 100644 index 0000000000000000000000000000000000000000..9dd45058fcd157cf7572c12c54eda46a720a3131 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/collection/UniqueKeySetTest.java @@ -0,0 +1,34 @@ +package cn.hutool.core.collection; + +import cn.hutool.core.lang.Console; +import lombok.AllArgsConstructor; +import lombok.Data; +import org.junit.Assert; +import org.junit.Test; + +import java.util.Set; + +public class UniqueKeySetTest { + + @Test + public void addTest(){ + Set set = new UniqueKeySet<>(UniqueTestBean::getId); + set.add(new UniqueTestBean("id1", "张三", "地球")); + set.add(new UniqueTestBean("id2", "李四", "火星")); + // id重复,替换之前的元素 + set.add(new UniqueTestBean("id2", "王五", "木星")); + + // 后两个ID重复 + Assert.assertEquals(2, set.size()); + + set.forEach(Console::log); + } + + @Data + @AllArgsConstructor + static class UniqueTestBean{ + private String id; + private String name; + private String address; + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/convert/ConvertTest.java b/hutool-core/src/test/java/cn/hutool/core/convert/ConvertTest.java index 402140e68094a93ce958ce824cd24a659e6b8802..18cd78b144915e99877adf53498b4f151b58722c 100644 --- a/hutool-core/src/test/java/cn/hutool/core/convert/ConvertTest.java +++ b/hutool-core/src/test/java/cn/hutool/core/convert/ConvertTest.java @@ -24,6 +24,7 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicIntegerArray; import java.util.concurrent.atomic.AtomicLongArray; +import java.util.concurrent.atomic.DoubleAdder; /** * 类型转换工具单元测试 @@ -361,4 +362,25 @@ public class ConvertTest { final float f = Convert.toFloat(value); Assert.assertEquals(406.1F, f, 2); } + + @Test + public void floatToDoubleTest(){ + float a = 0.45f; + double b = Convert.toDouble(a); + Assert.assertEquals(a, b, 5); + } + + @Test + public void floatToDoubleAddrTest(){ + float a = 0.45f; + final DoubleAdder adder = Convert.convert(DoubleAdder.class, a); + Assert.assertEquals(a, adder.doubleValue(), 5); + } + + @Test + public void doubleToFloatTest(){ + double a = 0.45f; + float b = Convert.toFloat(a); + Assert.assertEquals(a, b, 5); + } } diff --git a/hutool-core/src/test/java/cn/hutool/core/io/FileCopierTest.java b/hutool-core/src/test/java/cn/hutool/core/io/FileCopierTest.java index 80c50288dea1069452222ff653ec1c3b2a64828d..8419ca29d822cff92c4d8066feeba4c02c31a604 100644 --- a/hutool-core/src/test/java/cn/hutool/core/io/FileCopierTest.java +++ b/hutool-core/src/test/java/cn/hutool/core/io/FileCopierTest.java @@ -1,45 +1,59 @@ package cn.hutool.core.io; +import org.junit.Assert; import org.junit.Ignore; import org.junit.Test; import cn.hutool.core.io.file.FileCopier; +import java.io.File; + /** * 文件拷贝单元测试 - * @author Looly * + * @author Looly */ public class FileCopierTest { - + @Test @Ignore public void dirCopyTest() { FileCopier copier = FileCopier.create("D:\\Java", "e:/eclipse/eclipse2.zip"); copier.copy(); } - + @Test @Ignore public void dirCopyTest2() { //测试带.的文件夹复制 FileCopier copier = FileCopier.create("D:\\workspace\\java\\.metadata", "D:\\workspace\\java\\.metadata\\temp"); copier.copy(); - + FileUtil.copy("D:\\workspace\\java\\looly\\hutool\\.git", "D:\\workspace\\java\\temp", true); } - + @Test(expected = IORuntimeException.class) public void dirCopySubTest() { //测试父目录复制到子目录报错 FileCopier copier = FileCopier.create("D:\\workspace\\java\\.metadata", "D:\\workspace\\java\\.metadata\\temp"); copier.copy(); } - + @Test @Ignore public void copyFileToDirTest() { FileCopier copier = FileCopier.create("d:/GReen_Soft/XshellXftpPortable.zip", "c:/hp/"); copier.copy(); } + + @Test + @Ignore + public void copyFileByRelativePath(){ + // https://github.com/dromara/hutool/pull/2188 + // 当复制的目标文件位置是相对路径的时候可以通过 + FileCopier copier = FileCopier.create(new File("pom.xml"),new File("aaa.txt")); + copier.copy(); + final boolean delete = new File("aaa.txt").delete(); + Assert.assertTrue(delete); + } } diff --git a/hutool-core/src/test/java/cn/hutool/core/io/FileUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/io/FileUtilTest.java index d70490e8bad277739abe8cf7bc244718e976c919..8a50a764137971e287ca6de21f6494d03a85f115 100644 --- a/hutool-core/src/test/java/cn/hutool/core/io/FileUtilTest.java +++ b/hutool-core/src/test/java/cn/hutool/core/io/FileUtilTest.java @@ -392,6 +392,22 @@ public class FileUtilTest { path = FileUtil.isWindows() ? "d:\\aaa\\bbb\\cc\\fff.xlsx" : "~/Desktop/hutool/fff.xlsx"; mainName = FileUtil.extName(path); Assert.assertEquals("xlsx", mainName); + + path = FileUtil.isWindows() ? "d:\\aaa\\bbb\\cc\\fff.tar.gz" : "~/Desktop/hutool/fff.tar.gz"; + mainName = FileUtil.extName(path); + Assert.assertEquals("tar.gz", mainName); + + path = FileUtil.isWindows() ? "d:\\aaa\\bbb\\cc\\fff.tar.Z" : "~/Desktop/hutool/fff.tar.Z"; + mainName = FileUtil.extName(path); + Assert.assertEquals("tar.Z", mainName); + + path = FileUtil.isWindows() ? "d:\\aaa\\bbb\\cc\\fff.tar.bz2" : "~/Desktop/hutool/fff.tar.bz2"; + mainName = FileUtil.extName(path); + Assert.assertEquals("tar.bz2", mainName); + + path = FileUtil.isWindows() ? "d:\\aaa\\bbb\\cc\\fff.tar.xz" : "~/Desktop/hutool/fff.tar.xz"; + mainName = FileUtil.extName(path); + Assert.assertEquals("tar.xz", mainName); } @Test diff --git a/hutool-core/src/test/java/cn/hutool/core/lang/test/bean/ExamInfoDict.java b/hutool-core/src/test/java/cn/hutool/core/lang/test/bean/ExamInfoDict.java index a39e91238b6bcc7977844d10b5a9e7bb64f12c8b..b5068f2d8ae339be662bc48b1c089713a09863aa 100644 --- a/hutool-core/src/test/java/cn/hutool/core/lang/test/bean/ExamInfoDict.java +++ b/hutool-core/src/test/java/cn/hutool/core/lang/test/bean/ExamInfoDict.java @@ -1,16 +1,18 @@ package cn.hutool.core.lang.test.bean; +import lombok.Data; + import java.io.Serializable; -import java.util.Objects; /** - * + * * @author 质量过关 * */ +@Data public class ExamInfoDict implements Serializable { private static final long serialVersionUID = 3640936499125004525L; - + // 主键 private Integer id; // 可当作题号 // 试题类型 客观题 0主观题 1 @@ -18,49 +20,7 @@ public class ExamInfoDict implements Serializable { // 试题是否作答 private Integer answerIs; - public Integer getId() { - return id; - } public Integer getId(Integer defaultValue) { return this.id == null ? defaultValue : this.id; } - public void setId(Integer id) { - this.id = id; - } - - public Integer getExamType() { - return examType; - } - public void setExamType(Integer examType) { - this.examType = examType; - } - - public Integer getAnswerIs() { - return answerIs; - } - public void setAnswerIs(Integer answerIs) { - this.answerIs = answerIs; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - ExamInfoDict that = (ExamInfoDict) o; - return Objects.equals(id, that.id) && Objects.equals(examType, that.examType) && Objects.equals(answerIs, that.answerIs); - } - - @Override - public int hashCode() { - return Objects.hash(id, examType, answerIs); - } - - @Override - public String toString() { - return "ExamInfoDict{" + "id=" + id + ", examType=" + examType + ", answerIs=" + answerIs + '}'; - } } diff --git a/hutool-core/src/test/java/cn/hutool/core/map/MapUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/map/MapUtilTest.java index 19fdc83166db23ae4ce2952b0a629b98bed76051..ed47980fed928cd65baa527ae2de16422cecc09f 100644 --- a/hutool-core/src/test/java/cn/hutool/core/map/MapUtilTest.java +++ b/hutool-core/src/test/java/cn/hutool/core/map/MapUtilTest.java @@ -26,6 +26,24 @@ public class MapUtilTest { Assert.assertEquals("4", map2.get("d")); } + @Test + public void filterMapWrapperTest() { + Map map = MapUtil.newHashMap(); + map.put("a", "1"); + map.put("b", "2"); + map.put("c", "3"); + map.put("d", "4"); + + final Map camelCaseMap = MapUtil.toCamelCaseMap(map); + + Map map2 = MapUtil.filter(camelCaseMap, t -> Convert.toInt(t.getValue()) % 2 == 0); + + Assert.assertEquals(2, map2.size()); + + Assert.assertEquals("2", map2.get("b")); + Assert.assertEquals("4", map2.get("d")); + } + @Test public void filterContainsTest() { Map map = MapUtil.newHashMap(); diff --git a/hutool-core/src/test/java/cn/hutool/core/map/RowKeyTableTest.java b/hutool-core/src/test/java/cn/hutool/core/map/RowKeyTableTest.java new file mode 100644 index 0000000000000000000000000000000000000000..fdbaca39861c51cc088da578d69e82f6cb2b88ad --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/map/RowKeyTableTest.java @@ -0,0 +1,39 @@ +package cn.hutool.core.map; + +import cn.hutool.core.map.multi.RowKeyTable; +import cn.hutool.core.map.multi.Table; +import org.junit.Assert; +import org.junit.Test; + +import java.util.Map; + +public class RowKeyTableTest { + + @Test + public void putGetTest(){ + final Table table = new RowKeyTable<>(); + table.put(1, 2, 3); + table.put(1, 6, 4); + + Assert.assertEquals(new Integer(3), table.get(1, 2)); + Assert.assertNull(table.get(1, 3)); + + //判断row和column确定的二维点是否存在 + Assert.assertTrue(table.contains(1, 2)); + Assert.assertFalse(table.contains(1, 3)); + + //判断列 + Assert.assertTrue(table.containsColumn(2)); + Assert.assertFalse(table.containsColumn(3)); + + // 判断行 + Assert.assertTrue(table.containsRow(1)); + Assert.assertFalse(table.containsRow(2)); + + + // 获取列 + Map column = table.getColumn(6); + Assert.assertEquals(1, column.size()); + Assert.assertEquals(new Integer(4), column.get(1)); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/text/NamingCaseTest.java b/hutool-core/src/test/java/cn/hutool/core/text/NamingCaseTest.java index 62545d934c2c3a36bbce7b910c8d603a2bf08df9..2eec1a2485b1b069cdd1423a08b4f7f2a535ab2f 100644 --- a/hutool-core/src/test/java/cn/hutool/core/text/NamingCaseTest.java +++ b/hutool-core/src/test/java/cn/hutool/core/text/NamingCaseTest.java @@ -1,21 +1,44 @@ package cn.hutool.core.text; import cn.hutool.core.lang.Dict; +import cn.hutool.core.util.CharUtil; import org.junit.Assert; import org.junit.Test; public class NamingCaseTest { + @Test - public void toUnderlineCaseTest(){ - // https://github.com/dromara/hutool/issues/2070 + public void toCamelCaseTest() { Dict.create() - .set("customerNickV2", "customer_nick_v2") - .forEach((key, value) -> Assert.assertEquals(value, NamingCase.toUnderlineCase(key))); + .set("Table_Test_Of_day","tableTestOfDay") + .set("TableTestOfDay","TableTestOfDay") + .set("abc_1d","abc1d") + .forEach((key, value) -> Assert.assertEquals(value, NamingCase.toCamelCase(key))); + } + + @Test + public void toCamelCaseFromDashedTest() { + Dict.create() + .set("Table-Test-Of-day","tableTestOfDay") + .forEach((key, value) -> Assert.assertEquals(value, NamingCase.toCamelCase(key, CharUtil.DASHED))); } @Test - public void toUnderLineCaseTest2(){ - final String wPRunOZTime = NamingCase.toUnderlineCase("wPRunOZTime"); - Assert.assertEquals("w_P_run_OZ_time", wPRunOZTime); + public void toUnderLineCaseTest() { + Dict.create() + .set("Table_Test_Of_day", "table_test_of_day") + .set("_Table_Test_Of_day_", "_table_test_of_day_") + .set("_Table_Test_Of_DAY_", "_table_test_of_DAY_") + .set("_TableTestOfDAYToday", "_table_test_of_DAY_today") + .set("HelloWorld_test", "hello_world_test") + .set("H2", "H2") + .set("H#case", "H#case") + .set("PNLabel", "PN_label") + .set("wPRunOZTime", "w_P_run_OZ_time") + // https://github.com/dromara/hutool/issues/2070 + .set("customerNickV2", "customer_nick_v2") + // https://gitee.com/dromara/hutool/issues/I4X9TT + .set("DEPT_NAME","DEPT_NAME") + .forEach((key, value) -> Assert.assertEquals(value, NamingCase.toUnderlineCase(key))); } } diff --git a/hutool-core/src/test/java/cn/hutool/core/util/ArrayUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/util/ArrayUtilTest.java index 3ba18becf32dac57a630fc0a8f4c4423e61c34f5..92f3603a805c2354903b0d2371655d9e51dfac44 100644 --- a/hutool-core/src/test/java/cn/hutool/core/util/ArrayUtilTest.java +++ b/hutool-core/src/test/java/cn/hutool/core/util/ArrayUtilTest.java @@ -363,7 +363,7 @@ public class ArrayUtilTest { } @Test - public void indexOfSubTest2(){ + public void indexOfSubTest2() { Integer[] a = {0x12, 0x56, 0x34, 0x56, 0x78, 0x9A}; Integer[] b = {0x56, 0x78}; int i = ArrayUtil.indexOfSub(a, b); @@ -401,7 +401,7 @@ public class ArrayUtilTest { } @Test - public void lastIndexOfSubTest2(){ + public void lastIndexOfSubTest2() { Integer[] a = {0x12, 0x56, 0x78, 0x56, 0x21, 0x9A}; Integer[] b = {0x56, 0x78}; int i = ArrayUtil.indexOfSub(a, b); @@ -409,17 +409,17 @@ public class ArrayUtilTest { } @Test - public void reverseTest(){ - int[] a = {1,2,3,4}; + public void reverseTest() { + int[] a = {1, 2, 3, 4}; final int[] reverse = ArrayUtil.reverse(a); - Assert.assertArrayEquals(new int[]{4,3,2,1}, reverse); + Assert.assertArrayEquals(new int[]{4, 3, 2, 1}, reverse); } @Test - public void reverseTest2s(){ - Object[] a = {"1",'2',"3",4}; + public void reverseTest2s() { + Object[] a = {"1", '2', "3", 4}; final Object[] reverse = ArrayUtil.reverse(a); - Assert.assertArrayEquals(new Object[]{4,"3",'2',"1"}, reverse); + Assert.assertArrayEquals(new Object[]{4, "3", '2', "1"}, reverse); } @Test @@ -461,9 +461,57 @@ public class ArrayUtilTest { } @Test - public void getTest(){ + public void getTest() { String[] a = {"a", "b", "c"}; final Object o = ArrayUtil.get(a, -1); Assert.assertEquals("c", o); } + + @Test + public void replaceTest() { + String[] a = {"1", "2", "3", "4"}; + String[] b = {"a", "b", "c"}; + + // 在小于0的位置,-1位置插入,返回b+a,新数组 + String[] result = ArrayUtil.replace(a, -1, b); + Assert.assertArrayEquals(new String[]{"a", "b", "c", "1", "2", "3", "4"}, result); + + // 在第0个位置开始替换,返回a + result = ArrayUtil.replace(ArrayUtil.clone(a), 0, b); + Assert.assertArrayEquals(new String[]{"a", "b", "c", "4"}, result); + + // 在第1个位置替换,即"2"开始 + result = ArrayUtil.replace(ArrayUtil.clone(a), 1, b); + Assert.assertArrayEquals(new String[]{"1", "a", "b", "c"}, result); + + // 在第2个位置插入,即"3"之后 + result = ArrayUtil.replace(ArrayUtil.clone(a), 2, b); + Assert.assertArrayEquals(new String[]{"1", "2", "a", "b", "c"}, result); + + // 在第3个位置插入,即"4"之后 + result = ArrayUtil.replace(ArrayUtil.clone(a), 3, b); + Assert.assertArrayEquals(new String[]{"1", "2", "3", "a", "b", "c"}, result); + + // 在第4个位置插入,数组长度为4,在索引4出替换即两个数组相加 + result = ArrayUtil.replace(ArrayUtil.clone(a), 4, b); + Assert.assertArrayEquals(new String[]{"1", "2", "3", "4", "a", "b", "c"}, result); + + // 在大于3个位置插入,数组长度为4,即两个数组相加 + result = ArrayUtil.replace(ArrayUtil.clone(a), 5, b); + Assert.assertArrayEquals(new String[]{"1", "2", "3", "4", "a", "b", "c"}, result); + + String[] e = null; + String[] f = {"a", "b", "c"}; + + // e为null 返回 f + result = ArrayUtil.replace(e, -1, f); + Assert.assertArrayEquals(f, result); + + String[] g = {"a", "b", "c"}; + String[] h = null; + + // h为null 返回 g + result = ArrayUtil.replace(g, 0, h); + Assert.assertArrayEquals(g, result); + } } diff --git a/hutool-core/src/test/java/cn/hutool/core/util/ReflectUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/util/ReflectUtilTest.java index 9a90c8fe38a1294204c577bb10d2db8f42236866..6affbabb616a489e1db0231e227aacfb5d18e37b 100644 --- a/hutool-core/src/test/java/cn/hutool/core/util/ReflectUtilTest.java +++ b/hutool-core/src/test/java/cn/hutool/core/util/ReflectUtilTest.java @@ -23,7 +23,7 @@ public class ReflectUtilTest { @Test public void getMethodsTest() { Method[] methods = ReflectUtil.getMethods(ExamInfoDict.class); - Assert.assertEquals(22, methods.length); + Assert.assertEquals(20, methods.length); //过滤器测试 methods = ReflectUtil.getMethods(ExamInfoDict.class, t -> Integer.class.equals(t.getReturnType())); @@ -35,7 +35,7 @@ public class ReflectUtilTest { //null过滤器测试 methods = ReflectUtil.getMethods(ExamInfoDict.class, null); - Assert.assertEquals(22, methods.length); + Assert.assertEquals(20, methods.length); final Method method2 = methods[0]; Assert.assertNotNull(method2); } @@ -114,9 +114,9 @@ public class ReflectUtilTest { @Test @Ignore - public void getMethodBenchTest(){ + public void getMethodBenchTest() { // 预热 - getMethod(TestBenchClass.class, false, "getH"); + getMethodWithReturnTypeCheck(TestBenchClass.class, false, "getH"); final TimeInterval timer = DateUtil.timer(); timer.start(); @@ -127,7 +127,7 @@ public class ReflectUtilTest { timer.restart(); for (int i = 0; i < 100000000; i++) { - getMethod(TestBenchClass.class, false, "getH"); + getMethodWithReturnTypeCheck(TestBenchClass.class, false, "getH"); } Console.log(timer.interval()); } @@ -150,7 +150,7 @@ public class ReflectUtilTest { private String n; } - public static Method getMethod(Class clazz, boolean ignoreCase, String methodName, Class... paramTypes) throws SecurityException { + public static Method getMethodWithReturnTypeCheck(Class clazz, boolean ignoreCase, String methodName, Class... paramTypes) throws SecurityException { if (null == clazz || StrUtil.isBlank(methodName)) { return null; } @@ -169,4 +169,74 @@ public class ReflectUtilTest { } return res; } + + @Test + public void getMethodsFromClassExtends() { + // 继承情况下,需解决方法去重问题 + Method[] methods = ReflectUtil.getMethods(C2.class); + Assert.assertEquals(15, methods.length); + + // 排除Object中的方法 + // 3个方法包括类 + methods = ReflectUtil.getMethodsDirectly(C2.class, true, false); + Assert.assertEquals(3, methods.length); + + // getA属于本类 + Assert.assertEquals("public void cn.hutool.core.util.ReflectUtilTest$C2.getA()", methods[0].toString()); + // getB属于父类 + Assert.assertEquals("public void cn.hutool.core.util.ReflectUtilTest$C1.getB()", methods[1].toString()); + // getC属于接口中的默认方法 + Assert.assertEquals("public default void cn.hutool.core.util.ReflectUtilTest$TestInterface1.getC()", methods[2].toString()); + } + + @Test + public void getMethodsFromInterfaceTest() { + // 对于接口,直接调用Class.getMethods方法获取所有方法,因为接口都是public方法 + // 因此此处得到包括TestInterface1、TestInterface2、TestInterface3中一共4个方法 + final Method[] methods = ReflectUtil.getMethods(TestInterface3.class); + Assert.assertEquals(4, methods.length); + + // 接口里,调用getMethods和getPublicMethods效果相同 + final Method[] publicMethods = ReflectUtil.getPublicMethods(TestInterface3.class); + Assert.assertArrayEquals(methods, publicMethods); + } + + interface TestInterface1 { + void getA(); + + void getB(); + + default void getC() { + + } + } + + interface TestInterface2 extends TestInterface1 { + @Override + void getB(); + } + + interface TestInterface3 extends TestInterface2 { + void get3(); + } + + class C1 implements TestInterface2 { + + @Override + public void getA() { + + } + + @Override + public void getB() { + + } + } + + class C2 extends C1 { + @Override + public void getA() { + + } + } } diff --git a/hutool-core/src/test/java/cn/hutool/core/util/StrUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/util/StrUtilTest.java index ee1e1da5096bb44c417ab96ecdec3b0f287bb034..514efd089127dae38d89ded2f68e94f22024aace 100644 --- a/hutool-core/src/test/java/cn/hutool/core/util/StrUtilTest.java +++ b/hutool-core/src/test/java/cn/hutool/core/util/StrUtilTest.java @@ -391,40 +391,6 @@ public class StrUtilTest { Assert.assertEquals(text, str); } - @Test - public void toCamelCaseTest() { - String str = "Table_Test_Of_day"; - String result = StrUtil.toCamelCase(str); - Assert.assertEquals("tableTestOfDay", result); - - String str1 = "TableTestOfDay"; - String result1 = StrUtil.toCamelCase(str1); - Assert.assertEquals("TableTestOfDay", result1); - - String abc1d = StrUtil.toCamelCase("abc_1d"); - Assert.assertEquals("abc1d", abc1d); - - - String str2 = "Table-Test-Of-day"; - String result2 = StrUtil.toCamelCase(str2, CharUtil.DASHED); - System.out.println(result2); - Assert.assertEquals("tableTestOfDay", result2); - } - - @Test - public void toUnderLineCaseTest() { - Dict.create() - .set("Table_Test_Of_day", "table_test_of_day") - .set("_Table_Test_Of_day_", "_table_test_of_day_") - .set("_Table_Test_Of_DAY_", "_table_test_of_DAY_") - .set("_TableTestOfDAYToday", "_table_test_of_DAY_today") - .set("HelloWorld_test", "hello_world_test") - .set("H2", "H2") - .set("H#case", "H#case") - .set("PNLabel", "PN_label") - .forEach((key, value) -> Assert.assertEquals(value, StrUtil.toUnderlineCase(key))); - } - @Test public void containsAnyTest() { //字符 diff --git a/hutool-cron/pom.xml b/hutool-cron/pom.xml index 94c4538cc9c7ee6659c969cf9a1e76414a8bee41..190721d02302ada765edabdad106e54f37168205 100644 --- a/hutool-cron/pom.xml +++ b/hutool-cron/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 5.7.22 + 5.7.23-SNAPSHOT hutool-cron diff --git a/hutool-crypto/pom.xml b/hutool-crypto/pom.xml index cff3649a96571b109702d8816298e77914aab187..8dbd928e283da0c537f5d4c51fe4df6cd25f5943 100644 --- a/hutool-crypto/pom.xml +++ b/hutool-crypto/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 5.7.22 + 5.7.23-SNAPSHOT hutool-crypto diff --git a/hutool-crypto/src/main/java/cn/hutool/crypto/symmetric/XXTEA.java b/hutool-crypto/src/main/java/cn/hutool/crypto/symmetric/XXTEA.java new file mode 100644 index 0000000000000000000000000000000000000000..d413dce9d7a8c34f1bd0101d14866a78f3922ce3 --- /dev/null +++ b/hutool-crypto/src/main/java/cn/hutool/crypto/symmetric/XXTEA.java @@ -0,0 +1,157 @@ +package cn.hutool.crypto.symmetric; + +import cn.hutool.core.io.IoUtil; + +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Serializable; + +/** + * XXTEA(Corrected Block Tiny Encryption Algorithm)算法实现
+ * 来自:https://github.com/xxtea/xxtea-java + * + * @author Ma Bingyao + */ +public class XXTEA implements SymmetricEncryptor, SymmetricDecryptor, Serializable { + private static final long serialVersionUID = 1L; + + private static final int DELTA = 0x9E3779B9; + + private final byte[] key; + + /** + * 构造 + * + * @param key 密钥,16位 + */ + public XXTEA(byte[] key) { + this.key = key; + } + + @Override + public byte[] encrypt(byte[] data) { + if (data.length == 0) { + return data; + } + return toByteArray(encrypt( + toIntArray(data, true), + toIntArray(fixKey(key), false)), false); + } + + @Override + public void encrypt(InputStream data, OutputStream out, boolean isClose) { + IoUtil.write(out, isClose, encrypt(IoUtil.readBytes(data))); + } + + @Override + public byte[] decrypt(byte[] data) { + if (data.length == 0) { + return data; + } + return toByteArray(decrypt( + toIntArray(data, false), + toIntArray(fixKey(key), false)), true); + } + + @Override + public void decrypt(InputStream data, OutputStream out, boolean isClose) { + IoUtil.write(out, isClose, decrypt(IoUtil.readBytes(data))); + } + + //region Private Method + private static int[] encrypt(int[] v, int[] k) { + int n = v.length - 1; + + if (n < 1) { + return v; + } + int p, q = 6 + 52 / (n + 1); + int z = v[n], y, sum = 0, e; + + while (q-- > 0) { + sum = sum + DELTA; + e = sum >>> 2 & 3; + for (p = 0; p < n; p++) { + y = v[p + 1]; + z = v[p] += mx(sum, y, z, p, e, k); + } + y = v[0]; + z = v[n] += mx(sum, y, z, p, e, k); + } + return v; + } + + private static int[] decrypt(int[] v, int[] k) { + int n = v.length - 1; + + if (n < 1) { + return v; + } + int p, q = 6 + 52 / (n + 1); + int z, y = v[0], sum = q * DELTA, e; + + while (sum != 0) { + e = sum >>> 2 & 3; + for (p = n; p > 0; p--) { + z = v[p - 1]; + y = v[p] -= mx(sum, y, z, p, e, k); + } + z = v[n]; + y = v[0] -= mx(sum, y, z, p, e, k); + sum = sum - DELTA; + } + return v; + } + + private static int mx(int sum, int y, int z, int p, int e, int[] k) { + return (z >>> 5 ^ y << 2) + (y >>> 3 ^ z << 4) ^ (sum ^ y) + (k[p & 3 ^ e] ^ z); + } + + private static byte[] fixKey(byte[] key) { + if (key.length == 16) { + return key; + } + byte[] fixedkey = new byte[16]; + System.arraycopy(key, 0, fixedkey, 0, Math.min(key.length, 16)); + return fixedkey; + } + + private static int[] toIntArray(byte[] data, boolean includeLength) { + int n = (((data.length & 3) == 0) + ? (data.length >>> 2) + : ((data.length >>> 2) + 1)); + int[] result; + + if (includeLength) { + result = new int[n + 1]; + result[n] = data.length; + } else { + result = new int[n]; + } + n = data.length; + for (int i = 0; i < n; ++i) { + result[i >>> 2] |= (0x000000ff & data[i]) << ((i & 3) << 3); + } + return result; + } + + private static byte[] toByteArray(int[] data, boolean includeLength) { + int n = data.length << 2; + + if (includeLength) { + int m = data[data.length - 1]; + n -= 4; + if ((m < n - 3) || (m > n)) { + return null; + } + n = m; + } + byte[] result = new byte[n]; + + for (int i = 0; i < n; ++i) { + result[i] = (byte) (data[i >>> 2] >>> ((i & 3) << 3)); + } + return result; + } + //endregion +} diff --git a/hutool-crypto/src/test/java/cn/hutool/crypto/test/symmetric/TEATest.java b/hutool-crypto/src/test/java/cn/hutool/crypto/test/symmetric/TEATest.java new file mode 100644 index 0000000000000000000000000000000000000000..53916926a204f37f0342b9165e53c0c61948af25 --- /dev/null +++ b/hutool-crypto/src/test/java/cn/hutool/crypto/test/symmetric/TEATest.java @@ -0,0 +1,54 @@ +package cn.hutool.crypto.test.symmetric; + +import cn.hutool.crypto.symmetric.SymmetricCrypto; +import cn.hutool.crypto.symmetric.XXTEA; +import org.junit.Assert; +import org.junit.Test; + +/** + * TEA(Tiny Encryption Algorithm)和 XTEA算法单元测试 + */ +public class TEATest { + + @Test + public void teaTest() { + String data = "测试的加密数据 by Hutool"; + + // 密钥必须为128bit + final SymmetricCrypto tea = new SymmetricCrypto("TEA", "MyPassword123456".getBytes()); + final byte[] encrypt = tea.encrypt(data); + + // 解密 + final String decryptStr = tea.decryptStr(encrypt); + + Assert.assertEquals(data, decryptStr); + } + + @Test + public void xteaTest() { + String data = "测试的加密数据 by Hutool"; + + // 密钥必须为128bit + final SymmetricCrypto tea = new SymmetricCrypto("XTEA", "MyPassword123456".getBytes()); + final byte[] encrypt = tea.encrypt(data); + + // 解密 + final String decryptStr = tea.decryptStr(encrypt); + + Assert.assertEquals(data, decryptStr); + } + + @Test + public void xxteaTest() { + String data = "测试的加密数据 by Hutool"; + + // 密钥必须为128bit + final XXTEA tea = new XXTEA("MyPassword123456".getBytes()); + final byte[] encrypt = tea.encrypt(data); + + // 解密 + final String decryptStr = tea.decryptStr(encrypt); + + Assert.assertEquals(data, decryptStr); + } +} diff --git a/hutool-db/pom.xml b/hutool-db/pom.xml index c8c5ccff73ad133b95943a21832bf8e57222572f..ed7e1aed543c48d2f699cdd5de1800a0432288c6 100644 --- a/hutool-db/pom.xml +++ b/hutool-db/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 5.7.22 + 5.7.23-SNAPSHOT hutool-db @@ -20,10 +20,10 @@ 0.9.5.5 2.9.0 - 10.0.14 + 10.0.16 1.2.8 2.4.13 - 3.12.10 + 4.5.0 3.36.0.3 2.5.2 @@ -96,21 +96,12 @@ ${dbcp2.version} true - - - org.mongodb - mongo-java-driver - ${mongo.version} - true - - redis.clients diff --git a/hutool-db/src/main/java/cn/hutool/db/nosql/mongo/MongoDS.java b/hutool-db/src/main/java/cn/hutool/db/nosql/mongo/MongoDS.java index 8158f0d33ac626973c1fc466a1481f7fbd9298c7..65ce4fa7aad625214e8fb73f3bfde804416b324b 100644 --- a/hutool-db/src/main/java/cn/hutool/db/nosql/mongo/MongoDS.java +++ b/hutool-db/src/main/java/cn/hutool/db/nosql/mongo/MongoDS.java @@ -6,29 +6,35 @@ import cn.hutool.core.util.StrUtil; import cn.hutool.db.DbRuntimeException; import cn.hutool.log.Log; import cn.hutool.setting.Setting; -import com.mongodb.MongoClient; -import com.mongodb.MongoClientOptions; -import com.mongodb.MongoClientOptions.Builder; +import com.mongodb.MongoClientSettings; import com.mongodb.MongoCredential; import com.mongodb.ServerAddress; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; import com.mongodb.client.MongoCollection; import com.mongodb.client.MongoDatabase; +import com.mongodb.connection.ConnectionPoolSettings; +import com.mongodb.connection.SocketSettings; import org.bson.Document; import java.io.Closeable; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.concurrent.TimeUnit; /** - * MongoDB工具类 - * - * @author xiaoleilu + * MongoDB4工具类 * + * @author VampireAchao */ public class MongoDS implements Closeable { + private final static Log log = Log.get(); - /** 默认配置文件 */ + /** + * 默认配置文件 + */ public final static String MONGO_CONFIG_PATH = "config/mongo.setting"; // MongoDB配置文件 @@ -41,6 +47,7 @@ public class MongoDS implements Closeable { private MongoClient mongo; // --------------------------------------------------------------------------- Constructor start + /** * 构造MongoDB数据源
* 调用者必须持有MongoDS实例,否则会被垃圾回收导致写入失败! @@ -58,8 +65,8 @@ public class MongoDS implements Closeable { * 调用者必须持有MongoDS实例,否则会被垃圾回收导致写入失败! * * @param mongoSetting MongoDB的配置文件,如果是null则读取默认配置文件或者使用MongoDB默认客户端配置 - * @param host 主机(域名或者IP) - * @param port 端口 + * @param host 主机(域名或者IP) + * @param port 端口 */ public MongoDS(Setting mongoSetting, String host, int port) { this.setting = mongoSetting; @@ -86,7 +93,7 @@ public class MongoDS implements Closeable { * 官方文档: http://docs.mongodb.org/manual/administration/replica-sets/ * * @param mongoSetting MongoDB的配置文件,必须有 - * @param groups 分组列表,当为null或空时使用无分组配置,一个分组使用单一模式,否则使用副本集模式 + * @param groups 分组列表,当为null或空时使用无分组配置,一个分组使用单一模式,否则使用副本集模式 */ public MongoDS(Setting mongoSetting, String... groups) { if (mongoSetting == null) { @@ -146,11 +153,13 @@ public class MongoDS implements Closeable { final MongoCredential credentail = createCredentail(group); try { - if (null == credentail) { - mongo = new MongoClient(serverAddress, buildMongoClientOptions(group)); - } else { - mongo = new MongoClient(serverAddress, credentail, buildMongoClientOptions(group)); + MongoClientSettings.Builder clusterSettingsBuilder = MongoClientSettings.builder() + .applyToClusterSettings(b -> b.hosts(Collections.singletonList(serverAddress))); + buildMongoClientSettings(clusterSettingsBuilder, group); + if (null != credentail) { + clusterSettingsBuilder.credential(credentail); } + mongo = MongoClients.create(clusterSettingsBuilder.build()); } catch (Exception e) { throw new DbRuntimeException(StrUtil.format("Init MongoDB pool with connection to [{}] error!", serverAddress), e); } @@ -192,11 +201,13 @@ public class MongoDS implements Closeable { final MongoCredential credentail = createCredentail(StrUtil.EMPTY); try { - if (null == credentail) { - mongo = new MongoClient(addrList, buildMongoClientOptions(StrUtil.EMPTY)); - } else { - mongo = new MongoClient(addrList, credentail, buildMongoClientOptions(StrUtil.EMPTY)); + MongoClientSettings.Builder clusterSettingsBuilder = MongoClientSettings.builder() + .applyToClusterSettings(b -> b.hosts(addrList)); + buildMongoClientSettings(clusterSettingsBuilder, StrUtil.EMPTY); + if (null != credentail) { + clusterSettingsBuilder.credential(credentail); } + mongo = MongoClients.create(clusterSettingsBuilder.build()); } catch (Exception e) { log.error(e, "Init MongoDB connection error!"); return; @@ -234,7 +245,7 @@ public class MongoDS implements Closeable { /** * 获得MongoDB中指定集合对象 * - * @param dbName 库名 + * @param dbName 库名 * @param collectionName 集合名 * @return DBCollection */ @@ -248,6 +259,7 @@ public class MongoDS implements Closeable { } // --------------------------------------------------------------------------- Private method start + /** * 创建ServerAddress对象,会读取配置文件中的相关信息 * @@ -291,7 +303,7 @@ public class MongoDS implements Closeable { */ private MongoCredential createCredentail(String group) { final Setting setting = this.setting; - if(null == setting) { + if (null == setting) { return null; } final String user = setting.getStr("user", group, setting.getStr("user")); @@ -316,23 +328,13 @@ public class MongoDS implements Closeable { return MongoCredential.createCredential(userName, database, password.toCharArray()); } - /** - * 构件MongoDB连接选项
- * - * @param group 分组,当分组对应的选项不存在时会读取根选项,如果也不存在使用默认值 - * @return MongoClientOptions - */ - private MongoClientOptions buildMongoClientOptions(String group) { - return buildMongoClientOptions(MongoClientOptions.builder(), group).build(); - } - /** * 构件MongoDB连接选项
* * @param group 分组,当分组对应的选项不存在时会读取根选项,如果也不存在使用默认值 * @return Builder */ - private Builder buildMongoClientOptions(Builder builder, String group) { + private MongoClientSettings.Builder buildMongoClientSettings(MongoClientSettings.Builder builder, String group) { if (setting == null) { return builder; } @@ -348,8 +350,9 @@ public class MongoDS implements Closeable { if (StrUtil.isBlank(group) == false && connectionsPerHost == null) { connectionsPerHost = setting.getInt("connectionsPerHost"); } + ConnectionPoolSettings.Builder connectionPoolSettingsBuilder = ConnectionPoolSettings.builder(); if (connectionsPerHost != null) { - builder.connectionsPerHost(connectionsPerHost); + connectionPoolSettingsBuilder.maxSize(connectionsPerHost); log.debug("MongoDB connectionsPerHost: {}", connectionsPerHost); } @@ -359,9 +362,10 @@ public class MongoDS implements Closeable { setting.getInt("connectTimeout"); } if (connectTimeout != null) { - builder.connectTimeout(connectTimeout); + connectionPoolSettingsBuilder.maxWaitTime(connectTimeout, TimeUnit.MILLISECONDS); log.debug("MongoDB connectTimeout: {}", connectTimeout); } + builder.applyToConnectionPoolSettings(b -> b.applySettings(connectionPoolSettingsBuilder.build())); // 套接字超时时间;该值会被传递给Socket.setSoTimeout(int)。默以为0(无穷) --int Integer socketTimeout = setting.getInt(group + "socketTimeout"); @@ -369,7 +373,8 @@ public class MongoDS implements Closeable { setting.getInt("socketTimeout"); } if (socketTimeout != null) { - builder.socketTimeout(socketTimeout); + SocketSettings socketSettings = SocketSettings.builder().connectTimeout(socketTimeout, TimeUnit.MILLISECONDS).build(); + builder.applyToSocketSettings(b -> b.applySettings(socketSettings)); log.debug("MongoDB socketTimeout: {}", socketTimeout); } @@ -388,4 +393,5 @@ public class MongoDS implements Closeable { return this.setting; } // --------------------------------------------------------------------------- Private method end + } diff --git a/hutool-db/src/main/java/cn/hutool/db/nosql/mongo/MongoFactory.java b/hutool-db/src/main/java/cn/hutool/db/nosql/mongo/MongoFactory.java index 23fdf8aad391383ea844db23e4227807c617ced8..25717bcb77a3b636803366415c638373e2782eae 100644 --- a/hutool-db/src/main/java/cn/hutool/db/nosql/mongo/MongoFactory.java +++ b/hutool-db/src/main/java/cn/hutool/db/nosql/mongo/MongoFactory.java @@ -10,16 +10,20 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** - * MongoDB工厂类,用于创建 - * @author looly + * {@link MongoDS}工厂类,用于创建 * + * @author Looly, VampireAchao */ public class MongoFactory { - /** 各分组做组合key的时候分隔符 */ + /** + * 各分组做组合key的时候分隔符 + */ private final static String GROUP_SEPRATER = ","; - /** 数据源池 */ + /** + * 数据源池 + */ private static final Map DS_MAP = new ConcurrentHashMap<>(); // JVM关闭前关闭MongoDB连接 @@ -28,6 +32,7 @@ public class MongoFactory { } // ------------------------------------------------------------------------ Get DS start + /** * 获取MongoDB数据源
* @@ -80,7 +85,7 @@ public class MongoFactory { * 获取MongoDB数据源
* * @param setting 设定文件 - * @param groups 分组列表 + * @param groups 分组列表 * @return MongoDB连接 */ public static MongoDS getDS(Setting setting, String... groups) { @@ -99,7 +104,7 @@ public class MongoFactory { * 获取MongoDB数据源
* * @param setting 配置文件 - * @param groups 分组列表 + * @param groups 分组列表 * @return MongoDB连接 */ public static MongoDS getDS(Setting setting, Collection groups) { @@ -111,8 +116,8 @@ public class MongoFactory { * 关闭全部连接 */ public static void closeAll() { - if(MapUtil.isNotEmpty(DS_MAP)){ - for(MongoDS ds : DS_MAP.values()) { + if (MapUtil.isNotEmpty(DS_MAP)) { + for (MongoDS ds : DS_MAP.values()) { ds.close(); } DS_MAP.clear(); diff --git a/hutool-db/src/test/java/cn/hutool/db/nosql/MongoDBTest.java b/hutool-db/src/test/java/cn/hutool/db/nosql/MongoDBTest.java new file mode 100644 index 0000000000000000000000000000000000000000..866ba0b4cc8d5bd27133811671e7470efd6c305f --- /dev/null +++ b/hutool-db/src/test/java/cn/hutool/db/nosql/MongoDBTest.java @@ -0,0 +1,20 @@ +package cn.hutool.db.nosql; + +import cn.hutool.db.nosql.mongo.MongoFactory; +import com.mongodb.client.MongoDatabase; +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; + +/** + * @author VampireAchao + */ +public class MongoDBTest { + + @Test + @Ignore + public void mongoDSTest() { + MongoDatabase db = MongoFactory.getDS("master").getDb("test"); + Assert.assertEquals("test", db.getName()); + } +} diff --git a/hutool-db/src/test/resources/config/mongo.setting b/hutool-db/src/test/resources/config/mongo.setting new file mode 100644 index 0000000000000000000000000000000000000000..b50b4d0264a271ac09ec9053c89ba43f983a441f --- /dev/null +++ b/hutool-db/src/test/resources/config/mongo.setting @@ -0,0 +1,20 @@ +#每个主机答应的连接数(每个主机的连接池大小),当连接池被用光时,会被阻塞住 ,默以为10 --int +connectionsPerHost=100 +#线程队列数,它以connectionsPerHost值相乘的结果就是线程队列最大值。如果连接线程排满了队列就会抛出“Out of semaphores to get db”错误 --int +threadsAllowedToBlockForConnectionMultiplier=10 +#被阻塞线程从连接池获取连接的最长等待时间(ms) --int +maxWaitTime = 120000 +#在建立(打开)套接字连接时的超时时间(ms),默以为0(无穷) --int +connectTimeout=0 +#套接字超时时间;该值会被传递给Socket.setSoTimeout(int)。默以为0(无穷) --int +socketTimeout=0 +#是否打开长连接. defaults to false --boolean +socketKeepAlive=false + +#---------------------------------- MongoDB实例连接 +[master] +host = localhost:27017 + +[slave] +host = localhost:27018 +#----------------------------------------------------- diff --git a/hutool-dfa/pom.xml b/hutool-dfa/pom.xml index 65737363bbeb5dae33c72c3a5559e4b9b9fc9937..c16db7ae2117b9ff65a47311b6af836cad47c02c 100644 --- a/hutool-dfa/pom.xml +++ b/hutool-dfa/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 5.7.22 + 5.7.23-SNAPSHOT hutool-dfa diff --git a/hutool-extra/pom.xml b/hutool-extra/pom.xml index adff1d26e62d1df6b7168cc9e7d71cf90179d6f3..b86f833cfee8e02807e911eecbb0a0a1b0d9f6c7 100644 --- a/hutool-extra/pom.xml +++ b/hutool-extra/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 5.7.22 + 5.7.23-SNAPSHOT hutool-extra diff --git a/hutool-extra/src/main/java/cn/hutool/extra/spring/SpringCronUtil.java b/hutool-extra/src/main/java/cn/hutool/extra/spring/SpringCronUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..99ad38b08532fe5145b77cc891f6b17f6c99b31e --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/spring/SpringCronUtil.java @@ -0,0 +1,155 @@ +package cn.hutool.extra.spring; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.extra.spring.condition.ConditionalOnAnnotation; +import org.springframework.context.annotation.Lazy; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.support.CronTrigger; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ScheduledFuture; + +/** + * Spring 动态定时任务封装 + *
    + *
  1. 创建定时任务
  2. + *
  3. 修改定时任务
  4. + *
  5. 取消定时任务
  6. + *
  7. 高级操作
  8. + *
+ * + * @author JC、TCL + * @date 03/15 + * @see TaskScheduler + * @see TaskScheduler + */ +@Component +@ConditionalOnAnnotation(EnableScheduling.class) +public class SpringCronUtil { + /** + * 任务调度器 + */ + private static TaskScheduler taskScheduler; + + /** + * ID 与 Future 绑定 + */ + private static final Map> TASK_FUTURE = MapUtil.newConcurrentHashMap(); + + /** + * ID 与 Runnable 绑定 + */ + private static final Map TASK_RUNNABLE = MapUtil.newConcurrentHashMap(); + + /** + * 加入定时任务 + * + * @param task 任务 + * @param expression 定时任务执行时间的cron表达式 + * @return 定时任务ID + */ + public static String schedule(Runnable task, String expression) { + String id = IdUtil.fastUUID(); + return schedule(id, task, expression); + } + + /** + * 加入定时任务 + * + * @param id 定时任务ID + * @param expression 定时任务执行时间的cron表达式 + * @param task 任务 + * @return 定时任务ID + */ + public static String schedule(Serializable id, Runnable task, String expression) { + ScheduledFuture schedule = taskScheduler.schedule(task, new CronTrigger(expression)); + TASK_FUTURE.put(id, schedule); + TASK_RUNNABLE.put(id, task); + return id.toString(); + } + + /** + * 修改定时任务 + * + * @param id 定时任务ID + * @param expression 定时任务执行时间的cron表达式 + * @return 是否修改成功,{@code false}表示未找到对应ID的任务 + */ + public static boolean update(Serializable id, String expression) { + if (!TASK_FUTURE.containsKey(id)) { + return false; + } + ScheduledFuture future = TASK_FUTURE.get(id); + if (future == null) { + return false; + } + future.cancel(true); + schedule(id, TASK_RUNNABLE.get(id), expression); + return true; + } + + /** + * 移除任务 + * + * @param schedulerId 任务ID + * @return 是否移除成功,{@code false}表示未找到对应ID的任务 + */ + public static boolean cancel(Serializable schedulerId) { + if (!TASK_FUTURE.containsKey(schedulerId)) { + return false; + } + ScheduledFuture future = TASK_FUTURE.get(schedulerId); + boolean cancel = future.cancel(false); + if (cancel) { + TASK_FUTURE.remove(schedulerId); + TASK_RUNNABLE.remove(schedulerId); + } + return cancel; + } + + @Lazy + @Resource + public void setTaskScheduler(TaskScheduler taskScheduler) { + SpringCronUtil.taskScheduler = taskScheduler; + } + + /** + * @return 获得Scheduler对象 + */ + public static TaskScheduler getScheduler() { + return taskScheduler; + } + + /** + * 获得当前运行的所有任务 + * + * @return 所有任务 + */ + public static List getAllTask() { + if (CollUtil.isNotEmpty(TASK_FUTURE.keySet())) { + return new ArrayList<>(TASK_FUTURE.keySet()); + } + return new ArrayList<>(); + } + + /** + * 取消所有的任务 + */ + public static void destroy() { + for (ScheduledFuture future : TASK_FUTURE.values()) { + if (future != null) { + future.cancel(true); + } + } + TASK_FUTURE.clear(); + TASK_RUNNABLE.clear(); + } +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/spring/condition/ConditionalOnAnnotation.java b/hutool-extra/src/main/java/cn/hutool/extra/spring/condition/ConditionalOnAnnotation.java new file mode 100644 index 0000000000000000000000000000000000000000..8255ecb7464bb43e0165d7d24bb61789d1e703ec --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/spring/condition/ConditionalOnAnnotation.java @@ -0,0 +1,23 @@ +package cn.hutool.extra.spring.condition; + +import org.springframework.context.annotation.Conditional; + +import java.lang.annotation.*; + +/** + * @author JC + * @date 03/15 + */ +@Documented +@Conditional(OnAnnotationCondition.class) +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.METHOD}) +public @interface ConditionalOnAnnotation { + /** + *

需要检查的注解类型 当指定的注解都存在项目时, 则注入该组件

+ *

使用: @ConditionalOnAnnotation({Test.class})

+ * + * @return 检查结果 + */ + Class[] value() default {}; +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/spring/condition/ConditionalOnMissingAnnotation.java b/hutool-extra/src/main/java/cn/hutool/extra/spring/condition/ConditionalOnMissingAnnotation.java new file mode 100644 index 0000000000000000000000000000000000000000..94d4e50a45a711e6a1e22d853d2267c76774d856 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/spring/condition/ConditionalOnMissingAnnotation.java @@ -0,0 +1,17 @@ +package cn.hutool.extra.spring.condition; + +import org.springframework.context.annotation.Conditional; + +import java.lang.annotation.*; + +/** + * @author JC + * @date 03/15 + */ +@Documented +@Conditional(OnAnnotationCondition.class) +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.METHOD}) +public @interface ConditionalOnMissingAnnotation { + +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/spring/condition/OnAnnotationCondition.java b/hutool-extra/src/main/java/cn/hutool/extra/spring/condition/OnAnnotationCondition.java new file mode 100644 index 0000000000000000000000000000000000000000..08293d6f9c9c78703ca92facf3e51b7f4267d79c --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/spring/condition/OnAnnotationCondition.java @@ -0,0 +1,87 @@ +package cn.hutool.extra.spring.condition; + + +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.boot.autoconfigure.AutoConfigurationImportFilter; +import org.springframework.context.annotation.Condition; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.context.annotation.ConfigurationCondition; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.core.annotation.MergedAnnotations; +import org.springframework.core.annotation.Order; +import org.springframework.core.type.AnnotatedTypeMetadata; + +import java.lang.annotation.Annotation; + +/** + * {@link Condition} and {@link AutoConfigurationImportFilter} + * 检查是否存在特定注解 + * + * @author JC + * @date 03/15 + * @see ConditionalOnAnnotation + * @see ConditionalOnMissingAnnotation + */ +@Order(Ordered.HIGHEST_PRECEDENCE) +public class OnAnnotationCondition implements ConfigurationCondition { + private static final String VALUE = "value"; + + @NotNull + @Override + public ConfigurationPhase getConfigurationPhase() { + // 条件不匹配 @Configuration 也不会注入 + return ConfigurationPhase.REGISTER_BEAN; + } + + @Override + public boolean matches(@NotNull ConditionContext context, AnnotatedTypeMetadata metadata) { + MergedAnnotations annotations = metadata.getAnnotations(); + ConfigurableListableBeanFactory beanFactory = context.getBeanFactory(); + // 不存在 beanFactory 不进行注入 + if (beanFactory == null) { + return false; + } + + if (annotations.isPresent(ConditionalOnAnnotation.class)) { + return onAnnotation(beanFactory, annotations); + } + + if (annotations.isPresent(ConditionalOnMissingAnnotation.class)) { + return onMissingAnnotation(beanFactory, annotations); + } + return false; + } + + private boolean onAnnotation(ConfigurableListableBeanFactory beanFactory, MergedAnnotations annotations) { + // 注解参数中, 所有的值 + MergedAnnotation annotation = annotations.get(ConditionalOnAnnotation.class); + Class[] values = annotation.getClassArray(VALUE); + for (Class value : values) { + @SuppressWarnings("unchecked") // 该注解限定了 参数必须是注解, 故忽略警告 + String[] beanNames = beanFactory.getBeanNamesForAnnotation((Class) value); + // 验证 当前注解是否被其他类进行标注 + if (beanNames.length == 0) { + return false; + } + } + return true; + } + + private boolean onMissingAnnotation(ConfigurableListableBeanFactory beanFactory, MergedAnnotations annotations) { + // 注解参数中, 所有的值 + MergedAnnotation annotation = annotations.get(ConditionalOnMissingAnnotation.class); + Class[] values = annotation.getClassArray(VALUE); + for (Class value : values) { + @SuppressWarnings("unchecked") // 该注解限定了 参数必须是注解, 故忽略警告 + String[] beanNames = beanFactory.getBeanNamesForAnnotation((Class) value); + + // 验证 当前注解不能被使用(标注) + if (beanNames.length != 0) { + return false; + } + } + return true; + } +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/spring/condition/package-info.java b/hutool-extra/src/main/java/cn/hutool/extra/spring/condition/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..d9dea5a2860196f3c6ea299704ba41823a954bb1 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/spring/condition/package-info.java @@ -0,0 +1,7 @@ +/** + * Spring相关工具封装 + * + * @author looly + * + */ +package cn.hutool.extra.spring.condition; diff --git a/hutool-extra/src/main/java/cn/hutool/extra/spring/config/SpringCronConfig.java b/hutool-extra/src/main/java/cn/hutool/extra/spring/config/SpringCronConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..5182e784829e6f2d8c7fe4a2e6dab80cd8e11040 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/spring/config/SpringCronConfig.java @@ -0,0 +1,30 @@ +package cn.hutool.extra.spring.config; + +import cn.hutool.extra.spring.condition.ConditionalOnAnnotation; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; + +/** + * @author JC、TCL + * @date 03/15 + */ +@Configuration +public class SpringCronConfig { + @Bean + @ConditionalOnAnnotation(EnableScheduling.class) + @ConditionalOnMissingBean(value = {TaskScheduler.class}) + public TaskScheduler taskScheduler() { + ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); + // 任务线程池初始化 + scheduler.setThreadNamePrefix("TaskScheduler-"); + scheduler.setPoolSize(Runtime.getRuntime().availableProcessors() / 3 + 1); + + // 保证能立刻丢弃运行中的任务 + scheduler.setRemoveOnCancelPolicy(true); + return scheduler; + } +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/spring/config/package-info.java b/hutool-extra/src/main/java/cn/hutool/extra/spring/config/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..551a6e08e74ff97e5ed69bb288221408c9297ced --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/spring/config/package-info.java @@ -0,0 +1,6 @@ +/** + * Spring config package + * @author JC、TCL + * + */ +package cn.hutool.extra.spring.config; diff --git a/hutool-extra/src/main/resources/META-INF/spring.factories b/hutool-extra/src/main/resources/META-INF/spring.factories index 7b08ebdf82fb891e4e5e95614db7d2b17ab11c79..0f0032ef8a1785c9ae7df8128898fa3897c1be74 100644 --- a/hutool-extra/src/main/resources/META-INF/spring.factories +++ b/hutool-extra/src/main/resources/META-INF/spring.factories @@ -1,3 +1,5 @@ # Auto Configure org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ -cn.hutool.extra.spring.SpringUtil \ No newline at end of file +cn.hutool.extra.spring.SpringUtil,\ +cn.hutool.extra.spring.config.SpringCronConfig,\ +cn.hutool.extra.spring.SpringCronUtil diff --git a/hutool-extra/src/test/java/cn/hutool/extra/spring/SpringCronUtilTest.java b/hutool-extra/src/test/java/cn/hutool/extra/spring/SpringCronUtilTest.java new file mode 100644 index 0000000000000000000000000000000000000000..a580332c6a1efa93c45b6690a73ab609f66c9983 --- /dev/null +++ b/hutool-extra/src/test/java/cn/hutool/extra/spring/SpringCronUtilTest.java @@ -0,0 +1,126 @@ +package cn.hutool.extra.spring; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.extra.spring.config.SpringCronConfig; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.junit.After; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import java.io.Serializable; +import java.time.Duration; +import java.time.Instant; +import java.util.Arrays; +import java.util.List; + +/** + * 测试动态定时任务时, 必须用户手动指定 @EnableScheduling + * 否则无法进行测试 默认不开启 (跟Spring一致) + * @author JC + * @date 03/15 + */ +@Slf4j +@RunWith(SpringJUnit4ClassRunner.class) +@SpringBootTest(classes = {SpringUtil.class, SpringCronUtil.class, SpringCronConfig.class}) +public class SpringCronUtilTest { + /** + *

测试自定义注解是否生效

+ *

需要手动添加注解或取消注解进行测试

+ */ + @Test + public void existAnnotation() { + SpringCronUtil bean; + try { + bean = SpringUtil.getBean(SpringCronUtil.class); + } catch (NoSuchBeanDefinitionException e) { + // 没加注解 不会注入, 找不到时会报异常 + Assert.assertNull(null); + return; + } + // 例如: 在SpringUtil上加了@EnableScheduling时, 才会注入组件 + Assert.assertNotNull(bean); + } + + /** + *

创建一个定时任务

+ *

观察日志可进行验证

+ */ + @Test + public void registerTask() { + Long taskId = 666L; + String expression = "0/1 * * * * ?"; + String[] beanDefinitionNames = SpringUtil.getBeanFactory().getBeanDefinitionNames(); + log.info(Arrays.toString(beanDefinitionNames)); + String ID1 = SpringCronUtil.schedule(this::task, expression); + String ID2 = SpringCronUtil.schedule(taskId, this::task, expression); + log.info("taskId: {},{}", ID1, ID2); + } + + /** + * 修改一个定时任务 + */ + @Test + @SneakyThrows + public void updateTask() { + Long taskId = 888L; + String oidExpression = "0/1 * * * * ?"; + String newExpression = "0/5 * * * * ?"; + SpringCronUtil.schedule(taskId, this::task, oidExpression); + Thread.sleep(5000); + boolean update = SpringCronUtil.update(taskId, newExpression); + log.info("update task result: {}", update); + } + + /** + * 取消一个定时任务 + */ + @Test + @SneakyThrows + public void cancelTask() { + Long taskId = 999L; + String expression = "0/1 * * * * ?"; + SpringCronUtil.schedule(taskId, this::task, expression); + Thread.sleep(5000); + boolean cancel = SpringCronUtil.cancel(taskId); + log.info("cancel task result: {}", cancel); + } + + /** + * 高级用法 + * 参考:Spring 官网 + */ + @Test + public void senior() { + TaskScheduler scheduler = SpringCronUtil.getScheduler(); + // 给定时间 开始, 间隔时间.. + scheduler.scheduleAtFixedRate(this::task, Instant.now(), Duration.ofMinutes(10)); + // ... + } + + /** + * 取消全部定时任务 + * 查看当前所有的任务 + */ + @After + @SneakyThrows + public void cancelAll() { + Thread.sleep(10000); + List allTask = SpringCronUtil.getAllTask(); + log.info("allTask: {}", allTask); + + SpringCronUtil.destroy(); + + allTask = SpringCronUtil.getAllTask(); + log.info("allTask: {}", allTask); + } + + private void task() { + log.info("information only.. (date:{})", DateUtil.now()); + } +} diff --git a/hutool-http/pom.xml b/hutool-http/pom.xml index 5f686c62e45522b1e6088aaee9d4d265eea6955d..f3853f53c3d1bf08a0f0099badf1a7d765ef30e7 100644 --- a/hutool-http/pom.xml +++ b/hutool-http/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 5.7.22 + 5.7.23-SNAPSHOT hutool-http diff --git a/hutool-http/src/main/java/cn/hutool/http/HttpRequest.java b/hutool-http/src/main/java/cn/hutool/http/HttpRequest.java index 864363e19df56fa5f73d9e3e1fd216ec3b10e3f7..73883f0c78a7b3d45fe6a6b1c449a13938a14b36 100644 --- a/hutool-http/src/main/java/cn/hutool/http/HttpRequest.java +++ b/hutool-http/src/main/java/cn/hutool/http/HttpRequest.java @@ -9,6 +9,7 @@ import cn.hutool.core.io.resource.MultiFileResource; import cn.hutool.core.io.resource.Resource; import cn.hutool.core.lang.Assert; import cn.hutool.core.map.MapUtil; +import cn.hutool.core.map.TableMap; import cn.hutool.core.net.SSLUtil; import cn.hutool.core.net.url.UrlBuilder; import cn.hutool.core.util.ArrayUtil; @@ -32,7 +33,6 @@ import java.net.Proxy; import java.net.URLStreamHandler; import java.nio.charset.Charset; import java.util.Collection; -import java.util.LinkedHashMap; import java.util.Map; import java.util.function.Consumer; @@ -1161,10 +1161,11 @@ public class HttpRequest extends HttpBase { /** * 对于GET请求将参数加到URL中
- * 此处不对URL中的特殊字符做单独编码 + * 此处不对URL中的特殊字符做单独编码
+ * 对于非rest的GET请求,且处于重定向时,参数丢弃 */ private void urlWithParamIfGet() { - if (Method.GET.equals(method) && false == this.isRest) { + if (Method.GET.equals(method) && false == this.isRest && this.redirectCount > 0) { // 优先使用body形式的参数,不存在使用form if (ArrayUtil.isNotEmpty(this.bodyBytes)) { this.url.getQuery().parse(StrUtil.str(this.bodyBytes, this.charset), this.charset); @@ -1194,7 +1195,7 @@ public class HttpRequest extends HttpBase { if (responseCode != HttpURLConnection.HTTP_OK) { if (HttpStatus.isRedirected(responseCode)) { - setUrl(httpConnection.header(Header.LOCATION)); + setUrl(UrlBuilder.ofHttpWithoutEncode(httpConnection.header(Header.LOCATION))); if (redirectCount < this.maxRedirectCount) { redirectCount++; // 重定向不再走过滤器 @@ -1314,7 +1315,7 @@ public class HttpRequest extends HttpBase { return this; } if (null == this.form) { - this.form = new LinkedHashMap<>(); + this.form = new TableMap<>(16); } this.form.put(name, value); return this; diff --git a/hutool-json/pom.xml b/hutool-json/pom.xml index 7c769ba2ba0a9f9e57b09e2b5ced62f9015801a7..12f8201be008d656ac9464c6af9a4a77432cc824 100644 --- a/hutool-json/pom.xml +++ b/hutool-json/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 5.7.22 + 5.7.23-SNAPSHOT hutool-json diff --git a/hutool-json/src/test/java/cn/hutool/json/IssueI4XFMWTest.java b/hutool-json/src/test/java/cn/hutool/json/IssueI4XFMWTest.java new file mode 100644 index 0000000000000000000000000000000000000000..51cffe12c406742bb1a5ed377f82f3dbf27960f1 --- /dev/null +++ b/hutool-json/src/test/java/cn/hutool/json/IssueI4XFMWTest.java @@ -0,0 +1,42 @@ +package cn.hutool.json; + +import cn.hutool.core.annotation.Alias; +import lombok.Data; +import org.junit.Assert; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +/** + * https://gitee.com/dromara/hutool/issues/I4XFMW + */ +public class IssueI4XFMWTest { + + @Test + public void test() { + List entityList = new ArrayList<>(); + TestEntity entityA = new TestEntity(); + entityA.setId("123"); + entityA.setPassword("456"); + entityList.add(entityA); + + TestEntity entityB = new TestEntity(); + entityB.setId("789"); + entityB.setPassword("098"); + entityList.add(entityB); + + String jsonStr = JSONUtil.toJsonStr(entityList); + Assert.assertEquals("[{\"uid\":\"123\",\"password\":\"456\"},{\"uid\":\"789\",\"password\":\"098\"}]", jsonStr); + List testEntities = JSONUtil.toList(jsonStr, TestEntity.class); + Assert.assertEquals("123", testEntities.get(0).getId()); + Assert.assertEquals("789", testEntities.get(1).getId()); + } + + @Data + static class TestEntity { + @Alias("uid") + private String id; + private String password; + } +} diff --git a/hutool-jwt/pom.xml b/hutool-jwt/pom.xml index 32213fa80b426a25758fd5bef00ddab7cb50bd90..4836888d002e353fa8f58666fc950fca10c81f0c 100644 --- a/hutool-jwt/pom.xml +++ b/hutool-jwt/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 5.7.22 + 5.7.23-SNAPSHOT hutool-jwt diff --git a/hutool-log/pom.xml b/hutool-log/pom.xml index 68e3b1ccc3a5f99aa906207bf211bf5e7f9e89be..3169cdc7ecd9fa820c79458c8cdfc46ed9693443 100644 --- a/hutool-log/pom.xml +++ b/hutool-log/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 5.7.22 + 5.7.23-SNAPSHOT hutool-log diff --git a/hutool-poi/pom.xml b/hutool-poi/pom.xml index e9f56529111ac8bd2af49889f82fb6335f50c4e7..c737053c729d13a38ec2a438aea3ba8c23189d9e 100644 --- a/hutool-poi/pom.xml +++ b/hutool-poi/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 5.7.22 + 5.7.23-SNAPSHOT hutool-poi diff --git a/hutool-script/pom.xml b/hutool-script/pom.xml index fd29dd9a61cb847e451c051d416828dc9c84a174..35b6f525be50130cd001f130e3efc3c7ad59c718 100644 --- a/hutool-script/pom.xml +++ b/hutool-script/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 5.7.22 + 5.7.23-SNAPSHOT hutool-script diff --git a/hutool-script/src/main/java/cn/hutool/script/ScriptRuntimeException.java b/hutool-script/src/main/java/cn/hutool/script/ScriptRuntimeException.java index c609ebbf7bcefc48cc9bc7eeec2ba90034e96d88..48b62e0854828217695da4af720bf369e25cca8f 100644 --- a/hutool-script/src/main/java/cn/hutool/script/ScriptRuntimeException.java +++ b/hutool-script/src/main/java/cn/hutool/script/ScriptRuntimeException.java @@ -42,11 +42,11 @@ public class ScriptRuntimeException extends RuntimeException { } /** - * Creates a ScriptException with message, filename and linenumber to be used in error messages. + * Creates a {@code ScriptException} with message, filename and linenumber to be used in error messages. * * @param message The string to use in the message - * @param fileName The file or resource name describing the location of a script error causing the ScriptException to be thrown. - * @param lineNumber A line number describing the location of a script error causing the ScriptException to be thrown. + * @param fileName The file or resource name describing the location of a script error causing the {@code ScriptException} to be thrown. + * @param lineNumber A line number describing the location of a script error causing the {@code ScriptException} to be thrown. */ public ScriptRuntimeException(String message, String fileName, int lineNumber) { super(message); @@ -55,7 +55,7 @@ public class ScriptRuntimeException extends RuntimeException { } /** - * ScriptException constructor specifying message, filename, line number and column number. + * {@code ScriptException} constructor specifying message, filename, line number and column number. * * @param message The message. * @param fileName The filename diff --git a/hutool-setting/pom.xml b/hutool-setting/pom.xml index d7f1a860e973cb3d39faecad0336e6d49da9ef01..8cf940d71c081fdb6082c29f0b310db9c029a3d1 100644 --- a/hutool-setting/pom.xml +++ b/hutool-setting/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 5.7.22 + 5.7.23-SNAPSHOT hutool-setting diff --git a/hutool-socket/pom.xml b/hutool-socket/pom.xml index 838df72ec3b641ba88a0ef27e5e419188473d9b2..7659cce359e6d244000a004cab13ced890b9a5b1 100644 --- a/hutool-socket/pom.xml +++ b/hutool-socket/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 5.7.22 + 5.7.23-SNAPSHOT hutool-socket diff --git a/hutool-system/pom.xml b/hutool-system/pom.xml index 76f6ae1d0eaf82123e872f03a87a635b734d7a13..0f28acc20734522b634cec533d67074d6233a24a 100644 --- a/hutool-system/pom.xml +++ b/hutool-system/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 5.7.22 + 5.7.23-SNAPSHOT hutool-system diff --git a/pom.xml b/pom.xml index 771da27957e645a13e2b5e8e163f457d52a9b549..4800b8a1f66c5fc0b42b3723127b7003f15434b6 100644 --- a/pom.xml +++ b/pom.xml @@ -8,7 +8,7 @@ cn.hutool hutool-parent - 5.7.22 + 5.7.23-SNAPSHOT hutool Hutool是一个小而全的Java工具类库,通过静态方法封装,降低相关API的学习成本,提高工作效率,使Java拥有函数式语言般的优雅,让Java语言也可以“甜甜的”。 https://github.com/dromara/hutool