diff --git a/.gitee/PULL_REQUEST_TEMPLATE.zh-CN.md b/.gitee/PULL_REQUEST_TEMPLATE.zh-CN.md index 6a5dae969987ce553cff93b556dd1953d18bfa95..2cb5ddcbdf7296180bd46bfa94af7b5ae821f8d3 100644 --- a/.gitee/PULL_REQUEST_TEMPLATE.zh-CN.md +++ b/.gitee/PULL_REQUEST_TEMPLATE.zh-CN.md @@ -7,4 +7,13 @@ ### 修改描述(包括说明bug修复或者添加新特性) 1. [bug修复] balabala…… -2. [新特性] balabala…… \ No newline at end of file +2. [新特性] balabala…… + +### 提交前自测 +> 请在提交前自测确保代码没有问题,提交新代码应包含:测试用例、通过(mvn javadoc:javadoc)检验详细注释。 + +1. 本地如有多个JDK版本,可以设置临时JDk版本,如:`export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_331.jdk/Contents/Home`,具体替换为本地jdk目录 +2. 确保本地测试使用JDK8最新版本,`echo $JAVA_HOME`、`mvn -v`、`java -version`均正确。 +3. 执行打包生成文档,使用`mvn clean package -Dmaven.test.skip=true -U`,并确认通过,会自动执行打包、生成文档 +4. 如需要单独执行文档生成,执行:`mvn javadoc:javadoc `,并确认通过 +5. 如需要单独执行测试用例,执行:`mvn clean test`,并确认通过 diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 7c3886b87e271b59a40e4983db434a4ee6898fd1..ac5917c2f568c9b0748845e852a1ac8302811c67 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,4 +1,4 @@ # These are supported funding model platforms github: [looly] -custom: ['https://gitee.com/dromara/hutool', 'https://dromara.gitee.io/donate.html'] +custom: ['https://gitee.com/chinabugotech/hutool'] diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 6a5dae969987ce553cff93b556dd1953d18bfa95..6f6bbf520596595d5921a27ab8cc09f26ba0fbe7 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -7,4 +7,13 @@ ### 修改描述(包括说明bug修复或者添加新特性) 1. [bug修复] balabala…… -2. [新特性] balabala…… \ No newline at end of file +2. [新特性] balabala…… + +### 提交前自测 +> 请在提交前自测确保代码没有问题,提交新代码应包含:测试用例、通过(mvn javadoc:javadoc)检验详细注释。 + +1. 本地如有多个JDK版本,可以设置临时JDk版本,如:`export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_331.jdk/Contents/Home`,具体替换为本地jdk目录 +2. 确保本地测试使用JDK8最新版本,`echo $JAVA_HOME`、`mvn -v`、`java -version`均正确。 +3. 执行打包生成文档,使用`mvn clean package -Dmaven.test.skip=true -U`,并确认通过,会自动执行打包、生成文档 +4. 如需要单独执行文档生成,执行:`mvn javadoc:javadoc `,并确认通过 +5. 如需要单独执行测试用例,执行:`mvn clean test`,并确认通过 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 14c1f65d87d37a89f7dc524084d82cfbd7f52c6b..4f840970e1eae1ba8c53505aab609d2fe4810de3 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,13 +2,1061 @@ # 🚀Changelog ------------------------------------------------------------------------------------------------------------- +# 5.8.41(2025-10-12) -# 5.8.0.M5 (2022-04-27) +### 🐣新特性 +* 【core 】 增加`WeakKeyValueConcurrentMap`及其关联类,同时废弃`WeakConcurrentMap`并替换(issue#4039@Github) +* 【core 】 `MapUtil`增加`removeByValue`和`removeIf`方法 +* 【core 】 `ObjectUtil`增加`apply`方法 +* 【core 】 `ReferenceUtil`增加`get`方法 +* 【db 】 `Condition`增加构造方法支持BETWEEN(issue#4041@Github) +* 【core 】 `IoUtil.writeObjects`判空避免空指针(issue#4049@Github) +* 【extra 】 `OsInfo`增加`isWindows11`方法(pr#4054@Github) +* 【extra 】 `RedisDS`增加`getPool`和`getSetting`方法(issue#ICVWDI@Gitee) +* 【core 】 `NumberUtil.pow`增加重载,支持指数自定义保留位数(pr#4052@Github) +* 【core 】 `NumberUtil.isPrimes`优化判断(pr#4058@Github) +* 【extra 】 `Mail.buildContent`改进,正文部分总在最前(issue#4072@Github) +* 【core 】 `DataSizeUtil`改进,兼容`GiB`等单位名称(issue#ICXXVF@Github) +* 【ai 】 `Message`增加setter和构造方法(issue#ICXTP2@Gitee) +* 【extra 】 `PinyinUtil`增加判空(pr#4081@Github) +* 【core 】 `LocalDateTimeUtil.parseDate`注释修正(pr#4085@Github) +* 【core 】 `StrUtil`增加null检查处理(pr#4086@Github) +* 【json 】 增加Record支持(pr#4096@Github) +* 【crypto 】 增加`SpecUtil`,`KeyUtil`增加`generateRSAPrivateKey`重载,(issue#ID1EIK@Gitee) +* 【core 】 `RandomUtil`增加`randomStringLower`方法 + +### 🐞Bug修复 +* 【core 】 修复`ReflectUtil`中因class和Method关联导致的缓存无法回收问题(issue#4039@Github) +* 【db 】 修复`Condition`的`Condition("discount_end_time", "!=", (String) null)`方法生成SQL时,生成SQL不符合预期要求的错误(pr#4042@Github) +* 【core 】 修复`IoUtil`的`closeIfPosible`拼写错误,新建一个`closeIfPossible`方法,原方法标记deprecated(issue#4047@Github) +* 【http 】 修复`HttpRequest.sendRedirectIfPossible`未对308做判断问题。(issue#4053@Github) +* 【cron 】 修复`CronPatternUtil.nextDateAfter`当日为L时计算错误问题。(issue#4056@Github) +* 【db 】 修复`NamedSql.replaceVar`关键字处理问题(issue#4062@Github) +* 【db 】 修复`DialectRunner.count`方法中,去除包含多字段order by子句的SQL语句时错误问题(issue#4066@Github) +* 【extra 】 修复`JschSessionPool`并发问题(pr#4079@Github) +* 【extra 】 修复`Sftp`递归删除目录时使用相对路径可能导致死循环的问题(pr#1380@Gitee) +* 【db 】 修复`SqlUtil.removeOuterOrderBy`处理没有order by的语句导致异常问题(pr#4089@Github) +* 【extra 】 修复`Sftp.upload`目标路径为null时空指针问题(issue#ID14WX@Gitee) +* 【ai 】 修复`AIConfigBuilder`中方法名拼写错误(pr#1382@Gitee) +* 【core 】 修复`StrBuilder`charAt越界判断错误(pr#4094@Github) +* 【dfa 】 修复`WordTree.addWord`末尾为特殊字符导致的无法匹配问题(pr#4092@Github) +* 【core 】 修复`ServiceLoaderUtil.loadFirstAvailable`在JDK24+后未捕获异常导致的报错问题(pr#4098@Github) +* 【cron 】 修复`CronTimer`在任务非常多时,追赶系统时间导致遗漏任务的问题(issue#IB49EF@Gitee) + +------------------------------------------------------------------------------------------------------------- +# 5.8.40(2025-08-26) + +### 🐣新特性 +* 【captcha】 `MathGenerator`四则运算方式支持不生成负数结果(pr#1363@Gitee) +* 【core 】 增加`MapValueProvider`和`RecordConverter`并支持Record转换(issue#3985@Github) +* 【core 】 `CalendarUtil`增加`isSameYear`和`calendar`方法(issue#3995@Github) +* 【core 】 `DateUtil`增加`yyyy-MM-dd'T'HH:mmXXX`格式支持(pr#1367@Gitee) +* 【core 】 `MapUtil`增加flatten方法(pr#1368@Gitee) +* 【extra 】 `getClientIP`优先获取传入的请求头信息(pr#1373@Gitee) +* 【db 】 增加`Gbase8s`驱动支持(issue#ICSFAM@Gitee) +* 【db 】 增加TDSQL PostgreSQL版本、TDSQL-H LibraDB、Snowflake、Teradata 的驱动支持(pr#4024@Github) +* 【core 】 `EnumUtil`增加缓存支持(pr#1376@Gitee) + +### 🐞Bug修复 +* 【extra 】 `Sftp``reconnectIfTimeout`方法改为捕获所有异常(issue#3989@Github) +* 【core 】 修复`ChineseDate `闰年闰月节日获取问题(issue#ICL1BT@Gitee) +* 【core 】 修复`TreeBuilder`append重复向idTreeMap中put问题(pr#3992@Github) +* 【extra 】 修复`QLExpressEngine`allowClassSet无效问题(issue#3994@Github) +* 【core 】 修复`StrBuilder`insert插入计算错误问题(issue#ICTSRZ@Gitee) +* 【cron 】 修复`CronPatternUtil.nextDateAfter`计算下一个匹配表达式的日期时,计算错误问题(issue#4006@Github) +* 【cache 】 `ReentrantCache`修改get逻辑key锁改为全局锁,保证安全(issue#4022@Github) +* 【core 】 修复`NumberWordFormatter`formatSimple输出错误问题(pr#4034@Github) + +------------------------------------------------------------------------------------------------------------- +# 5.8.39(2025-06-20) + +### 🐣新特性 +* 【ai 】 增加SSE流式返回函数参数callback,增加超时时间配置,豆包、grok新增文生图接口,豆包生成视频支持使用model,新增HutoolAI平台 +* 【core 】 DesensitizedUtil新增护照号码脱敏功能(pr#1347@Gitee) +* 【core 】 优化XXXToMapCopier的部分性能(pr#1345@Gitee) +* 【http 】 `HttpConfig`增加参数`setIgnoreContentLength`可选忽略读取响应contentLength头(issue#ICB1B8@Gitee) +* 【core 】 `Assert`新增断言给定集合为空的方法以及单元测试用例(pr#3952@Github) +* 【db 】 Db添加FetchSize的全局设置(pr#3978@Github) +* 【core 】 增加可召回批处理线程池执行器`RecyclableBatchThreadPoolExecutor`(pr#1343@Gitee) +* +### 🐞Bug修复 +* 【core 】 修复`NumberUtil`isNumber方法以L结尾没有小数点判断问题(issue#3938@Github) +* 【core 】 修复`CharsequenceUtil`toLowerCase方法拼写错误(issue#3941@Github) +* 【core 】 修复`UUID`equals的问题,改为final类(issue#3948@Github) +* 【core 】 修复`Money`中金额分配的问题bug(issue#IC9Y35@Gitee) +* 【poi 】 修复`ExcelPicUtil`中可能的空指针异常 +* 【core 】 修复`LunarFestival`中重复节日问题(issue#ICC8X3@Gitee) +* 【core 】 修复`ThreadUtil`中中断异常处理丢失中断信息的问题,解决ConcurrencyTester资源未释放的问题(pr#1358@Gitee) +* 【core 】 修复`TEL_400_800`正则规则太窄问题(issue#3967@Github) +* 【core 】 修复`ClassUti`isNormalClass判断未排除String问题(issue#3965@Github) +* 【core 】 修复`ZipUtil`中zlib和unZlib调用后资源未释放问题(issue#3976@Github) +* 【core 】 修复`Money`类的setAmount方法没有获取当前币种的小数位数而是使用的默认小数位和在遇到非2小数位的币种(如日元使用 0 位)会导致金额设置错误问题(pr#3970@Github) +* 【cache 】 修复`AbstractCache`putWithoutLock方法可能导致的外部资源泄露问题(pr#3958@Github) + +------------------------------------------------------------------------------------------------------------- +# 5.8.38(2025-05-13) + +### 🐣新特性 +* 【core 】 `PathUtil#del`增加null检查(pr#1331@Gitee) +* 【db 】 增加SAP HANA识别及方言(pr#3914@Github) +* 【crypto 】 增加`Argon2`类,实现Argon2算法(issue#3890@Github) +* 【core 】 `CharSequenceUtil`增加toLoweCase和toUpperCase方法(issue#IC0H2B@Gitee) +* 【core 】 增加分段锁实现`SegmentLock`(pr#1330@Gitee) +* 【core 】 重载subtractToList方法,提供isLinked选项(pr#3923@Github) +* 【extra 】 `TemplateConfig`增加`setUseCache`方法(issue#IC3JRY@Gitee) +* 【extra 】 `AbstractFtp`增加`rename`方法(issue#IC3PMI@Gitee) +* 【core 】 优化`PropDesc`缓存注解判断,提升性能(pr#1335@Gitee) +* 【core 】 添加`RecordUtil`支持record类(issue#3931@Github) +* 【core 】 `Dict`的customKey方法访问权限修改为protected(pr#1340@Gitee) +* 【ai 】 增加hutool-ai模块,对AI大模型的封装实现(pr#3937@Github) + +### 🐞Bug修复 +* 【setting】 修复`Setting`autoLoad可能的加载为空的问题(issue#3919@Github) +* 【db 】 修复某些数据库的getParameterMetaData会返回NULL,导致空指针的问题。(pr#3936@Github) +* 【extra 】 修正`SshjSftp`在SftpSubsystem服务时报错问题(pr#1338@Gitee) + +------------------------------------------------------------------------------------------------------------- +# 5.8.37(2025-03-31) + +### 🐣新特性 +* 【json 】 ObjectMapper删除重复trim(pr#3859@Github) +* 【core 】 `FileWriter`增加方法,可选是否追加换行符(issue#3858@Github) +* 【core 】 `IdcardUtil`验证10位身份证兼容中英文括号(issue#IBP6T1@Gitee) +* 【extra 】 `PinyinUtil`增加重载可选是否返回声调(pr#3875@Github) +* 【extra 】 `PinyinEngine`增加重载可选是否返回声调(pr#3883@Github) +* 【core 】 增加`VersionUtil`版本比较工具(pr#3876@Github) +* 【db 】 增加GoldenDB识别(pr#3886@Github) +* 【http 】 改进`UrlQuery`对无参URL增加判断识别(issue#IBRVE4@Gitee) +* 【core 】 改进`PropDesc`中去除Transient引用避免NoClassDefFoundError(issue#3901@Github) +* 【core 】 `StrUtil.isBlank`增加`\u200c`判断(issue#3903@Github) +* 【core 】 优化`CombinationAnnotationElement`注解数组性能(pr#1323@Gitee) +* 【core 】 完善季度相关 API(pr#1324@Gitee) + +### 🐞Bug修复 +* 【setting】 修复`SettingLoader`load未抛出异常导致配置文件无法正常遍历的问题(pr#3868@Github) +* 【cache 】 修复`ReentrantCache#getOrRemoveExpired`方法丢失onRemove触发问题(pr#1315@Gitee) +* 【json 】 修复`JsonUtil.toBean`泛型数组类型丢失问题(pr#3876@Github) +* 【http 】 修复`HttpUtil.normalizeParams`规则问题(issue#IBQIYQ@Gitee) +* 【http 】 修复`NumberChineseFormatter.format`中自定义单位在0时错误问题(issue#3888@Github) + +------------------------------------------------------------------------------------------------------------- +# 5.8.36(2025-02-18) + +### 🐣新特性 +* 【crypto 】 增加BCUtil.decodeECPrivateKey方法(issue#3829@Github) +* 【core 】 增加HtmlUtil.cleanEmptyTag方法(pr#3838@Github) +* 【db 】 GlobalDbSetting优化默认配置读取规则,优先读取文件而非jar中的文件(issue#900@Github) +* 【dfa 】 删除StopChar类中存在重复字符(pr#3841@Github) +* 【http 】 支持鸿蒙设备 UA 解析(pr#1301@Gitee) + +### 🐞Bug修复 +* 【aop 】 修复ProxyUtil可能的空指针问题(issue#IBF20Z@Gitee) +* 【core 】 修复XmlUtil转义调用方法错误问题,修复XmlEscape未转义单引号问题(pr#3837@Github) +* 【core 】 修复FileUtil.isAbsolutePath没有判断smb路径问题(pr#1299@Gitee) +* 【core 】 修复AbstractFilter没有检查参数长度问题(issue#3854@Github) + +------------------------------------------------------------------------------------------------------------- +# 5.8.35(2024-12-25) + +### 🐣新特性 +* 【poi 】 优化ExcelWriter中使用比较器writer的方法,只对第一条数据进行排序(pr#3807@Github) +* 【extra 】 优化Ftp.download,返回false抛出异常(issue#3805@Github) +* 【core 】 优化MAC地址正则(issue#IB95X4@Gitee) +* 【json 】 JSON的getByPath方法新增更为通用的指定出参类型重载(pr#3814@Github) +* 【core 】 DateUtil.parseUTC方法标记废弃,改名为parseISO8601(issue#IBB6I5@Gitee) +* 【core 】 添加EnumUtil#getBy(Class, Func1, Object)方法(pr#1283@Gitee) +* 【db 】 添加Entity.addCondition方法(issue#IBCDL2@Gitee) +* 【poi 】 添加StopReadException,定义sax读取时用户可手动终止(issue#3820@Github) + +### 🐞Bug修复 +* 【crypto 】 修复JWTSignerUtil.createSigner中algorithmId未转换问题(issue#3806@Github) +* 【core 】 修复DateUtil.rangeContains未重置问题(issue#IB8OFS@Gitee) +* 【cache 】 修复StampedCache类get方法并发问题(issue#IBCIQG@Gitee) +* 【cache 】 修复FIFOCache类使用StampedCache导致并发读的并发问题(issue#IBCIQG@Gitee) +* 【cache 】 废弃StampedCache,可能造成Map循环调用导致死锁(issue#IBDGBZ@Gitee) + +------------------------------------------------------------------------------------------------------------- +# 5.8.34(2024-11-25) + +### 🐣新特性 +* 【http 】 增加Windows微信浏览器识别(issue#IB3SJF@Gitee) +* 【core 】 ZipUtil.unzip增加编码容错(issue#I3UZ28@Gitee) +* 【core 】 Calculator兼容`x`字符作为乘号(issue#3787@Github) +* 【poi 】 Excel07SaxReader中,对于小数类型,增加精度判断(issue#IB0EJ9@Gitee) +* 【extra 】 SpringUtil增加getBean重载(issue#3779@Github) +* 【core 】 DataSizeUtil 新增format方法(issue#IB6UUX@Gitee) + +### 🐞Bug修复 +* 【core 】 修复DateUtil.rangeToList中step小于等于0时无限循环问题(issue#3783@Github) +* 【cron 】 修复cron模块依赖log模块问题 +* 【extra 】 修复MailUtil发送html格式邮件无法正常展示图片问题(pr#1279@Gitee) +* 【core 】 【可能的向下兼容问题】修复双引号转义符转义错误问题,修改规则后,对非闭合双引号字段的策略变更,如"aa,则被识别为aa(issue#IB5UQ8@Gitee) +* 【extra 】 修复Sftp中传入Session重连时逻辑错误问题(issue#IB69U8@Gitee) +* 【json 】 修复JSONUtil.toBean()中将JSON数组字符串转Map对象返回错误问题(issue#3795@Github) + +------------------------------------------------------------------------------------------------------------- +# 5.8.33(2024-11-05) + +### 🐣新特性 +* 【core 】 SyncFinisher增加setExecutorService方法(issue#IANKQ1@Gitee) +* 【http 】 HttpConfig增加`setUseDefaultContentTypeIfNull`方法(issue#3719@Github) +* 【core 】 用ArrayList重新实现权重随机类:WeightListRandom(pr#3720@Github) +* 【crypto 】 SM2解密时,兼容GmSSL非压缩省略的04头的密文(issue#IAP1QJ@Gitee) +* 【core 】 兼容NumberUtil.add方法传入整型自动类型转换为浮点类型的精度丢失问题(pr#3721@Github) +* 【core 】 ModifierUtil明确注释,并增加hasAllModifiers方法(issue#IAQ2U0@Gitee) +* 【http 】 HttpRequest增加setFixedContentLength方法(issue#3462@Github) +* 【db 】 AbstractDb增加getDs方法(issue#IARKZL@Gitee) +* 【db 】 QrCodeUtil添加二维码logo支持配置圆角(pr#3747@Github) +* 【core 】 TreeUtil.buildSingle指定rootId节点存在时,作为根节点(issue#IAUSHR@Gitee) +* 【core 】 EscapeUtil.escapeHtml4增加空处理(issue#IAZMYU@Gitee) +* 【core 】 PropDesc.isTransientForGet使用className,避免Android下类找不到问题(issue#IB0JP5@Gitee) +* 【core 】 优化NumberUtil.count(pr#3772@Github) +* 【crypto 】 SM2.signHex改名为signHexFromHex,原名标记废弃,避免歧义(issue#IB0NVY@Gitee) +* 【all 】 优化所调用的ObjectUtil#defaultIfNull避免重复创建(pr#1274@Gitee) +* 【core 】 NetUtil.bigIntegerToIPv6增加长度修正(issue#IB27HV@Gitee) + +### 🐞Bug修复 +* 【json 】 修复JSONConfig.setDateFormat设置后toBean无效问题(issue#3713@Github) +* 【core 】 修复RegexPool.CHINESE_NAME范围太大的问题(issue#IAOGDR@Gitee) +* 【http 】 修复重定向没有按照RFC7231规范跳转的问题,修改为除了307外重定向使用GET方式(issue#3722@Github) +* 【core 】 修复ArrayUtil.lastIndexOfSub死循环问题(issue#IAQ16E@Gitee) +* 【core 】 修复ImgUtil.write写出临时文件未清理问题(issue#IAPZG7@Gitee) +* 【json 】 修复ignoreNullValue在JSONArray中无效问题(issue#3759@Github) + +------------------------------------------------------------------------------------------------------------- +**# 5.8.32(2024-08-30) + +### 🐣新特性 +* 【core 】 FileUtil.getTotalLines()支持CR换行符(issue#IAMZYR@Gitee) +* 【json 】 GlobalSerializeMapping增加null检查(issue#IANH1Y@Gitee) + +### 🐞Bug修复 +* 【http 】 修复getFileNameFromDisposition不符合规范问题(issue#IAKBPD@Gitee) +* 【crypto 】 修复SymmetricCrypto.setParams和setRandom没有加锁问题(issue#IAJIY3@Gitee) +* 【crypto 】 修复ZipUtil压缩成流的方法检查文件时报错问题(issue#3697@Github) +* 【core 】 修复CopyOptions.setFieldValueEditor后生成null值setIgnoreNullValue无效问题(issue#3702@Github) +* 【json 】 修复JSONConfig.setDateFormat设置后setWriteLongAsString失效问题(issue#IALQ0N@Gitee) +* 【core 】 修复Tree.cloneTree的Parent节点引用错误问题(issue#IANJTC@Gitee) + +-------------------------------------------------------------------------------------------------------------** +# 5.8.31(2024-08-12) + +### 🐣新特性 +* 【core 】 TreeUtil增加build方法,可以构建Bean的树结构(pr#3692@Github) + +### 🐞Bug修复 +* 【extra 】 修复JakartaMailUtil引用javax的问题 +* 【core 】 修复GraphicsUtil.drawString方法签名变化导致的问题(issue#3694@Github) + +------------------------------------------------------------------------------------------------------------- +# 5.8.30(2024-08-09) + +### 🐣新特性 +* 【core 】 Converter转换规则变更,空对象、空值转为Bean时,创建默认对象,而非null(issue#3649@Github) +* 【core 】 UrlQuery增加remove方法 +* 【extra 】 增加JakartaMailUtil,支持新包名的mail +* 【core 】 CharSequenceUtil增加removeAllPrefix和removeAllSuffix方法(pr#3655@Github) +* 【core 】 CharSequenceUtil增加stripAll方法(pr#3659@Github) +* 【crypto 】 支持"RSA/ECB/OAEPWithSHA-1AndMGF1Padding"的RSA加解密(pr#3675@Github) +* 【core 】 Opt增加ifFail(pr#1239@Gitee) +* 【poi 】 增加GlobalPoiConfig(issue#IAEHJH@Gitee) +* 【core 】 优化IndexedComparator性能(pr#1240@Gitee) +* 【http 】 改进ContentType.get忽略空格(pr#3664@Github) +* 【http 】 CompressUtil.createExtractor支持tgz自动识别(pr#3674@Github) +* 【poi 】 ExcelWriter.autoSizeColumn增加可选widthRatio参数,可配置中文字符宽度倍数(pr#3689@Github) +* 【mail 】 MailAccount增加自定义参数支持(issue#3687@Github) +* 【mail 】 增加文字颜色与背景颜色色差设置(pr#1252@gitee) +* 【mail 】 XmlUtil增加xmlToBean重载,支持CopyOptions参数(issue#IAISBB@gitee) +* 【core 】 增加默认色差方法(pr#1257@gitee) +* 【all 】 单元测试由Junit4变更为Junit5 + +### 🐞Bug修复 +* 【core 】 修复因RFC3986理解有误导致的UrlPath处理冒号转义问题(issue#IAAE88@Gitee) +* 【core 】 修复FileUtil.cleanEmpty无法正确清空递归空目录问题(pr#1233@Gitee) +* 【core 】 修复BeanUtil.copyProperties中mapToMap时key被转为String问题(issue#3645@Github) +* 【core 】 修复FileUtil.file末尾换行符导致路径判断错误的问题(issue#IAB65V@Gitee) +* 【core 】 修复FileTypeUtil.getType空指针问题(issue#IAD5JM@Gitee) +* 【core 】 修复IdcardUtil.isValidHKCard校验问题(issue#IAFOLI@Gitee) +* 【core 】 修复Convert.digitToChinese(0)输出金额无`元整问题`(issue#3662@Github) +* 【core 】 修复CsvParser中对正文中双引号处理逻辑问题(pr#1244@Gitee) +* 【core 】 修复ZipUtil.zip压缩到本目录时可能造成的死循环问题(issue#IAGYDG@Gitee) +* 【cache 】 修复AbstractCache.get中锁不一致导致的并发问题(issue#3686@Github) +* 【cron 】 修复CronPatternUtil.nextDateAfter栈溢出问题(issue#3685@Github) + +------------------------------------------------------------------------------------------------------------- +# 5.8.29(2024-07-03) + +### 🐣新特性 +* 【core 】 DateUtil增加offsetYear方法 +* 【core 】 ListUtil增加move方法(issue#3603@Github) +* 【core 】 CollUtil.subtract增加空判定(issue#3605@Github) +* 【core 】 优化DateUtil.format(Date date, String format)接口效率(pr#1226@Gitee) +* 【csv 】 CsvWriter.writeBeans增加重载,可选是否写出表头(issue#IA57W2@Gitee) +* 【core 】 BetweenFormatter支持自定义设置单位(pr#1228@Gitee) +* 【cache 】 Cache.put变更策略,对于替换的键值对,不清理队列(issue#3618@Github) +* 【core 】 添加 Windows 资源管理器风格字符串比较器(pr#3620@Github) +* 【core 】 Week.of支持中文名称(issue#3637@Github) +* 【core 】 ThreadUtil.newExecutor等方法变更方法签名,返回值变更为ThreadPoolExecutor(pr#1230@Gitee) + +### 🐞Bug修复 +* 【core 】 修复AnnotationUtil可能的空指针错误 +* 【core 】 修复BeanUtil.isBean判断Dict错误问题(issue#I9VTZG@Gitee) +* 【core 】 修复VersionComparator传入空字符串报错问题(pr#3614@Github) +* 【core 】 修复CaseInsensitiveLinkedMap顺序错误问题(issue#IA4K4F@Gitee) +* 【core 】 修复DateUtil.offset空指针问题(issue#3617@Github) +* 【core 】 修复PathMover.moveContent问题(issue#IA5Q8D@Gitee) +* 【db 】 修复PooledConnection可能的数据库驱动未找到问题(issue#IA6EUQ@Gitee) +* 【http 】 修复Mac下的微信浏览器被识别为移动端问题(issue#IA74K2@Gitee) +* 【core 】 修复Tailer指定初始读取行数的计算错误问题(issue#IA77ML@Gitee) +* 【http 】 修复getFileNameFromDisposition获取头错误问题(issue#3632@Github) +* 【core 】 修复\n#出现在双引号中解析错误问题(issue#IA8WE0@Gitee) +* 【core 】 修复FastDatePrinter处理YY错误问题(issue#3641@Github) + +------------------------------------------------------------------------------------------------------------- +# 5.8.28(2024-05-29) + +### 🐣新特性 +* 【core 】 修正XmlUtil的omitXmlDeclaration描述注释(issue#I9CPC7@Gitee) +* 【core 】 StrUtil增加toStringOrEmpty方法(issue#I9CPC7@Gitee) +* 【extra 】 设置jsch登录认证方式,跳过Kerberos身份验证(pr#3530@Github) +* 【extra 】 增加设置验证码大小和针对alias注释(pr#3533@Github) +* 【json 】 JSONConfig增加setWriteLongAsString可选是否将Long写出为String类型(issue#3541@Github) +* 【cache 】 CacheUtil.newTimedCache增加有schedulePruneDelay参数的重载方法(issue#I9HO25@Gitee) +* 【core 】 NumberChineseFormatter提供阿拉伯转中文支持多位小数的方法(pr#3552@Github) +* 【captcha】 Captcha.setBackground为null时背景透明(issue#3558@Github) +* 【captcha】 HttpDownloader.downloadBytes增加超时参数重载(issue#3556@Github) +* 【http 】 增加ExceptionFilter和DefaultExceptionFilter支持异常处理(issue#3568@Github) +* 【poi 】 增加ExcelWriter.addIgnoredErrors,支持忽略警告小标 +* 【core 】 PropertyComparator增加compareSelf构造重载(issue#3569@Github) +* 【db 】 增加OceanBase的driver推断(pr#1217@Gitee) +* 【http 】 HttpRequest#get不再尝试File路径(issue#I9O6DA@Gitee) +* 【core 】 增加IdConstants,提高Snowflake初始化性能(issue#3581@Github) +* 【core 】 优化 CharSequenceUtil工具类 startWithAny()、startWithAnyIgnoreCase() 参数命名错误问题(pr#1219@Gitee) +* 【core 】 ListUtil.setOrPadding增加重载,可选限制index大小(issue#3586@Github) +* 【http 】 getFileNameFromDisposition更加规范,从多个头的值中获取,且`filename*`优先级更高(pr#3590@Gitee) +* 【core 】 CsvWriter增加重载writeBeans方法,支持可选bean字段(pr#1222@Gitee) +* 【core 】 LocalDateTimeUtil增加beginOfDay和endOfDay重载(issue#3594@Github) +* 【core 】 NumberUtil.pow支持负数(issue#3598@Github) + +### 🐞Bug修复 +* 【http 】 修复HttpUtil.urlWithFormUrlEncoded方法重复编码问题(issue#3536@Github) +* 【core 】 修复FileMagicNumber.getMagicNumber空指针问题(issue#I9FE8B@Gitee) +* 【extra 】 修复CompressUtil工具多出\问题(issue#I71K5V@Gitee) +* 【db 】 解决oracle情况下setObject(inputStream)报错问题,java.sql.SQLException: 无效的列类型问题(pr#1207@Gitee) +* 【core 】 解决CalendarUtil.isSameDay时区不同导致结果错误问题(pr#3548@Github) +* 【core 】 修复RandomUtil.randomStringWithoutStr方法问题(pr#1209@Gitee) +* 【http 】 修复HttpRequest.header相同key被覆盖问题(issue#I9I61C@Gitee) +* 【core 】 修复TemporalAccessorConverter自定义格式转换问题(issue#I9HQQE@Gitee) +* 【cron 】 修复CronPattern.nextMatchAfter匹配初始值问题(issue#I9FQUA@Gitee) +* 【core 】 修复FileUtil.copyFile没有创建父目录导致的问题(issue#3557@Github) +* 【http 】 修复HttpDownloader全局超时无效问题(issue#3556@Github) +* 【core 】 修复ZipReader.checkZipBomb遇到空目录报错问题(issue#I9K494@Gitee) +* 【db 】 修复Oracle下特殊表名导致meta信息获取不到问题(issue#I9BANE@Gitee) +* 【db 】 修复FuncComparator.thenComparing不生效问题(issue#3569@Github) +* 【core 】 修复EnumUtil空指针问题(issue#I9NSZ4@Gitee) +* 【core 】 修复NumberWordFormatter.format小数问题(issue#3579@Github) +* 【db 】 修复JndiDSFactory空指针问题 +* 【core 】 修复BiMap.put错误的返回值(pr#1218@Gitee) +* 【core 】 修复BooleanUtil.andOfWrap针对null错误问题(issue#3587@Github) +* 【core 】 修复FileUtil#getTotalLines在JDK9+结果错误问题(issue#3591@Github) + +------------------------------------------------------------------------------------------------------------- +# 5.8.27(2024-03-29) + +### 🐣新特性 +* 【extra 】 FreemarkerEngine修改默认版本参数 +* 【db 】 增加达梦数据库方言(pr#1178@Gitee) +* 【core 】 HexUtil#format方法增加prefix参数(issue#I93PU9@Gitee) +* 【core 】 StrUtil.replace歧义,修改为replaceByCodePoint(issue#I96LWH@Gitee) +* 【core 】 FileUtil和PathUtil增加Resource重载(issue#I97FJT@Gitee) +* 【core 】 优化ThreadUtil.safeSleep,使用System.nanoTime()(issue#I9BMGK@Gitee) +* 【db 】 新增数据库Wrapper支持反解(pr#1192@Gitee) +* 【core 】 新增RFC2822日期格式解析支持(issue#I9C2D4@Gitee) + +### 🐞Bug修复 +* 【core 】 修复PathMover对目标已存在且只读文件报错错误问题(issue#I95CLT@Gitee) +* 【json 】 修复JSONUtil序列化和反序列化预期的结果不一致问题(pr#3507@Github) +* 【http 】 修复CVE-2022-22885,HttpGlobalConfig可选关闭信任host(issue#2042@Github) +* 【core 】 修复DateUtil.betweenYear闰年2月问题(issue#I97U3J@Gitee) +* 【captcha】 修复Graphics2D的资源没释放问题(issue#I98PYN@Gitee) +* 【core 】 修复ClassUtil.getTypeArgument() 获取泛型存在null问题(issue#3516@Github) +* 【core 】 修复图片操作未调用flush导致资源未释放问题(issue#I9C7NA@Gitee) +* 【cron 】 修复cron中在小月时使用“L”的计算问题(pr#1189@Gitee) + +------------------------------------------------------------------------------------------------------------- +# 5.8.26(2024-02-10) + +### 🐣新特性 +* 【db 】 RedisDS增加user支持(issue#I8XEQ4@Gitee) +* 【core 】 MapUtil增加partition方法(pr#1170@Gitee) +* 【core 】 增加Version类(issue#I8Z3VE@Gitee) + +### 🐞Bug修复 +* 【crypto】 修复BouncyCastleProvider导致graalvm应用报错UnsupportedFeatureError(pr#3464@Github) +* 【http 】 修复UserAgentUtil对QQ浏览器识别问题(issue#I8X5XQ@Gitee) +* 【core 】 修复BeanToMapCopier获取类型数组越界问题(issue#3468@Github) +* 【extra 】 修复SshjSftpSession关闭导致的问题(issue#3472@Github) +* 【http 】 修复HtmlUtil.removeHtmlAttr处理空格问题(issue#I8YV0K@Gitee) +* 【core 】 修复CollUtil.containsAll在coll2长度大于coll1时逻辑歧义问题(issue#I8Z2Q4@Gitee) +* 【poi 】 修复当sheetName 不存在时,ExcelUtil.getReader方法不会释放文件问题(issue#I8ZIQC@Gitee) +* 【crypto】 通过添加系统属性hutool.crypto.decodeHex强制关闭hex识别以解决hex和Base64歧义问题(issue#I90M9D@Gitee) +* 【core 】 修复VersionComparator违反传递问题(issue#I8Z3VE@Gitee) + +------------------------------------------------------------------------------------------------------------- +# 5.8.25(2024-01-11) + +### 🐣新特性 +* 【core 】 WatchServer新增通过Path获取WatchKey方法(pr#1145@Gitee) +* 【core 】 CopyOptions中增加setAutoTransCamelCase方法(issue#3452@Github) +* 【captcha】 验证码生成器增加构造方法,可自定义随机数字符集(pr#1147@Gitee) + +### 🐞Bug修复 +* 【core 】 修复StrJoin当append内容后调用length()会出现空指针问题(issue#3444@Github) +* 【core 】 修复PostgreSQL、H2使用upsert字段大小写问题(issue#I8PB4X@Gitee) +* 【core 】 修复RandomUtil.randomInt,RandomUtil.randomLong边界问题(pr#3450@Github) +* 【db 】 修复Druid连接池无法设置部分属性问题(issue#I8STFC@Gitee) +* 【core 】 修复金额转换为英文时缺少 trillion 单位问题(pr#3454@Github) +* 【json 】 增加ParseConfig,通过增加maxNestingDepth参数避免StackOverflowError问题,修复CVE-2022-45688漏洞(issue#2748@Github) +* 【system】 修复UserInfo中用户名加/问题(pr#3458@Github) +* 【core 】 修复NumberUtil.toBigDecimal方法报StackOverflowError(CVE-2023-51080)(issue#3423@Github) + +------------------------------------------------------------------------------------------------------------- +# 5.8.24(2023-12-23) + +### 🐣新特性 +* 【cache 】 Cache增加get重载,可自定义超时时间(issue#I8G0DL@Gitee) +* 【cache 】 JWT#sign增加重载,可选是否增加默认的typ参数(issue#3386@Github) +* 【db 】 增加识别OpenGauss的驱动类(issue#I8K6C0@Gitee) +* 【core 】 修复CharSequenceUtil注释和引用,避免循环引用 +* 【extra 】 SpringUtil增加getProperty重载(pr#1122@Gitee) +* 【core 】 FileTypeUtil增加null判断(issue#3419@Github) +* 【core 】 DateUtil.parse支持毫秒时间戳(issue#I8NMP7@Gitee) +* 【extra 】 优化TokenizerEngine使用IK分词器支持并发(pr#3427@Github) +* 【core 】 Opt.ofEmptyAble支持更多类型(issue#I8OOSY@Gitee) +* 【http 】 HTMLFilter保留p标签(issue#3433@Gitee) + +### 🐞Bug修复 +* 【core 】 修复LocalDateTime#parseDate未判断空问题(issue#I8FN7F@Gitee) +* 【http 】 修复RootAction send404 抛异常问题(pr#1107@Gitee) +* 【extra 】 修复Archiver 最后一个 Entry 为空文件夹时未关闭 Entry问题(pr#1123@Gitee) +* 【core 】 修复ImgUtil.convert png转jpg在jdk9+中失败问题(issue#I8L8UA@Gitee) +* 【cache 】 修复StampedCache的get方法非原子问题(issue#I8MEIX@Gitee) +* 【core 】 修复StrSplitter.splitByRegex使用空参数导致的OOM问题(issue#3421@Github) +* 【db 】 修复嵌套SQL中order by子句错误截断问题(issue#I89RXV@Gitee) +* 【http 】 修复graalvm编译后,未读取Content-Length可能导致的读取时间过长问题(issue#I6Q30X@Gitee) +* 【core 】 修复JavaSourceCompiler.addSource目录处理错误问题(issue#3425@Github) +* 【core 】 修复时间戳转Bean时异常问题(issue#I8NMP7@Gitee) +* 【core 】 修复PostgreSQL使用upsert字段大小写问题(issue#I8PB4X@Gitee) +* 【extra 】 修复TinyPinyinEngine可能的空指针问题(issue#3437@Github) +* 【core 】 修复graalvm原生打包使用http工具被转为file协议问题(issue#I8PY3Y@Gitee) +* 【poi 】 修复cloneSheet参数错误导致非XSSFWorkbook错误命名问题(issue#I8QIBB@Gitee) + +------------------------------------------------------------------------------------------------------------- +# 5.8.23(2023-11-12) + +### 🐣新特性 +* 【json 】 改进TemporalAccessorSerializer支持dayOfMonth和month枚举名(issue#I82AM8@Gitee) +* 【core 】 新增ProxySocketFactory +* 【http 】 UserAgent增加百度浏览器识别(issue#I847JY@Gitee) +* 【core 】 ReflectUtil.getFieldsValue增加Filter重载(pr#1090@Gitee) +* 【core 】 Snowflake增加方法:根据传入时间戳,计算ID起终点(pr#1096@Gitee) +* 【core 】 PathUtil增加loopFiles重载,可选是否追踪软链(issue#3353@Github) + +### 🐞Bug修复 +* 【cron 】 修复Cron表达式range解析错误问题(issue#I82CSH@Gitee) +* 【core 】 修复VersionComparator在极端数据排序时候违反了自反性问题(issue#I81N3H@Gitee) +* 【json 】 修复JSONStrFormatter:format函数对于转义符号处理逻辑错误问题(issue#I84V6I@Gitee) +* 【core 】 修复特定情况下BiMap覆盖Value后,仍能通过旧Value查询到Key问题(issue#I88R5M@Gitee) +* 【core 】 修复aop的afterException无法生效问题(issue#3329@Github) +* 【core 】 修复TypeUtil.getClass方法强转报错问题(pr#1092@Github) +* 【core 】 修复DataSize.parse(size)不支持空格问题(issue#I88Z4Z@Gitee) +* 【http 】 修复SimpleServer在添加的HttpFilter中有获取请求参数时报错问题(issue#3343@Github) +* 【http 】 修复options请求无响应体问题 +* 【core 】 ImgUtil的sliceByRowsAndCols背景无法透明问题(issue#3347@Github) +* 【core 】 修复ClassUtil#scanJar未正确关闭文件问题(issue#3361@Github) +* 【db 】 修复Column.getDigit返回值错误问题(issue#3370@Github) +* 【core 】 修复合成注解在并发环境无法保证正确缓存属性值的问题(pr#1097@Gitee) +* 【core 】 修复CollectorUtil.reduceListMap与collectors.groupby一起使用时出现与预期不符问题(pr#1102@Gitee) + +------------------------------------------------------------------------------------------------------------- +# 5.8.22(2023-09-13) + +### 🐣新特性 +* 【core 】 NumberUtil.nullToZero增加重载(issue#I7PPD2@Gitee) +* 【core 】 DesensitizedUtil增加清空策略(issue#I7PUJ2@Gitee) +* 【all 】 修改异常包装策略:运行时异常不包装,只包装非运行时异常(issue#I7RJZT@Gitee) +* 【core 】 增加IJSONTypeConverter,避免反射调用(pr#1051@Gitee) +* 【http 】 优化HttpUtil.urlWithForm方法(pr#1052@Gitee) +* 【http 】 优化HttpUtil.urlWithForm方法(pr#1052@Gitee) +* 【cron 】 优化PatternParser支持年的步进(issue#I7SMP7@Gitee) +* 【core 】 TreeUtil增加getParentsId方法(issue#I7TDCF@Gitee) + +### 🐞Bug修复 +* 【core 】 修复NumberUtil.toBigDecimal转换科学计数法问题(issue#3241@Github) +* 【core 】 修复PathUtil.moveContent当target不存在时会报错问题(issue#3238@Github) +* 【db 】 修复SqlUtil.formatSql 格式化的sql换行异常(pr#3247@Github) +* 【core 】 修复DateUtil.parse 给定一个时间解析错误问题(issue#I7QI6R@Gitee) +* 【core 】 去除默认的ACCEPT_LANGUAGE(issue#3258@Github) +* 【core 】 修复FieldsComparator比较结果不正确问题(issue#3259@Github) +* 【core 】 修复Db.findAll全局忽略大小写无效问题(issue#I7T30Y@Gitee) +* 【core 】 修复Ipv4Util.getEndIpLong 取反符号导致数据越界(issue#I7U1OQ@Gitee) +* 【http 】 修复302重定向时,Location中的问号被转义问题(issue#3265@Github) +* 【core 】 修复CombinationAnnotationElement判断循环问题(pr#3267@Github) +* 【core 】 修复StrUtil#containsAny NPE问题(pr#1063@Gitee) +* 【all 】 修复SONArray的add()方法抛出OutOfMemory异常问题(issue#3286@Github) +* 【core 】 修复fillColumns空指针问题(issue#3284@Github) +* 【core 】 修复Convert不能转换Optional和Opt问题(issue#I7WJHH@Gitee) +* 【core 】 修复DateUtil.age年龄计算问题(issue#I7XMYW@Gitee) +* 【core 】 修复JSONUtil.parse()溢出问题(issue#3289@Github) +* 【core 】 修复Tailer stop NPE问题(pr#1067@Gitee) +* 【json 】 修复toJSONString导致CPU使用率高的问题(issue#3297@Github) +* 【core 】 修复NumberUtil.parseInt 16进制解析错误的问题(pr#1071@Gitee) +* 【core 】 修复CopyOptions.setIgnoreCase和setIgnoreProperties冲突问题(issue#I80FP4@Gitee) +* 【core 】 修复LocalDateTimeUtil.of 某些特殊TemporalAccessor无法返回正确结果的问题(issue#3301@Github) + +------------------------------------------------------------------------------------------------------------- +# 5.8.21(2023-07-29) + +### 🐣新特性 +* 【core 】 list 为空时,CollUtil.max等返回null而非异常(pr#1027@Gitee) +* 【poi 】 ExcelReader.getWriter逻辑变更,当从非文件读取时,获取sheet,而非空表格。 +* 【core 】 Ipv4Util 新增方法:检测指定 IP 地址是否匹配通配符(pr#3171@Github) +* 【core 】 DateUtil.parse适配6位毫秒格式(issue#I7H34N@Gitee) +* 【core 】 RandomUtil增加可选是否包含边界的重载(issue#3182@Github) +* 【core 】 StrUtil增加truncateByByteLength方法(pr#3176@Github) +* 【core 】 身份证工具类isValidCard18、isValidCard15入参null直接返回null(pr#1034@Gitee) +* 【http 】 使用multiparty方式支持body参数(issue#3158@Github) +* 【core 】 ZipReader增加setMaxSizeDiff方法,自定义或关闭ZipBomb(issue#3018@Github) +* 【db 】 Query.of(entity)构建时传入fields(issue#I7M5JU@Gitee) +* 【db 】 clickhouse驱动名称变更为com.clickhouse.jdbc.ClickHouseDriver(issue#3224@Github) +* 【core 】 UrlResource增加size方法(issue#3226@Github) + +### 🐞Bug修复 +* 【core 】 修复MapUtil工具使用filter方法构造传入参数结果问题(issue#3162@Github) +* 【core 】 修复序列化和反序列化Class问题(issue#I7FQ29@Gitee) +* 【setting】 修复utf8-bom的setting文件读取问题(issue#I7G34E@Gitee) +* 【core 】 修复PathUtil.getMimeType可能造成的异常(issue#3179@Github) +* 【core 】 修复Pair序列化转换无效问题(issue#I7GPGX@Github) +* 【core 】 修复TypeUtil.getTypeArgument对实现接口获取不全面问题(issue#I7CRIW@Gitee) +* 【core 】 修复BeanUtil.isCommonFieldsEqual判空导致的问题 +* 【extra 】 修复CompressUtil.createArchiver 将文件压缩为tgz时文件名规则无效问题(issue#I7LLL7@Gitee) +* 【core 】 修复脱敏银行卡号长度bug(pr#3210@Github) +* 【jwt 】 修复JWTSignerUtil中ES256签名不符合规范问题(issue#3205@Github) +* 【core 】 修复UserInfo获取country问题(issue#I7MCKW@Gitee) +* 【extra 】 修复MVEL加载错误问题(issue#3214@Github) +* 【json 】 修复JSONBeanParser在遇到List时没有被正确递归问题(issue#I7M2GZ@Gitee) +* 【core 】 修复VersionComparator对1.0.3及1.0.2a比较有误的问题(pr#1043@Gitee) +* 【core 】 修复IOS系统下,chrome 浏览器的解析规则有误(pr#1044@Gitee) +* 【extra 】 修复多线程下Sftp中Channel关闭的问题(issue#I7OHIB@Gitee) +* 【extra 】 修复CVE-2023-24163漏洞(issue#I6AJWJ@Gitee) + +------------------------------------------------------------------------------------------------------------- +# 5.8.20(2023-06-16) + +### 🐣新特性 +* 【core 】 UrlQuery增加setStrict方法,区分是否严格模式(issue#I78PB1@Gitee) +* 【poi 】 添加系列方法writeCol,以支持按列输出(pr#1003@Gitee) +* 【core 】 CollUtil新增anyMatch和allMatch方法(pr#1008@Gitee) +* 【core 】 CsvWriter如果开启了append=true,默认自动开启endingLineBreak=true(pr#1010@Gitee) + +### 🐞Bug修复 +* 【core 】 修复TreeUtil.getParentsName()获取到的路径集合中存在值为null的路径名称问题(issue#I795IN@Gitee) +* 【core 】 修复umberUtil.parseNumber对+解析问题(issue#I79VS7@Gitee) +* 【core 】 修复IdcardUtil.getGenderByIdCard存在潜在的异常(pr#1007@Gitee) +* 【core 】 修复Table#contains空指针问题(issue#3135@Gitee) +* 【core 】 修复FileUtil.checkSlip方法缺陷(issue#3140@Github) +* 【extra 】 修复Sftp中exists方法父目录不存在时报错(issue#I7CSQ9@Gitee) +* 【extra 】 修复xml转json再转bean失败问题(issue#3139@Github) +* 【poi 】 修复RowUtil传入参数错误问题(issue#3139@Github) +* 【core 】 修复XmlUtil.xmlToBean空节点转换失败问题(issue#3136@Github) +* 【core 】 修复CVE-2023-3276漏洞,XmlUtil.readBySax问题(issue#I7DX8W@Gitee) + +------------------------------------------------------------------------------------------------------------- +# 5.8.19(2023-05-27) + +### 🐣新特性 +* 【db 】 优化HttpRequest.toString()内容打印(issue#3072@Github) +* 【poi 】 优化Sax方式读取时空白行返回0,修改为返回-1(issue#I6WYF6@Gitee) +* 【db 】 优化count查询兼容informix(issue#I713XQ@Gitee) +* 【core 】 去除Opt头部的GPL协议头(pr#995@Gitee) +* 【core 】 邮箱校验添加对中文的支持(pr#997@Gitee) +* 【core 】 FileUtil.getMimeType增加webp识别(pr#997@Gitee) +* 【core 】 SyncFinisher增加setExceptionHandler方法(issue#I716SX@Gitee) +* 【core 】 FileTypeUtil.getType增加文件判断(pr#3112@Github) +* 【core 】 增加CsvWriteConfig.setEndingLineBreak配置项(issue#I75K5G@Gitee) +* 【core 】 增加Tailer追踪文件时文件被删除的处理情况(pr#3115@Github) +* 【core 】 DelegatedExecutorService构造方法设置成public(issue#I77LUE@Gitee) +* 【core 】 切面代理工具中的cglib支持多参数构造生成(issue#I74EX7@Gitee) +* 【poi 】 添加writeCellValue的重载,以支持isHeader(pr#1002@Gitee) + +### 🐞Bug修复 +* 【core 】 修复URLUtil.decode无法解码UTF-16问题(issue#3063@Github) +* 【db 】 修复insertOrUpdate更新中条件字段没有移除问题(issue#I6W91Z@Gitee) +* 【core 】 修复VIN(车架号)正则问题(pr#3078@Github) +* 【core 】 修复HtmlUtil的removeHtmlAttr方法匹配问题(issue#I6YNTF@Gitee) +* 【core 】 修复JSONUtil.toBean目标存在Map字段无序问题(issue#I6YN2A@Gitee) +* 【http 】 修复HttpDownloader.downloadFile 方法缺少static问题(issue#I6Z8VU@Gitee) +* 【core 】 修复NumberUtil mul 传入null的string入参报错问题(issue#I70JB3@Gitee) +* 【core 】 修复ZipReader.get调用reset异常问题(issue#3099@Github) +* 【core 】 修复FileUtil.createTempFile可能导致的漏洞(issue#3103@Github) +* 【cron 】 修复SystemTimer无法结束进程问题(issue#3090@Github) +* 【core 】 修复BeanUtil.copyToList复制Long等类型错误问题(issue#3091@Github) +* 【poi 】 修复MapRowHandler结果Map无序问题(issue#I71SE8@Github) +* 【db 】 修复SqlExecutor.execute执行ORACLE insert into select报ORA-00933问题(issue#I778U7@Gitee) +* 【db 】 修复AbstractDb#page分页查询异常问题(issue#I73770@Gitee) + +------------------------------------------------------------------------------------------------------------- +# 5.8.18 (2023-04-27) + +### 🐣新特性 +* 【extra 】 JschUtil新增一个重载方法以支持私钥以byte数组形式载入(pr#3057@Github) +* 【crypto】 优化MD5性能(issue#I6ZIQH@Gitee) + +### 🐞Bug修复 +* 【core 】 修复CollUtil.reverseNew针对非可变列表异常(issue#3056@Github) +* 【all 】 修复junit被关联引入的bug(issue#3062@Github) + +------------------------------------------------------------------------------------------------------------- +# 5.8.17 (2023-04-12) + +### 🐣新特性 +* 【core 】 SerializeUtil.deserialize增加白名单类,避免RCE vulnerability(issue#3021@Github) +* 【poi 】 ExcelWriter在关闭后不清空currentRow,以便复用(issue#3025@Github) +* 【core 】 完善HttpStatus,参考相关规范,补全缺失的状态码(pr#968@Gitee) +* 【core 】 NumberUtil增加(pr#968@Gitee) +* 【core 】 Number128增加hash和equals方法(pr#968@Gitee) +* 【core 】 NamingCase.toCamelCase新增重载,可选是否转换其他字符为小写(issue#3031@ithub) +* 【core 】 新增JdkUtil +* 【core 】 DateUtil.getZodiac增加越界检查(issue#3036@Github) +* 【core 】 CsvReader修改策略,添加可选是否关闭Reader重载,默认不关闭Reader(issue#I6UAX1@Gitee) +* 【core 】 isNotEmpty修改规则,避开IDEA错误提示(pr#974@Gitee) + +### 🐞Bug修复 +* 【core 】 CollUtil.split优化切割列表参数判断,避免OOM(pr#3026@Github) +* 【core 】 修复FileUtil.move传入相同目录或子目录丢失源目录的问题(pr#3032@Github) +* 【core 】 修复SafeConcurrentHashMap.computeIfAbsent可能存在的结果为null的情况(issue#I6RVMY@Gitee) +* 【json 】 修复Pair反序列化报错问题(issue#I6SZYB@Gitee) +* 【core 】 修复使用AnnotationUtil.getAnnotationAlias获取注解时可能会出现空指针的问题(pr#975@Gitee) +* 【json 】 修复没有属性的对象转json字符串抛异常问题(issue#3051@Github) + +------------------------------------------------------------------------------------------------------------- +# 5.8.16 (2023-03-26) + +### 🐣新特性 +* 【core 】 改进Calculator.conversion,兼容乘法符号省略写法(issue#2964@Github) +* 【core 】 改进XmlUtil.xmlToBean,支持xml转bean时父节点忽略大小写 +* 【core 】 优化ArrayUtil的空判断(pr#2969@Github) +* 【extra 】 优化SpringUtil在非Spring环境下的异常(issue#2835@Github) +* 【core 】 StrUtil增加commonPrefix和commonSuffix方法(pr#3007@Github) +* 【core 】 NumberUtil增加重载parseXXX方法, 解析失败返回默认值(pr#3007@Github) +* 【core 】 FileUtil增加readLines重载,支持filter(pr#3006@Github) +* 【json 】 当用户选择ignoreError时,错误对象转JSON也忽略 + +### 🐞Bug修复 +* 【crypto】 修复NoSuchMethodError未捕获问题(issue#2966@Github) +* 【poi 】 修复SXSSFWorkbook调用setComment时错位的问题(issue#I6MBS5@Gitee) +* 【core 】 修复BeanUtil.hasGetter没有跳过getClass方法的问题(issue#I6MBS5@Gitee) +* 【core 】 修复FileMagicNumber长度判断问题导致的越界异常(issue#I6MACI@Gitee) +* 【core 】 修复DateUtil针对ISO8601时间格式部分场景下的解析存在问题(issue#2981@Github) +* 【core 】 修复JSONUtil.toBean可能的空指针问题(issue#2987@Github) +* 【core 】 修复CalendarUtil.isSameMonth没有判断公元前导致不一致的问题(issue#3011@Github) +* 【core 】 修复WatchUtil createModify maxDepth传递后没有使用问题(issue#3005@Github) +* 【core 】 修复NullComparator反转无效问题(pr#964@Gitee) +* 【setting】 修复props.toBean 数组字段未赋值问题(issue#3008@Github) + +------------------------------------------------------------------------------------------------------------- +# 5.8.15 (2023-03-09) + +### 🐣新特性 +* 【http 】 新增followRedirectsCookie配置,支持开启自动重定向携带cookie(pr#2961@Github) + +### 🐞Bug修复 +* 【all 】 修复Automatic-Module-Name错误问题(issue#2952@Github) +* 【core 】 修复NumberWithFormat导致转换Long异常问题(issue#I6L2LO@Gitee) + +------------------------------------------------------------------------------------------------------------- +# 5.8.14 (2023-03-05) + +### 🐣新特性 +* 【core 】 增加PathMover(issue#I666HB@Github) + +### 🐞Bug修复 +* 【core 】 修复FileUtil.moveContent会删除源目录的问题(issue#I666HB@Github) +* 【http 】 修复HttpBase.body导致的空指针问题 + +------------------------------------------------------------------------------------------------------------- + +# 5.8.13 (2023-03-03) + +### 🐣新特性 +* 【core 】 PhoneUtil.isTel400800支持400-XXX-XXXX格式(issue#2929@Github) +* 【core 】 build(pom): 添加 Automatic-Module-Name属性(pr#2926@Github) +* 【core 】 根据JDK-8080225修改了部分新建文件输入流和文件输出流的创建方式(pr#2930@Github) +* 【http 】 HttpRequest#body增加支持Resource重载(issue#2901@Github) +* 【core 】 JavaSourceCompiler#compile增加自定义options重载(issue#I6IVZK@Gitee) + +### 🐞Bug修复 +* 【db 】 修复识别JDBC驱动时重复问题(pr#940@Gitee) +* 【core 】 修复法定年龄计算的BUG(pr#935@Gitee) +* 【core 】 修复FileUtil.rename报NoSuchFileException问题(pr#2894@Github) +* 【core 】 修复StrUtil.split切分长度为0时的bug(pr#944@Gitee) +* 【core 】 修复ReUtil.delAll方法当 content 仅为空格时的问题(issue#I6GIMT@Gitee) +* 【core 】 修复ReUtil.delAll方法当 content 仅为空格时的问题(issue#I6GIMT@Gitee) +* 【core 】 修复文件内容跟随在调用stop后,文件依旧被占用问题(issue#I6GFD2@Gitee) +* 【core 】 修复ReflectUtil.invokeRaw方法中参数类型转换动作未生效的问题(pr#2912@Github) +* 【core 】 修复isXXX转换时的匹配问题(issue#I6H0XF@Gitee) +* 【core 】 修复MutableObj.equals空指针问题 +* 【core 】 修复JavaSourceFileObject在编译错误时抛出IOException异常而非CompilerException问题(pr#2942@Github) +* 【jwt 】 修复JWT自定义时间格式后的时间戳转换问题(issue#I6IS5B@Gitee) + +------------------------------------------------------------------------------------------------------------- + +# 5.8.12 (2023-02-09) + +### 🐣新特性 +* 【http 】 HttpGlobalConfig.allowPatch()调用时忽略错误(issue#2832@Github) +* 【core 】 重构根据file magic number判断文件类型(pr#2834@Github) +* 【core 】 增加WGS84 坐标与墨卡托投影互转(pr#2811@Github) +* 【extra 】 ServletUtil遵循rfc 3986优化(issue#I6ALAO@Gitee) +* 【http 】 HttpUtil.decodeParams增加isFormUrlEncoded重载(pr#918@Gitee) +* 【db 】 AbstractDb添加返回类型为PageResult的page重载方法(pr#916@Gitee) +* 【core 】 DesensitizedUtil增加对IPv4和IPv6支持(issue#I6ABCS@Gitee) +* 【core 】 针对CollUtil.subtract coll1 为只读集合的补偿(pr#2865@Github) +* 【core 】 DateUtil.date方法统一修改规则,传入null返回null(pr#2877@Github) +* 【core 】 DateUtil.parseUTC统一规范,舍弃3位毫秒数后的数字(pr#2889@Github) + +### 🐞Bug修复 +* 【core 】 修复HexUtil.isHexNumber()对"-"的判断问题(issue#2857@Github) +* 【core 】 修复FileTypeUtil判断wav后缀的录音文件类型不能匹配问题(pr#2834@Github) +* 【core 】 修复FileUtil的rename在newName与原文件夹名称一样时,文件夹会被删除问题(issue#2845@Github) +* 【core 】 修复IoUtil.readBytes使用SocketInputStream读取不完整问题(issue#I6AT49@Gitee) +* 【core 】 修复ClassScanner自定义classload无效问题(issue#I68TV2@Gitee) +* 【core 】 【重要】删除XmlUtil.readObjectFromXml方法,避免漏洞(issue#2855@Github) +* 【core 】 修复Ipv4Util.list()方法的bug(pr#929@Gitee) +* 【poi 】 修复“sax方式读取excel2003版本,会调用两次doAfterAllAnalysed方法”问题。(pr#919@Gitee) + +------------------------------------------------------------------------------------------------------------- + +# 5.8.11 (2022-12-27) + +### 🐣新特性 +* 【core 】 CharUtil.isBlankChar增加\u180e(pr#2738@Github) +* 【core 】 SyncFinisher线程同步结束器添加立即结束方法(pr#879@Gitee) +* 【core 】 HtmlUtil中escape方法,增加不断开空格(nbsp)转译,防止xss攻击(pr#2755@Github) +* 【extra 】 修正sftp.cd方法 方法注释和实际效果不符(issue#2758@Github) +* 【core 】 修改PhoneUtil容易歧义的注释(issue#I63GWK@Gitee) +* 【crypto】 KeyUtil中的读取KeyStore文件的方法增加全局Provider(issue#I6796G@Gitee) +* 【extra 】 CompressUtil 新增 stripComponents 参数(pr#904@Gitee) +* 【extra 】 ServletUtil和JakartaServletUtil新增获取所有响应头的方法(pr#2828@Github) +* 【core 】 BooleanUtil增加toString重载(pr#2816@Github) + +### 🐞Bug修复 +* 【json 】 修复普通byte数组转JSONArray时的异常(pr#875@Gitee) +* 【core 】 修复ArrayUtil.insert()不支持原始类型数组的问题(pr#874@Gitee) +* 【core 】 修复HexUtil.isHexNumber()判断逻辑超出long的精度问题(issue#I62H7K@Gitee) +* 【core 】 修复BiMap中未重写computeIfAbsent和putIfAbsent导致双向查找出问题(issue#I62X8O@Gitee) +* 【json 】 修复JSON解析栈溢出部分问题(issue#2746@Github) +* 【json 】 修复getMultistageReverseProxyIp未去除空格问题(issue#I64P9J@Gitee) +* 【db 】 修复NamedSql中in没有判断大小写问题(issue#2792@Github) +* 【core 】 修复ZIP bomb漏洞(issue#2797@Github) +* 【core 】 修复JSONXMLSerializer将Json转为XML时,遇到嵌套需要递归情况时会丢失contentKeys问题(pr#903@Gitee) +* 【db 】 修复使用mariadb通过jdbcurl创建SimpleDataSource报NullPointException(pr#900@Gitee) +* 【core 】 修复UrlBuilder中参数中包括"://"判断错误问题(pr#898@Gitee) +* 【core 】 修复IndexedComparator导致的数据错乱问题(ExcelWriter使用部分别名导致字段丢失)(issue#I66Z6B@Gitee) +* 【crypto】 修复sm2构造方法NullPointerException(pr#2820@Github) +* 【core 】 修复ConverterRegistry中无效加载导致的问题(issue#2812@Github) +* 【core 】 修复CoordinateUtil坐标转换参数错误(pr#895@Gitee) + +------------------------------------------------------------------------------------------------------------- + +# 5.8.10 (2022-11-17) + +### 🐣新特性 +* 【http 】 HttpResponse增加getFileNameFromDisposition方法(pr#2676@Github) +* 【core 】 FileUtil.copy,当来源为文件时,返回文件而非目录(issue#I5YCVL@Gitee) +* 【db 】 DialectFactory增加identifyDriver重载(issue#I5YWI6@Gitee) +* 【core 】 去除ClassloaderUtil的Cache(issue#I5YWI6@Gitee) +* 【core 】 ClassScanner 增加忽略加载错误类的扫描方法(pr#855@Gitee) +* 【core 】 DateUtil和LocalDateTimeUtil添加区间退化为点,点与区间,点与点之间关系判断。(pr#2725@Github) +* 【http 】 UserAgentUtil增加对钉钉PC端的支持(issue#I60UOP@Gitee) +* 【extra 】 兼容ZipArchiveInputStream多参数情况(issue#2736@Github) + +### 🐞Bug修复 +* 【db 】 修复分页时order by截断问题(issue#I5X6FM@Gitee) +* 【core 】 修复Partition计算size除数为0报错问题(pr#2677@Github) +* 【core 】 由于对于ASCII的编码解码有缺陷,且这种BCD实现并不规范,因此BCD标记为弃用(issue#I5XEC6@Gitee) +* 【core 】 修复IoUtil.copyByNIO方法写出时没有flush的问题 +* 【core 】 修复TreeBuilder中使用HashMap导致默认乱序问题(issue#I5Z8C5@Gitee) +* 【core 】 修复StrUtil.subWithLength负数问题(issue#I5YN49@Gitee) +* 【core 】 修复DefaultTrustManager空指针问题(issue#2716@Github) +* 【core 】 修复时间轮添加任务线程安全问题(pr#2712@Github) +* 【core 】 修复 BeanUtil#copyProperties 源对象与目标对象都是 Map 时设置忽略属性无效问题(pr#2698@Github) +* 【core 】 修复ChineseDate传入农历日期非闰月时获取公历错误问题(issue#I5YB1A@Gitee) +* 【core 】 修复key为弱引用 value为强引用 会导致key无法被回收 弱引用失效问题(pr#2723@Github) +* 【core 】 修复BeanUtil.copyProperties 包含EnumSet ,类型转换异常问题(pr#2684@Github) +* 【extra 】 修复Ftp.uploadFileOrDirectory上传目录错误调用错误问题(issue#I5R2DE@Gitee) +* 【extra 】 修复字节数组转float 返回类型却是double的bug(pr#867@Gitee) + +------------------------------------------------------------------------------------------------------------- + +# 5.8.9 (2022-10-22) + +### 🐣新特性 +* 【core 】 DateUtil增加isLastDayOfMonth、getLastDayOfMonth方法(pr#824@Gitee) +* 【core 】 AnnotationUtil类支持Lambda获取某注解属性值(pr#827@Gitee) +* 【core 】 CharUtil.isBlank添加Hangul Filler字符(issue#I5UGSQ@Gitee) +* 【poi 】 优化合并单元格读取(issue#I5UJZ1@Gitee) +* 【extra 】 增加QLExpress支持(issue#2653@Github) +* 【core 】 UrlBuilder增加getPortWithDefault方法(pr#835@Gitee) +* 【core 】 FuncKeyMap的子类,传入可被序列化的keyFunc(pr#838@Gitee) +* 【extra 】 SpringUtil支持SpringBoot3自动配置(pr#839@Gitee) +* 【core 】 CollectorUtil添加支持对值集合进行映射的分组方法(pr#844@Gitee) +* 【core 】 FileTypeUtil增加ppt识别(issue#2663@Github) + +### 🐞Bug修复 +* 【poi 】 修复ExcelReader读取只有标题行报错问题(issue#I5U1JA@Gitee) +* 【http 】 修复Http重定向时相对路径导致的问题(issue#I5TPSY@Gitee) +* 【http 】 修复Http重定全局设置无效问题(pr#2639@Github) +* 【core 】 修复ReUtil.replaceAll替换变量错误问题(pr#2639@Github) +* 【core 】 修复FileNameUtil.mainName二级扩展名获取错误问题(issue#2642@Github) +* 【cache 】 修复LRUCache移除事件监听失效问题(issue#2647@Github) +* 【core 】 修复MapToMap中ignoreNullValue无效问题(issue#2647@Github) +* 【core 】 修复ReflectUtil.invokeRaw方法转换失败抛出异常问题(pr#837@Gitee) +* 【core 】 修复TableMap没有default方法导致的问题(issue#I5WMST@Gitee) + +------------------------------------------------------------------------------------------------------------- + +# 5.8.8 (2022-09-26) + +### 🐣新特性 +* 【core 】 StreamUtil.of方法新增对 Iterator 支持;StreamUtil.of(Iterable) 方法优化(pr#807@Gitee) +* 【core 】 增加.wgt格式的MimeType(pr#2617@Github) +* 【core 】 EnumUtil.getBy增加带默认值重载(issue#I5RZU6@Gitee) +* 【core 】 ModifierUtil和ReflectUtil增加removeFinalModify(pr#810@Gitee) +* 【core 】 AbsCollValueMap添加removeValue和removeValues方法,用于list value值移除(pr#813@Gitee) +* 【extra 】 hutool-extra ftp 支持上传文件或目录(pr#821@Gitee) +* 【core 】 CharsetDetector增加默认识别的长度(issue#2547@Github) + +### 🐞Bug修复 +* 【core 】 修复FileNameUtil.cleanInvalid无法去除换行符问题(issue#I5RMZV@Gitee) +* 【core 】 修复murmur3_32实现错误(pr#2616@Github) +* 【core 】 修复PunyCode处理域名的问题(pr#2620@Github) +* 【core 】 修复ObjectUtil.defaultIfNull去掉误加的deprecated(issue#I5SIZT@Gitee) +* 【core 】 修复ReflectUtil 反射方法中桥接判断问题(issue#2625@Github) +* 【poi 】 修复ExcelWriter导出List引起的个数混乱问题(issue#2627@Github) +* 【poi 】 修复ExcelReader读取时间变成12小时形式问题(issue#I5Q1TW@Gitee) +* 【db 】 修复DB工具分页查询的时候oracle数据库会把ROWNUM_也带出来问题(issue#2618@Github) +* 【crypto 】 修复部分环境下使用 Bouncy Castle可能的JCE cannot authenticate the provider BC问题(issue#2631@Github) + +------------------------------------------------------------------------------------------------------------- + +# 5.8.7 (2022-09-15) + +### 🐣新特性 +* 【core 】 BooleanUtil的andOfWrap和orOfWrap()忽略null(issue#2599@Github) +* 【jwt 】 优化JWT自动识别header中的算法,并可自定义header中key的顺序(issue#I5QRUO@Gitee) +* 【core 】 IdcardUtil增加convert18To15方法(issue#I5QYCP@Gitee) +* 【core 】 新增AnsiColors(改自Spring Boot)、AnsiColorWrapper,优化QrCodeUtil(pr#778@Gitee) +* 【core 】 TemplateUtil的实现类增加getRawEngine方法(issues#2530@Github) +* 【core 】 ImgUtil中颜色相关方法剥离到ColorUtil中 +* 【core 】 增加SafeConcurrentHashMap + +### 🐞Bug修复 +* 【core 】 修复ObjectUtil.defaultIfXXX中NPE问题(pr#2603@Github) +* 【db 】 修复Hive2驱动无法识别问题(issue#2606@Github) +* 【core 】 修复computeIfAbsent问题(issue#I5PTN3@Gitee) +* 【extra 】 修复Ftp中路径问题(issue#I5R2DE@Gitee) +* 【core 】 修复ConcurrentHashMap.computeIfAbsent缺陷导致的问题 +* 【core 】 修复DateUtil.parseUTC时对-的处理问题(issue#2612@Github) +* 【core 】 修复Convert.chineseMoneyToNumber角分丢失问题(issue#2611@Github) + +------------------------------------------------------------------------------------------------------------- + +# 5.8.6 (2022-09-05) + +### ❌不兼容特性 +* 【json 】 由于设计缺陷,导致JSONObject#write方法中Filter中key的泛型不得已变动为Object,以解决无法递归的bug(issue#I5OMSC@Gitee) + +### 🐣新特性 +* 【core 】 CollUtil新增addIfAbsent方法(pr#750@Gitee) +* 【core 】 DateUtil.parseUTC支持只有时分的格式(issue#I5M6DP@Gitee) +* 【core 】 NumberUtil.parseInt忽略科学计数法(issue#I5M55F@Gitee) +* 【core 】 IterUtil.getFirst优化(pr#753@Gitee) +* 【core 】 增加Tree add 类型校验(pr#2542@Github) +* 【core 】 增加PunyCode处理完整域名(pr#2543@Github) +* 【core 】 增加替换字符串中第一个指定字符串和最后一个指定字符串方法(pr#2533@Github) +* 【jwt 】 JWT补充部分算法(pr#2546@Github) +* 【core 】 NumberUtil.roundStr() 修改为使用toPlainString(pr#775@Gitee) +* 【extra 】 QrCodeUtil新增SVG格式、Ascii Art字符画格式(pr#763@Gitee) +* 【jwt 】 JWTUtil的parseToken增加空值异常抛出(issue#I5OCQB@Gitee) +* 【extra 】 resource.loader等过期参数替换(issue#2571@Github) +* 【core 】 添加ObjectUtil的别名工具类ObjUtil +* 【core 】 扩展LocalDateTimeUtil.isIn方法使用场景(pr#2589@Github) +* 【core 】 MapUtil增加根据entry分组(pr#2591@Github) +* 【core 】 优化 getProcessorCount 潜在的获取不到的问题(pr#792@Gitee) +* 【core 】 ImgUtil增加sliceByRowsAndCols重载方法支持自定义图片格式(pr#793@Gitee) +* +### 🐞Bug修复 +* 【http 】 修复https下可能的Patch、Get请求失效问题(issue#I3Z3DH@Gitee) +* 【core 】 修复RandomUtil#randomString 入参length为负数时报错问题(issue#2515@Github) +* 【core 】 修复SecureUtil传入null的key抛出异常问题(pr#2521@Github) +* 【core 】 修复UrlBuilder的toURI方法将url重复编码(issue#2503@Github) +* 【core 】 修复CollUtil.lastIndexOf序号错误问题 +* 【core 】 修复zip被识别成jar和apk被识别成jar或zip的问题(pr#2548@Github) +* 【core 】 修复UrlBuilder.addPath 方法传入非有效路径字符串时,会出现空指针异常的问题(issue#I5O4ML@Gitee) +* 【core 】 修复FilterIter当参数filter为空时存在问题(issue#I5OG7U@Gitee) +* 【poi 】 修复Excel读取提示信息错误(issue#I5OSFC@Gitee) +* 【json 】 解决JSONObject#write无法递归的bug(issue#I5OMSC@Gitee) +* 【json 】 修复DayOfWeek转json异常问题(issue#2572@Github) +* 【extra 】 Ftp方法isDir和exist修复及改进(pr#2574@Github) +* 【json 】 修复JSON反序列化时,引用字段类型的自定义JsonDeserializer无效(issue#2555@Github) + +------------------------------------------------------------------------------------------------------------- + +# 5.8.5 (2022-07-29) + +### ❌不兼容特性 +* 【core 】 合成注解相关功能重构,增加@Link及其子注解(pr#702@Gitee) + +### 🐣新特性 +* 【core 】 NumberUtil新增isIn方法(pr#669@Gitee) +* 【core 】 修复注解工具类getAnnotations的NPE问题,注解扫描器添新功能(pr#671@Gitee) +* 【core 】 合成注解SyntheticAnnotation提取为接口,并为实现类添加注解选择器和属性处理器(pr#678@Gitee) +* 【core 】 增加BeanValueProvider(issue#I5FBHV@Gitee) +* 【core 】 Convert工具类中,新增中文大写数字金额转换为数字工具方法(pr#674@Gitee) +* 【core 】 新增CollectorUtil.reduceListMap()(pr#676@Gitee) +* 【core 】 CollStreamUtil为空返回空的集合变为可编辑(pr#681@Gitee) +* 【core 】 增加StrUtil.containsAll(pr#2437@Github) +* 【core 】 ForestMap添加getNodeValue方法(pr#699@Gitee) +* 【http 】 优化HttpUtil.isHttp判断,避免NPE(pr#698@Gitee) +* 【core 】 修复Dict#containsKey方法没区分大小写问题(pr#697@Gitee) +* 【core 】 增加比较两个LocalDateTime是否为同一天(pr#693@Gitee) +* 【core 】 增加TemporalAccessorUtil.isIn、LocalDateTimeUtil.isIn(issue#I5HBL0@Gitee) +* 【core 】 ReUtil增加getAllGroups重载(pr#2455@Github) +* 【core 】 PageUtil#totalPage增加totalCount为long类型的重载方法(pr#2442@Github) +* 【crypto 】 PemUtil.readPemPrivateKey支持pkcs#1格式,增加OpensslKeyUtil(pr#2456@Github) +* 【core 】 添加了通用的注解扫描器 `GenericAnnotationScanner`,并在 `AnnotationScanner` 接口中统一提供了提前配置好的扫描器静态实例(pr#715@Github) +* 【json 】 JSONConfig增加允许重复key配置,解决不规整json序列化的问题(pr#720@Github) +* 【core 】 完善了codec包下一些方法的入参空校验(pr#719@Gitee) +* 【extra 】 完善QrCodeUtil对于DATA_MATRIX生成的形状随机不可指定的功能(pr#722@Gitee) +* 【core 】 修改NetUtil.ipv6ToBigInteger,原方法标记为过期(pr#2485@Github) +* 【core 】 ZipUtil新增zip文件解压大小限制,防止zip炸弹(pr#726@Gitee) +* 【core 】 CompressUtil增加压缩和解压tgz(.tar.gz)文件(issue#I5J33E@Gitee) +* +### 🐞Bug修复 +* 【core 】 修复CollUtil里面关于可变参数传null造成的crash问题(pr#2428@Github) +* 【socket 】 修复异常socket没有关闭问题(pr#690@Gitee) +* 【core 】 修复当时间戳为Integer时时间转换问题(pr#2449@Github) +* 【core 】 修复bmp文件判断问题(issue#I5H93G@Gitee) +* 【core 】 修复CombinationAnnotationElement造成递归循环(issue#I5FQGW@Gitee) +* 【core 】 修复Dict缺少putIfAbsent、computeIfAbsent问题(issue#I5FQGW@Gitee) +* 【core 】 修复Console.log应该把异常信息输出位置错误问题(pr#716@Gitee) +* 【core 】 修复UrlBuilder无法配置末尾追加“/”问题(issue#2459@Github) +* 【core 】 修复SystemPropsUtil.getBoolean方法应该只有值为true时才返回true,其他情况都应该返回false(pr#717@Gitee) +* 【core 】 修复isBase64判断不准确的问题(pr#727@Gitee) +* 【core 】 修复Convert#toMap默认转成HashMap的问题(pr#729@Gitee) + +------------------------------------------------------------------------------------------------------------- + +# 5.8.4 (2022-06-27) + +### 🐣新特性 +* 【extra 】 Sftp增加构造重载,支持超时(pr#653@Gitee) +* 【core 】 BeanUtil增加isCommonFieldsEqual(pr#653@Gitee) +* 【json 】 修改byte[]统一转换为数组形式(issue#2377@Github) +* 【http 】 HttpResponse增加body方法,支持自定义返回内容(pr#655@Gitee) +* 【core 】 修改ObjectUtil.isNull逻辑(issue#I5COJF@Gitee) +* 【core 】 BlockPolicy增加线程池关闭后的逻辑(pr#660@Gitee) +* 【core 】 Ipv4Util增加ipv4ToLong重载(pr#661@Gitee) +* 【core 】 LocalDateTimeUtil.parse改为blank检查(issue#I5CZJ9@Gitee) +* 【core 】 BeanPath在空元素时默认加入map,修改根据下标类型赋值List or map(issue#2362@Github) +* 【core 】 localAddressList 添加重构方法(pr#665@Gitee) +* 【cron 】 从配置文件加载任务时,自定义ID避免重复从配置文件加载(issue#I5E7BM@Gitee) +* 【core 】 新增注解扫描器和合成注解(pr#654@Gitee) +* +### 🐞Bug修复 +* 【extra 】 修复createExtractor中抛出异常后流未关闭问题(pr#2384@Github) +* 【core 】 修复CsvData.getHeader没有判空导致空指针问题(issue#I5CK7Q@Gitee) +* 【core 】 修复单字母转换为数字的问题(issue#I5C4K1@Gitee) +* 【core 】 修复IterUtil.filter无效问题 +* 【core 】 修复NumberUtil传入null,返回了true(issue#I5DTSL@Gitee) +* 【core 】 修复NumberUtil.isDouble问题(pr#2400@Github) +* 【core 】 修复ZipUtil使用append替换文件时,父目录存在报错问题(issue#I5DRU0@Gitee) + +------------------------------------------------------------------------------------------------------------- + +# 5.8.3 (2022-06-10) + +### 🐣新特性 +* 【extra 】 mail增加writeTimeout参数支持(issue#2355@Github) +* 【core 】 FileTypeUtil增加pptx扩展名支持(issue#I5A0GO@Gitee) +* 【core 】 IterUtil.get增加判空(issue#I5B12A@Gitee) +* 【core 】 FileTypeUtil增加webp类型判断(issue#I5BGTF@Gitee) +### 🐞Bug修复 +* 【core 】 修复NumberUtil.isXXX空判断错误(issue#2356@Github) +* 【core 】 修复Convert.toSBC空指针问题(issue#I5APKK@Gitee) +* 【json 】 修复Bean中存在bytes,无法转换问题(issue#2365@Github) +* 【core 】 ArrayUtil.setOrAppend()传入空数组时,抛出异常(issue#I5APJE@Gitee) +* 【extra 】 JschSessionPool修复空指针检查问题(issue#I5BK4D@Gitee) +* 【core 】 修复使用ValueProvider中setFieldMapping无效问题(issue#I5B4R7@Gitee) +* 【json 】 修复byte[]作为JSONArray构造问题(issue#2369@Github) + +------------------------------------------------------------------------------------------------------------- + +# 5.8.2 (2022-05-27) + +### 🐣新特性 +* 【core 】 BeanUtil拷贝对象增加空检查(issue#I58CJ3@Gitee) +* 【db 】 Column#size改为long +* 【core 】 ClassUtil增加isInterface等方法(pr#623@Gitee) +* 【socket 】 增加ChannelUtil + +### 🐞Bug修复 +* 【extra 】 修复SshjSftp初始化未能代入端口配置问题(issue#2333@Github) +* 【core 】 修复Convert.numberToSimple转换问题(issue#2334@Github) +* 【core 】 修复TemporalAccessorConverter导致的转换问题(issue#2341@Github) +* 【core 】 修复NumberUtil除法空指针问题(issue#I58XKE@Gitee) +* 【core 】 修复CAR_VIN正则(pr#624@Gitee) +* 【db 】 修复count查询别名问题(issue#I590YB@Gitee) +* 【json 】 修复json中byte[]无法转换问题(issue#I59LW4@Gitee) +* 【core 】 修复NumberUtil.isXXX未判空问题(issue#2350@Github) +* 【core 】 修复Singleton中ConcurrentHashMap在JDK8下的bug引起的可能的死循环问题(issue#2349@Github) + +------------------------------------------------------------------------------------------------------------- + +# 5.8.1 (2022-05-16) + +### 🐣新特性 +* 【core 】 BooleanUtil增加toBooleanObject方法(issue#I56AG3@Gitee) +* 【core 】 CharSequenceUtil增加startWithAnyIgnoreCase方法(issue#2312@Github) +* 【system 】 JavaInfo增加版本(issue#2310@Github) +* 【core 】 新增CastUtil(pr#2313@Github) +* 【core 】 ByteUtil新增bytesToShort重载(issue#I57FA7@Gitee) +* 【core 】 ReflectUtil.invoke方法抛出运行时异常增加InvocationTargetRuntimeException(issue#I57GI2@Gitee) +* 【core 】 NumberUtil.parseNumber支持16进制(issue#2328@Github) + +### 🐞Bug修复 +* 【core 】 MapUtil.map对null友好,且修复了测试用例中分组问题(pr#614@Gitee) +* 【core 】 修复BeanUtil.beanToMap中properties为null的空指针问题(issue#2303@Github) +* 【db 】 DialectName中修正为POSTGRESQL(issue#2308@Github) +* 【core 】 修复BeanPath无法识别引号内的内容问题(issue#I56DE0@Gitee) +* 【core 】 修复Map.entry方法返回可变不可变相反问题 +* 【jwt 】 修复jwt的过期容忍时间问题(issue#2329@Gitee) + +------------------------------------------------------------------------------------------------------------- + +# 5.8.0 (2022-05-06) ### ❌不兼容特性 * 【extra 】 升级jakarta.validation-api到3.x,包名变更导致不能向下兼容 +* 【core 】 BeanUtil删除了beanToMap(Object)方法,因为有可变参数的方法,这个删除可能导致直接升级找不到方法,重新编译项目即可。 ### 🐣新特性 +* 【core 】 Singleton增加部分方法(pr#609@Gitee) +* 【core 】 BeanUtil增加beanToMap重载(pr#2292@Github) +* 【core 】 Assert增加对应的equals及notEquals方法(pr#612@Gitee) +* 【core 】 Assert增加对应的equals及notEquals方法(pr#612@Gitee) +* 【core 】 DigestUtil增加sha512方法(issue#2298@Github) ### 🐞Bug修复 * 【db 】 修复RedisDS无法设置maxWaitMillis问题(issue#I54TZ9@Gitee) @@ -43,7 +1091,7 @@ * 【core 】 修复SimpleCache线程安全问题 * 【core 】 修复ClassLoaderUtil中可能的关联ClassLoader错位问题 * 【extra 】 修复Sftp错误内容解析大小写问题(issue#I53GPI@Gitee) -* 【core 】 修复Tailer当文件内容为空时,会报异常问题(pr#602@Gitee) +* 【core 】 修复当文件内容为空时,会报异常问题(pr#602@Gitee) ------------------------------------------------------------------------------------------------------------- @@ -168,4 +1216,7 @@ * 【core 】 FileUtil.getMimeType增加rar、7z支持(issue#I4ZBN0@Gitee) * 【json 】 JSON修复transient设置无效问题(issue#2212@Github) * 【core 】 修复IterUtil.getElementType获取结果为null的问题(issue#2222@Github) -* 【core 】 修复农历转公历在闰月时错误(issue#I4ZSGJ@Gitee) \ No newline at end of file +* 【core 】 修复农历转公历在闰月时错误(issue#I4ZSGJ@Gitee) + +# 5.7.x 或更早版本 +* [https://gitee.com/chinabugotech/hutool/blob/v5-master/CHANGELOG_5.0-5.7.md](https://gitee.com/chinabugotech/hutool/blob/v5-master/CHANGELOG_5.0-5.7.md) \ No newline at end of file diff --git a/CHANGELOG_5.0-5.7.md b/CHANGELOG_5.0-5.7.md index 04fc0e922d3b9bc0733735e0c1b1a5751859738b..3072d65378d1e197a633008fd5cba0dc46f42824 100755 --- a/CHANGELOG_5.0-5.7.md +++ b/CHANGELOG_5.0-5.7.md @@ -1564,7 +1564,7 @@ ### 🐞Bug修复 * 【core 】 修复NumberWordFormatter拼写错误(issue#799@Github) * 【poi 】 修复xls文件下拉列表无效问题(issue#I1C79P@Gitee) -* 【poi 】 修复使用Cglib代理问题(issue#I1C79P@Gitee) +* 【poi 】 修复使用Cglib代理问题(issue#806@Github) * 【core 】 修复DateUtil.weekCount跨年计算问题 ------------------------------------------------------------------------------------------------------------- diff --git a/README-EN.md b/README-EN.md index 1f79bbb656ac57d3997657a2f3dfb1584e3980a8..6c7d66ceca58726e5c1780f92153cabf9b5261cf 100755 --- a/README-EN.md +++ b/README-EN.md @@ -12,46 +12,51 @@ - + - - + + - + - - + + - - star + + star - - github star + + github star + + + gitcode star


- - + +

------------------------------------------------------------------------------- + + [**🌎中文说明**](README.md) ------------------------------------------------------------------------------- ## 📚Introduction -**Hutool** is a small but comprehensive library of Java tools, encapsulation by static methods, reduce the cost of learning related APIs, increase productivity, and make Java as elegant as a functional programming language,let the Java be "sweet" too. +**Hutool** is a small but comprehensive library of Java tools, achieved by encapsulation through static methods, reduce the cost of learning related APIs, increase productivity, and make Java as elegant as a functional programming language,let the Java be "sweet" too. **Hutool** tools and methods from each user's crafted, it covers all aspects of the underlying code of Java development, it is a powerful tool for large project development to solve small problems, but also the efficiency of small projects; @@ -81,8 +86,8 @@ Hutool exists to reduce code search costs and avoid bugs caused by imperfect cod ## 🛠️Module A Java-based tool class for files, streams, encryption and decryption, transcoding, regular, thread, XML and other JDK methods for encapsulation,composing various Util tool classes, as well as providing the following modules: -| module | description | -| -------------------|-------------------------------------------------------------------------------------------------------------------------| +| module | description | +|--------------------|-------------------------------------------------------------------------------------------------------------------------| | hutool-aop | JDK dynamic proxy encapsulation to provide non-IOC faceting support | | hutool-bloomFilter | Bloom filtering to provide some Hash algorithm Bloom filtering | | hutool-cache | Simple cache | @@ -102,6 +107,7 @@ A Java-based tool class for files, streams, encryption and decryption, transcodi | hutool-poi | Tools for working with Excel and Word in POI | | hutool-socket | Java-based tool classes for NIO and AIO sockets | | hutool-jwt | JSON Web Token (JWT) implement | +| hutool-ai | AI implement | Each module can be introduced individually, or all modules can be introduced by introducing `hutool-all` as required. @@ -109,33 +115,17 @@ Each module can be introduced individually, or all modules can be introduced by ## 📝Doc -[📘Chinese documentation](https://www.hutool.cn/docs/) +[📘Chinese documentation](https://doc.hutool.cn/pages/index/) -[📘Chinese back-up documentation](https://plus.hutool.cn/docs/#/) +[📘Chinese back-up documentation](https://plus.hutool.cn/) -[📙API](https://apidoc.gitee.com/dromara/hutool/) +[📙API](https://plus.hutool.cn/apidocs/) [🎬Video](https://www.bilibili.com/video/BV1bQ4y1M7d9?p=2) ------------------------------------------------------------------------------- -## 🪙Support Hutool - -### 💳Donate - -If you think Hutool is good, you can donate to buy the author a pack of chili~, thanks in advance ^_^. - -[Gitee donate](https://gitee.com/dromara/hutool) - -[Dromara donate](https://dromara.gitee.io/donate.html) - -### 👕Shop about Hutool -We provide the T-Shirt and Sweater with Hutool Logo, please visit the shop: - -👉 [Hutool Shop](https://market.m.taobao.com/apps/market/content/index.html?wh_weex=true&contentId=331724720170) 👈 - -------------------------------------------------------------------------------- ## 📦Install @@ -144,18 +134,18 @@ We provide the T-Shirt and Sweater with Hutool Logo, please visit the shop: cn.hutool hutool-all - 5.8.0.M5 + 5.8.41 ``` ### 🍐Gradle ``` -implementation 'cn.hutool:hutool-all:5.8.0.M5' +implementation 'cn.hutool:hutool-all:5.8.41' ``` ## 📥Download -- [Maven Repo](https://repo1.maven.org/maven2/cn/hutool/hutool-all/5.8.0.M5/) +- [Maven Repo](https://repo1.maven.org/maven2/cn/hutool/hutool-all/5.8.41/) > 🔔️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. @@ -165,9 +155,9 @@ implementation 'cn.hutool:hutool-all:5.8.0.M5' Download the entire project source code -gitee:[https://gitee.com/dromara/hutool](https://gitee.com/dromara/hutool) +gitee:[https://gitee.com/chinabugotech/hutool](https://gitee.com/chinabugotech/hutool) -github:[https://github.com/dromara/hutool](https://github.com/dromara/hutool) +github:[https://github.com/chinabugotech/hutool](https://github.com/chinabugotech/hutool) ```sh cd ${hutool} @@ -191,8 +181,9 @@ Hutool's source code is divided into two branches: When submitting feedback, please indicate which JDK version, Hutool version, and related dependency library version you are using. -- [Gitee issue](https://gitee.com/dromara/hutool/issues) -- [Github issue](https://github.com/dromara/hutool/issues) +- [Gitee issue](https://gitee.com/chinabugotech/hutool/issues) +- [Github issue](https://github.com/chinabugotech/hutool/issues) +- [Gitcode issue](https://gitcode.com/chinabugotech/hutool/issues) ### 🧬Principles of PR(pull request) @@ -203,15 +194,12 @@ Hutool welcomes anyone to contribute code to Hutool, but the author suffers from 3. Newly added methods do not use third-party library methods,Unless the method tool is add to the '**extra module**'. 4. Please pull request to the `v5-dev` branch. Hutool uses a new branch after 5.x: `v5-master` is the master branch, which indicates the version of the central library that has been released, and this branch does not allow pr or modifications. -------------------------------------------------------------------------------- +### 📖 Documentation source code -## ⭐Star Hutool +[Documentation source code](https://gitee.com/loolly_admin/hutool-doc-handy) -[![Stargazers over time](https://starchart.cc/dromara/hutool.svg)](https://starchart.cc/dromara/hutool) +------------------------------------------------------------------------------- -## 📌WeChat Official Account +## ⭐Star Hutool -
- - -
\ No newline at end of file +[![Stargazers over time](https://starchart.cc/chinabugotech/hutool.svg)](https://starchart.cc/chinabugotech/hutool) diff --git a/README.md b/README.md index 601bf892d66f9aa9c4290197555e95d67e2a247a..061b727bc1e2054ee225a2d7abfc35993a65c16f 100755 --- a/README.md +++ b/README.md @@ -12,73 +12,70 @@ - + - - + + - + - - + + - - + + star - - star + + github star - - github star + + gitcode star


- - + +

------------------------------------------------------------------------------- +======= + [**🌎English Documentation**](README-EN.md) ------------------------------------------------------------------------------- ## 📚简介 -Hutool是一个小而全的Java工具类库,通过静态方法封装,降低相关API的学习成本,提高工作效率,使Java拥有函数式语言般的优雅,让Java语言也可以“甜甜的”。 - -Hutool中的工具方法来自每个用户的精雕细琢,它涵盖了Java开发底层代码中的方方面面,它既是大型项目开发中解决小问题的利器,也是小型项目中的效率担当; -Hutool是项目中“util”包友好的替代,它节省了开发人员对项目中公用类和公用工具方法的封装时间,使开发专注于业务,同时可以最大限度的避免封装不完善带来的bug。 +`Hutool`是一个功能丰富且易用的**Java工具库**,通过诸多实用工具类的使用,旨在帮助开发者快速、便捷地完成各类开发任务。 +这些封装的工具涵盖了字符串、数字、集合、编码、日期、文件、IO、加密、数据库JDBC、JSON、HTTP客户端等一系列操作, +可以满足各种不同的开发需求。 ### 🎁Hutool名称的由来 Hutool = Hu + tool,是原公司项目底层代码剥离后的开源库,“Hu”是公司名称的表示,tool表示工具。Hutool谐音“糊涂”,一方面简洁易懂,一方面寓意“难得糊涂”。 -### 🍺Hutool如何改变我们的coding方式 - -Hutool的目标是使用一个工具方法代替一段复杂代码,从而最大限度的避免“复制粘贴”代码的问题,彻底改变我们写代码的方式。 +### 🍺Hutool理念 -以计算MD5为例: +`Hutool`既是一个工具集,也是一个知识库,我们从不自诩代码原创,大多数工具类都是**搬运**而来,因此: -- 👴【以前】打开搜索引擎 -> 搜“Java MD5加密” -> 打开某篇博客-> 复制粘贴 -> 改改好用 -- 👦【现在】引入Hutool -> SecureUtil.md5() - -Hutool的存在就是为了减少代码搜索成本,避免网络上参差不齐的代码出现导致的bug。 +- 你可以引入使用,也可以**拷贝**和修改使用,而**不必标注任何信息**,只是希望能把bug及时反馈回来。 +- 我们努力健全**中文**注释,为源码学习者提供良好地学习环境,争取做到人人都能看得懂。 ------------------------------------------------------------------------------- ## 🛠️包含组件 一个Java基础工具类,对文件、流、加密解密、转码、正则、线程、XML等JDK方法进行封装,组成各种Util工具类,同时提供以下组件: -| 模块 | 介绍 | -| -------------------|---------------------------------------------------------------------------------- | +| 模块 | 介绍 | +|--------------------|---------------------------------------------------------------------------------- | | hutool-aop | JDK动态代理封装,提供非IOC下的切面支持 | | hutool-bloomFilter | 布隆过滤,提供一些Hash算法的布隆过滤 | | hutool-cache | 简单缓存实现 | @@ -98,6 +95,7 @@ Hutool的存在就是为了减少代码搜索成本,避免网络上参差不 | hutool-poi | 针对POI中Excel和Word的封装 | | hutool-socket | 基于Java的NIO和AIO的Socket封装 | | hutool-jwt | JSON Web Token (JWT)封装实现 | +| hutool-ai | AI大模型封装实现 | 可以根据需求对每个模块单独引入,也可以通过引入`hutool-all`方式引入所有模块。 @@ -105,35 +103,17 @@ Hutool的存在就是为了减少代码搜索成本,避免网络上参差不 ## 📝文档 -[📘中文文档](https://www.hutool.cn/docs/) +[📘中文文档](https://doc.hutool.cn/pages/index/) -[📘中文备用文档](https://plus.hutool.cn/docs/#/) +[📘中文备用文档](https://plus.hutool.cn/) -[📙参考API](https://apidoc.gitee.com/dromara/hutool/) +[📙参考API](https://plus.hutool.cn/apidocs/) [🎬视频介绍](https://www.bilibili.com/video/BV1bQ4y1M7d9?p=2) ------------------------------------------------------------------------------- -## 🪙支持Hutool - -### 💳捐赠 - -如果你觉得Hutool不错,可以捐赠请维护者吃包辣条~,在此表示感谢^_^。 - -[Gitee上捐赠](https://gitee.com/dromara/hutool) - -[捐赠给Dromara组织](https://dromara.gitee.io/donate.html) - -### 👕周边商店 -你也可以通过购买Hutool的周边商品来支持Hutool维护哦! - -我们提供了印有Hutool Logo的周边商品,欢迎点击购买支持: - -👉 [Hutool 周边商店](https://market.m.taobao.com/apps/market/content/index.html?wh_weex=true&contentId=331724720170) 👈 - -------------------------------------------------------------------------------- ## 📦安装 @@ -144,20 +124,20 @@ Hutool的存在就是为了减少代码搜索成本,避免网络上参差不 cn.hutool hutool-all - 5.8.0.M5 + 5.8.41 ``` ### 🍐Gradle ``` -implementation 'cn.hutool:hutool-all:5.8.0.M5' +implementation 'cn.hutool:hutool-all:5.8.41' ``` ### 📥下载jar 点击以下链接,下载`hutool-all-X.X.X.jar`即可: -- [Maven中央库](https://repo1.maven.org/maven2/cn/hutool/hutool-all/5.8.0.M5/) +- [Maven中央库](https://repo1.maven.org/maven2/cn/hutool/hutool-all/5.8.41/) > 🔔️注意 > Hutool 5.x支持JDK8+,对Android平台没有测试,不能保证所有工具类或工具方法可用。 @@ -165,7 +145,7 @@ implementation 'cn.hutool:hutool-all:5.8.0.M5' ### 🚽编译安装 -访问Hutool的Gitee主页:[https://gitee.com/dromara/hutool](https://gitee.com/dromara/hutool) 下载整个项目源码(v5-master或v5-dev分支都可)然后进入Hutool项目目录执行: +访问Hutool的Gitee主页:[https://gitee.com/chinabugotech/hutool](https://gitee.com/chinabugotech/hutool) 下载整个项目源码(v5-master或v5-dev分支都可)然后进入Hutool项目目录执行: ```sh ./hutool.sh install @@ -190,17 +170,18 @@ Hutool的源码分为两个分支,功能如下: 提交问题反馈请说明正在使用的JDK版本呢、Hutool版本和相关依赖库版本。 -- [Gitee issue](https://gitee.com/dromara/hutool/issues) -- [Github issue](https://github.com/dromara/hutool/issues) +- [Gitee issue](https://gitee.com/chinabugotech/hutool/issues) +- [Github issue](https://github.com/chinabugotech/hutool/issues) +- [Gitcode issue](https://gitcode.com/chinabugotech/hutool/issues) ### 🧬贡献代码的步骤 -1. 在Gitee或者Github上fork项目到自己的repo +1. 在Gitee或者Github/Gitcode上fork项目到自己的repo 2. 把fork过去的项目也就是你的项目clone到你的本地 3. 修改代码(记得一定要修改v5-dev分支) 4. commit后push到自己的库(v5-dev分支) -5. 登录Gitee或Github在你首页可以看到一个 pull request 按钮,点击它,填写一些说明信息,然后提交即可。 +5. 登录Gitee或Github/Gitcode在你首页可以看到一个 pull request 按钮,点击它,填写一些说明信息,然后提交即可。 6. 等待维护者合并 ### 📐PR遵照的原则 @@ -213,15 +194,12 @@ Hutool欢迎任何人为Hutool添砖加瓦,贡献代码,不过维护者是 4. 请pull request到`v5-dev`分支。Hutool在5.x版本后使用了新的分支:`v5-master`是主分支,表示已经发布中央库的版本,这个分支不允许pr,也不允许修改。 5. 我们如果关闭了你的issue或pr,请不要诧异,这是我们保持问题处理整洁的一种方式,你依旧可以继续讨论,当有讨论结果时我们会重新打开。 -------------------------------------------------------------------------------- +### 📖文档源码地址 -## ⭐Star Hutool +[文档源码地址](https://gitee.com/loolly_admin/hutool-doc-handy) 点击前往添砖加瓦 -[![Stargazers over time](https://starchart.cc/dromara/hutool.svg)](https://starchart.cc/dromara/hutool) +------------------------------------------------------------------------------- -## 📌公众号 +## ⭐Star Hutool -
- - -
\ No newline at end of file +[![Stargazers over time](https://starchart.cc/chinabugotech/hutool.svg)](https://starchart.cc/chinabugotech/hutool) diff --git a/SECURITY.md b/SECURITY.md index d0c2c55899035458a94663daf97100c9e12a47fa..e5b59f02c6aed476e5e18ceb54720bbaf05f7534 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -10,6 +10,6 @@ ## Reporting a Vulnerability(报告漏洞) -如果你发现有安全问题或漏洞,请发送邮件到`loolly@aliyun.com`。 +如果你发现有安全问题或漏洞,请发送邮件到`bugo@bugotech.cn`。 -To report any found security issues or vulnerabilities, please send a mail to `loolly@aliyun.com`. \ No newline at end of file +To report any found security issues or vulnerabilities, please send a mail to `bugo@bugotech.cn`. \ No newline at end of file diff --git a/bin/javadoc.sh b/bin/javadoc.sh index b9aaa3914757b117124ed1bf637479edc2678f14..0341e1d221d9087ac580c13a1813c8e0bc15c420 100755 --- a/bin/javadoc.sh +++ b/bin/javadoc.sh @@ -1,3 +1,11 @@ #!/bin/bash -exec mvn javadoc:javadoc +#exec mvn javadoc:javadoc + +# 多模块聚合文档,生成在target/site/apidocs +exec mvn javadoc:aggregate + +bin_home="$(dirname ${BASH_SOURCE[0]})" + +# 拷贝自定义的index.html到聚合文档目录 +cp -vf $bin_home/../docs/apidocs/index.html $bin_home/../target/reports/apidocs/ diff --git a/bin/push_dev.sh b/bin/push_dev.sh index 2e7f969360d024adfdf95ae1f190dca5ba6f17b3..5bca1c384cb1dfb562044dd014f6e57b97c3c5ef 100755 --- a/bin/push_dev.sh +++ b/bin/push_dev.sh @@ -3,7 +3,11 @@ echo -e "\033[32mCheckout to v5-dev\033[0m" git checkout v5-dev -echo -e "\033[32mPush to origin v5-dev\033[0m" +echo -e "\033[32mPush to Github(origin) v5-dev\033[0m" git push origin v5-dev -echo -e "\033[32mPush to osc v5-dev\033[0m" + +echo -e "\033[32mPush to Gitee v5-dev\033[0m" git push osc v5-dev + +echo -e "\033[32mPush to Gitcode v5-dev\033[0m" +git push gitcode v5-dev diff --git a/bin/push_master.sh b/bin/push_master.sh index 729541c981bc8cc106936fda74ce66f9f71956df..683910d49b5aeb868413ae14e021b58c7c1d4899 100755 --- a/bin/push_master.sh +++ b/bin/push_master.sh @@ -6,7 +6,11 @@ git checkout v5-master echo -e "\033[32mMerge v5-dev branch\033[0m" git merge v5-dev -m 'Prepare release' -echo -e "\033[32mPush to origin v5-master\033[0m" +echo -e "\033[32mPush to Github(origin) v5-master\033[0m" git push origin v5-master -echo -e "\033[32mPush to osc v5-master\033[0m" + +echo -e "\033[32mPush to Gitee v5-master\033[0m" git push osc v5-master + +echo -e "\033[32mPush to Gitcode v5-master\033[0m" +git push gitcode v5-master diff --git a/bin/sync.sh b/bin/sync.sh new file mode 100644 index 0000000000000000000000000000000000000000..393c032470d59c840117ef33b2f554d521f76cc7 --- /dev/null +++ b/bin/sync.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +git checkout v5-dev +git pull osc v5-dev +git pull origin v5-dev +git pull gitcode v5-dev diff --git a/bin/version.txt b/bin/version.txt index 0185a6be924e63de532889825043f0e0d02e997a..027269ea75577984022a9af9ae2f2976fda33986 100755 --- a/bin/version.txt +++ b/bin/version.txt @@ -1 +1 @@ -5.8.0.M5 +5.8.41 diff --git a/docs/apidocs/index.html b/docs/apidocs/index.html new file mode 100644 index 0000000000000000000000000000000000000000..cf1759990e18826fd07e6a23172b383a2a705345 --- /dev/null +++ b/docs/apidocs/index.html @@ -0,0 +1,78 @@ + + + + + + + Document + + + + + + +
+
+ + hutool + +
+
+ + +
+
+
+ +
+ + +
+
+
+ + + diff --git a/docs/js/version.js b/docs/js/version.js index 2ce39747beddc7e6d2185569373690c188b67de8..81406b3bb1c01022f5caad0e1d45a0b79adedde7 100755 --- a/docs/js/version.js +++ b/docs/js/version.js @@ -1 +1 @@ -var version = '5.8.0.M5' \ No newline at end of file +var version = '5.8.41' \ No newline at end of file diff --git a/hutool-ai/pom.xml b/hutool-ai/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..fc6192417065860a13c2248170f3da06d9c64dec --- /dev/null +++ b/hutool-ai/pom.xml @@ -0,0 +1,45 @@ + + + 4.0.0 + + cn.hutool + hutool-parent + 5.8.41 + + + hutool-ai + ${project.artifactId} + Hutool AI大模型封装 + + + cn.hutool.ai + + + + + cn.hutool + hutool-core + ${project.parent.version} + + + cn.hutool + hutool-http + ${project.parent.version} + + + cn.hutool + hutool-log + ${project.parent.version} + compile + + + cn.hutool + hutool-json + ${project.parent.version} + compile + + + + diff --git a/hutool-ai/src/main/java/cn/hutool/ai/AIException.java b/hutool-ai/src/main/java/cn/hutool/ai/AIException.java new file mode 100644 index 0000000000000000000000000000000000000000..57bad86ad1cf7e9b5844a09640c2c44caff41f84 --- /dev/null +++ b/hutool-ai/src/main/java/cn/hutool/ai/AIException.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.hutool.ai; + +import cn.hutool.core.util.StrUtil; + +/** + * 异常处理类 + */ +public class AIException extends RuntimeException { + private static final long serialVersionUID = 1L; + + /** + * 构造 + * + * @param e 异常 + */ + public AIException(final Throwable e) { + super(e); + } + + /** + * 构造 + * + * @param message 消息 + */ + public AIException(final String message) { + super(message); + } + + /** + * 构造 + * + * @param messageTemplate 消息模板 + * @param params 参数 + */ + public AIException(String messageTemplate, Object... params) { + super(StrUtil.format(messageTemplate, params)); + } + + /** + * 构造 + * + * @param message 消息 + * @param cause 被包装的子异常 + */ + public AIException(final String message, final Throwable cause) { + super(message, cause); + } + + /** + * 构造 + * + * @param message 消息 + * @param cause 被包装的子异常 + * @param enableSuppression 是否启用抑制 + * @param writableStackTrace 堆栈跟踪是否应该是可写的 + */ + public AIException(final String message, final Throwable cause, final boolean enableSuppression, final boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } + + /** + * 构造 + * + * @param throwable 被包装的子异常 + * @param messageTemplate 消息模板 + * @param params 参数 + */ + public AIException(Throwable throwable, String messageTemplate, Object... params) { + super(StrUtil.format(messageTemplate, params), throwable); + } +} diff --git a/hutool-ai/src/main/java/cn/hutool/ai/AIServiceFactory.java b/hutool-ai/src/main/java/cn/hutool/ai/AIServiceFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..23cb87403742a2ae5085a732bb4521e18095626e --- /dev/null +++ b/hutool-ai/src/main/java/cn/hutool/ai/AIServiceFactory.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.hutool.ai; + +import cn.hutool.ai.core.AIConfig; +import cn.hutool.ai.core.AIService; +import cn.hutool.ai.core.AIServiceProvider; +import cn.hutool.core.util.ServiceLoaderUtil; + +import java.util.Map; +import java.util.ServiceLoader; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 创建AIModelService的工厂类 + * + * @author elichow + * @since 5.8.38 + */ +public class AIServiceFactory { + + private static final Map providers = new ConcurrentHashMap<>(); + + // 加载所有 AIModelProvider 实现类 + static { + final ServiceLoader loader = ServiceLoaderUtil.load(AIServiceProvider.class); + for (final AIServiceProvider provider : loader) { + providers.put(provider.getServiceName().toLowerCase(), provider); + } + } + + /** + * 获取AI服务 + * + * @param config AIConfig配置 + * @return AI服务实例 + * @since 5.8.38 + */ + public static AIService getAIService(final AIConfig config) { + return getAIService(config, AIService.class); + } + + /** + * 获取AI服务 + * + * @param config AIConfig配置 + * @param clazz AI服务类 + * @return clazz对应的AI服务类实例 + * @since 5.8.38 + * @param AI服务类 + */ + @SuppressWarnings("unchecked") + public static T getAIService(final AIConfig config, final Class clazz) { + final AIServiceProvider provider = providers.get(config.getModelName().toLowerCase()); + if (provider == null) { + throw new IllegalArgumentException("Unsupported model: " + config.getModelName()); + } + + final AIService service = provider.create(config); + if (!clazz.isInstance(service)) { + throw new AIException("Model service is not of type: " + clazz.getSimpleName()); + } + + return (T) service; + } +} diff --git a/hutool-ai/src/main/java/cn/hutool/ai/AIUtil.java b/hutool-ai/src/main/java/cn/hutool/ai/AIUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..7f4659d629d71272d1eaeef12ba81619dd606942 --- /dev/null +++ b/hutool-ai/src/main/java/cn/hutool/ai/AIUtil.java @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.hutool.ai; + +import cn.hutool.ai.core.AIConfig; +import cn.hutool.ai.core.AIService; +import cn.hutool.ai.core.Message; +import cn.hutool.ai.model.deepseek.DeepSeekService; +import cn.hutool.ai.model.doubao.DoubaoService; +import cn.hutool.ai.model.grok.GrokService; +import cn.hutool.ai.model.hutool.HutoolService; +import cn.hutool.ai.model.openai.OpenaiService; + +import java.util.List; + +/** + * AI工具类 + * + * @author elichow + * @since 5.8.38 + */ +public class AIUtil { + + /** + * 获取AI模型服务,每个大模型提供的功能会不一样,可以调用此方法指定不同AI服务类,调用不同的功能 + * + * @param config 创建的AI服务模型的配置 + * @param clazz AI模型服务类 + * @return AIModelService的实现类实例 + * @since 5.8.38 + * @param AIService实现类 + */ + public static T getAIService(final AIConfig config, final Class clazz) { + return AIServiceFactory.getAIService(config, clazz); + } + + /** + * 获取AI模型服务 + * + * @param config 创建的AI服务模型的配置 + * @return AIModelService 其中只有公共方法 + * @since 5.8.38 + */ + public static AIService getAIService(final AIConfig config) { + return getAIService(config, AIService.class); + } + + /** + * 获取Hutool-AI服务 + * + * @param config 创建的AI服务模型的配置 + * @return HutoolService + * @since 5.8.39 + */ + public static HutoolService getHutoolService(final AIConfig config) { + return getAIService(config, HutoolService.class); + } + + /** + * 获取DeepSeek模型服务 + * + * @param config 创建的AI服务模型的配置 + * @return DeepSeekService + * @since 5.8.38 + */ + public static DeepSeekService getDeepSeekService(final AIConfig config) { + return getAIService(config, DeepSeekService.class); + } + + /** + * 获取Doubao模型服务 + * + * @param config 创建的AI服务模型的配置 + * @return DoubaoService + * @since 5.8.38 + */ + public static DoubaoService getDoubaoService(final AIConfig config) { + return getAIService(config, DoubaoService.class); + } + + /** + * 获取Grok模型服务 + * + * @param config 创建的AI服务模型的配置 + * @return GrokService + * @since 5.8.38 + */ + public static GrokService getGrokService(final AIConfig config) { + return getAIService(config, GrokService.class); + } + + /** + * 获取Openai模型服务 + * + * @param config 创建的AI服务模型的配置 + * @return OpenAIService + * @since 5.8.38 + */ + public static OpenaiService getOpenAIService(final AIConfig config) { + return getAIService(config, OpenaiService.class); + } + + /** + * AI大模型对话功能 + * + * @param config 创建的AI服务模型的配置 + * @param prompt 需要对话的内容 + * @return AI模型返回的Response响应字符串 + * @since 5.8.38 + */ + public static String chat(final AIConfig config, final String prompt) { + return getAIService(config).chat(prompt); + } + + /** + * AI大模型对话功能 + * + * @param config 创建的AI服务模型的配置 + * @param messages 由目前为止的对话组成的消息列表,可以设置role,content。详细参考官方文档 + * @return AI模型返回的Response响应字符串 + * @since 5.8.38 + */ + public static String chat(final AIConfig config, final List messages) { + return getAIService(config).chat(messages); + } + +} diff --git a/hutool-ai/src/main/java/cn/hutool/ai/ModelName.java b/hutool-ai/src/main/java/cn/hutool/ai/ModelName.java new file mode 100644 index 0000000000000000000000000000000000000000..6e680fe76deacb9f4452b2af59de7935a36f8017 --- /dev/null +++ b/hutool-ai/src/main/java/cn/hutool/ai/ModelName.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.hutool.ai; + +/** + * 模型厂商的名称(不指具体的模型) + * + * @author elichow + * @since 5.8.38 + */ +public enum ModelName { + + /** + * hutool + */ + HUTOOL("hutool"), + /** + * deepSeek + */ + DEEPSEEK("deepSeek"), + /** + * openai + */ + OPENAI("openai"), + /** + * doubao + */ + DOUBAO("doubao"), + /** + * grok + */ + GROK("grok"), + /** + * ollama + */ + OLLAMA("ollama"); + + private final String value; + + ModelName(final String value) { + this.value = value; + } + + /** + * 获取值 + * + * @return 值 + */ + public String getValue() { + return value; + } +} diff --git a/hutool-ai/src/main/java/cn/hutool/ai/Models.java b/hutool-ai/src/main/java/cn/hutool/ai/Models.java new file mode 100644 index 0000000000000000000000000000000000000000..c1dd6266d1f6c91ddf0ef74551df924eda23f538 --- /dev/null +++ b/hutool-ai/src/main/java/cn/hutool/ai/Models.java @@ -0,0 +1,210 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.hutool.ai; + +/** + * 各模型厂商包含的model(指具体的模型) + * + * @author elichow + * @since 5.8.38 + */ +public class Models { + + + // Hutool的模型 + public enum Hutool { + HUTOOL("hutool"); + + private final String model; + + Hutool(String model) { + this.model = model; + } + + public String getModel() { + return model; + } + } + + // DeepSeek的模型 + public enum DeepSeek { + DEEPSEEK_CHAT("deepseek-chat"), + DEEPSEEK_REASONER("deepseek-reasoner"); + + private final String model; + + DeepSeek(String model) { + this.model = model; + } + + public String getModel() { + return model; + } + } + + // Openai的模型 + public enum Openai { + GPT_4_5_PREVIEW("gpt-4.5-preview"), + GPT_4O("gpt-4o"), + CHATGPT_4O_LATEST("chatgpt-4o-latest"), + GPT_4O_MINI("gpt-4o-mini"), + O1("o1"), + O1_MINI("o1-mini"), + O1_PREVIEW("o1-preview"), + O3_MINI("o3-mini"), + GPT_4O_REALTIME_PREVIEW("gpt-4o-realtime-preview"), + GPT_4O_MINI_REALTIME_PREVIEW("gpt-4o-mini-realtime-preview"), + GPT_4O_AUDIO_PREVIEW("gpt-4o-audio-preview"), + GPT_4O_MINI_AUDIO_PREVIEW("gpt-4o-mini-audio-preview"), + GPT_4_TURBO("gpt-4-turbo"), + GPT_4_TURBO_PREVIEW("gpt-4-turbo-preview"), + GPT_4("gpt-4"), + GPT_3_5_TURBO_0125("gpt-3.5-turbo-0125"), + GPT_3_5_TURBO("gpt-3.5-turbo"), + GPT_3_5_TURBO_1106("gpt-3.5-turbo-1106"), + GPT_3_5_TURBO_INSTRUCT("gpt-3.5-turbo-instruct"), + DALL_E_3("dall-e-3"), + DALL_E_2("dall-e-2"), + TTS_1("tts-1"), + TTS_1_HD("tts-1-hd"), + WHISPER_1("whisper-1"), + TEXT_EMBEDDING_3_LARGE("text-embedding-3-large"), + TEXT_EMBEDDING_3_SMALL("text-embedding-3-small"), + TEXT_EMBEDDING_ADA_002("text-embedding-ada-002"), + OMNI_MODERATION_LATEST("omni-moderation-latest"), + OMNI_MODERATION_2024_09_26("omni-moderation-2024-09-26"), + TEXT_MODERATION_LATEST("text-moderation-latest"), + TEXT_MODERATION_STABLE("text-moderation-stable"), + TEXT_MODERATION_007("text-moderation-007"), + BABBAGE_002("babbage-002"), + DAVINCI_002("davinci-002"); + + private final String model; + + Openai(String model) { + this.model = model; + } + + public String getModel() { + return model; + } + } + + // Doubao的模型 + public enum Doubao { + DOUBAO_1_5_PRO_32K("doubao-1.5-pro-32k-250115"), + DOUBAO_1_5_PRO_256K("doubao-1.5-pro-256k-250115"), + DOUBAO_1_5_LITE_32K("doubao-1.5-lite-32k-250115"), + DEEPSEEK_R1("deepseek-r1-250120"), + DEEPSEEK_R1_DISTILL_QWEN_32B("deepseek-r1-distill-qwen-32b-250120"), + DEEPSEEK_R1_DISTILL_QWEN_7B("deepseek-r1-distill-qwen-7b-250120"), + DEEPSEEK_V3("deepseek-v3-241226"), + DOUBAO_PRO_4K_240515("doubao-pro-4k-240515"), + DOUBAO_PRO_4K_CHARACTER_240728("doubao-pro-4k-character-240728"), + DOUBAO_PRO_4K_FUNCTIONCALL_240615("doubao-pro-4k-functioncall-240615"), + DOUBAO_PRO_4K_BROWSING_240524("doubao-pro-4k-browsing-240524"), + DOUBAO_PRO_32K_241215("doubao-pro-32k-241215"), + DOUBAO_PRO_32K_FUNCTIONCALL_241028("doubao-pro-32k-functioncall-241028"), + DOUBAO_PRO_32K_BROWSING_241115("doubao-pro-32k-browsing-241115"), + DOUBAO_PRO_32K_CHARACTER_241215("doubao-pro-32k-character-241215"), + DOUBAO_PRO_128K_240628("doubao-pro-128k-240628"), + DOUBAO_PRO_256K_240828("doubao-pro-256k-240828"), + DOUBAO_LITE_4K_240328("doubao-lite-4k-240328"), + DOUBAO_LITE_4K_PRETRAIN_CHARACTER_240516("doubao-lite-4k-pretrain-character-240516"), + DOUBAO_LITE_32K_240828("doubao-lite-32k-240828"), + DOUBAO_LITE_32K_CHARACTER_241015("doubao-lite-32k-character-241015"), + DOUBAO_LITE_128K_240828("240828"), + MOONSHOT_V1_8K("moonshot-v1-8k"), + MOONSHOT_V1_32K("moonshot-v1-32k"), + MOONSHOT_V1_128K("moonshot-v1-128k"), + CHATGLM3_130B_FC("chatglm3-130b-fc-v1.0"), + CHATGLM3_130_FIN("chatglm3-130-fin-v1.0-update"), + MISTRAL_7B("mistral-7b-instruct-v0.2"), + DOUBAO_1_5_VISION_PRO_32K("doubao-1.5-vision-pro-32k-250115"), + DOUBAO_VISION_PRO_32K("doubao-vision-pro-32k-241008"), + DOUBAO_VISION_LITE_32K("doubao-vision-lite-32k-241015"), + DOUBAO_EMBEDDING_LARGE("doubao-embedding-large-text-240915"), + DOUBAO_EMBEDDING_TEXT_240715("doubao-embedding-text-240715"), + DOUBAO_EMBEDDING_VISION("doubao-embedding-vision-241215"), + DOUBAO_SEEDREAM_3_0_T2I("doubao-seedream-3-0-t2i-250415"), + Doubao_Seedance_1_0_lite_t2v("doubao-seedance-1-0-lite-t2v-250428"), + Doubao_Seedance_1_0_lite_i2v("doubao-seedance-1-0-lite-i2v-250428"), + Wan2_1_14B_t2v("wan2-1-14b-t2v-250225"), + Wan2_1_14B_i2v("wan2-1-14b-i2v-250225"); + + private final String model; + + Doubao(String model) { + this.model = model; + } + + public String getModel() { + return model; + } + } + + // Grok的模型 + public enum Grok { + GROK_3_BETA_LATEST("grok-3-beta"), + GROK_3_BETA("grok-3-beta"), + GROK_3("grok-3-beta"), + GROK_3_MINI_FAST_LATEST("grok-3-mini-fast-beta"), + GROK_3_MINI_FAST_BETA("grok-3-mini-fast-beta"), + GROK_3_MINI_FAST("grok-3-mini-fast-beta"), + GROK_3_FAST_LATEST("grok-3-fast-beta"), + GROK_3_FAST_BETA("grok-3-fast-beta"), + GROK_3_FAST("grok-3-fast-beta"), + GROK_3_MINI_LATEST("grok-3-mini-beta"), + GROK_3_MINI_BETA("grok-3-mini-beta"), + GROK_3_MINI("grok-3-mini-beta"), + GROK_2_IMAGE_LATEST("grok-2-image-1212"), + GROK_2_IMAGE("grok-2-image-1212"), + GROK_2_IMAGE_1212("grok-2-image-1212"), + grok_2_latest("grok-2-1212"), + GROK_2("grok-2-1212"), + GROK_2_1212("grok-2-1212"), + GROK_2_VISION_1212("grok-2-vision-1212"), + GROK_BETA("grok-beta"), + GROK_VISION_BETA("grok-vision-beta"); + + private final String model; + + Grok(String model) { + this.model = model; + } + + public String getModel() { + return model; + } + } + + // Ollama的模型 + public enum Ollama { + QWEN3_32B("qwen3:32b"); + + private final String model; + + Ollama(String model) { + this.model = model; + } + + public String getModel() { + return model; + } + } + +} diff --git a/hutool-ai/src/main/java/cn/hutool/ai/core/AIConfig.java b/hutool-ai/src/main/java/cn/hutool/ai/core/AIConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..8efbbca591b875991e2b07e6e3e763f8e6d1035a --- /dev/null +++ b/hutool-ai/src/main/java/cn/hutool/ai/core/AIConfig.java @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.hutool.ai.core; + +import java.util.Map; + +/** + * AI配置类 + * + * @author elichow + * @since 5.8.38 + */ +public interface AIConfig { + + /** + * 获取模型(厂商)名称 + * + * @return 模型(厂商)名称 + * @since 5.8.38 + */ + default String getModelName() { + return this.getClass().getSimpleName(); + } + + /** + * 设置apiKey + * + * @param apiKey apiKey + * @since 5.8.38 + */ + void setApiKey(String apiKey); + + /** + * 获取apiKey + * + * @return apiKey + * @since 5.8.38 + */ + String getApiKey(); + + /** + * 设置apiUrl + * + * @param apiUrl api请求地址 + * @since 5.8.38 + */ + void setApiUrl(String apiUrl); + + /** + * 获取apiUrl + * + * @return apiUrl + * @since 5.8.38 + */ + String getApiUrl(); + + /** + * 设置model + * + * @param model model + * @since 5.8.38 + */ + void setModel(String model); + + /** + * 返回model + * + * @return model + * @since 5.8.38 + */ + String getModel(); + + /** + * 设置动态参数 + * + * @param key 参数字段 + * @param value 参数值 + * @since 5.8.38 + */ + void putAdditionalConfigByKey(String key, Object value); + + /** + * 获取动态参数 + * + * @param key 参数字段 + * @return 参数值 + * @since 5.8.38 + */ + Object getAdditionalConfigByKey(String key); + + /** + * 获取动态参数列表 + * + * @return 参数列表Map + * @since 5.8.38 + */ + Map getAdditionalConfigMap(); + + /** + * 设置连接超时时间 + * + * @param timeout 连接超时时间 + * @since 5.8.39 + */ + void setTimeout(int timeout); + + /** + * 获取连接超时时间 + * + * @return timeout + * @since 5.8.39 + */ + int getTimeout(); + + /** + * 设置读取超时时间 + * + * @param readTimeout 连接超时时间 + * @since 5.8.39 + */ + void setReadTimeout(int readTimeout); + + /** + * 获取读取超时时间 + * + * @return readTimeout + * @since 5.8.39 + */ + int getReadTimeout(); + +} diff --git a/hutool-ai/src/main/java/cn/hutool/ai/core/AIConfigBuilder.java b/hutool-ai/src/main/java/cn/hutool/ai/core/AIConfigBuilder.java new file mode 100644 index 0000000000000000000000000000000000000000..bedd79071ae92b6ac26ef28c18403efcaa39ee1c --- /dev/null +++ b/hutool-ai/src/main/java/cn/hutool/ai/core/AIConfigBuilder.java @@ -0,0 +1,172 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.hutool.ai.core; + +import java.lang.reflect.Constructor; + +/** + * 用于AIConfig的创建,创建同时支持链式设置参数 + * + * @author elichow + * @since 5.8.38 + */ +public class AIConfigBuilder { + + private final AIConfig config; + + /** + * 构造 + * + * @param modelName 模型厂商的名称(注意不是指具体的模型) + */ + public AIConfigBuilder(final String modelName) { + try { + // 获取配置类 + final Class configClass = AIConfigRegistry.getConfigClass(modelName); + if (configClass == null) { + throw new IllegalArgumentException("Unsupported model: " + modelName); + } + + // 使用反射创建实例 + final Constructor constructor = configClass.getDeclaredConstructor(); + config = constructor.newInstance(); + } catch (final Exception e) { + throw new RuntimeException("Failed to create AIConfig instance", e); + } + } + + /** + * 设置apiKey + * + * @param apiKey apiKey + * @return config + * @since 5.8.38 + */ + public synchronized AIConfigBuilder setApiKey(final String apiKey) { + if (apiKey != null) { + config.setApiKey(apiKey); + } + return this; + } + + /** + * 设置AI模型请求API接口的地址,不设置为默认值 + * + * @param apiUrl API接口地址 + * @return config + * @since 5.8.38 + */ + public synchronized AIConfigBuilder setApiUrl(final String apiUrl) { + if (apiUrl != null) { + config.setApiUrl(apiUrl); + } + return this; + } + + /** + * 设置具体的model,不设置为默认值 + * + * @param model 具体model的名称 + * @return config + * @since 5.8.38 + */ + public synchronized AIConfigBuilder setModel(final String model) { + if (model != null) { + config.setModel(model); + } + return this; + } + + /** + * 动态设置Request请求体中的属性字段,每个模型功能支持的字段请参照对应的官方文档 + * + * @param key Request中的支持的属性名 + * @param value 设置的属性值 + * @return config + * @since 5.8.38 + */ + public AIConfigBuilder putAdditionalConfig(final String key, final Object value) { + if (value != null) { + config.putAdditionalConfigByKey(key, value); + } + return this; + } + + /** + * 设置连接超时时间,不设置为默认值 + * + * @param timeout 超时时间 + * @return config + * @since 5.8.39 + * @deprecated 请使用 {@link #setTimeout(int)} + */ + @Deprecated + public AIConfigBuilder setTimout(final int timeout) { + return setTimeout(timeout); + } + + /** + * 设置连接超时时间,不设置为默认值 + * + * @param timeout 超时时间 + * @return config + * @since 5.8.41 + */ + public synchronized AIConfigBuilder setTimeout(final int timeout) { + if (timeout > 0) { + config.setTimeout(timeout); + } + return this; + } + + /** + * 设置读取超时时间,不设置为默认值 + * + * @param readTimout 取超时时间 + * @return config + * @since 5.8.39 + * @deprecated 请使用 {@link #setReadTimeout(int)} + */ + @Deprecated + public AIConfigBuilder setReadTimout(final int readTimout) { + return setReadTimeout(readTimout); + } + + /** + * 设置读取超时时间,不设置为默认值 + * + * @param readTimeout 取超时时间 + * @return config + * @since 5.8.41 + */ + public synchronized AIConfigBuilder setReadTimeout(final int readTimeout) { + if (readTimeout > 0) { + config.setReadTimeout(readTimeout); + } + return this; + } + + /** + * 返回config实例 + * + * @return config + * @since 5.8.38 + */ + public AIConfig build() { + return config; + } +} diff --git a/hutool-ai/src/main/java/cn/hutool/ai/core/AIConfigRegistry.java b/hutool-ai/src/main/java/cn/hutool/ai/core/AIConfigRegistry.java new file mode 100644 index 0000000000000000000000000000000000000000..61664404950c343dd0976f67a5e0378e737ad0e1 --- /dev/null +++ b/hutool-ai/src/main/java/cn/hutool/ai/core/AIConfigRegistry.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.hutool.ai.core; + +import cn.hutool.core.util.ServiceLoaderUtil; + +import java.util.Map; +import java.util.ServiceLoader; +import java.util.concurrent.ConcurrentHashMap; + +/** + * AIConfig实现类的加载器 + * + * @author elichow + * @since 5.8.38 + */ +public class AIConfigRegistry { + + private static final Map> configClasses = new ConcurrentHashMap<>(); + + // 加载所有 AIConfig 实现类 + static { + final ServiceLoader loader = ServiceLoaderUtil.load(AIConfig.class); + for (final AIConfig config : loader) { + configClasses.put(config.getModelName().toLowerCase(), config.getClass()); + } + } + + /** + * 根据模型名称获取AIConfig实现类 + * + * @param modelName 模型名称 + * @return AIConfig实现类 + */ + public static Class getConfigClass(final String modelName) { + return configClasses.get(modelName.toLowerCase()); + } +} diff --git a/hutool-ai/src/main/java/cn/hutool/ai/core/AIService.java b/hutool-ai/src/main/java/cn/hutool/ai/core/AIService.java new file mode 100644 index 0000000000000000000000000000000000000000..3805f0e4a53de1392c2f25148b28cefedf54ff7d --- /dev/null +++ b/hutool-ai/src/main/java/cn/hutool/ai/core/AIService.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.hutool.ai.core; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +/** + * 模型公共的API功能,特有的功能在model.xx.XXService下定义 + * + * @author elichow + * @since 5.8.38 + */ +public interface AIService { + + /** + * 对话 + * + * @param prompt user题词 + * @return AI回答 + * @since 5.8.38 + */ + default String chat(String prompt){ + final List messages = new ArrayList<>(); + messages.add(new Message("system", "You are a helpful assistant")); + messages.add(new Message("user", prompt)); + return chat(messages); + } + + /** + * 对话-SSE流式输出 + * @param prompt user题词 + * @param callback 流式数据回调函数 + * @since 5.8.39 + */ + default void chat(String prompt, final Consumer callback){ + final List messages = new ArrayList<>(); + messages.add(new Message("system", "You are a helpful assistant")); + messages.add(new Message("user", prompt)); + chat(messages, callback); + } + + /** + * 对话 + * + * @param messages 由目前为止的对话组成的消息列表,可以设置role,content。详细参考官方文档 + * @return AI回答 + * @since 5.8.38 + */ + String chat(final List messages); + + + /** + * 对话-SSE流式输出 + * @param messages 由目前为止的对话组成的消息列表,可以设置role,content。详细参考官方文档 + * @param callback 流式数据回调函数 + * @since 5.8.39 + */ + void chat(final List messages, final Consumer callback); +} diff --git a/hutool-ai/src/main/java/cn/hutool/ai/core/AIServiceProvider.java b/hutool-ai/src/main/java/cn/hutool/ai/core/AIServiceProvider.java new file mode 100644 index 0000000000000000000000000000000000000000..341f43a7f38d234ddc84e43b4e8d6859770cddee --- /dev/null +++ b/hutool-ai/src/main/java/cn/hutool/ai/core/AIServiceProvider.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.hutool.ai.core; + +/** + * 用于加载AI服务,每一个通过SPI创建的AI服务都要实现此接口 + * + * @author elichow + * @since 5.8.38 + */ +public interface AIServiceProvider { + + /** + * 获取AI服务名称 + * + * @return AI服务名称 + * @since 5.8.38 + */ + String getServiceName(); + + /** + * 创建AI服务实例 + * + * @param config AIConfig配置 + * @param AIService实现类 + * @return AI服务实例 + * @since 5.8.38 + */ + T create(final AIConfig config); +} diff --git a/hutool-ai/src/main/java/cn/hutool/ai/core/BaseAIService.java b/hutool-ai/src/main/java/cn/hutool/ai/core/BaseAIService.java new file mode 100644 index 0000000000000000000000000000000000000000..1106e8bf7561f22f4d702cf5b06c2b29fac40847 --- /dev/null +++ b/hutool-ai/src/main/java/cn/hutool/ai/core/BaseAIService.java @@ -0,0 +1,158 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.hutool.ai.core; + +import cn.hutool.ai.AIException; +import cn.hutool.http.*; +import cn.hutool.json.JSONUtil; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.Map; +import java.util.function.Consumer; + +/** + * 基础AIService,包含基公共参数和公共方法 + * + * @author elichow + * @since 5.8.38 + */ +public class BaseAIService { + + protected final AIConfig config; + + /** + * 构造方法 + * + * @param config AI配置 + */ + public BaseAIService(final AIConfig config) { + this.config = config; + } + + /** + * 发送Get请求 + * @param endpoint 请求节点 + * @return 请求响应 + */ + protected HttpResponse sendGet(final String endpoint) { + //链式构建请求 + try { + //设置超时3分钟 + return HttpRequest.get(config.getApiUrl() + endpoint) + .header(Header.ACCEPT, "application/json") + .header(Header.AUTHORIZATION, "Bearer " + config.getApiKey()) + .timeout(config.getTimeout()) + .execute(); + } catch (final AIException e) { + throw new AIException("Failed to send GET request: " + e.getMessage(), e); + } + } + + /** + * 发送Post请求 + * @param endpoint 请求节点 + * @param paramJson 请求参数json + * @return 请求响应 + */ + protected HttpResponse sendPost(final String endpoint, final String paramJson) { + //链式构建请求 + try { + return HttpRequest.post(config.getApiUrl() + endpoint) + .header(Header.CONTENT_TYPE, "application/json") + .header(Header.ACCEPT, "application/json") + .header(Header.AUTHORIZATION, "Bearer " + config.getApiKey()) + .body(paramJson) + .timeout(config.getTimeout()) + .execute(); + } catch (final AIException e) { + throw new AIException("Failed to send POST request:" + e.getMessage(), e); + } + + } + + /** + * 发送表单请求 + * @param endpoint 请求节点 + * @param paramMap 请求参数map + * @return 请求响应 + */ + protected HttpResponse sendFormData(final String endpoint, final Map paramMap) { + //链式构建请求 + try { + //设置超时3分钟 + return HttpRequest.post(config.getApiUrl() + endpoint) + .header(Header.CONTENT_TYPE, "multipart/form-data") + .header(Header.ACCEPT, "application/json") + .header(Header.AUTHORIZATION, "Bearer " + config.getApiKey()) + .form(paramMap) + .timeout(config.getTimeout()) + .execute(); + } catch (final AIException e) { + throw new AIException("Failed to send POST request:" + e.getMessage(), e); + } + } + + /** + * 支持流式返回的 POST 请求 + * + * @param endpoint 请求地址 + * @param paramMap 请求参数 + * @param callback 流式数据回调函数 + */ + protected void sendPostStream(final String endpoint, final Map paramMap, Consumer callback) { + HttpURLConnection connection = null; + try { + // 创建连接 + URL apiUrl = new URL(config.getApiUrl() + endpoint); + connection = (HttpURLConnection) apiUrl.openConnection(); + connection.setRequestMethod(Method.POST.name()); + connection.setRequestProperty(Header.CONTENT_TYPE.getValue(), "application/json"); + connection.setRequestProperty(Header.AUTHORIZATION.getValue(), "Bearer " + config.getApiKey()); + connection.setDoOutput(true); + //5分钟 + connection.setReadTimeout(config.getReadTimeout()); + //3分钟 + connection.setConnectTimeout(config.getTimeout()); + // 发送请求体 + try (OutputStream os = connection.getOutputStream()) { + String jsonInputString = JSONUtil.toJsonStr(paramMap); + os.write(jsonInputString.getBytes()); + os.flush(); + } + + // 读取流式响应 + try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) { + String line; + while ((line = reader.readLine()) != null) { + // 调用回调函数处理每一行数据 + callback.accept(line); + } + } + } catch (Exception e) { + callback.accept("{\"error\": \"" + e.getMessage() + "\"}"); + } finally { + // 关闭连接 + if (connection != null) { + connection.disconnect(); + } + } + } +} diff --git a/hutool-ai/src/main/java/cn/hutool/ai/core/BaseConfig.java b/hutool-ai/src/main/java/cn/hutool/ai/core/BaseConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..3b99e9059c1695730c33dda5df2f03b277ff8666 --- /dev/null +++ b/hutool-ai/src/main/java/cn/hutool/ai/core/BaseConfig.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.hutool.ai.core; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Config基础类,定义模型配置的基本属性 + * + * @author elichow + * @since 5.8.38 + */ +public class BaseConfig implements AIConfig { + + //apiKey + protected volatile String apiKey; + //API请求地址 + protected volatile String apiUrl; + //具体模型 + protected volatile String model; + //动态扩展字段 + protected Map additionalConfig = new ConcurrentHashMap<>(); + //连接超时时间 + protected volatile int timeout = 180000; + //读取超时时间 + protected volatile int readTimeout = 300000; + + @Override + public void setApiKey(final String apiKey) { + this.apiKey = apiKey; + } + + @Override + public String getApiKey() { + return apiKey; + } + + @Override + public void setApiUrl(final String apiUrl) { + this.apiUrl = apiUrl; + } + + @Override + public String getApiUrl() { + return apiUrl; + } + + @Override + public void setModel(final String model) { + this.model = model; + } + + @Override + public String getModel() { + return model; + } + + @Override + public void putAdditionalConfigByKey(final String key, final Object value) { + this.additionalConfig.put(key, value); + } + + @Override + public Object getAdditionalConfigByKey(final String key) { + return additionalConfig.get(key); + } + + @Override + public Map getAdditionalConfigMap() { + return new ConcurrentHashMap<>(additionalConfig); + } + + @Override + public int getTimeout() { + return timeout; + } + + @Override + public void setTimeout(final int timeout) { + this.timeout = timeout; + } + + @Override + public int getReadTimeout() { + return readTimeout; + } + + @Override + public void setReadTimeout(final int readTimeout) { + this.readTimeout = readTimeout; + } +} diff --git a/hutool-ai/src/main/java/cn/hutool/ai/core/Message.java b/hutool-ai/src/main/java/cn/hutool/ai/core/Message.java new file mode 100644 index 0000000000000000000000000000000000000000..2c60e6f72690e9341c62371541f41daa3c2338ea --- /dev/null +++ b/hutool-ai/src/main/java/cn/hutool/ai/core/Message.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.hutool.ai.core; + +/** + * 公共Message类 + * + * @author elichow + * @since 5.8.38 + */ +public class Message { + //角色 注意:如果设置系统消息,请放在messages列表的第一位 + private String role; + //内容 + private Object content; + + /** + * 构造 + */ + public Message() { + } + + /** + * 构造 + * + * @param role 角色 + * @param content 内容 + */ + public Message(final String role, final Object content) { + this.role = role; + this.content = content; + } + + /** + * 设置角色 + * + * @param role 角色 + */ + public void setRole(final String role) { + this.role = role; + } + + /** + * 获取角色 + * + * @return 角色 + */ + public String getRole() { + return role; + } + + /** + * 获取内容 + * + * @return 内容 + */ + public Object getContent() { + return content; + } + + /** + * 设置内容 + * + * @param content 内容 + */ + public void setContent(final Object content) { + this.content = content; + } +} diff --git a/hutool-ai/src/main/java/cn/hutool/ai/core/package-info.java b/hutool-ai/src/main/java/cn/hutool/ai/core/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..855a610343951820eb252250eedbd35f7688f4fb --- /dev/null +++ b/hutool-ai/src/main/java/cn/hutool/ai/core/package-info.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * AI相关基础类 + * + * @author elichow + * @since 5.8.38 + */ + +package cn.hutool.ai.core; diff --git a/hutool-ai/src/main/java/cn/hutool/ai/model/deepseek/DeepSeekCommon.java b/hutool-ai/src/main/java/cn/hutool/ai/model/deepseek/DeepSeekCommon.java new file mode 100644 index 0000000000000000000000000000000000000000..c712b488ae1490ccb0f3c2ce37526ba81101c7da --- /dev/null +++ b/hutool-ai/src/main/java/cn/hutool/ai/model/deepseek/DeepSeekCommon.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.hutool.ai.model.deepseek; + +/** + * deepSeek公共类 + * + * @author elichow + * @since 5.8.38 + */ +public class DeepSeekCommon { + +} diff --git a/hutool-ai/src/main/java/cn/hutool/ai/model/deepseek/DeepSeekConfig.java b/hutool-ai/src/main/java/cn/hutool/ai/model/deepseek/DeepSeekConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..8497d6fd62b7efc5eaf3738834d4d87cadd6b822 --- /dev/null +++ b/hutool-ai/src/main/java/cn/hutool/ai/model/deepseek/DeepSeekConfig.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.hutool.ai.model.deepseek; + +import cn.hutool.ai.Models; +import cn.hutool.ai.core.BaseConfig; + +/** + * DeepSeek配置类,初始化API接口地址,设置默认的模型 + * + * @author elichow + * @since 5.8.38 + */ +public class DeepSeekConfig extends BaseConfig { + + private final String API_URL = "https://api.deepseek.com"; + + private final String DEFAULT_MODEL = Models.DeepSeek.DEEPSEEK_CHAT.getModel(); + + public DeepSeekConfig() { + setApiUrl(API_URL); + setModel(DEFAULT_MODEL); + } + + public DeepSeekConfig(String apiKey) { + this(); + setApiKey(apiKey); + } + + @Override + public String getModelName() { + return "deepSeek"; + } + +} diff --git a/hutool-ai/src/main/java/cn/hutool/ai/model/deepseek/DeepSeekProvider.java b/hutool-ai/src/main/java/cn/hutool/ai/model/deepseek/DeepSeekProvider.java new file mode 100644 index 0000000000000000000000000000000000000000..6fe344d928138e6015caf846ddd861eef5e318d4 --- /dev/null +++ b/hutool-ai/src/main/java/cn/hutool/ai/model/deepseek/DeepSeekProvider.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.hutool.ai.model.deepseek; + +import cn.hutool.ai.core.AIConfig; +import cn.hutool.ai.core.AIServiceProvider; + +/** + * 创建DeepSeek服务实现类 + * + * @author elichow + * @since 5.8.38 + */ +public class DeepSeekProvider implements AIServiceProvider { + + @Override + public String getServiceName() { + return "deepSeek"; + } + + @Override + public DeepSeekService create(final AIConfig config) { + return new DeepSeekServiceImpl(config); + } +} diff --git a/hutool-ai/src/main/java/cn/hutool/ai/model/deepseek/DeepSeekService.java b/hutool-ai/src/main/java/cn/hutool/ai/model/deepseek/DeepSeekService.java new file mode 100644 index 0000000000000000000000000000000000000000..d537a078d46427556a24a9c741e71a07516b4ec4 --- /dev/null +++ b/hutool-ai/src/main/java/cn/hutool/ai/model/deepseek/DeepSeekService.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.hutool.ai.model.deepseek; + +import cn.hutool.ai.core.AIService; +import java.util.function.Consumer; + +/** + * deepSeek支持的扩展接口 + * + * @author elichow + * @since 5.8.38 + */ +public interface DeepSeekService extends AIService { + + /** + * 模型beta功能 + * + * @param prompt 题词 + * @return AI的回答 + * @since 5.8.38 + */ + String beta(String prompt); + + /** + * 模型beta功能-SSE流式输出 + * @param prompt 题词 + * @param callback 流式数据回调函数 + * @since 5.8.39 + */ + void beta(String prompt, final Consumer callback); + + /** + * 列出所有模型列表 + * + * @return model列表 + * @since 5.8.38 + */ + String models(); + + /** + * 查询余额 + * + * @return 余额 + * @since 5.8.38 + */ + String balance(); +} diff --git a/hutool-ai/src/main/java/cn/hutool/ai/model/deepseek/DeepSeekServiceImpl.java b/hutool-ai/src/main/java/cn/hutool/ai/model/deepseek/DeepSeekServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..624b97aabf584f78e262163531f8b3dad5b659d2 --- /dev/null +++ b/hutool-ai/src/main/java/cn/hutool/ai/model/deepseek/DeepSeekServiceImpl.java @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.hutool.ai.model.deepseek; + +import cn.hutool.ai.core.AIConfig; +import cn.hutool.ai.core.BaseAIService; +import cn.hutool.ai.core.Message; +import cn.hutool.core.thread.ThreadUtil; +import cn.hutool.http.HttpResponse; +import cn.hutool.json.JSONUtil; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +/** + * DeepSeek服务,AI具体功能的实现 + * + * @author elichow + * @since 5.8.38 + */ +public class DeepSeekServiceImpl extends BaseAIService implements DeepSeekService { + + //对话补全 + private final String CHAT_ENDPOINT = "/chat/completions"; + //FIM补全(beta) + private final String BETA_ENDPOINT = "/beta/completions"; + //列出模型 + private final String MODELS_ENDPOINT = "/models"; + //余额查询 + private final String BALANCE_ENDPOINT = "/user/balance"; + + /** + * 构造函数 + * + * @param config AI配置 + */ + public DeepSeekServiceImpl(final AIConfig config) { + //初始化DeepSeek客户端 + super(config); + } + + @Override + public String chat(final List messages) { + final String paramJson = buildChatRequestBody(messages); + final HttpResponse response = sendPost(CHAT_ENDPOINT, paramJson); + return response.body(); + } + + @Override + public void chat(final List messages, final Consumer callback) { + Map paramMap = buildChatStreamRequestBody(messages); + ThreadUtil.newThread(() -> sendPostStream(CHAT_ENDPOINT, paramMap, callback::accept), "deepseek-chat-sse").start(); + } + + @Override + public String beta(final String prompt) { + final String paramJson = buildBetaRequestBody(prompt); + final HttpResponse response = sendPost(BETA_ENDPOINT, paramJson); + return response.body(); + } + + @Override + public void beta(final String prompt, final Consumer callback) { + Map paramMap = buildBetaStreamRequestBody(prompt); + ThreadUtil.newThread(() -> sendPostStream(BETA_ENDPOINT, paramMap, callback::accept), "deepseek-beta-sse").start(); + } + + @Override + public String models() { + final HttpResponse response = sendGet(MODELS_ENDPOINT); + return response.body(); + } + + @Override + public String balance() { + final HttpResponse response = sendGet(BALANCE_ENDPOINT); + return response.body(); + } + + // 构建chat请求体 + private String buildChatRequestBody(final List messages) { + //使用JSON工具 + final Map paramMap = new HashMap<>(); + paramMap.put("model", config.getModel()); + paramMap.put("messages", messages); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + + return JSONUtil.toJsonStr(paramMap); + } + + // 构建chatStream请求体 + private Map buildChatStreamRequestBody(final List messages) { + //使用JSON工具 + final Map paramMap = new HashMap<>(); + paramMap.put("stream", true); + paramMap.put("model", config.getModel()); + paramMap.put("messages", messages); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + + return paramMap; + } + + // 构建beta请求体 + private String buildBetaRequestBody(final String prompt) { + // 定义消息结构 + //使用JSON工具 + final Map paramMap = new HashMap<>(); + paramMap.put("model", config.getModel()); + paramMap.put("prompt", prompt); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + + return JSONUtil.toJsonStr(paramMap); + } + + // 构建betaStream请求体 + private Map buildBetaStreamRequestBody(final String prompt) { + //使用JSON工具 + final Map paramMap = new HashMap<>(); + paramMap.put("stream", true); + paramMap.put("model", config.getModel()); + paramMap.put("prompt", prompt); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + + return paramMap; + } + +} diff --git a/hutool-ai/src/main/java/cn/hutool/ai/model/deepseek/package-info.java b/hutool-ai/src/main/java/cn/hutool/ai/model/deepseek/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..99b1ca41469f7b4a204c771d8b646814f41b0e66 --- /dev/null +++ b/hutool-ai/src/main/java/cn/hutool/ai/model/deepseek/package-info.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * 对deepSeek的封装实现 + * + * @author elichow + * @since 5.8.38 + */ + +package cn.hutool.ai.model.deepseek; diff --git a/hutool-ai/src/main/java/cn/hutool/ai/model/doubao/DoubaoCommon.java b/hutool-ai/src/main/java/cn/hutool/ai/model/doubao/DoubaoCommon.java new file mode 100644 index 0000000000000000000000000000000000000000..494f2c66823b19e74a3359b52a445029f5d11653 --- /dev/null +++ b/hutool-ai/src/main/java/cn/hutool/ai/model/doubao/DoubaoCommon.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.hutool.ai.model.doubao; + +/** + * doubao公共类 + * + * @author elichow + * @since 5.8.38 + */ +public class DoubaoCommon { + + //doubao上下文缓存参数 + public enum DoubaoContext { + + SESSION("session"), + COMMON_PREFIX("common_prefix"); + + private final String mode; + + DoubaoContext(String mode) { + this.mode = mode; + } + + public String getMode() { + return mode; + } + } + + //doubao视觉参数 + public enum DoubaoVision { + + AUTO("auto"), + LOW("low"), + HIGH("high"); + + private final String detail; + + DoubaoVision(String detail) { + this.detail = detail; + } + + public String getDetail() { + return detail; + } + } + + //doubao视频生成参数 + public enum DoubaoVideo { + + //宽高比例 + RATIO_16_9("--rt", "16:9"),//[1280, 720] + RATIO_4_3("--rt", "4:3"),//[960, 720] + RATIO_1_1("--rt", "1:1"),//[720, 720] + RATIO_3_4("--rt", "3:4"),//[720, 960] + RATIO_9_16("--rt", "9:16"),//[720, 1280] + RATIO_21_9("--rt", "21:9"),//[1280, 544] + + //生成视频时长 + DURATION_5("--dur", 5),//文生视频,图生视频 + DURATION_10("--dur", 10),//文生视频 + + //帧率,即一秒时间内视频画面数量 + FPS_5("--fps", 24), + + //视频分辨率 + RESOLUTION_5("--rs", "720p"), + + //生成视频是否包含水印 + WATERMARK_TRUE("--wm", true), + WATERMARK_FALSE("--wm", false); + + private final String type; + private final Object value; + + DoubaoVideo(String type, Object value) { + this.type = type; + this.value = value; + } + + public String getType() { + return type; + } + + public Object getValue() { + if (value instanceof String) { + return (String) value; + } else if (value instanceof Integer) { + return (Integer) value; + } else if (value instanceof Boolean) { + return (Boolean) value; + } + return value; + } + + } +} diff --git a/hutool-ai/src/main/java/cn/hutool/ai/model/doubao/DoubaoConfig.java b/hutool-ai/src/main/java/cn/hutool/ai/model/doubao/DoubaoConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..fa7b1c8a52f02a77c02b3b62efe4d210102a3fb1 --- /dev/null +++ b/hutool-ai/src/main/java/cn/hutool/ai/model/doubao/DoubaoConfig.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.hutool.ai.model.doubao; + +import cn.hutool.ai.Models; +import cn.hutool.ai.core.BaseConfig; + +/** + * Doubao配置类,初始化API接口地址,设置默认的模型 + * + * @author elichow + * @since 5.8.38 + */ +public class DoubaoConfig extends BaseConfig { + + private final String API_URL = "https://ark.cn-beijing.volces.com/api/v3"; + + private final String DEFAULT_MODEL = Models.Doubao.DOUBAO_1_5_LITE_32K.getModel(); + + public DoubaoConfig() { + setApiUrl(API_URL); + setModel(DEFAULT_MODEL); + } + + public DoubaoConfig(String apiKey) { + this(); + setApiKey(apiKey); + } + + @Override + public String getModelName() { + return "doubao"; + } + +} diff --git a/hutool-ai/src/main/java/cn/hutool/ai/model/doubao/DoubaoProvider.java b/hutool-ai/src/main/java/cn/hutool/ai/model/doubao/DoubaoProvider.java new file mode 100644 index 0000000000000000000000000000000000000000..46d5be8d2d568c204ffa41414879982765510b8b --- /dev/null +++ b/hutool-ai/src/main/java/cn/hutool/ai/model/doubao/DoubaoProvider.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.hutool.ai.model.doubao; + +import cn.hutool.ai.core.AIConfig; +import cn.hutool.ai.core.AIServiceProvider; + +/** + * 创建Doubap服务实现类 + * + * @author elichow + * @since 5.8.38 + */ +public class DoubaoProvider implements AIServiceProvider { + + @Override + public String getServiceName() { + return "doubao"; + } + + @Override + public DoubaoService create(final AIConfig config) { + return new DoubaoServiceImpl(config); + } +} diff --git a/hutool-ai/src/main/java/cn/hutool/ai/model/doubao/DoubaoService.java b/hutool-ai/src/main/java/cn/hutool/ai/model/doubao/DoubaoService.java new file mode 100644 index 0000000000000000000000000000000000000000..e94ae258f5a403f586263e2aca5ce95dc37d4bd6 --- /dev/null +++ b/hutool-ai/src/main/java/cn/hutool/ai/model/doubao/DoubaoService.java @@ -0,0 +1,274 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.hutool.ai.model.doubao; + +import cn.hutool.ai.core.AIService; +import cn.hutool.ai.core.Message; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +/** + * doubao支持的扩展接口 + * + * @author elichow + * @since 5.8.38 + */ +public interface DoubaoService extends AIService { + + /** + * 图像理解:模型会依据传入的图片信息以及问题,给出回复。 + * + * @param prompt 提问 + * @param images 传入的图片列表地址/或者图片Base64编码图片列表(URI形式) + * @return AI回答 + * @since 5.8.38 + */ + default String chatVision(String prompt, final List images) { + return chatVision(prompt, images, DoubaoCommon.DoubaoVision.AUTO.getDetail()); + } + + /** + * 图像理解-SSE流式输出 + * + * @param prompt 提问 + * @param images 图片列表/或者图片Base64编码图片列表(URI形式) + * @param callback 流式数据回调函数 + * @since 5.8.39 + */ + default void chatVision(String prompt, final List images, final Consumer callback) { + chatVision(prompt, images, DoubaoCommon.DoubaoVision.AUTO.getDetail(), callback); + } + + /** + * 图像理解:模型会依据传入的图片信息以及问题,给出回复。 + * + * @param prompt 提问 + * @param images 图片列表/或者图片Base64编码图片列表(URI形式) + * @param detail 手动设置图片的质量,取值范围high、low、auto,默认为auto + * @return AI回答 + * @since 5.8.38 + */ + String chatVision(String prompt, final List images, String detail); + + /** + * 图像理解-SSE流式输出 + * + * @param prompt 提问 + * @param images 传入的图片列表地址/或者图片Base64编码图片列表(URI形式) + * @param detail 手动设置图片的质量,取值范围high、low、auto,默认为auto + * @param callback 流式数据回调函数 + * @since 5.8.39 + */ + void chatVision(String prompt, final List images, String detail, final Consumer callback); + + /** + * 创建视频生成任务 + * 注意:调用该方法时,配置config中的model为您创建的推理接入点(Endpoint)ID。详细参考官方文档 + * + * @param text 文本提示词 + * @param image 图片/或者图片Base64编码图片(URI形式) + * @param videoParams 视频参数列表 + * @return 生成任务id + * @since 5.8.38 + */ + String videoTasks(String text, String image, final List videoParams); + + /** + * 创建视频生成任务 + * 注意:调用该方法时,配置config中的model为生成视频的模型或者您创建的推理接入点(Endpoint)ID。详细参考官方文档 + * + * @param text 文本提示词 + * @param image 图片/或者图片Base64编码图片(URI形式) + * @return 生成任务id + * @since 5.8.38 + */ + default String videoTasks(String text, String image) { + return videoTasks(text, image, null); + } + + /** + * 查询视频生成任务信息 + * + * @param taskId 通过创建生成视频任务返回的生成任务id + * @return 生成任务信息 + * @since 5.8.38 + */ + String getVideoTasksInfo(String taskId); + + /** + * 文本向量化 + * + * @param input 需要向量化的内容列表,支持中文、英文 + * @return 处理后的向量信息 + * @since 5.8.38 + */ + String embeddingText(String[] input); + + /** + * 图文向量化:仅支持单一文本、单张图片或文本与图片的组合输入(即一段文本 + 一张图片),暂不支持批量文本 / 图片的同时处理 + * + * @param text 需要向量化的内容 + * @param image 需要向量化的图片地址/或者图片Base64编码图片(URI形式) + * @return 处理后的向量信息 + * @since 5.8.38 + */ + String embeddingVision(String text, String image); + + /** + * 应用(Bot) config中model设置为您创建的应用ID + * + * @param messages 由对话组成的消息列表。如系统人设,背景信息等,用户自定义的信息 + * @return AI回答 + * @since 5.8.38 + */ + String botsChat(final List messages); + + /** + * 应用(Bot)-SSE流式输出 config中model设置为您创建的应用ID + * + * @param messages 由对话组成的消息列表。如系统人设,背景信息等,用户自定义的信息 + * @param callback 流式数据回调函数 + * @since 5.8.39 + */ + void botsChat(final List messages, final Consumer callback); + + /** + * 分词:可以将文本转换为模型可理解的 token id,并返回文本的 tokens 数量、token id、 token 在原始文本中的偏移量等信息 + * + * @param text 需要分词的内容列表 + * @return 分词结果 + * @since 5.8.38 + */ + String tokenization(String[] text); + + /** + * 批量推理 Chat + * 注意:调用该方法时,配置config中的model为您创建的批量推理接入点(Endpoint)ID。详细参考官方文档 + * 该方法不支持流式 + * + * @param prompt chat内容 + * @return AI回答 + * @since 5.8.38 + */ + default String batchChat(String prompt){ + final List messages = new ArrayList<>(); + messages.add(new Message("system", "You are a helpful assistant")); + messages.add(new Message("user", prompt)); + return batchChat(messages); + } + + /** + * 批量推理 Chat + * 注意:调用该方法时,配置config中的model为您创建的批量推理接入点(Endpoint)ID。详细参考官方文档 + * 该方法不支持流式 + * + * @param messages 由对话组成的消息列表。如系统人设,背景信息等,用户自定义的信息 + * @return AI回答 + * @since 5.8.38 + */ + String batchChat(final List messages); + + /** + * 创建上下文缓存: 创建上下文缓存,获得缓存 id字段后,在上下文缓存对话 API中使用。 + * 注意:调用该方法时,配置config中的model为您创建的推理接入点(Endpoint)ID, + * 推理接入点中使用的模型需要在模型管理中开启缓存功能。详细参考官方文档 + * + * @param messages 由对话组成的消息列表。如系统人设,背景信息等,用户自定义的信息 + * @param mode 上下文缓存的类型,详细参考官方文档 默认为session + * @return 返回的缓存id + * @since 5.8.38 + */ + String createContext(final List messages, String mode); + + /** + * 创建上下文缓存: 创建上下文缓存,获得缓存 id字段后,在上下文缓存对话 API中使用。 + * 注意:调用该方法时,配置config中的model为您创建的推理接入点(Endpoint)ID, + * 推理接入点中使用的模型需要在模型管理中开启缓存功能。详细参考官方文档 + * + * @param messages 由对话组成的消息列表。如系统人设,背景信息等,用户自定义的信息 + * @return 返回的缓存id + * @since 5.8.38 + */ + default String createContext(final List messages) { + return createContext(messages, DoubaoCommon.DoubaoContext.SESSION.getMode()); + } + + /** + * 上下文缓存对话: 向大模型发起带上下文缓存的请求 + * 注意:配置config中的model可以为您创建的推理接入点(Endpoint)ID,也可以是支持chat的model + * + * @param prompt 对话的内容题词 + * @param contextId 创建上下文缓存后获取的缓存id + * @return AI的回答 + * @since 5.8.38 + */ + default String chatContext(String prompt, String contextId){ + final List messages = new ArrayList<>(); + messages.add(new Message("user", prompt)); + return chatContext(messages, contextId); + } + + /** + * 上下文缓存对话-SSE流式输出 + * 注意:配置config中的model可以为您创建的推理接入点(Endpoint)ID,也可以是支持chat的model + * + * @param prompt 对话的内容题词 + * @param contextId 创建上下文缓存后获取的缓存id + * @param callback 流式数据回调函数 + * @since 5.8.39 + */ + default void chatContext(String prompt, String contextId, final Consumer callback){ + final List messages = new ArrayList<>(); + messages.add(new Message("user", prompt)); + chatContext(messages, contextId, callback); + } + + /** + * 上下文缓存对话: 向大模型发起带上下文缓存的请求 + * 注意:配置config中的model可以为您创建的推理接入点(Endpoint)ID,也可以是支持chat的model + * + * @param messages 对话的信息 不支持最后一个元素的role设置为assistant。如使用session 缓存(mode设置为session)传入最新一轮对话的信息,无需传入历史信息 + * @param contextId 创建上下文缓存后获取的缓存id + * @return AI的回答 + * @since 5.8.38 + */ + String chatContext(final List messages, String contextId); + + /** + * 上下文缓存对话-SSE流式输出 + * 注意:配置config中的model可以为您创建的推理接入点(Endpoint)ID,也可以是支持chat的model + * + * @param messages 对话的信息 不支持最后一个元素的role设置为assistant。如使用session 缓存(mode设置为session)传入最新一轮对话的信息,无需传入历史信息 + * @param contextId 创建上下文缓存后获取的缓存id + * @param callback 流式数据回调函数 + * @since 5.8.39 + */ + void chatContext(final List messages, String contextId, final Consumer callback); + + /** + * 文生图 + * 请设置config中model为支持图片功能的模型,目前支持Doubao-Seedream-3.0-t2i + * + * @param prompt 题词 + * @return 包含生成图片的url + * @since 5.8.39 + */ + String imagesGenerations(String prompt); + +} diff --git a/hutool-ai/src/main/java/cn/hutool/ai/model/doubao/DoubaoServiceImpl.java b/hutool-ai/src/main/java/cn/hutool/ai/model/doubao/DoubaoServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..c1836fec44a6f611df8866634fd258899f863da5 --- /dev/null +++ b/hutool-ai/src/main/java/cn/hutool/ai/model/doubao/DoubaoServiceImpl.java @@ -0,0 +1,439 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.hutool.ai.model.doubao; + +import cn.hutool.ai.core.AIConfig; +import cn.hutool.ai.core.BaseAIService; +import cn.hutool.ai.core.Message; +import cn.hutool.core.thread.ThreadUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.http.HttpResponse; +import cn.hutool.json.JSONUtil; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +/** + * Doubao服务,AI具体功能的实现 + * + * @author elichow + * @since 5.8.38 + */ +public class DoubaoServiceImpl extends BaseAIService implements DoubaoService { + + //对话 + private final String CHAT_ENDPOINT = "/chat/completions"; + //文本向量化 + private final String EMBEDDING_TEXT = "/embeddings"; + //图文向量化 + private final String EMBEDDING_VISION = "/embeddings/multimodal"; + //应用bots + private final String BOTS_CHAT = "/bots/chat/completions"; + //分词 + private final String TOKENIZATION = "/tokenization"; + //批量推理chat + private final String BATCH_CHAT = "/batch/chat/completions"; + //创建上下文缓存 + private final String CREATE_CONTEXT = "/context/create"; + //上下文缓存对话 + private final String CHAT_CONTEXT = "/context/chat/completions"; + //创建视频生成任务 + private final String CREATE_VIDEO = "/contents/generations/tasks"; + //文生图 + private final String IMAGES_GENERATIONS = "/images/generations"; + + public DoubaoServiceImpl(final AIConfig config) { + //初始化doubao客户端 + super(config); + } + + @Override + public String chat(final List messages) { + String paramJson = buildChatRequestBody(messages); + final HttpResponse response = sendPost(CHAT_ENDPOINT, paramJson); + return response.body(); + } + + @Override + public void chat(final List messages, final Consumer callback) { + Map paramMap = buildChatStreamRequestBody(messages); + ThreadUtil.newThread(() -> sendPostStream(CHAT_ENDPOINT, paramMap, callback::accept), "doubao-chat-sse").start(); + } + + @Override + public String chatVision(String prompt, final List images, String detail) { + String paramJson = buildChatVisionRequestBody(prompt, images, detail); + final HttpResponse response = sendPost(CHAT_ENDPOINT, paramJson); + return response.body(); + } + + @Override + public void chatVision(String prompt, List images, String detail, Consumer callback) { + Map paramMap = buildChatVisionStreamRequestBody(prompt, images, detail); + ThreadUtil.newThread(() -> sendPostStream(CHAT_ENDPOINT, paramMap, callback::accept), "doubao-chatVision-sse").start(); + } + + @Override + public String videoTasks(String text, String image, final List videoParams) { + String paramJson = buildGenerationsTasksRequestBody(text, image, videoParams); + final HttpResponse response = sendPost(CREATE_VIDEO, paramJson); + return response.body(); + } + + @Override + public String getVideoTasksInfo(String taskId) { + final HttpResponse response = sendGet(CREATE_VIDEO + "/" + taskId); + return response.body(); + } + + + @Override + public String embeddingText(String[] input) { + String paramJson = buildEmbeddingTextRequestBody(input); + final HttpResponse response = sendPost(EMBEDDING_TEXT, paramJson); + return response.body(); + } + + @Override + public String embeddingVision(String text, String image) { + String paramJson = buildEmbeddingVisionRequestBody(text, image); + final HttpResponse response = sendPost(EMBEDDING_VISION, paramJson); + return response.body(); + } + + @Override + public String botsChat(final List messages) { + String paramJson = buildBotsChatRequestBody(messages); + final HttpResponse response = sendPost(BOTS_CHAT, paramJson); + return response.body(); + } + + @Override + public void botsChat(List messages, Consumer callback) { + Map paramMap = buildBotsChatStreamRequestBody(messages); + ThreadUtil.newThread(() -> sendPostStream(BOTS_CHAT, paramMap, callback::accept), "doubao-botsChat-sse").start(); + } + + @Override + public String tokenization(String[] text) { + String paramJson = buildTokenizationRequestBody(text); + final HttpResponse response = sendPost(TOKENIZATION, paramJson); + return response.body(); + } + + + @Override + public String batchChat(final List messages) { + String paramJson = buildBatchChatRequestBody(messages); + final HttpResponse response = sendPost(BATCH_CHAT, paramJson); + return response.body(); + } + + @Override + public String createContext(final List messages, String mode) { + String paramJson = buildCreateContextRequest(messages, mode); + final HttpResponse response = sendPost(CREATE_CONTEXT, paramJson); + return response.body(); + } + + @Override + public String chatContext(final List messages, String contextId) { + String paramJson = buildChatContentRequestBody(messages, contextId); + final HttpResponse response = sendPost(CHAT_CONTEXT, paramJson); + return response.body(); + } + + @Override + public void chatContext(final List messages, String contextId, final Consumer callback) { + Map paramMap = buildChatContentStreamRequestBody(messages, contextId); + ThreadUtil.newThread(() -> sendPostStream(CHAT_CONTEXT, paramMap, callback::accept), "doubao-chatContext-sse").start(); + } + + @Override + public String imagesGenerations(String prompt) { + String paramJson = buildImagesGenerationsRequestBody(prompt); + final HttpResponse response = sendPost(IMAGES_GENERATIONS, paramJson); + return response.body(); + } + + // 构建chat请求体 + private String buildChatRequestBody(final List messages) { + //使用JSON工具 + final Map paramMap = new HashMap<>(); + paramMap.put("model", config.getModel()); + paramMap.put("messages", messages); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + + return JSONUtil.toJsonStr(paramMap); + } + + // 构建chatStream请求体 + private Map buildChatStreamRequestBody(final List messages) { + //使用JSON工具 + final Map paramMap = new HashMap<>(); + paramMap.put("stream", true); + paramMap.put("model", config.getModel()); + paramMap.put("messages", messages); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + + return paramMap; + } + + //构建chatVision请求体 + private String buildChatVisionRequestBody(String prompt, final List images, String detail) { + // 定义消息结构 + final List messages = new ArrayList<>(); + final List content = new ArrayList<>(); + + final Map contentMap = new HashMap<>(); + contentMap.put("type", "text"); + contentMap.put("text", prompt); + content.add(contentMap); + for (String img : images) { + HashMap imgUrlMap = new HashMap<>(); + imgUrlMap.put("type", "image_url"); + HashMap urlMap = new HashMap<>(); + urlMap.put("url", img); + urlMap.put("detail", detail); + imgUrlMap.put("image_url", urlMap); + content.add(imgUrlMap); + } + + messages.add(new Message("user", content)); + + //使用JSON工具 + final Map paramMap = new HashMap<>(); + paramMap.put("model", config.getModel()); + paramMap.put("messages", messages); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + return JSONUtil.toJsonStr(paramMap); + } + + private Map buildChatVisionStreamRequestBody(String prompt, final List images, String detail) { + // 定义消息结构 + final List messages = new ArrayList<>(); + final List content = new ArrayList<>(); + + final Map contentMap = new HashMap<>(); + contentMap.put("type", "text"); + contentMap.put("text", prompt); + content.add(contentMap); + for (String img : images) { + HashMap imgUrlMap = new HashMap<>(); + imgUrlMap.put("type", "image_url"); + HashMap urlMap = new HashMap<>(); + urlMap.put("url", img); + urlMap.put("detail", detail); + imgUrlMap.put("image_url", urlMap); + content.add(imgUrlMap); + } + + messages.add(new Message("user", content)); + + //使用JSON工具 + final Map paramMap = new HashMap<>(); + paramMap.put("stream", true); + paramMap.put("model", config.getModel()); + paramMap.put("messages", messages); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + return paramMap; + } + + //构建文本向量化请求体 + private String buildEmbeddingTextRequestBody(String[] input) { + //使用JSON工具 + final Map paramMap = new HashMap<>(); + paramMap.put("model", config.getModel()); + paramMap.put("input", input); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + return JSONUtil.toJsonStr(paramMap); + } + + //构建图文向量化请求体 + private String buildEmbeddingVisionRequestBody(String text, String image) { + //使用JSON工具 + final Map paramMap = new HashMap<>(); + paramMap.put("model", config.getModel()); + + final List input = new ArrayList<>(); + //添加文本参数 + if (!StrUtil.isBlank(text)) { + final Map textMap = new HashMap<>(); + textMap.put("type", "text"); + textMap.put("text", text); + input.add(textMap); + } + //添加图片参数 + if (!StrUtil.isBlank(image)) { + final Map imgUrlMap = new HashMap<>(); + imgUrlMap.put("type", "image_url"); + final Map urlMap = new HashMap<>(); + urlMap.put("url", image); + imgUrlMap.put("image_url", urlMap); + input.add(imgUrlMap); + } + + paramMap.put("input", input); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + + return JSONUtil.toJsonStr(paramMap); + } + + //构建应用chat请求体 + private String buildBotsChatRequestBody(final List messages) { + return buildChatRequestBody(messages); + } + + private Map buildBotsChatStreamRequestBody(final List messages) { + return buildChatStreamRequestBody(messages); + } + + //构建分词请求体 + private String buildTokenizationRequestBody(String[] text) { + final Map paramMap = new HashMap<>(); + paramMap.put("model", config.getModel()); + paramMap.put("text", text); + return JSONUtil.toJsonStr(paramMap); + } + + //构建批量推理chat请求体 + private String buildBatchChatRequestBody(final List messages) { + return buildChatRequestBody(messages); + } + + private Map buildBatchChatStreamRequestBody(final List messages) { + return buildChatStreamRequestBody(messages); + } + + //构建创建上下文缓存请求体 + private String buildCreateContextRequest(final List messages, String mode) { + final Map paramMap = new HashMap<>(); + paramMap.put("messages", messages); + paramMap.put("model", config.getModel()); + paramMap.put("mode", mode); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + + return JSONUtil.toJsonStr(paramMap); + } + + //构建上下文缓存对话请求体 + private String buildChatContentRequestBody(final List messages, String contextId) { + //使用JSON工具 + final Map paramMap = new HashMap<>(); + paramMap.put("model", config.getModel()); + paramMap.put("messages", messages); + paramMap.put("context_id", contextId); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + + return JSONUtil.toJsonStr(paramMap); + } + + private Map buildChatContentStreamRequestBody(final List messages, String contextId) { + //使用JSON工具 + final Map paramMap = new HashMap<>(); + paramMap.put("stream", true); + paramMap.put("model", config.getModel()); + paramMap.put("messages", messages); + paramMap.put("context_id", contextId); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + + return paramMap; + } + + //构建创建视频任务请求体 + private String buildGenerationsTasksRequestBody(String text, String image, final List videoParams) { + //使用JSON工具 + final Map paramMap = new HashMap<>(); + paramMap.put("model", config.getModel()); + + final List content = new ArrayList<>(); + //添加文本参数 + final Map textMap = new HashMap<>(); + if (!StrUtil.isBlank(text)) { + textMap.put("type", "text"); + textMap.put("text", text); + content.add(textMap); + } + //添加图片参数 + if (!StrUtil.isBlank(image)) { + final Map imgUrlMap = new HashMap<>(); + imgUrlMap.put("type", "image_url"); + final Map urlMap = new HashMap<>(); + urlMap.put("url", image); + imgUrlMap.put("image_url", urlMap); + content.add(imgUrlMap); + } + + //添加视频参数 + if (videoParams != null && !videoParams.isEmpty()) { + //如果有文本参数就加在后面 + if (textMap != null && !textMap.isEmpty()) { + int textIndex = content.indexOf(textMap); + StringBuilder textBuilder = new StringBuilder(text); + for (DoubaoCommon.DoubaoVideo videoParam : videoParams) { + textBuilder.append(" ").append(videoParam.getType()).append(" ").append(videoParam.getValue()); + } + textMap.put("type", "text"); + textMap.put("text", textBuilder.toString()); + + if (textIndex != -1) { + content.set(textIndex, textMap); + } else { + content.add(textMap); + } + } else { + //如果没有文本参数就重新增加 + StringBuilder textBuilder = new StringBuilder(); + for (DoubaoCommon.DoubaoVideo videoParam : videoParams) { + textBuilder.append(videoParam.getType()).append(videoParam.getValue()).append(" "); + } + textMap.put("type", "text"); + textMap.put("text", textBuilder.toString()); + content.add(textMap); + } + } + + paramMap.put("content", content); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + return JSONUtil.toJsonStr(paramMap); + } + + //构建文生图请求体 + private String buildImagesGenerationsRequestBody(String prompt) { + final Map paramMap = new HashMap<>(); + paramMap.put("model", config.getModel()); + paramMap.put("prompt", prompt); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + + return JSONUtil.toJsonStr(paramMap); + } + +} diff --git a/hutool-ai/src/main/java/cn/hutool/ai/model/doubao/package-info.java b/hutool-ai/src/main/java/cn/hutool/ai/model/doubao/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..dd1e84371a84e9274a58e9c313135df86aebfc29 --- /dev/null +++ b/hutool-ai/src/main/java/cn/hutool/ai/model/doubao/package-info.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * 对doubao的封装实现 + * + * @author elichow + * @since 5.8.38 + */ + +package cn.hutool.ai.model.doubao; diff --git a/hutool-ai/src/main/java/cn/hutool/ai/model/grok/GrokCommon.java b/hutool-ai/src/main/java/cn/hutool/ai/model/grok/GrokCommon.java new file mode 100644 index 0000000000000000000000000000000000000000..2044e58e9ff218dc20869acda6b19ec3ed800b0b --- /dev/null +++ b/hutool-ai/src/main/java/cn/hutool/ai/model/grok/GrokCommon.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.hutool.ai.model.grok; + +/** + * grok公共类 + * + * @author elichow + * @since 5.8.38 + */ +public class GrokCommon { + + //grok视觉参数 + public enum GrokVision { + + AUTO("auto"), + LOW("low"), + HIGH("high"); + + private final String detail; + + GrokVision(String detail) { + this.detail = detail; + } + + public String getDetail() { + return detail; + } + } +} diff --git a/hutool-ai/src/main/java/cn/hutool/ai/model/grok/GrokConfig.java b/hutool-ai/src/main/java/cn/hutool/ai/model/grok/GrokConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..7a4727b95850f397acda67e575ef3a8bb8cfd329 --- /dev/null +++ b/hutool-ai/src/main/java/cn/hutool/ai/model/grok/GrokConfig.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.hutool.ai.model.grok; + +import cn.hutool.ai.Models; +import cn.hutool.ai.core.BaseConfig; + +/** + * Grok配置类,初始化API接口地址,设置默认的模型 + * + * @author elichow + * @since 5.8.38 + */ +public class GrokConfig extends BaseConfig { + + private final String API_URL = "https://api.x.ai/v1"; + + private final String DEFAULT_MODEL = Models.Grok.GROK_2_1212.getModel(); + + + public GrokConfig() { + setApiUrl(API_URL); + setModel(DEFAULT_MODEL); + } + + public GrokConfig(String apiKey) { + this(); + setApiKey(apiKey); + } + + @Override + public String getModelName() { + return "grok"; + } + +} diff --git a/hutool-ai/src/main/java/cn/hutool/ai/model/grok/GrokProvider.java b/hutool-ai/src/main/java/cn/hutool/ai/model/grok/GrokProvider.java new file mode 100644 index 0000000000000000000000000000000000000000..af62fd2195dd60a0ead3ed38c9c94454f7bf7920 --- /dev/null +++ b/hutool-ai/src/main/java/cn/hutool/ai/model/grok/GrokProvider.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.hutool.ai.model.grok; + +import cn.hutool.ai.core.AIConfig; +import cn.hutool.ai.core.AIServiceProvider; + +/**r + * 创建Grok服务实现类 + * + * @author elichow + * @since 5.8.38 + */ +public class GrokProvider implements AIServiceProvider { + + @Override + public String getServiceName() { + return "grok"; + } + + @Override + public GrokService create(final AIConfig config) { + return new GrokServiceImpl(config); + } +} diff --git a/hutool-ai/src/main/java/cn/hutool/ai/model/grok/GrokService.java b/hutool-ai/src/main/java/cn/hutool/ai/model/grok/GrokService.java new file mode 100644 index 0000000000000000000000000000000000000000..4658f766b025ce7d4fa0a577d108328b2598356a --- /dev/null +++ b/hutool-ai/src/main/java/cn/hutool/ai/model/grok/GrokService.java @@ -0,0 +1,192 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.hutool.ai.model.grok; + +import cn.hutool.ai.core.AIService; +import cn.hutool.ai.core.Message; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +/** + * grok支持的扩展接口 + * + * @author elichow + * @since 5.8.38 + */ +public interface GrokService extends AIService { + + /** + * 创建消息回复 + * + * @param prompt 题词 + * @param maxToken 最大token + * @return AI回答 + * @since 5.8.38 + */ + default String message(String prompt, int maxToken){ + // 定义消息结构 + final List messages = new ArrayList<>(); + messages.add(new Message("system", "You are a helpful assistant")); + messages.add(new Message("user", prompt)); + return message(messages, maxToken); + } + + /** + * 创建消息回复-SSE流式输出 + * + * @param prompt 题词 + * @param maxToken 最大token + * @param callback 流式数据回调函数 + * @since 5.8.39 + */ + default void message(String prompt, int maxToken, final Consumer callback){ + final List messages = new ArrayList<>(); + messages.add(new Message("system", "You are a helpful assistant")); + messages.add(new Message("user", prompt)); + message(messages, maxToken, callback); + } + + /** + * 创建消息回复 + * + * @param messages messages 由对话组成的消息列表。如系统人设,背景信息等,用户自定义的信息 + * @param maxToken 最大token + * @return AI回答 + * @since 5.8.39 + */ + String message(List messages, int maxToken); + + /** + * 创建消息回复-SSE流式输出 + * + * @param messages messages 由对话组成的消息列表。如系统人设,背景信息等,用户自定义的信息 + * @param maxToken 最大token + * @param callback 流式数据回调函数 + * @since 5.8.39 + */ + void message(List messages, int maxToken, final Consumer callback); + + /** + * 图像理解:模型会依据传入的图片信息以及问题,给出回复。 + * + * @param prompt 题词 + * @param images 图片列表/或者图片Base64编码图片列表(URI形式) + * @param detail 手动设置图片的质量,取值范围high、low、auto,默认为auto + * @return AI回答 + * @since 5.8.38 + */ + String chatVision(String prompt, final List images, String detail); + + /** + * 图像理解-SSE流式输出 + * + * @param prompt 题词 + * @param images 图片列表/或者图片Base64编码图片列表(URI形式) + * @param detail 手动设置图片的质量,取值范围high、low、auto,默认为auto + * @param callback 流式数据回调函数 + * @since 5.8.39 + */ + void chatVision(String prompt, final List images, String detail,final Consumer callback); + + /** + * 图像理解:模型会依据传入的图片信息以及问题,给出回复。 + * + * @param prompt 题词 + * @param images 传入的图片列表地址/或者图片Base64编码图片列表(URI形式) + * @return AI回答 + * @since 5.8.38 + */ + default String chatVision(String prompt, final List images) { + return chatVision(prompt, images, GrokCommon.GrokVision.AUTO.getDetail()); + } + + /** + * 图像理解:模型会依据传入的图片信息以及问题,给出回复。 + * + * @param prompt 题词 + * @param images 传入|的图片列表地址/或者图片Base64编码图片列表(URI形式) + * @param callback 流式数据回调函数 + * @since 5.8.39 + */ + default void chatVision(String prompt, final List images, final Consumer callback){ + chatVision(prompt, images, GrokCommon.GrokVision.AUTO.getDetail(), callback); + } + + /** + * 列出所有model列表 + * + * @return model列表 + * @since 5.8.38 + */ + String models(); + + /** + * 获取模型信息 + * + * @param modelId model ID + * @return model信息 + * @since 5.8.38 + */ + String getModel(String modelId); + + /** + * 列出所有语言model + * + * @return languageModel列表 + * @since 5.8.38 + */ + String languageModels(); + + /** + * 获取语言模型信息 + * + * @param modelId model ID + * @return model信息 + * @since 5.8.38 + */ + String getLanguageModel(String modelId); + + /** + * 分词:可以将文本转换为模型可理解的 token 信息 + * + * @param text 需要分词的内容 + * @return 分词结果 + * @since 5.8.38 + */ + String tokenizeText(String text); + + /** + * 从延迟对话中获取结果 + * + * @param requestId 延迟对话中的延迟请求ID + * @return AI回答 + * @since 5.8.38 + */ + String deferredCompletion(String requestId); + + /** + * 文生图 + * 请设置config中model为支持图片功能的模型,目前支持GROK_2_IMAGE + * + * @param prompt 题词 + * @return 包含生成图片的url + * @since 5.8.39 + */ + String imagesGenerations(String prompt); +} diff --git a/hutool-ai/src/main/java/cn/hutool/ai/model/grok/GrokServiceImpl.java b/hutool-ai/src/main/java/cn/hutool/ai/model/grok/GrokServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..1c53932e527711db2e35d7a00b38a21921e8353d --- /dev/null +++ b/hutool-ai/src/main/java/cn/hutool/ai/model/grok/GrokServiceImpl.java @@ -0,0 +1,275 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.hutool.ai.model.grok; + +import cn.hutool.ai.core.AIConfig; +import cn.hutool.ai.core.BaseAIService; +import cn.hutool.ai.core.Message; +import cn.hutool.core.thread.ThreadUtil; +import cn.hutool.http.HttpResponse; +import cn.hutool.json.JSONUtil; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +/** + * Grok服务,AI具体功能的实现 + * + * @author elichow + * @since 5.8.38 + */ +public class GrokServiceImpl extends BaseAIService implements GrokService { + + //对话补全 + private final String CHAT_ENDPOINT = "/chat/completions"; + //创建消息回复 + private final String MESSAGES = "/messages"; + //列出模型 + private final String MODELS_ENDPOINT = "/models"; + //列出语言模型 + private final String LANGUAGE_MODELS = "/language-models"; + //分词 + private final String TOKENIZE_TEXT = "/tokenize-text"; + //获取延迟对话 + private final String DEFERRED_COMPLETION = "/chat/deferred-completion"; + //文生图 + private final String IMAGES_GENERATIONS = "/images/generations"; + + public GrokServiceImpl(final AIConfig config) { + //初始化grok客户端 + super(config); + } + + @Override + public String chat(final List messages) { + String paramJson = buildChatRequestBody(messages); + final HttpResponse response = sendPost(CHAT_ENDPOINT, paramJson); + return response.body(); + } + + @Override + public void chat(List messages,Consumer callback) { + Map paramMap = buildChatStreamRequestBody(messages); + ThreadUtil.newThread(() -> sendPostStream(CHAT_ENDPOINT, paramMap, callback::accept), "grok-chat-sse").start(); + } + + @Override + public String message(final List messages, int maxToken) { + String paramJson = buildMessageRequestBody(messages, maxToken); + final HttpResponse response = sendPost(MESSAGES, paramJson); + return response.body(); + } + + @Override + public void message(List messages, int maxToken, final Consumer callback) { + Map paramMap = buildMessageStreamRequestBody(messages, maxToken); + ThreadUtil.newThread(() -> sendPostStream(MESSAGES, paramMap, callback::accept), "grok-message-sse").start(); + } + + @Override + public String chatVision(String prompt, final List images, String detail) { + String paramJson = buildChatVisionRequestBody(prompt, images, detail); + final HttpResponse response = sendPost(CHAT_ENDPOINT, paramJson); + return response.body(); + } + + @Override + public void chatVision(String prompt, List images, String detail, Consumer callback) { + Map paramMap = buildChatVisionStreamRequestBody(prompt, images, detail); + ThreadUtil.newThread(() -> sendPostStream(CHAT_ENDPOINT, paramMap, callback::accept), "grok-chatVision-sse").start(); + } + + @Override + public String models() { + final HttpResponse response = sendGet(MODELS_ENDPOINT); + return response.body(); + } + + @Override + public String getModel(String modelId) { + final HttpResponse response = sendGet(MODELS_ENDPOINT + "/" + modelId); + return response.body(); + } + + @Override + public String languageModels() { + final HttpResponse response = sendGet(LANGUAGE_MODELS); + return response.body(); + } + + @Override + public String getLanguageModel(String modelId) { + final HttpResponse response = sendGet(LANGUAGE_MODELS + "/" + modelId); + return response.body(); + } + + @Override + public String tokenizeText(String text) { + String paramJson = buildTokenizeRequestBody(text); + final HttpResponse response = sendPost(TOKENIZE_TEXT, paramJson); + return response.body(); + } + + @Override + public String deferredCompletion(String requestId) { + final HttpResponse response = sendGet(DEFERRED_COMPLETION + "/" + requestId); + return response.body(); + } + + @Override + public String imagesGenerations(String prompt) { + String paramJson = buildImagesGenerationsRequestBody(prompt); + final HttpResponse response = sendPost(IMAGES_GENERATIONS, paramJson); + return response.body(); + } + + // 构建chat请求体 + private String buildChatRequestBody(final List messages) { + //使用JSON工具 + final Map paramMap = new HashMap<>(); + paramMap.put("model", config.getModel()); + paramMap.put("messages", messages); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + + return JSONUtil.toJsonStr(paramMap); + } + + private Map buildChatStreamRequestBody(final List messages) { + //使用JSON工具 + final Map paramMap = new HashMap<>(); + paramMap.put("stream", true); + paramMap.put("model", config.getModel()); + paramMap.put("messages", messages); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + + return paramMap; + } + + //构建chatVision请求体 + private String buildChatVisionRequestBody(String prompt, final List images, String detail) { + // 定义消息结构 + final List messages = new ArrayList<>(); + final List content = new ArrayList<>(); + + final Map contentMap = new HashMap<>(); + contentMap.put("type", "text"); + contentMap.put("text", prompt); + content.add(contentMap); + for (String img : images) { + HashMap imgUrlMap = new HashMap<>(); + imgUrlMap.put("type", "image_url"); + HashMap urlMap = new HashMap<>(); + urlMap.put("url", img); + urlMap.put("detail", detail); + imgUrlMap.put("image_url", urlMap); + content.add(imgUrlMap); + } + + messages.add(new Message("user", content)); + + //使用JSON工具 + final Map paramMap = new HashMap<>(); + paramMap.put("model", config.getModel()); + paramMap.put("messages", messages); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + return JSONUtil.toJsonStr(paramMap); + } + + private Map buildChatVisionStreamRequestBody(String prompt, final List images, String detail) { + // 定义消息结构 + final List messages = new ArrayList<>(); + final List content = new ArrayList<>(); + + final Map contentMap = new HashMap<>(); + contentMap.put("type", "text"); + contentMap.put("text", prompt); + content.add(contentMap); + for (String img : images) { + HashMap imgUrlMap = new HashMap<>(); + imgUrlMap.put("type", "image_url"); + HashMap urlMap = new HashMap<>(); + urlMap.put("url", img); + urlMap.put("detail", detail); + imgUrlMap.put("image_url", urlMap); + content.add(imgUrlMap); + } + + messages.add(new Message("user", content)); + + //使用JSON工具 + final Map paramMap = new HashMap<>(); + paramMap.put("stream", true); + paramMap.put("model", config.getModel()); + paramMap.put("messages", messages); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + return paramMap; + } + + //构建消息回复请求体 + private String buildMessageRequestBody(final List messages, int maxToken) { + final Map paramMap = new HashMap<>(); + paramMap.put("model", config.getModel()); + paramMap.put("messages", messages); + paramMap.put("max_tokens", maxToken); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + + return JSONUtil.toJsonStr(paramMap); + } + + private Map buildMessageStreamRequestBody(final List messages, int maxToken) { + final Map paramMap = new HashMap<>(); + paramMap.put("stream", true); + paramMap.put("model", config.getModel()); + paramMap.put("messages", messages); + paramMap.put("max_tokens", maxToken); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + + return paramMap; + } + + //构建分词请求体 + private String buildTokenizeRequestBody(String text) { + //使用JSON工具 + final Map paramMap = new HashMap<>(); + paramMap.put("model", config.getModel()); + paramMap.put("text", text); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + + return JSONUtil.toJsonStr(paramMap); + } + + //构建文生图请求体 + private String buildImagesGenerationsRequestBody(String prompt) { + final Map paramMap = new HashMap<>(); + paramMap.put("model", config.getModel()); + paramMap.put("prompt", prompt); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + + return JSONUtil.toJsonStr(paramMap); + } +} diff --git a/hutool-ai/src/main/java/cn/hutool/ai/model/grok/package-info.java b/hutool-ai/src/main/java/cn/hutool/ai/model/grok/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..6541d40fd0bd818a2394e9c047b0aeb60a4fc2af --- /dev/null +++ b/hutool-ai/src/main/java/cn/hutool/ai/model/grok/package-info.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * 对grok的封装实现 + * + * @author elichow + * @since 5.8.38 + */ + +package cn.hutool.ai.model.grok; diff --git a/hutool-ai/src/main/java/cn/hutool/ai/model/hutool/HutoolCommon.java b/hutool-ai/src/main/java/cn/hutool/ai/model/hutool/HutoolCommon.java new file mode 100644 index 0000000000000000000000000000000000000000..4db506a64084e59e76ad2c8d24deea285f26a0ea --- /dev/null +++ b/hutool-ai/src/main/java/cn/hutool/ai/model/hutool/HutoolCommon.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.hutool.ai.model.hutool; + +/** + * hutool公共类 + * + * @author elichow + * @since 5.8.39 + */ +public class HutoolCommon { + + //hutool视觉参数 + public enum HutoolVision { + + AUTO("auto"), + LOW("low"), + HIGH("high"); + + private final String detail; + + HutoolVision(String detail) { + this.detail = detail; + } + + public String getDetail() { + return detail; + } + } + + //hutool音频参数 + public enum HutoolSpeech { + + ALLOY("alloy"), + ASH("ash"), + CORAL("coral"), + ECHO("echo"), + FABLE("fable"), + ONYX("onyx"), + NOVA("nova"), + SAGE("sage"), + SHIMMER("shimmer"); + + private final String voice; + + HutoolSpeech(String voice) { + this.voice = voice; + } + + public String getVoice() { + return voice; + } + } + + //hutool视频生成参数 + public enum HutoolVideo { + + //宽高比例 + RATIO_16_9("--rt", "16:9"),//[1280, 720] + RATIO_4_3("--rt", "4:3"),//[960, 720] + RATIO_1_1("--rt", "1:1"),//[720, 720] + RATIO_3_4("--rt", "3:4"),//[720, 960] + RATIO_9_16("--rt", "9:16"),//[720, 1280] + RATIO_21_9("--rt", "21:9"),//[1280, 544] + + //生成视频时长 + DURATION_5("--dur", 5),//文生视频,图生视频 + DURATION_10("--dur", 10),//文生视频 + + //帧率,即一秒时间内视频画面数量 + FPS_5("--fps", 24), + + //视频分辨率 + RESOLUTION_5("--rs", "720p"), + + //生成视频是否包含水印 + WATERMARK_TRUE("--wm", true), + WATERMARK_FALSE("--wm", false); + + private final String type; + private final Object value; + + HutoolVideo(String type, Object value) { + this.type = type; + this.value = value; + } + + public String getType() { + return type; + } + + public Object getValue() { + if (value instanceof String) { + return (String) value; + } else if (value instanceof Integer) { + return (Integer) value; + } else if (value instanceof Boolean) { + return (Boolean) value; + } + return value; + } + + } + +} diff --git a/hutool-ai/src/main/java/cn/hutool/ai/model/hutool/HutoolConfig.java b/hutool-ai/src/main/java/cn/hutool/ai/model/hutool/HutoolConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..a4f120b4c71852e8464d764a6d52eef78fdbe2bb --- /dev/null +++ b/hutool-ai/src/main/java/cn/hutool/ai/model/hutool/HutoolConfig.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.hutool.ai.model.hutool; + +import cn.hutool.ai.Models; +import cn.hutool.ai.core.BaseConfig; + +/** + * Hutool配置类,初始化API接口地址,设置默认的模型 + * + * @author elichow + * @since 5.8.39 + */ +public class HutoolConfig extends BaseConfig { + + private final String API_URL = "https://api.hutool.cn/ai/api"; + + private final String DEFAULT_MODEL = Models.Hutool.HUTOOL.getModel(); + + public HutoolConfig() { + setApiUrl(API_URL); + setModel(DEFAULT_MODEL); + } + + public HutoolConfig(String apiKey) { + this(); + setApiKey(apiKey); + } + + @Override + public String getModelName() { + return "hutool"; + } + +} diff --git a/hutool-ai/src/main/java/cn/hutool/ai/model/hutool/HutoolProvider.java b/hutool-ai/src/main/java/cn/hutool/ai/model/hutool/HutoolProvider.java new file mode 100644 index 0000000000000000000000000000000000000000..c983ced9f2927edd8fc860ba9b13598e2dbcfd4f --- /dev/null +++ b/hutool-ai/src/main/java/cn/hutool/ai/model/hutool/HutoolProvider.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.hutool.ai.model.hutool; + +import cn.hutool.ai.core.AIConfig; +import cn.hutool.ai.core.AIServiceProvider; + +/**r + * 创建Hutool服务实现类 + * + * @author elichow + * @since 5.8.39 + */ +public class HutoolProvider implements AIServiceProvider { + + @Override + public String getServiceName() { + return "hutool"; + } + + @Override + public HutoolService create(final AIConfig config) { + return new HutoolServiceImpl(config); + } +} diff --git a/hutool-ai/src/main/java/cn/hutool/ai/model/hutool/HutoolService.java b/hutool-ai/src/main/java/cn/hutool/ai/model/hutool/HutoolService.java new file mode 100644 index 0000000000000000000000000000000000000000..7293d5d0c12fc9ef67c6fa2e35a2835fc297ef21 --- /dev/null +++ b/hutool-ai/src/main/java/cn/hutool/ai/model/hutool/HutoolService.java @@ -0,0 +1,170 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.hutool.ai.model.hutool; + +import cn.hutool.ai.core.AIService; + +import java.io.File; +import java.io.InputStream; +import java.util.List; +import java.util.function.Consumer; + +/** + * hutool支持的扩展接口 + * + * @author elichow + * @since 5.8.39 + */ +public interface HutoolService extends AIService { + + /** + * 图像理解:模型会依据传入的图片信息以及问题,给出回复。 + * + * @param prompt 题词 + * @param images 图片列表/或者图片Base64编码图片列表(URI形式) + * @param detail 手动设置图片的质量,取值范围high、low、auto,默认为auto + * @return AI回答 + * @since 5.8.39 + */ + String chatVision(String prompt, final List images, String detail); + + /** + * 图像理解-SSE流式输出 + * + * @param prompt 题词 + * @param images 图片列表/或者图片Base64编码图片列表(URI形式) + * @param detail 手动设置图片的质量,取值范围high、low、auto,默认为auto + * @param callback 流式数据回调函数 + * @since 5.8.39 + */ + void chatVision(String prompt, final List images, String detail,final Consumer callback); + + /** + * 图像理解:模型会依据传入的图片信息以及问题,给出回复。 + * + * @param prompt 题词 + * @param images 传入的图片列表地址/或者图片Base64编码图片列表(URI形式) + * @return AI回答 + * @since 5.8.39 + */ + default String chatVision(String prompt, final List images) { + return chatVision(prompt, images, HutoolCommon.HutoolVision.AUTO.getDetail()); + } + + /** + * 图像理解:模型会依据传入的图片信息以及问题,给出回复。 + * + * @param prompt 题词 + * @param images 传入|的图片列表地址/或者图片Base64编码图片列表(URI形式) + * @param callback 流式数据回调函数 + * @since 5.8.39 + */ + default void chatVision(String prompt, final List images, final Consumer callback){ + chatVision(prompt, images, HutoolCommon.HutoolVision.AUTO.getDetail(), callback); + } + + /** + * 分词:可以将文本转换为模型可理解的 token 信息 + * + * @param text 需要分词的内容 + * @return 分词结果 + * @since 5.8.39 + */ + String tokenizeText(String text); + + /** + * 文生图 + * + * @param prompt 题词 + * @return 包含生成图片的url + * @since 5.8.39 + */ + String imagesGenerations(String prompt); + + /** + * 图文向量化:仅支持单一文本、单张图片或文本与图片的组合输入(即一段文本 + 一张图片),暂不支持批量文本 / 图片的同时处理 + * + * @param text 需要向量化的内容 + * @param image 需要向量化的图片地址/或者图片Base64编码图片(URI形式) + * @return 处理后的向量信息 + * @since 5.8.39 + */ + String embeddingVision(String text, String image); + + /** + * TTS文本转语音 + * + * @param input 需要转成语音的文本 + * @param voice AI的音色 + * @return 返回的音频mp3文件流 + * @since 5.8.39 + */ + InputStream tts(String input, final HutoolCommon.HutoolSpeech voice); + + /** + * TTS文本转语音 + * + * @param input 需要转成语音的文本 + * @return 返回的音频mp3文件流 + * @since 5.8.39 + */ + default InputStream tts(String input) { + return tts(input, HutoolCommon.HutoolSpeech.ALLOY); + } + + /** + * STT音频转文本 + * + * @param file 需要转成文本的音频文件 + * @return 返回的文本内容 + * @since 5.8.39 + */ + String stt(final File file); + + /** + * 创建视频生成任务 + * + * @param text 文本提示词 + * @param image 图片/或者图片Base64编码图片(URI形式) + * @param videoParams 视频参数列表 + * @return 生成任务id + * @since 5.8.39 + */ + String videoTasks(String text, String image, final List videoParams); + + /** + * 创建视频生成任务 + * + * @param text 文本提示词 + * @param image 图片/或者图片Base64编码图片(URI形式) + * @return 生成任务id + * @since 5.8.39 + */ + default String videoTasks(String text, String image) { + return videoTasks(text, image, null); + } + + /** + * 查询视频生成任务信息 + * + * @param taskId 通过创建生成视频任务返回的生成任务id + * @return 生成任务信息 + * @since 5.8.39 + */ + String getVideoTasksInfo(String taskId); + +} diff --git a/hutool-ai/src/main/java/cn/hutool/ai/model/hutool/HutoolServiceImpl.java b/hutool-ai/src/main/java/cn/hutool/ai/model/hutool/HutoolServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..27b90e75c445debc5f650eb7b82741526b898409 --- /dev/null +++ b/hutool-ai/src/main/java/cn/hutool/ai/model/hutool/HutoolServiceImpl.java @@ -0,0 +1,380 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.hutool.ai.model.hutool; + +import cn.hutool.ai.AIException; +import cn.hutool.ai.core.AIConfig; +import cn.hutool.ai.core.BaseAIService; +import cn.hutool.ai.core.Message; +import cn.hutool.core.thread.ThreadUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.http.HttpResponse; +import cn.hutool.json.JSONUtil; + +import java.io.File; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +/** + * Hutool服务,AI具体功能的实现 + * + * @author elichow + * @since 5.8.39 + */ +public class HutoolServiceImpl extends BaseAIService implements HutoolService { + + //对话补全 + private final String CHAT_ENDPOINT = "/chat/completions"; + //分词 + private final String TOKENIZE_TEXT = "/tokenize/text"; + //文生图 + private final String IMAGES_GENERATIONS = "/images/generations"; + //图文向量化 + private final String EMBEDDING_VISION = "/embeddings/multimodal"; + //文本转语音 + private final String TTS = "/audio/tts"; + //语音转文本 + private final String STT = "/audio/stt"; + //创建视频生成任务 + private final String CREATE_VIDEO = "/video/generations"; + + public HutoolServiceImpl(final AIConfig config) { + //初始化hutool客户端 + super(config); + } + + @Override + public String chat(final List messages) { + String paramJson = buildChatRequestBody(messages); + final HttpResponse response = sendPost(CHAT_ENDPOINT, paramJson); + return response.body(); + } + + @Override + public void chat(List messages,Consumer callback) { + Map paramMap = buildChatStreamRequestBody(messages); + ThreadUtil.newThread(() -> sendPostStream(CHAT_ENDPOINT, paramMap, callback::accept), "hutool-chat-sse").start(); + } + + @Override + public String chatVision(String prompt, final List images, String detail) { + String paramJson = buildChatVisionRequestBody(prompt, images, detail); + final HttpResponse response = sendPost(CHAT_ENDPOINT, paramJson); + return response.body(); + } + + @Override + public void chatVision(String prompt, List images, String detail, Consumer callback) { + Map paramMap = buildChatVisionStreamRequestBody(prompt, images, detail); + System.out.println(JSONUtil.toJsonStr(paramMap)); + ThreadUtil.newThread(() -> sendPostStream(CHAT_ENDPOINT, paramMap, callback::accept), "hutool-chatVision-sse").start(); + } + + @Override + public String tokenizeText(String text) { + String paramJson = buildTokenizeRequestBody(text); + final HttpResponse response = sendPost(TOKENIZE_TEXT, paramJson); + return response.body(); + } + + @Override + public String imagesGenerations(String prompt) { + String paramJson = buildImagesGenerationsRequestBody(prompt); + final HttpResponse response = sendPost(IMAGES_GENERATIONS, paramJson); + return response.body(); + } + + + @Override + public String embeddingVision(String text, String image) { + String paramJson = buildEmbeddingVisionRequestBody(text, image); + final HttpResponse response = sendPost(EMBEDDING_VISION, paramJson); + return response.body(); + } + + @Override + public InputStream tts(String input, final HutoolCommon.HutoolSpeech voice) { + try { + String paramJson = buildTTSRequestBody(input, voice.getVoice()); + final HttpResponse response = sendPost(TTS, paramJson); + + // 检查响应内容类型 + String contentType = response.header("Content-Type"); + if (contentType != null && contentType.startsWith("application/json")) { + // 如果是JSON响应,说明有错误 + String errorBody = response.body(); + throw new AIException("TTS请求失败: " + errorBody); + } + // 默认返回音频流 + return response.bodyStream(); + } catch (Exception e) { + throw new AIException("TTS处理失败: " + e.getMessage(), e); + } + } + + @Override + public String stt(final File file) { + final Map paramMap = buildSTTRequestBody(file); + final HttpResponse response = sendFormData(STT, paramMap); + return response.body(); + } + + + @Override + public String videoTasks(String text, String image, final List videoParams) { + String paramJson = buildGenerationsTasksRequestBody(text, image, videoParams); + final HttpResponse response = sendPost(CREATE_VIDEO, paramJson); + return response.body(); + } + + @Override + public String getVideoTasksInfo(String taskId) { + final HttpResponse response = sendGet(CREATE_VIDEO + "/" + taskId); + return response.body(); + } + + + // 构建chat请求体 + private String buildChatRequestBody(final List messages) { + //使用JSON工具 + final Map paramMap = new HashMap<>(); + paramMap.put("model", config.getModel()); + paramMap.put("messages", messages); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + + return JSONUtil.toJsonStr(paramMap); + } + + private Map buildChatStreamRequestBody(final List messages) { + //使用JSON工具 + final Map paramMap = new HashMap<>(); + paramMap.put("stream", true); + paramMap.put("model", config.getModel()); + paramMap.put("messages", messages); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + + return paramMap; + } + + //构建chatVision请求体 + private String buildChatVisionRequestBody(String prompt, final List images, String detail) { + // 定义消息结构 + final List messages = new ArrayList<>(); + final List content = new ArrayList<>(); + + final Map contentMap = new HashMap<>(); + contentMap.put("type", "text"); + contentMap.put("text", prompt); + content.add(contentMap); + for (String img : images) { + HashMap imgUrlMap = new HashMap<>(); + imgUrlMap.put("type", "image_url"); + HashMap urlMap = new HashMap<>(); + urlMap.put("url", img); + urlMap.put("detail", detail); + imgUrlMap.put("image_url", urlMap); + content.add(imgUrlMap); + } + + messages.add(new Message("user", content)); + + //使用JSON工具 + final Map paramMap = new HashMap<>(); + paramMap.put("model", config.getModel()); + paramMap.put("messages", messages); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + return JSONUtil.toJsonStr(paramMap); + } + + private Map buildChatVisionStreamRequestBody(String prompt, final List images, String detail) { + // 定义消息结构 + final List messages = new ArrayList<>(); + final List content = new ArrayList<>(); + + final Map contentMap = new HashMap<>(); + contentMap.put("type", "text"); + contentMap.put("text", prompt); + content.add(contentMap); + for (String img : images) { + HashMap imgUrlMap = new HashMap<>(); + imgUrlMap.put("type", "image_url"); + HashMap urlMap = new HashMap<>(); + urlMap.put("url", img); + urlMap.put("detail", detail); + imgUrlMap.put("image_url", urlMap); + content.add(imgUrlMap); + } + + messages.add(new Message("user", content)); + + //使用JSON工具 + final Map paramMap = new HashMap<>(); + paramMap.put("stream", true); + paramMap.put("model", config.getModel()); + paramMap.put("messages", messages); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + return paramMap; + } + + + //构建分词请求体 + private String buildTokenizeRequestBody(String text) { + //使用JSON工具 + final Map paramMap = new HashMap<>(); + paramMap.put("model", config.getModel()); + paramMap.put("text", text); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + + return JSONUtil.toJsonStr(paramMap); + } + + //构建文生图请求体 + private String buildImagesGenerationsRequestBody(String prompt) { + final Map paramMap = new HashMap<>(); + paramMap.put("model", config.getModel()); + paramMap.put("prompt", prompt); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + + return JSONUtil.toJsonStr(paramMap); + } + + //构建图文向量化请求体 + private String buildEmbeddingVisionRequestBody(String text, String image) { + //使用JSON工具 + final Map paramMap = new HashMap<>(); + paramMap.put("model", config.getModel()); + + final List input = new ArrayList<>(); + //添加文本参数 + if (!StrUtil.isBlank(text)) { + final Map textMap = new HashMap<>(); + textMap.put("type", "text"); + textMap.put("text", text); + input.add(textMap); + } + //添加图片参数 + if (!StrUtil.isBlank(image)) { + final Map imgUrlMap = new HashMap<>(); + imgUrlMap.put("type", "image_url"); + final Map urlMap = new HashMap<>(); + urlMap.put("url", image); + imgUrlMap.put("image_url", urlMap); + input.add(imgUrlMap); + } + + paramMap.put("input", input); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + System.out.println(JSONUtil.toJsonStr(paramMap)); + return JSONUtil.toJsonStr(paramMap); + } + + + //构建TTS请求体 + private String buildTTSRequestBody(String input, String voice) { + final Map paramMap = new HashMap<>(); + paramMap.put("model", config.getModel()); + paramMap.put("input", input); + paramMap.put("voice", voice); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + + return JSONUtil.toJsonStr(paramMap); + } + + //构建STT请求体 + private Map buildSTTRequestBody(final File file) { + final Map paramMap = new HashMap<>(); + paramMap.put("model", config.getModel()); + paramMap.put("file", file); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + + return paramMap; + } + + //构建创建视频任务请求体 + private String buildGenerationsTasksRequestBody(String text, String image, final List videoParams) { + //使用JSON工具 + final Map paramMap = new HashMap<>(); + paramMap.put("model", config.getModel()); + + final List content = new ArrayList<>(); + //添加文本参数 + final Map textMap = new HashMap<>(); + if (!StrUtil.isBlank(text)) { + textMap.put("type", "text"); + textMap.put("text", text); + content.add(textMap); + } + //添加图片参数 + if (!StrUtil.isBlank(image)) { + final Map imgUrlMap = new HashMap<>(); + imgUrlMap.put("type", "image_url"); + final Map urlMap = new HashMap<>(); + urlMap.put("url", image); + imgUrlMap.put("image_url", urlMap); + content.add(imgUrlMap); + } + + //添加视频参数 + if (videoParams != null && !videoParams.isEmpty()) { + //如果有文本参数就加在后面 + if (textMap != null && !textMap.isEmpty()) { + int textIndex = content.indexOf(textMap); + StringBuilder textBuilder = new StringBuilder(text); + for (HutoolCommon.HutoolVideo videoParam : videoParams) { + textBuilder.append(" ").append(videoParam.getType()).append(" ").append(videoParam.getValue()); + } + textMap.put("type", "text"); + textMap.put("text", textBuilder.toString()); + + if (textIndex != -1) { + content.set(textIndex, textMap); + } else { + content.add(textMap); + } + } else { + //如果没有文本参数就重新增加 + StringBuilder textBuilder = new StringBuilder(); + for (HutoolCommon.HutoolVideo videoParam : videoParams) { + textBuilder.append(videoParam.getType()).append(videoParam.getValue()).append(" "); + } + textMap.put("type", "text"); + textMap.put("text", textBuilder.toString()); + content.add(textMap); + } + } + + paramMap.put("content", content); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + System.out.println(JSONUtil.toJsonStr(paramMap)); + return JSONUtil.toJsonStr(paramMap); + } + +} diff --git a/hutool-ai/src/main/java/cn/hutool/ai/model/hutool/package-info.java b/hutool-ai/src/main/java/cn/hutool/ai/model/hutool/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..f89797d27a77b67c557b04be47ce27e92f3d9799 --- /dev/null +++ b/hutool-ai/src/main/java/cn/hutool/ai/model/hutool/package-info.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * 对hutool的封装实现 + * + * @author elichow + * @since 5.8.39 + */ + +package cn.hutool.ai.model.hutool; diff --git a/hutool-ai/src/main/java/cn/hutool/ai/model/ollama/OllamaCommon.java b/hutool-ai/src/main/java/cn/hutool/ai/model/ollama/OllamaCommon.java new file mode 100644 index 0000000000000000000000000000000000000000..e504c61c17bcc853bba25bddca4f7a0a57630cbb --- /dev/null +++ b/hutool-ai/src/main/java/cn/hutool/ai/model/ollama/OllamaCommon.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.hutool.ai.model.ollama; + +/** + * Ollama公共类 + * + * @author yangruoyu-yumeisoft + * @since 5.8.40 + */ +public class OllamaCommon { + + /** + * Ollama模型格式枚举 + */ + public enum OllamaFormat { + /** + * JSON格式 + */ + JSON("json"), + /** + * 无格式 + */ + NONE(""); + + private final String format; + + OllamaFormat(String format) { + this.format = format; + } + + public String getFormat() { + return format; + } + } + + /** + * Ollama选项常量 + */ + public static class Options { + /** + * 温度参数 + */ + public static final String TEMPERATURE = "temperature"; + /** + * top_p参数 + */ + public static final String TOP_P = "top_p"; + /** + * top_k参数 + */ + public static final String TOP_K = "top_k"; + /** + * 最大token数 + */ + public static final String NUM_PREDICT = "num_predict"; + /** + * 随机种子 + */ + public static final String SEED = "seed"; + } +} diff --git a/hutool-ai/src/main/java/cn/hutool/ai/model/ollama/OllamaConfig.java b/hutool-ai/src/main/java/cn/hutool/ai/model/ollama/OllamaConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..761c1f1b0f553ad6ed60f4e4db5f66188a20c209 --- /dev/null +++ b/hutool-ai/src/main/java/cn/hutool/ai/model/ollama/OllamaConfig.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.hutool.ai.model.ollama; + +import cn.hutool.ai.Models; +import cn.hutool.ai.core.BaseConfig; + +/** + * Ollama配置类,初始化API接口地址,设置默认的模型 + * + * @author yangruoyu-yumeisoft + * @since 5.8.40 + */ +public class OllamaConfig extends BaseConfig { + + private final String API_URL = "http://localhost:11434"; + + private final String DEFAULT_MODEL = Models.Ollama.QWEN3_32B.getModel(); + + public OllamaConfig() { + setApiUrl(API_URL); + setModel(DEFAULT_MODEL); + } + + public OllamaConfig(String apiUrl) { + this(); + setApiUrl(apiUrl); + } + + public OllamaConfig(String apiUrl, String model) { + this(); + setApiUrl(apiUrl); + setModel(model); + } + + @Override + public String getModelName() { + return "ollama"; + } + +} diff --git a/hutool-ai/src/main/java/cn/hutool/ai/model/ollama/OllamaProvider.java b/hutool-ai/src/main/java/cn/hutool/ai/model/ollama/OllamaProvider.java new file mode 100644 index 0000000000000000000000000000000000000000..ec2639b3340b3a32ad699ee7a5bea9c57afd12b0 --- /dev/null +++ b/hutool-ai/src/main/java/cn/hutool/ai/model/ollama/OllamaProvider.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.hutool.ai.model.ollama; + +import cn.hutool.ai.core.AIConfig; +import cn.hutool.ai.core.AIServiceProvider; + +/** + * 创建Ollama服务实现类 + * + * @author yangruoyu-yumeisoft + * @since 5.8.40 + */ +public class OllamaProvider implements AIServiceProvider { + + @Override + public String getServiceName() { + return "ollama"; + } + + @Override + public OllamaService create(final AIConfig config) { + return new OllamaServiceImpl(config); + } +} diff --git a/hutool-ai/src/main/java/cn/hutool/ai/model/ollama/OllamaService.java b/hutool-ai/src/main/java/cn/hutool/ai/model/ollama/OllamaService.java new file mode 100644 index 0000000000000000000000000000000000000000..e1a947e68e436744e4da4a8dab6a7b17e4508877 --- /dev/null +++ b/hutool-ai/src/main/java/cn/hutool/ai/model/ollama/OllamaService.java @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.hutool.ai.model.ollama; + +import cn.hutool.ai.core.AIService; +import cn.hutool.ai.core.Message; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +/** + * Ollama特有的功能 + * + * @author yangruoyu-yumeisoft + * @since 5.8.40 + */ +public interface OllamaService extends AIService { + + /** + * 生成文本补全 + * + * @param prompt 输入提示 + * @return AI回答 + * @since 5.8.40 + */ + String generate(String prompt); + + /** + * 生成文本补全-SSE流式输出 + * + * @param prompt 输入提示 + * @param callback 流式数据回调函数 + * @since 5.8.40 + */ + void generate(String prompt, Consumer callback); + + /** + * 生成文本补全(带选项) + * + * @param prompt 输入提示 + * @param format 响应格式 + * @return AI回答 + * @since 5.8.40 + */ + String generate(String prompt, String format); + + /** + * 生成文本补全(带选项)-SSE流式输出 + * + * @param prompt 输入提示 + * @param format 响应格式 + * @param callback 流式数据回调函数 + * @since 5.8.40 + */ + void generate(String prompt, String format, Consumer callback); + + /** + * 生成文本嵌入向量 + * + * @param prompt 输入文本 + * @return 嵌入向量结果 + * @since 5.8.40 + */ + String embeddings(String prompt); + + /** + * 列出本地可用的模型 + * + * @return 模型列表 + * @since 5.8.40 + */ + String listModels(); + + /** + * 显示模型信息 + * + * @param modelName 模型名称 + * @return 模型信息 + * @since 5.8.40 + */ + String showModel(String modelName); + + /** + * 拉取模型 + * + * @param modelName 模型名称 + * @return 拉取结果 + * @since 5.8.40 + */ + String pullModel(String modelName); + + /** + * 删除模型 + * + * @param modelName 模型名称 + * @return 删除结果 + * @since 5.8.40 + */ + String deleteModel(String modelName); + + /** + * 复制模型 + * + * @param source 源模型名称 + * @param destination 目标模型名称 + * @return 复制结果 + * @since 5.8.40 + */ + String copyModel(String source, String destination); + + /** + * 简化的对话方法 + * + * @param prompt 对话题词 + * @return AI回答 + * @since 5.8.40 + */ + default String chat(String prompt) { + final List messages = new ArrayList<>(); + messages.add(new Message("user", prompt)); + return chat(messages); + } + + /** + * 简化的对话方法-SSE流式输出 + * + * @param prompt 对话题词 + * @param callback 流式数据回调函数 + * @since 5.8.40 + */ + default void chat(String prompt, Consumer callback) { + final List messages = new ArrayList<>(); + messages.add(new Message("user", prompt)); + chat(messages, callback); + } +} diff --git a/hutool-ai/src/main/java/cn/hutool/ai/model/ollama/OllamaServiceImpl.java b/hutool-ai/src/main/java/cn/hutool/ai/model/ollama/OllamaServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..503a795c9f598d6de98144d445faa5633453ffc4 --- /dev/null +++ b/hutool-ai/src/main/java/cn/hutool/ai/model/ollama/OllamaServiceImpl.java @@ -0,0 +1,272 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.hutool.ai.model.ollama; + +import cn.hutool.ai.AIException; +import cn.hutool.ai.core.AIConfig; +import cn.hutool.ai.core.BaseAIService; +import cn.hutool.ai.core.Message; +import cn.hutool.core.bean.BeanPath; +import cn.hutool.core.thread.ThreadUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.http.Header; +import cn.hutool.http.HttpRequest; +import cn.hutool.http.HttpResponse; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +/** + * Ollama服务,AI具体功能的实现 + * + * @author yangruoyu-yumeisoft + * @since 5.8.40 + */ +public class OllamaServiceImpl extends BaseAIService implements OllamaService { + + // 对话补全 + private static final String CHAT_ENDPOINT = "/api/chat"; + // 文本生成 + private static final String GENERATE_ENDPOINT = "/api/generate"; + // 文本嵌入 + private static final String EMBEDDINGS_ENDPOINT = "/api/embeddings"; + // 列出模型 + private static final String LIST_MODELS_ENDPOINT = "/api/tags"; + // 显示模型信息 + private static final String SHOW_MODEL_ENDPOINT = "/api/show"; + // 拉取模型 + private static final String PULL_MODEL_ENDPOINT = "/api/pull"; + // 删除模型 + private static final String DELETE_MODEL_ENDPOINT = "/api/delete"; + // 复制模型 + private static final String COPY_MODEL_ENDPOINT = "/api/copy"; + + /** + * 构造函数 + * + * @param config AI配置 + */ + public OllamaServiceImpl(final AIConfig config) { + super(config); + } + + @Override + public String chat(final List messages) { + final String paramJson = buildChatRequestBody(messages); + final HttpResponse response = sendPost(CHAT_ENDPOINT, paramJson); + JSONObject responseJson = JSONUtil.parseObj(response.body()); + Object errorMessage = BeanPath.create("error").get(responseJson); + if(errorMessage!=null){ + throw new RuntimeException(errorMessage.toString()); + } + return BeanPath.create("message.content").get(responseJson).toString(); + } + + @Override + public void chat(final List messages, final Consumer callback) { + Map paramMap = buildChatStreamRequestBody(messages); + ThreadUtil.newThread(() -> sendPostStream(CHAT_ENDPOINT, paramMap, callback::accept), "ollama-chat-sse").start(); + } + + @Override + public String generate(String prompt) { + final String paramJson = buildGenerateRequestBody(prompt, null); + final HttpResponse response = sendPost(GENERATE_ENDPOINT, paramJson); + return response.body(); + } + + @Override + public void generate(String prompt, Consumer callback) { + Map paramMap = buildGenerateStreamRequestBody(prompt, null); + ThreadUtil.newThread(() -> sendPostStream(GENERATE_ENDPOINT, paramMap, callback::accept), "ollama-generate-sse").start(); + } + + @Override + public String generate(String prompt, String format) { + final String paramJson = buildGenerateRequestBody(prompt, format); + final HttpResponse response = sendPost(GENERATE_ENDPOINT, paramJson); + return response.body(); + } + + @Override + public void generate(String prompt, String format, Consumer callback) { + Map paramMap = buildGenerateStreamRequestBody(prompt, format); + ThreadUtil.newThread(() -> sendPostStream(GENERATE_ENDPOINT, paramMap, callback::accept), "ollama-generate-sse").start(); + } + + @Override + public String embeddings(String prompt) { + final String paramJson = buildEmbeddingsRequestBody(prompt); + final HttpResponse response = sendPost(EMBEDDINGS_ENDPOINT, paramJson); + return response.body(); + } + + @Override + public String listModels() { + final HttpResponse response = sendGet(LIST_MODELS_ENDPOINT); + return response.body(); + } + + @Override + public String showModel(String modelName) { + final String paramJson = buildShowModelRequestBody(modelName); + final HttpResponse response = sendPost(SHOW_MODEL_ENDPOINT, paramJson); + return response.body(); + } + + @Override + public String pullModel(String modelName) { + final String paramJson = buildPullModelRequestBody(modelName); + final HttpResponse response = sendPost(PULL_MODEL_ENDPOINT, paramJson); + return response.body(); + } + + @Override + public String deleteModel(String modelName) { + final String paramJson = buildDeleteModelRequestBody(modelName); + final HttpResponse response = sendDeleteRequest(DELETE_MODEL_ENDPOINT, paramJson); + return response.body(); + } + + @Override + public String copyModel(String source, String destination) { + final String paramJson = buildCopyModelRequestBody(source, destination); + final HttpResponse response = sendPost(COPY_MODEL_ENDPOINT, paramJson); + return response.body(); + } + + // 构建chat请求体 + private String buildChatRequestBody(final List messages) { + final Map paramMap = new HashMap<>(); + paramMap.put("stream",false); + paramMap.put("model", config.getModel()); + paramMap.put("messages", messages); + // 合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + + return JSONUtil.toJsonStr(paramMap); + } + + // 构建chatStream请求体 + private Map buildChatStreamRequestBody(final List messages) { + final Map paramMap = new HashMap<>(); + paramMap.put("stream", true); + paramMap.put("model", config.getModel()); + paramMap.put("messages", messages); + // 合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + + return paramMap; + } + + // 构建generate请求体 + private String buildGenerateRequestBody(final String prompt, final String format) { + final Map paramMap = new HashMap<>(); + paramMap.put("model", config.getModel()); + paramMap.put("prompt", prompt); + if (StrUtil.isNotBlank(format)) { + paramMap.put("format", format); + } + // 合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + + return JSONUtil.toJsonStr(paramMap); + } + + // 构建generateStream请求体 + private Map buildGenerateStreamRequestBody(final String prompt, final String format) { + final Map paramMap = new HashMap<>(); + paramMap.put("stream", true); + paramMap.put("model", config.getModel()); + paramMap.put("prompt", prompt); + if (StrUtil.isNotBlank(format)) { + paramMap.put("format", format); + } + // 合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + + return paramMap; + } + + // 构建embeddings请求体 + private String buildEmbeddingsRequestBody(final String prompt) { + final Map paramMap = new HashMap<>(); + paramMap.put("model", config.getModel()); + paramMap.put("prompt", prompt); + // 合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + + return JSONUtil.toJsonStr(paramMap); + } + + // 构建showModel请求体 + private String buildShowModelRequestBody(final String modelName) { + final Map paramMap = new HashMap<>(); + paramMap.put("name", modelName); + + return JSONUtil.toJsonStr(paramMap); + } + + // 构建pullModel请求体 + private String buildPullModelRequestBody(final String modelName) { + final Map paramMap = new HashMap<>(); + paramMap.put("name", modelName); + + return JSONUtil.toJsonStr(paramMap); + } + + // 构建deleteModel请求体 + private String buildDeleteModelRequestBody(final String modelName) { + final Map paramMap = new HashMap<>(); + paramMap.put("name", modelName); + + return JSONUtil.toJsonStr(paramMap); + } + + /** + * 发送DELETE请求 + * + * @param endpoint 请求端点 + * @param paramJson 请求参数JSON + * @return 响应结果 + */ + private HttpResponse sendDeleteRequest(String endpoint, String paramJson) { + try { + return HttpRequest.delete(config.getApiUrl() + endpoint) + .header(Header.CONTENT_TYPE, "application/json") + .header(Header.ACCEPT, "application/json") + .body(paramJson) + .timeout(config.getTimeout()) + .execute(); + } catch (Exception e) { + throw new AIException("Failed to send DELETE request: " + e.getMessage(), e); + } + } + + // 构建copyModel请求体 + private String buildCopyModelRequestBody(final String source, final String destination) { + Map requestBody = new HashMap<>(); + requestBody.put("source", source); + requestBody.put("destination", destination); + return JSONUtil.toJsonStr(requestBody); + } + +} diff --git a/hutool-ai/src/main/java/cn/hutool/ai/model/ollama/package-info.java b/hutool-ai/src/main/java/cn/hutool/ai/model/ollama/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..bf4a6e25321b97e5b396b564896e132d1baa01cc --- /dev/null +++ b/hutool-ai/src/main/java/cn/hutool/ai/model/ollama/package-info.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * 对Ollama的封装实现. + *

+ * 使用方法: + * // 创建AI服务 + *

{@code
+ * OllamaService aiService = AIServiceFactory.getAIService(
+ * new AIConfigBuilder(ModelName.OLLAMA.getValue())
+ * .setApiUrl("http://localhost:11434")
+ * .setModel("qwen2.5-coder:32b")
+ * .build(),
+ * OllamaService.class
+ * );
+ *
+ * // 构造上下文
+ * List messageList=new ArrayList<>();
+ * messageList.add(new Message("system","你是一个疯疯癫癫的机器人"));
+ * messageList.add(new Message("user","你能帮我做什么"));
+ *
+ * // 输出对话结果
+ * Console.log(aiService.chat(messageList));
+ * }
+ * + * @author yangruoyu-yumeisoft + * @since 5.8.40 + */ +package cn.hutool.ai.model.ollama; diff --git a/hutool-ai/src/main/java/cn/hutool/ai/model/openai/OpenaiCommon.java b/hutool-ai/src/main/java/cn/hutool/ai/model/openai/OpenaiCommon.java new file mode 100644 index 0000000000000000000000000000000000000000..5cca73f2804c3f59fdb51be3a87603b12cc1757a --- /dev/null +++ b/hutool-ai/src/main/java/cn/hutool/ai/model/openai/OpenaiCommon.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.hutool.ai.model.openai; + +/** + * openai公共类 + * + * @author elichow + * @since 5.8.38 + */ +public class OpenaiCommon { + + //openai推理参数 + public enum OpenaiReasoning { + + LOW("low"), + MEDIUM("medium"), + HIGH("high"); + + private final String effort; + + OpenaiReasoning(String effort) { + this.effort = effort; + } + + public String getEffort() { + return effort; + } + } + + //openai视觉参数 + public enum OpenaiVision { + + AUTO("auto"), + LOW("low"), + HIGH("high"); + + private final String detail; + + OpenaiVision(String detail) { + this.detail = detail; + } + + public String getDetail() { + return detail; + } + } + + //openai音频参数 + public enum OpenaiSpeech { + + ALLOY("alloy"), + ASH("ash"), + CORAL("coral"), + ECHO("echo"), + FABLE("fable"), + ONYX("onyx"), + NOVA("nova"), + SAGE("sage"), + SHIMMER("shimmer"); + + private final String voice; + + OpenaiSpeech(String voice) { + this.voice = voice; + } + + public String getVoice() { + return voice; + } + } +} diff --git a/hutool-ai/src/main/java/cn/hutool/ai/model/openai/OpenaiConfig.java b/hutool-ai/src/main/java/cn/hutool/ai/model/openai/OpenaiConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..0d3eaeb6b03e21d010257eb50fc21c1a34e70723 --- /dev/null +++ b/hutool-ai/src/main/java/cn/hutool/ai/model/openai/OpenaiConfig.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.hutool.ai.model.openai; + +import cn.hutool.ai.Models; +import cn.hutool.ai.core.BaseConfig; + +/** + * openai配置类,初始化API接口地址,设置默认的模型 + * + * @author elichow + * @since 5.8.38 + */ +public class OpenaiConfig extends BaseConfig { + + private final String API_URL = "https://api.openai.com/v1"; + + private final String DEFAULT_MODEL = Models.Openai.GPT_4O.getModel(); + + public OpenaiConfig() { + setApiUrl(API_URL); + setModel(DEFAULT_MODEL); + } + + public OpenaiConfig(String apiKey) { + this(); + setApiKey(apiKey); + } + + @Override + public String getModelName() { + return "openai"; + } + +} diff --git a/hutool-ai/src/main/java/cn/hutool/ai/model/openai/OpenaiProvider.java b/hutool-ai/src/main/java/cn/hutool/ai/model/openai/OpenaiProvider.java new file mode 100644 index 0000000000000000000000000000000000000000..2e11350b5c9b8d154082db069ce6e19fb5d85736 --- /dev/null +++ b/hutool-ai/src/main/java/cn/hutool/ai/model/openai/OpenaiProvider.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.hutool.ai.model.openai; + +import cn.hutool.ai.core.AIConfig; +import cn.hutool.ai.core.AIServiceProvider; + +/** + * 创建Openai服务实现类 + * + * @author elichow + * @since 5.8.38 + */ +public class OpenaiProvider implements AIServiceProvider { + + @Override + public String getServiceName() { + return "openai"; + } + + @Override + public OpenaiService create(final AIConfig config) { + return new OpenaiServiceImpl(config); + } +} diff --git a/hutool-ai/src/main/java/cn/hutool/ai/model/openai/OpenaiService.java b/hutool-ai/src/main/java/cn/hutool/ai/model/openai/OpenaiService.java new file mode 100644 index 0000000000000000000000000000000000000000..4e7edad7ff51766b624e8bf9a186df21c959e4bf --- /dev/null +++ b/hutool-ai/src/main/java/cn/hutool/ai/model/openai/OpenaiService.java @@ -0,0 +1,288 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.hutool.ai.model.openai; + +import cn.hutool.ai.core.AIService; +import cn.hutool.ai.core.Message; + +import java.io.File; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +/** + * openai支持的扩展接口 + * + * @author elichow + * @since 5.8.38 + */ +public interface OpenaiService extends AIService { + + /** + * 图像理解:模型会依据传入的图片信息以及问题,给出回复。 + * + * @param prompt 题词 + * @param images 图片列表/或者图片Base64编码图片列表(URI形式) + * @param detail 手动设置图片的质量,取值范围high、low、auto,默认为auto + * @return AI回答 + * @since 5.8.38 + */ + String chatVision(String prompt, final List images, String detail); + + /** + * 图像理解-SSE流式输出 + * + * @param prompt 题词 + * @param images 图片列表/或者图片Base64编码图片列表(URI形式) + * @param detail 手动设置图片的质量,取值范围high、low、auto,默认为auto + * @param callback 流式数据回调函数 + * @since 5.8.39 + */ + void chatVision(String prompt, final List images, String detail,final Consumer callback); + + + /** + * 图像理解:模型会依据传入的图片信息以及问题,给出回复。 + * + * @param prompt 题词 + * @param images 传入的图片列表地址/或者图片Base64编码图片列表(URI形式) + * @return AI回答 + * @since 5.8.38 + */ + default String chatVision(String prompt, final List images) { + return chatVision(prompt, images, OpenaiCommon.OpenaiVision.AUTO.getDetail()); + } + + /** + * 图像理解-SSE流式输出 + * + * @param prompt 题词 + * @param images 传入的图片列表地址/或者图片Base64编码图片列表(URI形式) + * @param callback 流式数据回调函数 + * @since 5.8.39 + */ + default void chatVision(String prompt, final List images, final Consumer callback){ + chatVision(prompt, images, OpenaiCommon.OpenaiVision.AUTO.getDetail(), callback); + } + + /** + * 文生图 请设置config中model为支持图片功能的模型 DALL·E系列 + * + * @param prompt 题词 + * @return 包含生成图片的url + * @since 5.8.38 + */ + String imagesGenerations(String prompt); + + /** + * 图片编辑 该方法仅支持 DALL·E 2 model + * + * @param prompt 题词 + * @param image 需要编辑的图像必须是 PNG 格式 + * @param mask 如果提供,则是一个与编辑图像大小相同的遮罩图像应该是灰度图,白色表示需要编辑的区域,黑色表示不需要编辑的区域。 + * @return 包含生成图片的url + * @since 5.8.38 + */ + String imagesEdits(String prompt, final File image, final File mask); + + /** + * 图片编辑 该方法仅支持 DALL·E 2 model + * + * @param prompt 题词 + * @param image 需要编辑的图像必须是 PNG 格式 + * @return 包含生成图片的url + * @since 5.8.38 + */ + default String imagesEdits(String prompt, final File image) { + return imagesEdits(prompt, image, null); + } + + /** + * 图片变形 该方法仅支持 DALL·E 2 model + * + * @param image 需要变形的图像必须是 PNG 格式 + * @return 包含生成图片的url + * @since 5.8.38 + */ + String imagesVariations(final File image); + + /** + * TTS文本转语音 请设置config中model为支持TTS功能的模型 TTS系列 + * + * @param input 需要转成语音的文本 + * @param voice AI的音色 + * @return 返回的音频mp3文件流 + * @since 5.8.38 + */ + InputStream textToSpeech(String input, final OpenaiCommon.OpenaiSpeech voice); + + /** + * TTS文本转语音 请设置config中model为支持TTS功能的模型 TTS系列 + * + * @param input 需要转成语音的文本 + * @return 返回的音频mp3文件流 + * @since 5.8.38 + */ + default InputStream textToSpeech(String input) { + return textToSpeech(input, OpenaiCommon.OpenaiSpeech.ALLOY); + } + + /** + * STT音频转文本 请设置config中model为支持STT功能的模型 whisper + * + * @param file 需要转成文本的音频文件 + * @return 返回的文本内容 + * @since 5.8.38 + */ + String speechToText(final File file); + + /** + * 文本向量化 请设置config中model为支持文本向量化功能的模型 text-embedding系列 + * + * @param input 需要向量化的内容 + * @return 处理后的向量信息 + * @since 5.8.38 + */ + String embeddingText(String input); + + /** + * 检查文本或图像是否具有潜在的危害性 + * 仅支持omni-moderation-latest和text-moderation-latest模型 + * + * @param text 需要检查的文本 + * @param imgUrl 需要检查的图片地址 + * @return AI返回结果 + * @since 5.8.38 + */ + String moderations(String text, String imgUrl); + + /** + * 检查文本是否具有潜在的危害性 + * 仅支持omni-moderation-latest和text-moderation-latest模型 + * + * @param text 需要检查的文本 + * @return AI返回结果 + * @since 5.8.38 + */ + default String moderations(String text) { + return moderations(text, null); + } + + /** + * 推理chat + * 支持o3-mini和o1 + * + * @param prompt 对话题词 + * @param reasoningEffort 推理程度 + * @return AI回答 + * @since 5.8.38 + */ + default String chatReasoning(String prompt, String reasoningEffort){ + final List messages = new ArrayList<>(); + messages.add(new Message("system", "You are a helpful assistant")); + messages.add(new Message("user", prompt)); + return chatReasoning(messages, reasoningEffort); + } + + /** + * 推理chat-SSE流式输出 + * 支持o3-mini和o1 + * + * @param prompt 对话题词 + * @param reasoningEffort 推理程度 + * @param callback 流式数据回调函数 + * @since 5.8.39 + */ + default void chatReasoning(String prompt, String reasoningEffort, final Consumer callback){ + final List messages = new ArrayList<>(); + messages.add(new Message("system", "You are a helpful assistant")); + messages.add(new Message("user", prompt)); + chatReasoning(messages, reasoningEffort, callback); + } + + /** + * 推理chat + * 支持o3-mini和o1 + * + * @param prompt 对话题词 + * @return AI回答 + * @since 5.8.38 + */ + default String chatReasoning(String prompt) { + return chatReasoning(prompt, OpenaiCommon.OpenaiReasoning.MEDIUM.getEffort()); + } + + /** + * 推理chat-SSE流式输出 + * 支持o3-mini和o1 + * + * @param prompt 对话题词 + * @param callback 流式数据回调函数 + * @since 5.8.39 + */ + default void chatReasoning(String prompt, final Consumer callback) { + chatReasoning(prompt, OpenaiCommon.OpenaiReasoning.MEDIUM.getEffort(), callback); + } + + /** + * 推理chat + * 支持o3-mini和o1 + * + * @param messages 消息列表 + * @param reasoningEffort 推理程度 + * @return AI回答 + * @since 5.8.38 + */ + String chatReasoning(final List messages, String reasoningEffort); + + /** + * 推理chat-SSE流式输出 + * 支持o3-mini和o1 + * + * @param messages 消息列表 + * @param reasoningEffort 推理程度 + * @param callback 流式数据回调函数 + * @since 5.8.39 + */ + void chatReasoning(final List messages, String reasoningEffort, final Consumer callback); + + /** + * 推理chat + * 支持o3-mini和o1 + * + * @param messages 消息列表 + * @return AI回答 + * @since 5.8.38 + */ + default String chatReasoning(final List messages) { + return chatReasoning(messages, OpenaiCommon.OpenaiReasoning.MEDIUM.getEffort()); + } + + /** + * 推理chat-SSE流式输出 + * 支持o3-mini和o1 + * + * @param messages 消息列表 + * @param callback 流式数据回调函数 + * @since 5.8.39 + */ + default void chatReasoning(final List messages, final Consumer callback) { + chatReasoning(messages, OpenaiCommon.OpenaiReasoning.MEDIUM.getEffort(), callback); + } + +} diff --git a/hutool-ai/src/main/java/cn/hutool/ai/model/openai/OpenaiServiceImpl.java b/hutool-ai/src/main/java/cn/hutool/ai/model/openai/OpenaiServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..67f7d255514394a7f6d41a27b9fcbcee4c214652 --- /dev/null +++ b/hutool-ai/src/main/java/cn/hutool/ai/model/openai/OpenaiServiceImpl.java @@ -0,0 +1,365 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.hutool.ai.model.openai; + +import cn.hutool.ai.core.AIConfig; +import cn.hutool.ai.core.BaseAIService; +import cn.hutool.ai.core.Message; +import cn.hutool.core.thread.ThreadUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.http.HttpResponse; +import cn.hutool.json.JSONUtil; + +import java.io.File; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +/** + * openai服务,AI具体功能的实现 + * + * @author elichow + * @since 5.8.38 + */ +public class OpenaiServiceImpl extends BaseAIService implements OpenaiService { + + //对话 + private final String CHAT_ENDPOINT = "/chat/completions"; + //文生图 + private final String IMAGES_GENERATIONS = "/images/generations"; + //图片编辑 + private final String IMAGES_EDITS = "/images/edits"; + //图片变形 + private final String IMAGES_VARIATIONS = "/images/variations"; + //文本转语音 + private final String TTS = "/audio/speech"; + //语音转文本 + private final String STT = "/audio/transcriptions"; + //文本向量化 + private final String EMBEDDINGS = "/embeddings"; + //检查文本或图片 + private final String MODERATIONS = "/moderations"; + + public OpenaiServiceImpl(final AIConfig config) { + //初始化Openai客户端 + super(config); + } + + @Override + public String chat(final List messages) { + String paramJson = buildChatRequestBody(messages); + final HttpResponse response = sendPost(CHAT_ENDPOINT, paramJson); + return response.body(); + } + + @Override + public void chat(List messages,Consumer callback) { + Map paramMap = buildChatStreamRequestBody(messages); + ThreadUtil.newThread(() -> sendPostStream(CHAT_ENDPOINT, paramMap, callback::accept), "openai-chat-sse").start(); + } + + @Override + public String chatVision(String prompt, final List images, String detail) { + String paramJson = buildChatVisionRequestBody(prompt, images, detail); + final HttpResponse response = sendPost(CHAT_ENDPOINT, paramJson); + return response.body(); + } + + @Override + public void chatVision(String prompt, List images, String detail, Consumer callback) { + Map paramMap = buildChatVisionStreamRequestBody(prompt, images, detail); + ThreadUtil.newThread(() -> sendPostStream(CHAT_ENDPOINT, paramMap, callback::accept), "openai-chatVision-sse").start(); + } + + @Override + public String imagesGenerations(String prompt) { + String paramJson = buildImagesGenerationsRequestBody(prompt); + final HttpResponse response = sendPost(IMAGES_GENERATIONS, paramJson); + return response.body(); + } + + @Override + public String imagesEdits(String prompt, final File image, final File mask) { + final Map paramMap = buildImagesEditsRequestBody(prompt, image, mask); + final HttpResponse response = sendFormData(IMAGES_EDITS, paramMap); + return response.body(); + } + + @Override + public String imagesVariations(final File image) { + final Map paramMap = buildImagesVariationsRequestBody(image); + final HttpResponse response = sendFormData(IMAGES_VARIATIONS, paramMap); + return response.body(); + } + + @Override + public InputStream textToSpeech(String input, final OpenaiCommon.OpenaiSpeech voice) { + String paramJson = buildTTSRequestBody(input, voice.getVoice()); + final HttpResponse response = sendPost(TTS, paramJson); + return response.bodyStream(); + } + + @Override + public String speechToText(final File file) { + final Map paramMap = buildSTTRequestBody(file); + final HttpResponse response = sendFormData(STT, paramMap); + return response.body(); + } + + @Override + public String embeddingText(String input) { + String paramJson = buildEmbeddingTextRequestBody(input); + final HttpResponse response = sendPost(EMBEDDINGS, paramJson); + return response.body(); + } + + @Override + public String moderations(String text, String imgUrl) { + String paramJson = buileModerationsRequestBody(text, imgUrl); + final HttpResponse response = sendPost(MODERATIONS, paramJson); + return response.body(); + } + + @Override + public String chatReasoning(final List messages, String reasoningEffort) { + String paramJson = buildChatReasoningRequestBody(messages, reasoningEffort); + final HttpResponse response = sendPost(CHAT_ENDPOINT, paramJson); + return response.body(); + } + + @Override + public void chatReasoning(List messages, String reasoningEffort, Consumer callback) { + Map paramMap = buildChatReasoningStreamRequestBody(messages, reasoningEffort); + ThreadUtil.newThread(() -> sendPostStream(CHAT_ENDPOINT, paramMap, callback::accept), "openai-chatReasoning-sse").start(); + } + + // 构建chat请求体 + private String buildChatRequestBody(final List messages) { + //使用JSON工具 + final Map paramMap = new HashMap<>(); + paramMap.put("model", config.getModel()); + paramMap.put("messages", messages); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + + return JSONUtil.toJsonStr(paramMap); + } + + private Map buildChatStreamRequestBody(final List messages) { + //使用JSON工具 + final Map paramMap = new HashMap<>(); + paramMap.put("stream", true); + paramMap.put("model", config.getModel()); + paramMap.put("messages", messages); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + + return paramMap; + } + + //构建chatVision请求体 + private String buildChatVisionRequestBody(String prompt, final List images, String detail) { + // 定义消息结构 + final List messages = new ArrayList<>(); + final List content = new ArrayList<>(); + + final Map contentMap = new HashMap<>(); + contentMap.put("type", "text"); + contentMap.put("text", prompt); + content.add(contentMap); + for (String img : images) { + final Map imgUrlMap = new HashMap<>(); + imgUrlMap.put("type", "image_url"); + final Map urlMap = new HashMap<>(); + urlMap.put("url", img); + urlMap.put("detail", detail); + imgUrlMap.put("image_url", urlMap); + content.add(imgUrlMap); + } + + messages.add(new Message("user", content)); + + //使用JSON工具 + final Map paramMap = new HashMap<>(); + paramMap.put("model", config.getModel()); + paramMap.put("messages", messages); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + return JSONUtil.toJsonStr(paramMap); + } + + private Map buildChatVisionStreamRequestBody(String prompt, final List images, String detail) { + // 定义消息结构 + final List messages = new ArrayList<>(); + final List content = new ArrayList<>(); + + final Map contentMap = new HashMap<>(); + contentMap.put("type", "text"); + contentMap.put("text", prompt); + content.add(contentMap); + for (String img : images) { + HashMap imgUrlMap = new HashMap<>(); + imgUrlMap.put("type", "image_url"); + HashMap urlMap = new HashMap<>(); + urlMap.put("url", img); + urlMap.put("detail", detail); + imgUrlMap.put("image_url", urlMap); + content.add(imgUrlMap); + } + + messages.add(new Message("user", content)); + + //使用JSON工具 + final Map paramMap = new HashMap<>(); + paramMap.put("stream", true); + paramMap.put("model", config.getModel()); + paramMap.put("messages", messages); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + return paramMap; + } + + //构建文生图请求体 + private String buildImagesGenerationsRequestBody(String prompt) { + final Map paramMap = new HashMap<>(); + paramMap.put("model", config.getModel()); + paramMap.put("prompt", prompt); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + + return JSONUtil.toJsonStr(paramMap); + } + + //构建图片编辑请求体 + private Map buildImagesEditsRequestBody(String prompt, final File image, final File mask) { + final Map paramMap = new HashMap<>(); + paramMap.put("model", config.getModel()); + paramMap.put("prompt", prompt); + paramMap.put("image", image); + if (mask != null) { + paramMap.put("mask", mask); + } + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + + return paramMap; + } + + //构建图片变形请求体 + private Map buildImagesVariationsRequestBody(final File image) { + final Map paramMap = new HashMap<>(); + paramMap.put("model", config.getModel()); + paramMap.put("image", image); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + + return paramMap; + } + + //构建TTS请求体 + private String buildTTSRequestBody(String input, String voice) { + final Map paramMap = new HashMap<>(); + paramMap.put("model", config.getModel()); + paramMap.put("input", input); + paramMap.put("voice", voice); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + + return JSONUtil.toJsonStr(paramMap); + } + + //构建STT请求体 + private Map buildSTTRequestBody(final File file) { + final Map paramMap = new HashMap<>(); + paramMap.put("model", config.getModel()); + paramMap.put("file", file); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + + return paramMap; + } + + //构建文本向量化请求体 + private String buildEmbeddingTextRequestBody(String input) { + //使用JSON工具 + final Map paramMap = new HashMap<>(); + paramMap.put("model", config.getModel()); + paramMap.put("input", input); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + return JSONUtil.toJsonStr(paramMap); + } + + //构建检查图片或文字请求体 + private String buileModerationsRequestBody(String text, String imgUrl) { + //使用JSON工具 + final Map paramMap = new HashMap<>(); + paramMap.put("model", config.getModel()); + + final List input = new ArrayList<>(); + //添加文本参数 + if (!StrUtil.isBlank(text)) { + final Map textMap = new HashMap<>(); + textMap.put("type", "text"); + textMap.put("text", text); + input.add(textMap); + } + //添加图片参数 + if (!StrUtil.isBlank(imgUrl)) { + final Map imgUrlMap = new HashMap<>(); + imgUrlMap.put("type", "image_url"); + final Map urlMap = new HashMap<>(); + urlMap.put("url", imgUrl); + imgUrlMap.put("image_url", urlMap); + input.add(imgUrlMap); + } + + paramMap.put("input", input); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + + return JSONUtil.toJsonStr(paramMap); + } + + //构建推理请求体 + private String buildChatReasoningRequestBody(final List messages, String reasoningEffort) { + final Map paramMap = new HashMap<>(); + paramMap.put("model", config.getModel()); + paramMap.put("messages", messages); + paramMap.put("reasoning_effort", reasoningEffort); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + + return JSONUtil.toJsonStr(paramMap); + } + + private Map buildChatReasoningStreamRequestBody(final List messages, String reasoningEffort) { + final Map paramMap = new HashMap<>(); + paramMap.put("stream", true); + paramMap.put("model", config.getModel()); + paramMap.put("messages", messages); + paramMap.put("reasoning_effort", reasoningEffort); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + + return paramMap; + } + +} diff --git a/hutool-ai/src/main/java/cn/hutool/ai/model/openai/package-info.java b/hutool-ai/src/main/java/cn/hutool/ai/model/openai/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..8622b9e90759f10773cb3a06dfe35de010a203ee --- /dev/null +++ b/hutool-ai/src/main/java/cn/hutool/ai/model/openai/package-info.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * 对openai的封装实现 + * + * @author elichow + * @since 5.8.38 + */ + +package cn.hutool.ai.model.openai; diff --git a/hutool-ai/src/main/java/cn/hutool/ai/model/package-info.java b/hutool-ai/src/main/java/cn/hutool/ai/model/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..bdac4d5f14eab7354b07c8c9ce51bdfcb3d05b9d --- /dev/null +++ b/hutool-ai/src/main/java/cn/hutool/ai/model/package-info.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * 对各个AI大模型的相关封装 + * + * @author elichow + * @since 5.8.38 + */ + +package cn.hutool.ai.model; diff --git a/hutool-ai/src/main/java/cn/hutool/ai/package-info.java b/hutool-ai/src/main/java/cn/hutool/ai/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..19f36c3ca38b2490ac64e5d5120848752ca15488 --- /dev/null +++ b/hutool-ai/src/main/java/cn/hutool/ai/package-info.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Hutool-ai主要用于AI大模型的封装,只需要对AI模型最基本的设置,即可调用AI大模型。 + * + * @author elichow + * @since 5.8.38 + */ + +package cn.hutool.ai; diff --git a/hutool-ai/src/main/resources/META-INF/services/cn.hutool.ai.core.AIConfig b/hutool-ai/src/main/resources/META-INF/services/cn.hutool.ai.core.AIConfig new file mode 100644 index 0000000000000000000000000000000000000000..f37c7fddd426ae9f56eff42ec0aff5a29f3d0e96 --- /dev/null +++ b/hutool-ai/src/main/resources/META-INF/services/cn.hutool.ai.core.AIConfig @@ -0,0 +1,6 @@ +cn.hutool.ai.model.hutool.HutoolConfig +cn.hutool.ai.model.deepseek.DeepSeekConfig +cn.hutool.ai.model.openai.OpenaiConfig +cn.hutool.ai.model.doubao.DoubaoConfig +cn.hutool.ai.model.grok.GrokConfig +cn.hutool.ai.model.ollama.OllamaConfig diff --git a/hutool-ai/src/main/resources/META-INF/services/cn.hutool.ai.core.AIServiceProvider b/hutool-ai/src/main/resources/META-INF/services/cn.hutool.ai.core.AIServiceProvider new file mode 100644 index 0000000000000000000000000000000000000000..58c8a5d549966242a757cbe6b2948a02ab0385e4 --- /dev/null +++ b/hutool-ai/src/main/resources/META-INF/services/cn.hutool.ai.core.AIServiceProvider @@ -0,0 +1,6 @@ +cn.hutool.ai.model.hutool.HutoolProvider +cn.hutool.ai.model.deepseek.DeepSeekProvider +cn.hutool.ai.model.openai.OpenaiProvider +cn.hutool.ai.model.doubao.DoubaoProvider +cn.hutool.ai.model.grok.GrokProvider +cn.hutool.ai.model.ollama.OllamaProvider diff --git a/hutool-ai/src/test/java/AIServiceFactoryTest.java b/hutool-ai/src/test/java/AIServiceFactoryTest.java new file mode 100644 index 0000000000000000000000000000000000000000..8a959bd1bf4de49cd80dc9b21bc370ae7113ecb5 --- /dev/null +++ b/hutool-ai/src/test/java/AIServiceFactoryTest.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import cn.hutool.ai.AIServiceFactory; +import cn.hutool.ai.ModelName; +import cn.hutool.ai.core.AIConfigBuilder; +import cn.hutool.ai.core.AIService; +import cn.hutool.ai.model.deepseek.DeepSeekService; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class AIServiceFactoryTest { + + String key = "your key"; + + @Test + void getAIService() { + final AIService aiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DEEPSEEK.getValue()).setApiKey(key).build()); + assertNotNull(aiService); + } + + @Test + void testGetAIService() { + final DeepSeekService deepSeekService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DEEPSEEK.getValue()).setApiKey(key).build(), DeepSeekService.class); + assertNotNull(deepSeekService); + } +} diff --git a/hutool-ai/src/test/java/AIUtilTest.java b/hutool-ai/src/test/java/AIUtilTest.java new file mode 100644 index 0000000000000000000000000000000000000000..2ae9d89be23c6735e2839401855fada5a41aac1f --- /dev/null +++ b/hutool-ai/src/test/java/AIUtilTest.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import cn.hutool.ai.AIUtil; +import cn.hutool.ai.ModelName; +import cn.hutool.ai.core.AIConfigBuilder; +import cn.hutool.ai.core.AIService; +import cn.hutool.ai.core.Message; +import cn.hutool.ai.model.deepseek.DeepSeekService; +import cn.hutool.ai.model.doubao.DoubaoService; +import cn.hutool.ai.model.grok.GrokService; +import cn.hutool.ai.model.hutool.HutoolService; +import cn.hutool.ai.model.openai.OpenaiService; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class AIUtilTest { + + String key = "your key"; + + @Test + void getAIService() { + final DeepSeekService deepSeekService = AIUtil.getAIService(new AIConfigBuilder(ModelName.DEEPSEEK.getValue()).setApiKey(key).build(), DeepSeekService.class); + assertNotNull(deepSeekService); + } + + @Test + void testGetAIService() { + final AIService aiService = AIUtil.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue()).setApiKey(key).build()); + assertNotNull(aiService); + } + + @Test + void getHutoolService() { + final HutoolService hutoolService = AIUtil.getHutoolService(new AIConfigBuilder(ModelName.HUTOOL.getValue()).setApiKey(key).build()); + assertNotNull(hutoolService); + } + + @Test + void getDeepSeekService() { + final DeepSeekService deepSeekService = AIUtil.getDeepSeekService(new AIConfigBuilder(ModelName.DEEPSEEK.getValue()).setApiKey(key).build()); + assertNotNull(deepSeekService); + } + + @Test + void getDoubaoService() { + final DoubaoService doubaoService = AIUtil.getDoubaoService(new AIConfigBuilder(ModelName.DOUBAO.getValue()).setApiKey(key).build()); + assertNotNull(doubaoService); + } + + @Test + void getGrokService() { + final GrokService grokService = AIUtil.getGrokService(new AIConfigBuilder(ModelName.GROK.getValue()).setApiKey(key).build()); + assertNotNull(grokService); + } + + @Test + void getOpenAIService() { + final OpenaiService openAIService = AIUtil.getOpenAIService(new AIConfigBuilder(ModelName.OPENAI.getValue()).setApiKey(key).build()); + assertNotNull(openAIService); + } + + @Test + void chat() { + final String chat = AIUtil.chat(new AIConfigBuilder(ModelName.DEEPSEEK.getValue()).setApiKey(key).build(), "写一首赞美我的诗"); + assertNotNull(chat); + } + + @Test + void testChat() { + final List messages = new ArrayList<>(); + messages.add(new Message("system","你是财神爷,只会说“我是财神”")); + messages.add(new Message("user","你是谁啊?")); + final String chat = AIUtil.chat(new AIConfigBuilder(ModelName.DEEPSEEK.getValue()).setApiKey(key).build(), messages); + assertNotNull(chat); + } +} diff --git a/hutool-ai/src/test/java/cn/hutool/ai/model/deepseek/DeepSeekServiceTest.java b/hutool-ai/src/test/java/cn/hutool/ai/model/deepseek/DeepSeekServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..629a3d004b5e37596d3f989ccf4417a75dd7a61c --- /dev/null +++ b/hutool-ai/src/test/java/cn/hutool/ai/model/deepseek/DeepSeekServiceTest.java @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.hutool.ai.model.deepseek; + +import cn.hutool.ai.AIServiceFactory; +import cn.hutool.ai.ModelName; +import cn.hutool.ai.core.AIConfigBuilder; +import cn.hutool.ai.core.Message; +import cn.hutool.core.thread.ThreadUtil; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class DeepSeekServiceTest { + + String key = "your key"; + DeepSeekService deepSeekService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DEEPSEEK.getValue()).setApiKey(key).build(),DeepSeekService.class); + + @Test + @Disabled + void chat(){ + final String chat = deepSeekService.chat("写一个疯狂星期四广告词"); + assertNotNull(chat); + } + + @Test + @Disabled + void chatStream() { + String prompt = "写一个疯狂星期四广告词"; + // 使用AtomicBoolean作为结束标志 + AtomicBoolean isDone = new AtomicBoolean(false); + + deepSeekService.chat(prompt, data -> { + assertNotNull(data); + if (data.contains("[DONE]")) { + // 设置结束标志 + isDone.set(true); + } else if (data.contains("\"error\"")) { + isDone.set(true); + } + + }); + // 轮询检查结束标志 + while (!isDone.get()) { + ThreadUtil.sleep(100); + } + } + + @Test + @Disabled + void testChat(){ + final List messages = new ArrayList<>(); + messages.add(new Message("system","你是个抽象大师,会说很抽象的话,最擅长说抽象的笑话")); + messages.add(new Message("user","给我说一个笑话")); + final String chat = deepSeekService.chat(messages); + assertNotNull(chat); + } + + @Test + @Disabled + void beta() { + final String beta = deepSeekService.beta("写一个疯狂星期四广告词"); + assertNotNull(beta); + + } + + @Test + @Disabled + void betaStream() { + String beta = "写一个疯狂星期四广告词"; + // 使用AtomicBoolean作为结束标志 + AtomicBoolean isDone = new AtomicBoolean(false); + + deepSeekService.beta(beta, data -> { + assertNotNull(data); + if (data.contains("[DONE]")) { + // 设置结束标志 + isDone.set(true); + } else if (data.contains("\"error\"")) { + isDone.set(true); + } + + }); + // 轮询检查结束标志 + while (!isDone.get()) { + ThreadUtil.sleep(100); + } + } + + @Test + @Disabled + void models() { + final String models = deepSeekService.models(); + assertNotNull(models); + } + + @Test + @Disabled + void balance() { + final String balance = deepSeekService.balance(); + assertNotNull(balance); + } +} diff --git a/hutool-ai/src/test/java/cn/hutool/ai/model/doubao/DoubaoServiceTest.java b/hutool-ai/src/test/java/cn/hutool/ai/model/doubao/DoubaoServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..c7b05756ab3e72f4c1abe3ee28afb2b585b27d34 --- /dev/null +++ b/hutool-ai/src/test/java/cn/hutool/ai/model/doubao/DoubaoServiceTest.java @@ -0,0 +1,311 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.hutool.ai.model.doubao; + +import cn.hutool.ai.AIServiceFactory; +import cn.hutool.ai.ModelName; +import cn.hutool.ai.Models; +import cn.hutool.ai.core.AIConfigBuilder; +import cn.hutool.ai.core.Message; +import cn.hutool.core.img.ImgUtil; +import cn.hutool.core.thread.ThreadUtil; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.awt.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class DoubaoServiceTest { + + String key = "your key"; + DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue()).setModel(Models.Doubao.DOUBAO_1_5_LITE_32K.getModel()).setApiKey(key).build(), DoubaoService.class); + + @Test + @Disabled + void chat(){ + final String chat = doubaoService.chat("写一个疯狂星期四广告词"); + assertNotNull(chat); + } + + @Test + @Disabled + void chatStream() { + String prompt = "写一个疯狂星期四广告词"; + // 使用AtomicBoolean作为结束标志 + AtomicBoolean isDone = new AtomicBoolean(false); + + doubaoService.chat(prompt, data -> { + assertNotNull(data); + if (data.contains("[DONE]")) { + // 设置结束标志 + isDone.set(true); + } else if (data.contains("\"error\"")) { + isDone.set(true); + } + + }); + // 轮询检查结束标志 + while (!isDone.get()) { + ThreadUtil.sleep(100); + } + } + + @Test + @Disabled + void testChat(){ + final List messages = new ArrayList<>(); + messages.add(new Message("system","你是个抽象大师,会说很抽象的话,最擅长说抽象的笑话")); + messages.add(new Message("user","给我说一个笑话")); + final String chat = doubaoService.chat(messages); + assertNotNull(chat); + } + + @Test + @Disabled + void chatVision() { + final DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue()) + .setApiKey(key).setModel(Models.Doubao.DOUBAO_1_5_VISION_PRO_32K.getModel()).build(), DoubaoService.class); + final String base64 = ImgUtil.toBase64DataUri(Toolkit.getDefaultToolkit().createImage("your imageUrl"), "png"); + final String chatVision = doubaoService.chatVision("图片上有些什么?", Arrays.asList(base64)); + assertNotNull(chatVision); + } + + @Test + @Disabled + void testChatVision() { + final DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue()) + .setApiKey(key).setModel(Models.Doubao.DOUBAO_1_5_VISION_PRO_32K.getModel()).build(), DoubaoService.class); + final String chatVision = doubaoService.chatVision("图片上有些什么?", Arrays.asList("https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544"),DoubaoCommon.DoubaoVision.HIGH.getDetail()); + assertNotNull(chatVision); + } + + @Test + @Disabled + void testChatVisionStream() { + final DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue()) + .setApiKey(key).setModel(Models.Doubao.DOUBAO_1_5_VISION_PRO_32K.getModel()).build(), DoubaoService.class); + + String prompt = "图片上有些什么?"; + List images = Arrays.asList("https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544"); + + // 使用AtomicBoolean作为结束标志 + AtomicBoolean isDone = new AtomicBoolean(false); + doubaoService.chatVision(prompt,images, data -> { + assertNotNull(data); + if (data.contains("[DONE]")) { + // 设置结束标志 + isDone.set(true); + } else if (data.contains("\"error\"")) { + isDone.set(true); + } + + }); + // 轮询检查结束标志 + while (!isDone.get()) { + ThreadUtil.sleep(100); + } + } + + @Test + @Disabled + void videoTasks() { + final DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue()) + .setApiKey(key).setModel(Models.Doubao.Doubao_Seedance_1_0_lite_i2v.getModel()).build(), DoubaoService.class); + final String videoTasks = doubaoService.videoTasks("生成一段动画视频,主角是大耳朵图图,一个活泼可爱的小男孩。视频中图图在公园里玩耍," + + "画面采用明亮温暖的卡通风格,色彩鲜艳,动作流畅。背景音乐轻快活泼,带有冒险感,音效包括鸟叫声、欢笑声和山洞回声。", "https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544"); + assertNotNull(videoTasks); + } + + @Test + @Disabled + void getVideoTasksInfo() { + //cgt-20250306170051-6r9gk + final DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue()) + .setApiKey(key).build(), DoubaoService.class); + final String videoTasksInfo = doubaoService.getVideoTasksInfo("cgt-20250306170051-6r9gk"); + assertNotNull(videoTasksInfo); + } + + @Test + @Disabled + void embeddingText() { + final DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue()) + .setApiKey(key).setModel(Models.Doubao.DOUBAO_EMBEDDING_TEXT_240715.getModel()).build(), DoubaoService.class); + final String embeddingText = doubaoService.embeddingText(new String[]{"阿斯顿", "马丁"}); + assertNotNull(embeddingText); + } + + @Test + @Disabled + void embeddingVision() { + final DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue()) + .setApiKey(key).setModel(Models.Doubao.DOUBAO_EMBEDDING_VISION.getModel()).build(), DoubaoService.class); + final String embeddingVision = doubaoService.embeddingVision("天空好难", "https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544"); + assertNotNull(embeddingVision); + } + + @Test + @Disabled + void botsChat() { + final DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue()) + .setApiKey(key).setModel("your bots id").build(), DoubaoService.class); + final ArrayList messages = new ArrayList<>(); + messages.add(new Message("system","你是什么都可以")); + messages.add(new Message("user","你想做些什么")); + final String botsChat = doubaoService.botsChat(messages); + assertNotNull(botsChat); + } + + @Test + @Disabled + void botsChatStream() { + final DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue()) + .setApiKey(key).setModel("your bots id").build(), DoubaoService.class); + final ArrayList messages = new ArrayList<>(); + messages.add(new Message("system","你是什么都可以")); + messages.add(new Message("user","你想做些什么")); + + // 使用AtomicBoolean作为结束标志 + AtomicBoolean isDone = new AtomicBoolean(false); + doubaoService.botsChat(messages, data -> { + assertNotNull(data); + if (data.contains("[DONE]")) { + // 设置结束标志 + isDone.set(true); + } else if (data.contains("\"error\"")) { + isDone.set(true); + } + + }); + // 轮询检查结束标志 + while (!isDone.get()) { + ThreadUtil.sleep(100); + } + } + + @Test + @Disabled + void tokenization() { + final String tokenization = doubaoService.tokenization(new String[]{"阿斯顿", "马丁"}); + assertNotNull(tokenization); + } + + @Test + @Disabled + void batchChat() { + final DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue()) + .setApiKey(key).setModel("your Endpoint ID").build(), DoubaoService.class); + final String batchChat = doubaoService.batchChat("写首歌词"); + assertNotNull(batchChat); + } + + @Test + @Disabled + void testBatchChat() { + final DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue()) + .setApiKey(key).setModel("your Endpoint ID").build(), DoubaoService.class); + final List messages = new ArrayList<>(); + messages.add(new Message("system","你是个抽象大师")); + messages.add(new Message("user","写一个KFC的抽象广告")); + final String batchChat = doubaoService.batchChat(messages); + assertNotNull(batchChat); + } + + @Test + @Disabled + void createContext() { + final DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue()) + .setApiKey(key).setModel("your Endpoint ID").build(), DoubaoService.class); + final List messages = new ArrayList<>(); + messages.add(new Message("system","你是个抽象大师,你真的很抽象")); + final String context = doubaoService.createContext(messages);//ctx-20250307092153-cvslm + assertNotNull(context); + } + + @Test + @Disabled + void testCreateContext() { + final DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue()) + .setApiKey(key).setModel("ep-20250305100610-bvbpc").build(), DoubaoService.class); + final List messages = new ArrayList<>(); + messages.add(new Message("system","你是个抽象大师,你真的很抽象")); + final String context = doubaoService.createContext(messages,DoubaoCommon.DoubaoContext.COMMON_PREFIX.getMode()); + assertNotNull(context);//ctx-20250307092153-cvslm + } + + @Test + @Disabled + void chatContext() { + //ctx-20250307092153-cvslm + final DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue()) + .setApiKey(key).setModel("your Endpoint ID").build(), DoubaoService.class); + final String chatContext = doubaoService.chatContext("你是谁?", "your contextId"); + assertNotNull(chatContext); + } + + @Test + @Disabled + void testChatContext() { + final DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue()) + .setApiKey(key).setModel("your Endpoint ID").build(), DoubaoService.class); + final List messages = new ArrayList<>(); + messages.add(new Message("user","你怎么看待意大利面拌水泥?")); + final String chatContext = doubaoService.chatContext(messages, "your contextId"); + assertNotNull(chatContext); + } + + @Test + @Disabled + void testChatContextStream() { + final DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue()) + .setApiKey(key).setModel("your Endpoint ID").build(), DoubaoService.class); + final List messages = new ArrayList<>(); + messages.add(new Message("user","你怎么看待意大利面拌水泥?")); + String contextId = "your contextId"; + + // 使用AtomicBoolean作为结束标志 + AtomicBoolean isDone = new AtomicBoolean(false); + doubaoService.chatContext(messages,contextId, data -> { + assertNotNull(data); + if (data.contains("[DONE]")) { + // 设置结束标志 + isDone.set(true); + } else if (data.contains("\"error\"")) { + isDone.set(true); + } + + }); + // 轮询检查结束标志 + while (!isDone.get()) { + ThreadUtil.sleep(100); + } + } + + @Test + @Disabled + void imagesGenerations() { + final DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue()) + .setApiKey(key).setModel(Models.Doubao.DOUBAO_SEEDREAM_3_0_T2I.getModel()).build(), DoubaoService.class); + final String imagesGenerations = doubaoService.imagesGenerations("一位年轻的宇航员站在未来感十足的太空站内,透过巨大的弧形落地窗凝望浩瀚宇宙。窗外,璀璨的星河与五彩斑斓的星云交织,远处隐约可见未知星球的轮廓,仿佛在召唤着探索的脚步。宇航服上的呼吸灯与透明显示屏上的星图交相辉映,象征着人类科技与宇宙奥秘的碰撞。画面深邃而神秘,充满对未知的渴望与无限可能的想象。"); + assertNotNull(imagesGenerations); + } +} diff --git a/hutool-ai/src/test/java/cn/hutool/ai/model/grok/GrokServiceTest.java b/hutool-ai/src/test/java/cn/hutool/ai/model/grok/GrokServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..52506006b072142f427a4e343e2e4d15354cc8be --- /dev/null +++ b/hutool-ai/src/test/java/cn/hutool/ai/model/grok/GrokServiceTest.java @@ -0,0 +1,205 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.hutool.ai.model.grok; + +import cn.hutool.ai.AIServiceFactory; +import cn.hutool.ai.ModelName; +import cn.hutool.ai.Models; +import cn.hutool.ai.core.AIConfigBuilder; +import cn.hutool.ai.core.Message; +import cn.hutool.core.img.ImgUtil; +import cn.hutool.core.thread.ThreadUtil; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.awt.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class GrokServiceTest { + + String key = "your key"; + GrokService grokService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.GROK.getValue()).setApiKey(key).build(), GrokService.class); + + + @Test + @Disabled + void chat(){ + final String chat = grokService.chat("写一个疯狂星期四广告词"); + assertNotNull(chat); + } + + @Test + @Disabled + void chatStream() { + String prompt = "写一个疯狂星期四广告词"; + // 使用AtomicBoolean作为结束标志 + AtomicBoolean isDone = new AtomicBoolean(false); + + grokService.chat(prompt, data -> { + assertNotNull(data); + if (data.contains("[DONE]")) { + // 设置结束标志 + isDone.set(true); + } else if (data.contains("\"error\"")) { + isDone.set(true); + } + + }); + // 轮询检查结束标志 + while (!isDone.get()) { + ThreadUtil.sleep(100); + } + } + + @Test + @Disabled + void testChat(){ + final List messages = new ArrayList<>(); + messages.add(new Message("system","你是个抽象大师,会说很抽象的话,最擅长说抽象的笑话")); + messages.add(new Message("user","给我说一个笑话")); + final String chat = grokService.chat(messages); + assertNotNull(chat); + } + + @Test + @Disabled + void message() { + final String message = grokService.message("给我一个KFC的广告词", 4096); + assertNotNull(message); + } + + @Test + @Disabled + void messageStream() { + String prompt = "给我一个KFC的广告词"; + + // 使用AtomicBoolean作为结束标志 + AtomicBoolean isDone = new AtomicBoolean(false); + grokService.message(prompt, 4096, data -> { + assertNotNull(data); + if (data.contains("[DONE]")) { + // 设置结束标志 + isDone.set(true); + } else if (data.contains("\"error\"")) { + isDone.set(true); + } + + }); + // 轮询检查结束标志 + while (!isDone.get()) { + ThreadUtil.sleep(100); + } + } + + @Test + @Disabled + void chatVision() { + final GrokService grokService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.GROK.getValue()).setModel(Models.Grok.GROK_2_VISION_1212.getModel()).setApiKey(key).build(), GrokService.class); + final String base64 = ImgUtil.toBase64DataUri(Toolkit.getDefaultToolkit().createImage("your imageUrl"), "png"); + final String chatVision = grokService.chatVision("图片上有些什么?", Arrays.asList(base64)); + assertNotNull(chatVision); + } + + @Test + @Disabled + void testChatVisionStream() { + final GrokService grokService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.GROK.getValue()).setModel(Models.Grok.GROK_2_VISION_1212.getModel()).setApiKey(key).build(), GrokService.class); + String prompt = "图片上有些什么?"; + List images = Arrays.asList("https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544"); + + // 使用AtomicBoolean作为结束标志 + AtomicBoolean isDone = new AtomicBoolean(false); + grokService.chatVision(prompt,images, data -> { + assertNotNull(data); + if (data.contains("[DONE]")) { + // 设置结束标志 + isDone.set(true); + } else if (data.contains("\"error\"")) { + isDone.set(true); + } + + }); + // 轮询检查结束标志 + while (!isDone.get()) { + ThreadUtil.sleep(100); + } + } + + @Test + @Disabled + void testChatVision() { + final GrokService grokService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.GROK.getValue()).setModel(Models.Grok.GROK_2_VISION_1212.getModel()).setApiKey(key).build(), GrokService.class); + final String chatVision = grokService.chatVision("图片上有些什么?", Arrays.asList("https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544")); + assertNotNull(chatVision); + } + + @Test + @Disabled + void models() { + final String models = grokService.models(); + assertNotNull(models); + } + + @Test + @Disabled + void getModel() { + final String model = grokService.getModel(""); + assertNotNull(model); + } + + @Test + @Disabled + void languageModels() { + final String languageModels = grokService.languageModels(); + assertNotNull(languageModels); + } + + @Test + @Disabled + void getLanguageModel() { + final String language = grokService.getLanguageModel(""); + assertNotNull(language); + } + + @Test + @Disabled + void tokenizeText() { + final String tokenizeText = grokService.tokenizeText(key); + assertNotNull(tokenizeText); + } + + @Test + @Disabled + void deferredCompletion() { + final String deferred = grokService.deferredCompletion(key); + assertNotNull(deferred); + } + + @Test + @Disabled + void imagesGenerations() { + final GrokService grokService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.GROK.getValue()) + .setApiKey(key).setModel(Models.Grok.GROK_2_IMAGE.getModel()).build(), GrokService.class); + final String imagesGenerations = grokService.imagesGenerations("一位年轻的宇航员站在未来感十足的太空站内,透过巨大的弧形落地窗凝望浩瀚宇宙。窗外,璀璨的星河与五彩斑斓的星云交织,远处隐约可见未知星球的轮廓,仿佛在召唤着探索的脚步。宇航服上的呼吸灯与透明显示屏上的星图交相辉映,象征着人类科技与宇宙奥秘的碰撞。画面深邃而神秘,充满对未知的渴望与无限可能的想象。"); + assertNotNull(imagesGenerations); + } +} diff --git a/hutool-ai/src/test/java/cn/hutool/ai/model/hutool/HutoolServiceTest.java b/hutool-ai/src/test/java/cn/hutool/ai/model/hutool/HutoolServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..c0ffd463809418a3e282ada59e508bc8b8e3ddae --- /dev/null +++ b/hutool-ai/src/test/java/cn/hutool/ai/model/hutool/HutoolServiceTest.java @@ -0,0 +1,191 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.hutool.ai.model.hutool; + +import cn.hutool.ai.AIException; +import cn.hutool.ai.AIServiceFactory; +import cn.hutool.ai.ModelName; +import cn.hutool.ai.core.AIConfigBuilder; +import cn.hutool.ai.core.Message; +import cn.hutool.core.img.ImgUtil; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.thread.ThreadUtil; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.awt.*; +import java.io.File; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.junit.jupiter.api.Assertions.*; + +class HutoolServiceTest { + + String key = "请前往Hutool-AI官网:https://ai.hutool.cn 获取"; + HutoolService hutoolService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.HUTOOL.getValue()).setApiKey(key).build(), HutoolService.class); + + + @Test + @Disabled + void chat(){ + final String chat = hutoolService.chat("写一个疯狂星期四广告词"); + assertNotNull(chat); + } + + @Test + @Disabled + void chatStream() { + String prompt = "写一个疯狂星期四广告词"; + // 使用AtomicBoolean作为结束标志 + AtomicBoolean isDone = new AtomicBoolean(false); + + hutoolService.chat(prompt, data -> { + assertNotNull(data); + if (data.contains("[DONE]")) { + // 设置结束标志 + isDone.set(true); + } else if (data.contains("\"error\"")) { + isDone.set(true); + } + + }); + // 轮询检查结束标志 + while (!isDone.get()) { + ThreadUtil.sleep(100); + } + } + + @Test + @Disabled + void testChat(){ + final List messages = new ArrayList<>(); + messages.add(new Message("system","你是个抽象大师,会说很抽象的话,最擅长说抽象的笑话")); + messages.add(new Message("user","给我说一个笑话")); + final String chat = hutoolService.chat(messages); + assertNotNull(chat); + } + + + @Test + @Disabled + void chatVision() { + final String base64 = ImgUtil.toBase64DataUri(Toolkit.getDefaultToolkit().createImage("your imageUrl"), "png"); + final String chatVision = hutoolService.chatVision("图片上有些什么?", Arrays.asList(base64)); + assertNotNull(chatVision); + } + + @Test + @Disabled + void testChatVisionStream() { + String prompt = "图片上有些什么?"; + List images = Arrays.asList("https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544"); + + // 使用AtomicBoolean作为结束标志 + AtomicBoolean isDone = new AtomicBoolean(false); + hutoolService.chatVision(prompt,images, data -> { + assertNotNull(data); + if (data.contains("[DONE]")) { + // 设置结束标志 + isDone.set(true); + } else if (data.contains("\"error\"")) { + isDone.set(true); + } + + }); + // 轮询检查结束标志 + while (!isDone.get()) { + ThreadUtil.sleep(100); + } + } + + @Test + @Disabled + void testChatVision() { + final String chatVision = hutoolService.chatVision("图片上有些什么?", Arrays.asList("https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544")); + assertNotNull(chatVision); + } + + @Test + @Disabled + void tokenizeText() { + final String tokenizeText = hutoolService.tokenizeText(key); + assertNotNull(tokenizeText); + } + + @Test + @Disabled + void imagesGenerations() { + final String imagesGenerations = hutoolService.imagesGenerations("一位年轻的宇航员站在未来感十足的太空站内,透过巨大的弧形落地窗凝望浩瀚宇宙。窗外,璀璨的星河与五彩斑斓的星云交织,远处隐约可见未知星球的轮廓,仿佛在召唤着探索的脚步。宇航服上的呼吸灯与透明显示屏上的星图交相辉映,象征着人类科技与宇宙奥秘的碰撞。画面深邃而神秘,充满对未知的渴望与无限可能的想象。"); + assertNotNull(imagesGenerations); + } + + @Test + @Disabled + void embeddingVision() { + final String embeddingVision = hutoolService.embeddingVision("天空好难", "https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544"); + assertNotNull(embeddingVision); + } + + @Test + @Disabled + void textToSpeech() { + try { + // 测试正常音频流返回 + final InputStream inputStream = hutoolService.tts("万里山河一夜白,\n" + + "千峰尽染玉龙哀。\n" + + "长风卷起琼花碎,\n" + + "直上九霄揽月来。", HutoolCommon.HutoolSpeech.NOVA); + assertNotNull(inputStream); + + // 保存音频文件 + final String filePath = "your filePath"; + FileUtil.writeFromStream(inputStream, new File(filePath)); + + } catch (Exception e) { + throw new AIException("TTS测试失败: " + e.getMessage()); + } + + } + + @Test + @Disabled + void speechToText() { + final File file = FileUtil.file("your filePath"); + final String speechToText = hutoolService.stt(file); + assertNotNull(speechToText); + } + + @Test + @Disabled + void videoTasks() { + final String videoTasks = hutoolService.videoTasks("生成一段动画视频,主角是大耳朵图图,一个活泼可爱的小男孩。视频中图图在公园里玩耍," + + "画面采用明亮温暖的卡通风格,色彩鲜艳,动作流畅。背景音乐轻快活泼,带有冒险感,音效包括鸟叫声、欢笑声和山洞回声。", "https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544"); + assertNotNull(videoTasks);//cgt-20250529154621-d7dq9 + } + + @Test + @Disabled + void getVideoTasksInfo() { + final String videoTasksInfo = hutoolService.getVideoTasksInfo("cgt-20250529154621-d7dq9"); + assertNotNull(videoTasksInfo); + } + +} diff --git a/hutool-ai/src/test/java/cn/hutool/ai/model/ollama/OllamaServiceTest.java b/hutool-ai/src/test/java/cn/hutool/ai/model/ollama/OllamaServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..09f5ebab86f1642469d64ff2d4b5af6305603d56 --- /dev/null +++ b/hutool-ai/src/test/java/cn/hutool/ai/model/ollama/OllamaServiceTest.java @@ -0,0 +1,256 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.hutool.ai.model.ollama; + +import cn.hutool.ai.AIServiceFactory; +import cn.hutool.ai.ModelName; +import cn.hutool.ai.core.AIConfigBuilder; +import cn.hutool.ai.core.Message; +import cn.hutool.core.bean.BeanPath; +import cn.hutool.core.thread.ThreadUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSON; +import cn.hutool.json.JSONArray; +import cn.hutool.json.JSONUtil; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/** + * OllamaService + * + * @author yangruoyu-yumeisoft + * @since 5.8.40 + */ +class OllamaServiceTest { + // 创建service + OllamaService ollamaService = AIServiceFactory.getAIService( + new AIConfigBuilder(ModelName.OLLAMA.getValue()) + // 这里填写Ollama服务的地址 + .setApiUrl("http://127.0.0.1:11434") + // 这里填写使用的模型 + .setModel("qwen2.5-coder:32b") + .build(), + OllamaService.class + ); + + // 假设有一个Java工程师的Agent提示词 + String javaEngineerPrompt="# 角色 \n" + + "你是一位精通Spring Boot 3.0的资深Java全栈工程师,具备以下核心能力: \n" + + "- 精通Spring Boot 3.0新特性与最佳实践 \n" + + "- 熟练整合Hutool工具包、Redis数据访问、Feign远程调用、FreeMarker模板引擎 \n" + + "- 能输出符合工程规范的代码结构和配置文件 \n" + + "- 注重代码可读性与注释规范 \n" + + "\n" + + "# 任务 \n" + + "请完成以下编程任务(按优先级排序): \n" + + "1. **核心要求** \n" + + " - 使用Spring Boot 3.0构建项目 \n" + + " - 必须包含以下依赖: \n" + + " - `cn.hutool:hutool-all`(最新版) \n" + + " - `org.springframework.boot:spring-boot-starter-data-redis` \n" + + " - `org.springframework.cloud:spring-cloud-starter-openfeign` \n" + + " - `org.springframework.boot:spring-boot-starter-freemarker` \n" + + "2. **约束条件** \n" + + " - 代码需符合Java 17语法规范 \n" + + " - 每个类必须包含Javadoc风格的类注释 \n" + + " - 关键方法需添加`@Api`/`@ApiOperation`注解(若涉及接口) \n" + + " - Redis操作需使用`RedisTemplate`实现 \n" + + "3. **实现流程** \n" + + " ``` \n" + + " 1. 生成pom.xml依赖配置 \n" + + " 2. 创建基础配置类(如RedisConfig) \n" + + " 3. 编写Feign客户端接口 \n" + + " 4. 实现FreeMarker模板渲染服务 \n" + + " 5. 提供完整Controller示例 \n" + + " ``` \n" + + "\n" + + "# 输出要求 \n" + + "请以严格Markdown格式输出,每个模块独立代码块: \n" + + "```markdown \n" + + "## 1. 项目依赖配置(pom.xml片段) \n" + + "```xml \n" + + "... \n" + + "``` \n" + + "\n" + + "## 2. Redis配置类 \n" + + "```java \n" + + "@Configuration \n" + + "public class RedisConfig { ... } \n" + + "``` \n" + + "\n" + + "## 3. Feign客户端示例 \n" + + "```java \n" + + "@FeignClient(name = \"...\") \n" + + "public interface ... { ... } \n" + + "``` \n" + + "\n" + + "## 4. FreeMarker模板服务 \n" + + "```java \n" + + "@Service \n" + + "public class TemplateService { ... } \n" + + "``` \n" + + "\n" + + "## 5. 控制器示例 \n" + + "```java \n" + + "@RestController \n" + + "@RequestMapping(\"/example\") \n" + + "public class ExampleController { ... } \n" + + "``` \n" + + "``` \n" + + "\n" + + "# 示例片段(供格式参考) \n" + + "```java \n" + + "/** \n" + + " * 示例Feign客户端 \n" + + " * @since 1.0.0 \n" + + " */ \n" + + "@FeignClient(name = \"demo-service\", url = \"${demo.service.url}\") \n" + + "public interface DemoClient { \n" + + "\n" + + " @GetMapping(\"/data/{id}\") \n" + + " @ApiOperation(\"获取示例数据\") \n" + + " ResponseEntity getData(@PathVariable(\"id\") Long id); \n" + + "} \n" + + "``` \n" + + "\n" + + "请按此规范输出完整代码结构,确保自动化程序可直接解析生成项目文件。"; + + /** + * 同步方式调用 + */ + @Test + @Disabled + void testSimple() { + final String answer = ollamaService.chat("写一个疯狂星期四广告词"); + assertNotNull(answer); + } + + /** + * 按流方式输出 + */ + @Test + @Disabled + void testStream() { + AtomicBoolean isDone = new AtomicBoolean(false); + AtomicReference errorMessage = new AtomicReference<>(); + ollamaService.chat("写一个疯狂星期四广告词", data -> { + // 输出到控制台 + JSON streamData = JSONUtil.parse(data); + if (streamData.getByPath("error") != null) { + isDone.set(true); + errorMessage.set(streamData.getByPath("error").toString()); + return; + } + + if ("true".equals(streamData.getByPath("done").toString())) { + isDone.set(true); + } + }); + // 轮询检查结束标志 + while (!isDone.get()) { + ThreadUtil.sleep(100); + } + if (errorMessage.get() != null) { + throw new RuntimeException(errorMessage.get()); + } + } + + /** + * 带历史上下文的同步方式调用 + */ + @Test + @Disabled + void testSimpleWithHistory(){ + List messageList=new ArrayList<>(); + messageList.add(new Message("system",javaEngineerPrompt)); + messageList.add(new Message("user","帮我写一个Java通过Post方式发送JSON给HTTP接口,请求头带有token")); + String result = ollamaService.chat(messageList); + assertNotNull(result); + } + + @Test + @Disabled + void testStreamWithHistory(){ + List messageList=new ArrayList<>(); + messageList.add(new Message("system",javaEngineerPrompt)); + messageList.add(new Message("user","帮我写一个Java通过Post方式发送JSON给HTTP接口,请求头带有token")); + AtomicBoolean isDone = new AtomicBoolean(false); + AtomicReference errorMessage = new AtomicReference<>(); + ollamaService.chat(messageList, data -> { + // 输出到控制台 + JSON streamData = JSONUtil.parse(data); + if (streamData.getByPath("error") != null) { + isDone.set(true); + errorMessage.set(streamData.getByPath("error").toString()); + return; + } + + if ("true".equals(streamData.getByPath("done").toString())) { + isDone.set(true); + } + }); + // 轮询检查结束标志 + while (!isDone.get()) { + ThreadUtil.sleep(100); + } + if (errorMessage.get() != null) { + throw new RuntimeException(errorMessage.get()); + } + } + + /** + * 列出所有已经拉取到服务器上的模型 + */ + @Test + @Disabled + void testListModels(){ + String models = ollamaService.listModels(); + JSONArray modelList = JSONUtil.parse(models).getByPath("models", JSONArray.class); + } + + /** + * 让Ollama拉取模型 + */ + @Test + @Disabled + void testPullModel(){ + String result = ollamaService.pullModel("qwen2.5:0.5b"); + List lines = StrUtil.splitTrim(result, "\n"); + for (String line : lines) { + if(line.contains("error")){ + throw new RuntimeException(JSONUtil.parse(line).getByPath("error").toString()); + } + } + } + + /** + * 让Ollama删除已经存在的模型 + */ + @Test + @Disabled + void testDeleteModel(){ + // 不会返回任何信息 + ollamaService.deleteModel("qwen2.5:0.5b"); + } +} diff --git a/hutool-ai/src/test/java/cn/hutool/ai/model/openai/OpenaiServiceTest.java b/hutool-ai/src/test/java/cn/hutool/ai/model/openai/OpenaiServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..d773517d5d16fa43b5644d98f0f145730eebf62d --- /dev/null +++ b/hutool-ai/src/test/java/cn/hutool/ai/model/openai/OpenaiServiceTest.java @@ -0,0 +1,245 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.hutool.ai.model.openai; + +import cn.hutool.ai.AIServiceFactory; +import cn.hutool.ai.ModelName; +import cn.hutool.ai.Models; +import cn.hutool.ai.core.AIConfigBuilder; +import cn.hutool.ai.core.Message; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.thread.ThreadUtil; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class OpenaiServiceTest { + + String key = "your key"; + OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue()).setApiKey(key).build(), OpenaiService.class); + + + @Test + @Disabled + void chat(){ + final String chat = openaiService.chat("写一个疯狂星期四广告词"); + assertNotNull(chat); + } + + @Test + @Disabled + void chatStream() { + String prompt = "写一个疯狂星期四广告词"; + // 使用AtomicBoolean作为结束标志 + AtomicBoolean isDone = new AtomicBoolean(false); + + openaiService.chat(prompt, data -> { + assertNotNull(data); + if (data.contains("[DONE]")) { + // 设置结束标志 + isDone.set(true); + } else if (data.contains("\"error\"")) { + isDone.set(true); + } + + }); + // 轮询检查结束标志 + while (!isDone.get()) { + ThreadUtil.sleep(100); + } + } + + @Test + @Disabled + void testChat(){ + final List messages = new ArrayList<>(); + messages.add(new Message("system","你是个抽象大师,会说很抽象的话,最擅长说抽象的笑话")); + messages.add(new Message("user","给我说一个笑话")); + final String chat = openaiService.chat(messages); + assertNotNull(chat); + } + + @Test + @Disabled + void chatVision() { + final OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue()) + .setApiKey(key).setModel(Models.Openai.GPT_4O_MINI.getModel()).build(), OpenaiService.class); + final String chatVision = openaiService.chatVision("图片上有些什么?", Arrays.asList("https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544","https://img2.baidu.com/it/u=1682510685,1244554634&fm=253&fmt=auto&app=138&f=JPEG?w=803&h=800")); + assertNotNull(chatVision); + } + + @Test + @Disabled + void testChatVisionStream() { + final OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue()) + .setApiKey(key).setModel(Models.Openai.GPT_4O_MINI.getModel()).build(), OpenaiService.class); + String prompt = "图片上有些什么?"; + List images = Arrays.asList("https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544\",\"https://img2.baidu.com/it/u=1682510685,1244554634&fm=253&fmt=auto&app=138&f=JPEG?w=803&h=800"); + + // 使用AtomicBoolean作为结束标志 + AtomicBoolean isDone = new AtomicBoolean(false); + openaiService.chatVision(prompt,images, data -> { + assertNotNull(data); + if (data.contains("[DONE]")) { + // 设置结束标志 + isDone.set(true); + } else if (data.contains("\"error\"")) { + isDone.set(true); + } + + }); + // 轮询检查结束标志 + while (!isDone.get()) { + ThreadUtil.sleep(100); + } + } + + @Test + @Disabled + void imagesGenerations() { + final OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue()) + .setApiKey(key).setModel(Models.Openai.DALL_E_3.getModel()).build(), OpenaiService.class); + final String imagesGenerations = openaiService.imagesGenerations("一位年轻的宇航员站在未来感十足的太空站内,透过巨大的弧形落地窗凝望浩瀚宇宙。窗外,璀璨的星河与五彩斑斓的星云交织,远处隐约可见未知星球的轮廓,仿佛在召唤着探索的脚步。宇航服上的呼吸灯与透明显示屏上的星图交相辉映,象征着人类科技与宇宙奥秘的碰撞。画面深邃而神秘,充满对未知的渴望与无限可能的想象。"); + assertNotNull(imagesGenerations); + //https://oaidalleapiprodscus.blob.core.windows.net/private/org-l99H6T0zCZejctB2TqdYrXFB/user-LilDVU1V8cUxJYwVAGRkUwYd/img-yA9kNatHnBiUHU5lZGim1hP2.png?st=2025-03-07T01%3A04%3A18Z&se=2025-03-07T03%3A04%3A18Z&sp=r&sv=2024-08-04&sr=b&rscd=inline&rsct=image/png&skoid=d505667d-d6c1-4a0a-bac7-5c84a87759f8&sktid=a48cca56-e6da-484e-a814-9c849652bcb3&skt=2025-03-06T15%3A04%3A42Z&ske=2025-03-07T15%3A04%3A42Z&sks=b&skv=2024-08-04&sig=rjcRzC5U7Y3pEDZ4ME0CiviAPdIpoGO2rRTXw3m8rHw%3D + } + + @Test + @Disabled + void imagesEdits() { + final OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue()) + .setApiKey(key).setModel(Models.Openai.DALL_E_2.getModel()).build(), OpenaiService.class); + final File file = FileUtil.file("your imgUrl"); + final String imagesEdits = openaiService.imagesEdits("茂密的森林中,有一只九色鹿若隐若现",file); + assertNotNull(imagesEdits); + } + + @Test + @Disabled + void imagesVariations() { + final OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue()) + .setApiKey(key).setModel(Models.Openai.DALL_E_2.getModel()).build(), OpenaiService.class); + final File file = FileUtil.file("your imgUrl"); + final String imagesVariations = openaiService.imagesVariations(file); + assertNotNull(imagesVariations); + } + + @Test + @Disabled + void textToSpeech() { + final OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue()) + .setApiKey(key).setModel(Models.Openai.TTS_1_HD.getModel()).build(), OpenaiService.class); + final InputStream inputStream = openaiService.textToSpeech("万里山河一夜白,\n" + + "千峰尽染玉龙哀。\n" + + "长风卷起琼花碎,\n" + + "直上九霄揽月来。", OpenaiCommon.OpenaiSpeech.NOVA); + + final String filePath = "your filePath"; + final Path path = Paths.get(filePath); + try (final FileOutputStream outputStream = new FileOutputStream(filePath)) { + Files.createDirectories(path.getParent()); + final byte[] buffer = new byte[1024]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, bytesRead); + } + } catch (final IOException e) { + throw new RuntimeException(e); + } + + } + + @Test + @Disabled + void speechToText() { + final OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue()) + .setApiKey(key).setModel(Models.Openai.WHISPER_1.getModel()).build(), OpenaiService.class); + final File file = FileUtil.file("your filePath"); + final String speechToText = openaiService.speechToText(file); + assertNotNull(speechToText); + } + + @Test + @Disabled + void embeddingText() { + final OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue()) + .setApiKey(key).setModel(Models.Openai.TEXT_EMBEDDING_3_SMALL.getModel()).build(), OpenaiService.class); + final String embeddingText = openaiService.embeddingText("萬里山河一夜白,千峰盡染玉龍哀,長風捲起瓊花碎,直上九霄闌月來"); + assertNotNull(embeddingText); + } + + @Test + @Disabled + void moderations() { + final OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue()) + .setApiKey(key).setModel(Models.Openai.OMNI_MODERATION_LATEST.getModel()).build(), OpenaiService.class); + final String moderations = openaiService.moderations("你要杀人", "https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544"); + assertNotNull(moderations); + } + + @Test + @Disabled + void chatReasoning() { + final OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue()) + .setApiKey(key).setModel(Models.Openai.O3_MINI.getModel()).build(), OpenaiService.class); + final List messages = new ArrayList<>(); + messages.add(new Message("system","你是现代抽象家")); + messages.add(new Message("user","给我一个KFC疯狂星期四的文案")); + final String chatReasoning = openaiService.chatReasoning(messages, OpenaiCommon.OpenaiReasoning.HIGH.getEffort()); + assertNotNull(chatReasoning); + } + + @Test + @Disabled + void chatReasoningStream() { + final OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue()) + .setApiKey(key).setModel(Models.Openai.O3_MINI.getModel()).build(), OpenaiService.class); + final List messages = new ArrayList<>(); + messages.add(new Message("system","你是现代抽象家")); + messages.add(new Message("user","给我一个KFC疯狂星期四的文案")); + + // 使用AtomicBoolean作为结束标志 + AtomicBoolean isDone = new AtomicBoolean(false); + openaiService.chatReasoning(messages,OpenaiCommon.OpenaiReasoning.HIGH.getEffort(), data -> { + assertNotNull(data); + if (data.contains("[DONE]")) { + // 设置结束标志 + isDone.set(true); + } else if (data.contains("\"error\"")) { + isDone.set(true); + } + + }); + // 轮询检查结束标志 + while (!isDone.get()) { + ThreadUtil.sleep(100); + } + } +} diff --git a/hutool-all/pom.xml b/hutool-all/pom.xml index c18a06212ee90f286bfbde548044025faeb66078..b596a84e20e5eefe99ea75981e3b82cac7661866 100755 --- a/hutool-all/pom.xml +++ b/hutool-all/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 5.8.0.M5 + 5.8.41 hutool-all @@ -113,6 +113,11 @@ hutool-jwt ${project.parent.version} + + cn.hutool + hutool-ai + ${project.parent.version} + diff --git a/hutool-aop/pom.xml b/hutool-aop/pom.xml index b365e396c402257558a7a880d538c793eb4b4873..d8540e4e88f8af6ad4d91b8135149e9cea6baf9d 100755 --- a/hutool-aop/pom.xml +++ b/hutool-aop/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 5.8.0.M5 + 5.8.41 hutool-aop @@ -17,9 +17,10 @@ Hutool 动态代理(AOP) + cn.hutool.aop 3.3.0 - 5.3.19 + 5.3.27 @@ -43,4 +44,5 @@ true + diff --git a/hutool-aop/src/main/java/cn/hutool/aop/ProxyUtil.java b/hutool-aop/src/main/java/cn/hutool/aop/ProxyUtil.java old mode 100644 new mode 100755 diff --git a/hutool-aop/src/main/java/cn/hutool/aop/aspects/Aspect.java b/hutool-aop/src/main/java/cn/hutool/aop/aspects/Aspect.java old mode 100644 new mode 100755 diff --git a/hutool-aop/src/main/java/cn/hutool/aop/aspects/SimpleAspect.java b/hutool-aop/src/main/java/cn/hutool/aop/aspects/SimpleAspect.java old mode 100644 new mode 100755 diff --git a/hutool-aop/src/main/java/cn/hutool/aop/aspects/TimeIntervalAspect.java b/hutool-aop/src/main/java/cn/hutool/aop/aspects/TimeIntervalAspect.java old mode 100644 new mode 100755 diff --git a/hutool-aop/src/main/java/cn/hutool/aop/aspects/package-info.java b/hutool-aop/src/main/java/cn/hutool/aop/aspects/package-info.java old mode 100644 new mode 100755 diff --git a/hutool-aop/src/main/java/cn/hutool/aop/interceptor/CglibInterceptor.java b/hutool-aop/src/main/java/cn/hutool/aop/interceptor/CglibInterceptor.java old mode 100644 new mode 100755 index f0f7f9e900ce2a67b9a4393917ab77572370d347..39381f7253a389c53b4cf40840427ff775f6c08a --- a/hutool-aop/src/main/java/cn/hutool/aop/interceptor/CglibInterceptor.java +++ b/hutool-aop/src/main/java/cn/hutool/aop/interceptor/CglibInterceptor.java @@ -43,10 +43,14 @@ public class CglibInterceptor implements MethodInterceptor, Serializable { try { // result = proxy.invokeSuper(obj, args); result = proxy.invoke(target, args); - } catch (InvocationTargetException e) { + } catch (final Throwable e) { + Throwable throwable = e; + if(throwable instanceof InvocationTargetException){ + throwable = ((InvocationTargetException) throwable).getTargetException(); + } // 异常回调(只捕获业务代码导致的异常,而非反射导致的异常) - if (aspect.afterException(target, method, args, e.getTargetException())) { - throw e; + if (aspect.afterException(target, method, args, throwable)) { + throw throwable; } } } diff --git a/hutool-aop/src/main/java/cn/hutool/aop/interceptor/JdkInterceptor.java b/hutool-aop/src/main/java/cn/hutool/aop/interceptor/JdkInterceptor.java old mode 100644 new mode 100755 diff --git a/hutool-aop/src/main/java/cn/hutool/aop/interceptor/SpringCglibInterceptor.java b/hutool-aop/src/main/java/cn/hutool/aop/interceptor/SpringCglibInterceptor.java old mode 100644 new mode 100755 index 8db4834690ecd76e086516f993e96d9f4270a9c0..7a3697e771f1e0210891b41f02c4b4d8fff6eedf --- a/hutool-aop/src/main/java/cn/hutool/aop/interceptor/SpringCglibInterceptor.java +++ b/hutool-aop/src/main/java/cn/hutool/aop/interceptor/SpringCglibInterceptor.java @@ -48,9 +48,14 @@ public class SpringCglibInterceptor implements MethodInterceptor, Serializable { try { // result = proxy.invokeSuper(obj, args); result = proxy.invoke(target, args); - } catch (InvocationTargetException e) { + } catch (Throwable e) { + Throwable throwable = e; + if(throwable instanceof InvocationTargetException){ + throwable = ((InvocationTargetException) throwable).getTargetException(); + } + // 异常回调(只捕获业务代码导致的异常,而非反射导致的异常) - if (aspect.afterException(target, method, args, e.getTargetException())) { + if (aspect.afterException(target, method, args, throwable)) { throw e; } } diff --git a/hutool-aop/src/main/java/cn/hutool/aop/interceptor/package-info.java b/hutool-aop/src/main/java/cn/hutool/aop/interceptor/package-info.java old mode 100644 new mode 100755 diff --git a/hutool-aop/src/main/java/cn/hutool/aop/package-info.java b/hutool-aop/src/main/java/cn/hutool/aop/package-info.java old mode 100644 new mode 100755 diff --git a/hutool-aop/src/main/java/cn/hutool/aop/proxy/CglibProxyFactory.java b/hutool-aop/src/main/java/cn/hutool/aop/proxy/CglibProxyFactory.java old mode 100644 new mode 100755 index d7510035c9fd1a7218baa9c3ea951f44b9b3fd7e..ee7a5b119d7e9784a831c867ccb872826631304c --- a/hutool-aop/src/main/java/cn/hutool/aop/proxy/CglibProxyFactory.java +++ b/hutool-aop/src/main/java/cn/hutool/aop/proxy/CglibProxyFactory.java @@ -2,8 +2,12 @@ package cn.hutool.aop.proxy; import cn.hutool.aop.aspects.Aspect; import cn.hutool.aop.interceptor.CglibInterceptor; +import cn.hutool.core.util.ClassUtil; +import cn.hutool.core.util.ReflectUtil; import net.sf.cglib.proxy.Enhancer; +import java.lang.reflect.Constructor; + /** * 基于Cglib的切面代理工厂 * @@ -14,12 +18,46 @@ public class CglibProxyFactory extends ProxyFactory{ private static final long serialVersionUID = 1L; @Override - @SuppressWarnings("unchecked") public T proxy(T target, Aspect aspect) { + final Class targetClass = target.getClass(); + final Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(target.getClass()); enhancer.setCallback(new CglibInterceptor(target, aspect)); - return (T) enhancer.create(); + return create(enhancer, targetClass); } + /** + * 创建代理对象
+ * https://gitee.com/chinabugotech/hutool/issues/I74EX7
+ * 某些对象存在非空参数构造,则需遍历查找需要的构造完成代理对象构建。 + * + * @param 代理对象类型 + * @param enhancer {@link org.springframework.cglib.proxy.Enhancer} + * @param targetClass 目标类型 + * @return 代理对象 + */ + @SuppressWarnings("unchecked") + private static T create(final Enhancer enhancer, final Class targetClass) { + final Constructor[] constructors = ReflectUtil.getConstructors(targetClass); + Class[] parameterTypes; + Object[] values; + IllegalArgumentException finalException = null; + for (final Constructor constructor : constructors) { + parameterTypes = constructor.getParameterTypes(); + values = ClassUtil.getDefaultValues(parameterTypes); + + try { + return (T) enhancer.create(parameterTypes, values); + } catch (final IllegalArgumentException e) { + //ignore + finalException = e; + } + } + if (null != finalException) { + throw finalException; + } + + throw new IllegalArgumentException("No constructor provided"); + } } diff --git a/hutool-aop/src/main/java/cn/hutool/aop/proxy/JdkProxyFactory.java b/hutool-aop/src/main/java/cn/hutool/aop/proxy/JdkProxyFactory.java old mode 100644 new mode 100755 index 91a5a712984525d8b6d2f1a27eb755c7a37adf89..166b45e2ae359d3152cab4bb08f0c9e61313ca7a --- a/hutool-aop/src/main/java/cn/hutool/aop/proxy/JdkProxyFactory.java +++ b/hutool-aop/src/main/java/cn/hutool/aop/proxy/JdkProxyFactory.java @@ -12,6 +12,11 @@ import cn.hutool.aop.interceptor.JdkInterceptor; public class JdkProxyFactory extends ProxyFactory { private static final long serialVersionUID = 1L; + /** + * 获取单例 + */ + public static JdkProxyFactory INSTANCE = new JdkProxyFactory(); + @Override public T proxy(T target, Aspect aspect) { return ProxyUtil.newProxyInstance(// diff --git a/hutool-aop/src/main/java/cn/hutool/aop/proxy/ProxyFactory.java b/hutool-aop/src/main/java/cn/hutool/aop/proxy/ProxyFactory.java old mode 100644 new mode 100755 index 96d988971cbd3dab160b08ca0830ff8700729f11..db45b9d45d630e5c71b7302be910a2f9c8124745 --- a/hutool-aop/src/main/java/cn/hutool/aop/proxy/ProxyFactory.java +++ b/hutool-aop/src/main/java/cn/hutool/aop/proxy/ProxyFactory.java @@ -59,7 +59,13 @@ public abstract class ProxyFactory implements Serializable { * @return 代理对象 */ public static T createProxy(T target, Aspect aspect) { - return create().proxy(target, aspect); + ProxyFactory factory = create(); + if(null == factory){ + // issue#IBF20Z + // 可能的空指针问题 + factory = JdkProxyFactory.INSTANCE; + } + return factory.proxy(target, aspect); } /** diff --git a/hutool-aop/src/main/java/cn/hutool/aop/proxy/SpringCglibProxyFactory.java b/hutool-aop/src/main/java/cn/hutool/aop/proxy/SpringCglibProxyFactory.java old mode 100644 new mode 100755 index caa43e59615029b9fdceb0715bdea59ec54e271b..4e709ad82e0712ef39880c615d28af6430256841 --- a/hutool-aop/src/main/java/cn/hutool/aop/proxy/SpringCglibProxyFactory.java +++ b/hutool-aop/src/main/java/cn/hutool/aop/proxy/SpringCglibProxyFactory.java @@ -2,8 +2,12 @@ package cn.hutool.aop.proxy; import cn.hutool.aop.aspects.Aspect; import cn.hutool.aop.interceptor.SpringCglibInterceptor; +import cn.hutool.core.util.ClassUtil; +import cn.hutool.core.util.ReflectUtil; import org.springframework.cglib.proxy.Enhancer; +import java.lang.reflect.Constructor; + /** * 基于Spring-cglib的切面代理工厂 * @@ -14,12 +18,46 @@ public class SpringCglibProxyFactory extends ProxyFactory{ private static final long serialVersionUID = 1L; @Override - @SuppressWarnings("unchecked") public T proxy(T target, Aspect aspect) { + final Class targetClass = target.getClass(); + final Enhancer enhancer = new Enhancer(); - enhancer.setSuperclass(target.getClass()); + enhancer.setSuperclass(targetClass); enhancer.setCallback(new SpringCglibInterceptor(target, aspect)); - return (T) enhancer.create(); + return create(enhancer, targetClass); } + /** + * 创建代理对象
+ * https://gitee.com/chinabugotech/hutool/issues/I74EX7
+ * 某些对象存在非空参数构造,则需遍历查找需要的构造完成代理对象构建。 + * + * @param 代理对象类型 + * @param enhancer {@link Enhancer} + * @param targetClass 目标类型 + * @return 代理对象 + */ + @SuppressWarnings("unchecked") + private static T create(final Enhancer enhancer, final Class targetClass) { + final Constructor[] constructors = ReflectUtil.getConstructors(targetClass); + Class[] parameterTypes; + Object[] values; + IllegalArgumentException finalException = null; + for (final Constructor constructor : constructors) { + parameterTypes = constructor.getParameterTypes(); + values = ClassUtil.getDefaultValues(parameterTypes); + + try { + return (T) enhancer.create(parameterTypes, values); + } catch (final IllegalArgumentException e) { + //ignore + finalException = e; + } + } + if (null != finalException) { + throw finalException; + } + + throw new IllegalArgumentException("No constructor provided"); + } } diff --git a/hutool-aop/src/main/java/cn/hutool/aop/proxy/package-info.java b/hutool-aop/src/main/java/cn/hutool/aop/proxy/package-info.java old mode 100644 new mode 100755 diff --git a/hutool-aop/src/main/resources/META-INF/services/cn.hutool.aop.proxy.ProxyFactory b/hutool-aop/src/main/resources/META-INF/services/cn.hutool.aop.proxy.ProxyFactory old mode 100644 new mode 100755 diff --git a/hutool-aop/src/test/java/cn/hutool/aop/test/AopTest.java b/hutool-aop/src/test/java/cn/hutool/aop/test/AopTest.java old mode 100644 new mode 100755 index 079858775374493311e34c75925a4ed7fbfe8638..a22ebd42413398558530bc964e3f715c362549ad --- a/hutool-aop/src/test/java/cn/hutool/aop/test/AopTest.java +++ b/hutool-aop/src/test/java/cn/hutool/aop/test/AopTest.java @@ -4,8 +4,8 @@ import cn.hutool.aop.ProxyUtil; import cn.hutool.aop.aspects.TimeIntervalAspect; import cn.hutool.core.lang.Console; import lombok.Data; -import org.junit.Assert; -import org.junit.Test; +import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.Test; /** * AOP模块单元测试 @@ -18,7 +18,7 @@ public class AopTest { public void aopTest() { Animal cat = ProxyUtil.proxy(new Cat(), TimeIntervalAspect.class); String result = cat.eat(); - Assert.assertEquals("猫吃鱼", result); + assertEquals("猫吃鱼", result); cat.seize(); } @@ -26,7 +26,7 @@ public class AopTest { public void aopByAutoCglibTest() { Dog dog = ProxyUtil.proxy(new Dog(), TimeIntervalAspect.class); String result = dog.eat(); - Assert.assertEquals("狗吃肉", result); + assertEquals("狗吃肉", result); dog.seize(); } @@ -78,7 +78,7 @@ public class AopTest { TagObj proxy = ProxyUtil.proxy(target, TimeIntervalAspect.class); //代理类获取标记tag (断言错误) - Assert.assertEquals("tag", proxy.getTag()); + assertEquals("tag", proxy.getTag()); } @Data diff --git a/hutool-aop/src/test/java/cn/hutool/aop/test/IssueI74EX7Test.java b/hutool-aop/src/test/java/cn/hutool/aop/test/IssueI74EX7Test.java new file mode 100644 index 0000000000000000000000000000000000000000..825cb520a8867fe4094412378de72a7ac40d4808 --- /dev/null +++ b/hutool-aop/src/test/java/cn/hutool/aop/test/IssueI74EX7Test.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2023 looly(loolly@aliyun.com) + * Hutool is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +package cn.hutool.aop.test; + +import cn.hutool.aop.aspects.SimpleAspect; +import cn.hutool.aop.proxy.CglibProxyFactory; +import cn.hutool.aop.proxy.JdkProxyFactory; +import cn.hutool.aop.proxy.ProxyFactory; +import cn.hutool.aop.proxy.SpringCglibProxyFactory; +import cn.hutool.core.lang.Console; +import lombok.Setter; +import org.junit.jupiter.api.Test; + +public class IssueI74EX7Test { + @Test + public void proxyTest() { + final SmsBlend smsBlend = new SmsBlendImpl(1); + final ProxyFactory engine = new JdkProxyFactory(); + engine.proxy(smsBlend, new SimpleAspect()); + } + + /** + * https://gitee.com/chinabugotech/hutool/issues/I74EX7
+ * Enhancer.create()默认调用无参构造,有参构造或者多个构造没有很好的兼容。 + * + */ + @Test + public void cglibProxyTest() { + final SmsBlend smsBlend = new SmsBlendImpl(1); + final ProxyFactory engine = new CglibProxyFactory(); + engine.proxy(smsBlend, new SimpleAspect()); + } + + /** + * https://gitee.com/chinabugotech/hutool/issues/I74EX7
+ * Enhancer.create()默认调用无参构造,有参构造或者多个构造没有很好的兼容。 + * + */ + @Test + public void springCglibProxyTest() { + final SmsBlend smsBlend = new SmsBlendImpl(1); + final ProxyFactory engine = new SpringCglibProxyFactory(); + engine.proxy(smsBlend, new SimpleAspect()); + } + + @Test + public void springCglibProxyWithoutConstructorTest() { + final SmsBlend smsBlend = new SmsBlendImplWithoutConstructor(); + final ProxyFactory engine = new SpringCglibProxyFactory(); + engine.proxy(smsBlend, new SimpleAspect()); + } + + public interface SmsBlend{ + void send(); + } + + public static class SmsBlendImpl implements SmsBlend{ + + private final int status; + + public SmsBlendImpl(final int status) { + this.status = status; + } + + @Override + public void send() { + Console.log("sms send." + status); + } + } + + @Setter + public static class SmsBlendImplWithoutConstructor implements SmsBlend{ + + private int status; + + @Override + public void send() { + Console.log("sms send." + status); + } + } +} diff --git a/hutool-aop/src/test/java/cn/hutool/aop/test/IssueIBF20ZTest.java b/hutool-aop/src/test/java/cn/hutool/aop/test/IssueIBF20ZTest.java new file mode 100644 index 0000000000000000000000000000000000000000..eb56a1246c0f17cfd9e031420b48fd51c708aee3 --- /dev/null +++ b/hutool-aop/src/test/java/cn/hutool/aop/test/IssueIBF20ZTest.java @@ -0,0 +1,48 @@ +package cn.hutool.aop.test; + +import cn.hutool.aop.proxy.ProxyFactory; +import cn.hutool.core.thread.ThreadUtil; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class IssueIBF20ZTest { + + @Test + public void testLoadFirstAvailableConcurrent() throws InterruptedException { + // 创建一个固定大小的线程池 + int threadCount = 1000; + ExecutorService executorService = ThreadUtil.newExecutor(threadCount); + + // 创建一个 CountDownLatch,用于等待所有任务完成 + CountDownLatch latch = new CountDownLatch(threadCount); + + // 计数器用于统计成功加载服务提供者的次数 + AtomicInteger successCount = new AtomicInteger(0); + + // 提交多个任务到线程池 + for (int i = 0; i < threadCount; i++) { + executorService.submit(() -> { + ProxyFactory factory = ProxyFactory.create(); + if (factory != null) { + //Console.log(factory.getClass()); + successCount.incrementAndGet(); + } + latch.countDown(); // 每个任务完成时,计数减一 + }); + } + + // 等待所有任务完成 + latch.await(); + + // 关闭线程池并等待所有任务完成 + executorService.shutdown(); + + // 验证所有线程都成功加载了服务提供者 + assertEquals(threadCount, successCount.get()); + } +} diff --git a/hutool-bloomFilter/pom.xml b/hutool-bloomFilter/pom.xml index d38e38057c341a050788d12075f35803e7f0950d..4fc60fdbd217606de67b36ce3335568b65c5262b 100755 --- a/hutool-bloomFilter/pom.xml +++ b/hutool-bloomFilter/pom.xml @@ -9,13 +9,17 @@ cn.hutool hutool-parent - 5.8.0.M5 + 5.8.41 hutool-bloomFilter ${project.artifactId} Hutool 布隆过滤器 + + cn.hutool.bloomfilter + + cn.hutool @@ -23,4 +27,5 @@ ${project.parent.version} + diff --git a/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/BitMapBloomFilter.java b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/BitMapBloomFilter.java old mode 100644 new mode 100755 index b1da492d6d3f2c8359c01294b762686ba55c3c66..e7257e0f91dfbeac474717fcfa13b41b75ff1e8a --- a/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/BitMapBloomFilter.java +++ b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/BitMapBloomFilter.java @@ -8,7 +8,7 @@ import cn.hutool.bloomfilter.filter.SDBMFilter; import cn.hutool.core.util.NumberUtil; /** - * BlommFilter 实现
+ * BloomFilter 实现
* 1.构建hash算法
* 2.散列hash映射到数组的bit位置
* 3.验证
@@ -79,4 +79,4 @@ public class BitMapBloomFilter implements BloomFilter { } return true; } -} \ No newline at end of file +} diff --git a/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/BitSetBloomFilter.java b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/BitSetBloomFilter.java old mode 100644 new mode 100755 index 318d25dc5c43b736ce546af16afab56462b2459b..19df2ceee73bd957578c9e7c4281ebe1cccf94f8 --- a/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/BitSetBloomFilter.java +++ b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/BitSetBloomFilter.java @@ -25,11 +25,11 @@ public class BitSetBloomFilter implements BloomFilter { private final int hashFunctionNumber; /** - * 构造一个布隆过滤器,过滤器的容量为c * n 个bit. + * 构造一个布隆过滤器,过滤器的容量为c * k 个bit. * * @param c 当前过滤器预先开辟的最大包含记录,通常要比预计存入的记录多一倍. * @param n 当前过滤器预计所要包含的记录. - * @param k 哈希函数的个数,等同每条记录要占用的bit数. + * @param k 哈希函数的个数,等同每条记录要占用的bit数,此处值取值为1~8 */ public BitSetBloomFilter(int c, int n, int k) { this.hashFunctionNumber = k; diff --git a/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/BloomFilter.java b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/BloomFilter.java old mode 100644 new mode 100755 diff --git a/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/BloomFilterUtil.java b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/BloomFilterUtil.java old mode 100644 new mode 100755 index 6ba79067eab6aad3f8901ea67b2d7a90ed0433e8..137e3b6b2e63dfa12706c8587d0f49357f412eb1 --- a/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/BloomFilterUtil.java +++ b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/BloomFilterUtil.java @@ -9,7 +9,7 @@ package cn.hutool.bloomfilter; public class BloomFilterUtil { /** - * 创建一个BitSet实现的布隆过滤器,过滤器的容量为c * n 个bit. + * 创建一个BitSet实现的布隆过滤器,过滤器的容量为c * k 个bit. * * @param c 当前过滤器预先开辟的最大包含记录,通常要比预计存入的记录多一倍. * @param n 当前过滤器预计所要包含的记录. diff --git a/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/bitMap/BitMap.java b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/bitMap/BitMap.java old mode 100644 new mode 100755 diff --git a/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/bitMap/IntMap.java b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/bitMap/IntMap.java old mode 100644 new mode 100755 diff --git a/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/bitMap/LongMap.java b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/bitMap/LongMap.java old mode 100644 new mode 100755 diff --git a/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/bitMap/package-info.java b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/bitMap/package-info.java old mode 100644 new mode 100755 diff --git a/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/AbstractFilter.java b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/AbstractFilter.java old mode 100644 new mode 100755 index bc37e729d575ba52abd70c2a3df53472ec682ab3..327fad055e7fc8d86467455e43ce9feb26e20506 --- a/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/AbstractFilter.java +++ b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/AbstractFilter.java @@ -4,6 +4,7 @@ import cn.hutool.bloomfilter.BloomFilter; import cn.hutool.bloomfilter.bitMap.BitMap; import cn.hutool.bloomfilter.bitMap.IntMap; import cn.hutool.bloomfilter.bitMap.LongMap; +import cn.hutool.core.lang.Assert; /** * 抽象Bloom过滤器 @@ -46,7 +47,7 @@ public abstract class AbstractFilter implements BloomFilter { * @param machineNum 机器位数 */ public void init(long maxValue, int machineNum) { - this.size = maxValue; + this.size = Assert.checkBetween(maxValue, 1, Integer.MAX_VALUE); switch (machineNum) { case BitMap.MACHINE32: bm = new IntMap((int) (size / machineNum)); diff --git a/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/DefaultFilter.java b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/DefaultFilter.java old mode 100644 new mode 100755 diff --git a/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/ELFFilter.java b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/ELFFilter.java old mode 100644 new mode 100755 diff --git a/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/FNVFilter.java b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/FNVFilter.java old mode 100644 new mode 100755 diff --git a/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/FuncFilter.java b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/FuncFilter.java old mode 100644 new mode 100755 diff --git a/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/HfFilter.java b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/HfFilter.java old mode 100644 new mode 100755 diff --git a/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/HfIpFilter.java b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/HfIpFilter.java old mode 100644 new mode 100755 diff --git a/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/JSFilter.java b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/JSFilter.java old mode 100644 new mode 100755 diff --git a/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/PJWFilter.java b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/PJWFilter.java old mode 100644 new mode 100755 diff --git a/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/RSFilter.java b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/RSFilter.java old mode 100644 new mode 100755 diff --git a/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/SDBMFilter.java b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/SDBMFilter.java old mode 100644 new mode 100755 diff --git a/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/TianlFilter.java b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/TianlFilter.java old mode 100644 new mode 100755 diff --git a/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/package-info.java b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/package-info.java old mode 100644 new mode 100755 diff --git a/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/package-info.java b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/package-info.java old mode 100644 new mode 100755 diff --git a/hutool-bloomFilter/src/test/java/cn/hutool/bloomfilter/BitMapBloomFilterTest.java b/hutool-bloomFilter/src/test/java/cn/hutool/bloomfilter/BitMapBloomFilterTest.java old mode 100644 new mode 100755 index ad104d63223a9d897576a6a42df204c4d6819328..93bd54e9ee28664ed5d48ad73a1200062f290cf6 --- a/hutool-bloomFilter/src/test/java/cn/hutool/bloomfilter/BitMapBloomFilterTest.java +++ b/hutool-bloomFilter/src/test/java/cn/hutool/bloomfilter/BitMapBloomFilterTest.java @@ -1,8 +1,8 @@ package cn.hutool.bloomfilter; -import org.junit.Assert; -import org.junit.Ignore; -import org.junit.Test; +import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; import cn.hutool.bloomfilter.bitMap.IntMap; import cn.hutool.bloomfilter.bitMap.LongMap; @@ -16,13 +16,13 @@ public class BitMapBloomFilterTest { filter.add("abc"); filter.add("ddd"); - Assert.assertTrue(filter.contains("abc")); - Assert.assertTrue(filter.contains("ddd")); - Assert.assertTrue(filter.contains("123")); + assertTrue(filter.contains("abc")); + assertTrue(filter.contains("ddd")); + assertTrue(filter.contains("123")); } @Test - @Ignore + @Disabled public void testIntMap(){ IntMap intMap = new IntMap(); @@ -38,7 +38,7 @@ public class BitMapBloomFilterTest { } @Test - @Ignore + @Disabled public void testLongMap(){ LongMap longMap = new LongMap(); diff --git a/hutool-bom/pom.xml b/hutool-bom/pom.xml index f332b3d891b000f7e09622c419db2e720435d7fb..f5bca65b3cc081d023ff5c3bb0661ec332f9a10c 100755 --- a/hutool-bom/pom.xml +++ b/hutool-bom/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 5.8.0.M5 + 5.8.41 hutool-bom diff --git a/hutool-cache/pom.xml b/hutool-cache/pom.xml index 15f9986f7c0251a33cb77036a9a231f55e05fb1a..5d0609121aaf0b7e53c153d574e2595f7daab1f2 100755 --- a/hutool-cache/pom.xml +++ b/hutool-cache/pom.xml @@ -9,13 +9,17 @@ cn.hutool hutool-parent - 5.8.0.M5 + 5.8.41 hutool-cache ${project.artifactId} Hutool 缓存 + + cn.hutool.cache + + cn.hutool diff --git a/hutool-cache/src/main/java/cn/hutool/cache/Cache.java b/hutool-cache/src/main/java/cn/hutool/cache/Cache.java old mode 100644 new mode 100755 index ec021402ecfe174ba793d2031b27ac52bf3413c3..19f93046cda72726fc04ef324cb7c59a0e11814f --- a/hutool-cache/src/main/java/cn/hutool/cache/Cache.java +++ b/hutool-cache/src/main/java/cn/hutool/cache/Cache.java @@ -66,7 +66,7 @@ public interface Cache extends Iterable, Serializable { /** * 从缓存中获得对象,当对象不在缓存中或已经过期返回Func0回调产生的对象 *

- * 调用此方法时,会检查上次调用时间,如果与当前时间差值大于超时时间返回{@code null},否则返回值。 + * 调用此方法时,会检查上次调用时间,如果与当前时间差值大于超时时间返回返回supplier回调产生的对象,否则返回值。 *

* 每次调用此方法会刷新最后访问时间,也就是说会重新计算超时时间。 * @@ -81,7 +81,7 @@ public interface Cache extends Iterable, Serializable { /** * 从缓存中获得对象,当对象不在缓存中或已经过期返回Func0回调产生的对象 *

- * 调用此方法时,会检查上次调用时间,如果与当前时间差值大于超时时间返回{@code null},否则返回值。 + * 调用此方法时,会检查上次调用时间,如果与当前时间差值大于超时时间返回supplier回调产生的对象,否则返回值。 *

* 每次调用此方法会可选是否刷新最后访问时间,{@code true}表示会重新计算超时时间。 * @@ -92,6 +92,21 @@ public interface Cache extends Iterable, Serializable { */ V get(K key, boolean isUpdateLastAccess, Func0 supplier); + /** + * 从缓存中获得对象,当对象不在缓存中或已经过期返回Func0回调产生的对象 + *

+ * 调用此方法时,会检查上次调用时间,如果与当前时间差值大于超时时间返回supplier回调产生的对象,否则返回值。 + *

+ * 每次调用此方法会可选是否刷新最后访问时间,{@code true}表示会重新计算超时时间。 + * + * @param key 键 + * @param isUpdateLastAccess 是否更新最后访问时间,即重新计算超时时间。 + * @param timeout 自定义超时时间 + * @param supplier 如果不存在回调方法,用于生产值对象 + * @return 值对象 + */ + V get(K key, boolean isUpdateLastAccess, long timeout, Func0 supplier); + /** * 从缓存中获得对象,当对象不在缓存中或已经过期返回{@code null} *

diff --git a/hutool-cache/src/main/java/cn/hutool/cache/CacheListener.java b/hutool-cache/src/main/java/cn/hutool/cache/CacheListener.java old mode 100644 new mode 100755 diff --git a/hutool-cache/src/main/java/cn/hutool/cache/CacheUtil.java b/hutool-cache/src/main/java/cn/hutool/cache/CacheUtil.java old mode 100644 new mode 100755 index b9318d91b948d92d969c6a170ba1fc955fdfa6bc..31b93cde86403509adae11dc04ffd851364ea67d --- a/hutool-cache/src/main/java/cn/hutool/cache/CacheUtil.java +++ b/hutool-cache/src/main/java/cn/hutool/cache/CacheUtil.java @@ -9,58 +9,59 @@ import cn.hutool.cache.impl.WeakCache; /** * 缓存工具类 + * * @author Looly - *@since 3.0.1 + * @since 3.0.1 */ public class CacheUtil { /** * 创建FIFO(first in first out) 先进先出缓存. * - * @param Key类型 - * @param Value类型 + * @param Key类型 + * @param Value类型 * @param capacity 容量 - * @param timeout 过期时长,单位:毫秒 + * @param timeout 过期时长,单位:毫秒 * @return {@link FIFOCache} */ - public static FIFOCache newFIFOCache(int capacity, long timeout){ + public static FIFOCache newFIFOCache(int capacity, long timeout) { return new FIFOCache<>(capacity, timeout); } /** * 创建FIFO(first in first out) 先进先出缓存. * - * @param Key类型 - * @param Value类型 + * @param Key类型 + * @param Value类型 * @param capacity 容量 * @return {@link FIFOCache} */ - public static FIFOCache newFIFOCache(int capacity){ + public static FIFOCache newFIFOCache(int capacity) { return new FIFOCache<>(capacity); } /** * 创建LFU(least frequently used) 最少使用率缓存. * - * @param Key类型 - * @param Value类型 + * @param Key类型 + * @param Value类型 * @param capacity 容量 - * @param timeout 过期时长,单位:毫秒 + * @param timeout 过期时长,单位:毫秒 * @return {@link LFUCache} */ - public static LFUCache newLFUCache(int capacity, long timeout){ + public static LFUCache newLFUCache(int capacity, long timeout) { return new LFUCache<>(capacity, timeout); } /** * 创建LFU(least frequently used) 最少使用率缓存. * - * @param Key类型 - * @param Value类型 + * @param Key类型 + * @param Value类型 * @param capacity 容量 * @return {@link LFUCache} */ - public static LFUCache newLFUCache(int capacity){ + public static LFUCache newLFUCache(int capacity) { return new LFUCache<>(capacity); } @@ -68,50 +69,66 @@ public class CacheUtil { /** * 创建LRU (least recently used)最近最久未使用缓存. * - * @param Key类型 - * @param Value类型 + * @param Key类型 + * @param Value类型 * @param capacity 容量 - * @param timeout 过期时长,单位:毫秒 + * @param timeout 过期时长,单位:毫秒 * @return {@link LRUCache} */ - public static LRUCache newLRUCache(int capacity, long timeout){ + public static LRUCache newLRUCache(int capacity, long timeout) { return new LRUCache<>(capacity, timeout); } /** * 创建LRU (least recently used)最近最久未使用缓存. * - * @param Key类型 - * @param Value类型 + * @param Key类型 + * @param Value类型 * @param capacity 容量 * @return {@link LRUCache} */ - public static LRUCache newLRUCache(int capacity){ + public static LRUCache newLRUCache(int capacity) { return new LRUCache<>(capacity); } + /** + * 创建定时缓存,通过定时任务自动清除过期缓存对象 + * + * @param Key类型 + * @param Value类型 + * @param timeout 过期时长,单位:毫秒 + * @param schedulePruneDelay 间隔时长,单位毫秒 + * @return {@link TimedCache} + * @since 5.8.28 + */ + public static TimedCache newTimedCache(long timeout, long schedulePruneDelay) { + final TimedCache cache = newTimedCache(timeout); + cache.schedulePrune(schedulePruneDelay); + return cache; + } + /** * 创建定时缓存. * - * @param Key类型 - * @param Value类型 + * @param Key类型 + * @param Value类型 * @param timeout 过期时长,单位:毫秒 * @return {@link TimedCache} */ - public static TimedCache newTimedCache(long timeout){ + public static TimedCache newTimedCache(long timeout) { return new TimedCache<>(timeout); } /** * 创建弱引用缓存. * - * @param Key类型 - * @param Value类型 + * @param Key类型 + * @param Value类型 * @param timeout 过期时长,单位:毫秒 * @return {@link WeakCache} * @since 3.0.7 */ - public static WeakCache newWeakCache(long timeout){ + public static WeakCache newWeakCache(long timeout) { return new WeakCache<>(timeout); } @@ -122,7 +139,7 @@ public class CacheUtil { * @param Value类型 * @return {@link NoCache} */ - public static NoCache newNoCache(){ + public static NoCache newNoCache() { return new NoCache<>(); } } diff --git a/hutool-cache/src/main/java/cn/hutool/cache/GlobalPruneTimer.java b/hutool-cache/src/main/java/cn/hutool/cache/GlobalPruneTimer.java old mode 100644 new mode 100755 diff --git a/hutool-cache/src/main/java/cn/hutool/cache/file/AbstractFileCache.java b/hutool-cache/src/main/java/cn/hutool/cache/file/AbstractFileCache.java old mode 100644 new mode 100755 diff --git a/hutool-cache/src/main/java/cn/hutool/cache/file/LFUFileCache.java b/hutool-cache/src/main/java/cn/hutool/cache/file/LFUFileCache.java old mode 100644 new mode 100755 diff --git a/hutool-cache/src/main/java/cn/hutool/cache/file/LRUFileCache.java b/hutool-cache/src/main/java/cn/hutool/cache/file/LRUFileCache.java old mode 100644 new mode 100755 diff --git a/hutool-cache/src/main/java/cn/hutool/cache/file/package-info.java b/hutool-cache/src/main/java/cn/hutool/cache/file/package-info.java old mode 100644 new mode 100755 diff --git a/hutool-cache/src/main/java/cn/hutool/cache/impl/AbstractCache.java b/hutool-cache/src/main/java/cn/hutool/cache/impl/AbstractCache.java old mode 100644 new mode 100755 index a52ac471397e15a1e76789ea8cec29ce4c062fcd..da3d72d2b39d15752b421e5f1cab65e6e55f93a5 --- a/hutool-cache/src/main/java/cn/hutool/cache/impl/AbstractCache.java +++ b/hutool-cache/src/main/java/cn/hutool/cache/impl/AbstractCache.java @@ -5,11 +5,11 @@ import cn.hutool.cache.CacheListener; import cn.hutool.core.lang.func.Func0; import cn.hutool.core.lang.mutable.Mutable; import cn.hutool.core.lang.mutable.MutableObj; +import cn.hutool.core.map.SafeConcurrentHashMap; import java.util.Iterator; import java.util.Map; import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.LongAdder; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; @@ -35,7 +35,7 @@ public abstract class AbstractCache implements Cache { /** * 写的时候每个key一把锁,降低锁的粒度 */ - protected final Map keyLockMap = new ConcurrentHashMap<>(); + protected final SafeConcurrentHashMap keyLockMap = new SafeConcurrentHashMap<>(); /** * 返回缓存容量,{@code 0}表示无大小限制 @@ -84,10 +84,21 @@ public abstract class AbstractCache implements Cache { if (timeout != 0) { existCustomTimeout = true; } - if (isFull()) { - pruneCache(); + + final MutableObj mKey = MutableObj.of(key); + + // issue#3618 对于替换的键值对,不做满队列检查和清除 + final CacheObj oldObj = cacheMap.get(mKey); + if (null != oldObj) { + onRemove(oldObj.key, oldObj.obj); + // 存在相同key,覆盖之 + cacheMap.put(mKey, co); + } else { + if (isFull()) { + pruneCache(); + } + cacheMap.put(mKey, co); } - cacheMap.put(MutableObj.of(key), co); } // ---------------------------------------------------------------- put end @@ -108,6 +119,11 @@ public abstract class AbstractCache implements Cache { @Override public V get(K key, boolean isUpdateLastAccess, Func0 supplier) { + return get(key, isUpdateLastAccess, this.timeout, supplier); + } + + @Override + public V get(K key, boolean isUpdateLastAccess, long timeout, Func0 supplier) { V v = get(key, isUpdateLastAccess); if (null == v && null != supplier) { //每个key单独获取一把锁,降低锁的粒度提高并发能力,see pr#1385@Github @@ -115,17 +131,11 @@ public abstract class AbstractCache implements Cache { keyLock.lock(); try { // 双重检查锁,防止在竞争锁的过程中已经有其它线程写入 - final CacheObj co = getWithoutLock(key); - if (null == co || co.isExpired()) { - try { - v = supplier.call(); - } catch (Exception e) { - throw new RuntimeException(e); - } - put(key, v, this.timeout); - } else { - v = co.get(isUpdateLastAccess); - } + // issue#3686 由于这个方法内的加锁是get独立锁,不和put锁互斥,而put和pruneCache会修改cacheMap,导致在pruneCache过程中get会有并发问题 + // 因此此处需要使用带全局锁的get获取值 + v = get(key, isUpdateLastAccess); + v = supplier.callWithRuntimeException(); + put(key, v, timeout); } finally { keyLock.unlock(); keyLockMap.remove(key); @@ -246,16 +256,10 @@ public abstract class AbstractCache implements Cache { * 移除key对应的对象,不加锁 * * @param key 键 - * @param withMissCount 是否计数丢失数 * @return 移除的对象,无返回null */ - protected CacheObj removeWithoutLock(K key, boolean withMissCount) { - final CacheObj co = cacheMap.remove(MutableObj.of(key)); - if (withMissCount) { - // 在丢失计数有效的情况下,移除一般为get时的超时操作,此处应该丢失数+1 - this.missCount.increment(); - } - return co; + protected CacheObj removeWithoutLock(K key) { + return cacheMap.remove(MutableObj.of(key)); } /** diff --git a/hutool-cache/src/main/java/cn/hutool/cache/impl/CacheObj.java b/hutool-cache/src/main/java/cn/hutool/cache/impl/CacheObj.java old mode 100644 new mode 100755 diff --git a/hutool-cache/src/main/java/cn/hutool/cache/impl/CacheObjIterator.java b/hutool-cache/src/main/java/cn/hutool/cache/impl/CacheObjIterator.java old mode 100644 new mode 100755 diff --git a/hutool-cache/src/main/java/cn/hutool/cache/impl/CacheValuesIterator.java b/hutool-cache/src/main/java/cn/hutool/cache/impl/CacheValuesIterator.java old mode 100644 new mode 100755 diff --git a/hutool-cache/src/main/java/cn/hutool/cache/impl/FIFOCache.java b/hutool-cache/src/main/java/cn/hutool/cache/impl/FIFOCache.java old mode 100644 new mode 100755 index a3fc3508529fff8f842b06c179857e1690d0b39c..68de47ef8f34e1e3cd11035c33b92824a5bf251d --- a/hutool-cache/src/main/java/cn/hutool/cache/impl/FIFOCache.java +++ b/hutool-cache/src/main/java/cn/hutool/cache/impl/FIFOCache.java @@ -16,7 +16,7 @@ import java.util.LinkedHashMap; * @param 值类型 * @author Looly */ -public class FIFOCache extends StampedCache { +public class FIFOCache extends ReentrantCache { private static final long serialVersionUID = 1L; /** @@ -71,7 +71,7 @@ public class FIFOCache extends StampedCache { // 清理结束后依旧是满的,则删除第一个被缓存的对象 if (isFull() && null != first) { - removeWithoutLock(first.key, false); + removeWithoutLock(first.key); onRemove(first.key, first.obj); count++; } diff --git a/hutool-cache/src/main/java/cn/hutool/cache/impl/LFUCache.java b/hutool-cache/src/main/java/cn/hutool/cache/impl/LFUCache.java old mode 100644 new mode 100755 index b3bcdffc99d2487c8db19269e63920bf9b17b4b7..6ac8e6299ff494243c38713ad16cf20042ba75ca --- a/hutool-cache/src/main/java/cn/hutool/cache/impl/LFUCache.java +++ b/hutool-cache/src/main/java/cn/hutool/cache/impl/LFUCache.java @@ -15,7 +15,7 @@ import java.util.Iterator; * @param 键类型 * @param 值类型 */ -public class LFUCache extends StampedCache { +public class LFUCache extends ReentrantCache { private static final long serialVersionUID = 1L; /** diff --git a/hutool-cache/src/main/java/cn/hutool/cache/impl/LRUCache.java b/hutool-cache/src/main/java/cn/hutool/cache/impl/LRUCache.java old mode 100644 new mode 100755 index c95b97f2efd523d3637a29ab49c5e904a6b9f4fc..f3116ba58c4a4ed65cd2bc360ec52e7339810419 --- a/hutool-cache/src/main/java/cn/hutool/cache/impl/LRUCache.java +++ b/hutool-cache/src/main/java/cn/hutool/cache/impl/LRUCache.java @@ -1,5 +1,6 @@ package cn.hutool.cache.impl; +import cn.hutool.core.lang.mutable.Mutable; import cn.hutool.core.map.FixedLinkedHashMap; import java.util.Iterator; @@ -42,7 +43,13 @@ public class LRUCache extends ReentrantCache { this.timeout = timeout; //链表key按照访问顺序排序,调用get方法后,会将这次访问的元素移至头部 - cacheMap = new FixedLinkedHashMap<>(capacity); + final FixedLinkedHashMap, CacheObj> fixedLinkedHashMap = new FixedLinkedHashMap<>(capacity); + fixedLinkedHashMap.setRemoveListener(entry -> { + if(null != listener){ + listener.onRemove(entry.getKey().get(), entry.getValue().getValue()); + } + }); + cacheMap = fixedLinkedHashMap; } // ---------------------------------------------------------------- prune diff --git a/hutool-cache/src/main/java/cn/hutool/cache/impl/NoCache.java b/hutool-cache/src/main/java/cn/hutool/cache/impl/NoCache.java old mode 100644 new mode 100755 index 02b8173e4b2d872fe12eff127e6f36f176c5fb68..96bc03b98e90ded0b5abc3725da5f9f93f5779e3 --- a/hutool-cache/src/main/java/cn/hutool/cache/impl/NoCache.java +++ b/hutool-cache/src/main/java/cn/hutool/cache/impl/NoCache.java @@ -1,6 +1,7 @@ package cn.hutool.cache.impl; import cn.hutool.cache.Cache; +import cn.hutool.core.exceptions.ExceptionUtil; import cn.hutool.core.lang.func.Func0; import java.util.Iterator; @@ -57,10 +58,15 @@ public class NoCache implements Cache { @Override public V get(K key, boolean isUpdateLastAccess, Func0 supplier) { + return get(key, isUpdateLastAccess, 0, supplier); + } + + @Override + public V get(K key, boolean isUpdateLastAccess, long timeout, Func0 supplier) { try { return (null == supplier) ? null : supplier.call(); } catch (Exception e) { - throw new RuntimeException(e); + throw ExceptionUtil.wrapRuntime(e); } } diff --git a/hutool-cache/src/main/java/cn/hutool/cache/impl/ReentrantCache.java b/hutool-cache/src/main/java/cn/hutool/cache/impl/ReentrantCache.java old mode 100644 new mode 100755 index e454740403c5e28c5b6c7a992a7d0d7e2b4b3cfb..4c87bdf7981ff05910035e7afdf4f36aa35fc185 --- a/hutool-cache/src/main/java/cn/hutool/cache/impl/ReentrantCache.java +++ b/hutool-cache/src/main/java/cn/hutool/cache/impl/ReentrantCache.java @@ -1,8 +1,12 @@ package cn.hutool.cache.impl; import cn.hutool.core.collection.CopiedIter; +import cn.hutool.core.lang.func.Func0; +import cn.hutool.core.lang.mutable.Mutable; +import java.util.HashSet; import java.util.Iterator; +import java.util.Set; import java.util.concurrent.locks.ReentrantLock; /** @@ -33,49 +37,37 @@ public abstract class ReentrantCache extends AbstractCache { @Override public boolean containsKey(K key) { - lock.lock(); - try { - // 不存在或已移除 - final CacheObj co = getWithoutLock(key); - if (co == null) { - return false; - } - - if (false == co.isExpired()) { - // 命中 - return true; - } - } finally { - lock.unlock(); - } - - // 过期 - remove(key, true); - return false; + return null != getOrRemoveExpired(key, false, false); } @Override public V get(K key, boolean isUpdateLastAccess) { - CacheObj co; - lock.lock(); - try { - co = getWithoutLock(key); - } finally { - lock.unlock(); - } + return getOrRemoveExpired(key, isUpdateLastAccess, true); + } - // 未命中 - if (null == co) { - missCount.increment(); - return null; - } else if (false == co.isExpired()) { - hitCount.increment(); - return co.get(isUpdateLastAccess); + @Override + public V get(final K key, final boolean isUpdateLastAccess, final long timeout, final Func0 valueFactory) { + V v = get(key, isUpdateLastAccess); + + // 对象不存在,则加锁创建 + if (null == v && null != valueFactory) { + // 按照pr#1385提议,使用key锁可以避免对象创建等待问题,但是会带来循环锁问题,见:issue#4022 + // 因此此处依旧采用全局锁,在对象创建过程中,全局等待,避免循环锁依赖 + // 这样避免了循环锁,但是会存在一个缺点,即对象创建过程中,其它线程无法获得锁,从而无法使用缓存,因此需要考虑对象创建的耗时问题 + lock.lock(); + try { + // 双重检查锁,防止在竞争锁的过程中已经有其它线程写入 + final CacheObj co = getWithoutLock(key); + if (null == co) { + // supplier的创建是一个耗时过程,此处创建与全局锁无关,而与key锁相关,这样就保证每个key只创建一个value,且互斥 + v = valueFactory.callWithRuntimeException(); + putWithoutLock(key, v, timeout); + } + } finally { + lock.unlock(); + } } - - // 过期,既不算命中也不算非命中 - remove(key, true); - return null; + return v; } @Override @@ -102,14 +94,30 @@ public abstract class ReentrantCache extends AbstractCache { @Override public void remove(K key) { - remove(key, false); + lock.lock(); + CacheObj co; + try { + co = removeWithoutLock(key); + } finally { + lock.unlock(); + } + if (null != co) { + onRemove(co.key, co.obj); + } } @Override public void clear() { lock.lock(); try { - cacheMap.clear(); + // 获取所有键的副本 + Set> keys = new HashSet<>(cacheMap.keySet()); + for (Mutable key : keys) { + CacheObj co = removeWithoutLock(key.get()); + if (co != null) { + onRemove(co.key, co.obj); // 触发资源释放 + } + } } finally { lock.unlock(); } @@ -126,21 +134,38 @@ public abstract class ReentrantCache extends AbstractCache { } /** - * 移除key对应的对象 - * - * @param key 键 - * @param withMissCount 是否计数丢失数 + * 获得值或清除过期值 + * @param key 键 + * @param isUpdateLastAccess 是否更新最后访问时间 + * @param isUpdateCount 是否更新计数器 + * @return 值或null */ - private void remove(K key, boolean withMissCount) { - lock.lock(); + private V getOrRemoveExpired(final K key, final boolean isUpdateLastAccess, final boolean isUpdateCount) { CacheObj co; + lock.lock(); try { - co = removeWithoutLock(key, withMissCount); + co = getWithoutLock(key); + if(null != co && co.isExpired()){ + //过期移除 + removeWithoutLock(key); + onRemove(co.key, co.obj); + co = null; + } } finally { lock.unlock(); } - if (null != co) { - onRemove(co.key, co.obj); + + // 未命中 + if (null == co) { + if(isUpdateCount){ + missCount.increment(); + } + return null; + } + + if(isUpdateCount){ + hitCount.increment(); } + return co.get(isUpdateLastAccess); } } diff --git a/hutool-cache/src/main/java/cn/hutool/cache/impl/StampedCache.java b/hutool-cache/src/main/java/cn/hutool/cache/impl/StampedCache.java old mode 100644 new mode 100755 index deaec664145652991c0034d26e55b35c578d09f4..a3ca63a309f2fa5a26f45d10a8e065a11baf0a05 --- a/hutool-cache/src/main/java/cn/hutool/cache/impl/StampedCache.java +++ b/hutool-cache/src/main/java/cn/hutool/cache/impl/StampedCache.java @@ -12,8 +12,10 @@ import java.util.concurrent.locks.StampedLock; * @param 值类型 * @author looly * @since 5.7.15 + * @deprecated Map使用StampedLock可能造成数据不一致甚至Map循环调用,此缓存废弃 */ -public abstract class StampedCache extends AbstractCache{ +@Deprecated +public abstract class StampedCache extends AbstractCache { private static final long serialVersionUID = 1L; // 乐观锁,此处使用乐观锁解决读多写少的场景 @@ -33,54 +35,12 @@ public abstract class StampedCache extends AbstractCache{ @Override public boolean containsKey(K key) { - final long stamp = lock.readLock(); - try { - // 不存在或已移除 - final CacheObj co = getWithoutLock(key); - if (co == null) { - return false; - } - - if (false == co.isExpired()) { - // 命中 - return true; - } - } finally { - lock.unlockRead(stamp); - } - - // 过期 - remove(key, true); - return false; + return null != get(key, false, false); } @Override public V get(K key, boolean isUpdateLastAccess) { - // 尝试读取缓存,使用乐观读锁 - long stamp = lock.tryOptimisticRead(); - CacheObj co = getWithoutLock(key); - if(false == lock.validate(stamp)){ - // 有写线程修改了此对象,悲观读 - stamp = lock.readLock(); - try { - co = getWithoutLock(key); - } finally { - lock.unlockRead(stamp); - } - } - - // 未命中 - if (null == co) { - missCount.increment(); - return null; - } else if (false == co.isExpired()) { - hitCount.increment(); - return co.get(isUpdateLastAccess); - } - - // 过期,既不算命中也不算非命中 - remove(key, true); - return null; + return get(key, isUpdateLastAccess, true); } @Override @@ -107,7 +67,16 @@ public abstract class StampedCache extends AbstractCache{ @Override public void remove(K key) { - remove(key, false); + final long stamp = lock.writeLock(); + CacheObj co; + try { + co = removeWithoutLock(key); + } finally { + lock.unlockWrite(stamp); + } + if (null != co) { + onRemove(co.key, co.obj); + } } @Override @@ -121,21 +90,94 @@ public abstract class StampedCache extends AbstractCache{ } /** - * 移除key对应的对象 + * 获取值,使用乐观锁,但是此方法可能导致读取脏数据,但对于缓存业务可容忍。情况如下: + *

+	 *     1. 读取时无写入,不冲突,直接获取值
+	 *     2. 读取时无写入,但是乐观读时触发了并发异常,此时获取同步锁,获取新值
+	 *     4. 读取时有写入,此时获取同步锁,获取新值
+	 * 
+ * + * @param key 键 + * @param isUpdateLastAccess 是否更新最后修改时间 + * @param isUpdateCount 是否更新命中数,get时更新,contains时不更新 + * @return 值或null + */ + private V get(K key, boolean isUpdateLastAccess, boolean isUpdateCount) { + // 尝试读取缓存,使用乐观读锁 + CacheObj co = null; + long stamp = lock.tryOptimisticRead(); + boolean isReadError = true; + if(lock.validate(stamp)){ + try{ + // 乐观读,可能读取脏数据,在缓存中可容忍,分两种情况 + // 1. 读取时无线程写入 + // 2. 读取时有线程写入,导致数据不一致,此时读取未更新的缓存值 + co = getWithoutLock(key); + isReadError = false; + } catch (final Exception ignore){ + // ignore + } + } + + if(isReadError){ + // 转换为悲观读 + // 原因可能为无锁读时触发并发异常,或者锁被占(正在写) + stamp = lock.readLock(); + try { + co = getWithoutLock(key); + } finally { + lock.unlockRead(stamp); + } + } + + // 未命中 + if (null == co) { + if (isUpdateCount) { + missCount.increment(); + } + return null; + } else if (false == co.isExpired()) { + if (isUpdateCount) { + hitCount.increment(); + } + return co.get(isUpdateLastAccess); + } + + // 悲观锁,二次检查 + return getOrRemoveExpired(key, isUpdateCount); + } + + /** + * 同步获取值,如果过期则移除之 * * @param key 键 - * @param withMissCount 是否计数丢失数 + * @param isUpdateCount 是否更新命中数,get时更新,contains时不更新 + * @return 有效值或null */ - private void remove(K key, boolean withMissCount) { + private V getOrRemoveExpired(K key, boolean isUpdateCount) { final long stamp = lock.writeLock(); CacheObj co; try { - co = removeWithoutLock(key, withMissCount); + co = getWithoutLock(key); + if (null == co) { + return null; + } + if (false == co.isExpired()) { + // 首先尝试获取值,如果值存在且有效,返回之 + if (isUpdateCount) { + hitCount.increment(); + } + return co.getValue(); + } + + // 无效移除 + co = removeWithoutLock(key); } finally { lock.unlockWrite(stamp); } if (null != co) { onRemove(co.key, co.obj); } + return null; } } diff --git a/hutool-cache/src/main/java/cn/hutool/cache/impl/TimedCache.java b/hutool-cache/src/main/java/cn/hutool/cache/impl/TimedCache.java old mode 100644 new mode 100755 index dfccd942ed62c6cfb3454d4b7d00157e3fd4fc48..3733a899c0b0152073ff33d3eb6d6ca29eeaee8f --- a/hutool-cache/src/main/java/cn/hutool/cache/impl/TimedCache.java +++ b/hutool-cache/src/main/java/cn/hutool/cache/impl/TimedCache.java @@ -17,7 +17,7 @@ import java.util.concurrent.ScheduledFuture; * @param 键类型 * @param 值类型 */ -public class TimedCache extends StampedCache { +public class TimedCache extends ReentrantCache { private static final long serialVersionUID = 1L; /** 正在执行的定时任务 */ diff --git a/hutool-cache/src/main/java/cn/hutool/cache/impl/WeakCache.java b/hutool-cache/src/main/java/cn/hutool/cache/impl/WeakCache.java index a6fb236900b6d3e23c7dd77c47f3f973cf2fce0b..37150588e1d1dddf1d71c8bc14190a224140443f 100755 --- a/hutool-cache/src/main/java/cn/hutool/cache/impl/WeakCache.java +++ b/hutool-cache/src/main/java/cn/hutool/cache/impl/WeakCache.java @@ -3,9 +3,8 @@ package cn.hutool.cache.impl; import cn.hutool.cache.CacheListener; import cn.hutool.core.lang.Opt; import cn.hutool.core.lang.mutable.Mutable; -import cn.hutool.core.map.WeakConcurrentMap; - -import java.lang.ref.Reference; +import cn.hutool.core.lang.ref.Ref; +import cn.hutool.core.map.reference.WeakKeyValueConcurrentMap; /** * 弱引用缓存
@@ -27,16 +26,18 @@ public class WeakCache extends TimedCache{ * @param timeout 超时时常,单位毫秒,-1或0表示无限制 */ public WeakCache(long timeout) { - super(timeout, new WeakConcurrentMap<>()); + super(timeout, new WeakKeyValueConcurrentMap<>()); } @Override public WeakCache setListener(CacheListener listener) { super.setListener(listener); - final WeakConcurrentMap, CacheObj> map = (WeakConcurrentMap, CacheObj>) this.cacheMap; + final WeakKeyValueConcurrentMap, CacheObj> map = (WeakKeyValueConcurrentMap, CacheObj>) this.cacheMap; // WeakKey回收之后,key对应的值已经是null了,因此此处的key也为null - map.setPurgeListener((key, value)-> listener.onRemove(Opt.ofNullable(key).map(Reference::get).map(Mutable::get).get(), value.getValue())); + map.setPurgeListener((key, value)-> listener.onRemove( + Opt.ofNullable(key).map(Ref::get).map(Mutable::get).get(), + Opt.ofNullable(value).map(Ref::get).map(CacheObj::getValue).get())); return this; } diff --git a/hutool-cache/src/main/java/cn/hutool/cache/impl/package-info.java b/hutool-cache/src/main/java/cn/hutool/cache/impl/package-info.java old mode 100644 new mode 100755 diff --git a/hutool-cache/src/main/java/cn/hutool/cache/package-info.java b/hutool-cache/src/main/java/cn/hutool/cache/package-info.java old mode 100644 new mode 100755 diff --git a/hutool-cache/src/test/java/cn/hutool/cache/CacheConcurrentTest.java b/hutool-cache/src/test/java/cn/hutool/cache/CacheConcurrentTest.java old mode 100644 new mode 100755 index 22445b28b59a2232a5315b609e40a58d6af91f89..02860839ec93534e1c8854be64fe0973412936d8 --- a/hutool-cache/src/test/java/cn/hutool/cache/CacheConcurrentTest.java +++ b/hutool-cache/src/test/java/cn/hutool/cache/CacheConcurrentTest.java @@ -6,12 +6,13 @@ import cn.hutool.cache.impl.WeakCache; import cn.hutool.core.lang.Console; import cn.hutool.core.thread.ConcurrencyTester; import cn.hutool.core.thread.ThreadUtil; -import org.junit.Assert; -import org.junit.Ignore; -import org.junit.Test; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; import java.util.concurrent.atomic.AtomicInteger; +import static org.junit.jupiter.api.Assertions.assertTrue; + /** * 缓存单元测试 * @@ -21,7 +22,7 @@ import java.util.concurrent.atomic.AtomicInteger; public class CacheConcurrentTest { @Test - @Ignore + @Disabled public void fifoCacheTest() { int threadCount = 4000; final Cache cache = new FIFOCache<>(3); @@ -52,7 +53,7 @@ public class CacheConcurrentTest { } @Test - @Ignore + @Disabled public void lruCacheTest() { int threadCount = 40000; final Cache cache = new LRUCache<>(1000); @@ -88,6 +89,7 @@ public class CacheConcurrentTest { } @Test + @Disabled public void effectiveTest() { // 模拟耗时操作消耗时间 int delay = 2000; @@ -102,6 +104,6 @@ public class CacheConcurrentTest { }); long interval = concurrencyTester.getInterval(); // 总耗时应与单次操作耗时在同一个数量级 - Assert.assertTrue(interval < delay * 2); + assertTrue(interval < delay * 2); } } diff --git a/hutool-cache/src/test/java/cn/hutool/cache/CacheTest.java b/hutool-cache/src/test/java/cn/hutool/cache/CacheTest.java old mode 100644 new mode 100755 index 1509cd56db421d89efc48f08f914fbe7cd0ba91a..ed862b9be1ac26ecc5e2898b53f29a752cd81f57 --- a/hutool-cache/src/test/java/cn/hutool/cache/CacheTest.java +++ b/hutool-cache/src/test/java/cn/hutool/cache/CacheTest.java @@ -4,13 +4,17 @@ import cn.hutool.cache.impl.TimedCache; import cn.hutool.core.date.DateUnit; import cn.hutool.core.thread.ThreadUtil; import cn.hutool.core.util.RandomUtil; -import org.junit.Assert; -import org.junit.Test; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.jupiter.api.Assertions.*; /** * 缓存测试用例 - * @author Looly * + * @author Looly */ public class CacheTest { @@ -19,8 +23,8 @@ public class CacheTest { Cache fifoCache = CacheUtil.newFIFOCache(3); fifoCache.setListener((key, value)->{ // 监听测试,此测试中只有key1被移除,测试是否监听成功 - Assert.assertEquals("key1", key); - Assert.assertEquals("value1", value); + assertEquals("key1", key); + assertEquals("value1", value); }); fifoCache.put("key1", "value1", DateUnit.SECOND.getMillis() * 3); @@ -30,7 +34,7 @@ public class CacheTest { //由于缓存容量只有3,当加入第四个元素的时候,根据FIFO规则,最先放入的对象将被移除 String value1 = fifoCache.get("key1"); - Assert.assertNull(value1); + assertNull(value1); } @Test @@ -39,7 +43,7 @@ public class CacheTest { for (int i = 0; i < RandomUtil.randomInt(100, 1000); i++) { fifoCache.put("key" + i, "value" + i); } - Assert.assertEquals(100, fifoCache.size()); + assertEquals(100, fifoCache.size()); } @Test @@ -56,9 +60,16 @@ public class CacheTest { String value1 = lfuCache.get("key1"); String value2 = lfuCache.get("key2"); String value3 = lfuCache.get("key3"); - Assert.assertNotNull(value1); - Assert.assertNull(value2); - Assert.assertNull(value3); + assertNotNull(value1); + assertNull(value2); + assertNull(value3); + } + + @Test + public void lfuCacheTest2(){ + Cache lfuCache = CacheUtil.newLFUCache(3); + final String s = lfuCache.get(null); + assertNull(s); } @Test @@ -74,10 +85,10 @@ public class CacheTest { lruCache.put("key4", "value4", DateUnit.SECOND.getMillis() * 3); String value1 = lruCache.get("key1"); - Assert.assertNotNull(value1); + assertNotNull(value1); //由于缓存容量只有3,当加入第四个元素的时候,根据LRU规则,最少使用的将被移除(2被移除) String value2 = lruCache.get("key2"); - Assert.assertNull(value2); + assertNull(value2); } @Test @@ -96,22 +107,63 @@ public class CacheTest { //5毫秒后由于value2设置了5毫秒过期,因此只有value2被保留下来 String value1 = timedCache.get("key1"); - Assert.assertNull(value1); + assertNull(value1); String value2 = timedCache.get("key2"); - Assert.assertEquals("value2", value2); + assertEquals("value2", value2); //5毫秒后,由于设置了默认过期,key3只被保留4毫秒,因此为null String value3 = timedCache.get("key3"); - Assert.assertNull(value3); + assertNull(value3); String value3Supplier = timedCache.get("key3", () -> "Default supplier"); - Assert.assertEquals("Default supplier", value3Supplier); + assertEquals("Default supplier", value3Supplier); // 永不过期 String value4 = timedCache.get("key4"); - Assert.assertEquals("value4", value4); + assertEquals("value4", value4); //取消定时清理 timedCache.cancelPruneSchedule(); } + + + /** + * TimedCache的数据过期后不是每次都触发监听器onRemove,而是偶尔触发onRemove + * https://gitee.com/chinabugotech/hutool/issues/IBP752 + */ + @Test + public void whenContainsKeyTimeout_shouldCallOnRemove() { + int timeout = 50; + final TimedCache ALARM_CACHE = new TimedCache<>(timeout); + + AtomicInteger counter = new AtomicInteger(0); + ALARM_CACHE.setListener((key, value) -> { + counter.incrementAndGet(); + }); + + ALARM_CACHE.put(1, "value1"); + + ThreadUtil.sleep(100); + + assertFalse(ALARM_CACHE.containsKey(1)); + assertEquals(1, counter.get()); + } + + /** + * ReentrantCache类clear()方法、AbstractCache.putWithoutLock方法可能导致资源泄露 + * https://github.com/chinabugotech/hutool/issues/3957 + */ + @Test + public void reentrantCache_clear_Method_Test() { + final AtomicInteger removeCount = new AtomicInteger(); + final Cache lruCache = CacheUtil.newLRUCache(4); + lruCache.setListener((key, cachedObject) -> removeCount.getAndIncrement()); + lruCache.put("key1","String1"); + lruCache.put("key2","String2"); + lruCache.put("key3","String3"); + lruCache.put("key1","String4");//key已经存在,原始putWithoutLock方法存在资源泄露 + lruCache.put("key4","String5"); + lruCache.clear();//ReentrantCache类clear()方法存在资源泄露 + Assertions.assertEquals(5, removeCount.get()); + } } diff --git a/hutool-cache/src/test/java/cn/hutool/cache/FileCacheTest.java b/hutool-cache/src/test/java/cn/hutool/cache/FileCacheTest.java old mode 100644 new mode 100755 index eaadb83db7742a12f28fddf0857cb20982fc8acc..152c778c0f0ba3d3d0eca8a86d4d25f37714804b --- a/hutool-cache/src/test/java/cn/hutool/cache/FileCacheTest.java +++ b/hutool-cache/src/test/java/cn/hutool/cache/FileCacheTest.java @@ -1,7 +1,7 @@ package cn.hutool.cache; -import org.junit.Assert; -import org.junit.Test; +import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.Test; import cn.hutool.cache.file.LFUFileCache; @@ -14,6 +14,6 @@ public class FileCacheTest { @Test public void lfuFileCacheTest() { LFUFileCache cache = new LFUFileCache(1000, 500, 2000); - Assert.assertNotNull(cache); + assertNotNull(cache); } } diff --git a/hutool-cache/src/test/java/cn/hutool/cache/Issue3618Test.java b/hutool-cache/src/test/java/cn/hutool/cache/Issue3618Test.java new file mode 100644 index 0000000000000000000000000000000000000000..5a097ef3ebc2bc52c20e58cf844c1d7b2e391704 --- /dev/null +++ b/hutool-cache/src/test/java/cn/hutool/cache/Issue3618Test.java @@ -0,0 +1,22 @@ +package cn.hutool.cache; + +import cn.hutool.cache.impl.FIFOCache; +import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.Test; + +public class Issue3618Test { + @Test + public void putTest() { + FIFOCache cache = CacheUtil.newFIFOCache(3); + cache.put(1, 1); + cache.put(2, 1); + cache.put(3, 1); + + assertEquals(3, cache.size()); + + // issue#3618 对于替换的键值对,不做满队列检查和清除 + cache.put(3, 2); + + assertEquals(3, cache.size()); + } +} diff --git a/hutool-cache/src/test/java/cn/hutool/cache/IssueI8MEIXTest.java b/hutool-cache/src/test/java/cn/hutool/cache/IssueI8MEIXTest.java new file mode 100644 index 0000000000000000000000000000000000000000..eb7565cd67fb5b3e8275163153100d79816aeded --- /dev/null +++ b/hutool-cache/src/test/java/cn/hutool/cache/IssueI8MEIXTest.java @@ -0,0 +1,30 @@ +package cn.hutool.cache; + +import cn.hutool.cache.impl.TimedCache; +import cn.hutool.core.lang.Console; +import cn.hutool.core.thread.ThreadUtil; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +public class IssueI8MEIXTest { + + @Test + @Disabled + public void getRemoveTest() { + final TimedCache cache = new TimedCache<>(200); + cache.put("a", "123"); + + ThreadUtil.sleep(300); + + // 测试时,在get后的remove前加sleep测试在读取过程中put新值的问题 + ThreadUtil.execute(()->{ + Console.log(cache.get("a")); + }); + + ThreadUtil.execute(()->{ + cache.put("a", "456"); + }); + + ThreadUtil.sleep(1000); + } +} diff --git a/hutool-cache/src/test/java/cn/hutool/cache/LRUCacheTest.java b/hutool-cache/src/test/java/cn/hutool/cache/LRUCacheTest.java old mode 100644 new mode 100755 index fdc6ea4be159d1c0cd6077aa3a2657903d1468d1..a3ab1aee15624279ed8e03639feb75da70015491 --- a/hutool-cache/src/test/java/cn/hutool/cache/LRUCacheTest.java +++ b/hutool-cache/src/test/java/cn/hutool/cache/LRUCacheTest.java @@ -3,24 +3,27 @@ package cn.hutool.cache; import cn.hutool.cache.impl.LRUCache; import cn.hutool.core.thread.ThreadUtil; import cn.hutool.core.util.RandomUtil; -import org.junit.Assert; -import org.junit.Ignore; -import org.junit.Test; +import cn.hutool.core.util.StrUtil; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.jupiter.api.Assertions.assertEquals; /** - * 见:https://github.com/dromara/hutool/issues/1895
+ * 见:https://github.com/chinabugotech/hutool/issues/1895
* 并发问题测试,在5.7.15前,LRUCache存在并发问题,多线程get后,map结构变更,导致null的位置不确定, * 并可能引起死锁。 */ public class LRUCacheTest { @Test - @Ignore + @Disabled public void putTest(){ - //https://github.com/dromara/hutool/issues/2227 - LRUCache cache = CacheUtil.newLRUCache(100, 10); + //https://github.com/chinabugotech/hutool/issues/2227 + final LRUCache cache = CacheUtil.newLRUCache(100, 10); for (int i = 0; i < 10000; i++) { //ThreadUtil.execute(()-> cache.put(RandomUtil.randomString(5), "1243", 10)); ThreadUtil.execute(()-> cache.get(RandomUtil.randomString(5), ()->RandomUtil.randomString(10))); @@ -30,15 +33,15 @@ public class LRUCacheTest { @Test public void readWriteTest() throws InterruptedException { - LRUCache cache = CacheUtil.newLRUCache(10); + final LRUCache cache = CacheUtil.newLRUCache(10); for (int i = 0; i < 10; i++) { cache.put(i, i); } - CountDownLatch countDownLatch = new CountDownLatch(10); + final CountDownLatch countDownLatch = new CountDownLatch(10); // 10个线程分别读0-9 10000次 for (int i = 0; i < 10; i++) { - int finalI = i; + final int finalI = i; new Thread(() -> { for (int j = 0; j < 10000; j++) { cache.get(finalI); @@ -49,19 +52,38 @@ public class LRUCacheTest { // 等待读线程结束 countDownLatch.await(); // 按顺序读0-9 - StringBuilder sb1 = new StringBuilder(); + final StringBuilder sb1 = new StringBuilder(); for (int i = 0; i < 10; i++) { sb1.append(cache.get(i)); } - Assert.assertEquals("0123456789", sb1.toString()); + assertEquals("0123456789", sb1.toString()); // 新加11,此时0最久未使用,应该淘汰0 cache.put(11, 11); - StringBuilder sb2 = new StringBuilder(); + final StringBuilder sb2 = new StringBuilder(); for (int i = 0; i < 10; i++) { sb2.append(cache.get(i)); } - Assert.assertEquals("null123456789", sb2.toString()); + assertEquals("null123456789", sb2.toString()); + } + + @Test + public void issue2647Test(){ + final AtomicInteger removeCount = new AtomicInteger(); + + final LRUCache cache = CacheUtil.newLRUCache(3,1); + cache.setListener((key, value) -> { + // 共移除7次 + removeCount.incrementAndGet(); + //Console.log("Start remove k-v, key:{}, value:{}", key, value); + }); + + for (int i = 0; i < 10; i++) { + cache.put(StrUtil.format("key-{}", i), i); + } + + assertEquals(7, removeCount.get()); + assertEquals(3, cache.size()); } } diff --git a/hutool-cache/src/test/java/cn/hutool/cache/WeakCacheTest.java b/hutool-cache/src/test/java/cn/hutool/cache/WeakCacheTest.java index f61ed77b7829a77eecabcc4c87b10b6e80012f52..0f5cc48d2df2af47fbb6463e34deae99c6b80c2b 100755 --- a/hutool-cache/src/test/java/cn/hutool/cache/WeakCacheTest.java +++ b/hutool-cache/src/test/java/cn/hutool/cache/WeakCacheTest.java @@ -2,9 +2,10 @@ package cn.hutool.cache; import cn.hutool.cache.impl.WeakCache; import cn.hutool.core.lang.Console; -import org.junit.Assert; -import org.junit.Ignore; -import org.junit.Test; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; public class WeakCacheTest { @@ -14,24 +15,24 @@ public class WeakCacheTest { cache.put("abc", "123"); cache.put("def", "456"); - Assert.assertEquals(2, cache.size()); + assertEquals(2, cache.size()); // 检查被MutableObj包装的key能否正常移除 cache.remove("abc"); - Assert.assertEquals(1, cache.size()); + assertEquals(1, cache.size()); } @Test - @Ignore + @Disabled public void removeByGcTest(){ - // https://gitee.com/dromara/hutool/issues/I51O7M + // https://gitee.com/chinabugotech/hutool/issues/I51O7M WeakCache cache = new WeakCache<>(-1); cache.put("a", "1"); cache.put("b", "2"); // 监听 - Assert.assertEquals(2, cache.size()); + assertEquals(2, cache.size()); cache.setListener(Console::log); // GC测试 diff --git a/hutool-captcha/pom.xml b/hutool-captcha/pom.xml index 157efcfb40990cfda8bb693cf08d3262f89e0aaf..284d020c2883c7fead1e9e3262b6ef2171c23ed0 100755 --- a/hutool-captcha/pom.xml +++ b/hutool-captcha/pom.xml @@ -9,13 +9,17 @@ cn.hutool hutool-parent - 5.8.0.M5 + 5.8.41 hutool-captcha ${project.artifactId} Hutool 验证码工具 + + cn.hutool.captcha + + cn.hutool @@ -23,4 +27,5 @@ ${project.parent.version} + diff --git a/hutool-captcha/src/main/java/cn/hutool/captcha/AbstractCaptcha.java b/hutool-captcha/src/main/java/cn/hutool/captcha/AbstractCaptcha.java old mode 100644 new mode 100755 index 85ebcd295a7fb3b5fb38220585ef9542d296061c..adb48c32ec12aaf637c7b3c193071bc171514894 --- a/hutool-captcha/src/main/java/cn/hutool/captcha/AbstractCaptcha.java +++ b/hutool-captcha/src/main/java/cn/hutool/captcha/AbstractCaptcha.java @@ -60,7 +60,7 @@ public abstract class AbstractCaptcha implements ICaptcha { /** * 背景色 */ - protected Color background; + protected Color background = Color.WHITE; /** * 文字透明度 */ @@ -95,6 +95,25 @@ public abstract class AbstractCaptcha implements ICaptcha { this.font = new Font(Font.SANS_SERIF, Font.PLAIN, (int) (this.height * 0.75)); } + + /** + * 构造 + * + * @param width 图片宽 + * @param height 图片高 + * @param generator 验证码生成器 + * @param interfereCount 验证码干扰元素个数 + * @param size 字体的大小 高度的倍数 + */ + public AbstractCaptcha(int width, int height, CodeGenerator generator, int interfereCount, float size) { + this.width = width; + this.height = height; + this.generator = generator; + this.interfereCount = interfereCount; + // 字体高度设为验证码高度-2,留边距 + this.font = new Font(Font.SANS_SERIF, Font.PLAIN, (int) (this.height * size)); + } + @Override public void createCode() { generateCode(); @@ -201,7 +220,7 @@ public abstract class AbstractCaptcha implements ICaptcha { * @return 图片带文件格式的 Base64 * @since 5.3.11 */ - public String getImageBase64Data(){ + public String getImageBase64Data() { return URLUtil.getDataUriBase64("image/png", getImageBase64()); } diff --git a/hutool-captcha/src/main/java/cn/hutool/captcha/CaptchaUtil.java b/hutool-captcha/src/main/java/cn/hutool/captcha/CaptchaUtil.java old mode 100644 new mode 100755 index dcca26e3b629e792458cd4cd8a7739d59651033f..491e2fb62164d049b7eaecdbb67e98dfbf5bcaf9 --- a/hutool-captcha/src/main/java/cn/hutool/captcha/CaptchaUtil.java +++ b/hutool-captcha/src/main/java/cn/hutool/captcha/CaptchaUtil.java @@ -1,5 +1,7 @@ package cn.hutool.captcha; +import cn.hutool.captcha.generator.CodeGenerator; + /** * 图形验证码工具 * @@ -32,6 +34,34 @@ public class CaptchaUtil { return new LineCaptcha(width, height, codeCount, lineCount); } + /** + * 创建线干扰的验证码 + * + * @param width 图片宽 + * @param height 图片高 + * @param generator 验证码生成器 + * @param lineCount 干扰线条数 + * @return {@link LineCaptcha} + */ + public static LineCaptcha createLineCaptcha(int width, int height, CodeGenerator generator, int lineCount) { + return new LineCaptcha(width, height, generator, lineCount); + } + + /** + * 创建线干扰的验证码 + * + * @param width 图片宽 + * @param height 图片高 + * @param codeCount 字符个数 + * @param lineCount 干扰线条数 + * @param size 字体的大小 高度的倍数 + * @return {@link LineCaptcha} + */ + public static LineCaptcha createLineCaptcha(int width, int height, int codeCount, int lineCount, float size) { + return new LineCaptcha(width, height, codeCount, lineCount, size); + } + // ------------------------- lineCaptcha end ------------------------- + /** * 创建圆圈干扰的验证码,默认5位验证码,15个干扰圈 * @@ -58,6 +88,34 @@ public class CaptchaUtil { return new CircleCaptcha(width, height, codeCount, circleCount); } + /** + * 创建圆圈干扰的验证码 + * + * @param width 图片宽 + * @param height 图片高 + * @param generator 验证码生成器 + * @param circleCount 干扰圆圈条数 + * @return {@link CircleCaptcha} + */ + public static CircleCaptcha createCircleCaptcha(int width, int height, CodeGenerator generator, int circleCount) { + return new CircleCaptcha(width, height, generator, circleCount); + } + + /** + * 创建圆圈干扰的验证码 + * + * @param width 图片宽 + * @param height 图片高 + * @param codeCount 字符个数 + * @param circleCount 干扰圆圈条数 + * @param size 字体的大小 高度的倍数 + * @return {@link CircleCaptcha} + */ + public static CircleCaptcha createCircleCaptcha(int width, int height, int codeCount, int circleCount, float size) { + return new CircleCaptcha(width, height, codeCount, circleCount, size); + } + // ------------------------- circleCaptcha end ------------------------- + /** * 创建扭曲干扰的验证码,默认5位验证码 * @@ -84,10 +142,38 @@ public class CaptchaUtil { return new ShearCaptcha(width, height, codeCount, thickness); } + /** + * 创建扭曲干扰的验证码,默认5位验证码 + * + * @param width 图片宽 + * @param height 图片高 + * @param generator 验证码生成器 + * @param thickness 干扰线宽度 + * @return {@link ShearCaptcha} + */ + public static ShearCaptcha createShearCaptcha(int width, int height, CodeGenerator generator, int thickness) { + return new ShearCaptcha(width, height, generator, thickness); + } + + /** + * 创建扭曲干扰的验证码,默认5位验证码 + * + * @param width 图片宽 + * @param height 图片高 + * @param codeCount 字符个数 + * @param thickness 干扰线宽度 + * @param size 字体的大小 高度的倍数 + * @return {@link ShearCaptcha} + */ + public static ShearCaptcha createShearCaptcha(int width, int height, int codeCount, int thickness, float size) { + return new ShearCaptcha(width, height, codeCount, thickness, size); + } + // ------------------------- shearCaptcha end ------------------------- + /** * 创建GIF验证码 * - * @param width 宽 + * @param width 宽 * @param height 高 * @return {@link GifCaptcha} */ @@ -98,12 +184,41 @@ public class CaptchaUtil { /** * 创建GIF验证码 * - * @param width 宽 - * @param height 高 + * @param width 宽 + * @param height 高 * @param codeCount 字符个数 * @return {@link GifCaptcha} */ public static GifCaptcha createGifCaptcha(int width, int height, int codeCount) { return new GifCaptcha(width, height, codeCount); } + + /** + * 创建GIF验证码 + * + * @param width 宽 + * @param height 高 + * @param generator 验证码生成器 + * @param thickness 验证码干扰元素个数 + * @return {@link GifCaptcha} + */ + public static GifCaptcha createGifCaptcha(int width, int height, CodeGenerator generator, int thickness) { + return new GifCaptcha(width, height, generator, thickness); + } + + /** + * 创建圆圈干扰的验证码 + * + * @param width 图片宽 + * @param height 图片高 + * @param codeCount 字符个数 + * @param thickness 验证码干扰元素个数 + * @param size 字体的大小 高度的倍数 + * @return {@link GifCaptcha} + */ + public static GifCaptcha createGifCaptcha(int width, int height, int codeCount, int thickness, float size) { + return new GifCaptcha(width, height, codeCount, thickness, size); + } + // ------------------------- gifCaptcha end ------------------------- + } diff --git a/hutool-captcha/src/main/java/cn/hutool/captcha/CircleCaptcha.java b/hutool-captcha/src/main/java/cn/hutool/captcha/CircleCaptcha.java old mode 100644 new mode 100755 index 9e16a90b50b8aea64df146456abfad1a4c4d0420..f528cf359ecb030f597a621698f441d27518e1ed --- a/hutool-captcha/src/main/java/cn/hutool/captcha/CircleCaptcha.java +++ b/hutool-captcha/src/main/java/cn/hutool/captcha/CircleCaptcha.java @@ -1,13 +1,12 @@ package cn.hutool.captcha; +import cn.hutool.captcha.generator.CodeGenerator; +import cn.hutool.captcha.generator.RandomGenerator; import cn.hutool.core.img.GraphicsUtil; import cn.hutool.core.img.ImgUtil; -import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.RandomUtil; -import java.awt.Color; -import java.awt.Graphics2D; -import java.awt.Image; +import java.awt.*; import java.awt.image.BufferedImage; import java.util.concurrent.ThreadLocalRandom; @@ -24,7 +23,7 @@ public class CircleCaptcha extends AbstractCaptcha { /** * 构造 * - * @param width 图片宽 + * @param width 图片宽 * @param height 图片高 */ public CircleCaptcha(int width, int height) { @@ -34,8 +33,8 @@ public class CircleCaptcha extends AbstractCaptcha { /** * 构造 * - * @param width 图片宽 - * @param height 图片高 + * @param width 图片宽 + * @param height 图片高 * @param codeCount 字符个数 */ public CircleCaptcha(int width, int height, int codeCount) { @@ -45,25 +44,55 @@ public class CircleCaptcha extends AbstractCaptcha { /** * 构造 * - * @param width 图片宽 - * @param height 图片高 - * @param codeCount 字符个数 + * @param width 图片宽 + * @param height 图片高 + * @param codeCount 字符个数 * @param interfereCount 验证码干扰元素个数 */ public CircleCaptcha(int width, int height, int codeCount, int interfereCount) { - super(width, height, codeCount, interfereCount); + this(width, height, new RandomGenerator(codeCount), interfereCount); + } + + /** + * 构造 + * + * @param width 图片宽 + * @param height 图片高 + * @param generator 验证码生成器 + * @param interfereCount 验证码干扰元素个数 + */ + public CircleCaptcha(int width, int height, CodeGenerator generator, int interfereCount) { + super(width, height, generator, interfereCount); } + /** + * 构造 + * + * @param width 图片宽 + * @param height 图片高 + * @param codeCount 字符个数 + * @param interfereCount 验证码干扰元素个数 + * @param size 字体的大小 高度的倍数 + */ + public CircleCaptcha(int width, int height, int codeCount, int interfereCount, float size) { + super(width, height, new RandomGenerator(codeCount), interfereCount, size); + } + + @Override public Image createImage(String code) { - final BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); - final Graphics2D g = ImgUtil.createGraphics(image, ObjectUtil.defaultIfNull(this.background, Color.WHITE)); + final BufferedImage image = new BufferedImage(width, height, (null == this.background) ? BufferedImage.TYPE_4BYTE_ABGR : BufferedImage.TYPE_INT_RGB); + final Graphics2D g = ImgUtil.createGraphics(image, this.background); - // 随机画干扰圈圈 - drawInterfere(g); + try { + // 随机画干扰圈圈 + drawInterfere(g); - // 画字符串 - drawString(g, code); + // 画字符串 + drawString(g, code); + } finally { + g.dispose(); + } return image; } @@ -72,7 +101,7 @@ public class CircleCaptcha extends AbstractCaptcha { /** * 绘制字符串 * - * @param g {@link Graphics2D}画笔 + * @param g {@link Graphics2D}画笔 * @param code 验证码 */ private void drawString(Graphics2D g, String code) { diff --git a/hutool-captcha/src/main/java/cn/hutool/captcha/GifCaptcha.java b/hutool-captcha/src/main/java/cn/hutool/captcha/GifCaptcha.java old mode 100644 new mode 100755 index 2d996693c073b9690de718fe5392d060d8199ba3..d32c195d3d592dac11bd06b2f77beb994d4cf23d --- a/hutool-captcha/src/main/java/cn/hutool/captcha/GifCaptcha.java +++ b/hutool-captcha/src/main/java/cn/hutool/captcha/GifCaptcha.java @@ -1,8 +1,10 @@ package cn.hutool.captcha; +import cn.hutool.captcha.generator.CodeGenerator; +import cn.hutool.captcha.generator.RandomGenerator; +import cn.hutool.core.img.ImgUtil; import cn.hutool.core.img.gif.AnimatedGifEncoder; -import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.RandomUtil; import java.awt.AlphaComposite; @@ -47,7 +49,42 @@ public class GifCaptcha extends AbstractCaptcha { * @param codeCount 验证码个数 */ public GifCaptcha(int width, int height, int codeCount) { - super(width, height, codeCount, 10); + this(width, height, codeCount, 10); + } + + /** + * @param width 验证码宽度 + * @param height 验证码高度 + * @param codeCount 验证码个数 + * @param interfereCount 验证码干扰元素个数 + */ + public GifCaptcha(int width, int height, int codeCount, int interfereCount) { + this(width, height, new RandomGenerator(codeCount), interfereCount); + } + + /** + * 构造 + * + * @param width 图片宽 + * @param height 图片高 + * @param generator 验证码生成器 + * @param interfereCount 验证码干扰元素个数 + */ + public GifCaptcha(int width, int height, CodeGenerator generator, int interfereCount) { + super(width, height, generator, interfereCount); + } + + /** + * 构造 + * + * @param width 图片宽 + * @param height 图片高 + * @param codeCount 验证码个数 + * @param interfereCount 验证码干扰元素个数 + * @param size 字体的大小 高度的倍数 + */ + public GifCaptcha(int width, int height, int codeCount, int interfereCount, float size) { + super(width, height, new RandomGenerator(codeCount), interfereCount, size); } /** @@ -76,9 +113,7 @@ public class GifCaptcha extends AbstractCaptcha { * @return this */ public GifCaptcha setRepeat(int repeat) { - if (repeat >= 0) { - this.repeat = repeat; - } + this.repeat = Math.max(repeat, 0); return this; } @@ -144,35 +179,36 @@ public class GifCaptcha extends AbstractCaptcha { * @return BufferedImage */ private BufferedImage graphicsImage(char[] chars, Color[] fontColor, char[] words, int flag) { - BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + final BufferedImage image = new BufferedImage(width, height, (null == this.background) ? BufferedImage.TYPE_4BYTE_ABGR : BufferedImage.TYPE_INT_RGB); //或得图形上下文 - Graphics2D g2d = image.createGraphics(); - //利用指定颜色填充背景 - g2d.setColor(ObjectUtil.defaultIfNull(this.background, Color.WHITE)); - g2d.fillRect(0, 0, width, height); - AlphaComposite ac; - // 字符的y坐标 - float y = (height >> 1) + (font.getSize() >> 1); - float m = 1.0f * (width - (chars.length * font.getSize())) / chars.length; - //字符的x坐标 - float x = Math.max(m / 2.0f, 2); - g2d.setFont(font); - // 指定透明度 - if (null != this.textAlpha) { - g2d.setComposite(this.textAlpha); - } - for (int i = 0; i < chars.length; i++) { - ac = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, getAlpha(chars.length, flag, i)); - g2d.setComposite(ac); - g2d.setColor(fontColor[i]); - g2d.drawOval( - RandomUtil.randomInt(width), - RandomUtil.randomInt(height), - RandomUtil.randomInt(5, 30), 5 + RandomUtil.randomInt(5, 30) - );//绘制椭圆边框 - g2d.drawString(words[i] + "", x + (font.getSize() + m) * i, y); + final Graphics2D g2d = ImgUtil.createGraphics(image, this.background); + try { + //利用指定颜色填充背景 + AlphaComposite ac; + // 字符的y坐标 + float y = (height >> 1) + (font.getSize() >> 1); + float m = 1.0f * (width - (chars.length * font.getSize())) / chars.length; + //字符的x坐标 + float x = Math.max(m / 2.0f, 2); + g2d.setFont(font); + // 指定透明度 + if (null != this.textAlpha) { + g2d.setComposite(this.textAlpha); + } + for (int i = 0; i < chars.length; i++) { + ac = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, getAlpha(chars.length, flag, i)); + g2d.setComposite(ac); + g2d.setColor(fontColor[i]); + g2d.drawOval( + RandomUtil.randomInt(width), + RandomUtil.randomInt(height), + RandomUtil.randomInt(5, 30), 5 + RandomUtil.randomInt(5, 30) + );//绘制椭圆边框 + g2d.drawString(words[i] + "", x + (font.getSize() + m) * i, y); + } + } finally { + g2d.dispose(); } - g2d.dispose(); return image; } diff --git a/hutool-captcha/src/main/java/cn/hutool/captcha/ICaptcha.java b/hutool-captcha/src/main/java/cn/hutool/captcha/ICaptcha.java old mode 100644 new mode 100755 diff --git a/hutool-captcha/src/main/java/cn/hutool/captcha/LineCaptcha.java b/hutool-captcha/src/main/java/cn/hutool/captcha/LineCaptcha.java old mode 100644 new mode 100755 index 8bf3376354de5ab66efd4d39ddca3c50924f007c..a4d556da422f17e99c6f158e3c41487c9ca3ed7f --- a/hutool-captcha/src/main/java/cn/hutool/captcha/LineCaptcha.java +++ b/hutool-captcha/src/main/java/cn/hutool/captcha/LineCaptcha.java @@ -1,17 +1,15 @@ package cn.hutool.captcha; -import java.awt.Color; -import java.awt.Graphics; -import java.awt.Graphics2D; -import java.awt.Image; -import java.awt.image.BufferedImage; -import java.util.concurrent.ThreadLocalRandom; - +import cn.hutool.captcha.generator.CodeGenerator; +import cn.hutool.captcha.generator.RandomGenerator; import cn.hutool.core.img.GraphicsUtil; import cn.hutool.core.img.ImgUtil; -import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.RandomUtil; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.util.concurrent.ThreadLocalRandom; + /** * 使用干扰线方式生成的图形验证码 * @@ -25,7 +23,7 @@ public class LineCaptcha extends AbstractCaptcha { /** * 构造,默认5位验证码,150条干扰线 * - * @param width 图片宽 + * @param width 图片宽 * @param height 图片高 */ public LineCaptcha(int width, int height) { @@ -35,27 +33,58 @@ public class LineCaptcha extends AbstractCaptcha { /** * 构造 * - * @param width 图片宽 - * @param height 图片高 + * @param width 图片宽 + * @param height 图片高 * @param codeCount 字符个数 * @param lineCount 干扰线条数 */ public LineCaptcha(int width, int height, int codeCount, int lineCount) { - super(width, height, codeCount, lineCount); + this(width, height, new RandomGenerator(codeCount), lineCount); } + + /** + * 构造 + * + * @param width 图片宽 + * @param height 图片高 + * @param generator 验证码生成器 + * @param interfereCount 验证码干扰元素个数 + */ + public LineCaptcha(int width, int height, CodeGenerator generator, int interfereCount) { + super(width, height, generator, interfereCount); + } + + /** + * 构造 + * + * @param width 图片宽 + * @param height 图片高 + * @param codeCount 字符个数 + * @param interfereCount 验证码干扰元素个数 + * @param size 字体的大小 高度的倍数 + */ + public LineCaptcha(int width, int height, int codeCount, int interfereCount, float size) { + super(width, height, new RandomGenerator(codeCount), interfereCount, size); + } + + // -------------------------------------------------------------------- Constructor end @Override public Image createImage(String code) { // 图像buffer - final BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); - final Graphics2D g = GraphicsUtil.createGraphics(image, ObjectUtil.defaultIfNull(this.background, Color.WHITE)); + final BufferedImage image = new BufferedImage(width, height, (null == this.background) ? BufferedImage.TYPE_4BYTE_ABGR : BufferedImage.TYPE_INT_RGB); + final Graphics2D g = ImgUtil.createGraphics(image, this.background); - // 干扰线 - drawInterfere(g); + try { + // 干扰线 + drawInterfere(g); - // 字符串 - drawString(g, code); + // 字符串 + drawString(g, code); + } finally { + g.dispose(); + } return image; } @@ -64,7 +93,7 @@ public class LineCaptcha extends AbstractCaptcha { /** * 绘制字符串 * - * @param g {@link Graphics}画笔 + * @param g {@link Graphics}画笔 * @param code 验证码 */ private void drawString(Graphics2D g, String code) { @@ -93,4 +122,4 @@ public class LineCaptcha extends AbstractCaptcha { } } // ----------------------------------------------------------------------------------------------------- Private method start -} \ No newline at end of file +} diff --git a/hutool-captcha/src/main/java/cn/hutool/captcha/ShearCaptcha.java b/hutool-captcha/src/main/java/cn/hutool/captcha/ShearCaptcha.java old mode 100644 new mode 100755 index 0f43dcc1dc1d0138d9e5d3c8fef7905cfffacf57..f0c2a148245c6857e841bf396c86ea532fefbb35 --- a/hutool-captcha/src/main/java/cn/hutool/captcha/ShearCaptcha.java +++ b/hutool-captcha/src/main/java/cn/hutool/captcha/ShearCaptcha.java @@ -1,5 +1,7 @@ package cn.hutool.captcha; +import cn.hutool.captcha.generator.CodeGenerator; +import cn.hutool.captcha.generator.RandomGenerator; import cn.hutool.core.img.GraphicsUtil; import cn.hutool.core.img.ImgUtil; import cn.hutool.core.util.ObjectUtil; @@ -51,21 +53,51 @@ public class ShearCaptcha extends AbstractCaptcha { * @param thickness 干扰线宽度 */ public ShearCaptcha(int width, int height, int codeCount, int thickness) { - super(width, height, codeCount, thickness); + this(width, height, new RandomGenerator(codeCount), thickness); + } + + /** + * 构造 + * + * @param width 图片宽 + * @param height 图片高 + * @param generator 验证码生成器 + * @param interfereCount 验证码干扰元素个数 + */ + public ShearCaptcha(int width, int height, CodeGenerator generator, int interfereCount) { + super(width, height, generator, interfereCount); } - @Override - public Image createImage(String code) { - final BufferedImage image = new BufferedImage(this.width, this.height, BufferedImage.TYPE_INT_RGB); - final Graphics2D g = GraphicsUtil.createGraphics(image, ObjectUtil.defaultIfNull(this.background, Color.WHITE)); - // 画字符串 - drawString(g, code); + /** + * 构造 + * + * @param width 图片宽 + * @param height 图片高 + * @param codeCount 字符个数 + * @param interfereCount 验证码干扰元素个数 + * @param size 字体的大小 高度的倍数 + */ + public ShearCaptcha(int width, int height, int codeCount, int interfereCount, float size) { + super(width, height, new RandomGenerator(codeCount), interfereCount, size); + } - // 扭曲 - shear(g, this.width, this.height, ObjectUtil.defaultIfNull(this.background, Color.WHITE)); - // 画干扰线 - drawInterfere(g, 0, RandomUtil.randomInt(this.height) + 1, this.width, RandomUtil.randomInt(this.height) + 1, this.interfereCount, ImgUtil.randomColor()); + @Override + public Image createImage(String code) { + final BufferedImage image = new BufferedImage(width, height, (null == this.background) ? BufferedImage.TYPE_4BYTE_ABGR : BufferedImage.TYPE_INT_RGB); + final Graphics2D g = ImgUtil.createGraphics(image, this.background); + + try{ + // 画字符串 + drawString(g, code); + + // 扭曲 + shear(g, this.width, this.height, ObjectUtil.defaultIfNull(this.background, Color.WHITE)); + // 画干扰线 + drawInterfere(g, 0, RandomUtil.randomInt(this.height) + 1, this.width, RandomUtil.randomInt(this.height) + 1, this.interfereCount, ImgUtil.randomColor()); + } finally { + g.dispose(); + } return image; } diff --git a/hutool-captcha/src/main/java/cn/hutool/captcha/generator/AbstractGenerator.java b/hutool-captcha/src/main/java/cn/hutool/captcha/generator/AbstractGenerator.java old mode 100644 new mode 100755 diff --git a/hutool-captcha/src/main/java/cn/hutool/captcha/generator/CodeGenerator.java b/hutool-captcha/src/main/java/cn/hutool/captcha/generator/CodeGenerator.java old mode 100644 new mode 100755 diff --git a/hutool-captcha/src/main/java/cn/hutool/captcha/generator/MathGenerator.java b/hutool-captcha/src/main/java/cn/hutool/captcha/generator/MathGenerator.java old mode 100644 new mode 100755 index 7395b3ee57114f2f2b941bb0b931452918d2bbb7..af49557e003e49338ed0501a554da33ce0099cdc --- a/hutool-captcha/src/main/java/cn/hutool/captcha/generator/MathGenerator.java +++ b/hutool-captcha/src/main/java/cn/hutool/captcha/generator/MathGenerator.java @@ -16,14 +16,30 @@ public class MathGenerator implements CodeGenerator { private static final String operators = "+-*"; - /** 参与计算数字最大长度 */ + /** + * 参与计算数字最大长度 + */ private final int numberLength; + /** + * 计算结果是否允许负数 + */ + private final boolean resultHasNegativeNumber; + /** * 构造 */ public MathGenerator() { - this(2); + this(2, true); + } + + /** + * 构造 + * + * @param resultHasNegativeNumber 结果是否允许负数 + */ + public MathGenerator(boolean resultHasNegativeNumber) { + this(2, resultHasNegativeNumber); } /** @@ -32,22 +48,43 @@ public class MathGenerator implements CodeGenerator { * @param numberLength 参与计算最大数字位数 */ public MathGenerator(int numberLength) { + this(numberLength, true); + } + + /** + * 构造 + * + * @param numberLength 参与计算最大数字位数 + * @param resultHasNegativeNumber 结果是否允许负数 + */ + public MathGenerator(int numberLength, boolean resultHasNegativeNumber) { this.numberLength = numberLength; + this.resultHasNegativeNumber = resultHasNegativeNumber; } @Override public String generate() { final int limit = getLimit(); - String number1 = Integer.toString(RandomUtil.randomInt(limit)); - String number2 = Integer.toString(RandomUtil.randomInt(limit)); + char operator = RandomUtil.randomChar(operators); + int numberInt1 = 0; + int numberInt2 = 0; + numberInt1 = RandomUtil.randomInt(limit); + // 如果禁止了结果有负数,且计算方式正好计算为减法,需要第二个数小于第一个数 + if (!resultHasNegativeNumber && CharUtil.equals('-', operator, false)) { + //如果第一个数为0,第二个数必须为0,随机[0,0)的数字会报错 + numberInt2 = numberInt1 == 0 ? 0 : RandomUtil.randomInt(0, numberInt1); + } else { + numberInt2 = RandomUtil.randomInt(limit); + } + String number1 = Integer.toString(numberInt1); + String number2 = Integer.toString(numberInt2); number1 = StrUtil.padAfter(number1, this.numberLength, CharUtil.SPACE); number2 = StrUtil.padAfter(number2, this.numberLength, CharUtil.SPACE); - return StrUtil.builder()// - .append(number1)// - .append(RandomUtil.randomChar(operators))// - .append(number2)// - .append('=').toString(); + .append(number1)// + .append(operator)// + .append(number2)// + .append('=').toString(); } @Override diff --git a/hutool-captcha/src/main/java/cn/hutool/captcha/generator/RandomGenerator.java b/hutool-captcha/src/main/java/cn/hutool/captcha/generator/RandomGenerator.java old mode 100644 new mode 100755 diff --git a/hutool-captcha/src/main/java/cn/hutool/captcha/generator/package-info.java b/hutool-captcha/src/main/java/cn/hutool/captcha/generator/package-info.java old mode 100644 new mode 100755 diff --git a/hutool-captcha/src/main/java/cn/hutool/captcha/package-info.java b/hutool-captcha/src/main/java/cn/hutool/captcha/package-info.java old mode 100644 new mode 100755 diff --git a/hutool-captcha/src/test/java/cn/hutool/captcha/CaptchaTest.java b/hutool-captcha/src/test/java/cn/hutool/captcha/CaptchaTest.java old mode 100644 new mode 100755 index 965cbc1e72cc615c59739a9fdf8e5db06b324245..a6b5f8ecf1f136f56d7ddafccbed734b4cec8886 --- a/hutool-captcha/src/test/java/cn/hutool/captcha/CaptchaTest.java +++ b/hutool-captcha/src/test/java/cn/hutool/captcha/CaptchaTest.java @@ -2,9 +2,9 @@ package cn.hutool.captcha; import cn.hutool.captcha.generator.MathGenerator; import cn.hutool.core.lang.Console; -import org.junit.Assert; -import org.junit.Ignore; -import org.junit.Test; +import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; import java.awt.*; @@ -19,21 +19,30 @@ public class CaptchaTest { public void lineCaptchaTest1() { // 定义图形验证码的长和宽 LineCaptcha lineCaptcha = CaptchaUtil.createLineCaptcha(200, 100); - Assert.assertNotNull(lineCaptcha.getCode()); - Assert.assertTrue(lineCaptcha.verify(lineCaptcha.getCode())); + assertNotNull(lineCaptcha.getCode()); + assertTrue(lineCaptcha.verify(lineCaptcha.getCode())); } @Test - @Ignore + @Disabled public void lineCaptchaTest3() { // 定义图形验证码的长和宽 LineCaptcha lineCaptcha = CaptchaUtil.createLineCaptcha(200, 70, 4, 15); + lineCaptcha.setBackground(null); + lineCaptcha.write("d:/test/captcha/tellow.png"); + } + + @Test + @Disabled + public void lineCaptchaTestWithSize() { + // 定义图形验证码的长和宽 + LineCaptcha lineCaptcha = CaptchaUtil.createLineCaptcha(200, 70, 4, 15, 0.65f); lineCaptcha.setBackground(Color.yellow); lineCaptcha.write("f:/test/captcha/tellow.png"); } @Test - @Ignore + @Disabled public void lineCaptchaWithMathTest() { // 定义图形验证码的长和宽 LineCaptcha lineCaptcha = CaptchaUtil.createLineCaptcha(200, 80); @@ -43,7 +52,7 @@ public class CaptchaTest { } @Test - @Ignore + @Disabled public void lineCaptchaTest2() { // 定义图形验证码的长和宽 @@ -63,7 +72,7 @@ public class CaptchaTest { } @Test - @Ignore + @Disabled public void circleCaptchaTest() { // 定义图形验证码的长和宽 @@ -75,12 +84,22 @@ public class CaptchaTest { captcha.verify("1234"); } + + @Test + @Disabled + public void circleCaptchaTestWithSize() { + // 定义图形验证码的长和宽 + CircleCaptcha captcha = CaptchaUtil.createCircleCaptcha(200, 70, 4, 15, 0.65f); + captcha.setBackground(Color.yellow); + captcha.write("f:/test/captcha/circle.png"); + } + @Test - @Ignore + @Disabled public void shearCaptchaTest() { // 定义图形验证码的长和宽 - ShearCaptcha captcha = CaptchaUtil.createShearCaptcha(203, 100, 4, 4); + ShearCaptcha captcha = CaptchaUtil.createShearCaptcha(200, 100, 4, 4); // ShearCaptcha captcha = new ShearCaptcha(200, 100, 4, 4); // 图形验证码写出,可以写出到文件,也可以写出到流 captcha.write("f:/captcha/shear.png"); @@ -89,7 +108,7 @@ public class CaptchaTest { } @Test - @Ignore + @Disabled public void shearCaptchaTest2() { // 定义图形验证码的长和宽 @@ -101,7 +120,7 @@ public class CaptchaTest { } @Test - @Ignore + @Disabled public void ShearCaptchaWithMathTest() { // 定义图形验证码的长和宽 ShearCaptcha captcha = CaptchaUtil.createShearCaptcha(200, 45, 4, 4); @@ -113,8 +132,18 @@ public class CaptchaTest { captcha.verify("1234"); } + + @Test + @Disabled + public void ShearCaptchaTestWithSize() { + // 定义图形验证码的长和宽 + ShearCaptcha captcha = CaptchaUtil.createShearCaptcha(200, 70, 4, 15, 0.65f); + captcha.setBackground(Color.yellow); + captcha.write("f:/test/captcha/shear.png"); + } + @Test - @Ignore + @Disabled public void GifCaptchaTest() { GifCaptcha captcha = CaptchaUtil.createGifCaptcha(200, 100, 4); captcha.write("d:/test/gif_captcha.gif"); @@ -122,8 +151,17 @@ public class CaptchaTest { } @Test - @Ignore - public void bgTest(){ + @Disabled + public void GifCaptchaTestWithSize() { + // 定义图形验证码的长和宽 + GifCaptcha captcha = CaptchaUtil.createGifCaptcha(200, 70, 4, 15, 0.65f); + captcha.setBackground(Color.yellow); + captcha.write("f:/test/captcha/gif.png"); + } + + @Test + @Disabled + public void bgTest() { LineCaptcha captcha = CaptchaUtil.createLineCaptcha(200, 100, 4, 1); captcha.setBackground(Color.WHITE); captcha.write("d:/test/test.jpg"); diff --git a/hutool-captcha/src/test/java/cn/hutool/captcha/CaptchaUtilTest.java b/hutool-captcha/src/test/java/cn/hutool/captcha/CaptchaUtilTest.java old mode 100644 new mode 100755 index d57cc393de54ae5bc1598bc6b06757fa27e26b9d..7fc66cf0d304617f3a7a985510b7e4d01857cf11 --- a/hutool-captcha/src/test/java/cn/hutool/captcha/CaptchaUtilTest.java +++ b/hutool-captcha/src/test/java/cn/hutool/captcha/CaptchaUtilTest.java @@ -1,15 +1,120 @@ package cn.hutool.captcha; -import org.junit.Ignore; -import org.junit.Test; +import cn.hutool.core.img.GraphicsUtil; +import cn.hutool.core.img.ImgUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.RandomUtil; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.awt.Color; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Image; +import java.awt.image.BufferedImage; +import java.util.concurrent.ThreadLocalRandom; public class CaptchaUtilTest { @Test - @Ignore + @Disabled public void createTest() { for(int i = 0; i < 1; i++) { CaptchaUtil.createShearCaptcha(320, 240); } } + + @Test + @Disabled + public void drawStringColourfulColorDistanceTest() { + for(int i = 0; i < 10; i++) { + AbstractCaptcha lineCaptcha = new TestLineCaptchaColorDistance(200, 100, 5, 10); + lineCaptcha.write("d:/captcha/line1-"+i+".png"); + } + } + + @Test + @Disabled + public void drawStringColourfulDefaultColorDistanceTest() { + for(int i = 0; i < 10; i++) { + AbstractCaptcha lineCaptcha = new TestLineCaptchaColorDistanceDefaultColorDistance(200, 100, 5, 10); + lineCaptcha.write("d:/captcha/line2-"+i+".png"); + } + } + + static class TestLineCaptchaColorDistance extends AbstractCaptcha{ + private static final long serialVersionUID = -558846929114465692L; + + public TestLineCaptchaColorDistance(int width, int height, int codeCount, int interfereCount) { + super(width, height, codeCount, interfereCount); + } + + @Override + protected Image createImage(String code) { + // 图像buffer + final BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + final Graphics2D g = GraphicsUtil.createGraphics(image, ObjectUtil.defaultIfNull(this.background, Color.WHITE)); + + // 干扰线 + drawInterfere(g); + + // 字符串 + drawString(g, code); + + return image; + } + + // ----------------------------------------------------------------------------------------------------- Private method start + /** + * 绘制字符串 + * + * @param g {@link Graphics}画笔 + * @param code 验证码 + */ + protected void drawString(Graphics2D g, String code) { + // 指定透明度 + if (null != this.textAlpha) { + g.setComposite(this.textAlpha); + } + // 自定义与背景颜色的色差值,200是基于Color.WHITE较为居中的值 + GraphicsUtil.drawStringColourful(g, code, this.font, this.width, this.height,Color.WHITE,200); + } + + /** + * 绘制干扰线 + * + * @param g {@link Graphics2D}画笔 + */ + private void drawInterfere(Graphics2D g) { + final ThreadLocalRandom random = RandomUtil.getRandom(); + // 干扰线 + for (int i = 0; i < this.interfereCount; i++) { + int xs = random.nextInt(width); + int ys = random.nextInt(height); + int xe = xs + random.nextInt(width / 3); + int ye = ys + random.nextInt(height / 3); + g.setColor(ImgUtil.randomColor(random)); + g.drawLine(xs, ys, xe, ye); + } + } + // ----------------------------------------------------------------------------------------------------- Private method start + } + + static class TestLineCaptchaColorDistanceDefaultColorDistance extends TestLineCaptchaColorDistance { + + + public TestLineCaptchaColorDistanceDefaultColorDistance(int width, int height, int codeCount, int interfereCount) { + super(width, height, codeCount, interfereCount); + } + + @Override + protected void drawString(Graphics2D g, String code) { + // 指定透明度 + if (null != this.textAlpha) { + g.setComposite(this.textAlpha); + } + // 使用默认色差设置 + GraphicsUtil.drawStringColourful(g, code, this.font, this.width, this.height,Color.WHITE); + } + } } diff --git a/hutool-captcha/src/test/java/cn/hutool/captcha/GeneratorTest.java b/hutool-captcha/src/test/java/cn/hutool/captcha/GeneratorTest.java old mode 100644 new mode 100755 index 999be5c5229229477ac5599be125f0874fb66b4f..f30ae93c00dcf21299812a94c4b0ac43d31b65ac --- a/hutool-captcha/src/test/java/cn/hutool/captcha/GeneratorTest.java +++ b/hutool-captcha/src/test/java/cn/hutool/captcha/GeneratorTest.java @@ -1,7 +1,8 @@ package cn.hutool.captcha; import cn.hutool.captcha.generator.MathGenerator; -import org.junit.Test; +import cn.hutool.core.math.Calculator; +import org.junit.jupiter.api.Test; public class GeneratorTest { @@ -11,5 +12,13 @@ public class GeneratorTest { for (int i = 0; i < 1000; i++) { mathGenerator.verify(mathGenerator.generate(), "0"); } + + final MathGenerator mathGenerator1 = new MathGenerator(false); + for (int i = 0; i < 1000; i++) { + String generate = mathGenerator1.generate(); + if( Calculator.conversion(generate) < 0){ + throw new RuntimeException("No Pass"); + } + } } } diff --git a/hutool-captcha/src/test/java/cn/hutool/captcha/GifCaptchaUtilTest.java b/hutool-captcha/src/test/java/cn/hutool/captcha/GifCaptchaUtilTest.java new file mode 100644 index 0000000000000000000000000000000000000000..2125aa64a39b199dca74f58a163e34e209b42e1f --- /dev/null +++ b/hutool-captcha/src/test/java/cn/hutool/captcha/GifCaptchaUtilTest.java @@ -0,0 +1,116 @@ +package cn.hutool.captcha; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +import static org.junit.jupiter.api.Assertions.*; + +public class GifCaptchaUtilTest { + + private GifCaptcha captcha; + + @BeforeEach + public void setUp() { + // 初始化 GifCaptcha 类的实例 + captcha = new GifCaptcha(200, 100, 4, 10); // width, height, codeCount, interfereCount + } + + // 使用反射调用私有方法 + private Object invokePrivateMethod(String methodName, Class[] parameterTypes, Object[] parameters) throws Exception { + Method method = GifCaptcha.class.getDeclaredMethod(methodName, parameterTypes); + method.setAccessible(true); // 允许访问私有方法 + return method.invoke(captcha, parameters); + } + + // 测试 setQuality() 方法 + @Test + public void testSetQuality() throws Exception { + captcha.setQuality(20); + // 通过反射获取 quality 字段的值并进行断言 + assertEquals(20, getPrivateField("quality"), "Quality 应该设置为 20"); + + captcha.setQuality(0); // 设置无效值,应该被设置为 1 + assertEquals(1, getPrivateField("quality"), "Quality 应该设置为 1,如果小于 1"); + } + + // 测试 setRepeat() 方法 + @Test + public void testSetRepeat() throws Exception { + captcha.setRepeat(5); + // 通过反射获取 repeat 字段的值并进行断言 + assertEquals(5, getPrivateField("repeat"), "Repeat 应该设置为 5"); + + captcha.setRepeat(-1); // 设置无效值,应该保持为 0 + assertEquals(0, getPrivateField("repeat"), "Repeat 应该设置为 0,如果设置了负值"); + } + + // 测试 setColorRange() 方法 + @Test + public void testSetColorRange() throws Exception { + captcha.setMinColor(100).setMaxColor(200); + // 通过反射获取 minColor 和 maxColor 字段的值并进行断言 + assertEquals(100, getPrivateField("minColor"), "Min color 应该设置为 100"); + assertEquals(200, getPrivateField("maxColor"), "Max color 应该设置为 200"); + } + + // 测试生成验证码图像的方法 createCode() + @Test + public void testCreateCode() throws Exception { + captcha.createCode(); + byte[] imageBytes = captcha.getImageBytes(); + + // 检查生成的图片字节是否不为 null 或空 + assertNotNull(imageBytes, "生成的图片字节不应该为 null"); + assertTrue(imageBytes.length > 0, "生成的图片字节不应该为空"); + + // 可选:你也可以通过解码图片字节,检查它是否是有效的 GIF 格式 + ByteArrayOutputStream out = new ByteArrayOutputStream(); + out.write(imageBytes); + + // 解码图片检查它是否为有效的 GIF(假设你有库可以解码 GIF) + // ImageIO.read(new ByteArrayInputStream(imageBytes)); // 可以取消注释来检查它是否是有效的 GIF + } + + // 测试 graphicsImage() 方法 + @Test + public void testGraphicsImage() throws Exception { + char[] chars = new char[]{'A', 'B', 'C', 'D'}; + Color[] colors = new Color[]{ + Color.RED, Color.GREEN, Color.BLUE, Color.YELLOW + }; + + // 使用反射调用 private 方法 graphicsImage + Object result = invokePrivateMethod("graphicsImage", new Class[]{char[].class, Color[].class, char[].class, int.class}, new Object[]{chars, colors, chars, 0}); + + assertNotNull(result, "生成的图片不应该为 null"); + assertInstanceOf(BufferedImage.class, result, "返回的结果应该是 BufferedImage 类型"); + } + + // 测试 getRandomColor() 方法 + @Test + public void testRandomColor() throws Exception { + // 使用反射调用 private 方法 getRandomColor + Object result = invokePrivateMethod("getRandomColor", new Class[]{int.class, int.class}, new Object[]{0, 255}); + + assertNotNull(result, "生成的颜色不应该为 null"); + assertInstanceOf(Color.class, result, "返回的结果应该是 Color 类型"); + + Color color = (Color) result; + assertTrue(color.getRed() >= 0 && color.getRed() <= 255, "颜色的红色分量应该在 0 到 255 之间"); + assertTrue(color.getGreen() >= 0 && color.getGreen() <= 255, "颜色的绿色分量应该在 0 到 255 之间"); + assertTrue(color.getBlue() >= 0 && color.getBlue() <= 255, "颜色的蓝色分量应该在 0 到 255 之间"); + } + + // 辅助方法:通过反射获取私有字段的值 + private Object getPrivateField(String fieldName) throws NoSuchFieldException, IllegalAccessException { + Field field = GifCaptcha.class.getDeclaredField(fieldName); + field.setAccessible(true); // 允许访问私有字段 + return field.get(captcha); + } +} diff --git a/hutool-captcha/src/test/java/cn/hutool/captcha/ShearCaptchaTest.java b/hutool-captcha/src/test/java/cn/hutool/captcha/ShearCaptchaTest.java new file mode 100644 index 0000000000000000000000000000000000000000..b75b46eb15cbc33fb07638c3f434cb31530fb69b --- /dev/null +++ b/hutool-captcha/src/test/java/cn/hutool/captcha/ShearCaptchaTest.java @@ -0,0 +1,145 @@ +package cn.hutool.captcha; + +import cn.hutool.captcha.generator.RandomGenerator; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.awt.*; +import java.awt.image.BufferedImage; +import java.lang.reflect.Method; + +import static org.junit.jupiter.api.Assertions.*; + +public class ShearCaptchaTest { + + private ShearCaptcha captcha; + + @BeforeEach + public void setUp() { + // 初始化 ShearCaptcha 实例 + captcha = new ShearCaptcha(200, 100); + } + + // 测试构造函数和基本功能 + @Test + public void testConstructor() { + assertNotNull(captcha, "Captcha 实例应该被成功创建"); + } + + // 测试生成验证码图片的功能 + @Test + public void testCreateImage() { + String code = "ABCD"; + Image image = captcha.createImage(code); + assertNotNull(image, "验证码图片不应该为 null"); + assertInstanceOf(BufferedImage.class, image, "生成的图片应该是 BufferedImage 类型"); + + // 可选:进一步测试图像的内容 + BufferedImage bufferedImage = (BufferedImage) image; + assertEquals(200, bufferedImage.getWidth(), "图像宽度应该为 200"); + assertEquals(100, bufferedImage.getHeight(), "图像高度应该为 100"); + } + + // 测试绘制字符串的方法 + @Test + public void testDrawString() throws Exception { + String code = "ABCD"; + Method drawStringMethod = ShearCaptcha.class.getDeclaredMethod("drawString", Graphics2D.class, String.class); + drawStringMethod.setAccessible(true); + + Graphics2D g2d = (Graphics2D) new BufferedImage(200, 100, BufferedImage.TYPE_INT_ARGB).getGraphics(); + drawStringMethod.invoke(captcha, g2d, code); + + assertNotNull(g2d, "Graphics2D 对象不应该为 null"); + assertTrue(g2d.getRenderingHints().containsKey(RenderingHints.KEY_ANTIALIASING), "应该启用抗锯齿"); + } + + // 测试 shear() 方法 + @Test + public void testShear() throws Exception { + // 使用反射测试 shear 方法 + Method shearMethod = ShearCaptcha.class.getDeclaredMethod("shear", Graphics.class, int.class, int.class, Color.class); + shearMethod.setAccessible(true); + + Graphics g = new BufferedImage(200, 100, BufferedImage.TYPE_INT_ARGB).getGraphics(); + shearMethod.invoke(captcha, g, 200, 100, Color.WHITE); + + // 假设没有明显的错误输出,认为测试通过 + assertNotNull(g, "Graphics 对象不应该为 null"); + } + + // 测试 shearX() 方法 + @Test + public void testShearX() throws Exception { + // 使用反射测试 shearX 方法 + Method shearXMethod = ShearCaptcha.class.getDeclaredMethod("shearX", Graphics.class, int.class, int.class, Color.class); + shearXMethod.setAccessible(true); + + Graphics g = new BufferedImage(200, 100, BufferedImage.TYPE_INT_ARGB).getGraphics(); + shearXMethod.invoke(captcha, g, 200, 100, Color.RED); + + // 假设没有明显的错误输出,认为测试通过 + assertNotNull(g, "Graphics 对象不应该为 null"); + } + + // 测试 shearY() 方法 + @Test + public void testShearY() throws Exception { + // 使用反射测试 shearY 方法 + Method shearYMethod = ShearCaptcha.class.getDeclaredMethod("shearY", Graphics.class, int.class, int.class, Color.class); + shearYMethod.setAccessible(true); + + Graphics g = new BufferedImage(200, 100, BufferedImage.TYPE_INT_ARGB).getGraphics(); + shearYMethod.invoke(captcha, g, 200, 100, Color.BLUE); + + // 假设没有明显的错误输出,认为测试通过 + assertNotNull(g, "Graphics 对象不应该为 null"); + } + + // 测试 drawInterfere() 方法 + @Test + public void testDrawInterfere() throws Exception { + // 使用反射测试 drawInterfere 方法 + Method drawInterfereMethod = ShearCaptcha.class.getDeclaredMethod("drawInterfere", Graphics.class, int.class, int.class, int.class, int.class, int.class, Color.class); + drawInterfereMethod.setAccessible(true); + + Graphics g = new BufferedImage(200, 100, BufferedImage.TYPE_INT_ARGB).getGraphics(); + drawInterfereMethod.invoke(captcha, g, 0, 0, 200, 100, 4, Color.GREEN); + + // 假设没有明显的错误输出,认为测试通过 + assertNotNull(g, "Graphics 对象不应该为 null"); + } + + // 测试验证码生成时的干扰线 + @Test + public void testDrawInterfereLines() { + // 设置干扰线数量 + captcha = new ShearCaptcha(200, 100, 4); + Image image = captcha.createImage("ABCD"); + + // 检查图像内容,判断干扰线是否正确绘制 + assertNotNull(image, "生成的验证码图片不应该为空"); + } + + // 测试验证码的尺寸 + @Test + public void testCaptchaSize() { + captcha = new ShearCaptcha(300, 150); + + String code = "XYZ"; + Image image = captcha.createImage(code); + + BufferedImage bufferedImage = (BufferedImage) image; + assertEquals(300, bufferedImage.getWidth(), "图像宽度应该为 300"); + assertEquals(150, bufferedImage.getHeight(), "图像高度应该为 150"); + } + + // 测试生成随机验证码字符 + @Test + public void testRandomGenerator() { + RandomGenerator randomGenerator = new RandomGenerator(4); + String code = randomGenerator.generate(); + assertNotNull(code, "生成的验证码字符不应该为 null"); + assertEquals(4, code.length(), "验证码字符长度应该为 4"); + } +} diff --git a/hutool-core/pom.xml b/hutool-core/pom.xml index bc6a64195dc4c0312040d629760f3d55c37862a8..df7747659ad2ccac835973eb24af1041a81c6bd3 100755 --- a/hutool-core/pom.xml +++ b/hutool-core/pom.xml @@ -9,11 +9,14 @@ cn.hutool hutool-parent - 5.8.0.M5 + 5.8.41 hutool-core ${project.artifactId} Hutool核心,包括集合、字符串、Bean等工具 + + cn.hutool.core + diff --git a/hutool-core/src/main/java/cn/hutool/core/annotation/AbstractAnnotationSynthesizer.java b/hutool-core/src/main/java/cn/hutool/core/annotation/AbstractAnnotationSynthesizer.java new file mode 100644 index 0000000000000000000000000000000000000000..7ffdb0fb01541a1e26a6b73dd55797ada659890e --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/annotation/AbstractAnnotationSynthesizer.java @@ -0,0 +1,176 @@ +package cn.hutool.core.annotation; + +import cn.hutool.core.annotation.scanner.AnnotationScanner; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; + +import java.lang.annotation.Annotation; +import java.util.Collection; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; + +/** + * {@link AnnotationSynthesizer}的基本实现 + * + * @author huangchengxing + */ +public abstract class AbstractAnnotationSynthesizer implements AnnotationSynthesizer { + + /** + * 合成注解来源最初来源 + */ + protected final T source; + + /** + * 包含根注解以及其元注解在内的全部注解实例 + */ + protected final Map, SynthesizedAnnotation> synthesizedAnnotationMap; + + /** + * 已经合成过的注解对象 + */ + private final Map, Annotation> synthesizedProxyAnnotations; + + /** + * 合成注解选择器 + */ + protected final SynthesizedAnnotationSelector annotationSelector; + + /** + * 合成注解属性处理器 + */ + protected final Collection postProcessors; + + /** + * 注解扫描器 + */ + protected final AnnotationScanner annotationScanner; + + /** + * 构造一个注解合成器 + * + * @param source 当前查找的注解对象 + * @param annotationSelector 合成注解选择器 + * @param annotationPostProcessors 注解后置处理器 + * @param annotationScanner 注解扫描器,该扫描器需要支持扫描注解类 + */ + protected AbstractAnnotationSynthesizer( + T source, + SynthesizedAnnotationSelector annotationSelector, + Collection annotationPostProcessors, + AnnotationScanner annotationScanner) { + Assert.notNull(source, "source must not null"); + Assert.notNull(annotationSelector, "annotationSelector must not null"); + Assert.notNull(annotationPostProcessors, "annotationPostProcessors must not null"); + Assert.notNull(annotationPostProcessors, "annotationScanner must not null"); + + this.source = source; + this.annotationSelector = annotationSelector; + this.annotationScanner = annotationScanner; + this.postProcessors = CollUtil.unmodifiable( + CollUtil.sort(annotationPostProcessors, Comparator.comparing(SynthesizedAnnotationPostProcessor::order)) + ); + this.synthesizedProxyAnnotations = new LinkedHashMap<>(); + this.synthesizedAnnotationMap = MapUtil.unmodifiable(loadAnnotations()); + annotationPostProcessors.forEach(processor -> + synthesizedAnnotationMap.values().forEach(synthesized -> processor.process(synthesized, this)) + ); + } + + /** + * 加载合成注解的必要属性 + * + * @return 合成注解 + */ + protected abstract Map, SynthesizedAnnotation> loadAnnotations(); + + /** + * 根据指定的注解类型和对应注解对象,合成最终所需的合成注解 + * + * @param annotationType 注解类型 + * @param annotation 合成注解对象 + * @param 注解类型 + * @return 最终所需的合成注解 + */ + protected abstract A synthesize(Class annotationType, SynthesizedAnnotation annotation); + + /** + * 获取合成注解来源最初来源 + * + * @return 合成注解来源最初来源 + */ + @Override + public T getSource() { + return source; + } + + /** + * 合成注解选择器 + * + * @return 注解选择器 + */ + @Override + public SynthesizedAnnotationSelector getAnnotationSelector() { + return annotationSelector; + } + + /** + * 获取合成注解后置处理器 + * + * @return 合成注解后置处理器 + */ + @Override + public Collection getAnnotationPostProcessors() { + return postProcessors; + } + + /** + * 获取已合成的注解 + * + * @param annotationType 注解类型 + * @return 已合成的注解 + */ + @Override + public SynthesizedAnnotation getSynthesizedAnnotation(Class annotationType) { + return synthesizedAnnotationMap.get(annotationType); + } + + /** + * 获取全部的合成注解 + * + * @return 合成注解 + */ + @Override + public Map, SynthesizedAnnotation> getAllSynthesizedAnnotation() { + return synthesizedAnnotationMap; + } + + /** + * 获取合成注解 + * + * @param annotationType 注解类型 + * @param 注解类型 + * @return 类型 + */ + @SuppressWarnings("unchecked") + @Override + public A synthesize(Class annotationType) { + A annotation = (A)synthesizedProxyAnnotations.get(annotationType); + if (Objects.nonNull(annotation)) { + return annotation; + } + synchronized (synthesizedProxyAnnotations) { + annotation = (A)synthesizedProxyAnnotations.get(annotationType); + if (Objects.isNull(annotation)) { + final SynthesizedAnnotation synthesizedAnnotation = synthesizedAnnotationMap.get(annotationType); + annotation = synthesize(annotationType, synthesizedAnnotation); + synthesizedProxyAnnotations.put(annotationType, annotation); + } + } + return annotation; + } + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/annotation/AbstractLinkAnnotationPostProcessor.java b/hutool-core/src/main/java/cn/hutool/core/annotation/AbstractLinkAnnotationPostProcessor.java new file mode 100644 index 0000000000000000000000000000000000000000..0f97dbc26d56a772b13f2764d6c5375c402dfabf --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/annotation/AbstractLinkAnnotationPostProcessor.java @@ -0,0 +1,163 @@ +package cn.hutool.core.annotation; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.lang.Opt; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ObjectUtil; + +import java.lang.annotation.Annotation; +import java.util.HashMap; +import java.util.Map; + +/** + * {@link SynthesizedAnnotationPostProcessor}的基本实现, + * 用于处理注解中带有{@link Link}注解的属性。 + * + * @author huangchengxing + * @see MirrorLinkAnnotationPostProcessor + * @see AliasLinkAnnotationPostProcessor + */ +public abstract class AbstractLinkAnnotationPostProcessor implements SynthesizedAnnotationPostProcessor { + + /** + * 若一个注解属性上存在{@link Link}注解,注解的{@link Link#type()}返回值在{@link #processTypes()}中存在, + * 且此{@link Link}指定的注解对象在当前的{@link SynthesizedAggregateAnnotation}中存在, + * 则从聚合器中获取类型对应的合成注解对象,与该对象中的指定属性,然后将全部关联数据交给 + * {@link #processLinkedAttribute}处理。 + * + * @param synthesizedAnnotation 合成的注解 + * @param synthesizer 合成注解聚合器 + */ + @Override + public void process(SynthesizedAnnotation synthesizedAnnotation, AnnotationSynthesizer synthesizer) { + final Map attributeMap = new HashMap<>(synthesizedAnnotation.getAttributes()); + attributeMap.forEach((originalAttributeName, originalAttribute) -> { + // 获取注解 + final Link link = getLinkAnnotation(originalAttribute, processTypes()); + if (ObjectUtil.isNull(link)) { + return; + } + // 获取注解属性 + final SynthesizedAnnotation linkedAnnotation = getLinkedAnnotation(link, synthesizer, synthesizedAnnotation.annotationType()); + if (ObjectUtil.isNull(linkedAnnotation)) { + return; + } + final AnnotationAttribute linkedAttribute = linkedAnnotation.getAttributes().get(link.attribute()); + // 处理 + processLinkedAttribute( + synthesizer, link, + synthesizedAnnotation, synthesizedAnnotation.getAttributes().get(originalAttributeName), + linkedAnnotation, linkedAttribute + ); + }); + } + + // =========================== 抽象方法 =========================== + + /** + * 当属性上存在{@link Link}注解时,仅当{@link Link#type()}在本方法返回值内存在时才进行处理 + * + * @return 支持处理的{@link RelationType}类型 + */ + protected abstract RelationType[] processTypes(); + + /** + * 对关联的合成注解对象及其关联属性的处理 + * + * @param synthesizer 注解合成器 + * @param annotation {@code originalAttribute}上的{@link Link}注解对象 + * @param originalAnnotation 当前正在处理的{@link SynthesizedAnnotation}对象 + * @param originalAttribute {@code originalAnnotation}上的待处理的属性 + * @param linkedAnnotation {@link Link}指向的关联注解对象 + * @param linkedAttribute {@link Link}指向的{@code originalAnnotation}中的关联属性,该参数可能为空 + */ + protected abstract void processLinkedAttribute( + AnnotationSynthesizer synthesizer, Link annotation, + SynthesizedAnnotation originalAnnotation, AnnotationAttribute originalAttribute, + SynthesizedAnnotation linkedAnnotation, AnnotationAttribute linkedAttribute + ); + + // =========================== @Link注解的处理 =========================== + + /** + * 从注解属性上获取指定类型的{@link Link}注解 + * + * @param attribute 注解属性 + * @param relationTypes 类型 + * @return 注解 + */ + protected Link getLinkAnnotation(AnnotationAttribute attribute, RelationType... relationTypes) { + return Opt.ofNullable(attribute) + .map(t -> AnnotationUtil.getSynthesizedAnnotation(attribute.getAttribute(), Link.class)) + .filter(a -> ArrayUtil.contains(relationTypes, a.type())) + .get(); + } + + /** + * 从合成注解中获取{@link Link#type()}指定的注解对象 + * + * @param annotation {@link Link}注解 + * @param synthesizer 注解合成器 + * @param defaultType 默认类型 + * @return {@link SynthesizedAnnotation} + */ + protected SynthesizedAnnotation getLinkedAnnotation(Link annotation, AnnotationSynthesizer synthesizer, Class defaultType) { + final Class targetAnnotationType = getLinkedAnnotationType(annotation, defaultType); + return synthesizer.getSynthesizedAnnotation(targetAnnotationType); + } + + /** + * 若{@link Link#annotation()}获取的类型{@code Annotation#getClass()},则返回{@code defaultType}, + * 否则返回{@link Link#annotation()}指定的类型 + * + * @param annotation {@link Link}注解 + * @param defaultType 默认注解类型 + * @return 注解类型 + */ + protected Class getLinkedAnnotationType(Link annotation, Class defaultType) { + return ObjectUtil.equals(annotation.annotation(), Annotation.class) ? + defaultType : annotation.annotation(); + } + + // =========================== 注解属性的校验 =========================== + + /** + * 校验两个注解属性的返回值类型是否一致 + * + * @param original 原属性 + * @param alias 别名属性 + */ + protected void checkAttributeType(AnnotationAttribute original, AnnotationAttribute alias) { + Assert.equals( + original.getAttributeType(), alias.getAttributeType(), + "return type of the linked attribute [{}] is inconsistent with the original [{}]", + original.getAttribute(), alias.getAttribute() + ); + } + + /** + * 检查{@link Link}指向的注解属性是否就是本身 + * + * @param original {@link Link}注解的属性 + * @param linked {@link Link}指向的注解属性 + */ + protected void checkLinkedSelf(AnnotationAttribute original, AnnotationAttribute linked) { + boolean linkSelf = (original == linked) || ObjectUtil.equals(original.getAttribute(), linked.getAttribute()); + Assert.isFalse(linkSelf, "cannot link self [{}]", original.getAttribute()); + } + + /** + * 检查{@link Link}指向的注解属性是否存在 + * + * @param original {@link Link}注解的属性 + * @param linkedAttribute {@link Link}指向的注解属性 + * @param annotation {@link Link}注解 + */ + protected void checkLinkedAttributeNotNull(AnnotationAttribute original, AnnotationAttribute linkedAttribute, Link annotation) { + Assert.notNull(linkedAttribute, "cannot find linked attribute [{}] of original [{}] in [{}]", + original.getAttribute(), annotation.attribute(), + getLinkedAnnotationType(annotation, original.getAnnotationType()) + ); + } + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/annotation/AbstractWrappedAnnotationAttribute.java b/hutool-core/src/main/java/cn/hutool/core/annotation/AbstractWrappedAnnotationAttribute.java new file mode 100644 index 0000000000000000000000000000000000000000..2dc13d4768f82de0edfe23220fec68cae75b0bdb --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/annotation/AbstractWrappedAnnotationAttribute.java @@ -0,0 +1,71 @@ +package cn.hutool.core.annotation; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ObjectUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * {@link WrappedAnnotationAttribute}的基本实现 + * + * @author huangchengxing + * @see ForceAliasedAnnotationAttribute + * @see AliasedAnnotationAttribute + * @see MirroredAnnotationAttribute + */ +public abstract class AbstractWrappedAnnotationAttribute implements WrappedAnnotationAttribute { + + protected final AnnotationAttribute original; + protected final AnnotationAttribute linked; + + protected AbstractWrappedAnnotationAttribute(AnnotationAttribute original, AnnotationAttribute linked) { + Assert.notNull(original, "target must not null"); + Assert.notNull(linked, "linked must not null"); + this.original = original; + this.linked = linked; + } + + @Override + public AnnotationAttribute getOriginal() { + return original; + } + + @Override + public AnnotationAttribute getLinked() { + return linked; + } + + @Override + public AnnotationAttribute getNonWrappedOriginal() { + AnnotationAttribute curr = null; + AnnotationAttribute next = original; + while (next != null) { + curr = next; + next = next.isWrapped() ? ((WrappedAnnotationAttribute)curr).getOriginal() : null; + } + return curr; + } + + @Override + public Collection getAllLinkedNonWrappedAttributes() { + List leafAttributes = new ArrayList<>(); + collectLeafAttribute(this, leafAttributes); + return leafAttributes; + } + + private void collectLeafAttribute(AnnotationAttribute curr, List leafAttributes) { + if (ObjectUtil.isNull(curr)) { + return; + } + if (!curr.isWrapped()) { + leafAttributes.add(curr); + return; + } + WrappedAnnotationAttribute wrappedAttribute = (WrappedAnnotationAttribute)curr; + collectLeafAttribute(wrappedAttribute.getOriginal(), leafAttributes); + collectLeafAttribute(wrappedAttribute.getLinked(), leafAttributes); + } + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/annotation/AggregateAnnotation.java b/hutool-core/src/main/java/cn/hutool/core/annotation/AggregateAnnotation.java new file mode 100644 index 0000000000000000000000000000000000000000..000c99175565236c24ef73cb67138623d75b9cc5 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/annotation/AggregateAnnotation.java @@ -0,0 +1,27 @@ +package cn.hutool.core.annotation; + +import java.lang.annotation.Annotation; + +/** + * 表示一组被聚合在一起的注解对象 + * + * @author huangchengxing + */ +public interface AggregateAnnotation extends Annotation { + + /** + * 在聚合中是否存在的指定类型注解对象 + * + * @param annotationType 注解类型 + * @return 是否 + */ + boolean isAnnotationPresent(Class annotationType); + + /** + * 获取聚合中的全部注解对象 + * + * @return 注解对象 + */ + Annotation[] getAnnotations(); + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/annotation/AliasAnnotationPostProcessor.java b/hutool-core/src/main/java/cn/hutool/core/annotation/AliasAnnotationPostProcessor.java new file mode 100644 index 0000000000000000000000000000000000000000..ac5481a275a1272ffb702f70e77a0e2cf2483e69 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/annotation/AliasAnnotationPostProcessor.java @@ -0,0 +1,66 @@ +package cn.hutool.core.annotation; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.lang.Opt; +import cn.hutool.core.map.ForestMap; +import cn.hutool.core.map.LinkedForestMap; +import cn.hutool.core.map.TreeEntry; +import cn.hutool.core.util.ClassUtil; +import cn.hutool.core.util.ObjectUtil; + +import java.util.Map; + +/** + *

用于处理注解对象中带有{@link Alias}注解的属性。
+ * 当该处理器执行完毕后,{@link Alias}注解指向的目标注解的属性将会被包装并替换为 + * {@link ForceAliasedAnnotationAttribute}。 + * + * @author huangchengxing + * @see Alias + * @see ForceAliasedAnnotationAttribute + */ +public class AliasAnnotationPostProcessor implements SynthesizedAnnotationPostProcessor { + + @Override + public int order() { + return Integer.MIN_VALUE; + } + + @Override + public void process(SynthesizedAnnotation synthesizedAnnotation, AnnotationSynthesizer synthesizer) { + final Map attributeMap = synthesizedAnnotation.getAttributes(); + + // 记录别名与属性的关系 + final ForestMap attributeAliasMappings = new LinkedForestMap<>(false); + attributeMap.forEach((attributeName, attribute) -> { + final String alias = Opt.ofNullable(attribute.getAnnotation(Alias.class)) + .map(Alias::value) + .orElse(null); + if (ObjectUtil.isNull(alias)) { + return; + } + final AnnotationAttribute aliasAttribute = attributeMap.get(alias); + Assert.notNull(aliasAttribute, "no method for alias: [{}]", alias); + attributeAliasMappings.putLinkedNodes(alias, aliasAttribute, attributeName, attribute); + }); + + // 处理别名 + attributeMap.forEach((attributeName, attribute) -> { + final AnnotationAttribute resolvedAttribute = Opt.ofNullable(attributeName) + .map(attributeAliasMappings::getRootNode) + .map(TreeEntry::getValue) + .orElse(attribute); + Assert.isTrue( + ObjectUtil.isNull(resolvedAttribute) + || ClassUtil.isAssignable(attribute.getAttributeType(), resolvedAttribute.getAttributeType()), + "return type of the root alias method [{}] is inconsistent with the original [{}]", + resolvedAttribute.getClass(), attribute.getAttributeType() + ); + if (attribute != resolvedAttribute) { + attributeMap.put(attributeName, new ForceAliasedAnnotationAttribute(attribute, resolvedAttribute)); + } + }); + synthesizedAnnotation.setAttributes(attributeMap); + } + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/annotation/AliasFor.java b/hutool-core/src/main/java/cn/hutool/core/annotation/AliasFor.java new file mode 100644 index 0000000000000000000000000000000000000000..bf163238b437beaf0b6a06a4512cf15c84a88237 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/annotation/AliasFor.java @@ -0,0 +1,39 @@ +package cn.hutool.core.annotation; + +import java.lang.annotation.*; + +/** + *

{@link Link}的子注解。表示“原始属性”将作为“关联属性”的别名。 + *

    + *
  • 当“原始属性”为默认值时,获取“关联属性”将返回“关联属性”本身的值;
  • + *
  • 当“原始属性”不为默认值时,获取“关联属性”将返回“原始属性”的值;
  • + *
+ * 注意,该注解与{@link Link}、{@link ForceAliasFor}或{@link MirrorFor}一起使用时,将只有被声明在最上面的注解会生效 + * + * @author huangchengxing + * @see Link + * @see RelationType#ALIAS_FOR + */ +@Link(type = RelationType.ALIAS_FOR) +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) +public @interface AliasFor { + + /** + * 产生关联的注解类型,当不指定时,默认指注释的属性所在的类 + * + * @return 注解类型 + */ + @Link(annotation = Link.class, attribute = "annotation", type = RelationType.FORCE_ALIAS_FOR) + Class annotation() default Annotation.class; + + /** + * {@link #annotation()}指定注解中关联的属性 + * + * @return 关联属性 + */ + @Link(annotation = Link.class, attribute = "attribute", type = RelationType.FORCE_ALIAS_FOR) + String attribute() default ""; + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/annotation/AliasLinkAnnotationPostProcessor.java b/hutool-core/src/main/java/cn/hutool/core/annotation/AliasLinkAnnotationPostProcessor.java new file mode 100644 index 0000000000000000000000000000000000000000..0cf5b2255026c1c80bc49448195a0d7108f0a7b4 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/annotation/AliasLinkAnnotationPostProcessor.java @@ -0,0 +1,126 @@ +package cn.hutool.core.annotation; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.lang.Opt; +import cn.hutool.core.util.ObjectUtil; + +import java.util.function.BinaryOperator; + +/** + *

用于处理注解对象中带有{@link Link}注解,且{@link Link#type()}为 + * {@link RelationType#ALIAS_FOR}或{@link RelationType#FORCE_ALIAS_FOR}的属性。
+ * 当该处理器执行完毕后,{@link Link}注解指向的目标注解的属性将会被包装并替换为 + * {@link AliasedAnnotationAttribute}或{@link ForceAliasedAnnotationAttribute}。 + * + * @author huangchengxing + * @see RelationType#ALIAS_FOR + * @see AliasedAnnotationAttribute + * @see RelationType#FORCE_ALIAS_FOR + * @see ForceAliasedAnnotationAttribute + */ +public class AliasLinkAnnotationPostProcessor extends AbstractLinkAnnotationPostProcessor { + + private static final RelationType[] PROCESSED_RELATION_TYPES = new RelationType[]{ RelationType.ALIAS_FOR, RelationType.FORCE_ALIAS_FOR }; + + @Override + public int order() { + return Integer.MIN_VALUE + 2; + } + + /** + * 该处理器只处理{@link Link#type()}类型为{@link RelationType#ALIAS_FOR}和{@link RelationType#FORCE_ALIAS_FOR}的注解属性 + * + * @return 含有{@link RelationType#ALIAS_FOR}和{@link RelationType#FORCE_ALIAS_FOR}的数组 + */ + @Override + protected RelationType[] processTypes() { + return PROCESSED_RELATION_TYPES; + } + + /** + * 获取{@link Link}指向的目标注解属性,并根据{@link Link#type()}的类型是 + * {@link RelationType#ALIAS_FOR}或{@link RelationType#FORCE_ALIAS_FOR} + * 将目标注解属性包装为{@link AliasedAnnotationAttribute}或{@link ForceAliasedAnnotationAttribute}, + * 然后用包装后注解属性在对应的合成注解中替换原始的目标注解属性 + * + * @param synthesizer 注解合成器 + * @param annotation {@code originalAttribute}上的{@link Link}注解对象 + * @param originalAnnotation 当前正在处理的{@link SynthesizedAnnotation}对象 + * @param originalAttribute {@code originalAnnotation}上的待处理的属性 + * @param linkedAnnotation {@link Link}指向的关联注解对象 + * @param linkedAttribute {@link Link}指向的{@code originalAnnotation}中的关联属性,该参数可能为空 + */ + @Override + protected void processLinkedAttribute( + AnnotationSynthesizer synthesizer, Link annotation, + SynthesizedAnnotation originalAnnotation, AnnotationAttribute originalAttribute, + SynthesizedAnnotation linkedAnnotation, AnnotationAttribute linkedAttribute) { + // 校验别名关系 + checkAliasRelation(annotation, originalAttribute, linkedAttribute); + // 处理aliasFor类型的关系 + if (RelationType.ALIAS_FOR.equals(annotation.type())) { + wrappingLinkedAttribute(synthesizer, originalAttribute, linkedAttribute, AliasedAnnotationAttribute::new); + return; + } + // 处理forceAliasFor类型的关系 + wrappingLinkedAttribute(synthesizer, originalAttribute, linkedAttribute, ForceAliasedAnnotationAttribute::new); + } + + /** + * 对指定注解属性进行包装,若该属性已被包装过,则递归以其为根节点的树结构,对树上全部的叶子节点进行包装 + */ + private void wrappingLinkedAttribute( + AnnotationSynthesizer synthesizer, AnnotationAttribute originalAttribute, AnnotationAttribute aliasAttribute, BinaryOperator wrapping) { + // 不是包装属性 + if (!aliasAttribute.isWrapped()) { + processAttribute(synthesizer, originalAttribute, aliasAttribute, wrapping); + return; + } + // 是包装属性 + final AbstractWrappedAnnotationAttribute wrapper = (AbstractWrappedAnnotationAttribute)aliasAttribute; + wrapper.getAllLinkedNonWrappedAttributes().forEach( + t -> processAttribute(synthesizer, originalAttribute, t, wrapping) + ); + } + + /** + * 获取指定注解属性,然后将其再进行一层包装 + */ + private void processAttribute( + AnnotationSynthesizer synthesizer, AnnotationAttribute originalAttribute, + AnnotationAttribute target, BinaryOperator wrapping) { + Opt.ofNullable(target.getAnnotationType()) + .map(synthesizer::getSynthesizedAnnotation) + .ifPresent(t -> t.replaceAttribute(target.getAttributeName(), old -> wrapping.apply(old, originalAttribute))); + } + + /** + * 基本校验 + */ + private void checkAliasRelation(Link annotation, AnnotationAttribute originalAttribute, AnnotationAttribute linkedAttribute) { + checkLinkedAttributeNotNull(originalAttribute, linkedAttribute, annotation); + checkAttributeType(originalAttribute, linkedAttribute); + checkCircularDependency(originalAttribute, linkedAttribute); + } + + /** + * 检查两个属性是否互为别名 + */ + private void checkCircularDependency(AnnotationAttribute original, AnnotationAttribute alias) { + checkLinkedSelf(original, alias); + Link annotation = getLinkAnnotation(alias, RelationType.ALIAS_FOR, RelationType.FORCE_ALIAS_FOR); + if (ObjectUtil.isNull(annotation)) { + return; + } + final Class aliasAnnotationType = getLinkedAnnotationType(annotation, alias.getAnnotationType()); + if (ObjectUtil.notEqual(aliasAnnotationType, original.getAnnotationType())) { + return; + } + Assert.notEquals( + annotation.attribute(), original.getAttributeName(), + "circular reference between the alias attribute [{}] and the original attribute [{}]", + alias.getAttribute(), original.getAttribute() + ); + } + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/annotation/AliasedAnnotationAttribute.java b/hutool-core/src/main/java/cn/hutool/core/annotation/AliasedAnnotationAttribute.java new file mode 100644 index 0000000000000000000000000000000000000000..1c78d81758ff4bafcc2ca0c57fb7ca61209a441f --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/annotation/AliasedAnnotationAttribute.java @@ -0,0 +1,36 @@ +package cn.hutool.core.annotation; + +/** + *

表示一个具有别名的属性。 + * 当别名属性值为默认值时,优先返回原属性的值,当别名属性不为默认值时,优先返回别名属性的值 + * + * @author huangchengxing + * @see AliasLinkAnnotationPostProcessor + * @see RelationType#ALIAS_FOR + */ +public class AliasedAnnotationAttribute extends AbstractWrappedAnnotationAttribute { + + protected AliasedAnnotationAttribute(AnnotationAttribute origin, AnnotationAttribute linked) { + super(origin, linked); + } + + /** + * 若{@link #linked}为默认值,则返回{@link #original}的值,否则返回{@link #linked}的值 + * + * @return 属性值 + */ + @Override + public Object getValue() { + return linked.isValueEquivalentToDefaultValue() ? super.getValue() : linked.getValue(); + } + + /** + * 当{@link #original}与{@link #linked}都为默认值时返回{@code true} + * + * @return 是否 + */ + @Override + public boolean isValueEquivalentToDefaultValue() { + return linked.isValueEquivalentToDefaultValue() && original.isValueEquivalentToDefaultValue(); + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/annotation/AnnotationAttribute.java b/hutool-core/src/main/java/cn/hutool/core/annotation/AnnotationAttribute.java new file mode 100644 index 0000000000000000000000000000000000000000..f3d976d40e5a369476550dc991f8ef81306be43c --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/annotation/AnnotationAttribute.java @@ -0,0 +1,104 @@ +package cn.hutool.core.annotation; + +import cn.hutool.core.util.ReflectUtil; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; + +/** + *

表示注解的某个属性,等同于绑定的调用对象的{@link Method}方法。
+ * 在{@link SynthesizedAggregateAnnotation}的解析以及取值过程中, + * 可以通过设置{@link SynthesizedAnnotation}的注解属性, + * 从而使得可以从一个注解对象中属性获取另一个注解对象的属性值 + * + *

一般情况下,注解属性的处理会发生在{@link SynthesizedAnnotationPostProcessor}调用时 + * + * @author huangchengxing + * @see SynthesizedAnnotationPostProcessor + * @see WrappedAnnotationAttribute + * @see CacheableAnnotationAttribute + * @see AbstractWrappedAnnotationAttribute + * @see ForceAliasedAnnotationAttribute + * @see AliasedAnnotationAttribute + * @see MirroredAnnotationAttribute + */ +public interface AnnotationAttribute { + + /** + * 获取注解对象 + * + * @return 注解对象 + */ + Annotation getAnnotation(); + + /** + * 获取注解属性对应的方法 + * + * @return 注解属性对应的方法 + */ + Method getAttribute(); + + /** + * 获取声明属性的注解类 + * + * @return 声明注解的注解类 + */ + default Class getAnnotationType() { + return getAttribute().getDeclaringClass(); + } + + /** + * 获取属性名称 + * + * @return 属性名称 + */ + default String getAttributeName() { + return getAttribute().getName(); + } + + /** + * 获取注解属性 + * + * @return 注解属性 + */ + default Object getValue() { + return ReflectUtil.invoke(getAnnotation(), getAttribute()); + } + + /** + * 该注解属性的值是否等于默认值 + * + * @return 该注解属性的值是否等于默认值 + */ + boolean isValueEquivalentToDefaultValue(); + + /** + * 获取属性类型 + * + * @return 属性类型 + */ + default Class getAttributeType() { + return getAttribute().getReturnType(); + } + + /** + * 获取属性上的注解 + * + * @param 注解类型 + * @param annotationType 注解类型 + * @return 注解对象 + */ + default T getAnnotation(Class annotationType) { + return getAttribute().getAnnotation(annotationType); + } + + /** + * 当前注解属性是否已经被{@link WrappedAnnotationAttribute}包装 + * + * @return boolean + */ + default boolean isWrapped() { + return false; + } + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/annotation/AnnotationAttributeValueProvider.java b/hutool-core/src/main/java/cn/hutool/core/annotation/AnnotationAttributeValueProvider.java new file mode 100644 index 0000000000000000000000000000000000000000..b127d75c47f4e3b109130b0e2481f26291f9efae --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/annotation/AnnotationAttributeValueProvider.java @@ -0,0 +1,18 @@ +package cn.hutool.core.annotation; + +/** + * 表示一个可以从当前接口的实现类中,获得特定的属性值 + */ +@FunctionalInterface +public interface AnnotationAttributeValueProvider { + + /** + * 获取注解属性值 + * + * @param attributeName 属性名称 + * @param attributeType 属性类型 + * @return 注解属性值 + */ + Object getAttributeValue(String attributeName, Class attributeType); + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/annotation/AnnotationProxy.java b/hutool-core/src/main/java/cn/hutool/core/annotation/AnnotationProxy.java index bcf2ba4b5829e8b74054c3ee227c443c4ff73389..d1491dc6a8bd17659d1b18b546b14cac2776ecc3 100644 --- a/hutool-core/src/main/java/cn/hutool/core/annotation/AnnotationProxy.java +++ b/hutool-core/src/main/java/cn/hutool/core/annotation/AnnotationProxy.java @@ -29,9 +29,9 @@ public class AnnotationProxy implements Annotation, Invoca * * @param annotation 注解 */ + @SuppressWarnings("unchecked") public AnnotationProxy(T annotation) { this.annotation = annotation; - //noinspection unchecked this.type = (Class) annotation.annotationType(); this.attributes = initAttributes(); } diff --git a/hutool-core/src/main/java/cn/hutool/core/annotation/AnnotationSynthesizer.java b/hutool-core/src/main/java/cn/hutool/core/annotation/AnnotationSynthesizer.java new file mode 100644 index 0000000000000000000000000000000000000000..298a33dbc0ed0012452b73292a6141bddb436f42 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/annotation/AnnotationSynthesizer.java @@ -0,0 +1,77 @@ +package cn.hutool.core.annotation; + +import java.lang.annotation.Annotation; +import java.util.Collection; +import java.util.Map; + +/** + *

注解合成器,用于处理一组给定的与{@link #getSource()}具有直接或间接联系的注解对象, + * 并返回与原始注解对象具有不同属性的“合成”注解。 + * + *

合成注解一般被用于处理类层级结果中具有直接或间接关联的注解对象, + * 当实例被创建时,会获取到这些注解对象,并使用{@link SynthesizedAnnotationSelector}对类型相同的注解进行过滤, + * 并最终得到类型不重复的有效注解对象。这些有效注解将被包装为{@link SynthesizedAnnotation}, + * 然后最终用于“合成”一个{@link SynthesizedAggregateAnnotation}。
+ * {@link SynthesizedAnnotationSelector}是合成注解生命周期中的第一个钩子, + * 自定义选择器以拦截原始注解被扫描的过程。 + * + *

当合成注解完成对待合成注解的扫描,并完成了必要属性的加载后, + * 将会按顺序依次调用{@link SynthesizedAnnotationPostProcessor}, + * 注解后置处理器允许用于对完成注解的待合成注解进行二次调整, + * 该钩子一般用于根据{@link Link}注解对属性进行调整。
+ * {@link SynthesizedAnnotationPostProcessor}是合成注解生命周期中的第二个钩子, + * 自定义后置处理器以拦截原始在转为待合成注解后的初始化过程。 + * + *

使用{@link #synthesize(Class)}用于获取“合成”后的注解, + * 该注解对象的属性可能会与原始的对象属性不同。 + * + * @author huangchengxing + */ +public interface AnnotationSynthesizer { + + /** + * 获取合成注解来源最初来源 + * + * @return 合成注解来源最初来源 + */ + Object getSource(); + + /** + * 合成注解选择器 + * + * @return 注解选择器 + */ + SynthesizedAnnotationSelector getAnnotationSelector(); + + /** + * 获取合成注解后置处理器 + * + * @return 合成注解后置处理器 + */ + Collection getAnnotationPostProcessors(); + + /** + * 获取已合成的注解 + * + * @param annotationType 注解类型 + * @return 已合成的注解 + */ + SynthesizedAnnotation getSynthesizedAnnotation(Class annotationType); + + /** + * 获取全部的合成注解 + * + * @return 合成注解 + */ + Map, SynthesizedAnnotation> getAllSynthesizedAnnotation(); + + /** + * 获取合成注解 + * + * @param annotationType 注解类型 + * @param 注解类型 + * @return 类型 + */ + T synthesize(Class annotationType); + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/annotation/AnnotationUtil.java b/hutool-core/src/main/java/cn/hutool/core/annotation/AnnotationUtil.java index a75affb8d9af208157bd3d3bc66eddd9ca7cd044..74cc9afb66aea6449c7157498d93b86297d6b1b9 100755 --- a/hutool-core/src/main/java/cn/hutool/core/annotation/AnnotationUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/annotation/AnnotationUtil.java @@ -1,22 +1,24 @@ package cn.hutool.core.annotation; +import cn.hutool.core.annotation.scanner.AnnotationScanner; +import cn.hutool.core.annotation.scanner.MetaAnnotationScanner; +import cn.hutool.core.annotation.scanner.MethodAnnotationScanner; +import cn.hutool.core.annotation.scanner.TypeAnnotationScanner; +import cn.hutool.core.collection.CollUtil; import cn.hutool.core.exceptions.UtilException; -import cn.hutool.core.util.ArrayUtil; -import cn.hutool.core.util.ReflectUtil; - -import java.lang.annotation.Annotation; -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Inherited; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; +import cn.hutool.core.lang.Opt; +import cn.hutool.core.lang.func.Func1; +import cn.hutool.core.lang.func.LambdaUtil; +import cn.hutool.core.util.*; + +import java.lang.annotation.*; +import java.lang.invoke.SerializedLambda; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Method; import java.lang.reflect.Proxy; -import java.util.HashMap; -import java.util.Map; +import java.util.*; import java.util.function.Predicate; +import java.util.stream.Collectors; /** * 注解工具类
@@ -27,6 +29,58 @@ import java.util.function.Predicate; */ public class AnnotationUtil { + /** + * 元注解 + */ + static final Set> META_ANNOTATIONS = CollUtil.newHashSet(Target.class, // + Retention.class, // + Inherited.class, // + Documented.class, // + SuppressWarnings.class, // + Override.class, // + Deprecated.class// + ); + + /** + * 是否为Jdk自带的元注解。
+ * 包括: + *

    + *
  • {@link Target}
  • + *
  • {@link Retention}
  • + *
  • {@link Inherited}
  • + *
  • {@link Documented}
  • + *
  • {@link SuppressWarnings}
  • + *
  • {@link Override}
  • + *
  • {@link Deprecated}
  • + *
+ * + * @param annotationType 注解类型 + * @return 是否为Jdk自带的元注解 + */ + public static boolean isJdkMetaAnnotation(Class annotationType) { + return META_ANNOTATIONS.contains(annotationType); + } + + /** + * 是否不为Jdk自带的元注解。
+ * 包括: + *
    + *
  • {@link Target}
  • + *
  • {@link Retention}
  • + *
  • {@link Inherited}
  • + *
  • {@link Documented}
  • + *
  • {@link SuppressWarnings}
  • + *
  • {@link Override}
  • + *
  • {@link Deprecated}
  • + *
+ * + * @param annotationType 注解类型 + * @return 是否为Jdk自带的元注解 + */ + public static boolean isNotJdkMateAnnotation(Class annotationType) { + return false == isJdkMetaAnnotation(annotationType); + } + /** * 将指定的被注解的元素转换为组合注解元素 * @@ -74,13 +128,13 @@ public class AnnotationUtil { * @return 注解对象数组 * @since 5.8.0 */ + @SuppressWarnings("unchecked") public static T[] getAnnotations(AnnotatedElement annotationEle, boolean isToCombination, Class annotationType) { final Annotation[] annotations = getAnnotations(annotationEle, isToCombination, (annotation -> null == annotationType || annotationType.isAssignableFrom(annotation.getClass()))); final T[] result = ArrayUtil.newArray(annotationType, annotations.length); for (int i = 0; i < annotations.length; i++) { - //noinspection unchecked result[i] = (T) annotations[i]; } return result; @@ -92,7 +146,7 @@ public class AnnotationUtil { * @param annotationEle {@link AnnotatedElement},可以是Class、Method、Field、Constructor、ReflectPermission * @param isToCombination 是否为转换为组合注解,组合注解可以递归获取注解的注解 * @param predicate 过滤器,{@link Predicate#test(Object)}返回{@code true}保留,否则不保留 - * @return 注解对象 + * @return 注解对象,如果提供的{@link AnnotatedElement}为{@code null},返回{@code null} * @since 5.8.0 */ public static Annotation[] getAnnotations(AnnotatedElement annotationEle, boolean isToCombination, Predicate predicate) { @@ -138,6 +192,30 @@ public class AnnotationUtil { return null != getAnnotation(annotationEle, annotationType); } + /** + * 检查是否包含指定注解
+ * 注解类传入全名,通过{@link Class#forName(String)}加载,避免不存在的注解导致的ClassNotFoundException + * + * @param annotationEle {@link AnnotatedElement},可以是Class、Method、Field、Constructor、ReflectPermission + * @param annotationTypeName 注解类型完整类名 + * @return 是否包含指定注解 + * @since 5.8.37 + */ + @SuppressWarnings({"rawtypes", "unchecked"}) + public static boolean hasAnnotation(final AnnotatedElement annotationEle, final String annotationTypeName) { + Class aClass = null; + try { + // issue#IB0JP5,Android可能无这个类 + aClass = Class.forName(annotationTypeName); + } catch (final ClassNotFoundException e) { + // ignore + } + if(null != aClass){ + return hasAnnotation(annotationEle, aClass); + } + return false; + } + /** * 获取指定注解默认值
* 如果无指定的属性方法返回null @@ -176,6 +254,29 @@ public class AnnotationUtil { return ReflectUtil.invoke(annotation, method); } + /** + * 获取指定注解属性的值
+ * 如果无指定的属性方法返回null + * + * @param
注解类型 + * @param 注解类型值 + * @param annotationEle {@link AnnotatedElement},可以是Class、Method、Field、Constructor、ReflectPermission + * @param propertyName 属性名,例如注解中定义了name()方法,则 此处传入name + * @return 注解对象 + * @throws UtilException 调用注解中的方法时执行异常 + * @since 5.8.9 + */ + public static R getAnnotationValue(AnnotatedElement annotationEle, Func1 propertyName) { + if (propertyName == null) { + return null; + } else { + final SerializedLambda lambda = LambdaUtil.resolve(propertyName); + final String instantiatedMethodType = lambda.getInstantiatedMethodType(); + final Class annotationClass = ClassUtil.loadClass(StrUtil.sub(instantiatedMethodType, 2, StrUtil.indexOf(instantiatedMethodType, ';'))); + return getAnnotationValue(annotationEle, annotationClass, lambda.getImplMethodName()); + } + } + /** * 获取指定注解中所有属性值
* 如果无指定的属性方法返回null @@ -266,6 +367,84 @@ public class AnnotationUtil { return annotationType.isAnnotationPresent(Inherited.class); } + /** + * 扫描注解类,以及注解类的{@link Class}层级结构中的注解,将返回除了{@link #META_ANNOTATIONS}中指定的JDK默认注解外, + * 按元注解对象与{@code annotationType}的距离和{@link Class#getAnnotations()}顺序排序的注解对象集合 + * + *

比如:
+ * 若{@code annotationType}为 A,且A存在元注解B,B又存在元注解C和D,则有: + *

+	 *                              |-> C.class [@a, @b]
+	 *     A.class -> B.class [@a] -|
+	 *                              |-> D.class [@a, @c]
+	 * 
+ * 扫描A,则该方法最终将返回 {@code [@a, @a, @b, @a, @c]} + * + * @param annotationType 注解类 + * @return 注解对象集合 + * @see MetaAnnotationScanner + */ + public static List scanMetaAnnotation(Class annotationType) { + return AnnotationScanner.DIRECTLY_AND_META_ANNOTATION.getAnnotationsIfSupport(annotationType); + } + + /** + *

扫描类以及类的{@link Class}层级结构中的注解,将返回除了{@link #META_ANNOTATIONS}中指定的JDK默认元注解外, + * 全部类/接口的{@link Class#getAnnotations()}方法返回的注解对象。
+ * 层级结构将按广度优先递归,遵循规则如下: + *

    + *
  • 同一层级中,优先处理父类,然后再处理父接口;
  • + *
  • 同一个接口在不同层级出现,优先选择层级距离{@code targetClass}更近的接口;
  • + *
  • 同一个接口在相同层级出现,优先选择其子类/子接口被先解析的那个;
  • + *
+ * 注解根据其声明类/接口被扫描的顺序排序,若注解都在同一个{@link Class}中被声明,则还会遵循{@link Class#getAnnotations()}的顺序。 + * + *

比如:
+ * 若{@code targetClass}为{@code A.class},且{@code A.class}存在父类{@code B.class}、父接口{@code C.class}, + * 三个类的注解声明情况如下: + *

+	 *                   |-> B.class [@a, @b]
+	 *     A.class [@a] -|
+	 *                   |-> C.class [@a, @c]
+	 * 
+ * 则该方法最终将返回 {@code [@a, @a, @b, @a, @c]} + * + * @param targetClass 类 + * @return 注解对象集合 + * @see TypeAnnotationScanner + */ + public static List scanClass(Class targetClass) { + return AnnotationScanner.TYPE_HIERARCHY.getAnnotationsIfSupport(targetClass); + } + + /** + *

扫描方法,以及该方法所在类的{@link Class}层级结构中的具有相同方法签名的方法, + * 将返回除了{@link #META_ANNOTATIONS}中指定的JDK默认元注解外, + * 全部匹配方法上{@link Method#getAnnotations()}方法返回的注解对象。
+ * 方法所在类的层级结构将按广度优先递归,遵循规则如下: + *

    + *
  • 同一层级中,优先处理父类,然后再处理父接口;
  • + *
  • 同一个接口在不同层级出现,优先选择层级距离{@code targetClass}更近的接口;
  • + *
  • 同一个接口在相同层级出现,优先选择其子类/子接口被先解析的那个;
  • + *
+ * 方法上的注解根据方法的声明类/接口被扫描的顺序排序,若注解都在同一个类的同一个方法中被声明,则还会遵循{@link Method#getAnnotations()}的顺序。 + * + *

比如:
+ * 若方法X声明于{@code A.class},且重载/重写自父类{@code B.class},并且父类中的方法X由重写至其实现的接口{@code C.class}, + * 三个类的注解声明情况如下: + *

+	 *     A#X()[@a] -> B#X()[@b] -> C#X()[@c]
+	 * 
+ * 则该方法最终将返回 {@code [@a, @b, @c]} + * + * @param method 方法 + * @return 注解对象集合 + * @see MethodAnnotationScanner + */ + public static List scanMethod(Method method) { + return AnnotationScanner.TYPE_HIERARCHY.getAnnotationsIfSupport(method); + } + /** * 设置新的注解的属性(字段)值 * @@ -280,6 +459,17 @@ public class AnnotationUtil { memberValues.put(annotationField, value); } + /** + * 该注解对象是否为通过代理类生成的合成注解 + * + * @param annotation 注解对象 + * @return 是否 + * @see SynthesizedAnnotationProxy#isProxyAnnotation(Class) + */ + public static boolean isSynthesizedAnnotation(Annotation annotation) { + return SynthesizedAnnotationProxy.isProxyAnnotation(annotation.getClass()); + } + /** * 获取别名支持后的注解 * @@ -289,9 +479,125 @@ public class AnnotationUtil { * @return 别名支持后的注解 * @since 5.7.23 */ - @SuppressWarnings("unchecked") public static T getAnnotationAlias(AnnotatedElement annotationEle, Class annotationType) { final T annotation = getAnnotation(annotationEle, annotationType); - return (T) Proxy.newProxyInstance(annotationType.getClassLoader(), new Class[]{annotationType}, new AnnotationProxy<>(annotation)); + if (null == annotation) { + return null; + } + return aggregatingFromAnnotation(annotation).synthesize(annotationType); } + + /** + * 将指定注解实例与其元注解转为合成注解 + * + * @param annotationType 注解类 + * @param annotations 注解对象 + * @param 注解类型 + * @return 合成注解 + * @see SynthesizedAggregateAnnotation + */ + public static T getSynthesizedAnnotation(Class annotationType, Annotation... annotations) { + // TODO 缓存合成注解信息,避免重复解析 + return Opt.ofNullable(annotations) + .filter(ArrayUtil::isNotEmpty) + .map(AnnotationUtil::aggregatingFromAnnotationWithMeta) + .map(a -> a.synthesize(annotationType)) + .get(); + } + + /** + *

获取元素上距离指定元素最接近的合成注解 + *

    + *
  • 若元素是类,则递归解析全部父类和全部父接口上的注解;
  • + *
  • 若元素是方法、属性或注解,则只解析其直接声明的注解;
  • + *
+ * + *

注解合成规则如下: + * 若{@code AnnotatedEle}按顺序从上到下声明了A,B,C三个注解,且三注解存在元注解如下: + *

+	 *    A -> M3
+	 *    B -> M1 -> M2 -> M3
+	 *    C -> M2 -> M3
+	 * 
+ * 此时入参{@code annotationType}类型为{@code M2},则最终将优先返回基于根注解B合成的合成注解 + * + * @param annotatedEle {@link AnnotatedElement},可以是Class、Method、Field、Constructor、ReflectPermission + * @param annotationType 注解类 + * @param 注解类型 + * @return 合成注解 + * @see SynthesizedAggregateAnnotation + */ + public static T getSynthesizedAnnotation(AnnotatedElement annotatedEle, Class annotationType) { + T target = annotatedEle.getAnnotation(annotationType); + if (ObjectUtil.isNotNull(target)) { + return target; + } + return AnnotationScanner.DIRECTLY + .getAnnotationsIfSupport(annotatedEle).stream() + .map(annotation -> getSynthesizedAnnotation(annotationType, annotation)) + .filter(Objects::nonNull) + .findFirst() + .orElse(null); + } + + /** + * 获取元素上所有指定注解 + *
    + *
  • 若元素是类,则递归解析全部父类和全部父接口上的注解;
  • + *
  • 若元素是方法、属性或注解,则只解析其直接声明的注解;
  • + *
+ * + *

注解合成规则如下: + * 若{@code AnnotatedEle}按顺序从上到下声明了A,B,C三个注解,且三注解存在元注解如下: + *

+	 *    A -> M1 -> M2
+	 *    B -> M3 -> M1 -> M2
+	 *    C -> M2
+	 * 
+ * 此时入参{@code annotationType}类型为{@code M1},则最终将返回基于根注解A与根注解B合成的合成注解。 + * + * @param annotatedEle {@link AnnotatedElement},可以是Class、Method、Field、Constructor、ReflectPermission + * @param annotationType 注解类 + * @param 注解类型 + * @return 合成注解 + * @see SynthesizedAggregateAnnotation + */ + public static List getAllSynthesizedAnnotations(AnnotatedElement annotatedEle, Class annotationType) { + return AnnotationScanner.DIRECTLY + .getAnnotationsIfSupport(annotatedEle).stream() + .map(annotation -> getSynthesizedAnnotation(annotationType, annotation)) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + /** + * 对指定注解对象进行聚合 + * + * @param annotations 注解对象 + * @return 聚合注解 + */ + public static SynthesizedAggregateAnnotation aggregatingFromAnnotation(Annotation... annotations) { + return new GenericSynthesizedAggregateAnnotation(Arrays.asList(annotations), AnnotationScanner.NOTHING); + } + + /** + * 对指定注解对象及其元注解进行聚合 + * + * @param annotations 注解对象 + * @return 聚合注解 + */ + public static SynthesizedAggregateAnnotation aggregatingFromAnnotationWithMeta(Annotation... annotations) { + return new GenericSynthesizedAggregateAnnotation(Arrays.asList(annotations), AnnotationScanner.DIRECTLY_AND_META_ANNOTATION); + } + + /** + * 方法是否为注解属性方法。
+ * 方法无参数,且有返回值的方法认为是注解属性的方法。 + * + * @param method 方法 + */ + static boolean isAttributeMethod(Method method) { + return method.getParameterCount() == 0 && method.getReturnType() != void.class; + } + } diff --git a/hutool-core/src/main/java/cn/hutool/core/annotation/CacheableAnnotationAttribute.java b/hutool-core/src/main/java/cn/hutool/core/annotation/CacheableAnnotationAttribute.java new file mode 100644 index 0000000000000000000000000000000000000000..5ef49f1ec833db7147f6c287325bc95369d78fb9 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/annotation/CacheableAnnotationAttribute.java @@ -0,0 +1,67 @@ +package cn.hutool.core.annotation; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.ReflectUtil; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; + +/** + * {@link AnnotationAttribute}的基本实现 + * + * @author huangchengxing + */ +public class CacheableAnnotationAttribute implements AnnotationAttribute { + + private volatile boolean valueInvoked; + private Object value; + + private boolean defaultValueInvoked; + private Object defaultValue; + + private final Annotation annotation; + private final Method attribute; + + public CacheableAnnotationAttribute(Annotation annotation, Method attribute) { + Assert.notNull(annotation, "annotation must not null"); + Assert.notNull(attribute, "attribute must not null"); + this.annotation = annotation; + this.attribute = attribute; + this.valueInvoked = false; + this.defaultValueInvoked = false; + } + + @Override + public Annotation getAnnotation() { + return this.annotation; + } + + @Override + public Method getAttribute() { + return this.attribute; + } + + @Override + public Object getValue() { + if (!valueInvoked) { + synchronized (this) { + if (!valueInvoked) { + valueInvoked = true; + value = ReflectUtil.invoke(annotation, attribute); + } + } + } + return value; + } + + @Override + public boolean isValueEquivalentToDefaultValue() { + if (!defaultValueInvoked) { + defaultValue = attribute.getDefaultValue(); + defaultValueInvoked = true; + } + return ObjectUtil.equals(getValue(), defaultValue); + } + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/annotation/CacheableSynthesizedAnnotationAttributeProcessor.java b/hutool-core/src/main/java/cn/hutool/core/annotation/CacheableSynthesizedAnnotationAttributeProcessor.java new file mode 100644 index 0000000000000000000000000000000000000000..c104856d75d537fbfd1b49baf791afae4152169d --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/annotation/CacheableSynthesizedAnnotationAttributeProcessor.java @@ -0,0 +1,65 @@ +package cn.hutool.core.annotation; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.multi.RowKeyTable; +import cn.hutool.core.map.multi.Table; + +import java.util.Collection; +import java.util.Comparator; +import java.util.Objects; + +/** + *

带缓存功能的{@link SynthesizedAnnotationAttributeProcessor}实现, + * 构建时需要传入比较器,获取属性值时将根据比较器对合成注解进行排序, + * 然后选择具有所需属性的,排序最靠前的注解用于获取属性值 + * + *

通过该处理器获取合成注解属性值时会出现隐式别名, + * 即子注解和元注解中同时存在类型和名称皆相同的属性时,元注解中属性总是会被该属性覆盖, + * 并且该覆盖关系并不会通过{@link Alias}或{@link Link}被传递到关联的属性中。 + * + * @author huangchengxing + */ +public class CacheableSynthesizedAnnotationAttributeProcessor implements SynthesizedAnnotationAttributeProcessor { + + private final Table, Object> valueCaches = new RowKeyTable<>(); + private final Comparator annotationComparator; + + /** + * 创建一个带缓存的注解值选择器 + * + * @param annotationComparator 注解比较器,排序更靠前的注解将被优先用于获取值 + */ + public CacheableSynthesizedAnnotationAttributeProcessor(Comparator annotationComparator) { + Assert.notNull(annotationComparator, "annotationComparator must not null"); + this.annotationComparator = annotationComparator; + } + + /** + * 创建一个带缓存的注解值选择器, + * 默认按{@link SynthesizedAnnotation#getVerticalDistance()}和{@link SynthesizedAnnotation#getHorizontalDistance()}排序, + * 越靠前的越优先被取值。 + */ + public CacheableSynthesizedAnnotationAttributeProcessor() { + this(Hierarchical.DEFAULT_HIERARCHICAL_COMPARATOR); + } + + @SuppressWarnings("unchecked") + @Override + public T getAttributeValue(String attributeName, Class attributeType, Collection synthesizedAnnotations) { + Object value = valueCaches.get(attributeName, attributeType); + if (Objects.isNull(value)) { + synchronized (valueCaches) { + value = valueCaches.get(attributeName, attributeType); + if (Objects.isNull(value)) { + value = synthesizedAnnotations.stream() + .filter(ma -> ma.hasAttribute(attributeName, attributeType)) + .min(annotationComparator) + .map(ma -> ma.getAttributeValue(attributeName)) + .orElse(null); + valueCaches.put(attributeName, attributeType, value); + } + } + } + return (T)value; + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/annotation/CombinationAnnotationElement.java b/hutool-core/src/main/java/cn/hutool/core/annotation/CombinationAnnotationElement.java index 542793f79a4bffef43e4c9770d29b89f953a2353..c9e52b6b9775e331adff5951555bdb9f239d9b2c 100755 --- a/hutool-core/src/main/java/cn/hutool/core/annotation/CombinationAnnotationElement.java +++ b/hutool-core/src/main/java/cn/hutool/core/annotation/CombinationAnnotationElement.java @@ -1,19 +1,13 @@ package cn.hutool.core.annotation; -import cn.hutool.core.collection.CollUtil; import cn.hutool.core.map.TableMap; +import cn.hutool.core.util.ArrayUtil; import java.io.Serializable; import java.lang.annotation.Annotation; -import java.lang.annotation.Documented; -import java.lang.annotation.Inherited; -import java.lang.annotation.Retention; -import java.lang.annotation.Target; import java.lang.reflect.AnnotatedElement; -import java.util.Arrays; import java.util.Collection; import java.util.Map; -import java.util.Set; import java.util.function.Predicate; /** @@ -39,18 +33,6 @@ public class CombinationAnnotationElement implements AnnotatedElement, Serializa return new CombinationAnnotationElement(element, predicate); } - /** - * 元注解 - */ - private static final Set> META_ANNOTATIONS = CollUtil.newHashSet(Target.class, // - Retention.class, // - Inherited.class, // - Documented.class, // - SuppressWarnings.class, // - Override.class, // - Deprecated.class// - ); - /** * 注解类型与注解对象对应表 */ @@ -120,7 +102,9 @@ public class CombinationAnnotationElement implements AnnotatedElement, Serializa parseDeclared(declaredAnnotations); final Annotation[] annotations = element.getAnnotations(); - if (Arrays.equals(declaredAnnotations, annotations)) { + // pr#1323 如果子类重写了父类的注解,虽然两者数组内部元素一样的,但是数组中的顺序可能不一样 + // getAnnotations()的包含父类,getDeclaredAnnotations()不包含父类。他们两是一个包含关系,只会存在后者的注解元素大于等于前者的情况。 + if (declaredAnnotations.length == annotations.length) { this.annotationMap = this.declaredAnnotationMap; } else { this.annotationMap = new TableMap<>(); @@ -134,11 +118,17 @@ public class CombinationAnnotationElement implements AnnotatedElement, Serializa * @param annotations Class, Method, Field等 */ private void parseDeclared(Annotation[] annotations) { + if(ArrayUtil.isEmpty(annotations)){ + return; + } + Class annotationType; // 直接注解 for (Annotation annotation : annotations) { annotationType = annotation.annotationType(); - if (false == META_ANNOTATIONS.contains(annotationType)) { + // issue#I5FQGW@Gitee:跳过元注解和已经处理过的注解,防止递归调用 + if (AnnotationUtil.isNotJdkMateAnnotation(annotationType) + && false == declaredAnnotationMap.containsKey(annotationType)) { if(test(annotation)){ declaredAnnotationMap.put(annotationType, annotation); } @@ -154,10 +144,16 @@ public class CombinationAnnotationElement implements AnnotatedElement, Serializa * @param annotations Class, Method, Field等 */ private void parse(Annotation[] annotations) { + if(ArrayUtil.isEmpty(annotations)){ + return; + } + Class annotationType; for (Annotation annotation : annotations) { annotationType = annotation.annotationType(); - if (false == META_ANNOTATIONS.contains(annotationType)) { + // issue#I5FQGW@Gitee:跳过元注解和已经处理过的注解,防止递归调用 + if (AnnotationUtil.isNotJdkMateAnnotation(annotationType) + && false == annotationMap.containsKey(annotationType)) { if(test(annotation)){ annotationMap.put(annotationType, annotation); } diff --git a/hutool-core/src/main/java/cn/hutool/core/annotation/ForceAliasFor.java b/hutool-core/src/main/java/cn/hutool/core/annotation/ForceAliasFor.java new file mode 100644 index 0000000000000000000000000000000000000000..7549a152561f6d99e2a5a1f75b113e2ec082d81c --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/annotation/ForceAliasFor.java @@ -0,0 +1,35 @@ +package cn.hutool.core.annotation; + +import java.lang.annotation.*; + +/** + *

{@link Link}的子注解。表示“原始属性”将强制作为“关联属性”的别名。效果等同于在“原始属性”上添加{@link Alias}注解, + * 任何情况下,获取“关联属性”的值都将直接返回“原始属性”的值 + * 注意,该注解与{@link Link}、{@link AliasFor}或{@link MirrorFor}一起使用时,将只有被声明在最上面的注解会生效 + * + * @author huangchengxing + * @see Link + * @see RelationType#FORCE_ALIAS_FOR + */ +@Link(type = RelationType.FORCE_ALIAS_FOR) +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) +public @interface ForceAliasFor { + + /** + * 产生关联的注解类型,当不指定时,默认指注释的属性所在的类 + * + * @return 关联注解类型 + */ + @Link(annotation = Link.class, attribute = "annotation", type = RelationType.FORCE_ALIAS_FOR) + Class annotation() default Annotation.class; + + /** + * {@link #annotation()}指定注解中关联的属性 + * + * @return 关联的属性 + */ + @Link(annotation = Link.class, attribute = "attribute", type = RelationType.FORCE_ALIAS_FOR) + String attribute() default ""; +} diff --git a/hutool-core/src/main/java/cn/hutool/core/annotation/ForceAliasedAnnotationAttribute.java b/hutool-core/src/main/java/cn/hutool/core/annotation/ForceAliasedAnnotationAttribute.java new file mode 100644 index 0000000000000000000000000000000000000000..312219c39cea9386747047a664302c4c6d290c19 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/annotation/ForceAliasedAnnotationAttribute.java @@ -0,0 +1,49 @@ +package cn.hutool.core.annotation; + +/** + * 表示一个被指定了强制别名的注解属性。 + * 当调用{@link #getValue()}时,总是返回{@link #linked}的值 + * + * @author huangchengxing + * @see AliasAnnotationPostProcessor + * @see AliasLinkAnnotationPostProcessor + * @see RelationType#ALIAS_FOR + * @see RelationType#FORCE_ALIAS_FOR + */ +public class ForceAliasedAnnotationAttribute extends AbstractWrappedAnnotationAttribute { + + protected ForceAliasedAnnotationAttribute(AnnotationAttribute origin, AnnotationAttribute linked) { + super(origin, linked); + } + + /** + * 总是返回{@link #linked}的{@link AnnotationAttribute#getValue()}的返回值 + * + * @return {@link #linked}的{@link AnnotationAttribute#getValue()}的返回值 + */ + @Override + public Object getValue() { + return linked.getValue(); + } + + /** + * 总是返回{@link #linked}的{@link AnnotationAttribute#isValueEquivalentToDefaultValue()}的返回值 + * + * @return {@link #linked}的{@link AnnotationAttribute#isValueEquivalentToDefaultValue()}的返回值 + */ + @Override + public boolean isValueEquivalentToDefaultValue() { + return linked.isValueEquivalentToDefaultValue(); + } + + /** + * 总是返回{@link #linked}的{@link AnnotationAttribute#getAttributeType()}的返回值 + * + * @return {@link #linked}的{@link AnnotationAttribute#getAttributeType()}的返回值 + */ + @Override + public Class getAttributeType() { + return linked.getAttributeType(); + } + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/annotation/GenericSynthesizedAggregateAnnotation.java b/hutool-core/src/main/java/cn/hutool/core/annotation/GenericSynthesizedAggregateAnnotation.java new file mode 100644 index 0000000000000000000000000000000000000000..8a5f7b7b90c5ea79fb44e669fef18a9758c61c42 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/annotation/GenericSynthesizedAggregateAnnotation.java @@ -0,0 +1,318 @@ +package cn.hutool.core.annotation; + +import cn.hutool.core.annotation.scanner.AnnotationScanner; +import cn.hutool.core.annotation.scanner.MetaAnnotationScanner; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.lang.Opt; +import cn.hutool.core.util.ObjectUtil; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.util.*; + +/** + * {@link SynthesizedAggregateAnnotation}的基本实现,表示基于多个注解对象, + * 或多个根注解对象与他们的多层元注解对象的聚合得到的注解。 + * + *

假设现有注解A,若指定的{@link #annotationScanner}支持扫描注解A的元注解, + * 且A上存在元注解B,B上存在元注解C,则对注解A进行解析,将得到包含根注解A,以及其元注解B、C在内的合成元注解聚合{@link GenericSynthesizedAggregateAnnotation}。 + * 从{@link AnnotatedElement}的角度来说,得到的合成注解是一个同时承载有ABC三个注解对象的被注解元素, + * 因此通过调用{@link AnnotatedElement}的相关方法将返回对应符合语义的注解对象。 + * + *

在扫描指定根注解及其元注解时,若在不同的层级出现了类型相同的注解实例, + * 将会根据实例化时指定的{@link SynthesizedAnnotationSelector}选择最优的注解, + * 完成对根注解及其元注解的扫描后,合成注解中每种类型的注解对象都将有且仅有一个。
+ * 默认情况下,将使用{@link SynthesizedAnnotationSelector#NEAREST_AND_OLDEST_PRIORITY}作为选择器, + * 此时若出现扫描时得到了多个同类型的注解对象,有且仅有最接近根注解的注解对象会被作为有效注解。 + * + *

当扫描的注解对象经过{@link SynthesizedAnnotationSelector}处理后, + * 将会被转为{@link MetaAnnotation},并使用在实例化时指定的{@link AliasAnnotationPostProcessor} + * 进行后置处理。
+ * 默认情况下,将注册以下后置处理器以对{@link Alias}与{@link Link}和其扩展注解提供支持: + *

    + *
  • {@link AliasAnnotationPostProcessor};
  • + *
  • {@link MirrorLinkAnnotationPostProcessor};
  • + *
  • {@link AliasLinkAnnotationPostProcessor};
  • + *
+ * 若用户需要自行扩展,则需要保证上述三个处理器被正确注入当前实例。 + * + *

{@link GenericSynthesizedAggregateAnnotation}支持通过{@link #getAttributeValue(String, Class)}, + * 或通过{@link #synthesize(Class)}获得注解代理对象后获取指定类型的注解属性值, + * 返回的属性值将根据合成注解中对应原始注解属性上的{@link Alias}与{@link Link}注解而有所变化。 + * 通过当前实例获取属性值时,将经过{@link SynthesizedAnnotationAttributeProcessor}的处理。
+ * 默认情况下,实例将会注册{@link CacheableSynthesizedAnnotationAttributeProcessor}, + * 该处理器将令元注解中与子注解类型与名称皆一致的属性被子注解的属性覆盖,并且缓存最终获取到的属性值。 + * + * @author huangchengxing + * @see AnnotationUtil + * @see SynthesizedAnnotationProxy + * @see SynthesizedAnnotationSelector + * @see SynthesizedAnnotationAttributeProcessor + * @see SynthesizedAnnotationPostProcessor + * @see AnnotationSynthesizer + * @see AnnotationScanner + */ +public class GenericSynthesizedAggregateAnnotation + extends AbstractAnnotationSynthesizer> + implements SynthesizedAggregateAnnotation { + + /** + * 根对象 + */ + private final Object root; + + /** + * 距离根对象的垂直距离 + */ + private final int verticalDistance; + + /** + * 距离根对象的水平距离 + */ + private final int horizontalDistance; + + /** + * 合成注解属性处理器 + */ + private final SynthesizedAnnotationAttributeProcessor attributeProcessor; + + /** + * 基于指定根注解,为其与其元注解的层级结构中的全部注解构造一个合成注解。 + * 当层级结构中出现了相同的注解对象时,将优先选择以距离根注解最近,且优先被扫描的注解对象, + * 当获取值时,同样遵循该规则。 + * + * @param source 源注解 + */ + public GenericSynthesizedAggregateAnnotation(Annotation... source) { + this(Arrays.asList(source), new MetaAnnotationScanner()); + } + + /** + * 基于指定根注解,为其层级结构中的全部注解构造一个合成注解。 + * 若扫描器支持对注解的层级结构进行扫描,则若层级结构中出现了相同的注解对象时, + * 将优先选择以距离根注解最近,且优先被扫描的注解对象,并且当获取注解属性值时同样遵循该规则。 + * + * @param source 源注解 + * @param annotationScanner 注解扫描器,该扫描器必须支持扫描注解类 + */ + public GenericSynthesizedAggregateAnnotation(List source, AnnotationScanner annotationScanner) { + this( + source, SynthesizedAnnotationSelector.NEAREST_AND_OLDEST_PRIORITY, + new CacheableSynthesizedAnnotationAttributeProcessor(), + Arrays.asList( + SynthesizedAnnotationPostProcessor.ALIAS_ANNOTATION_POST_PROCESSOR, + SynthesizedAnnotationPostProcessor.MIRROR_LINK_ANNOTATION_POST_PROCESSOR, + SynthesizedAnnotationPostProcessor.ALIAS_LINK_ANNOTATION_POST_PROCESSOR + ), + annotationScanner + ); + } + + /** + * 基于指定根注解,为其层级结构中的全部注解构造一个合成注解 + * + * @param source 当前查找的注解对象 + * @param annotationSelector 合成注解选择器 + * @param attributeProcessor 注解属性处理器 + * @param annotationPostProcessors 注解后置处理器 + * @param annotationScanner 注解扫描器,该扫描器必须支持扫描注解类 + */ + public GenericSynthesizedAggregateAnnotation( + List source, + SynthesizedAnnotationSelector annotationSelector, + SynthesizedAnnotationAttributeProcessor attributeProcessor, + Collection annotationPostProcessors, + AnnotationScanner annotationScanner) { + this( + null, 0, 0, + source, annotationSelector, attributeProcessor, annotationPostProcessors, annotationScanner + ); + } + + /** + * 基于指定根注解,为其层级结构中的全部注解构造一个合成注解 + * + * @param root 根对象 + * @param verticalDistance 距离根对象的水平距离 + * @param horizontalDistance 距离根对象的垂直距离 + * @param source 当前查找的注解对象 + * @param annotationSelector 合成注解选择器 + * @param attributeProcessor 注解属性处理器 + * @param annotationPostProcessors 注解后置处理器 + * @param annotationScanner 注解扫描器,该扫描器必须支持扫描注解类 + */ + GenericSynthesizedAggregateAnnotation( + Object root, int verticalDistance, int horizontalDistance, + List source, + SynthesizedAnnotationSelector annotationSelector, + SynthesizedAnnotationAttributeProcessor attributeProcessor, + Collection annotationPostProcessors, + AnnotationScanner annotationScanner) { + super(source, annotationSelector, annotationPostProcessors, annotationScanner); + Assert.notNull(attributeProcessor, "attributeProcessor must not null"); + + this.root = ObjectUtil.defaultIfNull(root, this); + this.verticalDistance = verticalDistance; + this.horizontalDistance = horizontalDistance; + this.attributeProcessor = attributeProcessor; + } + + /** + * 获取根对象 + * + * @return 根对象 + */ + @Override + public Object getRoot() { + return root; + } + + /** + * 获取与根对象的垂直距离 + * + * @return 与根对象的垂直距离 + */ + @Override + public int getVerticalDistance() { + return verticalDistance; + } + + /** + * 获取与根对象的水平距离 + * + * @return 获取与根对象的水平距离 + */ + @Override + public int getHorizontalDistance() { + return horizontalDistance; + } + + /** + * 按广度优先扫描{@link #source}上的元注解 + */ + @Override + protected Map, SynthesizedAnnotation> loadAnnotations() { + Map, SynthesizedAnnotation> annotationMap = new LinkedHashMap<>(); + + // 根注解默认水平坐标为0,根注解的元注解坐标从1开始 + for (int i = 0; i < source.size(); i++) { + final Annotation sourceAnnotation = source.get(i); + Assert.isFalse(AnnotationUtil.isSynthesizedAnnotation(sourceAnnotation), "source [{}] has been synthesized"); + annotationMap.put(sourceAnnotation.annotationType(), new MetaAnnotation(sourceAnnotation, sourceAnnotation, 0, i)); + Assert.isTrue( + annotationScanner.support(sourceAnnotation.annotationType()), + "annotation scanner [{}] cannot support scan [{}]", + annotationScanner, sourceAnnotation.annotationType() + ); + annotationScanner.scan( + (index, annotation) -> { + SynthesizedAnnotation oldAnnotation = annotationMap.get(annotation.annotationType()); + SynthesizedAnnotation newAnnotation = new MetaAnnotation(sourceAnnotation, annotation, index + 1, annotationMap.size()); + if (ObjectUtil.isNull(oldAnnotation)) { + annotationMap.put(annotation.annotationType(), newAnnotation); + } else { + annotationMap.put(annotation.annotationType(), annotationSelector.choose(oldAnnotation, newAnnotation)); + } + }, + sourceAnnotation.annotationType(), null + ); + } + return annotationMap; + } + + /** + * 获取合成注解属性处理器 + * + * @return 合成注解属性处理器 + */ + @Override + public SynthesizedAnnotationAttributeProcessor getAnnotationAttributeProcessor() { + return this.attributeProcessor; + } + + /** + * 根据指定的属性名与属性类型获取对应的属性值,若存在{@link Alias}则获取{@link Alias#value()}指定的别名属性的值 + *

当不同层级的注解之间存在同名同类型属性时,将优先获取更接近根注解的属性 + * + * @param attributeName 属性名 + * @param attributeType 属性类型 + * @return 属性 + */ + @Override + public Object getAttributeValue(String attributeName, Class attributeType) { + return attributeProcessor.getAttributeValue(attributeName, attributeType, synthesizedAnnotationMap.values()); + } + + /** + * 获取合成注解中包含的指定注解 + * + * @param annotationType 注解类型 + * @param 注解类型 + * @return 注解对象 + */ + @Override + public T getAnnotation(Class annotationType) { + return Opt.ofNullable(annotationType) + .map(synthesizedAnnotationMap::get) + .map(SynthesizedAnnotation::getAnnotation) + .map(annotationType::cast) + .orElse(null); + } + + /** + * 当前合成注解中是否存在指定元注解 + * + * @param annotationType 注解类型 + * @return 是否 + */ + @Override + public boolean isAnnotationPresent(Class annotationType) { + return synthesizedAnnotationMap.containsKey(annotationType); + } + + /** + * 获取合成注解中包含的全部注解 + * + * @return 注解对象 + */ + @Override + public Annotation[] getAnnotations() { + return synthesizedAnnotationMap.values().stream() + .map(SynthesizedAnnotation::getAnnotation) + .toArray(Annotation[]::new); + } + + /** + * 若合成注解在存在指定元注解,则使用动态代理生成一个对应的注解实例 + * + * @param annotationType 注解类型 + * @return 合成注解对象 + * @see SynthesizedAnnotationProxy#create(Class, AnnotationAttributeValueProvider, SynthesizedAnnotation) + */ + @Override + public T synthesize(Class annotationType, SynthesizedAnnotation annotation) { + return SynthesizedAnnotationProxy.create(annotationType, this, annotation); + } + + /** + * 注解包装类,表示{@link #source}以及{@link #source}所属层级结构中的全部关联注解对象 + * + * @author huangchengxing + */ + public static class MetaAnnotation extends GenericSynthesizedAnnotation { + + /** + * 创建一个合成注解 + * + * @param root 根对象 + * @param annotation 被合成的注解对象 + * @param verticalDistance 距离根对象的水平距离 + * @param horizontalDistance 距离根对象的垂直距离 + */ + protected MetaAnnotation(Annotation root, Annotation annotation, int verticalDistance, int horizontalDistance) { + super(root, annotation, verticalDistance, horizontalDistance); + } + + } + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/annotation/GenericSynthesizedAnnotation.java b/hutool-core/src/main/java/cn/hutool/core/annotation/GenericSynthesizedAnnotation.java new file mode 100644 index 0000000000000000000000000000000000000000..303424995ea73151866d233f35e89e5357c8adde --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/annotation/GenericSynthesizedAnnotation.java @@ -0,0 +1,197 @@ +package cn.hutool.core.annotation; + +import cn.hutool.core.lang.Opt; +import cn.hutool.core.util.ClassUtil; +import cn.hutool.core.util.ObjectUtil; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; +import java.util.function.UnaryOperator; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * {@link SynthesizedAnnotation}的基本实现 + * + * @param 根对象类型 + * @param 注解类型 + * @author huangchengxing + */ +public class GenericSynthesizedAnnotation implements SynthesizedAnnotation { + + private final R root; + private final T annotation; + private final Map attributeMethodCaches; + private final int verticalDistance; + private final int horizontalDistance; + + /** + * 创建一个合成注解 + * + * @param root 根对象 + * @param annotation 被合成的注解对象 + * @param verticalDistance 距离根对象的水平距离 + * @param horizontalDistance 距离根对象的垂直距离 + */ + protected GenericSynthesizedAnnotation( + R root, T annotation, int verticalDistance, int horizontalDistance) { + this.root = root; + this.annotation = annotation; + this.verticalDistance = verticalDistance; + this.horizontalDistance = horizontalDistance; + this.attributeMethodCaches = new HashMap<>(); + this.attributeMethodCaches.putAll(loadAttributeMethods()); + } + + /** + * 加载注解属性 + * + * @return 注解属性 + */ + protected Map loadAttributeMethods() { + return Stream.of(ClassUtil.getDeclaredMethods(annotation.annotationType())) + .filter(AnnotationUtil::isAttributeMethod) + .collect(Collectors.toMap(Method::getName, method -> new CacheableAnnotationAttribute(annotation, method))); + } + + /** + * 元注解是否存在该属性 + * + * @param attributeName 属性名 + * @return 是否存在该属性 + */ + public boolean hasAttribute(String attributeName) { + return attributeMethodCaches.containsKey(attributeName); + } + + /** + * 元注解是否存在该属性,且该属性的值类型是指定类型或其子类 + * + * @param attributeName 属性名 + * @param returnType 返回值类型 + * @return 是否存在该属性 + */ + @Override + public boolean hasAttribute(String attributeName, Class returnType) { + return Opt.ofNullable(attributeMethodCaches.get(attributeName)) + .filter(method -> ClassUtil.isAssignable(returnType, method.getAttributeType())) + .isPresent(); + } + + /** + * 获取该注解的全部属性 + * + * @return 注解属性 + */ + @Override + public Map getAttributes() { + return this.attributeMethodCaches; + } + + /** + * 设置属性值 + * + * @param attributeName 属性名称 + * @param attribute 注解属性 + */ + @Override + public void setAttribute(String attributeName, AnnotationAttribute attribute) { + attributeMethodCaches.put(attributeName, attribute); + } + + /** + * 替换属性值 + * + * @param attributeName 属性名 + * @param operator 替换操作 + */ + @Override + public void replaceAttribute(String attributeName, UnaryOperator operator) { + AnnotationAttribute old = attributeMethodCaches.get(attributeName); + if (ObjectUtil.isNotNull(old)) { + attributeMethodCaches.put(attributeName, operator.apply(old)); + } + } + + /** + * 获取属性值 + * + * @param attributeName 属性名 + * @return 属性值 + */ + @Override + public Object getAttributeValue(String attributeName) { + return Opt.ofNullable(attributeMethodCaches.get(attributeName)) + .map(AnnotationAttribute::getValue) + .get(); + } + + /** + * 获取该合成注解对应的根节点 + * + * @return 合成注解对应的根节点 + */ + @Override + public R getRoot() { + return root; + } + + /** + * 获取被合成的注解对象 + * + * @return 注解对象 + */ + @Override + public T getAnnotation() { + return annotation; + } + + /** + * 获取该合成注解与根对象的垂直距离。 + * 默认情况下,该距离即为当前注解与根对象之间相隔的层级数。 + * + * @return 合成注解与根对象的垂直距离 + */ + @Override + public int getVerticalDistance() { + return verticalDistance; + } + + /** + * 获取该合成注解与根对象的水平距离。 + * 默认情况下,该距离即为当前注解与根对象之间相隔的已经被扫描到的注解数。 + * + * @return 合成注解与根对象的水平距离 + */ + @Override + public int getHorizontalDistance() { + return horizontalDistance; + } + + /** + * 获取被合成的注解类型 + * + * @return 被合成的注解类型 + */ + @Override + public Class annotationType() { + return annotation.annotationType(); + } + + /** + * 获取注解属性值 + * + * @param attributeName 属性名称 + * @param attributeType 属性类型 + * @return 注解属性值 + */ + @Override + public Object getAttributeValue(String attributeName, Class attributeType) { + return Opt.ofNullable(attributeMethodCaches.get(attributeName)) + .filter(method -> ClassUtil.isAssignable(attributeType, method.getAttributeType())) + .map(AnnotationAttribute::getValue) + .get(); + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/annotation/Hierarchical.java b/hutool-core/src/main/java/cn/hutool/core/annotation/Hierarchical.java new file mode 100644 index 0000000000000000000000000000000000000000..00c9938222b9fce71b3be76ce69fe24340782287 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/annotation/Hierarchical.java @@ -0,0 +1,155 @@ +package cn.hutool.core.annotation; + + +import java.util.Comparator; + +/** + *

描述以一个参照物为对象,存在于该参照物的层级结构中的对象。 + * + *

该对象可通过{@link #getVerticalDistance()}与{@link #getHorizontalDistance()} + * 描述其在以参照物为基点的坐标坐标系中的位置。
+ * 在需要对该接口的实现类进行按优先级排序时,距离{@link #getRoot()}对象越近,则该实现类的优先级越高。 + * 默认提供了{@link #DEFAULT_HIERARCHICAL_COMPARATOR}用于实现该比较规则。
+ * 一般情况下,{@link #getRoot()}返回值相同的对象之间的比较才有意义。 + * + *

此外,还提供了{@link Selector}接口用于根据一定的规则从两个{@link Hierarchical}实现类中选择并返回一个最合适的对象, + * 默认提供了四个实现类: + *

    + *
  • {@link Selector#NEAREST_AND_OLDEST_PRIORITY}: 返回距离根对象更近的对象,当距离一样时优先返回旧对象;
  • + *
  • {@link Selector#NEAREST_AND_NEWEST_PRIORITY}: 返回距离根对象更近的对象,当距离一样时优先返回新对象;
  • + *
  • {@link Selector#FARTHEST_AND_OLDEST_PRIORITY}: 返回距离根对象更远的对象,当距离一样时优先返回旧对象;
  • + *
  • {@link Selector#FARTHEST_AND_NEWEST_PRIORITY}: 返回距离根对象更远的对象,当距离一样时优先返回新对象;
  • + *
+ * + * @author huangchengxing + */ +public interface Hierarchical extends Comparable { + + // ====================== compare ====================== + + /** + * 默认{@link #getHorizontalDistance()}与{@link #getVerticalDistance()}排序的比较器 + */ + Comparator DEFAULT_HIERARCHICAL_COMPARATOR = Comparator + .comparing(Hierarchical::getVerticalDistance) + .thenComparing(Hierarchical::getHorizontalDistance); + + /** + * 按{@link #getVerticalDistance()}和{@link #getHorizontalDistance()}排序 + * + * @param o {@link SynthesizedAnnotation}对象 + * @return 比较值 + */ + @Override + default int compareTo(Hierarchical o) { + return DEFAULT_HIERARCHICAL_COMPARATOR.compare(this, o); + } + + // ====================== hierarchical ====================== + + /** + * 参照物,即坐标为{@code (0, 0)}的对象。 + * 当对象本身即为参照物时,该方法应当返回其本身 + * + * @return 参照物 + */ + Object getRoot(); + + /** + * 获取该对象与参照物的垂直距离。 + * 默认情况下,该距离即为当前对象与参照物之间相隔的层级数。 + * + * @return 合成注解与根对象的垂直距离 + */ + int getVerticalDistance(); + + /** + * 获取该对象与参照物的水平距离。 + * 默认情况下,该距离即为当前对象在与参照物{@link #getVerticalDistance()}相同的情况下条, + * 该对象被扫描到的顺序。 + * + * @return 合成注解与根对象的水平距离 + */ + int getHorizontalDistance(); + + // ====================== selector ====================== + + /** + * {@link Hierarchical}选择器,用于根据一定的规则从两个{@link Hierarchical}实现类中选择并返回一个最合适的对象 + */ + @FunctionalInterface + interface Selector { + + /** + * 返回距离根对象更近的对象,当距离一样时优先返回旧对象 + */ + Selector NEAREST_AND_OLDEST_PRIORITY = new NearestAndOldestPrioritySelector(); + + /** + * 返回距离根对象更近的对象,当距离一样时优先返回新对象 + */ + Selector NEAREST_AND_NEWEST_PRIORITY = new NearestAndNewestPrioritySelector(); + + /** + * 返回距离根对象更远的对象,当距离一样时优先返回旧对象 + */ + Selector FARTHEST_AND_OLDEST_PRIORITY = new FarthestAndOldestPrioritySelector(); + + /** + * 返回距离根对象更远的对象,当距离一样时优先返回新对象 + */ + Selector FARTHEST_AND_NEWEST_PRIORITY = new FarthestAndNewestPrioritySelector(); + + /** + * 比较两个被合成的对象,选择其中的一个并返回 + * + * @param 复合注解类型 + * @param prev 上一对象,该参数不允许为空 + * @param next 下一对象,该参数不允许为空 + * @return 对象 + */ + T choose(T prev, T next); + + /** + * 返回距离根对象更近的注解,当距离一样时优先返回旧注解 + */ + class NearestAndOldestPrioritySelector implements Selector { + @Override + public T choose(T oldAnnotation, T newAnnotation) { + return newAnnotation.getVerticalDistance() < oldAnnotation.getVerticalDistance() ? newAnnotation : oldAnnotation; + } + } + + /** + * 返回距离根对象更近的注解,当距离一样时优先返回新注解 + */ + class NearestAndNewestPrioritySelector implements Selector { + @Override + public T choose(T oldAnnotation, T newAnnotation) { + return newAnnotation.getVerticalDistance() <= oldAnnotation.getVerticalDistance() ? newAnnotation : oldAnnotation; + } + } + + /** + * 返回距离根对象更远的注解,当距离一样时优先返回旧注解 + */ + class FarthestAndOldestPrioritySelector implements Selector { + @Override + public T choose(T oldAnnotation, T newAnnotation) { + return newAnnotation.getVerticalDistance() > oldAnnotation.getVerticalDistance() ? newAnnotation : oldAnnotation; + } + } + + /** + * 返回距离根对象更远的注解,当距离一样时优先返回新注解 + */ + class FarthestAndNewestPrioritySelector implements Selector { + @Override + public T choose(T oldAnnotation, T newAnnotation) { + return newAnnotation.getVerticalDistance() >= oldAnnotation.getVerticalDistance() ? newAnnotation : oldAnnotation; + } + } + + } + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/annotation/Link.java b/hutool-core/src/main/java/cn/hutool/core/annotation/Link.java new file mode 100644 index 0000000000000000000000000000000000000000..8f4e20f3f5c41403849ce749c480e7336e31602a --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/annotation/Link.java @@ -0,0 +1,49 @@ +package cn.hutool.core.annotation; + +import java.lang.annotation.*; + +/** + *

用于在同一注解中,或具有一定关联的不同注解的属性中,表明这些属性之间具有特定的关联关系。 + * 在通过{@link SynthesizedAggregateAnnotation}获取合成注解后,合成注解获取属性值时会根据该注解进行调整。
+ * + *

该注解存在三个字注解:{@link MirrorFor}、{@link ForceAliasFor}或{@link AliasFor}, + * 使用三个子注解等同于{@link Link}。但是需要注意的是, + * 当注解中的属性同时存在多个{@link Link}或基于{@link Link}的子注解时, + * 仅有声明在被注解的属性最上方的注解会生效,其余注解都将被忽略。 + * + * 注意:该注解的优先级低于{@link Alias} + * + * @author huangchengxing + * @see SynthesizedAggregateAnnotation + * @see RelationType + * @see AliasFor + * @see MirrorFor + * @see ForceAliasFor + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) +public @interface Link { + + /** + * 产生关联的注解类型,当不指定时,默认指注释的属性所在的类 + * + * @return 关联的注解类型 + */ + Class annotation() default Annotation.class; + + /** + * {@link #annotation()}指定注解中关联的属性 + * + * @return 属性名 + */ + String attribute() default ""; + + /** + * {@link #attribute()}指定属性与当前注解的属性建的关联关系类型 + * + * @return 关系类型 + */ + RelationType type() default RelationType.MIRROR_FOR; + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/annotation/MirrorFor.java b/hutool-core/src/main/java/cn/hutool/core/annotation/MirrorFor.java new file mode 100644 index 0000000000000000000000000000000000000000..7d69b34a076b2c14bde712877ecfa3f3f5f8e34e --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/annotation/MirrorFor.java @@ -0,0 +1,42 @@ +package cn.hutool.core.annotation; + +import java.lang.annotation.*; + +/** + *

{@link Link}的子注解。表示注解的属性与指定的属性互为镜像,通过一个属性将能够获得对方的值。
+ * 它们遵循下述规则: + *

    + *
  • 互为镜像的两个属性,必须同时通过指定模式为{@code MIRROR_FOR}的{@link Link}注解指定对方;
  • + *
  • 互为镜像的两个属性,类型必须一致;
  • + *
  • 互为镜像的两个属性在获取值,且两者的值皆不同时,必须且仅允许有一个非默认值,该值被优先返回;
  • + *
  • 互为镜像的两个属性,在值都为默认值或都不为默认值时,两者的值必须相等;
  • + *
+ * 注意,该注解与{@link Link}、{@link ForceAliasFor}或{@link AliasFor}一起使用时,将只有被声明在最上面的注解会生效 + * + * @author huangchengxing + * @see Link + * @see RelationType#MIRROR_FOR + */ +@Link(type = RelationType.MIRROR_FOR) +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) +public @interface MirrorFor { + + /** + * 产生关联的注解类型,当不指定时,默认指注释的属性所在的类 + * + * @return 关联的注解类型 + */ + @Link(annotation = Link.class, attribute = "annotation", type = RelationType.FORCE_ALIAS_FOR) + Class annotation() default Annotation.class; + + /** + * {@link #annotation()}指定注解中关联的属性 + * + * @return 属性名 + */ + @Link(annotation = Link.class, attribute = "attribute", type = RelationType.FORCE_ALIAS_FOR) + String attribute() default ""; + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/annotation/MirrorLinkAnnotationPostProcessor.java b/hutool-core/src/main/java/cn/hutool/core/annotation/MirrorLinkAnnotationPostProcessor.java new file mode 100644 index 0000000000000000000000000000000000000000..1fc0a203b367163e1345f1d39ac6c912e39be06c --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/annotation/MirrorLinkAnnotationPostProcessor.java @@ -0,0 +1,132 @@ +package cn.hutool.core.annotation; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.text.CharSequenceUtil; +import cn.hutool.core.util.ObjectUtil; + +/** + *

用于处理注解对象中带有{@link Link}注解,且{@link Link#type()}为{@link RelationType#MIRROR_FOR}的属性。
+ * 当该处理器执行完毕后,原始合成注解中被{@link Link}注解的属性与{@link Link}注解指向的目标注解的属性, + * 都将会被被包装并替换为{@link MirroredAnnotationAttribute}。 + * + * @author huangchengxing + * @see RelationType#MIRROR_FOR + * @see MirroredAnnotationAttribute + */ +public class MirrorLinkAnnotationPostProcessor extends AbstractLinkAnnotationPostProcessor { + + private static final RelationType[] PROCESSED_RELATION_TYPES = new RelationType[]{ RelationType.MIRROR_FOR }; + + @Override + public int order() { + return Integer.MIN_VALUE + 1; + } + + /** + * 该处理器只处理{@link Link#type()}类型为{@link RelationType#MIRROR_FOR}的注解属性 + * + * @return 仅有{@link RelationType#MIRROR_FOR}数组 + */ + @Override + protected RelationType[] processTypes() { + return PROCESSED_RELATION_TYPES; + } + + /** + * 将存在镜像关系的合成注解属性分别包装为{@link MirroredAnnotationAttribute}对象, + * 并使用包装后{@link MirroredAnnotationAttribute}替换在它们对应合成注解实例中的{@link AnnotationAttribute} + * + * @param synthesizer 注解合成器 + * @param annotation {@code originalAttribute}上的{@link Link}注解对象 + * @param originalAnnotation 当前正在处理的{@link SynthesizedAnnotation}对象 + * @param originalAttribute {@code originalAnnotation}上的待处理的属性 + * @param linkedAnnotation {@link Link}指向的关联注解对象 + * @param linkedAttribute {@link Link}指向的{@code originalAnnotation}中的关联属性,该参数可能为空 + */ + @Override + protected void processLinkedAttribute( + AnnotationSynthesizer synthesizer, Link annotation, + SynthesizedAnnotation originalAnnotation, AnnotationAttribute originalAttribute, + SynthesizedAnnotation linkedAnnotation, AnnotationAttribute linkedAttribute) { + + // 镜像属性必然成对出现,因此此处必定存在三种情况: + // 1.两属性都不为镜像属性,此时继续进行后续处理; + // 2.两属性都为镜像属性,并且指向对方,此时无需后续处理; + // 3.两属性仅有任意一属性为镜像属性,此时镜像属性必然未指向当前原始属性,此时应该抛出异常; + if (originalAttribute instanceof MirroredAnnotationAttribute + || linkedAttribute instanceof MirroredAnnotationAttribute) { + checkMirrored(originalAttribute, linkedAttribute); + return; + } + + // 校验镜像关系 + checkMirrorRelation(annotation, originalAttribute, linkedAttribute); + // 包装这一对镜像属性,并替换原注解中的对应属性 + final AnnotationAttribute mirroredOriginalAttribute = new MirroredAnnotationAttribute(originalAttribute, linkedAttribute); + originalAnnotation.setAttribute(originalAttribute.getAttributeName(), mirroredOriginalAttribute); + final AnnotationAttribute mirroredTargetAttribute = new MirroredAnnotationAttribute(linkedAttribute, originalAttribute); + linkedAnnotation.setAttribute(annotation.attribute(), mirroredTargetAttribute); + } + + /** + * 检查映射关系是否正确 + */ + private void checkMirrored(AnnotationAttribute original, AnnotationAttribute mirror) { + final boolean originalAttributeMirrored = original instanceof MirroredAnnotationAttribute; + final boolean mirrorAttributeMirrored = mirror instanceof MirroredAnnotationAttribute; + + // 校验通过 + final boolean passed = originalAttributeMirrored && mirrorAttributeMirrored + && ObjectUtil.equals(((MirroredAnnotationAttribute)original).getLinked(), ((MirroredAnnotationAttribute)mirror).getOriginal()); + if (passed) { + return; + } + + // 校验失败,拼装异常信息用于抛出异常 + String errorMsg; + // 原始字段已经跟其他字段形成镜像 + if (originalAttributeMirrored && !mirrorAttributeMirrored) { + errorMsg = CharSequenceUtil.format( + "attribute [{}] cannot mirror for [{}], because it's already mirrored for [{}]", + original.getAttribute(), mirror.getAttribute(), ((MirroredAnnotationAttribute)original).getLinked() + ); + } + // 镜像字段已经跟其他字段形成镜像 + else if (!originalAttributeMirrored && mirrorAttributeMirrored) { + errorMsg = CharSequenceUtil.format( + "attribute [{}] cannot mirror for [{}], because it's already mirrored for [{}]", + mirror.getAttribute(), original.getAttribute(), ((MirroredAnnotationAttribute)mirror).getLinked() + ); + } + // 两者都形成了镜像,但是都未指向对方,理论上不会存在该情况 + else { + errorMsg = CharSequenceUtil.format( + "attribute [{}] cannot mirror for [{}], because [{}] already mirrored for [{}] and [{}] already mirrored for [{}]", + mirror.getAttribute(), original.getAttribute(), + mirror.getAttribute(), ((MirroredAnnotationAttribute)mirror).getLinked(), + original.getAttribute(), ((MirroredAnnotationAttribute)original).getLinked() + ); + } + + throw new IllegalArgumentException(errorMsg); + } + + /** + * 基本校验 + */ + private void checkMirrorRelation(Link annotation, AnnotationAttribute original, AnnotationAttribute mirror) { + // 镜像属性必须存在 + checkLinkedAttributeNotNull(original, mirror, annotation); + // 镜像属性返回值必须一致 + checkAttributeType(original, mirror); + // 镜像属性上必须存在对应的注解 + final Link mirrorAttributeAnnotation = getLinkAnnotation(mirror, RelationType.MIRROR_FOR); + Assert.isTrue( + ObjectUtil.isNotNull(mirrorAttributeAnnotation) && RelationType.MIRROR_FOR.equals(mirrorAttributeAnnotation.type()), + "mirror attribute [{}] of original attribute [{}] must marked by @Link, and also @LinkType.type() must is [{}]", + mirror.getAttribute(), original.getAttribute(), RelationType.MIRROR_FOR + ); + checkLinkedSelf(original, mirror); + } + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/annotation/MirroredAnnotationAttribute.java b/hutool-core/src/main/java/cn/hutool/core/annotation/MirroredAnnotationAttribute.java new file mode 100644 index 0000000000000000000000000000000000000000..6bb800079f0177ce921d98e178d95e26da2c3b21 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/annotation/MirroredAnnotationAttribute.java @@ -0,0 +1,48 @@ +package cn.hutool.core.annotation; + +import cn.hutool.core.lang.Assert; + +/** + * 表示存在对应镜像属性的注解属性,当获取值时将根据{@link RelationType#MIRROR_FOR}的规则进行处理 + * + * @author huangchengxing + * @see MirrorLinkAnnotationPostProcessor + * @see RelationType#MIRROR_FOR + */ +public class MirroredAnnotationAttribute extends AbstractWrappedAnnotationAttribute { + + public MirroredAnnotationAttribute(AnnotationAttribute origin, AnnotationAttribute linked) { + super(origin, linked); + } + + @Override + public Object getValue() { + final boolean originIsDefault = original.isValueEquivalentToDefaultValue(); + final boolean targetIsDefault = linked.isValueEquivalentToDefaultValue(); + final Object originValue = original.getValue(); + final Object targetValue = linked.getValue(); + + // 都为默认值,或都为非默认值时,两方法的返回值必须相等 + if (originIsDefault == targetIsDefault) { + Assert.equals( + originValue, targetValue, + "the values of attributes [{}] and [{}] that mirror each other are different: [{}] <==> [{}]", + original.getAttribute(), linked.getAttribute(), originValue, targetValue + ); + return originValue; + } + + // 两者有一者不为默认值时,优先返回非默认值 + return originIsDefault ? targetValue : originValue; + } + + /** + * 当{@link #original}与{@link #linked}都为默认值时返回{@code true} + * + * @return 是否 + */ + @Override + public boolean isValueEquivalentToDefaultValue() { + return original.isValueEquivalentToDefaultValue() && linked.isValueEquivalentToDefaultValue(); + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/annotation/RelationType.java b/hutool-core/src/main/java/cn/hutool/core/annotation/RelationType.java new file mode 100644 index 0000000000000000000000000000000000000000..1c2248cf37269a58fb9e1fae28463dbf72303093 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/annotation/RelationType.java @@ -0,0 +1,50 @@ +package cn.hutool.core.annotation; + +/** + *

注解属性的关系类型
+ * 若将被{@link Link}注解的属性称为“原始属性”,而在{@link Link}注解中指向的注解属性称为“关联属性”, + * 则该枚举用于描述“原始属性”与“关联属性”在{@link SynthesizedAggregateAnnotation}处理过程中的作用关系。
+ * 根据在{@link Link#type()}中指定的关系类型的不同,通过{@link SynthesizedAggregateAnnotation}合成的注解的属性值也将有所变化。 + * + *

当一个注解中的所有属性同时具备多种关系时,将依次按下述顺序处理: + *

    + *
  1. 属性上的{@link Alias}注解;
  2. + *
  3. 属性上的{@link Link}注解,且{@link Link#type()}为{@link #MIRROR_FOR};
  4. + *
  5. 属性上的{@link Link}注解,且{@link Link#type()}为{@link #FORCE_ALIAS_FOR};
  6. + *
  7. 属性上的{@link Link}注解,且{@link Link#type()}为{@link #ALIAS_FOR};
  8. + *
+ * + * @author huangchengxing + * @see SynthesizedAggregateAnnotation + * @see Link + */ +public enum RelationType { + + /** + *

表示注解的属性与指定的属性互为镜像,通过一个属性将能够获得对方的值。
+ * 它们遵循下述规则: + *

    + *
  • 互为镜像的两个属性,必须同时通过指定模式为{@code MIRROR_FOR}的{@link Link}注解指定对方;
  • + *
  • 互为镜像的两个属性,类型必须一致;
  • + *
  • 互为镜像的两个属性在获取值,且两者的值皆不同时,必须且仅允许有一个非默认值,该值被优先返回;
  • + *
  • 互为镜像的两个属性,在值都为默认值或都不为默认值时,两者的值必须相等;
  • + *
+ */ + MIRROR_FOR, + + /** + *

表示“原始属性”将作为“关联属性”的别名。 + *

    + *
  • 当“原始属性”为默认值时,获取“关联属性”将返回“关联属性”本身的值;
  • + *
  • 当“原始属性”不为默认值时,获取“关联属性”将返回“原始属性”的值;
  • + *
+ */ + ALIAS_FOR, + + /** + *

表示“原始属性”将强制作为“关联属性”的别名。效果等同于在“原始属性”上添加{@link Alias}注解, + * 任何情况下,获取“关联属性”的值都将直接返回“原始属性”的值 + */ + FORCE_ALIAS_FOR; + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/annotation/SynthesizedAggregateAnnotation.java b/hutool-core/src/main/java/cn/hutool/core/annotation/SynthesizedAggregateAnnotation.java new file mode 100644 index 0000000000000000000000000000000000000000..e006f3f0813a8658156f6e189a4a5a7406402e1a --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/annotation/SynthesizedAggregateAnnotation.java @@ -0,0 +1,102 @@ +package cn.hutool.core.annotation; + +import java.lang.annotation.Annotation; + +/** + *

表示基于特定规则聚合,将一组注解聚合而来的注解对象, + * 该注解对象允许根据一定规则“合成”一些跟原始注解属性不一样合成注解。 + * + *

合成注解一般被用于处理类层级结果中具有直接或间接关联的注解对象, + * 当实例被创建时,会获取到这些注解对象,并使用{@link SynthesizedAnnotationSelector}对类型相同的注解进行过滤, + * 并最终得到类型不重复的有效注解对象。这些有效注解将被包装为{@link SynthesizedAnnotation}, + * 然后最终用于“合成”一个{@link SynthesizedAggregateAnnotation}。
+ * {@link SynthesizedAnnotationSelector}是合成注解生命周期中的第一个钩子, + * 自定义选择器以拦截原始注解被扫描的过程。 + * + *

当合成注解完成对待合成注解的扫描,并完成了必要属性的加载后, + * 将会按顺序依次调用{@link SynthesizedAnnotationPostProcessor}, + * 注解后置处理器允许用于对完成注解的待合成注解进行二次调整, + * 该钩子一般用于根据{@link Link}注解对属性进行调整。
+ * {@link SynthesizedAnnotationPostProcessor}是合成注解生命周期中的第二个钩子, + * 自定义后置处理器以拦截原始在转为待合成注解后的初始化过程。 + * + *

合成注解允许通过{@link #synthesize(Class)}合成一个指定的注解对象, + * 该方法返回的注解对象可能是原始的注解对象,也有可能通过动态代理的方式生成, + * 该对象实例的属性不一定来自对象本身,而是来自于经过{@link SynthesizedAnnotationAttributeProcessor} + * 处理后的、用于合成当前实例的全部关联注解的相关属性。
+ * {@link SynthesizedAnnotationAttributeProcessor}是合成注解生命周期中的第三个钩子, + * 自定义属性处理器以拦截合成注解的取值过程。 + * + * @author huangchengxing + * @see AnnotationSynthesizer + * @see SynthesizedAnnotation + * @see SynthesizedAnnotationSelector + * @see SynthesizedAnnotationAttributeProcessor + * @see SynthesizedAnnotationPostProcessor + * @see GenericSynthesizedAggregateAnnotation + */ +public interface SynthesizedAggregateAnnotation extends AggregateAnnotation, Hierarchical, AnnotationSynthesizer, AnnotationAttributeValueProvider { + + // ================== hierarchical ================== + + /** + * 距离{@link #getRoot()}返回值的垂直距离, + * 默认聚合注解即为根对象,因此返回0 + * + * @return 距离{@link #getRoot()}返回值的水平距离, + */ + @Override + default int getVerticalDistance() { + return 0; + } + + /** + * 距离{@link #getRoot()}返回值的水平距离, + * 默认聚合注解即为根对象,因此返回0 + * + * @return 距离{@link #getRoot()}返回值的水平距离, + */ + @Override + default int getHorizontalDistance() { + return 0; + } + + // ================== synthesize ================== + + /** + * 获取在聚合中存在的指定注解对象 + * + * @param annotationType 注解类型 + * @param 注解类型 + * @return 注解对象 + */ + T getAnnotation(Class annotationType); + + /** + * 获取合成注解属性处理器 + * + * @return 合成注解属性处理器 + */ + SynthesizedAnnotationAttributeProcessor getAnnotationAttributeProcessor(); + + /** + * 获取当前的注解类型 + * + * @return 注解类型 + */ + @Override + default Class annotationType() { + return this.getClass(); + } + + /** + * 从聚合中获取指定类型的属性值 + * + * @param attributeName 属性名称 + * @param attributeType 属性类型 + * @return 属性值 + */ + @Override + Object getAttributeValue(String attributeName, Class attributeType); + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/annotation/SynthesizedAnnotation.java b/hutool-core/src/main/java/cn/hutool/core/annotation/SynthesizedAnnotation.java new file mode 100644 index 0000000000000000000000000000000000000000..418059974a43de2b3564728381845f5104b805fe --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/annotation/SynthesizedAnnotation.java @@ -0,0 +1,96 @@ +package cn.hutool.core.annotation; + +import cn.hutool.core.collection.CollUtil; + +import java.lang.annotation.Annotation; +import java.util.Map; +import java.util.function.UnaryOperator; + +/** + *

用于在{@link SynthesizedAggregateAnnotation}中表示一个处于合成状态的注解对象。
+ * 当对多个合成注解排序时,默认使用{@link #DEFAULT_HIERARCHICAL_COMPARATOR}进行排序, + * 从保证合成注解按{@link #getVerticalDistance()}与{@link #getHorizontalDistance()}的返回值保持有序, + * 从而使得距离根元素更接近的注解对象在被处理是具有更高的优先级。 + * + * @author huangchengxing + * @see SynthesizedAggregateAnnotation + */ +public interface SynthesizedAnnotation extends Annotation, Hierarchical, AnnotationAttributeValueProvider { + + /** + * 获取被合成的注解对象 + * + * @return 注解对象 + */ + Annotation getAnnotation(); + + /** + * 获取该合成注解与根对象的垂直距离。 + * 默认情况下,该距离即为当前注解与根对象之间相隔的层级数。 + * + * @return 合成注解与根对象的垂直距离 + */ + @Override + int getVerticalDistance(); + + /** + * 获取该合成注解与根对象的水平距离。 + * 默认情况下,该距离即为当前注解与根对象之间相隔的已经被扫描到的注解数。 + * + * @return 合成注解与根对象的水平距离 + */ + @Override + int getHorizontalDistance(); + + /** + * 注解是否存在该名称相同,且类型一致的属性 + * + * @param attributeName 属性名 + * @param returnType 返回值类型 + * @return 是否存在该属性 + */ + boolean hasAttribute(String attributeName, Class returnType); + + /** + * 获取该注解的全部属性 + * + * @return 注解属性 + */ + Map getAttributes(); + + /** + * 设置该注解的全部属性 + * + * @param attributes 注解属性 + */ + default void setAttributes(Map attributes) { + if (CollUtil.isNotEmpty(attributes)) { + attributes.forEach(this::setAttribute); + } + } + + /** + * 设置属性值 + * + * @param attributeName 属性名称 + * @param attribute 注解属性 + */ + void setAttribute(String attributeName, AnnotationAttribute attribute); + + /** + * 替换属性值 + * + * @param attributeName 属性名 + * @param operator 替换操作 + */ + void replaceAttribute(String attributeName, UnaryOperator operator); + + /** + * 获取属性值 + * + * @param attributeName 属性名 + * @return 属性值 + */ + Object getAttributeValue(String attributeName); + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/annotation/SynthesizedAnnotationAttributeProcessor.java b/hutool-core/src/main/java/cn/hutool/core/annotation/SynthesizedAnnotationAttributeProcessor.java new file mode 100644 index 0000000000000000000000000000000000000000..7a7debdc1a754417527226ca868b246b479f074e --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/annotation/SynthesizedAnnotationAttributeProcessor.java @@ -0,0 +1,24 @@ +package cn.hutool.core.annotation; + +import java.util.Collection; + +/** + * 合成注解属性选择器。用于在{@link SynthesizedAggregateAnnotation}中从指定类型的合成注解里获取到对应的属性值 + * + * @author huangchengxing + */ +@FunctionalInterface +public interface SynthesizedAnnotationAttributeProcessor { + + /** + * 从一批被合成注解中,获取指定名称与类型的属性值 + * + * @param attributeName 属性名称 + * @param attributeType 属性类型 + * @param synthesizedAnnotations 被合成的注解 + * @param 属性类型 + * @return 属性值 + */ + R getAttributeValue(String attributeName, Class attributeType, Collection synthesizedAnnotations); + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/annotation/SynthesizedAnnotationPostProcessor.java b/hutool-core/src/main/java/cn/hutool/core/annotation/SynthesizedAnnotationPostProcessor.java new file mode 100644 index 0000000000000000000000000000000000000000..91e701be8a647f6771104d647bed3ea1485ab0c7 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/annotation/SynthesizedAnnotationPostProcessor.java @@ -0,0 +1,71 @@ +package cn.hutool.core.annotation; + +import cn.hutool.core.comparator.CompareUtil; + +import java.util.Comparator; + +/** + *

被合成注解后置处理器,用于在{@link SynthesizedAggregateAnnotation}加载完所有待合成注解后, + * 再对加载好的{@link SynthesizedAnnotation}进行后置处理。
+ * 当多个{@link SynthesizedAnnotationPostProcessor}需要一起执行时,将按照{@link #order()}的返回值进行排序, + * 该值更小的处理器将被优先执行。 + * + *

该接口存在多个实现类,调用者应当保证在任何时候,对一批后置处理器的调用顺序都符合: + *

    + *
  • {@link AliasAnnotationPostProcessor};
  • + *
  • {@link MirrorLinkAnnotationPostProcessor};
  • + *
  • {@link AliasLinkAnnotationPostProcessor};
  • + *
  • 其他后置处理器;
  • + *
+ * + * @author huangchengxing + * @see AliasAnnotationPostProcessor + * @see MirrorLinkAnnotationPostProcessor + * @see AliasLinkAnnotationPostProcessor + */ +public interface SynthesizedAnnotationPostProcessor extends Comparable { + + /** + * 属性上带有{@link Alias}的注解对象的后置处理器 + */ + AliasAnnotationPostProcessor ALIAS_ANNOTATION_POST_PROCESSOR = new AliasAnnotationPostProcessor(); + + /** + * 属性上带有{@link Link},且与其他注解的属性存在镜像关系的注解对象的后置处理器 + */ + MirrorLinkAnnotationPostProcessor MIRROR_LINK_ANNOTATION_POST_PROCESSOR = new MirrorLinkAnnotationPostProcessor(); + + /** + * 属性上带有{@link Link},且与其他注解的属性存在别名关系的注解对象的后置处理器 + */ + AliasLinkAnnotationPostProcessor ALIAS_LINK_ANNOTATION_POST_PROCESSOR = new AliasLinkAnnotationPostProcessor(); + + /** + * 在一组后置处理器中被调用的顺序,越小越靠前 + * + * @return 排序值 + */ + default int order() { + return Integer.MAX_VALUE; + } + + /** + * 比较两个后置处理器的{@link #order()}返回值 + * + * @param o 比较对象 + * @return 大小 + */ + @Override + default int compareTo(SynthesizedAnnotationPostProcessor o) { + return CompareUtil.compare(this, o, Comparator.comparing(SynthesizedAnnotationPostProcessor::order)); + } + + /** + * 给定指定被合成注解与其所属的合成注解聚合器实例,经过处理后返回最终 + * + * @param synthesizedAnnotation 合成的注解 + * @param synthesizer 注解合成器 + */ + void process(SynthesizedAnnotation synthesizedAnnotation, AnnotationSynthesizer synthesizer); + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/annotation/SynthesizedAnnotationProxy.java b/hutool-core/src/main/java/cn/hutool/core/annotation/SynthesizedAnnotationProxy.java new file mode 100644 index 0000000000000000000000000000000000000000..00b86c95de591362128dba325d4fdc1295a57bf1 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/annotation/SynthesizedAnnotationProxy.java @@ -0,0 +1,163 @@ +package cn.hutool.core.annotation; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.lang.Opt; +import cn.hutool.core.text.CharSequenceUtil; +import cn.hutool.core.util.ClassUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.ReflectUtil; + +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.function.BiFunction; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * 合成注解代理类,用于为{@link SynthesizedAnnotation}生成对应的合成注解代理对象 + * + * @author huangchengxing + * @see SynthesizedAnnotation + * @see AnnotationAttributeValueProvider + */ +public class SynthesizedAnnotationProxy implements InvocationHandler { + + private final AnnotationAttributeValueProvider annotationAttributeValueProvider; + private final SynthesizedAnnotation annotation; + private final Map> methods; + + /** + * 创建一个代理注解,生成的代理对象将是{@link SyntheticProxyAnnotation}与指定的注解类的子类。 + * + * @param 注解类型 + * @param annotationType 注解类型 + * @param annotationAttributeValueProvider 注解属性值获取器 + * @param annotation 合成注解 + * @return 代理注解 + */ + @SuppressWarnings("unchecked") + public static T create( + Class annotationType, + AnnotationAttributeValueProvider annotationAttributeValueProvider, + SynthesizedAnnotation annotation) { + if (ObjectUtil.isNull(annotation)) { + return null; + } + final SynthesizedAnnotationProxy proxyHandler = new SynthesizedAnnotationProxy(annotationAttributeValueProvider, annotation); + if (ObjectUtil.isNull(annotation)) { + return null; + } + return (T) Proxy.newProxyInstance( + annotationType.getClassLoader(), + new Class[]{annotationType, SyntheticProxyAnnotation.class}, + proxyHandler + ); + } + + /** + * 创建一个代理注解,生成的代理对象将是{@link SyntheticProxyAnnotation}与指定的注解类的子类。 + * + * @param 注解类型 + * @param annotationType 注解类型 + * @param annotation 合成注解 + * @return 代理注解 + */ + public static T create( + Class annotationType, SynthesizedAnnotation annotation) { + return create(annotationType, annotation, annotation); + } + + /** + * 该类是否为通过{@code SynthesizedAnnotationProxy}生成的代理类 + * + * @param annotationType 注解类型 + * @return 是否 + */ + public static boolean isProxyAnnotation(Class annotationType) { + return ClassUtil.isAssignable(SyntheticProxyAnnotation.class, annotationType); + } + + SynthesizedAnnotationProxy(AnnotationAttributeValueProvider annotationAttributeValueProvider, SynthesizedAnnotation annotation) { + Assert.notNull(annotationAttributeValueProvider, "annotationAttributeValueProvider must not null"); + Assert.notNull(annotation, "annotation must not null"); + this.annotationAttributeValueProvider = annotationAttributeValueProvider; + this.annotation = annotation; + this.methods = new HashMap<>(9); + loadMethods(); + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + return Opt.ofNullable(methods.get(method.getName())) + .map(m -> m.apply(method, args)) + .orElseGet(() -> ReflectUtil.invoke(annotation.getAnnotation(), method, args)); + } + + // ========================= 代理方法 ========================= + + void loadMethods() { + // 非用户属性 + methods.put("toString", (method, args) -> proxyToString()); + methods.put("hashCode", (method, args) -> proxyHashCode()); + methods.put("getSynthesizedAnnotation", (method, args) -> proxyGetSynthesizedAnnotation()); + methods.put("getRoot", (method, args) -> annotation.getRoot()); + methods.put("getVerticalDistance", (method, args) -> annotation.getVerticalDistance()); + methods.put("getHorizontalDistance", (method, args) -> annotation.getHorizontalDistance()); + methods.put("hasAttribute", (method, args) -> annotation.hasAttribute((String) args[0], (Class) args[1])); + methods.put("getAttributes", (method, args) -> annotation.getAttributes()); + methods.put("setAttribute", (method, args) -> { + throw new UnsupportedOperationException("proxied annotation can not reset attributes"); + }); + methods.put("getAttributeValue", (method, args) -> annotation.getAttributeValue((String) args[0])); + methods.put("annotationType", (method, args) -> annotation.annotationType()); + + // 可以被合成的用户属性 + Stream.of(ClassUtil.getDeclaredMethods(annotation.getAnnotation().annotationType())) + .filter(m -> !methods.containsKey(m.getName())) + .forEach(m -> methods.put(m.getName(), (method, args) -> proxyAttributeValue(method))); + } + + private String proxyToString() { + final String attributes = Stream.of(ClassUtil.getDeclaredMethods(annotation.getAnnotation().annotationType())) + .filter(AnnotationUtil::isAttributeMethod) + .map(method -> CharSequenceUtil.format( + "{}={}", method.getName(), proxyAttributeValue(method)) + ) + .collect(Collectors.joining(", ")); + return CharSequenceUtil.format("@{}({})", annotation.annotationType().getName(), attributes); + } + + private int proxyHashCode() { + return Objects.hash(annotationAttributeValueProvider, annotation); + } + + private Object proxyGetSynthesizedAnnotation() { + return annotation; + } + + private Object proxyAttributeValue(Method attributeMethod) { + return annotationAttributeValueProvider.getAttributeValue(attributeMethod.getName(), attributeMethod.getReturnType()); + } + + /** + * 通过代理类生成的合成注解 + * + * @author huangchengxing + */ + interface SyntheticProxyAnnotation extends SynthesizedAnnotation { + + /** + * 获取该代理注解对应的已合成注解 + * + * @return 理注解对应的已合成注解 + */ + SynthesizedAnnotation getSynthesizedAnnotation(); + + } + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/annotation/SynthesizedAnnotationSelector.java b/hutool-core/src/main/java/cn/hutool/core/annotation/SynthesizedAnnotationSelector.java new file mode 100644 index 0000000000000000000000000000000000000000..fa2b91eb73cd38bcb6a460dbfeb166137b023416 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/annotation/SynthesizedAnnotationSelector.java @@ -0,0 +1,82 @@ +package cn.hutool.core.annotation; + +/** + * 注解选择器,指定两个注解,选择其中一个返回。
+ * 该接口用于在{@link SynthesizedAggregateAnnotation}中用于从一批相同的注解对象中筛选最终用于合成注解对象。 + * + * @author huangchengxing + */ +@FunctionalInterface +public interface SynthesizedAnnotationSelector { + + /** + * 返回距离根对象更近的注解,当距离一样时优先返回旧注解 + */ + SynthesizedAnnotationSelector NEAREST_AND_OLDEST_PRIORITY = new NearestAndOldestPrioritySelector(); + + /** + * 返回距离根对象更近的注解,当距离一样时优先返回新注解 + */ + SynthesizedAnnotationSelector NEAREST_AND_NEWEST_PRIORITY = new NearestAndNewestPrioritySelector(); + + /** + * 返回距离根对象更远的注解,当距离一样时优先返回旧注解 + */ + SynthesizedAnnotationSelector FARTHEST_AND_OLDEST_PRIORITY = new FarthestAndOldestPrioritySelector(); + + /** + * 返回距离根对象更远的注解,当距离一样时优先返回新注解 + */ + SynthesizedAnnotationSelector FARTHEST_AND_NEWEST_PRIORITY = new FarthestAndNewestPrioritySelector(); + + /** + * 比较两个被合成的注解,选择其中的一个并返回 + * + * @param 复合注解类型 + * @param oldAnnotation 已存在的注解,该参数不允许为空 + * @param newAnnotation 新获取的注解,该参数不允许为空 + * @return 被合成的注解 + */ + T choose(T oldAnnotation, T newAnnotation); + + /** + * 返回距离根对象更近的注解,当距离一样时优先返回旧注解 + */ + class NearestAndOldestPrioritySelector implements SynthesizedAnnotationSelector { + @Override + public T choose(T oldAnnotation, T newAnnotation) { + return Hierarchical.Selector.NEAREST_AND_OLDEST_PRIORITY.choose(oldAnnotation, newAnnotation); + } + } + + /** + * 返回距离根对象更近的注解,当距离一样时优先返回新注解 + */ + class NearestAndNewestPrioritySelector implements SynthesizedAnnotationSelector { + @Override + public T choose(T oldAnnotation, T newAnnotation) { + return Hierarchical.Selector.NEAREST_AND_NEWEST_PRIORITY.choose(oldAnnotation, newAnnotation); + } + } + + /** + * 返回距离根对象更远的注解,当距离一样时优先返回旧注解 + */ + class FarthestAndOldestPrioritySelector implements SynthesizedAnnotationSelector { + @Override + public T choose(T oldAnnotation, T newAnnotation) { + return Hierarchical.Selector.FARTHEST_AND_OLDEST_PRIORITY.choose(oldAnnotation, newAnnotation); + } + } + + /** + * 返回距离根对象更远的注解,当距离一样时优先返回新注解 + */ + class FarthestAndNewestPrioritySelector implements SynthesizedAnnotationSelector { + @Override + public T choose(T oldAnnotation, T newAnnotation) { + return Hierarchical.Selector.FARTHEST_AND_NEWEST_PRIORITY.choose(oldAnnotation, newAnnotation); + } + } + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/annotation/WrappedAnnotationAttribute.java b/hutool-core/src/main/java/cn/hutool/core/annotation/WrappedAnnotationAttribute.java new file mode 100644 index 0000000000000000000000000000000000000000..6ca5a3f527662f699ad835f2a28db8b5314c433a --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/annotation/WrappedAnnotationAttribute.java @@ -0,0 +1,125 @@ +package cn.hutool.core.annotation; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.Collection; + +/** + *

表示一个被包装过的{@link AnnotationAttribute}, + * 该实例中的一些方法可能会被代理到另一个注解属性对象中, + * 从而使得通过原始的注解属性的方法获取到另一注解属性的值。
+ * 除了{@link #getValue()}以外,其他方法的返回值应当尽可能与{@link #getOriginal()} + * 返回的{@link AnnotationAttribute}对象的方法返回值一致。 + * + *

当包装类被包装了多层后,则规则生效优先级按包装的先后顺序倒序排序, + * 比如a、b互为镜像,此时a、b两属性应当都被{@link MirroredAnnotationAttribute}包装, + * 若再指定c为a的别名字段,则c、a、b都要在原基础上再次包装一层{@link AliasedAnnotationAttribute}。
+ * 此时a、b同时被包装了两层,则执行时,优先执行{@link AliasedAnnotationAttribute}的逻辑, + * 当该规则不生效时,比如c只有默认值,此时上一次的{@link MirroredAnnotationAttribute}的逻辑才会生效。 + * + *

被包装的{@link AnnotationAttribute}实际结构为一颗二叉树, + * 当包装类再次被包装时,实际上等于又添加了一个新的根节点, + * 此时需要同时更新树的全部关联叶子节点。 + * + * @author huangchengxing + * @see AnnotationAttribute + * @see ForceAliasedAnnotationAttribute + * @see AliasedAnnotationAttribute + * @see MirroredAnnotationAttribute + */ +public interface WrappedAnnotationAttribute extends AnnotationAttribute { + + // =========================== 新增方法 =========================== + + /** + * 获取被包装的{@link AnnotationAttribute}对象,该对象也可能是{@link AnnotationAttribute} + * + * @return 被包装的{@link AnnotationAttribute}对象 + */ + AnnotationAttribute getOriginal(); + + /** + * 获取最初的被包装的{@link AnnotationAttribute} + * + * @return 最初的被包装的{@link AnnotationAttribute} + */ + AnnotationAttribute getNonWrappedOriginal(); + + /** + * 获取包装{@link #getOriginal()}的{@link AnnotationAttribute}对象,该对象也可能是{@link AnnotationAttribute} + * + * @return 包装对象 + */ + AnnotationAttribute getLinked(); + + /** + * 遍历以当前实例为根节点的树结构,获取所有未被包装的属性 + * + * @return 叶子节点 + */ + Collection getAllLinkedNonWrappedAttributes(); + + // =========================== 代理实现 =========================== + + /** + * 获取注解对象 + * + * @return 注解对象 + */ + @Override + default Annotation getAnnotation() { + return getOriginal().getAnnotation(); + } + + /** + * 获取注解属性对应的方法 + * + * @return 注解属性对应的方法 + */ + @Override + default Method getAttribute() { + return getOriginal().getAttribute(); + } + + /** + * 该注解属性的值是否等于默认值
+ * 默认仅当{@link #getOriginal()}与{@link #getLinked()}返回的注解属性 + * 都为默认值时,才返回{@code true} + * + * @return 该注解属性的值是否等于默认值 + */ + @Override + boolean isValueEquivalentToDefaultValue(); + + /** + * 获取属性类型 + * + * @return 属性类型 + */ + @Override + default Class getAttributeType() { + return getOriginal().getAttributeType(); + } + + /** + * 获取属性上的注解 + * + * @param annotationType 注解类型 + * @return 注解对象 + */ + @Override + default T getAnnotation(Class annotationType) { + return getOriginal().getAnnotation(annotationType); + } + + /** + * 当前注解属性是否已经被{@link WrappedAnnotationAttribute}包装 + * + * @return boolean + */ + @Override + default boolean isWrapped() { + return true; + } + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/annotation/scanner/AbstractTypeAnnotationScanner.java b/hutool-core/src/main/java/cn/hutool/core/annotation/scanner/AbstractTypeAnnotationScanner.java new file mode 100644 index 0000000000000000000000000000000000000000..8a3b2e7d1aa71ed794c8f7f1c6e7c412f18a7e5b --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/annotation/scanner/AbstractTypeAnnotationScanner.java @@ -0,0 +1,288 @@ +package cn.hutool.core.annotation.scanner; + +import cn.hutool.core.annotation.AnnotationUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ObjectUtil; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Proxy; +import java.util.*; +import java.util.function.BiConsumer; +import java.util.function.Predicate; +import java.util.function.UnaryOperator; + +/** + * 为需要从类的层级结构中获取注解的{@link AnnotationScanner}提供基本实现 + * + * @author huangchengxing + */ +public abstract class AbstractTypeAnnotationScanner> implements AnnotationScanner { + + /** + * 是否允许扫描父类 + */ + private boolean includeSuperClass; + + /** + * 是否允许扫描父接口 + */ + private boolean includeInterfaces; + + /** + * 过滤器,若类型无法通过该过滤器,则该类型及其树结构将直接不被查找 + */ + private Predicate> filter; + + /** + * 排除的类型,以上类型及其树结构将直接不被查找 + */ + private final Set> excludeTypes; + + /** + * 转换器 + */ + private final List>> converters; + + /** + * 是否有转换器 + */ + private boolean hasConverters; + + /** + * 当前实例 + */ + private final T typedThis; + + /** + * 构造一个类注解扫描器 + * + * @param includeSuperClass 是否允许扫描父类 + * @param includeInterfaces 是否允许扫描父接口 + * @param filter 过滤器 + * @param excludeTypes 不包含的类型 + */ + @SuppressWarnings("unchecked") + protected AbstractTypeAnnotationScanner(boolean includeSuperClass, boolean includeInterfaces, Predicate> filter, Set> excludeTypes) { + Assert.notNull(filter, "filter must not null"); + Assert.notNull(excludeTypes, "excludeTypes must not null"); + this.includeSuperClass = includeSuperClass; + this.includeInterfaces = includeInterfaces; + this.filter = filter; + this.excludeTypes = excludeTypes; + this.converters = new ArrayList<>(); + this.typedThis = (T) this; + } + + /** + * 是否允许扫描父类 + * + * @return 是否允许扫描父类 + */ + public boolean isIncludeSuperClass() { + return includeSuperClass; + } + + /** + * 是否允许扫描父接口 + * + * @return 是否允许扫描父接口 + */ + public boolean isIncludeInterfaces() { + return includeInterfaces; + } + + /** + * 设置过滤器,若类型无法通过该过滤器,则该类型及其树结构将直接不被查找 + * + * @param filter 过滤器 + * @return 当前实例 + */ + public T setFilter(Predicate> filter) { + Assert.notNull(filter, "filter must not null"); + this.filter = filter; + return typedThis; + } + + /** + * 添加不扫描的类型,该类型及其树结构将直接不被查找 + * + * @param excludeTypes 不扫描的类型 + * @return 当前实例 + */ + public T addExcludeTypes(Class... excludeTypes) { + CollUtil.addAll(this.excludeTypes, excludeTypes); + return typedThis; + } + + /** + * 添加转换器 + * + * @param converter 转换器 + * @return 当前实例 + * @see JdkProxyClassConverter + */ + public T addConverters(UnaryOperator> converter) { + Assert.notNull(converter, "converter must not null"); + this.converters.add(converter); + if (!this.hasConverters) { + this.hasConverters = CollUtil.isNotEmpty(this.converters); + } + return typedThis; + } + + /** + * 是否允许扫描父类 + * + * @param includeSuperClass 是否 + * @return 当前实例 + */ + protected T setIncludeSuperClass(boolean includeSuperClass) { + this.includeSuperClass = includeSuperClass; + return typedThis; + } + + /** + * 是否允许扫描父接口 + * + * @param includeInterfaces 是否 + * @return 当前实例 + */ + protected T setIncludeInterfaces(boolean includeInterfaces) { + this.includeInterfaces = includeInterfaces; + return typedThis; + } + + /** + * 则根据广度优先递归扫描类的层级结构,并对层级结构中类/接口声明的层级索引和它们声明的注解对象进行处理 + * + * @param consumer 对获取到的注解和注解对应的层级索引的处理 + * @param annotatedEle 注解元素 + * @param filter 注解过滤器,无法通过过滤器的注解不会被处理。该参数允许为空。 + */ + @Override + public void scan(BiConsumer consumer, AnnotatedElement annotatedEle, Predicate filter) { + filter = ObjectUtil.defaultIfNull(filter, a -> annotation -> true); + final Class sourceClass = getClassFormAnnotatedElement(annotatedEle); + final Deque>> classDeque = CollUtil.newLinkedList(CollUtil.newArrayList(sourceClass)); + final Set> accessedTypes = new LinkedHashSet<>(); + int index = 0; + while (!classDeque.isEmpty()) { + final List> currClassQueue = classDeque.removeFirst(); + final List> nextClassQueue = new ArrayList<>(); + for (Class targetClass : currClassQueue) { + targetClass = convert(targetClass); + // 过滤不需要处理的类 + if (isNotNeedProcess(accessedTypes, targetClass)) { + continue; + } + accessedTypes.add(targetClass); + // 扫描父类 + scanSuperClassIfNecessary(nextClassQueue, targetClass); + // 扫描接口 + scanInterfaceIfNecessary(nextClassQueue, targetClass); + // 处理层级索引和注解 + final Annotation[] targetAnnotations = getAnnotationsFromTargetClass(annotatedEle, index, targetClass); + for (final Annotation annotation : targetAnnotations) { + if (AnnotationUtil.isNotJdkMateAnnotation(annotation.annotationType()) && filter.test(annotation)) { + consumer.accept(index, annotation); + } + } + index++; + } + if (CollUtil.isNotEmpty(nextClassQueue)) { + classDeque.addLast(nextClassQueue); + } + } + } + + /** + * 从要搜索的注解元素上获得要递归的类型 + * + * @param annotatedElement 注解元素 + * @return 要递归的类型 + */ + protected abstract Class getClassFormAnnotatedElement(AnnotatedElement annotatedElement); + + /** + * 从类上获取最终所需的目标注解 + * + * @param source 最初的注解元素 + * @param index 类的层级索引 + * @param targetClass 类 + * @return 最终所需的目标注解 + */ + protected abstract Annotation[] getAnnotationsFromTargetClass(AnnotatedElement source, int index, Class targetClass); + + /** + * 当前类是否不需要处理 + * + * @param accessedTypes 访问类型 + * @param targetClass 目标类型 + * @return 是否不需要处理 + */ + protected boolean isNotNeedProcess(Set> accessedTypes, Class targetClass) { + return ObjectUtil.isNull(targetClass) + || accessedTypes.contains(targetClass) + || excludeTypes.contains(targetClass) + || filter.negate().test(targetClass); + } + + /** + * 若{@link #includeInterfaces}为{@code true},则将目标类的父接口也添加到nextClasses + * + * @param nextClasses 下一个类集合 + * @param targetClass 目标类型 + */ + protected void scanInterfaceIfNecessary(List> nextClasses, Class targetClass) { + if (includeInterfaces) { + final Class[] interfaces = targetClass.getInterfaces(); + if (ArrayUtil.isNotEmpty(interfaces)) { + CollUtil.addAll(nextClasses, interfaces); + } + } + } + + /** + * 若{@link #includeSuperClass}为{@code true},则将目标类的父类也添加到nextClasses + * + * @param nextClassQueue 下一个类队列 + * @param targetClass 目标类型 + */ + protected void scanSuperClassIfNecessary(List> nextClassQueue, Class targetClass) { + if (includeSuperClass) { + final Class superClass = targetClass.getSuperclass(); + if (!ObjectUtil.equals(superClass, Object.class) && ObjectUtil.isNotNull(superClass)) { + nextClassQueue.add(superClass); + } + } + } + + /** + * 若存在转换器,则使用转换器对目标类进行转换 + * + * @param target 目标类 + * @return 转换后的类 + */ + protected Class convert(Class target) { + if (hasConverters) { + for (final UnaryOperator> converter : converters) { + target = converter.apply(target); + } + } + return target; + } + + /** + * 若类型为jdk代理类,则尝试转换为原始被代理类 + */ + public static class JdkProxyClassConverter implements UnaryOperator> { + @Override + public Class apply(Class sourceClass) { + return Proxy.isProxyClass(sourceClass) ? apply(sourceClass.getSuperclass()) : sourceClass; + } + } + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/annotation/scanner/AnnotationScanner.java b/hutool-core/src/main/java/cn/hutool/core/annotation/scanner/AnnotationScanner.java new file mode 100644 index 0000000000000000000000000000000000000000..dc108cb3544a04cee76eaf024a62eabb4fb8e73c --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/annotation/scanner/AnnotationScanner.java @@ -0,0 +1,198 @@ +package cn.hutool.core.annotation.scanner; + +import cn.hutool.core.annotation.AnnotationUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ObjectUtil; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Inherited; +import java.lang.reflect.AnnotatedElement; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + *

注解扫描器,用于从支持的可注解元素上获取所需注解 + * + *

默认提供了以下扫描方式: + *

    + *
  • {@link #NOTHING}:什么都不做,什么注解都不扫描;
  • + *
  • {@link #DIRECTLY}:扫描元素本身直接声明的注解,包括父类带有{@link Inherited}、被传递到元素上的注解;
  • + *
  • + * {@link #DIRECTLY_AND_META_ANNOTATION}:扫描元素本身直接声明的注解,包括父类带有{@link Inherited}、被传递到元素上的注解, + * 以及这些注解的元注解; + *
  • + *
  • {@link #SUPERCLASS}:扫描元素本身以及父类的层级结构中声明的注解;
  • + *
  • {@link #SUPERCLASS_AND_META_ANNOTATION}:扫描元素本身以及父类的层级结构中声明的注解,以及这些注解的元注解;
  • + *
  • {@link #INTERFACE}:扫描元素本身以及父接口的层级结构中声明的注解;
  • + *
  • {@link #INTERFACE_AND_META_ANNOTATION}:扫描元素本身以及父接口的层级结构中声明的注解,以及这些注解的元注解;
  • + *
  • {@link #TYPE_HIERARCHY}:扫描元素本身以及父类、父接口的层级结构中声明的注解;
  • + *
  • {@link #TYPE_HIERARCHY_AND_META_ANNOTATION}:扫描元素本身以及父接口、父接口的层级结构中声明的注解,以及这些注解的元注解;
  • + *
+ * + * @author huangchengxing + * @see TypeAnnotationScanner + * @see MethodAnnotationScanner + * @see FieldAnnotationScanner + * @see MetaAnnotationScanner + * @see ElementAnnotationScanner + * @see GenericAnnotationScanner + */ +public interface AnnotationScanner { + + // ============================ 预置的扫描器实例 ============================ + + /** + * 不扫描任何注解 + */ + AnnotationScanner NOTHING = new EmptyAnnotationScanner(); + + /** + * 扫描元素本身直接声明的注解,包括父类带有{@link Inherited}、被传递到元素上的注解的扫描器 + */ + AnnotationScanner DIRECTLY = new GenericAnnotationScanner(false, false, false); + + /** + * 扫描元素本身直接声明的注解,包括父类带有{@link Inherited}、被传递到元素上的注解,以及这些注解的元注解的扫描器 + */ + AnnotationScanner DIRECTLY_AND_META_ANNOTATION = new GenericAnnotationScanner(true, false, false); + + /** + * 扫描元素本身以及父类的层级结构中声明的注解的扫描器 + */ + AnnotationScanner SUPERCLASS = new GenericAnnotationScanner(false, true, false); + + /** + * 扫描元素本身以及父类的层级结构中声明的注解,以及这些注解的元注解的扫描器 + */ + AnnotationScanner SUPERCLASS_AND_META_ANNOTATION = new GenericAnnotationScanner(true, true, false); + + /** + * 扫描元素本身以及父接口的层级结构中声明的注解的扫描器 + */ + AnnotationScanner INTERFACE = new GenericAnnotationScanner(false, false, true); + + /** + * 扫描元素本身以及父接口的层级结构中声明的注解,以及这些注解的元注解的扫描器 + */ + AnnotationScanner INTERFACE_AND_META_ANNOTATION = new GenericAnnotationScanner(true, false, true); + + /** + * 扫描元素本身以及父类、父接口的层级结构中声明的注解的扫描器 + */ + AnnotationScanner TYPE_HIERARCHY = new GenericAnnotationScanner(false, true, true); + + /** + * 扫描元素本身以及父接口、父接口的层级结构中声明的注解,以及这些注解的元注解的扫描器 + */ + AnnotationScanner TYPE_HIERARCHY_AND_META_ANNOTATION = new GenericAnnotationScanner(true, true, true); + + // ============================ 静态方法 ============================ + + /** + * 给定一组扫描器,使用第一个支持处理该类型元素的扫描器获取元素上可能存在的注解 + * + * @param annotatedEle {@link AnnotatedElement},可以是Class、Method、Field、Constructor、ReflectPermission + * @param scanners 注解扫描器 + * @return 注解 + */ + static List scanByAnySupported(AnnotatedElement annotatedEle, AnnotationScanner... scanners) { + if (ObjectUtil.isNull(annotatedEle) && ArrayUtil.isNotEmpty(scanners)) { + return Collections.emptyList(); + } + return Stream.of(scanners) + .filter(scanner -> scanner.support(annotatedEle)) + .findFirst() + .map(scanner -> scanner.getAnnotations(annotatedEle)) + .orElseGet(Collections::emptyList); + } + + /** + * 根据指定的扫描器,扫描元素上可能存在的注解 + * + * @param annotatedEle {@link AnnotatedElement},可以是Class、Method、Field、Constructor、ReflectPermission + * @param scanners 注解扫描器 + * @return 注解 + */ + static List scanByAllSupported(AnnotatedElement annotatedEle, AnnotationScanner... scanners) { + if (ObjectUtil.isNull(annotatedEle) && ArrayUtil.isNotEmpty(scanners)) { + return Collections.emptyList(); + } + return Stream.of(scanners) + .map(scanner -> scanner.getAnnotationsIfSupport(annotatedEle)) + .flatMap(Collection::stream) + .collect(Collectors.toList()); + } + + // ============================ 抽象方法 ============================ + + /** + * 判断是否支持扫描该注解元素 + * + * @param annotatedEle {@link AnnotatedElement},可以是Class、Method、Field、Constructor、ReflectPermission + * @return 是否支持扫描该注解元素 + */ + default boolean support(AnnotatedElement annotatedEle) { + return false; + } + + /** + * 获取注解元素上的全部注解。调用该方法前,需要确保调用{@link #support(AnnotatedElement)}返回为true + * + * @param annotatedEle {@link AnnotatedElement},可以是Class、Method、Field、Constructor、ReflectPermission + * @return 注解 + */ + default List getAnnotations(AnnotatedElement annotatedEle) { + final List annotations = new ArrayList<>(); + scan((index, annotation) -> annotations.add(annotation), annotatedEle, null); + return annotations; + } + + /** + * 若{@link #support(AnnotatedElement)}返回{@code true}, + * 则调用并返回{@link #getAnnotations(AnnotatedElement)}结果, + * 否则返回{@link Collections#emptyList()} + * + * @param annotatedEle {@link AnnotatedElement},可以是Class、Method、Field、Constructor、ReflectPermission + * @return 注解 + */ + default List getAnnotationsIfSupport(AnnotatedElement annotatedEle) { + return support(annotatedEle) ? getAnnotations(annotatedEle) : Collections.emptyList(); + } + + /** + * 扫描注解元素的层级结构(若存在),然后对获取到的注解和注解对应的层级索引进行处理。 + * 调用该方法前,需要确保调用{@link #support(AnnotatedElement)}返回为true + * + * @param consumer 对获取到的注解和注解对应的层级索引的处理 + * @param annotatedEle {@link AnnotatedElement},可以是Class、Method、Field、Constructor、ReflectPermission + * @param filter 注解过滤器,无法通过过滤器的注解不会被处理。该参数允许为空。 + */ + default void scan(BiConsumer consumer, AnnotatedElement annotatedEle, Predicate filter) { + filter = ObjectUtil.defaultIfNull(filter, (a)->annotation -> true); + for (final Annotation annotation : annotatedEle.getAnnotations()) { + if (AnnotationUtil.isNotJdkMateAnnotation(annotation.annotationType()) && filter.test(annotation)) { + consumer.accept(0, annotation); + } + } + } + + /** + * 若{@link #support(AnnotatedElement)}返回{@code true},则调用{@link #scan(BiConsumer, AnnotatedElement, Predicate)} + * + * @param consumer 对获取到的注解和注解对应的层级索引的处理 + * @param annotatedEle {@link AnnotatedElement},可以是Class、Method、Field、Constructor、ReflectPermission + * @param filter 注解过滤器,无法通过过滤器的注解不会被处理。该参数允许为空。 + */ + default void scanIfSupport(BiConsumer consumer, AnnotatedElement annotatedEle, Predicate filter) { + if (support(annotatedEle)) { + scan(consumer, annotatedEle, filter); + } + } + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/annotation/scanner/ElementAnnotationScanner.java b/hutool-core/src/main/java/cn/hutool/core/annotation/scanner/ElementAnnotationScanner.java new file mode 100644 index 0000000000000000000000000000000000000000..88b22bc42d76238d61711c8bd619eb2613e25bad --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/annotation/scanner/ElementAnnotationScanner.java @@ -0,0 +1,44 @@ +package cn.hutool.core.annotation.scanner; + +import cn.hutool.core.util.ObjectUtil; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.util.function.BiConsumer; +import java.util.function.Predicate; +import java.util.stream.Stream; + +/** + * 扫描{@link AnnotatedElement}上的注解,不支持处理层级对象 + * + * @author huangchengxing + */ +public class ElementAnnotationScanner implements AnnotationScanner { + + /** + * 判断是否支持扫描该注解元素,仅当注解元素不为空时返回{@code true} + * + * @param annotatedEle {@link AnnotatedElement},可以是Class、Method、Field、Constructor、ReflectPermission + * @return 是否支持扫描该注解元素 + */ + @Override + public boolean support(AnnotatedElement annotatedEle) { + return ObjectUtil.isNotNull(annotatedEle); + } + + /** + * 扫描{@link AnnotatedElement}上直接声明的注解,调用前需要确保调用{@link #support(AnnotatedElement)}返回为true + * + * @param consumer 对获取到的注解和注解对应的层级索引的处理 + * @param annotatedEle {@link AnnotatedElement},可以是Class、Method、Field、Constructor、ReflectPermission + * @param filter 注解过滤器,无法通过过滤器的注解不会被处理。该参数允许为空。 + */ + @Override + public void scan(BiConsumer consumer, AnnotatedElement annotatedEle, Predicate filter) { + filter = ObjectUtil.defaultIfNull(filter,a-> t -> true); + Stream.of(annotatedEle.getAnnotations()) + .filter(filter) + .forEach(annotation -> consumer.accept(0, annotation)); + } + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/annotation/scanner/EmptyAnnotationScanner.java b/hutool-core/src/main/java/cn/hutool/core/annotation/scanner/EmptyAnnotationScanner.java new file mode 100644 index 0000000000000000000000000000000000000000..972d1d758954a6274eebc876ba9fa753a902fbd7 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/annotation/scanner/EmptyAnnotationScanner.java @@ -0,0 +1,31 @@ +package cn.hutool.core.annotation.scanner; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.util.Collections; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Predicate; + +/** + * 默认不扫描任何元素的扫描器 + * + * @author huangchengxing + */ +public class EmptyAnnotationScanner implements AnnotationScanner { + + @Override + public boolean support(AnnotatedElement annotatedEle) { + return true; + } + + @Override + public List getAnnotations(AnnotatedElement annotatedEle) { + return Collections.emptyList(); + } + + @Override + public void scan(BiConsumer consumer, AnnotatedElement annotatedEle, Predicate filter) { + // do nothing + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/annotation/scanner/FieldAnnotationScanner.java b/hutool-core/src/main/java/cn/hutool/core/annotation/scanner/FieldAnnotationScanner.java new file mode 100644 index 0000000000000000000000000000000000000000..e7d87cd61bf9ace8ae34610022a2e5d748ac787c --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/annotation/scanner/FieldAnnotationScanner.java @@ -0,0 +1,47 @@ +package cn.hutool.core.annotation.scanner; + +import cn.hutool.core.annotation.AnnotationUtil; +import cn.hutool.core.util.ObjectUtil; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Field; +import java.util.function.BiConsumer; +import java.util.function.Predicate; + +/** + * 扫描{@link Field}上的注解 + * + * @author huangchengxing + */ +public class FieldAnnotationScanner implements AnnotationScanner { + + /** + * 判断是否支持扫描该注解元素,仅当注解元素是{@link Field}时返回{@code true} + * + * @param annotatedEle {@link AnnotatedElement},可以是Class、Method、Field、Constructor、ReflectPermission + * @return 是否支持扫描该注解元素 + */ + @Override + public boolean support(AnnotatedElement annotatedEle) { + return annotatedEle instanceof Field; + } + + /** + * 扫描{@link Field}上直接声明的注解,调用前需要确保调用{@link #support(AnnotatedElement)}返回为true + * + * @param consumer 对获取到的注解和注解对应的层级索引的处理 + * @param annotatedEle {@link AnnotatedElement},可以是Class、Method、Field、Constructor、ReflectPermission + * @param filter 注解过滤器,无法通过过滤器的注解不会被处理。该参数允许为空。 + */ + @Override + public void scan(BiConsumer consumer, AnnotatedElement annotatedEle, Predicate filter) { + filter = ObjectUtil.defaultIfNull(filter, a -> annotation -> true); + for (final Annotation annotation : annotatedEle.getAnnotations()) { + if (AnnotationUtil.isNotJdkMateAnnotation(annotation.annotationType()) && filter.test(annotation)) { + consumer.accept(0, annotation); + } + } + } + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/annotation/scanner/GenericAnnotationScanner.java b/hutool-core/src/main/java/cn/hutool/core/annotation/scanner/GenericAnnotationScanner.java new file mode 100644 index 0000000000000000000000000000000000000000..fe40f100ef61c995efb25d1efd9b9d6f80c157a7 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/annotation/scanner/GenericAnnotationScanner.java @@ -0,0 +1,149 @@ +package cn.hutool.core.annotation.scanner; + +import cn.hutool.core.map.multi.ListValueMap; +import cn.hutool.core.util.ObjectUtil; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.function.BiConsumer; +import java.util.function.Predicate; + +/** + *

通用注解扫描器,支持按不同的层级结构扫描{@link AnnotatedElement}上的注解。 + * + *

当{@link AnnotatedElement}类型不同时,“层级结构”指向的对象将有所区别: + *

    + *
  • + * 当元素为{@link Method}时,此处层级结构指声明方法的类的层级结构, + * 扫描器将从层级结构中寻找与该方法签名相同的方法,并对其进行扫描; + *
  • + *
  • + * 当元素为{@link Class}时,此处层级结构即指类本身与其父类、父接口共同构成的层级结构, + * 扫描器将扫描层级结构中类、接口声明的注解; + *
  • + *
  • 当元素不为{@link Method}或{@link Class}时,则其层级结构仅有其本身一层;
  • + *
+ * 此外,扫描器支持在获取到层级结构中的注解对象后,再对注解对象的元注解进行扫描。 + * + * @author huangchengxing + * @see TypeAnnotationScanner + * @see MethodAnnotationScanner + * @see MetaAnnotationScanner + * @see ElementAnnotationScanner + */ +public class GenericAnnotationScanner implements AnnotationScanner { + + /** + * 类型扫描器 + */ + private final AnnotationScanner typeScanner; + + /** + * 方法扫描器 + */ + private final AnnotationScanner methodScanner; + + /** + * 元注解扫描器 + */ + private final AnnotationScanner metaScanner; + + /** + * 普通元素扫描器 + */ + private final AnnotationScanner elementScanner; + + /** + * 通用注解扫描器支持扫描所有类型的{@link AnnotatedElement} + * + * @param annotatedEle {@link AnnotatedElement},可以是Class、Method、Field、Constructor、ReflectPermission + * @return 是否支持扫描该注解元素 + */ + @Override + public boolean support(AnnotatedElement annotatedEle) { + return true; + } + + /** + * 构造一个通用注解扫描器 + * + * @param enableScanMetaAnnotation 是否扫描注解上的元注解 + * @param enableScanSupperClass 是否扫描父类 + * @param enableScanSupperInterface 是否扫描父接口 + */ + public GenericAnnotationScanner( + boolean enableScanMetaAnnotation, + boolean enableScanSupperClass, + boolean enableScanSupperInterface) { + + this.metaScanner = enableScanMetaAnnotation ? new MetaAnnotationScanner() : new EmptyAnnotationScanner(); + this.typeScanner = new TypeAnnotationScanner( + enableScanSupperClass, enableScanSupperInterface, a -> true, Collections.emptySet() + ); + this.methodScanner = new MethodAnnotationScanner( + enableScanSupperClass, enableScanSupperInterface, a -> true, Collections.emptySet() + ); + this.elementScanner = new ElementAnnotationScanner(); + } + + /** + * 扫描注解元素的层级结构(若存在),然后对获取到的注解和注解对应的层级索引进行处理 + * + * @param consumer 对获取到的注解和注解对应的层级索引的处理 + * @param annotatedEle {@link AnnotatedElement},可以是Class、Method、Field、Constructor、ReflectPermission + * @param filter 注解过滤器,无法通过过滤器的注解不会被处理。该参数允许为空。 + */ + @Override + public void scan(BiConsumer consumer, AnnotatedElement annotatedEle, Predicate filter) { + filter = ObjectUtil.defaultIfNull(filter, a -> t -> true); + if (ObjectUtil.isNull(annotatedEle)) { + return; + } + // 注解元素是类 + if (annotatedEle instanceof Class) { + scanElements(typeScanner, consumer, annotatedEle, filter); + } + // 注解元素是方法 + else if (annotatedEle instanceof Method) { + scanElements(methodScanner, consumer, annotatedEle, filter); + } + // 注解元素是其他类型 + else { + scanElements(elementScanner, consumer, annotatedEle, filter); + } + } + + /** + * 扫描注解类的层级结构(若存在),然后对获取到的注解和注解对应的层级索引进行处理 + * + * @param scanner 使用的扫描器 + * @param consumer 对获取到的注解和注解对应的层级索引的处理 + * @param annotatedEle {@link AnnotatedElement},可以是Class、Method、Field、Constructor、ReflectPermission + * @param filter 注解过滤器,无法通过过滤器的注解不会被处理。该参数允许为空。 + */ + private void scanElements( + AnnotationScanner scanner, + BiConsumer consumer, + AnnotatedElement annotatedEle, + Predicate filter) { + // 扫描类上注解 + final ListValueMap classAnnotations = new ListValueMap<>(new LinkedHashMap<>()); + scanner.scan((index, annotation) -> { + if (filter.test(annotation)) { + classAnnotations.putValue(index, annotation); + } + }, annotatedEle, filter); + + // 扫描元注解 + classAnnotations.forEach((index, annotations) -> + annotations.forEach(annotation -> { + consumer.accept(index, annotation); + metaScanner.scan(consumer, annotation.annotationType(), filter); + }) + ); + } + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/annotation/scanner/MetaAnnotationScanner.java b/hutool-core/src/main/java/cn/hutool/core/annotation/scanner/MetaAnnotationScanner.java new file mode 100644 index 0000000000000000000000000000000000000000..a054232b252e5941ace9575001ae0fe71a65c78d --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/annotation/scanner/MetaAnnotationScanner.java @@ -0,0 +1,110 @@ +package cn.hutool.core.annotation.scanner; + +import cn.hutool.core.annotation.AnnotationUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ClassUtil; +import cn.hutool.core.util.ObjectUtil; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.util.*; +import java.util.function.BiConsumer; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * 扫描注解类上存在的注解,支持处理枚举实例或枚举类型 + * 需要注意,当待解析是枚举类时,有可能与{@link TypeAnnotationScanner}冲突 + * + * @author huangchengxing + * @see TypeAnnotationScanner + */ +public class MetaAnnotationScanner implements AnnotationScanner { + + /** + * 获取当前注解的元注解后,是否继续递归扫描的元注解的元注解 + */ + private final boolean includeSupperMetaAnnotation; + + /** + * 构造一个元注解扫描器 + * + * @param includeSupperMetaAnnotation 获取当前注解的元注解后,是否继续递归扫描的元注解的元注解 + */ + public MetaAnnotationScanner(boolean includeSupperMetaAnnotation) { + this.includeSupperMetaAnnotation = includeSupperMetaAnnotation; + } + + /** + * 构造一个元注解扫描器,默认在扫描当前注解上的元注解后,并继续递归扫描元注解 + */ + public MetaAnnotationScanner() { + this(true); + } + + /** + * 判断是否支持扫描该注解元素,仅当注解元素是{@link Annotation}接口的子类{@link Class}时返回{@code true} + * + * @param annotatedEle {@link AnnotatedElement},可以是Class、Method、Field、Constructor、ReflectPermission + * @return 是否支持扫描该注解元素 + */ + @Override + public boolean support(AnnotatedElement annotatedEle) { + return (annotatedEle instanceof Class && ClassUtil.isAssignable(Annotation.class, (Class) annotatedEle)); + } + + /** + * 获取注解元素上的全部注解。调用该方法前,需要确保调用{@link #support(AnnotatedElement)}返回为true + * + * @param annotatedEle {@link AnnotatedElement},可以是Class、Method、Field、Constructor、ReflectPermission + * @return 注解 + */ + @Override + public List getAnnotations(AnnotatedElement annotatedEle) { + final List annotations = new ArrayList<>(); + scan( + (index, annotation) -> annotations.add(annotation), annotatedEle, + annotation -> ObjectUtil.notEqual(annotation, annotatedEle) + ); + return annotations; + } + + /** + * 按广度优先扫描指定注解上的元注解,对扫描到的注解与层级索引进行操作 + * + * @param consumer 当前层级索引与操作 + * @param annotatedEle {@link AnnotatedElement},可以是Class、Method、Field、Constructor、ReflectPermission + * @param filter 过滤器 + */ + @SuppressWarnings("unchecked") + @Override + public void scan(BiConsumer consumer, AnnotatedElement annotatedEle, Predicate filter) { + filter = ObjectUtil.defaultIfNull(filter, a -> t -> true); + Set> accessed = new HashSet<>(); + final Deque>> deque = CollUtil.newLinkedList(CollUtil.newArrayList((Class) annotatedEle)); + int distance = 0; + do { + final List> annotationTypes = deque.removeFirst(); + for (final Class type : annotationTypes) { + final List metaAnnotations = Stream.of(type.getAnnotations()) + .filter(a -> !AnnotationUtil.isJdkMetaAnnotation(a.annotationType())) + .filter(filter) + .collect(Collectors.toList()); + for (final Annotation metaAnnotation : metaAnnotations) { + consumer.accept(distance, metaAnnotation); + } + accessed.add(type); + List> next = metaAnnotations.stream() + .map(Annotation::annotationType) + .filter(t -> !accessed.contains(t)) + .collect(Collectors.toList()); + if (CollUtil.isNotEmpty(next)) { + deque.addLast(next); + } + } + distance++; + } while (includeSupperMetaAnnotation && !deque.isEmpty()); + } + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/annotation/scanner/MethodAnnotationScanner.java b/hutool-core/src/main/java/cn/hutool/core/annotation/scanner/MethodAnnotationScanner.java new file mode 100644 index 0000000000000000000000000000000000000000..a494c31fbab61ca545bdcde21d81c20648eedd29 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/annotation/scanner/MethodAnnotationScanner.java @@ -0,0 +1,133 @@ +package cn.hutool.core.annotation.scanner; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ClassUtil; +import cn.hutool.core.util.StrUtil; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Method; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Stream; + +/** + * 扫描{@link Method}上的注解 + * + * @author huangchengxing + */ +public class MethodAnnotationScanner extends AbstractTypeAnnotationScanner implements AnnotationScanner { + + /** + * 构造一个类注解扫描器,仅扫描该方法上直接声明的注解 + */ + public MethodAnnotationScanner() { + this(false); + } + + /** + * 构造一个类注解扫描器 + * + * @param scanSameSignatureMethod 是否扫描类层级结构中具有相同方法签名的方法 + */ + public MethodAnnotationScanner(boolean scanSameSignatureMethod) { + this(scanSameSignatureMethod, targetClass -> true, CollUtil.newLinkedHashSet()); + } + + /** + * 构造一个方法注解扫描器 + * + * @param scanSameSignatureMethod 是否扫描类层级结构中具有相同方法签名的方法 + * @param filter 过滤器 + * @param excludeTypes 不包含的类型 + */ + public MethodAnnotationScanner(boolean scanSameSignatureMethod, Predicate> filter, Set> excludeTypes) { + super(scanSameSignatureMethod, scanSameSignatureMethod, filter, excludeTypes); + } + + /** + * 构造一个方法注解扫描器 + * + * @param includeSuperClass 是否允许扫描父类中具有相同方法签名的方法 + * @param includeInterfaces 是否允许扫描父接口中具有相同方法签名的方法 + * @param filter 过滤器 + * @param excludeTypes 不包含的类型 + */ + public MethodAnnotationScanner(boolean includeSuperClass, boolean includeInterfaces, Predicate> filter, Set> excludeTypes) { + super(includeSuperClass, includeInterfaces, filter, excludeTypes); + } + + /** + * 判断是否支持扫描该注解元素,仅当注解元素是{@link Method}时返回{@code true} + * + * @param annotatedEle {@link AnnotatedElement},可以是Class、Method、Field、Constructor、ReflectPermission + * @return boolean 是否支持扫描该注解元素 + */ + @Override + public boolean support(AnnotatedElement annotatedEle) { + return annotatedEle instanceof Method; + } + + /** + * 获取声明该方法的类 + * + * @param annotatedElement 注解元素 + * @return 要递归的类型 + * @see Method#getDeclaringClass() + */ + @Override + protected Class getClassFormAnnotatedElement(AnnotatedElement annotatedElement) { + return ((Method)annotatedElement).getDeclaringClass(); + } + + /** + * 若父类/父接口中方法具有相同的方法签名,则返回该方法上的注解 + * + * @param source 原始方法 + * @param index 类的层级索引 + * @param targetClass 类 + * @return 最终所需的目标注解 + */ + @Override + protected Annotation[] getAnnotationsFromTargetClass(AnnotatedElement source, int index, Class targetClass) { + final Method sourceMethod = (Method) source; + return Stream.of(ClassUtil.getDeclaredMethods(targetClass)) + .filter(superMethod -> !superMethod.isBridge()) + .filter(superMethod -> hasSameSignature(sourceMethod, superMethod)) + .map(AnnotatedElement::getAnnotations) + .flatMap(Stream::of) + .toArray(Annotation[]::new); + } + + /** + * 设置是否扫描类层级结构中具有相同方法签名的方法 + * + * @param scanSuperMethodIfOverride 是否扫描类层级结构中具有相同方法签名的方法 + * @return 当前实例 + */ + public MethodAnnotationScanner setScanSameSignatureMethod(boolean scanSuperMethodIfOverride) { + setIncludeInterfaces(scanSuperMethodIfOverride); + setIncludeSuperClass(scanSuperMethodIfOverride); + return this; + } + + /** + * 该方法是否具备与扫描的方法相同的方法签名 + */ + private boolean hasSameSignature(Method sourceMethod, Method superMethod) { + if (false == StrUtil.equals(sourceMethod.getName(), superMethod.getName())) { + return false; + } + final Class[] sourceParameterTypes = sourceMethod.getParameterTypes(); + final Class[] targetParameterTypes = superMethod.getParameterTypes(); + if (sourceParameterTypes.length != targetParameterTypes.length) { + return false; + } + if (!ArrayUtil.containsAll(sourceParameterTypes, targetParameterTypes)) { + return false; + } + return ClassUtil.isAssignable(superMethod.getReturnType(), sourceMethod.getReturnType()); + } + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/annotation/scanner/TypeAnnotationScanner.java b/hutool-core/src/main/java/cn/hutool/core/annotation/scanner/TypeAnnotationScanner.java new file mode 100644 index 0000000000000000000000000000000000000000..15b6fe5201733d88845961350eaaa73bafac3dfa --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/annotation/scanner/TypeAnnotationScanner.java @@ -0,0 +1,105 @@ +package cn.hutool.core.annotation.scanner; + +import cn.hutool.core.collection.CollUtil; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Proxy; +import java.util.Set; +import java.util.function.Predicate; +import java.util.function.UnaryOperator; + +/** + * 扫描{@link Class}上的注解 + * + * @author huangchengxing + */ +public class TypeAnnotationScanner extends AbstractTypeAnnotationScanner implements AnnotationScanner { + + /** + * 构造一个类注解扫描器 + * + * @param includeSupperClass 是否允许扫描父类 + * @param includeInterfaces 是否允许扫描父接口 + * @param filter 过滤器 + * @param excludeTypes 不包含的类型 + */ + public TypeAnnotationScanner(boolean includeSupperClass, boolean includeInterfaces, Predicate> filter, Set> excludeTypes) { + super(includeSupperClass, includeInterfaces, filter, excludeTypes); + } + + /** + * 构建一个类注解扫描器,默认允许扫描指定元素的父类以及父接口 + */ + public TypeAnnotationScanner() { + this(true, true, t -> true, CollUtil.newLinkedHashSet()); + } + + /** + * 判断是否支持扫描该注解元素,仅当注解元素是{@link Class}接时返回{@code true} + * + * @param annotatedEle {@link AnnotatedElement},可以是Class、Method、Field、Constructor、ReflectPermission + * @return 是否支持扫描该注解元素 + */ + @Override + public boolean support(AnnotatedElement annotatedEle) { + return annotatedEle instanceof Class; + } + + /** + * 将注解元素转为{@link Class} + * + * @param annotatedEle {@link AnnotatedElement},可以是Class、Method、Field、Constructor、ReflectPermission + * @return 要递归的类型 + */ + @Override + protected Class getClassFormAnnotatedElement(AnnotatedElement annotatedEle) { + return (Class)annotatedEle; + } + + /** + * 获取{@link Class#getAnnotations()} + * + * @param source 最初的注解元素 + * @param index 类的层级索引 + * @param targetClass 类 + * @return 类上直接声明的注解 + */ + @Override + protected Annotation[] getAnnotationsFromTargetClass(AnnotatedElement source, int index, Class targetClass) { + return targetClass.getAnnotations(); + } + + /** + * 是否允许扫描父类 + * + * @param includeSuperClass 是否允许扫描父类 + * @return 当前实例 + */ + @Override + public TypeAnnotationScanner setIncludeSuperClass(boolean includeSuperClass) { + return super.setIncludeSuperClass(includeSuperClass); + } + + /** + * 是否允许扫描父接口 + * + * @param includeInterfaces 是否允许扫描父类 + * @return 当前实例 + */ + @Override + public TypeAnnotationScanner setIncludeInterfaces(boolean includeInterfaces) { + return super.setIncludeInterfaces(includeInterfaces); + } + + /** + * 若类型为jdk代理类,则尝试转换为原始被代理类 + */ + public static class JdkProxyClassConverter implements UnaryOperator> { + @Override + public Class apply(Class sourceClass) { + return Proxy.isProxyClass(sourceClass) ? apply(sourceClass.getSuperclass()) : sourceClass; + } + } + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/annotation/scanner/package-info.java b/hutool-core/src/main/java/cn/hutool/core/annotation/scanner/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..3d42d61c056040c08d8fef35dd60259e750de642 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/annotation/scanner/package-info.java @@ -0,0 +1,7 @@ +/** + * 注解包扫描封装 + * + * @author looly + * + */ +package cn.hutool.core.annotation.scanner; diff --git a/hutool-core/src/main/java/cn/hutool/core/bean/BeanDesc.java b/hutool-core/src/main/java/cn/hutool/core/bean/BeanDesc.java index 976f40d29693efcb543ab633d2b6da01271b6e33..da3b1657ef0d490f1a0b642a062eeafb6d58d57f 100644 --- a/hutool-core/src/main/java/cn/hutool/core/bean/BeanDesc.java +++ b/hutool-core/src/main/java/cn/hutool/core/bean/BeanDesc.java @@ -12,6 +12,7 @@ import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.Collection; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; /** @@ -48,7 +49,11 @@ public class BeanDesc implements Serializable { public BeanDesc(Class beanClass) { Assert.notNull(beanClass); this.beanClass = beanClass; - init(); + if(RecordUtil.isRecord(beanClass)){ + initForRecord(); + }else{ + init(); + } } /** @@ -153,6 +158,27 @@ public class BeanDesc implements Serializable { return this; } + /** + * 针对Record类的反射初始化 + */ + private void initForRecord() { + final Class beanClass = this.beanClass; + final Map propMap = this.propMap; + + final List getters = ReflectUtil.getPublicMethods(beanClass, method -> 0 == method.getParameterCount()); + // 排除静态属性和对象子类 + final Field[] fields = ReflectUtil.getFields(beanClass, field -> !ModifierUtil.isStatic(field) && !ReflectUtil.isOuterClassField(field)); + for (final Field field : fields) { + for (final Method getter : getters) { + if (field.getName().equals(getter.getName())) { + //record对象,getter方法与字段同名 + final PropDesc prop = new PropDesc(field, getter, null); + propMap.putIfAbsent(prop.getFieldName(), prop); + } + } + } + } + /** * 根据字段创建属性描述
* 查找Getter和Setter方法时会: @@ -181,7 +207,8 @@ public class BeanDesc implements Serializable { prop.setter = propIgnoreCase.setter; } } - + // 所有属性完成填充后的初始化逻辑 + prop.initialize(); return prop; } diff --git a/hutool-core/src/main/java/cn/hutool/core/bean/BeanDescCache.java b/hutool-core/src/main/java/cn/hutool/core/bean/BeanDescCache.java index fa29e1d94740af52101511ef3d72e446c857850d..3e38e776cd4ade9da2b7cd1ece2e27c1f4cc89e8 100755 --- a/hutool-core/src/main/java/cn/hutool/core/bean/BeanDescCache.java +++ b/hutool-core/src/main/java/cn/hutool/core/bean/BeanDescCache.java @@ -1,7 +1,7 @@ package cn.hutool.core.bean; import cn.hutool.core.lang.func.Func0; -import cn.hutool.core.map.WeakConcurrentMap; +import cn.hutool.core.map.reference.WeakKeyValueConcurrentMap; /** * Bean属性缓存
@@ -12,7 +12,7 @@ import cn.hutool.core.map.WeakConcurrentMap; public enum BeanDescCache { INSTANCE; - private final WeakConcurrentMap, BeanDesc> bdCache = new WeakConcurrentMap<>(); + private final WeakKeyValueConcurrentMap, BeanDesc> bdCache = new WeakKeyValueConcurrentMap<>(); /** * 获得属性名和{@link BeanDesc}Map映射 diff --git a/hutool-core/src/main/java/cn/hutool/core/bean/BeanInfoCache.java b/hutool-core/src/main/java/cn/hutool/core/bean/BeanInfoCache.java index 63fc224d5d3d725130750a4aa58302cde78e7634..2cd29c3e65b03d10cae7c3370efa93f598e110dc 100755 --- a/hutool-core/src/main/java/cn/hutool/core/bean/BeanInfoCache.java +++ b/hutool-core/src/main/java/cn/hutool/core/bean/BeanInfoCache.java @@ -1,8 +1,8 @@ package cn.hutool.core.bean; import cn.hutool.core.lang.func.Func0; -import cn.hutool.core.map.ReferenceConcurrentMap; -import cn.hutool.core.map.WeakConcurrentMap; +import cn.hutool.core.map.reference.ReferenceConcurrentMap; +import cn.hutool.core.map.reference.WeakKeyValueConcurrentMap; import java.beans.PropertyDescriptor; import java.util.Map; @@ -16,8 +16,8 @@ import java.util.Map; public enum BeanInfoCache { INSTANCE; - private final WeakConcurrentMap, Map> pdCache = new WeakConcurrentMap<>(); - private final WeakConcurrentMap, Map> ignoreCasePdCache = new WeakConcurrentMap<>(); + private final WeakKeyValueConcurrentMap, Map> pdCache = new WeakKeyValueConcurrentMap<>(); + private final WeakKeyValueConcurrentMap, Map> ignoreCasePdCache = new WeakKeyValueConcurrentMap<>(); /** * 获得属性名和{@link PropertyDescriptor}Map映射 diff --git a/hutool-core/src/main/java/cn/hutool/core/bean/BeanPath.java b/hutool-core/src/main/java/cn/hutool/core/bean/BeanPath.java index ce92e395125c2e10c5ac5bf7ed88e721195c712f..d7f1db7f5a53f36af571f36e2edc0b3cc37d896b 100644 --- a/hutool-core/src/main/java/cn/hutool/core/bean/BeanPath.java +++ b/hutool-core/src/main/java/cn/hutool/core/bean/BeanPath.java @@ -1,20 +1,16 @@ package cn.hutool.core.bean; import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.ListUtil; import cn.hutool.core.convert.Convert; import cn.hutool.core.map.MapUtil; -import cn.hutool.core.text.StrBuilder; import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.CharUtil; +import cn.hutool.core.util.NumberUtil; import cn.hutool.core.util.StrUtil; import java.io.Serializable; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; /** * Bean路径表达式,用于获取多层嵌套Bean中的字段值或Bean对象
@@ -23,7 +19,7 @@ import java.util.Map; *
  • .表达式,可以获取Bean对象中的属性(字段)值或者Map中key对应的值
  • *
  • []表达式,可以获取集合等对象中对应index的值
  • * - * + *

    * 表达式栗子: * *

    @@ -37,11 +33,13 @@ import java.util.Map;
      * @author Looly
      * @since 4.0.6
      */
    -public class BeanPath implements Serializable{
    +public class BeanPath implements Serializable {
     	private static final long serialVersionUID = 1L;
     
    -	/** 表达式边界符号数组 */
    -	private static final char[] EXP_CHARS = { CharUtil.DOT, CharUtil.BRACKET_START, CharUtil.BRACKET_END };
    +	/**
    +	 * 表达式边界符号数组
    +	 */
    +	private static final char[] EXP_CHARS = {CharUtil.DOT, CharUtil.BRACKET_START, CharUtil.BRACKET_END};
     
     	private boolean isStartWith = false;
     	protected List patternParts;
    @@ -54,7 +52,7 @@ public class BeanPath implements Serializable{
     	 * 
  • .表达式,可以获取Bean对象中的属性(字段)值或者Map中key对应的值
  • *
  • []表达式,可以获取集合等对象中对应index的值
  • * - * + *

    * 表达式栗子: * *

    @@ -68,7 +66,7 @@ public class BeanPath implements Serializable{
     	 * @param expression 表达式
     	 * @return BeanPath
     	 */
    -	public static BeanPath create(String expression) {
    +	public static BeanPath create(final String expression) {
     		return new BeanPath(expression);
     	}
     
    @@ -77,17 +75,26 @@ public class BeanPath implements Serializable{
     	 *
     	 * @param expression 表达式
     	 */
    -	public BeanPath(String expression) {
    +	public BeanPath(final String expression) {
     		init(expression);
     	}
     
    +	/**
    +	 * 获取表达式解析后的分段列表
    +	 *
    +	 * @return 表达式分段列表
    +	 */
    +	public List getPatternParts() {
    +		return this.patternParts;
    +	}
    +
     	/**
     	 * 获取Bean中对应表达式的值
     	 *
     	 * @param bean Bean对象或Map或List等
     	 * @return 值,如果对应值不存在,则返回null
     	 */
    -	public Object get(Object bean) {
    +	public Object get(final Object bean) {
     		return get(this.patternParts, bean, false);
     	}
     
    @@ -101,13 +108,20 @@ public class BeanPath implements Serializable{
     	 * 2. 如果为数组,如果下标不大于数组长度,则替换原有值,否则追加值
     	 * 
    * - * @param bean Bean、Map或List + * @param bean Bean、Map或List * @param value 值 */ - public void set(Object bean, Object value) { - set(bean, this.patternParts, value); + public void set(final Object bean, final Object value) { + set(bean, this.patternParts, lastIsNumber(this.patternParts), value); } + @Override + public String toString() { + return this.patternParts.toString(); + } + + //region Private Methods + /** * 设置表达式指定位置(或filed对应)的值
    * 若表达式指向一个List则设置其坐标对应位置的值,若指向Map则put对应key的值,Bean则设置字段的值
    @@ -118,30 +132,57 @@ public class BeanPath implements Serializable{ * 2. 如果为数组,如果下标不大于数组长度,则替换原有值,否则追加值 *
    * - * @param bean Bean、Map或List - * @param patternParts 表达式块列表 - * @param value 值 + * @param bean Bean、Map或List + * @param patternParts 表达式块列表 + * @param nextNumberPart 下一个值是否 + * @param value 值 */ - private void set(Object bean, List patternParts, Object value) { - Object subBean = get(patternParts, bean, true); - if(null == subBean) { - set(bean, patternParts.subList(0, patternParts.size() - 1), new HashMap<>()); + private void set(Object bean, List patternParts, boolean nextNumberPart, Object value) { + Object subBean = this.get(patternParts, bean, true); + if (null == subBean) { + // 当前节点是空,则先创建父节点 + final List parentParts = getParentParts(patternParts); + this.set(bean, parentParts, lastIsNumber(parentParts), nextNumberPart ? new ArrayList<>() : new HashMap<>()); //set中有可能做过转换,因此此处重新获取bean - subBean = get(patternParts, bean, true); + subBean = this.get(patternParts, bean, true); } - BeanUtil.setFieldValue(subBean, patternParts.get(patternParts.size() - 1), value); + + final Object newSubBean = BeanUtil.setFieldValue(subBean, patternParts.get(patternParts.size() - 1), value); + if(newSubBean != subBean){ + // 对象变更,重新加入 + this.set(bean, getParentParts(patternParts), nextNumberPart, newSubBean); + } + } + + /** + * 判断path列表中末尾的标记是否为数字 + * + * @param patternParts path列表 + * @return 是否为数字 + */ + private static boolean lastIsNumber(List patternParts) { + return NumberUtil.isInteger(patternParts.get(patternParts.size() - 1)); + } + + /** + * 获取父级路径列表 + * + * @param patternParts 路径列表 + * @return 父级路径列表 + */ + private static List getParentParts(List patternParts) { + return patternParts.subList(0, patternParts.size() - 1); } - // ------------------------------------------------------------------------------------------------------------------------------------- Private method start /** * 获取Bean中对应表达式的值 * * @param patternParts 表达式分段列表 - * @param bean Bean对象或Map或List等 - * @param ignoreLast 是否忽略最后一个值,忽略最后一个值则用于set,否则用于read + * @param bean Bean对象或Map或List等 + * @param ignoreLast 是否忽略最后一个值,忽略最后一个值则用于set,否则用于read * @return 值,如果对应值不存在,则返回null */ - private Object get(List patternParts, Object bean, boolean ignoreLast) { + private Object get(final List patternParts, final Object bean, final boolean ignoreLast) { int length = patternParts.size(); if (ignoreLast) { length--; @@ -166,7 +207,7 @@ public class BeanPath implements Serializable{ } @SuppressWarnings("unchecked") - private static Object getFieldValue(Object bean, String expression) { + private static Object getFieldValue(final Object bean, final String expression) { if (StrUtil.isBlank(expression)) { return null; } @@ -174,8 +215,8 @@ public class BeanPath implements Serializable{ if (StrUtil.contains(expression, ':')) { // [start:end:step] 模式 final List parts = StrUtil.splitTrim(expression, ':'); - int start = Integer.parseInt(parts.get(0)); - int end = Integer.parseInt(parts.get(1)); + final int start = Integer.parseInt(parts.get(0)); + final int end = Integer.parseInt(parts.get(1)); int step = 1; if (3 == parts.size()) { step = Integer.parseInt(parts.get(2)); @@ -218,13 +259,14 @@ public class BeanPath implements Serializable{ * * @param expression 表达式 */ - private void init(String expression) { - List localPatternParts = new ArrayList<>(); - int length = expression.length(); + private void init(final String expression) { + final List localPatternParts = new ArrayList<>(); + final int length = expression.length(); - final StrBuilder builder = StrUtil.strBuilder(); + final StringBuilder builder = new StringBuilder(); char c; boolean isNumStart = false;// 下标标识符开始 + boolean isInWrap = false; //标识是否在引号内 for (int i = 0; i < length; i++) { c = expression.charAt(i); if (0 == i && '$' == c) { @@ -233,7 +275,13 @@ public class BeanPath implements Serializable{ continue; } - if (ArrayUtil.contains(EXP_CHARS, c)) { + if ('\'' == c) { + // 结束 + isInWrap = (false == isInWrap); + continue; + } + + if (false == isInWrap && ArrayUtil.contains(EXP_CHARS, c)) { // 处理边界符号 if (CharUtil.BRACKET_END == c) { // 中括号(数字下标)结束 @@ -253,9 +301,9 @@ public class BeanPath implements Serializable{ // 每一个边界符之前的表达式是一个完整的KEY,开始处理KEY } if (builder.length() > 0) { - localPatternParts.add(unWrapIfPossible(builder)); + localPatternParts.add(builder.toString()); } - builder.reset(); + builder.setLength(0); } else { // 非边界符号,追加字符 builder.append(c); @@ -267,25 +315,12 @@ public class BeanPath implements Serializable{ throw new IllegalArgumentException(StrUtil.format("Bad expression '{}':{}, we find '[' but no ']' !", expression, length - 1)); } else { if (builder.length() > 0) { - localPatternParts.add(unWrapIfPossible(builder)); + localPatternParts.add(builder.toString()); } } // 不可变List - this.patternParts = Collections.unmodifiableList(localPatternParts); - } - - /** - * 对于非表达式去除单引号 - * - * @param expression 表达式 - * @return 表达式 - */ - private static String unWrapIfPossible(CharSequence expression) { - if (StrUtil.containsAny(expression, " = ", " > ", " < ", " like ", ",")) { - return expression.toString(); - } - return StrUtil.unWrap(expression, '\''); + this.patternParts = ListUtil.unmodifiable(localPatternParts); } - // ------------------------------------------------------------------------------------------------------------------------------------- Private method end + //endregion } diff --git a/hutool-core/src/main/java/cn/hutool/core/bean/BeanUtil.java b/hutool-core/src/main/java/cn/hutool/core/bean/BeanUtil.java index eabce8de0a5b465a37734ce6afec50958cc403c9..4e1ca2936c516789b26fbf982eeff35a83dccf27 100755 --- a/hutool-core/src/main/java/cn/hutool/core/bean/BeanUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/bean/BeanUtil.java @@ -4,31 +4,18 @@ import cn.hutool.core.bean.copier.BeanCopier; import cn.hutool.core.bean.copier.CopyOptions; import cn.hutool.core.bean.copier.ValueProvider; import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.ListUtil; import cn.hutool.core.convert.Convert; +import cn.hutool.core.lang.Dict; import cn.hutool.core.lang.Editor; import cn.hutool.core.map.CaseInsensitiveMap; import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.ArrayUtil; -import cn.hutool.core.util.ClassUtil; -import cn.hutool.core.util.ModifierUtil; -import cn.hutool.core.util.ObjectUtil; -import cn.hutool.core.util.ReflectUtil; -import cn.hutool.core.util.StrUtil; - -import java.beans.BeanInfo; -import java.beans.IntrospectionException; -import java.beans.Introspector; -import java.beans.PropertyDescriptor; -import java.beans.PropertyEditor; -import java.beans.PropertyEditorManager; +import cn.hutool.core.util.*; + +import java.beans.*; import java.lang.reflect.Field; import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.function.Consumer; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -88,6 +75,14 @@ public class BeanUtil { * @since 4.2.2 */ public static boolean hasSetter(Class clazz) { + if(null == clazz){ + return false; + } + // issue#I9VTZG,排除定义setXXX的预定义类 + if(Dict.class == clazz){ + return false; + } + if (ClassUtil.isNormalClass(clazz)) { for (Method method : clazz.getMethods()) { if (method.getParameterCount() == 1 && method.getName().startsWith("set")) { @@ -111,8 +106,11 @@ public class BeanUtil { if (ClassUtil.isNormalClass(clazz)) { for (Method method : clazz.getMethods()) { if (method.getParameterCount() == 0) { - if (method.getName().startsWith("get") || method.getName().startsWith("is")) { - return true; + final String name = method.getName(); + if (name.startsWith("get") || name.startsWith("is")) { + if (false == "getClass".equals(name)) { + return true; + } } } } @@ -128,6 +126,9 @@ public class BeanUtil { * @since 5.1.0 */ public static boolean hasPublicField(Class clazz) { + if(null == clazz){ + return false; + } if (ClassUtil.isNormalClass(clazz)) { for (Field field : clazz.getFields()) { if (ModifierUtil.isPublic(field) && false == ModifierUtil.isStatic(field)) { @@ -303,24 +304,32 @@ public class BeanUtil { /** * 设置字段值,通过反射设置字段值,并不调用setXXX方法
    - * 对象同样支持Map类型,fieldNameOrIndex即为key + * 对象同样支持Map类型,fieldNameOrIndex即为key,支持: + *
      + *
    • Map
    • + *
    • List
    • + *
    • Bean
    • + *
    * * @param bean Bean * @param fieldNameOrIndex 字段名或序号,序号支持负数 * @param value 值 + * @return bean,当为数组时,返回一个新的数组 */ @SuppressWarnings({"unchecked", "rawtypes"}) - public static void setFieldValue(Object bean, String fieldNameOrIndex, Object value) { + public static Object setFieldValue(Object bean, String fieldNameOrIndex, Object value) { if (bean instanceof Map) { ((Map) bean).put(fieldNameOrIndex, value); } else if (bean instanceof List) { - CollUtil.setOrAppend((List) bean, Convert.toInt(fieldNameOrIndex), value); + ListUtil.setOrPadding((List) bean, Convert.toInt(fieldNameOrIndex), value); } else if (ArrayUtil.isArray(bean)) { - ArrayUtil.setOrAppend(bean, Convert.toInt(fieldNameOrIndex), value); + // issue#3008,追加产生新数组,此处返回新数组 + return ArrayUtil.setOrAppend(bean, Convert.toInt(fieldNameOrIndex), value); } else { // 普通Bean对象 ReflectUtil.setFieldValue(bean, fieldNameOrIndex, value); } + return bean; } /** @@ -411,7 +420,9 @@ public class BeanUtil { * @param isToCamelCase 是否将Map中的下划线风格key转换为驼峰风格 * @param copyOptions 转Bean选项 * @return Bean + * @deprecated isToCamelCase参数无效,请使用 {@link #toBean(Object, Class, CopyOptions)} */ + @Deprecated public static T mapToBean(Map map, Class beanClass, boolean isToCamelCase, CopyOptions copyOptions) { return fillBeanWithMap(map, ReflectUtil.newInstanceIfPossible(beanClass), isToCamelCase, copyOptions); } @@ -440,7 +451,9 @@ public class BeanUtil { * @param isToCamelCase 是否将下划线模式转换为驼峰模式 * @param isIgnoreError 是否忽略注入错误 * @return Bean + * @deprecated isToCamelCase参数无效,请使用{@link #fillBeanWithMap(Map, Object, boolean)} */ + @Deprecated public static T fillBeanWithMap(Map map, T bean, boolean isToCamelCase, boolean isIgnoreError) { return fillBeanWithMap(map, bean, isToCamelCase, CopyOptions.create().setIgnoreError(isIgnoreError)); } @@ -468,7 +481,11 @@ public class BeanUtil { * @return Bean */ public static T fillBeanWithMap(Map map, T bean, CopyOptions copyOptions) { - return fillBeanWithMap(map, bean, false, copyOptions); + if (MapUtil.isEmpty(map)) { + return bean; + } + copyProperties(map, bean, copyOptions); + return bean; } /** @@ -481,14 +498,18 @@ public class BeanUtil { * @param copyOptions 属性复制选项 {@link CopyOptions} * @return Bean * @since 3.3.1 + * @deprecated isToCamelCase参数无效,请使用{@link #fillBeanWithMap(Map, Object, CopyOptions)} */ + @Deprecated public static T fillBeanWithMap(Map map, T bean, boolean isToCamelCase, CopyOptions copyOptions) { if (MapUtil.isEmpty(map)) { return bean; } - if (isToCamelCase) { - map = MapUtil.toCamelCaseMap(map); - } + + // issue#3452,参数无效,MapToBeanCopier中已经有转驼峰逻辑 +// if (isToCamelCase) { +// map = MapUtil.toCamelCaseMap(map); +// } copyProperties(map, bean, copyOptions); return bean; } @@ -607,13 +628,25 @@ public class BeanUtil { // --------------------------------------------------------------------------------------------- beanToMap /** - * 对象转Map,不进行驼峰转下划线,不忽略值为空的字段 + * 将bean的部分属性转换成map
    + * 可选拷贝哪些属性值,默认是不忽略值为{@code null}的值的。 * - * @param bean bean对象 + * @param bean bean + * @param properties 需要拷贝的属性值,{@code null}或空表示拷贝所有值 * @return Map + * @since 5.8.0 */ - public static Map beanToMap(Object bean) { - return beanToMap(bean, false, false); + public static Map beanToMap(Object bean, String... properties) { + int mapSize = 16; + Editor keyEditor = null; + if (ArrayUtil.isNotEmpty(properties)) { + mapSize = properties.length; + final Set propertiesSet = CollUtil.set(false, properties); + keyEditor = property -> propertiesSet.contains(property) ? property : null; + } + + // 指明了要复制的属性 所以不忽略null值 + return beanToMap(bean, new LinkedHashMap<>(mapSize, 1), false, keyEditor); } /** @@ -716,6 +749,9 @@ public class BeanUtil { * @return 目标对象 */ public static T copyProperties(Object source, Class tClass, String... ignoreProperties) { + if (null == source) { + return null; + } T target = ReflectUtil.newInstanceIfPossible(tClass); copyProperties(source, target, CopyOptions.create().setIgnoreProperties(ignoreProperties)); return target; @@ -753,6 +789,9 @@ public class BeanUtil { * @param copyOptions 拷贝选项,见 {@link CopyOptions} */ public static void copyProperties(Object source, Object target, CopyOptions copyOptions) { + if (null == source) { + return; + } BeanCopier.create(source, target, ObjectUtil.defaultIfNull(copyOptions, CopyOptions::create)).copy(); } @@ -774,6 +813,12 @@ public class BeanUtil { if (collection.isEmpty()) { return new ArrayList<>(0); } + + // issue#3091 + if(ClassUtil.isBasicType(targetType) || String.class == targetType){ + return Convert.toList(targetType, collection); + } + return collection.stream().map((source) -> { final T target = ReflectUtil.newInstanceIfPossible(targetType); copyProperties(source, target, copyOptions); @@ -797,8 +842,8 @@ public class BeanUtil { /** * 给定的Bean的类名是否匹配指定类名字符串
    - * 如果isSimple为{@code false},则只匹配类名而忽略包名,例如:cn.hutool.TestEntity只匹配TestEntity
    - * 如果isSimple为{@code true},则匹配包括包名的全类名,例如:cn.hutool.TestEntity匹配cn.hutool.TestEntity + * 如果isSimple为{@code true},则只匹配类名而忽略包名,例如:cn.hutool.TestEntity只匹配TestEntity
    + * 如果isSimple为{@code false},则匹配包括包名的全类名,例如:cn.hutool.TestEntity匹配cn.hutool.TestEntity * * @param bean Bean * @param beanClassName Bean的类名 @@ -873,12 +918,12 @@ public class BeanUtil { * 判断Bean是否为非空对象,非空对象表示本身不为{@code null}或者含有非{@code null}属性的对象 * * @param bean Bean对象 - * @param ignoreFiledNames 忽略检查的字段名 + * @param ignoreFieldNames 忽略检查的字段名 * @return 是否为非空,{@code true} - 非空 / {@code false} - 空 * @since 5.0.7 */ - public static boolean isNotEmpty(Object bean, String... ignoreFiledNames) { - return false == isEmpty(bean, ignoreFiledNames); + public static boolean isNotEmpty(Object bean, String... ignoreFieldNames) { + return false == isEmpty(bean, ignoreFieldNames); } /** @@ -886,17 +931,17 @@ public class BeanUtil { * 此方法不判断static属性 * * @param bean Bean对象 - * @param ignoreFiledNames 忽略检查的字段名 + * @param ignoreFieldNames 忽略检查的字段名 * @return 是否为空,{@code true} - 空 / {@code false} - 非空 * @since 4.1.10 */ - public static boolean isEmpty(Object bean, String... ignoreFiledNames) { + public static boolean isEmpty(Object bean, String... ignoreFieldNames) { if (null != bean) { for (Field field : ReflectUtil.getFields(bean.getClass())) { if (ModifierUtil.isStatic(field)) { continue; } - if ((false == ArrayUtil.contains(ignoreFiledNames, field.getName())) + if ((false == ArrayUtil.contains(ignoreFieldNames, field.getName())) && null != ReflectUtil.getFieldValue(bean, field)) { return false; } @@ -910,11 +955,11 @@ public class BeanUtil { * 对象本身为{@code null}也返回true * * @param bean Bean对象 - * @param ignoreFiledNames 忽略检查的字段名 + * @param ignoreFieldNames 忽略检查的字段名 * @return 是否包含值为null的属性,{@code true} - 包含 / {@code false} - 不包含 * @since 4.1.10 */ - public static boolean hasNullField(Object bean, String... ignoreFiledNames) { + public static boolean hasNullField(Object bean, String... ignoreFieldNames) { if (null == bean) { return true; } @@ -922,7 +967,7 @@ public class BeanUtil { if (ModifierUtil.isStatic(field)) { continue; } - if ((false == ArrayUtil.contains(ignoreFiledNames, field.getName())) + if ((false == ArrayUtil.contains(ignoreFieldNames, field.getName())) && null == ReflectUtil.getFieldValue(bean, field)) { return true; } @@ -953,4 +998,40 @@ public class BeanUtil { throw new IllegalArgumentException("Invalid Getter or Setter name: " + getterOrSetterName); } } + + /** + * 判断source与target的所有公共字段的值是否相同 + * + * @param source 待检测对象1 + * @param target 待检测对象2 + * @param ignoreProperties 不需要检测的字段 + * @return 判断结果,如果为true则证明所有字段的值都相同 + * @author Takak11 + * @since 5.8.4 + */ + public static boolean isCommonFieldsEqual(Object source, Object target, String... ignoreProperties) { + + if (null == source && null == target) { + return true; + } + if (null == source || null == target) { + return false; + } + + final Map sourceFieldsMap = BeanUtil.beanToMap(source); + final Map targetFieldsMap = BeanUtil.beanToMap(target); + + final Set sourceFields = sourceFieldsMap.keySet(); + sourceFields.removeAll(Arrays.asList(ignoreProperties)); + + for (String field : sourceFields) { + if(sourceFieldsMap.containsKey(field) && targetFieldsMap.containsKey(field)){ + if (ObjectUtil.notEqual(sourceFieldsMap.get(field), targetFieldsMap.get(field))) { + return false; + } + } + } + + return true; + } } diff --git a/hutool-core/src/main/java/cn/hutool/core/bean/PropDesc.java b/hutool-core/src/main/java/cn/hutool/core/bean/PropDesc.java index af0d9edefcfe28cc12d9bcb138028b89caaa4c84..eba279382f1026e78ebc3433db6e6724ae1b745d 100644 --- a/hutool-core/src/main/java/cn/hutool/core/bean/PropDesc.java +++ b/hutool-core/src/main/java/cn/hutool/core/bean/PropDesc.java @@ -8,7 +8,6 @@ import cn.hutool.core.util.ModifierUtil; import cn.hutool.core.util.ReflectUtil; import cn.hutool.core.util.TypeUtil; -import java.beans.Transient; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Type; @@ -20,6 +19,11 @@ import java.lang.reflect.Type; */ public class PropDesc { + /** + * Transient注解的类名 + */ + private static final String TRANSIENT_CLASS_NAME = "java.beans.Transient"; + /** * 字段 */ @@ -32,6 +36,22 @@ public class PropDesc { * Setter方法 */ protected Method setter; + /** + * get方法或字段上有无transient关键字和@Transient注解 + */ + private boolean transientForGet; + /** + * set方法或字段上有无transient关键字和@Transient注解 + */ + private boolean transientForSet; + /** + * 检查set方法和字段有无@PropIgnore注解 + */ + private boolean ignoreGet; + /** + * 检查set方法和字段有无@PropIgnore注解 + */ + private boolean ignoreSet; /** * 构造
    @@ -47,6 +67,21 @@ public class PropDesc { this.setter = ClassUtil.setAccessible(setter); } + /** + * 在对象的所有属性设置完成后,执行初始化逻辑。 + *

    + * 预先计算transient关键字和@Transient注解、{@link PropIgnore}注解信息
    + * 见:https://gitee.com/chinabugotech/hutool/pulls/1335 + * + * @since 5.8.38 + */ + public void initialize() { + transientForGet = isTransientForGet(); + transientForSet = isTransientForSet(); + ignoreGet = isIgnoreGet(); + ignoreSet = isIgnoreSet(); + } + /** * 获取字段名,如果存在Alias注解,读取注解的值作为名称 * @@ -133,12 +168,12 @@ public class PropDesc { } // 检查transient关键字和@Transient注解 - if (checkTransient && isTransientForGet()) { + if (checkTransient && transientForGet) { return false; } // 检查@PropIgnore注解 - return false == isIgnoreGet(); + return false == ignoreGet; } /** @@ -203,12 +238,12 @@ public class PropDesc { } // 检查transient关键字和@Transient注解 - if (checkTransient && isTransientForSet()) { + if (checkTransient && transientForSet) { return false; } // 检查@PropIgnore注解 - return false == isIgnoreSet(); + return false == ignoreSet; } /** @@ -339,7 +374,7 @@ public class PropDesc { */ private boolean isIgnoreSet() { return AnnotationUtil.hasAnnotation(this.field, PropIgnore.class) - || AnnotationUtil.hasAnnotation(this.setter, PropIgnore.class); + || AnnotationUtil.hasAnnotation(this.setter, PropIgnore.class); } /** @@ -354,7 +389,7 @@ public class PropDesc { */ private boolean isIgnoreGet() { return AnnotationUtil.hasAnnotation(this.field, PropIgnore.class) - || AnnotationUtil.hasAnnotation(this.getter, PropIgnore.class); + || AnnotationUtil.hasAnnotation(this.getter, PropIgnore.class); } /** @@ -372,7 +407,7 @@ public class PropDesc { // 检查注解 if (false == isTransient) { - isTransient = AnnotationUtil.hasAnnotation(this.getter, Transient.class); + isTransient = AnnotationUtil.hasAnnotation(this.getter, TRANSIENT_CLASS_NAME); } } @@ -394,7 +429,7 @@ public class PropDesc { // 检查注解 if (false == isTransient) { - isTransient = AnnotationUtil.hasAnnotation(this.setter, Transient.class); + isTransient = AnnotationUtil.hasAnnotation(this.setter, TRANSIENT_CLASS_NAME); } } diff --git a/hutool-core/src/main/java/cn/hutool/core/bean/RecordUtil.java b/hutool-core/src/main/java/cn/hutool/core/bean/RecordUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..388472669a6f99d4bb917292bbfbb778a0845e85 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/bean/RecordUtil.java @@ -0,0 +1,110 @@ +package cn.hutool.core.bean; + +import cn.hutool.core.bean.copier.ValueProvider; +import cn.hutool.core.util.ClassUtil; +import cn.hutool.core.util.JdkUtil; +import cn.hutool.core.util.ReflectUtil; + +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.AbstractMap; +import java.util.Map; + +/** + * java.lang.Record 相关工具类封装
    + * 来自于FastJSON2的BeanUtils + * + * @author fastjson2, Looly + * @since 5.8.38 + */ +public class RecordUtil { + + private static volatile Class RECORD_CLASS; + + private static volatile Method METHOD_GET_RECORD_COMPONENTS; + private static volatile Method METHOD_COMPONENT_GET_NAME; + private static volatile Method METHOD_COMPONENT_GET_GENERIC_TYPE; + + /** + * 判断给定类是否为Record类 + * + * @param clazz 类 + * @return 是否为Record类 + */ + public static boolean isRecord(final Class clazz) { + if (JdkUtil.JVM_VERSION < 14) { + // JDK14+支持Record类 + return false; + } + final Class superClass = clazz.getSuperclass(); + if (superClass == null) { + return false; + } + + if (RECORD_CLASS == null) { + // 此处不使用同步代码,重复赋值并不影响判断 + final String superclassName = superClass.getName(); + if ("java.lang.Record".equals(superclassName)) { + RECORD_CLASS = superClass; + return true; + } else { + return false; + } + } + + return superClass == RECORD_CLASS; + } + + /** + * 获取Record类中所有字段名称,getter方法名与字段同名 + * + * @param recordClass Record类 + * @return 字段数组 + */ + @SuppressWarnings("unchecked") + public static Map.Entry[] getRecordComponents(final Class recordClass) { + if (JdkUtil.JVM_VERSION < 14) { + // JDK14+支持Record类 + return new Map.Entry[0]; + } + if (null == METHOD_GET_RECORD_COMPONENTS) { + METHOD_GET_RECORD_COMPONENTS = ReflectUtil.getMethod(Class.class, "getRecordComponents"); + } + + final Class recordComponentClass = ClassUtil.loadClass("java.lang.reflect.RecordComponent"); + if (METHOD_COMPONENT_GET_NAME == null) { + METHOD_COMPONENT_GET_NAME = ReflectUtil.getMethod(recordComponentClass, "getName"); + } + if (METHOD_COMPONENT_GET_GENERIC_TYPE == null) { + METHOD_COMPONENT_GET_GENERIC_TYPE = ReflectUtil.getMethod(recordComponentClass, "getGenericType"); + } + + final Object[] components = ReflectUtil.invoke(recordClass, METHOD_GET_RECORD_COMPONENTS); + final Map.Entry[] entries = new Map.Entry[components.length]; + for (int i = 0; i < components.length; i++) { + entries[i] = new AbstractMap.SimpleEntry<>( + ReflectUtil.invoke(components[i], METHOD_COMPONENT_GET_NAME), + ReflectUtil.invoke(components[i], METHOD_COMPONENT_GET_GENERIC_TYPE) + ); + } + + return entries; + } + + /** + * 实例化Record类 + * + * @param recordClass 类 + * @param valueProvider 参数值提供器 + * @return Record类 + */ + public static Object newInstance(final Class recordClass, final ValueProvider valueProvider) { + final Map.Entry[] recordComponents = getRecordComponents(recordClass); + final Object[] args = new Object[recordComponents.length]; + for (int i = 0; i < args.length; i++) { + args[i] = valueProvider.value(recordComponents[i].getKey(), recordComponents[i].getValue()); + } + + return ReflectUtil.newInstance(recordClass, args); + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/bean/copier/BeanCopier.java b/hutool-core/src/main/java/cn/hutool/core/bean/copier/BeanCopier.java old mode 100644 new mode 100755 index 8d3fd418f2aa048a917c6d68bc7c670830b1eeec..e7a5b47fe6e55ce17f366b99fba1ca6f71db3444 --- a/hutool-core/src/main/java/cn/hutool/core/bean/copier/BeanCopier.java +++ b/hutool-core/src/main/java/cn/hutool/core/bean/copier/BeanCopier.java @@ -1,5 +1,6 @@ package cn.hutool.core.bean.copier; +import cn.hutool.core.lang.Assert; import cn.hutool.core.lang.copier.Copier; import java.io.Serializable; @@ -62,6 +63,8 @@ public class BeanCopier implements Copier, Serializable { * @param copyOptions 拷贝属性选项 */ public BeanCopier(Object source, T target, Type targetType, CopyOptions copyOptions) { + Assert.notNull(source, "Source bean must be not null!"); + Assert.notNull(target, "Target bean must be not null!"); Copier copier; if (source instanceof Map) { if (target instanceof Map) { diff --git a/hutool-core/src/main/java/cn/hutool/core/bean/copier/BeanToBeanCopier.java b/hutool-core/src/main/java/cn/hutool/core/bean/copier/BeanToBeanCopier.java index e91c1914f707a5c6c9c6c5cecd6cf8bafbc51613..804b56176b57c45952e80cfcdb0f8494533e6834 100755 --- a/hutool-core/src/main/java/cn/hutool/core/bean/copier/BeanToBeanCopier.java +++ b/hutool-core/src/main/java/cn/hutool/core/bean/copier/BeanToBeanCopier.java @@ -11,6 +11,7 @@ import java.util.Map; /** * Bean属性拷贝到Bean中的拷贝器 * + * @author Admin * @param 源Bean类型 * @param 目标Bean类型 * @since 5.8.0 @@ -59,8 +60,13 @@ public class BeanToBeanCopier extends AbsCopier { return; } + // 忽略不需要拷贝的 key, + if (false == copyOptions.testKeyFilter(sFieldName)) { + return; + } + // 检查目标字段可写性 - final PropDesc tDesc = targetPropDescMap.get(sFieldName); + final PropDesc tDesc = this.copyOptions.findPropDesc(targetPropDescMap, sFieldName); if (null == tDesc || false == tDesc.isWritable(this.copyOptions.transientSupport)) { // 字段不可写,跳过之 return; diff --git a/hutool-core/src/main/java/cn/hutool/core/bean/copier/BeanToMapCopier.java b/hutool-core/src/main/java/cn/hutool/core/bean/copier/BeanToMapCopier.java index 36a6d1f4b65f90b45f0674af8bb7e3af1d89e2f1..e487e7566160e2e445c1fba133bbdb8b3147eaaf 100755 --- a/hutool-core/src/main/java/cn/hutool/core/bean/copier/BeanToMapCopier.java +++ b/hutool-core/src/main/java/cn/hutool/core/bean/copier/BeanToMapCopier.java @@ -16,22 +16,20 @@ import java.util.Map; @SuppressWarnings("rawtypes") public class BeanToMapCopier extends AbsCopier { - /** - * 目标的Map类型(用于泛型类注入) - */ - private final Type targetType; + // 提前获取目标值真实类型 + private final Type[] targetTypeArguments; /** * 构造 * * @param source 来源Map - * @param target 目标Bean对象 + * @param target 目标Map对象 * @param targetType 目标泛型类型 * @param copyOptions 拷贝选项 */ public BeanToMapCopier(Object source, Map target, Type targetType, CopyOptions copyOptions) { super(source, target, copyOptions); - this.targetType = targetType; + this.targetTypeArguments = TypeUtil.getTypeArguments(targetType); } @Override @@ -57,20 +55,26 @@ public class BeanToMapCopier extends AbsCopier { return; } + // 忽略不需要拷贝的 key, + if (false == copyOptions.testKeyFilter(sFieldName)) { + return; + } + // 检查源对象属性是否过滤属性 Object sValue = sDesc.getValue(this.source); if (false == copyOptions.testPropertyFilter(sDesc.getField(), sValue)) { return; } - // 获取目标值真实类型并转换源值 - final Type[] typeArguments = TypeUtil.getTypeArguments(this.targetType); - if(null != typeArguments){ + // 尝试转换源值 + if(null != targetTypeArguments && targetTypeArguments.length > 1){ //sValue = Convert.convertWithCheck(typeArguments[1], sValue, null, this.copyOptions.ignoreError); - sValue = this.copyOptions.convertField(typeArguments[1], sValue); - sValue = copyOptions.editFieldValue(sFieldName, sValue); + sValue = this.copyOptions.convertField(targetTypeArguments[1], sValue); } + // 自定义值 + sValue = copyOptions.editFieldValue(sFieldName, sValue); + // 目标赋值 if(null != sValue || false == copyOptions.ignoreNullValue){ //noinspection unchecked diff --git a/hutool-core/src/main/java/cn/hutool/core/bean/copier/CopyOptions.java b/hutool-core/src/main/java/cn/hutool/core/bean/copier/CopyOptions.java old mode 100644 new mode 100755 index 2a48a38fb3d436d43fde0b0ed064cd81ae4a3581..d7e479772b2cfabde947e3befac648269572beaa --- a/hutool-core/src/main/java/cn/hutool/core/bean/copier/CopyOptions.java +++ b/hutool-core/src/main/java/cn/hutool/core/bean/copier/CopyOptions.java @@ -1,15 +1,22 @@ package cn.hutool.core.bean.copier; +import cn.hutool.core.bean.PropDesc; +import cn.hutool.core.collection.CollUtil; import cn.hutool.core.convert.Convert; import cn.hutool.core.convert.TypeConverter; +import cn.hutool.core.convert.impl.DateConverter; import cn.hutool.core.lang.Editor; import cn.hutool.core.lang.func.Func1; import cn.hutool.core.lang.func.LambdaUtil; import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ClassUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; import java.io.Serializable; import java.lang.reflect.Field; import java.lang.reflect.Type; +import java.util.Date; import java.util.Map; import java.util.Set; import java.util.function.BiFunction; @@ -67,11 +74,43 @@ public class CopyOptions implements Serializable { */ protected boolean override = true; + /** + * 是否自动转换为驼峰方式 + */ + protected boolean autoTransCamelCase = true; + + /** + * 源对象和目标对象都是 {@code Map} 时, 需要忽略的源对象 {@code Map} key + */ + private Set ignoreKeySet; + /** * 自定义类型转换器,默认使用全局万能转换器转换 */ - protected TypeConverter converter = (type, value) -> - Convert.convertWithCheck(type, value, null, ignoreError); + protected TypeConverter converter = (type, value) -> { + if (null == value) { + return null; + } + + // 快速处理简单值类型的转换 + if (type instanceof Class){ + Class targetType = (Class) type; + if (ClassUtil.isSimpleValueType(targetType) && targetType.isInstance(value)) { + return targetType.cast(value); + } + } + + if (value instanceof IJSONTypeConverter) { + return ((IJSONTypeConverter) value).toBean(ObjectUtil.defaultIfNull(type, Object.class)); + } + + return Convert.convertWithCheck(type, value, null, ignoreError); + }; + + /** + * 在Bean转换时,如果源是String,目标对象是Date或LocalDateTime,则可自定义转换格式 + */ + private String formatIfDate; //region create @@ -168,7 +207,8 @@ public class CopyOptions implements Serializable { * @return CopyOptions */ public CopyOptions setIgnoreProperties(String... ignoreProperties) { - return setPropertiesFilter((field, o) -> false == ArrayUtil.contains(ignoreProperties, field.getName())); + this.ignoreKeySet = CollUtil.newHashSet(ignoreProperties); + return this; } /** @@ -182,8 +222,8 @@ public class CopyOptions implements Serializable { */ @SuppressWarnings("unchecked") public CopyOptions setIgnoreProperties(Func1... funcs) { - final Set ignoreProperties = ArrayUtil.mapToSet(funcs, LambdaUtil::getFieldName); - return setPropertiesFilter((field, o) -> false == ignoreProperties.contains(field.getName())); + this.ignoreKeySet = ArrayUtil.mapToSet(funcs, LambdaUtil::getFieldName); + return this; } /** @@ -229,7 +269,8 @@ public class CopyOptions implements Serializable { } /** - * 设置拷贝属性的字段映射,用于不同的属性之前拷贝做对应表用 + * 设置拷贝属性的字段映射,用于不同的属性之前拷贝做对应表用
    + * 需要注意的是,当使用ValueProvider作为数据提供者时,这个映射是相反的,即fieldMapping中key为目标Bean的名称,而value是提供者中的key * * @param fieldMapping 拷贝属性的字段映射,用于不同的属性之前拷贝做对应表用 * @return CopyOptions @@ -241,7 +282,8 @@ public class CopyOptions implements Serializable { /** * 设置字段属性编辑器,用于自定义属性转换规则,例如驼峰转下划线等
    * 此转换器只针对源端的字段做转换,请确认转换后与目标端字段一致
    - * 当转换后的字段名为null时忽略这个字段 + * 当转换后的字段名为null时忽略这个字段
    + * 需要注意的是,当使用ValueProvider作为数据提供者时,这个映射是相反的,即fieldMapping中key为目标Bean的名称,而value是提供者中的key * * @param fieldNameEditor 字段属性编辑器,用于自定义属性转换规则,例如驼峰转下划线等 * @return CopyOptions @@ -274,7 +316,7 @@ public class CopyOptions implements Serializable { */ protected Object editFieldValue(String fieldName, Object fieldValue) { return (null != this.fieldValueEditor) ? - this.fieldValueEditor.apply(fieldName, fieldValue) : fieldValue; + this.fieldValueEditor.apply(fieldName, fieldValue) : fieldValue; } /** @@ -290,7 +332,7 @@ public class CopyOptions implements Serializable { } /** - * 设置是否覆盖目标值,如果不覆盖,会先读取目标对象的值,非{@code null}则写,否则忽略。如果覆盖,则不判断直接写 + * 设置是否覆盖目标值,如果不覆盖,会先读取目标对象的值,为{@code null}则写,否则忽略。如果覆盖,则不判断直接写 * * @param override 是否覆盖目标值 * @return this @@ -301,6 +343,26 @@ public class CopyOptions implements Serializable { return this; } + /** + * 设置是否自动转换为驼峰方式
    + * 一般用于map转bean和bean转bean出现非驼峰格式时,在尝试转换失败的情况下,是否二次检查转为驼峰匹配
    + * 此设置用于解决Bean和Map转换中的匹配问题而设置,并不是一个强制参数。 + *
      + *
    1. 当map转bean时,如果map中是下划线等非驼峰模式,自动匹配对应的驼峰字段,避免出现字段不拷贝问题。
    2. + *
    3. 当bean转bean时,由于字段命名不规范,使用了非驼峰方式,增加兼容性。
    4. + *
    + *

    + * 但是bean转Map和map转map时,没有使用这个参数,是因为没有匹配的必要,转map不存在无法匹配到的问题,因此此参数无效。 + * + * @param autoTransCamelCase 是否自动转换为驼峰方式 + * @return this + * @since 5.8.25 + */ + public CopyOptions setAutoTransCamelCase(final boolean autoTransCamelCase) { + this.autoTransCamelCase = autoTransCamelCase; + return this; + } + /** * 设置自定义类型转换器,默认使用全局万能转换器转换。 * @@ -313,6 +375,24 @@ public class CopyOptions implements Serializable { return this; } + /** + * 获取日期格式,用于日期转字符串,默认为{@code null} + * @return 日期格式 + */ + public String getFormatIfDate() { + return formatIfDate; + } + + /** + * 设置日期格式,用于日期转字符串,默认为{@code null} + * @param formatIfDate 日期格式 + * @return this + */ + public CopyOptions setFormatIfDate(String formatIfDate) { + this.formatIfDate = formatIfDate; + return this; + } + /** * 使用自定义转换器转换字段值
    * 如果自定义转换器为{@code null},则返回原值。 @@ -322,9 +402,14 @@ public class CopyOptions implements Serializable { * @return 编辑后的字段值 * @since 5.8.0 */ + @SuppressWarnings({"unchecked", "rawtypes"}) protected Object convertField(Type targetType, Object fieldValue) { + if((targetType instanceof Class && Date.class.isAssignableFrom((Class) targetType)) && null != this.formatIfDate){ + return new DateConverter((Class) targetType, this.formatIfDate).convert(fieldValue, null); + } + return (null != this.converter) ? - this.converter.convert(targetType, fieldValue) : fieldValue; + this.converter.convert(targetType, fieldValue) : fieldValue; } /** @@ -348,4 +433,48 @@ public class CopyOptions implements Serializable { protected boolean testPropertyFilter(Field field, Object value) { return null == this.propertiesFilter || this.propertiesFilter.test(field, value); } + + /** + * 测试是否保留key, {@code true} 不保留, {@code false} 保留 + * + * @param key {@link Map} key + * @return 是否保留 + */ + protected boolean testKeyFilter(Object key) { + if (CollUtil.isEmpty(this.ignoreKeySet)) { + return true; + } + + if (ignoreCase) { + // 忽略大小写时要遍历检查 + for (final String ignoreKey : this.ignoreKeySet) { + if (StrUtil.equalsIgnoreCase(key.toString(), ignoreKey)) { + return false; + } + } + } + + return false == this.ignoreKeySet.contains(key); + } + + /** + * 查找Map对应Bean的名称
    + * 尝试原名称、转驼峰名称、isXxx去掉is的名称 + * + * @param targetPropDescMap 目标bean的属性描述Map + * @param sKeyStr 键或字段名 + * @return {@link PropDesc} + */ + protected PropDesc findPropDesc(final Map targetPropDescMap, final String sKeyStr) { + PropDesc propDesc = targetPropDescMap.get(sKeyStr); + // 转驼峰尝试查找 + if (null == propDesc && this.autoTransCamelCase) { + final String camelCaseKey = StrUtil.toCamelCase(sKeyStr); + if (!StrUtil.equals(sKeyStr, camelCaseKey)) { + // 只有转换为驼峰后与原key不同才重复查询,相同说明本身就是驼峰,不需要二次查询 + propDesc = targetPropDescMap.get(camelCaseKey); + } + } + return propDesc; + } } diff --git a/hutool-core/src/main/java/cn/hutool/core/bean/copier/IJSONTypeConverter.java b/hutool-core/src/main/java/cn/hutool/core/bean/copier/IJSONTypeConverter.java new file mode 100644 index 0000000000000000000000000000000000000000..195c58a6fe1c813f8b8a99fc9530953dbd693d63 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/bean/copier/IJSONTypeConverter.java @@ -0,0 +1,24 @@ +package cn.hutool.core.bean.copier; + +import java.lang.reflect.Type; + +/** + * JSON自定义转换扩展接口,因core模块无法直接调用json模块而创建, + * 使用此接口避免使用反射调用toBean方法而性能太差。 + * + * @author mkeq + * @since 5.8.22 + */ +public interface IJSONTypeConverter { + + /** + * 转为实体类对象 + * + * @param Bean类型 + * @param type {@link Type} + * @return 实体类对象 + * @since 3.0.8 + */ + T toBean(Type type); + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/bean/copier/MapToBeanCopier.java b/hutool-core/src/main/java/cn/hutool/core/bean/copier/MapToBeanCopier.java index 349bf7d6cf5ddbb1a004b670519d12ed0845e0ba..ef12c0adbeb0ff314c467973c410b223ffbcace1 100755 --- a/hutool-core/src/main/java/cn/hutool/core/bean/copier/MapToBeanCopier.java +++ b/hutool-core/src/main/java/cn/hutool/core/bean/copier/MapToBeanCopier.java @@ -5,7 +5,6 @@ import cn.hutool.core.bean.PropDesc; import cn.hutool.core.lang.Assert; import cn.hutool.core.map.CaseInsensitiveMap; import cn.hutool.core.map.MapWrapper; -import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.TypeUtil; import java.lang.reflect.Type; @@ -67,8 +66,13 @@ public class MapToBeanCopier extends AbsCopier, T> { return; } + // 忽略不需要拷贝的 key, + if (false == copyOptions.testKeyFilter(sKeyStr)) { + return; + } + // 检查目标字段可写性 - PropDesc tDesc = findPropDesc(targetPropDescMap, sKeyStr); + final PropDesc tDesc = this.copyOptions.findPropDesc(targetPropDescMap, sKeyStr); if (null == tDesc || false == tDesc.isWritable(this.copyOptions.transientSupport)) { // 字段不可写,跳过之 return; @@ -84,6 +88,7 @@ public class MapToBeanCopier extends AbsCopier, T> { final Type fieldType = TypeUtil.getActualType(this.targetType, tDesc.getFieldType()); //Object newValue = Convert.convertWithCheck(fieldType, sValue, null, this.copyOptions.ignoreError); Object newValue = this.copyOptions.convertField(fieldType, sValue); + // 自定义值 newValue = copyOptions.editFieldValue(sKeyStr, newValue); // 目标赋值 @@ -91,35 +96,4 @@ public class MapToBeanCopier extends AbsCopier, T> { }); return this.target; } - - /** - * 查找Map对应Bean的名称
    - * 尝试原名称、转驼峰名称、isXxx去掉is的名称 - * - * @param targetPropDescMap 目标bean的属性描述Map - * @param sKeyStr 键或字段名 - * @return {@link PropDesc} - */ - private PropDesc findPropDesc(Map targetPropDescMap, String sKeyStr){ - PropDesc propDesc = targetPropDescMap.get(sKeyStr); - if(null != propDesc){ - return propDesc; - } - - // 转驼峰尝试查找 - sKeyStr = StrUtil.toCamelCase(sKeyStr); - propDesc = targetPropDescMap.get(sKeyStr); - if(null != propDesc){ - return propDesc; - } - - // boolean类型参数名转换尝试查找 - if(sKeyStr.startsWith("is")){ - sKeyStr = StrUtil.removePreAndLowerFirst(sKeyStr, 2); - propDesc = targetPropDescMap.get(sKeyStr); - return propDesc; - } - - return null; - } } diff --git a/hutool-core/src/main/java/cn/hutool/core/bean/copier/MapToMapCopier.java b/hutool-core/src/main/java/cn/hutool/core/bean/copier/MapToMapCopier.java index e32eaf2e0d82f8f7126b0e7ec6c2086a981845f9..017566147bc1ef856b7773319a9d9d48ba0c77c1 100755 --- a/hutool-core/src/main/java/cn/hutool/core/bean/copier/MapToMapCopier.java +++ b/hutool-core/src/main/java/cn/hutool/core/bean/copier/MapToMapCopier.java @@ -13,10 +13,8 @@ import java.util.Map; @SuppressWarnings({"rawtypes", "unchecked"}) public class MapToMapCopier extends AbsCopier { - /** - * 目标的类型(用于泛型类注入) - */ - private final Type targetType; + // 提前获取目标值真实类型 + private final Type[] targetTypeArguments; /** * 构造 @@ -28,7 +26,7 @@ public class MapToMapCopier extends AbsCopier { */ public MapToMapCopier(Map source, Map target, Type targetType, CopyOptions copyOptions) { super(source, target, copyOptions); - this.targetType = targetType; + targetTypeArguments = TypeUtil.getTypeArguments(targetType); } @Override @@ -37,28 +35,42 @@ public class MapToMapCopier extends AbsCopier { if (null == sKey) { return; } - final String sKeyStr = copyOptions.editFieldName(sKey.toString()); - // 对key做转换,转换后为null的跳过 - if (null == sKeyStr) { + + if(sKey instanceof String){ + sKey = copyOptions.editFieldName((String) sKey); + // 对key做转换,转换后为null的跳过 + if (null == sKey) { + return; + } + } + + // 忽略不需要拷贝的 key, + if (false == copyOptions.testKeyFilter(sKey)) { return; } - final Object targetValue = target.get(sKeyStr); + final Object targetValue = target.get(sKey); // 非覆盖模式下,如果目标值存在,则跳过 if (false == copyOptions.override && null != targetValue) { return; } - // 获取目标值真实类型并转换源值 - final Type[] typeArguments = TypeUtil.getTypeArguments(this.targetType); - if(null != typeArguments){ + // 尝试转换源值 + if (null != targetTypeArguments) { //sValue = Convert.convertWithCheck(typeArguments[1], sValue, null, this.copyOptions.ignoreError); - sValue = this.copyOptions.convertField(typeArguments[1], sValue); - sValue = copyOptions.editFieldValue(sKeyStr, sValue); + sValue = this.copyOptions.convertField(targetTypeArguments[1], sValue); + } + + // 自定义值 + sValue = copyOptions.editFieldValue(sKey.toString(), sValue); + + // 忽略空值 + if (true == copyOptions.ignoreNullValue && sValue == null) { + return; } // 目标赋值 - target.put(sKeyStr, sValue); + target.put(sKey, sValue); }); return this.target; } diff --git a/hutool-core/src/main/java/cn/hutool/core/bean/copier/ValueProviderToBeanCopier.java b/hutool-core/src/main/java/cn/hutool/core/bean/copier/ValueProviderToBeanCopier.java index f2bc0f7562686cd0c52f0bb77c4517544943870b..952923a519f3fc7e66cea82d8d6ca78396cb4ff5 100755 --- a/hutool-core/src/main/java/cn/hutool/core/bean/copier/ValueProviderToBeanCopier.java +++ b/hutool-core/src/main/java/cn/hutool/core/bean/copier/ValueProviderToBeanCopier.java @@ -49,12 +49,22 @@ public class ValueProviderToBeanCopier extends AbsCopier extends AbsCopier { + + private final Object source; + private final boolean ignoreError; + final Map sourcePdMap; + + /** + * 构造 + * + * @param bean Bean + * @param ignoreCase 是否忽略字段大小写 + * @param ignoreError 是否忽略字段值读取错误 + */ + public BeanValueProvider(Object bean, boolean ignoreCase, boolean ignoreError) { + this(bean, ignoreCase, ignoreError, null); + } + + /** + * 构造 + * + * @param bean Bean + * @param ignoreCase 是否忽略字段大小写 + * @param ignoreError 是否忽略字段值读取错误 + * @param keyEditor 键编辑器 + */ + public BeanValueProvider(Object bean, boolean ignoreCase, boolean ignoreError, Editor keyEditor) { + this.source = bean; + this.ignoreError = ignoreError; + final Map sourcePdMap = BeanUtil.getBeanDesc(source.getClass()).getPropMap(ignoreCase); + // issue#2202@Github + // 如果用户定义了键编辑器,则提供的map中的数据必须全部转换key + // issue#I5VRHW@Gitee 使Function可以被序列化 + this.sourcePdMap = new FuncKeyMap<>(new HashMap<>(sourcePdMap.size(), 1), (Function & Serializable)(key) -> { + if (ignoreCase && key instanceof CharSequence) { + key = key.toString().toLowerCase(); + } + if (null != keyEditor) { + key = keyEditor.edit(key.toString()); + } + return key.toString(); + }); + this.sourcePdMap.putAll(sourcePdMap); + } + + @Override + public Object value(String key, Type valueType) { + final PropDesc sourcePd = getPropDesc(key, valueType); + + Object result = null; + if (null != sourcePd) { + result = sourcePd.getValue(this.source, valueType, this.ignoreError); + } + return result; + } + + @Override + public boolean containsKey(String key) { + final PropDesc sourcePd = getPropDesc(key, null); + + // 字段描述不存在或忽略读的情况下,表示不存在 + return null != sourcePd && sourcePd.isReadable(false); + } + + /** + * 获得属性描述 + * + * @param key 字段名 + * @param valueType 值类型,用于判断是否为Boolean,可以为null + * @return 属性描述 + */ + private PropDesc getPropDesc(String key, Type valueType) { + PropDesc sourcePd = sourcePdMap.get(key); + if (null == sourcePd && (null == valueType || Boolean.class == valueType || boolean.class == valueType)) { + //boolean类型字段字段名支持两种方式 + sourcePd = sourcePdMap.get(StrUtil.upperFirstAndAddPre(key, "is")); + } + + return sourcePd; + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/bean/copier/provider/MapValueProvider.java b/hutool-core/src/main/java/cn/hutool/core/bean/copier/provider/MapValueProvider.java new file mode 100644 index 0000000000000000000000000000000000000000..b37fb8f816685c4e6eb314480df1825c8255aa4f --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/bean/copier/provider/MapValueProvider.java @@ -0,0 +1,38 @@ +package cn.hutool.core.bean.copier.provider; + +import cn.hutool.core.bean.copier.ValueProvider; +import cn.hutool.core.convert.Convert; + +import java.lang.reflect.Type; +import java.util.Map; + +/** + * Map值提供者 + * + * @author Looly + * @since 5.8.40 + */ +@SuppressWarnings("rawtypes") +public class MapValueProvider implements ValueProvider { + + private final Map map; + + /** + * 构造 + * + * @param map map + */ + public MapValueProvider(final Map map) { + this.map = map; + } + + @Override + public Object value(String key, Type valueType) { + return Convert.convert(valueType, map.get(key)); + } + + @Override + public boolean containsKey(String key) { + return map.containsKey(key); + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/codec/BCD.java b/hutool-core/src/main/java/cn/hutool/core/codec/BCD.java index 6600133ccdd042959a6bb648e901f3a1762fe3ee..0bd558a04f174bbed494a319bbfca70cde5f2be9 100644 --- a/hutool-core/src/main/java/cn/hutool/core/codec/BCD.java +++ b/hutool-core/src/main/java/cn/hutool/core/codec/BCD.java @@ -8,7 +8,9 @@ import cn.hutool.core.lang.Assert; * see http://cuisuqiang.iteye.com/blog/1429956 * @author Looly * + * @deprecated 由于对于ASCII的编码解码有缺陷,且这种BCD实现并不规范,因此会在6.0.0中移除 */ +@Deprecated public class BCD { /** @@ -17,6 +19,7 @@ public class BCD { * @return BCD */ public static byte[] strToBcd(String asc) { + Assert.notNull(asc, "ASCII must not be null!"); int len = asc.length(); int mod = len % 2; if (mod != 0) { diff --git a/hutool-core/src/main/java/cn/hutool/core/codec/Base64.java b/hutool-core/src/main/java/cn/hutool/core/codec/Base64.java old mode 100644 new mode 100755 index 2ffc710c2e646ac771a4901c83bd17e6e99d4b4c..680aa652987cbef5c848072f3652d67589b92d8a --- a/hutool-core/src/main/java/cn/hutool/core/codec/Base64.java +++ b/hutool-core/src/main/java/cn/hutool/core/codec/Base64.java @@ -30,6 +30,9 @@ public class Base64 { * @return 编码后的bytes */ public static byte[] encode(byte[] arr, boolean lineSep) { + if (arr == null) { + return null; + } return lineSep ? java.util.Base64.getMimeEncoder().encode(arr) : java.util.Base64.getEncoder().encode(arr); @@ -137,6 +140,9 @@ public class Base64 { * @return 被加密后的字符串 */ public static String encode(byte[] source) { + if (source == null) { + return null; + } return java.util.Base64.getEncoder().encodeToString(source); } @@ -148,6 +154,9 @@ public class Base64 { * @since 5.5.2 */ public static String encodeWithoutPadding(byte[] source) { + if (source == null) { + return null; + } return java.util.Base64.getEncoder().withoutPadding().encodeToString(source); } @@ -159,6 +168,9 @@ public class Base64 { * @since 3.0.6 */ public static String encodeUrlSafe(byte[] source) { + if (source == null) { + return null; + } return java.util.Base64.getUrlEncoder().withoutPadding().encodeToString(source); } @@ -239,7 +251,7 @@ public class Base64 { * base64解码 * * @param source 被解码的base64字符串 - * @return 被加密后的字符串 + * @return 密文解密的结果 * @since 4.3.2 */ public static String decodeStrGbk(CharSequence source) { @@ -250,7 +262,7 @@ public class Base64 { * base64解码 * * @param source 被解码的base64字符串 - * @return 被加密后的字符串 + * @return 密文解密的结果 */ public static String decodeStr(CharSequence source) { return Base64Decoder.decodeStr(source); @@ -261,7 +273,7 @@ public class Base64 { * * @param source 被解码的base64字符串 * @param charset 字符集 - * @return 被加密后的字符串 + * @return 密文解密的结果 */ public static String decodeStr(CharSequence source, String charset) { return decodeStr(source, CharsetUtil.charset(charset)); @@ -272,7 +284,7 @@ public class Base64 { * * @param source 被解码的base64字符串 * @param charset 字符集 - * @return 被加密后的字符串 + * @return 密文解密的结果 */ public static String decodeStr(CharSequence source, Charset charset) { return Base64Decoder.decodeStr(source, charset); @@ -352,6 +364,9 @@ public class Base64 { * @since 5.7.5 */ public static boolean isBase64(byte[] base64Bytes) { + if (base64Bytes == null || base64Bytes.length < 3) { + return false; + } boolean hasPadding = false; for (byte base64Byte : base64Bytes) { if (hasPadding) { diff --git a/hutool-core/src/main/java/cn/hutool/core/codec/Caesar.java b/hutool-core/src/main/java/cn/hutool/core/codec/Caesar.java index c7c94f40e196b51ac1e4d78f807b9b2d1be2c5e1..d1b5d36f95fd4326d875f61d5c3e1fa102b9920b 100644 --- a/hutool-core/src/main/java/cn/hutool/core/codec/Caesar.java +++ b/hutool-core/src/main/java/cn/hutool/core/codec/Caesar.java @@ -1,5 +1,7 @@ package cn.hutool.core.codec; +import cn.hutool.core.lang.Assert; + /** * 凯撒密码实现
    * 算法来自:https://github.com/zhaorenjie110/SymmetricEncryptionAndDecryption @@ -19,6 +21,7 @@ public class Caesar { * @return 加密后的内容 */ public static String encode(String message, int offset) { + Assert.notNull(message, "message must be not null!"); final int len = message.length(); final char[] plain = message.toCharArray(); char c; @@ -40,6 +43,7 @@ public class Caesar { * @return 解密后的内容 */ public static String decode(String cipherText, int offset) { + Assert.notNull(cipherText, "cipherText must be not null!"); final int len = cipherText.length(); final char[] plain = cipherText.toCharArray(); char c; diff --git a/hutool-core/src/main/java/cn/hutool/core/codec/Morse.java b/hutool-core/src/main/java/cn/hutool/core/codec/Morse.java index 403ba21c370e19d256e7751a557bbd87828f8892..0ed10b05bdac23c07ea206b7f1cdf7ac6ae74b44 100644 --- a/hutool-core/src/main/java/cn/hutool/core/codec/Morse.java +++ b/hutool-core/src/main/java/cn/hutool/core/codec/Morse.java @@ -10,7 +10,7 @@ import cn.hutool.core.util.StrUtil; /** * 莫尔斯电码的编码和解码实现
    - * 参考:https://github.com/TakWolf/Java-MorseCoder + * 参考:https://github.com/TakWolf-Deprecated/Java-MorseCoder * * @author looly, TakWolf * @since 4.4.1 diff --git a/hutool-core/src/main/java/cn/hutool/core/codec/PercentCodec.java b/hutool-core/src/main/java/cn/hutool/core/codec/PercentCodec.java old mode 100644 new mode 100755 index 88fd210a048d086b4f6a34d9c99e830a6229fcfe..9d91cd17d231fef3f7089f36b4202bdf04e42789 --- a/hutool-core/src/main/java/cn/hutool/core/codec/PercentCodec.java +++ b/hutool-core/src/main/java/cn/hutool/core/codec/PercentCodec.java @@ -1,5 +1,6 @@ package cn.hutool.core.codec; +import cn.hutool.core.lang.Assert; import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.CharUtil; import cn.hutool.core.util.HexUtil; @@ -51,6 +52,7 @@ public class PercentCodec implements Serializable { * @return PercentCodec */ public static PercentCodec of(CharSequence chars) { + Assert.notNull(chars, "chars must not be null"); final PercentCodec codec = new PercentCodec(); final int length = chars.length(); for (int i = 0; i < length; i++) { diff --git a/hutool-core/src/main/java/cn/hutool/core/codec/PunyCode.java b/hutool-core/src/main/java/cn/hutool/core/codec/PunyCode.java index fd3fc38646677fb870c9682cb484ae9a6164b53d..ea3b32db5f3abf054c4834627913c62541731b15 100644 --- a/hutool-core/src/main/java/cn/hutool/core/codec/PunyCode.java +++ b/hutool-core/src/main/java/cn/hutool/core/codec/PunyCode.java @@ -2,8 +2,11 @@ package cn.hutool.core.codec; import cn.hutool.core.exceptions.UtilException; import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.CharUtil; import cn.hutool.core.util.StrUtil; +import java.util.List; + /** * Punycode是一个根据RFC 3492标准而制定的编码系统,主要用于把域名从地方语言所采用的Unicode编码转换成为可用于DNS系统的编码 *

    @@ -24,6 +27,27 @@ public class PunyCode { public static final String PUNY_CODE_PREFIX = "xn--"; + /** + * punycode转码域名 + * + * @param domain 域名 + * @return 编码后的域名 + * @throws UtilException 计算异常 + */ + public static String encodeDomain(String domain) throws UtilException { + Assert.notNull(domain, "domain must not be null!"); + final List split = StrUtil.split(domain, CharUtil.DOT); + final StringBuilder result = new StringBuilder(domain.length() * 4); + for (final String str : split) { + if (result.length() != 0) { + result.append(CharUtil.DOT); + } + result.append(encode(str, true)); + } + + return result.toString(); + } + /** * 将内容编码为PunyCode * @@ -44,6 +68,7 @@ public class PunyCode { * @throws UtilException 计算异常 */ public static String encode(CharSequence input, boolean withPrefix) throws UtilException { + Assert.notNull(input, "input must not be null!"); int n = INITIAL_N; int delta = 0; int bias = INITIAL_BIAS; @@ -60,6 +85,10 @@ public class PunyCode { } // Append delimiter if (b > 0) { + if(b == length){ + // 无需要编码的字符 + return output.toString(); + } output.append(DELIMITER); } int h = b; @@ -118,6 +147,27 @@ public class PunyCode { return output.toString(); } + /** + * 解码punycode域名 + * + * @param domain PunyCode域名 + * @return 解码后的域名 + * @throws UtilException 计算异常 + */ + public static String decodeDomain(String domain) throws UtilException { + Assert.notNull(domain, "domain must not be null!"); + final List split = StrUtil.split(domain, CharUtil.DOT); + final StringBuilder result = new StringBuilder(domain.length() / 4 + 1); + for (final String str : split) { + if (result.length() != 0) { + result.append(CharUtil.DOT); + } + result.append(StrUtil.startWithIgnoreEquals(str, PUNY_CODE_PREFIX) ? decode(str) : str); + } + + return result.toString(); + } + /** * 解码 PunyCode为字符串 * @@ -126,6 +176,7 @@ public class PunyCode { * @throws UtilException 计算异常 */ public static String decode(String input) throws UtilException { + Assert.notNull(input, "input must not be null!"); input = StrUtil.removePrefixIgnoreCase(input, PUNY_CODE_PREFIX); int n = INITIAL_N; diff --git a/hutool-core/src/main/java/cn/hutool/core/codec/Rot.java b/hutool-core/src/main/java/cn/hutool/core/codec/Rot.java index ff0cdd135d32440e49e9af080d170c149e2da819..d0826aee18de155afcd73a3841003a0deb73350a 100644 --- a/hutool-core/src/main/java/cn/hutool/core/codec/Rot.java +++ b/hutool-core/src/main/java/cn/hutool/core/codec/Rot.java @@ -1,5 +1,7 @@ package cn.hutool.core.codec; +import cn.hutool.core.lang.Assert; + /** * RotN(rotate by N places),回转N位密码,是一种简易的替换式密码,也是过去在古罗马开发的凯撒加密的一种变体。
    * 代码来自:https://github.com/orclight/jencrypt @@ -30,11 +32,11 @@ public class Rot { * Rot-13编码 * * @param message 被编码的消息 - * @param isEnocdeNumber 是否编码数字 + * @param isEncodeNumber 是否编码数字 * @return 编码后的字符串 */ - public static String encode13(String message, boolean isEnocdeNumber) { - return encode(message, 13, isEnocdeNumber); + public static String encode13(String message, boolean isEncodeNumber) { + return encode(message, 13, isEncodeNumber); } /** @@ -42,15 +44,16 @@ public class Rot { * * @param message 被编码的消息 * @param offset 位移,常用位移13 - * @param isEnocdeNumber 是否编码数字 + * @param isEncodeNumber 是否编码数字 * @return 编码后的字符串 */ - public static String encode(String message, int offset, boolean isEnocdeNumber) { + public static String encode(String message, int offset, boolean isEncodeNumber) { + Assert.notNull(message, "message must not be null"); final int len = message.length(); final char[] chars = new char[len]; for (int i = 0; i < len; i++) { - chars[i] = encodeChar(message.charAt(i), offset, isEnocdeNumber); + chars[i] = encodeChar(message.charAt(i), offset, isEncodeNumber); } return new String(chars); } @@ -85,6 +88,7 @@ public class Rot { * @return 解码后的字符串 */ public static String decode(String rot, int offset, boolean isDecodeNumber) { + Assert.notNull(rot, "rot must not be null"); final int len = rot.length(); final char[] chars = new char[len]; diff --git a/hutool-core/src/main/java/cn/hutool/core/collection/CollStreamUtil.java b/hutool-core/src/main/java/cn/hutool/core/collection/CollStreamUtil.java index b70f24a62739c67ecfbaf6a5e298dbb0d9d5906d..e99f3433385b30774c36353e6d2c1e866e52f2a3 100644 --- a/hutool-core/src/main/java/cn/hutool/core/collection/CollStreamUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/collection/CollStreamUtil.java @@ -7,7 +7,6 @@ import cn.hutool.core.stream.CollectorUtil; import cn.hutool.core.stream.StreamUtil; import java.util.Collection; -import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -22,7 +21,7 @@ import java.util.stream.Collectors; /** * 集合的stream操作封装 * - * @author 528910437@QQ.COM, VampireAchao<achao1441470436@gmail.com> + * @author 528910437@QQ.COM, VampireAchao<achao1441470436@gmail.com>Lion Li> * @since 5.5.2 */ public class CollStreamUtil { @@ -55,7 +54,7 @@ public class CollStreamUtil { */ public static Map toIdentityMap(Collection collection, Function key, boolean isParallel) { if (CollUtil.isEmpty(collection)) { - return Collections.emptyMap(); + return MapUtil.newHashMap(0); } return toMap(collection, (v) -> Opt.ofNullable(v).map(key).get(), Function.identity(), isParallel); } @@ -88,7 +87,7 @@ public class CollStreamUtil { */ public static Map toMap(Collection collection, Function key, Function value, boolean isParallel) { if (CollUtil.isEmpty(collection)) { - return Collections.emptyMap(); + return MapUtil.newHashMap(0); } return StreamUtil.of(collection, isParallel) .collect(HashMap::new, (m, v) -> m.put(key.apply(v), value.apply(v)), HashMap::putAll); @@ -122,7 +121,7 @@ public class CollStreamUtil { */ public static Map> groupByKey(Collection collection, Function key, boolean isParallel) { if (CollUtil.isEmpty(collection)) { - return Collections.emptyMap(); + return MapUtil.newHashMap(0); } return groupBy(collection, key, Collectors.toList(), isParallel); } @@ -160,7 +159,7 @@ public class CollStreamUtil { public static Map>> groupBy2Key(Collection collection, Function key1, Function key2, boolean isParallel) { if (CollUtil.isEmpty(collection)) { - return Collections.emptyMap(); + return MapUtil.newHashMap(0); } return groupBy(collection, key1, CollectorUtil.groupingBy(key2, Collectors.toList()), isParallel); } @@ -197,7 +196,7 @@ public class CollStreamUtil { public static Map> group2Map(Collection collection, Function key1, Function key2, boolean isParallel) { if (CollUtil.isEmpty(collection) || key1 == null || key2 == null) { - return Collections.emptyMap(); + return MapUtil.newHashMap(0); } return groupBy(collection, key1, CollectorUtil.toMap(key2, Function.identity(), (l, r) -> l), isParallel); } @@ -235,7 +234,7 @@ public class CollStreamUtil { public static Map> groupKeyValue(Collection collection, Function key, Function value, boolean isParallel) { if (CollUtil.isEmpty(collection)) { - return Collections.emptyMap(); + return MapUtil.newHashMap(0); } return groupBy(collection, key, Collectors.mapping(v -> Opt.ofNullable(v).map(value).orElse(null), Collectors.toList()), isParallel); } @@ -254,7 +253,7 @@ public class CollStreamUtil { */ public static Map groupBy(Collection collection, Function key, Collector downstream) { if (CollUtil.isEmpty(collection)) { - return Collections.emptyMap(); + return MapUtil.newHashMap(0); } return groupBy(collection, key, downstream, false); } @@ -275,7 +274,7 @@ public class CollStreamUtil { */ public static Map groupBy(Collection collection, Function key, Collector downstream, boolean isParallel) { if (CollUtil.isEmpty(collection)) { - return Collections.emptyMap(); + return MapUtil.newHashMap(0); } return StreamUtil.of(collection, isParallel).collect(CollectorUtil.groupingBy(key, downstream)); } @@ -307,7 +306,7 @@ public class CollStreamUtil { */ public static List toList(Collection collection, Function function, boolean isParallel) { if (CollUtil.isEmpty(collection)) { - return Collections.emptyList(); + return CollUtil.newArrayList(); } return StreamUtil.of(collection, isParallel) .map(function) @@ -342,7 +341,7 @@ public class CollStreamUtil { */ public static Set toSet(Collection collection, Function function, boolean isParallel) { if (CollUtil.isEmpty(collection)) { - return Collections.emptySet(); + return CollUtil.newHashSet(); } return StreamUtil.of(collection, isParallel) .map(function) @@ -365,11 +364,11 @@ public class CollStreamUtil { */ public static Map merge(Map map1, Map map2, BiFunction merge) { if (MapUtil.isEmpty(map1) && MapUtil.isEmpty(map2)) { - return Collections.emptyMap(); + return MapUtil.newHashMap(0); } else if (MapUtil.isEmpty(map1)) { - map1 = Collections.emptyMap(); + map1 = MapUtil.newHashMap(0); } else if (MapUtil.isEmpty(map2)) { - map2 = Collections.emptyMap(); + map2 = MapUtil.newHashMap(0); } Set key = new HashSet<>(); key.addAll(map1.keySet()); @@ -385,4 +384,5 @@ public class CollStreamUtil { } return map; } + } diff --git a/hutool-core/src/main/java/cn/hutool/core/collection/CollUtil.java b/hutool-core/src/main/java/cn/hutool/core/collection/CollUtil.java index cae324ed4c11f7932a053eca7972fad18b42ed44..05e0d78d962e34c205e5e232a1610f77e6ca9512 100755 --- a/hutool-core/src/main/java/cn/hutool/core/collection/CollUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/collection/CollUtil.java @@ -7,47 +7,16 @@ import cn.hutool.core.comparator.PropertyComparator; import cn.hutool.core.convert.Convert; import cn.hutool.core.convert.ConverterRegistry; import cn.hutool.core.exceptions.UtilException; -import cn.hutool.core.lang.Editor; -import cn.hutool.core.lang.Filter; -import cn.hutool.core.lang.Matcher; +import cn.hutool.core.lang.*; import cn.hutool.core.lang.func.Func1; import cn.hutool.core.lang.hash.Hash32; import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.ArrayUtil; -import cn.hutool.core.util.CharUtil; -import cn.hutool.core.util.ClassUtil; -import cn.hutool.core.util.ObjectUtil; -import cn.hutool.core.util.ReflectUtil; -import cn.hutool.core.util.StrUtil; -import cn.hutool.core.util.TypeUtil; +import cn.hutool.core.util.*; import java.io.Serializable; import java.lang.reflect.Type; -import java.util.AbstractCollection; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.Deque; -import java.util.EnumSet; -import java.util.Enumeration; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.Map.Entry; -import java.util.NavigableSet; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.SortedSet; -import java.util.Stack; -import java.util.TreeMap; -import java.util.TreeSet; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.CopyOnWriteArrayList; @@ -108,6 +77,9 @@ public class CollUtil { * @return 并集的集合,返回 {@link ArrayList} */ public static Collection union(Collection coll1, Collection coll2) { + if (isEmpty(coll1) && isEmpty(coll2)) { + return new ArrayList<>(); + } if (isEmpty(coll1)) { return new ArrayList<>(coll2); } else if (isEmpty(coll2)) { @@ -145,6 +117,9 @@ public class CollUtil { public static Collection union(Collection coll1, Collection coll2, Collection... otherColls) { Collection union = union(coll1, coll2); for (Collection coll : otherColls) { + if (isEmpty(coll)) { + continue; + } union = union(union, coll); } return union; @@ -177,6 +152,9 @@ public class CollUtil { if (ArrayUtil.isNotEmpty(otherColls)) { for (Collection otherColl : otherColls) { + if (isEmpty(otherColl)) { + continue; + } result.addAll(otherColl); } } @@ -198,24 +176,39 @@ public class CollUtil { */ @SafeVarargs public static List unionAll(Collection coll1, Collection coll2, Collection... otherColls) { - final List result; - if (isEmpty(coll1)) { - result = new ArrayList<>(); - } else { - result = new ArrayList<>(coll1); + if (CollUtil.isEmpty(coll1) && CollUtil.isEmpty(coll2) && ArrayUtil.isEmpty(otherColls)) { + return new ArrayList<>(0); } - if (isNotEmpty(coll2)) { - result.addAll(coll2); + // 计算元素总数 + int totalSize = 0; + totalSize += size(coll1); + totalSize += size(coll2); + if (otherColls != null) { + for (final Collection otherColl : otherColls) { + totalSize += size(otherColl); + } } - if (ArrayUtil.isNotEmpty(otherColls)) { - for (Collection otherColl : otherColls) { - result.addAll(otherColl); + // 根据size创建,防止多次扩容 + final List res = new ArrayList<>(totalSize); + if (coll1 != null) { + res.addAll(coll1); + } + if (coll2 != null) { + res.addAll(coll2); + } + if (otherColls == null) { + return res; + } + + for (final Collection otherColl : otherColls) { + if (otherColl != null) { + res.addAll(otherColl); } } - return result; + return res; } /** @@ -315,7 +308,7 @@ public class CollUtil { } /** - * 两个集合的差集
    + * 两个集合的对称差集 (A-B)∪(B-A)
    * 针对一个集合中存在多个相同元素的情况,计算两个集合中此元素的个数,保留两个集合中此元素个数差的个数
    * 例如: * @@ -368,12 +361,23 @@ public class CollUtil { * @return 单差集 */ public static Collection subtract(Collection coll1, Collection coll2) { + if(isEmpty(coll1) || isEmpty(coll2)){ + return coll1; + } + Collection result = ObjectUtil.clone(coll1); - if (null == result) { - result = CollUtil.create(coll1.getClass()); + try { + if (null == result) { + result = CollUtil.create(coll1.getClass()); + result.addAll(coll1); + } + result.removeAll(coll2); + } catch (UnsupportedOperationException e) { + // 针对 coll1 为只读集合的补偿 + result = CollUtil.create(AbstractCollection.class); result.addAll(coll1); + result.removeAll(coll2); } - result.removeAll(coll2); return result; } @@ -391,22 +395,57 @@ public class CollUtil { * @since 5.3.5 */ public static List subtractToList(Collection coll1, Collection coll2) { + return subtractToList(coll1, coll2, true); + } + /** + * 计算集合的单差集,即只返回【集合1】中有,但是【集合2】中没有的元素 + * 只要【集合1】中的某个元素在【集合2】中存在(equals和hashcode),就会被排除 + * + *

    +	 * 示例:
    +	 * 1. subtractToList([null, null, null, null], [null, null]) → []
    +	 * 2. subtractToList([null, null, null, null], [null, null, "c"]) → []
    +	 * 3. subtractToList(["a", "b", "c"], ["a", "b", "c"]) → []
    +	 * 4. subtractToList([], ["a", "b", "c"]) → []
    +	 * 5. subtractToList(["a", "b", "c"], []) → ["a", "b", "c"]
    +	 * 6. subtractToList(["a", "a", "b", "b", "c", "c", "d"], ["b", "c"]) → ["a", "a", "d"]
    +	 * 7. subtractToList(["a", null, "b"], ["a", "c"]) → [null, "b"]
    +	 * 8. subtractToList(["a", "b", "c"], ["d", "e", "f"]) → ["a", "b", "c"]
    +	 * 9. subtractToList(["a", "a", "b", "b", "c"], ["d", "e", "f"]) → ["a", "a", "b", "b", "c"]
    +	 * 
    + * + * @param coll1 集合1,需要计算差集的源集合 + * @param coll2 集合2,需要从集合1中排除的元素所在集合 + * @param isLinked 返回的集合类型是否是LinkedList,{@code true}返回{@link LinkedList},{@code false}返回{@link ArrayList} + * @param 元素类型 + * @return 单差集结果。当【集合1】为空时返回空列表;当【集合2】为空时返回集合1的拷贝;否则返回【集合1】中排除【集合2】中所有元素后的结果 + */ + public static List subtractToList(Collection coll1, Collection coll2, boolean isLinked) { if (isEmpty(coll1)) { return ListUtil.empty(); } + if (isEmpty(coll2)) { - return ListUtil.list(true, coll1); + return ListUtil.list(isLinked, coll1); } - //将被交数用链表储存,防止因为频繁扩容影响性能 - final List result = new LinkedList<>(); + /* + 返回的集合最大不会超过 coll1.size + 所以这里创建 ArrayList 时 initialCapacity 给的是 coll1.size + 这样做可以避免频繁扩容 + */ + final List result = isLinked + ? new LinkedList<>() + : new ArrayList<>(coll1.size()); + Set set = new HashSet<>(coll2); for (T t : coll1) { - if (false == set.contains(t)) { + if (!set.contains(t)) { result.add(t); } } + return result; } @@ -430,7 +469,7 @@ public class CollUtil { * * @param collection 集合 * @param value 需要查找的值 - * @return 果集合为空(null或者空),返回{@code false},否则找到元素返回{@code true} + * @return 如果集合为空(null或者空),返回{@code false},否则找到元素返回{@code true} * @since 5.7.16 */ public static boolean safeContains(Collection collection, Object value) { @@ -493,13 +532,16 @@ public class CollUtil { } /** - * 集合1中是否包含集合2中所有的元素,即集合2是否为集合1的子集 + * 集合1中是否包含集合2中所有的元素。
    + * 当集合1和集合2都为空时,返回{@code true} + * 当集合2为空时,返回{@code true} * * @param coll1 集合1 * @param coll2 集合2 * @return 集合1中是否包含集合2中所有的元素 * @since 4.5.12 */ + @SuppressWarnings("SuspiciousMethodCalls") public static boolean containsAll(Collection coll1, Collection coll2) { if (isEmpty(coll1)) { return isEmpty(coll2); @@ -509,12 +551,31 @@ public class CollUtil { return true; } - if (coll1.size() < coll2.size()) { - return false; + // Set直接判定 + if(coll1 instanceof Set){ + return coll1.containsAll(coll2); } - for (Object object : coll2) { - if (false == coll1.contains(object)) { + // 参考Apache commons collection4 + // 将时间复杂度降低到O(n + m) + final Iterator it = coll1.iterator(); + final Set elementsAlreadySeen = new HashSet<>(coll1.size(), 1); + for (final Object nextElement : coll2) { + if (elementsAlreadySeen.contains(nextElement)) { + continue; + } + + boolean foundCurrentElement = false; + while (it.hasNext()) { + final Object p = it.next(); + elementsAlreadySeen.add(p); + if (Objects.equals(nextElement, p)) { + foundCurrentElement = true; + break; + } + } + + if (false == foundCurrentElement) { return false; } } @@ -664,6 +725,36 @@ public class CollUtil { return currentAlaDatas; } + /** + * 是否至少有一个符合判断条件 + * + * @param 集合元素类型 + * @param collection 集合 + * @param predicate 自定义判断函数 + * @return 是否有一个值匹配 布尔值 + */ + public static boolean anyMatch(Collection collection,Predicate predicate){ + if(isEmpty(collection)){ + return Boolean.FALSE; + } + return collection.stream().anyMatch(predicate); + } + + /** + * 是否全部匹配判断条件 + * + * @param 集合元素类型 + * @param collection 集合 + * @param predicate 自定义判断函数 + * @return 是否全部匹配 布尔值 + */ + public static boolean allMatch(Collection collection,Predicate predicate){ + if(isEmpty(collection)){ + return Boolean.FALSE; + } + return collection.stream().allMatch(predicate); + } + // ----------------------------------------------------------------------------------------------- new HashSet /** @@ -969,7 +1060,7 @@ public class CollUtil { * @since 3.3.0 */ public static BlockingQueue newBlockingQueue(int capacity, boolean isLinked) { - BlockingQueue queue; + final BlockingQueue queue; if (isLinked) { queue = new LinkedBlockingDeque<>(capacity); } else { @@ -986,9 +1077,22 @@ public class CollUtil { * @return 集合类型对应的实例 * @since 3.0.8 */ - @SuppressWarnings({"unchecked", "rawtypes"}) public static Collection create(Class collectionType) { - Collection list; + return create(collectionType, null); + } + + /** + * 创建新的集合对象,返回具体的泛型集合 + * + * @param 集合元素类型 + * @param collectionType 集合类型,rawtype 如 ArrayList.class, EnumSet.class ... + * @param elementType 集合元素类型 + * @return 集合类型对应的实例 + * @since v5 + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + public static Collection create(Class collectionType, Class elementType) { + final Collection list; if (collectionType.isAssignableFrom(AbstractCollection.class)) { // 抽象集合默认使用ArrayList list = new ArrayList<>(); @@ -1008,7 +1112,7 @@ public class CollUtil { return CompareUtil.compare(o1.toString(), o2.toString()); }); } else if (collectionType.isAssignableFrom(EnumSet.class)) { - list = (Collection) EnumSet.noneOf((Class) ClassUtil.getTypeArgument(collectionType)); + list = (Collection) EnumSet.noneOf(Assert.notNull((Class) elementType)); } // List @@ -1022,7 +1126,7 @@ public class CollUtil { else { try { list = (Collection) ReflectUtil.newInstance(collectionType); - } catch (Exception e) { + } catch (final Exception e) { // 无法创建当前类型的对象,尝试创建父类型对象 final Class superclass = collectionType.getSuperclass(); if (null != superclass && collectionType != superclass) { @@ -1055,10 +1159,11 @@ public class CollUtil { * 根据函数生成的KEY去重集合,如根据Bean的某个或者某些字段完成去重。
    * 去重可选是保留最先加入的值还是后加入的值 * - * @param 集合元素类型 - * @param 唯一键类型 - * @param collection 集合 - * @param override 是否覆盖模式,如果为{@code true},加入的新值会覆盖相同key的旧值,否则会忽略新加值 + * @param 集合元素类型 + * @param 唯一键类型 + * @param collection 集合 + * @param uniqueGenerator 唯一键生成器 + * @param override 是否覆盖模式,如果为{@code true},加入的新值会覆盖相同key的旧值,否则会忽略新加值 * @return {@link ArrayList} * @since 5.8.0 */ @@ -1172,11 +1277,12 @@ public class CollUtil { return result; } - ArrayList subList = new ArrayList<>(size); + final int initSize = Math.min(collection.size(), size); + List subList = new ArrayList<>(initSize); for (T t : collection) { if (subList.size() >= size) { result.add(subList); - subList = new ArrayList<>(size); + subList = new ArrayList<>(initSize); } subList.add(t); } @@ -1595,12 +1701,12 @@ public class CollUtil { } int matchIndex = -1; if (isNotEmpty(collection)) { - int index = collection.size(); + int index = 0; for (T t : collection) { if (null == matcher || matcher.match(t)) { matchIndex = index; } - index--; + index++; } } return matchIndex; @@ -1723,7 +1829,7 @@ public class CollUtil { * @return 是否为非空 */ public static boolean isNotEmpty(Collection collection) { - return false == isEmpty(collection); + return !isEmpty(collection); } /** @@ -2062,6 +2168,33 @@ public class CollUtil { return IterUtil.toMap(null == values ? null : values.iterator(), map, keyFunc, valueFunc); } + /** + * 一个对象不为空且不存在于该集合中时,加入到该集合中
    + *
    +	 *     null, null -> false
    +	 *     [], null -> false
    +	 *     null, "123" -> false
    +	 *     ["123"], "123" -> false
    +	 *     [], "123" -> true
    +	 *     ["456"], "123" -> true
    +	 *     [Animal{"name": "jack"}], Dog{"name": "jack"} -> true
    +	 * 
    + * + * @param collection 被加入的集合 + * @param object 要添加到集合的对象 + * @param 集合元素类型 + * @param 要添加的元素类型【为集合元素类型的类型或子类型】 + * @return 是否添加成功 + * @author Cloud-Style + */ + public static boolean addIfAbsent(Collection collection, S object) { + if (object == null || collection == null || collection.contains(object)) { + return false; + } + + return collection.add(object); + } + /** * 将指定对象全部加入到集合中
    * 提供的对象如果为集合类型,会自动转换为目标元素类型
    @@ -2100,7 +2233,13 @@ public class CollUtil { if (value instanceof Iterator) { iter = (Iterator) value; } else if (value instanceof Iterable) { - iter = ((Iterable) value).iterator(); + if(value instanceof Map && BeanUtil.isBean(TypeUtil.getClass(elementType))){ + //https://github.com/chinabugotech/hutool/issues/3139 + // 如果值为Map,而目标为一个Bean,则Map应整体转换为Bean,而非拆分成Entry转换 + iter = new ArrayIter<>(new Object[]{value}); + }else{ + iter = ((Iterable) value).iterator(); + } } else if (value instanceof Enumeration) { iter = new EnumerationIter<>((Enumeration) value); } else if (ArrayUtil.isArray(value)) { @@ -2803,7 +2942,7 @@ public class CollUtil { * @since 4.6.5 */ public static > T max(Collection coll) { - return Collections.max(coll); + return isEmpty(coll) ? null : Collections.max(coll); } /** @@ -2816,7 +2955,7 @@ public class CollUtil { * @since 4.6.5 */ public static > T min(Collection coll) { - return Collections.min(coll); + return isEmpty(coll) ? null : Collections.min(coll); } /** diff --git a/hutool-core/src/main/java/cn/hutool/core/collection/ConcurrentHashSet.java b/hutool-core/src/main/java/cn/hutool/core/collection/ConcurrentHashSet.java index 7e1229e08b472fe7d22549499870c034ed5c6b5c..bac6e1b491ca48863516e93ee62a8d25360ef45f 100644 --- a/hutool-core/src/main/java/cn/hutool/core/collection/ConcurrentHashSet.java +++ b/hutool-core/src/main/java/cn/hutool/core/collection/ConcurrentHashSet.java @@ -1,12 +1,13 @@ package cn.hutool.core.collection; +import cn.hutool.core.map.SafeConcurrentHashMap; + import java.util.AbstractSet; import java.util.Collection; import java.util.Iterator; -import java.util.concurrent.ConcurrentHashMap; /** - * 通过{@link ConcurrentHashMap}实现的线程安全HashSet + * 通过{@link SafeConcurrentHashMap}实现的线程安全HashSet * * @author Looly * @@ -18,7 +19,7 @@ public class ConcurrentHashSet extends AbstractSet implements java.io.Seri /** 持有对象。如果值为此对象表示有数据,否则无数据 */ private static final Boolean PRESENT = true; - private final ConcurrentHashMap map; + private final SafeConcurrentHashMap map; // ----------------------------------------------------------------------------------- Constructor start /** @@ -26,7 +27,7 @@ public class ConcurrentHashSet extends AbstractSet implements java.io.Seri * 触发因子为默认的0.75 */ public ConcurrentHashSet() { - map = new ConcurrentHashMap<>(); + map = new SafeConcurrentHashMap<>(); } /** @@ -36,7 +37,7 @@ public class ConcurrentHashSet extends AbstractSet implements java.io.Seri * @param initialCapacity 初始大小 */ public ConcurrentHashSet(int initialCapacity) { - map = new ConcurrentHashMap<>(initialCapacity); + map = new SafeConcurrentHashMap<>(initialCapacity); } /** @@ -46,7 +47,7 @@ public class ConcurrentHashSet extends AbstractSet implements java.io.Seri * @param loadFactor 加载因子。此参数决定数据增长时触发的百分比 */ public ConcurrentHashSet(int initialCapacity, float loadFactor) { - map = new ConcurrentHashMap<>(initialCapacity, loadFactor); + map = new SafeConcurrentHashMap<>(initialCapacity, loadFactor); } /** @@ -57,7 +58,7 @@ public class ConcurrentHashSet extends AbstractSet implements java.io.Seri * @param concurrencyLevel 线程并发度 */ public ConcurrentHashSet(int initialCapacity, float loadFactor, int concurrencyLevel) { - map = new ConcurrentHashMap<>(initialCapacity, loadFactor, concurrencyLevel); + map = new SafeConcurrentHashMap<>(initialCapacity, loadFactor, concurrencyLevel); } /** @@ -67,10 +68,10 @@ public class ConcurrentHashSet extends AbstractSet implements java.io.Seri public ConcurrentHashSet(Iterable iter) { if(iter instanceof Collection) { final Collection collection = (Collection)iter; - map = new ConcurrentHashMap<>((int)(collection.size() / 0.75f)); + map = new SafeConcurrentHashMap<>((int)(collection.size() / 0.75f)); this.addAll(collection); }else { - map = new ConcurrentHashMap<>(); + map = new SafeConcurrentHashMap<>(); for (E e : iter) { this.add(e); } diff --git a/hutool-core/src/main/java/cn/hutool/core/collection/FilterIter.java b/hutool-core/src/main/java/cn/hutool/core/collection/FilterIter.java old mode 100644 new mode 100755 index 9c5ae22016578b9440f33f318e202c7efa717655..f4496eb5d2f88c44e05851cf190a2025416208cb --- a/hutool-core/src/main/java/cn/hutool/core/collection/FilterIter.java +++ b/hutool-core/src/main/java/cn/hutool/core/collection/FilterIter.java @@ -84,7 +84,7 @@ public class FilterIter implements Iterator { private boolean setNextObject() { while (iterator.hasNext()) { final E object = iterator.next(); - if (null != filter && filter.accept(object)) { + if (null == filter || filter.accept(object)) { nextObject = object; nextObjectSet = true; return true; diff --git a/hutool-core/src/main/java/cn/hutool/core/collection/IterUtil.java b/hutool-core/src/main/java/cn/hutool/core/collection/IterUtil.java old mode 100644 new mode 100755 index dd0445e653836d6354ad43247fbf41a7e6ea66fa..c930ac4684a5511beda0b3bc536f1e3f8a776d63 --- a/hutool-core/src/main/java/cn/hutool/core/collection/IterUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/collection/IterUtil.java @@ -553,6 +553,9 @@ public class IterUtil { * @since 5.8.0 */ public static E get(final Iterator iterator, int index) throws IndexOutOfBoundsException { + if(null == iterator){ + return null; + } Assert.isTrue(index >= 0, "[index] must be >= 0"); while (iterator.hasNext()) { index--; @@ -572,6 +575,11 @@ public class IterUtil { * @return 第一个元素,为空返回{@code null} */ public static T getFirst(Iterable iterable) { + if (iterable instanceof List) { + final List list = (List) iterable; + return CollUtil.isEmpty(list) ? null: list.get(0); + } + return getFirst(getIter(iterable)); } @@ -686,7 +694,7 @@ public class IterUtil { for (T t : iter) { modified = (null == editor) ? t : editor.edit(t); if (null != modified) { - result.add(t); + result.add(modified); } } return result; diff --git a/hutool-core/src/main/java/cn/hutool/core/collection/ListUtil.java b/hutool-core/src/main/java/cn/hutool/core/collection/ListUtil.java old mode 100644 new mode 100755 index b0d8bfe12386d2f3713242f739bee4766f082f93..e1e5219660d8dbf4f6d974de0c7c6c3fb12e5aa1 --- a/hutool-core/src/main/java/cn/hutool/core/collection/ListUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/collection/ListUtil.java @@ -2,20 +2,14 @@ package cn.hutool.core.collection; import cn.hutool.core.comparator.PinyinComparator; import cn.hutool.core.comparator.PropertyComparator; +import cn.hutool.core.exceptions.ValidateException; +import cn.hutool.core.lang.Assert; import cn.hutool.core.lang.Matcher; import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.PageUtil; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.Enumeration; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; -import java.util.RandomAccess; +import java.util.*; import java.util.concurrent.CopyOnWriteArrayList; import java.util.function.Consumer; @@ -370,7 +364,13 @@ public class ListUtil { // 不支持clone list2 = new ArrayList<>(list); } - return reverse(list2); + + try { + return reverse(list2); + } catch (final UnsupportedOperationException e) { + // 提供的列表不可编辑,新建列表 + return reverse(list(false, list)); + } } /** @@ -384,6 +384,7 @@ public class ListUtil { * @since 4.1.2 */ public static List setOrAppend(List list, int index, T element) { + Assert.notNull(list, "List must be not null !"); if (index < list.size()) { list.set(index, element); } else { @@ -392,6 +393,68 @@ public class ListUtil { return list; } + /** + * 在指定位置设置元素。当index小于List的长度时,替换指定位置的值,否则追加{@code null}直到到达index后,设置值 + * + * @param 元素类型 + * @param list List列表 + * @param index 位置 + * @param element 新元素 + * @return 原List + * @since 5。8.4 + */ + public static List setOrPadding(List list, int index, T element) { + return setOrPadding(list, index, element, null); + } + + /** + * 在指定位置设置元素。当index小于List的长度时,替换指定位置的值,否则追加{@code paddingElement}直到到达index后,设置值
    + * 注意:为避免OOM问题,此方法限制index的最大值为{@code (list.size() + 1) * 10} + * + * @param 元素类型 + * @param list List列表 + * @param index 位置 + * @param element 新元素 + * @param paddingElement 填充的值 + * @return 原List + * @since 5.8.4 + */ + public static List setOrPadding(List list, int index, T element, T paddingElement) { + return setOrPadding(list, index, element, paddingElement, (list.size() + 1) * 10); + } + + /** + * 在指定位置设置元素。当index小于List的长度时,替换指定位置的值,否则追加{@code paddingElement}直到到达index后,设置值 + * + * @param 元素类型 + * @param list List列表 + * @param index 位置 + * @param element 新元素 + * @param paddingElement 填充的值 + * @param indexLimit 最大索引限制 + * @return 原List + * @since 5.8.28 + */ + public static List setOrPadding(List list, int index, T element, T paddingElement, int indexLimit) { + Assert.notNull(list, "List must be not null !"); + final int size = list.size(); + if (index < size) { + list.set(index, element); + } else { + if (indexLimit > 0) { + // issue#3286, 增加安全检查 + if (index > indexLimit) { + throw new ValidateException("Index [{}] is too large for limit: [{}]", index, indexLimit); + } + } + for (int i = size; i < index; i++) { + list.add(paddingElement); + } + list.add(element); + } + return list; + } + /** * 截取集合的部分 * @@ -401,7 +464,8 @@ public class ListUtil { * @param end 结束位置(不包含) * @return 截取后的数组,当开始位置超过最大时,返回空的List */ - public static List sub(List list, int start, int end) { + public static List + sub(List list, int start, int end) { return sub(list, start, end, 1); } @@ -544,8 +608,8 @@ public class ListUtil { } return (list instanceof RandomAccess) - ? new RandomAccessPartition<>(list, size) - : new Partition<>(list, size); + ? new RandomAccessPartition<>(list, size) + : new Partition<>(list, size); } /** @@ -590,8 +654,8 @@ public class ListUtil { } return (list instanceof RandomAccess) - ? new RandomAccessAvgPartition<>(list, limit) - : new AvgPartition<>(list, limit); + ? new RandomAccessAvgPartition<>(list, limit) + : new AvgPartition<>(list, limit); } /** @@ -632,4 +696,29 @@ public class ListUtil { } } } + + /** + * 将元素移动到指定列表的新位置。 + *
      + *
    • 如果元素不在列表中,则将其添加到新位置。
    • + *
    • 如果元素已在列表中,则先移除它,然后再将其添加到新位置。
    • + *
    + * + * @param list 原始列表,元素将在这个列表上进行操作。 + * @param element 需要移动的元素。 + * @param newPosition 元素的新位置,从0开始计数,位置计算是以移除元素后的列表位置计算的 + * @param 列表和元素的通用类型。 + * @return 更新后的列表。 + * @since 5.8.29 + */ + public static List move(List list, T element, int newPosition) { + Assert.notNull(list); + if (false == list.contains(element)) { + list.add(newPosition, element); + } else { + list.remove(element); + list.add(newPosition, element); + } + return list; + } } diff --git a/hutool-core/src/main/java/cn/hutool/core/collection/Partition.java b/hutool-core/src/main/java/cn/hutool/core/collection/Partition.java index fa555b97440f34a71b516dad4e835c35f4bd0d80..8b1075853f91484a39548ee58b2363a6c5a2ca73 100644 --- a/hutool-core/src/main/java/cn/hutool/core/collection/Partition.java +++ b/hutool-core/src/main/java/cn/hutool/core/collection/Partition.java @@ -1,5 +1,7 @@ package cn.hutool.core.collection; +import cn.hutool.core.lang.Assert; + import java.util.AbstractList; import java.util.List; @@ -21,18 +23,18 @@ public class Partition extends AbstractList> { /** * 列表分区 * - * @param list 被分区的列表 - * @param size 每个分区的长度 + * @param list 被分区的列表,非空 + * @param size 每个分区的长度,必须>0 */ public Partition(List list, int size) { - this.list = list; - this.size = Math.min(size, list.size()); + this.list = Assert.notNull(list); + this.size = Math.min(list.size(), size); } @Override public List get(int index) { - int start = index * size; - int end = Math.min(start + size, list.size()); + final int start = index * size; + final int end = Math.min(start + size, list.size()); return list.subList(start, end); } @@ -40,12 +42,14 @@ public class Partition extends AbstractList> { public int size() { // 此处采用动态计算,以应对list变 final int size = this.size; - final int total = list.size(); - int length = total / size; - if(total % size > 0){ - length += 1; + if(0 == size){ + return 0; } - return length; + + final int total = list.size(); + // 类似于判断余数,当总数非整份size时,多余的数>=1,则相当于被除数多一个size,做到+1目的 + // 类似于:if(total % size > 0){length += 1;} + return (total + size - 1) / size; } @Override diff --git a/hutool-core/src/main/java/cn/hutool/core/collection/TransCollection.java b/hutool-core/src/main/java/cn/hutool/core/collection/TransCollection.java old mode 100644 new mode 100755 diff --git a/hutool-core/src/main/java/cn/hutool/core/comparator/BaseFieldComparator.java b/hutool-core/src/main/java/cn/hutool/core/comparator/BaseFieldComparator.java old mode 100644 new mode 100755 diff --git a/hutool-core/src/main/java/cn/hutool/core/comparator/FieldComparator.java b/hutool-core/src/main/java/cn/hutool/core/comparator/FieldComparator.java index b4a1dd19a085a2e6943079b89e4de0f2103e9ba3..482b1466e059de25e488d281167db5b897ff0c29 100644 --- a/hutool-core/src/main/java/cn/hutool/core/comparator/FieldComparator.java +++ b/hutool-core/src/main/java/cn/hutool/core/comparator/FieldComparator.java @@ -33,19 +33,21 @@ public class FieldComparator extends FuncComparator { * @param field 字段 */ public FieldComparator(Field field) { - this(true, field); + this(true, true, field); } /** * 构造 * * @param nullGreater 是否{@code null}在后 + * @param compareSelf 在字段值相同情况下,是否比较对象本身。 + * 如果此项为{@code false},字段值比较后为0会导致对象被认为相同,可能导致被去重。 * @param field 字段 */ - public FieldComparator(boolean nullGreater, Field field) { - super(nullGreater, (bean) -> - (Comparable) ReflectUtil.getFieldValue(bean, - Assert.notNull(field, "Field must be not null!"))); + public FieldComparator(boolean nullGreater, boolean compareSelf, Field field) { + super(nullGreater, compareSelf, (bean) -> + (Comparable) ReflectUtil.getFieldValue(bean, + Assert.notNull(field, "Field must be not null!"))); } /** diff --git a/hutool-core/src/main/java/cn/hutool/core/comparator/FieldsComparator.java b/hutool-core/src/main/java/cn/hutool/core/comparator/FieldsComparator.java index 5c10eda78a32fd39f8c6f6f01f82be3711de868d..539e6b5d70994708e5c4242c904a34287796f302 100644 --- a/hutool-core/src/main/java/cn/hutool/core/comparator/FieldsComparator.java +++ b/hutool-core/src/main/java/cn/hutool/core/comparator/FieldsComparator.java @@ -38,7 +38,8 @@ public class FieldsComparator extends NullComparator { for (String fieldName : fieldNames) { field = ClassUtil.getDeclaredField(beanClass, fieldName); Assert.notNull(field, "Field [{}] not found in Class [{}]", fieldName, beanClass.getName()); - final int compare = new FieldComparator<>(field).compare(a, b); + // issue#3259,多个字段比较时,允许字段值重复 + final int compare = new FieldComparator<>(true, false, field).compare(a, b); if (0 != compare) { return compare; } diff --git a/hutool-core/src/main/java/cn/hutool/core/comparator/FuncComparator.java b/hutool-core/src/main/java/cn/hutool/core/comparator/FuncComparator.java index 78ea09840fe05852f09676f4b0a811ef2573f983..eb17c9c97ba76164b1d005f22659b28f33a1197e 100644 --- a/hutool-core/src/main/java/cn/hutool/core/comparator/FuncComparator.java +++ b/hutool-core/src/main/java/cn/hutool/core/comparator/FuncComparator.java @@ -1,7 +1,5 @@ package cn.hutool.core.comparator; -import cn.hutool.core.util.ObjectUtil; - import java.util.function.Function; /** @@ -13,51 +11,44 @@ import java.util.function.Function; public class FuncComparator extends NullComparator { private static final long serialVersionUID = 1L; - private final Function> func; - /** * 构造 * * @param nullGreater 是否{@code null}在后 - * @param func 比较项获取函数 + * @param func 比较项获取函数,此函数根据传入的一个对象,生成对应的可比较对象,然后根据这个返回值比较 */ public FuncComparator(boolean nullGreater, Function> func) { - super(nullGreater, null); - this.func = func; - } - - @Override - protected int doCompare(T a, T b) { - Comparable v1; - Comparable v2; - try { - v1 = func.apply(a); - v2 = func.apply(b); - } catch (Exception e) { - throw new ComparatorException(e); - } - - return compare(a, b, v1, v2); + this(nullGreater, true, func); } /** - * 对象及对应比较的值的综合比较
    - * 考虑到如果对象对应的比较值相同,如对象的字段值相同,则返回相同结果,此时在TreeMap等容器比较去重时会去重。
    - * 因此需要比较下对象本身以避免去重 + * 构造 * - * @param o1 对象1 - * @param o2 对象2 - * @param v1 被比较的值1 - * @param v2 被比较的值2 - * @return 比较结果 + * @param nullGreater 是否{@code null}在后 + * @param compareSelf 在字段值相同情况下,是否比较对象本身。 + * 如果此项为{@code false},字段值比较后为0会导致对象被认为相同,可能导致被去重。 + * @param func 比较项获取函数 */ - @SuppressWarnings({"rawtypes", "unchecked"}) - private int compare(T o1, T o2, Comparable v1, Comparable v2) { - int result = ObjectUtil.compare(v1, v2); - if (0 == result) { - //避免TreeSet / TreeMap 过滤掉排序字段相同但是对象不相同的情况 - result = CompareUtil.compare(o1, o2, this.nullGreater); - } - return result; + public FuncComparator(final boolean nullGreater, final boolean compareSelf, final Function> func) { + super(nullGreater, (a, b)->{ + // 通过给定函数转换对象为指定规则的可比较对象 + final Comparable v1; + final Comparable v2; + try { + v1 = func.apply(a); + v2 = func.apply(b); + } catch (final Exception e) { + throw new ComparatorException(e); + } + + // 首先比较用户自定义的转换结果,如果为0,根据compareSelf参数决定是否比较对象本身。 + // compareSelf为false时,主要用于多规则比较,比如多字段比较的情况 + int result = CompareUtil.compare(v1, v2, nullGreater); + if (compareSelf && 0 == result) { + //避免TreeSet / TreeMap 过滤掉排序字段相同但是对象不相同的情况 + result = CompareUtil.compare(a, b, nullGreater); + } + return result; + }); } } diff --git a/hutool-core/src/main/java/cn/hutool/core/comparator/IndexedComparator.java b/hutool-core/src/main/java/cn/hutool/core/comparator/IndexedComparator.java index 8afd9c9967465bd0d28a177928d784158fa42d41..b34d657a8ac543e99c76f58504a8d9a3c7fb3079 100644 --- a/hutool-core/src/main/java/cn/hutool/core/comparator/IndexedComparator.java +++ b/hutool-core/src/main/java/cn/hutool/core/comparator/IndexedComparator.java @@ -1,9 +1,10 @@ package cn.hutool.core.comparator; import cn.hutool.core.lang.Assert; -import cn.hutool.core.util.ArrayUtil; import java.util.Comparator; +import java.util.HashMap; +import java.util.Map; /** * 按照数组的顺序正序排列,数组的元素位置决定了对象的排序先后
    @@ -16,7 +17,10 @@ import java.util.Comparator; public class IndexedComparator implements Comparator { private final boolean atEndIfMiss; - private final T[] array; + /** + * map存储对象类型所在列表的位置,k为对象,v为位置 + */ + private final Map map; /** * 构造 @@ -28,6 +32,19 @@ public class IndexedComparator implements Comparator { this(false, objs); } + + /** + * 构造 + * + * @param atEndIfMiss 如果不在列表中是否排在后边 + * @param map 参与排序的map,map中的value值大小决定了对象的排序先后 + */ + @SuppressWarnings("unchecked") + private IndexedComparator(boolean atEndIfMiss, Map map) { + this.atEndIfMiss = atEndIfMiss; + this.map = map; + } + /** * 构造 * @@ -38,7 +55,10 @@ public class IndexedComparator implements Comparator { public IndexedComparator(boolean atEndIfMiss, T... objs) { Assert.notNull(objs, "'objs' array must not be null"); this.atEndIfMiss = atEndIfMiss; - this.array = objs; + map = new HashMap<>(objs.length, 1); + for (int i = 0; i < objs.length; i++) { + map.put(objs[i], i); + } } @Override @@ -46,19 +66,29 @@ public class IndexedComparator implements Comparator { final int index1 = getOrder(o1); final int index2 = getOrder(o2); + if (index1 == index2) { + if (index1 < 0 || index1 == this.map.size()) { + // 任意一个元素不在map中, 返回原顺序 + return 1; + } + + // 位置一样,认为是同一个元素 + return 0; + } + return Integer.compare(index1, index2); } /** - * 查找对象类型所在列表的位置 + * 查找对象类型所对应的顺序值,即在原列表中的顺序 * * @param object 对象 - * @return 位置,未找到位置根据{@link #atEndIfMiss}取不同值,false返回-1,否则返回列表长度 + * @return 位置,未找到位置根据{@link #atEndIfMiss}取不同值,false返回-1,否则返回map长度 */ private int getOrder(T object) { - int order = ArrayUtil.indexOf(array, object); - if (order < 0) { - order = this.atEndIfMiss ? this.array.length : -1; + Integer order = map.get(object); + if (order == null) { + order = this.atEndIfMiss ? this.map.size() : -1; } return order; } diff --git a/hutool-core/src/main/java/cn/hutool/core/comparator/LengthComparator.java b/hutool-core/src/main/java/cn/hutool/core/comparator/LengthComparator.java new file mode 100755 index 0000000000000000000000000000000000000000..0c8f0ea3dff92d56a6fe7779ab1414735d26e858 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/comparator/LengthComparator.java @@ -0,0 +1,25 @@ +package cn.hutool.core.comparator; + +import java.util.Comparator; + +/** + * 字符串长度比较器,短在前 + * + * @author looly + * @since 5.8.9 + */ +public class LengthComparator implements Comparator { + /** + * 单例的字符串长度比较器,短在前 + */ + public static final LengthComparator INSTANCE = new LengthComparator(); + + @Override + public int compare(CharSequence o1, CharSequence o2) { + int result = Integer.compare(o1.length(), o2.length()); + if (0 == result) { + result = CompareUtil.compare(o1.toString(), o2.toString()); + } + return result; + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/comparator/NullComparator.java b/hutool-core/src/main/java/cn/hutool/core/comparator/NullComparator.java index 7f3544d2741360b6d550005d9c00a9c17af8662f..c2a3a192b31528411af17fdec945385cddd9b950 100644 --- a/hutool-core/src/main/java/cn/hutool/core/comparator/NullComparator.java +++ b/hutool-core/src/main/java/cn/hutool/core/comparator/NullComparator.java @@ -51,11 +51,6 @@ public class NullComparator implements Comparator, Serializable { return new NullComparator<>(nullGreater, comparator == null ? other : comparator.thenComparing(other)); } - @Override - public Comparator reversed() { - return new NullComparator<>((false == nullGreater), comparator == null ? null : comparator.reversed()); - } - /** * 不检查{@code null}的比较方法
    * 用户可自行重写此方法自定义比较方式 diff --git a/hutool-core/src/main/java/cn/hutool/core/comparator/PropertyComparator.java b/hutool-core/src/main/java/cn/hutool/core/comparator/PropertyComparator.java index c94ced29ebe0fbc57e6afa01cedef7e440638b94..af8853710fdab0eb22cb35328d720dc3bbf8364c 100644 --- a/hutool-core/src/main/java/cn/hutool/core/comparator/PropertyComparator.java +++ b/hutool-core/src/main/java/cn/hutool/core/comparator/PropertyComparator.java @@ -6,9 +6,8 @@ import cn.hutool.core.bean.BeanUtil; * Bean属性排序器
    * 支持读取Bean多层次下的属性 * - * @author Looly - * * @param 被比较的Bean + * @author Looly */ public class PropertyComparator extends FuncComparator { private static final long serialVersionUID = 9157326766723846313L; @@ -25,10 +24,23 @@ public class PropertyComparator extends FuncComparator { /** * 构造 * - * @param property 属性名 + * @param property 属性名 * @param isNullGreater null值是否排在后(从小到大排序) */ public PropertyComparator(String property, boolean isNullGreater) { - super(isNullGreater, (bean)-> BeanUtil.getProperty(bean, property)); + this(property, true, isNullGreater); + } + + /** + * 构造 + * + * @param property 属性名 + * @param compareSelf 在字段值相同情况下,是否比较对象本身。 + * 如果此项为{@code false},字段值比较后为0会导致对象被认为相同,可能导致被去重。 + * @param isNullGreater null值是否排在后(从小到大排序) + * @since 5.8.28 + */ + public PropertyComparator(String property, final boolean compareSelf, boolean isNullGreater) { + super(isNullGreater, compareSelf, (bean) -> BeanUtil.getProperty(bean, property)); } } diff --git a/hutool-core/src/main/java/cn/hutool/core/comparator/VersionComparator.java b/hutool-core/src/main/java/cn/hutool/core/comparator/VersionComparator.java index 834123c44bf110676451a0ccdc2b3f7c7e78c03c..0cb62d4cfab4e998bc98ed31bf8343ba74029da0 100644 --- a/hutool-core/src/main/java/cn/hutool/core/comparator/VersionComparator.java +++ b/hutool-core/src/main/java/cn/hutool/core/comparator/VersionComparator.java @@ -1,19 +1,17 @@ package cn.hutool.core.comparator; -import cn.hutool.core.util.CharUtil; -import cn.hutool.core.util.ObjectUtil; -import cn.hutool.core.util.StrUtil; +import cn.hutool.core.lang.Version; +import cn.hutool.core.util.*; import java.io.Serializable; import java.util.Comparator; -import java.util.List; /** * 版本比较器
    * 比较两个版本的大小
    * 排序时版本从小到大排序,即比较时小版本在前,大版本在后
    * 支持如:1.3.20.8,6.82.20160101,8.5a/8.5c等版本形式
    - * 参考:https://www.cnblogs.com/shihaiming/p/6286575.html + * 参考:java.lang.module.ModuleDescriptor.Version * * @author Looly * @since 4.0.2 @@ -41,6 +39,7 @@ public class VersionComparator implements Comparator, Serializable { * compare("v1", null) > 0 * compare("1.0.0", "1.0.2") < 0 * compare("1.0.2", "1.0.2a") < 0 + * compare("1.0.3", "1.0.2a") > 0 * compare("1.13.0", "1.12.1c") > 0 * compare("V0.0.20170102", "V0.0.20170101") > 0 * @@ -55,34 +54,12 @@ public class VersionComparator implements Comparator, Serializable { } if (version1 == null && version2 == null) { return 0; - } else if (version1 == null) {// null视为最小版本,排在前 + } else if (version1 == null) {// null或""视为最小版本,排在前 return -1; } else if (version2 == null) { return 1; } - final List v1s = StrUtil.split(version1, CharUtil.DOT); - final List v2s = StrUtil.split(version2, CharUtil.DOT); - - int diff = 0; - int minLength = Math.min(v1s.size(), v2s.size());// 取最小长度值 - String v1; - String v2; - for (int i = 0; i < minLength; i++) { - v1 = v1s.get(i); - v2 = v2s.get(i); - // 先比较长度 - diff = v1.length() - v2.length(); - if (0 == diff) { - diff = v1.compareTo(v2); - } - if(diff != 0) { - //已有结果,结束 - break; - } - } - - // 如果已经分出大小,则直接返回,如果未分出大小,则再比较位数,有子版本的为大; - return (diff != 0) ? diff : v1s.size() - v2s.size(); + return CompareUtil.compare(Version.of(version1), Version.of(version2)); } } diff --git a/hutool-core/src/main/java/cn/hutool/core/comparator/WindowsExplorerStringComparator.java b/hutool-core/src/main/java/cn/hutool/core/comparator/WindowsExplorerStringComparator.java new file mode 100644 index 0000000000000000000000000000000000000000..b1561d0533f8f55150e4e7f320a1f7b4964865cc --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/comparator/WindowsExplorerStringComparator.java @@ -0,0 +1,86 @@ +package cn.hutool.core.comparator; + +import cn.hutool.core.util.StrUtil; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Windows 资源管理器风格字符串比较器 + * + *

    此比较器模拟了 Windows 资源管理器的文件名排序方式,可得到与其相同的排序结果。

    + * + *

    假设有一个数组,包含若干个文件名 {@code {"abc2.doc", "abc1.doc", "abc12.doc"}}

    + *

    在 Windows 资源管理器中以名称排序时,得到 {@code {"abc1.doc", "abc2.doc", "abc12.doc" }}

    + *

    调用 {@code Arrays.sort(filenames);} 时,得到 {@code {"abc1.doc", "abc12.doc", "abc2.doc" }}

    + *

    调用 {@code Arrays.sort(filenames, new WindowsExplorerStringComparator());} 时,得到 {@code {"abc1.doc", "abc2.doc", + * "abc12.doc" }},这与在资源管理器中看到的相同

    + * + * @author YMNNs + * @see Java - Sort Strings like Windows Explorer + */ +public class WindowsExplorerStringComparator implements Comparator { + + /** + * 单例 + */ + public static final WindowsExplorerStringComparator INSTANCE = new WindowsExplorerStringComparator(); + + private static final Pattern splitPattern = Pattern.compile("\\d+|\\.|\\s"); + + @Override + public int compare(CharSequence str1, CharSequence str2) { + Iterator i1 = splitStringPreserveDelimiter(str1).iterator(); + Iterator i2 = splitStringPreserveDelimiter(str2).iterator(); + while (true) { + //Til here all is equal. + if (!i1.hasNext() && !i2.hasNext()) { + return 0; + } + //first has no more parts -> comes first + if (!i1.hasNext()) { + return -1; + } + //first has more parts than i2 -> comes after + if (!i2.hasNext()) { + return 1; + } + + String data1 = i1.next(); + String data2 = i2.next(); + int result; + try { + //If both data are numbers, then compare numbers + result = Long.compare(Long.parseLong(data1), Long.parseLong(data2)); + //If numbers are equal than longer comes first + if (result == 0) { + result = -Integer.compare(data1.length(), data2.length()); + } + } catch (NumberFormatException ex) { + //compare text case insensitive + result = data1.compareToIgnoreCase(data2); + } + + if (result != 0) { + return result; + } + } + } + + private List splitStringPreserveDelimiter(CharSequence str) { + Matcher matcher = splitPattern.matcher(str); + List list = new ArrayList<>(); + int pos = 0; + while (matcher.find()) { + list.add(StrUtil.sub(str, pos, matcher.start())); + list.add(matcher.group()); + pos = matcher.end(); + } + list.add(StrUtil.subSuf(str, pos)); + return list; + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/compiler/JavaSourceCompiler.java b/hutool-core/src/main/java/cn/hutool/core/compiler/JavaSourceCompiler.java index 39e08d28f8d54e9cb4166f3e48f3a30323090163..5a9f84b536e70d6d2be21997ca7b1f6faaa3c3b6 100644 --- a/hutool-core/src/main/java/cn/hutool/core/compiler/JavaSourceCompiler.java +++ b/hutool-core/src/main/java/cn/hutool/core/compiler/JavaSourceCompiler.java @@ -168,6 +168,16 @@ public class JavaSourceCompiler { * @return 类加载器 */ public ClassLoader compile() { + return compile(null); + } + + /** + * 编译所有文件并返回类加载器 + * + * @param options 编译参数 + * @return 类加载器 + */ + public ClassLoader compile(List options) { // 获得classPath final List classPath = getClassPath(); final URL[] urLs = URLUtil.getURLs(classPath.toArray(new File[0])); @@ -181,7 +191,9 @@ public class JavaSourceCompiler { final JavaClassFileManager javaFileManager = new JavaClassFileManager(ucl, CompilerUtil.getFileManager()); // classpath - final List options = new ArrayList<>(); + if (null == options) { + options = new ArrayList<>(); + } if (false == classPath.isEmpty()) { final List cp = CollUtil.map(classPath, File::getAbsolutePath, true); options.add("-cp"); @@ -232,7 +244,7 @@ public class JavaSourceCompiler { for (Resource resource : this.sourceList) { if (resource instanceof FileResource) { final File file = ((FileResource) resource).getFile(); - FileUtil.walkFiles(file, (subFile) -> list.addAll(JavaFileObjectUtil.getJavaFileObjects(file))); + FileUtil.walkFiles(file, (subFile) -> list.addAll(JavaFileObjectUtil.getJavaFileObjects(subFile))); } else { list.add(new JavaSourceFileObject(resource.getName(), resource.getStream())); } diff --git a/hutool-core/src/main/java/cn/hutool/core/compiler/JavaSourceFileObject.java b/hutool-core/src/main/java/cn/hutool/core/compiler/JavaSourceFileObject.java index db0c0dd8c41860066a3cb8ec71b20310c46321e4..d7dd33aae79b0214608c7b35e4c913162e47bbb2 100644 --- a/hutool-core/src/main/java/cn/hutool/core/compiler/JavaSourceFileObject.java +++ b/hutool-core/src/main/java/cn/hutool/core/compiler/JavaSourceFileObject.java @@ -28,6 +28,11 @@ class JavaSourceFileObject extends SimpleJavaFileObject { */ private InputStream inputStream; + /** + * Source code. + */ + private String sourceCode; + /** * 构造,支持File等路径类型的源码 * @@ -82,9 +87,12 @@ class JavaSourceFileObject extends SimpleJavaFileObject { */ @Override public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException { - try(final InputStream in = openInputStream()){ - return IoUtil.readUtf8(in); + if (sourceCode == null) { + try(final InputStream in = openInputStream()){ + sourceCode = IoUtil.readUtf8(in); + } } + return sourceCode; } -} \ No newline at end of file +} diff --git a/hutool-core/src/main/java/cn/hutool/core/compress/ZipCopyVisitor.java b/hutool-core/src/main/java/cn/hutool/core/compress/ZipCopyVisitor.java index 3423097bea88a9311db010a09d52d7ca38290d31..01fe26f9124279dfb40bef24c9ca95d31f31450b 100644 --- a/hutool-core/src/main/java/cn/hutool/core/compress/ZipCopyVisitor.java +++ b/hutool-core/src/main/java/cn/hutool/core/compress/ZipCopyVisitor.java @@ -4,6 +4,7 @@ import cn.hutool.core.util.StrUtil; import java.io.IOException; import java.nio.file.CopyOption; +import java.nio.file.DirectoryNotEmptyException; import java.nio.file.FileAlreadyExistsException; import java.nio.file.FileSystem; import java.nio.file.FileVisitResult; @@ -48,10 +49,13 @@ public class ZipCopyVisitor extends SimpleFileVisitor { // 在目标的Zip文件中的相对位置创建目录 try { Files.copy(dir, targetDir, copyOptions); + } catch (final DirectoryNotEmptyException ignore) { + // 目录已经存在,则跳过 } catch (FileAlreadyExistsException e) { if (false == Files.isDirectory(targetDir)) { throw e; } + // 目录非空情况下,跳过创建目录 } } diff --git a/hutool-core/src/main/java/cn/hutool/core/compress/ZipReader.java b/hutool-core/src/main/java/cn/hutool/core/compress/ZipReader.java index 9899e95aa00183e29eae7d75f1d30c361677339d..384b6d2a70b9e78ff7c85a5588a2b95c995311df 100755 --- a/hutool-core/src/main/java/cn/hutool/core/compress/ZipReader.java +++ b/hutool-core/src/main/java/cn/hutool/core/compress/ZipReader.java @@ -1,5 +1,6 @@ package cn.hutool.core.compress; +import cn.hutool.core.exceptions.UtilException; import cn.hutool.core.io.FileUtil; import cn.hutool.core.io.IORuntimeException; import cn.hutool.core.io.IoUtil; @@ -26,8 +27,15 @@ import java.util.zip.ZipInputStream; */ public class ZipReader implements Closeable { + // size of uncompressed zip entry shouldn't be bigger of compressed in MAX_SIZE_DIFF times + private static final int DEFAULT_MAX_SIZE_DIFF = 100; + private ZipFile zipFile; private ZipInputStream in; + /** + * 检查ZipBomb文件差异倍数,-1表示不检查ZipBomb + */ + private int maxSizeDiff = DEFAULT_MAX_SIZE_DIFF; /** * 创建ZipReader @@ -89,6 +97,18 @@ public class ZipReader implements Closeable { this.in = zin; } + /** + * 设置检查ZipBomb文件差异倍数,-1表示不检查ZipBomb + * + * @param maxSizeDiff 检查ZipBomb文件差异倍数,-1表示不检查ZipBomb + * @return this + * @since 5.8.21 + */ + public ZipReader setMaxSizeDiff(final int maxSizeDiff) { + this.maxSizeDiff = maxSizeDiff; + return this; + } + /** * 获取指定路径的文件流
    * 如果是文件模式,则直接获取Entry对应的流,如果是流模式,则遍历entry后,找到对应流返回 @@ -105,7 +125,6 @@ public class ZipReader implements Closeable { } } else { try { - this.in.reset(); ZipEntry zipEntry; while (null != (zipEntry = in.getNextEntry())) { if (zipEntry.getName().equals(path)) { @@ -203,7 +222,7 @@ public class ZipReader implements Closeable { private void readFromZipFile(Consumer consumer) { final Enumeration em = zipFile.entries(); while (em.hasMoreElements()) { - consumer.accept(em.nextElement()); + consumer.accept(checkZipBomb(em.nextElement())); } } @@ -218,9 +237,37 @@ public class ZipReader implements Closeable { ZipEntry zipEntry; while (null != (zipEntry = in.getNextEntry())) { consumer.accept(zipEntry); + // 检查ZipBomb放在读取内容之后,以便entry中的信息正常读取 + checkZipBomb(zipEntry); } } catch (IOException e) { throw new IORuntimeException(e); } } + + /** + * 检查Zip bomb漏洞 + * + * @param entry {@link ZipEntry} + * @return 检查后的{@link ZipEntry} + */ + private ZipEntry checkZipBomb(ZipEntry entry) { + if (null == entry) { + return null; + } + if(maxSizeDiff < 0 || entry.isDirectory()){ + // 目录不检查 + return entry; + } + + final long compressedSize = entry.getCompressedSize(); + final long uncompressedSize = entry.getSize(); + if (compressedSize < 0 || uncompressedSize < 0 || + // 默认压缩比例是100倍,一旦发现压缩率超过这个阈值,被认为是Zip bomb + compressedSize * maxSizeDiff < uncompressedSize) { + throw new UtilException("Zip bomb attack detected, invalid sizes: compressed {}, uncompressed {}, name {}", + compressedSize, uncompressedSize, entry.getName()); + } + return entry; + } } diff --git a/hutool-core/src/main/java/cn/hutool/core/compress/ZipWriter.java b/hutool-core/src/main/java/cn/hutool/core/compress/ZipWriter.java index fef99f0621482fbc085182fab6c29c81ff3276cf..c7aad381d42c8b63d7d87ee651154b1626059fa8 100755 --- a/hutool-core/src/main/java/cn/hutool/core/compress/ZipWriter.java +++ b/hutool-core/src/main/java/cn/hutool/core/compress/ZipWriter.java @@ -24,6 +24,7 @@ import java.util.zip.ZipOutputStream; * @author looly * @since 5.7.8 */ +@SuppressWarnings("resource") public class ZipWriter implements Closeable { /** @@ -48,6 +49,7 @@ public class ZipWriter implements Closeable { return new ZipWriter(out, charset); } + private File zipFile; private final ZipOutputStream out; /** @@ -57,6 +59,7 @@ public class ZipWriter implements Closeable { * @param charset 编码 */ public ZipWriter(File zipFile, Charset charset) { + this.zipFile = zipFile; this.out = getZipOutputStream(zipFile, charset); } @@ -254,6 +257,11 @@ public class ZipWriter implements Closeable { } } } else { + // issue#IAGYDG 检查加入的文件是否为压缩结果文件本身,避免死循环 + if (null != this.zipFile && FileUtil.equals(file, zipFile)) { + return this; + } + // 如果是文件或其它符号,则直接压缩该文件 putEntry(subPath, FileUtil.getInputStream(file)); } diff --git a/hutool-core/src/main/java/cn/hutool/core/convert/AbstractConverter.java b/hutool-core/src/main/java/cn/hutool/core/convert/AbstractConverter.java index c725211500a9e05766ae0530dddbd28420b8d4f5..abeb0e9f4117e0a015877e7de530e74911270240 100644 --- a/hutool-core/src/main/java/cn/hutool/core/convert/AbstractConverter.java +++ b/hutool-core/src/main/java/cn/hutool/core/convert/AbstractConverter.java @@ -12,8 +12,8 @@ import java.util.Map; * 抽象转换器,提供通用的转换逻辑,同时通过convertInternal实现对应类型的专属逻辑
    * 转换器不会抛出转换异常,转换失败时会返回{@code null} * + * @param 转换的目标类型 * @author Looly - * */ public abstract class AbstractConverter implements Converter, Serializable { private static final long serialVersionUID = 1L; @@ -22,7 +22,7 @@ public abstract class AbstractConverter implements Converter, Serializable * 不抛异常转换
    * 当转换失败时返回默认值 * - * @param value 被转换的值 + * @param value 被转换的值 * @param defaultValue 默认值 * @return 转换后的值 * @since 4.5.7 @@ -55,11 +55,11 @@ public abstract class AbstractConverter implements Converter, Serializable // 除Map外,已经是目标类型,不需要转换(Map类型涉及参数类型,需要单独转换) return targetType.cast(value); } - T result = convertInternal(value); + final T result = convertInternal(value); return ((null == result) ? defaultValue : result); } else { throw new IllegalArgumentException( - StrUtil.format("Default value [{}]({}) is not the instance of [{}]", defaultValue, defaultValue.getClass(), targetType)); + StrUtil.format("Default value [{}]({}) is not the instance of [{}]", defaultValue, defaultValue.getClass(), targetType)); } } @@ -98,9 +98,9 @@ public abstract class AbstractConverter implements Converter, Serializable return value.toString(); } else if (ArrayUtil.isArray(value)) { return ArrayUtil.toString(value); - } else if(CharUtil.isChar(value)) { + } else if (CharUtil.isChar(value)) { //对于ASCII字符使用缓存加速转换,减少空间创建 - return CharUtil.toString((char)value); + return CharUtil.toString((char) value); } return value.toString(); } diff --git a/hutool-core/src/main/java/cn/hutool/core/convert/BasicType.java b/hutool-core/src/main/java/cn/hutool/core/convert/BasicType.java index 5c919eb7d7bff2515aafa4dbc0e43e5fd593e11a..d62dbb16c5f2e8ab2dc386c7a9045c1f38b00b45 100644 --- a/hutool-core/src/main/java/cn/hutool/core/convert/BasicType.java +++ b/hutool-core/src/main/java/cn/hutool/core/convert/BasicType.java @@ -1,7 +1,8 @@ package cn.hutool.core.convert; +import cn.hutool.core.map.SafeConcurrentHashMap; + import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; /** * 基本变量类型的枚举
    @@ -12,9 +13,9 @@ public enum BasicType { BYTE, SHORT, INT, INTEGER, LONG, DOUBLE, FLOAT, BOOLEAN, CHAR, CHARACTER, STRING; /** 包装类型为Key,原始类型为Value,例如: Integer.class =》 int.class. */ - public static final Map, Class> WRAPPER_PRIMITIVE_MAP = new ConcurrentHashMap<>(8); + public static final Map, Class> WRAPPER_PRIMITIVE_MAP = new SafeConcurrentHashMap<>(8); /** 原始类型为Key,包装类型为Value,例如: int.class =》 Integer.class. */ - public static final Map, Class> PRIMITIVE_WRAPPER_MAP = new ConcurrentHashMap<>(8); + public static final Map, Class> PRIMITIVE_WRAPPER_MAP = new SafeConcurrentHashMap<>(8); static { WRAPPER_PRIMITIVE_MAP.put(Boolean.class, boolean.class); diff --git a/hutool-core/src/main/java/cn/hutool/core/convert/CastUtil.java b/hutool-core/src/main/java/cn/hutool/core/convert/CastUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..5279bb7e55dbb5b7d26b1603f8be9fbd5fbd40d9 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/convert/CastUtil.java @@ -0,0 +1,118 @@ +package cn.hutool.core.convert; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * 转换工具类,提供集合、Map等向上向下转换工具 + * + * @author looly + * @since 5.8.1 + */ +public class CastUtil { + /** + * 泛型集合向上转型。例如将Collection<Integer>转换为Collection<Number> + * + * @param collection 集合 + * @param 元素类型 + * @return 转换后的集合 + * @since 5.8.1 + */ + @SuppressWarnings("unchecked") + public static Collection castUp(Collection collection) { + return (Collection) collection; + } + + /** + * 泛型集合向下转型。例如将Collection<Number>转换为Collection<Integer> + * + * @param collection 集合 + * @param 元素类型 + * @return 转换后的集合 + * @since 5.8.1 + */ + @SuppressWarnings("unchecked") + public static Collection castDown(Collection collection) { + return (Collection) collection; + } + + /** + * 泛型集合向上转型。例如将Set<Integer>转换为Set<Number> + * + * @param set 集合 + * @param 泛型 + * @return 泛化集合 + * @since 5.8.1 + */ + @SuppressWarnings("unchecked") + public static Set castUp(Set set) { + return (Set) set; + } + + /** + * 泛型集合向下转型。例如将Set<Number>转换为Set<Integer> + * + * @param set 集合 + * @param 泛型子类 + * @return 泛化集合 + * @since 5.8.1 + */ + @SuppressWarnings("unchecked") + public static Set castDown(Set set) { + return (Set) set; + } + + /** + * 泛型接口向上转型。例如将List<Integer>转换为List<Number> + * + * @param list 集合 + * @param 泛型的父类 + * @return 泛化集合 + */ + @SuppressWarnings("unchecked") + public static List castUp(List list) { + return (List) list; + } + + /** + * 泛型集合向下转型。例如将List<Number>转换为List<Integer> + * + * @param list 集合 + * @param 泛型的子类 + * @return 泛化集合 + */ + @SuppressWarnings("unchecked") + public static List castDown(List list) { + return (List) list; + } + + /** + * 泛型集合向下转型。例如将Map<Integer, Integer>转换为Map<Number,Number> + * + * @param map 集合 + * @param 泛型父类 + * @param 泛型父类 + * @return 泛化集合 + * @since 5.8.1 + */ + @SuppressWarnings("unchecked") + public static Map castUp(Map map) { + return (Map) map; + } + + /** + * 泛型集合向下转型。例如将Map<Number,Number>转换为Map<Integer, Integer> + * + * @param map 集合 + * @param 泛型子类 + * @param 泛型子类 + * @return 泛化集合 + * @since 5.8.1 + */ + @SuppressWarnings("unchecked") + public static Map castDown(Map map) { + return (Map) map; + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/convert/Convert.java b/hutool-core/src/main/java/cn/hutool/core/convert/Convert.java old mode 100644 new mode 100755 index 4d00a1ca8dc52e3356447d30d30ba735a071b1fd..0f2f04d7e9daccd9884e71c226b12e791d35ed31 --- a/hutool-core/src/main/java/cn/hutool/core/convert/Convert.java +++ b/hutool-core/src/main/java/cn/hutool/core/convert/Convert.java @@ -6,12 +6,7 @@ import cn.hutool.core.convert.impl.MapConverter; import cn.hutool.core.lang.Assert; import cn.hutool.core.lang.TypeReference; import cn.hutool.core.text.UnicodeUtil; -import cn.hutool.core.util.ByteUtil; -import cn.hutool.core.util.CharUtil; -import cn.hutool.core.util.CharsetUtil; -import cn.hutool.core.util.ClassUtil; -import cn.hutool.core.util.HexUtil; -import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.*; import java.lang.reflect.Type; import java.math.BigDecimal; @@ -19,21 +14,13 @@ import java.math.BigInteger; import java.nio.charset.Charset; import java.time.Instant; import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Date; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; import java.util.concurrent.TimeUnit; /** * 类型转换器 * * @author xiaoleilu - * */ public class Convert { @@ -42,7 +29,7 @@ public class Convert { * 如果给定的值为null,或者转换失败,返回默认值
    * 转换失败不会报错 * - * @param value 被转换的值 + * @param value 被转换的值 * @param defaultValue 转换错误时的默认值 * @return 结果 */ @@ -78,7 +65,7 @@ public class Convert { * 如果给定的值为null,或者转换失败,返回默认值
    * 转换失败不会报错 * - * @param value 被转换的值 + * @param value 被转换的值 * @param defaultValue 转换错误时的默认值 * @return 结果 */ @@ -114,7 +101,7 @@ public class Convert { * 如果给定的值为{@code null},或者转换失败,返回默认值
    * 转换失败不会报错 * - * @param value 被转换的值 + * @param value 被转换的值 * @param defaultValue 转换错误时的默认值 * @return 结果 */ @@ -161,7 +148,7 @@ public class Convert { * 如果给定的值为{@code null},或者转换失败,返回默认值
    * 转换失败不会报错 * - * @param value 被转换的值 + * @param value 被转换的值 * @param defaultValue 转换错误时的默认值 * @return 结果 */ @@ -197,7 +184,7 @@ public class Convert { * 如果给定的值为空,或者转换失败,返回默认值
    * 转换失败不会报错 * - * @param value 被转换的值 + * @param value 被转换的值 * @param defaultValue 转换错误时的默认值 * @return 结果 */ @@ -233,7 +220,7 @@ public class Convert { * 如果给定的值为空,或者转换失败,返回默认值
    * 转换失败不会报错 * - * @param value 被转换的值 + * @param value 被转换的值 * @param defaultValue 转换错误时的默认值 * @return 结果 */ @@ -268,7 +255,7 @@ public class Convert { * 如果给定的值为空,或者转换失败,返回默认值
    * 转换失败不会报错 * - * @param value 被转换的值 + * @param value 被转换的值 * @param defaultValue 转换错误时的默认值 * @return 结果 */ @@ -303,7 +290,7 @@ public class Convert { * 如果给定的值为空,或者转换失败,返回默认值
    * 转换失败不会报错 * - * @param value 被转换的值 + * @param value 被转换的值 * @param defaultValue 转换错误时的默认值 * @return 结果 */ @@ -338,7 +325,7 @@ public class Convert { * 如果给定的值为空,或者转换失败,返回默认值
    * 转换失败不会报错 * - * @param value 被转换的值 + * @param value 被转换的值 * @param defaultValue 转换错误时的默认值 * @return 结果 */ @@ -373,7 +360,7 @@ public class Convert { * String支持的值为:true、false、yes、ok、no,1,0 如果给定的值为空,或者转换失败,返回默认值
    * 转换失败不会报错 * - * @param value 被转换的值 + * @param value 被转换的值 * @param defaultValue 转换错误时的默认值 * @return 结果 */ @@ -408,7 +395,7 @@ public class Convert { * 如果给定的值为空,或者转换失败,返回默认值
    * 转换失败不会报错 * - * @param value 被转换的值 + * @param value 被转换的值 * @param defaultValue 转换错误时的默认值 * @return 结果 */ @@ -433,7 +420,7 @@ public class Convert { * 如果给定的值为空,或者转换失败,返回默认值
    * 转换失败不会报错 * - * @param value 被转换的值 + * @param value 被转换的值 * @param defaultValue 转换错误时的默认值 * @return 结果 */ @@ -458,7 +445,7 @@ public class Convert { * 如果给定的值为空,或者转换失败,返回默认值
    * 转换失败不会报错 * - * @param value 被转换的值 + * @param value 被转换的值 * @param defaultValue 转换错误时的默认值 * @return 结果 * @since 4.1.6 @@ -472,7 +459,7 @@ public class Convert { * 如果给定的值为空,或者转换失败,返回默认值
    * 转换失败不会报错 * - * @param value 被转换的值 + * @param value 被转换的值 * @param defaultValue 转换错误时的默认值 * @return 结果 * @since 5.0.7 @@ -498,7 +485,7 @@ public class Convert { * 如果给定的值为空,或者转换失败,返回默认值
    * 转换失败不会报错 * - * @param value 被转换的值 + * @param value 被转换的值 * @param defaultValue 转换错误时的默认值 * @return 结果 * @since 5.0.7 @@ -524,9 +511,9 @@ public class Convert { * 转换为Enum对象
    * 如果给定的值为空,或者转换失败,返回默认值
    * - * @param 枚举类型 - * @param clazz Enum的Class - * @param value 值 + * @param 枚举类型 + * @param clazz Enum的Class + * @param value 值 * @param defaultValue 默认值 * @return Enum */ @@ -539,7 +526,7 @@ public class Convert { * 转换为Enum对象
    * 如果给定的值为空,或者转换失败,返回默认值{@code null}
    * - * @param 枚举类型 + * @param 枚举类型 * @param clazz Enum的Class * @param value 值 * @return Enum @@ -552,8 +539,8 @@ public class Convert { * 转换为集合类 * * @param collectionType 集合类型 - * @param elementType 集合中元素类型 - * @param value 被转换的值 + * @param elementType 集合中元素类型 + * @param value 被转换的值 * @return {@link Collection} * @since 3.0.8 */ @@ -575,9 +562,9 @@ public class Convert { /** * 转换为ArrayList * - * @param 元素类型 + * @param 元素类型 * @param elementType 集合中元素类型 - * @param value 被转换的值 + * @param value 被转换的值 * @return {@link ArrayList} * @since 4.1.20 */ @@ -589,9 +576,9 @@ public class Convert { /** * 转换为HashSet * - * @param 元素类型 + * @param 元素类型 * @param elementType 集合中元素类型 - * @param value 被转换的值 + * @param value 被转换的值 * @return {@link HashSet} * @since 5.7.3 */ @@ -601,96 +588,116 @@ public class Convert { } /** - * 转换为Map + * 转换为Map,若value原本就是Map,则转为原始类型,若不是则默认转为HashMap * - * @param 键类型 - * @param 值类型 - * @param keyType 键类型 + * @param 键类型 + * @param 值类型 + * @param keyType 键类型 * @param valueType 值类型 - * @param value 被转换的值 + * @param value 被转换的值 * @return {@link Map} * @since 4.6.8 */ @SuppressWarnings("unchecked") public static Map toMap(Class keyType, Class valueType, Object value) { - return (Map) new MapConverter(HashMap.class, keyType, valueType).convert(value, null); + if (value instanceof Map) { + return toMap((Class>) value.getClass(), keyType, valueType, value); + } else { + return toMap(HashMap.class, keyType, valueType, value); + } + } + + /** + * 转换为Map + * + * @param mapType 转后的具体Map类型 + * @param 键类型 + * @param 值类型 + * @param keyType 键类型 + * @param valueType 值类型 + * @param value 被转换的值 + * @return {@link Map} + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + public static Map toMap(Class mapType, Class keyType, Class valueType, Object value) { + return (Map) new MapConverter(mapType, keyType, valueType).convert(value, null); } /** * 转换值为指定类型,类型采用字符串表示 * - * @param 目标类型 + * @param 目标类型 * @param className 类的字符串表示 - * @param value 值 + * @param value 值 * @return 转换后的值 - * @since 4.0.7 * @throws ConvertException 转换器不存在 + * @since 4.0.7 */ - public static T convertByClassName(String className, Object value) throws ConvertException{ + public static T convertByClassName(String className, Object value) throws ConvertException { return convert(ClassUtil.loadClass(className), value); } /** * 转换值为指定类型 * - * @param 目标类型 - * @param type 类型 + * @param 目标类型 + * @param type 类型 * @param value 值 * @return 转换后的值 - * @since 4.0.0 * @throws ConvertException 转换器不存在 + * @since 4.0.0 */ - public static T convert(Class type, Object value) throws ConvertException{ - return convert((Type)type, value); + public static T convert(Class type, Object value) throws ConvertException { + return convert((Type) type, value); } /** * 转换值为指定类型 * - * @param 目标类型 + * @param 目标类型 * @param reference 类型参考,用于持有转换后的泛型类型 - * @param value 值 + * @param value 值 * @return 转换后的值 * @throws ConvertException 转换器不存在 */ - public static T convert(TypeReference reference, Object value) throws ConvertException{ + public static T convert(TypeReference reference, Object value) throws ConvertException { return convert(reference.getType(), value, null); } /** * 转换值为指定类型 * - * @param 目标类型 - * @param type 类型 + * @param 目标类型 + * @param type 类型 * @param value 值 * @return 转换后的值 * @throws ConvertException 转换器不存在 */ - public static T convert(Type type, Object value) throws ConvertException{ + public static T convert(Type type, Object value) throws ConvertException { return convert(type, value, null); } /** * 转换值为指定类型 * - * @param 目标类型 - * @param type 类型 - * @param value 值 + * @param 目标类型 + * @param type 类型 + * @param value 值 * @param defaultValue 默认值 * @return 转换后的值 * @throws ConvertException 转换器不存在 * @since 4.0.0 */ public static T convert(Class type, Object value, T defaultValue) throws ConvertException { - return convert((Type)type, value, defaultValue); + return convert((Type) type, value, defaultValue); } /** * 转换值为指定类型 * - * @param 目标类型 - * @param type 类型 - * @param value 值 + * @param 目标类型 + * @param type 类型 + * @param value 值 * @param defaultValue 默认值 * @return 转换后的值 * @throws ConvertException 转换器不存在 @@ -703,8 +710,8 @@ public class Convert { * 转换值为指定类型,不抛异常转换
    * 当转换失败时返回{@code null} * - * @param 目标类型 - * @param type 目标类型 + * @param 目标类型 + * @param type 目标类型 * @param value 值 * @return 转换后的值,转换失败返回null * @since 4.5.10 @@ -717,9 +724,9 @@ public class Convert { * 转换值为指定类型,不抛异常转换
    * 当转换失败时返回默认值 * - * @param 目标类型 - * @param type 目标类型 - * @param value 值 + * @param 目标类型 + * @param type 目标类型 + * @param value 值 * @param defaultValue 默认值 * @return 转换后的值 * @since 4.5.10 @@ -732,11 +739,11 @@ public class Convert { * 转换值为指定类型,可选是否不抛异常转换
    * 当转换失败时返回默认值 * - * @param 目标类型 - * @param type 目标类型 - * @param value 值 + * @param 目标类型 + * @param type 目标类型 + * @param value 值 * @param defaultValue 默认值 - * @param quietly 是否静默转换,true不抛异常 + * @param quietly 是否静默转换,true不抛异常 * @return 转换后的值 * @since 5.3.2 */ @@ -745,7 +752,7 @@ public class Convert { try { return registry.convert(type, value, defaultValue); } catch (Exception e) { - if(quietly){ + if (quietly) { return defaultValue; } throw e; @@ -753,24 +760,28 @@ public class Convert { } // ----------------------------------------------------------------------- 全角半角转换 + /** - * 半角转全角 + * 半角转全角,{@code null}返回{@code null} * * @param input String. - * @return 全角字符串. + * @return 全角字符串,{@code null}返回{@code null} */ public static String toSBC(String input) { return toSBC(input, null); } /** - * 半角转全角 + * 半角转全角,{@code null}返回{@code null} * - * @param input String + * @param input String * @param notConvertSet 不替换的字符集合 - * @return 全角字符串. + * @return 全角字符串,{@code null}返回{@code null} */ public static String toSBC(String input, Set notConvertSet) { + if (StrUtil.isEmpty(input)) { + return input; + } final char[] c = input.toCharArray(); for (int i = 0; i < c.length; i++) { if (null != notConvertSet && notConvertSet.contains(c[i])) { @@ -800,12 +811,12 @@ public class Convert { /** * 替换全角为半角 * - * @param text 文本 + * @param text 文本 * @param notConvertSet 不替换的字符集合 * @return 替换后的字符 */ public static String toDBC(String text, Set notConvertSet) { - if(StrUtil.isBlank(text)) { + if (StrUtil.isBlank(text)) { return text; } final char[] c = text.toCharArray(); @@ -827,10 +838,11 @@ public class Convert { } // --------------------------------------------------------------------- hex + /** * 字符串转换成十六进制字符串,结果为小写 * - * @param str 待转换的ASCII字符串 + * @param str 待转换的ASCII字符串 * @param charset 编码 * @return 16进制字符串 * @see HexUtil#encodeHexStr(String, Charset) @@ -864,7 +876,7 @@ public class Convert { /** * 十六进制转换字符串 * - * @param hexStr Byte字符串(Byte之间无分隔符 如:[616C6B]) + * @param hexStr Byte字符串(Byte之间无分隔符 如:[616C6B]) * @param charset 编码 {@link Charset} * @return 对应的字符串 * @see HexUtil#decodeHexStr(String, Charset) @@ -900,9 +912,9 @@ public class Convert { * 给定字符串转换字符编码
    * 如果参数为空,则返回原字符串,不报错。 * - * @param str 被转码的字符串 + * @param str 被转码的字符串 * @param sourceCharset 原字符集 - * @param destCharset 目标字符集 + * @param destCharset 目标字符集 * @return 转换后的字符串 * @see CharsetUtil#convert(String, String, String) */ @@ -918,8 +930,8 @@ public class Convert { * 转换时间单位 * * @param sourceDuration 时长 - * @param sourceUnit 源单位 - * @param destUnit 目标单位 + * @param sourceUnit 源单位 + * @param destUnit 目标单位 * @return 目标单位的时长 */ public static long convertTime(long sourceDuration, TimeUnit sourceUnit, TimeUnit destUnit) { @@ -929,13 +941,14 @@ public class Convert { } // --------------------------------------------------------------- 原始包装类型转换 + /** * 原始类转为包装类,非原始类返回原类 * - * @see BasicType#wrap(Class) * @param clazz 原始类 * @return 包装类 * @see BasicType#wrap(Class) + * @see BasicType#wrap(Class) */ public static Class wrap(Class clazz) { return BasicType.wrap(clazz); @@ -944,16 +957,17 @@ public class Convert { /** * 包装类转为原始类,非包装类返回原类 * - * @see BasicType#unWrap(Class) * @param clazz 包装类 * @return 原始类 * @see BasicType#unWrap(Class) + * @see BasicType#unWrap(Class) */ public static Class unWrap(Class clazz) { return BasicType.unWrap(clazz); } // -------------------------------------------------------------------------- 数字和英文转换 + /** * 将阿拉伯数字转为英文表达方式 * @@ -983,7 +997,7 @@ public class Convert { /** * 将阿拉伯数字转为中文表达方式 * - * @param number 数字 + * @param number 数字 * @param isUseTraditional 是否使用繁体字(金额形式) * @return 中文 * @since 3.2.3 @@ -1003,7 +1017,7 @@ public class Convert { * @return 数字 * @since 5.6.0 */ - public static int chineseToNumber(String number){ + public static int chineseToNumber(String number) { return NumberChineseFormatter.chineseToNumber(number); } @@ -1015,13 +1029,28 @@ public class Convert { * @since 3.2.3 */ public static String digitToChinese(Number n) { - if(null == n) { - return "零"; + if (null == n) { + n = 0; } return NumberChineseFormatter.format(n.doubleValue(), true, true); } + /** + * 中文大写数字金额转换为数字,返回结果以元为单位的BigDecimal类型数字
    + * 如: + * “陆万柒仟伍佰伍拾陆元叁角贰分”返回“67556.32” + * “叁角贰分”返回“0.32” + * + * @param chineseMoneyAmount 中文大写数字金额 + * @return 返回结果以元为单位的BigDecimal类型数字 + * @since 5.8.5 + */ + public static BigDecimal chineseMoneyToNumber(String chineseMoneyAmount) { + return NumberChineseFormatter.chineseMoneyToNumber(chineseMoneyAmount); + } + // -------------------------------------------------------------------------- 数字转换 + /** * int转byte * @@ -1096,7 +1125,7 @@ public class Convert { /** * long转byte数组
    * 默认以小端序转换
    - * from: https://stackoverflow.com/questions/4485128/how-do-i-convert-long-to-byte-and-back-in-java + * from: https://stackoverflow.com/questions/4485128/how-do-i-convert-long-to-byte-and-back-in-java * * @param longValue long值 * @return byte数组 @@ -1109,7 +1138,7 @@ public class Convert { /** * byte数组转long
    * 默认以小端序转换
    - * from: https://stackoverflow.com/questions/4485128/how-do-i-convert-long-to-byte-and-back-in-java + * from: https://stackoverflow.com/questions/4485128/how-do-i-convert-long-to-byte-and-back-in-java * * @param bytes byte数组 * @return long值 diff --git a/hutool-core/src/main/java/cn/hutool/core/convert/ConverterRegistry.java b/hutool-core/src/main/java/cn/hutool/core/convert/ConverterRegistry.java old mode 100644 new mode 100755 index bbc1fd347045c30cb275d14350dab26d3386052e..2e427e53a885fc14820578d978368258b910a700 --- a/hutool-core/src/main/java/cn/hutool/core/convert/ConverterRegistry.java +++ b/hutool-core/src/main/java/cn/hutool/core/convert/ConverterRegistry.java @@ -1,46 +1,14 @@ package cn.hutool.core.convert; import cn.hutool.core.bean.BeanUtil; -import cn.hutool.core.convert.impl.ArrayConverter; -import cn.hutool.core.convert.impl.AtomicBooleanConverter; -import cn.hutool.core.convert.impl.AtomicIntegerArrayConverter; -import cn.hutool.core.convert.impl.AtomicLongArrayConverter; -import cn.hutool.core.convert.impl.AtomicReferenceConverter; -import cn.hutool.core.convert.impl.BeanConverter; -import cn.hutool.core.convert.impl.BooleanConverter; -import cn.hutool.core.convert.impl.CalendarConverter; -import cn.hutool.core.convert.impl.CharacterConverter; -import cn.hutool.core.convert.impl.CharsetConverter; -import cn.hutool.core.convert.impl.ClassConverter; -import cn.hutool.core.convert.impl.CollectionConverter; -import cn.hutool.core.convert.impl.CurrencyConverter; -import cn.hutool.core.convert.impl.DateConverter; -import cn.hutool.core.convert.impl.DurationConverter; -import cn.hutool.core.convert.impl.EnumConverter; -import cn.hutool.core.convert.impl.LocaleConverter; -import cn.hutool.core.convert.impl.MapConverter; -import cn.hutool.core.convert.impl.NumberConverter; -import cn.hutool.core.convert.impl.OptConverter; -import cn.hutool.core.convert.impl.OptionalConverter; -import cn.hutool.core.convert.impl.PathConverter; -import cn.hutool.core.convert.impl.PeriodConverter; -import cn.hutool.core.convert.impl.PrimitiveConverter; -import cn.hutool.core.convert.impl.ReferenceConverter; -import cn.hutool.core.convert.impl.StackTraceElementConverter; -import cn.hutool.core.convert.impl.StringConverter; -import cn.hutool.core.convert.impl.TemporalAccessorConverter; -import cn.hutool.core.convert.impl.TimeZoneConverter; -import cn.hutool.core.convert.impl.URIConverter; -import cn.hutool.core.convert.impl.URLConverter; -import cn.hutool.core.convert.impl.UUIDConverter; +import cn.hutool.core.bean.RecordUtil; +import cn.hutool.core.convert.impl.*; import cn.hutool.core.date.DateTime; import cn.hutool.core.lang.Opt; +import cn.hutool.core.lang.Pair; import cn.hutool.core.lang.TypeReference; -import cn.hutool.core.util.ClassUtil; -import cn.hutool.core.util.ObjectUtil; -import cn.hutool.core.util.ReflectUtil; -import cn.hutool.core.util.ServiceLoaderUtil; -import cn.hutool.core.util.TypeUtil; +import cn.hutool.core.map.SafeConcurrentHashMap; +import cn.hutool.core.util.*; import java.io.Serializable; import java.lang.ref.SoftReference; @@ -52,33 +20,10 @@ import java.net.URI; import java.net.URL; import java.nio.charset.Charset; import java.nio.file.Path; -import java.time.Duration; -import java.time.Instant; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.LocalTime; -import java.time.OffsetDateTime; -import java.time.OffsetTime; -import java.time.Period; -import java.time.ZonedDateTime; +import java.time.*; import java.time.temporal.TemporalAccessor; -import java.util.Calendar; -import java.util.Collection; -import java.util.Currency; -import java.util.Locale; -import java.util.Map; -import java.util.Optional; -import java.util.TimeZone; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicIntegerArray; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.AtomicLongArray; -import java.util.concurrent.atomic.AtomicReference; -import java.util.concurrent.atomic.DoubleAdder; -import java.util.concurrent.atomic.LongAdder; +import java.util.*; +import java.util.concurrent.atomic.*; /** * 转换器登记中心 @@ -97,7 +42,7 @@ public class ConverterRegistry implements Serializable { /** * 默认类型转换器 */ - private Map> defaultConverterMap; + private Map, Converter> defaultConverterMap; /** * 用户自定义类型转换器 */ @@ -168,7 +113,7 @@ public class ConverterRegistry implements Serializable { if (null == customConverterMap) { synchronized (this) { if (null == customConverterMap) { - customConverterMap = new ConcurrentHashMap<>(); + customConverterMap = new SafeConcurrentHashMap<>(); } } } @@ -209,7 +154,8 @@ public class ConverterRegistry implements Serializable { */ @SuppressWarnings("unchecked") public Converter getDefaultConverter(Type type) { - return (null == defaultConverterMap) ? null : (Converter) defaultConverterMap.get(type); + final Class key = TypeUtil.getClass(type); + return (null == defaultConverterMap || null == key) ? null : (Converter) defaultConverterMap.get(key); } /** @@ -248,10 +194,29 @@ public class ConverterRegistry implements Serializable { type = defaultValue.getClass(); } + // issue#I7WJHH,Opt和Optional处理 + if (value instanceof Opt) { + value = ((Opt) value).get(); + if (ObjUtil.isNull(value)) { + return defaultValue; + } + } + if (value instanceof Optional) { + value = ((Optional) value).orElse(null); + if (ObjUtil.isNull(value)) { + return defaultValue; + } + } + if (type instanceof TypeReference) { type = ((TypeReference) type).getType(); } + // 自定义对象转换 + if(value instanceof TypeConverter){ + return ObjUtil.defaultIfNull((T) ((TypeConverter) value).convert(type, value), defaultValue); + } + // 标准转换器 final Converter converter = getConverter(type, isCustomFirst); if (null != converter) { @@ -348,6 +313,12 @@ public class ConverterRegistry implements Serializable { return (T) mapConverter.convert(value, (Map) defaultValue); } + // Map类型(不可以默认强转) + if (Map.Entry.class.isAssignableFrom(rowType)) { + final EntryConverter mapConverter = new EntryConverter(type); + return (T) mapConverter.convert(value, (Map.Entry) defaultValue); + } + // 默认强转 if (rowType.isInstance(value)) { return (T) value; @@ -364,6 +335,24 @@ public class ConverterRegistry implements Serializable { return (T) arrayConverter.convert(value, defaultValue); } + // issue#I7FQ29 Class + if("java.lang.Class".equals(rowType.getName())){ + final ClassConverter converter = new ClassConverter(); + return (T) converter.convert(value, (Class) defaultValue); + } + + // 空值转空Bean + if(ObjectUtil.isEmpty(value)){ + // issue#3649 空值转空对象,则直接实例化 + return ReflectUtil.newInstanceIfPossible(rowType); + } + + // record + // issue#3985@Github since 5.8.40 + if(RecordUtil.isRecord(rowType)){ + return (T) new RecordConverter(rowType).convert(value, defaultValue); + } + // 表示非需要特殊转换的对象 return null; } @@ -374,7 +363,7 @@ public class ConverterRegistry implements Serializable { * @return 转换器 */ private ConverterRegistry defaultConverter() { - defaultConverterMap = new ConcurrentHashMap<>(); + defaultConverterMap = new SafeConcurrentHashMap<>(); // 原始类型转换器 defaultConverterMap.put(int.class, new PrimitiveConverter(int.class)); @@ -427,6 +416,9 @@ public class ConverterRegistry implements Serializable { defaultConverterMap.put(ZonedDateTime.class, new TemporalAccessorConverter(ZonedDateTime.class)); defaultConverterMap.put(OffsetDateTime.class, new TemporalAccessorConverter(OffsetDateTime.class)); defaultConverterMap.put(OffsetTime.class, new TemporalAccessorConverter(OffsetTime.class)); + defaultConverterMap.put(DayOfWeek.class, new TemporalAccessorConverter(DayOfWeek.class)); + defaultConverterMap.put(Month.class, new TemporalAccessorConverter(Month.class)); + defaultConverterMap.put(MonthDay.class, new TemporalAccessorConverter(MonthDay.class)); defaultConverterMap.put(Period.class, new PeriodConverter()); defaultConverterMap.put(Duration.class, new DurationConverter()); @@ -440,7 +432,6 @@ public class ConverterRegistry implements Serializable { defaultConverterMap.put(AtomicLongArray.class, new AtomicLongArrayConverter()); // 其它类型 - defaultConverterMap.put(Class.class, new ClassConverter()); defaultConverterMap.put(TimeZone.class, new TimeZoneConverter()); defaultConverterMap.put(Locale.class, new LocaleConverter()); defaultConverterMap.put(Charset.class, new CharsetConverter()); @@ -450,6 +441,7 @@ public class ConverterRegistry implements Serializable { defaultConverterMap.put(StackTraceElement.class, new StackTraceElementConverter());// since 4.5.2 defaultConverterMap.put(Optional.class, new OptionalConverter());// since 5.0.0 defaultConverterMap.put(Opt.class, new OptConverter());// since 5.7.16 + defaultConverterMap.put(Pair.class, new PairConverter(Pair.class));// since 5.8.17 return this; } diff --git a/hutool-core/src/main/java/cn/hutool/core/convert/NumberChineseFormatter.java b/hutool-core/src/main/java/cn/hutool/core/convert/NumberChineseFormatter.java index 6ec3f0e1601799a15bfceec6d898f2a6681258e3..dd85a8eaeb077829dcbbf76589729439b1d215ca 100644 --- a/hutool-core/src/main/java/cn/hutool/core/convert/NumberChineseFormatter.java +++ b/hutool-core/src/main/java/cn/hutool/core/convert/NumberChineseFormatter.java @@ -2,9 +2,16 @@ package cn.hutool.core.convert; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.CharUtil; import cn.hutool.core.util.NumberUtil; import cn.hutool.core.util.StrUtil; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + /** * 数字转中文类
    * 包括: @@ -40,6 +47,19 @@ public class NumberChineseFormatter { new ChineseUnit('亿', 1_0000_0000, true), }; + /** + * 口语化映射 + */ + private static final Map COLLOQUIAL_WORDS = new HashMap() { + private static final long serialVersionUID = 1L; + { + put("一十", "十"); + put("一拾", "拾"); + put("负一十", "负十"); + put("负一拾", "负拾"); + } + }; + /** * 阿拉伯数字转换成中文,小数点后四舍五入保留两位. 使用于整数、小数的转换. * @@ -71,8 +91,12 @@ public class NumberChineseFormatter { * @since 5.7.23 */ public static String format(double amount, boolean isUseTraditional, boolean isMoneyMode, String negativeName, String unitName) { + if(StrUtil.isNullOrUndefined(unitName)){ + unitName = "元"; + } + if (0 == amount) { - return "零"; + return isMoneyMode ? "零" + unitName + "整" : "零"; } Assert.checkBetween(amount, -99_9999_9999_9999.99, 99_9999_9999_9999.99, "Number support only: (-99999999999999.99 ~ 99999999999999.99)!"); @@ -96,7 +120,7 @@ public class NumberChineseFormatter { // 金额模式下,无需“零元” chineseStr.append(longToChinese(yuan, isUseTraditional)); if (isMoneyMode) { - chineseStr.append(StrUtil.isNullOrUndefined(unitName) ? "元" : unitName); + chineseStr.append(unitName); } } @@ -217,6 +241,40 @@ public class NumberChineseFormatter { return chinese; } + /** + * 阿拉伯数字转换成中文. 使用于整数、小数的转换. + * 支持多位小数 + * + * @param amount 数字 + * @param isUseTraditional 是否使用繁体 + * @param isUseColloquial 是否使用口语化(e.g. 一十 -》 十) + * @return 中文 + * @since 5.8.28 + */ + public static String format(BigDecimal amount, boolean isUseTraditional, boolean isUseColloquial) { + String formatAmount; + if (amount.scale() <= 0) { + formatAmount = NumberChineseFormatter.format(amount.longValue(), isUseTraditional); + } else { + List numberList = StrUtil.split(amount.toPlainString(), CharUtil.DOT); + // 小数部分逐个数字转换为汉字 + StringBuilder decimalPartStr = new StringBuilder(); + for (char decimalChar : numberList.get(1).toCharArray()) { + decimalPartStr.append(NumberChineseFormatter.numberCharToChinese(decimalChar, isUseTraditional)); + } + formatAmount = NumberChineseFormatter.format(amount.longValue(), isUseTraditional) + "点" + decimalPartStr; + } + if (isUseColloquial) { + for (Map.Entry colloquialWord : COLLOQUIAL_WORDS.entrySet()) { + if (formatAmount.startsWith(colloquialWord.getKey())) { + formatAmount = formatAmount.replaceFirst(colloquialWord.getKey(), colloquialWord.getValue()); + break; + } + } + } + return formatAmount; + } + /** * 数字字符转中文,非数字字符原样返回 * @@ -232,6 +290,85 @@ public class NumberChineseFormatter { return String.valueOf(numberToChinese(c - '0', isUseTraditional)); } + /** + * 中文大写数字金额转换为数字,返回结果以元为单位的BigDecimal类型数字 + * 如: + * “陆万柒仟伍佰伍拾陆元叁角贰分”返回“67556.32” + * “叁角贰分”返回“0.32” + * + * @param chineseMoneyAmount 中文大写数字金额 + * @return 返回结果以元为单位的BigDecimal类型数字 + */ + @SuppressWarnings("ConstantConditions") + public static BigDecimal chineseMoneyToNumber(String chineseMoneyAmount){ + if(StrUtil.isBlank(chineseMoneyAmount)){ + return null; + } + + int yi = chineseMoneyAmount.indexOf("元"); + if(yi == -1){ + yi = chineseMoneyAmount.indexOf("圆"); + } + final int ji = chineseMoneyAmount.indexOf("角"); + final int fi = chineseMoneyAmount.indexOf("分"); + + // 先找到单位为元的数字 + String yStr = null; + if(yi > 0) { + yStr = chineseMoneyAmount.substring(0, yi); + } + + // 再找到单位为角的数字 + String jStr = null; + if(ji > 0){ + if(yi >= 0){ + //前面有元,角肯定要在元后面 + if(ji > yi){ + jStr = chineseMoneyAmount.substring(yi+1, ji); + } + }else{ + //没有元,只有角 + jStr = chineseMoneyAmount.substring(0, ji); + } + } + + // 再找到单位为分的数字 + String fStr = null; + if(fi > 0){ + if(ji >= 0){ + //有角,分肯定在角后面 + if(fi > ji){ + fStr = chineseMoneyAmount.substring(ji+1, fi); + } + }else if(yi > 0){ + //没有角,有元,那就坐元后面找 + if(fi > yi){ + fStr = chineseMoneyAmount.substring(yi+1, fi); + } + }else { + //没有元、角,只有分 + fStr = chineseMoneyAmount.substring(0, fi); + } + } + + //元、角、分 + int y = 0, j = 0, f = 0; + if(StrUtil.isNotBlank(yStr)) { + y = NumberChineseFormatter.chineseToNumber(yStr); + } + if(StrUtil.isNotBlank(jStr)){ + j = NumberChineseFormatter.chineseToNumber(jStr); + } + if(StrUtil.isNotBlank(fStr)){ + f = NumberChineseFormatter.chineseToNumber(fStr); + } + + BigDecimal amount = new BigDecimal(y); + amount = amount.add(BigDecimal.valueOf(j).divide(BigDecimal.TEN, 2, RoundingMode.HALF_UP)); + amount = amount.add(BigDecimal.valueOf(f).divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP)); + return amount; + } + /** * 阿拉伯数字整数部分转换成中文,只支持正数 * @@ -357,7 +494,6 @@ public class NumberChineseFormatter { /** * 把中文转换为数字 如 二百二十 220
    - * 见:https://www.d5.nz/read/sfdlq/text-part0000_split_030.html *