注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

生产环境到底能用Docker部署MySQL吗?

程序员小李:“老王,我有个问题想请教您。MySQL 能不能部署在 Docker 里?我听说很多人说不行,性能会有瓶颈。” 架构师老王:“摸摸自己光突突的脑袋, 小李啊,这个问题可不简单。以前确实很多人说不行,但现在技术发展这么快,情况可能不一样了。” 小李:“...
继续阅读 »

程序员小李:“老王,我有个问题想请教您。MySQL 能不能部署在 Docker 里?我听说很多人说不行,性能会有瓶颈。”


架构师老王:“摸摸自己光突突的脑袋, 小李啊,这个问题可不简单。以前确实很多人说不行,但现在技术发展这么快,情况可能不一样了。”


小李:“那您的意思是,现在可以了?”


老王:“也不能这么说。性能、数据安全、运维复杂度,这些都是需要考虑的。不过,已经有不少公司在生产环境里用 Docker 跑 MySQL 了,效果还不错。”


Docker(鲸鱼)+MySQL(海豚)到底如何,我们来具体看看:



一、业界大厂


我们来看看业界使用情况:


1.1、京东超70%的MySQL在Docker中



刘风才是京东的资深数据库专家,他分享了京东在MySQL数据库Docker化方面的实践经验。京东从最初的小规模使用,到现在超过70%的MySQL数据库运行在Docker容器中


当然京东也不是所有的业务都适合把 mysql 部署在 docker 容器中。比如,


刘风才演讲中也提出:数据文件多于1T多的情况下是不太合适部署在Docker上的;再有就是在性能上要求特别高的,特别重要的核心系统目前仍跑在物理机上,后面随着Docker技术不断的改进,会陆续地迁到Docker上。


1.2、 同程艺龙:大规模 MySQL 容器化实践



同程艺龙的机票事业群 CTO 王晓波在QCon北京2018大会上做了《MySQL的Docker容器化大规模实践》的主题演讲。他分享了同程艺龙如何大规模实践基于Docker的MySQL私有云平台,集成了高可用、快速部署、自动化备份、性能监控等多项自动化运维功能。该平台支撑了总量90%以上的MySQL服务(实际数量超过2000个),资源利用率提升了30倍,数据库交付能力提升了70倍,并经受住了业务高峰期的考验。


当然不仅仅是京东、同程像阿里云、腾讯、字节、美团等都有把 Mysql 部署在 Docker 容器中的案例。


二、官方情况


MySql 官方文档提供了 mysql 的 docker 部署方式,文档中并没有明确的表明这种方式是适用于开发、测试或生产。那就是通用性的,也就是说生产也可以使用


以下就是安装的脚本可以看到配置文件和数据都是挂载到宿主机上。


docker run --name=mysql1 \
--mount type=bind,src=/path-on-host-machine/my.cnf,dst=/etc/my.cnf \
--mount type=bind,src=/path-on-host-machine/datadir,dst=/var/lib/mysql \
-d container-registry.oracle.com/mysql/community-server:tag

再看看镜像文件,可以看到 oralce 官方 7 年前就发布了 mysql5.7 的镜像。



三、具体分析


反方观点:生产环境MySQL不该部署在Docker里


反方主要担心数据持久化、性能、复杂性、备份恢复和安全性等问题,觉得在Docker里跑MySQL风险挺大。


正方观点:生产环境MySQL可以部署在Docker里


正方则认为Docker的灵活性、可移植性、资源隔离、自动化管理以及社区支持都挺好,生产环境用Docker部署MySQL是可行的,而且有成熟的解决方案来应对数据持久化和性能等问题。


总结


争议的焦点主要在于Docker容器会不会影响性能。其实 Docker和虚拟机不一样,虚拟机是模拟物理机硬件,而Docker是基于Linux内核的cgroups和namespaces技术,实现了CPU、内存、网络和I/O的共享与隔离,性能损失很小。



Docker 和传统虚拟化方式的不同之处,在于 Docker 是在操作系统层面上实现虚拟化,直接复用本地主机的操作系统,而传统方式则是在硬件层面实现。


Docker的特点:



  • 轻量级:共享宿主机内核,启动快,资源占用少。

  • 隔离性:容器之间相互隔离,不会互相干扰。

  • 可移植性:容器可以在任何支持Docker的平台上运行,不用改代码。


四、结尾


Docker虚拟化操作系统而不是硬件


随着技术的发展,Docker在数据库部署中的应用可能会越来越多。


所以,生产环境在Docker里部署MySQL,虽然有争议,但大厂都在用,官方也支持,技术也在不断进步,未来可能是个趋势。


我是栈江湖,如果你喜欢此文章,不要忘记点赞+关注


作者:栈江湖
来源:juejin.cn/post/7497057694530502665
收起阅读 »

Spring之父:自从我创立了 Spring Framework以来,我从未如此确信需要一个新项目

大家好,这里是小奏,觉得文章不错可以关注公众号小奏技术 Spring框架之父再出发:发布JVM智能体框架Embabel,赋能企业级AI应用 当今,人工智能的浪潮正以前所未有的势头席卷技术世界,Python 凭借其强大的生态系统成为了AI开发的“通用语”。 然...
继续阅读 »

大家好,这里是小奏,觉得文章不错可以关注公众号小奏技术



Spring框架之父再出发:发布JVM智能体框架Embabel,赋能企业级AI应用


当今,人工智能的浪潮正以前所未有的势头席卷技术世界,Python 凭借其强大的生态系统成为了AI开发的“通用语”。


然而,Spring 框架的创始人Rod Johnson 却发出了不同的声音。


”自从我创立 Spring 框架以来,我从未如此坚信一个新项目的必要性。自从我开创了依赖注入(Dependency Injection)和其他 Spring 核心概念以来,我从未如此坚信一种新编程模型的必要性,也从未如此确定它应该是什么样子“


为此,他亲手打造并开源了一个全新的项目——Embabel:一个为 JVM 生态量身定制的 AI 智能体(Agent)框架


我们为什么需要一个智能体框架


难道大型语言模型(LLM)还不够聪明,无法直接解决我们的问题吗?难道多聊天协议(MCP)工具不就是我们让它们解决复杂问题所需要的一切吗?


不。MCP 是向前迈出的重要一步,Embabel 自然也拥抱它,就像它让使用多模型变得简单一样。


但是,我们需要一个更高级别的编排技术,尤其是对于业务应用程序,原因有很多。以下是一些最重要的原因



  • 可解释性(Explainability): 在解决问题时,选择是如何做出的?

  • 可发现性(Discoverability): MCP 绕开了这个重要问题。我们如何在每个节点找到正确的工具,并确保模型在它们之间进行选择时不会混淆?

  • 混合模型的能力(Ability to mix models): 这样我们就不用依赖于“上帝模型”,而是可以为许多任务使用本地的、更便宜的、私有的模型。

  • 在流程的任何节点注入“护栏”(guardrails)的能力。

  • 管理流程执行并引入更高弹性的能力。

  • 大规模流程的可组合性(Composability)。 我们很快将看到的不仅是在一个系统上运行的智能体,而是智能体的联邦。

  • 与敏感的现有系统(如数据库)进行更安全的集成,在这些地方,即使是最好的 LLM,给予其写权限也是危险的。


这些问题在企业环境中尤为突出,它们需要的不是一个简单的问答机器人,而是一个可解释、可控制、可组合且足够安全的高级编排系统。这正是智能体框架的价值所在。


为什么是JVM,而不是Python?


PythonAI 研究和数据科学领域地位稳固,但 GenAI 的核心是连接与整合。当我们构建企业级 AI 应用时,真正的挑战在于如何将 AI 能力与数十年积累的、运行在 JVM 上的海量业务逻辑、基础设施和数据无缝对接。


在企业应用开发、复杂系统构建和关键业务逻辑承载方面,JVM 生态(Java/Kotlin)拥有无与伦比的优势和成熟度。因此,与其让业务逻辑去追赶 AI 技术栈,不如让 AI 技术栈主动融入业务核心——JVM。


Embabel:为超越而生的下一代智能体框架


Embabel 的目标并非简单地追赶 Python 社区的同类框架,而是要实现跨越式超越。它带来了几个革命性的特性:



  1. 确定性的智能规划:Embabel 创新地引入了非 LLM 的 AI 规划算法。它能自动从你的代码中发现可用的“能力”和“目标”,并根据用户输入智能地规划出最优执行路径。这意味着你的系统是可扩展的,增加新功能不再需要重构复杂的逻辑,同时整个规划过程是确定且可解释的。

  2. 类型安全的领域模型:Embabel 鼓励开发者使用 Kotlin data classJava record 构建丰富的领域模型。这使得与 LLM 交互的提示(Prompt)变得类型安全、易于工具检查和代码重构,从根本上提升了代码质量和可维护性。

  3. Spring无缝集成:EmbabelKotlin 构建,并承诺为 Java 开发者提供同等一流的体验。更重要的是,它与 Spring 框架深度集成。对于数百万 Spring 开发者来说,构建一个 AI 智能体将像开发一个 REST API 一样自然、简单。


加入我们,共创未来


对于JVM 开发者来说,这是一个激动人心的时代。Embabel 提供了一个绝佳的机会,让你可以利用自己早已熟练掌握的技能,为你现有的 Java/Kotlin 应用注入强大的 AI 能力,从而释放巨大的商业价值。


项目尚在早期,但蓝图宏大。Embabel 的目标是成为全球最好的智能体平台。现在就去 GitHub 关注 Embabel,加入社区,贡献你的力量,一同构建企业级 AI 应用的未来。


参考



作者:小奏技术
来源:juejin.cn/post/7507438828178849828
收起阅读 »

这篇 Git 教程太清晰了,很多 3 年经验程序员都收藏了

引言 📌 Git 是现代开发中不可或缺的版本控制工具,尤其适用于团队协作和代码管理。本文将带你了解 Git 的基础操作命令,包括 git init、git add、git commit、git diff、git log、.gitignore 等,快速上手版本控...
继续阅读 »

引言


📌 Git 是现代开发中不可或缺的版本控制工具,尤其适用于团队协作和代码管理。本文将带你了解 Git 的基础操作命令,包括 git initgit addgit commitgit diffgit log.gitignore 等,快速上手版本控制。




🛠️ 一、初始化仓库:git init


使用 Git 前,需先初始化一个本地仓库:


git init

执行后会在当前目录生成一个 .git 文件夹,Git 会在此目录下跟踪项目的变更记录。




👤 二、配置用户信息


首次使用 Git 时,推荐设置用户名和邮箱:


git config --global user.name "xxxxx"
git config --global user.email "xxxx@qq.com"

加上 --global 会全局生效,仅对当前项目配置可以省略该参数。




📦 三、代码暂存区(Staging Area)是什么?


Git 的提交操作分为两个阶段:暂存(staging)提交(commit)



  • 当你修改了文件,Git 并不会立即记录这些改动;

  • 你需要先使用 git add 命令,把改动“放进暂存区”,告诉 Git:“这些改动我准备好了,可以提交”;

  • 然后再使用 git commit 将暂存区的内容提交到本地仓库,记录为一个快照。



🧠 可以把暂存区类比为“快照准备区”,你可以反复修改文件、添加到暂存区,最后一口气提交,确保每次提交都是有意义的逻辑单元。



🎯 举个例子:


# 修改了 index.html 和 style.css
git add index.html # 把 index.html 放入暂存区
git add style.css # 再把 style.css 放入暂存区
git commit -m "更新首页结构和样式" # 一起提交

💡 小贴士:你可以分批使用 git add 管理暂存内容,按逻辑分组提交更利于协作和回溯。




📝 四、查看当前状态:git status


在进行任何修改之前,查看当前仓库的状态是非常重要的。git status 是最常用的命令之一,能让你清楚了解哪些文件被修改了,哪些文件已加入暂存区,哪些文件未被跟踪。


git status

它的输出通常会分为三部分:



  1. 已暂存的文件:这些文件已使用 git add 添加到暂存区,准备提交。

  2. 未暂存的文件:这些文件被修改,但还未添加到暂存区。

  3. 未跟踪的文件:这些文件是新创建的,Git 并未跟踪它们。


例如:


On branch main
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
modified: index.html
new file: style.css

Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: app.js

🎯 通过 git status,你可以随时了解当前工作区和暂存区的状态,帮助你决定接下来的操作。




📥 五、添加文件到暂存区:git add


当你修改或新增文件后,使用 git add 将其添加到 Git 的暂存区:


git add 文件名

也可以批量添加所有修改:


git add .



💾 六、提交更改:git commit -m


将暂存区的内容提交至本地仓库:


git commit -m "提交说明"

-m 后面是提交信息,建议语义清晰,例如:


git commit -m "新增用户登录功能"



🚀 七、推送到远程仓库:git push origin main


本地提交之后,需要推送代码到远程仓库(如 GitHub、Gitee):


git push origin main


  • origin 是默认的远程仓库别名;

  • main 是目标分支名(如果你使用的是 master,请替换);



✅ 提交后远程成员就可以拉取(pull)你最新的修改了。



🔗 如果你还没有远程仓库,请先去 GitHub / Gitee 创建一个,然后关联远程仓库地址:


git remote add origin https://github.com/yourname/your-repo.git



🕵️ 八、查看文件改动:git diff


commit 之前,可用 git diff 查看修改内容:


git diff



📜 九、查看提交历史:git log --oneline


快速查看历史提交记录:


git log --oneline

输出示例:


e3f1a1b 添加登录功能
2c3d9a7 初始提交



🛑 十、忽略某些文件:.gitignore


在项目中,有些文件无需提交到 Git 仓库,例如缓存、编译结果、配置文件等。使用 .gitignore 文件可忽略这些文件:


# 忽略 node_modules 文件夹
node_modules/

# 忽略所有 .log 文件
*.log

# 忽略 .env 环境变量文件
.env



🌿 十一、重命名默认分支:git branch -M main


很多平台(如 GitHub)推荐使用 main 作为主分支名称:


git branch -M main

这样可以将默认分支由 master 改为 main




✅ 总结命令一览表


命令作用
git init初始化仓库
git config设置用户名与邮箱
git status查看当前文件状态
git add添加改动到暂存区
git commit -m提交改动
git push origin main推送代码到远程 main 分支
git diff查看未提交的改动
git log --oneline查看提交历史
.gitignore忽略文件
git branch -M main重命名分支为 main



🧠 写在最后


Git 是每个开发者都必须掌握的技能之一。掌握好这些常用命令,就能覆盖 90% 的使用场景。未来如果你要进行多人协作、分支合并、冲突解决,这些基础就是你的武器库。




觉得实用就点个赞、关注、收藏支持一下吧 🙌


作者:小公主
来源:juejin.cn/post/7506776151315922971
收起阅读 »

从热衷到放弃:我的 Cursor 续费终止之路

前言 从我最开始用 Cursor 到现在已经有几个月了,然而随着对它的使用时间越来越长,我感觉帮助反而慢慢变小了,一度我这个月想着不续费了,然后我以为 13 到期、结果却是 7 号到期,所以又自动续费了。 最开始接触 我已经用了 Cursor 好几个月了,而...
继续阅读 »

前言


从我最开始用 Cursor 到现在已经有几个月了,然而随着对它的使用时间越来越长,我感觉帮助反而慢慢变小了,一度我这个月想着不续费了,然后我以为 13 到期、结果却是 7 号到期,所以又自动续费了。


image.png


最开始接触


我已经用了 Cursor 好几个月了,而我最开始用 Cursor 是在好几个月前。


最开始在社交平台上了解到它的功能后,我就很激动了,感觉这也太神了,就一直想体验。但那时也了解到它的价格的何等的贵,所以我开始并没有直接去下载它,而是找了平替 Windsurf


image.png


但别说用了,连注册都注册不了,反复试了几天,也没有注册成功。换手机、换科学上网的不同提供商,都没用。这不,才开始用 Cursor。


无限续杯 之 走到终点


等我用了之后,体验了它的14天免费时间。之后我就觉得它太强了。很多我可以写的功能,它可以以更好的方式,很快的方式生成出来。然而很多我还不会写的。它依然可以去实现。但14天很快就过去了,接着就是一段时间 帐号的删除与注册。但好景不长...


终于在多次反复删除账号又注册。这种操作对我来说已经失效了。


看过了掘金的大多教程,都没有什么用,最后一次有用,但第二天就又不行了。


但那段时间刚巧,我之前在找工作,那几天刚好入职。我就在工作中用了它,有意思的是,我们公司并没有人知道 cursor,甚至没人知道有 ai编辑器。过了几天,无限续杯 也刚好达到极限。


充会员 -> 早下班


当我体验了 Cursor 的威力后,我已经要离不开它了。


我是前端,而 Cursor 可以支持发给它图片,让它画页面。其实画的还挺不错的,这一点就深得我心,所以我痛定思痛充了会员。


image.png


那段时间,我就没加过班、自在的我自己都不知道该如何描述了。但刚开始还是不怎么舒服,因为它老是给我生成用 element-plus 组件直接写的,而我们公司有不少组件是二次封装的,导致我总是要改它。但用了十几次后,它就知道我要什么了。


用了哪些功能 与 自己的感受


Tab 的好与不好


我常用的功能就是tab,主要的是它比较灵活,生成的速度也比较快。而且用的越多它就越可以生成想要的代码。比如项目中自己封装的组件。用 tab 几次之后,它就自己可以去用这个组件,但更多情况下,它生成的会是之前写过的内容。


不过它也有一些缺点,比如:不能去预判一些复杂的思路。如果我们写了一个按钮,并在按钮身上绑定一个 Click 事件,名称叫做 search。Cursor 的 Tab 就可以自动会生成 search函数。但如果你只是在这里写了一个按钮,想要做的功能是导出。你没有在按钮上写导出两个字,也没有去绑定一个 Click事件 叫 export。那 Cursor 根本就不知道你要做的是导出,也就不会去自动实现这些功能。


另一个 Tab 的缺点,那就是影响复制功能。经常准备复制内容时,Tab 就给出了它的预判,然而原本你打算复制10个字,此时它的预判在 10个字中间加了 30个字。你要是想复制,正常就会用鼠标选中字,可一旦你鼠标点下那个位置,Tab就来了。我多次遇到这个问题,如果你没有遇到过,请教我一下方法。


对话模式


对话分两种,一种是全局,一种是局部。


先来说一下全局。


全局对话 cmd + i


image.png


由于 Cursor 默认会将所有文件自动追踪索引。所以当我们进行全局对话时, Cursor 会基于全局所有文件的索引为基础。去修改现在的代码,但如果我们只想改当前一个页面,它依然会去分析全局,增加了要处理的数据量,就导致时间比较长。


不知道是不是我的科学上网工具问题,我几乎只要用全局问答,就要好几分钟,要是改错了,又要重来,所以现在几乎就不用了。


另一个是后面代码变多了,时间就更长,而且它老是给我优化我不要优化的,因为它经常优化错了。比如关于接口的 type,我都是在 api 文件夹中定义的。但它总说在那个文件中没有这个 type,然后就自动在当前文件附近又创建 types.ts ,然后声明的类型和接口都不是对应的。


当然了,它的好处是分析的全面、如果要跨多个文件修改同一个功能,则它再慢,也得等着。


之后我就又想起了 局部 对话


局部对话 cmd + k


image.png


我是上段时间才开始用这个的,因为全局的太慢了,就突然想起来还有局部的 cmd ➕ k 。这个还不错,我最初是用来写 API 数据的。


因为我们是用 ApiPost,我就直接在左边接口标题处,点击复制,然后进代码,在局部问答中发给他,然后说,写出接口和类型。基本没出过错。


用了几天后也发现它的局限性了,就是它貌似只能在问的位置下方生成,如果我要它跨越几个地方添加就没用了。如:在template中生成页面展示的,在JS中生成脚本,在style中生成样式。


但之后发现这种方式不仅能生成,如果你选中了内容,它还能修改。然后我就随机一动,直接全选当前文件,则实现了对一整个文件的局部修改。但说实话,速度也并没有太快。


cursorrules


后来我又加了cursorrules,最初我以为只能用一个rules文件,直到在一个微信群里看见别人分享的照片,他有6个左右的rules。之后我就用了两天时间自己写了4个rules。但经常没有效果,而且还开启了always。


image.png


之后,我就在开始的位置写上这样这句话:


自动激活

这些指令在本项目的所有对话中自动生效。当使用到该 rules 时,要打印出这个rules的名字,如"使用了 项目规则.mdc 文件",让我知道你使用了这个文件。


之后有一次就突然出现了这句话


image.png


可是,只出现过这个 项目规则.mdc ,其它的mdc 都没有出现过,但其它的文件中 我也写了类型的 自动激活的话。不知道为什么没有生效。


MCP


server-sequential-thinking


MCP 之前使用过,那时主要火的是 server-sequential-thinking, 它的主要功能是思维更有条理。如果你在对话中 说了类似 " 思考 " 的话,那就会激活它。之后它就一句话一句话的分析,也一句一句的解释。因为工作中比较少的有这么有深度的思考,我几乎没用过它。而且用了它之后,话也变多了,导致效率也慢,外加 科学上网 的工具并不好,就更慢了。 上段时间我又开始使用它了,但一直没生效,不知道为啥?


playwright 自动化测试


用这个可能比较复杂,其实我就是希望 Cursor 可以自己调接口,然后根据 api 文件中的 对接口的声明、参数类型与返回类型。自动帮我实现 增删改查 ,如果一个表单,我的字段写错了,它就自动修改,然后继续填写数据再调接口。直到跑通为止。 因为这确实很费时间,也没意思。但至今也没有做到。


browser-tool-mcp


这个是用来让 Cursor 监控浏览器,它可以查看浏览器的 控制台、DOM 结构 等等,但用了一段时间后,发现直接把 控制台的报错 发给 Cursor 更快,也就没怎么用了。


结语


上面 MCP 用的不怎么好的一个原因,是因为没有打通 自动化的流程,所以总是需要我手动的操作。


这个星期打算把 claude 的提示词看一下,看看能不能改善一下 Cursor 的使用情况。


作者:自学前端_又又
来源:juejin.cn/post/7501966297334497290
收起阅读 »

Android 16 适配重点全解读 | OPPO 技术专场直播回顾

5月22日,OPPO举办「OTalk | Android 16 开发者交流专场」,特邀OPPO高级工程师团队深度解读Android 16核心技术要点与适配策略。活动以线上直播形式展开,吸引了众多开发者实时观看并参与讨论,为他们提供了从技术解析到工具支持的全流程...
继续阅读 »

5月22日,OPPO举办「OTalk | Android 16 开发者交流专场」,特邀OPPO高级工程师团队深度解读Android 16核心技术要点与适配策略。活动以线上直播形式展开,吸引了众多开发者实时观看并参与讨论,为他们提供了从技术解析到工具支持的全流程适配解决方案。

、Android 16开发者适配计划

根据Google规划,Android 16.0及16.1版本将于2025年分阶段发布,所有应用须在7月1日前完成适配,覆盖目标版本为36(API 36)的新开发及存量应用,涉及行为变更、API 调整和新功能兼容。开发者可尽早启动测试,以免适配延迟对应用上架和用户体验造成不利影响。

配图1.jpeg

、Android 16核心新特性及适配建议

自适应适配:大屏设备体验的优化

随着折叠屏、平板等多样化设备形态的普及,大屏适配已成为开发者面临的重要技术挑战。在 Android 16.0 中,当应用 Target SDK=36 且运行在最小宽度≥600dp 的设备时,系统将忽略传统的屏幕方向、尺寸可调整性等设置限制,为大屏设备带来更出色的视觉体验。

不过,以下三种情况不在新特性的范围内:

  1. 游戏类应用(需要在清单属性中配置 android:appCategory);

  2. 小于 sw600dp 的屏幕(常见手机设备不受影响);

  3. 用户在系统设置中启用了宽高比配置。

适配建议:

遵循谷歌适配指南,完成大屏布局优化,以提供更佳的用户体验;

若暂不支持,可在 Activity 或 Application 节点添加 PROPERTY_COMPAT_ALLOW_RESTRICTED_RESIZABILITY 属性临时豁免,但需注意,该配置可能会在 Android 17 中被取消,因此建议开发者优先完成适配。

针对大屏适配,开发者可以参考由 OPPO、vivo、小米等厂商共同制定的《ITGSA 大屏设备应用适配白皮书 2.0》。同时,建议开发者逐步迁移到 Compose 开发,使后续适配工作更加简单高效。

配图2.jpg

预测性返回:手势导航的交互变革

预测性返回是 Android 13 引入的手势导航增强功能,用户在侧滑返回时可以预览目标界面。在 Android 16 中,目标 SDK≥36 的应用默认启用预测性返回动画,系统不再调用 onBackPressed 也不会再调度 KeyEvent.KEYCODE_BACK。

适配建议:迁移至 onBackInvokedCallback 回调处理返回逻辑;若需保留原有逻辑,可在清单中设置 android:enableOnBackInvokedCallback="false" 停用。

配图3.jpg

ART 内部变更:提升性能与兼容性

Android 16 包含 Android 运行时(ART)的最新更新,这些更新旨在提升 ART 的性能,并支持更多的 Java 功能。依赖 ART 内部结构的代码(如私有反射、非 SDK 接口)将全面失效。

适配建议:全面测试应用稳定性,替换非公开 API 为系统提供的公共 API。

配图4.jpg

JobScheduler 配额优化:后台任务的效率革命

为了降低系统负载,Android 16 对 JobScheduler 的执行配额进行了动态管理,根据应用待机分桶和前台服务状态动态分配 JobScheduler 执行配额,活跃应用获得更多配额,后台任务仍需遵守配额限制。

适配建议:减少非必要后台任务,高优先级任务使用 setExpedited() 标记;通过 WorkInfo.getStopReason() 记录任务终止原因并调整调度策略。

配图5.jpg

健康与健身权限:隐私管控的升级

Android 16 将 BODY_SENSOR 权限迁移至 “健康数据共享” 权限组。对于 Target SDK≥36 的应用,需要请求新的权限。

适配建议:更新权限请求逻辑,引导用户在系统级 “健康数据共享” 页面授权。

配图6.jpg

setImportantWhileForeground 接口失效:后台任务的约束

setImportantWhileForeground 接口曾用于让前台任务豁免后台限制,但从 Android 16 开始,该接口的功能已被彻底移除。依赖此接口的下载任务、实时同步等场景可能出现延迟,影响用户体验。

适配建议:改用 jobInfo.setExpedited() 标记加急任务,确保关键操作优先执行。

配图7.jpg

息屏场景自动停止屏幕分享:隐私与管控的平衡

为提升隐私安全,Android 16会在手机息屏或通话结束后,自动释放 MediaProjection。

适配建议:在 onStop 回调中处理异常,如需持续投屏,需重新获取 MediaProjection 权限。

配图8.jpg

此外,在 Android 16 中,多项关键特性同样值得注意。优雅字体 API 被废弃,开发者需手动调整文字布局以确保显示效果。更安全的 Intent 机制要求显式 Intent 与目标组件的 Intent 过滤器相匹配,提升应用安全性。以进度为中心的通知功能增强,通过Notification.ProgressStyle实现更直观的进度可视化。MediaProvider 扩展了能力,PhotoPicker 支持 PDF 读取并增强权限鉴权,同时统一了界面风格。这些变更体现了 Android 16 在安全性、用户体验和功能上的优化。

配图9.jpg

配图10.jpg

在互动答疑环节,有开发者提出预测性返回动画是否是系统强制的问题,纪昌杰表示预测性返回特性需要应用 targetsdk 升级到 36 才会强制生效,未升级的应用则需通过配置使其生效,应用要主动适配,适配重点在于防止系统不再调用 onBackPressed 和不再调度 KeyEvent.KEYCODE_BACK 导致应用逻辑异常。而对于一个开发人员如何高效适配大屏的问题,纪昌杰再次强调,建议开发者逐步迁移到 Compose 平台开发,以获得谷歌更多支持,开发资源有限的开发者可以参考金标联盟制定的大屏适配 2.0 标准,其内容大多基于 View + XML 开发模式进行指导。

OPPO一站式支持体系

在本次交流专场中,纪昌杰还介绍了 OPPO 为助力 Android 16 适配所构建的一站式开发者支持体系。该体系涵盖了详尽的兼容性适配文档,为开发者提供了清晰明确的适配指引;免费的云真机 / 云测服务,赋能开发者随时随地开展高效调试与验证工作。此外,还包括开发者预览版,便于开发者提前评估应用在新系统上的表现,以及应用商店新特性检测,确保应用完全符合 Android 16 的各项标准。同时,开发者可借助适配答疑交流社群和 OPPO 开放平台支持专区等多元渠道,获取全方位支持,有效提升适配效率。

配图11.jpg

此次「OTalk | Android 16 适配开发者交流专场」聚焦前沿技术洞察与实战指南,开发者提供了系统性适配路径与高效解决方案。活动分享的适配策略、高频问题解答等核心资料,将在「OPPO开放平台」公众号及OPPO开发者社区官网发布,开发者可免费查阅并应用于实际开发流程。

作为Android生态的重要推动者,OPPO将持续提供全链路适配支持服务,并通过技术沙龙、开发者社群及线上交流平台,与开发者紧密协作,共同探索Android 16的创新边界,助力移动应用生态实现高质量演进。

收起阅读 »

个人开发者如何发送短信?这个方案太香了!

还在为无法发送短信验证码而烦恼?今天分享一个超实用的解决方案,个人开发者也能用! 最近国内很多平台暂停了针对个人用户的短信发送,这给个人开发者带来了不少困扰。不过别担心,我发现了一个超实用的解决方案——Spug推送平台,它能很好地满足我们发送短信等需求。 为...
继续阅读 »

还在为无法发送短信验证码而烦恼?今天分享一个超实用的解决方案,个人开发者也能用!



最近国内很多平台暂停了针对个人用户的短信发送,这给个人开发者带来了不少困扰。不过别担心,我发现了一个超实用的解决方案——Spug推送平台,它能很好地满足我们发送短信等需求。


为什么选择这个方案?



  1. 无需企业认证:个人开发者直接可用

  2. 新用户福利:注册即送测试短信

  3. 价格实惠:0.05元/条,按量计费

  4. 接口简单:几行代码就能搞定

  5. 支持丰富:短信、电话、微信、企业微信、飞书、钉钉、邮件等


三步搞定短信发送


第一步:注册账户


打开push.spug.cc,使用微信扫码直接登录,无需繁琐的认证流程。


第二步:创建模板



  1. 点击"消息模板" → "新建"

  2. 输入模版名称

  3. 选择推送通道

  4. 选择短信模板

  5. 选择推送对象

  6. 保存模板


第三步:发送短信


复制模版ID,通过API调用即可发送短信。


发送短信验证码代码示例(多种语言)


Python版(推荐)


import requests

def send_sms(template_id, code, phone):
url = f"https://push.spug.cc/send/{template_id}"
params = {
"code": code,
"targets": phone
}
response = requests.get(url, params=params)
return response.json()

# 使用示例
result = send_sms("abc", "6677", "151xxxx0875")
print(result)

Go版


package main

import (
"fmt"
"net/http"
"io/ioutil"
)

func sendSMS(templateID, code, phone string) (string, error) {
url := fmt.Sprintf("https://push.spug.cc/send/%s?code=%s&targets=%s",
templateID, code, phone)

resp, err := http.Get(url)
if err != nil {
return "", err
}
defer resp.Body.Close()

body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}

return string(body), nil
}

func main() {
result, err := sendSMS("abc", "6677", "151xxxx0875")
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println(result)
}

Java版


import java.net.HttpURLConnection;
import java.net.URL;
import java.io.BufferedReader;
import java.io.InputStreamReader;

public class SMSSender {
public static String sendSMS(String templateId, String code, String phone) throws Exception {
String url = String.format("https://push.spug.cc/send/%s?code=%s&targets=%s",
templateId, code, phone);

URL obj = new URL(url);
HttpURLConnection con = (HttpURLConnection) obj.openConnection();
con.setRequestMethod("GET");

BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()));
String inputLine;
StringBuilder response = new StringBuilder();

while ((inputLine = in.readLine()) != null) {
response.append(inputLine);
}
in.close();

return response.toString();
}

public static void main(String[] args) {
try {
String result = sendSMS("abc", "6677", "151xxxx0875");
System.out.println(result);
} catch (Exception e) {
e.printStackTrace();
}
}
}

使用技巧



  1. 参数说明



    • code:验证码内容

    • targets:接收短信的手机号

    • 使用targets参数会覆盖模板配置的手机号



  2. 最佳实践



    • 选择合适的短信模版

    • 验证手机号格式

    • 管理验证码有效期

    • 添加错误处理

    • 确保账户余额充足




作者:外滩运维专家
来源:juejin.cn/post/7495570300124119052
收起阅读 »

我用AI+高德MCP 10分钟搞定苏州三日游

清明节后回到工作岗位,同事们都在讨论"五一"小长假要去哪里,我悄悄地笑了——作为一名AI玩家,旅行规划这种事,早就甩手给AI工具了!前两天,我用AI+高德地图MCP不到10分钟就搞定了一份详细的苏州三日游攻略,发给朋友们看了规划都惊呆了。"这...这么详细?连...
继续阅读 »

清明节后回到工作岗位,同事们都在讨论"五一"小长假要去哪里,我悄悄地笑了——作为一名AI玩家,旅行规划这种事,早就甩手给AI工具了!

前两天,我用AI+高德地图MCP不到10分钟就搞定了一份详细的苏州三日游攻略,发给朋友们看了规划都惊呆了。

"这...这么详细?连每天天气、门票价格、交通方式都安排好了?"

没错,它全都搞定了!想当年我策划旅行,那可是"人间疾苦":

  • 在小红书翻攻略翻到眼睛发酸
  • 在地图上反复规划路线怀疑人生
  • 十几个浏览器标签切换到想砸电脑

现在?10分钟搞定,而且比人工规划更合理、更高效。

想学吗?我现在就手把手教你,怎么让AI+高德MCP为你定制完美旅行计划。

四步上手,成为旅行规划大师

步骤1:获取高德地图开发权限(超简单)

先去高德开发者平台(lbs.amap.com)注册个账号。

怕麻烦?直接用支付宝扫码就能登录,一分钟搞定!

注册完成后,系统会让你验证身份——这是为了确认你不是机器人,讽刺的是我们要用这个来教AI做事🤣

验证过程很简单,照着提示操作就行,最终你会成为一名光荣的"高德地图开发者"。

步骤2:创建应用并获取API Key

登录成功后,进入控制台:

  1. 点击"应用管理",创建一个新应用

  1. 应用名称随便填,比如"我的旅行助手"
  2. 平台选择"Web服务"

  1. 创建应用后点击"添加Key",复制生成的密钥

这个Key就是打开高德地图宝库的钥匙,下面要把它交给我们的AI助手。

步骤3:配置AI的地图能力

这一步的关键——我们要让AI获得调用高德地图的超能力:

  1. 打开Claude Desktop(或其他支持MCP的AI,比如Cursor)
  2. File->Setting->Developer->Edit Config
  3. 配置MCP配置文件,配置高德地图MCP服务,贴入刚才获取的API Key
  4. 保存配置,重启应用

如果你使用的是Claude,添加下面的代码(记得替换成你自己的key)建议重启下应用:

{
"mcpServers": {
"amap-maps": {
"command": "npx",
"args": ["-y", "@amap/amap-maps-mcp-server"],
"env": {"AMAP_MAPS_API_KEY": "这里粘贴你的key"}
}
}
}

确认配置无误后,AI现在已经具备了调用高德地图的能力,它可以查询实时天气、景点信息、路线规划和交通状况等数据。

步骤4:一句指令,生成完美攻略

现在是见证奇迹的时刻!在对话框中输入:

用高德MCP,做苏州三天旅游指南

然后静静等待几秒钟,AI会开始调用高德地图API,搜集各种数据并为你生成一份详尽的旅行规划。

我的苏州三日游攻略包含了:

  • 每天详细的行程安排和时间规划
  • 景点介绍、门票价格和开放时间
  • 周边餐厅推荐和特色美食
  • 不同景点间的交通方式和预计用时
  • 三天的天气预报
  • 住宿和购物建议
  • 各种实用小贴士

最妙的是,AI还能根据天气情况自动调整行程——我看到第二天苏州预报有大雨,它贴心地提醒我准备雨具,并建议安排更多室内活动。

锦上添花:生成打印版旅行攻略

如果你想更进一步,可以让AI为你生成一份精美的A4旅行规划表,方便打印随身携带。

只需输入: 帮我设计一个A4纸张大小的旅行规划表,适合打印出来随身携带

这是我的提示词

# 旅行规划表设计提示词
你是一位优秀的平面设计师和前端开发工程师,具有丰富的旅行信息可视化经验,曾为众多知名旅游平台设计过清晰实用的旅行规划表。现在需要为我创建一个A4纸张大小的旅行规划表,适合打印出来随身携带使用。请使用HTML、CSS和JavaScript代码实现以下要求:
## 基本要求
尺寸与基础结构
- 严格符合A4纸尺寸(210mm×297mm),比例为1:1.414
- 适合打印的设计,预留适当的打印边距(建议上下左右各10mm)
- 采用单页设计,所有重要信息必须在一页内完整呈现
- 信息分区清晰,使用网格布局确保整洁有序
- 打印友好的配色方案,避免过深的背景色和过小的字体
技术实现
- 使用打印友好的CSS设计
- 提供专用的打印按钮,优化打印样式
- 使用高对比度的配色方案,确保打印后清晰可读
- 可选择性地添加虚线辅助剪裁线
- 使用Google Fonts或其他CDN加载适合的现代字体
- 引用Font Awesome提供图标支持
专业设计技巧
- 使用图标和颜色编码区分不同类型的活动(景点、餐饮、交通等)
- 为景点和活动设计简洁的时间轴或表格布局
- 使用简明的图示代替冗长文字描述
- 为重要信息添加视觉强调(如框线、加粗、不同颜色等)
- 在设计中融入城市地标元素作为装饰,增强辨识度
## 设计风格
- 实用为主的旅行工具风格:以清晰的信息呈现为首要目标
- 专业旅行指南风格:参考Lonely Planet等专业旅游指南的排版和布局
- 信息图表风格:将复杂行程转化为直观的图表和时间轴
- 简约现代设计:干净的线条、充分的留白和清晰的层次结构
- 整洁的表格布局:使用表格组织景点、活动和时间信息
- 地图元素整合:在合适位置添加简化的路线或位置示意图
- 打印友好的灰度设计:即使黑白打印也能保持良好的可读性和美观
## 内容区块
1. 行程标题区:
- 目的地名称(主标题,醒目位置)
- 旅行日期和总天数
- 旅行者姓名/团队名称(可选)
- 天气信息摘要
2. 行程概览区:
- 按日期分区的行程简表
- 每天主要活动/景点的概览
- 使用图标标识不同类型的活动
3. 详细时间表区:
- 以表格或时间轴形式呈现详细行程
- 包含时间、地点、活动描述
- 每个景点的停留时间
- 标注门票价格和必要预订信息
4. 交通信息区:
- 主要交通换乘点及方式
- 地铁/公交线路和站点信息
- 预计交通时间
- 使用箭头或连线表示行程路线
5. 住宿与餐饮区:
- 酒店/住宿地址和联系方式
- 入住和退房时间
- 推荐餐厅列表(标注特色菜和价格区间)
- 附近便利设施(如超市、药店等)
6. 实用信息区:
- 紧急联系电话
- 重要提示和注意事项
- 预算摘要
- 行李清单提醒
## 示例内容(基于深圳一日游)
目的地:深圳一日游
日期:2025年4月15日(星期二)
天气:晴,24°C/18°C,东南风2-3级
时间表:
| 时间 | 活动 | 地点 | 详情 |
|------|------|------|------|
| 09:00-11:30 | 参观世界之窗 | 南山区深南大道9037号 | 门票:190元 |
| 12:00-13:30 | 海上世界午餐 | 蛇口海上世界 | 推荐:海鲜、客家菜 |
| 14:00-16:00 | 游览深圳湾公园 | 南山区滨海大道 | 免费活动 |
| 16:30-18:30 | 逛深圳欢乐海岸 | 南山区白石路 | 购物娱乐 |
| 19:00-21:00 | 福田CBD夜景或莲花山夜游 | 福田中心区 | 免费活动 |
交通路线:
- 世界之窗→海上世界:乘坐地铁2号线(世界之窗站→海上世界站),步行5分钟,约20分钟
- 海上世界→深圳湾公园:乘坐公交线路380路,约15分钟
- 深圳湾→欢乐海岸:步行或乘坐出租车,约10分钟
- 欢乐海岸→福田CBD:地铁2号线→地铁4号线,约35分钟
实用提示:
- 下载"深圳地铁"APP查询路线
- 准备防晒用品,深圳日照强烈
- 世界之窗建议提前网上购票避免排队
- 使用深圳通交通卡或移动支付
- 深圳湾傍晚可观赏日落美景和香港夜景
- 周末景点人流较大,建议工作日出行
重要电话:
- 旅游咨询:0755-12301
- 紧急求助:110(警察)/120(急救)
请创建一个既美观又实用的旅行规划表,适合打印在A4纸上随身携带,帮助用户清晰掌握行程安排。

AI会立刻为你创建一个格式优美、信息完整的HTML文档,包含所有行程信息,分区清晰,配色考虑了打印需求,真正做到了拿来即用!

告别旅行规划焦虑症

这套方法彻底改变了我规划旅行的方式。以前要花半天甚至几天的工作,现在10分钟就能完成,而且质量更高:

  • 基于实时数据:不会推荐已关闭的景点或过时信息
  • 路线最优化:自动计算景点间最合理的游览顺序
  • 个性化定制:想要美食之旅?亲子游?文艺路线?只需一句话
  • 省时又省力:把宝贵时间用在享受旅行上,而不是规划过程中

最让我满意的是,这整套流程不需要任何编程知识,人人都能轻松上手。我妈妈都能用!

更多玩法等你探索

除了基础攻略,你还可以用更具体的指令获取定制内容:

"我想了解苏州有什么值得打卡的特色美食" "帮我规划一条适合老人和小孩的苏州慢游路线" "我只去苏州一天,哪些景点必须打卡?" "设计一条苏州园林主题的摄影路线"

每一个问题,AI都能结合高德地图的数据给你最专业的建议。

以后旅行前,不用再痛苦地翻攻略、对比信息、反复规划了。一杯咖啡的时间,完美行程就在你手中。

这大概就是科技改变生活的最好证明吧!下次出行,不妨也试试这个方法,让AI做你的专属旅行规划师!

阿里云宣布全面支持MCP服务部署和调用

前天群里还有小伙伴想玩下MCP服务呢,昨天阿里云百炼平台就宣布全面支持MCP服务部署与调用,打通AI应用爆发的最后一公里。

这里是地址:bailian.console.aliyun.com/?tab=mcp#/m…

当然昨晚我也研究了下,简直不要太简单,连注册都省了,下面点立即开通呢就能玩了

下面这个知名爬虫服务我也体验了把,非常简单易懂

创建完应用,提示词录入进去就能用了,连cursor,claude的mcp配置都免了,感兴趣的朋友可以去体验下。


作者:易安说AI
来源:juejin.cn/post/7491553973112111115
收起阅读 »

聊一下MCP,希望能让各位清醒一点吧🧐

最近正在忙着准备AI产品示例项目的数据,但是有好几群友问了MCP的问题,先花点时间给大家安排一下MCP。作为一个带队落地AI应用的真实玩家,是怎么看待MCP的。 先说观点:MCP不错,但它仅仅是个协议而已,很多科普文章中,提到的更多都是愿景,而不是落地的场景。...
继续阅读 »

最近正在忙着准备AI产品示例项目的数据,但是有好几群友问了MCP的问题,先花点时间给大家安排一下MCP。作为一个带队落地AI应用的真实玩家,是怎么看待MCP的。


先说观点:MCP不错,但它仅仅是个协议而已,很多科普文章中,提到的更多都是愿景,而不是落地的场景。


本文不再重新陈述MCP的基本概念,而是旨在能让大家了解的是MCP 有什么用?怎么用?要不要用?


我准备了一份MCP实现的核心代码,只保留必要的内容,五分钟就能看明白MCP回事。


先上代码,让我们看看实现MCP最核心的部分我们都干了些什么东西。顺便让大家看看MCP到底和Function call是个什么关系



此处只贴用于讲解的代码,其他代码基本都是逻辑处理与调用。也可关注公众号:【华洛AI转型纪实】,发送mcpdemo,来获取完整代码。



MCP代码核心逻辑


我们在本地运行的MCP,所以使用的是Stdio模式的客户端和服务端。也就是:StdioServerTransportStdioClientTransport


先看打满日志的demo运行起来起来后,我们获得的信息:


mcpdemo.jpg

我们的服务端写了两个简单的工具,加法减法


服务端启动成功之后,客户端成功的从服务端获取到了这两个工具。


我们发起了一个问题:计算1+1


接下来做的事情就是MCP的客户端核心三步逻辑:



  1. 客户端调用AI的function call能力,由AI决定是否使用工具,使用哪个工具。

  2. 客户端把确定要使用的工具和参数发送回服务端,由服务端实现API调用并返回结果。

  3. 客户端根据结果,再次调用AI,由AI进行回答。


我们一边看代码一边说里面的问题:


第一步调用AI,决定使用工具


客户端代码:


  const response = await this.openai.chat.completions.create({
model: model,
messages,
tools: this.tools, // ! 重点看这里,this.tools是服务端返回的工具列表
});

看到了么?这里用的还是Function call! 谣言一:MCP和Function call没关系,MCP就可以让大家调用工具,终结了。MCP就是用的function call的能力来实现的工具调用。当然我们也可以不用Function call,我们就直接用提示词判断,也是可以的。


这里要说的是:MCP就是个协议。并没有给大模型带来任何新的能力,也没有某些人说的MCP提升了Function call的能力,以后不用Function call了,用MCP就够了这种话,千万不要被误导。


MCP并没有让大模型的工具调用能力提升


在真实的生产环境中,目前Function call主要的问题有:



  • 工具调用准确性不够。
    真正的生成环境可能不是三个五个工具,而是三十个五十个。工具之间的界限不够清晰的话,就会存在模型判断不准确的情况。

  • 参数提取准确性不够。
    特别是当一个工具必填加选填的参数达到十个以上的时候,面对复杂问题,参数的提取准确率会下降。

  • 多意图的识别。

    用户的一个问题涉及到多个工具时,目前没有能够稳定提取的模型。


第二步把工具和参数发回服务端,由服务端调用API


客户端代码:


const result = await this.mcp.callTool({
name: toolName,
arguments: toolArgs,
});

服务端的代码:


server.tool(
"加法",
"计算数字相加",
{
"a": z.number().describe("加法的第一个数字"),
"b": z.number().describe("加法的第二个数字"),
},
async ({ a, b, c }) => {
console.error(`服务端: 收到加法API,计算${a}${b}两个数的和。模型API发送`)
// 这里模拟API的发送和使用
let data = a + b
return {
content: [
{
type: "text",
text: a + '+' + b + '的结果是:' + data,
},
],
};
},
);

发现问题了么? API是要有MCP服务器提供者调用的。要花钱的朋友!


每一台MCP服务器背后都是要成本的,收费产品进行MCP服务器的支持还说的过去,不收费的产品全靠爱发电。更不要说,谁敢在生成环境接一个不收费的私人的小服务器?


百度地图核心API全面兼容MCP了,百度地图是收费的,进行多场景的支持是很正常的行为。


来看看百炼吧,阿里的百炼目前推出了MCP的功能,支持在百炼上部署MCP server。


也是要花钱的朋友~,三方API调用费用另算。


blfy.jpg

阿里的魔塔社区提供了大量的MCP,可以看到有一些大厂的服务在,当然有收费的有免费的,各位可以尝试


mota.jpg

第三步客户端根据结果,再次调用AI,由AI进行回答。


客户端代码:


messages.push({
role: "user",
content: result.content,
});

const aiResponse = await this.openai.chat.completions.create({
model: model,
messages: messages,
});

从服务端返回的结果,添加到messages中,配合提示词由大模型进行回复即可。


这一步属于正常的流程,没什么好说的。


那么问题是:我们使用MCP来实现,和我们自己实现这套流程有什么区别么?我们为什么要用MCP呢?


当初群里朋友第一次提到MCP的时候,我去看了一眼文档,给了这样的结论:


大厂为了抢生态做的事情,给落地的流程中定义了一些概念,多了脑力负担,流程和自己实现没区别。


对于工具的使用,自己实现和用MCP实现有什么区别么?


自己实现的流程和逻辑是这样的:



  1. 我们的提示词工程师写好每个工具的提示词

  2. 我们的后端工程师写好模型的调用,使用的是前面写好的提示词

  3. 提供接口给前端,等待前端调用

  4. 前端调用传入query,后端通过AI获取了工具

  5. 通过工具配置调用API,拿到数据交给AI,流式返回用户。


MCP的逻辑是这样的:



  1. 我们的提示词工程师写好每个工具的提示词

  2. 我们后端工程师分别写好MCP服务端、MCP客户端

  3. MCP客户端提供个接口给前端,等待前端调用

  4. 前端调用传入query,MCP客户端调用AI,获取了工具。

  5. 客户端把确定要使用的工具和参数发送会服务端,由服务端实现API调用并返回结果。

  6. 客户端根据结果,再次调用AI,由AI进行回答,流式返回用户。


看吧,本质上是没有区别的。


什么?你说MCP服务端,如果日后需要与其他企业进行合作,可以方便的让对方的MCP客户端调用?
我们的客户端也可以很方便的接入别人的MCP服务端。


不好意思,不用MCP也可以,因为Function call的参数格式已经确定了,这里原本存在差异性就极小。而且MCP也并没有解决这个差异性。还是需要客户端进行修改的。


MCP真正的意义


现在还是诸神混战时期,整个AI产品的上下游所有的点,都具有极高的不确定性。


MCP给出了一个技术标准化的协议,是大家共建AI的愿景中的一环,潜力是有的。


但是Anthropic真的只是在乎这个协议么?前面的内容我们也看到了,MCP和我们自己实现的流程几乎是一样的。但是为什么还要提出MCP呢?


为了生态控制权和行业话语权。


MCP它表面上是一个开放的协议,旨在解决AI模型与外部工具集成的碎片化问题,但其实他就是Anthropic对未来AI生态主导权的竞争。


未来MCP如果真的作为一个标准的协议成为大家的共识,围绕这个协议,甚至每家模型的工具调用格式都将被统一,此时Anthropic在委员会里的位置呢?不言而喻啊。


结语


最后把我的策略分享给大家吧:


打算在圈子里玩的部分,就和大家用一样的,不在圈子里玩的,其实自己团队实现也是OK的。


我这边更多的是自己团队实现的,而且在这个实现过程中大家对模型应用、AI产品的理解不断地在提升。


希望各位读者也多进行尝试,这样未来面对新出的各路牛鬼蛇神时大家才能有更多的判断力。


共勉吧!



☺️你好,我是华洛,如果你对程序员转型AI产品负责人感兴趣,请给我点个赞。


你可以在这里联系我👉http://www.yuque.com/hualuo-fztn…


已入驻公众号【华洛AI转型纪实】,欢迎大家围观,后续会分享大量最近三年来的经验和踩过的坑。



实战专栏


# 从0到1打造企业级AI售前机器人——实战指南一:根据产品需求和定位进行agent流程设计🧐


# 从0到1打造企业级AI售前机器人——实战指南二:RAG工程落地之数据处理篇🧐


# 实战派!百万PV的AI产品如何搭建RAG系统?


# 团队落地AI产品的全流程


# 30行代码,随时进行云端大模型能力评估!


# 5000字长文,AI时代下程序员的巨大优势!


作者:华洛
来源:juejin.cn/post/7492271537010671635
收起阅读 »

长安马自达全球车型MAZDA 6e启航欧洲,全球化战略迈入新里程

4月22日,上海外高桥码头,长安马自达首批发往欧洲市场的纯电旗舰轿车MAZDA 6e正式装船启航。此次发运标志着MAZDA 6e在欧洲市场进入交付倒计时阶段,长安马自达“双百翻番”战略计划逐步落地,中国“合资智造”正加速赋能马自达全球电动化布局,传递着中国新能...
继续阅读 »

4月22日,上海外高桥码头,长安马自达首批发往欧洲市场的纯电旗舰轿车MAZDA 6e正式装船启航。此次发运标志着MAZDA 6e在欧洲市场进入交付倒计时阶段,长安马自达“双百翻番”战略计划逐步落地,中国“合资智造”正加速赋能马自达全球电动化布局,传递着中国新能源产业的技术自信。长安马自达汽车有限公司管理层和临港片区管委会代表、物流合作伙伴出席装船仪式,共同见证这一里程碑时刻。

上午10时,外高桥码头海风轻拂,首批600辆MAZDA 6e整齐列队,与停泊在蓝天碧海间的巨型滚装运输船交相辉映。随着发运按钮的正式启动,首辆MAZDA 6e平稳驶入船舱,现场响起热烈掌声。这批车辆预计将于5月抵达比利时港口,并于今年夏天交付至欧洲多国经销商。MAZDA 6e的到来,将为欧洲市场客户带来全新的电动旗舰轿车选择,并将进一步丰富马自达欧洲市场的产品阵容。

自今年1月10日首次亮相2025比利时布鲁塞尔车展以来,MAZDA 6e的全球化进程在不断加速。MAZDA 6e是以MAZDA EZ-6为基础推出的符合欧洲市场环境,且能满足欧洲客户和马自达忠实粉丝的期待、彰显马自达特色的最新款电动汽车。MAZDA 6e的开发过程集合了长安马自达南京产品研发中心、马自达日本广岛总部以及马自达欧洲研发中心三地工程师的智慧与力量。从设计、研发到生产均严格遵循马自达全球统一的制造标准,既是中国车,也是全球车。南京工厂作为马自达在华唯一新能源生产基地,汇聚了马自达百年造车工艺与长安汽车领先的电动化技术,以智能化生产线和精益管理模式确保每一辆MAZDA 6e的品质达均能达到全球顶尖水平。

长安马自达汽车有限公司总裁松田英久表示:“MAZDA 6e拥有符合欧盟最新法规的三电系统和安全性能、超低风阻的「魂动」美学设计,以及电感「人马一体」的驾控性能,精准契合欧洲消费者对高端电动轿车的期待。MAZDA 6e的欧洲首航,代表着长安马自达正从‘合资企业’向‘全球新能源技术创新基地’转型。托中国在电动化、智能化领域的先发优势,长安马自达未来将成为马自达全球技术研发的关键支点”。

同时,MAZDA EZ-6不断加快产品焕新节奏。在现有的赤麂棕色高配内饰色之外,新增兼具时尚气质和高级质感的鹭羽白浅色内饰,快速回应用户对于浅色系内饰的需求,更为用户带来“增色不加价”的新选择。目前,购MAZDA EZ-6可享受至高40,000元补贴(15,000元置换厂补+20,000元置换国补+5,000元保险补贴)、100,000元尾款可享6年0息(和置换厂补二选一),还可享价值7,999元不限车主、不限里程终身零燃权益。

4月23日,长安马自达第二款全球化新能源车型MAZDA EZ-60将登陆2025上海国际车展6.1馆展台,迎来全球首发。以MAZDA 6e出海为起点,长安马自达还将持续推出更多面向全球市场的新能源车型,覆盖更多细分市场用户需求,以更快的节奏、更强的技术、更广的布局,迎接全球电动化市场的无限可能。

收起阅读 »

【Fiddler】Fiddler抓包工具(详细讲解)_抓包工具fiddler

抓包工具使用指南 序章 Fiddler 是一款功能强大的抓包工具,能够截获、重发、编辑和转存网络传输中的数据包,同时也常用于网络安全检测。它的功能丰富,但在学习过程中可能会遇到一些隐藏的小功能,容易遗忘。因此,本文总结了 Fiddler 的常用功能,并结合 S...
继续阅读 »

抓包工具使用指南


序章


Fiddler 是一款功能强大的抓包工具,能够截获、重发、编辑和转存网络传输中的数据包,同时也常用于网络安全检测。它的功能丰富,但在学习过程中可能会遇到一些隐藏的小功能,容易遗忘。因此,本文总结了 Fiddler 的常用功能,并结合 SniffMaster 抓包大师的特点,帮助大家更好地掌握抓包工具的使用。




1. Fiddler 抓包简介


Fiddler 通过改写 HTTP 代理来监控和截取数据包。当 Fiddler 启动时,它会自动设置浏览器的代理,关闭时则会还原代理设置,非常方便。


1.1 字段说明


Fiddler 抓取的数据包会显示在列表中,以下是各字段的含义:


名称含义
#抓取 HTTP 请求的顺序,从 1 开始递增
ResultHTTP 状态码
Protocol请求使用的协议(如 HTTP/HTTPS/FTP 等)
Host请求地址的主机名
URL请求资源的位置
Body请求的大小
Caching请求的缓存过期时间或缓存控制值
Content-Type请求响应的类型
Process发送此请求的进程 ID
Comments用户为此会话添加的备注
Custom用户设置的自定义值

1.2 Statistics 请求性能数据分析


点击任意请求,可以在右侧查看该请求的性能数据和分析结果。


1.3 Inspectors 查看数据内容


Inspectors 用于查看会话的请求和响应内容,上半部分显示请求内容,下半部分显示响应内容。


1.4 AutoResponder 拦截指定请求


AutoResponder 允许拦截符合特定规则的请求,并返回本地资源或 Fiddler 资源,从而替代服务器响应。例如,可以将关键字 "baidu" 与本地图片绑定,访问百度时会被劫持并显示该图片。


1.5 Composer 自定义请求发送


Composer 允许自定义请求并发送到服务器。可以手动创建新请求,或从会话表中拖拽现有请求进行修改。


1.6 Filters 请求过滤规则


Filters 用于过滤请求,避免无关请求干扰。常用的过滤条件包括 Zone(内网或互联网)和 Host(指定域名)。


1.7 Timeline 请求响应时间


Timeline 显示指定内容从服务器传输到客户端的时间,帮助分析请求的响应速度。




2. Fiddler 设置解密 HTTPS 数据


Fiddler 可以通过伪造 CA 证书来解密 HTTPS 数据包。具体步骤如下:



  1. 打开 Fiddler,点击 Tools -> Fiddler Options -> HTTPS

  2. 勾选 Decrypt HTTPS Traffic

  3. 点击 OK 保存设置。




3. 抓取移动端数据包


3.1 设置代理



  1. 打开 Fiddler,点击 Tools -> Fiddler Options -> Connections

  2. 设置代理端口为 8888,并勾选 Allow remote computers to connect

  3. 在手机端连接与电脑相同的 WiFi,并设置代理 IP 和端口。


3.2 安装证书



  1. 在手机浏览器中访问 http://<电脑IP>:8888,下载 Fiddler 根证书。

  2. 安装证书并信任。


3.3 抓取数据包


配置完成后,手机访问应用时,Fiddler 会截取到数据包。




4. Fiddler 内置命令与断点


Fiddler 提供了命令行功能,方便快速操作。常用命令包括:


命令功能示例
?匹配包含指定字符串的请求?google
匹配请求大小大于指定值的请求>1000
<匹配请求大小小于指定值的请求<100
=匹配指定 HTTP 返回码的请求=200
@匹配指定域名的请求@http://www.baidu.com
select匹配指定响应类型的请求select image
cls清空当前所有请求cls
dump将所有请求打包成 saz 文件dump
start开始监听请求start
stop停止监听请求stop

断点功能


Fiddler 的断点功能可以截获请求并暂停发送,方便修改请求内容。常用断点命令包括:



  • bpafter:中断包含指定字符串的请求。

  • bpu:中断响应。

  • bps:中断指定状态码的请求。

  • bpv:中断指定 HTTP 方法的请求。




5. SniffMaster 抓包大师


SniffMaster 是一款跨平台抓包工具,支持 Android、iOS 和 PC 端抓包。与 Fiddler 相比,SniffMaster 具有以下优势:



  • 自动生成证书:无需手动配置 HTTPS 解密。

  • 多设备支持:支持同时抓取多个设备的数据包。

  • 智能过滤:按协议、域名等条件快速筛选数据。

  • 可视化界面:提供更直观的数据分析和展示。


5.1 SniffMaster 使用场景



  • 移动端抓包:支持 Android 和 iOS 设备,自动配置代理和证书。

  • HTTPS 解密:内置 HTTPS 解密功能,无需手动安装证书。

  • 多平台支持:支持 Windows、macOS 和 Linux 系统。




总结


Fiddler 和 SniffMaster 都是强大的抓包工具,适用于不同的场景。Fiddler 适合需要深度定制和高级功能的用户,而 SniffMaster 则更适合新手和需要快速抓包的用户。无论是开发调试还是网络安全检测,这两款工具都能提供极大的帮助。


作者:用户958221080790
来源:juejin.cn/post/7481463851298635827
收起阅读 »

“新E代弯道王”MAZDA EZ-6鹭羽白内饰焕新

今日,“新E代弯道王”MAZDA EZ-6(以下称EZ-6)宣布鹭羽白内饰焕新,现在购车可享补贴后9.98万起。新车在现有的赤麂棕色高配内饰色之外,新增兼具时尚气质和高级质感的鹭羽白浅色内饰,不仅快速回应了部分用户对于浅色系内饰的需求,更为用户带来“增色不加价...
继续阅读 »

今日,“新E代弯道王”MAZDA EZ-6(以下称EZ-6)宣布鹭羽白内饰焕新,现在购车可享补贴后9.98万起。新车在现有的赤麂棕色高配内饰色之外,新增兼具时尚气质和高级质感的鹭羽白浅色内饰,不仅快速回应了部分用户对于浅色系内饰的需求,更为用户带来“增色不加价”的新选择。

EZ-6自推出补贴后9.98万起售的超高智价比购车模式以来,市场热度持续攀升。在春日出游季到来之际,长安马自达精准捕捉用户对浅色高质感内饰的喜好,将纤细轻柔,丝般细腻,又蓬松似云的鹭羽白色融入座椅工艺,与那些追求色泽明快简约大气的用户相得益彰,彰显出他们对高品质生活的高雅品味。

EZ-6的座椅采用了和MAZDA CX-90相同的菱形衍缝工艺,包裹性极强。Nappa真皮工艺,经鞣制后软度大幅提升,冬暖夏凉的亲肤感让身体一秒沦陷,配合10向电动调节,3档座椅通风&加热,能够满足各种身材驾驶者对理想坐姿的需求和温度需求。此外,EZ-6内饰材质均通过EPD环保产品声明、VEGAN「素食」产品、OEKO-TEX Standard 100婴儿级生态产品三大权威认证,打造让用户安全、安心更健康的乘坐体验。

作为合资B级电动轿车市场唯一同时提供增程和纯电动力选择的车型,EZ-6满足了用户全场景、全工况的出行需求。线性流畅的加速、自信安心的刹车、舒适愉悦的过弯、精准稳定的转向、迅捷的车身响应,EZ-6在电动化时代,依然能够为用户带来「人马一体」的驾乘愉悦。

目前,购EZ-6全系可享至高40,000元补贴,包括至高20,000元置换国补+15,000元置换/增购厂补+5,000元保险补贴;选择金融购车的用户可享100,000元尾款6年0息(和置换厂补二选一),在安全领域,长安马自达再次送出价值7,999元不限车主、不限里程终身零燃权益,彻底消除用户的后顾之忧无论是你的第一辆车之选,还是家庭之选,都能享受高品质的新能源出行乐趣。现在,登录长安马自达悦马星空」APP或小程序预约试驾,或亲临全国授权经销商门店试驾,即可解锁EZ-6全场景驾控乐趣

收起阅读 »

为什么把私钥写在代码里是一个致命错误

为什么把私钥写在代码里是一个致命错误 在技术社区经常能看到一些开发者分享的教训,前几天就有人发帖讲述一位Java开发者因同事将私钥直接硬编码在代码里而感到愤怒的事情。这种情况虽然听起来可笑,但在开发团队中却相当常见,尤其对于经验不足的程序员来说。 为什么把私钥...
继续阅读 »

为什么把私钥写在代码里是一个致命错误


在技术社区经常能看到一些开发者分享的教训,前几天就有人发帖讲述一位Java开发者因同事将私钥直接硬编码在代码里而感到愤怒的事情。这种情况虽然听起来可笑,但在开发团队中却相当常见,尤其对于经验不足的程序员来说。


为什么把私钥写在代码里如此危险?


1. 代码会被分享和同步


代码通常会提交到Git或SVN等版本控制系统中。一旦私钥被提交,团队中的每个人都能看到这些敏感信息。即使后来删除了私钥,在历史记录中依然可以找到。有开发者就分享过真实案例:团队成员意外将AWS密钥提交到GitHub,结果第二天账单暴增数千元——有人利用泄露的密钥进行了挖矿活动。


2. 违反安全和职责分离原则


在规范的开发流程中,密钥管理和代码开发应该严格分离。通常由运维团队负责密钥管理,而开发人员则不需要(也不应该)直接接触生产环境的密钥。这是基本的安全实践。


3. 环境迁移的噩梦


当应用从开发环境迁移到测试环境,再到生产环境时,如果密钥硬编码在代码中,每次环境切换都需要修改代码并重新编译。这不仅效率低下,还容易出错。


正确的做法


业内已有多种成熟的解决方案:



  • 使用环境变量存储敏感信息

  • 采用专门的配置文件(确保加入.gitignore)

  • 使用AWS KMS、HashiCorp Vault等专业密钥管理系统

  • 在CI/CD流程中动态注入密钥


有开发团队就曾经花费两周时间清理代码中的硬编码密钥。其中甚至发现了一个已离职员工留下的"临时"数据库密码,注释中写着"临时用,下周改掉"——然而那个"下周"已经过去五年了。


作为专业开发者,应当始终保持良好的安全习惯。将私钥硬编码进代码,就像把家门钥匙贴在门上一样不可理喻。


这个教训值得所有软件工程师引以为戒。


作者:Asthenian
来源:juejin.cn/post/7489043337290203163
收起阅读 »

双Token无感刷新方案

提醒一下 双Token机制并没有从根本上解决安全性的问题,本文章只是提供一个思路,具体是否选择请大家仔细斟酌考虑,笔者水平有限,非常抱歉对你造成不好的体验。 token有效期设置问题 最近在做用户认证模块的后端功能开发,之前就有一个问题困扰了我好久,就是如何设...
继续阅读 »

提醒一下


双Token机制并没有从根本上解决安全性的问题,本文章只是提供一个思路,具体是否选择请大家仔细斟酌考虑,笔者水平有限,非常抱歉对你造成不好的体验。


token有效期设置问题


最近在做用户认证模块的后端功能开发,之前就有一个问题困扰了我好久,就是如何设置token的过期时间,前端在申请后端登录接口成功之后,会返回一个token值,存储在用户端本地,用户要访问后端的其他接口必须通过请求头带上这个token值,但是这个token的有效期应该设置为多少?



  1. 如果设置的太短,比如1小时,那么用户一小时之后。再访问其他接口,需要再次重新登录,对用户的体验极差

  2. 如果设置为一个星期,那么在这个时间内







      • 一旦token泄露,攻击者可长期冒充用户身份,直到token过期,服务端无法限制其访问用户数据

      • 虽然可以依赖黑名单机制,但会增加系统复杂度,还要进行系统监测

      • 如果在这段时间恶意用户利用未过期的条款持续调用后端API将会导致资源耗尽或产生巨额费用






所以有没有两者都兼顾的方案呢?


双token无感刷新方案


传统的token方案要么频繁要求用户重新登录,要么面临长期有效的安全风险


但是双token无感刷新机制,通过组合设计,在保证安全性的情况下,实现无感知的认证续期


核心设计



  1. access_token:访问令牌,有效期一般设置为15~30分钟,主要用于对后端请求API的交互

  2. refresh_token:刷新令牌,一般设置为一个星期到一个月,主要用于获取新的access_token


大致的执行流程如下


用户登录之后,后端返回access_tokenrefresh_token响应给前端,前端将两个token存储在用户本地



在用户端发起前端请求,访问后端接口,在请求头中携带上access_token



前端会对access_token的过期时间进行检测,当access_token过期前一分钟,前端通过refresh_token向后端发起请求,后端判断refresh_token是否有效,有效则重新获取新的access_token,返回给前端替换掉之前的access_token存储在用户本地,无效则要求用户重新认证



这样的话对于用户而言token的刷新是无感知的,不会影响用户体验,只有当refresh_token失效之后,才需要用户重新进行登录认证,同时,后端可以通过对用户refresh_token的管理来限制用户对后端接口的请求,大大提高了安全性


有了这个思路,写代码就简单了


@Service
public class LoginServiceImpl implements LoginService {

@Autowired
private JwtUtils jwtUtils;

// token过期时间
private static final Integer TOKEN_EXPIRE_DAYS =5;
// token续期时间

private static final Integer TOKEN_RENEWAL_MINUTE =15;

@Override
public boolean verify(String refresh_token) {
Long uid = jwtUtils.getUidOrNull(refresh_token);
if (Objects.isNull(uid)) {
return false;
}
String key = RedisKey.getKey(RedisKey.USER_REFRESH_TOKEN,uid);
String realToken = RedisUtils.getStr(key);
return Objects.equals(refresh_token, realToken);
}

@Override
public void renewalTokenIfNecessary(String refresh_token) {
Long uid = jwtUtils.getUidOrNull(refresh_token);
if (Objects.isNull(uid)) {
return;
}
String refresh_key = RedisKey.getKey(RedisKey.USER_REFRESH_TOKEN, uid);
long expireSeconds = RedisUtils.getExpire(refresh_key, TimeUnit.SECONDS);
if (expireSeconds == -2) { // key不存在,refresh_token已过期
return;
}
String access_key = RedisKey.getKey(RedisKey.USER_ACCESS_TOKEN, uid);
RedisUtils.expire(access_key, TOKEN_RENEWAL_MINUTE, TimeUnit.MINUTES);
}

@Override
@Transactional(rollbackFor = Exception.class)
@RedissonLock(key = "#uid")
public LoginTokenResponse login(Long uid) {
String refresh_key = RedisKey.getKey(RedisKey.USER_REFRESH_TOKEN, uid);
String access_key = RedisKey.getKey(RedisKey.USER_ACCESS_TOKEN, uid);
String refresh_token = RedisUtils.getStr(refresh_key);
String access_token;
if (StrUtil.isNotBlank(refresh_token)) { //刷新令牌不为空
access_token = jwtUtils.createToken(uid);
RedisUtils.set(access_key, access_token, TOKEN_RENEWAL_MINUTE, TimeUnit.MINUTES);
return LoginTokenResponse.builder()
.refresh_token(refresh_token).access_token(access_token)
.build();
}
refresh_token = jwtUtils.createToken(uid);
RedisUtils.set(refresh_key, refresh_token, TOKEN_EXPIRE_DAYS, TimeUnit.DAYS);
access_token = jwtUtils.createToken(uid);
RedisUtils.set(access_key, access_token, TOKEN_RENEWAL_MINUTE, TimeUnit.MINUTES);
return LoginTokenResponse.builder()
.refresh_token(refresh_token).access_token(access_token)
.build();
}
}}

注意事项



  1. 安全存储Refresh Token时,优先使用HttpOnly+Secure Cookie而非LocalStorage

  2. 在颁发新Access Token时,重置旧Token的生存周期(滑动过期)而非简单续期

  3. 针对高敏感操作(如支付、改密),建议强制二次认证以突破Token机制的限制


安全问题


双Token机制并没有从根本上解决安全性的问题,它只是尝试通过改进设计,优化用户体验,全面的安全策略需要多层防护,分别针对不同类型的威胁和风险,而不仅仅依赖于Token的管理方式或数量


安全是一个持续对抗的过程,关键在于提高攻击者的成本,而非追求绝对防御。


"完美的认证方案不存在,但聪明的权衡永远存在。"


本笔者水平有限,望各位海涵


如果文章中有不对的地方,欢迎大家指正。


作者:昔年种柳
来源:juejin.cn/post/7486782063422717962
收起阅读 »

程序员,你使用过灰度发布吗?

大家好呀,我是猿java。 在分布式系统中,我们经常听到灰度发布这个词,那么,什么是灰度发布?为什么需要灰度发布?如何实现灰度发布?这篇文章,我们来聊一聊。 1. 什么是灰度发布? 简单来说,灰度发布也叫做渐进式发布或金丝雀发布,它是一种逐步将新版本应用到生产...
继续阅读 »

大家好呀,我是猿java


在分布式系统中,我们经常听到灰度发布这个词,那么,什么是灰度发布?为什么需要灰度发布?如何实现灰度发布?这篇文章,我们来聊一聊。


1. 什么是灰度发布?


简单来说,灰度发布也叫做渐进式发布金丝雀发布,它是一种逐步将新版本应用到生产环境中的策略。相比于一次性全量发布,灰度发布可以让我们在小范围内先行测试新功能,监控其表现,再决定是否全面推开。这样做的好处是显而易见的:



  1. 降低风险:新版本如果存在 bug,只影响少部分用户,减少了对整体用户体验的冲击。

  2. 快速回滚:在小范围内发现问题,可以更快地回到旧版本。

  3. 收集反馈:可以在真实环境中收集用户反馈,优化新功能。


2. 原理解析


要理解灰度发布,我们需要先了解一下它的基本流程:



  1. 准备阶段:在生产环境中保留旧版本,同时引入新版本。

  2. 小范围发布:将新版本先部署到一小部分用户,例如1%-10%。

  3. 监控与评估:监控新版本的性能和稳定性,收集用户反馈。

  4. 逐步扩展:如果一切正常,将新版本逐步推广到更多用户。

  5. 全面切换:当确认新版本稳定后,全面替换旧版本。


在这个过程中,关键在于如何切分流量,确保新旧版本平稳过渡。常见的切分方式包括:



  • 基于用户ID:根据用户的唯一标识,将部分用户指向新版本。

  • 基于地域:先在特定地区进行发布,观察效果后再扩展到其他地区。

  • 基于设备:例如,先在Android或iOS用户中进行发布。


3. 示例演示


为了更好地理解灰度发布,接下来,我们通过一个简单的 Java示例来演示基本的灰度发布策略。假设我们有一个简单的 Web应用,有两个版本的登录接口/login/v1/login/v2,我们希望将百分之十的流量引导到v2,其余流量继续使用v1


3.1 第一步:引入灰度策略


我们可以通过拦截器(Interceptor)来实现流量的切分。以下是一个基于Spring Boot的简单实现:


import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Random;

@Component
public class GrayReleaseInterceptor implements HandlerInterceptor {

private static final double GRAY_RELEASE_PERCENT = 0.1; // 10% 流量

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String uri = request.getRequestURI();
if ("/login".equals(uri)) {
if (isGrayRelease()) {
// 重定向到新版本接口
response.sendRedirect("/login/v2");
return false;
} else {
// 使用旧版本接口
response.sendRedirect("/login/v1");
return false;
}
}
return true;
}

private boolean isGrayRelease() {
Random random = new Random();
return random.nextDouble() < GRAY_RELEASE_PERCENT;
}
}

3.2 第二步:配置拦截器


在Spring Boot中,我们需要将拦截器注册到应用中:


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.*;

@Configuration
public class WebConfig implements WebMvcConfigurer {

@Autowired
private GrayReleaseInterceptor grayReleaseInterceptor;

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(grayReleaseInterceptor).addPathPatterns("/login");
}
}

3.3 第三步:实现不同版本的登录接口


import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/login")
public class LoginController {

@GetMapping("/v1")
public String loginV1(@RequestParam String username, @RequestParam String password) {
// 旧版本登录逻辑
return "登录成功 - v1";
}

@GetMapping("/v2")
public String loginV2(@RequestParam String username, @RequestParam String password) {
// 新版本登录逻辑
return "登录成功 - v2";
}
}

在上面三个步骤之后,我们就实现了登录接口地灰度发布:



  • 当用户访问/login时,拦截器会根据设定的灰度比例(10%)决定请求被重定向到/login/v1还是/login/v2

  • 大部分用户会体验旧版本接口,少部分用户会体验新版本接口。


3.4 灰度发布优化


上述示例,我们只是一个简化的灰度发布实现,实际生产环境中,我们可能需要更精细的灰度策略,例如:



  1. 基于用户属性:不仅仅是随机切分,可以根据用户的地理位置、设备类型等更复杂的条件。

  2. 动态配置:通过配置中心动态调整灰度比例,无需重启应用。

  3. 监控与告警:集成监控系统,实时监控新版本的性能指标,异常时自动回滚。

  4. A/B 测试:结合A/B测试,进一步优化用户体验和功能效果。


grayscale-release.png


4. 为什么需要灰度发布?


在实际工作中,为什么我们要使用灰度发布?这里我们总结了几个重要的原因。


4.1 降低发布风险


每次发布新版本,尤其是功能性更新或架构调整,都会伴随着一定的风险。即使经过了充分的测试,实际生产环境中仍可能出现意想不到的问题。灰度发布通过将新版本逐步推向部分用户,可以有效降低全量发布可能带来的风险。


举个例子,假设你上线了一个全新的支付功能,直接面向所有用户开放。如果这个功能存在严重 bug,可能导致大量用户无法完成支付,甚至影响公司声誉。而如果采用灰度发布,先让10%的用户体验新功能,发现问题后只需影响少部分用户,修复起来也更为迅速和容易。


4.2 快速回滚


在传统的全量发布中,一旦发现问题,回滚到旧版本可能需要耗费大量时间和精力,尤其是在高并发系统中,数据状态的同步与恢复更是复杂。而灰度发布由于新版本只覆盖部分流量,问题定位和回滚变得更加简单和快速。


比如说,你在灰度发布阶段发现新版本的某个功能在某些特定条件下会导致系统崩溃,立即可以停止向新用户推送这个版本,甚至只针对受影响的用户进行回滚操作,而不用影响全部用户的正常使用。


4.3 实时监控与反馈


灰度发布让你有机会在真实的生产环境中监控新版本的表现,并收集用户的反馈。这些数据对于评估新功能的实际效果至关重要,有助于做出更明智的决策。


举个具体的场景,你新增了一个推荐算法,希望提升用户的点击率。在灰度发布阶段,你可以监控新算法带来的点击率变化、服务器负载情况等指标,确保新算法确实带来了预期的效果,而不是引入了新的问题。


4.4 提升用户体验


通过灰度发布,你可以在推出新功能时,逐步优化用户体验。先让一部分用户体验新功能,收集他们的使用反馈,根据反馈不断改进,最终推出一个更成熟、更符合用户需求的版本。


举个例子,你开发了一项新的用户界面设计,直接全量发布可能会让一部分用户感到不适应或不满意。灰度发布允许你先让一部分用户体验新界面,收集他们的意见,进行必要的调整,再逐步扩大使用范围,确保最终发布的版本能获得更多用户的认可和喜爱。


4.5 支持A/B测试


灰度发布是实现A/B测试的基础。通过将用户随机分配到不同的版本,你可以比较不同版本的表现,选择最优方案进行全面推行。这对于优化产品功能和提升用户体验具有重要意义。


比如说,你想测试两个不同的推荐算法,看哪个能带来更高的转化率。通过灰度发布,将用户随机分配到使用算法A和算法B的版本,比较它们的表现,最终选择效果更好的算法进行全面部署。


4.6 应对复杂的业务需求


在一些复杂的业务场景中,全量发布可能无法满足灵活的需求,比如分阶段推出新功能、针对不同用户群体进行差异化体验等。灰度发布提供了更高的灵活性和可控性,能够更好地适应多变的业务需求。


例如,你正在开发一个面向企业用户的新功能,希望先让部分高价值客户试用,收集他们的反馈后再决定是否全面推广。灰度发布让这一过程变得更加顺畅和可控。


5. 总结


本文,我们详细地分析了灰度发布,它是一种强大而灵活的部署策略,能有效降低新版本上线带来的风险,提高系统的稳定性和用户体验。作为Java开发者,掌握灰度发布的原理和实现方法,不仅能提升我们的技术能力,还能为团队的项目成功保驾护航。


对于灰度发布,如果你有更多的问题或想法,欢迎随时交流!


6. 学习交流


如果你觉得文章有帮助,请帮忙转发给更多的好友,或关注公众号:猿java,持续输出硬核文章。


作者:猿java
来源:juejin.cn/post/7488321730764603402
收起阅读 »

URL地址末尾加不加”/“有什么区别

URL 结尾是否带 / 主要影响的是 服务器如何解析请求 以及 相对路径的解析方式,具体区别如下: 1. 基础概念 URL(统一资源定位符) :用于唯一标识互联网资源,如网页、图片、API等。 目录 vs. 资源: 以 / 结尾的 URL 通常表示目录,...
继续阅读 »

URL 结尾是否带 / 主要影响的是 服务器如何解析请求 以及 相对路径的解析方式,具体区别如下:




1. 基础概念



  • URL(统一资源定位符) :用于唯一标识互联网资源,如网页、图片、API等。

  • 目录 vs. 资源



    • / 结尾的 URL 通常表示目录,例如:


      https://example.com/folder/


    • 不以 / 结尾的 URL 通常指向具体的资源(如文件),例如:


      https://example.com/file







2. / 和不带 / 的具体区别


(1)目录 vs. 资源



  • https://example.com/folder/



    • 服务器通常会将其解析为 目录,并尝试返回该目录下的默认文件(如 index.html)。



  • https://example.com/folder



    • 服务器可能会将其视为 文件,如果 folder 不是文件,而是目录,服务器可能会返回 301 重定向到 folder/




📌 示例





(2)相对路径解析


URL 末尾是否有 / 会影响相对路径的解析


假设 HTML 页面包含以下 <img> 标签:


<img src="image.png">

📌 示例:



原因:



  • / 结尾的 URL,浏览器会认为它是一个目录,相对路径会基于 folder/ 解析。

  • 不带 /,浏览器可能认为 folder文件,相对路径解析可能会出现错误。




(3)SEO 影响


搜索引擎对 https://example.com/folder/https://example.com/folder 可能会视为两个不同的页面,导致 重复内容问题,影响 SEO 排名。因此:





(4)API 请求


对于 RESTful API,带 / 和不带 / 可能导致不同的行为:



一些 API 服务器对 / 非常敏感,因此最好遵循 API 文档的规范。




3. 总结


URL 形式作用影响
https://example.com/folder/目录通常返回 folder/ 下的默认文件,如 index.html,相对路径解析基于 folder/
https://example.com/folder资源(或重定向)可能被解析为文件,或者服务器重定向到 folder/,相对路径解析可能错误
https://api.example.com/data/API 路径可能与 https://api.example.com/data 表现不同,具体由 API 设计决定

如果你在开发网站,建议:



  1. 统一 URL 规则,例如所有目录都加 / 或者所有请求都不加 /,然后用 301 重定向 确保一致性。

  2. 测试 API 的行为,确认带 / 和不带 / 是否影响请求结果。


作者:Chiyamin
来源:juejin.cn/post/7468112128928350242
收起阅读 »

公司来的新人用字符串存储日期,被组长怒怼了...

在日常的软件开发工作中,存储时间是一项基础且常见的需求。无论是记录数据的操作时间、金融交易的发生时间,还是行程的出发时间、用户的下单时间等等,时间信息与我们的业务逻辑和系统功能紧密相关。因此,正确选择和使用 MySQL 的日期时间类型至关重要,其恰当与否甚至可...
继续阅读 »

在日常的软件开发工作中,存储时间是一项基础且常见的需求。无论是记录数据的操作时间、金融交易的发生时间,还是行程的出发时间、用户的下单时间等等,时间信息与我们的业务逻辑和系统功能紧密相关。因此,正确选择和使用 MySQL 的日期时间类型至关重要,其恰当与否甚至可能对业务的准确性和系统的稳定性产生显著影响。


本文旨在帮助开发者重新审视并深入理解 MySQL 中不同的时间存储方式,以便做出更合适项目业务场景的选择。


不要用字符串存储日期


和许多数据库初学者一样,笔者在早期学习阶段也曾尝试使用字符串(如 VARCHAR)类型来存储日期和时间,甚至一度认为这是一种简单直观的方法。毕竟,'YYYY-MM-DD HH:MM:SS' 这样的格式看起来清晰易懂。


但是,这是不正确的做法,主要会有下面两个问题:



  1. 空间效率:与 MySQL 内建的日期时间类型相比,字符串通常需要占用更多的存储空间来表示相同的时间信息。

  2. 查询与计算效率低下

    • 比较操作复杂且低效:基于字符串的日期比较需要按照字典序逐字符进行,这不仅不直观(例如,'2024-05-01' 会小于 '2024-1-10'),而且效率远低于使用原生日期时间类型进行的数值或时间点比较。

    • 计算功能受限:无法直接利用数据库提供的丰富日期时间函数进行运算(例如,计算两个日期之间的间隔、对日期进行加减操作等),需要先转换格式,增加了复杂性。

    • 索引性能不佳:基于字符串的索引在处理范围查询(如查找特定时间段内的数据)时,其效率和灵活性通常不如原生日期时间类型的索引。




DATETIME 和 TIMESTAMP 选择


DATETIMETIMESTAMP 是 MySQL 中两种非常常用的、用于存储包含日期和时间信息的数据类型。它们都可以存储精确到秒(MySQL 5.6.4+ 支持更高精度的小数秒)的时间值。那么,在实际应用中,我们应该如何在这两者之间做出选择呢?


下面我们从几个关键维度对它们进行对比:


时区信息


DATETIME 类型存储的是字面量的日期和时间值,它本身不包含任何时区信息。当你插入一个 DATETIME 值时,MySQL 存储的就是你提供的那个确切的时间,不会进行任何时区转换。


这样就会有什么问题呢? 如果你的应用需要支持多个时区,或者服务器、客户端的时区可能发生变化,那么使用 DATETIME 时,应用程序需要自行处理时区的转换和解释。如果处理不当(例如,假设所有存储的时间都属于同一个时区,但实际环境变化了),可能会导致时间显示或计算上的混乱。


TIMESTAMP 和时区有关。存储时,MySQL 会将当前会话时区下的时间值转换成 UTC(协调世界时)进行内部存储。当查询 TIMESTAMP 字段时,MySQL 又会将存储的 UTC 时间转换回当前会话所设置的时区来显示。


这意味着,对于同一条记录的 TIMESTAMP 字段,在不同的会话时区设置下查询,可能会看到不同的本地时间表示,但它们都对应着同一个绝对时间点(UTC 时间)。这对于需要全球化、多时区支持的应用来说非常有用。


下面实际演示一下!


建表 SQL 语句:


CREATE TABLE `time_zone_test` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`date_time` datetime DEFAULT NULL,
`time_stamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

插入一条数据(假设当前会话时区为系统默认,例如 UTC+0)::


INSERT INTO time_zone_test(date_time,time_stamp) VALUES(NOW(),NOW());

查询数据(在同一时区会话下):


SELECT date_time, time_stamp FROM time_zone_test;

结果:


+---------------------+---------------------+
| date_time | time_stamp |
+---------------------+---------------------+
| 2020-01-11 09:53:32 | 2020-01-11 09:53:32 |
+---------------------+---------------------+

现在,修改当前会话的时区为东八区 (UTC+8):


SET time_zone = '+8:00';

再次查询数据:


# TIMESTAMP 的值自动转换为 UTC+8 时间
+---------------------+---------------------+
| date_time | time_stamp |
+---------------------+---------------------+
| 2020-01-11 09:53:32 | 2020-01-11 17:53:32 |
+---------------------+---------------------+

扩展:MySQL 时区设置常用 SQL 命令


# 查看当前会话时区
SELECT @@session.time_zone;
# 设置当前会话时区
SET time_zone = 'Europe/Helsinki';
SET time_zone = "+00:00";
# 数据库全局时区设置
SELECT @@global.time_zone;
# 设置全局时区
SET GLOBAL time_zone = '+8:00';
SET GLOBAL time_zone = 'Europe/Helsinki';

占用空间


下图是 MySQL 日期类型所占的存储空间(官方文档传送门:dev.mysql.com/doc/refman/…):



在 MySQL 5.6.4 之前,DateTime 和 TIMESTAMP 的存储空间是固定的,分别为 8 字节和 4 字节。但是从 MySQL 5.6.4 开始,它们的存储空间会根据毫秒精度的不同而变化,DateTime 的范围是 58 字节,TIMESTAMP 的范围是 47 字节。


表示范围


TIMESTAMP 表示的时间范围更小,只能到 2038 年:



  • DATETIME:'1000-01-01 00:00:00.000000' 到 '9999-12-31 23:59:59.999999'

  • TIMESTAMP:'1970-01-01 00:00:01.000000' UTC 到 '2038-01-19 03:14:07.999999' UTC


性能


由于 TIMESTAMP 在存储和检索时需要进行 UTC 与当前会话时区的转换,这个过程可能涉及到额外的计算开销,尤其是在需要调用操作系统底层接口获取或处理时区信息时。虽然现代数据库和操作系统对此进行了优化,但在某些极端高并发或对延迟极其敏感的场景下,DATETIME 因其不涉及时区转换,处理逻辑相对更简单直接,可能会表现出微弱的性能优势。


为了获得可预测的行为并可能减少 TIMESTAMP 的转换开销,推荐的做法是在应用程序层面统一管理时区,或者在数据库连接/会话级别显式设置 time_zone 参数,而不是依赖服务器的默认或操作系统时区。


数值时间戳是更好的选择吗?


除了上述两种类型,实践中也常用整数类型(INTBIGINT)来存储所谓的“Unix 时间戳”(即从 1970 年 1 月 1 日 00:00:00 UTC 起至目标时间的总秒数,或毫秒数)。


这种存储方式的具有 TIMESTAMP 类型的所具有一些优点,并且使用它的进行日期排序以及对比等操作的效率会更高,跨系统也很方便,毕竟只是存放的数值。缺点也很明显,就是数据的可读性太差了,你无法直观的看到具体时间。


时间戳的定义如下:



时间戳的定义是从一个基准时间开始算起,这个基准时间是「1970-1-1 00:00:00 +0:00」,从这个时间开始,用整数表示,以秒计时,随着时间的流逝这个时间整数不断增加。这样一来,我只需要一个数值,就可以完美地表示时间了,而且这个数值是一个绝对数值,即无论的身处地球的任何角落,这个表示时间的时间戳,都是一样的,生成的数值都是一样的,并且没有时区的概念,所以在系统的中时间的传输中,都不需要进行额外的转换了,只有在显示给用户的时候,才转换为字符串格式的本地时间。



数据库中实际操作:


-- 将日期时间字符串转换为 Unix 时间戳 (秒)
mysql> SELECT UNIX_TIMESTAMP('2020-01-11 09:53:32');
+---------------------------------------+
| UNIX_TIMESTAMP('2020-01-11 09:53:32') |
+---------------------------------------+
| 1578707612 |
+---------------------------------------+
1 row in set (0.00 sec)

-- 将 Unix 时间戳 (秒) 转换为日期时间格式
mysql> SELECT FROM_UNIXTIME(1578707612);
+---------------------------+
| FROM_UNIXTIME(1578707612) |
+---------------------------+
| 2020-01-11 09:53:32 |
+---------------------------+
1 row in set (0.01 sec)

PostgreSQL 中没有 DATETIME


由于有读者提到 PostgreSQL(PG) 的时间类型,因此这里拓展补充一下。PG 官方文档对时间类型的描述地址:http://www.postgresql.org/docs/curren…


PostgreSQL 时间类型总结


可以看到,PG 没有名为 DATETIME 的类型:



  • PG 的 TIMESTAMP WITHOUT TIME ZONE在功能上最接近 MySQL 的 DATETIME。它存储日期和时间,但不包含任何时区信息,存储的是字面值。

  • PG 的TIMESTAMP WITH TIME ZONE (或 TIMESTAMPTZ) 相当于 MySQL 的 TIMESTAMP。它在存储时会将输入值转换为 UTC,并在检索时根据当前会话的时区进行转换显示。


对于绝大多数需要记录精确发生时间点的应用场景,TIMESTAMPTZ是 PostgreSQL 中最推荐、最健壮的选择,因为它能最好地处理时区复杂性。


总结


MySQL 中时间到底怎么存储才好?DATETIME?TIMESTAMP?还是数值时间戳?


并没有一个银弹,很多程序员会觉得数值型时间戳是真的好,效率又高还各种兼容,但是很多人又觉得它表现的不够直观。


《高性能 MySQL 》这本神书的作者就是推荐 TIMESTAMP,原因是数值表示时间不够直观。下面是原文:



每种方式都有各自的优势,根据实际场景选择最合适的才是王道。下面再对这三种方式做一个简单的对比,以供大家实际开发中选择正确的存放时间的数据类型:


类型存储空间日期格式日期范围是否带时区信息
DATETIME5~8 字节YYYY-MM-DD hh:mm:ss[.fraction]1000-01-01 00:00:00[.000000] ~ 9999-12-31 23:59:59[.999999]
TIMESTAMP4~7 字节YYYY-MM-DD hh:mm:ss[.fraction]1970-01-01 00:00:01[.000000] ~ 2038-01-19 03:14:07[.999999]
数值型时间戳4 字节全数字如 15787076121970-01-01 00:00:01 之后的时间

选择建议小结:



  • TIMESTAMP 的核心优势在于其内建的时区处理能力。数据库负责 UTC 存储和基于会话时区的自动转换,简化了需要处理多时区应用的开发。如果应用需要处理多时区,或者希望数据库能自动管理时区转换,TIMESTAMP 是自然的选择(注意其时间范围限制,也就是 2038 年问题)。

  • 如果应用场景不涉及时区转换,或者希望应用程序完全控制时区逻辑,并且需要表示 2038 年之后的时间,DATETIME 是更稳妥的选择。

  • 如果极度关注比较性能,或者需要频繁跨系统传递时间数据,并且可以接受可读性的牺牲(或总是在应用层转换),数值时间戳是一个强大的选项。


作者:JavaGuide
来源:juejin.cn/post/7488927722774937609
收起阅读 »

websocket和socket有什么区别?

WebSocket 和 Socket 的区别 WebSocket 和 Socket 是两种不同的网络通信技术,它们在使用场景、协议、功能等方面有显著的差异。以下是它们之间的主要区别: 1. 定义 Socket:Socket 是一种网络通信的工具,可以实现不同...
继续阅读 »

WebSocket 和 Socket 的区别


WebSocket 和 Socket 是两种不同的网络通信技术,它们在使用场景、协议、功能等方面有显著的差异。以下是它们之间的主要区别:


1. 定义



  • Socket:Socket 是一种网络通信的工具,可以实现不同计算机之间的数据交换。它是操作系统提供的 API,广泛应用于 TCP/IP 网络编程中。Socket 可以是流式(TCP)或数据报(UDP)类型的,用于低层次的网络通信。

  • WebSocket:WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议。它允许服务器和客户端之间实时地交换数据。WebSocket 是建立在 HTTP 协议之上的,主要用于 Web 应用程序,以实现实时数据传输。


2. 协议层次



  • Socket:Socket 是一种底层通信机制,通常与 TCP/IP 协议一起使用。它允许开发者通过编程语言直接访问网络接口。

  • WebSocket:WebSocket 是一种应用层协议,建立在 HTTP 之上。在初始握手时,使用 HTTP 协议进行连接,之后切换到 WebSocket 协议进行数据传输。


3. 连接方式



  • Socket:Socket 通常需要手动管理连接的建立和关闭。通过调用相关的 API,开发者需要处理连接的状态,确保数据的可靠传输。

  • WebSocket:WebSocket 的连接管理相对简单。建立连接后,不需要频繁地进行握手,可以保持持久连接,随时进行数据交换。


4. 数据传输模式



  • Socket:Socket 可以实现单向或双向的数据传输,但通常需要在发送和接收之间进行明确的控制。

  • WebSocket:WebSocket 支持全双工通信,客户端和服务器之间可以随时互相发送数据,无需等待响应。这使得实时通信变得更加高效。


5. 适用场景



  • Socket:Socket 常用于需要高性能、低延迟的场景,如游戏开发、文件传输、P2P 网络等。由于其底层特性,Socket 适合对网络性能有严格要求的应用。

  • WebSocket:WebSocket 主要用于 Web 应用程序,如即时聊天、实时通知、在线游戏等。由于其易用性和高效性,WebSocket 特别适合需要实时更新和交互的前端应用。


6. 数据格式



  • Socket:Socket 发送的数据通常是二进制流或文本流,需要开发者自行定义数据格式和解析方式。

  • WebSocket:WebSocket 支持多种数据格式,包括文本(如 JSON)和二进制(如 Blob、ArrayBuffer)。WebSocket 的数据传输格式非常灵活,易于与 JavaScript 进行交互。


7. 性能



  • Socket:Socket 对于大量并发连接的处理性能较高,但需要开发者进行优化和管理。

  • WebSocket:WebSocket 在建立连接后可以保持长连接,减少了握手带来的延迟,适合高频率的数据交换场景。


8. 安全性



  • Socket:Socket 的安全性取决于使用的协议(如 TCP、UDP)和应用层的实现。开发者需要自行处理安全问题,如加密和身份验证。

  • WebSocket:WebSocket 支持通过 WSS(WebSocket Secure)进行加密,提供更高层次的安全保障。它可以很好地与 HTTPS 集成,确保数据在传输过程中的安全性。


9. 浏览器支持



  • Socket:Socket 是底层的网络通信技术,通常不直接在浏览器中使用。Web 开发者需要通过后端语言(如 Node.js、Java、Python)来实现 Socket 通信。

  • WebSocket:WebSocket 是专为 Web 应用设计的,所有现代浏览器均支持 WebSocket 协议,开发者可以直接在客户端使用 JavaScript API 进行通信。


10. 工具和库



  • Socket:使用 Socket 进行开发时,开发者通常需要使用底层网络编程库,如 BSD Sockets、Java Sockets、Python's socket 模块等。

  • WebSocket:WebSocket 提供了简单的 API,开发者可以使用原生 JavaScript 或第三方库(如 Socket.IO)轻松实现 WebSocket 通信。


结论


总结来说,WebSocket 是一种为现代 Web 应用量身定制的协议,具有实时、双向通信的优势,而 Socket 是一种底层的网络通信机制,提供更灵活的使用方式。选择使用哪种技术取决于具体的应用场景和需求。对于需要实时交互的 Web 应用,WebSocket 是更合适的选择;而对于底层或高性能要求的网络通信,Socket 提供了更多的控制和灵活性。


作者:Riesenzahn
来源:juejin.cn/post/7485631488114278454
收起阅读 »

完蛋,被扣工资了,都是JSON惹的祸

JSON是一种轻量级的数据交换格式,基于ECMAScript的一个子集设计,采用完全独立于编程语言的文本格式来表示数据。它易于人类阅读和编写,同时也便于机器解析和生成,这使得JSON在数据交换中具有高效性。‌ JSON也就成了每一个程序员每天都要使用一个小类库...
继续阅读 »

JSON是一种轻量级的数据交换格式,基于ECMAScript的一个子集设计,采用完全独立于编程语言的文本格式来表示数据。它易于人类阅读和编写,同时也便于机器解析和生成,这使得JSON在数据交换中具有高效性。‌


JSON也就成了每一个程序员每天都要使用一个小类库。无论你使用的谷歌的gson,阿里巴巴的fastjson,框架自带的jackjson,还是第三方的hutool的json等。总之,每天都要和他打交道。


但是,却在阴沟里翻了船。


1、平平无奇的接口


 /**
* 获取vehicleinfo 信息
*
* @RequestParam vehicleId
* @return Vehicle的json字符串
*/

String loadVehicleInfo(Integer vehicleId);

该接口就是通过一个vehicleId参数获取Vehicle对象,返回的数据是Vehicle的JSON字符串,也就是将获取的对象信息序列化成JSON字符串了。


2、无懈可击的引用


String jsonStr = auctVehicleService.loadVehicleInfo(freezeDetail.getVehicle().getId());
if (StringUtils.isNotBlank(jsonStr)) {
Vehicle vehicle = JSON.parseObject(jsonStr, Vehicle.class);
if (vehicle != null) {
// 后续省略 ...
}
}

看似无懈可击的引用,隐藏着魔鬼。为什么无懈可击,因为做了健壮性的判断,非空字符串、非空对象等的判断,根除了空指针异常。


但是,魔鬼隐藏在哪里呢?


3、故障引发



线上直接出现类似的故障(此报错信息为线下模拟)。


现在测试为什么没有问题:主要的测试了基础数据,测试的数据中恰好没有Date 类型的数据,所以线下没有测出来。


4、故障原因分析


从报错日志可以看出,是因为日期类型的参数导致的。Mar 24, 2025 1:23:10 PM 这样的日期格式无法使用Fastjson解析。


深入代码查看:


@Override
public String loadVehicleInfo(Integer vehicleId) {
String key = VEHICLE_KEY + vehicleId;
Object obj = cacheService.get(key);

if (null != obj && StringUtils.isNotEmpty(obj.toString())
&& !"null".equals(obj.toString())) {
String result = (String)obj;
return result;
}

String json = null;
try {
Vehicle vInfo = overrideVehicleAttributes(vehicleId);
// 使用了Gson序列化对象
json = gson.toJson(vInfo);
cacheService.setExpireSec(key, gson.toJson(vInfo), 5 * 60);
} catch (Exception e) {
cacheService.setExpireSec(key, "", 1 * 60);
} finally {
}

return json;
}

原来接口的实现里面采用了谷歌的Gson对返回的对象做了序列化。调用的地方又使用了阿里巴巴的Fastjson发序列化,导致参数解析异常。



完蛋,上榜是要被扣工资的!!!


5、小结


问题虽小,但是影响却很大。坊间一直讨论着,程序员为什么不能写出没有bug的程序。这也许是其中的一种答案吧。


肉疼,被扣钱了!!!


--END--




喜欢就点赞收藏,也可以关注我的微信公众号:编程朝花夕拾


作者:SimonKing
来源:juejin.cn/post/7485560281955958794
收起阅读 »

JDK 24 发布,新特性解读!

真快啊!Java 24 这两天已经正式发布啦!这是自 Java 21 以来的第三个非长期支持版本,和 Java 22、Java 23一样。 下一个长期支持版是 Java 25,预计今年 9 月份发布。 Java 24 带来的新特性还是蛮多的,一共 24 个。J...
继续阅读 »

真快啊!Java 24 这两天已经正式发布啦!这是自 Java 21 以来的第三个非长期支持版本,和 Java 22Java 23一样。


下一个长期支持版是 Java 25,预计今年 9 月份发布。


Java 24 带来的新特性还是蛮多的,一共 24 个。Java 23 和 Java 23 都只有 12 个,Java 24的新特性相当于这两次的总和了。因此,这个版本还是非常有必要了解一下的。


下图是从 JDK8 到 JDK 24 每个版本的更新带来的新特性数量和更新时间:



我在昨天晚上详细看了一下 Java 24 的详细更新,并对其中比较重要的新特性做了详细的解读,希望对你有帮助!


本文内容概览



JEP 478: 密钥派生函数 API(预览)


密钥派生函数 API 是一种用于从初始密钥和其他数据派生额外密钥的加密算法。它的核心作用是为不同的加密目的(如加密、认证等)生成多个不同的密钥,避免密钥重复使用带来的安全隐患。 这在现代加密中是一个重要的里程碑,为后续新兴的量子计算环境打下了基础


通过该 API,开发者可以使用最新的密钥派生算法(如 HKDF 和未来的 Argon2):


// 创建一个 KDF 对象,使用 HKDF-SHA256 算法
KDF hkdf = KDF.getInstance("HKDF-SHA256");

// 创建 Extract 和 Expand 参数规范
AlgorithmParameterSpec params =
HKDFParameterSpec.ofExtract()
.addIKM(initialKeyMaterial) // 设置初始密钥材料
.addSalt(salt) // 设置盐值
.thenExpand(info, 32); // 设置扩展信息和目标长度

// 派生一个 32 字节的 AES 密钥
SecretKey key = hkdf.deriveKey("AES", params);

// 可以使用相同的 KDF 对象进行其他密钥派生操作

JEP 483: 提前类加载和链接


在传统 JVM 中,应用在每次启动时需要动态加载和链接类。这种机制对启动时间敏感的应用(如微服务或无服务器函数)带来了显著的性能瓶颈。该特性通过缓存已加载和链接的类,显著减少了重复工作的开销,显著减少 Java 应用程序的启动时间。测试表明,对大型应用(如基于 Spring 的服务器应用),启动时间可减少 40% 以上。


这个优化是零侵入性的,对应用程序、库或框架的代码无需任何更改,启动也方式保持一致,仅需添加相关 JVM 参数(如 -XX:+ClassDataSharing)。


JEP 484: 类文件 API


类文件 API 在 JDK 22 进行了第一次预览(JEP 457),在 JDK 23 进行了第二次预览并进一步完善(JEP 466)。最终,该特性在 JDK 24 中顺利转正。


类文件 API 的目标是提供一套标准化的 API,用于解析、生成和转换 Java 类文件,取代过去对第三方库(如 ASM)在类文件处理上的依赖。


// 创建一个 ClassFile 对象,这是操作类文件的入口。
ClassFile cf = ClassFile.of();
// 解析字节数组为 ClassModel
ClassModel classModel = cf.parse(bytes);

// 构建新的类文件,移除以 "debug" 开头的所有方法
byte[] newBytes = cf.build(classModel.thisClass().asSymbol(),
classBuilder -> {
// 遍历所有类元素
for (ClassElement ce : classModel) {
// 判断是否为方法 且 方法名以 "debug" 开头
if (!(ce instanceof MethodModel mm
&& mm.methodName().stringValue().startsWith("debug"))) {
// 添加到新的类文件中
classBuilder.with(ce);
}
}
});

JEP 485: 流收集器


流收集器 Stream::gather(Gatherer) 是一个强大的新特性,它允许开发者定义自定义的中间操作,从而实现更复杂、更灵活的数据转换。Gatherer 接口是该特性的核心,它定义了如何从流中收集元素,维护中间状态,并在处理过程中生成结果。


与现有的 filtermapdistinct 等内置操作不同,Stream::gather 使得开发者能够实现那些难以用标准 Stream 操作完成的任务。例如,可以使用 Stream::gather 实现滑动窗口、自定义规则的去重、或者更复杂的状态转换和聚合。 这种灵活性极大地扩展了 Stream API 的应用范围,使开发者能够应对更复杂的数据处理场景。


基于 Stream::gather(Gatherer) 实现字符串长度的去重逻辑:


var result = Stream.of("foo", "bar", "baz", "quux")
.gather(Gatherer.ofSequential(
HashSet::new, // 初始化状态为 HashSet,用于保存已经遇到过的字符串长度
(set, str, downstream) -> {
if (set.add(str.length())) {
return downstream.push(str);
}
return true; // 继续处理流
}
))
.toList();// 转换为列表

// 输出结果 ==> [foo, quux]

JEP 486: 永久禁用安全管理器


JDK 24 不再允许启用 Security Manager,即使通过 java -Djava.security.manager命令也无法启用,这是逐步移除该功能的关键一步。虽然 Security Manager 曾经是 Java 中限制代码权限(如访问文件系统或网络、读取或写入敏感文件、执行系统命令)的重要工具,但由于复杂性高、使用率低且维护成本大,Java 社区决定最终移除它。


JEP 487: 作用域值 (第四次预览)


作用域值(Scoped Values)可以在线程内和线程间共享不可变的数据,优于线程局部变量,尤其是在使用大量虚拟线程时。


final static ScopedValue<...> V = new ScopedValue<>();

// In some method
ScopedValue.where(V, <value>)
.run(() -> { ... V.get() ... call methods ... });

// In a method called directly or indirectly from the lambda expression
... V.get() ...

作用域值允许在大型程序中的组件之间安全有效地共享数据,而无需求助于方法参数。


JEP 491: 虚拟线程的同步而不固定平台线程


优化了虚拟线程与 synchronized 的工作机制。 虚拟线程在 synchronized 方法和代码块中阻塞时,通常能够释放其占用的操作系统线程(平台线程),避免了对平台线程的长时间占用,从而提升应用程序的并发能力。 这种机制避免了“固定 (Pinning)”——即虚拟线程长时间占用平台线程,阻止其服务于其他虚拟线程的情况。


现有的使用 synchronized 的 Java 代码无需修改即可受益于虚拟线程的扩展能力。 例如,一个 I/O 密集型的应用程序,如果使用传统的平台线程,可能会因为线程阻塞而导致并发能力下降。 而使用虚拟线程,即使在 synchronized 块中发生阻塞,也不会固定平台线程,从而允许平台线程继续服务于其他虚拟线程,提高整体的并发性能。


JEP 493:在没有 JMOD 文件的情况下链接运行时镜像


默认情况下,JDK 同时包含运行时镜像(运行时所需的模块)和 JMOD 文件。这个特性使得 jlink 工具无需使用 JDK 的 JMOD 文件就可以创建自定义运行时镜像,减少了 JDK 的安装体积(约 25%)。


说明:



  • Jlink 是随 Java 9 一起发布的新命令行工具。它允许开发人员为基于模块的 Java 应用程序创建自己的轻量级、定制的 JRE。

  • JMOD 文件是 Java 模块的描述文件,包含了模块的元数据和资源。


JEP 495: 简化的源文件和实例主方法(第四次预览)


这个特性主要简化了 main 方法的的声明。对于 Java 初学者来说,这个 main 方法的声明引入了太多的 Java 语法概念,不利于初学者快速上手。


没有使用该特性之前定义一个 main 方法:


public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}

使用该新特性之后定义一个 main 方法:


class HelloWorld {
void main() {
System.out.println("Hello, World!");
}
}

进一步简化(未命名的类允许我们省略类名)


void main() {
System.out.println("Hello, World!");
}

JEP 497: 量子抗性数字签名算法 (ML-DSA)


JDK 24 引入了支持实施抗量子的基于模块晶格的数字签名算法 (Module-Lattice-Based Digital Signature Algorithm, ML-DSA),为抵御未来量子计算机可能带来的威胁做准备。


ML-DSA 是美国国家标准与技术研究院(NIST)在 FIPS 204 中标准化的量子抗性算法,用于数字签名和身份验证。


JEP 498: 使用 sun.misc.Unsafe 内存访问方法时发出警告


JDK 23(JEP 471) 提议弃用 sun.misc.Unsafe 中的内存访问方法,这些方法将来的版本中会被移除。在 JDK 24 中,当首次调用 sun.misc.Unsafe 的任何内存访问方法时,运行时会发出警告。


这些不安全的方法已有安全高效的替代方案:



  • java.lang.invoke.VarHandle :JDK 9 (JEP 193) 中引入,提供了一种安全有效地操作堆内存的方法,包括对象的字段、类的静态字段以及数组元素。

  • java.lang.foreign.MemorySegment :JDK 22 (JEP 454) 中引入,提供了一种安全有效地访问堆外内存的方法,有时会与 VarHandle 协同工作。


这两个类是 Foreign Function & Memory API(外部函数和内存 API) 的核心组件,分别用于管理和操作堆外内存。Foreign Function & Memory API 在 JDK 22 中正式转正,成为标准特性。


import jdk.incubator.foreign.*;
import java.lang.invoke.VarHandle;

// 管理堆外整数数组的类
class OffHeapIntBuffer {

// 用于访问整数元素的VarHandle
private static final VarHandle ELEM_VH = ValueLayout.JAVA_INT.arrayElementVarHandle();

// 内存管理器
private final Arena arena;

// 堆外内存段
private final MemorySegment buffer;

// 构造函数,分配指定数量的整数空间
public OffHeapIntBuffer(long size) {
this.arena = Arena.ofShared();
this.buffer = arena.allocate(ValueLayout.JAVA_INT, size);
}

// 释放内存
public void deallocate() {
arena.close();
}

// 以volatile方式设置指定索引的值
public void setVolatile(long index, int value) {
ELEM_VH.setVolatile(buffer, 0L, index, value);
}

// 初始化指定范围的元素为0
public void initialize(long start, long n) {
buffer.asSlice(ValueLayout.JAVA_INT.byteSize() * start,
ValueLayout.JAVA_INT.byteSize() * n)
.fill((byte) 0);
}

// 将指定范围的元素复制到新数组
public int[] copyToNewArray(long start, int n) {
return buffer.asSlice(ValueLayout.JAVA_INT.byteSize() * start,
ValueLayout.JAVA_INT.byteSize() * n)
.toArray(ValueLayout.JAVA_INT);
}
}

JEP 499: 结构化并发(第四次预览)


JDK 19 引入了结构化并发,一种多线程编程方法,目的是为了通过结构化并发 API 来简化多线程编程,并不是为了取代java.util.concurrent,目前处于孵化器阶段。


结构化并发将不同线程中运行的多个任务视为单个工作单元,从而简化错误处理、提高可靠性并增强可观察性。也就是说,结构化并发保留了单线程代码的可读性、可维护性和可观察性。


结构化并发的基本 API 是StructuredTaskScope,它支持将任务拆分为多个并发子任务,在它们自己的线程中执行,并且子任务必须在主任务继续之前完成。


StructuredTaskScope 的基本用法如下:


    try (var scope = new StructuredTaskScope<Object>()) {
// 使用fork方法派生线程来执行子任务
Future<Integer> future1 = scope.fork(task1);
Future<String> future2 = scope.fork(task2);
// 等待线程完成
scope.join();
// 结果的处理可能包括处理或重新抛出异常
... process results/exceptions ...
} // close

结构化并发非常适合虚拟线程,虚拟线程是 JDK 实现的轻量级线程。许多虚拟线程共享同一个操作系统线程,从而允许非常多的虚拟线程。


Java 新特性系列解读


如果你想系统了解 Java 8 以及之后版本的新特性,可以在 JavaGuide 上阅读对应的文章:



比较推荐这几篇:



作者:JavaGuide
来源:juejin.cn/post/7483478667143626762
收起阅读 »

年少不知自增好,错把UUID当个宝!!!

在 MySQL 中,使用 UUID 作为主键 在大表中可能会导致性能问题,尤其是在插入和修改数据时效率较低。以下是详细的原因分析,以及为什么修改数据会导致索引刷新,以及字符主键为什么效率较低。 1. UUID 作为主键的问题 (1)UUID 的特性 UUI...
继续阅读 »

在 MySQL 中,使用 UUID 作为主键 在大表中可能会导致性能问题,尤其是在插入和修改数据时效率较低。以下是详细的原因分析,以及为什么修改数据会导致索引刷新,以及字符主键为什么效率较低。




1. UUID 作为主键的问题


(1)UUID 的特性



  • UUID 是一个 128 位的字符串,通常表示为 36 个字符(例如:550e8400-e29b-41d4-a716-446655440000)。

  • UUID 是全局唯一的,适合分布式系统中生成唯一标识。


(2)UUID 作为主键的缺点


1. 索引效率低


  • 索引大小:UUID 是字符串类型,占用空间较大(36 字节),而整型主键(如 BIGINT)仅占用 8 字节。索引越大,存储和查询的效率越低。

  • 索引分裂:UUID 是无序的,插入新数据时,可能会导致索引树频繁分裂和重新平衡,影响性能。


2. 插入性能差


  • 随机性:UUID 是无序的,每次插入新数据时,新记录可能会插入到索引树的任意位置,导致索引树频繁调整。

  • 页分裂:InnoDB 存储引擎使用 B+ 树作为索引结构,随机插入会导致页分裂,增加磁盘 I/O 操作。


3. 查询性能差


  • 比较效率低:字符串比较比整型比较慢,尤其是在大表中,查询性能会显著下降。

  • 索引扫描范围大:UUID 索引占用的空间大,导致索引扫描的范围更大,查询效率降低。




2. 修改数据导致索引刷新的原因


(1)索引的作用



  • 索引是为了加速查询而创建的数据结构(如 B+ 树)。

  • 当数据被修改时,索引也需要同步更新,以保持数据的一致性。


(2)修改数据对索引的影响



  • 更新主键



    • 如果修改了主键值,MySQL 需要删除旧的主键索引记录,并插入新的主键索引记录。

    • 这个过程会导致索引树的调整,增加磁盘 I/O 操作。



  • 更新非主键列



    • 如果修改的列是索引列(如唯一索引、普通索引),MySQL 需要更新对应的索引记录。

    • 这个过程也会导致索引树的调整。




(3)UUID 主键的额外开销



  • 由于 UUID 是无序的,修改主键值时,新值可能会插入到索引树的不同位置,导致索引树频繁调整。

  • 相比于有序的主键(如自增 ID),UUID 主键的修改操作代价更高。




3. 字符主键导致效率降低的原因


(1)存储空间大



  • 字符主键(如 UUID)占用的存储空间比整型主键大。

  • 索引的大小直接影响查询性能,索引越大,查询时需要的磁盘 I/O 操作越多。


(2)比较效率低



  • 字符串比较比整型比较慢,尤其是在大表中,查询性能会显著下降。

  • 例如,WHERE id = '550e8400-e29b-41d4-a716-446655440000' 的效率低于 WHERE id = 12345


(3)索引分裂



  • 字符主键通常是无序的,插入新数据时,可能会导致索引树频繁分裂和重新平衡,影响性能。




4. 如何优化 UUID 主键的性能


(1)使用有序 UUID



  • 使用有序 UUID(如 UUIDv7),减少索引分裂和页分裂。

  • 有序 UUID 的生成方式可以基于时间戳,保证插入顺序。


(2)将 UUID 存储为二进制



  • 将 UUID 存储为 BINARY(16) 而不是 CHAR(36),减少存储空间。


    CREATE TABLE users (
    id BINARY(16) PRIMARY KEY,
    name VARCHAR(255)
    );



(3)使用自增主键 + UUID



  • 使用自增主键作为物理主键,UUID 作为逻辑主键。


    CREATE TABLE users (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    uuid CHAR(36) UNIQUE,
    name VARCHAR(255)
    );



(4)分区表



  • 对大表进行分区,减少单个索引树的大小,提高查询性能。




~Summary



  • UUID 作为主键的缺点



    • 索引效率低,插入和查询性能差。

    • 修改数据时,索引需要频繁刷新,导致性能下降。



  • 字符主键效率低的原因



    • 存储空间大,比较效率低,索引分裂频繁。



  • 优化建议



    • 使用有序 UUID 或二进制存储。

    • 结合自增主键和 UUID。

    • 对大表进行分区。




作者:码农liuxin
来源:juejin.cn/post/7478495083374559270
收起阅读 »

如何限制一个账号只能在一处登录

如何限制一个账号只能在一处登录要实现账号单点登录(一处登录限制),需结合 会话管理、实时状态同步 和 冲突处理机制。以下是完整技术方案:一、核心实现方案服务端会话控制(推荐)// 用户登录时生成唯一令牌并记录 public Str...
继续阅读 »

如何限制一个账号只能在一处登录

要实现账号单点登录(一处登录限制),需结合 会话管理实时状态同步 和 冲突处理机制。以下是完整技术方案:

一、核心实现方案

  1. 服务端会话控制(推荐)
// 用户登录时生成唯一令牌并记录
public String login(String username, String password) {
// 1. 验证账号密码
User user = userService.authenticate(username, password);

// 2. 生成新令牌并失效旧会话
String newToken = UUID.randomUUID().toString();
redis.del("user:" + user.getId() + ":token"); // 清除旧token
redis.setex("user:" + user.getId() + ":token", 3600, newToken);

// 3. 返回新令牌
return newToken;
}
  1. WebSocket实时踢出(增强体验)
// 前端建立长连接
const socket = new WebSocket(`wss://api.example.com/ws?token=${token}`);

socket.onmessage = (event) => {
if (event.data === 'force_logout') {
alert('您的账号在其他设备登录');
location.href = '/logout';
}
};
  1. 登录设备指纹识别
// 生成设备指纹(前端)
function generateDeviceFingerprint() {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
ctx.textBaseline = 'top';
ctx.font = "14px Arial";
ctx.fillText("BrowserFingerprint", 2, 2);
return canvas.toDataURL().hashCode(); // 简化示例
}

// 服务端校验
if (storedFingerprint != currentFingerprint) {
forceLogout(storedToken);
}

二、多端适配策略

客户端类型实现方案
Web浏览器JWT令牌 + Redis黑名单
移动端APP设备ID绑定 + FCM/iMessage推送踢出
桌面应用硬件指纹 + 本地令牌失效检测
微信小程序UnionID绑定 + 服务端订阅消息

三、关键代码实现

  1. JWT令牌增强方案
// 生成带设备信息的JWT
public String generateToken(User user, String deviceId) {
return Jwts.builder()
.setSubject(user.getId())
.claim("device", deviceId) // 绑定设备
.setExpiration(new Date(System.currentTimeMillis() + 3600000))
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}

// 校验令牌时检查设备
public boolean validateToken(String token, String currentDevice) {
Claims claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
return claims.get("device").equals(currentDevice);
}
  1. Redis实时状态管理
# 使用Redis Hash存储登录状态
def login(user_id, token, device_info):
# 删除该用户所有活跃会话
r.delete(f"user_sessions:{user_id}")

# 记录新会话
r.hset(f"user_sessions:{user_id}",
mapping={
"token": token,
"device": device_info,
"last_active": datetime.now()
})
r.expire(f"user_sessions:{user_id}", 3600)

# 中间件校验
def check_token(request):
user_id = get_user_id_from_token(request.token)
stored_token = r.hget(f"user_sessions:{user_id}", "token")
if stored_token != request.token:
raise ForceLogoutError()

四、异常处理机制

场景处理方案
网络延迟冲突采用CAS(Compare-And-Swap)原子操作更新令牌
令牌被盗用触发二次验证(短信/邮箱验证码)
多设备同时登录后登录者优先,前会话立即失效(可配置为保留第一个登录)

五、性能与安全优化

  1. 会话同步优化

    # Redis Pub/Sub 跨节点同步
    PUBLISH user:123 "LOGOUT"
  2. 安全增强

    // 前端敏感操作二次确认
    function sensitiveOperation() {
    if (loginTime < lastServerCheckTime) {
    showReauthModal();
    }
    }
  3. 监控看板

    指标报警阈值
    并发登录冲突率>5%/分钟
    强制踢出成功率<99%

六、行业实践参考

  1. 金融级方案

    • 每次操作都验证设备指纹
    • 异地登录需视频人工审核
  2. 社交应用方案

    • 允许最多3个设备在线
    • 分设备类型控制(手机+PC+平板)
  3. ERP系统方案

    • 绑定特定MAC地址
    • VPN网络白名单限制

通过以上方案可实现:

  • 严格模式:后登录者踢出前会话(适合银行系统)
  • 宽松模式:多设备在线但通知告警(适合社交应用)
  • 混合模式:关键操作时强制单设备(适合电商系统)

部署建议:

  1. 根据业务需求选择合适严格度
  2. 关键系统增加异地登录二次验证
  3. 用户界面明确显示登录设备列表

作者:Epicurus
来源:juejin.cn/post/7485384798569250868

收起阅读 »

Sa-Token v1.41.0 发布 🚀,来看看有没有令你心动的功能!

Sa-Token 是一个轻量级 Java 权限认证框架,主要解决:登录认证、权限认证、单点登录、OAuth2.0、微服务网关鉴权 等一系列权限相关问题。🔐 目前最新版本 v1.41.0 已推送至 Maven 中央仓库 🎉,大家可以通过如下方式引入: <!...
继续阅读 »

Sa-Token 是一个轻量级 Java 权限认证框架,主要解决:登录认证权限认证单点登录OAuth2.0微服务网关鉴权 等一系列权限相关问题。🔐


目前最新版本 v1.41.0 已推送至 Maven 中央仓库 🎉,大家可以通过如下方式引入:


<!-- Sa-Token 权限认证 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.41.0</version>
</dependency>

该版本包含大量 ⛏️️️新增特性、⛏️底层重构、⛏️️️代码优化 等,下面容我列举几条比较重要的更新内容供大家参阅:


🛡️ 更新点1:防火墙模块新增 hooks 扩展机制


本次更新针对防火墙新增了多条校验规则,之前的规则为:



  • path 白名单放行。

  • path 黑名单拦截。

  • path 危险字符校验。


本次新增规则为:



  • path 禁止字符校验。

  • path 目录遍历符检测(优化了检测算法)。

  • 请求 host 检测。

  • 请求 Method 检测。

  • 请求 Header 头检测。

  • 请求参数检测。


并且本次更新开放了 hooks 机制,允许开发者注册自定义的校验规则 🛠️,参考如下:


@PostConstruct
public void saTokenPostConstruct() {
// 注册新 hook 演示,拦截所有带有 pwd 参数的请求,拒绝响应
SaFirewallStrategy.instance.registerHook((req, res, extArg)->{
if(req.getParam("pwd") != null) {
throw new FirewallCheckException("请求中不可包含 pwd 参数");
}
});
}

文档直达地址:Sa-Token 防火墙 🔗


💡 更新点2:新增基于 SPI 机制的插件体系


之前在 Sa-Token 中也有插件体系,不过都是利用 SpringBoot 的 SPI 机制完成组件注册的。


这种注册机制有一个问题,就是插件只能在 SpringBoot 环境下正常工作,在其它环境,比如 Solon 项目中,就只能手动注册插件才行 😫。


也就是说,严格来讲,这些插件只能算是 SpringBoot 的插件,而非 Sa-Token 框架的插件 🌐。


为了提高插件的通用性,Sa-Token 设计了自己的 SPI 机制,使得这些插件可以在更多的项目环境下正常工作 🚀。


第一步:实现插件注册类,此类需要 implements SaTokenPlugin 接口 👨💻:


/**
* SaToken 插件安装:插件作用描述
*/

public class SaTokenPluginForXxx implements SaTokenPlugin {
@Override
public void install() {
// 书写需要在项目启动时执行的代码,例如:
// SaManager.setXxx(new SaXxxForXxx());
}
}

第二步:在项目的 resources\META-INF\satoken\ 文件夹下 📂 创建 cn.dev33.satoken.plugin.SaTokenPlugin 文件,内容为该插件注册类的完全限定名:


cn.dev33.satoken.plugin.SaTokenPluginForXxx

这样便可以在项目启动时,被 Sa-Token 插件管理器加载到此插件,执行插件注册类的 install 方法,完成插件安装 ✅。


文档直达地址:Sa-Token 插件开发指南 🔗


🎛️ 更新点3:重构缓存体系,将数据读写与序列化操作分离


在之前的版本中,Redis 集成通常和具体的序列化方式耦合在一起,这不仅让 Redis 相关插件产生大量的重复冗余代码,也让大家在选择 Redis 插件时严重受限。⚠️


本次版本更新彻底重构了此模块,将数据读写与序列化操作分离,使其每一块都可以单独自定义实现类,做到灵活扩展 ✨,例如:



  • 1️⃣ SaTokenDao 数据读写可以选择:RedisTemplate、Redisson、ConcurrentHashMap、Hutool-Timed-Cache 等不同实现类。

  • 2️⃣ SaSerializerTemplate 序列化器可以选择:Base64编码、Hex编码、ISO-8859-1编码、JSON序列化等不同方式。

  • 3️⃣ JSON 序列化可以选择:Jackson、Fastjson、Snack3 等组件。


所有实现类均可以按需选择,自由搭配,大大提高灵活性🏗️。


⚙️️ 更新点4:SaLoginParameter 登录参数类新增大量配置项


SaLoginParameter (前SaLoginModel) 用于控制登录操作中的部分细节行为,本次新增的配置项有:



  • isConcurrent:决定是否允许同一账号多地同时登录(为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)。🌍

  • isShare:在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token)。🔄

  • maxLoginCount:同一账号最大登录数量,超出此数量的客户端将被自动注销,-1代表不限制数量。🚫

  • maxTryTimes:在创建 token 时的最高循环次数,用于保证 token 唯一性(-1=不循环尝试,直接使用。⏳

  • deviceId:此次登录的客户端设备id,用于判断后续某次登录是否为可信任设备。📱

  • terminalExtraData:本次登录挂载到 SaTerminalInfo 的自定义扩展数据。📦


以上大部分配置项在之前的版本中也有支持,不过它们都被定义在了全局配置类 SaTokenConfig 之上,本次更新支持在 SaLoginParameter 中定义这些配置项,
这将让登录策略的控制变得更加灵活。✨


🚪 更新点5:新增 SaLogoutParameter 注销参数类


SaLogoutParameter 用于控制注销操作中的部分细节行为️,例如:


通过 Range 参数决定注销范围 🎯:


// 注销范围: TOKEN=只注销当前 token 的会话,ACCOUNT=注销当前 token 指向的 loginId 其所有客户端会话
StpUtil.logout(new SaLogoutParameter().setRange(SaLogoutRange.TOKEN));

通过 DeviceType 参数决定哪些登录设备类型参与注销 💻:


// 指定 10001 账号,所有 PC 端注销下线,其它端如 APP 端不受影响 
StpUtil.logout(10001, new SaLogoutParameter().setDeviceType("PC"));

还有其它参数此处暂不逐一列举,文档直达地址:Sa-Token 登录参数 & 注销参数 🔗


🐞 更新点6:修复 StpUtil.setTokenValue("xxx")loginParameter.getIsWriteHeader() 空指针的问题。


这个没啥好说的,有 bug 🐛 必须修复。


fix issue:#IBKSM0 🔗


✨ 更新点7:API 参数签名模块升级



  • 1、新增了 @SaCheckSign 注解,现在 API 参数签名模块也支持注解鉴权了。🆕

  • 2、新增自定义签名的摘要算法,现在不仅可以 md5 算法计算签名,也支持 sha1、sha256 等算法了。🔐

  • 3、新增多应用模式:


多应用模式就是指,允许在对接多个系统时分别使用不同的秘钥等配置项,配置示例如下 📝:


sa-token: 
# API 签名配置 多应用模式
sign-many:
# 应用1
xm-shop:
secret-key: 0123456789abcdefg
digest-algo: md5
# 应用2
xm-forum:
secret-key: 0123456789hijklmnopq
digest-algo: sha256
# 应用3
xm-video:
secret-key: 12341234aaaaccccdddd
digest-algo: sha512

然后在签名时通过指定 appid 的方式获取对应的 SignTemplate 进行操作 👨💻:


// 创建签名示例
String paramStr = SaSignMany.getSignTemplate("xm-shop").addSignParamsAndJoin(paramMap);

// 校验签名示例
SaSignMany.getSignTemplate("xm-shop").checkRequest(SaHolder.getRequest());

⚡ 更新点8:新增 sa-token-caffeine 插件,用于整合 Caffeine


Caffeine 是一个基于 Java 的高性能本地缓存库,本次新增 sa-token-caffeine 插件用于将 Caffeine 作为 Sa-Token 的缓存层,存储会话鉴权数据。🚀
这进一步丰富了 Sa-Token 的缓存层插件生态。🌱


<!-- Sa-Token 整合 Caffeine -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-caffeine</artifactId>
<version>1.41.0</version>
</dependency>

🎪 更新点9:新增 sa-token-serializer-features 序列化扩展包


引入此插件可以为 Sa-Token 提供一些有意思的序列化方案。(娱乐向,不建议上生产 🎭)


例如:以base64 编码,采用:元素周期表 🧪、特殊符号 🔣、或 emoji 😊 作为元字符集存储数据 :


sa-custom-serializer-yszqb.png


sa-custom-serializer-tsfh.png


sa-custom-serializer-emoji.png


sa-custom-serializer-emoji2.png


📜 完整更新日志


除了以上提到的几点以外,还有更多更新点无法逐一详细介绍,下面是 v1.41.0 版本的完整更新日志:



  • core:

    • 修复:修复 StpUtil.setTokenValue("xxx")loginParameter.getIsWriteHeader() 空指针的问题。 fix: #IBKSM0

    • 修复:将 SaDisableWrapperInfo.createNotDisabled() 默认返回值封禁等级改为 -2,以保证向之前版本兼容。

    • 新增:新增基于 SPI 的插件体系。 [重要]

    • 重构:JSON 转换器模块。 [重要]

    • 新增:新增 serializer 序列化模块,控制 ObjectString 的序列化方式。 [重要]

    • 重构:重构防火墙模块,增加 hooks 机制。 [重要]

    • 新增:防火墙新增:请求 path 禁止字符校验、Host 检测、请求 Method 检测、请求头检测、请求参数检测。重构目录遍历符检测算法。

    • 重构:重构 SaTokenDao 模块,将序列化与存储操作分离。 [重要]

    • 重构:重构 SaTokenDao 默认实现类,优化底层设计。

    • 新增:isLastingCookie 配置项支持在全局配置中定义了。

    • 重构:SaLoginModel -> SaLoginParameter[不向下兼容]

    • 重构:TokenSign -> SaTerminalInfo[不向下兼容]

    • 新增:SaTerminalInfo 新增 extraData 自定义扩展数据设置。

    • 新增:SaLoginParameter 支持配置 isConcurrentisSharemaxLoginCountmaxTryTimes

    • 新增:新增 SaLogoutParameter,用于控制注销会话时的各种细节。 [重要]

    • 新增:新增 StpLogic#isTrustDeviceId 方法,用于判断指定设备是否为可信任设备。

    • 新增:新增 StpUtil.getTerminalListByLoginId(loginId)StpUtil.forEachTerminalList(loginId) 方法,以更方便的实现单账号会话管理。

    • 升级:API 参数签名配置支持自定义摘要算法。

    • 新增:新增 @SaCheckSign 注解鉴权,用于 API 签名参数校验。

    • 新增:API 参数签名模块新增多应用模式。 fix: #IAK2BI, #I9SPI1, #IAC0P9 [重要]

    • 重构:全局配置 is-share 默认值改为 false。 [不向下兼容]

    • 重构:踢人下线、顶人下线默认将删除对应的 token-session 对象。

    • 优化:优化注销会话相关 API。

    • 重构:登录默认设备类型值改为 DEF。 [不向下兼容]

    • 重构:BCrypt 标注为 @Deprecated

    • 新增:sa-token-quick-login 支持 SpringBoot3 项目。 fix: #IAFQNE#673

    • 新增:SaTokenConfig 新增 replacedRangeoverflowLogoutModelogoutRangeisLogoutKeepFreezeOpsisLogoutKeepTokenSession 配置项。



  • OAuth2:

    • 重构:重构 sa-token-oauth2 插件,使注解鉴权处理器的注册过程改为 SPI 插件加载。



  • 插件:

    • 新增:sa-token-serializer-features 插件,用于实现各种形式的自定义字符集序列化方案。

    • 新增:sa-token-fastjson 插件。

    • 新增:sa-token-fastjson2 插件。

    • 新增:sa-token-snack3 插件。

    • 新增:sa-token-caffeine 插件。



  • 单元测试:

    • 新增:sa-token-json-test json 模块单元测试。

    • 新增:sa-token-serializer-test 序列化模块单元测试。



  • 文档:

    • 新增:QA “多个项目共用同一个 redis,怎么防止冲突?”

    • 优化:补全 OAuth2 模块遗漏的相关配置项。

    • 优化:优化 OAuth2 简述章节描述文档。

    • 优化:完善 “SSO 用户数据同步 / 迁移” 章节文档。

    • 修正:补全项目目录结构介绍文档。

    • 新增:文档新增 “登录参数 & 注销参数” 章节。

    • 优化:优化“技术求助”按钮的提示文字。

    • 新增:新增 preview-doc.bat 文件,一键启动文档预览。

    • 完善:完善 Redis 集成文档。

    • 新增:新增单账号会话查询的操作示例。

    • 新增:新增顶人下线 API 介绍。

    • 新增:新增 自定义序列化插件 章节。



  • 其它:

    • 新增:新增 sa-token-demo/pom.xml 以便在 idea 中一键导入所有 demo 项目。

    • 删除:删除不必要的 .gitignore 文件

    • 重构:重构 sa-token-solon-plugin 插件。

    • 新增:新增设备锁登录示例。




更新日志在线文档直达链接:sa-token.cc/doc.html#/m…


🌟 其它


代码仓库地址:gitee.com/dromara/sa-…


框架功能结构图:


js


作者:省长
来源:juejin.cn/post/7484191942358499368
收起阅读 »

这个排队系统设计碉堡了

先赞后看,Java进阶一大半 各位好,我是南哥。 我在网上看到某厂最后一道面试题:如何设计一个排队系统? 关于系统设计的问题,大家还是要多多思考,可能这道题考的不是针对架构师的职位,而是关于你的业务设计能力。如果单单只会用开源软件的API,那似乎我们的竞争力...
继续阅读 »

先赞后看,Java进阶一大半



各位好,我是南哥。


我在网上看到某厂最后一道面试题:如何设计一个排队系统?


关于系统设计的问题,大家还是要多多思考,可能这道题考的不是针对架构师的职位,而是关于你的业务设计能力。如果单单只会用开源软件的API,那似乎我们的竞争力还可以再强些。学习设计东西、创作东西,把我们设计的产品给别人用,那竞争力一下子提了上来。


15岁的初中生开源了 AI 一站式 B/C 端解决方案chatnio,该产品在上个月被以几百万的价格收购了。这值得我们思考,程序创造力、设计能力在未来会变得越来越重要。


在这里插入图片描述



⭐⭐⭐收录在《Java学习/进阶/面试指南》:https://github/JavaSouth



精彩文章推荐



1.1 数据结构


排队的一个特点是一个元素排在另一个元素的后面,形成条状的队列。List结构、LinkedList链表结构都可以满足排队的业务需求,但如果这是一道算法题,我们要考虑的是性能因素。


排队并不是每个人都老老实实排队,现实会有多种情况发生,例如有人退号,那属于这个人的元素要从队列中删除;特殊情况安排有人插队,那插入位置的后面那批元素都要往后挪一挪。结合这个情况用LinkedList链表结构会更加合适,相比于List,LinkedList的性能优势就是增、删的效率更优。


但我们这里做的是一个业务系统,采用LinkedList这个结构也可以,不过要接受修改、维护起来困难,后面接手程序的人难以理解。大家都知道,在实际开发我们更常用List,而不是LinkedList。


List数据结构我更倾向于把它放在Redis里,有以下好处。


(1)数据存储与应用程序拆分。放在应用程序内存里,如果程序崩溃,那整条队列数据都会丢失。


(2)性能更优。相比于数据库存储,Redis处理数据的性能更加优秀,结合排队队列排完则销毁的特点,甚至可以不存储到数据库。可以补充排队记录到数据库里。


简单用Redis命令模拟下List结构排队的处理。


# 入队列(将用户 ID 添加到队列末尾)
127.0.0.1:6379> RPUSH queue:large user1
127.0.0.1:6379> RPUSH queue:large user2

#
出队列(将队列的第一个元素出队)
127.0.0.1:6379> LPOP queue:large

#
退号(从队列中删除指定用户 ID)
127.0.0.1:6379> LREM queue:large 1 user2

#
插队(将用户 ID 插入到指定位置,假设在 user1 之前插入 user3)
127.0.0.1:6379> LINSERT queue:large BEFORE user1 user3

1.2 业务功能


先给大家看看,南哥用过的费大厨的排队系统,它是在公众号里进行排队。


我们可以看到自己现在的排队进度。


在这里插入图片描述


同时每过 10 号,公众号会进行推送通知;如果 10 号以内,每过 1 号会微信公众号通知用户实时排队进度。最后每过 1 号就通知挺人性化,安抚用户排队的焦急情绪。


在这里插入图片描述


总结下来,我们梳理下功能点。虽然上面看起来是简简单单的查看、通知,背后可能隐藏许多要实现的功能。


在这里插入图片描述


1.3 后台端


(1)排队开始


后台管理员创建排队活动,后端在Redis创建List类型的数据结构,分别创建大桌、中桌、小桌三条队列,同时设置没有过期时间。


// 创建排队接口
@Service
public class QueueManagementServiceImpl {

@Autowired
private RedisTemplate<String, String> redisTemplate;

// queueType为桌型
public void createQueue(String queueType) {
String queueKey = "queue:" + queueType;
redisTemplate.delete(queueKey); // 删除队列,保证队列重新初始化
}
}


(2)排队操作


前面顾客用餐完成后,后台管理员点击下一号,在Redis的表现为把第一个元素从List中踢出,次数排队的总人数也减 1。


// 排队操作
@Service
public class QueueManagementServiceImpl {

@Autowired
private RedisTemplate<String, String> redisTemplate;

/**
* 将队列中的第一个用户出队
*/

public void dequeueNextUser(String queueType) {
String queueKey = "queue:" + queueType;
String userId = redisTemplate.opsForList().leftPop(queueKey);
}
}

1.4 用户端


(1)点击排队


用户点击排队,把用户标识添加到Redis队列中。


// 用户排队
@Service
public class QueueServiceImpl {

@Autowired
private RedisTemplate<String, String> redisTemplate;

public void enterQueue(String queueType, String userId) {
String queueKey = "queue:" + queueType;
redisTemplate.opsForList().rightPush(queueKey, userId);
log.info("用户 " + userId + " 已加入 " + queueType + " 队列");
}
}


(2)排队进度


用户可以查看三条队列的总人数情况,直接从Redis三条队列中查询队列个数。此页面不需要实时刷新,当然可以用WebSocket实时刷新或者长轮询,但具备了后面的用户通知功能,这个不实现也不影响用户体验。


而用户的个人排队进度,则计算用户所在队列前面的元素个数。


// 查询排队进度
@Service
public class QueueServiceImpl {

@Autowired
private RedisTemplate<String, String> redisTemplate;

public long getUserPositionInQueue(String queueType, String userId) {
String queueKey = "queue:" + queueType;
List<String> queue = redisTemplate.opsForList().range(queueKey, 0, -1);
if (queue != null) {
return queue.indexOf(userId);
}
return -1;
}
}


(3)用户通知


当某一个顾客用餐完成后,后台管理员点击下一号。此时后续的后端逻辑应该包括用户通知。


从三个队列里取出当前用户进度是 10 的倍数的元素,微信公众号通知该用户现在是排到第几桌了。


从三个队列里取出排名前 10 的元素,微信公众号通知该用户现在的进度。


// 用户通知
@Service
public class NotificationServiceImpl {

@Autowired
private RedisTemplate<String, String> redisTemplate;

private void notifyUsers(String queueType) {
String queueKey = "queue:" + queueType;
// 获取当前队列中的所有用户
List<String> queueList = jedis.lrange(queueKey, 0, -1);

// 通知排在10的倍数的用户
for (int i = 0; i < queueList.size(); i++) {
if ((i + 1) % 10 == 0) {
String userId = queueList.get(i);
sendNotification(userId, "您的排队进度是第 " + (i + 1) + " 位,请稍作准备!");
}
}

// 通知前10位用户
int notifyLimit = Math.min(10, queueList.size()); // 避免队列小于10时出错
for (int i = 0; i < notifyLimit; i++) {
String userId = queueList.get(i);
sendNotification(userId, "您已经在前 10 位,准备好就餐!");
}
}
}

这段逻辑应该移动到前面后台端的排队操作。


1.5 存在问题


上面的业务情况,实际上排队人员不会太多,一般会比较稳定。但如果每一条队列人数激增的情况下,可以预见到会有问题了。


对于Redis的List结构,我们需要查询某一个元素的排名情况,最坏情况下需要遍历整条队列,时间复杂度是O(n),而查询用户排名进度这个功能又是经常使用到。


对于上面情况,我们可以选择Redis另一种数据结构:Zset。有序集合类型Zset可以在O(lgn)的时间复杂度判断某元素的排名情况,使用ZRANK命令即可。


# zadd命令添加元素
127.0.0.1:6379> zadd 100run:ranking 13 mike
(integer) 1
127.0.0.1:6379> zadd 100run:ranking 12 jake
(integer) 1
127.0.0.1:6379> zadd 100run:ranking 16 tom
(integer) 1

# zrank命令查看排名
127.0.0.1:6379> zrank 100run:ranking jake
(integer) 0
127.0.0.1:6379> zrank 100run:ranking tom
(integer) 2

# zscore判断元素是否存在
127.0.0.1:6379> zscore 100run:ranking jake
"12"

我是南哥,南就南在Get到你的点赞点赞点赞。


在这里插入图片描述



创作不易,不妨点赞、收藏、关注支持一下,各位的支持就是我创作的最大动力❤️



作者:JavaSouth南哥
来源:juejin.cn/post/7436658089703145524
收起阅读 »

Spring 6.0 + Boot 3.0:秒级启动、万级并发的开发新姿势

Spring生态重大升级全景图 一、Spring 6.0核心特性详解 1. Java版本基线升级 最低JDK 17:全面拥抱Java模块化特性,优化现代JVM性能 虚拟线程(Loom项目):轻量级线程支持高并发场景(需JDK 19+) // 示例:虚拟...
继续阅读 »

Spring生态重大升级全景图


Spring 6.0 + Boot 3.0 技术体系.png




一、Spring 6.0核心特性详解


1. Java版本基线升级



  • 最低JDK 17:全面拥抱Java模块化特性,优化现代JVM性能

  • 虚拟线程(Loom项目):轻量级线程支持高并发场景(需JDK 19+)


// 示例:虚拟线程使用
Thread.ofVirtual().name("my-virtual-thread").start(() -> {
// 业务逻辑
});




    1. 虚拟线程(Project Loom)



  • 应用场景:电商秒杀系统、实时聊天服务等高并发场景


// 传统线程池 vs 虚拟线程
// 旧方案(平台线程)
ExecutorService executor = Executors.newFixedThreadPool(200);
// 新方案(虚拟线程)
ExecutorService virtualExecutor = Executors.newVirtualThreadPerTaskExecutor();
// 处理10000个并发请求
IntStream.range(0, 10000).forEach(i ->
virtualExecutor.submit(() -> {
// 处理订单逻辑
processOrder(i);
})
);

2. HTTP接口声明式客户端



  • @HttpExchange注解:类似Feign的声明式REST调用


@HttpExchange(url = "/api/users")
public interface UserClient {
@GetExchange
List<User> listUsers();
}

应用场景:微服务间API调用


@HttpExchange(url = "/products", accept = "application/json")
public interface ProductServiceClient {
@GetExchange("/{id}")
Product getProduct(@PathVariable String id);
@PostExchange
Product createProduct(@RequestBody Product product);
}
// 自动注入使用
@Service
public class OrderService {
@Autowired
private ProductServiceClient productClient;

public void validateProduct(String productId) {
Product product = productClient.getProduct(productId);
// 校验逻辑...
}
}

3. ProblemDetail异常处理



  • RFC 7807标准:标准化错误响应格式


{
"type": "https://example.com/errors/insufficient-funds",
"title": "余额不足",
"status": 400,
"detail": "当前账户余额为50元,需支付100元"
}


  • 应用场景:统一API错误响应格式


@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ProductNotFoundException.class)
public ProblemDetail handleProductNotFound(ProductNotFoundException ex) {
ProblemDetail problem = ProblemDetail.forStatus(HttpStatus.NOT_FOUND);
problem.setType(URI.create("/errors/product-not-found"));
problem.setTitle("商品不存在");
problem.setDetail("商品ID: " + ex.getProductId());
return problem;
}
}
// 触发异常示例
@GetMapping("/products/{id}")
public Product getProduct(@PathVariable String id) {
return productRepo.findById(id)
.orElseThrow(() -> new ProductNotFoundException(id));
}

4. GraalVM原生镜像支持



  • AOT编译优化:启动时间缩短至毫秒级,内存占用降低50%+

  • 编译命令示例:


native-image -jar myapp.jar



二、Spring Boot 3.0突破性改进


1. 基础架构升级



  • Jakarta EE 9+:包名javax→jakarta全量替换

  • 自动配置优化:更智能的条件装配策略



    1. OAuth2授权服务器
      应用场景:构建企业级认证中心




# application.yml配置
spring:
security:
oauth2:
authorization-server:
issuer-url: https://auth.yourcompany.com
token:
access-token-time-to-live: 1h

定义权限端点


@Configuration
@EnableWebSecurity
public class AuthServerConfig {
@Bean
public SecurityFilterChain authServerFilterChain(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorize -> authorize
.anyRequest().authenticated()
)
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
return http.build();
}
}

2. GraalVM原生镜像支持


应用场景:云原生Serverless函数


# 打包命令(需安装GraalVM)
mvn clean package -Pnative
# 运行效果对比
传统JAR启动:启动时间2.3s | 内存占用480MB
原生镜像启动:启动时间0.05s | 内存占用85MB

3. 增强监控(Prometheus集成)



  • Micrometer 1.10+:支持OpenTelemetry标准

  • 全新/actuator/prometheus端点:原生Prometheus格式指标

  • 应用场景:微服务健康监测


// 自定义业务指标
@RestController
public class OrderController {
private final Counter orderCounter = Metrics.counter("orders.total");
@PostMapping("/orders")
public Order createOrder() {
orderCounter.increment();
// 创建订单逻辑...
}
}
# Prometheus监控指标示例
orders_total{application="order-service"} 42
http_server_requests_seconds_count{uri="/orders"} 15



三、升级实施路线图


升级准备阶段.png


四、新特性组合实战案例


场景:电商平台升级


// 商品查询服务(组合使用新特性)
@RestController
public class ProductController {
// 声明式调用库存服务
@Autowired
private StockServiceClient stockClient;
// 虚拟线程处理高并发查询
@GetMapping("/products/{id}")
public ProductDetail getProduct(@PathVariable String id) {
return CompletableFuture.supplyAsync(() -> {
Product product = productRepository.findById(id)
.orElseThrow(() -> new ProductNotFoundException(id));

// 并行查询库存
Integer stock = stockClient.getStock(id);
return new ProductDetail(product, stock);
}, Executors.newVirtualThreadPerTaskExecutor()).join();
}
}



四、升级实践建议



  1. 环境检查:确认JDK版本≥17,IDE支持Jakarta包名

  2. 渐进式迁移

    • 先升级Spring Boot 3.x → 再启用Spring 6特性

    • 使用spring-boot-properties-migrator检测配置变更



  3. 性能测试:对比GraalVM原生镜像与传统JAR包运行指标


通过以上升级方案:



  1. 使用虚拟线程支撑万级并发查询

  2. 声明式客户端简化服务间调用

  3. ProblemDetail统一异常格式

  4. Prometheus监控接口性能




本次升级标志着Spring生态正式进入云原生时代。重点关注:虚拟线程的资源管理策略、GraalVM的反射配置优化、OAuth2授权服务器的定制扩展等深度实践方向。


作者:后端出路在何方
来源:juejin.cn/post/7476389305881296934
收起阅读 »

让闲置 Ubuntu 服务器华丽转身为家庭影院

让闲置 Ubuntu 服务器华丽转身为家庭影院在数字化的时代,家里的设备更新换代频繁,很容易就会有闲置的服务器吃灰。我家里就有一台闲置的 Ubuntu 24.04 服务器,一直放在角落,总觉得有些浪费。于是,我决定让它重新发挥作用,打造一个属于自己的家庭影院。...
继续阅读 »

让闲置 Ubuntu 服务器华丽转身为家庭影院

在数字化的时代,家里的设备更新换代频繁,很容易就会有闲置的服务器吃灰。我家里就有一台闲置的 Ubuntu 24.04 服务器,一直放在角落,总觉得有些浪费。于是,我决定让它重新发挥作用,打造一个属于自己的家庭影院。

一、实现 Windows 与 Ubuntu 服务器文件互通

要打造家庭影院,首先得让本地 Windows 电脑和 Ubuntu 服务器之间能够方便地传输电影文件。我选择安装 Samba 来实现这一目的。

  1. 安装 Samba:在 Ubuntu 服务器的终端中输入命令

    sudo apt-get install samba samba-common

    系统会自动下载并安装 Samba 相关的软件包。

  2. 备份配置文件:为了以防万一,我先将原来的 Samba 配置文件进行备份,执行命令

    mv /etc/samba/smb.conf /etc/samba/smb.conf.bak
  3. 新建配置文件:使用 vim /etc/samba/smb.conf 命令打开编辑器,写入以下配置内容:
[global]
server min protocol = CORE
workgroup = WORKGR0UP
netbios name = Nas
security = user
map to guest = bad user
guest account = nobody
client min protocol = SMB2
server min protocol = SMB2
server smb encrypt = off
[NAS]
comment = NASserver
path = /home/bddxg/nas
public = Yes
browseable = Yes
writable = Yes
guest ok = Yes
passdb backend = tdbsam
create mask = 0775
directory mask = 0775

这里需要注意的是,我计划的媒体库目录是个人目录下的 nas/,所以 path 是 /home/bddxg/nas ,如果大家要部署的话记得根据自己的实际情况修改为对应的位置。 

  1. 连接 Windows 电脑:在 Windows 电脑这边基本不需要什么复杂配置,因为在网络里无法直接看到 Ubuntu,我直接在电脑上添加了网络位置。假设服务器地址是 192.168.10.100,那么添加网络位置就是 \\192.168.10.100\nas,这样就可以在 Windows 电脑和 Ubuntu 服务器之间传输文件了。

二、安装 Jellyfin 搭建家庭影院

文件传输的问题解决后,接下来就是安装 Jellyfin 来实现家庭影院的功能了。

  1. 尝试 Docker 安装失败:一开始我选择使用 Docker 安装,毕竟 Docker 有很多优点,使用起来也比较方便。按照官网指南进行操作,在第三步启动 Docker 并挂载本地目录的时候却一直失败。报错信息为:

    docker: Error response from daemon: error while creating mount source path '/srv/jellyfin/cache': mkdir /srv/jellyfin: read-only file system.

    即使我给 /srv/jellyfin 赋予了 777 权限也没有效果。无奈之下,我决定放弃 Docker 安装方式,直接安装 server 版本的 Jellyfin。

  1. 安装 server 版本的 Jellyfin:在终端中输入命令 curl https://repo.jellyfin.org/install-debuntu.sh | sudo bash,安装过程非常顺利。

  1. 配置 Jellyfin:安装完成后,通过浏览器访问 http://192.168.10.100:8096 进入配置页面。在添加媒体库这里,我遇到了一个麻烦,网页只能选择到 /home/bddxg 目录,无法继续往下选择到我的媒体库位置 /home/bddxg/nas。于是我向 deepseek 求助,它告诉我需要执行命令:

    sudo usermod -aG bddxg jellyfin
    # 并且重启 Jellyfin 服务
    sudo systemctl restart jellyfin

    按照它的建议操作后,我刷新了网页,重新配置了 Jellyfin,终于可以正常添加媒体库了。

  2. 电视端播放:在电视上安装好 Jellyfin apk 客户端后,现在终于可以正常读取 Ubuntu 服务器上的影视资源了,坐在沙发上,享受着大屏观影的乐趣,这种感觉真的太棒了!

 通过这次折腾,我成功地让闲置的 Ubuntu 服务器重新焕发生机,变成了一个功能强大的家庭影院。希望我的经验能够对大家有所帮助,也欢迎大家一起交流更多关于服务器利用和家庭影院搭建的经验。

[!WARNING] 令人遗憾的是,目前 jellyfin 似乎不支持rmvb 格式的影片, 下载资源的时候注意影片格式,推荐直接下载 mp4 格式的资源


本次使用到的软件名称和版本如下:

软件名版本号安装命令
sambaVersion 4.19.5-Ubuntusudo apt-get install samba samba-common
jellyfinJellyfin.Server 10.10.6.0curl https://repo.jellyfin.org/install-debuntu.sh | sudo bash
ffmpeg(jellyfin 内自带)ffmpeg version 7.0.2-Jellyfinnull

作者:冰冻大西瓜
来源:juejin.cn/post/7476614823883833382

收起阅读 »

Mybatis接口方法参数不加@Param,照样流畅取值

在 MyBatis 中,如果 Mapper 接口的方法有多个参数,但没有使用 @Param 注解,默认情况下,MyBatis 会将这些参数放入一个 Map 中,键名为 param1、param2 等,或者使用索引 0、1 等来访问。以下是具体的使用方法和注意事...
继续阅读 »

在 MyBatis 中,如果 Mapper 接口的方法有多个参数,但没有使用 @Param 注解,默认情况下,MyBatis 会将这些参数放入一个 Map 中,键名为 param1param2 等,或者使用索引 01 等来访问。以下是具体的使用方法和注意事项。




一、Mapper 接口方法


假设有一个 Mapper 接口方法,包含多个参数但没有使用 @Param 注解:


public interface UserMapper {
User selectUserByNameAndAge(String name, int age);
}



二、XML 文件中的参数引用


在 XML 文件中,可以通过以下方式引用参数:


1. 使用 param1param2 等


MyBatis 会自动为参数生成键名 param1param2 等:


<select id="selectUserByNameAndAge" resultType="User">
SELECT * FROM user WHERE name = #{param1} AND age = #{param2}
</select>

2. 使用索引 01 等


也可以通过索引 01 等来引用参数:


<select id="selectUserByNameAndAge" resultType="User">
SELECT * FROM user WHERE name = #{0} AND age = #{1}
</select>



三、注意事项



  1. 可读性问题



    • 使用 param1param2 或索引 01 的方式可读性较差,容易混淆。

    • 建议使用 @Param 注解明确参数名称。



  2. 参数顺序问题



    • 如果参数顺序发生变化,XML 文件中的引用也需要同步修改,容易出错。



  3. 推荐使用 @Param 注解



    • 使用 @Param 注解可以为参数指定名称,提高代码可读性和可维护性。


      public interface UserMapper {
      User selectUserByNameAndAge(@Param("name") String name, @Param("age") int age);
      }

      XML 文件:


      <select id="selectUserByNameAndAge" resultType="User">
      SELECT * FROM user WHERE name = #{name} AND age = #{age}
      </select>







四、示例代码


1. Mapper 接口


public interface UserMapper {
User selectUserByNameAndAge(String name, int age);
}

2. XML 文件


<select id="selectUserByNameAndAge" resultType="User">
SELECT * FROM user WHERE name = #{param1} AND age = #{param2}
</select>

或者:


<select id="selectUserByNameAndAge" resultType="User">
SELECT * FROM user WHERE name = #{0} AND age = #{1}
</select>

3. 测试代码


SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = mapper.selectUserByNameAndAge("John", 25);
System.out.println(user);
sqlSession.close();




  • 如果 Mapper 接口方法有多个参数且没有使用 @Param 注解,可以通过 param1param2 或索引 01 等方式引用参数。

  • 这种方式可读性较差,容易出错,推荐使用 @Param 注解明确参数名称。

  • 使用 @Param 注解后,XML 文件中的参数引用会更加清晰和易于维护。


作者:码农liuxin
来源:juejin.cn/post/7475643579781333029
收起阅读 »

Java web后端转Java游戏后端

作为Java后端开发者转向游戏后端开发,虽然核心编程能力相通,但游戏开发在架构设计、协议选择、实时性处理等方面有显著差异。以下从实际工作流程角度详细说明游戏后端开发的核心要点及前后端协作流程: 一、游戏后端核心职责 实时通信管理 采用WebSocket/...
继续阅读 »

作为Java后端开发者转向游戏后端开发,虽然核心编程能力相通,但游戏开发在架构设计、协议选择、实时性处理等方面有显著差异。以下从实际工作流程角度详细说明游戏后端开发的核心要点及前后端协作流程:




一、游戏后端核心职责



  1. 实时通信管理



    • 采用WebSocket/TCP长连接(90%以上MMO游戏选择)

    • 使用Netty/Mina框架处理高并发连接(单机支撑5W+连接是基本要求)

    • 心跳机制设计(15-30秒间隔,检测断线)



  2. 游戏逻辑处理



    • 战斗计算(需在50ms内完成复杂技能伤害计算)

    • 状态同步(通过Delta同步优化带宽,减少60%数据传输量)

    • 定时器管理(Quartz/时间轮算法处理活动开启等)



  3. 数据持久化



    • Redis集群缓存热点数据(玩家属性缓存命中率需>95%)

    • 分库分表设计(例如按玩家ID取模分128个库)

    • 异步落库机制(使用Disruptor队列实现每秒10W+写入)






二、开发全流程实战(以MMORPG为例)


阶段1:预研设计(2-4周)



  • 协议设计
    // 使用Protobuf定义移动协议
    message PlayerMove {
    int32 player_id = 1;
    Vector3 position = 2; // 三维坐标
    float rotation = 3; // 朝向
    int64 timestamp = 4; // 客户端时间戳
    }

    message BattleSkill {
    int32 skill_id = 1;
    repeated int32 target_ids = 2; // 多目标锁定
    Coordinate cast_position = 3; // 技能释放位置
    }


  • 架构设计
    graph TD
    A[Gateway] --> B[BattleServer]
    A --> C[SocialServer]
    B --> D[RedisCluster]
    C --> E[MySQLCluster]
    F[MatchService] --> B



阶段2:核心系统开发(6-8周)



  1. 网络层实现


    // Netty WebSocket处理器示例
    @ChannelHandler.Sharable
    public class GameServerHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame frame) {
    ProtocolMsg msg = ProtocolParser.parse(frame.text());
    switch (msg.getType()) {
    case MOVE:
    handleMovement(ctx, (MoveMsg)msg);
    break;
    case SKILL_CAST:
    validateSkillCooldown((SkillMsg)msg);
    broadcastToAOI(ctx.channel(), msg);
    break;
    }
    }
    }


  2. AOI(Area of Interest)管理



    • 九宫格算法实现视野同步

    • 动态调整同步频率(近距离玩家100ms/次,远距离500ms/次)



  3. 战斗系统



    • 采用确定性帧同步(Lockstep)

    • 使用FixedPoint替代浮点数运算保证一致性






三、前后端协作关键点



  1. 协议版本控制



    • 强制版本校验:每个消息头包含协议版本号


    {
    "ver": "1.2.3",
    "cmd": 1001,
    "data": {...}
    }


  2. 调试工具链建设



    • 开发GM指令系统:


    /debug latency 200  // 模拟200ms延迟
    /simulate 5000 // 生成5000个机器人


  3. 联调流程



    • 使用Wireshark抓包分析时序问题

    • Unity引擎侧实现协议回放功能

    • 自动化测试覆盖率要求:

      • 基础协议:100%

      • 战斗用例:>85%








四、性能优化实践



  1. JVM层面



    • G1GC参数优化:


    -XX:+UseG1GC -XX:MaxGCPauseMillis=50 
    -XX:InitiatingHeapOccupancyPercent=35


  2. 网络优化



    • 启用Snappy压缩协议(降低30%流量)

    • 合并小包(Nagle算法+50ms合并窗口)



  3. 数据库优化



    • 玩家数据冷热分离:

      • 热数据:位置、状态(Redis)

      • 冷数据:成就、日志(MySQL)








五、上线后运维



  1. 监控体系



    • 关键指标报警阈值设置:

      • 单服延迟:>200ms

      • 消息队列积压:>1000

      • CPU使用率:>70%持续5分钟





  2. 紧急处理预案



    • 自动扩容规则:
      if conn_count > 40000:
      spin_up_new_instance()
      if qps > 5000:
      enable_rate_limiter()







六、常见问题解决方案


问题场景:战斗不同步

排查步骤



  1. 对比客户端帧日志与服务端校验日志

  2. 检查确定性随机数种子一致性

  3. 验证物理引擎的FixedUpdate时序


问题场景:登录排队

优化方案



  1. 令牌桶限流算法控制进入速度

  2. 预计等待时间动态计算:
    wait_time = current_queue_size * avg_process_time / available_instances



通过以上流程,Java后端开发者可逐步掌握游戏开发特性,重点需要转变的思维模式包括:从请求响应模式到实时状态同步、从CRUD主导到复杂逻辑计算、从分钟级延迟到毫秒级响应的要求。建议从简单的棋牌类游戏入手,逐步过渡到大型实时游戏开发。


作者:加瓦点灯
来源:juejin.cn/post/7475292103146684479
收起阅读 »

记一次 CDN 流量被盗刷经历

先说损失,被刷了 70 多RMB,还好止损相对即时了,亏得不算多,PCDN 真可恶啊。 600多G流量,100多万次请求。 怎么发现的 先是看到鱼皮大佬发了一篇推文突发,众多网站流量被盗刷!我特么也中招了。 抱着看热闹的心情点开阅读了。。。心想,看看自己的中...
继续阅读 »

先说损失,被刷了 70 多RMB,还好止损相对即时了,亏得不算多,PCDN 真可恶啊。



600多G流量,100多万次请求。


怎么发现的


先是看到鱼皮大佬发了一篇推文突发,众多网站流量被盗刷!我特么也中招了


抱着看热闹的心情点开阅读了。。。心想,看看自己的中招没,结果就真中招了 🍉。


被盗刷资源分析


笔者在 缤纷云七牛云又拍云 都有存放一些图片资源。本次中招的是 缤纷云,下面是被刷的资源。



IP来源


查了几个 IP 和文章里描述的大差不差,都是来自山西联通的请求。



大小流量计算


按日志时间算的话,QPS 大概在 20 左右,单文件 632 K,1分钟大概就760MB ,1小时约 45G 左右。


看了几天前的日志,都是 1 小时刷 40G 就停下,从 9 点左右开始,刷到 12 点。


07-0907-08

但是 10 号的就变多了,60-70 GB 1次了。也是这天晚上才开始做的反制,不知道是不是加策略的时候影响到它计算流量大小了 😝。



反制手段


Referer 限制


通过观察这些资源的请求头,发现 Referer 和请求资源一致,通常情况下,不应该这样,应该是笔者的博客地址https://sugarat.top



于是第一次就限制了 Referer 头不能为空,同时将 cdn.bitiful.sugarat.top 的来源都拉黑。


这个办法还比较好使,后面的请求都给 403 了。



但这个还是临时解决方案,在 V 站上看到讨论,说资源是人为筛选的,意味着 Referer 换个资源还是会发生变化。


IP 限制


有 GitHub 仓库 unclemcz/ban-pcdn-ip 收集了此次恶意刷流量的 IP。


CDN 平台一般支持按 IP 或 IP 段屏蔽请求(虽然后者可能会屏蔽一些正常请求),可以将 IP 段配置到平台上,这样就能限制掉这些 IP 的请求。


缤纷云上这块限制还比较弱,我就直接把缤纷云的 CDN 直接关了,七牛云和又拍云上都加上了 IP 和 地域运营商的限制,等这阵风头过去再恢复。


七牛云又拍云

限速


限制单 IP 的QPS和峰值流量。



但是这个只能避免说让它刷得慢一点,还是不治本。



最后


用了CDN的话,日常还是多看看,能加阈值控制的平台优先加上,常规的访问控制防盗链的啥的安排上。



作者:粥里有勺糖
来源:juejin.cn/post/7390678994998526003
收起阅读 »

新来的总监,把闭包讲得那叫一个透彻

😃文章首发于公众号[精益码农]。 闭包作为前端面试的必考题目,常让1-3年工作经验的Javascripter感到困惑,我的主力语言C#/GO均有闭包。 1. 闭包:关键点在于函数是否捕获了其外部作用域的变量 闭包的形成: 定义函数时, 函数引用了其外部作用域的...
继续阅读 »

😃文章首发于公众号[精益码农]。


闭包作为前端面试的必考题目,常让1-3年工作经验的Javascripter感到困惑,我的主力语言C#/GO均有闭包。


1. 闭包:关键点在于函数是否捕获了其外部作用域的变量


闭包的形成: 定义函数时, 函数引用了其外部作用域的变量, 之后就形成了闭包。


闭包的结果: 引用的变量和定义的函数都会一同存在(即使已经脱离了函数定义/引用的变量的作用域),一直到闭包被消灭。


    public  static Action Closure()
{
var x = 1;
Action action= () =>
{
var y = 1;
var result = x + y;
Console.WriteLine(result);
x++;
};
return action;
}

public static void Main() {
var a=Closure();
a();
a();
}
// 调用函数输出
2
3

委托action是一个函数,它使用了“x”这个外部作用域的变量(x变量不是函数内局部变量),变量引用将被捕获形成闭包。


即使action被返回了(即使“x”已经脱离了它被引用时的作用域环境(Closure)),但是两次执行能输出2,3 说明它脱离原引用环境仍然能用。




当你在代码调试器(debugger)里观察“action”时,可以看到一个Target属性,里面封装了捕获的x变量:




实际上,委托,匿名函数和lambda都是继承自Delegate类
Delegate不允许开发者直接使用,只有编译器才能使用, 也就是说delegate Action都是语法糖。



  • Method:MethodInfo反射类型- 方法执行体

  • Target:当前委托执行的对象,这些语法糖由编译器生成了继承自Delegate类型的对象,包含了捕获的自由变量。



再给一个反例:


public class Program
{
private static int x = 1; // 静态字段
public static void Main()
{
var action = NoClosure();
action();
action();
}

public static Action NoClosure(){
Action action=()=>{
var y =1;
var sum = x+y;
Console.WriteLine($"sum = { sum }");
x++;
};
return action;
}
}

x 是静态字段,在程序中有独立的存储区域, 不在线程的函数堆栈区,不属于某个特定的作用域。


匿名函数使用了 x,但没有捕获外部作用域的变量,因此不构成闭包, Target属性对象无捕获的字段。


从编程设计的角度:闭包开创了除全局变量传值, 函数参数传值之外的第三种变量使用方式。


2. 闭包的形成时机和效果


闭包是词法闭包的简称,维基百科上是这样定义的:

在计算机科学中,闭包是在词法环境中绑定自由变量的一等函数”。


闭包的形成时机:



  • 一等函数

  • 外部作用域变量


闭包的形态:

会捕获闭包函数内引用的外部作用域变量, 一直持有,直到闭包函数不再使用被销毁。



内部实现是形成了一个对象(包含执行函数和捕获的变量,参考Target对象), 只有形成堆内存,才有后续闭包销毁的行为,当闭包这个对象不再被引用时,闭包被GC清理。



闭包的作用周期:


离不开作用域这个概念,函数理所当然管控了函数内的局部变量作用域,但当它引用了外部有作用域的变量时, 就形成了闭包函数。
当闭包(例如一个委托或 lambda 表达式)不再被任何变量、对象或事件持有引用时,它就变成了“不可达”对象, 闭包被gc清理,其实就是堆内存被清理。


2.1 一等函数


一等函数很容易理解,就是在各语言, 函数被认为是某类数据类型, 定义函数就成了定义变量, 函数也可以像变量一样被传递。


很明显,在C#中我们常使用的匿名函数、lambda表达式都是一等函数。


Func<string,string> myFunc = delegate(string var1)
{
return "some value";
};
Func<string,string> myFunc = var1 => "some value";

string myVar = myFunc("something");

2.2 自由变量


在函数中被引用的外部作用域变量, 注意, 这个变量是外部有作用域的变量,也就说排除全局变量(这些变量在程序的独立区域, 不属于任何作用域)。


public void Test() 
{
var myVar = "this is good";
Func<string,string> myFunc = delegate(string var1)
{
return var1 + myVar;
};
}

上面这个示例,myFunc形成了闭包,捕获了myVar这个外部作用域的变量;
即使Test函数返回了委托myFunc(脱离了定义myVar变量的作用域),闭包依然持有myVar的变量引用,
注意,引用变量,并不是使用当时变量的副本值


我们再回过头来看结合了线程调度的闭包面试题。


3. 闭包函数关联线程调度: 依次打印连续的数字


 static void Closure1()
{
for (int i = 0; i < 10; i++)
{
Task.Run(()=> Console.WriteLine(i));
}
}

每次输出数字不固定


并不是预期的 0.1.2.3.4.5.6.7.8.9


首先形成了闭包函数()=> Console.WriteLine(i), 捕获了外部有作用域变量i的引用, 此处捕获的变量i相对于函数是全局变量。
但是Task调度闭包函数的时机不确定, 所以打印的是被调度时引用的变量i值。


数字符合但乱序:为每个闭包函数绑定独立变量


循环内增加局部变量, 解绑全局变量 (或者可以换成foreach,foreach相当于内部给你整了一个局部变量)。


能输出乱序的0,1,2,3,4,5,6,7,8,9


因为每次循环内产生的闭包函数捕获了对应的局部变量j,这样每个任务执行环境均独立维护了一个变量j, 这个j不是全局变量, 但是由于Task启动时机依然不确定,故是乱序。



数字符合且有序


核心是解决 Task调度问题。


思路是:一个共享变量,每个任务打印该变量自增的一个阶段,但是该自增不允许被打断。


 public static void Main(string[] args)
{
var s =0;
var lo = new Program();
for (int i = 0; i < 10; i++)
{
Task.Run(()=>
{
lock(lo)
{
Console.WriteLine(s); // 依然形成了闭包函数, 之后闭包函数被线程调度
s++;
}
});
}
Thread.Sleep(2000);
} // 上面是一个明显的锁争用

3.Golang闭包的应用


gin 框架中中间件的默认形态是:


package middleware
func AuthenticationMiddleware(c *gin.Context) {
......
}

// Use方法的参数签名是这样: type HandlerFunc func(*Context), 不支持入参
router.Use(middleware.AuthenticationMiddleware)

实际实践上我们又需要给中间件传参, 闭包提供了这一能力。


func Authentication2Middleware(log *zap.Logger) gin.HandlerFunc  {
return func(c *gin.Context) {
... 这里面可以利用log 参数。
}
}

var logger *zap.Logger
api.Use(middleware.Authentication2Middleware(logger))

总结


本文屏蔽语言差异,理清了[闭包]的概念核心: 函数引用了其外部作用域的变量,


核心特征:一等函数、自由变量,核心结果: 即使脱离了原捕获变量的原作用域,闭包函数依然持有该变量引用。


不仅能帮助我们应对多语种有关闭包的面试题, 也帮助我们了解[闭包]在通用语言中的设计初衷。


另外我们通过C# 调试器巩固了Delegate 抽象类,这是lambda表达式,委托,匿名函数的底层抽象数据结构类,包含两个重要属性 Method Target,分别表征了方法执行体、当前委托作用的对象,


可想而知,其他语言也是通过这个机制捕获闭包当中的自由变量。


作者:不卷牛马
来源:juejin.cn/post/7474982751365038106
收起阅读 »

Java利用Deepseek进行项目代码审查

一、为什么需要AI代码审查?写代码就像做饭,即使是最有经验的厨师(程序员),也难免会忘记关火(资源未释放)、放错调料(逻辑错误)或者切到手(空指针异常)。Deepseek就像一位24小时待命的厨房监理,能帮我们实时发现这些"安全隐患"。二、环境准备(5分钟搞定...
继续阅读 »

一、为什么需要AI代码审查?

写代码就像做饭,即使是最有经验的厨师(程序员),也难免会忘记关火(资源未释放)、放错调料(逻辑错误)或者切到手(空指针异常)。Deepseek就像一位24小时待命的厨房监理,能帮我们实时发现这些"安全隐患"。

二、环境准备(5分钟搞定)

  1. 安装Deepseek插件(以VSCode为例):
    • 插件市场搜索"Deepseek Code Review"
    • 点击安装(就像安装手机APP一样简单)

  1. Java项目配置:

<dependency>
<groupId>com.deepseekgroupId>
<artifactId>code-analyzerartifactId>
<version>1.3.0version>
dependency>

三、真实案例:用户管理系统漏洞检测

原始问题代码:

public class UserService {
// 漏洞1:未处理空指针
public String getUserRole(String userId) {
return UserDB.query(userId).getRole();
}

// 漏洞2:资源未关闭
public void exportUsers() {
FileOutputStream fos = new FileOutputStream("users.csv");
fos.write(getAllUsers().getBytes());
}

// 漏洞3:SQL注入风险
public void deleteUser(String input) {
Statement stmt = conn.createStatement();
stmt.execute("DELETE FROM users WHERE id = " + input);
}
}

使用Deepseek审查后:

智能修复建议:

  1. 空指针防护 → 建议添加Optional处理
  2. 流资源 → 推荐try-with-resources语法
  3. SQL注入 → 提示改用PreparedStatement

修正后的代码:

public class UserService {
// 修复1:Optional处理空指针
public String getUserRole(String userId) {
return Optional.ofNullable(UserDB.query(userId))
.map(User::getRole)
.orElse("guest");
}

// 修复2:自动资源管理
public void exportUsers() {
try (FileOutputStream fos = new FileOutputStream("users.csv")) {
fos.write(getAllUsers().getBytes());
}
}

// 修复3:预编译防注入
public void deleteUser(String input) {
PreparedStatement pstmt = conn.prepareStatement(
"DELETE FROM users WHERE id = ?");
pstmt.setString(1, input);
pstmt.executeUpdate();
}
}

四、实现原理揭秘

Deepseek的代码审查就像"X光扫描仪",通过以下三步工作:

  1. 模式识别:比对数千万个代码样本
    • 就像老师批改作业时发现常见错误
  1. 上下文理解:分析代码的"人际关系"
    • 数据库连接有没有"成对出现"(打开/关闭)
    • 敏感操作有没有"保镖"(权限校验)
  1. 智能推理:预测代码的"未来"
    • 这个变量走到这里会不会变成null?
    • 这个循环会不会变成"无限列车"?

五、进阶使用技巧

  1. 自定义审查规则(配置文件示例):
rules:
security:
sql_injection: error
performance:
loop_complexity: warning
style:
var_naming: info

2. 与CI/CD集成(GitHub Action示例):

- name: Deepseek Code Review
uses: deepseek-ai/code-review-action@v2
with:
severity_level: warning
fail_on: error

六、开发者常见疑问

Q:AI会不会误判我的代码?
A:就像导航偶尔会绕路,Deepseek给出的是"建议"而非"判决",最终决策权在你手中

Q:处理历史遗留项目要多久?
A:10万行代码项目约需3-5分钟,支持增量扫描

七、效果对比数据

指标人工审查Deepseek+人工
平均耗时4小时30分钟
漏洞发现率78%95%
误报率5%12%
知识库更新速度季度实时

作者:Java技术小馆
来源:juejin.cn/post/7473799336675639308

收起阅读 »

再见Typora,这款大小不到3M的Markdown编辑器,满足你的所有幻想!

Typora 是一款广受欢迎的 Markdown 编辑器,以其所见即所得的编辑模式和优雅的界面而闻名,长期以来是许多 Markdown 用户的首选。然而,从 2021 年起,Typora 不再免费,采用一次性付费授权模式。虽然费用不高,但对于轻量使用者或预算有...
继续阅读 »

Typora 是一款广受欢迎的 Markdown 编辑器,以其所见即所得的编辑模式和优雅的界面而闻名,长期以来是许多 Markdown 用户的首选。然而,从 2021 年起,Typora 不再免费,采用一次性付费授权模式。虽然费用不高,但对于轻量使用者或预算有限的用户可能并不友好。



今天来推荐一款开源替代品,一款更加轻量化、注重隐私且完全免费的 Markdown 编辑器,专为 macOS 用户开发。


项目介绍


MarkEdit 是一款轻量级且高效的 Markdown 编辑器,专为 macOS 用户设计,安装包大小不到 3 MB。它以简洁的设计和流畅的性能,成为技术写作、笔记记录、博客创作以及项目文档编辑的理想工具。无论是编写技术文档、撰写博客文章,还是编辑 README 文件,MarkEdit 都能以快速响应和便捷操作帮助用户专注于内容创作。


图片


根据官方介绍,MarkEdit 免费的原因如下:



MarkEdit 是完全免费和开源的,没有任何广告或其他服务。我们之所以发布它,是因为我们喜欢它,我们不期望从中获得任何收入。



功能特性


MarkEdit 的核心功能围绕 Markdown 写作展开,注重实用与高效,以下是其主要特性:



  • 实时语法高亮:清晰呈现 Markdown 的结构,让文档层次分明。

  • 多种主题:提供不同的配色方案,总有一种适合你。

  • 分屏实时预览:支持所见即所得的写作体验,左侧编辑,右侧实时渲染。

  • 文件树视图:适合多文件项目管理,方便在项目间快速切换。

  • 文档导出:支持将 Markdown 文件导出为 PDF 或 HTML 格式,方便分享和发布。

  • CodeMirror 插件支持:通过插件扩展功能,满足更多 Markdown 使用需求。

  • ......


MarkEdit 的特点让它能胜任多种写作场合:



  • 技术文档:帮助开发者快速记录项目相关文档。

  • 博客创作:支持实时预览,让博客排版更直观。

  • 个人笔记:轻量且启动迅速,适合日常记录。

  • 项目文档:文件管理功能让多文件项目的编辑更加高效。


效果展示


多种主题风格,总有一种适合你:




实时预览,让博客排版更直观:



设置界面,清晰直观:



安装方法


方法 1:安装包下载


找到 MarkEdit 的最新版本安装包下载使用即可,地址:github.com/MarkEdit-ap…


方法 2:通过 Homebrew


在终端中运行相关命令即可完成安装。


brew install markedit

注意:MarkEdit 支持 macOS Sonoma 和 macOS Sequoia, 历史兼容版本包括 macOS 12 和 macOS 13。


总结


MarkEdit 是一款专注于 Markdown 写作的 macOS 原生编辑器,以简洁、高效、隐私友好为核心设计理念。无论是日常写作还是处理复杂文档,它都能提供流畅的体验和强大的功能。对于追求高效写作的 macOS 用户来说,MarkEdit 是一个不可多得的优秀工具。


项目地址:github.com/MarkEdit-ap…


作者:Github掘金计划
来源:juejin.cn/post/7456685819047919651
收起阅读 »

前端哪有什么设计模式

前言 常网IT源码上线啦! 本篇录入吊打面试官专栏,希望能祝君拿下Offer一臂之力,各位看官感兴趣可移步🚶。 有人说面试造火箭,进去拧螺丝;其实个人觉得问的问题是项目中涉及的点 || 热门的技术栈都是很好的面试体验,不要是旁门左道冷门的知识,实际上并不会用...
继续阅读 »

前言



  • 常网IT源码上线啦!

  • 本篇录入吊打面试官专栏,希望能祝君拿下Offer一臂之力,各位看官感兴趣可移步🚶。

  • 有人说面试造火箭,进去拧螺丝;其实个人觉得问的问题是项目中涉及的点 || 热门的技术栈都是很好的面试体验,不要是旁门左道冷门的知识,实际上并不会用到的。

  • 接下来想分享一些自己在项目中遇到的技术选型以及问题场景。





你生命的前半辈子或许属于别人,活在别人的认为里。那把后半辈子还给你自己,去追随你内在的声音。



1.jpg


一、前言


之前在讨论设计模式、算法的时候,一个后端组长冷嘲热讽的说:前端哪有什么设计模式、算法,就好像只有后端语言有一样,至今还记得那不屑的眼神。


今天想起来,就随便列几个,给这位眼里前端无设计模式的人,睁眼看世界。


二、观察者模式 (Observer Pattern)


观察者模式的核心是当数据发生变化时,自动通知并更新相关的视图。在 Vue 中,这通过其响应式系统实现。


Vue 2.x:Object.defineProperty


在 Vue 2.x 中,响应式系统是通过 Object.defineProperty 实现的。每当访问某个对象的属性时,getter 会被触发;当设置属性时,setter 会触发,从而实现数据更新时视图的重新渲染。


源码(简化版):


function defineReactive(obj, key, val) {
// 创建一个 dep 实例,用于收集依赖
const dep = new Dep();

Object.defineProperty(obj, key, {
get() {
// 当访问属性时,触发 getter,并把当前 watcher 依赖收集到 dep 中
if (Dep.target) {
dep.addDep(Dep.target);
}
return val;
},
set(newVal) {
if (newVal !== val) {
val = newVal;
dep.notify(); // 数据更新时,通知所有依赖重新渲染
}
}
});
}


  • Dep :它管理依赖,addDep 用于添加依赖,notify 用于通知所有依赖更新。


class Dep {
constructor() {
this.deps = [];
}

addDep(dep) {
this.deps.push(dep);
}

notify() {
this.deps.forEach(dep => dep.update());
}
}


  • 依赖收集:当 Vue 组件渲染时,会创建一个 watcher 对象,表示一个视图的更新需求。当视图渲染过程中访问数据时,getter 会触发,并将 watcher 添加到 dep 的依赖列表中。


Vue 3.x:Proxy


Vue 3.x 使用了 Proxy 来替代 Object.defineProperty,从而实现了更高效的响应式机制,支持深度代理。


源码(简化版):


function reactive(target) {
const handler = {
get(target, key) {
// 依赖收集:当访问某个属性时,触发 getter,收集依赖
track(target, key);
return target[key];
},
set(target, key, value) {
// 数据更新时,通知相关的视图更新
target[key] = value;
trigger(target, key);
return true;
}
};

return new Proxy(target, handler);
}



  • track:收集依赖,确保只有相关组件更新。

  • trigger:当数据发生变化时,通知所有依赖重新渲染。


三、发布/订阅模式 (Publish/Subscribe Pattern)


发布/订阅模式通过中央事件总线(Event Bus)实现不同组件间的解耦,Vue 2.x 中,组件间的通信就是基于这种模式实现的。


Vue 2.x:事件总线(Event Bus)


事件总线就是一个中央的事件处理器,Vue 实例可以充当事件总线,用来处理不同组件之间的消息传递。


// 创建一个 Vue 实例作为事件总线
const EventBus = new Vue();

// 组件 A 发布事件
EventBus.$emit('message', 'Hello from A');

// 组件 B 订阅事件
EventBus.$on('message', (msg) => {
console.log(msg); // 输出 'Hello from A'
});


  • $emit:用于发布事件。

  • $on:用于订阅事件。

  • $off:用于取消订阅事件。


四、工厂模式 (Factory Pattern)


工厂模式通过一个函数生成对象或实例,Vue 的组件化机制和动态组件加载就是通过工厂模式来实现的。


Vue 的 render 函数和 functional 组件支持动态生成组件实例。例如,functional 组件本质上是一个工厂函数,通过给定的 props 返回一个 VNode。


Vue.component('dynamic-component', {
functional: true,
render(h, context) {
// 工厂模式:根据传入的 props 创建不同的 VNode
return h(context.props.type);
}
});


  • functional 组件:它没有实例,所有的逻辑都是在 render 函数中处理,返回的 VNode 就是组件的“产物”。


五、单例模式 (Singleton Pattern)


单例模式确保某个类只有一个实例,Vue 实例就是全局唯一的。


在 Vue 中,全局的 Vue 构造函数本身就是一个单例对象,通常只会创建一个 Vue 实例,用于管理应用的生命周期和全局配置。


const app = new Vue({
data: {
message: 'Hello, Vue!'
}
});


  • 单例保证:整个应用只有一个 Vue 实例,所有全局的配置(如 Vue.config)都是共享的。


六、模板方法模式 (Template Method Pattern)


模板方法模式定义了一个操作中的算法框架,而将一些步骤延迟到子类中。Vue 的生命周期钩子就是一个模板方法模式的实现。


Vue 定义了一系列生命周期钩子(如 createdmountedupdated 等),它们实现了组件从创建到销毁的完整过程。开发者可以在这些钩子中插入自定义逻辑。


Vue.component('my-component', {
data() {
return {
message: 'Hello, 泽!'
};
},
created() {
console.log('Component created');
},
mounted() {
console.log('Component mounted');
},
template: '<div>{{ message }}</div>'
});

Vue 组件的生命周期钩子实现了模板方法模式的核心思想,开发者可以根据需要重写生命周期钩子,而 Vue 保证生命周期的流程和框架。


七、策略模式 (Strategy Pattern)


策略模式通过定义一系列算法,将它们封装起来,使它们可以相互替换。Vue 的 计算属性(computed)方法(methods) 可以看作是策略模式的应用。


计算属性允许我们定义动态的属性,其值是基于其他属性的计算结果。Vue 会根据依赖关系缓存计算结果,只有在依赖的属性发生变化时,计算属性才会重新计算。


new Vue({
data() {
return {
num1: 10,
num2: 20
};
},
computed: {
sum() {
return this.num1 + this.num2;
}
}
});

八、装饰器模式 (Decorator Pattern)


装饰器模式允许动态地给对象添加功能,而无需改变其结构。在 Vue 中,指令就是一种装饰器模式的应用,它通过指令来动态地改变元素的行为。


<div v-bind:class="className"></div>
<div v-if="isVisible">谁的疯太谍</div>

这些指令动态地修改 DOM 元素的行为,类似于装饰器在不修改对象结构的情况下,动态地增强其功能。


九、代理模式 (Proxy Pattern)


代理模式通过创建一个代理对象来控制对目标对象的访问。在 Vue 3.x 中,响应式系统就是通过 Proxy 来代理对象的访问。


vue3


const state = reactive({
count: 0
});

state.count++; // 会触发依赖更新

reactive:使用 Proxy 对对象进行代理,当对象的属性被访问或修改时,都会触发代理器的 get 和 set 操作。


function reactive(target) {
const handler = {
get(target, key) {
// 依赖收集:当访问某个属性时,触发 getter,收集依赖
track(target, key);
return target[key];
},
set(target, key, value) {
// 数据更新时,触发依赖更新
target[key] = value;
trigger(target, key);
return true;
}
};

return new Proxy(target, handler);
}


  • track:当读取目标对象的属性时,收集依赖,这通常涉及到将当前的 watcher 加入到依赖列表中。

  • trigger:当对象的属性发生改变时,通知所有相关的依赖(如组件)更新。


这个 Proxy 机制使得 Vue 可以动态地观察和更新对象的变化,比 Object.defineProperty 更具灵活性。


十、适配器模式 (Adapter Pattern)


适配器模式用于将一个类的接口转换成客户端期望的另一个接口,使得原本不兼容的接口可以一起工作。Vue 的插槽(Slots)和组件的跨平台支持某种程度上借用了适配器模式的思想。


Vue 插槽机制


Vue 的插槽机制是通过提供一个适配层,将父组件传入的内容插入到子组件的指定位置。开发者可以使用具名插槽、作用域插槽等方式,实现灵活的插槽传递。


<template>
<child-component>
<template #header>
<h1>This is the header</h1>
</template>
<p>This is the default content</p>
</child-component>

</template>

父组件通过 #header 插槽插入了一个标题内容,而 child-component 会将其插入到适当的位置。这里,插槽充当了一个适配器,允许父组件插入的内容与子组件的内容结构灵活匹配。


十全十美


至此撒花~


后记


我相信技术不分界,不深入了解,就不要轻易断言。


一个圆,有了一个缺口,不知道的东西就更多了。


但是没有缺口,不知道的东西就少了。


这也就是为什么,知道得越多,不知道的就越多。


谢谢!


最后,祝君能拿下满意的offer。


我是Dignity_呱,来交个朋友呀,有朋自远方来,不亦乐乎呀!深夜末班车



👍 如果对您有帮助,您的点赞是我前进的润滑剂。



以往推荐


小小导出,我大前端足矣!


靓仔,说一下keep-alive缓存组件后怎么更新及原理?


面试官问我watch和computed的区别以及选择?


面试官问我new Vue阶段做了什么?


前端仔,快把dist部署到Nginx上


多图详解,一次性啃懂原型链(上万字)


Vue-Cli3搭建组件库


Vue实现动态路由(和面试官吹项目亮点)


项目中你不知道的Axios骚操作(手写核心原理、兼容性)


VuePress搭建项目组件文档


原文链接


juejin.cn/post/744421…


作者:Dignity_呱
来源:juejin.cn/post/7444215159289102347
收起阅读 »

再见 XShell!一款万能通用的终端工具,用完爱不释手!

作为一名后端开发,我们经常需要使用终端工具来管理Linux服务器。最近发现一款比Xshell更好用终端工具XPipe,能支持SSH、Docker、K8S等多种环境,还具有强大的文件管理工具,分享给大家! XPipe简介 XPipe是一款全新的终端管理工具,具...
继续阅读 »

作为一名后端开发,我们经常需要使用终端工具来管理Linux服务器。最近发现一款比Xshell更好用终端工具XPipe,能支持SSH、Docker、K8S等多种环境,还具有强大的文件管理工具,分享给大家!



XPipe简介


XPipe是一款全新的终端管理工具,具有强大的文件管理功能,目前在Github上已有4.8k+Star。它可以基于你本地安装的命令行工具(例如PowerShell)来执行远程命令,反应速度非常快。如果你有使用 ssh、docker、kubectl 等命令行工具来管理服务器的需求,使用它就可以了。


XPipe具有如下特性:



  • 连接中心:能轻松实现所有类型的远程连接,支持SSH、Docker、Podman、Kubernetes、Powershell等环境。

  • 强大的文件管理功能:具有对远程系统专门优化的文件管理功能。

  • 多种命令行环境支持:包括bash、zsh、cmd、PowerShell等。

  • 多功能脚本系统:可以方便地管理可重用脚本。

  • 密码保险箱:所有远程连接账户均完全存储于您本地系统中的一个加密安全的存储库中。


下面是XPipe使用过程中的截图,界面还是挺炫酷的!




这或许是一个对你有用的开源项目,mall项目是一套基于 SpringBoot3 + Vue 的电商系统(Github标星60K),后端支持多模块和2024最新微服务架构 ,采用Docker和K8S部署。包括前台商城项目和后台管理系统,能支持完整的订单流程!涵盖商品、订单、购物车、权限、优惠券、会员、支付等功能!



项目演示:



使用



  • 首先去XPipe的Release页面下载它的安装包,我这里下载的是Portable版本,解压即可使用,地址:github.com/xpipe-io/xp…




  • 下载完成后进行解压,解压后双击xpiped.exe即可使用;




  • 这里我们先进行一些设置,将语言设置成中文,然后设置下主题,个人比较喜欢黑色主题;




  • 接下来新建一个SSH连接,输入服务器地址后,选择添加预定义身份




  • 这个预定义身份相当于一个可重用的Linux访问账户;




  • 然后输入连接名称,点击完成即可创建连接;




  • 我们可以发现XPipe能自动发现服务器器上的Docker环境并创建连接选项,如果你安装了K8S环境的话,也是可以发现到的;




  • 然后我们单击下Linux-local这个连接,就可以通过本地命令行工具来管理Linux服务器了;




  • 如果你想连接到某个Docker容器的话,直接点击对应容器即可连接,这里以mysql为例;




  • 选中左侧远程服务器,点击右侧的文件浏览器按钮可以直接管理远程服务器上的文件,非常方便;




  • 所有脚本功能中,可以存储我们的可重用脚本;




  • 所有身份中存储着我们的账号密码,之前创建的Linux root账户在这里可以进行修改。



总结


今天给大家分享了一款好用的终端工具XPipe,界面炫酷功能强大,它的文件管理功能确实惊艳到我了。而且它可以用本地命令行工具来执行SSH命令,对比一些套壳的跨平台终端工具,反应速度还是非常快的!


项目地址


github.com/xpipe-io/xp…


作者:MacroZheng
来源:juejin.cn/post/7475662844789637160
收起阅读 »

Java 泛型中的通配符 T,E,K,V,?有去搞清楚吗?

前言不久前,被人问到Java 泛型中的通配符 T,E,K,V,? 是什么?有什么用?这不经让我有些回忆起该开始学习Java那段日子,那是对泛型什么的其实有些迷迷糊糊的,学的不这么样,是在做项目的过程中,渐渐有又看到别人的代码、在看源码的时候老是遇见,之后就专门...
继续阅读 »

前言

不久前,被人问到Java 泛型中的通配符 T,E,K,V,? 是什么?有什么用?这不经让我有些回忆起该开始学习Java那段日子,那是对泛型什么的其实有些迷迷糊糊的,学的不这么样,是在做项目的过程中,渐渐有又看到别人的代码、在看源码的时候老是遇见,之后就专门去了解学习,才对这几个通配符 T,E,K,V,?有所了解。

泛型有什么用?

在介绍这几个通配符之前,我们先介绍介绍泛型,看看泛型带给我们的好处。
Java泛型是JDK5中引入的一个新特性,泛型提供了编译是类型安全检测机制,这个机制允许开发者在编译是检测非法类型。泛型的本质就是参数化类型,就是在编译时对输入的参数指定一个数据类型。

  1. 类型安全:编译是检查类型是否匹配,避免了ClassCastexception的发生。
// 非泛型写法(存在类型转换风险)
List list1 = new ArrayList();
list1.add("a");
Integer num = (Long) list1.get(0); // 运行时抛出 ClassCastException

// 泛型写法(编译时检查类型)
List list2 = new ArrayList<>();
// list.add(1); // 编译报错
list2.add("a");
String str = list2.get(0); // 无需强制转换
  1. 消除代码强制类型转换:减少了一些类型转换操作。
// 非泛型写法
Map map1 = new HashMap();
map1.put("user", new User());
User user1 = (User) map1.get("user");

// 泛型写法
Map map2 = new HashMap<>();
map2.put("user", new User());
// 自动转换
User user2 = map2.get("user");

3.代码复用:可以支持多种数据类型,不要重复编写代码,例如:我们常用的统一响应结果类。

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result {
/**
* 响应状态码
*/

private int code;

/**
* 响应信息
*/

private String message;

/**
* 响应数据
*/

private T data;

/**
* 时间戳
*/

private long timestamp;
其他代码省略...
  1. 增强可读性:通过类型参数就直接能看出要填入什么类型。
List list = new ArrayList<>();

泛型里的通配符

我们在使用泛型的时候,经常会使用或者看见多种不同的通配符,常见的 T,E,K,V,?这几种,相信大家一定不陌生,但是真的问你他们有什么作用?有什么区别时,很多人应该是不能很好的介绍它们的,接下来我就来给大家介绍介绍。

T,E,K,V

  1. T(Type) T表示任意类型参数,我们举个例子
pubile class A{
prvate T t;
//其他省略...
}

//创建一个不带泛型参数的A
A a = new A();
a.set(new B());
B b = (B) a.get();//需要进行强制类型转换

//创建一个带泛型参数的A
A a = new A();
a.set(new B());
B b = a.get();
  1. E(Element) E表示集合中的元素类型
List list = new ArrayList<>();
  1. K(Key) K表示映射的键的数据类型
Map map = new HashMap<>();
  1. V(Value) V表示映射的值的数据类型
Map map = new HashMap<>();

通配符 ?

  1. 无界通配符 表示未知类型,接收任意类型
   // 使用无界通配符处理任意类型的查询结果
public void logQueryResult(List resultList) {
resultList.forEach(obj -> log.info("Result: {}", obj));
}
  1. 上界通配符 表示类型是T或者是子类
 // 使用上界通配符读取缓存
public extends Serializable> T getCache(String key, Class clazz) {
Object value = redisTemplate.opsForValue().get(key);
return clazz.cast(value);
}
  1. 下界通配符 表示类型是T或者是父类
  // 使用下界通配符写入缓存
public void setCache(String key, super Serializable> value) {
redisTemplate.opsForValue().set(key, value);
}


综合示例:

import java.util.ArrayList;
import java.util.List;

public class Demo {
//实体类
class Animal {
void eat() {
System.out.println("Animal is eating");
}
}

class Dog extends Animal {
@Override
void eat() {
System.out.println("Dog is eating");
}
}

class Husky extends Dog {
@Override
void eat() {
System.out.println("Husky is eating");
}
}

/**
* 无界通配符
*/

// 只能读取元素,不能写入(除null外)
public static void printAllElements(List list) {
for (Object obj : list) {
System.out.println(obj);
}
// list.add("test"); // 编译错误!无法写入具体类型
list.add(null); // 唯一允许的写入操作
}

/**
* 上界通配符
*/

// 安全读取为Animal,但不能写入(生产者场景)
public static void processAnimals(List animals) {
for (Animal animal : animals) {
animal.eat();
}
// animals.add(new Dog()); // 编译错误!无法确定具体子类型
}

/**
* 下界通配符
*/

// 安全写入Dog,读取需要强制转换(消费者场景)
public static void addDogs(Listsuper Dog> dogList) {
dogList.add(new Dog());
dogList.add(new Husky()); // Husky是Dog子类
// dogList.add(new Animal()); // 编译错误!Animal不是Dog的超类

Object obj = dogList.get(0); // 读取只能为Object
if (obj instanceof Dog) {
Dog dog = (Dog) obj; // 需要显式类型转换
}
}

public static void main(String[] args) {
// 测试无界通配符
List strings = List.of("A", "B", "C");
printAllElements(strings);

List integers = List.of(1, 2, 3);
printAllElements(integers);

// 测试上界通配符
List dogs = new ArrayList<>();
dogs.add(new Dog());
processAnimals(dogs);

List huskies = new ArrayList<>();
huskies.add(new Husky());
processAnimals(huskies);

// 测试下界通配符
List animals = new ArrayList<>();
addDogs(animals);
System.out.println(animals);

List objects = new ArrayList<>();
addDogs(objects);
}
}

我们需要清楚,这些只是我们开发过程中约定,不是强制规定,但遵循它们可以提高代码的可读性。



我们在很多时候只是单纯的会使用某些技术,但是对它们里面许许多多常见的都是一知半解的,只是会使用确实很重要,但是如果有时间,我们不妨好好的在对这些技术进行深入学习,不仅知其然,而且知其所以然,这样我们的技术才会不断提升进步。


作者:镜花水月linyi
来源:juejin.cn/post/7475629913329008649

总结

收起阅读 »

入职第一天,看了公司代码,牛马沉默了

入职第一天就干活的,就问还有谁,搬来一台N手电脑,第一分钟开机,第二分钟派活,第三分钟干活,巴适。。。。。。打开代码发现问题不断读取配置文件居然读取两个配置文件,一个读一点,不清楚为什么不能一个配置文件进行配置 一边获取WEB-INF下的配置文件,一...
继续阅读 »

入职第一天就干活的,就问还有谁,搬来一台N手电脑,第一分钟开机,第二分钟派活,第三分钟干活,巴适。。。。。。

4f7ca8c685324356868f65dd8862f101~tplv-k3u1fbpfcp-jj-mark_3024_0_0_0_q75.jpg

打开代码发现问题不断

  1. 读取配置文件居然读取两个配置文件,一个读一点,不清楚为什么不能一个配置文件进行配置

image.png

image.png

image.png 一边获取WEB-INF下的配置文件,一边用外部配置文件进行覆盖,有人可能会问既然覆盖,那可以全在外部配置啊,问的好,如果全用外部配置,咱们代码获取属性有的加上了项目前缀(上面的两个put),有的没加,这样配置文件就显得很乱不可取,所以形成了分开配置的局面,如果接受混乱,就写在外部配置;不能全写在内部配置,因为

prop_c.setProperty(key, value);

value获取外部配置为空的时候会抛出异常;properties底层集合用的是hashTable

public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
}
}
  1. 很多参数写死在代码里,如果有改动,工作量会变得异常庞大,举例权限方面伪代码
role.haveRole("ADMIN_USE")
  1. 日志打印居然sout和log混合双打

image.png

image.png

先不说双打的事,对于上图这个,应该输出包括堆栈信息,不然定位问题很麻烦,有人可能会说e.getMessage()最好,可是生产问题看多了发现还是打堆栈好;还有如果不是定向返回信息,仅仅是记录日志,完全没必要catch多个异常,一个Exception足够了,不知道原作者这么写的意思是啥;还是就是打印日志要用logger,用sout打印在控制台,那我日志文件干啥;

4.提交的代码没有技术经理把关,下发生产包是个人就可以发导致生产环境代码和本地代码或者数据库数据出现不一致的现象,数据库数据的同步是生产最容易忘记执行的一个事情;比如我的这家公司上传文件模板变化了,但是没同步,导致出问题时开发环境复现问题真是麻烦;

5.随意更改生产数据库,出不出问题全靠开发的职业素养;

6.Maven依赖的问题,Maven引pom,而pom里面却是另一个pom文件,没有生成的jar供引入,是的,我们可以在dependency里加上

<type>pom

来解决这个问题,但是公司内的,而且实际也是引入这个pom里面的jar的,我实在不知道这么做的用意是什么,有谁知道;求教 a972880380654b389246a3179add2cca~tplv-k3u1fbpfcp-jj-mark_3024_0_0_0_q75.jpg

以上这些都是我最近一家公司出现的问题,除了默默接受还能怎么办;

那有什么优点呢:

  1. 不用太怎么写文档
  2. 束缚很小
  3. 学到了js的全局调用怎么写的(下一篇我来写,顺便巩固一下)

解决之道

怎么解决这些问题呢,首先对于现有的新项目或升级的项目来说,spring的application.xml/yml 完全可以写我们的配置,开发环境没必要整外部文件,如果是生产环境我们可以在脚本或启动命令添加 nohup java -Dfile.encoding=UTF-8 -Dspring.config.location=server/src/main/config/application.properties -jar xxx.jar & 来告诉jar包引哪里的配置文件;也可以加上动态配置,都很棒的,

其次就是规范代码,养成良好的规范,跟着节奏,不要另辟蹊径;老老实实的,如果原项目上迭代,不要动源代码,追加即可,没有时间去重构的;

我也曾是个快乐的童鞋,也有过崇高的理想,直到我面前堆了一座座山,脚下多了一道道坑,我。。。。。。!


作者:小红帽的大灰狼
来源:juejin.cn/post/7371986999164928010
收起阅读 »

为什么程序员痴迷于错误信息上报?

前言上一篇已经聊过日志上报的调度原理,讲述如何处理日志上报堆积、上报失败以及上报优化等方案。从上家公司开始,监控就由我们组身强体壮的同事来负责,而我只能开发Admin和H5;经过一系列焦虑的面试后,咸鱼翻身,这辈子我也做上监控了。千万不要以为我是因为监控的重要...
继续阅读 »

前言

上一篇已经聊过日志上报的调度原理,讲述如何处理日志上报堆积、上报失败以及上报优化等方案。

从上家公司开始,监控就由我们组身强体壮的同事来负责,而我只能开发AdminH5;经过一系列焦虑的面试后,咸鱼翻身,这辈子我也做上监控了。千万不要以为我是因为监控的重要性才这么执着,人往往得不到的东西才是最有吸引力的。

在写这篇文章时,我也在思考,为什么走到哪里都会有一群程序员喜欢封装监控呢?即使换个公司、换个组,依然可能需要有人来迭代监控。嗯,话不多说,先点关注,正文开始

错误监控的核心价值

如果让你封装一个前端监控,你会怎么设计监控的上报优先级?

对于一个网页来说,能否带给用户好的体验,核心指标就是 白屏时长 和 FMP时长,这两项指标直接影响用户的 留存率 和 体验

下面通过数据加强理解:

  • 白屏时间 > 3秒 导致用户流失率上升47%
  • 接口错误率 > 0.5%  造成订单转化率下降23%
  • JS错误数 > 1/千次访问 预示着系统稳定性风险

设想一下,当你访问页面时白屏等待了3秒,并且页面没有骨架屏或者Loading态时,你会不会觉得这个页面挂了?这时候如果我们的监控优先关注的是性能,可能用户已经退出了,我们的上报还没调用到。

在这个白屏等待的过程中,JS Error可能已经打印在控制台了,接口可能已经返回了错误信息,但是程序员却毫无感知。

优先上报错误信息,本质是为了提升生产环境的错误响应速度、减少生产环境的损失、提高上线流程的规范。以下是错误响应的黄金时间轴:

时间窗口响应动作业务影响
< 1分钟自动熔断异常接口避免错误扩散
1-5分钟触发告警通知值班人员降低MTTR(平均修复时间)
>5分钟生成故障诊断报告优化事后复盘流程

重要章节

一:错误类型,你需要关注的五大场景

技术本质:任何错误收集系统都需要先明确错误边界。前端错误主要分为两类: 显性错误(直接阻断执行)和 隐性错误(资源加载、异步异常等)。

// 显性错误(同步执行阶段)
function criticalFunction() {
undefinedVariable.access(); // ReferenceError
}

// 隐性错误(异步场景)
fetchData().then(() => {
invalidJSON.parse(); // 异步代码中的错误
});

关键分类: 通过错误本质将前端常见错误分为5种类型,图示如下。 image.png

  1. 语法层错误(SyntaxError)
    ESLint 可拦截,但运行时需注意动态语法(如 eval,这个用法不推荐)。
  2. 运行时异常
    错误的时机场景大部分是在页面渲染完成后,用户对页面发生交互行为,触发JS执行异常。以下是模拟报错的一个例子,用于学习。 // 典型场景 element.addEventListener('click', () => { throw new Error('Event handler crash'); });
  3. 资源加载失败
    常见的资源比如图片、JS脚本、字体文件、外链引入的三方依赖等。我们可以通过全局监听处理,比如使用document.addEventListener('error', handler, true)来捕获资源加载失败的情况。但需要注意以下几点:

收起阅读 »

某程序员自曝:凡是打断点调试代码的,都不是真正的程序员,都是外行

大家好,我是大明哥,一个专注 「死磕 Java」 的硬核程序员。 某天我在逛今日头条的时候,看到一个大佬,说凡是打断点调试代码的,都不是真正的程序员,都是外行。 我靠,我敲了 10 多年代码,打了 10 多年的断点,竟然说我是外行!!我还说,真正的大佬都是...
继续阅读 »

大家好,我是大明哥,一个专注 「死磕 Java」 的硬核程序员。



某天我在逛今日头条的时候,看到一个大佬,说凡是打断点调试代码的,都不是真正的程序员,都是外行。



我靠,我敲了 10 多年代码,打了 10 多年的断点,竟然说我是外行!!我还说,真正的大佬都是用文档编辑器来写代码呢!!!


其实,打断点不丢脸,丢脸的是工作若干年后只知道最基础的断点调试!大明哥就见过有同事因为 for 循环里面实体对象报空指针异常,不知道怎么调试,选择一条一条得看,极其浪费时间!!所以,大明哥来分享一些 debug 技巧,赶紧收藏,日后好查阅!!


Debug 分类


对于很多同学来说,他们几乎就只知道在代码上面打断点,其实断点可以打在多个地方。


行断点


行断点的投标就是一个红色的圆形点。在需要断点的代码行头点击即可打上:



方法断点


方法断点就是将断点打在某个具体的方法上面,当方法执行的时候,就会进入断点。这个当我们阅读源码或者跟踪业务流程时比较有用。尤其是我们在阅读源码的时候,我们知道优秀的源码(不优秀的源码你也不会阅读)各种设计模式使用得飞起,什么策略、模板方法等等。具体要走到哪个具体得实现,还真不是猜出来,比如下面代码:


public interface Service {
void test();
}

public class ServiceA implements Service{
@Override
public void test() {
System.out.println("ServiceA");
}
}


public class ServiceB implements Service{
@Override
public void test() {
System.out.println("ServiceB");
}
}


public class ServiceC implements Service{
@Override
public void test() {
System.out.println("ServiceC");
}
}


public class DebugTest {
public static void main(String[] args) {
Service service = new ServiceA();
service.test();
}
}

在运行时,你怎么知道他要进入哪个类的 test() 方法呢?有些小伙伴可能就会在 ServiceAServiceBServiceC 中都打断点(曾经我也是这么干的,初学者可以理解...),这样就可以知道进入哪个了。其实我们可以直接在接口 Servicetest() 方法上面打断点,这样也是可以进入具体的实现类的方法:



当然,也可以在方法调用的地方打断点,进入这个断点后,按 F7 就可以了。


属性断点


我们也可以在某个属性字段上面打断点,这样就可以监听这个属性的读写变化过程。比如,我们定义这样的:


@Getter
@Setter
@AllArgsConstructor
public class Student {

private String name;

private Integer age;
}

public class ServiceA implements Service{
@Override
public void test() {
Student student = new Student("张三",12);

System.out.println(student.getName());

student.setName("李四");
}
}

如下:



断点技巧


条件断点


在某些场景下,我们需要在特定的条件进入断点,尤其是 for 循环中(我曾经在公司看到一个小伙伴在循环内部看 debug 数据,惊呆我了),比如下面代码:


public class DebugTest {

public static void main(String[] args) {
List<Student> studentList = new ArrayList<>();
for (int i = 1 ; i < 1000 ; i++) {
if (new Random().nextInt(100) % 10 == 0) {
studentList.add(new Student("" + i, i));
} else {
studentList.add(new Student("" + i, i));
}
}

for (Student student : studentList) {
System.out.println(student.toString());
}
}
}

我们在 System.out.println(student.toString()); 打个断点,但是要 name"skjava" 开头时才进入,这个时候我们就可以使用条件断点了:



条件断点是非常有用的一个断点技巧,对于我们调试复杂的业务场景,尤其是 for、if 代码块时,可以节省我们很多的调试时间。


模拟异常


这个技巧也是很有用,在开发阶段我们就需要人为制造异常场景来验证我们的异常处理逻辑是否正确。比如如下代码:


public class DebugTest {

public static void main(String[] args) {
methodA();

try {
methodB();
} catch (Exception e) {
e.printStackTrace();
// do something
}

methodC();
}

public static void methodA() {
System.out.println("methodA...");
}

public static void methodB() {
System.out.println("methodA...");
}

public static void methodC() {
System.out.println("methodA...");
}
}

我们希望在 methodB() 方法中抛出异常,来验证 catch(Exception e) 中的 do something 是否处理正确。以前大明哥是直接在 methodB() 中 throw 一个异常,或者 1 / 0。这样做其实并没有什么错,只不过不是很优雅,同时也会有一个风险,就是可能会忘记删除这个测试代码,将异常提交上去了,最可怕的还是上了生产。


所以,我们可以使用 idea 模拟异常。



  • 我们首先在 methodB() 打上一个断点

  • 运行代码,进入断点处

  • 在 Frames 中找到对应的断点记录,右键,选择 Throw Execption

  • 输入你想抛出的异常,点击 ok 即可



这个技巧在我们调试异常场景时非常有用!!!


多线程调试


不知道有小伙伴遇到过这样的场景:在你和前端进行本地调试时,你同时又要调试自己写的代码,前端也要访问你的本地调试,这个时候你打断点了,前端是无法你本地的。为什么呢?因为 Idea 在 debug 时默认阻塞级别为 ALL,如果你进入 debug 场景了,idea 就会阻塞其他线程,只有当前调试线程完成后才会走其他线程。


这个时候,我们可以在 View Breakpoints 中选择 Thread,同时点击 Make Default设置为默认选项。这样,你就可以调试你的代码,前端又可以访问你的应用了。



或者



调试 Stream


Java 中的 Stream 好用是好用,但是依然有一些小伙伴不怎么使用它,最大的一个原因就是它不好调试。你利用 Stream 处理一个 List 对象后,发现结果不对,但是你很难判断到底是哪一行出来问题。我们看下面代码:


public class DebugTest {

public static void main(String[] args) {
List<Student> studentList = new ArrayList<>();
for (int i = 1; i < 1000; i++) {
if (new Random().nextInt(100) % 10 == 0) {
studentList.add(new Student("" + i, i));
} else {
studentList.add(new Student("" + i, i));
}
}

studentList = studentList.stream()
.filter(student -> student.getName().startsWith(""))
.peek(item -> {
item.setName(item.getName() + "-**");
item.setAge(item.getAge() * 10);
}).collect(Collectors.toList());
}
}

在 stream() 打上断点,运行代码,进入断点后,我们只需要点击下图中的按钮:




在这个窗口中会记录这个 Stream 操作的每一个步骤,我们可以点击每个标签来看数据处理是否符合预期。这样是不是就非常方便了。


有些小伙伴的 idea 版本可能过低,需要安装 Java Stream Debugger 插件才能使用。


操作回退


我们 debug 调试的时候肯定不是一行一行代码的调试,而是在每个关注点处打断点,然后跳着看。但是跳到某个断点处时,突然发现有个变量的值你没有关注到需要回退到这个变量值的赋值处,这个时候怎么办?我们通常的做法是重新来一遍。虽然,可以达到我们的预期效果,但是会比较麻烦,其实 idea 有一个回退断点的功能,非常强大。在 idea 中有两种回退:



  • Reset Frame


看下面代码:


public class DebugTest {

public static void main(String[] args) {
int a = 1;
int b = 2;
int c = (a + b) * 2;

int d = addProcessor(a, b,c);

System.out.println();
}

private static int addProcessor(int a, int b, int c) {
a = a++;
b = b++;
return a + b + c;
}
}

我们在 addProcessor()return a + b + c; 打上断点,到了这里 ab 的值已经发生了改变,如果我们想要知道他们两的原始值,就只能回到开始的地方。idea 提供了一个 Reset Frame 功能,这个功能可以回到上一个方法处。如下图:




  • Jump To Line


Reset Frame 虽然可以用,但是它有一定的局限性,它只能方法级别回退,是没有办法向前或向后跳着我们想要执行的代码处。但 Jump To Line 可以做到。


Jump To Line 是一个插件,所以,需要先安装它。



由于大明哥使用的 idea 版本是 2024.2,这个插件貌似不支持,所以就在网上借鉴了一张图:



在执行到 debug 处时,会出现一个黄颜色的箭头,我们可以将这个箭头拖动到你想执行的代码处就可以了。向前、向后都可以,是不是非常方便。


目前这 5 个 debug 技巧是大明哥在工作中运用最多的,还有一个就是远程 debug 调试,但是这个我个人认为是野路子,大部分公司一般是不允许这么做的,所以大明哥就不演示了!


作者:大明哥_
来源:juejin.cn/post/7470185977434144778
收起阅读 »

产品:大哥,你这列表查询有问题啊!

前言 👳‍♂️产品大哥(怒气冲冲跑过来): “大哥你这查询列表有问题啊,每次点一下查询,返回的数据不一样呢” 👦我:“FKY 之前不是说好的吗,加了排序查询很卡,就取消了” 🧔技术经理:“卡主要是因为分页查询加了排序之后,mybatisPlus 生成的 cou...
继续阅读 »

前言


👳‍♂️产品大哥(怒气冲冲跑过来): “大哥你这查询列表有问题啊,每次点一下查询,返回的数据不一样呢”


👦:“FKY 之前不是说好的吗,加了排序查询很卡,就取消了”


🧔技术经理:“卡主要是因为分页查询加了排序之后,mybatisPlus 生成的 count 也会有Order by就 很慢,自己实现一个count 就行了”


👦:“分页插件在执行统计操作的时候,一般都会对Sql 简单的优化,会去掉排序的”



今天就来看看分页插件处理 count 的时候的优化逻辑,是否能去除order by


同时 简单阐述一下 order bylimit 的运行原理


往期好文:最近发现一些同事的代码问题



mybatisPlus分页插件count 运行原理


分页插件都是基于MyBatis 的拦截器接口Interceptor实现,这个就不用多说了。下面看一下分页插件的处理count的代码,以及优化的逻辑。



详细代码见:com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor.class



count sql 从获取到执行的主要流程


1.确认count sql MappedStatement 对象:

先查询Page对象中 是否有countId(countId 为mapper sql id),有的话就用自定义的count sql,没有的话就自己通过查询语句构建一个count MappedStatement


2.优化count sql

得到countMs构建成功之后对count SQL进行优化,最后 执行count SQL,将结果 set 到page对象中。


public boolean willDoQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
IPage<?> page = ParameterUtils.findPage(parameter).orElse(null);
if (page == null || page.getSize() < 0 || !page.searchCount()) {
return true;
}

BoundSql countSql;
// -------------------------------- 根据“countId”获取自定义的count MappedStatement
MappedStatement countMs = buildCountMappedStatement(ms, page.countId());
if (countMs != null) {
countSql = countMs.getBoundSql(parameter);
} else {
//-------------------------------------------根据查询ms 构建统计SQL的MS
countMs = buildAutoCountMappedStatement(ms);
//-------------------------------------------优化count SQL
String countSqlStr = autoCountSql(page, boundSql.getSql());
PluginUtils.MPBoundSql mpBoundSql = PluginUtils.mpBoundSql(boundSql);
countSql = new BoundSql(countMs.getConfiguration(), countSqlStr, mpBoundSql.parameterMappings(), parameter);
PluginUtils.setAdditionalParameter(countSql, mpBoundSql.additionalParameters());
}

CacheKey cacheKey = executor.createCacheKey(countMs, parameter, rowBounds, countSql);
//----------------------------------------------- 统计SQL
List<Object> result = executor.query(countMs, parameter, rowBounds, resultHandler, cacheKey, countSql);
long total = 0;
if (CollectionUtils.isNotEmpty(result)) {
// 个别数据库 count 没数据不会返回 0
Object o = result.get(0);
if (o != null) {
total = Long.parseLong(o.toString());
}
}
// ---------------------------------------set count ret
page.setTotal(total);
return continuePage(page);
}

count SQL 优化逻辑


主要优化的是以下两点



  1. 去除 SQl 中的order by

  2. 去除 left join


哪些情况count 优化限制



  1. SQL 中 有 这些集合操作的 INTERSECT,EXCEPT,MINUS,UNION 直接不优化count

  2. 包含groupBy 不去除orderBy

  3. order by 里带参数,不去除order by

  4. 查看select 字段中是否动态条件,如果有条件字段,则不会优化 Count SQL

  5. 包含 distinct、groupBy不优化

  6. 如果 left join 是子查询,并且子查询里包含 ?(代表有入参) 或者 where 条件里包含使用 join 的表的字段作条件,就不移除 join

  7. 如果 where 条件里包含使用 join 的表的字段作条件,就不移除 join

  8. 如果 join 里包含 ?(代表有入参) 就不移除 join


详情可阅读一下代码:


/**
* 获取自动优化的 countSql
*
* @param page 参数
* @param sql sql
* @return countSql
*/

protected String autoCountSql(IPage<?> page, String sql) {
if (!page.optimizeCountSql()) {
return lowLevelCountSql(sql);
}
try {
Select select = (Select) CCJSqlParserUtil.parse(sql);
SelectBody selectBody = select.getSelectBody();
// https://github.com/baomidou/mybatis-plus/issues/3920 分页增加union语法支持
//----------- SQL 中 有 这些集合操作的 INTERSECT,EXCEPT,MINUS,UNION 直接不优化count

if (selectBody instanceof SetOperationList) {
// ----lowLevelCountSql 具体实现: String.format("SELECT COUNT(*) FROM (%s) TOTAL", originalSql)
return lowLevelCountSql(sql);
}
....................省略.....................
if (CollectionUtils.isNotEmpty(orderBy)) {
boolean canClean = true;
if (groupBy != null) {
// 包含groupBy 不去除orderBy
canClean = false;
}
if (canClean) {
for (OrderByElement order : orderBy) {
//-------------- order by 里带参数,不去除order by
Expression expression = order.getExpression();
if (!(expression instanceof Column) && expression.toString().contains(StringPool.QUESTION_MARK)) {
canClean = false;
break;
}
}
}
//-------- 清除order by
if (canClean) {
plainSelect.setOrderByElements(null);
}
}
//#95 Github, selectItems contains #{} ${}, which will be translated to ?, and it may be in a function: power(#{myInt},2)
// ----- 查看select 字段中是否动态条件,如果有条件字段,则不会优化 Count SQL
for (SelectItem item : plainSelect.getSelectItems()) {
if (item.toString().contains(StringPool.QUESTION_MARK)) {
return lowLevelCountSql(select.toString());
}
}
// ---------------包含 distinct、groupBy不优化
if (distinct != null || null != groupBy) {
return lowLevelCountSql(select.toString());
}
// ------------包含 join 连表,进行判断是否移除 join 连表
if (optimizeJoin && page.optimizeJoinOfCountSql()) {
List<Join> joins = plainSelect.getJoins();
if (CollectionUtils.isNotEmpty(joins)) {
boolean canRemoveJoin = true;
String whereS = Optional.ofNullable(plainSelect.getWhere()).map(Expression::toString).orElse(StringPool.EMPTY);
// 不区分大小写
whereS = whereS.toLowerCase();
for (Join join : joins) {
if (!join.isLeft()) {
canRemoveJoin = false;
break;
}
.........................省略..............
} else if (rightItem instanceof SubSelect) {
SubSelect subSelect = (SubSelect) rightItem;
/* ---------如果 left join 是子查询,并且子查询里包含 ?(代表有入参) 或者 where 条件里包含使用 join 的表的字段作条件,就不移除 join */
if (subSelect.toString().contains(StringPool.QUESTION_MARK)) {
canRemoveJoin = false;
break;
}
str = subSelect.getAlias().getName() + StringPool.DOT;
}
// 不区分大小写
str = str.toLowerCase();
if (whereS.contains(str)) {
/*--------------- 如果 where 条件里包含使用 join 的表的字段作条件,就不移除 join */
canRemoveJoin = false;
break;
}

for (Expression expression : join.getOnExpressions()) {
if (expression.toString().contains(StringPool.QUESTION_MARK)) {
/* 如果 join 里包含 ?(代表有入参) 就不移除 join */
canRemoveJoin = false;
break;
}
}
}
// ------------------ 移除join
if (canRemoveJoin) {
plainSelect.setJoins(null);
}
}
}
// 优化 SQL-------------
plainSelect.setSelectItems(COUNT_SELECT_ITEM);
return select.toString();
} catch (JSQLParserException e) {
..............
}
return lowLevelCountSql(sql);
}

order by 运行原理


order by 排序,具体怎么排取决于优化器的选择,如果优化器认为走索引更快,那么就会用索引排序,否则,就会使用filesort (执行计划中extra中提示:using filesort),但是能走索引排序的情况并不多,并且确定性也没有那么强,很多时候,还是走的filesort


索引排序


索引排序,效率是最高的,就算order by 后面的字段是 索引列,也不一定就是通过索引排序。这个过程是否一定用索引,完全取决于优化器的选择。


filesort 排序


如果不能走索引排序, MySQL 会执行filesort操作以读取表中的行并对它们进行排序。


在进行排序时,MySQL 会给每个线程分配一块内存用于排序,称为 sort_buffer,它的大小是由sort_buffer_size控制的。


sort_buffer_size的大小不同,会在不同的地方进行排序操作:



  • 如果要排序的数据量小于 sort_buffer_size,那么排序就在内存中完成。

  • 如果排序数据量大于sort_buffer_size,则需要利用磁盘临时文件辅助排序。

    采用多路归并排序的方式将磁盘上的多个有序子文件合并成一个有序的结果集





filesort 排序 具体实现方式


FileSort是MySQL中用于对数据进行排序的一种机制,主要有以下几种实现方式:


全字段排序


  • 原理:将查询所需的所有字段,包括用于排序的字段以及其他SELECT列表中的字段,都读取到排序缓冲区中进行排序。这样可以在排序的同时获取到完整的行数据,减少访问原表数据的次数。

  • 适用场景:当排序字段和查询返回字段较少,并且排序缓冲区能够容纳这些数据时,全字段排序效率较高。


行指针排序


  • 原理:只将排序字段和行指针(指向原表中数据行的指针)读取到排序缓冲区中进行排序。排序完成后,再根据行指针回表读取所需的其他字段数据。

  • 适用场景:当查询返回的字段较多,而排序缓冲区无法容纳全字段数据时,行指针排序可以减少排序缓冲区的占用,提高排序效率。但由于需要回表操作,可能会增加一定的I/O开销。


多趟排序


  • 原理:如果数据量非常大,即使采用行指针排序,排序缓冲区也无法一次容纳所有数据,MySQL会将数据分成多个较小的部分,分别在排序缓冲区中进行排序,生成多个有序的临时文件。然后再将这些临时文件进行多路归并,最终得到完整的有序结果。

  • 适用场景:适用于处理超大数据量的排序操作,能够在有限的内存资源下完成排序任务,但会产生较多的磁盘I/O操作,性能相对较低


优先队列排序


  • 原理:结合优先队列数据结构进行排序。对于带有LIMIT子句的查询,MySQL会创建一个大小为LIMIT值的优先队列。在读取数据时,将数据放入优先队列中,根据排序条件进行比较和调整。当读取完所有数据或达到一定条件后,优先队列中的数据就是满足LIMIT条件的有序结果。

  • 适用场景:特别适用于需要获取少量排序后数据的情况,如查询排名前几的数据。可以避免对大量数据进行全量排序,提高查询效率。



❗所以减少查询字段 ,以及 减少 返回的行数,对于排序SQL 的优化也是非常重要

❗以及order by 后面尽量使用索引字段,以及行数限制



limit 运行原理


limit执行过程
对于 SQL 查询中 LIMIT 的使用,像 LIMIT 10000, 100 这种形式,MySQL 的执行顺序大致如下:



  1. 从数据表中读取所有符合条件的数据(包括排序和过滤)。

  2. 将数据按照 ORDER BY 排序。

  3. 根据 LIMIT 参数选择返回的记录:

    • 跳过前 10000 行数据(这个过程是通过丢弃数据来实现的)。

    • 然后返回接下来的 100 行数据。




所以,LIMIT 是先检索所有符合条件的数据,然后丢弃掉前面的行,再返回指定的行数。这解释了为什么如果数据集很大,LIMIT 会带来性能上的一些问题,尤其是在有很大的偏移量(比如 LIMIT 10000, 100)时。


总结


本篇文章分析,mybatisPlus 分页插件处理count sql 的逻辑,以及优化过程,同时也简单分析order bylimit 执行原理。


希望这篇文章能够让你对SQL优化 有不一样的认知,最后感谢各位老铁一键三连!



ps: 云服务器找我返点;面试宝典私;收徒ING;



作者:提前退休的java猿
来源:juejin.cn/post/7457934738356338739
收起阅读 »

字节2面:为了性能,你会违反数据库三范式吗?

大家好,我是猿java。 数据库的三大范式,它是数据库设计中最基本的三个规范,那么,三大范式是什么?在实际开发中,我们一定要严格遵守三大范式吗?这篇文章,我们一起来聊一聊。 1. 三大范式 1. 第一范式(1NF,确保每列保持原子性) 第一范式要求数据库中的每...
继续阅读 »

大家好,我是猿java


数据库的三大范式,它是数据库设计中最基本的三个规范,那么,三大范式是什么?在实际开发中,我们一定要严格遵守三大范式吗?这篇文章,我们一起来聊一聊。


1. 三大范式


1. 第一范式(1NF,确保每列保持原子性)


第一范式要求数据库中的每个表格的每个字段(列)都具有原子性,即字段中的值不可再分割。换句话说,每个字段只能存储一个单一的值,不能包含集合、数组或重复的组。


如下示例: 假设有一个学生表 Student,结构如下:


学生ID姓名电话号码
1张三123456789, 987654321
2李四555555555

在这个表中,电话号码字段包含多个号码,违反了1NF的原子性要求。为了满足1NF,需要将电话号码拆分为单独的记录或创建一个新的表。


满足 1NF后的设计:


学生表 Student


学生ID姓名
1张三
2李四

电话表 Phone


电话ID学生ID电话号码
11123456789
21987654321
32555555555

1.2 第二范式(2NF,确保表中的每列都和主键相关)


第二范式要求满足第一范式,并且消除表中的部分依赖,即非主键字段必须完全依赖于主键,而不是仅依赖于主键的一部分。这主要适用于复合主键的情况。


如下示例:假设有一个订单详情表 OrderDetail,结构如下:


订单ID商品ID商品名称数量单价
1001A01苹果102.5
1001A02橙子53.0
1002A01苹果72.5

在上述表中,主键是复合主键 (订单ID, 商品ID)商品名称单价只依赖于复合主键中的商品ID,而不是整个主键,存在部分依赖,违反了2NF。


满足 2NF后的设计:


订单详情表 OrderDetail


订单ID商品ID数量
1001A0110
1001A025
1002A017

商品表 Product


商品ID商品名称单价
A01苹果2.5
A02橙子3.0

1.3 第三范式(3NF,确保每列都和主键列直接相关,而不是间接相关)


第三范式要求满足第二范式,并且消除表中的传递依赖,即非主键字段不应依赖于其他非主键字段。换句话说,所有非主键字段必须直接依赖于主键,而不是通过其他非主键字段间接依赖。


如下示例:假设有一个员工表 Employee,结构如下:


员工ID员工姓名部门ID部门名称
E01王五D01销售部
E02赵六D02技术部
E03孙七D01销售部

在这个表中,部门名称依赖于部门ID,而部门ID依赖于主键员工ID,形成了传递依赖,违反了3NF。


满足3NF后的设计:


员工表 Employee


员工ID员工姓名部门ID
E01王五D01
E02赵六D02
E03孙七D01

部门表 Department


部门ID部门名称
D01销售部
D02技术部

通过将部门信息移到单独的表中,消除了传递依赖,使得数据库结构符合第三范式。


最后,我们总结一下数据库设计的三大范式:



  • 第一范式(1NF): 确保每个字段的值都是原子性的,不可再分。

  • 第二范式(2NF): 在满足 1NF的基础上,消除部分依赖,确保非主键字段完全依赖于主键。

  • 第三范式(3NF): 在满足 2NF的基础上,消除传递依赖,确保非主键字段直接依赖于主键。


2. 破坏三范式


在实际工作中,尽管遵循数据库的三大范式(1NF、2NF、3NF)有助于提高数据的一致性和减少冗余,但在某些情况下,为了满足性能、简化设计或特定业务需求,我们可能需要违反这些范式。


下面列举了一些常见的破坏三范式的原因及对应的示例。


2.1 性能优化


在高并发、大数据量的应用场景中,严格遵循三范式可能导致频繁的联表查询,增加查询时间和系统负载。为了提高查询性能,设计者可能会通过冗余数据来减少联表操作。


假设有一个电商系统,包含订单表 Orders 和用户表 Users。在严格 3NF设计中,订单表只存储 用户ID,需要通过联表查询获取用户的详细信息。


但是,为了查询性能,我们通常会在订单表中冗余存储 用户姓名用户地址等信息,因此,查询订单信息时无需联表查询 Users 表,从而提升查询速度。


破坏 3NF后的设计:


订单ID用户ID用户姓名用户地址订单日期总金额
1001U01张三北京市2023-10-01500元
1002U02李四上海市2023-10-02300元

2.2 简化查询和开发


严格规范化可能导致数据库结构过于复杂,增加开发和维护的难度,为了简化查询逻辑和减少开发复杂度,我们也可能会选择适当的冗余。


比如,在内容管理系统(CMS)中,文章表 Articles 和分类表 Categories 通常是独立的,如果频繁需要显示文章所属的分类名称,联表查询可能增加复杂性。因此,通过在 Articles 表中直接存储 分类名称,可以简化前端展示逻辑,减少开发工作量。


破坏 3NF后的设计:


文章ID标题内容分类ID分类名称
A01文章一C01技术
A02文章二C02生活

2.3 报表和数据仓库


在数据仓库和报表系统中,通常需要快速读取和聚合大量数据。为了优化查询性能和数据分析,可能会采用冗余的数据结构,甚至使用星型或雪花型模式,这些模式并不完全符合三范式。


在销售数据仓库中,为了快速生成销售报表,可能会创建一个包含维度信息的事实表。


破坏 3NF后的设计:


销售ID产品ID产品名称类别销售数量销售金额销售日期
S01P01手机电子10050000元2023-10-01
S02P02书籍教育20020000元2023-10-02

在事实表中直接存储 产品名称类别,避免了需要联表查询维度表,提高了报表生成的效率。


2.4 特殊业务需求


在某些业务场景下,可能需要快速响应特定的查询或操作,这时通过适当的冗余设计可以满足业务需求。


比如,在实时交易系统中,为了快速计算用户的账户余额,可能会在用户表中直接存储当前余额,而不是每次交易时都计算。


破坏 3NF后的设计:


用户ID用户名当前余额
U01王五10000元
U02赵六5000元

在交易记录表中存储每笔交易的增减,但直接在用户表中维护 当前余额,避免了每次查询时的复杂计算。


2.5 兼顾读写性能


在某些应用中,读操作远多于写操作。为了优化读性能,可能会通过数据冗余来提升查询速度,而接受在数据写入时需要额外的维护工作。


社交媒体平台中,用户的好友数常被展示在用户主页上。如果每次请求都计算好友数量,效率低下。可以在用户表中维护一个 好友数 字段。


破坏3NF后的设计:


用户ID用户名好友数
U01Alice150
U02Bob200

通过在 Users 表中冗余存储 好友数,可以快速展示,无需实时计算。


2.6 快速迭代和灵活性


在快速发展的产品或初创企业中,数据库设计可能需要频繁调整。过度规范化可能导致设计不够灵活,影响迭代速度。适当的冗余设计可以提高开发的灵活性和速度。


一个初创电商平台在初期快速上线,数据库设计时为了简化开发,可能会将用户的收货地址直接存储在订单表中,而不是单独创建地址表。


破坏3NF后的设计:


订单ID用户ID用户名收货地址订单日期总金额
O1001U01李雷北京市海淀区…2023-10-01800元
O1002U02韩梅梅上海市浦东新区…2023-10-021200元

这样设计可以快速上线,后续根据需求再进行规范化和优化。


2.7 降低复杂性和提高可理解性


有时,过度规范化可能使数据库结构变得复杂,难以理解和维护。适度的冗余可以降低设计的复杂性,提高团队对数据库结构的理解和沟通效率。


在一个学校管理系统中,如果将学生的班级信息独立为多个表,可能增加理解难度。为了简化设计,可以在学生表中直接存储班级名称。


破坏3NF后的设计:


学生ID姓名班级ID班级名称班主任
S01张三C01三年级一班李老师
S02李四C02三年级二班王老师

通过在学生表中直接存储 班级名称班主任,减少了表的数量,简化了设计。


3. 总结


本文,我们分析了数据库的三范式以及对应的示例,它是数据库设计的基本规范。但是,在实际工作中,为了满足性能、简化设计、快速迭代或特定业务需求,我们很多时候并不会严格地遵守三范式。


所以说,架构很多时候都是业务需求、数据一致性、系统性能、开发效率等各种因素权衡的结果,我们需要根据具体应用场景做出合理的设计选择。


4. 学习交流


如果你觉得文章有帮助,请帮忙转发给更多的好友,或关注公众号:猿java,持续输出硬核文章。


作者:猿java
来源:juejin.cn/post/7455635421529145359
收起阅读 »

Redis - 全局ID生成器 RedisIdWorker

概述 定义:一种分布式系统下用来生成全局唯一 ID 的工具 特点 唯一性,满足优惠券需要唯一的 ID 标识用于核销 高可用,随时能够生成正确的 ID 高性能,生成 ID 的速度很快 递增性,生成的 ID 是逐渐变大的,有利于数据库形成索引 安全性,生成的 ...
继续阅读 »

概述



  1. 定义:一种分布式系统下用来生成全局唯一 ID 的工具

  2. 特点



    1. 唯一性,满足优惠券需要唯一的 ID 标识用于核销

    2. 高可用,随时能够生成正确的 ID

    3. 高性能,生成 ID 的速度很快

    4. 递增性,生成的 ID 是逐渐变大的,有利于数据库形成索引

    5. 安全性,生成的 ID 无明显规律,可以避免间接泄露信息

    6. 生成量大,可满足优惠券订单数据量大的需求



  3. ID 组成部分



    1. 符号位:1bit,永远为0

    2. 时间戳:31bit,以秒为单位,可以使用69年

    3. 序列号:32bit,秒内的计数器,支持每秒产生2^32个不同ID




image.png


代码实现



  1. 目标:手动实现一个简单的全局 ID 生成器

  2. 实现流程



    1. 创建生成器:在 utils 包下创建 RedisIdWorker 类,作为 ID 生成器

    2. 创建时间戳:创建一个时间戳,即 RedisId 的高32位

    3. 获取当前日期:创建当前日期对象 date,用于自增 id 的生成

    4. count:设置 Id 格式,保证 Id 严格自增长

    5. 拼接 Id 并将其返回



  3. 代码实现


    @Component
    public class RedisIdWorker {

    // 开始时间戳
    private static final long BEGIN_TIMESTAMP = 1640995200L;

    // 序列号的位数
    private static final int COUNT_BITS = 32;

    private StringRedisTemplate stringRedisTemplate;

    public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {
    this.stringRedisTemplate = stringRedisTemplate;
    }

    // 获取下一个自动生成的 id
    public long nextId(String keyPrefix){
    // 1.生成时间戳
    LocalDateTime now = LocalDateTime.now();
    long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
    long timestamp = nowSecond - BEGIN_TIMESTAMP;

    // 3.获取当前日期
    String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
    // 4.获取自增长值:生成一个递增计数值。每次调用 increment 方法时,它会在这个key之前的自增值的基础上+1(第一次为0)
    long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);
    // 5.拼接并返回
    return timestamp << COUNT_BITS | count;
    }
    }



测试


一、CountDownLatch 工具类



  1. 定义:一个同步工具类,用于协调多个线程的等待与唤醒

  2. 功能



    1. 控制多个线程的执行顺序和同步

    2. 确保主线程在所有子线程完成后才继续执行

    3. 防止主线程过早结束导致子线程执行状态丢失



  3. 常用方法



    1. await:用于主线程的阻塞方法,使其阻塞等待直到计数器归零

    2. countDown:用于子线程的计数方法,使计数器递减




二、ExecutorService & Executors



  1. 定义:Java 提供的线程池管理接口

  2. 功能



    1. 简化异步任务的执行管理

    2. 提供有关 “线程池” 和 “任务执行” 的标准 API



  3. 常用方法


    方法说明
    Executors.newFixedThreadPool(xxxThreads)Executors 提供的工厂方法,用于创建 ExecutorService 实例
    execute(functionName)调用线程执行 functionName 任务,无返回值
    ⭐ submit(functionName)调用线程执行 functionName 任务,返回一个 Future 类
    invokeAny(functionName)调用线程执行一组 functionName 任务,返回首成功执行的任务的结果
    invokeAll(functionName)调用线程执行一组 functionName 任务,返回所有任务执行的结果
    ⭐ shutdown()停止接受新任务,并在所有正在运行的线程完成当前工作后关闭
    ⭐ awaitTermination()停止接受新任务,在指定时间内等待所有任务完成


  4. 参考资料:一文秒懂 Java ExecutorService

  5. 代码实现



    1. 目标:测试 redisIdWorker 在高并发场景下的表现(共生成 30000 个 id)


    private ExecutorService es = Executors.newFixedThreadPool(500);     // 创建一个含有 500 个线程的线程池

    @Test
    void testIdWorker() throws InterruptedException
    {

    CountDownLatch latch = new CountDownLatch(300); // 定义一个工具类,统计线程执行300次task的进度

    // 创建函数,供线程执行
    Runnable task = () -> {
    for(int i = 0; i < 100; i ++) {
    long id = redisIdWorker.nextId("order");
    System.out.println("id = " + id);
    }
    latch.countDown();
    }

    long begin = System.currentTimeMillis();
    for( int i = 0; i < 300 ; i ++) {
    es.submit(task);
    }
    latch.await(); // 主线程等待,直到 CountDownLatch 的计数归
    long end = System.currentTimeMillis();
    System.out.println("time = " + (end - begin)); // 打印任务执行的总耗时
    }





超卖问题



  1. 目标:通过数据库的 SQL 语句直接实现库存扣减(存在超卖问题)


一、乐观锁



  1. 定义:一种并发控制机制,不使用数据库锁,而是在更新时通过版本号或条件判断来确保数据一致性

  2. 优点:并发性能高,不会产生死锁,适合读多写少的场景

  3. 实现方式:CAS (Compare and Swap) - 比较并交换操作

  4. 实现示例 (基于版本号的乐观锁)


    boolean success = seckillVoucherService.update()
    .setSql("stock = stock - 1, version = version + 1")
    .eq("voucher_id", voucherId)
    .eq("version", version)
    .gt("stock", 0)
    .update();


  5. 分布式环境的局限性



    1. **原子性问题:**多个线程同时检查库存并更新时,可能导致超卖。这是因为检查和更新操作不是原子的

    2. **事务隔离:**在默认的"读已提交"隔离级别下,分布式环境中的多个节点可能读取到不一致的数据状态

    3. **分布式一致性:**在分布式环境中,不同的应用服务器可能同时操作数据库,而数据库层本身并不能感知跨服务器的事务一致性




二、悲观锁



  1. 定义:一种并发控制机制,通过添加同步锁强制线程串行执行

  2. 优点:实现简单,可以确保数据一致性

  3. 缺点:由于串行执行导致性能较低,不适合高并发场景

  4. 事务隔离级别:读已提交及以上

  5. 实现方法:使用 SQL 的 forUpdate() 子句,可以在查询时锁定选中的数据行。被锁定的行在当前事务提交或回滚前,其他事务无法对其进行修改或读取


三、事务隔离级别



  1. 定义:数据库事务并发执行时的隔离程度,用于解决并发事务可能带来的问题

  2. 优点:可以防止脏读、不可重复读和幻读等并发问题

  3. 缺点:隔离级别越高,并发性能越低

  4. 实现方法:



    • 读未提交(Read Uncommitted):允许读取未提交的数据

    • 读已提交(Read Committed):只允许读取已提交的数据

    • 可重复读(Repeatable Read):在同一事务中多次读取同样数据的结果是一致的

    • 串行化(Serializable):最高隔离级别,完全串行化执行






一人一单问题


一、单服务器系统解决方案



  1. 需求:每个人只能抢购一张大额优惠券,避免同一用户购买多张优惠券

  2. 重点



    1. 事务:库存扣减操作必须在事务中执行

    2. 粒度:事务粒度必须够小,避免影响性能

    3. 锁:事务开启时必须确保拿到当前下单用户的订单,并依据用户 Id 加锁

    4. 找到事务的代理对象,避免 Spring 事务注解失效 (需要给启动类加 @EnableAspectJAutoProxy(exposeProxy = true) 注解)



  3. 实现逻辑



    1. 获取优惠券 id、当前登录用户 id

    2. 查询数据库的优惠券表(voucher_order)



      1. 如果存在优惠券 id 和当前登录用户 id 都匹配的 order 则拒绝创建订单,返回 fail()

      2. 如果不存在则创建订单 voucherOrder 并保存至 voucher_order 表中,返回 ok()






二、分布式系统解决方案 (通过 Lua 脚本保证原子性)


一、优惠券下单逻辑


image.png


二、代码实现 (Lua脚本)


--1. 参数列表
--1.1. 优惠券id
local voucherId = ARGV[1]
--1.2. 用户id
local userId = ARGV[2]
--1.3. 订单id
local orderId = ARGV[3]

--2. 数据key
--2.1. 库存key
local stockKey = 'seckill:stock:' .. voucherId
--2.2. 订单key
local orderKey = 'seckill:order' .. voucherId

--3. 脚本业务
--3.1. 判断库存是否充足 get stockKey
if( tonumber( redis.call('get', stockKey) ) <= 0 ) then
return 1
end
--3.2. 判断用户是否下单 SISMEMBER orderKey userId
if( redis.call( 'sismember', orderKey, userId ) == 1 ) then
return 2
end
--3.4 扣库存: stockKey 的库存 -1
redis.call( 'incrby', stockKey, -1 )
--3.5 下单(保存用户): orderKey 集合中添加 userId
redis.call( 'sadd', orderKey, userId )
-- 3.6. 发送消息到队列中
redis.call( 'xadd', 'stream.orders', '*', 'userId', userId, 'voucherId', voucherId, 'id', orderId )

三、加载 Lua 脚本



  1. RedisScript 接口:用于绑定一个具体的 Lua 脚本

  2. DefaultRedisScript 实现类



    1. 定义:RedisScript 接口的实现类

    2. 功能:提前加载 Lua 脚本

    3. 示例


      // 创建Lua脚本对象
      private static final DefaultRedisScript<Long> SECKILL_SCRIPT;

      // Lua脚本初始化 (通过静态代码块)
      static {
      SECKILL_SCRIPT = new DefaultRedisScript<>();
      SECKILL_SCRIPT.setLocation(new ClassPathResource("/path/to/lua_script.lua"));
      SECKILL_SCRIPT.setResultType(Long.class);
      }





四、执行 Lua 脚本



  1. 调用Lua脚本 API :StringRedisTemplate.execute( RedisScript script, List keys, Object… args )

  2. 示例



    1. 执行 ”下单脚本” (此时不需要 key,因为下单时只需要用 userId 和 voucherId 查询是否有锁)


      Long result = stringRedisTemplate.execute(
      SECKILL_SCRIPT, // 要执行的脚本
      Collections.emptyList(), // KEY
      voucherId.toString(), userId.toString(), String.valueOf(orderId) // VALUES
      );


    2. 执行 “unlock脚本”






实战:添加优惠券 & 单服务器创建订单


添加优惠券



  1. 目标:商家在主页上添加一个优惠券的抢购链接,可以点击抢购按钮抢购优惠券


一、普通优惠券



  1. 定义:日常可获取的资源

  2. 代码实现


    @PostMapping
    public Result addVoucher(@RequestBody Voucher voucher) {
    voucherService.save(voucher);
    return Result.ok(voucher.getId());
    }



二、限量优惠券



  1. 定义:限制数量,需要设置时间限制、面对高并发请求的资源

  2. 下单流程



    1. 查询优惠券:通过 voucherId 查询优惠券

    2. 时间判断:判断是否在抢购优惠券的固定时间范围内

    3. 库存判断:判断优惠券库存是否 ≥ 1

    4. 扣减库存

    5. 创建订单:创建订单 VoucherOrder 对象,指定订单号,指定全局唯一 voucherId,指定用户 id

    6. 保存订单:保存订单到数据库

    7. 返回结果:Result.ok(orderId)



  3. 代码实现



    1. VoucherController


      @PostMapping("seckill")
      public Result addSeckillVoucher( @RequestBody Voucher voucher ){
      voucherService.addSeckillVoucher(voucher);
      return Result.o(voucher.getId());
      }


    2. VoucherServiceImpl


      @Override
      @Transactional
      public void addSeckillVoucher(Voucher voucher) {
      // 保存优惠券到数据库
      save(voucher);
      // 保存优惠券信息
      SeckillVoucher seckillVoucher = new SeckillVoucher();
      seckillVoucher.setVoucherId(voucher.getId());
      seckillVoucher.setStock(voucher.getStock());
      seckillVoucher.setBeginTime(voucher.getBeginTime());
      seckillVoucher.setEndTime(voucher.getEndTime());
      seckillVoucherService.save(seckillVoucher);
      // 保存优惠券到Redis中
      stringRedisTemplate.opsForValue().set(SECKILL_STOCK_KEY + voucher.getId(), voucher.getStock().toString());
      }





(缺陷) 优惠券下单功能


一、功能说明



  1. 目标:用户抢购代金券,保证用户成功获得优惠券,保证效率并且避免超卖

  2. 工作流程



    1. 提交优惠券 ID

    2. 查询优惠券信息 (下单时间是否合法,下单时库存是否充足)

    3. 扣减库存,创建订单

    4. 返回订单 ID




四、代码实现



  • VoucherOrderServiceImpl (下述代码在分布式环境下仍然存在超卖问题)


    @Service
    public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService{

    @Resource
    private ISeckillVoucherService seckillVoucherService;

    @Override
    public Result seckillVoucher(Long voucherId) {

    // 查询优惠券
    SeckillVoucher voucher = seckillVoucherService.getById(voucherId);

    // 优惠券抢购时间判断
    if(voucher.getBeginTime().isAfter(LocalDateTime.now) || voucher.getEndTime().isBefore(LocalDateTime.now()){
    return Result.fail("当前不在抢购时间!");
    }

    // 库存判断
    if(voucher.getStock() < 1){
    return Result.fail("库存不足!");
    }

    // !!! 实现一人一单功能 !!!
    Long userId = UserHolder.getUser().getId();
    synchronized (userId.toString().intern()) {
    IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
    return proxy.createVoucherOrder(voucherId);
    }
    }

    @Transactional
    public Result createVoucherOrder(Long userId) {
    Long userId = UserHolder.getUser().getId();

    // 查询当前用户是否已经购买过优惠券
    int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
    if( count > 0 ) {
    return Result.fail("当前用户不可重复购买!");

    // !!! 实现乐观锁 !!!
    // 扣减库存
    boolean success = seckillVoucherService.update()
    .setSql("stock = stock - 1") // set stock = stock - 1;
    .eq("voucher_id", voucherId).gt("stock", 0) // where voucher_id = voucherId and stock > 0;
    .update();
    if(!success) {
    return Result.fail("库存不足!");
    }

    // 创建订单
    VoucherOrder voucherOrder = new VoucherOrder();
    voucherOrder.setId(redisIdWorker.nextId("order"));
    voucherOrder.setUserId(UserHolder.getUser().getId());
    voucherOrder.setVoucherId(voucherId);
    save(voucherOrder);

    // 返回订单id
    return Result.ok(orderId);
    }



作者:LoopLee
来源:juejin.cn/post/7448119568567189530
收起阅读 »

虾皮开的很高,还有签字费。

大家好,我是二哥呀。 虾皮在去年之前,还是很多大厂人外逃的首选项,因为总部在新加坡,比较有外企范,但去年就突然急转直下,队伍收紧了不少。 作为东南亚电商市场的领头羊,市场覆盖了新加坡、马来西亚、泰国、菲律宾、印尼、越南等地,目前也开始进军巴西和墨西哥等新兴市场...
继续阅读 »

大家好,我是二哥呀。


虾皮在去年之前,还是很多大厂人外逃的首选项,因为总部在新加坡,比较有外企范,但去年就突然急转直下,队伍收紧了不少。


作为东南亚电商市场的领头羊,市场覆盖了新加坡、马来西亚、泰国、菲律宾、印尼、越南等地,目前也开始进军巴西和墨西哥等新兴市场。


我从 offershow 上也统计了一波 25 届虾皮目前开出来的薪资状况,方便大家做个参考。




  • 本科 985,后端岗,给了 32k,还有 5 万签字费,自己硬 A 出来的,15 天年假,base 上海,早 9.30 晚 7 点

  • 硕士双一流,后端给了 40 万年包,但已经签了其他的三方,拒了,11 月 31 日下午开的

  • 硕士 985,后端开发,给到了 23k,白菜价,主要面试的时候表现太差了

  • 硕士海归,后端开发给了 26.5k,还有三万签字费,咩别的高,就释放了

  • 硕士211,测试岗,只给了 21k,还有 3 万年终奖,但拒了


从目前统计到的情况来看,虾皮其实还蛮舍得给钱的,似乎有点超出了外界对他的期待。但很多同学因为去年的情况,虾皮只能拿来做备胎,不太敢去。


从虾皮母公司 Sea 发布的2024 年第三季度财报来看,电子商务(主要是 Shopee)收入增长了 42.6%,达到了 31.8 亿美元,均超预期。


总之,希望能尽快扭转颓势吧,这样学 Java 的小伙伴也可以有更多的选择。


那接下来,我们就以 Java 面试指南中收录的虾皮面经同学 13 一面为例,来看看下面的面试难度,自己是否有一战之力。


背八股就认准三分恶的面渣逆袭


虾皮面经同学 13 一面


tcp为什么是可靠的


TCP 首先通过三次握手和四次挥手来保证连接的可靠性,然后通过校验和、序列号、确认应答、超时重传、滑动窗口等机制来保证数据的可靠传输。


①、校验和:TCP 报文段包括一个校验和字段,用于检测报文段在传输过程中的变化。如果接收方检测到校验和错误,就会丢弃这个报文段。


推荐阅读:TCP 校验和计算方法


三分恶面渣逆袭:TCP 校验和


②、序列号/确认机制:TCP 将数据分成多个小段,每段数据都有唯一的序列号,以确保数据包的顺序传输和完整性。同时,发送方如果没有收到接收方的确认应答,会重传数据。


三分恶面渣逆袭:序列号/确认应答


③、流量控制:接收方会发送窗口大小告诉发送方它的接收能力。发送方会根据窗口大小调整发送速度,避免网络拥塞。


三分恶面渣逆袭:滑动窗口简图


④、超时重传:如果发送方发送的数据包超过了最大生存时间,接收方还没有收到,发送方会重传数据包以保证丢失数据重新传输。


三分恶面渣逆袭:超时重传


⑤、拥塞控制:TCP 会采用慢启动的策略,一开始发的少,然后逐步增加,当检测到网络拥塞时,会降低发送速率。在网络拥塞缓解后,传输速率也会自动恢复。


三分恶面渣逆袭:拥塞控制简略示意图


http的get和post区别


三分恶面渣逆袭:Get 和 Post 区别


GET 请求主要用于获取数据,参数附加在 URL 中,存在长度限制,且容易被浏览器缓存,有安全风险;而 POST 请求用于提交数据,参数放在请求体中,适合提交大量或敏感的数据。


另外,GET 请求是幂等的,多次请求不会改变服务器状态;而 POST 请求不是幂等的,可能对服务器数据有影响。


https使用过吗 怎么保证安全


HTTP 是明文传输的,存在数据窃听、数据篡改和身份伪造等问题。而 HTTPS 通过引入 SSL/TLS,解决了这些问题。


SSL/TLS 在加密过程中涉及到了两种类型的加密方法:



  • 非对称加密:服务器向客户端发送公钥,然后客户端用公钥加密自己的随机密钥,也就是会话密钥,发送给服务器,服务器用私钥解密,得到会话密钥。

  • 对称加密:双方用会话密钥加密通信内容。


三分恶面渣逆袭:HTTPS 主要流程


客户端会通过数字证书来验证服务器的身份,数字证书由 CA 签发,包含了服务器的公钥、证书的颁发机构、证书的有效期等。


https能不能抓包


可以,HTTPS 可以抓包,但因为通信内容是加密的,需要解密后才能查看。


MonkeyWie:wireshark抓HTTPS


其原理是通过一个中间人,伪造服务器证书,并取得客户端的信任,然后将客户端的请求转发给服务器,将服务器的响应转发给客户端,完成中间人攻击。


常用的抓包工具有 Wireshark、Fiddler、Charles 等。


threadlocal 原理 怎么避免垃圾回收?


ThreadLocal 的实现原理就是,每个线程维护一个 Map,key 为 ThreadLocal 对象,value 为想要实现线程隔离的对象。


1、当需要存线程隔离的对象时,通过 ThreadLocal 的 set 方法将对象存入 Map 中。


2、当需要取线程隔离的对象时,通过 ThreadLocal 的 get 方法从 Map 中取出对象。


3、Map 的大小由 ThreadLocal 对象的多少决定。


ThreadLocal 的结构


通常情况下,随着线程 Thread 的结束,其内部的 ThreadLocalMap 也会被回收,从而避免了内存泄漏。


但如果一个线程一直在运行,并且其 ThreadLocalMap 中的 Entry.value 一直指向某个强引用对象,那么这个对象就不会被回收,从而导致内存泄漏。当 Entry 非常多时,可能就会引发更严重的内存溢出问题。


ThreadLocalMap 内存溢出


使用完 ThreadLocal 后,及时调用 remove() 方法释放内存空间。remove() 方法会将当前线程的 ThreadLocalMap 中的所有 key 为 null 的 Entry 全部清除,这样就能避免内存泄漏问题。


mysql慢查询


慢 SQL 也就是执行时间较长的 SQL 语句,MySQL 中 long_query_time 默认值是 10 秒,也就是执行时间超过 10 秒的 SQL 语句会被记录到慢查询日志中。


可通过 show variables like 'long_query_time'; 查看当前的 long_query_time 值。


沉默王二:long_query_time


不过,生产环境中,10 秒太久了,超过 1 秒的都可以认为是慢 SQL 了。


mysql事务隔离级别


事务的隔离级别定了一个事务可能受其他事务影响的程度,MySQL 支持的四种隔离级别分别是:读未提交、读已提交、可重复读和串行化。


三分恶面渣逆袭:事务的四个隔离级别


遇到过mysql死锁或者数据不安全吗


有,一次典型的场景是在技术派项目中,两个事务分别更新两张表,但是更新顺序不一致,导致了死锁。


-- 创建表/插入数据
CREATE TABLE account (
id INT AUTO_INCREMENT PRIMARY KEY,
balance INT NOT NULL
);

INSERT INTO account (balance) VALUES (100), (200);

-- 事务 1
START TRANSACTION;
-- 锁住 id=1 的行
UPDATE account SET balance = balance - 10 WHERE id = 1;

-- 等待锁住 id=2 的行(事务 2 已锁住)
UPDATE account SET balance = balance + 10 WHERE id = 2;

-- 事务 2
START TRANSACTION;
-- 锁住 id=2 的行
UPDATE account SET balance = balance - 10 WHERE id = 2;

-- 等待锁住 id=1 的行(事务 1 已锁住)
UPDATE account SET balance = balance + 10 WHERE id = 1;

两个事务访问相同的资源,但是访问顺序不同,导致了死锁。


死锁


解决方法:


第一步,使用 SHOW ENGINE INNODB STATUS\G; 查看死锁信息。


查看死锁


第二步,调整事务的资源访问顺序,保持一致。


怎么解决依赖冲突的


比如在一个项目中,Spring Boot 和其他库对 Jackson 的版本有不同要求,导致序列化和反序列化功能出错。


这时候,可以先使用 mvn dependency:tree分析依赖树,找到冲突;然后在 dependencyManagement 中强制统一 Jackson 版本,或者在传递依赖中使用 exclusion 排除不需要的版本。


spring事务


在 Spring 中,事务管理可以分为两大类:声明式事务管理和编程式事务管理。


三分恶面渣逆袭:Spring事务分类


编程式事务可以使用 TransactionTemplate 和 PlatformTransactionManager 来实现,需要显式执行事务。允许我们在代码中直接控制事务的边界,通过编程方式明确指定事务的开始、提交和回滚。


声明式事务是建立在 AOP 之上的。其本质是通过 AOP 功能,对方法前后进行拦截,将事务处理的功能编织到拦截的方法中,也就是在目标方法开始之前启动一个事务,在目标方法执行完之后根据执行情况提交或者回滚事务。


相比较编程式事务,优点是不需要在业务逻辑代码中掺杂事务管理的代码,Spring 推荐通过 @Transactional 注解的方式来实现声明式事务管理,也是日常开发中最常用的。


常见的linux命令


我自己常用的 Linux 命令有 top 查看系统资源、ps 查看进程、netstat 查看网络连接、ping 测试网络连通性、find 查找文件、chmod 修改文件权限、kill 终止进程、df 查看磁盘空间、free 查看内存使用、service 启动服务、mkdir 创建目录、rm 删除文件、rmdir 删除目录、cp 复制文件、mv 移动文件、zip 压缩文件、unzip 解压文件等等这些。


git命令



  • git clone <repository-url>:克隆远程仓库。

  • git status:查看工作区和暂存区的状态。

  • git add <file>:将文件添加到暂存区。

  • git commit -m "message":提交暂存区的文件到本地仓库。

  • git log:查看提交历史。

  • git merge <branch-name>:合并指定分支到当前分支。

  • git checkout <branch-name>:切换分支。

  • git pull:拉取远程仓库的更新。


内容来源


三分恶的面渣逆袭:javabetter.cn/sidebar/san…
二哥的 Java 进阶之路(GitHub 已有 13000+star):github.com/itwanger/to…


最后,把二哥的座右铭送给大家:没有什么使我停留——除了目的,纵然岸旁有玫瑰、有绿荫、有宁静的港湾,我是不系之舟。共勉 💪。


作者:沉默王二
来源:juejin.cn/post/7451638008409554994
收起阅读 »

AI赋能剪纸艺术,剪映助力多地文旅点亮新春

近日,一场别开生面的文化盛宴在社交媒体拉开帷幕。多地文旅纷纷在官方账号发布剪纸风格的视频,以独特的视角展现当地丰富的文旅资源,将传统非遗文化与春节的喜庆氛围完美融合,这一创新形式收获网友大量点赞。在这些令人眼前一亮的视频中,各地的标志性景点和特色风土人情以剪纸...
继续阅读 »

近日,一场别开生面的文化盛宴在社交媒体拉开帷幕。多地文旅纷纷在官方账号发布剪纸风格的视频,以独特的视角展现当地丰富的文旅资源,将传统非遗文化与春节的喜庆氛围完美融合,这一创新形式收获网友大量点赞。

在这些令人眼前一亮的视频中,各地的标志性景点和特色风土人情以剪纸艺术的形式生动呈现。细腻的线条勾勒出西安大雁塔的宏伟庄严,鲜艳的色彩展现出塞上江南的瑰丽,精致的图案描绘出江南水乡的温婉秀丽。每一幅剪纸都仿佛在诉说着一个地方的故事,让大众在感受剪纸艺术魅力的同时,领略到祖国大地的壮美多姿。

图片1.png

图片来源:陕西文旅、威海文旅、内蒙古文旅的官方社交媒体账号

记者注意到,本次剪纸效果采用了剪映提供的“中式剪纸”模板功能。作为字节跳动旗下的视频创作工具产品,剪映团队发挥技术优势,将AI新技术与传统剪纸艺术深度融合,为创作者提供了便捷且强大的创作工具。通过AI算法,用户只需上传照片素材,就能快速生成效果精细的剪纸风格视频,大大降低了创作门槛,让更多人参与到创作中来。

除了风景类的剪纸视频模板,剪映在春节期间还推出了丰富多样的其他模板,如人物剪纸模板。用户可以通过这些模板,将自己或身边人的形象创作为剪纸风格的人物,为视频增添更多趣味性和个性化元素。无论是阖家团圆的场景,还是展现个人风采的画面,都能通过这些模板以独特的剪纸艺术形式呈现。

剪映相关负责人表示,新春将至,希望通过AI技术的应用让剪纸艺术突破地域和传统展示形式的限制,激发更多人对家乡的热爱,鼓励大家用这种新颖的方式秀出自己家乡的风景,共同分享美好。(作者:刘洪)

收起阅读 »

synchronized就该这么学

先赞后看,Java进阶一大半 早期sychonrized重量级锁开销大,于是JDK1.5引入了ReentrantLock,包含现在很多偏见都是认为ReentrantLock性能要优于sychonrized。但JDK1.6引入的锁升级,不断迭代,怕是性能往往还...
继续阅读 »

先赞后看,Java进阶一大半



早期sychonrized重量级锁开销大,于是JDK1.5引入了ReentrantLock,包含现在很多偏见都是认为ReentrantLock性能要优于sychonrized。但JDK1.6引入的锁升级,不断迭代,怕是性能往往还优于ReentrantLock。


在这里插入图片描述


我是南哥,相信对你通关面试、拿下Offer有所帮助。


敲黑板:本文总结了多线程相关的synchronized、volatile常见的面试题!



⭐⭐⭐收录在《Java学习/进阶/面试指南》:https://github/JavaSouth



精彩文章推荐



1. synchronized


1.1 可重入锁



面试官:知道可重入锁有哪些吗?



可重入意味着获取锁的粒度是线程而不是调用,如果大家知道这个概念,会更容易理解可重入锁的作用。


既然获取锁的粒度是线程,意味着线程自己是可以获取自己的内部锁的,而如果获取锁的粒度是调用则每次经过同步代码块都需要重新获取锁。


举个例子。线程A获取了某个对象锁,但在线程代码的流程中仍需再次获取该对象锁,此时线程A可以继续执行不需要重新再获取该对象锁。另外线程如果要使用父类的同步方法,由于可重入锁也无需再次获取锁。


在Java中,可重入锁主要有ReentrantLock、synchronized


1.2 synchronized实现原理



面试官:你先说说synchronized的实现原理?



synchronized的实现是基于monitor的。任何对象都有一个monitor与之关联,当monitor被持有后,对象就会处于锁定状态。而在同步代码块的开始位置,在编译期间会被插入monitorenter指令


当线程执行到monitorenter指令时,就会尝试获取monitor的所有权,如果获取得到则代表获得锁资源。


1.3 synchronized缺点



面试官:那synchronized有什么缺点?



在Java SE 1.6还没有对synchronized进行了各种优化前,很多人都会称synchronized为重量级锁,因为它对资源消耗是比较大的。



  1. synchronized需要频繁的获得锁、释放锁,这会带来了不少性能消耗。

  2. 另外没有获得锁的线程会被操作系统进行挂起阻塞、唤醒。而唤醒操作需要保存当前线程状态,切换到下一个线程,也就是进行上下文切换。上下文切换是很耗费资源的一种操作。


1.4 保存线程状态



面试官:为什么上下文切换要保存当前线程状态?



这就跟读英文课文时查字典一样,我们要先记住课文里的页数,查完字典好根据页数翻到英文课文原来的位置。


同理,CPU要保证可以切换到上一个线程的状态,就需要保存当前线程的状态。


1.5 锁升级



面试官:可以怎么解决synchronized资源消耗吗?



上文我有提到Java SE 1.6对synchronized进行了各种优化,具体的实现是给synchronized引入了锁升级的概念。synchronized锁一共有四种状态,级别从低到高依次是无锁、偏向锁、轻量级锁、重量级锁。


大家思考下,其实多线程环境有着各种不同的场景,同一个锁状态并不能够适应所有的业务场景。而这四种锁状态就是为了适应各种不同场景来使得线程并发的效率最高。



  1. 没有任何线程访问同步代码块,此时synchronized是无锁状态。

  2. 只有一个线程访问同步代码块的场景的话,会进入偏向锁状态。偏向锁顾名思义会偏向访问它的线程,使其加锁、解锁不需要额外的消耗。

  3. 少量线程竞争的场景的话,偏向锁会升级为轻量级锁。而轻量级采用CAS操作来获得锁,CAS操作不需要获得锁、释放锁,减少了像synchronized重量级锁带来的上下文切换资源消耗。

  4. 轻量级锁通过CAS自旋来获得锁,如果自旋10次失败,为了减少CPU的消耗则锁会膨胀为重量级锁。此时synchronized重量级锁就回归到了悲观锁的状态,其他获取不到锁的都会进入阻塞状态。


1.6 锁升级优缺点



面试官:它们都有什么优缺点呢?



由于每个锁状态都有其不同的优缺点,也意味着有其不同的适应场景。



  1. 偏向锁的优点是加锁和解锁操作不需要额外的消耗;缺点是如果线程之间存在锁竞争,偏向锁会撤销,这也带来额外的撤销消耗;所以偏向锁适用的是只有一个线程的业务场景。

  2. 轻量级锁状态下,优点是线程不会阻塞,提高了程序执行效率;但如果始终获取不到锁的线程会进行自旋,而自旋动作是需要消耗CPU的;所以轻量级锁适用的是追求响应时间、同时同步代码块执行速度快的业务场景。

  3. 重量级锁的优点是不需要自旋消耗CPU;但缺点很明显,线程会阻塞、响应时间也慢;重量级锁更适用在同步代码块执行速度较长的业务场景。


2. volatile


2.1 指令重排序



面试官:重排序知道吧?



指令重排序字面上听起来很高级,但只要理解了并不难掌握。我们先来看看指令重排序究竟有什么作用。


指令重排序的主要作用是可以优化编译器和处理器的执行效率,提高程序性能。例如多条执行顺序不同的指令,可以重排序让轻耗时的指令先执行,从而让出CPU流水线资源供其他指令使用。


但如果指令之间存在着数据依赖关系,则编译器和处理器不会对相关操作进行指令重排序,避免程序执行结果改变。这个规则也称为as-if-serial语义。例如以下代码。


String book = "JavaGetOffer"; // A
String avator = "思考的陈"; // B
String msg = book + abator; // C

对于A、B,它们之间并没有依赖关系,谁先执行对程序的结果没有任何影响。但C却依赖于A、B,不能出现类似C -> A -> B或C -> B -> A或A -> C -> B或B -> C -> A之类的指令重排,否则程序执行结果将改变。


2.2 重排序的问题



面试官:那重排序不会有什么问题吗?



在单线程环境下,有as-if-serial语义的保护,我们无需担心程序执行结果被改变。但在多线程环境下,指令重排序会出现数据不一致的问题。举个多线程的例子方便大家理解。


       int number = 0;
boolean flag = false;
public void method1() {
number = 6; // A
flag = true; // B
}
public void method2() {
if (flag) { // C
int i = number * 6; // D
}
}

假如现在有两个线程,线程1执行method1、线程2执行method2。因为method1其中的A、B之间没有数据依赖关系,可能出现B -> A的指令重排序,大家注意这个指令重排序会影响到线程2执行的结果。


当B指令执行后A指令还没有执行number = 6,此时如果线程2执行method2同时给i赋值为0 * 6。很明显程序运行结果和我们预期的并不一致。


2.3 volatile特性



面试官:有什么办法可以解决?



关于上文的重排序问题,可以使用volatile关键字来解决。volatile一共有以下特性:



  1. 可见性。volatile修饰的变量每次被修改后的值,对于任何线程都是可见的,即任何线程会读取到最后写入的变量值。

  2. 原子性。volatile变量的读写具有原子性。

  3. 禁止代码重排序。对于volatile变量操作的相关代码不允许重排序。


       int number = 0;
volatile boolean flag = false;
public void method1() {
number = 6; // A
flag = true; // B
}
public void method2() {
if (flag) { // C
int i = number * 6; // D
}
}

由于volatile具有禁止代码重排序的特性,所以不会出现上文的B -> A的指令重排序。另外volatile具有可见性,falg的修改对线程2来说是可见的,线程会立刻感知到flag = ture从而执行对i的赋值。以上问题可以通过volatile解决,和使用synchronized加锁是一样的效果。


另外大家注意一点,volatile的原子性指的是对volatile的读、写操作的原子性,但类似于volatile++这种复合操作是没有原子性的。


2.5 可见性原理



面试官:那volatile可见性的原理是什么?



内存一共分为两种,线程的本地内存和线程外的主内存。对于一个volatile修饰的变量,任何线程对该变量的修改都会同步到主内存。而当读一个volatile修饰的变量时,JMM(Java Memory Model)会把该线程对应的本地内存置为无效,从而线程读取变量时读取的是主内存。


线程每次读操作都是读取主内存中最新的数据,所以volatile能够实现可见性的特性。


2.3 volatile局限性



面试官:volatile有什么缺点吗?



企业生产上还是比较少用到volatile的,对于加锁操作会使用的更多些。



  1. synchronized加锁操作虽然开销比volatile大,但却适合复杂的业务场景。而volatile只适用于状态独立的场景,例如上文对flag变量的读写。

  2. volatile编写的代码是比较难以理解的,不清楚整个流程和原理很难维护代码。

  3. 类似于volatile++这种复合操作,volatile不能确保原子性。



⭐⭐⭐本文收录在《Java学习/进阶/面试指南》:https://github/JavaSouth



我是南哥,南就南在Get到你的点赞点赞点赞。


在这里插入图片描述



创作不易,不妨点赞、收藏、关注支持一下,各位的支持就是我创作的最大动力❤️



作者:JavaSouth南哥
来源:juejin.cn/post/7435894119103430665
收起阅读 »

如何进行千万级别数据跑批优化

最近观看公司前辈文档,看到对大数据量跑批的优化方案,参照自己的理解和之前相关经验整理了一份优化方案~ Background 定义: 跑批通常指代的是我们应用程序在固定日期针对某一批大量数据定时进行特定的处理,在金融业务中一般跑批的场景有分户日结、账务计提、账单...
继续阅读 »

最近观看公司前辈文档,看到对大数据量跑批的优化方案,参照自己的理解和之前相关经验整理了一份优化方案~


Background


定义: 跑批通常指代的是我们应用程序在固定日期针对某一批大量数据定时进行特定的处理,在金融业务中一般跑批的场景有分户日结、账务计提、账单逾期、不良资产处理等等,它具有高连贯性特点,通常我们执行完跑批后还要对跑批数据进行进一步处理,比如发 MQ 给下游消费,数仓拉取分析等。。。



跑批最怕的就是上来就干,从不考虑涉及到第三方接口时的响应时间、大事务等问题。



Problem


针对大数据量跑批会有很多的问题,比如我们要在指定日期指定时间的大数据量去处理,还要保证处理期间尽可能的高效,在出现错误时也要进行相应的补偿措施避免影响到其它业务等 ~



  1. OOM 查询跑批数据,未进行分片处理,随着业务纵向发展数据膨胀一旦上来,就容易导致 OOM 悲剧;

  2. 未对数据进行批量处理: 针对业务中间的处理未采用批量处理的思维,造成花费大量的时间,另外频繁的 IO 也是问题之一;

  3. 避免大事务: 直接用 @Transaction 去覆盖所有的业务是不可取的,问题定位困难不说,方法处理时间变久了;

  4. 下游接口的承受能力: 下游的承载能力也要在我们的考虑范围之内,比如大数量分批一直发,你是爽了,下游没有足够的能力消费就会造成灾难性的问题;

  5. 任务时间上的隔离: 通常大数据量跑批后面还有一些业务上的处理,对于时间和健壮性上要严格控制;

  6. 失败任务补偿: 分布式任务调度创建跑批任务,然后拆分子任务并发到消息队列,线程池执行任务调用远程接口,这中间的任何步骤都有可能会出问题导致任务失败;


Analyze


通过以上问题的总结,我们可以得出要完整的进行大数据量跑批任务我们的代码设计需要具备以下的几点素质:



  1. 健壮性: 跑批任务是要通过定时的去处理这些数据,不能因为其中一条数据出现异常从而导致整批数据无法继续进行操作,所以它必须是健壮的;

  2. 可靠性: 针对于异常数据我们后续可进行补偿处理,所以它必须是可靠的;

  3. 隔离性: 避免干扰任何其他应用程序的正常运行;

  4. 高性能: 通常跑批任务要处理的数据量较大,我们不能让它处理的时间过于久,这样会挤压后续的其它连贯性业务处理时间,所以我们必须考虑其性能处理;


Solution


大数据量的数据是很庞大的,如果一次性都加载到内存里面将会是灾难性的后果,因此我们要对大数据量数据进行分割处理,这是防止 OOM 必要的一环!此外,监控、异常等方法措施也要实施到位,到问题出现再补救就晚了~


1、数据库问题


使用数据库扫表问题:
遍历数据对数据库的压力是很大的,越往后速度越慢;


解决:
遍历数据库越往后查压力越大,可以设置在每次查询的时候携带上一次的极值,让你分页查找的offect永远控制在0


2、分片广播


分片: 在生产环境中,都是采用集群部署,如果一个跑批任务只跑在一个机器上,那效率肯定很低,我们可以利用 xxl-job「分片广播」 和 「动态分片」 功能;


image.png


分布式调度幂等: 分布式任务调度只能保证准时调到一个节点上,而且通常都有失败重试的功能。所以任务幂等都是要的,一般都是通过分布式锁来实现,这里遵循简单原则使用数据库就可以了,可以通过在任务表里 insert 一条唯一的任务记录,通过唯一键来防止重复调度。


除了用唯一键,还可以在记录中增加一个状态字段,使用乐观锁来更新状态。比如开始是初始化状态,更新成正在运行的状态,更新失败说明别的节点已经在跑这个任务。当然分布式锁的实现方案有很多,比如 redis、zk 等等。


集群分布式任务调度 xxl-job: 执行器集群部署时,“分片广播” 以执行器为维度进行分片,任务路由策略选择”分片广播”情况下,一次任务调度将会广播触发对应集群中所有执行器执行一次任务,同时系统自动传递分片参数;可根据分片参数开发分片任务;



  • 分片任务场景:10个执行器的集群来处理10w条数据,每台机器只需要处理1w条数据,耗时降低10倍;

  • 广播任务场景:广播执行器机器运行shell脚本、广播集群节点进行缓存更新等


// Index 是属于 Total 第几个序列(从0开始)
int shardIndex = XxlJobHelper.getShardIndex();
// Total 是总的执行器数量
int shardTotal = XxlJobHelper.getShardTotal();

3、分批获取



  1. 设置步长: 分派到一个 Pod 负责的数据也是庞大的,一下查出来耗时太久容易导致超时,通常我们会引入步长的概念,比如分派给 Pod 1w条数据,我们可以将它划分 10 次查出,一次查出 1k 数据,进而避免了数据库查询数据耗时太久 ~

  2. 空间换时间: 跑批可能会涉及到数据准备的过程,边循环跑批数据边去查找所需的数据,涉及多个 for 嵌套的循环处理时,可以采用空间换时间的思想,将数据加载到内存中进行筛选查找,但是要做好 OOM 防范措施,比如用包装类接查找出来的数据等等,毕竟内存不是无限大的!

  3. 深分页: 分批查询时 limit 的偏移量越大,执行时间越长。比如 limit a, b 会查询前 a + b 条数据,然后丢弃前 a 条数据,select * 会查询所有的列,也会有回表操作。我们可以使用 子查询 优化 SQL ,先查出 id 后分页,尽量用覆盖索引 来优化


4、事务控制



  1. 这些操作自身是无法回滚的,这就会导致数据的不一致。可能 RPC 调用成功了,但是本地事务回滚了,可是 PRC 调用无法回滚了;

  2. 在事务中有远程调用,就会拉长整个事务导致本事务的数据库连接一直被占用,从而导致数据库连接池耗尽或者单个链接超时,因此要熟悉调用链路,将事务粒度控制在最小范围内


5、充分利用服务器资源


需要充分利用服务器资源,采用多线程,MySQL的CPU在罚息期间也是低于 50%、IOPS 使用率低于 50%;
其实跑数据是 io 密集型的,不需要非得压榨服务器资源 ~


6、MQ 消费任务并行


MQ 消费消息队列的消息时要在每个节点上同时跑多个子任务才能资源利用最大化。那么就使用到线程池了,如果选择的是Kafka或者 RocketMQ,他们的客户端本来就是线程池消费的,只需要合理调整客户端参数就可以了。如果使用的是 Redis,那就需要自己创建一个线程池,然后让一个 EventLoop 线程从 Redis 队列中取任务。放入线程池中运行,因为我们已经使用 Redis 队列做缓冲,所以线程池的队列长度设为0,这里直接使用JDK提供的 SynchronousQueue。(这里以java为例)


7、动态调整并发度


跑批任务中能动态调整速度是很重要的,有 2 个地方可以进行操作:



  1. 任务中调用远程接口,这个速度控制其实用 Thread.sleep() 就好了。

  2. 控制任务并发度,就是有多少个线程同时运行任务。这个控制可以通过调整线程池的线程数来实现,但是线程池动态调整线程数比较麻烦。动态调整可以通过开源的限流组件来实现,比如 Guava 的 RateLimiter。可以在每次调用远程接口前调用限流组件来控制并发速度。


8、失败任务如何继续


一般分布式调度路径:



  1. 分布式 任务调度创建跑批任务;

  2. 拆分子任务 多线程 并发的发送到 消息队列

  3. 线程池 执行任务调用远程接口;


在这个链条中,可能导致任务失败或者中止的原因无非下面几个。



  1. 服务器 Pod 因为其它业务影响重启导致任务中止;

  2. 任务消费过程中失败,达到最大的重试次数;

  3. 业务逻辑不合理或者数据膨胀导致 OOM ;

  4. 消费时调用远程接口超时(这个很多人专注自己的业务逻辑从而忽略第三方接口的调用)


其实解决起来也简单,因为其它因素导致失败,你需要记录下任务的进度,然后在失败的点去再次重试 ~



  1. 记录进度: 我们需要知道这个任务执行到哪里了,同时也要记录更新的时间,这样才知道补偿哪里,比如进行跑批捞取时,要记录我们捞取的数据区间 ~

  2. 任务重试: 编写一个补偿式的任务(比如FixJob),定时的去扫面处在中间态的任务,如果扫到就触发补偿机制,将这个任务改成待执行状态投入消息队列;


9、下游接口时间


跑批最怕的就是上来就干,从不考虑涉及到第三方接口时的响应时间,如果不考虑第三方接口调用时间,那么在测试时候你会发现频繁的 YGC,这是很致命的问题,属于你设计之外的事件,但也是你必须要考虑的~


解决起来也简单,在业务可以容忍的情况下,我们可以将调用接口的业务逻辑设计一个中间态,然后挂起我们的这个业务,随后用定时任务去查询我们的业务结果,在收到信息后继续我们的业务逻辑,避免它一直在内存中堆积 ~


10、线程安全


在进行跑批时,一般会采用多线程的方式进行处理,因此要考虑线程安全的问题,比如使用线程安全的容器,使用JUC包下的工具类。


11、异常 & 监控



  1. 异常: 要保证程序的健壮性,做好异常处理,不能因为一处报错,导致整个任务执行失败,对于异常的数据可以跳过,不影响其他数据的正常执行;

  2. 监控: 一般大数据量跑批是业务核心中的核心,一次异常就是很大的灾难,对业务的损伤不可预估,因此要配置相应的监控措施,在发送异常前及时察觉,进而做补偿措施;


Reference


京东云定时任务优化总结(从半个小时优化到秒级)
记一次每日跑批任务耗时性能从六分钟优化到半分钟历程及总结


作者:Point
来源:juejin.cn/post/7433315676051406888
收起阅读 »

别再混淆了!一文带你搞懂@Valid和@Validated的区别

上篇文章我们简单介绍和使用了一下Springboot的参数校验,同时也用到了 @Valid 注解和 @Validated 注解,那它们之间有什么不同呢?区别先总结一下它们的区别:来源@Validated :是S...
继续阅读 »

上篇文章我们简单介绍和使用了一下Springboot的参数校验,同时也用到了 @Valid 注解和 @Validated 注解,那它们之间有什么不同呢?

区别

先总结一下它们的区别:

  1. 来源

    • @Validated :是Spring框架特有的注解,属于Spring的一部分,也是JSR 303的一个变种。它提供了一些 @Valid 所没有的额外功能,比如分组验证。
    • @Valid:Java EE提供的标准注解,它是JSR 303规范的一部分,主要用于Hibernate Validation等场景。
  2. 注解位置

    • @Validated : 用在类、方法和方法参数上,但不能用于成员属性。
    • @Valid:可以用在方法、构造函数、方法参数和成员属性上。
  3. 分组

    • @Validated :支持分组验证,可以更细致地控制验证过程。此外,由于它是Spring专有的,因此可以更好地与Spring的其他功能(如Spring的依赖注入)集成。
    • @Valid:主要支持标准的Bean验证功能,不支持分组验证。
  4. 嵌套验证

    • @Validated :不支持嵌套验证。
    • @Valid:支持嵌套验证,可以嵌套验证对象内部的属性。

这些理论性的东西没什么好说的,记住就行。我们主要看分组嵌套验证是什么,它们怎么用。

实操阶段

话不多说,通过代码来看一下分组嵌套验证

为了提示友好,修改一下全局异常处理类:

@RestControllerAdvice
public class GlobalExceptionHandler {

/**
* 参数校检异常
* @param e
* @return
*/

@ExceptionHandler(value = MethodArgumentNotValidException.class)
public ResponseResult handle(MethodArgumentNotValidException e) {
BindingResult bindingResult = e.getBindingResult();

StringJoiner joiner = new StringJoiner(";");

for (ObjectError error : bindingResult.getAllErrors()) {
String code = error.getCode();
String[] codes = error.getCodes();

String property = codes[1];
property = property.replace(code ,"").replaceFirst(".","");

String defaultMessage = error.getDefaultMessage();
joiner.add(property+defaultMessage);
}
return handleException(joiner.toString());
}

private ResponseResult handleException(String msg) {
ResponseResult result = new ResponseResult<>();
result.setMessage(msg);
result.setCode(500);
return result;
}
}

分组校验

分组验证是为了在不同的验证场景下能够对对象的属性进行灵活地验证,从而提高验证的精细度和适用性。一般我们在对同一个对象进行保存或修改时,会使用同一个类作为入参。那么在创建时,就不需要校验id,更新时则需要校验用户id,这个时候就需要用到分组校验了。

对于定义分组有两点要特别注意

  1. 定义分组必须使用接口。
  2. 要校验字段上必须加上分组,分组只对指定分组生效,不加分组不校验。

有这样一个需求,在创建用户时校验用户名,修改用户时校验用户id。下面对我们对这个需求进行一个简单的实现。

  1. 创建分组

CreationGr0up 用于创建时指定的分组:

public interface CreationGr0up {
}

UpdateGr0up 用于更新时指定的分组:

public interface UpdateGr0up {
}
  1. 创建用户类

创建一个UserBean用户类,分别校验 username 字段不能为空和id字段必须大于0,然后加上CreationGr0up和 UpdateGr0up 分组。

/**
* @author 公众号-索码理(suncodernote)
*/

@Data
public class UserBean {

@NotEmpty( groups = {CreationGr0up.class})
private String username;

@Min(value = 18)
private Integer age;

@Email(message = "邮箱格式不正确")
private String email;

@Min(value = 1 ,groups = {UpdateGr0up.class})
private Long id;
}
  1. 创建接口

ValidationController 中新建两个接口 updateUser 和 createUser

@RestController
@RequestMapping("validation")
public class ValidationController {

@GetMapping("updateUser")
public UserBean updateUser(@Validated({UpdateGr0up.class}) UserBean userBean){
return userBean;
}

@GetMapping("createUser")
public UserBean createUser(@Validated({CreationGr0up.class}) UserBean userBean){
return userBean;
}
}
  1. 测试

先对 createUser接口进行测试,我们将id的值设置为0,也就是不满足id必须大于0的条件,同样 username 不传值,即不满足 username 不能为空的条件。  通过测试结果我们可以看到,虽然id没有满足条件,但是并没有提示,只提示了username不能为空。

再对 updateUser接口进行测试,条件和测试 createUser接口的条件一样,再看测试结果,和 createUser接口测试结果完全相反,只提示了id最小不能小于1。 

至此,分组功能就演示完毕了。

嵌套校验

介绍嵌套校验之前先看一下两个概念:

  1. 嵌套校验(Nested Validation) 指的是在验证对象时,对对象内部包含的其他对象进行递归验证的过程。当一个对象中包含另一个对象作为属性,并且需要对这个被包含的对象也进行验证时,就需要进行嵌套校验。
  2. 嵌套属性指的是在一个对象中包含另一个对象作为其属性的情况。换句话说,当一个对象的属性本身又是一个对象,那么这些被包含的对象就可以称为嵌套属性

有这样一个需求,在保存用户时,用户地址必须要填写。下面来简单看下示例:

  1. 创建地址类 AddressBean

AddressBean 设置 countrycity两个属性为必填项。

@Data
public class AddressBean {

@NotBlank
private String country;

@NotBlank
private String city;
}
  1. 修改用户类,将AddressBean作为用户类的一个嵌套属性

特别提示:想要嵌套校验生效,必须在嵌套属性上加 @Valid 注解。

@Data
public class UserBean {

@NotEmpty(groups = {CreationGr0up.class})
private String username;

@Min(value = 18)
private Integer age;

private String email;

@Min(value = 1 ,groups = {UpdateGr0up.class})
private Long id;

//嵌套验证必须要加上@Valid
@Valid
@NotNull
private AddressBean address;
}
  1. 创建一个嵌套校验测试接口
@PostMapping("nestValid")
public UserBean nestValid(@Validated @RequestBody UserBean userBean){
System.out.println(userBean);
return userBean;
}
  1. 测试

我们在传参时,只传 country字段,通过响应结果可以看到提示了city 字段不能为空。 响应结果

可以看到使用了 @Valid 注解来对 Address 对象进行验证,这会触发对其中的 Address 对象的验证。通过这种方式,可以确保嵌套属性内部的对象也能够参与到整体对象的验证过程中,从而提高验证的完整性和准确性。

总结

本文介绍了@Valid注解和@Validated注解的不同,同时也进一步介绍了Springboot 参数校验的使用。不管是 JSR-303JSR-380又或是 Hibernate Validator ,它们提供的参数校验注解都是有限的,实际工作中这些注解可能是不够用的,这个时候就需要我们自定义参数校验了。下篇文章将介绍一下如何自定义一个参数校验器。


作者:索码理
来源:juejin.cn/post/7344958089429434406
收起阅读 »

Java 实现责任链模式 + 策略模式:优雅处理多级请求的方式

一、什么是责任链模式?责任链模式(Chain of Responsibility Pattern) 是一种行为设计模式,它允许将请求沿着一个处理链传递,直到链中的某个对象处理它。这样,发送者无需知道哪个对象将处理请求,所有的处理对象都可以尝试处理请求...
继续阅读 »

一、什么是责任链模式?

责任链模式(Chain of Responsibility Pattern) 是一种行为设计模式,它允许将请求沿着一个处理链传递,直到链中的某个对象处理它。这样,发送者无需知道哪个对象将处理请求,所有的处理对象都可以尝试处理请求或将请求传递给链上的下一个对象。

image.png

核心思想:将请求的发送者与接收者解耦,通过让多个对象组成一条链,使得请求沿着链传递,直到被处理。


二、责任链模式的特点

  1. 解耦请求发出者和处理者:请求的发送者不需要知道具体的处理者是谁,增强了系统的灵活性和扩展性。
  2. 动态组合处理逻辑:可以根据需要动态改变链的结构,添加或移除处理者。
  3. 职责单一:责任链模式可以将每个验证逻辑封装到一个独立的处理器中,每个处理器负责单一的验证职责,符合单一职责原则。
  4. 可扩展性: 增加新的验证逻辑时,处理者只需继承一个统一的接口,并添加新的处理器,而不需要修改现有的代码。
  5. 清晰的流程: 将所有验证逻辑组织在一起,使得代码结构更加清晰,易于理解。

三、责任链模式和策略模式结合的意义

  • 责任链模式的作用
    • 用于动态处理请求,将多个处理逻辑串联起来。
  • 策略模式的作用
    • 用于封装一组算法,使得可以在运行时动态选择需要的算法。

结合两者:

  • 责任链模式负责串联和传递请求,而策略模式定义了每一个处理者的具体处理逻辑
  • 两者结合可以实现既动态构建责任链,又灵活应用不同策略来处理请求的需求。

四、责任链模式解决的问题

  1. 耦合过高:将请求的处理者从请求的发送者中解耦,使得处理者可以独立扩展或变更。
  2. 复杂的多条件判断:避免在代码中使用过多 if-else  switch-case 语句。
  3. 灵活性不足:通过链的动态组合可以轻松调整请求的传递逻辑或插入新的处理者。
  4. 代码重复:每个处理者只专注于处理它关心的部分,减少重复代码。

五、代码中的责任链模式解析

场景 1:商品上架逻辑(多重校验)

实现一个类似的场景——商品上架逻辑(如校验商品信息、库存信息等),可以按照以下步骤实现:

  1. 定义责任链抽象接口
public interface MerchantAdminAbstractChainHandler extends Ordered {

/**
* 执行责任链逻辑
*
* @param requestParam 责任链执行入参
*/

void handler(T requestParam);

/**
* @return 责任链组件标识
*/

String mark();
}
  1. 定义商品上架的责任链标识:
public enum ChainBizMarkEnum {
MERCHANT_ADMIN_CREATE_PRODUCT_TEMPLATE_KEY,
MERCHANT_ADMIN_PRODUCT_UPSHELF_KEY; // 新增商品上架责任链标识
}

  1. 定义每个处理器的通用行为:
@Component
public final class MerchantAdminChainContext implements ApplicationContextAware, CommandLineRunner {
/**
* 应用上下文,通过Spring IOC获取Bean实例
*/

private ApplicationContext applicationContext;

/**
* 保存商品上架责任链实现类
*


* Key:{@link MerchantAdminAbstractChainHandler#mark()}
* Val:{@link MerchantAdminAbstractChainHandler} 一组责任链实现 Spring Bean 集合
*


* 比如有一个商品上架模板创建责任链,实例如下:
* Key:MERCHANT_ADMIN_CREATE_PRODUCT_TEMPLATE_KEY
* Val:
* - 验证商品信息基本参数是否必填 —— 执行器 {
@link ProductInfoNotNullChainFilter}
* - 验证商品库存 —— 执行器 {
@link ProductInventoryCheckChainFilter}
*/

private final Map> abstractChainHandlerContainer = new HashMap<>();

/**
* 责任链组件执行
* @param mark 责任链组件标识
* @param requestObj 请求参数
*/

public void handler(String mark,T requestObj){
// 根据 mark 标识从责任链容器中获取一组责任链实现 Bean 集合
List abstractChainHandlers = abstractChainHandlerContainer.get(mark);
if (CollectionUtils.isEmpty(abstractChainHandlers)) {
throw new RuntimeException(String.format("[%s] Chain of Responsibility ID is undefined.", mark));
}
abstractChainHandlers.forEach(each -> each.handler(requestObj));
}

/**
* 执行方法,接收可变参数
* 本方法主要用于初始化和处理商品上架抽象责任链容器
* 它从Spring容器中获取所有MerchantAdminAbstractChainHandler类型的Bean,
* 并根据它们的mark进行分类和排序,以便后续处理
*
* @param args 可变参数,可能包含方法运行所需的额外信息
* @throws Exception 如果方法执行过程中遇到错误,抛出异常
*/

@Override
public void run(String... args) throws Exception {
// 从 Spring IOC 容器中获取指定接口 Spring Bean 集合
Map chainFilterMap = applicationContext.getBeansOfType(MerchantAdminAbstractChainHandler.class);
// 遍历所有获取到的Bean,并将它们根据mark分类存入抽象责任链容器中
chainFilterMap.forEach((beanName, bean) -> {
// 判断 Mark 是否已经存在抽象责任链容器中,如果已经存在直接向集合新增;如果不存在,创建 Mark 和对应的集合
List abstractChainHandlers = abstractChainHandlerContainer.getOrDefault(bean.mark(), new ArrayList<>());
abstractChainHandlers.add(bean);
abstractChainHandlerContainer.put(bean.mark(), abstractChainHandlers);
});
// 遍历抽象责任链容器,对每个 Mark 对应的责任链实现类集合进行排序
abstractChainHandlerContainer.forEach((mark, chainHandlers) -> {
// 对每个 Mark 对应的责任链实现类集合进行排序,优先级小的在前
chainHandlers.sort(Comparator.comparing(Ordered::getOrder));
});
}

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}

  1. 定义商品上架的责任链处理器:
@Component
public class ProductInfoNotNullChainFilter implements MerchantAdminAbstractChainHandler {
@Override
public void handler(ProductUpShelfReqDTO requestParam) {
if (StringUtils.isEmpty(requestParam.getProductName())) {
throw new RuntimeException("商品名称不能为空!");
}
if (requestParam.getPrice() == null || requestParam.getPrice() <= 0) {
throw new RuntimeException("商品价格必须大于0!");
}
System.out.println("商品信息非空校验通过");
}

@Override
public int getOrder() {
return 1;
}

@Override
public String mark() {
return ChainBizMarkEnum.MERCHANT_ADMIN_PRODUCT_UPSHELF_KEY.name();
}
}

@Component
public class ProductInventoryCheckChainFilter implements MerchantAdminAbstractChainHandler {
@Override
public void handler(ProductUpShelfReqDTO requestParam) {
if (requestParam.getStock() <= 0) {
throw new RuntimeException("商品库存不足,无法上架!");
}
System.out.println("商品库存校验通过");
}

@Override
public int getOrder() {
return 2;
}

@Override
public String mark() {
return ChainBizMarkEnum.MERCHANT_ADMIN_PRODUCT_UPSHELF_KEY.name();
}
}

  1. 调用责任链进行处理:
@Service
@RequiredArgsConstructor
public class ProductServiceImpl {
private final MerchantAdminChainContext merchantAdminChainContext;

public void upShelfProduct(ProductUpShelfReqDTO requestParam) {
// 调用责任链进行校验
merchantAdminChainContext.handler(
ChainBizMarkEnum.MERCHANT_ADMIN_PRODUCT_UPSHELF_KEY.name(),
requestParam
);
System.out.println("商品上架逻辑开始执行...");
// 后续的商品上架业务逻辑
}
}

上述代码实现了一个基于 责任链模式 的电商系统,主要用于处理复杂的业务逻辑,如商品上架模板的创建。这种模式的设计使得每个业务逻辑通过一个独立的处理器(Handler)进行处理,并将这些处理器串联成一个链,通过统一的入口执行每一步处理操作。


1. 代码的组成部分与职责解析

(1) 责任链抽象接口:MerchantAdminAbstractChainHandler

  • 定义了责任链中的基础行为:
  • void handler(T requestParam)

    • 定义了该处理器的具体逻辑。
    • 这是责任链的核心方法,每个处理器都会接收到传入的参数 requestParam,并根据具体的业务逻辑进行相应的处理。
  • 设计思想:

    • T 是一个泛型参数,可以适配不同类型的业务场景(如对象校验、数据处理等)。
    • 如果某个处理器不满足条件,可以抛出异常或者提供返回值来中断后续处理器的运行。
    • 每个处理器只负责完成自己的一部分逻辑,保持模块化设计。

(2) 抽象处理器接口:MerchantAdminAbstractChainHandler

  • 定义了责任链中每个节点的通用行为:
  • void handler(T requestParam)

    • 责任链的核心方法,定义了如何处理传入的请求参数 requestParam
    • 每个实现类都会根据具体的业务需求,在该方法中实现自己的处理逻辑,比如参数校验、数据转换等。
    • 如果某个处理环节中发生错误,可以通过抛出异常中断责任链的执行。
    • handler(T requestParam):执行具体的处理逻辑。
    • mark():返回处理器所属的责任链标识(Mark)。
  • String mark()

    • 返回当前处理器所属的责任链标识(Mark)。
    • 不同的责任链可以通过 mark() 值进行分组管理。
    • 比如在商品上架创建责任链中,mark() 可以返回 MERCHANT_ADMIN_CREATE_PRODUCT_TEMPLATE_KEY
  • int getOrder()

    • 用于定义处理器的执行顺序。
    • 通过实现 Ordered 接口的 getOrder() 方法,开发者可以灵活地控制每个处理器在责任链中的执行顺序。
    • 默认值为 Ordered.LOWEST_PRECEDENCE(优先级最低),可以根据需求覆盖此方法返回更高的优先级(数值越小优先级越高)。
  • 通过继承Ordered 接口来用于指定处理器的执行顺序,优先级小的会先执行。(模版如下)
import org.springframework.core.Ordered;

/**
* 商家上架责任链处理器抽象接口
*
* @param 处理参数的泛型类型(比如请求参数)
*/

public interface MerchantAdminAbstractChainHandler extends Ordered {

/**
* 执行责任链的具体逻辑
*
* @param requestParam 责任链执行的入参
*/

void handler(T requestParam);

/**
* 获取责任链处理器的标识(mark)
*
* 每个处理器所属的责任链标识需要唯一,用于区分不同的责任链。
*
* @return 责任链组件标识
*/

String mark();

/**
* 获取责任链执行顺序
*
* Spring 的 {@link Ordered} 接口方法,数值越小优先级越高。
* 默认返回 `Ordered.LOWEST_PRECEDENCE`,表示优先级最低。
*
* @return 处理器的执行顺序。
*/

@Override
default int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
}

(2) 责任链上下文:MerchantAdminChainContext

  • 负责管理责任链的初始化和执行:

    • 在 Spring 容器启动时 (CommandLineRunner),扫描实现了 MerchantAdminAbstractChainHandler 接口的所有 Spring Bean,并根据它们的 mark() 属性将它们归类到不同的链条中。
    • 在链条内部,根据 Ordered 的优先级对处理器进行排序。
    • 提供统一的 handler() 方法,根据标识 (Mark) 执行对应的责任链。

(3) 业务服务层:ProductInventoryCheckChainFilter

  • 通过 MerchantAdminChainContext 调用对应的责任链,完成业务参数校验逻辑。
  • 责任链完成校验后,后续可以继续执行其他具体的业务逻辑。

责任链的执行流程

通过 MerchantAdminChainContext,上述两个处理器会被自动扫描并加载到责任链中。运行时,根据 mark() 和 getOrder() 的值,系统自动按顺序执行它们。

123.png

五、Java 实现责任链模式 + 策略模式

以下是实现一个责任链 + 策略模式的完整 Java 示例。

场景:模拟用户请求的审核流程(如普通用户审批、管理员审批、高级管理员审批),并结合不同策略处理请求。

1. 定义处理请求的接口

// 抽象处理者接口
public interface RequestHandler {
// 设置下一个处理者
void setNextHandler(RequestHandler nextHandler);

// 处理请求的方法
void handleRequest(UserRequest request);
}

2. 定义用户请求类

// 请求类
public class UserRequest {
private String userType; // 用户类型(普通用户、管理员等)
private String requestContent; // 请求内容

public UserRequest(String userType, String requestContent) {
this.userType = userType;
this.requestContent = requestContent;
}

public String getUserType() {
return userType;
}

public String getRequestContent() {
return requestContent;
}
}

3. 定义不同的策略(处理逻辑)

// 策略接口
public interface RequestStrategy {
void process(UserRequest request);
}

// 普通用户处理策略
public class BasicUserStrategy implements RequestStrategy {
@Override
public void process(UserRequest request) {
System.out.println("普通用户的请求正在处理:" + request.getRequestContent());
}
}

// 管理员处理策略
public class AdminUserStrategy implements RequestStrategy {
@Override
public void process(UserRequest request) {
System.out.println("管理员的请求正在处理:" + request.getRequestContent());
}
}

// 高级管理员处理策略
public class SuperAdminStrategy implements RequestStrategy {
@Override
public void process(UserRequest request) {
System.out.println("高级管理员的请求正在处理:" + request.getRequestContent());
}
}

4. 实现责任链模式的处理者

// 具体处理者,结合策略
public class RequestHandlerImpl implements RequestHandler {
private RequestStrategy strategy; // 策略
private RequestHandler nextHandler; // 下一个处理者

public RequestHandlerImpl(RequestStrategy strategy) {
this.strategy = strategy;
}

@Override
public void setNextHandler(RequestHandler nextHandler) {
this.nextHandler = nextHandler;
}

@Override
public void handleRequest(UserRequest request) {
// 策略处理
strategy.process(request);
// 将请求传递给下一个处理者
if (nextHandler != null) {
nextHandler.handleRequest(request);
}
}
}

5. 测试责任链 + 策略模式

public class ChainStrategyExample {
public static void main(String[] args) {
// 创建策略
RequestStrategy basicStrategy = new BasicUserStrategy();
RequestStrategy adminStrategy = new AdminUserStrategy();
RequestStrategy superAdminStrategy = new SuperAdminStrategy();

// 创建责任链处理者,并设置链条
RequestHandler basicHandler = new RequestHandlerImpl(basicStrategy);
RequestHandler adminHandler = new RequestHandlerImpl(adminStrategy);
RequestHandler superAdminHandler = new RequestHandlerImpl(superAdminStrategy);

basicHandler.setNextHandler(adminHandler);
adminHandler.setNextHandler(superAdminHandler);

// 模拟用户请求
UserRequest basicRequest = new UserRequest("普通用户", "请求访问资源 A");
UserRequest adminRequest = new UserRequest("管理员", "请求修改资源 B");
UserRequest superAdminRequest = new UserRequest("高级管理员", "请求删除资源 C");

// 处理请求
System.out.println("处理普通用户请求:");
basicHandler.handleRequest(basicRequest);

System.out.println("\n处理管理员请求:");
adminHandler.handleRequest(adminRequest);

System.out.println("\n处理高级管理员请求:");
superAdminHandler.handleRequest(superAdminRequest);
}
}

六、为何责任链模式和策略模式结合使用?

  1. 责任链控制流程,策略定义处理逻辑

    • 责任链模式将处理请求的逻辑连接成链,便于动态调整请求传递的流程。
    • 策略模式将处理逻辑封装为独立的策略,可以灵活复用和替换。
  2. 职责分离

    • 责任链模式负责管理请求的传递,策略模式专注于实现具体的业务逻辑。
    • 结合使用可以让代码结构更清晰,职责分配更明确。
  3. 增强灵活性和可扩展性

    • 责任链可以动态增删处理者,策略可以动态选择或扩展新的处理逻辑,两者结合大大增强了系统的适配性和扩展性。

通过责任链模式与策略模式的结合,可以应对复杂的处理流程和多变的业务需求,同时保持代码的简洁与高内聚的设计结构。


作者:后端出路在何方
来源:juejin.cn/post/7457366224823124003

收起阅读 »

权限模型-ABAC模型

权限模型-ABAC模型📝 ABAC 的概念ABAC 的概念ABAC(Attribute-Based Access Control)基于属性的访问控制的权限模型,是一种细粒度的权限控制模型,通过对请求中的各种属性进行分析和匹配,实现对权限的灵活的、动态的控制。A...
继续阅读 »

权限模型-ABAC模型

📝 ABAC 的概念

ABAC 的概念

ABAC(Attribute-Based Access Control)基于属性的访问控制的权限模型,是一种细粒度的权限控制模型,通过对请求中的各种属性进行分析和匹配,实现对权限的灵活的、动态的控制

ABAC 的组成部分

  1. 主体(Subject): 发起访问资源请求的用户或实体(如应用程序、系统)。主体具有多种属性,例如角色、身份、部门、敏感级别、创建时间。

💡Tips: 实际中可能就是存储用户信息的记录表、发起请求的设备信息等。

  1. 对象(Object): 被访问的资源或数据。对象可以是文件、数据库表、API 接口等,同样具备多种属性,如文件名、文件类型、敏感级别、创建时间等。
  2. 操作(Action): 用户试图对资源的操作 ,例如”读”、“写”、“创建“、”删除“、”复制“等。
  3. 环境(Environment): 外部环境属性,如访问时间、地点、网络状态、安全级别等,用于动态调整访问策略。
  4. 策略(Policy): 定义允许或拒绝的访问的规则。策略基于主体、对象、操作和环境属性的组合,通过逻辑规则决定是否允许访问。

💡Tips: 策略如何定义?
一般来说 策略都有自己的语法设计,以 XML、JSON 这种形式去描述一个访问策略如何构成。 策略也是访问规则,本文中不再做区分。

ABAC 工作的基本原理

ABAC 的基本原理是:系统根据主体、对象、操作、环境的属性,以及预定义的策略,动态生成访问决策。

简单流程

LP3BIiD058RtynH3Lxemu6NbGleE9RjkGksYeB6qST-M1g8sjHgD5skyO8qWIGYYJKXh7wOv9vDLNy6HGQIxuUV_lsyunQQcDBJ3_JsYLBI31fMRrGPLcbGcTPxNAhMwecgqmFnPVkLZtmNZA_j8ikHXtkeKVeibGcIwjaDBT9kkIt1wnZx7eis2COQTihe2FHq6xscKf5Dhtcf34BFmYJ_GCjFfS9MK_lOR4lJYN3V5Cesy.png

  1. 发起请求 : 通常一个访问请求由主体、对象(资源)、环境、操作中的一个或者多个组成。每个组成部分又有各自的属性,需要用到各个组成部分的属性,去动态构建一个访问规则。

    例如 中午 12 点后禁止 A 部门的人访问 B 系统这个访问规则。

    中午 12 点以后:环境(时间属性)

    A 部门的人:主体(身份属性)

    B 系统:对象(被访问的资源)

    访问: 操作

  2. 匹配属性:在预设的访问规则库中查找与请求匹配的规则或规则集合。
  3. 规则评估:根据匹配的访问规则中的具体规则来评估请求。将请求中的属性值与规则中的条件进行对比,判断请求是否满足规则中的条件。例如请求中包含了访问的时间在 12 点以后,那么访问控制系统就会对比访问时间这个属性值。
  4. 返回结果:向用户返回最终的规则执行的结果。

💡Tips: 图中只是演示了一个大致的工作流程,实际要设计一个 ABAC 权限系统要复杂的多。

因为所有的规则条件是动态的、逻辑也是动态执行的。

ABAC的难点

试想以下场景:

  • 当前文档是文档的拥有者且是拥有者才能编辑。
  • 售卖的产品只能是上海地区的用户才能可见。
  • 中午 12 点后禁止 A 部门的人访问 B 系统。

如果使用 RBAC 模型很难实现以上的需求。RBAC 是静态的权限模型,没有对象的属性动态参与计算的,所以很难实现以上场景。

ABAC 系统非常灵活但实现比较困难,

1.属性收集和管理复杂度

  • 属性管理

    访问规则依赖属性属性值去构建,特别是动态属性(实时位置,时间),如何确保获取到最新的属性值。

💡Tips: 属性是否可以动态增加也是构建系统的一部分,例如用户属性中你增加了一项职业,那该属性的值类型和获取该值的方法如何定义也是属性管理的一个难点。

  • 数据一致性和同步

    分布式系统中,各种属性的数据源可能分散在不同的系统中,如何准确的、高效的获取该属性和属性值。

2.访问规则的复杂度

  • 条件逻辑

    构建一个访问规则通常包含复杂的条件,条件可能是大于、小于、区间、地理位置等。这些条件需要仔细的定义和维护。

  • 多条件组合

    访问规则需要涵盖不同属性的组合条件,属性组合的数量随着属性的增加呈指数型增长。

  • 策略管理

    如果访问规则在数量一直增长,访问规则的生命周期(更新、删除)将变得复杂。如果其中属性的变动也会影响现有存在的访问规则。

  • 动态性

    ABAC进行决策时需要实时评估所有相关属性的当前值,并与策略条件进行匹配。这种评估会增加计算的开销,尤其在处理大量请求时对计算资源要求更高。

3.透明性

  • 可追溯性

    ABAC的动态决策过程复杂,审计和跟踪某个过程中的条件匹配和组合算法变得困难。为了便于审计和问题排查,ABAC 系统通常需要记录详细的决策日志,这增加了额外的复杂性。

  • 决策透明度

    复杂的条件和组合逻辑使得管理员在排查和解释某个请求的决策较为困难,用户请求被拒绝可能难以理解其原因,这对系统性提出了挑战。

ABAC 的实现

标准实现-XACML

XACML 是一种基于 XML 的标准访问控制控制策略语言,用于定义和管理复杂的访问控制需求。

💡Tips: XACML 是 ABAC 的一个标准实现,用 XML 文件定义访问的策略集,然后提交 XACML 引擎来进行权限决策。由于这个太过复杂,这里不讲述了。有兴趣的可以看下官网XACML version 3.0。(当然还有其他标准的实现)

ABAC 的权限系统设计的核心

目前 ABAC 系统没有单独使用的,基本都是搭配RBAC (基于角色的权限模型)来使用。目前已有的类似方案如 AWS的 IAM (Identity And Access Management)也都是借鉴了 ABAC 的设计理念来实现的精细权限控制。

一个 ABAC 系统通常需要考虑以下核心的三个关键步骤:

  1. 属性管理:属性的定义、结构和属性值的获取。
  2. 访问规则:访问规则的结构化和语法定义。
  3. 规则编辑器:规则编辑器和规则匹配引擎。

💡Tips: 目前这里探讨的设计因为没有具体的场景,这里的所说的设计权做参考。顺便一提属性管理、规则编辑器、访问规则其实设计的思路和CDP系统非常相似。

属性管理

属性有动态属性和静态属性,如性别那就是静态的,年龄、角色、地理位置这些都是动态的。

属性管理的难点是属性的定义和属性值获取。

  • 属性的定义

    一般来说属性的定义包括属性名、字段类型、来源(获取该属性值的方式)。

    业务确认属性的范围,也就是说设计时确认了目前业务要用到哪些属性就不能进行更改(修改、删除)操作。如果是需要动态的进行属性的新增、修改就需要更抽象的灵活设计。

💡Tips: 如果其他的访问规则中使用了该属性,修改和删除都会影响使用该属性的访问规则。

  • 属性值的获取

    属性和属性值的来源单一 例如用户的属性就一张用户表,那直接读取用户表就好了。如果你的用户数据是分散在不同来源的,需要考虑的如何聚合数据和保证数据一致性的问题。

访问规则

目前现在是有一些ABAC 的设计系统大多都是采用 JSON 语言描述访问规则。该 JSON 中包含了访问规则中所用到的属性、条件、操作等。示例如下:

{
"subject": {
"role": "manager",
"department": "finance"
},
"object": {
"type": "document",
"sensitivity": "confidential"
},
"action": "view",
"environment": {
"ip": "192.168.1.*",
"time": {
"start": "09:00",
"end": "18:00"
}
},
"effect": "allow"
}

💡 实际开发中根据业务场景,系统中描述JSON 中的结构语义都有自己的规范。

规则编辑和规则匹配

ABAC 的核心部分是**如何构建一个规则编辑器和规则匹配引擎**,这个规则编辑器需要满足各种复杂条件的组合(这里的条件指的是一个条件或多个条件)。这里的条件之间的关系可能不止“且”的关系,可能还存在“或”。

一些地方描述构建规则为动态 SQL 的构建,但是这种方式需要对应的资源需要映射为数据库记录且有对应的属性存在表结构中,简单就是理解是宽表+属性。可有些属性是没办法在数据库结构中体现的如访问位置、访问时间等,这些就需要在规则匹配引擎中做设计了。

💡Tips: 目前所说的规则编辑和规则匹配都是为了动态 SQL 的构建,这块比较有通用性(有些数据权限设计采用的就是该种思路。)。至于其他方式需要考虑具体的业务环境。

规则编辑器通常都设计成管理界面,通过界面上选取中的属性和条件构成一个JSON ,然后提交到规则匹配引擎去执行,将JSON 转换成动态 SQL,发起访问请求时去拿到访问规则构建的SQL去执行。

💡Tips 这里的规则匹配引擎最主要的工作就是根据 JSON 中的描述的规则,动态生成一段 SQL。

总结

事实上 ABAC 现在没有什么标准建模,借鉴ABAC 的设计思维达到你想要的基于属性控制权限就可以了。至于采用何种方式、是否搭配其他权限模型,具体业务、具体分析。


作者:newrain_zh
来源:juejin.cn/post/7445219433017376780

收起阅读 »

用java做一套离线且免费的智能语音系统,ASR+LLM+TTS

其实调用第三方接口完成一个智能语音系统是非常简单的,像阿里、科大讯飞、微软都有相关接口,直接根据官方文档集成就可以,但想要离线的就要麻烦一点了,主要是想不花钱,现在人工智能基本是python的天下,不得不感慨,再不学python感觉自己要被淘汰了。 言归正传,...
继续阅读 »

其实调用第三方接口完成一个智能语音系统是非常简单的,像阿里、科大讯飞、微软都有相关接口,直接根据官方文档集成就可以,但想要离线的就要麻烦一点了,主要是想不花钱,现在人工智能基本是python的天下,不得不感慨,再不学python感觉自己要被淘汰了。


言归正传,首先说一下标题中的ASR+LLM+TTS,ASR就是语音识别,LLM就是大语言模型,TTS就是文字转语音,要想把这几个功能做好对电脑性能要求还是蛮高的,本次方案是一个新的尝试也是减少对性能的消耗


1.先看效果


image.png
生成的音频效果放百度网盘 通过网盘分享的文件:result.wav
链接: pan.baidu.com/s/19ImtqunH… 提取码: hm67
听完之后是不是感觉效果很好,但是。。。后面再说吧


2.如何做


2.1ASR功能


添加依赖



<!-- 获取音频信息 -->
<dependency>
<groupId>org</groupId>
<artifactId>jaudiotagger</artifactId>
<version>2.0.3</version>
</dependency>

<!-- 语音识别 -->
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna</artifactId>
<version>5.7.0</version>
</dependency>
<dependency>
<groupId>com.alphacephei</groupId>
<artifactId>vosk</artifactId>
<version>0.3.32</version>
</dependency>



代码实现,需要提前下载模型,去vosk官网下载:VOSK Models (alphacephei.com)中文模型一个大的一个小的,小的识别速度快准确率低,大的识别速度慢准确率高


image.png

提前预加载模型,提升识别速度


private static final Model model = loadModel();

private static Model loadModel() {
try {
String path=System.getProperty("user.dir");
return new Model(path+"\vosk-model-small-cn-0.22");
} catch (Exception e) {
throw new RuntimeException("Failed to load model", e);
}
}

语音转文字方法实现


public  String voiceToText(String filePath) {
File file = new File(filePath);

LibVosk.setLogLevel(LogLevel.DEBUG);
String msg = null;
int sampleRate = 0;
RandomAccessFile rdf = null;
/**
* "r": 以只读方式打开。调用结果对象的任何 write 方法都将导致抛出 IOException。
* "rw": 打开以便读取和写入。
* "rws": 打开以便读取和写入。相对于 "rw","rws" 还要求对“文件的内容”或“元数据”的每个更新都同步写入到基础存储设备。
* "rwd" : 打开以便读取和写入,相对于 "rw","rwd" 还要求对“文件的内容”的每个更新都同步写入到基础存储设备。
*/
try {
rdf = new RandomAccessFile(file, "r");
sampleRate=toInt(read(rdf));
System.out.println(file.getName() + " SampleRate:" + sampleRate); // 采样率、音频采样级别 8000 = 8KHz
rdf.close();
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
try (
InputStream ais = AudioSystem.getAudioInputStream(file);
Recognizer recognizer = new Recognizer(model, 16000)) {
int bytes;
byte[] b = new byte[1024];
while ((bytes = ais.read(b)) >= 0) {
recognizer.acceptWaveForm(b, bytes);
}
String result=recognizer.getResult();
JSONObject jsonObject = JSONObject.parseObject(result);
msg=jsonObject.getString("text");
} catch (UnsupportedAudioFileException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
return msg;
}

2.2LLM问答功能


这个就要请出神奇的羊驼ollama(链接:Ollama),下载即用非常简单,可以运行大部分主流大语言模型,在官网models选择要加载的模型在控制台运行对应的命令即可


image.png
添加依赖


<dependency>
<groupId>io.springboot.ai</groupId>
<artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
<version>1.0.3</version>
</dependency>

加入配置


spring:
ai:
ollama:
base-url: http://10.3.0.178:11434 //接口地址 默认端口11434
chat:
options:
model: qwen2 //模型名称
enabled: true

代码实现,引入OllamaChatClient,然后调用call方法


@Resource
private OllamaChatClient ollamaChatClient;
//msg为提问的信息
String ask=ollamaChatClient.call(msg);

2.3TTS功能


添加依赖


<dependency>
<groupId>com.hynnet</groupId>
<artifactId>jacob</artifactId>
<version>1.18</version>
</dependency>

代码实现


public boolean localTextToSpeech(String text, int volume, int speed,String outPath) {
try {
// 调用dll朗读方法
ActiveXComponent ax = new ActiveXComponent("Sapi.SpVoice");
// 音量 0 - 100
ax.setProperty("Volume", new Variant(volume));
// 语音朗读速度 -10 到 +10
ax.setProperty("Rate", new Variant(speed));
// 输入的语言内容
Dispatch dispatch = ax.getObject();
// 本地执行朗读
// Dispatch.call(dispatch, "Speak", new Variant(text));

//开始生成语音文件,构建文件流
ax = new ActiveXComponent("Sapi.SpFileStream");
Dispatch sfFileStream = ax.getObject();
//设置文件生成格式
ax = new ActiveXComponent("Sapi.SpAudioFormat");
Dispatch fileFormat = ax.getObject();
// 设置音频流格式
Dispatch.put(fileFormat, "Type", new Variant(22));
// 设置文件输出流格式
Dispatch.putRef(sfFileStream, "Format", fileFormat);
// 调用输出文件流打开方法,创建一个音频文件
Dispatch.call(sfFileStream, "Open", new Variant(outPath), new Variant(3), new Variant(true));
// 设置声音对应输出流为输出文件对象
Dispatch.putRef(dispatch, "AudioOutputStream", sfFileStream);
// 设置音量
Dispatch.put(dispatch, "Volume", new Variant(volume));
// 设置速度
Dispatch.put(dispatch, "Rate", new Variant(speed));
// 执行朗读
Dispatch.call(dispatch, "Speak", new Variant(text));
// 关闭输出文件
Dispatch.call(sfFileStream, "Close");
Dispatch.putRef(dispatch, "AudioOutputStream", null);

// 关闭资源
sfFileStream.safeRelease();
fileFormat.safeRelease();
// 关闭朗读的操作
dispatch.safeRelease();
ax.safeRelease();
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}

到此为止功能就全部实现了,在不联网的情况下也可以免费使用,但这个TTS生成出来的语音太机器了,所以在上面的示例中我说但是,因为机器音实在太难受了,所以用了另一个方案,但这个方案需要联网,暂时看是不需要收费(后续使用发现是有接口调用次数限制)
,这个大家就看原作者介绍吧(ikfly/java-tts: java-tts 文本转语音 (github.com)


最终实现效果来看还是能接受的吧,整个过程速度还比较快,python的语音相关项目也看了很多,如果部署一个python的TTS服务,效果可能还会好点,但是我试过的速度都有点慢啊,还是容我再继续研究一下吧


作者:北冥有鱼518
来源:juejin.cn/post/7409329136555048999
收起阅读 »

从MySQL迁移到PostgreSQL经验总结

背景最近一两周在做从MySQL迁移到PostgreSQL的任务(新项目,历史包袱较小,所以迁移比较顺利), 感觉还是有一些知识,可以拿出来分享,希望对大家有所帮助。为什么要转到PostgreSQL因架构团队安全组安全需求,需要将Mysql迁移到PostgreS...
继续阅读 »

背景

最近一两周在做从MySQL迁移到PostgreSQL的任务(新项目,历史包袱较小,所以迁移比较顺利), 感觉还是有一些知识,可以拿出来分享,希望对大家有所帮助。

为什么要转到PostgreSQL

因架构团队安全组安全需求,需要将Mysql迁移到PostgreSQL。实际迁移下来,发现PostgreSQL挺优秀的,比MySQL严谨很多,很不错。

迁移经验

引入PostgreSQL驱动,调整链接字符串

pagehelper方言调整

涉及order, group,name, status, type 等关键字,要用引号括起来

JSON字段及JsonTypeHandler

项目中用到了比较多的JSON字段。在mysql中,也有JSON字段类型,但是有时候我们用了varchar或text,在mybatis typehandler中是当成字符来处理的。但是在postgresql中,相对严谨,如果字段类型是json,那么在java中会被封装为PGObject,所以我们原来的JsonTypeHandler就要被改造。

/**
* JSON类型处理器
*
* @author james.h.fu
* @create 2024/10/9 20:45
*/

@Slf4j
public class JsonTypeHandler extends BaseTypeHandler {
private static final ObjectMapper mapper = new ObjectMapper();

static {
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, Boolean.FALSE);
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
}

private final Class clazz;
private TypeReferenceextends T> typeReference;

public JsonTypeHandler(Class clazz) {
if (clazz == null) throw new IllegalArgumentException("Type argument cannot be null");
this.clazz = clazz;
}

@Override
public void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
setObject(ps, i, parameter);
}

@Override
public T getNullableResult(ResultSet rs, String columnName) throws SQLException {
return toObject(rs, columnName);
}

@Override
public T getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return toObject(rs, columnIndex);
}

@Override
public T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return toObject(cs, columnIndex);
}

protected TypeReferenceextends T> getTypeReference() {
return new TypeReference() {};
}

private String toJson(T object) {
try {
return mapper.writeValueAsString(object);
} catch (Exception ex) {
log.error("JsonTypeHandler error on toJson content:{}", JsonUtil.toJson(object), ex);
throw new RuntimeException("JsonTypeHandler error on toJson", ex);
}
}

private T toObject(String content) {
if (!StringUtils.hasText(content)) {
return null;
}
try {
if (clazz.getName().equals("java.util.List")) {
if (Objects.isNull(typeReference)) {
typeReference = getTypeReference();
}
return (T) mapper.readValue(content, typeReference);
}
return mapper.readValue(content, clazz);
} catch (Exception ex) {
log.error("JsonTypeHandler error on toObject content:{},class:{}", content, clazz.getName(), ex);
throw new RuntimeException("JsonTypeHandler error on toObject", ex);
}
}

// protected boolean isPostgre() {
// SqlSessionFactory sqlSessionFactory = SpringUtil.getBean(SqlSessionFactory.class);
// Configuration conf = sqlSessionFactory.getConfiguration();
// DataSource dataSource = conf.getEnvironment().getDataSource();
// try (Connection connection = dataSource.getConnection()) {
// String url = connection.getMetaData().getURL();
// return url.contains("postgresql");
// } catch (SQLException e) {
// throw new RuntimeException("Failed to determine database type", e);
// }
// }

@SneakyThrows
private void setObject(PreparedStatement ps, int i, T parameter) {
PGobject jsonObject = new PGobject();
jsonObject.setType("json");
jsonObject.setValue(JsonUtil.toJson(parameter));
ps.setObject(i, jsonObject);
}

@SneakyThrows
private T toObject(ResultSet rs, String columnName) {
Object object = rs.getObject(columnName);
return toObject(object);
}

@SneakyThrows
private T toObject(ResultSet rs, int columnIndex) {
Object object = rs.getObject(columnIndex);
return toObject(object);
}

@SneakyThrows
private T toObject(CallableStatement rs, int columnIndex) {
Object object = rs.getObject(columnIndex);
return toObject(object);
}

public T toObject(Object object) {
if (object instanceof String json) {
return this.toObject(json);
}
if (object instanceof PGobject pgObject) {
String json = pgObject.getValue();
return this.toObject(json);
}

return null;
}
}


<result column="router_info" jdbcType="OTHER" property="routerInfo" typeHandler="***.cms.cmslib.mybatis.JsonTypeHandler"/>


<set>
<if test="routerInfo != null">
router_info = #{routerInfo,typeHandler=***.cms.cmslib.mybatis.JsonTypeHandler}
if>
set>
where id = #{id}

如果JSON中存储是的List, Map,Set等类型时, 会存在泛型类型中类型擦除的问题。因此,如果存在这种情况,我们需要扩展子类,在子类中提供详细的类型信息TypeReference

/**
* @author james.h.fu
* @create 2024/12/9 20:45
*/

public class ComponentUpdateListJsonTypeHandler extends JsonTypeHandler> {
public ComponentUpdateListJsonTypeHandler(Class<List<ComponentUpdate>> clazz) {
super(clazz);
}

@Override
protected TypeReference getTypeReference() {
return new TypeReference<List<ComponentUpdate>>() {
};
}
}
  1. pgsql不支持mysql insert ignore语法,  pgsql提供了类似的语法:
INSERT INTO orders (product_id, user_id)

VALUES (101202)

ON CONFLICT (product_id, user_id) DO NOTHING;

但是与mysql insert ignore并不完全等价,  关于这个点如何改造,  需要结合场景或者业务逻辑来斟酌定夺.

  1. pgsql也不支持INSERT ... ON DUPLICATE KEY UPDATE,   如果代码中有使用这个语法,  pgsql提供了类似的语法:
INSERT INTO users (email, name, age)
VALUES ('test@example.com''John'30)
ON CONFLICT (email)
DO UPDATE SET
name = EXCLUDED.name,
age = EXCLUDED.age;

EXCLUDED 是一个特殊的表别名,用于引用因冲突而被排除(Excluded)的、尝试插入的那条数据.

CONFLICT也可以直接面向唯一性约束.  假如users有一个唯一性约束: unique_email_constraint,  上述SQL可以改成:

INSERT INTO users (email, name, age)
VALUES ('test@example.com''John'30)
ON CONFLICT ON CONSTRAINT unique_email_constraint
DO UPDATE SET
name = EXCLUDED.name,
age = EXCLUDED.age;
  1. 分页:mysql的分页使用的是:   limit B(offset),A(count), 但是pgsql不支持这种语法, pgsql支持的是如下两种:

(1)、limit A offset B; (2)、OFFSET B ROWS FETCH NEXT A ROWS ONLY;

  1. pgsql查询区分大小写,  而mysql是不区分的
  2. 其它情况 (1)、代码中存在取1个数据的场景,原来mysql写法是limit 0,1, 要调整为limit 1; (2)、在mysql中BIT(1)或tinyint(值0,1)可以转换为Boolean。但是在pgsql中不支持。需要明确使用boolean类型或INT类型, 或者使用typerhandler处理。
ALTER TABLE layout 
ALTER COLUMN init_instance TYPE INT2
USING CASE
WHEN init_instance = B'1' THEN 1
WHEN init_instance = B'0' THEN 0
ELSE NULL
END;

update component c
set init_instance = cp.init_instance
from component_publish cp
where c.init_instance is null and c.id = cp.component_id ;

(3)、迁移数据后,统一将自增列修改

DO $$
DECLARE
rec RECORD;
BEGIN
FOR rec IN
SELECT
tc.sequencename
FROM pg_sequences tc
LOOP
EXECUTE format('ALTER SEQUENCE %I RESTART WITH 100000', rec.sequencename);
RAISE NOTICE 'Reset sequence % to 100000', rec.sequencename;
END LOOP;
END $$;

总结

在日常开发中,我们一定要再严谨一些,规范编码。这样能让写我的代码质量更好,可移植性更高。

附录

在PostgreSQL 中,有哪些数据类型?

PostgreSQL 支持多种数据类型,下面列出一些常用的数据类型:

  • 数值类型

    • smallint:2字节整数
    • integer:4字节整数
    • bigint:8字节整数
    • decimal 或 numeric:任意精度的数值
    • real:4字节浮点数
    • double precision:8字节浮点数
    • smallserial:2字节序列整数
    • serial:4字节序列整数
    • bigserial:8字节序列整数
  • 字符与字符串类型

    • character varying(n) 或 varchar(n):变长字符串,最大长度为n
    • character(n) 或 char(n):定长字符串,长度为n
    • text:变长字符串,没有长度限制
  • 日期/时间类型

    • date:存储日期(年月日)
    • time [ (p) ] [ without time zone ]:存储时间(时分秒),可指定精度p,默认不带时区
    • time [ (p) ] with time zone:存储时间(时分秒),可指定精度p,带时区
    • timestamp [ (p) ] [ without time zone ]:存储日期和时间,默认不带时区
    • timestamp [ (p) ] with time zone:存储日期和时间,带时区
    • interval:存储时间间隔
  • 布尔类型

    • boolean:存储真或假值
  • 二进制数据类型

    • bytea:存储二进制字符串
  • 几何类型

    • point:二维坐标点
    • line:无限长直线
    • lseg:线段
    • box:矩形框
    • path:闭合路径或多边形
    • polygon:多边形
    • circle:圆
  • 网络地址类型

    • cidr:存储IPv4或IPv6网络地址
    • inet:存储IPv4或IPv6主机地址和可选的CIDR掩码
    • macaddr:存储MAC地址
  • 枚举类型

    • enum:用户定义的一组排序标签
  • 位串类型

    • bit( [n] ):固定长度位串
    • bit varying( [n] ):变长位串
  • JSON类型

    • json:存储JSON数据
    • jsonb:存储JSON数据,以二进制形式存储,并支持查询操作
  • UUID类型

    • uuid:存储通用唯一标识符
  • XML类型

    • xml:存储XML数据

这些数据类型可以满足大多数应用的需求。在创建表时,根据实际需要选择合适的数据类型是非常重要的。

在MyBatis中,jdbcType有哪些?

jdbcType 是 MyBatis 和其他 JDBC 相关框架中用于指定 Java 类型和 SQL 类型之间映射的属性。以下是常见的 jdbcType 值及其对应的 SQL 数据类型:

  • NULL:表示 SQL NULL 类型
  • VARCHAR:表示 SQL VARCHAR 或 VARCHAR2 类型
  • CHAR:表示 SQL CHAR 类型
  • NUMERIC:表示 SQL NUMERIC 类型
  • DECIMAL:表示 SQL DECIMAL 类型
  • BIT:表示 SQL BIT 类型
  • TINYINT:表示 SQL TINYINT 类型
  • SMALLINT:表示 SQL SMALLINT 类型
  • INTEGER:表示 SQL INTEGER 类型
  • BIGINT:表示 SQL BIGINT 类型
  • REAL:表示 SQL REAL 类型
  • FLOAT:表示 SQL FLOAT 类型
  • DOUBLE:表示 SQL DOUBLE 类型
  • DATE:表示 SQL DATE 类型(只包含日期部分)
  • TIME:表示 SQL TIME 类型(只包含时间部分)
  • TIMESTAMP:表示 SQL TIMESTAMP 类型(包含日期和时间部分)
  • BLOB:表示 SQL BLOB 类型(二进制大对象)
  • CLOB:表示 SQL CLOB 类型(字符大对象)
  • ARRAY:表示 SQL ARRAY 类型
  • DISTINCT:表示 SQL DISTINCT 类型
  • STRUCT:表示 SQL STRUCT 类型
  • REF:表示 SQL REF 类型
  • DATALINK:表示 SQL DATALINK 类型
  • BOOLEAN:表示 SQL BOOLEAN 类型
  • ROWID:表示 SQL ROWID 类型
  • LONGNVARCHAR:表示 SQL LONGNVARCHAR 类型
  • NVARCHAR:表示 SQL NVARCHAR 类型
  • NCHAR:表示 SQL NCHAR 类型
  • NCLOB:表示 SQL NCLOB 类型
  • SQLXML:表示 SQL XML 类型
  • JAVA_OBJECT:表示 SQL JAVA_OBJECT 类型
  • OTHER:表示 SQL OTHER 类型
  • LONGVARBINARY:表示 SQL LONGVARBINARY 类型
  • VARBINARY:表示 SQL VARBINARY 类型
  • LONGVARCHAR:表示 SQL LONGVARCHAR 类型

在使用 MyBatis 或其他 JDBC 框架时,选择合适的 jdbcType 可以确保数据正确地在 Java 和数据库之间进行转换。


作者:Java分布式架构实战
来源:juejin.cn/post/7460410854775455794

收起阅读 »

如何统一管理枚举类?

Hello,大家好,今天我们来聊一下关于系统中的枚举是如何统一进行管理的。 业务场景 我们公司有这样的一个业务场景前端表单中 下拉选择的枚举值,是需要从后端获取的。那么这时候有个问题,我们不可能每次新增加一个枚举,都需要 改造获取枚举 的相关接口(getEnu...
继续阅读 »

Hello,大家好,今天我们来聊一下关于系统中的枚举是如何统一进行管理的。


业务场景


我们公司有这样的一个业务场景前端表单中 下拉选择的枚举值,是需要从后端获取的。那么这时候有个问题,我们不可能每次新增加一个枚举,都需要 改造获取枚举 的相关接口(getEnum),所以我们就需要对系统中的所有枚举类,进行统一的一个管理。


核心思路


为了解决这个问题,我们采用了如下的方案



  1. 当服务启动时,统一对 枚举类 进行 注册发现

  2. 枚举管理类,对外暴露一个方法,可以根据我的key 去获取对应的枚举值


相关实现


枚举定义


基于以上的思想,我们对枚举类定义了如下的规范,例如


@**IsEnum**
public enum BooleanEnum implements BaseEnums {
YES("是", "1"),
NO("否", "2")
;

private String text;
@EnumValue
private String value;

YesNoEnum(String text, String value) {
this.text = text;
this.value = value;
}

public String getText() {
return text;
}

@Override
public String getValue() {
return value;
}

@Override
public String toString() {
return "YesNoEnum{" +
"text='" + text + '\'' +
", value=" + value +
'}';
}

@Override
public EnumRequest toJson() {
return new EnumRequest(value, text);
}

@JsonCreator
public static YesNoEnum fromCode(String value) {
for (YesNoEnum status : YesNoEnum.values()) {
if (status.value.equals(value)) {
return status;
}
}
return null; // 或抛出异常
}
}


  1. 所有枚举均使用 @IsEnum进行标记(这是一个自定义注解)

  2. 所有枚举均实现 BaseEnums 接口(具体作用后续会提到)

  3. 所有的枚举的 value 值 统一使用 String 类型,并使用 @EnumValue 注解进行标记



    1. 这主要是为了兼容Mybatis Plus 查表时 基础数据类型与枚举类型的转化

    2. 如果将 value 定义为Object类型,Mybatis Plus 无法正确识别,会报错



  4. 使用 @JsonCreator 注解标记转化函数



    1. 这个注解是有Jackson提供的,使用了从 基础数据类型到枚举类型的转化




注册发现


那么我们是如何实现服务的注册发现的呢?在这里主要是 使用了 SpringBoot 提供的接口CommandLineRunner (关于CommandLineRunner 可以参考这篇文章blog.csdn.net/python113/a…


在应用启动时,我们回去扫描 枚举所在的 包,通过 类上 是否包含 IsEnum 注解,判断是否需要对外暴露


 // 指定要扫描的包
String basePackage = "com.xxx.enums";

// 创建扫描器
ClassPathScanningCandidateComponentProvider scanner =
new ClassPathScanningCandidateComponentProvider(false);
scanner.addIncludeFilter(new AnnotationTypeFilter(EnumMarker.class));

// 扫描指定包
Set<BeanDefinition> beans = scanner.findCandidateComponents(basePackage);

// 注册枚举
for (org.springframework.beans.factory.config.BeanDefinition beanDefinition : beans) {
try {
Class<?> enumClass = Class.forName(beanDefinition.getBeanClassName());
if (Enum.class.isAssignableFrom(enumClass)) {
enumManager.registerEnum((Class<? extends Enum<?>>) enumClass);
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}

ClassPathScanningCandidateComponentProvider 是 Spring 框架中的一个类,主要用于扫描指定路径下符合条件的候选组件(通常是 Bean 定义)。它允许我们在类路径中扫描并筛选符合特定条件(如标注特定注解、实现某接口等)的类,以实现自动化配置、依赖注入等功能。


典型用途


在基于注解的 Spring 应用中,我们可以使用它来动态扫描特定包路径下的类并注册成 Spring Bean。例如,Spring 扫描 @Component@Service@Repository 等注解标注的类时就会用到它。


主要方法



  • addIncludeFilter(TypeFilter filter) :添加一个包含过滤器,用于筛选扫描过程中包含的类。

  • findCandidateComponents(String basePackage) :扫描指定包路径下的候选组件(符合条件的类),并返回符合条件的 BeanDefinition 对象集合。

  • addExcludeFilter(TypeFilter filter) :添加一个排除过滤器,用于排除特定类。


最终呢,会将找到的枚举值,放在一个EnumManager中的一个Map集合中


 private final Map<Class<?>, List<Enum<?>>> enumMap = new HashMap<>(); // 类与枚举类型的映射关系
private final Map<String, List<Enum<?>>> enumNameMap = new HashMap<>(); // 名称与枚举的映射管理

 public <E extends Enum<E>> void registerEnum(Class<? extends Enum<?>> enumClass) {
Enum<?>[] enumConstants = enumClass.getEnumConstants();
final List<Enum<?>> list = Arrays.asList(enumConstants);
enumMap.put(enumClass, list);
enumNameMap.put(enumClass.getSimpleName(), list);
}

enumClass.getEnumConstants() 是 Java 反射 API 中的一个方法,用于获取某个枚举类中定义的所有枚举实例。getEnumConstants() 会返回一个包含所有枚举常量的数组,每个元素都是该枚举类的一个实例。


这样子我们就可以通过枚举的名称或者class 获取枚举列表返回给前端


enumMap.get(enumClass); enumNameMap.get(enumName);

请求与响应


我们项目中使用的序列化器 是Jackson,通过 @JsonValue@JsonCreator两个注解实现的。


@JsonValue:对象序列化为json时会调用这个注解标记的方法


@JsonValue
public EnumRequest toJson() {
return new EnumRequest(value, text);
}

@JsonCreator :json反序列化对象时会调用这个注解标记的方法


 @JsonCreator
public static YesNoEnum fromCode(String value) {
for (YesNoEnum status : YesNoEnum.values()) {
if (status.value.equals(value)) {
return status;
}
}
return null; // 或抛出异常
}

但是这里有个坑,我们SpringBoot的版本是2.5,使用 @JsonCreator 时会报错,这时候只需要降低jackson 的版本就可以了


 // 排除 spring-boot-starter-web 中的jsckson
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</exclusion>
<exclusion>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</exclusion>
<exclusion>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</exclusion>
</exclusions>
</dependency>

<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.10.5</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.10.5</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.10.5</version>
</dependency>

Mybatis Plus 枚举中的使用



  • 在applicaton.yml文件中添加如下参数


# 在控制台输出sql
mybatis-plus:
type-enums-package: com.xxx.enums // 定义枚举的扫描包


  • 将枚举值中 存入到数据库的字段 使用 @EnumValue注解进行标记,例如:上面提供的YesNoEnum 类中的value字段使用 @EnumValue 进行了标记 数据库中实际保存的就是 value 的值(1/2)

  • 将domian 中 直接定义为枚举类型就可以了,是不是非常简单呢?


以上就是本篇文章的全部内容,如果有更好的方案欢迎小伙伴们评论区讨论!


作者:学编程的小菜鸟
来源:juejin.cn/post/7431838327844995098
收起阅读 »