全栈编程技术,源码分享,php,java,uniapp,开源软件使用评测
这个图标工具，治好了我的选择困难症
这个图标工具，治好了我的选择困难症说起来，上周三下午在星巴克和几个朋友聊天，大家都在吐槽App图标制作的痛点。作为AI Native Coder，我对这个话题还挺有感触的。之前我写过一篇关于开发工具效率的文章，提到过在移动应用开发和网站建设过程中，图标制作往往是一个繁琐但必不可少的环节。不同的平台对图标的尺寸和格式都有着各自严格的要求，这让很多开发者头疼不已。我自己也深有体会。每次做新的项目，我都要花大量时间在图标上。从设计到导出不同尺寸的格式，再到适配各个平台，整个流程既繁琐又容易出错。最近我找到了一个不错的解决方案：一款在线的App图标/Favicon生成工具。我用了几次，感觉体验还不错。今天想分享一下我的使用感受。什么让我眼前一亮这个工具最吸引我的地方在于它对全平台的覆盖。我做项目经常要跨多个平台开发，需要制作不同尺寸的图标。这个工具内置了主流平台的标准图标尺寸预设，包括：iOS：iPhone, iPad, Apple Watch等全系列尺寸（20px - 1024px）。Android：适配各类分辨率屏幕的标准图标（36px - 512px）。微信小程序：覆盖小程序所需的各类图标尺寸。Web Favicon：自动生成适用于网站浏览器的.ico文件及多种PNG尺寸。这个数据还挺猛的，几乎涵盖了我平时用到的所有图标尺寸。之前我需要在PS里调整多个文件，现在只需上传一张高清原图，工具就能自动按照各平台规范进行裁剪和缩放。最让我惊喜的是实时预览功能。我可以在页面上立刻看到生成效果，所见即所得。这种直观的反馈让我能够快速调整，避免了反复修改的麻烦。细节里的用心除了基本功能，这个工具在细节处理上也让我印象深刻。它提供了强大的自定义功能。如果我有特殊的尺寸需求，可以直接输入数值添加自定义预设。更棒的是，我的自定义设置会自动保存到本地，下次访问时依然可用。这个小细节让我感觉非常贴心。一键打包下载功能也很实用。生成满意后，点击下载按钮就能获得包含所有尺寸图标的压缩包（ZIP格式）。文件结构清晰，文件名规范，直接拖入项目就能使用。不过，让我最满意的还是它的安全性。这个工具采用纯前端技术开发，所有图片处理都在我的浏览器本地完成，原图不会上传至服务器，彻底杜绝隐私泄露风险。对于我们开发者来说，这一点非常重要。适合谁用我觉得这个工具特别适合几类人群：独立开发者：快速生成上架所需的整套图标资源，节省时间和精力。UI/UX设计师：验证图标在不同尺寸下的显示效果，确保视觉一致性。网站站长：一键生成兼容性最好的Favicon，提升专业度。我自己作为独立开发者，每次新项目都会用它来生成图标。过去需要几个小时的工作，现在几分钟就能搞定，效率提升了很多。体验地址网站地址：http://ico.qmkj.top（ 图标生成工具 ）一点建议当然，任何工具都不是完美的。我觉得这个工具如果能加入更多自定义风格选项就更好了。比如支持一键生成不同风格的图标，或者提供一些设计模板，这样就能满足更多个性化需求。不过总体来说，这个工具已经相当不错了。它解决了图标制作的核心痛点，让开发者能够更专注于产品本身。如果你也经常被图标制作困扰，我推荐你试试这个免费、高效、安全的图标生成工具。相信它会像我一样，释放你的生产力。
Git修改文件按原目录结构复制——Windows下的最佳实践
引言在使用Git进行版本控制的开发过程中，有时我们需要将所有已修改但尚未提交的文件复制到另一个位置，同时保持原来的目录结构。这在诸多场景中非常有用：例如为变更创建备份、将修改的代码发送给同事审查、在不同环境中测试变更等。虽然这看似简单的需求，但在Windows系统中并没有一个直接的命令可以完成此操作。本文将详细介绍如何使用PowerShell脚本和批处理文件在Windows系统中高效地将Git已修改文件按原目录结构复制出来的方法。无论你是前端开发人员、后端开发人员，还是DevOps工程师，这些方法都能帮助你更高效地管理代码变更。基本概念在开始之前，让我们明确一下Git中几个重要的文件状态：已修改未暂存的文件：已修改但尚未执行git add命令的文件已暂存的文件：已执行git add命令但尚未提交的文件未跟踪的新文件：新增但未被Git跟踪的文件我们的目标是提供一种方法，能够将上述所有类型的文件按照它们在项目中的原始目录结构复制到指定目录。方案实现方案一：PowerShell直接执行命令最直接的方法是在PowerShell中执行以下命令：$TargetDir = "D:\您想要的目标目录"; if (-not (Test-Path -Path $TargetDir)) { New-Item -ItemType Directory -Path $TargetDir -Force | Out-Null; Write-Host "创建目标目录: $TargetDir" -ForegroundColor Green }; $modifiedFiles = git ls-files --modified; $stagedFiles = git diff --cached --name-only; $untrackedFiles = git ls-files --others --exclude-standard; $allChangedFiles = $modifiedFiles + $stagedFiles + $untrackedFiles | Sort-Object -Unique; $filesCopied = 0; foreach ($file in $allChangedFiles) { if (-not $file) { continue }; if (-not (Test-Path -Path $file)) { Write-Host "警告: 文件不存在: $file" -ForegroundColor Yellow; continue }; $destPath = Join-Path -Path $TargetDir -ChildPath $file; $destDir = Split-Path -Path $destPath -Parent; if (-not (Test-Path -Path $destDir)) { New-Item -ItemType Directory -Path $destDir -Force | Out-Null }; Copy-Item -Path $file -Destination $destPath -Force; Write-Host "已复制: $file" -ForegroundColor Cyan; $filesCopied++ }; Write-Host "`n完成! 共复制了 $filesCopied 个已更改的文件到 $TargetDir" -ForegroundColor Green这个命令执行以下操作：设置目标目录并确保它存在获取所有已修改、已暂存和未跟踪的文件列表遍历所有文件，创建必要的目录结构，并复制文件显示进度和结果信息这种方法的优点是不需要创建任何脚本文件，但缺点是命令较长，不便于记忆和重复使用。方案二：PowerShell脚本（需要修改执行策略）为了更方便地重复使用，我们可以创建PowerShell脚本文件。这里提供三个不同功能的脚本：1. 仅复制已修改未暂存的文件# 复制Git已修改文件脚本 # 使用方法: .\copy-modified-files.ps1 -TargetDir "D:\目标文件夹" param ( [Parameter(Mandatory=$true)] [string]$TargetDir ) # 确保目标目录存在 if (-not (Test-Path -Path $TargetDir)) { New-Item -ItemType Directory -Path $TargetDir -Force | Out-Null Write-Host "创建目标目录: $TargetDir" -ForegroundColor Green } # 获取git已修改文件列表 $modifiedFiles = git ls-files --modified $filesCopied = 0 foreach ($file in $modifiedFiles) { # 创建目标文件路径 $destPath = Join-Path -Path $TargetDir -ChildPath $file # 确保目标文件的父目录存在 $destDir = Split-Path -Path $destPath -Parent if (-not (Test-Path -Path $destDir)) { New-Item -ItemType Directory -Path $destDir -Force | Out-Null } # 复制文件 Copy-Item -Path $file -Destination $destPath -Force Write-Host "已复制: $file" -ForegroundColor Cyan $filesCopied++ } Write-Host "`n完成! 共复制了 $filesCopied 个已修改的文件到 $TargetDir" -ForegroundColor Green2. 复制已修改和已暂存的文件# 复制所有Git已修改文件脚本（包括已暂存和未暂存） # 使用方法: .\copy-all-modified-files.ps1 -TargetDir "D:\目标文件夹" param ( [Parameter(Mandatory=$true)] [string]$TargetDir ) # 确保目标目录存在 if (-not (Test-Path -Path $TargetDir)) { New-Item -ItemType Directory -Path $TargetDir -Force | Out-Null Write-Host "创建目标目录: $TargetDir" -ForegroundColor Green } # 获取已修改但未暂存的文件 $modifiedFiles = git ls-files --modified # 获取已暂存的文件 $stagedFiles = git diff --cached --name-only # 合并文件列表并去重 $allModifiedFiles = $modifiedFiles + $stagedFiles | Sort-Object -Unique $filesCopied = 0 foreach ($file in $allModifiedFiles) { # 创建目标文件路径 $destPath = Join-Path -Path $TargetDir -ChildPath $file # 确保目标文件的父目录存在 $destDir = Split-Path -Path $destPath -Parent if (-not (Test-Path -Path $destDir)) { New-Item -ItemType Directory -Path $destDir -Force | Out-Null } # 复制文件 Copy-Item -Path $file -Destination $destPath -Force Write-Host "已复制: $file" -ForegroundColor Cyan $filesCopied++ } Write-Host "`n完成! 共复制了 $filesCopied 个修改的文件到 $TargetDir" -ForegroundColor Green3. 复制所有变更（包括未跟踪的新文件）# 复制所有Git更改文件脚本（包括已修改、已暂存和未跟踪的新文件） # 使用方法: .\copy-all-git-changes.ps1 -TargetDir "D:\目标文件夹" param ( [Parameter(Mandatory=$true)] [string]$TargetDir ) # 确保目标目录存在 if (-not (Test-Path -Path $TargetDir)) { New-Item -ItemType Directory -Path $TargetDir -Force | Out-Null Write-Host "创建目标目录: $TargetDir" -ForegroundColor Green } # 获取已修改但未暂存的文件 $modifiedFiles = git ls-files --modified # 获取已暂存的文件 $stagedFiles = git diff --cached --name-only # 获取未跟踪的文件 $untrackedFiles = git ls-files --others --exclude-standard # 合并文件列表并去重 $allChangedFiles = $modifiedFiles + $stagedFiles + $untrackedFiles | Sort-Object -Unique $filesCopied = 0 foreach ($file in $allChangedFiles) { # 跳过空字符串（如果有） if (-not $file) { continue } # 确保文件存在 if (-not (Test-Path -Path $file)) { Write-Host "警告: 文件不存在: $file" -ForegroundColor Yellow continue } # 创建目标文件路径 $destPath = Join-Path -Path $TargetDir -ChildPath $file # 确保目标文件的父目录存在 $destDir = Split-Path -Path $destPath -Parent if (-not (Test-Path -Path $destDir)) { New-Item -ItemType Directory -Path $destDir -Force | Out-Null } # 复制文件 Copy-Item -Path $file -Destination $destPath -Force Write-Host "已复制: $file" -ForegroundColor Cyan $filesCopied++ } Write-Host "`n完成! 共复制了 $filesCopied 个已更改的文件到 $TargetDir" -ForegroundColor Green使用这些脚本前，需要临时修改PowerShell的执行策略：# 临时修改当前会话的执行策略 Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass # 然后运行脚本 .\copy-all-git-changes.ps1 -TargetDir "D:\您想要的目标目录"这种方法的优点是功能更丰富，可以根据需要选择不同的脚本；缺点是需要修改PowerShell的执行策略，这在某些环境中可能受到限制。方案三：批处理文件（推荐）为了避免修改PowerShell执行策略的问题，我们可以创建一个批处理文件来调用PowerShell命令：@echo off REM 复制Git已修改文件的批处理脚本 REM 使用方法: copy-git-changes.bat "D:\目标文件夹" IF "%~1"=="" ( echo 错误: 请提供目标目录路径 echo 用法: copy-git-changes.bat "D:\目标文件夹" exit /b 1 ) SET TargetDir=%~1 echo 正在复制Git中已修改的文件到 %TargetDir% ... powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "$TargetDir = '%TargetDir%'; if (-not (Test-Path -Path $TargetDir)) { New-Item -ItemType Directory -Path $TargetDir -Force | Out-Null; Write-Host \"创建目标目录: $TargetDir\" -ForegroundColor Green }; $modifiedFiles = git ls-files --modified; $stagedFiles = git diff --cached --name-only; $untrackedFiles = git ls-files --others --exclude-standard; $allChangedFiles = $modifiedFiles + $stagedFiles + $untrackedFiles | Sort-Object -Unique; $filesCopied = 0; foreach ($file in $allChangedFiles) { if (-not $file) { continue }; if (-not (Test-Path -Path $file)) { Write-Host \"警告: 文件不存在: $file\" -ForegroundColor Yellow; continue }; $destPath = Join-Path -Path $TargetDir -ChildPath $file; $destDir = Split-Path -Path $destPath -Parent; if (-not (Test-Path -Path $destDir)) { New-Item -ItemType Directory -Path $destDir -Force | Out-Null }; Copy-Item -Path $file -Destination $destPath -Force; Write-Host \"已复制: $file\" -ForegroundColor Cyan; $filesCopied++ }; Write-Host \"`n完成! 共复制了 $filesCopied 个已更改的文件到 $TargetDir\" -ForegroundColor Green" echo 操作完成！使用方法：.\copy-git-changes.bat "D:\您想要的目标目录"这种方法的优点是：不需要修改任何系统安全设置使用简单，只需要运行批处理文件并指定目标目录可以永久保存批处理文件以便将来使用自动创建必要的目录结构并保持原目录结构工作原理解析这些脚本的工作原理是利用Git提供的命令来获取不同状态的文件列表：git ls-files --modified：获取已修改但未暂存的文件git diff --cached --name-only：获取已暂存但未提交的文件git ls-files --others --exclude-standard：获取未跟踪的新文件然后，脚本将这些文件列表合并并去重，接着遍历每个文件并将其复制到目标目录中的相应位置，同时保持原目录结构。实际应用场景代码备份：在进行大型重构前备份已修改的文件问题排查：将修改过的文件复制到测试环境进行问题复现代码审查：将修改的文件提供给同事进行离线审查环境迁移：在不同环境间转移尚未提交的变更增量部署：仅部署已修改的文件到服务器常见问题与解决方案PowerShell脚本无法执行问题：执行PowerShell脚本时提示"在此系统上禁止运行脚本"。解决方案：使用批处理文件方法（方案三）或临时修改执行策略：Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass某些文件未被复制问题：部分文件未被复制到目标目录。解决方案：确保文件已被Git识别（已修改、已暂存或未跟踪）对于二进制文件或符号链接，可能需要特殊处理检查文件是否被.gitignore忽略目录结构不正确问题：目标目录中的文件结构与原项目不一致。解决方案：确保在项目根目录运行脚本检查脚本中的路径处理逻辑总结在Windows系统中将Git已修改的文件按原目录结构复制出来有多种方法，其中使用批处理文件的方法（方案三）最为推荐，因为它不需要修改任何系统设置，使用简单，并且可以永久保存以便将来使用。通过本文介绍的方法，你可以轻松地将Git中已修改、已暂存和未跟踪的文件复制到指定目录，同时保持原目录结构，这对于代码备份、问题排查、代码审查等场景非常有用。希望这篇文章能够帮助你更高效地管理代码变更。如果你有任何问题或建议，欢迎在评论区留言。参考资料Git官方文档 - git-ls-filesPowerShell官方文档 - Copy-ItemWindows批处理脚本教程
同事看了想打人！这8个SQL写法让数据库慢到崩溃
坑点1：翻页越往后越卡当你用LIMIT 1000000,10查数据时，数据库就像翻一本100万页的书——明明只要第100万页的10行字，却得从第一页开始数。优化大招：记住上次看到哪儿。比如上次翻页最后一条的创建时间是"2023-01-01 12:00:00"，下页查询直接加条件WHERE create_time > '2023-01-01 12:00:00'，秒出结果。问题案例：-- 传统分页（偏移量越大性能越差） SELECT * FROM logs WHERE log_type='ERROR' ORDER BY create_time LIMIT 1000000, 10; -- 需遍历前100万条记录优化方案：-- 游标分页法（基于最后时间戳） SELECT * FROM logs WHERE log_type='ERROR' AND create_time > '2024-02-17 00:00:00' -- 使用上一页末尾时间 ORDER BY create_time LIMIT 10; -- 执行时间稳定在5ms内坑点2：乱填参数害死索引字段明明是varchar类型，程序却传了数字进来。这就好比用家门钥匙开汽车锁，数据库只能暴力拆锁全表扫描。检查方法：执行SHOW WARNINGS看到类型转换警告，赶紧让程序员改代码对齐类型。问题案例：-- 字段phone定义为VARCHAR(20)，但传入数值参数 SELECT * FROM users WHERE phone = 13800138000; -- 触发类型转换，索引失效 -- 查看执行计划警告 SHOW WARNINGS; > Warning: Cannot use ref access on index 'phone' due to type conversion 优化方案：-- 添加引号保持类型一致 SELECT * FROM users WHERE phone = '13800138000'; -- 正确使用索引坑点3：更新语句慢如蜗牛用UPDATE...WHERE id IN (子查询)更新数据？MySQL会傻乎乎地循环查几千次。救星方案：改成JOIN写法，让数据库一次性关联更新。实测有个案例从7秒降到2毫秒。问题案例：-- 错误写法（嵌套子查询） UPDATE orders o SET status = 'expired' WHERE o.id IN ( SELECT id FROM ( SELECT id FROM orders WHERE create_time < '2023-01-01' LIMIT 1000 ) tmp ); -- 执行时间>10秒优化方案：-- 改用JOIN重写 UPDATE orders o JOIN ( SELECT id FROM orders WHERE create_time < '2023-01-01' LIMIT 1000 ) tmp ON o.id = tmp.id SET status = 'expired'; -- 执行时间<100ms坑点4：混搭排序逼疯数据库 既要按评分升序，又要按时间降序，索引直接罢工。破解术：拆成两次查询再合并。比如先查未回复的订单按时间排序，再查已回复的，最后拼起来。问题案例：-- 混合排序导致全表扫描 SELECT * FROM product_reviews ORDER BY is_approved ASC, review_time DESC LIMIT 20; -- 无法使用(is_approved,review_time)索引优化方案：-- 拆分查询+UNION ALL (SELECT * FROM product_reviews WHERE is_approved=0 ORDER BY review_time DESC LIMIT 20) UNION ALL (SELECT * FROM product_reviews WHERE is_approved=1 ORDER BY review_time DESC LIMIT 20) ORDER BY is_approved ASC, review_time DESC LIMIT 20; -- 执行效率提升80倍坑点5：乱用SELECT * 拖垮网络动不动就SELECT *，把不要的字段也查出来。特别是text大字段，传输速度直接腰斩。保命原则：需要什么字段就写什么，别让数据库做快递小哥送垃圾。问题案例：-- 低效写法 SELECT * FROM employees e WHERE EXISTS ( SELECT 1 FROM salaries s WHERE s.emp_id = e.id AND s.year = 2023 ); -- 嵌套循环执行优化方案：-- 改用JOIN优化 SELECT e.* FROM employees e INNER JOIN salaries s ON e.id = s.emp_id AND s.year = 2023; -- 利用索引快速定位坑点6：模糊搜索堵死CPULIKE '%关键字%'这种查询，数据库只能玩命扫描。应急方案：上Elasticsearch做专业搜索，或者业务上限制必须带前缀查询。问题案例：-- 用户搜索包含"error"的日志（无前缀模糊匹配） SELECT * FROM server_logs WHERE log_message LIKE '%error%'; -- 执行计划：全表扫描，耗时12秒（数据量500万行）优化方案：-- 方案1：强制前缀匹配（利用索引） SELECT * FROM server_logs WHERE log_message LIKE 'error%'; -- 使用前缀索引 -- 方案2：集成Elasticsearch（专业分词检索） PUT /logs/_search { "query": { "match": { "message": "error" } } }原理说明： B+树索引无法逆向检索，LIKE '%xx'会导致索引失效。前缀匹配可触发索引范围扫描，而全文检索场景建议使用专用引擎（如ES）的分词能力。坑点7：乱堆OR条件让索引哭晕WHERE a=1 OR b=2这种写法，数据库往往直接放弃治疗全表扫描。改造技巧：拆成两个查询用UNION合并，速度提升50倍。问题案例：-- 查询订单状态为"paid"或物流单号为"SF123"的订单 SELECT * FROM orders WHERE status = 'paid' OR tracking_no = 'SF123'; -- 执行计划：全表扫描，耗时8秒（数据量200万行）优化方案：-- 拆分为UNION查询（分别利用不同索引） (SELECT * FROM orders WHERE status = 'paid') UNION (SELECT * FROM orders WHERE tracking_no = 'SF123'); -- 执行时间：0.2秒（提升40倍）原理说明： OR条件导致优化器无法选择有效索引。拆分为UNION后，可分别利用status和tracking_no的单列索引，再合并结果集。坑点8：子查询嵌套套到死在WHERE里疯狂嵌套子查询，MySQL会被逼成单线程工人。优化真经：多用临时表或JOIN代替，给数据库留条活路。问题案例：-- 查询有评论的商品（嵌套子查询） SELECT * FROM products WHERE id IN ( SELECT product_id FROM comments WHERE create_time > '2024-01-01' ); -- 执行计划：DEPENDENT SUBQUERY，耗时15秒 优化方案：-- 方案1：改用JOIN关联（触发索引嵌套循环） SELECT p.* FROM products p JOIN comments c ON p.id = c.product_id WHERE c.create_time > '2024-01-01'; -- 方案2：临时表预存子查询结果 CREATE TEMPORARY TABLE tmp_products AS SELECT product_id FROM comments WHERE create_time > '2024-01-01'; SELECT p.* FROM products p JOIN tmp_products t ON p.id = t.product_id; -- 执行时间均降至0.5秒内 原理说明： 嵌套子查询会导致循环执行，而JOIN或临时表可将多次查询优化为单次数据关联。通过执行计划分析，JOIN写法通常能触发更优的Index Nested-Loop Join算法。看完赶紧检查下自己写的SQL，别让同事半夜打电话骂人！遇到过更奇葩SQL坑货的，欢迎留言区吐槽才哥最近开发了一个针对Java开发人员的面试刷题小程序，计划准备面试的伙伴们可以去试试，手机在线刷题更方便
Java泛型的功能主要是在编译阶段进行类型安全的检查，让开发者能够编写出更具通用性和灵活性的代码，有效避免在程序运行过程中出现类型转换的错误。泛型的作用：确保类型安全：泛型能够在编译时进行严格的类型检查，确保在操作集合或其他泛型类时，不会出现类型不一致的情况，从而降低运行时出现ClassCastException异常的风险。提升代码复用性：借助泛型，代码可以适用于多种不同的数据类型，减少了代码的重复编写，使得代码的可读性和可维护性得到提升。避免显式类型转换：通过在编译时明确指定类型参数，泛型省去了在运行时进行显式类型转换的步骤，简化了代码的编写。示例代码：List<String> stringList = new ArrayList<>(); stringList.add("Hello"); String message = stringList.get(0); // 无需进行类型转换泛型的实际应用场景集合框架中的应用：Java的集合框架大量运用了泛型。像List<T>、Set<T>以及Map<K, V>等接口，能够针对不同的数据类型实现统一的操作方式。泛型方法的定义：不仅可以定义泛型类，还可以定义泛型方法，这样可以使方法具备处理多种不同数据类型的能力。例如：public static <T> void printElements(T[] elements) { for (T element : elements) { System.out.println(element); } }为何需要泛型的通俗解释在Java 5版本之前，泛型是不存在的，那时的代码也能正常运行，那么为何要引入泛型呢，它能为我们带来哪些好处呢？先来看下面这段代码：List list = new ArrayList(); list.add("yes"); // 添加字符串 list.add(233); // 添加整数在没有泛型的时期，集合中添加的数据不会受到任何类型限制，全部被视为Object类型。或许有人会觉得这样很自由，确实，自由度很高，但是代码的约束性越弱，出错的概率就越高，在使用上也会带来诸多不便，比如在获取数据时需要进行强制类型转换。如果一不小心获取了错误的类型，虽然代码能够通过编译，但在运行时却会抛出异常。综上所述，Java引入了泛型机制。泛型的作用就是增加了一层类型约束。有了这层约束，由于已经声明了类型，所以在编译阶段就能识别出不准确的类型元素。这样可以让错误提前被发现，避免在运行时才暴露出来。并且也不需要在代码中显式地进行强制类型转换，从下面的代码可以看出，可以直接获取到String类型的元素。我们再总结一下泛型的优势：提升了代码的可读性，能够一眼看出集合（或其他泛型类）所存储的数据类型能在编译阶段检查类型安全，增强程序的健壮性省去了显式类型转换的麻烦（实际上内部已经完成了类型转换）提高了代码的复用性，定义好的泛型可以让一个方法（或类）适用于所有类型（虽然以前使用Object也可以实现，但相对较为繁琐）为何有人说Java的泛型是伪泛型来看下面这段代码：可以看到，虽然声明的是一个String类型的集合，但通过反射的方式却能够成功地向集合中插入int类型的数据。这表明在运行时，泛型并没有起到任何作用！也就是说，在运行过程中，JVM无法获取到泛型的信息，也不会对其进行任何约束。可以认为Java的泛型仅在编译时有效，而在运行时则不存在泛型，这也是人们常说Java是伪泛型的原因。简而言之，Java的泛型仅在编译阶段发挥作用，而在JVM运行时则不存在泛型的概念。近期才哥整理出了一个可用于快速刷面试题的小程序，其中收录了常见面试题及其答案，涵盖了基础、并发、JVM、MySQL、Redis、Spring、SpringMVC、SpringBoot、SpringCloud、消息队列等多个类型，感兴趣的扫下方的小程序码进行体验。
Java 插件模式SPI学习与理解（Java SPI 、Spring SPI、Dubbo SPI）
SPI 全称为 Service Provider Interface，是一种服务发现机制，其核心在于将接口实现类的全限定名配置在文件中，由服务加载器读取并加载实现类，从而可在运行时为接口动态替换实现类，为程序提供拓展功能。1. 重新阐述 Java SPI 示例定义接口和实现类：首先定义一个名为 Robot 的接口，包含 sayHello 方法。public interface Robot { void sayHello(); } 然后创建两个实现类 OptimusPrime 和 Bumblebee，它们都实现了 Robot 接口并实现了 sayHello 方法。 public class OptimusPrime implements Robot { @Override public void sayHello() { System.out.println("Hello, I am Optimus Prime."); } } public class Bumblebee implements Robot { @Override public void sayHello() { System.out.println("Hello, I am Bumblebee."); } } 配置文件：在 META-INF/services 文件夹下创建名为 org.apache.spi.Robot 的文件（这里假设接口全限定名为 org.apache.spi.Robot），文件内容包含实现类的全限定名： org.apache.spi.OptimusPrime org.apache.spi.Bumblebee 测试代码： public class JavaSPITest { @Test public void sayHello() throws Exception { ServiceLoader<Robot> serviceLoader = ServiceLoader.load(Robot.class); System.out.println("Java SPI"); // 两种遍历模式 serviceLoader.forEach(Robot::sayHello); Iterator<Robot> iterator = serviceLoader.iterator(); while (iterator.hasNext()) { Robot robot = iterator.next(); // 可以调用 robot.sayHello() 进行测试 } } } 2. 经典 Java SPI 应用：JDBC DriverManager传统 JDBC 驱动加载：在 JDBC4.0 之前，需要先使用 Class.forName("com.mysql.jdbc.Driver") 加载数据库驱动，然后进行连接操作。 // STEP 1: Register JDBC driver Class.forName("com.mysql.jdbc.Driver"); // STEP 2: Open a connection String url = "jdbc:xxxx://xxxx:xxxx/xxxx"; Connection conn = DriverManager.getConnection(url,username,password); 使用 SPI 后的 JDBC 驱动加载：JDBC4.0 之后利用 Java 的 SPI 扩展机制，无需手动调用 Class.forName 加载驱动，可直接获取连接。DriverManager 类是驱动管理器，在其静态初始化块中调用 loadInitialDrivers 方法，其中涉及 SPI 的使用： static { loadInitialDrivers(); println("JDBC DriverManager initialized"); } 加载驱动的四个步骤：从系统变量获取驱动定义。用 SPI 获取驱动实现类（字符串形式）。ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class); 遍历使用 SPI 获取的实现，实例化各实现类。 Iterator<Driver> driversIterator = loadedDrivers.iterator(); while(driversIterator.hasNext()) { driversIterator.next(); } 根据第一步的驱动列表实例化具体实现类。3. Java SPI 机制源码解析ServiceLoader 类的 load 方法：public static ServiceLoader load(Class service) { ClassLoader cl = Thread.currentThread().getContextClassLoader(); return ServiceLoader.load(service, cl); } public static ServiceLoader load(Class service, ClassLoader loader) { return new ServiceLoader<>(service, loader); } 该方法根据服务类型和类加载器创建 ServiceLoader 对象。- `ServiceLoader` 构造器：private ServiceLoader(Class svc, ClassLoader cl) { service = Objects.requireNonNull(svc, "Service interface cannot be null"); loader = (cl == null)? ClassLoader.getSystemClassLoader() : cl; acc = (System.getSecurityManager()!= null)? AccessController.getContext() : null; reload(); } private LinkedHashMap<String,S> providers = new LinkedHashMap<>(); public void reload() { providers.clear(); lookupIterator = new LazyIterator(service, loader); } 私有构造器创建懒迭代器 LazyIterator 对象，只有调用迭代方法时才执行加载逻辑。迭代器的 hasNext() 方法： public boolean hasNext() { if (acc == null) { return hasNextService(); } else { PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() { public Boolean run() { return hasNextService(); } }; return AccessController.doPrivileged(action, acc); } } 该方法最终调用 hasNextService 方法，会通过加载器获取配置对象并解析 META-INF/services/ 目录下的文件。迭代器的 next() 方法： public S next() { if (acc == null) { return nextService(); } else { PrivilegedAction action = new PrivilegedAction() { public S run() { return nextService(); } }; return AccessController.doPrivileged(action, acc); } } 本质是调用 nextService 方法，通过反射加载类对象，实例化类并缓存到 providers 对象中。4. Java SPI 机制的缺陷无法按需加载，会遍历并实例化所有实现，即使部分实现类不需要或实例化耗时。获取实现类的方式仅能通过 Iterator，不够灵活，不能根据参数获取特定实现类。多线程使用 ServiceLoader 实例时不安全。5. Spring SPI 机制创建接口和实现类：定义 MyTestService 接口： public interface MyTestService { void printMylife(); } 创建实现类 WorkTestService 和 FamilyTestService： public class WorkTestService implements MyTestService { public WorkTestService(){ System.out.println("WorkTestService"); } public void printMylife() { System.out.println("我的工作"); } } public class FamilyTestService implements MyTestService { public FamilyTestService(){ System.out.println("FamilyTestService"); } public void printMylife() { System.out.println("我的家庭"); } } 配置文件：在 META-INF/spring.factories 中配置接口和实现类：com.courage.platform.sms.demo.service.MyTestService = com.courage.platform.sms.demo.service.impl.FamilyTestService,com.courage.platform.sms.demo.service.impl.WorkTestService 测试代码： List<MyTestService> myTestServices = SpringFactoriesLoader.loadFactories( MyTestService.class, Thread.currentThread().getContextClassLoader() ); for (MyTestService testService : myTestServices) { testService.printMylife(); } 与 Java SPI 的区别：Java SPI 一个服务接口对应一个配置文件，Spring SPI 一个 spring.factories 配置文件存放多个接口及实现类，以接口全限定名作为 key，实现类作为 value，多个实现类用逗号分隔。两者都无法获取特定的实现，只能按顺序获取所有实现。6. Dubbo SPI 机制配置文件：配置文件放在 META-INF/dubbo 路径下，以键值对方式配置实现类： optimusPrime = org.apache.spi.OptimusPrime bumblebee = org.apache.spi.Bumblebee 测试代码： public class DubboSPITest { @Test public void sayHello() throws Exception { ExtensionLoader<Robot> extensionLoader = ExtensionLoader.getExtensionLoader(Robot.class); Robot optimusPrime = extensionLoader.getExtension("optimusPrime"); optimusPrime.sayHello(); Robot bumblebee = extensionLoader.getExtension("bumblebee"); bumblebee.sayHello(); } } 特点：支持按需加载接口实现类，可通过键值对方式指定要加载的实现。相比 Java SPI 增加了 IOC 和 AOP 等特性。近期才哥整理出了一个可用于快速刷面试题的小程序，其中收录了常见面试题及其答案，涵盖了基础、并发、JVM、MySQL、Redis、Spring、SpringMVC、SpringBoot、SpringCloud、消息队列等多个类型，感兴趣的可以点击下方试试。
JWT一文全解，禁止这样使用，被发现立即领盒饭！
在当今的网络开发领域，JSON Web Tokens（JWT）被广泛提及和应用，但对于其在存储 Session 方面的适用性，却存在诸多误解。本文旨在深入探讨为何 JWT 并不适合担当存储 Session 的重任，并揭示其可能引发的安全问题，以期帮助开发者们更全面、准确地理解和运用 JWT。一、术语明确在展开讨论之前，先对一些关键术语进行清晰的界定，这有助于避免后续的混淆和歧义：无状态 JWT（Stateless JWT）：这类 JWT Token 直接将 Session 数据编码于其中，所有与 Session 相关的信息都被封装在 Token 内部。有状态 JWT（Stateful JWT）：此类型的 JWT Token 仅包含对 Session 的引用或者 Session ID，实际的 Session 数据则存放在服务端。Session token（亦称 Session cookie）：这是一种标准的、可被签名的 Session ID 形式，许多 Web 框架（如 Laravel）长期以来都采用这种成熟的 Session 机制，其 Session 数据同样是存储在服务端。需要着重强调的是，本文的目的并非倡导“绝对不使用 JWT”，而是要明确指出 JWT 在作为 Session 机制时存在的弊端和风险。实际上，JWT 在其他特定场景下仍具有其独特的价值和用途，在本文结尾部分，将会简要介绍一些 JWT 的合理应用场景。二、对比对象的澄清常常有人错误地将 Cookies 和 JWT 进行不恰当的比较，这就如同将内存和硬盘简单对比一样，缺乏实际意义。Cookies 本质上是一种存储机制，而 JWT Tokens 则是经过加密和签名处理的令牌，二者并非相互对立的关系，相反，它们既可以独立使用，也能够结合运用。正确的比较方式应该是：Session 与 JWT 相对比，以及 Cookies 与 Local Storage 相对比。在后续的讨论中，本文将主要围绕 JWT Tokens 和 Session 的对比展开，并在必要时涉及 Cookie 和 Local Storage 的比较，这样的对比方式更能准确地反映实际情况和问题本质。三、JWT 被广泛宣扬的优势剖析（一）易于水平扩展？在众多关于 JWT 的宣传中，易于水平扩展被列为其优势之一。从技术层面来看，这一说法在使用无状态 JWT Tokens 时确实存在一定的合理性，但前提是要有相应的应用场景需求。然而，在实际情况中，绝大多数项目几乎并不需要这种高度的横向扩展能力。在常见的系统扩展场景中，存在许多更为简便易行的解决方案。例如：当需要在单台服务器上运行多个后端进程时，只需在该服务器上部署 Redis 服务来存储 Session 即可，这种方式简单高效，能够满足大部分常规业务的需求。对于运行多台服务器的情况，配备一台专门的 Redis 服务器用于集中存储 Session 数据，就能轻松实现 Session 的共享和管理，确保系统的稳定运行。在更为复杂的多集群环境中，通过会话保持（即粘滞会话）技术，也能够很好地应对 Session 的管理问题，而且这些技术在现有的软件系统架构中都得到了良好的支持，通常不需要应用层进行特殊的复杂处理。或许有人会考虑为未来的业务发展预留更多的调整空间，担心以后可能会需要无状态的会话机制。但从实际经验来看，后期对 Session 机制进行替换并非难事，唯一可能产生的影响是在迁移过程中所有用户需要重新登录一次。因此，综合考虑 JWT 可能带来的负面影响，在项目前期并没有必要为了一个不太确定的未来需求而强行采用 JWT 来实现 Session 管理。另外，对于近期有面试跳槽计划的开发者，建议在[Java面试库]()小程序上进行在线刷题，该小程序涵盖了 3000 余道 Java 面试题，几乎囊括了所有主流技术面试题，能够帮助开发者更好地准备面试，提升自身竞争力。（二）易于使用？实际上，JWT 在使用便利性方面并没有优势。使用 JWT 时，无论是在客户端还是服务端，都需要开发者自行处理 Session 的管理逻辑。相比之下，标准的 Session cookies 则具有开箱即用的特点，许多 Web 框架都对其进行了良好的封装和支持，开发者无需过多关注底层的实现细节，即可轻松使用 Session 功能。而 JWT 目前尚未被广泛集成到各种开箱即用的框架中，这就意味着研发人员需要投入更多的精力来处理与 JWT 相关的复杂逻辑，从这个角度来看，JWT 并不比传统的 Session 机制更易于使用。（三）更加灵活？关于 JWT 更加灵活这一说法，目前并没有确凿的证据和清晰的阐述来支撑。在实际应用中，几乎每个主流的 Session 实现都允许开发者直接将数据存储到 Session 中，这种数据存储和管理方式与 JWT 的机制并没有本质上的区别。因此，所谓的“JWT 更加灵活”更多的可能只是一种流行但缺乏实际依据的说法，在实际的开发过程中，开发者很难真切地感受到这种灵活性带来的实际价值和优势。（四）更加安全？有不少人认为 JWT Tokens 更加安全，其主要理由是 JWT 使用了加密技术。然而，这种观点是不准确的。实际上，签名后的 Cookies 相较于未签名的 Cookies 同样具有更高的安全性，而且这并不是 JWT 所独有的特性，许多优秀的 Session 实现都采用了签名后的 Cookies 来保障数据的安全传输和完整性（例如 Laravel 框架）。需要明确的是，单纯的“使用加密技术”并不能自动使某个事物变得更加安全，加密技术必须服务于特定的安全目标，并且要确保其应用方式是针对该目标的有效解决方案。如果加密技术使用不当，反而可能会引入新的安全风险，导致安全性降低。此外，还有一种关于“JWT 更加安全”的常见论述，即“JWT 不使用 Cookies 传输 Tokens”，这种说法是荒谬的。Cookie 仅仅是一条 HTTP 头信息，使用 Cookies 本身并不会导致不安全的情况发生。事实上，Cookies 在设计和实现上受到了特别良好的保护，能够有效地防止恶意的客户端代码对其进行篡改和窃取。如果担心 Session cookies 被他人拦截，正确的做法是采用 TLS（Transport Layer Security）加密协议来保障数据传输的安全。如果不使用 TLS，那么无论是哪种类型的 Session 机制，包括 JWT，都存在被拦截和窃取的风险，因为在未加密的网络环境中，数据的安全性无法得到有效的保障。（五）内置过期时间功能？JWT 的内置过期时间功能实际上并没有太大的实际价值和优势。虽然 JWT 可以在 Token 中设置过期时间，但在服务端同样能够轻松实现对 Session 过期时间的控制，而且许多现有的 Session 实现正是采用这种方式。相比之下，服务端的过期控制更加合理和灵活，因为这样可以让应用程序及时清除那些不再需要的 Session 数据，从而优化系统资源的利用和管理。而如果依赖无状态 JWT Tokens 的过期机制，由于其无法在服务端直接对过期的 Token 进行有效的管理和清理，可能会导致一些潜在的问题，例如 Session 数据的残留和浪费，以及在某些情况下可能会增加系统的复杂度和管理难度。（六）无需询问用户“本网站使用 Cookies”？这种说法是完全错误的。并不存在所谓的“Cookies 法律”专门针对 Cookies 的使用进行限制，实际上，有关 Cookies 的各种法律法规涵盖的范围更广，包括任何类型的“对某项服务的正常运行非严格必须的持久性 ID”，这意味着几乎所有可以想到的 Session 机制都在其监管范围内。具体来说：如果 Session 或 Token 是出于系统功能目的（例如保持用户的登录状态）而使用的，那么无论采用何种方式存储 Session，都无需征得用户的同意，因为这是保障系统正常运行所必需的功能。然而，如果 Session 或 Token 是用于其他目的（例如数据分析、用户追踪等），那么无论选择怎样的 Session 存储机制，都需要明确询问用户是否允许使用，以确保符合相关法律法规的要求和保护用户的隐私权益。（七）防止 CSRF 攻击？JWT 并不能有效地防止 CSRF（Cross-Site Request Forgery）攻击。目前，存储 JWT Tokens 主要有两种常见方式：一种是将其存入 Cookie 中，但这样做仍然容易受到 CSRF 攻击，与传统的 Session 机制类似，仍然需要采取特殊的防护措施来保护 Token 不被恶意利用，例如使用 CSRF Tokens 等技术来增强安全性。另一种方式是将 JWT 存储在其他地方，如 Local Storage 中。虽然这种方式在一定程度上可以避免 CSRF 攻击，但却带来了新的问题。因为使用 Local Storage 需要依赖 JavaScript 来获取和使用 Token，这就要求网站必须支持 JavaScript 才能正常访问，而且这还可能引发另一个完全不同且可能更加严重的漏洞，后续将会对此进行详细说明。需要明确的是，预防 CSRF 攻击的唯一正确方法是使用 CSRF Tokens，而 Session 机制本身与防止 CSRF 攻击并没有直接的关联，不能将防止 CSRF 攻击的期望寄托于 JWT 或其他 Session 机制上，而应该采用专门的 CSRF 防护技术来保障系统的安全。（八）更适用于移动端？这种说法是毫无根据的。目前市面上几乎所有可用的浏览器都对 Cookies 提供了良好的支持，因此也能够很好地支持基于 Cookies 的 Session 机制。同样，主流的移动端开发框架以及严谨的 HTTP 客户端库也都对 Session 机制有着完善的支持，无论是在移动端还是桌面端，Session 机制都能够稳定、高效地运行，不存在 JWT 在移动端具有独特优势的情况，所以这并不是一个选择 JWT 作为 Session 机制的合理理由。（九）适用于阻止 Cookies 的用户？这种情况发生的可能性非常低。通常情况下，用户如果选择阻止 Cookies，往往也会阻止其他任何形式的持久化数据存储，而不仅仅是针对 Cookies 本身。例如，Local Storage 以及其他能够实现 Session 持久化存储的机制（无论是否使用 JWT）都会受到影响。因此，即使采用 JWT 来试图解决用户阻止 Cookies 的问题，也很难达到预期的效果，因为这是一个更为复杂和独立的用户行为和隐私设置问题，与具体的 Session 存储技术关系不大。而且，对于大多数禁用 Cookies 的用户来说，他们往往清楚这会导致网站的身份认证功能无法正常使用，因此在实际使用中，他们通常会针对自己关心的特定站点单独解锁 Cookies 功能，以确保能够正常访问和使用这些网站的服务。所以，从开发者的角度来看，这并不是一个需要通过采用特殊技术（如 JWT）来解决的关键问题，更好的做法是向用户清晰、详细地解释为什么网站需要使用 Cookies 才能正常运行，以提高用户对 Cookies 使用的理解和接受程度。四、JWT 的劣势揭示通过对上述常见误解的分析和澄清，我们已经了解到 JWT 在作为 Session 机制时，所谓的优势大多并不成立。然而，问题远不止于此，使用 JWT 作为 Session 机制还存在许多严重的缺点，其中一些甚至会引发重大的安全问题，以下将对这些劣势进行详细阐述：（一）更费空间JWT Tokens 的实际数据量并不小，尤其是在使用无状态 JWT 时，由于所有的 Session 数据都需要直接编码到 Tokens 内部，这很容易导致 Token 的长度迅速增加，从而可能超过 Cookies 或 URL 的长度限制。在这种情况下，可能会有人考虑将 JWT Tokens 存储到 Local Storage 中，但这又会引发其他一系列问题，后续将会详细说明。（二）更不安全当将 JWT Tokens 存储到 Cookies 中时，其安全性与其他传统的 Session 机制相比并没有本质上的提升或差异。然而，如果将 JWT 存储在其他位置，如 Local Storage，就会引入一个新的安全漏洞。具体来说，Local Storage 是 HTML5 提供的一项功能，它允许浏览器进行 Key/Value 形式的存储操作，对于可能较大的 JWT Tokens 来说，由于 Cookies 的存储容量通常在 4k 左右，对于较大的 Tokens 可能无法满足存储需求，而 Local Storage 似乎成为了一个可行的解决方案。但需要注意的是，Local Storage 并没有像 Cookies 那样提供完善的安全保护措施。与 Cookies 不同，Local Storage 中的数据不会在每次 HTTP 请求时自动发送到服务器端，获取这些数据的唯一途径是通过 JavaScript 代码。这就意味着，如果攻击者能够成功注入恶意的 JavaScript 脚本，并且该脚本能够通过网站的内容安全策略检查，那么攻击者就可以轻易地访问和泄露 Local Storage 中存储的 JWT Tokens，从而导致严重的安全事故。此外，JavaScript 在处理数据时并不关心或追踪数据是否通过安全的 HTTPS 连接进行传输，对于 JavaScript 而言，这些数据仅仅是普通的数据，浏览器会按照常规的数据处理方式来对待它们。这与我们长期以来通过各种技术手段确保 Cookies 安全的努力背道而驰，因为我们花费了大量的精力来防止 Cookies 被恶意接触和窃取，而现在使用 Local Storage 存储 JWT Tokens 却忽视了这些宝贵的经验教训，这无疑是一种安全意识上的倒退。综上所述，无论是否采用 JWT，“使用 Cookies 仍然是保障数据安全传输和存储的重要手段”，不应该轻易地放弃 Cookies 而选择其他可能存在安全隐患的存储方式。（三）无法单独销毁使用 JWT 作为 Session 机制还存在一个严重的安全问题，那就是无法单独销毁 Session。与传统的 Sessions 不同，传统 Sessions 可以在服务端随时根据需要进行单独的销毁操作，例如当检测到用户的异常行为或安全威胁时，能够及时销毁相应的 Session，以防止潜在的风险。然而，无状态 JWT Tokens 由于其设计特点，在 Token 过期之前无法被单独地销毁。例如，在检测到攻击行为时，由于无法立即销毁攻击者的 Session，这就使得攻击者仍然可以利用有效的 Token 继续访问系统，从而给系统带来持续的安全威胁。同样，在用户修改密码等关键操作后，也无法及时销毁旧的 Sessions，这可能会导致旧的 Token 仍然被泄露或滥用，进一步增加了系统的安全风险。针对这一问题，目前几乎没有有效的解决方案，除非重新构建一套复杂的、有状态（Stateful）的基础设施，专门用于明确地检测和拒绝特定的 Session，但这样做不仅成本高昂，而且完全违背了使用无状态 JWT Tokens 的初衷和优势，使得系统的架构变得更加复杂和难以维护。（四）数据延迟与上述安全问题相关联的是，使用无状态 JWT Tokens 还可能存在数据延迟的问题，这也构成了一个潜在的安全隐患。由于无状态 Tokens 中存储的数据是在某个特定时间点生成的，随着时间的推移，这些数据可能会逐渐“过时”，无法反映数据库中最新的信息。例如，用户在个人信息页面修改了某些数据，如个人资料图片的 URL，但由于 JWT Token 中仍然保留着旧的 URL，这就可能导致在某些情况下，系统仍然使用旧的、不准确的信息进行操作，从而出现数据不一致的情况。更为严重的是，如果涉及到权限相关的信息，例如用户原本具有管理员权限，但后来该权限被撤销，然而由于无法销毁之前颁发的具有管理员权限的 Token，系统可能仍然会认为该用户具有管理员权限，这就可能导致严重的安全漏洞，除非关闭整个系统，否则很难完全避免这种情况的发生。（五）实现库缺乏生产环境验证或压根不存在在考虑使用 JWT 作为 Session 机制时，还需要关注其实现库的成熟度和稳定性问题。许多人可能认为上述提到的问题主要是针对无状态 JWT，但实际上，即使使用有状态 Tokens，虽然在功能上与传统的 Session cookies 大致相当，但目前市场上缺乏经过大量生产环境验证的成熟实现库。相比之下，现有的许多传统 Session 实现（例如适用于 Express 的 express-sessionhttps://github.com/expressjs/sessio）已经在生产环境中使用了多年，其安全性和稳定性经过了大量实际项目的检验和改良，开发者可以放心地使用这些成熟的实现，并且能够受益于社区的持续支持和更新。而如果选择使用 JWT 作为 Session cookies 的临时替代品，不仅无法享受到这些成熟实现带来的好处，而且还需要自行不断改进和完善其实现细节，这在过程中很容易引入新的漏洞和问题。即使选择使用第三方的 JWT 实现库，由于这些库尚未在真实世界中得到广泛的应用和验证，仍然存在一定的风险和不确定性。例如，Laravel Passport 虽然采用了类似“有状态 JWT”的方式来存储 OAuth Access Token，但幸运的是，Passport 已经在一些实际应用中得到了验证，并且不完全依赖于 JWT 的原生特性，而是结合了其他成熟的技术和机制来保障安全性和稳定性。五、结论综上所述，无状态 JWT Tokens 在作为 Session 机制时存在诸多无法忽视的问题，如无法单独销毁或更新、可能导致长度限制问题以及潜在的安全隐患等。而有状态 JWT Tokens 虽然在功能上与传统的 Session cookies 相似，但由于缺乏广泛的生产环境验证、经过大量 Review 的成熟实现以及良好的客户端支持，其在实际应用中的可靠性和稳定性仍然存在一定的疑虑。因此，除非是在像 BAT 这样规模庞大、具有特殊技术需求和挑战的公司，对于大多数普通的项目和开发者来说，并没有充分的理由选择使用 JWT 作为 Session 机制。在实际开发中，建议仍然优先选择传统的、经过长期实践验证的 Session 机制，这样能够更好地保障系统的安全性、稳定性和可维护另外，近期才哥整理出了一个可用于快速刷面试题的小程序，其中收录了常见面试题及其答案，涵盖了基础、并发、JVM、MySQL、Redis、Spring、SpringMVC、SpringBoot、SpringCloud、消息队列等多个类型，感兴趣的可以点击下方试试
上100个免费API接口，调用完全不限次数，以后会用得着！
在当今数字化时代，API 接口为开发者们提供了丰富多样的功能拓展途径。以下整理了来自聚合数据、API Store 以及其他平台的一些免费且无次数限制的 API 接口，助力开发者们快速构建各类创新应用。一、聚合数据平台免费 API 接口（一）信息查询类手机号码归属地 API 接口：可精准查询手机号码所属地区等信息。接口地址：https://www.juhe.cn/docs/api/id/11历史上的今天 API 接口：获取历史上特定日期的重大事件等内容。接口地址：https://www.juhe.cn/docs/api/id/63身份证查询 API 接口：查询身份证相关信息，如归属地、出生日期等。接口地址：https://www.juhe.cn/docs/api/id/38邮编查询接口：通过地址或地区查询对应的邮编。接口地址：https://www.juhe.cn/docs/api/id/66IP 地址接口：查询 IP 地址的相关信息，如地理位置等。接口地址：https://www.juhe.cn/docs/api/id/1（二）金融财经类股票数据 API 接口：获取股票行情、交易数据等信息。接口地址：https://www.juhe.cn/docs/api/id/21基金财务数据接口：提供基金的财务状况等数据。接口地址：https://www.juhe.cn/docs/api/id/28汇率 API 接口：查询不同货币之间的汇率信息。接口地址：https://www.juhe.cn/docs/api/id/80（三）生活娱乐类星座运势接口：提供星座每日、每周等运势信息。接口地址：https://www.juhe.cn/docs/api/id/58笑话大全 API 接口：获取丰富多样的笑话资源。接口地址：https://www.juhe.cn/docs/api/id/95周公解梦接口：解析梦境含义，提供周公解梦相关信息。接口地址：https://www.juhe.cn/docs/api/id/64电影票房接口：查询电影票房数据，了解影片市场表现。接口地址：https://www.juhe.cn/docs/api/id/44（四）科技数据类语音识别接口：实现语音到文字的转换功能。接口地址：https://www.juhe.cn/docs/api/id/134成语词典接口：查询成语的释义、出处等信息。接口地址：https://www.juhe.cn/docs/api/id/157问答机器人接口：进行智能问答交互。接口地址：https://www.juhe.cn/docs/api/id/112（五）其他实用类全国 WIFI 接口：获取全国范围内的 WIFI 相关信息。接口地址：https://www.juhe.cn/docs/api/id/18网站安全检测接口：检测网站的安全性状况。接口地址：https://www.juhe.cn/docs/api/id/19手机固话来电显示接口：识别手机或固话来电的相关信息。接口地址：https://www.juhe.cn/docs/api/id/72新闻头条接口：获取各类新闻头条信息。接口地址：https://www.juhe.cn/docs/api/id/235万年历 API 接口：提供万年历相关的日期、节气等信息。接口地址：https://www.juhe.cn/docs/api/id/177NBA 赛事接口：查询 NBA 赛事的赛程、比分等信息。接口地址：https://www.juhe.cn/docs/api/id/92二、API Store 平台免费 API 接口（一）信息查询类IP 地址查询：接口地址：http://apistore.baidu.com/apiworks/servicedetail/114.html身份证查询：接口地址：http://apistore.baidu.com/apiworks/servicedetail/113.html手机号码归属地：接口地址：http://apistore.baidu.com/apiworks/servicedetail/794.html（二）媒体资讯类频道新闻 API_易源：获取特定频道的新闻资讯。接口地址：http://apistore.baidu.com/apiworks/servicedetail/688.html微信热门精选：获取微信热门文章等内容。接口地址：http://apistore.baidu.com/apiworks/servicedetail/632.html体育新闻：查询体育领域的新闻报道。接口地址：http://apistore.baidu.com/apiworks/servicedetail/711.html科技新闻：获取科技行业的新闻消息。接口地址：http://apistore.baidu.com/apiworks/servicedetail/1061.html国际新闻：关注国际范围内的新闻动态。接口地址：http://apistore.baidu.com/apiworks/servicedetail/823.html（三）娱乐资源类美女图片：获取各类美女图片资源。接口地址：http://apistore.baidu.com/apiworks/servicedetail/720.html音乐搜索：进行音乐资源的搜索查询。接口地址：http://apistore.baidu.com/apiworks/servicedetail/1020.html笑话大全：提供丰富的笑话内容。接口地址：http://apistore.baidu.com/apiworks/servicedetail/864.html（四）智能交互类图灵机器人：实现智能对话交互功能。接口地址：http://apistore.baidu.com/apiworks/servicedetail/736.html语音合成：将文字转换为语音输出。接口地址：http://apistore.baidu.com/apiworks/servicedetail/867.html（五）生活服务类天气查询：查询当地或指定地区的天气信息。接口地址：http://apistore.baidu.com/apiworks/servicedetail/112.html中国和世界天气预报：提供更广泛范围的天气预报服务。接口地址：http://apistore.baidu.com/apiworks/servicedetail/478.html去哪儿网火车票：查询火车票相关信息。接口地址：http://apistore.baidu.com/apiworks/servicedetail/697.html去哪儿景点门票查询：查询景点门票价格、预订等信息。接口地址：http://apistore.baidu.com/apiworks/servicedetail/140.html天狗健康菜谱：获取健康饮食菜谱信息。接口地址：http://apistore.baidu.com/apiworks/servicedetail/987.html天狗药品查询：查询药品相关信息。接口地址：http://apistore.baidu.com/apiworks/servicedetail/916.html天狗医院大全：查询医院相关信息。接口地址：http://apistore.baidu.com/apiworks/servicedetail/988.html（六）文本处理类pullword 在线分词服务：对文本进行分词处理。接口地址：http://apistore.baidu.com/apiworks/servicedetail/143.html汉字转拼音：将汉字转换为拼音形式。接口地址：http://apistore.baidu.com/apiworks/servicedetail/1124.html三、其他平台免费 API 接口（一）社交文化类豆瓣开放：提供豆瓣平台相关的 API 服务，如书籍、电影等数据查询。接口地址：https://developers.douban.com/wiki/?title=guide（二）电商平台类淘宝开放平台：可进行淘宝商品数据查询、交易接口调用等（需遵循平台规则）。接口地址：http://open.taobao.com/?spm=a219a.7395905.1.1.YdFDV6（三）语音技术类图灵语音：提供语音交互相关的 API 功能。接口地址：http://www.tuling123.com/help/h_cent_andriodsdk.jhtml?nav=doc讯飞语音：具备语音识别、合成等丰富的语音 API 服务。接口地址：http://www.xfyun.cn/robots/solution（四）即时通讯类融云 IM：实现即时通讯功能的 API 接口，适用于社交、客服等应用场景。接口地址：https://developer.rongcloud.cn/signin?returnUrl=%2Fapp%2Fappkey%2FPv4vYQwaxSZdfpLX5AI%3D（五）地图导航类高德地图：提供地图定位、导航、地理信息查询等 API 服务。接口地址：http://lbs.amap.com/（六）音频平台类蜻蜓：可获取音频内容相关的 API 资源，如电台节目等。接口地址：http://open.qingting.fm这些免费的 API 接口涵盖了众多领域，开发者们可以根据自身项目需求灵活选用，为应用开发增添更多功能与魅力，降低开发成本与周期，加速创新应用的落地实施。 另外，近期才哥整理出了一个可用于快速刷面试题的小程序，其中收录了常见面试题及其答案，涵盖了基础、并发、JVM、MySQL、Redis、Spring、SpringMVC、SpringBoot、SpringCloud、消息队列等多个类型，感兴趣体验下方小程序
Java基础面试题—— 包装类型和基本类型的区别是什么？
面试回答重点基本类型 ：Java 中有 8 种基本数据类型（int、long、float、double、char、byte、boolean、short），它们是直接存储数值的变量，位于栈上（局部变量），性能较高，且不支持 null。包装类型 ：每个基本类型都有一个对应的包装类型（Integer、Long、Float、Double、Character、Byte、Boolean、Short）。包装类型是类，存储在堆中，可以用于面向对象编程，并且支持 null。区别总结：（1）性能区别：基本类型：占用内存小，效率高，适合频繁使用的简单操作。包装类型：因为是对象，涉及内存分配和垃圾回收，性能相对较低。（2）比较方式不同：基本类型 ：比较用 ==，直接比较数值。包装类型 ：比较时，== 比较的是对象的内存地址，而 equals() 比较的是对象的值。（3）默认值不同：基本类型：默认值是 0，false 等。包装类型：默认为 null。（4）初始化的方式不同：基本类型：直接赋值。包装类型：需要采用 new 的方式创建。（5）存储方式不同：基本类型：如果是局部变量则保存在栈上面，如果是成员变量则在堆中。包装类型：保存在堆上（成员变量，在不考虑 JIT 优化的栈上分配时，都是随着对象一起保存在堆上的）。扩展知识自动装箱与拆箱Java 是一种面向对象语言，很多地方都需要使用对象而不是基本数据类型。比如，在集合类中，我们是无法将int 、double等类型放进去的。因为集合的容器要求元素是 Object类型。为了让基本类型也具有对象的特征，就出现了包装类型，它相当于将基本类型"包装起来"，使得它具有了对象的性质，并且为其添加了属性和方法，丰富了基本类型的操作。装箱：基本类型自动转换为包装类型对象。拆箱：包装类型对象自动转换为基本类型的值。缓存机制包装类型中的 Byte、Short、Integer 和 Long 对某些范围内的值（例如 Integer 缓存 -128 到 127）会使用对象缓存来提升性能。因此，同一数值的包装类型对象可能是同一个实例。例如： Integer a = 100; Integer b = 100; System.out.println(a == b); // true Integer c = 200; Integer d = 200; System.out.println(c == d); // false 基础类型与包装类长度和范围分类基本数据类型包装类长度表示范围布尔型booleanBoolean// byteByte1 字节-128 ~ 127 shortShort2 字节-32768 ~ 32767整型intInteger4 字节-2,147,483,648 ~ 2,147,483,647 longLong8 字节-9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807字符型charCharacter2 字节Unicode 字符集中的任何字符浮点型floatFloat4 字节约 -3.4E38 ~ 3.4E38 doubleDouble8 字节约 -1.7E308 ~ 1.7E308另外，近期才哥整理出了一个可用于快速刷面试题的小程序，其中收录了常见面试题及其答案，涵盖了基础、并发、JVM、MySQL、Redis、Spring、SpringMVC、SpringBoot、SpringCloud、消息队列等多个类型。可以看下方小程序试试
Java基础面试题 ——参数传递是按值还是按引用？
回答重点在 Java 中，参数传递只有按值传递，不论是基本类型还是引用类型。基本数据类型 （如 int, char, boolean 等）：传递的是值的副本，即基本类型的数值本身。因此，对方法参数的任何修改都不会影响原始变量。引用数据类型（如对象引用）：传递的是引用的副本，即对象引用的内存地址。因此，方法内可以通过引用修改对象的属性，但不能改变引用本身，使其指向另一个对象。扩展知识基本类型与引用类型的区别基本类型 ：包括 int, float, double, char, boolean 等，存储在栈内存中。方法中对基本类型参数的操作只会影响传递的副本，原始变量的值不受影响。引用类型 ：包括所有的对象和数组，引用类型的变量存储的是对象在堆内存中的地址。当引用类型作为参数传递时，传递的是这个地址的副本。因此，方法内的修改可以影响到传入的对象的内容，但不会影响对象引用本身的地址。示例代码分析 public class ParameterPassing { public static void main(String[] args) { int a = 5; modifyPrimitive(a); System.out.println("After modifyPrimitive: " + a); // 输出: 5 MyObject obj = new MyObject(); obj.value = 10; modifyObject(obj); System.out.println("After modifyObject: " + obj.value); // 输出: 20 resetReference(obj); System.out.println("After resetReference: " + obj.value); // 输出: 20 } public static void modifyPrimitive(int num) { num = 10; // 仅仅修改了副本，不影响原始变量 } public static void modifyObject(MyObject obj) { obj.value = 20; // 修改了对象的属性，会影响原始对象 } public static void resetReference(MyObject obj) { obj = new MyObject(); // 修改的是引用的副本，不影响原始对象 obj.value = 30; } } class MyObject { int value; } 在 modifyPrimitive 方法中，num 是基本类型的副本，因此对它的修改不影响原始变量 a。在 modifyObject 方法中，obj 是引用类型的副本，但这个副本仍指向原始对象，因此修改 value 属性会影响原始对象。在 resetReference 方法中，obj 被重新赋值为一个新对象，这个变化只影响副本，不影响原始引用。另外，近期才哥整理出了一个可用于快速刷面试题的小程序，其中收录了常见面试题及其答案，涵盖了基础、并发、JVM、MySQL、Redis、Spring、SpringMVC、SpringBoot、SpringCloud、消息队列等多个类型，感兴趣的微信扫码体验
Java中String、StringBuffer 和 StringBuilder有什么区别？
面试回答重点这三个都是 Java 中处理字符串的类，区别主要体现在可变性 、线程安全性 和性能上一、各自特征（1）String不可变 ：String 是不可变类，字符串一旦创建，其内容无法更改。每次对 String 进行修改操作（如拼接、截取等），都会创建新的 String 对象。适合场景 ：String 适用于字符串内容不会频繁变化的场景，例如少量的字符串拼接操作或字符串常量。（2）StringBuffer可变 ：StringBuffer 是可变的，可以进行字符串的追加、删除、插入等操作。线程安全 ：StringBuffer 是线程安全的，内部使用了 synchronized 关键字来保证多线程环境下的安全性。适合场景 ：StringBuffer 适用于在多线程环境中需要频繁修改字符串的场景。（3）StringBuilder可变 ：StringBuilder 也是可变的，提供了与 StringBuffer 类似的操作接口。非线程安全 ：StringBuilder 不保证线程安全，性能比 StringBuffer 更高。适合场景 ：StringBuilder 适用于单线程环境中需要大量修改字符串的场景，如高频拼接操作。二、总结String：不可变，适合少量字符串操作。StringBuffer：可变且线程安全，适合多线程环境中的频繁字符串修改。StringBuilder：可变且非线程安全，适合单线程环境中的高性能字符串处理。扩展知识在 Java 8 及以后，编译器会对字符串的常量拼接做优化，将字符串拼接转换为 StringBuilder 操作。这种优化提高了代码性能，但是在动态拼接或多线程场景下，手动使用 StringBuilder 和 StringBuffer 仍然更合适。String 是 Java 中基础且重要的类，并且 String 也是 Immutable 类的典型实现，被声明为 final class，除了 hash 这个属性其它属性都声明为 final。因为它的不可变性，所以例如拼接字符串时候会产生很多无用的中间对象，如果频繁的进行这样的操作对性能有所影响。StringBuffer 就是为了解决大量拼接字符串时产生很多中间对象问题而提供的一个类 ，提供 append 和 insert 方法，可以将字符串添加到已有序列的末尾或指定位置。它的本质是一个线程安全的可修改的字符序列，把所有修改数据的方法都加上了 synchronized。但是保证了线程安全是需要性能的代价的。在很多情况下我们的字符串拼接操作不需要线程安全，这时候StringBuilder登场了，StringBuilder是JDK1.5发布的，它和 StringBuffer 本质上没什么区别，就是去掉了保证线程安全的那部分，减少了开销。所以如果我们有大量的字符串拼接，如果能预知大小的话最好在 new StringBuffer 或者 StringBuilder 的时候设置好 capacity，避免多次扩容的开销（扩容要抛弃原有数组，还要进行数组拷贝创建新的数组）。另外，近期才哥整理出了一个可用于快速刷面试题的小程序，其中收录了常见面试题及其答案，涵盖了基础、并发、JVM、MySQL、Redis、Spring、SpringMVC、SpringBoot、SpringCloud、消息队列等多个类型，感兴趣的可以点击下方扫码试试