Merge branch 'v5-dev' of https://gitee.com/shadowli_admin/hutool into v5-dev

This commit is contained in:
晴雨夜 2020-09-04 11:08:25 +08:00
commit 6d52779006
82 changed files with 1192 additions and 272 deletions

View File

@ -3,7 +3,23 @@
-------------------------------------------------------------------------------------------------------------
# 5.4.1 (2020-08-24)
# 5.4.2 (2020-09-03)
### 新特性
* 【core 】 lock放在try外边pr#1050@Github
* 【core 】 MailUtil增加错误信息issue#I1TAKJ@Gitee
* 【core 】 JschUtil添加远程转发功能pr#171@Gitee
* 【db 】 AbstractDb增加executeBatch重载issue#1053@Github
* 【extra 】 新增方便引入SpringUtil的注解@EnableSpringUtilpr#172@Gitee
* 【poi 】 RowUtil增加插入和删除行pr#1060@Github
### Bug修复#
* 【core 】 重新整理农历节假日解决一个pr过来的玩笑导致的问题
* 【poi 】 修复ExcelFileUtil.isXls判断问题pr#1055@Github
-------------------------------------------------------------------------------------------------------------
# 5.4.1 (2020-08-29)
### 新特性
* 【core 】 StrUtil增加firstNonXXX方法issue#1020@Github
@ -17,10 +33,16 @@
* 【core 】 增加Ipv4Utilpr#161@Gitee
* 【core 】 增加CalendarUtil和DateUtil增加isSameMonth方法pr#161@Gitee
* 【core 】 Dict增加of方法issue#1035@Github
* 【core 】 StrUtil.wrapAll方法不明确修改改为wrapAllWithPairissue#1042@Github
* 【core 】 EnumUtil.getEnumAt负数返回nullpr#167@Gitee
* 【core 】 ChineseDate增加天干地支和转换为公历方法pr#169@Gitee
* 【core 】 Img增加stroke描边方法issue#1033@Github
### Bug修复#
* 【poi 】 修复ExcelBase.isXlsx方法判断问题issue#I1S502@Gitee
* 【poi 】 修复Excel03SaxReader日期方法判断问题pr#1026@Github
* 【core 】 修复StrUtil.indexOf空指针问题issue#1038@Github
* 【extra 】 修复VelocityEngine编码问题和路径前缀问题issue#I1T0IG@Gitee
-------------------------------------------------------------------------------------------------------------
@ -130,7 +152,7 @@
* 【core 】 增加StrUtil.removeAny方法(issue#923@Github)
* 【db 】 增加部分Connection参数支持(issue#924@Github)
* 【core 】 FileUtil增加别名方法(pr#926@Github)
* 【poi 】 EcelReader中增加read重载提供每个单元格单独处理的方法(issue#I1JZTL@Gitee)
* 【poi 】 ExcelReader中增加read重载提供每个单元格单独处理的方法(issue#I1JZTL@Gitee)
### Bug修复
* 【json 】 修复append方法导致的JSONConfig传递失效问题issue#906@Github
@ -163,7 +185,7 @@
* 【extra 】 新增 QRCode base64 编码形式返回pr#878@Github
* 【core 】 ImgUtil增加toBase64DateUriURLUtil增加getDataUri方法
* 【core 】 IterUtil添加List转Map的工具方法pr#123@Gitee
* 【core 】 BeanValuePovider转换失败时返回原数据而非null
* 【core 】 BeanValueProvider转换失败时返回原数据而非null
* 【core 】 支持BeanUtil.toBean(object, Map.class)转换issue#I1I4HC@Gitee
* 【core 】 MapUtil和CollUtil增加clear方法issue#I1I4HC@Gitee
* 【core 】 增加FontUtil可定义pressText是否从中间issue#I1HSWU@Gitee

View File

@ -121,19 +121,19 @@ Each module can be introduced individually, or all modules can be introduced by
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.4.1</version>
<version>5.4.2</version>
</dependency>
```
### Gradle
```
compile 'cn.hutool:hutool-all:5.4.1'
compile 'cn.hutool:hutool-all:5.4.2'
```
## Download
- [Maven1](https://repo1.maven.org/maven2/cn/hutool/hutool-all/5.4.1/)
- [Maven2](http://repo2.maven.org/maven2/cn/hutool/hutool-all/5.4.1/)
- [Maven1](https://repo1.maven.org/maven2/cn/hutool/hutool-all/5.4.2/)
- [Maven2](http://repo2.maven.org/maven2/cn/hutool/hutool-all/5.4.2/)
> 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.

View File

@ -120,21 +120,21 @@ Hutool的存在就是为了减少代码搜索成本避免网络上参差不
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.4.1</version>
<version>5.4.2</version>
</dependency>
```
### Gradle
```
compile 'cn.hutool:hutool-all:5.4.1'
compile 'cn.hutool:hutool-all:5.4.2'
```
### 非Maven项目
点击以下任一链接,下载`hutool-all-X.X.X.jar`即可:
- [Maven中央库1](https://repo1.maven.org/maven2/cn/hutool/hutool-all/5.4.1/)
- [Maven中央库2](http://repo2.maven.org/maven2/cn/hutool/hutool-all/5.4.1/)
- [Maven中央库1](https://repo1.maven.org/maven2/cn/hutool/hutool-all/5.4.2/)
- [Maven中央库2](http://repo2.maven.org/maven2/cn/hutool/hutool-all/5.4.2/)
> 注意
> Hutool 5.x支持JDK8+对Android平台没有测试不能保证所有工具类或工具方法可用。

View File

@ -1 +1 @@
5.4.1
5.4.2

View File

@ -1 +1 @@
var version = '5.4.1'
var version = '5.4.2'

View File

@ -9,7 +9,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.4.1-SNAPSHOT</version>
<version>5.4.2-SNAPSHOT</version>
</parent>
<artifactId>hutool-all</artifactId>

View File

@ -9,7 +9,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.4.1-SNAPSHOT</version>
<version>5.4.2-SNAPSHOT</version>
</parent>
<artifactId>hutool-aop</artifactId>

View File

@ -7,7 +7,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.4.1-SNAPSHOT</version>
<version>5.4.2-SNAPSHOT</version>
</parent>
<artifactId>hutool-bloomFilter</artifactId>

View File

@ -7,7 +7,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.4.1-SNAPSHOT</version>
<version>5.4.2-SNAPSHOT</version>
</parent>
<artifactId>hutool-bom</artifactId>

View File

@ -7,7 +7,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.4.1-SNAPSHOT</version>
<version>5.4.2-SNAPSHOT</version>
</parent>
<artifactId>hutool-cache</artifactId>

View File

@ -7,7 +7,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.4.1-SNAPSHOT</version>
<version>5.4.2-SNAPSHOT</version>
</parent>
<artifactId>hutool-captcha</artifactId>

View File

@ -9,7 +9,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.4.1-SNAPSHOT</version>
<version>5.4.2-SNAPSHOT</version>
</parent>
<artifactId>hutool-core</artifactId>

View File

@ -333,7 +333,7 @@ public class BeanDesc implements Serializable {
// ------------------------------------------------------------------------------------------------------ Private method end
/**
* 属性描述
* 属性描述包括了字段gettersetter和相应的方法执行
*
* @author looly
*/

View File

@ -105,7 +105,7 @@ public class ConcurrentHashSet<E> extends AbstractSet<E> implements java.io.Seri
@Override
public boolean remove(Object o) {
return map.remove(o) == PRESENT;
return PRESENT.equals(map.remove(o));
}
@Override

View File

@ -16,9 +16,6 @@ public class AtomicBooleanConverter extends AbstractConverter<AtomicBoolean> {
@Override
protected AtomicBoolean convertInternal(Object value) {
if (boolean.class == value.getClass()) {
return new AtomicBoolean((boolean) value);
}
if (value instanceof Boolean) {
return new AtomicBoolean((Boolean) value);
}

View File

@ -1,11 +1,14 @@
package cn.hutool.core.date;
import cn.hutool.core.convert.NumberChineseFormatter;
import cn.hutool.core.date.chinese.ChineseMonth;
import cn.hutool.core.date.chinese.GanZhi;
import cn.hutool.core.date.chinese.LunarFestival;
import cn.hutool.core.date.chinese.LunarInfo;
import cn.hutool.core.date.chinese.SolarTerms;
import cn.hutool.core.util.StrUtil;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
@ -15,62 +18,26 @@ import java.util.List;
* @since 5.1.1
*/
public class ChineseDate {
// private static final Date baseDate = DateUtil.parseDate("1900-01-31");
/**
* 1900-01-31
*/
private static final long baseDate = -2206425943000L;
//农历年
private final int year;
//农历月
private final int month;
//农历日
private final int day;
//公历年
private final int gyear;
//公历月
private final int gmonth;
//公里日
private final int gday;
//是否闰年
private boolean leap;
private final String[] chineseNumber = {"", "", "", "", "", "", "", "", "", "", "十一", "十二"};
private final String[] chineseNumberName = {"", "", "", "", "", "", "", "", "", "", "十一", ""};
/**
* 此表来自https://github.com/jjonline/calendar.js/blob/master/calendar.js
*农历表示
* 1. 表示当年有无闰年有的话为闰月的月份没有的话为0
* 2-4.为除了闰月外的正常月份是大月还是小月1为30天0为29天
* 5. 表示闰月是大月还是小月仅当存在闰月的情况下有意义
*/
private final long[] lunarInfo = new long[]{
0x04bd8,0x04ae0,0x0a570,0x054d5,0x0d260,0x0d950,0x16554,0x056a0,0x09ad0,0x055d2,//1900-1909
0x04ae0,0x0a5b6,0x0a4d0,0x0d250,0x1d255,0x0b540,0x0d6a0,0x0ada2,0x095b0,0x14977,//1910-1919
0x04970,0x0a4b0,0x0b4b5,0x06a50,0x06d40,0x1ab54,0x02b60,0x09570,0x052f2,0x04970,//1920-1929
0x06566,0x0d4a0,0x0ea50,0x16a95,0x05ad0,0x02b60,0x186e3,0x092e0,0x1c8d7,0x0c950,//1930-1939
0x0d4a0,0x1d8a6,0x0b550,0x056a0,0x1a5b4,0x025d0,0x092d0,0x0d2b2,0x0a950,0x0b557,//1940-1949
0x06ca0,0x0b550,0x15355,0x04da0,0x0a5b0,0x14573,0x052b0,0x0a9a8,0x0e950,0x06aa0,//1950-1959
0x0aea6,0x0ab50,0x04b60,0x0aae4,0x0a570,0x05260,0x0f263,0x0d950,0x05b57,0x056a0,//1960-1969
0x096d0,0x04dd5,0x04ad0,0x0a4d0,0x0d4d4,0x0d250,0x0d558,0x0b540,0x0b6a0,0x195a6,//1970-1979
0x095b0,0x049b0,0x0a974,0x0a4b0,0x0b27a,0x06a50,0x06d40,0x0af46,0x0ab60,0x09570,//1980-1989
0x04af5,0x04970,0x064b0,0x074a3,0x0ea50,0x06b58,0x05ac0,0x0ab60,0x096d5,0x092e0,//1990-1999
0x0c960,0x0d954,0x0d4a0,0x0da50,0x07552,0x056a0,0x0abb7,0x025d0,0x092d0,0x0cab5,//2000-2009
0x0a950,0x0b4a0,0x0baa4,0x0ad50,0x055d9,0x04ba0,0x0a5b0,0x15176,0x052b0,0x0a930,//2010-2019
0x07954,0x06aa0,0x0ad50,0x05b52,0x04b60,0x0a6e6,0x0a4e0,0x0d260,0x0ea65,0x0d530,//2020-2029
0x05aa0,0x076a3,0x096d0,0x04afb,0x04ad0,0x0a4d0,0x1d0b6,0x0d250,0x0d520,0x0dd45,//2030-2039
0x0b5a0,0x056d0,0x055b2,0x049b0,0x0a577,0x0a4b0,0x0aa50,0x1b255,0x06d20,0x0ada0,//2040-2049
0x14b63,0x09370,0x049f8,0x04970,0x064b0,0x168a6,0x0ea50, 0x06b20,0x1a6c4,0x0aae0,//2050-2059
0x092e0,0x0d2e3,0x0c960,0x0d557,0x0d4a0,0x0da50,0x05d55,0x056a0,0x0a6d0,0x055d4,//2060-2069
0x052d0,0x0a9b8,0x0a950,0x0b4a0,0x0b6a6,0x0ad50,0x055a0,0x0aba4,0x0a5b0,0x052b0,//2070-2079
0x0b273,0x06930,0x07337,0x06aa0,0x0ad50,0x14b55,0x04b60,0x0a570,0x054e4,0x0d160,//2080-2089
0x0e968,0x0d520,0x0daa0,0x16aa6,0x056d0,0x04ae0,0x0a9d4,0x0a2d0,0x0d150,0x0f252,//2090-2099
};
//农历节日 *表示放假日
private final String[] lFtv = new String[]{
"0101 春节", "0102 大年初二", "0103 大年初三", "0104 大年初四",
"0105 大年初五", "0106 大年初六", "0107 大年初七", "0105 路神生日",
"0115 元宵节", "0202 龙抬头", "0219 观世音圣诞", "0404 寒食节",
"0408 佛诞节 ", "0505 端午节", "0606 天贶节 姑姑节", "0624 彝族火把节",
"0707 七夕情人节", "0714 鬼节(南方)", "0715 盂兰节", "0730 地藏节",
"0815 中秋节", "0909 重阳节", "1001 祭祖节", "1117 阿弥陀佛圣诞",
"1208 腊八节 释迦如来成道日", "1223 过小年", "1229 腊月二十九", "1230 除夕"};
/**
* 构造方法传入日期
@ -80,23 +47,22 @@ public class ChineseDate {
public ChineseDate(Date date) {
// 求出和1900年1月31日相差的天数
int offset = (int) ((date.getTime() - baseDate) / DateUnit.DAY.getMillis());
// 计算农历年份
// 用offset减去每农历年的天数计算当天是农历第几天offset是当年的第几天
int daysOfYear;
int iYear = 1900;
final int maxYear = iYear + lunarInfo.length - 1;
int iYear = LunarInfo.BASE_YEAR;
final int maxYear = LunarInfo.getMaxYear();
for (; iYear <= maxYear; iYear++) {
daysOfYear = yearDays(iYear);
daysOfYear = LunarInfo.yearDays(iYear);
if (offset < daysOfYear) {
break;
}
offset -= daysOfYear;
}
year = iYear;
year = iYear;
// 计算农历月份
int leapMonth = leapMonth(iYear); // 闰哪个月,1-12
int leapMonth = LunarInfo.leapMonth(iYear); // 闰哪个月,1-12
// 用当年的天数offset,逐个减去每月农历的天数求出当天是本月的第几天
int iMonth;
int daysOfMonth = 0;
@ -105,14 +71,14 @@ public class ChineseDate {
if (leapMonth > 0 && iMonth == (leapMonth + 1) && false == leap) {
--iMonth;
leap = true;
daysOfMonth = leapDays(year);
daysOfMonth = LunarInfo.leapDays(year);
} else {
daysOfMonth = monthDays(year, iMonth);
daysOfMonth = LunarInfo.monthDays(year, iMonth);
}
offset -= daysOfMonth;
// 解除闰月
if (leap && iMonth == (leapMonth + 1)){
if (leap && iMonth == (leapMonth + 1)) {
leap = false;
}
}
@ -126,14 +92,20 @@ public class ChineseDate {
--iMonth;
}
}
// offset小于0时也要校正
if (offset < 0) {
offset += daysOfMonth;
--iMonth;
}
month = iMonth;
day = offset + 1;
// 公历
final DateTime dt = DateUtil.date(date);
gyear = dt.year();
gmonth = dt.month() + 1;
gday = dt.dayOfMonth();
}
/**
@ -149,6 +121,25 @@ public class ChineseDate {
this.month = chineseMonth;
this.year = chineseYear;
this.leap = DateUtil.isLeapYear(chineseYear);
//先判断传入的月份是不是闰月
int leapMonth = LunarInfo.leapMonth(chineseYear);
final DateTime dateTime = lunar2solar(chineseYear, chineseMonth, chineseDay, chineseMonth == leapMonth);
if (null != dateTime) {
//初始化公历年
this.gday = dateTime.dayOfMonth();
//初始化公历月
this.gmonth = dateTime.month() + 1;
//初始化公历日
this.gyear = dateTime.year();
} else {
//初始化公历年
this.gday = -1;
//初始化公历月
this.gmonth = -1;
//初始化公历日
this.gyear = -1;
}
}
/**
@ -170,13 +161,14 @@ public class ChineseDate {
return this.month;
}
/**
* 获得农历月份中文例如二月十二月或者润一月
*
* @return 返回农历月份
*/
public String getChineseMonth() {
return (leap ? "" : "") + NumberChineseFormatter.format(this.month, false) + "";
return ChineseMonth.getChineseMonthName(this.leap, this.month, false);
}
/**
@ -185,7 +177,7 @@ public class ChineseDate {
* @return 返回农历月份称呼
*/
public String getChineseMonthName() {
return (leap ? "" : "") + chineseNumberName[month - 1] + "";
return ChineseMonth.getChineseMonthName(this.leap, this.month, true);
}
/**
@ -228,25 +220,7 @@ public class ChineseDate {
* @return 获得农历节日
*/
public String getFestivals() {
StringBuilder currentChineseDate = new StringBuilder();
if (month < 10) {
currentChineseDate.append('0');
}
currentChineseDate.append(this.month);
if (day < 10) {
currentChineseDate.append('0');
}
currentChineseDate.append(this.day);
List<String> getFestivalsList = new ArrayList<>();
for (String fv : lFtv) {
final List<String> split = StrUtil.split(fv, ' ');
if (split.get(0).contentEquals(currentChineseDate)) {
getFestivalsList.add(split.get(1));
}
}
return StrUtil.join(",", getFestivalsList);
return StrUtil.join(",", LunarFestival.getFestivals(this.month, this.day));
}
/**
@ -265,8 +239,19 @@ public class ChineseDate {
* @return 获得天干地支
*/
public String getCyclical() {
int num = year - 1900 + 36;
return (cyclicalm(num));
return GanZhi.cyclicalm(year - LunarInfo.BASE_YEAR + 36);
}
/**
* 干支纪年信息
*
* @return 获得天干地支的年月日信息
*/
public String getCyclicalYMD() {
if (gyear >= LunarInfo.BASE_YEAR && gmonth > 0 && gday > 0){
return (cyclicalm(gyear, gmonth, gday));
}
return null;
}
/**
@ -287,64 +272,77 @@ public class ChineseDate {
// ------------------------------------------------------- private method start
/**
* 传入 月日的offset 传回干支, 0=甲子
*/
private static String cyclicalm(int num) {
final String[] Gan = new String[]{"", "", "", "", "", "", "", "", "", ""};
final String[] Zhi = new String[]{"", "", "", "", "", "", "", "", "", "", "", ""};
return (Gan[num % 10] + Zhi[num % 12]);
}
/**
* 传回农历 y年的总天数
* 这里同步处理年月日的天干地支信息
*
* @param y
* @return 总天数
* @param Y
* @param M
* @param D
* @return 天干地支信息
*/
private int yearDays(int y) {
int i, sum = 348;
for (i = 0x8000; i > 0x8; i >>= 1) {
if ((lunarInfo[y - 1900] & i) != 0)
sum += 1;
private String cyclicalm(int Y, int M, int D) {
String gzyear = GanZhi.cyclicalm(year - LunarInfo.BASE_YEAR + 36);
//天干地支处理
//返回当月为几日开始
int firstNode = SolarTerms.getTerm(Y, (M * 2 - 1));
// 依据12节气修正干支月
String gzM = GanZhi.cyclicalm((Y - LunarInfo.BASE_YEAR) * 12 + M + 11);
if (D >= firstNode) {
gzM = GanZhi.cyclicalm((Y - LunarInfo.BASE_YEAR) * 12 + M + 12);
}
return (sum + leapDays(y));
int dayCyclical = (int) ((DateUtil.parseDate(Y + "-" + M + "-" + "1").getTime() - baseDate + 2592000000L) / DateUnit.DAY.getMillis()) + 10;
String gzD = GanZhi.cyclicalm(dayCyclical + D - 1);
return gzyear + "" + gzM + "" + gzD + "";
}
/**
* 传回农历 y年闰月的天
* 通过农历年月日信息 返回公历信息 提供给构造函
*
* @param y
* @return 闰月的天数
* @param chineseYear 农历年
* @param chineseMonth 农历月
* @param chineseDay 农历日
* @param isLeapMonth 传入的月是不是闰月
* @return 公历信息
*/
private int leapDays(int y) {
if (leapMonth(y) != 0) {
return (lunarInfo[y - 1900] & 0x10000) != 0 ? 30 : 29;
private DateTime lunar2solar(int chineseYear, int chineseMonth, int chineseDay, boolean isLeapMonth) {
//超出了最大极限值
if ((chineseYear == 2100 && chineseMonth == 12 && chineseDay > 1) ||
(chineseYear == LunarInfo.BASE_YEAR && chineseMonth == 1 && chineseDay < 31)) {
return null;
}
return 0;
}
/**
* 传回农历 y年m月的总天数
*
* @param y
* @param m
* @return 总天数
*/
private int monthDays(int y, int m) {
return (lunarInfo[y - 1900] & (0x10000 >> m)) == 0 ? 29 : 30;
}
/**
* 传回农历 y年闰哪个月 1-12 , 没闰传回 0
*
* @param y
* @return 润的月
*/
private int leapMonth(int y) {
return (int) (lunarInfo[y - 1900] & 0xf);
int day = LunarInfo.monthDays(chineseYear, chineseMonth);
int _day = day;
if (isLeapMonth) {
_day = LunarInfo.leapDays(chineseYear);
}
//参数合法性效验
if (chineseYear < LunarInfo.BASE_YEAR || chineseYear > 2100 || chineseDay > _day) {
return null;
}
//计算农历的时间差
int offset = 0;
for (int i = LunarInfo.BASE_YEAR; i < chineseYear; i++) {
offset += LunarInfo.yearDays(i);
}
int leap;
boolean isAdd = false;
for (int i = 1; i < chineseMonth; i++) {
leap = LunarInfo.leapMonth(chineseYear);
if (false == isAdd) {//处理闰月
if (leap <= i && leap > 0) {
offset += LunarInfo.leapDays(chineseYear);
isAdd = true;
}
}
offset += LunarInfo.monthDays(chineseYear, i);
}
//转换闰月农历 需补充该年闰月的前一个月的时差
if (isLeapMonth) {
offset += day;
}
//1900年农历正月一日的公历时间为1900年1月30日0时0分0秒(该时间也是本农历的最开始起始点) -2203804800000
return DateUtil.date(((offset + chineseDay - 31) * 86400000L) - 2203804800000L);
}
// ------------------------------------------------------- private method end
}
}

View File

@ -399,6 +399,15 @@ public class DateTime extends Date {
return getField(DateField.MONTH);
}
/**
* 获取月从1开始计数
* @return 月份1表示一月
* @since 5.4.1
*/
public int monthBaseOne(){
return month() + 1;
}
/**
* 获得月份从1开始计数<br>
* 由于{@link Calendar} 中的月份按照0开始计数导致某些需求容易误解因此如果想用1表示一月2表示二月则调用此方法
@ -446,18 +455,18 @@ public class DateTime extends Date {
}
/**
* 获得指定日期是这个日期所在月份的第几天<br>
* 获得指定日期是这个日期所在月份的第几天从1开始
*
* @return
* @return 1表示第一天
*/
public int dayOfMonth() {
return getField(DateField.DAY_OF_MONTH);
}
/**
* 获得指定日期是这个日期所在年份的第几天<br>
* 获得指定日期是这个日期所在年份的第几天从1开始
*
* @return
* @return 1表示第一天
* @since 5.3.6
*/
public int dayOfYear() {

View File

@ -0,0 +1,27 @@
package cn.hutool.core.date.chinese;
/**
* 农历月份表示
*
* @author looly
* @since 5.4.1
*/
public class ChineseMonth {
private static final String[] MONTH_NAME = {"", "", "", "", "", "", "", "", "", "", "十一", "十二"};
private static final String[] MONTH_NAME_TRADITIONAL = {"", "", "", "", "", "", "", "", "", "", "十一", ""};
/**
* 获得农历月称呼<br>
* 当为传统表示时表示为二月腊月或者润正月等
* 当为非传统表示时二月十二月或者润一月等
*
* @param isLeep 是否闰月
* @param month 月份从1开始
* @param isTraditional 是否传统表示例如一月传统表示为正月
* @return 返回农历月份称呼
*/
public static String getChineseMonthName(boolean isLeep, int month, boolean isTraditional) {
return (isLeep ? "" : "") + (isTraditional ? MONTH_NAME_TRADITIONAL : MONTH_NAME)[month - 1] + "";
}
}

View File

@ -0,0 +1,23 @@
package cn.hutool.core.date.chinese;
/**
* 天干地支类
*
* @author looly
* @since 5.4.1
*/
public class GanZhi {
private static final String[] GAN = new String[]{"", "", "", "", "", "", "", "", "", ""};
private static final String[] ZHI = new String[]{"", "", "", "", "", "", "", "", "", "", "", ""};
/**
* 传入 月日的offset 传回干支, 0=甲子
*
* @param num 月日的offset
* @return 干支
*/
public static String cyclicalm(int num) {
return (GAN[num % 10] + ZHI[num % 12]);
}
}

View File

@ -0,0 +1,95 @@
package cn.hutool.core.date.chinese;
import cn.hutool.core.lang.Pair;
import cn.hutool.core.map.TableMap;
import java.util.List;
/**
* 节假日农历封装
*
* @author looly
* @since 5.4.1
*/
public class LunarFestival {
//农历节日 *表示放假日
// 来自https://baike.baidu.com/item/%E4%B8%AD%E5%9B%BD%E4%BC%A0%E7%BB%9F%E8%8A%82%E6%97%A5/396100
private static final TableMap<Pair<Integer, Integer>, String> lFtv = new TableMap<>(16);
static{
lFtv.put(new Pair<>(1, 1), "春节");
lFtv.put(new Pair<>(1, 2), "犬日");
lFtv.put(new Pair<>(1, 3), "猪日");
lFtv.put(new Pair<>(1, 4), "羊日");
lFtv.put(new Pair<>(1, 5), "牛日 破五日");
lFtv.put(new Pair<>(1, 6), "马日,送穷日");
lFtv.put(new Pair<>(1, 7), "人日 人胜节");
lFtv.put(new Pair<>(1, 8), "谷日 八仙日");
lFtv.put(new Pair<>(1, 9), "天日 九皇会");
lFtv.put(new Pair<>(1, 10), "地日 石头生日");
lFtv.put(new Pair<>(1, 12), "火日 老鼠娶媳妇日");
lFtv.put(new Pair<>(1, 13), "上(试)灯日 关公升天日");
lFtv.put(new Pair<>(1, 15), "元宵节");
lFtv.put(new Pair<>(1, 18), "落灯日");
// 二月
lFtv.put(new Pair<>(2, 1), "中和节 太阳生日");
lFtv.put(new Pair<>(2, 2), "龙抬头");
lFtv.put(new Pair<>(2, 12), "花朝节");
lFtv.put(new Pair<>(2, 19), "观世音圣诞");
// 三月
lFtv.put(new Pair<>(3, 3), "上巳节");
// 四月
lFtv.put(new Pair<>(4, 1), "祭雹神");
lFtv.put(new Pair<>(4, 4), "文殊菩萨诞辰");
lFtv.put(new Pair<>(4, 8), "佛诞节");
// 五月
lFtv.put(new Pair<>(5, 5), "端午节");
// 六月
lFtv.put(new Pair<>(6, 6), "晒衣节 姑姑节");
lFtv.put(new Pair<>(6, 6), "天贶节");
lFtv.put(new Pair<>(6, 24), "彝族火把节");
// 七月
lFtv.put(new Pair<>(7, 7), "七夕");
lFtv.put(new Pair<>(7, 14), "鬼节(南方)");
lFtv.put(new Pair<>(7, 15), "中元节");
lFtv.put(new Pair<>(7, 15), "盂兰盆节");
lFtv.put(new Pair<>(7, 30), "地藏节");
// 八月
lFtv.put(new Pair<>(8, 15), "中秋节");
// 九月
lFtv.put(new Pair<>(9, 9), "重阳节");
// 十月
lFtv.put(new Pair<>(10, 1), "祭祖节");
lFtv.put(new Pair<>(10, 15), "下元节");
// 十一月
lFtv.put(new Pair<>(11, 17), "阿弥陀佛圣诞");
// 腊月
lFtv.put(new Pair<>(12, 8), "腊八节");
lFtv.put(new Pair<>(12, 16), "尾牙");
lFtv.put(new Pair<>(12, 23), "小年");
lFtv.put(new Pair<>(12, 29), "除夕");
lFtv.put(new Pair<>(12, 30), "除夕");
}
/**
* 获得节日列表
*
* @param month
* @param day
* @return 获得农历节日
*/
public static List<String> getFestivals(int month, int day) {
return lFtv.getValues(new Pair<>(month, day));
}
}

View File

@ -0,0 +1,111 @@
package cn.hutool.core.date.chinese;
/**
* 阴历农历信息
*
* @author looly
* @since 5.4.1
*/
public class LunarInfo {
public static final int BASE_YEAR = 1900;
/**
* 此表来自https://github.com/jjonline/calendar.js/blob/master/calendar.js
* 农历表示
* 1. 表示当年有无闰年有的话为闰月的月份没有的话为0
* 2-4.为除了闰月外的正常月份是大月还是小月1为30天0为29天
* 5. 表示闰月是大月还是小月仅当存在闰月的情况下有意义
*/
private static final long[] LUNAR_CODE = new long[]{
0x04bd8, 0x04ae0, 0x0a570, 0x054d5, 0x0d260, 0x0d950, 0x16554, 0x056a0, 0x09ad0, 0x055d2,//1900-1909
0x04ae0, 0x0a5b6, 0x0a4d0, 0x0d250, 0x1d255, 0x0b540, 0x0d6a0, 0x0ada2, 0x095b0, 0x14977,//1910-1919
0x04970, 0x0a4b0, 0x0b4b5, 0x06a50, 0x06d40, 0x1ab54, 0x02b60, 0x09570, 0x052f2, 0x04970,//1920-1929
0x06566, 0x0d4a0, 0x0ea50, 0x16a95, 0x05ad0, 0x02b60, 0x186e3, 0x092e0, 0x1c8d7, 0x0c950,//1930-1939
0x0d4a0, 0x1d8a6, 0x0b550, 0x056a0, 0x1a5b4, 0x025d0, 0x092d0, 0x0d2b2, 0x0a950, 0x0b557,//1940-1949
0x06ca0, 0x0b550, 0x15355, 0x04da0, 0x0a5b0, 0x14573, 0x052b0, 0x0a9a8, 0x0e950, 0x06aa0,//1950-1959
0x0aea6, 0x0ab50, 0x04b60, 0x0aae4, 0x0a570, 0x05260, 0x0f263, 0x0d950, 0x05b57, 0x056a0,//1960-1969
0x096d0, 0x04dd5, 0x04ad0, 0x0a4d0, 0x0d4d4, 0x0d250, 0x0d558, 0x0b540, 0x0b6a0, 0x195a6,//1970-1979
0x095b0, 0x049b0, 0x0a974, 0x0a4b0, 0x0b27a, 0x06a50, 0x06d40, 0x0af46, 0x0ab60, 0x09570,//1980-1989
0x04af5, 0x04970, 0x064b0, 0x074a3, 0x0ea50, 0x06b58, 0x05ac0, 0x0ab60, 0x096d5, 0x092e0,//1990-1999
0x0c960, 0x0d954, 0x0d4a0, 0x0da50, 0x07552, 0x056a0, 0x0abb7, 0x025d0, 0x092d0, 0x0cab5,//2000-2009
0x0a950, 0x0b4a0, 0x0baa4, 0x0ad50, 0x055d9, 0x04ba0, 0x0a5b0, 0x15176, 0x052b0, 0x0a930,//2010-2019
0x07954, 0x06aa0, 0x0ad50, 0x05b52, 0x04b60, 0x0a6e6, 0x0a4e0, 0x0d260, 0x0ea65, 0x0d530,//2020-2029
0x05aa0, 0x076a3, 0x096d0, 0x04afb, 0x04ad0, 0x0a4d0, 0x1d0b6, 0x0d250, 0x0d520, 0x0dd45,//2030-2039
0x0b5a0, 0x056d0, 0x055b2, 0x049b0, 0x0a577, 0x0a4b0, 0x0aa50, 0x1b255, 0x06d20, 0x0ada0,//2040-2049
0x14b63, 0x09370, 0x049f8, 0x04970, 0x064b0, 0x168a6, 0x0ea50, 0x06b20, 0x1a6c4, 0x0aae0,//2050-2059
0x092e0, 0x0d2e3, 0x0c960, 0x0d557, 0x0d4a0, 0x0da50, 0x05d55, 0x056a0, 0x0a6d0, 0x055d4,//2060-2069
0x052d0, 0x0a9b8, 0x0a950, 0x0b4a0, 0x0b6a6, 0x0ad50, 0x055a0, 0x0aba4, 0x0a5b0, 0x052b0,//2070-2079
0x0b273, 0x06930, 0x07337, 0x06aa0, 0x0ad50, 0x14b55, 0x04b60, 0x0a570, 0x054e4, 0x0d160,//2080-2089
0x0e968, 0x0d520, 0x0daa0, 0x16aa6, 0x056d0, 0x04ae0, 0x0a9d4, 0x0a2d0, 0x0d150, 0x0f252,//2090-2099
};
/**
* 获取支持的最大年包括
*
* @return 最大年包括
*/
public static int getMaxYear() {
return BASE_YEAR + LUNAR_CODE.length - 1;
}
/**
* 传回农历 y年的总天数
*
* @param y
* @return 总天数
*/
public static int yearDays(int y) {
int i, sum = 348;
for (i = 0x8000; i > 0x8; i >>= 1) {
if ((getCode(y) & i) != 0)
sum += 1;
}
return (sum + leapDays(y));
}
/**
* 传回农历 y年闰月的天数
*
* @param y
* @return 闰月的天数
*/
public static int leapDays(int y) {
if (leapMonth(y) != 0) {
return (getCode(y) & 0x10000) != 0 ? 30 : 29;
}
return 0;
}
/**
* 传回农历 y年m月的总天数
*
* @param y
* @param m
* @return 总天数
*/
public static int monthDays(int y, int m) {
return (getCode(y) & (0x10000 >> m)) == 0 ? 29 : 30;
}
/**
* 传回农历 y年闰哪个月 1-12 , 没闰传回 0
*
* @param y
* @return 润的月, 没闰传回 0
*/
public static int leapMonth(int y) {
return (int) (getCode(y) & 0xf);
}
/**
* 获取对应年的农历信息
*
* @param year
* @return 农历信息
*/
private static long getCode(int year) {
return LUNAR_CODE[year - BASE_YEAR];
}
}

View File

@ -0,0 +1,115 @@
package cn.hutool.core.date.chinese;
import cn.hutool.core.util.NumberUtil;
/**
* 24节气相关信息
*
* @author looly
* @since 5.4.1
*/
public class SolarTerms {
/**
* 根据节气修正干支月
*
* @param y
* @param n 节气
* @return 干支月
*/
public static int getTerm(int y, int n) {
if (y < 1900 || y > 2100) {
return -1;
}
if (n < 1 || n > 24) {
return -1;
}
String _table = sTermInfo[y - 1900];
Integer[] _info = new Integer[6];
for (int i = 0; i < 6; i++) {
_info[i] = NumberUtil.parseInt("0x" + _table.substring(i * 5, 5 * (i + 1)));
}
String[] _calday = new String[24];
for (int i = 0; i < 6; i++) {
_calday[4 * i] = _info[i].toString().substring(0, 1);
_calday[4 * i + 1] = _info[i].toString().substring(1, 3);
_calday[4 * i + 2] = _info[i].toString().substring(3, 4);
_calday[4 * i + 3] = _info[i].toString().substring(4, 6);
}
return NumberUtil.parseInt(_calday[n - 1]);
}
/**
* 1900-2100各年的24节气日期速查表
* 此表来自https://github.com/jjonline/calendar.js/blob/master/calendar.js
*/
private static final String[] sTermInfo = new String[]{
"9778397bd097c36b0b6fc9274c91aa", "97b6b97bd19801ec9210c965cc920e", "97bcf97c3598082c95f8c965cc920f",
"97bd0b06bdb0722c965ce1cfcc920f", "b027097bd097c36b0b6fc9274c91aa", "97b6b97bd19801ec9210c965cc920e",
"97bcf97c359801ec95f8c965cc920f", "97bd0b06bdb0722c965ce1cfcc920f", "b027097bd097c36b0b6fc9274c91aa",
"97b6b97bd19801ec9210c965cc920e", "97bcf97c359801ec95f8c965cc920f", "97bd0b06bdb0722c965ce1cfcc920f",
"b027097bd097c36b0b6fc9274c91aa", "9778397bd19801ec9210c965cc920e", "97b6b97bd19801ec95f8c965cc920f",
"97bd09801d98082c95f8e1cfcc920f", "97bd097bd097c36b0b6fc9210c8dc2", "9778397bd197c36c9210c9274c91aa",
"97b6b97bd19801ec95f8c965cc920e", "97bd09801d98082c95f8e1cfcc920f", "97bd097bd097c36b0b6fc9210c8dc2",
"9778397bd097c36c9210c9274c91aa", "97b6b97bd19801ec95f8c965cc920e", "97bcf97c3598082c95f8e1cfcc920f",
"97bd097bd097c36b0b6fc9210c8dc2", "9778397bd097c36c9210c9274c91aa", "97b6b97bd19801ec9210c965cc920e",
"97bcf97c3598082c95f8c965cc920f", "97bd097bd097c35b0b6fc920fb0722", "9778397bd097c36b0b6fc9274c91aa",
"97b6b97bd19801ec9210c965cc920e", "97bcf97c3598082c95f8c965cc920f", "97bd097bd097c35b0b6fc920fb0722",
"9778397bd097c36b0b6fc9274c91aa", "97b6b97bd19801ec9210c965cc920e", "97bcf97c359801ec95f8c965cc920f",
"97bd097bd097c35b0b6fc920fb0722", "9778397bd097c36b0b6fc9274c91aa", "97b6b97bd19801ec9210c965cc920e",
"97bcf97c359801ec95f8c965cc920f", "97bd097bd097c35b0b6fc920fb0722", "9778397bd097c36b0b6fc9274c91aa",
"97b6b97bd19801ec9210c965cc920e", "97bcf97c359801ec95f8c965cc920f", "97bd097bd07f595b0b6fc920fb0722",
"9778397bd097c36b0b6fc9210c8dc2", "9778397bd19801ec9210c9274c920e", "97b6b97bd19801ec95f8c965cc920f",
"97bd07f5307f595b0b0bc920fb0722", "7f0e397bd097c36b0b6fc9210c8dc2", "9778397bd097c36c9210c9274c920e",
"97b6b97bd19801ec95f8c965cc920f", "97bd07f5307f595b0b0bc920fb0722", "7f0e397bd097c36b0b6fc9210c8dc2",
"9778397bd097c36c9210c9274c91aa", "97b6b97bd19801ec9210c965cc920e", "97bd07f1487f595b0b0bc920fb0722",
"7f0e397bd097c36b0b6fc9210c8dc2", "9778397bd097c36b0b6fc9274c91aa", "97b6b97bd19801ec9210c965cc920e",
"97bcf7f1487f595b0b0bb0b6fb0722", "7f0e397bd097c35b0b6fc920fb0722", "9778397bd097c36b0b6fc9274c91aa",
"97b6b97bd19801ec9210c965cc920e", "97bcf7f1487f595b0b0bb0b6fb0722", "7f0e397bd097c35b0b6fc920fb0722",
"9778397bd097c36b0b6fc9274c91aa", "97b6b97bd19801ec9210c965cc920e", "97bcf7f1487f531b0b0bb0b6fb0722",
"7f0e397bd097c35b0b6fc920fb0722", "9778397bd097c36b0b6fc9274c91aa", "97b6b97bd19801ec9210c965cc920e",
"97bcf7f1487f531b0b0bb0b6fb0722", "7f0e397bd07f595b0b6fc920fb0722", "9778397bd097c36b0b6fc9274c91aa",
"97b6b97bd19801ec9210c9274c920e", "97bcf7f0e47f531b0b0bb0b6fb0722", "7f0e397bd07f595b0b0bc920fb0722",
"9778397bd097c36b0b6fc9210c91aa", "97b6b97bd197c36c9210c9274c920e", "97bcf7f0e47f531b0b0bb0b6fb0722",
"7f0e397bd07f595b0b0bc920fb0722", "9778397bd097c36b0b6fc9210c8dc2", "9778397bd097c36c9210c9274c920e",
"97b6b7f0e47f531b0723b0b6fb0722", "7f0e37f5307f595b0b0bc920fb0722", "7f0e397bd097c36b0b6fc9210c8dc2",
"9778397bd097c36b0b70c9274c91aa", "97b6b7f0e47f531b0723b0b6fb0721", "7f0e37f1487f595b0b0bb0b6fb0722",
"7f0e397bd097c35b0b6fc9210c8dc2", "9778397bd097c36b0b6fc9274c91aa", "97b6b7f0e47f531b0723b0b6fb0721",
"7f0e27f1487f595b0b0bb0b6fb0722", "7f0e397bd097c35b0b6fc920fb0722", "9778397bd097c36b0b6fc9274c91aa",
"97b6b7f0e47f531b0723b0b6fb0721", "7f0e27f1487f531b0b0bb0b6fb0722", "7f0e397bd097c35b0b6fc920fb0722",
"9778397bd097c36b0b6fc9274c91aa", "97b6b7f0e47f531b0723b0b6fb0721", "7f0e27f1487f531b0b0bb0b6fb0722",
"7f0e397bd097c35b0b6fc920fb0722", "9778397bd097c36b0b6fc9274c91aa", "97b6b7f0e47f531b0723b0b6fb0721",
"7f0e27f1487f531b0b0bb0b6fb0722", "7f0e397bd07f595b0b0bc920fb0722", "9778397bd097c36b0b6fc9274c91aa",
"97b6b7f0e47f531b0723b0787b0721", "7f0e27f0e47f531b0b0bb0b6fb0722", "7f0e397bd07f595b0b0bc920fb0722",
"9778397bd097c36b0b6fc9210c91aa", "97b6b7f0e47f149b0723b0787b0721", "7f0e27f0e47f531b0723b0b6fb0722",
"7f0e397bd07f595b0b0bc920fb0722", "9778397bd097c36b0b6fc9210c8dc2", "977837f0e37f149b0723b0787b0721",
"7f07e7f0e47f531b0723b0b6fb0722", "7f0e37f5307f595b0b0bc920fb0722", "7f0e397bd097c35b0b6fc9210c8dc2",
"977837f0e37f14998082b0787b0721", "7f07e7f0e47f531b0723b0b6fb0721", "7f0e37f1487f595b0b0bb0b6fb0722",
"7f0e397bd097c35b0b6fc9210c8dc2", "977837f0e37f14998082b0787b06bd", "7f07e7f0e47f531b0723b0b6fb0721",
"7f0e27f1487f531b0b0bb0b6fb0722", "7f0e397bd097c35b0b6fc920fb0722", "977837f0e37f14998082b0787b06bd",
"7f07e7f0e47f531b0723b0b6fb0721", "7f0e27f1487f531b0b0bb0b6fb0722", "7f0e397bd097c35b0b6fc920fb0722",
"977837f0e37f14998082b0787b06bd", "7f07e7f0e47f531b0723b0b6fb0721", "7f0e27f1487f531b0b0bb0b6fb0722",
"7f0e397bd07f595b0b0bc920fb0722", "977837f0e37f14998082b0787b06bd", "7f07e7f0e47f531b0723b0b6fb0721",
"7f0e27f1487f531b0b0bb0b6fb0722", "7f0e397bd07f595b0b0bc920fb0722", "977837f0e37f14998082b0787b06bd",
"7f07e7f0e47f149b0723b0787b0721", "7f0e27f0e47f531b0b0bb0b6fb0722", "7f0e397bd07f595b0b0bc920fb0722",
"977837f0e37f14998082b0723b06bd", "7f07e7f0e37f149b0723b0787b0721", "7f0e27f0e47f531b0723b0b6fb0722",
"7f0e397bd07f595b0b0bc920fb0722", "977837f0e37f14898082b0723b02d5", "7ec967f0e37f14998082b0787b0721",
"7f07e7f0e47f531b0723b0b6fb0722", "7f0e37f1487f595b0b0bb0b6fb0722", "7f0e37f0e37f14898082b0723b02d5",
"7ec967f0e37f14998082b0787b0721", "7f07e7f0e47f531b0723b0b6fb0722", "7f0e37f1487f531b0b0bb0b6fb0722",
"7f0e37f0e37f14898082b0723b02d5", "7ec967f0e37f14998082b0787b06bd", "7f07e7f0e47f531b0723b0b6fb0721",
"7f0e37f1487f531b0b0bb0b6fb0722", "7f0e37f0e37f14898082b072297c35", "7ec967f0e37f14998082b0787b06bd",
"7f07e7f0e47f531b0723b0b6fb0721", "7f0e27f1487f531b0b0bb0b6fb0722", "7f0e37f0e37f14898082b072297c35",
"7ec967f0e37f14998082b0787b06bd", "7f07e7f0e47f531b0723b0b6fb0721", "7f0e27f1487f531b0b0bb0b6fb0722",
"7f0e37f0e366aa89801eb072297c35", "7ec967f0e37f14998082b0787b06bd", "7f07e7f0e47f149b0723b0787b0721",
"7f0e27f1487f531b0b0bb0b6fb0722", "7f0e37f0e366aa89801eb072297c35", "7ec967f0e37f14998082b0723b06bd",
"7f07e7f0e47f149b0723b0787b0721", "7f0e27f0e47f531b0723b0b6fb0722", "7f0e37f0e366aa89801eb072297c35",
"7ec967f0e37f14998082b0723b06bd", "7f07e7f0e37f14998083b0787b0721", "7f0e27f0e47f531b0723b0b6fb0722",
"7f0e37f0e366aa89801eb072297c35", "7ec967f0e37f14898082b0723b02d5", "7f07e7f0e37f14998082b0787b0721",
"7f07e7f0e47f531b0723b0b6fb0722", "7f0e36665b66aa89801e9808297c35", "665f67f0e37f14898082b0723b02d5",
"7ec967f0e37f14998082b0787b0721", "7f07e7f0e47f531b0723b0b6fb0722", "7f0e36665b66a449801e9808297c35",
"665f67f0e37f14898082b0723b02d5", "7ec967f0e37f14998082b0787b06bd", "7f07e7f0e47f531b0723b0b6fb0721",
"7f0e36665b66a449801e9808297c35", "665f67f0e37f14898082b072297c35", "7ec967f0e37f14998082b0787b06bd",
"7f07e7f0e47f531b0723b0b6fb0721", "7f0e26665b66a449801e9808297c35", "665f67f0e37f1489801eb072297c35",
"7ec967f0e37f14998082b0787b06bd", "7f07e7f0e47f531b0723b0b6fb0721", "7f0e27f1487f531b0b0bb0b6fb0722"};
}

View File

@ -0,0 +1,7 @@
/**
* 农历相关类汇总包括农历月天干地支农历节日24节气等
*
* @author looly
*
*/
package cn.hutool.core.date.chinese;

View File

@ -13,6 +13,7 @@ import javax.imageio.ImageIO;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.stream.ImageOutputStream;
import java.awt.AlphaComposite;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
@ -20,6 +21,7 @@ import java.awt.Image;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Stroke;
import java.awt.Toolkit;
import java.awt.color.ColorSpace;
import java.awt.geom.AffineTransform;
@ -554,6 +556,45 @@ public class Img implements Serializable {
return this;
}
/**
* 描边此方法为向内描边会覆盖图片相应的位置
*
* @param color 描边颜色默认黑色
* @param width 边框粗细
* @return this
* @since 5.4.1
*/
public Img stroke(Color color, float width){
return stroke(color, new BasicStroke(width));
}
/**
* 描边此方法为向内描边会覆盖图片相应的位置
*
* @param color 描边颜色默认黑色
* @param stroke 描边属性包括粗细线条类型等{@link BasicStroke}
* @return this
* @since 5.4.1
*/
public Img stroke(Color color, Stroke stroke){
final BufferedImage image = ImgUtil.toBufferedImage(getValidSrcImg());
int width = image.getWidth(null);
int height = image.getHeight(null);
Graphics2D g = image.createGraphics();
g.setColor(ObjectUtil.defaultIfNull(color, Color.BLACK));
if(null != stroke){
g.setStroke(stroke);
}
g.drawRect(0, 0, width -1 , height - 1);
g.dispose();
this.targetImage = image;
return this;
}
// ----------------------------------------------------------------------------------------------------------------- Write
/**

View File

@ -119,12 +119,14 @@ public class UUID implements java.io.Serializable, Comparable<UUID> {
public static UUID randomUUID(boolean isSecure) {
final Random ng = isSecure ? Holder.numberGenerator : RandomUtil.getRandom();
byte[] randomBytes = new byte[16];
final byte[] randomBytes = new byte[16];
ng.nextBytes(randomBytes);
randomBytes[6] &= 0x0f; /* clear version */
randomBytes[6] |= 0x40; /* set to version 4 */
randomBytes[8] &= 0x3f; /* clear variant */
randomBytes[8] |= 0x80; /* set to IETF variant */
return new UUID(randomBytes);
}

View File

@ -282,6 +282,8 @@ public class MapUtil {
/**
* 根据给定的Pair数组创建Map对象
*
* @param <K> 键类型
* @param <V> 值类型
* @param pairs 键值对
* @return Map
* @since 5.4.1

View File

@ -107,7 +107,7 @@ public final class CsvRow implements List<String> {
* @since 5.3.6
*/
public <T> T toBean(Class<T> clazz){
return BeanUtil.mapToBean(getFieldMap(), clazz, true);
return BeanUtil.toBeanIgnoreError(getFieldMap(), clazz);
}
/**

View File

@ -1,5 +1,7 @@
package cn.hutool.core.thread;
import cn.hutool.core.lang.Assert;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.AbstractExecutorService;
@ -18,69 +20,79 @@ import java.util.concurrent.TimeoutException;
public class DelegatedExecutorService extends AbstractExecutorService {
private final ExecutorService e;
/**
* 构造
*
* @param executor {@link ExecutorService}
*/
DelegatedExecutorService(ExecutorService executor) {
Assert.notNull(executor, "executor must be not null !");
e = executor;
}
@SuppressWarnings("NullableProblems")
@Override
public void execute(Runnable command) {
e.execute(command);
}
@Override
public void shutdown() {
e.shutdown();
}
@SuppressWarnings("NullableProblems")
@Override
public List<Runnable> shutdownNow() {
return e.shutdownNow();
}
@Override
public boolean isShutdown() {
return e.isShutdown();
}
@Override
public boolean isTerminated() {
return e.isTerminated();
}
@SuppressWarnings("NullableProblems")
@Override
public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {
return e.awaitTermination(timeout, unit);
}
@SuppressWarnings("NullableProblems")
@Override
public Future<?> submit(Runnable task) {
return e.submit(task);
}
@SuppressWarnings("NullableProblems")
@Override
public <T> Future<T> submit(Callable<T> task) {
return e.submit(task);
}
@SuppressWarnings("NullableProblems")
@Override
public <T> Future<T> submit(Runnable task, T result) {
return e.submit(task, result);
}
@SuppressWarnings("NullableProblems")
@Override
public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException {
return e.invokeAll(tasks);
}
@SuppressWarnings("NullableProblems")
@Override
public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit)
throws InterruptedException {
return e.invokeAll(tasks, timeout, unit);
}
@SuppressWarnings("NullableProblems")
@Override
public <T> T invokeAny(Collection<? extends Callable<T>> tasks)
throws InterruptedException, ExecutionException {
return e.invokeAny(tasks);
}
@Override
public <T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException {
return e.invokeAny(tasks, timeout, unit);

View File

@ -1,5 +1,8 @@
package cn.hutool.core.thread;
import cn.hutool.core.builder.Builder;
import cn.hutool.core.util.ObjectUtil;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
@ -11,12 +14,16 @@ import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import cn.hutool.core.builder.Builder;
import cn.hutool.core.util.ObjectUtil;
/**
* {@link ThreadPoolExecutor} 建造者
*
* <pre>
* 1. 如果池中任务数 &lt; corePoolSize - 放入立即执行
* 2. 如果池中任务数 &gt; corePoolSize - 放入队列等待
* 3. 队列满 - 新建线程立即执行
* 4. 执行中的线程 &gt; maxPoolSize - 触发handlerRejectedExecutionHandler异常
* </pre>
*
* @author looly
* @since 4.1.9
*/
@ -39,7 +46,7 @@ public class ExecutorBuilder implements Builder<ThreadPoolExecutor> {
*/
private long keepAliveTime = TimeUnit.SECONDS.toNanos(60);
/**
* 队列用于存未执行的线程
* 队列用于存未执行的线程
*/
private BlockingQueue<Runnable> workQueue;
/**
@ -105,7 +112,7 @@ public class ExecutorBuilder implements Builder<ThreadPoolExecutor> {
*
* <pre>
* 1. {@link SynchronousQueue} 它将任务直接提交给线程而不保持它们当运行线程小于maxPoolSize时会创建新线程否则触发异常策略
* 2. {@link LinkedBlockingQueue} 默认无界队列当运行线程大于corePoolSize时始终放入此队列此时maximumPoolSize无效
* 2. {@link LinkedBlockingQueue} 默认无界队列当运行线程大于corePoolSize时始终放入此队列此时maxPoolSize无效
* 当构造LinkedBlockingQueue对象时传入参数变为有界队列队列满时运行线程小于maxPoolSize时会创建新线程否则触发异常策略
* 3. {@link ArrayBlockingQueue} 有界队列相对无界队列有利于控制队列大小队列满时运行线程小于maxPoolSize时会创建新线程否则触发异常策略
* </pre>

View File

@ -8,6 +8,12 @@ import java.util.concurrent.ExecutorService;
* @author loolly
*/
public class FinalizableDelegatedExecutorService extends DelegatedExecutorService {
/**
* 构造
*
* @param executor {@link ExecutorService}
*/
FinalizableDelegatedExecutorService(ExecutorService executor) {
super(executor);
}

View File

@ -16,9 +16,10 @@ import java.util.concurrent.ExecutorService;
* ps:
* //模拟1000个线程并发
* SyncFinisher sf = new SyncFinisher(1000);
* concurrencyTestUtil.run(() -&gt; {
* sf.addWorker(() -&gt; {
* // 需要并发测试的业务代码
* });
* sf.start()
* </pre>
*
*

View File

@ -31,7 +31,7 @@ public class ThreadUtil {
* @param corePoolSize 同时执行的线程数大小
* @return ExecutorService
*/
public static ExecutorService newExecutor(int corePoolSize) {
public static ExecutorService newExecutor(int corePoolSize) {
ExecutorBuilder builder = ExecutorBuilder.create();
if (corePoolSize > 0) {
builder.setCorePoolSize(corePoolSize);

View File

@ -61,7 +61,7 @@ public class EnumUtil {
*/
public static <E extends Enum<E>> E getEnumAt(Class<E> enumClass, int index) {
final E[] enumConstants = enumClass.getEnumConstants();
return index < enumConstants.length ? enumConstants[index] : null;
return index >= 0 && index < enumConstants.length ? enumConstants[index] : null;
}
/**

View File

@ -2731,14 +2731,14 @@ public class StrUtil {
}
/**
* 包装多个字符串
* 使用单个字符包装多个字符串
*
* @param prefixAndSuffix 前缀和后缀
* @param strs 多个字符串
* @return 包装的字符串数组
* @since 4.0.7
* @since 5.4.1
*/
public static String[] wrapAll(CharSequence prefixAndSuffix, CharSequence... strs) {
public static String[] wrapAllWithPair(CharSequence prefixAndSuffix, CharSequence... strs) {
return wrapAll(prefixAndSuffix, prefixAndSuffix, strs);
}
@ -2792,14 +2792,14 @@ public class StrUtil {
}
/**
* 包装多个字符串如果已经包装则不再包装
* 使用成对的字符包装多个字符串如果已经包装则不再包装
*
* @param prefixAndSuffix 前缀和后缀
* @param strs 多个字符串
* @return 包装的字符串数组
* @since 4.0.7
* @since 5.4.1
*/
public static String[] wrapAllIfMissing(CharSequence prefixAndSuffix, CharSequence... strs) {
public static String[] wrapAllWithPairIfMissing(CharSequence prefixAndSuffix, CharSequence... strs) {
return wrapAllIfMissing(prefixAndSuffix, prefixAndSuffix, strs);
}
@ -3427,7 +3427,7 @@ public class StrUtil {
* @param start 起始位置如果小于0从0开始查找
* @return 位置
*/
public static int indexOf(final CharSequence str, char searchChar, int start) {
public static int indexOf(CharSequence str, char searchChar, int start) {
if (str instanceof String) {
return ((String) str).indexOf(searchChar, start);
} else {
@ -3445,6 +3445,9 @@ public class StrUtil {
* @return 位置
*/
public static int indexOf(final CharSequence str, char searchChar, int start, int end) {
if(isEmpty(str)){
return INDEX_NOT_FOUND;
}
final int len = str.length();
if (start < 0 || start > len) {
start = 0;
@ -3457,7 +3460,7 @@ public class StrUtil {
return i;
}
}
return -1;
return INDEX_NOT_FOUND;
}
/**

View File

@ -45,4 +45,27 @@ public class ChineseDateTest {
date = new ChineseDate(DateUtil.parseDate("1996-07-15"));
Assert.assertEquals("丙子鼠年 五月三十", date.toString());
}
@Test
public void getCyclicalYMDTest(){
//通过公历构建
ChineseDate chineseDate = new ChineseDate(DateUtil.parseDate("1993-01-06"));
String cyclicalYMD = chineseDate.getCyclicalYMD();
Assert.assertEquals("壬申年癸丑月丁亥日",cyclicalYMD);
}
@Test
public void getCyclicalYMDTest2(){
//通过农历构建
ChineseDate chineseDate = new ChineseDate(1992,12,14);
String cyclicalYMD = chineseDate.getCyclicalYMD();
Assert.assertEquals("壬申年癸丑月丁亥日",cyclicalYMD);
}
@Test
public void getCyclicalYMDTest3(){
//通过公历构建
ChineseDate chineseDate = new ChineseDate(DateUtil.parseDate("2020-08-28"));
String cyclicalYMD = chineseDate.getCyclicalYMD();
Assert.assertEquals("庚子年甲申月癸卯日",cyclicalYMD);
}
}

View File

@ -55,4 +55,12 @@ public class ImgTest {
.pressImage(ImgUtil.read("d:/test/617180969474805871.jpg"), new Rectangle(0, 0, 800, 800), 1f)
.write(FileUtil.file("d:/test/pressImg_result.jpg"));
}
@Test
@Ignore
public void strokeTest(){
Img.from(FileUtil.file("d:/test/公章3.png"))
.stroke(null, 2f)
.write(FileUtil.file("d:/test/stroke_result.png"));
}
}

View File

@ -0,0 +1,21 @@
package cn.hutool.core.lang;
import cn.hutool.core.collection.ConcurrentHashSet;
import cn.hutool.core.thread.ThreadUtil;
import org.junit.Assert;
import org.junit.Test;
import java.util.Set;
public class UUIDTest {
/**
* 测试UUID是否存在重复问题
*/
@Test
public void fastUUIDTest(){
Set<String> set = new ConcurrentHashSet<>(100);
ThreadUtil.concurrencyTest(100, ()-> set.add(UUID.fastUUID().toString()));
Assert.assertEquals(100, set.size());
}
}

View File

@ -458,4 +458,13 @@ public class StrUtilTest {
String cleanBlank = StrUtil.filter(" 你 好 ", c -> !CharUtil.isBlankChar(c));
Assert.assertEquals("你好", cleanBlank);
}
@Test
public void wrapAllTest(){
String[] strings = StrUtil.wrapAll("`", "`", StrUtil.splitToArray("1,2,3,4", ','));
Assert.assertEquals("[`1`, `2`, `3`, `4`]", StrUtil.utf8Str(strings));
strings = StrUtil.wrapAllWithPair("`", StrUtil.splitToArray("1,2,3,4", ','));
Assert.assertEquals("[`1`, `2`, `3`, `4`]", StrUtil.utf8Str(strings));
}
}

View File

@ -56,4 +56,5 @@ public class TypeUtilTest {
public void service(String string) {
}
}
}

View File

@ -7,7 +7,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.4.1-SNAPSHOT</version>
<version>5.4.2-SNAPSHOT</version>
</parent>
<artifactId>hutool-cron</artifactId>

View File

@ -52,8 +52,8 @@ public class TaskTable implements Serializable {
*/
public TaskTable add(String id, CronPattern pattern, Task task) {
final Lock writeLock = lock.writeLock();
writeLock.lock();
try {
writeLock.lock();
if (ids.contains(id)) {
throw new CronException("Id [{}] has been existed!", id);
}
@ -75,8 +75,8 @@ public class TaskTable implements Serializable {
*/
public List<String> getIds() {
final Lock readLock = lock.readLock();
readLock.lock();
try {
readLock.lock();
return Collections.unmodifiableList(this.ids);
} finally {
readLock.unlock();
@ -91,8 +91,8 @@ public class TaskTable implements Serializable {
*/
public List<CronPattern> getPatterns() {
final Lock readLock = lock.readLock();
readLock.lock();
try {
readLock.lock();
return Collections.unmodifiableList(this.patterns);
} finally {
readLock.unlock();
@ -107,8 +107,8 @@ public class TaskTable implements Serializable {
*/
public List<Task> getTasks() {
final Lock readLock = lock.readLock();
readLock.lock();
try {
readLock.lock();
return Collections.unmodifiableList(this.tasks);
} finally {
readLock.unlock();
@ -122,8 +122,8 @@ public class TaskTable implements Serializable {
*/
public void remove(String id) {
final Lock writeLock = lock.writeLock();
writeLock.lock();
try {
writeLock.lock();
final int index = ids.indexOf(id);
if (index > -1) {
tasks.remove(index);
@ -146,8 +146,8 @@ public class TaskTable implements Serializable {
*/
public boolean updatePattern(String id, CronPattern pattern) {
final Lock writeLock = lock.writeLock();
writeLock.lock();
try {
writeLock.lock();
final int index = ids.indexOf(id);
if (index > -1) {
patterns.set(index, pattern);
@ -168,8 +168,8 @@ public class TaskTable implements Serializable {
*/
public Task getTask(int index) {
final Lock readLock = lock.readLock();
readLock.lock();
try {
readLock.lock();
return tasks.get(index);
} finally {
readLock.unlock();
@ -200,8 +200,8 @@ public class TaskTable implements Serializable {
*/
public CronPattern getPattern(int index) {
final Lock readLock = lock.readLock();
readLock.lock();
try {
readLock.lock();
return patterns.get(index);
} finally {
readLock.unlock();
@ -250,8 +250,8 @@ public class TaskTable implements Serializable {
*/
public void executeTaskIfMatch(long millis) {
final Lock readLock = lock.readLock();
readLock.lock();
try {
readLock.lock();
executeTaskIfMatchInternal(millis);
} finally {
readLock.unlock();

View File

@ -9,7 +9,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.4.1-SNAPSHOT</version>
<version>5.4.2-SNAPSHOT</version>
</parent>
<artifactId>hutool-crypto</artifactId>

View File

@ -140,8 +140,8 @@ public class RC4 implements Serializable {
*/
public byte[] crypt(final byte[] msg) {
final ReadLock readLock = this.lock.readLock();
readLock.lock();
byte[] code;
readLock.lock();
try {
final int[] sbox = this.sbox.clone();
code = new byte[msg.length];

View File

@ -204,7 +204,7 @@ public class SymmetricCrypto implements Serializable {
} else {
cipher.init(Cipher.ENCRYPT_MODE, secretKey, params);
}
return cipher.doFinal(paddingDataWithZero(data, cipher.getBlockSize()));
return cipher.doFinal(paddingDataWithZero(data, cipher.getBlockSize()));
} catch (Exception e) {
throw new CryptoException(e);
} finally {

View File

@ -1,4 +1,4 @@
package cn.hutool.crypto.test;
package cn.hutool.crypto.test.asymmetric;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.asymmetric.ECIES;

View File

@ -1,4 +1,4 @@
package cn.hutool.crypto.test;
package cn.hutool.crypto.test.asymmetric;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.util.ArrayUtil;

View File

@ -1,4 +1,4 @@
package cn.hutool.crypto.test;
package cn.hutool.crypto.test.asymmetric;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.util.CharsetUtil;
@ -19,9 +19,8 @@ import java.security.PublicKey;
/**
* SM2算法单元测试
*
* @author Looly, Gsealy
*
* @author Looly, Gsealy
*/
public class SM2Test {
@ -71,7 +70,7 @@ public class SM2Test {
byte[] decrypt = sm2.decrypt(encrypt, KeyType.PrivateKey);
Assert.assertEquals("我是一段测试aaaa", StrUtil.str(decrypt, CharsetUtil.CHARSET_UTF_8));
}
@Test
public void sm2BcdTest() {
String text = "我是一段测试aaaa";
@ -138,6 +137,19 @@ public class SM2Test {
KeyPair pair = SecureUtil.generateKeyPair("SM2");
final SM2 sm2 = new SM2(pair.getPrivate(), pair.getPublic());
byte[] sign = sm2.sign(content.getBytes());
boolean verify = sm2.verify(content.getBytes(), sign);
Assert.assertTrue(verify);
}
@Test
public void sm2SignAndVerifyUseKeyTest2() {
String content = "我是Hanley.";
KeyPair pair = SecureUtil.generateKeyPair("SM2");
final SM2 sm2 = new SM2(//
HexUtil.encodeHexStr(pair.getPrivate().getEncoded()), //
HexUtil.encodeHexStr(pair.getPublic().getEncoded())//
@ -162,7 +174,7 @@ public class SM2Test {
}
@Test
public void sm2WithPointTest(){
public void sm2WithPointTest() {
String d = "FAB8BBE670FAE338C9E9382B9FB6485225C11A3ECB84C938F10F20A93B6215F0";
String x = "9EF573019D9A03B16B0BE44FC8A5B4E8E098F56034C97B312282DD0B4810AFC3";
String y = "CC759673ED0FC9B9DC7E6FA38F0E2B121E02654BF37EA6B63FAF2A0D6013EADF";
@ -176,7 +188,7 @@ public class SM2Test {
}
@Test
public void sm2PlainWithPointTest(){
public void sm2PlainWithPointTest() {
// 测试地址https://i.goto327.top/CryptTools/SM2.aspx?tdsourcetag=s_pctim_aiomsg
String d = "FAB8BBE670FAE338C9E9382B9FB6485225C11A3ECB84C938F10F20A93B6215F0";

View File

@ -1,4 +1,4 @@
package cn.hutool.crypto.test;
package cn.hutool.crypto.test.asymmetric;
import cn.hutool.core.map.MapUtil;
import org.junit.Assert;

View File

@ -1,4 +1,4 @@
package cn.hutool.crypto.test;
package cn.hutool.crypto.test.digest;
import org.junit.Assert;
import org.junit.Test;

View File

@ -1,4 +1,4 @@
package cn.hutool.crypto.test;
package cn.hutool.crypto.test.symmetric;
import cn.hutool.crypto.Mode;
import cn.hutool.crypto.Padding;

View File

@ -1,4 +1,4 @@
package cn.hutool.crypto.test;
package cn.hutool.crypto.test.symmetric;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.Mode;

View File

@ -1,4 +1,4 @@
package cn.hutool.crypto.test;
package cn.hutool.crypto.test.symmetric;
import org.junit.Assert;
import org.junit.Test;

View File

@ -1,4 +1,4 @@
package cn.hutool.crypto.test;
package cn.hutool.crypto.test.symmetric;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.RandomUtil;

View File

@ -9,7 +9,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.4.1-SNAPSHOT</version>
<version>5.4.2-SNAPSHOT</version>
</parent>
<artifactId>hutool-db</artifactId>

View File

@ -248,6 +248,25 @@ public abstract class AbstractDb implements Serializable {
}
}
/**
* 批量执行非查询语句
*
* @param sql SQL
* @param paramsBatch 批量的参数
* @return 每个SQL执行影响的行数
* @throws SQLException SQL执行异常
* @since 5.4.2
*/
public int[] executeBatch(String sql, Iterable<Object[]> paramsBatch) throws SQLException {
Connection conn = null;
try {
conn = this.getConnection();
return SqlExecutor.executeBatch(conn, sql, paramsBatch);
} finally {
this.closeConnection(conn);
}
}
/**
* 批量执行非查询语句
*
@ -266,6 +285,24 @@ public abstract class AbstractDb implements Serializable {
}
}
/**
* 批量执行非查询语句
*
* @param sqls SQL列表
* @return 每个SQL执行影响的行数
* @throws SQLException SQL执行异常
* @since 5.4.2
*/
public int[] executeBatch(Iterable<String> sqls) throws SQLException {
Connection conn = null;
try {
conn = this.getConnection();
return SqlExecutor.executeBatch(conn, sqls);
} finally {
this.closeConnection(conn);
}
}
// ---------------------------------------------------------------------------- CRUD start
/**

View File

@ -7,7 +7,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.4.1-SNAPSHOT</version>
<version>5.4.2-SNAPSHOT</version>
</parent>
<artifactId>hutool-dfa</artifactId>

View File

@ -9,7 +9,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.4.1-SNAPSHOT</version>
<version>5.4.2-SNAPSHOT</version>
</parent>
<artifactId>hutool-extra</artifactId>

View File

@ -1,11 +1,9 @@
package cn.hutool.extra.cglib;
import cn.hutool.core.lang.SimpleCache;
import cn.hutool.core.lang.func.Func0;
import cn.hutool.core.util.StrUtil;
import net.sf.cglib.beans.BeanCopier;
import java.beans.PropertyDescriptor;
import net.sf.cglib.core.Converter;
/**
* BeanCopier属性缓存<br>
@ -15,20 +13,39 @@ import java.beans.PropertyDescriptor;
* @since 5.4.1
*/
public enum BeanCopierCache {
/**
* BeanCopier属性缓存单例
*/
INSTANCE;
private final SimpleCache<String, BeanCopier> cache = new SimpleCache<>();
/**
* 获得属性名和{@link PropertyDescriptor}Map映射
* 获得类与转换器生成的key在{@link BeanCopier}的Map中对应的元素
*
* @param srcClass 源Bean的类
* @param targetClass 目标Bean的类
* @param supplier 缓存对象产生函数
* @return 属性名和{@link PropertyDescriptor}Map映射
* @since 5.4.1
* @param converter 转换器
* @return Map中对应的BeanCopier
*/
public BeanCopier get(Class<?> srcClass, Class<?> targetClass, Func0<BeanCopier> supplier) {
return this.cache.get(StrUtil.format("{}_{}", srcClass.getName(), srcClass.getName()), supplier);
public BeanCopier get(Class<?> srcClass, Class<?> targetClass, Converter converter) {
final String key = genKey(srcClass, targetClass, converter);
return cache.get(key, () -> BeanCopier.create(srcClass, targetClass, converter != null));
}
/**
* 获得类与转换器生成的key
*
* @param srcClass 源Bean的类
* @param targetClass 目标Bean的类
* @param converter 转换器
* @return 属性名和Map映射的key
*/
private String genKey(Class<?> srcClass, Class<?> targetClass, Converter converter) {
String key = StrUtil.format("{}#{}", srcClass.getName(), targetClass.getName());
if(null != converter){
key += "#" + converter.getClass().getName();
}
return key;
}
}

View File

@ -6,6 +6,12 @@ import net.sf.cglib.beans.BeanCopier;
import net.sf.cglib.beans.BeanMap;
import net.sf.cglib.core.Converter;
import java.util.Collection;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.Supplier;
import java.util.stream.Collectors;
/**
* Cglib工具类
*
@ -39,7 +45,7 @@ public class CglibUtil {
*/
public static <T> T copy(Object source, Class<T> targetClass, Converter converter) {
final T target = ReflectUtil.newInstanceIfPossible(targetClass);
copy(source, target);
copy(source, target, converter);
return target;
}
@ -66,18 +72,82 @@ public class CglibUtil {
final Class<?> sourceClass = source.getClass();
final Class<?> targetClass = target.getClass();
final BeanCopier beanCopier = BeanCopierCache.INSTANCE.get(
sourceClass, targetClass,
() -> BeanCopier.create(sourceClass, targetClass, null != converter));
final BeanCopier beanCopier = BeanCopierCache.INSTANCE.get(sourceClass, targetClass, converter);
beanCopier.copy(source, target, converter);
}
/**
* 拷贝List Bean对象属性
*
* @param <S> 源bean类型
* @param <T> 目标bean类型
* @param source 源bean对象list
* @param target 目标bean对象
* @return 目标bean对象list
*/
public static <S, T> List<T> copyList(Collection<S> source, Supplier<T> target) {
return copyList(source, target, null, null);
}
/**
* 拷贝List Bean对象属性
*
* @param source 源bean对象list
* @param target 目标bean对象
* @param converter 转换器无需可传{@code null}
* @param <S> 源bean类型
* @param <T> 目标bean类型
* @return 目标bean对象list
* @since 5.4.1
*/
public static <S, T> List<T> copyList(Collection<S> source, Supplier<T> target, Converter converter) {
return copyList(source, target, converter, null);
}
/**
* 拷贝List Bean对象属性
*
* @param source 源bean对象list
* @param target 目标bean对象
* @param callback 回调对象
* @param <S> 源bean类型
* @param <T> 目标bean类型
* @return 目标bean对象list
* @since 5.4.1
*/
public static <S, T> List<T> copyList(Collection<S> source, Supplier<T> target, BiConsumer<S, T> callback) {
return copyList(source, target, null, callback);
}
/**
* 拷贝List Bean对象属性
*
* @param source 源bean对象list
* @param target 目标bean对象
* @param converter 转换器无需可传{@code null}
* @param callback 回调对象
* @param <S> 源bean类型
* @param <T> 目标bean类型
* @return 目标bean对象list
*/
public static <S, T> List<T> copyList(Collection<S> source, Supplier<T> target, Converter converter, BiConsumer<S, T> callback) {
return source.stream().map(s -> {
T t = target.get();
copy(source, t, converter);
if (callback != null) {
callback.accept(s, t);
}
return t;
}).collect(Collectors.toList());
}
/**
* 将Bean转换为Map
*
* @param bean Bean对象
* @return {@link BeanMap}
* @since 5.4.1
*/
public static BeanMap toMap(Object bean) {
return BeanMap.create(bean);

View File

@ -11,9 +11,11 @@ import javax.activation.DataHandler;
import javax.activation.DataSource;
import javax.activation.FileDataSource;
import javax.activation.FileTypeMap;
import javax.mail.Address;
import javax.mail.Authenticator;
import javax.mail.MessagingException;
import javax.mail.Multipart;
import javax.mail.SendFailedException;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.MimeBodyPart;
@ -355,6 +357,12 @@ public class Mail {
try {
return doSend();
} catch (MessagingException e) {
if(e instanceof SendFailedException){
// 当地址无效时显示更加详细的无效地址信息
final Address[] invalidAddresses = ((SendFailedException) e).getInvalidAddresses();
final String msg = StrUtil.format("Invalid Addresses: {}", ArrayUtil.toString(invalidAddresses));
throw new MailException(msg, e);
}
throw new MailException(e);
}
}

View File

@ -0,0 +1,19 @@
package cn.hutool.extra.spring;
import org.springframework.context.annotation.Import;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 启用SpringUtil, 即注入SpringUtil到容器中
*
* @author sidian
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(SpringUtil.class)
public @interface EnableSpringUtil {
}

View File

@ -6,13 +6,7 @@ import cn.hutool.core.lang.Assert;
import cn.hutool.core.net.LocalPortGenerater;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.StrUtil;
import com.jcraft.jsch.Channel;
import com.jcraft.jsch.ChannelExec;
import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.ChannelShell;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import com.jcraft.jsch.*;
import java.io.IOException;
import java.io.InputStream;
@ -232,6 +226,31 @@ public class JschUtil {
return false;
}
/**
* 绑定ssh服务端的serverPort端口, 到host主机的port端口上. <br>
* 即数据从ssh服务端的serverPort端口, 流经ssh客户端, 达到host:port上.
*
* @param session 与ssh服务端建立的会话
* @param bindPort ssh服务端上要被绑定的端口
* @param host 转发到的host
* @param port host上的端口
* @return 成功与否
* @throws JschRuntimeException 端口绑定失败异常
* @since 5.4.2
*/
public static boolean bindRemotePort(Session session, int bindPort, String host, int port) throws JschRuntimeException {
if (session != null && session.isConnected()) {
try {
session.setPortForwardingR(bindPort, host, port);
} catch (JSchException e) {
throw new JschRuntimeException(e, "From [{}] mapping to [{}] error", bindPort, port);
}
return true;
}
return false;
}
/**
* 解除端口映射
*

View File

@ -1,11 +1,10 @@
package cn.hutool.extra.template.engine.velocity;
import org.apache.velocity.app.Velocity;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.template.Template;
import cn.hutool.extra.template.TemplateConfig;
import cn.hutool.extra.template.TemplateEngine;
import org.apache.velocity.app.Velocity;
/**
* Velocity模板引擎
@ -16,6 +15,7 @@ import cn.hutool.extra.template.TemplateEngine;
public class VelocityEngine implements TemplateEngine {
private org.apache.velocity.app.VelocityEngine engine;
private TemplateConfig config;
// --------------------------------------------------------------------------------- Constructor start
/**
@ -47,6 +47,7 @@ public class VelocityEngine implements TemplateEngine {
if(null == config){
config = TemplateConfig.DEFAULT;
}
this.config = config;
init(createEngine(config));
return this;
}
@ -74,7 +75,25 @@ public class VelocityEngine implements TemplateEngine {
if(null == this.engine){
init(TemplateConfig.DEFAULT);
}
return VelocityTemplate.wrap(engine.getTemplate(resource));
// 目录前缀
String root;
// 自定义编码
String charsetStr = null;
if(null != this.config){
root = this.config.getPath();
charsetStr = this.config.getCharsetStr();
// 修正template目录在classpath或者web_root模式下按照配置添加默认前缀
// 如果用户已经自行添加了前缀则忽略之
final TemplateConfig.ResourceMode resourceMode = this.config.getResourceMode();
if(TemplateConfig.ResourceMode.CLASSPATH == resourceMode
|| TemplateConfig.ResourceMode.WEB_ROOT == resourceMode){
resource = StrUtil.addPrefixIfNot(resource, StrUtil.addSuffixIfNot(root, "/"));
}
}
return VelocityTemplate.wrap(engine.getTemplate(resource, charsetStr));
}
/**
@ -98,7 +117,9 @@ public class VelocityEngine implements TemplateEngine {
// loader
switch (config.getResourceMode()) {
case CLASSPATH:
ve.setProperty("file.resource.loader.class", "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader");
// 新版Velocity弃用
// ve.setProperty("file.resource.loader.class", "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader");
ve.setProperty("resource.loader.file.class", "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader");
break;
case FILE:
// path

View File

@ -0,0 +1,24 @@
package cn.hutool.extra.spring;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
/**
* @author sidian
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = EnableSprintUtilTest.class)
@EnableSpringUtil
public class EnableSprintUtilTest {
@Test
public void test() {
// 使用@EnableSpringUtil注解后, 能获取上下文
Assert.assertNotNull(SpringUtil.getApplicationContext());
// 不使用时, 为null
// Assert.assertNull(SpringUtil.getApplicationContext());
}
}

View File

@ -3,6 +3,7 @@ package cn.hutool.extra.ssh;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.lang.Console;
import com.jcraft.jsch.Session;
import org.junit.Assert;
import org.junit.Ignore;
import org.junit.Test;
@ -13,7 +14,7 @@ import org.junit.Test;
*
*/
public class JschUtilTest {
@Test
@Ignore
public void bindPortTest() {
@ -22,7 +23,22 @@ public class JschUtilTest {
// 将堡垒机保护的内网8080端口映射到localhost我们就可以通过访问http://localhost:8080/访问内网服务了
JschUtil.bindPort(session, "172.20.12.123", 8080, 8080);
}
@Test
@Ignore
public void bindRemotePort() throws InterruptedException {
// 建立会话
Session session = JschUtil.getSession("looly.centos", 22, "test", "123456");
// 绑定ssh服务端8089端口到本机的8000端口上
boolean b = JschUtil.bindRemotePort(session, 8089, "localhost", 8000);
Assert.assertTrue(b);
// 保证一直运行
// while (true){
// Thread.sleep(3000);
// }
}
@Test
@Ignore
public void sftpTest() {

View File

@ -1,13 +1,5 @@
package cn.hutool.extra.template;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
import org.junit.Assert;
import org.junit.Ignore;
import org.junit.Test;
import cn.hutool.core.lang.Dict;
import cn.hutool.extra.template.TemplateConfig.ResourceMode;
import cn.hutool.extra.template.engine.beetl.BeetlEngine;
@ -16,6 +8,13 @@ import cn.hutool.extra.template.engine.freemarker.FreemarkerEngine;
import cn.hutool.extra.template.engine.rythm.RythmEngine;
import cn.hutool.extra.template.engine.thymeleaf.ThymeleafEngine;
import cn.hutool.extra.template.engine.velocity.VelocityEngine;
import org.junit.Assert;
import org.junit.Ignore;
import org.junit.Test;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
/**
* 模板引擎单元测试
@ -99,10 +98,13 @@ public class TemplateUtilTest {
//ClassPath模板
engine = TemplateUtil.createEngine(
new TemplateConfig("templates", ResourceMode.CLASSPATH).setCustomEngine(VelocityEngine.class));
template = engine.getTemplate("velocity_test.vtl");
result = template.render(Dict.create().set("name", "hutool"));
Assert.assertEquals("你好,hutool", result);
template = engine.getTemplate("templates/velocity_test.vtl");
result = template.render(Dict.create().set("name", "hutool"));
Assert.assertEquals("你好,hutool", result);
}
@Test

View File

@ -0,0 +1,21 @@
package cn.hutool.extra.template;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.extra.template.engine.velocity.VelocityEngine;
import org.junit.Assert;
import org.junit.Test;
public class VelocityTest {
@Test
public void charsetTest(){
final TemplateConfig config = new TemplateConfig("templates", TemplateConfig.ResourceMode.CLASSPATH);
config.setCustomEngine(VelocityEngine.class);
config.setCharset(CharsetUtil.CHARSET_GBK);
final TemplateEngine engine = TemplateUtil.createEngine(config);
Template template = engine.getTemplate("velocity_test_gbk.vtl");
String result = template.render(Dict.create().set("name", "hutool"));
Assert.assertEquals("你好,hutool", result);
}
}

View File

@ -0,0 +1 @@
ÄăşĂ,$name

View File

@ -9,7 +9,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.4.1-SNAPSHOT</version>
<version>5.4.2-SNAPSHOT</version>
</parent>
<artifactId>hutool-http</artifactId>

View File

@ -30,6 +30,7 @@ import java.net.HttpCookie;
import java.net.HttpURLConnection;
import java.net.Proxy;
import java.net.URLStreamHandler;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@ -413,6 +414,18 @@ public class HttpRequest extends HttpBase<HttpRequest> {
return this;
}
/**
* 设置Cookie<br>
* 自定义Cookie后会覆盖Hutool的默认Cookie行为
*
* @param cookies Cookie值数组如果为{@code null}则设置无效使用默认Cookie行为
* @return this
* @since 5.4.1
*/
public HttpRequest cookie(Collection<HttpCookie> cookies) {
return cookie(CollUtil.isEmpty(cookies) ? null : cookies.toArray(new HttpCookie[0]));
}
/**
* 设置Cookie<br>
* 自定义Cookie后会覆盖Hutool的默认Cookie行为

View File

@ -9,7 +9,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.4.1-SNAPSHOT</version>
<version>5.4.2-SNAPSHOT</version>
</parent>
<artifactId>hutool-json</artifactId>

View File

@ -5,10 +5,11 @@ import org.junit.Test;
public class XMLTest {
@SuppressWarnings("ConstantConditions")
@Test
public void toXmlTest(){
final JSONObject put = JSONUtil.createObj().put("aaa", "你好").put("键2", "test");
final JSONObject put = JSONUtil.createObj()
.set("aaa", "你好")
.set("键2", "test");
final String s = JSONUtil.toXmlStr(put);
Assert.assertEquals("<aaa>你好</aaa><键2>test</键2>", s);
}

View File

@ -9,7 +9,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.4.1-SNAPSHOT</version>
<version>5.4.2-SNAPSHOT</version>
</parent>
<artifactId>hutool-log</artifactId>

View File

@ -8,7 +8,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.4.1-SNAPSHOT</version>
<version>5.4.2-SNAPSHOT</version>
</parent>
<artifactId>hutool-poi</artifactId>

View File

@ -1,14 +1,11 @@
package cn.hutool.poi.excel;
import cn.hutool.core.io.IORuntimeException;
import org.apache.poi.poifs.filesystem.FileMagic;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PushbackInputStream;
import org.apache.poi.poifs.filesystem.FileMagic;
import cn.hutool.core.io.IORuntimeException;
import cn.hutool.core.io.IoUtil;
/**
* Excel文件工具类
@ -26,9 +23,18 @@ public class ExcelFileUtil {
* @return 是否为XLS格式的Excel文件HSSF
*/
public static boolean isXls(InputStream in) {
final PushbackInputStream pin = IoUtil.toPushbackStream(in, 8);
/*
* {@link java.io.PushbackInputStream}
* PushbackInputStream的markSupported()为false并不支持mark和reset
* 如果强转成PushbackInputStream在调用FileMagic.valueOf(inputStream)时会报错
* {@link FileMagic}
* 报错内容getFileMagic() only operates on streams which support mark(int)
* 此处修改成 final InputStream inputStream = FileMagic.prepareToCheckMagic(in)
* @author kefan.qu
*/
final InputStream inputStream = FileMagic.prepareToCheckMagic(in);
try {
return FileMagic.valueOf(pin) == FileMagic.OLE2;
return FileMagic.valueOf(inputStream) == FileMagic.OLE2;
} catch (IOException e) {
throw new IORuntimeException(e);
}

View File

@ -289,7 +289,7 @@ public class ExcelUtil {
*/
public static ExcelReader getReader(InputStream bookStream, int sheetIndex) {
try {
return new ExcelReader(bookStream, sheetIndex, true);
return new ExcelReader(bookStream, sheetIndex);
} catch (NoClassDefFoundError e) {
throw new DependencyException(ObjectUtil.defaultIfNull(e.getCause(), e), PoiChecker.NO_POI_ERROR_MSG);
}
@ -303,10 +303,12 @@ public class ExcelUtil {
* @param closeAfterRead 读取结束是否关闭流
* @return {@link ExcelReader}
* @since 4.0.3
* @deprecated 使用完毕无论是否closeAfterReadpoi会关闭流此参数无意义
*/
@Deprecated
public static ExcelReader getReader(InputStream bookStream, int sheetIndex, boolean closeAfterRead) {
try {
return new ExcelReader(bookStream, sheetIndex, closeAfterRead);
return new ExcelReader(bookStream, sheetIndex);
} catch (NoClassDefFoundError e) {
throw new DependencyException(ObjectUtil.defaultIfNull(e.getCause(), e), PoiChecker.NO_POI_ERROR_MSG);
}
@ -322,7 +324,7 @@ public class ExcelUtil {
*/
public static ExcelReader getReader(InputStream bookStream, String sheetName) {
try {
return new ExcelReader(bookStream, sheetName, true);
return new ExcelReader(bookStream, sheetName);
} catch (NoClassDefFoundError e) {
throw new DependencyException(ObjectUtil.defaultIfNull(e.getCause(), e), PoiChecker.NO_POI_ERROR_MSG);
}
@ -335,10 +337,12 @@ public class ExcelUtil {
* @param sheetName sheet名第一个默认是sheet1
* @param closeAfterRead 读取结束是否关闭流
* @return {@link ExcelReader}
* @deprecated 使用完毕无论是否closeAfterReadpoi会关闭流此参数无意义
*/
@Deprecated
public static ExcelReader getReader(InputStream bookStream, String sheetName, boolean closeAfterRead) {
try {
return new ExcelReader(bookStream, sheetName, closeAfterRead);
return new ExcelReader(bookStream, sheetName);
} catch (NoClassDefFoundError e) {
throw new DependencyException(ObjectUtil.defaultIfNull(e.getCause(), e), PoiChecker.NO_POI_ERROR_MSG);
}

View File

@ -7,9 +7,14 @@ import cn.hutool.poi.excel.cell.CellUtil;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.ss.util.CellRangeUtil;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
/**
* Excel中的行{@link Row}封装工具类
@ -97,4 +102,88 @@ public class RowUtil {
i++;
}
}
/**
* 插入行
*
* @param sheet 工作表
* @param startRow 插入的起始行
* @param insertNumber 插入的行数
* @since 5.4.2
*/
public static void insertRow(Sheet sheet, int startRow, int insertNumber) {
if (insertNumber <= 0) {
return;
}
// 插入位置的行如果插入的行不存在则创建新行
Row sourceRow = Optional.ofNullable(sheet.getRow(startRow)).orElseGet(() -> sheet.createRow(insertNumber));
// 从插入行开始到最后一行向下移动
sheet.shiftRows(startRow, sheet.getLastRowNum(), insertNumber, true, false);
// 填充移动后留下的空行
IntStream.range(startRow, startRow + insertNumber).forEachOrdered(i -> {
Row row = sheet.createRow(i);
row.setHeightInPoints(sourceRow.getHeightInPoints());
short lastCellNum = sourceRow.getLastCellNum();
IntStream.range(0, lastCellNum).forEachOrdered(j -> {
Cell cell = row.createCell(j);
cell.setCellStyle(sourceRow.getCell(j).getCellStyle());
});
});
}
/**
* 从工作表中删除指定的行此方法修复sheet.shiftRows删除行时会拆分合并的单元格的问题
*
* @param row 需要删除的行
* @see <a href="https://bz.apache.org/bugzilla/show_bug.cgi?id=56454">sheet.shiftRows的bug</a>
* @since 5.4.2
*/
public static void removeRow(Row row) {
if (row == null) {
return;
}
int rowIndex = row.getRowNum();
Sheet sheet = row.getSheet();
int lastRow = sheet.getLastRowNum();
if (rowIndex >= 0 && rowIndex < lastRow) {
List<CellRangeAddress> updateMergedRegions = new ArrayList<>();
// 找出需要调整的合并单元格
IntStream.range(0, sheet.getNumMergedRegions())
.forEach(i -> {
CellRangeAddress mr = sheet.getMergedRegion(i);
if (!mr.containsRow(rowIndex)) {
return;
}
// 缩减以后变成单个单元格则删除合并单元格
if (mr.getFirstRow() == mr.getLastRow() - 1 && mr.getFirstColumn() == mr.getLastColumn()) {
return;
}
updateMergedRegions.add(mr);
});
// 将行上移
sheet.shiftRows(rowIndex + 1, lastRow, -1);
// 找出删除行所在的合并单元格
List<Integer> removeMergedRegions = IntStream.range(0, sheet.getNumMergedRegions())
.filter(i -> updateMergedRegions.stream().
anyMatch(umr -> CellRangeUtil.contains(umr, sheet.getMergedRegion(i))))
.boxed()
.collect(Collectors.toList());
sheet.removeMergedRegions(removeMergedRegions);
updateMergedRegions.forEach(mr -> {
mr.setLastRow(mr.getLastRow() - 1);
sheet.addMergedRegion(mr);
});
sheet.validateMergedRegions();
}
if (rowIndex == lastRow) {
Row removingRow = sheet.getRow(rowIndex);
if (removingRow != null) {
sheet.removeRow(removingRow);
}
}
}
}

View File

@ -8,7 +8,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.4.1-SNAPSHOT</version>
<version>5.4.2-SNAPSHOT</version>
</parent>
<artifactId>hutool-script</artifactId>

View File

@ -9,7 +9,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.4.1-SNAPSHOT</version>
<version>5.4.2-SNAPSHOT</version>
</parent>
<artifactId>hutool-setting</artifactId>

View File

@ -9,7 +9,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.4.1-SNAPSHOT</version>
<version>5.4.2-SNAPSHOT</version>
</parent>
<artifactId>hutool-socket</artifactId>

View File

@ -9,7 +9,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.4.1-SNAPSHOT</version>
<version>5.4.2-SNAPSHOT</version>
</parent>
<artifactId>hutool-system</artifactId>

View File

@ -8,7 +8,7 @@
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.4.1-SNAPSHOT</version>
<version>5.4.2-SNAPSHOT</version>
<name>hutool</name>
<description>Hutool是一个小而全的Java工具类库通过静态方法封装降低相关API的学习成本提高工作效率使Java拥有函数式语言般的优雅让Java语言也可以“甜甜的”。</description>
<url>https://github.com/looly/hutool</url>