diff --git a/CHANGELOG.md b/CHANGELOG.md
index edb403142..fff71d756 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,33 @@
-------------------------------------------------------------------------------------------------------------
+# 5.7.16 (2021-10-31)
+
+### 🐣新特性
+* 【core 】 增加DateTime.toLocalDateTime
+* 【core 】 CharSequenceUtil增加normalize方法(pr#444@Gitee)
+* 【core 】 MailAccount增加setEncodefilename()方法,可选是否编码附件的文件名(issue#I4F160@Gitee)
+* 【core 】 MailAccount中charset增加null时的默认规则
+* 【core 】 NumberUtil.compare修正注释说明(issue#I4FAJ1@Gitee)
+* 【core 】 增加RFC3986类
+* 【extra 】 Sftp增加put和upload重载(issue#I4FGDH@Gitee)
+* 【core 】 TemporalUtil增加toChronoUnit、toTimeUnit方法(issue#I4FGDH@Gitee)
+* 【core 】 StopWatch增加prettyPrint重载(issue#1910@Github)
+* 【core 】 修改RegexPool中Ipv4正则
+* 【json 】 Filter改为MutablePair,以便编辑键值对(issue#1921@Github)
+* 【core 】 Opt增加peeks方法(pr#445@Gitee)
+* 【extra 】 MailAccount中user默认值改为邮箱全称(issue#I4FYVY@Gitee)
+
+### 🐞Bug修复
+* 【core 】 修复UrlBuilder.addPath歧义问题(issue#1912@Github)
+* 【core 】 修复StrBuilder中总长度计算问题(issue#I4F9L7@Gitee)
+* 【core 】 修复CharSequenceUtil.wrapIfMissing预定义长度计算问题(issue#I4FDZ2@Gitee)
+* 【poi 】 修复合并单元格为日期时,导出单元格数据为数字问题(issue#1911@Github)
+* 【core 】 修复CompilerUtil.getFileManager参数没有使用的问题(issue#I4FIO6@Gitee)
+* 【core 】 修复NetUtil.isInRange的cidr判断问题(pr#1917@Github)
+
+-------------------------------------------------------------------------------------------------------------
+
# 5.7.15 (2021-10-21)
### 🐣新特性
@@ -17,6 +44,8 @@
* 【core 】 ZipUtil增加append方法(pr#441@Gitee)
* 【core 】 CollUtil增加重载(issue#I4E9FS@Gitee)
* 【core 】 CopyOptions新增setFieldValueEditor(issue#I4E08T@Gitee)
+* 【core 】 增加SystemPropsUtil(issue#1918@Gitee)
+* 【core 】 增加`hutool.date.lenient`系统属性(issue#1918@Gitee)
### 🐞Bug修复
* 【core 】 修复CollUtil.isEqualList两个null返回错误问题(issue#1885@Github)
diff --git a/README-EN.md b/README-EN.md
index d11d3dfaa..f73d4c304 100644
--- a/README-EN.md
+++ b/README-EN.md
@@ -142,18 +142,18 @@ We provide the T-Shirt and Sweater with Hutool Logo, please visit the shop:
+ * 百分号编码可用于URI的编码,也可以用于"application/x-www-form-urlencoded"的MIME准备数据。
+ *
+ *
+ * 百分号编码会对 URI 中不允许出现的字符或者其他特殊情况的允许的字符进行编码,对于被编码的字符,最终会转为以百分号"%“开头,后面跟着两位16进制数值的形式。 + * 举个例子,空格符(SP)是不允许的字符,在 ASCII 码对应的二进制值是"00100000”,最终转为"%20"。 + *
+ *+ * 对于不同场景应遵循不同规范: + * + *
+ * StopWatch '[id]': running time = [total] ns + ** * @return 任务信息 */ public String shortSummary() { - return StrUtil.format("StopWatch '{}': running time = {} ns", this.id, this.totalTimeNanos); + return shortSummary(null); + } + + /** + * 获取任务信息,类似于: + *
+ * StopWatch '[id]': running time = [total] [unit] + *+ * + * @param unit 时间单位,{@code null}则默认为{@link TimeUnit#NANOSECONDS} + * @return 任务信息 + */ + public String shortSummary(TimeUnit unit) { + if(null == unit){ + unit = TimeUnit.NANOSECONDS; + } + return StrUtil.format("StopWatch '{}': running time = {} {}", + this.id, getTotal(unit), DateUtil.getShotName(unit)); + } + + /** + * 生成所有任务的一个任务花费时间表,单位纳秒 + * + * @return 任务时间表 + */ + public String prettyPrint() { + return prettyPrint(null); } /** * 生成所有任务的一个任务花费时间表 * + * @param unit 时间单位,{@code null}则默认{@link TimeUnit#NANOSECONDS} 纳秒 * @return 任务时间表 + * @since 5.7.16 */ - public String prettyPrint() { - StringBuilder sb = new StringBuilder(shortSummary()); + public String prettyPrint(TimeUnit unit) { + if (null == unit) { + unit = TimeUnit.NANOSECONDS; + } + + final StringBuilder sb = new StringBuilder(shortSummary(unit)); sb.append(FileUtil.getLineSeparator()); if (null == this.taskList) { sb.append("No task info kept"); } else { sb.append("---------------------------------------------").append(FileUtil.getLineSeparator()); - sb.append("ns % Task name").append(FileUtil.getLineSeparator()); + sb.append(DateUtil.getShotName(unit)).append(" % Task name").append(FileUtil.getLineSeparator()); sb.append("---------------------------------------------").append(FileUtil.getLineSeparator()); final NumberFormat nf = NumberFormat.getNumberInstance(); @@ -334,11 +381,12 @@ public class StopWatch { nf.setGroupingUsed(false); final NumberFormat pf = NumberFormat.getPercentInstance(); - pf.setMinimumIntegerDigits(3); + pf.setMinimumIntegerDigits(2); pf.setGroupingUsed(false); + for (TaskInfo task : getTaskInfo()) { - sb.append(nf.format(task.getTimeNanos())).append(" "); - sb.append(pf.format((double) task.getTimeNanos() / getTotalTimeNanos())).append(" "); + sb.append(nf.format(task.getTime(unit))).append(" "); + sb.append(pf.format((double) task.getTimeNanos() / getTotalTimeNanos())).append(" "); sb.append(task.getTaskName()).append(FileUtil.getLineSeparator()); } } @@ -370,6 +418,12 @@ public class StopWatch { private final String taskName; private final long timeNanos; + /** + * 构造 + * + * @param taskName 任务名称 + * @param timeNanos 花费时间(纳秒) + */ TaskInfo(String taskName, long timeNanos) { this.taskName = taskName; this.timeNanos = timeNanos; @@ -384,6 +438,17 @@ public class StopWatch { return this.taskName; } + /** + * 获取指定单位的任务花费时间 + * + * @param unit 单位 + * @return 任务花费时间 + * @since 5.7.16 + */ + public long getTime(TimeUnit unit) { + return unit.convert(this.timeNanos, TimeUnit.NANOSECONDS); + } + /** * 获取任务花费时间(单位:纳秒) * @@ -403,7 +468,7 @@ public class StopWatch { * @see #getTimeSeconds() */ public long getTimeMillis() { - return DateUtil.nanosToMillis(this.timeNanos); + return getTime(TimeUnit.MILLISECONDS); } /** diff --git a/hutool-core/src/main/java/cn/hutool/core/date/TemporalUtil.java b/hutool-core/src/main/java/cn/hutool/core/date/TemporalUtil.java index 28878b69a..d003f4971 100644 --- a/hutool-core/src/main/java/cn/hutool/core/date/TemporalUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/date/TemporalUtil.java @@ -3,6 +3,7 @@ package cn.hutool.core.date; import java.time.Duration; import java.time.temporal.ChronoUnit; import java.time.temporal.Temporal; +import java.util.concurrent.TimeUnit; /** * {@link Temporal} 工具类封装 @@ -38,4 +39,67 @@ public class TemporalUtil { public static long between(Temporal startTimeInclude, Temporal endTimeExclude, ChronoUnit unit) { return unit.between(startTimeInclude, endTimeExclude); } + + /** + * 将 {@link TimeUnit} 转换为 {@link ChronoUnit}. + * + * @param unit 被转换的{@link TimeUnit}单位,如果为{@code null}返回{@code null} + * @return {@link ChronoUnit} + * @since 5.7.16 + */ + public static ChronoUnit toChronoUnit(TimeUnit unit) throws IllegalArgumentException { + if (null == unit) { + return null; + } + switch (unit) { + case NANOSECONDS: + return ChronoUnit.NANOS; + case MICROSECONDS: + return ChronoUnit.MICROS; + case MILLISECONDS: + return ChronoUnit.MILLIS; + case SECONDS: + return ChronoUnit.SECONDS; + case MINUTES: + return ChronoUnit.MINUTES; + case HOURS: + return ChronoUnit.HOURS; + case DAYS: + return ChronoUnit.DAYS; + default: + throw new IllegalArgumentException("Unknown TimeUnit constant"); + } + } + + /** + * 转换 {@link ChronoUnit} 到 {@link TimeUnit}. + * + * @param unit {@link ChronoUnit},如果为{@code null}返回{@code null} + * @return {@link TimeUnit} + * @throws IllegalArgumentException 如果{@link TimeUnit}没有对应单位抛出 + * @since 5.7.16 + */ + public static TimeUnit toTimeUnit(ChronoUnit unit) throws IllegalArgumentException { + if (null == unit) { + return null; + } + switch (unit) { + case NANOS: + return TimeUnit.NANOSECONDS; + case MICROS: + return TimeUnit.MICROSECONDS; + case MILLIS: + return TimeUnit.MILLISECONDS; + case SECONDS: + return TimeUnit.SECONDS; + case MINUTES: + return TimeUnit.MINUTES; + case HOURS: + return TimeUnit.HOURS; + case DAYS: + return TimeUnit.DAYS; + default: + throw new IllegalArgumentException("ChronoUnit cannot be converted to TimeUnit: " + unit); + } + } } diff --git a/hutool-core/src/main/java/cn/hutool/core/lang/Assert.java b/hutool-core/src/main/java/cn/hutool/core/lang/Assert.java index fd4751d6a..a33b2a4ed 100644 --- a/hutool-core/src/main/java/cn/hutool/core/lang/Assert.java +++ b/hutool-core/src/main/java/cn/hutool/core/lang/Assert.java @@ -840,6 +840,7 @@ public class Assert { /** * 检查值是否在指定范围内 * + * @param
属于 {@link #ifPresent}的链式拓展 + *
属于 {@link #peek(Consumer)}的动态拓展
+ *
+ * @param actions 值存在时执行的操作,动态参数,可传入数组,当数组为一个空数组时并不会抛出 {@code NPE}
+ * @return this
+ * @throws NullPointerException 如果值存在,并且传入的操作集中的元素为 {@code null}
+ * @author VampireAchao
+ */
+ @SafeVarargs
+ public final Opt
+ * 采用分组方式便于解析地址的每一个段
*/
- String IPV4 = "\\b((?!\\d\\d\\d)\\d+|1\\d\\d|2[0-4]\\d|25[0-5])\\.((?!\\d\\d\\d)\\d+|1\\d\\d|2[0-4]\\d|25[0-5])\\.((?!\\d\\d\\d)\\d+|1\\d\\d|2[0-4]\\d|25[0-5])\\.((?!\\d\\d\\d)\\d+|1\\d\\d|2[0-4]\\d|25[0-5])\\b";
+ //String IPV4 = "\\b((?!\\d\\d\\d)\\d+|1\\d\\d|2[0-4]\\d|25[0-5])\\.((?!\\d\\d\\d)\\d+|1\\d\\d|2[0-4]\\d|25[0-5])\\.((?!\\d\\d\\d)\\d+|1\\d\\d|2[0-4]\\d|25[0-5])\\.((?!\\d\\d\\d)\\d+|1\\d\\d|2[0-4]\\d|25[0-5])\\b";
+ String IPV4 = "^(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)$";
/**
* IP v6
*/
diff --git a/hutool-core/src/main/java/cn/hutool/core/lang/mutable/MutableDouble.java b/hutool-core/src/main/java/cn/hutool/core/lang/mutable/MutableDouble.java
index 31a72d8bd..17702fbe7 100644
--- a/hutool-core/src/main/java/cn/hutool/core/lang/mutable/MutableDouble.java
+++ b/hutool-core/src/main/java/cn/hutool/core/lang/mutable/MutableDouble.java
@@ -3,7 +3,7 @@ package cn.hutool.core.lang.mutable;
import cn.hutool.core.util.NumberUtil;
/**
- * 可变 double
类型
+ * 可变 {@code double} 类型
*
* @see Double
* @since 3.0.1
@@ -150,12 +150,12 @@ public class MutableDouble extends Number implements Comparable
*
*
* @param obj 比对的对象
- * @return 相同返回true
,否则 false
+ * @return 相同返回true
,否则 {@code false}
*/
@Override
public boolean equals(final Object obj) {
@@ -175,7 +175,7 @@ public class MutableDouble extends Number implements Comparable
+ * 对于{@code null}处理规则如下:
+ *
+ *
*
* @param charset encode编码,null表示不做encode编码
- * @param isEncode 是否转义键和值
+ * @param isEncode 是否转义键和值,转义遵循rfc3986规范
* @return URL查询字符串
* @since 5.7.13
*/
@@ -233,21 +239,18 @@ public class UrlQuery {
}
final StringBuilder sb = new StringBuilder();
- boolean isFirst = true;
- CharSequence key;
+ CharSequence name;
CharSequence value;
for (Map.Entry
+ * 因此使用此方法归一为一种表示形式,默认按照W3C通常建议的,在NFC中交换文本。
+ *
+ * @param str 归一化的字符串
+ * @return 归一化后的字符串
+ * @see Normalizer#normalize(CharSequence, Normalizer.Form)
+ * @since 5.7.16
+ */
+ public static String normalize(CharSequence str) {
+ return Normalizer.normalize(str, Normalizer.Form.NFC);
}
}
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 0de657e51..69bc75f04 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
@@ -579,7 +579,7 @@ public class StrBuilder implements CharSequence, Appendable, Serializable {
private static int totalLength(CharSequence... strs) {
int totalLength = 0;
for (CharSequence str : strs) {
- totalLength += (null == str ? 4 : str.length());
+ totalLength += (null == str ? 0 : str.length());
}
return totalLength;
}
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 5912fd56b..adc11b2f8 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
@@ -22,7 +22,6 @@ import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
-import java.util.Objects;
import java.util.Random;
import java.util.Set;
import java.util.function.Function;
@@ -118,7 +117,7 @@ public class ArrayUtil extends PrimitiveArrayUtil {
public static
+ * 此工具用于读取系统属性或环境变量信息,封装包括:
+ *
+ *
+ *
+ * @author looly
+ * @since 5.7.16
+ */
+public class SystemPropsUtil {
+
+ /** Hutool自定义系统属性:是否解析日期字符串采用严格模式 */
+ public static String HUTOOL_DATE_LENIENT = "hutool.date.lenient";
+
+ /**
+ * 取得系统属性,如果因为Java安全的限制而失败,则将错误打在Log中,然后返回 defaultValue
+ *
+ * @param name 属性名
+ * @param defaultValue 默认值
+ * @return 属性值或defaultValue
+ * @see System#getProperty(String)
+ * @see System#getenv(String)
+ */
+ public static String get(String name, String defaultValue) {
+ return StrUtil.nullToDefault(get(name, false), defaultValue);
+ }
+
+ /**
+ * 取得系统属性,如果因为Java安全的限制而失败,则将错误打在Log中,然后返回 {@code null}
+ *
+ * @param name 属性名
+ * @param quiet 安静模式,不将出错信息打在{@code System.err}中
+ * @return 属性值或{@code null}
+ * @see System#getProperty(String)
+ * @see System#getenv(String)
+ */
+ public static String get(String name, boolean quiet) {
+ String value = null;
+ try {
+ value = System.getProperty(name);
+ } catch (SecurityException e) {
+ if (false == quiet) {
+ Console.error("Caught a SecurityException reading the system property '{}'; " +
+ "the SystemUtil property value will default to null.", name);
+ }
+ }
+
+ if (null == value) {
+ try {
+ value = System.getenv(name);
+ } catch (SecurityException e) {
+ if (false == quiet) {
+ Console.error("Caught a SecurityException reading the system env '{}'; " +
+ "the SystemUtil env value will default to null.", name);
+ }
+ }
+ }
+
+ return value;
+ }
+
+ /**
+ * 获得System属性
+ *
+ * @param key 键
+ * @return 属性值
+ * @see System#getProperty(String)
+ * @see System#getenv(String)
+ */
+ public static String get(String key) {
+ return get(key, null);
+ }
+
+ /**
+ * 获得boolean类型值
+ *
+ * @param key 键
+ * @param defaultValue 默认值
+ * @return 值
+ */
+ public static boolean getBoolean(String key, boolean defaultValue) {
+ String value = get(key);
+ if (value == null) {
+ return defaultValue;
+ }
+
+ value = value.trim().toLowerCase();
+ if (value.isEmpty()) {
+ return true;
+ }
+
+ return Convert.toBool(value, defaultValue);
+ }
+
+ /**
+ * 获得int类型值
+ *
+ * @param key 键
+ * @param defaultValue 默认值
+ * @return 值
+ */
+ public static long getInt(String key, int defaultValue) {
+ return Convert.toInt(get(key), defaultValue);
+ }
+
+ /**
+ * 获得long类型值
+ *
+ * @param key 键
+ * @param defaultValue 默认值
+ * @return 值
+ */
+ public static long getLong(String key, long defaultValue) {
+ return Convert.toLong(get(key), defaultValue);
+ }
+
+ /**
+ * @return 属性列表
+ */
+ public static Properties getProps() {
+ return System.getProperties();
+ }
+
+ /**
+ * 设置系统属性,value为{@code null}表示移除此属性
+ *
+ * @param key 属性名
+ * @param value 属性值,{@code null}表示移除此属性
+ */
+ public static void set(String key, String value) {
+ if (null == value) {
+ System.clearProperty(key);
+ } else {
+ System.setProperty(key, value);
+ }
+ }
+}
diff --git a/hutool-core/src/main/java/cn/hutool/core/util/W84Util.java b/hutool-core/src/main/java/cn/hutool/core/util/W84Util.java
new file mode 100644
index 000000000..e093d3b96
--- /dev/null
+++ b/hutool-core/src/main/java/cn/hutool/core/util/W84Util.java
@@ -0,0 +1,172 @@
+package cn.hutool.core.util;
+
+/**
+ * 坐标转换相关工具类
+ *
+ * 坐标转换相关参考网址: https://tool.lu/coordinate/
+ * @author hongzhe.qin
+ * @email qin462328037@163.com
+ * @since 6.0
+ */
+public class W84Util {
+
+ /**
+ * 坐标转换参数:(暂时位置具体名称)
+ */
+ public static final Double X_PI = 3.14159265358979324 * 3000.0 / 180.0;
+
+ /**
+ * 坐标转换参数:π
+ */
+ public static final Double PI = 3.1415926535897932384626;
+
+ /**
+ * 地球半径
+ */
+ public static final Double RADIUS = 6378245.0;
+
+ /**
+ * 修正参数
+ */
+ public static final Double CORRECTION_PARAM = 0.00669342162296594323;
+
+
+ /**
+ * 计算维度坐标
+ *
+ * @param lng 经度
+ * @param lat 维度
+ * @return ret 计算完成后的
+ */
+ private static Double transForMLat(double lng, double lat) {
+ Double ret = -100.0 + 2.0 * lng + 3.0 * lat + 0.2 * lat * lat + 0.1 * lng * lat + 0.2 * Math.sqrt(Math.abs(lng));
+ ret = transCore(ret, lng, lat);
+ ret += (160.0 * Math.sin(lat / 12.0 * PI) + 320 * Math.sin(lat * PI / 30.0)) * 2.0 / 3.0;
+ return ret;
+ }
+
+ /**
+ * 计算经度坐标
+ *
+ * @param lng 经度坐标
+ * @param lat 维度坐标
+ * @return ret 计算完成后的
+ */
+ private static Double transForMLng(Double lng, Double lat) {
+ Double ret = 300.0 + lng + 2.0 * lat + 0.1 * lng * lng + 0.1 * lng * lat + 0.1 * Math.sqrt(Math.abs(lng));
+ ret = transCore(ret, lng, lat);
+ ret += (150.0 * Math.sin(lng / 12.0 * PI) + 300.0 * Math.sin(lng / 30.0 * PI)) * 2.0 / 3.0;
+ return ret;
+ }
+
+ /**
+ * 转换坐标公共核心
+ *
+ * @param ret 计算需要返回结果
+ * @param lng 经度坐标
+ * @param lat 维度坐标
+ * @return 返回结果
+ */
+ private static Double transCore(Double ret, Double lng, Double lat) {
+ ret += (20.0 * Math.sin(6.0 * lng * PI) + 20.0 * Math.sin(2.0 * lng * PI)) * 2.0 / 3.0;
+ ret += (20.0 * Math.sin(lng * PI) + 40.0 * Math.sin(lng / 3.0 * PI)) * 2.0 / 3.0;
+ return ret;
+ }
+
+ private static Double transW84Core(Double lat){
+ return 1 - CORRECTION_PARAM * Math.sin(lat / 180.0 * PI) * Math.sin(lat / 180.0 * PI);
+ }
+
+ /**
+ * 火星坐标系 (GCJ-02) 核心计算
+ * @param lng 经度值
+ * @param lat 纬度值
+ * @return 坐标
+ */
+ private static Double[] transGCJ02Core(Double lng,Double lat){
+ Double dlat = transForMLat(lng - 105.0, lat - 35.0);
+ Double dlng = transForMLng(lng - 105.0, lat - 35.0);
+ Double magic = transW84Core(lat);
+ Double sqrtmagic = Math.sqrt(magic);
+ dlat = (dlat * 180.0) / ((RADIUS * (1 - CORRECTION_PARAM)) / (magic * sqrtmagic) * PI);
+ dlng = (dlng * 180.0) / (RADIUS / sqrtmagic * Math.cos(lat / 180.0 * PI) * PI);
+ Double mglat = lat + dlat;
+ Double mglng = lng + dlng;
+ return new Double[]{mglng,mglat};
+ }
+
+ /**
+ * WGS84 坐标转为 百度坐标系 (BD-09) 坐标
+ * @param lng 经度值
+ * @param lat 维度值
+ * @return bd09 坐标
+ */
+ public static Double[] wgs84tobd09(Double lng, Double lat) {
+ // 第一次转换
+ Double dlat = transForMLat(lng - 105.0, lat - 35.0);
+ Double dlng = transForMLng(lng - 105.0, lat - 35.0);
+ Double magic = transW84Core(lat);
+ Double sqrtmagic = Math.sqrt(1 - CORRECTION_PARAM * Math.sin(lat / 180.0 * PI) * Math.sin(lat / 180.0 * PI));
+ Double mglat = lat + (dlat * 180.0) / ((RADIUS * (1 - CORRECTION_PARAM)) / (magic * sqrtmagic) * PI);
+ Double mglng = lng + (dlng * 180.0) / (RADIUS / sqrtmagic * Math.cos(lat / 180.0 * PI) * PI);
+ // 第二次转换
+ Double z = Math.sqrt(mglng * mglng + mglat * mglat) + 0.00002 * Math.sin(mglat * X_PI);
+ Double theta = Math.atan2(mglat, mglng) + 0.000003 * Math.cos(mglng * X_PI);
+ Double bd_lng = z * Math.cos(theta) + 0.0065;
+ Double bd_lat = z * Math.sin(theta) + 0.006;
+ return new Double[]{bd_lng,bd_lat};
+ }
+
+ /**
+ * WGS84 转换为 火星坐标系 (GCJ-02)
+ *
+ * @param lng
+ * @param lat
+ * @returns {*[]}
+ */
+ public static Double[] wgs84togcj02(Double lng, Double lat) {
+ return transGCJ02Core(lng,lat);
+ }
+
+ /**
+ * 火星坐标系 (GCJ-02) 转换为 WGS84
+ * @param lng 经度坐标
+ * @param lat 维度坐标
+ * @return WGS84 坐标
+ */
+ public static Double[] gcj02towgs84(Double lng, Double lat) {
+ return transGCJ02Core(lng,lat);
+ }
+
+ /**
+ * 百度坐标系 (BD-09) 与 火星坐标系 (GCJ-02)的转换
+ * 即 百度 转 谷歌、高德
+ * @param bd_lon 经度值
+ * @param bd_lat 纬度值
+ * @return GCJ-02 坐标
+ */
+ public static Double[] bd09togcj02(Double bd_lon, Double bd_lat) {
+ Double x = bd_lon - 0.0065;
+ Double y = bd_lat - 0.006;
+ Double z = Math.sqrt(x * x + y * y) - 0.00002 * Math.sin(y * X_PI);
+ Double theta = Math.atan2(y, x) - 0.000003 * Math.cos(x * X_PI);
+ Double gg_lng = z * Math.cos(theta);
+ Double gg_lat = z * Math.sin(theta);
+ return new Double[]{gg_lng,gg_lat};
+ }
+
+ /**
+ * 火星坐标系 (GCJ-02) 与百度坐标系 (BD-09) 的转换
+ * @param lng 经度值
+ * @param lat 纬度值
+ * @return BD-09 坐标
+ */
+ public static Double[] gcj02tobd09(Double lng, Double lat) {
+ double z = Math.sqrt(lng * lng + lat * lat) + 0.00002 * Math.sin(lat * X_PI);
+ double theta = Math.atan2(lat, lng) + 0.000003 * Math.cos(lng * X_PI);
+ double bd_lng = z * Math.cos(theta) + 0.0065;
+ double bd_lat = z * Math.sin(theta) + 0.006;
+ return new Double[]{bd_lng,bd_lat};
+ }
+
+}
diff --git a/hutool-core/src/main/java/cn/hutool/core/util/ZipUtil.java b/hutool-core/src/main/java/cn/hutool/core/util/ZipUtil.java
index a813242bf..37e77b5d5 100644
--- a/hutool-core/src/main/java/cn/hutool/core/util/ZipUtil.java
+++ b/hutool-core/src/main/java/cn/hutool/core/util/ZipUtil.java
@@ -91,9 +91,10 @@ public class ZipUtil {
* @param zipPath zip文件的Path
* @param appendFilePath 待添加文件Path(可以是文件夹)
* @param options 拷贝选项,可选是否覆盖等
+ * @throws IORuntimeException IO异常
* @since 5.7.15
*/
- public static void append(Path zipPath, Path appendFilePath, CopyOption... options) throws IOException {
+ public static void append(Path zipPath, Path appendFilePath, CopyOption... options) throws IORuntimeException {
try (FileSystem zipFileSystem = FileSystemUtil.createZip(zipPath.toString())) {
if (Files.isDirectory(appendFilePath)) {
Path source = appendFilePath.getParent();
@@ -107,6 +108,8 @@ public class ZipUtil {
}
} catch (FileAlreadyExistsException ignored) {
// 不覆盖情况下,文件已存在, 跳过
+ } catch (IOException e){
+ throw new IORuntimeException(e);
}
}
diff --git a/hutool-core/src/test/java/cn/hutool/core/lang/OptTest.java b/hutool-core/src/test/java/cn/hutool/core/lang/OptTest.java
index 9c52e98fe..8e7f5da2f 100644
--- a/hutool-core/src/test/java/cn/hutool/core/lang/OptTest.java
+++ b/hutool-core/src/test/java/cn/hutool/core/lang/OptTest.java
@@ -63,6 +63,34 @@ public class OptTest {
Assert.assertEquals("hutool", name);
}
+ @Test
+ public void peeksTest() {
+ User user = new User();
+ // 相当于上面peek的动态参数调用,更加灵活,你可以像操作数组一样去动态设置中间的步骤,也可以使用这种方式去编写你的代码
+ // 可以一行搞定
+ Opt.ofNullable("hutool").peeks(user::setUsername, user::setNickname);
+ // 也可以在适当的地方换行使得代码的可读性提高
+ Opt.of(user).peeks(
+ u -> Assert.assertEquals("hutool", u.getNickname()),
+ u -> Assert.assertEquals("hutool", u.getUsername())
+ );
+ Assert.assertEquals("hutool", user.getNickname());
+ Assert.assertEquals("hutool", user.getUsername());
+
+ // 注意,传入的lambda中,对包裹内的元素执行赋值操作并不会影响到原来的元素,这是java语言的特性。。。
+ // 这也是为什么我们需要getter和setter而不直接给bean中的属性赋值中的其中一个原因
+ String name = Opt.ofNullable("hutool").peeks(
+ username -> username = "123", username -> username = "456",
+ n -> Assert.assertEquals("hutool", n)).get();
+ Assert.assertEquals("hutool", name);
+
+ // 当然,以下情况不会抛出NPE,但也没什么意义
+ Opt.ofNullable("hutool").peeks().peeks().peeks();
+ Opt.ofNullable(null).peeks(i -> {
+ });
+
+ }
+
@Test
public void orTest() {
// 这是jdk9 Optional中的新函数,直接照搬了过来
diff --git a/hutool-core/src/test/java/cn/hutool/core/lang/ValidatorTest.java b/hutool-core/src/test/java/cn/hutool/core/lang/ValidatorTest.java
index a14a3d8dd..cfcd7866f 100644
--- a/hutool-core/src/test/java/cn/hutool/core/lang/ValidatorTest.java
+++ b/hutool-core/src/test/java/cn/hutool/core/lang/ValidatorTest.java
@@ -21,7 +21,7 @@ public class ValidatorTest {
}
@Test
- public void hasNumberTest() throws Exception {
+ public void hasNumberTest() {
String var1 = "";
String var2 = "str";
String var3 = "180";
@@ -218,4 +218,13 @@ public class ValidatorTest {
public void isCarDrivingLicenceTest(){
Assert.assertTrue(Validator.isCarDrivingLicence("430101758218"));
}
+
+ @Test
+ public void validateIpv4Test(){
+ Validator.validateIpv4("192.168.1.1", "Error ip");
+ Validator.validateIpv4("8.8.8.8", "Error ip");
+ Validator.validateIpv4("0.0.0.0", "Error ip");
+ Validator.validateIpv4("255.255.255.255", "Error ip");
+ Validator.validateIpv4("127.0.0.0", "Error ip");
+ }
}
diff --git a/hutool-core/src/test/java/cn/hutool/core/net/Ipv4UtilTest.java b/hutool-core/src/test/java/cn/hutool/core/net/Ipv4UtilTest.java
index 5a472963c..13d5ddbd1 100644
--- a/hutool-core/src/test/java/cn/hutool/core/net/Ipv4UtilTest.java
+++ b/hutool-core/src/test/java/cn/hutool/core/net/Ipv4UtilTest.java
@@ -1,11 +1,10 @@
package cn.hutool.core.net;
-import cn.hutool.core.lang.Console;
import org.junit.Assert;
import org.junit.Test;
+import org.junit.function.ThrowingRunnable;
import java.util.List;
-import org.junit.function.ThrowingRunnable;
public class Ipv4UtilTest {
@@ -40,7 +39,7 @@ public class Ipv4UtilTest {
String ip = "192.168.1.1";
final int maskBitByMask = Ipv4Util.getMaskBitByMask("255.255.255.0");
final String endIpStr = Ipv4Util.getEndIpStr(ip, maskBitByMask);
- Console.log(endIpStr);
+ Assert.assertEquals("192.168.1.255", endIpStr);
}
@Test
@@ -75,4 +74,16 @@ public class Ipv4UtilTest {
boolean maskBitValid = Ipv4Util.isMaskBitValid(33);
Assert.assertFalse("掩码位非法检验", maskBitValid);
}
+
+ @Test
+ public void ipv4ToLongTest(){
+ long l = Ipv4Util.ipv4ToLong("127.0.0.1");
+ Assert.assertEquals(2130706433L, l);
+ l = Ipv4Util.ipv4ToLong("114.114.114.114");
+ Assert.assertEquals(1920103026L, l);
+ l = Ipv4Util.ipv4ToLong("0.0.0.0");
+ Assert.assertEquals(0L, l);
+ l = Ipv4Util.ipv4ToLong("255.255.255.255");
+ Assert.assertEquals(4294967295L, l);
+ }
}
diff --git a/hutool-core/src/test/java/cn/hutool/core/net/NetUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/net/NetUtilTest.java
index ff467456a..0bed82723 100644
--- a/hutool-core/src/test/java/cn/hutool/core/net/NetUtilTest.java
+++ b/hutool-core/src/test/java/cn/hutool/core/net/NetUtilTest.java
@@ -101,4 +101,15 @@ public class NetUtilTest {
Console.log(txt);
}
+ @Test
+ public void isInRangeTest(){
+ Assert.assertTrue(NetUtil.isInRange("114.114.114.114","0.0.0.0/0"));
+ Assert.assertTrue(NetUtil.isInRange("192.168.3.4","192.0.0.0/8"));
+ Assert.assertTrue(NetUtil.isInRange("192.168.3.4","192.168.0.0/16"));
+ Assert.assertTrue(NetUtil.isInRange("192.168.3.4","192.168.3.0/24"));
+ Assert.assertTrue(NetUtil.isInRange("192.168.3.4","192.168.3.4/32"));
+ Assert.assertFalse(NetUtil.isInRange("8.8.8.8","192.0.0.0/8"));
+ Assert.assertFalse(NetUtil.isInRange("114.114.114.114","192.168.3.4/32"));
+ }
+
}
diff --git a/hutool-core/src/test/java/cn/hutool/core/net/UrlBuilderTest.java b/hutool-core/src/test/java/cn/hutool/core/net/UrlBuilderTest.java
index 78e4d93ee..ea7604161 100644
--- a/hutool-core/src/test/java/cn/hutool/core/net/UrlBuilderTest.java
+++ b/hutool-core/src/test/java/cn/hutool/core/net/UrlBuilderTest.java
@@ -281,4 +281,29 @@ public class UrlBuilderTest {
final UrlBuilder urlBuilder = UrlBuilder.ofHttp(url);
Assert.assertEquals(url, urlBuilder.toString());
}
+
+ @Test
+ public void addPathEncodeTest(){
+ String url = UrlBuilder.create()
+ .setScheme("https")
+ .setHost("domain.cn")
+ .addPath("api")
+ .addPath("xxx")
+ .addPath("bbb")
+ .build();
+
+ Assert.assertEquals("https://domain.cn/api/xxx/bbb", url);
+ }
+
+ @Test
+ public void addPathEncodeTest2(){
+ // https://github.com/dromara/hutool/issues/1912
+ String url = UrlBuilder.create()
+ .setScheme("https")
+ .setHost("domain.cn")
+ .addPath("/api/xxx/bbb")
+ .build();
+
+ Assert.assertEquals("https://domain.cn/api/xxx/bbb", url);
+ }
}
diff --git a/hutool-core/src/test/java/cn/hutool/core/net/UrlQueryTest.java b/hutool-core/src/test/java/cn/hutool/core/net/UrlQueryTest.java
index d4ab80682..85d11ec00 100644
--- a/hutool-core/src/test/java/cn/hutool/core/net/UrlQueryTest.java
+++ b/hutool-core/src/test/java/cn/hutool/core/net/UrlQueryTest.java
@@ -63,4 +63,40 @@ public class UrlQueryTest {
query = URLUtil.buildQuery(map, StandardCharsets.UTF_8);
Assert.assertEquals("password=123456&username=SSM", query);
}
+
+ @Test
+ public void buildHasNullTest() {
+ Mapnull
生成随机密钥
+ * @param key 密钥,如果为{@code null}生成随机密钥
* @return {@link HMac}
* @since 3.0.3
*/
@@ -436,7 +435,7 @@ public class DigestUtil {
* 创建HMac对象,调用digest方法可获得hmac值
*
* @param algorithm {@link HmacAlgorithm}
- * @param key 密钥{@link SecretKey},如果为null
生成随机密钥
+ * @param key 密钥{@link SecretKey},如果为{@code null}生成随机密钥
* @return {@link HMac}
* @since 3.0.3
*/
diff --git a/hutool-db/pom.xml b/hutool-db/pom.xml
index dcfd6d332..99574b86d 100644
--- a/hutool-db/pom.xml
+++ b/hutool-db/pom.xml
@@ -9,7 +9,7 @@
- * 1. path为null或""上传到当前路径
- * 2. path为相对路径则相对于当前路径的子路径
- * 3. path为绝对路径则上传到此路径
+ * 1. destPath为null或""上传到当前路径
+ * 2. destPath为相对路径则相对于当前路径的子路径
+ * 3. destPath为绝对路径则上传到此路径
*
*
* @param file 文件
@@ -512,9 +512,9 @@ public class Ftp extends AbstractFtp {
* 上传文件到指定目录,可选:
*
*
- * 1. path为null或""上传到当前路径
- * 2. path为相对路径则相对于当前路径的子路径
- * 3. path为绝对路径则上传到此路径
+ * 1. destPath为null或""上传到当前路径
+ * 2. destPath为相对路径则相对于当前路径的子路径
+ * 3. destPath为绝对路径则上传到此路径
*
*
* @param destPath 服务端路径,可以为{@code null} 或者相对路径或绝对路径
diff --git a/hutool-extra/src/main/java/cn/hutool/extra/mail/InternalMailUtil.java b/hutool-extra/src/main/java/cn/hutool/extra/mail/InternalMailUtil.java
index a4e015cf3..64eddb20a 100644
--- a/hutool-extra/src/main/java/cn/hutool/extra/mail/InternalMailUtil.java
+++ b/hutool-extra/src/main/java/cn/hutool/extra/mail/InternalMailUtil.java
@@ -17,11 +17,11 @@ import java.util.List;
* @since 3.2.3
*/
public class InternalMailUtil {
-
+
/**
* 将多个字符串邮件地址转为{@link InternetAddress}列表
* 单个字符串地址可以是多个地址合并的字符串
- *
+ *
* @param addrStrs 地址数组
* @param charset 编码(主要用于中文用户名的编码)
* @return 地址数组
@@ -38,12 +38,12 @@ public class InternalMailUtil {
}
return resultList.toArray(new InternetAddress[0]);
}
-
+
/**
* 解析第一个地址
- *
+ *
* @param address 地址字符串
- * @param charset 编码
+ * @param charset 编码,{@code null}表示使用系统属性定义的编码或系统编码
* @return 地址列表
*/
public static InternetAddress parseFirstAddress(String address, Charset charset) {
@@ -61,9 +61,9 @@ public class InternalMailUtil {
/**
* 将一个地址字符串解析为多个地址
* 地址间使用" "、","、";"分隔
- *
+ *
* @param address 地址字符串
- * @param charset 编码
+ * @param charset 编码,{@code null}表示使用系统属性定义的编码或系统编码
* @return 地址列表
*/
public static InternetAddress[] parseAddress(String address, Charset charset) {
@@ -75,9 +75,10 @@ public class InternalMailUtil {
}
//编码用户名
if (ArrayUtil.isNotEmpty(addresses)) {
+ final String charsetStr = null == charset ? null : charset.name();
for (InternetAddress internetAddress : addresses) {
try {
- internetAddress.setPersonal(internetAddress.getPersonal(), charset.name());
+ internetAddress.setPersonal(internetAddress.getPersonal(), charsetStr);
} catch (UnsupportedEncodingException e) {
throw new MailException(e);
}
@@ -90,7 +91,7 @@ public class InternalMailUtil {
/**
* 编码中文字符
* 编码失败返回原字符串
- *
+ *
* @param text 被编码的文本
* @param charset 编码
* @return 编码后的结果
diff --git a/hutool-extra/src/main/java/cn/hutool/extra/mail/Mail.java b/hutool-extra/src/main/java/cn/hutool/extra/mail/Mail.java
index 1f28d16a9..2d71b2dcc 100644
--- a/hutool-extra/src/main/java/cn/hutool/extra/mail/Mail.java
+++ b/hutool-extra/src/main/java/cn/hutool/extra/mail/Mail.java
@@ -21,6 +21,7 @@ import javax.mail.Transport;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMultipart;
+import javax.mail.internet.MimeUtility;
import javax.mail.util.ByteArrayDataSource;
import java.io.File;
import java.io.IOException;
@@ -36,6 +37,7 @@ import java.util.Date;
* @since 3.2.0
*/
public class Mail implements Builder
+ * System.setProperty("mail.mime.charset", charset);
+ *
*
- * @param charset 字符集编码
+ * @param charset 字符集编码,{@code null} 则表示使用全局设置的默认编码,全局编码为mail.mime.charset系统属性
* @return this
*/
public MailAccount setCharset(Charset charset) {
@@ -324,7 +336,11 @@ public class MailAccount implements Serializable {
}
/**
- * 设置对于超长参数是否切分为多份,默认为false(国内邮箱附件不支持切分的附件名)
+ * 设置对于超长参数是否切分为多份,默认为false(国内邮箱附件不支持切分的附件名)
+ * 注意此项为全局设置,此项会调用
+ *
+ * System.setProperty("mail.mime.splitlongparameters", true)
+ *
*
* @param splitlongparameters 对于超长参数是否切分为多份
*/
@@ -332,6 +348,32 @@ public class MailAccount implements Serializable {
this.splitlongparameters = splitlongparameters;
}
+ /**
+ * 对于文件名是否使用{@link #charset}编码,默认为 {@code true}
+ *
+ * @return 对于文件名是否使用{@link #charset}编码,默认为 {@code true}
+ * @since 5.7.16
+ */
+ public boolean isEncodefilename() {
+
+ return encodefilename;
+ }
+
+ /**
+ * 设置对于文件名是否使用{@link #charset}编码,此选项不会修改全局配置
+ * 如果此选项设置为{@code false},则是否编码取决于两个系统属性:
+ *
+ *
+ *
+ * @param encodefilename 对于文件名是否使用{@link #charset}编码
+ * @since 5.7.16
+ */
+ public void setEncodefilename(boolean encodefilename) {
+ this.encodefilename = encodefilename;
+ }
+
/**
* 是否使用 STARTTLS安全连接,STARTTLS是对纯文本通信协议的扩展。它将纯文本连接升级为加密连接(TLS或SSL), 而不是使用一个单独的加密通信端口。
*
@@ -374,6 +416,7 @@ public class MailAccount implements Serializable {
/**
* 获取SSL协议,多个协议用空格分隔
+ *
* @return SSL协议,多个协议用空格分隔
* @since 5.5.7
*/
@@ -565,8 +608,9 @@ public class MailAccount implements Serializable {
this.host = StrUtil.format("smtp.{}", StrUtil.subSuf(fromAddress, fromAddress.indexOf('@') + 1));
}
if (StrUtil.isBlank(user)) {
- // 如果用户名为空,默认为发件人邮箱前缀
- this.user = StrUtil.subPre(fromAddress, fromAddress.indexOf('@'));
+ // 如果用户名为空,默认为发件人(issue#I4FYVY@Gitee)
+ //this.user = StrUtil.subPre(fromAddress, fromAddress.indexOf('@'));
+ this.user = fromAddress;
}
if (null == this.auth) {
// 如果密码非空白,则使用认证模式
diff --git a/hutool-extra/src/main/java/cn/hutool/extra/qrcode/QrCodeUtil.java b/hutool-extra/src/main/java/cn/hutool/extra/qrcode/QrCodeUtil.java
index f9b800282..36e048d2b 100644
--- a/hutool-extra/src/main/java/cn/hutool/extra/qrcode/QrCodeUtil.java
+++ b/hutool-extra/src/main/java/cn/hutool/extra/qrcode/QrCodeUtil.java
@@ -313,9 +313,10 @@ public class QrCodeUtil {
// 默认配置
config = new QrConfig();
}
+
BitMatrix bitMatrix;
try {
- bitMatrix = multiFormatWriter.encode(content, format, config.width, config.height, config.toHints());
+ bitMatrix = multiFormatWriter.encode(content, format, config.width, config.height, config.toHints(format));
} catch (WriterException e) {
throw new QrCodeException(e);
}
diff --git a/hutool-extra/src/main/java/cn/hutool/extra/qrcode/QrConfig.java b/hutool-extra/src/main/java/cn/hutool/extra/qrcode/QrConfig.java
index f9aae03e7..0c2789c87 100644
--- a/hutool-extra/src/main/java/cn/hutool/extra/qrcode/QrConfig.java
+++ b/hutool-extra/src/main/java/cn/hutool/extra/qrcode/QrConfig.java
@@ -3,6 +3,7 @@ package cn.hutool.extra.qrcode;
import cn.hutool.core.img.ImgUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.CharsetUtil;
+import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
@@ -331,13 +332,31 @@ public class QrConfig {
* @return 配置
*/
public HashMap
+ * 1. path为null或""上传到当前路径
+ * 2. path为相对路径则相对于当前路径的子路径
+ * 3. path为绝对路径则上传到此路径
+ *
+ *
+ * @param destPath 服务端路径,可以为{@code null} 或者相对路径或绝对路径
+ * @param fileName 文件名
+ * @param fileStream 文件流
+ * @return 是否上传成功
+ * @since 5.7.16
+ */
+ public boolean upload(String destPath, String fileName, InputStream fileStream) {
+ destPath = StrUtil.addSuffixIfNot(destPath, StrUtil.SLASH) + StrUtil.removePrefix(fileName, StrUtil.SLASH);
+ put(fileStream, destPath, null, Mode.OVERWRITE);
+ return true;
+ }
+
/**
* 将本地文件上传到目标服务器,目标文件名为destPath,若destPath为目录,则目标文件名将与srcFilePath文件名相同。覆盖模式
*
@@ -506,6 +528,25 @@ public class Sftp extends AbstractFtp {
return this;
}
+ /**
+ * 将本地数据流上传到目标服务器,目标文件名为destPath,目标必须为文件
+ *
+ * @param srcStream 本地的数据流
+ * @param destPath 目标路径,
+ * @param monitor 上传进度监控,通过实现此接口完成进度显示
+ * @param mode {@link Mode} 模式
+ * @return this
+ * @since 5.7.16
+ */
+ public Sftp put(InputStream srcStream, String destPath, SftpProgressMonitor monitor, Mode mode) {
+ try {
+ channel.put(srcStream, destPath, monitor, mode.ordinal());
+ } catch (SftpException e) {
+ throw new JschRuntimeException(e);
+ }
+ return this;
+ }
+
@Override
public void download(String src, File destFile) {
get(src, FileUtil.getAbsolutePath(destFile));
diff --git a/hutool-extra/src/test/java/cn/hutool/extra/qrcode/QrCodeUtilTest.java b/hutool-extra/src/test/java/cn/hutool/extra/qrcode/QrCodeUtilTest.java
index cfb5dd251..7f31d796f 100644
--- a/hutool-extra/src/test/java/cn/hutool/extra/qrcode/QrCodeUtilTest.java
+++ b/hutool-extra/src/test/java/cn/hutool/extra/qrcode/QrCodeUtilTest.java
@@ -4,6 +4,7 @@ import cn.hutool.core.codec.Base64;
import cn.hutool.core.img.ImgUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.lang.Console;
+import com.google.zxing.BarcodeFormat;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import org.junit.Assert;
import org.junit.Ignore;
@@ -87,4 +88,10 @@ public class QrCodeUtilTest {
final String decode = QrCodeUtil.decode(ImgUtil.read("d:/test/qr_a.png"), false, true);
Console.log(decode);
}
+
+ @Test
+ public void pdf417Test(){
+ final BufferedImage image = QrCodeUtil.generate("content111", BarcodeFormat.PDF_417, QrConfig.create());
+ Assert.assertNotNull(image);
+ }
}
diff --git a/hutool-extra/src/test/resources/config/mail.setting b/hutool-extra/src/test/resources/config/mail.setting
index a7aa08342..2fd907007 100644
--- a/hutool-extra/src/test/resources/config/mail.setting
+++ b/hutool-extra/src/test/resources/config/mail.setting
@@ -11,7 +11,7 @@ port = 465
# 发件人(必须正确,否则发送失败)
from = 小磊