注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

😡同事查日志太慢,我现场教他一套 grep 组合拳!

前言 最近公司来了个新同事,年轻有活力,就是查日志的方式让我有点裂开。 事情是这样的:他写的代码在测试环境报错了,报警信息也被钉钉机器人发到了我们群里。作为资深摸鱼战士,我寻思正好借机摸个鱼顺便指导一下新人,就凑过去看了眼。 结果越看我越急,差点当场喊出:“兄...
继续阅读 »

前言


最近公司来了个新同事,年轻有活力,就是查日志的方式让我有点裂开。


事情是这样的:他写的代码在测试环境报错了,报警信息也被钉钉机器人发到了我们群里。作为资深摸鱼战士,我寻思正好借机摸个鱼顺便指导一下新人,就凑过去看了眼。


结果越看我越急,差点当场喊出:“兄弟你是来写代码的,还是和日志谈恋爱的?”


来看看他是怎么查日志的


他先敲了一句:


tail -f a.log | grep "java.lang.NullPointerException"

想着等下次报错就能立刻看到。等了半天,终于蹦出来一行:


2025-07-03 11:38:48.339 [http-nio-8960-exec-1] [47gK4n32jEYvTYX8AYti48] [INFO] [GlobalExceptionHandler] java.lang.NullPointerException, ex: java.lang.NullPointerException
java.lang.NullPointerException: null

我提醒他:“这样看不到堆栈信息啊。”


他“哦”了一声,灵机一动,用 vi把整个文件打开,/NullPointerException 搜关键词,一个 n 一个 n 地翻……半分钟过去了,异常在哪都没找全,我都快给他跪下了。



于是我当场掏出了一套我压箱底的“查日志组合拳”,一招一式手把手教他。他当场就“悟了”,连连称妙,并表示想让我写成文章好让他发给他前同事看——因为他前同事也是这样查的……


现在,这套组合拳我也分享给你,希望你下次查日志的时候,能让你旁边的同事开开眼。


正式教学


核心的工具其实还是 grep 命令,下面我将分场景给你讲讲我的实战经验,保证你能直接套用!


场景一:查异常堆栈,不能只看一行!


Java 异常堆栈通常都是多行的,仅仅用 grep "NullPointerException" 只能看到最上面那一行,问题根源在哪你压根找不到。


这时候使用 **grep****-A** (After) 参数来显示匹配行之后的N行。


# 查找 NullPointerException,并显示后面 50 行
grep -A 50 "java.lang.NullPointerException" a.log

如果你发现异常太多,屏幕一闪而过,也可以用less加上分页查看:


grep -A 50 "java.lang.NullPointerException" a.log | less

less 视图中,你可以:



  • 使用 箭头↑↓Page Up/Down 键来上下滚动

  • 输入 G 直接翻到末尾,方便快速查看最新的日志

  • 输入 /Exception 继续搜索

  • q 键退出


这样你就能第一时间拿到完整异常上下文信息,告别反复 vi + / 的低效操作!



场景二:实时看新日志怎么打出来的


如果你的应用正在运行,并且你怀疑它会随时抛出异常,你可以实时监控日志文件的增长。


使用 tail -f 结合 grep


# 实时监控 a.log 文件的新增内容,并只显示包含 "java.lang.NullPointerException" 的行及其后50行
tail -f a.log | grep -A 50 "java.lang.NullPointerException"

只要异常一出现,它就会自动打出来,堆栈信息也一并送到你面前!



  • 想停下?Ctrl + C

  • 想更准确?加 -i 忽略大小写,防止大小写拼错找不到


场景三:翻历史日志 or 查压缩日志


服务器上的日志一般都会按天或按大小分割并压缩,变成 .log.2025-07-02.gz 这种格式,查找这些文件的异常信息怎么办?


🔍 查找当前目录所有 .log 文件:


# 在当前目录下查找所有以 .log 结尾的文件,-H 参数可以顺便打印出文件名
grep -H -A 50 "java.lang.NullPointerException" *.log

其中 -H 会帮你打印出是哪个文件中出现的问题,防止你找完还不知道是哪天的事。


🔍 查找 .gz 文件(压缩日志):


zgrep -H -A 50 "java.lang.NullPointerException" *.gz

zgrep 是专门处理 .gzgrep,它的功能和 grep 完全一样,无需手动解压,直接开整!


场景四:统计异常数量(快速判断异常是否频繁)


有时候你需要知道某个异常到底出现了多少次,是偶发还是成灾,使用 grep -c(count):


grep -c "java.lang.NullPointerException" a.log

如果你要统计所有日志里的数量:


grep -c "java.lang.NullPointerException" *.log

其他常用的 grep 参数


参数作用
-B N匹配行之前的 N 行(Before)
-A N匹配行之后的 N 行(After)
-C N匹配行上下共 N 行(Context)
-i忽略大小写
-H显示匹配的文件名
-r递归搜索目录下所有文件

比如:


grep -C 25 "java.lang.NullPointerException" a.log

这个命令就能让你一眼看到异常前后的上下文,帮助定位代码逻辑是不是哪里先出问题了。


尾声


好了,这套组合拳我已经传授给你了,要是别人问你在哪学的,记得报我杆师傅的大名(doge)。


其实还有其他查日志的工具,比如awkwc 等。


但是我留了一手,没有全部教给我这个同事,毕竟江湖规则,哪有一出手就把看家本领全都交出去的道理?


如果你也想学,先拜个师交个学费(点赞、收藏、关注),等学费凑够了,我下次再开新课,传授给大家~



作者:阿杆
来源:juejin.cn/post/7524216834619408430
收起阅读 »

为什么我不再相信 Tailwind?三个月重构项目教会我的事

Tailwind 曾经是我最爱的工具,直到它让我维护不下去整个项目。 前情提要:我是如何变成 Tailwind 重度用户的 作为一个多年写 CSS 的前端,我曾经深陷“命名地狱”: 什么 .container-title, .btn-primary, .f...
继续阅读 »

Tailwind 曾经是我最爱的工具,直到它让我维护不下去整个项目。





前情提要:我是如何变成 Tailwind 重度用户的


作为一个多年写 CSS 的前端,我曾经深陷“命名地狱”:

什么 .container-title, .btn-primary, .form-item-error,一个项目下来能写几百个类名,然后改样式时不知道该去哪动刀,甚至删个类都心慌。


直到我遇见了 Tailwind CSS——一切原子化,想改样式就加 class,别管名字叫什么,直接调属性即可。


于是我彻底拥抱它,团队项目里我把所有 SCSS 全部清除,组件中也只保留了 Tailwind class,一切都干净、轻便、高效。


但故事从这里开始转变。




三个月后的重构期,我被 Tailwind“反噬”


我们的后台管理系统迎来一次大版本升级,我负责重构 UI 样式逻辑,目标是:



  • 统一设计规范;

  • 提高代码可维护性;

  • 降低多人协作时的样式冲突。


刚开始我信心满满,毕竟 Tailwind 提供了:



  • 原子化 class;

  • @apply 合成组件级 class;

  • 配置主题色/字体/间距系统;

  • 插件支持动画/form 控件/typography;


但随着项目深入,我开始发现 几个巨大的问题,并最终决定停用 Tailwind。




一、class 污染:结构和样式纠缠成灾


来看一个真实例子:


<div class="flex items-center justify-between bg-white p-4 rounded-lg shadow-sm border border-gray-200">
<h2 class="text-lg font-semibold text-gray-800">订单信息</h2>
<button class="text-sm px-2 py-1 bg-blue-500 text-white rounded hover:bg-blue-600">编辑</button>
</div>

你能看出这个组件的“设计意图”吗?

你能快速改它的样式吗?


一个看似简单的按钮,一眼看不到设计语言,只看到一坨 class,你根本不知道:



  • px-2 py-1 是从哪里决定的?

  • bg-blue-500 是哪个品牌色?

  • hover:bg-blue-600 是统一交互吗?


Tailwind 让样式变得快,但也让样式“变得不可读”。




二、复用失败:想复用样式还得靠 SCSS


我天真地以为 @apply 能帮我合成组件级样式,比如:


.btn-primary {
@apply text-white bg-blue-500 px-4 py-2 rounded;
}

但问题来了:



  • @apply 不能用在媒体查询内

  • @apply 不支持复杂嵌套、hover/focus 的组合;

  • 响应式伪类写在 HTML 里更乱,如:lg:hover:bg-blue-700

  • 没法动态拼接 class,逻辑和样式混在组件逻辑层了


最终结果就是:复用失败、样式重复、维护困难。




三、设计规范无法沉淀


我们设计系统中定义了若干基础变量:



  • 主色:#0052D9

  • 次色:#A0AEC0

  • 字体尺寸规范:12/14/16/18/20/24/32px

  • 组件间距:8/16/24


本来我们希望 Tailwind 的 theme.extend 能承载这套设计系统,结果发现:



  • tailwind.config.js 修改后,需要全员重启 dev server

  • 新增设计 token 非常繁琐,不如直接写 SCSS 变量;

  • 多人改配置时容易冲突;

  • 和设计稿同步代价高。


这让我明白:配置式设计系统不适合快速演进的产品团队。




四、多人协作混乱:Tailwind 并不直观


当我招了一位新同事,给他一个组件代码时,他的第一句话是:



“兄弟,这些 class 是从设计稿复制的吗?”



他根本看不懂 gap-6, text-gray-700, tracking-wide 分别是什么意思,只看到一堆“魔法 class”


更糟糕的是,每个人心中对 text-smtext-base 的视觉认知不同,导致多个组件在微调时出现样式不一致、间距不统一的问题。


Tailwind 的语义脱离了设计意图,协作就失去了基础。




最终决定:我切回了 SCSS + BEM + 设计 token


我们开始回归传统模式:



  • 所有组件都有独立 .scss 文件;

  • 使用 BEM 命名规范:.button, .button--primary, .button--disabled

  • 所有颜色/间距/字体等统一放在 _variables.scss 中;

  • 每个组件样式文件都注释设计规范来源。


这种模式虽然看起来“原始”,但它:



  • 清晰分离结构和样式

  • 强制大家遵守设计规范

  • 组件样式可复用,可继承,可重写

  • 新人一眼看懂,不需要会 Tailwind 语法。




总结:Tailwind 不是错,是错用的代价太高


Tailwind 在以下场景表现极好:



  • 个人项目 / 小程序:快速开发、无需复用;

  • 组件库原型:试验颜色、排版效果;

  • 纯前端工程师独立开发的项目:没有协作负担。


但在以下情况,Tailwind 会成为维护灾难:



  • 多人协作;

  • UI 不断迭代,设计语言需频繁调整;

  • 有强复用需求(组件抽象);

  • 与设计系统严格对齐的场景;




我为什么写这篇文章?


不是为了黑 Tailwind,而是为了让你在选择技术栈时更慎重。


就像当年我们争论 Sass vs Less,今天的 Tailwind vs 原子/语义 CSS 并没有标准答案。


Tailwind 很强,但不是所有团队都适合。


也许你正在享受它的爽感,但三个月后你可能会像我一样,把所有 .w-full h-screen text-gray-800 替换成 .layout-container




尾声:如果你非要继续用 Tailwind,我建议你做这几件事



  1. 强制使用 @apply 形成组件级 class,不允许直接使用长串 class;

  2. 抽离公共样式,写在一个统一的组件样式文件中;

  3. 和设计团队对齐 Tailwind 的 spacing/font/color;

  4. 用 tailwind.config.js 做好 token 映射和语义名设计

  5. 每个页面都进行 CSS code review,不然很快就会变垃圾堆。




作者:ErpanOmer
来源:juejin.cn/post/7511602231508664361
收起阅读 »

用了十年 Docker,我为什么决定换掉它?

一、Docker 不再万能,我们该何去何从? 过去十年,Docker 改变了整个软件开发世界。它以“一次构建,到处运行”的理念,架起了开发者和运维人员之间的桥梁,推动了 DevOps 与微服务架构的广泛落地。 从自动化部署、持续集成到快速交付,Docker 一...
继续阅读 »

一、Docker 不再万能,我们该何去何从?


过去十年,Docker 改变了整个软件开发世界。它以“一次构建,到处运行”的理念,架起了开发者和运维人员之间的桥梁,推动了 DevOps 与微服务架构的广泛落地。


自动化部署、持续集成到快速交付,Docker 一度是不可或缺的技术基石。


然而到了 2025 年,越来越多开发者开始重新审视 Docker。


image.png


系统规模在不断膨胀,开发场景也更加多元,不再是当初以单一后端应用为主的架构。


如今,开发者面临的不只是如何部署一个服务,更要关注架构的可扩展性、容器的安全性、本地与云端的适配性,以及资源的最优利用。


在这种背景下,Docker 开始显得不再那么“全能”,它在部分场景下的臃肿、安全隐患和与 Kubernetes 的解耦问题,使得不少团队正在寻找更轻、更适合自身的替代方案。


之所以写下这篇文章就是为了帮助你认清 Docker 当前的局限,了解新的技术趋势,并发现适用于不同场景的下一代容器化工具。


二、Docker 的贡献与瓶颈


不可否认,Docker 曾是容器化革命的引擎。从过去到现在,它的最大价值在于降低了环境配置的复杂度,让开发与运维团队之间的协作更加顺畅,带动了整个容器生态的发展。


很多团队正是依赖 Docker 才实现了快速构建镜像、构建流水线、部署微服务的能力。


但与此同时,Docker 本身也逐渐显露出局限性。比如,它高度依赖守护进程,导致资源占用明显高于预期,启动速度也难以令人满意


更关键的是,Docker 默认以 root 权限运行容器,极易放大潜在攻击面,在安全合规日益严格的今天,这一点令人担忧。Kubernetes 的官方运行时也已从 Docker 切换为 containerd 与 runc,表明行业主流已在悄然转向。


这并不意味着 Docker 已过时,它依旧在许多团队中扮演重要角色。但如果你期待更高的性能、更低的资源消耗和更强的安全隔离,那么,是时候拓宽视野了。


image.png


三、本地开发的难题与新解法


特别是在本地开发场景中,Docker 的“不够轻”问题尤为突出。为了启动一个简单的 PHP 或 Node 项目,很多人不得不拉起庞大的容器,等待镜像下载、构建,甚至调试端口映射,最终电脑风扇轰鸣,开发体验直线下降。


一些开发者试图回归传统,通过 Homebrew 或 apt 手动配置开发环境,但这又陷入了“版本冲突”“依赖错位”等老问题。


这时,ServBay 的出现带来了新的可能。作为专为本地开发设计的轻量级工具,ServBay 不依赖 Docker,也无需繁琐配置。用户只需一键启动,即可在本地运行 PHP、Python、Golang、Java 等多种语言环境,并能自由切换版本与服务组合。它不仅启动迅速,资源占用也极低,非常适合 WordPress、Laravel、ThinkPHP 等项目的本地调试与开发


更重要的是,ServBay 不再强制开发者理解复杂的镜像构建与容器编排逻辑,而是将本地开发流程变得像打开编辑器一样自然。对于 Web 后端和全栈开发者来说,它提供了一种“摆脱 Docker”的全新路径。


image.png


四、当 Docker 不再是运行时的唯一选择


容器运行时的格局也在悄然生变。containerd 和 runc 成为了 Kubernetes 官方推荐的运行时,它们更轻、更专注,仅提供核心的容器管理功能,剥离了不必要的附加组件。与此同时,CRI-O 正在被越来越多团队采纳,它是专为 Kubernetes 打造的运行时,直接对接 CRI 接口,减少了依赖层级。


另一款备受好评的是 Podman,它的最大亮点在于支持 rootless 模式,使容器运行更加安全。同时,它的命令行几乎与 Docker 完全兼容,开发者几乎不需要重新学习。


对于安全隔离要求极高的场景,还可以选择 gVisor 或 Kata Containers。前者通过用户态内核方式拦截系统调用,构建沙箱化环境;后者则将轻量虚拟机与容器结合,兼顾性能与隔离性。这些方案正在逐步替代传统 Docker,成为新一代容器架构的基石。


image.png


五、容器编排:Kubernetes 之后的路在何方?


虽然 Kubernetes 仍然是企业级容器编排的标准选项,但它的复杂性和陡峭的学习曲线也让不少中小团队望而却步。一个简单的应用部署可能涉及上百行 YAML 文件,过度的抽象与组件拆分反而拉高了运维门槛。


这也促使“微型 Kubernetes”方案逐渐兴起。K3s 是其中的代表,它对 Kubernetes 进行了极大简化,专为边缘计算和资源受限场景优化。此外,像 KubeEdge 等项目,也在积极拓展容器编排在边缘设备上的适配能力。


与此同时,AI 驱动的编排平台正在探索新路径。CAST AI、Loft Labs 等团队推出的智能调度系统,可以自动分析工作负载并进行优化部署,最大化资源利用率。更进一步,Serverless 与容器的融合也逐渐成熟,比如 AWS Fargate、Google Cloud Run 等服务,让开发者无需再关心节点管理,容器真正变成了“即用即走”的计算单元。



六、未来趋势:容器走向“定制化生长”


未来的容器化,我们将看到更细化的技术选型:开发环境选择轻量灵活的本地容器,测试环境强调快速重建与自动化部署,生产环境则关注安全隔离与高可用性。


安全性也会成为核心关键词。rootless 容器、沙箱机制和系统调用过滤将成为主流实践,容器从“不可信”向“可信执行环境”演进。与此同时,人工智能将在容器调度中发挥更大作用,不仅提升弹性伸缩的效率,还可能引领“自愈系统”发展,让集群具备自我诊断与恢复能力。


容器标准如 OCI 的持续完善,将让不同运行时之间更加兼容,为整个生态的整合提供可能。而在部署端,我们也将看到容器由本地向云端、再向边缘设备的自然扩展,真正成为“无处不在的基础设施”。


image.png


七、结语:容器化的新纪元已经到来


Docker 的故事并没有结束,它依然是很多开发者最熟悉的工具,也在部分场景中继续发挥作用。但可以确定的是,它不再是唯一选择。2025 年的容器世界,早已迈入了多元化、场景化、智能化的阶段。从轻量级的 ServBay 到更安全的 Podman,从微型编排到 Serverless 混合模式,我们手中可选的工具越来越丰富,技术栈的自由度也空前提升


下一个十年,容器不只是为了“装下服务”,它将成为构建现代基础设施的关键砖块。愿你也能在这场演进中,找到属于自己的工具组合,打造更轻、更快、更自由的开发与部署体验。


作者:Jacob0234
来源:juejin.cn/post/7521927128524210212
收起阅读 »

放弃 JSON.parse(JSON.stringify()) 吧!试试现代深拷贝!

作者:程序员成长指北 原文:mp.weixin.qq.com/s/WuZlo_92q… 最近小组里的小伙伴,暂且叫小A吧,问了一个bug: 提示数据循环引用,相信不少小伙伴都遇到过类似问题,于是我问他: 我:你知道问题报错的点在哪儿吗 小A: 知道,就是下面...
继续阅读 »

作者:程序员成长指北


原文:mp.weixin.qq.com/s/WuZlo_92q…


最近小组里的小伙伴,暂且叫小A吧,问了一个bug:图片


提示数据循环引用,相信不少小伙伴都遇到过类似问题,于是我问他:



我:你知道问题报错的点在哪儿吗


小A: 知道,就是下面这个代码,但不知道怎么解决。



onst a = {};
const b = { parent: a };
a.child = b; // 形成循环引用

try {
  const clone = JSON.parse(JSON.stringify(a));
} catch (error) {
  console.error('Error:', error.message); // 会报错:Converting circular structure to JSON
}

上面是我将小A的业务代码提炼为简单示例,方便阅读。



  • 这里 a.child 指向 b,而 b.parent 又指回 a,形成了循环引用。

  • 用 JSON.stringify 时会抛出 Converting circular structure to JSON 的错误。


我顺手查了一下小A项目里 JSON.parse(JSON.stringify()) 的使用情况:


图片


一看有50多处都使用了, 使用频率相当高了。


我继续提问:



我:你有找解决方案吗?


小A: 我看网上说可以自己实现一个递归来解决,但是我不太会实现



于是我帮他实现了一版简单的递归深拷贝:


function deepClone(obj, hash = new Map()) {
if (typeof obj !== 'object' || obj === null) return obj;
if (hash.has(obj)) return hash.get(obj);

const clone = Array.isArray(obj) ? [] : {};
  hash.set(obj, clone);

for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      clone[key] = deepClone(obj[key], hash);
    }
  }
return clone;
}

// 测试
const a = {};
const b = { parent: a };
a.child = b;

const clone = deepClone(a);
console.log(clone.child.parent === clone); // true

此时,为了给他拓展一下,我顺势抛出新问题:



我: 你知道原生Web API 现在已经提供了一个深拷贝 API吗?


小A:???



于是我详细介绍了一下:


主角 structuredClone登场


structuredClone() 是浏览器原生提供的 深拷贝 API,可以完整复制几乎所有常见类型的数据,包括复杂的嵌套对象、数组、Map、Set、Date、正则表达式、甚至是循环引用。


它遵循的标准是:HTML Living Standard - Structured Clone Algorithm(结构化克隆算法)。


语法:


const clone = structuredClone(value);

一行代码,优雅地解决刚才的问题:


const a = {};
const b = { parent: a };
a.child = b; // 形成循环引用

const clone = structuredClone(a);

console.log(clone !== a); // true
console.log(clone.child !== b); // true
console.log(clone.child.parent === clone); // true,循环引用关系被保留

为什么增加 structuredClone


在 structuredClone 出现之前,常用的深拷贝方法有:


方法是否支持函数/循环引用是否支持特殊对象
JSON.parse(JSON.stringify(obj))❌ 不支持函数、循环引用❌ 丢失 DateRegExpMapSet
第三方库 lodash.cloneDeep✅ 支持✅ 支持,但体积大,速度较慢
手写递归✅ 可支持❌ 复杂、易出错

structuredClone 是 原生、极速、支持更多数据类型且无需额外依赖 的现代解决方案。


支持的数据类型


类型支持
Object✔️
Array✔️
Map / Set✔️
Date✔️
RegExp✔️
ArrayBuffer / TypedArray✔️
Blob / File / FileList✔️
ImageData / DOMException / MessagePort✔️
BigInt✔️
Symbol(保持引用)✔️
循环引用✔️

❌ 不支持:



  • 函数(Function)

  • DOM 节点

  • WeakMap、WeakSet


常见使用示例


1. 克隆普通对象


const obj = { a: 1, b: { c: 2 } };
const clone = structuredClone(obj);
console.log(clone);  // { a: 1, b: { c: 2 } }
console.log(clone !== obj); // true

2. 支持循环引用


const obj = { name: 'Tom' };
obj.self = obj;
const clone = structuredClone(obj);
console.log(clone.self === clone);  // true

3. 克隆 Map、Set、Date、RegExp


const complex = {
  mapnew Map([["key""value"]]),
  setnew Set([123]),
  datenew Date(),
  regex/abc/gi
};
const clone = structuredClone(complex);
console.log(clone);

兼容性


提到新的API,肯定得考虑兼容性问题:


图片



  • Chrome 98+

  • Firefox 94+

  • Safari 15+

  • Node.js 17+ (global.structuredClone)


如果需要兼容旧浏览器:



  • 可以降级使用 lodash.cloneDeep

  • 或使用 MessageChannel Hack


很多小伙伴一看到兼容性问题,可能心里就有些犹豫:


"新API虽然好,但旧浏览器怎么办?"


但技术的发展离不开新技术的应用和推广,只有更多人开始尝试并使用,才能让新API真正普及开来,最终成为主流。


建议:


如果你的项目运行在现代浏览器或 Node.js 环境,structuredClone 是目前最推荐的深拷贝方案。 Node.js 17+:可以直接使用 global.structuredClone


作者:独立开阀者_FwtCoder
来源:juejin.cn/post/7524232022124085257
收起阅读 »

localhost 和 127.0.0.1 到底有啥区别?

在开发中,我们经常会接触到 localhost 和 127.0.0.1。很多人可能觉得它们是一样的,甚至可以互换使用。实际上,它们确实有很多相似之处,但细究起来,也存在一些重要的区别。 本篇文章就带大家一起来深入了解 localhost 和 127.0.0.1...
继续阅读 »

在开发中,我们经常会接触到 localhost127.0.0.1。很多人可能觉得它们是一样的,甚至可以互换使用。实际上,它们确实有很多相似之处,但细究起来,也存在一些重要的区别。


本篇文章就带大家一起来深入了解 localhost127.0.0.1,并帮助你搞清楚它们各自的特点和适用场景。




一、什么是 localhost


localhost 是一个域名,它被广泛用于表示当前这台主机(也就是你自己的电脑)。当你在浏览器地址栏输入 localhost 时,操作系统会查找 hosts 文件(在 Windows 中通常位于 C:\Windows\System32\drivers\etc\hosts,在 MacOS 或者 Linux 系统中,一般位于 /etc/hosts),查找 localhost 对应的 IP 地址。如果没有找到,它将默认解析为 127.0.0.1


特点



  • 是一个域名,默认指向当前设备。

  • 不需要联网也能工作。

  • 用于测试本地服务,例如开发中的 Web 应用或 API。



小知识 🌟:域名和 IP 地址的关系就像联系人名字和电话号码。我们用名字联系某个人,实际上是依赖后台的通讯录解析到实际号码来拨号。





二、什么是 127.0.0.1


127.0.0.1 是一个特殊的 IP 地址,它被称为 回环地址(loopback address)。这个地址专门用于通信时指向本机,相当于告诉电脑“别出门,就在家里转一圈”。你可以试一试在浏览器中访问 127.0.0.2 看看会访问到什么?你会发现,它同样会指向本地服务!环回地址的范围是 127.0.0.0/8,即所有以 127 开头的地址都属于环回网络,但最常用的是 127.0.0.1


特点



  • 127.0.0.1 不需要 DNS 解析,因为它是一个硬编码的地址,直接指向本地计算机。

  • 是 IPv4 地址范围中的一个保留地址。

  • 只用于本机网络通信,不能通过这个地址访问外部设备或网络。

  • 是开发测试中最常用的 IP 地址之一。



小知识 🌟:所有从 127.0.0.0127.255.255.255 的 IP 地址都属于回环地址,但通常只用 127.0.0.1





三、两者的相似点



  1. 都指向本机



    • 不管是输入 localhost 还是 127.0.0.1,最终都会将请求发送到你的电脑,而不是其他地方。



  2. 常用于本地测试



    • 在开发中,我们需要在本机运行服务并测试,localhost127.0.0.1 都是标准的本地访问方式。



  3. 无需网络支持



    • 即使你的电脑没有连接网络,这两个也可以正常使用,因为它们完全依赖于本机的网络栈。






四、两者的不同点


区别localhost127.0.0.1
类型域名IP 地址
解析过程需要通过 DNS 或 hosts 文件解析为 IP 地址不需要解析,直接使用
协议版本支持同时支持 IPv4 和 IPv6仅支持 IPv4
访问速度解析时可能稍慢(视 DNS 配置而定)通常更快,因为不需要额外的解析步骤



五、为什么 localhost127.0.0.1 有时表现不同?


在大多数情况下,localhost127.0.0.1 是等效的,但在一些特殊环境下,它们可能会表现出差异:


1. IPv4 和 IPv6 的影响


localhost 默认可以解析为 IPv4(127.0.0.1)或 IPv6(::1)地址,具体取决于系统配置。如果你的程序只支持 IPv4,而 localhost 被解析为 IPv6 地址,可能会导致连接失败。


示例:


# 测试 localhost 是否解析为 IPv6
ping localhost

可能的结果:



  • 如果返回 ::1,说明解析为 IPv6。

  • 如果返回 127.0.0.1,说明解析为 IPv4。


2. hosts 文件配置


在某些情况下,你的 localhost 并不一定指向 127.0.0.1。这是因为域名解析优先会查找系统的 hosts 文件:



  • WindowsC:\Windows\System32\drivers\etc\hosts

  • Linux/macOS/etc/hosts


示例:自定义 localhost


# 修改 hosts 文件
127.0.0.1 my-local

之后访问 http://my-local 会指向 127.0.0.1,但如果 localhost 被误配置成其他地址,可能会导致问题。


3. 防火墙或网络配置的限制


某些网络工具或防火墙规则会区别对待域名和 IP 地址。如果只允许 127.0.0.1 通信,而不允许 localhost,可能会引发问题。




六、在开发中如何选择?



  • 优先使用 localhost

    因为它是更高层次的表示方式,更通用。如果将来需要切换到不同的 IP 地址(例如 IPv6),不需要修改代码。

  • 需要精准控制时用 127.0.0.1

    如果你明确知道程序只需要使用 IPv4 环境,或者想避免域名解析可能带来的问题,直接用 IP 地址更稳妥。


示例:用 Python 测试


# 使用 localhost
import socket
print(socket.gethostbyname('localhost')) # 输出可能是 127.0.0.1 或 ::1

# 使用 127.0.0.1
print(socket.gethostbyname('127.0.0.1')) # 输出始终是 127.0.0.1



七、总结


虽然 localhost127.0.0.1 大部分情况下可以互换使用,但它们的本质不同:



  • localhost 是域名,更抽象。

  • 127.0.0.1 是 IP 地址,更具体。


在开发中,我们应根据场景合理选择,尤其是在涉及到跨平台兼容性或网络配置时,理解它们的差异性会让你事半功倍。



最后,记得动手实践,多跑几个测试。毕竟,编程是用代码说话的艺术!😄





如果你觉得这篇文章对你有帮助,记得点个赞或分享给更多人!有其他技术问题想了解?欢迎评论区留言哦~ 😊


作者:左诗右码
来源:juejin.cn/post/7511583779578200115
收起阅读 »

都说了布尔类型的变量不要加 is 前缀,非要加,这不是坑人了嘛

开心一刻 今天心情不好,给哥们发语音 我:哥们,晚上出来喝酒聊天吧 哥们:咋啦,心情不好? 我:嗯,刚刚在公交车上看见前女友了 哥们:然后呢? 我:给她让座时,发现她怀孕了... 哥们:所以难受了? 我:不是她怀孕让我难受,是她怀孕还坐公交车让我难受 哥们:不...
继续阅读 »

开心一刻


今天心情不好,给哥们发语音

我:哥们,晚上出来喝酒聊天吧

哥们:咋啦,心情不好?

我:嗯,刚刚在公交车上看见前女友了

哥们:然后呢?

我:给她让座时,发现她怀孕了...

哥们:所以难受了?

我:不是她怀孕让我难受,是她怀孕还坐公交车让我难受

哥们:不是,她跟着你就不用坐公交车了?不还是也要坐,有区别吗?

我默默的挂断了语音,心情更难受了


痛苦面具

Java开发手册


作为一个 javaer,我们肯定看过 AlibabaJava开发手册,作为国内Java开发领域的标杆性编码规范,我们或多或少借鉴了其中的一些规范,其中有一点


布尔值变量命名规约

我印象特别深,也一直在奉行,自己还从未试过用 is 作为布尔类型变量的前缀,不知道会有什么坑;正好前段时间同事这么用了,很不幸,他挖坑,我踩坑,阿西吧!


坑爹了

is前缀的布尔变量有坑


为了复现问题,我先简单搞个 demo;调用很简单,服务 workflow 通过 openfeign 调用 offline-sync,代码结构如下


项目模块结构


qsl-data-govern-common:整个项目的公共模块


qsl-offline-sync:离线同步



  • qsl-offline-sync-api:向外提供 openfeign 接口

  • qsl-offline-sync-common:离线同步公共模块

  • qsl-offline-sync-server:离线同步服务


qsl-workflow:工作流



  • qsl-workflow-api:向外提供 openfeign 接口,暂时空实现

  • qsl-workflow-common:工作流公共模块

  • qsl-workflow-server:工作流服务


完整代码:qsl-data-govern



qsl-offline-sync-server 提供删除接口


/**
* @author 青石路
*/

@RestController
@RequestMapping("/task")
public class SyncTaskController {

private static final Logger LOG = LoggerFactory.getLogger(SyncTaskController.class);

@PostMapping("/delete")
public ResultEntity<String> delete(@RequestBody SyncTaskDTO syncTask) {
// TODO 删除处理
LOG.info("删除任务[taskId={}]", syncTask.getTaskId());
return ResultEntity.success("删除成功");
}
}

qsl-offline-sync-api 对外提供 openfeign 接口


/**
* @author 青石路
*/

@FeignClient(name = "data-govern-offline-sync", contextId = "dataGovernOfflineSync", url = "${offline.sync.server.url}")
public interface OfflineSyncApi {

@PostMapping(value = "/task/delete")
ResultEntity<String> deleteTask(@RequestBody SyncTaskDTO syncTaskDTO);
}

qsl-workflow-server 调用 openfeign 接口


/**
* @author 青石路
*/

@RestController
@RequestMapping("/definition")
public class WorkflowController {

private static final Logger LOG = LoggerFactory.getLogger(WorkflowController.class);

@Resource
private OfflineSyncApi offlineSyncApi;

@PostMapping("/delete")
public ResultEntity<String> delete(@RequestBody WorkflowDTO workflow) {
LOG.info("删除工作流[workflowId={}]", workflow.getWorkflowId());
// 1.查询工作流节点,查到离线同步节点(taskId = 1)
// 2.删除工作流节点,删除离线同步节点
ResultEntity<String> syncDeleteResult = offlineSyncApi.deleteTask(new SyncTaskDTO(1L));
if (syncDeleteResult.getCode() != 200) {
LOG.error("删除离线同步任务[taskId={}]失败:{}", 1, syncDeleteResult.getMessage());
ResultEntity.fail(syncDeleteResult.getMessage());
}
return ResultEntity.success("删除成功");
}
}

逻辑是不是很简单?我们启动两个服务,然后发起 http 请求



POST http://localhost:8081/data-govern/workflow/definition/delete
Content-Type: application/json


{
"workflowId": 99
}



此时 qsl-offline-sync-server 日志输出如下



2025-06-30 14:53:06.165|INFO|http-nio-8080-exec-4|25|c.q.s.s.controller.SyncTaskController :删除任务[taskId=1]



至此,一切都很正常,第一版也是这么对接的;后面 offline-sync 进行调整,删除接口增加了一个参数:isClearData


public class SyncTaskDTO {

public SyncTaskDTO(){}

public SyncTaskDTO(Long taskId, Boolean isClearData) {
this.taskId = taskId;
this.isClearData = isClearData;
}

private Long taskId;
private Boolean isClearData = false;

public Long getTaskId() {
return taskId;
}

public void setTaskId(Long taskId) {
this.taskId = taskId;
}

public Boolean getClearData() {
return isClearData;
}

public void setClearData(Boolean clearData) {
isClearData = clearData;
}
}

然后实现对应的逻辑


/**
* @author 青石路
*/

@RestController
@RequestMapping("/task")
public class SyncTaskController {

private static final Logger LOG = LoggerFactory.getLogger(SyncTaskController.class);

@PostMapping("/delete")
public ResultEntity<String> delete(@RequestBody SyncTaskDTO syncTask) {
// TODO 删除处理
LOG.info("删除任务[taskId={}]", syncTask.getTaskId());
if (syncTask.getClearData()) {
LOG.info("清空任务[taskId={}]历史数据", syncTask.getTaskId());
// TODO 清空历史数据
}
return ResultEntity.success("删除成功");
}
}

调整完之后,同事通知我,让我做对 qsl-workflow 做对应的调整。调整很简单,qsl-workflow 删除时直接传 true 即可


/**
* @author 青石路
*/

@RestController
@RequestMapping("/definition")
public class WorkflowController {

private static final Logger LOG = LoggerFactory.getLogger(WorkflowController.class);

@Resource
private OfflineSyncApi offlineSyncApi;

@PostMapping("/delete")
public ResultEntity<String> delete(@RequestBody WorkflowDTO workflow) {
LOG.info("删除工作流[workflowId={}]", workflow.getWorkflowId());
// 1.查询工作流节点,查到离线同步节点(taskId = 1)
// 2.删除工作流节点,删除离线同步节点
// 删除离线同步任务,isClearData直接传true
ResultEntity<String> syncDeleteResult = offlineSyncApi.deleteTask(new SyncTaskDTO(1L, true));
if (syncDeleteResult.getCode() != 200) {
LOG.error("删除离线同步任务[taskId={}]失败:{}", 1, syncDeleteResult.getMessage());
ResultEntity.fail(syncDeleteResult.getMessage());
}
return ResultEntity.success("删除成功");
}
}

调整完成之后,发起 http 请求,发现历史数据没有被清除,看日志发现



LOG.info("清空任务[taskId={}]历史数据", syncTask.getTaskId());



没有打印,参数明明传的是 true 吖!!!



offlineSyncApi.deleteTask(new SyncTaskDTO(1L, true));



这是哪里出了问题?


20240115000802

问题排查


因为 qsl-offline-sync-api 是直接引入的,并非我实现的,所以我第一时间找到了其实现者,反馈了问题后让其自测下;一开始他还很自信,说这么简单怎么会有问题


640 (15)

当他启动 qsl-offline-sync-server 后,发起 http 请求



POST http://localhost:8080/data-govern/sync/task/delete
Content-Type: application/json


{
"taskId": 123,
"isClearData": true
}



发现 isClearData 的值是 false


isClearData是false

此刻,疑问从我的额头转移到了他的额头上,他懵逼了,我轻松了。为了功能能够正常交付,我还是决定看下这个问题,没有了心理压力,也许更容易发现问题所在。第一眼看到 isClearData,我就隐约觉得有问题,所以我决定仔细看下 SyncTaskDTO 这个类,发现 isClearDatasettergetter 方法有点不一样


private Boolean isClearData = false;

public Boolean getClearData() {
return isClearData;
}

public void setClearData(Boolean clearData) {
isClearData = clearData;
}

方法名是不是少了 Is?带着这个疑问我找到了同事,问他 settergetter 为什么要这么命名?他说是 idea 工具自动生成的(也就是我们平时用到的idea自动生成setter、getter方法的功能)


idea_setter-getter

我让他把 Is 补上试试


private Boolean isClearData = false;

public Boolean getIsClearData() {
return isClearData;
}

public void setIsClearData(Boolean isClearData) {
this.isClearData = isClearData;
}

发现传值正常了,他回过头看着我,我看着他,两人同时提问



他:为什么加了 Is 就可以了?


我:布尔类型的变量,你为什么要加 is 前缀?



问题延申


作为一个严谨的开发,不只是要知其然,更要知其所以然;关于



为什么加了 Is 就可以了



这个问题,我们肯定是要会上一会的;会这个问题之前,我们先来捋一下参数的流转,因为是基于 Spring MVC 实现的 Web 应用,所以我们可以这么问 deepseek



Spring MVC 是如何将前端参数转换成POJO的



能够查到如下重点信息


springmvc参数转换

RequestResponseBodyMethodProcessorresolveArgument


/**
* Throws MethodArgumentNotValidException if validation fails.
* @throws HttpMessageNotReadableException if {@link RequestBody#required()}
* is {@code true} and there is no body content or if there is no suitable
* converter to read the content with.
*/

@Override
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory)
throws Exception {

parameter = parameter.nestedIfOptional();
Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
String name = Conventions.getVariableNameForParameter(parameter);

if (binderFactory != null) {
WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
if (arg != null) {
validateIfApplicable(binder, parameter);
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
}
}
if (mavContainer != null) {
mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
}
}

return adaptArgumentIfNecessary(arg, parameter);
}

正是解析参数的地方,我们打个断点,再发起一次 http 请求


断点调试

很明显,readWithMessageConverters 是处理并转换参数的地方,继续跟进去会来到 MappingJackson2HttpMessageConverterreadJavaType 方法


jackson绑定参数

此刻我们可以得到,是通过 jackson 完成数据绑定与数据转换的。继续跟进,会看到 isClearData 的赋值过程


set反射设值

通过前端传过来的参数 isClearData 找对应的 setter方法是 setIsClearData,而非 setClearData,所以问题



为什么加了 Is 就可以了



是不是就清楚了?


问题解决



  1. 按上述方式调整 isClearDatasettergetter 方法


    带上 is


    public Boolean getIsClearData() {
    return isClearData;
    }

    public void setIsClearData(Boolean isClearData) {
    this.isClearData = isClearData;
    }


  2. 布尔类型的变量,不用 is 前缀


    可以用 if 前缀


    private Boolean ifClearData = false;

    public Boolean getIfClearData() {
    return ifClearData;
    }

    public void setIfClearData(Boolean ifClearData) {
    this.ifClearData = ifClearData;
    }


  3. 可以结合 @JsonProperty 来处理


    @JsonProperty("isClearData")
    private Boolean isClearData = false;



总结



  1. Spring MVC 对参数的绑定与转换,内容不同,采用的处理器也不同



    1. form表单数据(application/x-www-form-urlencoded)


      处理器:ServletModelAttributeMethodProcessor


    2. JSON 数据 (application/json)


      处理器:RequestResponseBodyMethodProcessor
      转换器:MappingJackson2HttpMessageConverter


    3. 多部分文件 (multipart/form-data)


      处理器:MultipartResolver




  2. POJO 的布尔类型变量,不要加 is 前缀


    命名不符合规范,集成第三方框架的时候就很容易出不好排查的问题



    成不了规范的制定者,那就老老实实遵循规范!





作者:青石路
来源:juejin.cn/post/7521642915278422070
收起阅读 »

这5种规则引擎,真香!

前言 核心痛点:业务规则高频变更与系统稳定性之间的矛盾 想象一个电商促销场景: // 传统硬编码方式(噩梦开始...) public BigDecimal calculateDiscount(Order order) { BigDecimal disc...
继续阅读 »

前言


核心痛点:业务规则高频变更与系统稳定性之间的矛盾


想象一个电商促销场景:


// 传统硬编码方式(噩梦开始...)
public BigDecimal calculateDiscount(Order order) {
BigDecimal discount = BigDecimal.ZERO;

if (order.getTotalAmount().compareTo(new BigDecimal("100")) >= 0) {
discount = discount.add(new BigDecimal("10"));
}

if (order.getUser().isVip()) {
discount = discount.add(new BigDecimal("5"));
}

// 更多if-else嵌套...
return discount;
}

当规则变成:"非VIP用户满200减30,VIP用户满150减40,且周二全场额外95折"时,代码将陷入维护地狱!


规则引擎通过分离规则逻辑解决这个问题:



  1. 规则外置存储(数据库/文件)

  2. 支持动态加载

  3. 声明式规则语法

  4. 独立执行环境


下面给大家分享5种常用的规则引擎,希望对你会有所帮助。


最近准备面试的小伙伴,可以看一下这个宝藏网站(Java突击队):www.susan.net.cn,里面:面试八股文、场景题、面试真题、项目实战、工作内推什么都有


1.五大常用规则引擎


1.1 Drools:企业级规则引擎扛把子


官网http://www.drools.org/


适用场景:



  • 金融风控规则(上百条复杂规则)

  • 保险理赔计算

  • 电商促销体系


实战:折扣规则配置


// 规则文件 discount.drl
rule "VIP用户满100减20"
when
$user: User(level == "VIP")
$order: Order(amount > 100)
then
$order.addDiscount(20);
end

Java调用代码:


KieServices kieServices = KieServices.Factory.get();
KieContainer kContainer = kieServices.getKieClasspathContainer();
KieSession kSession = kContainer.newKieSession("discountSession");

kSession.insert(user);
kSession.insert(order);
kSession.fireAllRules();

优点



  • 完整的RETE算法实现

  • 支持复杂的规则网络

  • 完善的监控管理控制台


缺点



  • 学习曲线陡峭

  • 内存消耗较大

  • 需要依赖Kie容器



适合:不差钱的大厂,规则复杂度高的场景



1.2 Easy Rules:轻量级规则引擎之王


官网github.com/j-easy/easy…


适用场景:



  • 参数校验

  • 简单风控规则

  • 审批流引擎


注解式开发:


@Rule(name = "雨天打折规则", description = "下雨天全场9折")
public class RainDiscountRule {

@Condition
public boolean when(@Fact("weather") String weather) {
return "rainy".equals(weather);
}

@Action
public void then(@Fact("order") Order order) {
order.setDiscount(0.9);
}
}

引擎执行:


RulesEngineParameters params = new RulesEngineParameters()
.skipOnFirstAppliedRule(true); // 匹配即停止

RulesEngine engine = new DefaultRulesEngine(params);
engine.fire(rules, facts);

优点



  • 五分钟上手

  • 零第三方依赖

  • 支持规则组合


缺点



  • 不支持复杂规则链

  • 缺少可视化界面



适合:中小项目快速落地,开发人员不足时



1.3 QLExpress:阿里系脚本引擎之光


官网github.com/alibaba/QLE…


适用场景:



  • 动态配置计算逻辑

  • 财务公式计算

  • 营销规则灵活变更


执行动态脚本:


ExpressRunner runner = new ExpressRunner();
DefaultContext<String, Object> context = new DefaultContext<>();
context.put("user", user);
context.put("order", order);

String express = "if (user.level == 'VIP') { order.discount = 0.85; }";
runner.execute(express, context, null, true, false);

高级特性:


// 1. 函数扩展
runner.addFunction("计算税费", new Operator() {
@Override
public Object execute(Object[] list) {
return (Double)list[0] * 0.06;
}
});

// 2. 宏定义
runner.addMacro("是否新用户", "user.regDays < 30");

优点



  • 脚本热更新

  • 语法接近Java

  • 完善的沙箱安全


缺点



  • 调试困难

  • 复杂规则可读性差



适合:需要频繁修改规则的业务(如运营活动)



1.4 Aviator:高性能表达式专家


官网github.com/killme2008/…


适用场景:



  • 实时定价引擎

  • 风控指标计算

  • 大数据字段加工


性能对比(执行10万次):


// Aviator 表达式
Expression exp = AviatorEvaluator.compile("user.age > 18 && order.amount > 100");
exp.execute(map);

// Groovy 脚本
new GroovyShell().evaluate("user.age > 18 && order.amount > 100");

引擎耗时
Aviator220ms
Groovy1850ms

编译优化:


// 开启编译缓存(默认开启)
AviatorEvaluator.getInstance().useLRUExpressionCache(1000);

// 字节码生成模式(JDK8+)
AviatorEvaluator.setOption(Options.ASM, true);

优点



  • 性能碾压同类引擎

  • 支持字节码生成

  • 轻量无依赖


缺点



  • 只支持表达式

  • 不支持流程控制



适合:对性能有极致要求的计算场景



这里有复杂的商城项目实战,使用技术:SpringBoot、Spring Security、MySQL、Mybatis、shardingsphere、Nacos、JWT、ElasticSearch、Redis、RocketMQ、MongoDB、Caffeine、FreeMaker、Redisson、Minio、WebSocket、hanlp、mahout、jsoup、Docker等等,非常值得一看


1.5 LiteFlow:规则编排新物种


官网:liteflow.com/


适用场景:



  • 复杂业务流程

  • 订单状态机

  • 审核工作流


编排示例:


<chain name="orderProcess">
<then value="checkStock,checkCredit"/> <!-- 并行执行 -->
<when value="isVipUser">
<then value="vipDiscount"/>
</when>
<otherwise>
<then value="normalDiscount"/>
</otherwise>
<then value="saveOrder"/>
</chain>

Java调用:


LiteflowResponse response = FlowExecutor.execute2Resp("orderProcess", order, User.class);
if (response.isSuccess()) {
System.out.println("流程执行成功");
} else {
System.out.println("失败原因:" + response.getCause());
}

优点



  • 可视化流程编排

  • 支持异步、并行、条件分支

  • 热更新规则


缺点



  • 新框架文档较少

  • 社区生态待完善



适合:需要灵活编排的复杂业务流



2 五大规则引擎横向评测



性能压测数据(单机1万次执行):


引擎耗时内存占用特点
Drools420ms功能全面
Easy Rules38ms轻量易用
QLExpress65ms阿里系脚本引擎
Aviator28ms极低高性能表达式
LiteFlow120ms流程编排专家

3 如何技术选型?



黄金法则:



  1. 简单场景:EasyRules + Aviator 组合拳

  2. 金融风控:Drools 稳如老狗

  3. 电商运营:QLExpress 灵活应变

  4. 工作流驱动:LiteFlow 未来可期


4 避坑指南



  1. Drools内存溢出


// 设置无状态会话(避免内存积累)
KieSession session = kContainer.newStatelessKieSession();


  1. QLExpress安全漏洞


// 禁用危险方法
runner.addFunctionOfServiceMethod("exit", System.class, "exit", null, null);


  1. 规则冲突检测


// Drools冲突处理策略
KieSessionConfiguration config = KieServices.Factory.get().newKieSessionConfiguration();
config.setProperty("drools.sequential", "true"); // 按顺序执行

总结



  1. 能用:替换if/else(新手村)

  2. 用好:规则热更新+可视化(进阶)

  3. 用精:规则编排+性能优化(大师级)


曾有人问我:“规则引擎会不会让程序员失业?” 我的回答是:“工具永远淘汰不了思考者,只会淘汰手工作坊”


真正的高手,不是写更多代码,而是用更优雅的方式解决问题。



最后送句话:技术选型没有最好的,只有最合适的



最后说一句(求关注,别白嫖我)


如果这篇文章对您有所帮助,或者有所启发的话,帮忙关注一下我的同名公众号:苏三说技术,您的支持是我坚持写作最大的动力。


求一键三连:点赞、转发、在看。


关注公众号:【苏三说技术】,在公众号中回复:进大厂,可以免费获取我最近整理的10万字的面试宝典,好多小伙伴靠这个宝典拿到了多家大厂的offer。


作者:苏三说技术
来源:juejin.cn/post/7517854096175988762
收起阅读 »

用户登录成功后,判断用户在10分钟内有没有操作,无操作自动退出登录怎么实现?

需求详细描述:用户登录成功后,默认带入10min的初始值,可针对该用户进行单独设置,单位:分钟,设置范围:1-15,用户在系统没有操作后满足该时长自动退出登录; 疑问:怎么判断用户在10分钟内有没有操作? 实现步骤 ✅ 一、功能点描述: 默认超时时间,登录...
继续阅读 »

需求详细描述:用户登录成功后,默认带入10min的初始值,可针对该用户进行单独设置,单位:分钟,设置范围:1-15,用户在系统没有操作后满足该时长自动退出登录;

疑问:怎么判断用户在10分钟内有没有操作?


实现步骤


✅ 一、功能点描述:
默认超时时间,登录后默认为 10 分钟,
支持自定义设置 用户可修改自己的超时时间(1~15 分钟)
自动登出逻辑 用户在设定时间内没有“操作”,就触发登出.

✅ 二、关键问题:如何判断用户是否操作了?

🔍 操作的定义:
这里的“操作”可以理解为任何与页面交互的行为,
例如:
点击按钮、
鼠标移动、
键盘输入、
页面滚动、路由变化等。

✅ 三、解决方案:
使用全局事件监听器来检测用户的活跃状态,并重置计时器。

✅ 四、实现思路(Vue3 + Composition API)
我们可以通过以下步骤实现:

1. 定义一个响应式的 inactivityTime 变量(单位:分钟)


const inactivityTime = ref(10); // 默认10分钟

2. 创建一个定时器变量


let logoutTimer = null;

3. 重置定时器函数


function resetTimer() {
if (logoutTimer) {
clearTimeout(logoutTimer);
}

logoutTimer = setTimeout(() => {
console.log('用户已超时,执行登出');
// 这里执行登出操作,如清除 token、跳转到登录页等
store.dispatch('logout'); // 假设你用了 Vuex/Pinia
}, inactivityTime.value * 60 * 1000); // 转换为毫秒
}

4. 监听用户活动并重置定时器


function setupActivityListeners() {
const events = ['click', 'mousemove', 'keydown', 'scroll', 'touchstart'];

events.forEach(event => {
window.addEventListener(event, resetTimer, true);
});
}

function removeActivityListeners() {
const events = ['click', 'mousemove', 'keydown', 'scroll', 'touchstart'];

events.forEach(event => {
window.removeEventListener(event, resetTimer, true);
});
}

5. 在组件挂载时初始化定时器和监听器


<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import { useRouter } from 'vue-router';

const router = useRouter();
const inactivityTime = ref(10); // 默认10分钟
let logoutTimer = null;

function resetTimer() {
if (logoutTimer) {
clearTimeout(logoutTimer);
}

logoutTimer = setTimeout(() => {
alert('由于长时间未操作,您已被自动登出');
localStorage.removeItem('token'); // 清除 token
router.push('/login'); // 跳转到登录页
}, inactivityTime.value * 60 * 1000);
}

function setupActivityListeners() {
const events = ['click', 'mousemove', 'keydown', 'scroll', 'touchstart'];
events.forEach(event => {
window.addEventListener(event, resetTimer, true);
});
}

function removeActivityListeners() {
const events = ['click', 'mousemove', 'keydown', 'scroll', 'touchstart'];
events.forEach(event => {
window.removeEventListener(event, resetTimer, true);
});
}

onMounted(() => {
setupActivityListeners();
resetTimer(); // 初始化定时器
});

onUnmounted(() => {
removeActivityListeners();
if (logoutTimer) clearTimeout(logoutTimer);
});
</script>

✅ 四、支持用户自定义设置(进阶)
你可以通过接口获取用户的个性化超时时间:


// 假设你从接口获取到了用户的设置
api.getUserSettings().then(res => {
const userTimeout = res.autoLogoutTime; // 单位:分钟,假设值为 5-15
if (userTimeout >= 1 && userTimeout <= 15) {
inactivityTime.value = userTimeout;
resetTimer(); // 更新定时器
}
});

✅ 五、完整逻辑流程图(文字版)


image.png


✅ 六、注意事项
不要监听太少的事件,比如只监听 click,可能会漏掉键盘操作,
使用 true 参数添加事件监听器,表示捕获阶段监听,更可靠
多标签页场景,如果用户开了多个标签页,需考虑同步机制(比如使用 LocalStorage)
移动端适配,加入 touchstart 等移动端事件监听,
登出前最好加个提示,比如弹窗让用户选择是否继续会话。

✅ 七、推荐封装方式
你可以把这个逻辑封装成一个 Vue 自定义 Hook,例如 useAutoLogout.js,然后在需要的页面调用它即可。


// useAutoLogout.js
export function useAutoLogout(timeoutMinutes = 10) {
let timer = null;

function resetTimer() {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
alert('由于长时间未操作,您已被自动登出');
localStorage.removeItem('token');
window.location.href = '/login';
}, timeoutMinutes * 60 * 1000);
}

function setupListeners() {
const events = ['click', 'mousemove', 'keydown', 'scroll', 'touchstart'];
events.forEach(event => {
window.addEventListener(event, resetTimer, true);
});
}

function removeListeners() {
const events = ['click', 'mousemove', 'keydown', 'scroll', 'touchstart'];
events.forEach(event => {
window.removeEventListener(event, resetTimer, true);
});
}

onMounted(() => {
setupListeners();
resetTimer();
});

onUnmounted(() => {
removeListeners();
if (timer) clearTimeout(timer);
});
}

然后在组件中:


import { useAutoLogout } from '@/hooks/useAutoLogout'

export default {
setup() {
useAutoLogout(10); // 设置默认10分钟
}
}

✅ 八、总结:

实现方式:
判断用户是否有操作,监听 click、 mousemove、 keydown 等事件,
自动登出设置定时器,在无操作后触发,
用户自定义超时时间,接口获取后动态设置定时器时间,
页面间复用,封装为 Vue 自定义 Hook 更好维护。


使用优化


如果把它封装成一个自定义 Hook(如 useAutoLogout),这种写法确实需要在每个需要用到自动登出功能的页面里手动引入并调用它,麻烦且不优雅,不适合大型项目。


✅ 一、进阶方案:通过路由守卫自动注入
你可以利用 Vue Router 的 beforeEach 钩子,在用户进入页面时自动触发 useAutoLogout。
步骤如下:



  1. 创建一个可复用的方法(比如放到 utils.js 或 autoLogout.js 中)


// src/utils/autoLogout.js
import { useAutoLogout } from '@/hooks/useAutoLogout'

export function enableAutoLogout(timeout = 10) {
useAutoLogout(timeout)
}

2. 在路由配置中使用 meta 标记是否启用自动登出


// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router';
import { useAutoLogout } from '@/hooks/useAutoLogout';
import store from './store'; // 假设你有一个 Vuex 或 Pinia 状态管理库用于保存用户设置

const routes = [
{
path: '/dashboard',
name: 'Dashboard',
component: () => import('@/views/Dashboard.vue'),
meta: { autoLogout: true } // 表示这个页面需要自动登出功能
},
{
path: '/login',
name: 'Login',
component: () => import('@/views/Login.vue')
// 不加 meta.autoLogout 表示不启用
}
];

const router = createRouter({
history: createWebHistory(),
routes,
});

router.beforeEach(async (to, from, next) => {
if (to.meta.autoLogout) {
// 获取用户的自定义超时时间
let timeout = 10; // 默认值
try {
// 这里假设从后端获取用户的自定义超时时间
const userSettings = await store.dispatch('fetchUserSettings'); // 根据实际情况调整
timeout = userSettings.autoLogoutTime || timeout;
} catch (error) {
console.error("Failed to fetch user settings:", error);
}

// 使用自定义超时时间初始化或重置计时器
const resetTimer = useAutoLogout(timeout);
resetTimer(); // 初始设置计时器
}
next();
});

export default router;

⚠️ 注意事项:



  • 组件实例
    Vue 3 Composition API 中,不能直接在 beforeEach 中访问组件实例,需要把 enableAutoLogout 改为在组件内部调用,或者结合 Vuex/Pinia 做状态管理。

  • 状态管理: 如果用户可以在应用运行期间更改其自动登出时间设置,你需要一种机制来实时更新这些设置。这通常涉及到状态管理库(如Vuex/Pinia)以及与后端同步用户偏好设置。

  • 避免重复监听事件: 在每次导航时都添加新的事件监听器会导致内存泄漏。上述代码通过在组件卸载时移除监听器解决了这个问题,但如果你选择其他方式实现,请确保也处理了这一点。

  • 用户体验: 在实际应用中,最好在即将登出前给用户提示,让用户有机会延长会话。


✅ 三、终极方案:创建一个全局插件(最优雅)
你可以把这个逻辑封装成一个 Vue 插件,这样只需要一次引入,就能全局生效。


示例:创建一个插件文件 autoLogoutPlugin.js


// src/plugins/autoLogoutPlugin.js
import { useAutoLogout } from '@/hooks/useAutoLogout'

export default {
install: (app, options = {}) => {
const timeout = options.timeout || 10

app.mixin({
setup() {
useAutoLogout(timeout)
}
})
}
}

使用插件:


// main.js
import AutoLogoutPlugin from './plugins/autoLogoutPlugin'

const app = createApp(App)

app.use(AutoLogoutPlugin, { timeout: 10 }) // 设置默认超时时间

app.mount('#app')

✅ 这样做之后,所有页面都会自动应用 useAutoLogout,无需手动导入。


插件使用解释



  • ✅ export default 是一个 Vue 插件对象,必须包含 install 方法
    Vue 插件是一个对象,它提供了一个 install(app, options) 方法。这个方法会在你调用 app.use(Plugin) 的时候执行。

  • ✅ install: (app, options = {}) => { ... }
    app: 是你的 Vue 应用实例(也就是通过 createApp(App) 创建的那个)
    options: 是你在调用 app.use(AutoLogoutPlugin, { timeout: 10 }) 时传入的配置项
    所以你可以在这里拿到你设置的超时时间 { timeout: 10 }。

  • ✅ const timeout = options.timeout || 10
    这是一个默认值逻辑:如果用户传了 timeout,就使用用户的;
    否则使用默认值 10 分钟。

  • ✅ app.mixin({ ... })
    这是关键部分!



    • 💡 什么是 mixin?
      mixin 是 Vue 中的“混入”,可以理解为:向所有组件中注入一些公共的逻辑或配置。

    • 举个例子:如果你有一个功能要在每个页面都启用,比如日志记录、权限检查、自动登出等,就可以用 mixin 实现一次写好,到处生效。

    • ✅ setup() 中调用 useAutoLogout(timeout)
      每个组件在创建时都会执行一次 setup() 函数。
      在这里调用 useAutoLogout(timeout),相当于:
      在每一个页面组件中都自动调用了 useAutoLogout(10)
      也就是说,自动注册了监听器 + 自动设置了计时器



  • 为什么这样就能全局监听用户操作?因为你在每个组件中都执行了 useAutoLogout(timeout),而这个函数内部做了以下几件事:


function useAutoLogout(timeout) {
// 设置定时器
// 添加事件监听器(点击、移动鼠标、键盘输入等)
// 组件卸载时清除监听器和定时器
}

因此,只要某个组件被加载,就会自动启动自动登出机制;组件卸载后,又会自动清理资源,避免内存泄漏。


总结一下整个流程

1️⃣ 在 main.js 中调用 app.use(AutoLogoutPlugin, { timeout: 10 })

2️⃣ 插件的 install() 被执行,获取到 timeout 值

3️⃣ 使用 app.mixin() 向所有组件中注入一段逻辑

4️⃣ 每个组件在 setup() 阶段自动调用 useAutoLogout(timeout)

5️⃣ 每个组件都注册了全局事件监听器,并设置了登出定时器
✅ 这样一来,所有组件页面都拥有了自动登出功能,不需要你手动去每个页面加代码。


注意事项

❗ 不是所有页面都需要自动登出 比如登录页、错误页可能不需要。可以在 mixin 中加判断,例如:根据路由或 meta 字段过滤
⚠️ 性能问题? 不会有明显影响,因为只添加了一次监听器,且组件卸载时会清理
🔄 登录后如何动态更新超时时间? 可以结合 Vuex/Pinia,在 store 改变时重新调用 useAutoLogout(newTimeout)
🧪 测试建议 手动测试几种情况:
• 页面切换是否重置计时
• 用户操作是否刷新倒计时
• 超时后是否跳转登录页

进阶建议:支持按需开启(可选)
如果你想只在某些页面启用自动登出功能,而不是全局启用,也可以这样改写:


app.mixin({
setup() {
// 判断当前组件是否启用了 autoLogout
const route = useRoute()
if (route.meta.autoLogout !== false) {
useAutoLogout(timeout)
}
}
})

然后在路由配置中:


{
path: '/dashboard',
name: 'Dashboard',
component: () => import('@/views/Dashboard.vue'),
meta: { autoLogout: true }
}

最终效果你只需要在 main.js 中引入插件并配置一次:
app.use(AutoLogoutPlugin, { timeout: 10 })
就能让整个项目中的所有页面都拥有自动登出功能,无需在每个页面单独导入和调用。


✅ 四、总结对比


🟢 大型项目、统一行为控制,所有页面都启用自动登出 ➜ 推荐使用 插件方式
🟡 中型项目、统一管理页面行为,只在某些页面启用 ➜ 推荐使用 路由守卫 + meta
🔴 小型项目、部分页面控制,只在个别页面启用 ➜ 继续使用 手动调用

作者:一只猫猫熊
来源:juejin.cn/post/7510044998433030180
收起阅读 »

如何优雅的防止按钮重复点击

1. 业务背景 在前端的业务场景中:点击按钮,发起请求。在请求还未结束的时候,一个按钮可以重复点击,导致接口重新请求多次(如果后端不做限制)。轻则浪费服务器资源,重则业务逻辑错误,尤其是入库操作。 传统解决方案:使用防抖函数,但是无法解决接口响应时间过长的问题...
继续阅读 »

1. 业务背景


在前端的业务场景中:点击按钮,发起请求。在请求还未结束的时候,一个按钮可以重复点击,导致接口重新请求多次(如果后端不做限制)。轻则浪费服务器资源,重则业务逻辑错误,尤其是入库操作。


传统解决方案:使用防抖函数,但是无法解决接口响应时间过长的问题,当接口一旦响应时间超过防抖时间,测试单身20年的手速照样还是可以点击多次。


更稳妥的方式:给button添加loadng,只有接口响应结果后才能再次点击按钮。需要在每个使用按钮的页面逻辑中单独维护loading变量,代码变得臃肿。


那如果是在react项目中,这种问题有没有比较优雅的解决方式呢?



vue项目解决方案参考:juejin.cn/post/749541…



2. useAsyncButton


在 React 项目中,对于这种按钮重复点击的问题,可以使用自定义 Hook 来优雅地处理。以下是一个完整的解决方案:



  1. 首先创建一个自定义 Hook useAsyncButton


import { useState, useCallback } from 'react';

interface RequestOptions {
onSuccess?: (data: any) => void;
onError?: (error: any) => void;
}

export function useAsyncButton<T>(
requestFn: (...args: any[]) => Promise<T>,
options: RequestOptions = {}
) {
const [loading, setLoading] = useState(false);

const run = useCallback(
async (...args: any[]) => {
if (loading) return; // 如果正在加载,直接返回

try {
setLoading(true);
const data = await requestFn(...args);
options.onSuccess?.(data);
return data;
} catch (error) {
options.onError?.(error);
throw error;
} finally {
setLoading(false);
}
},
[loading, requestFn, options]
);

return {
loading,
run
};
}


  1. 在组件中使用这个 Hook:


import { useAsyncButton } from '../hooks/useAsyncButton';

const MyButton = () => {
const { loading, run } = useAsyncButton(async () => {
// 这里是你的接口请求
const response = await fetch('your-api-endpoint');
const data = await response.json();
return data;
}, {
onSuccess: (data) => {
console.log('请求成功:', data);
},
onError: (error) => {
console.error('请求失败:', error);
}
});

return (
<button
onClick={() =>
run()}
disabled={loading}
>
{loading ? '加载中...' : '点击请求'}
</button>

);
};

export default MyButton;

这个解决方案有以下优点:



  1. 统一管理:将请求状态管理逻辑封装在一个 Hook 中,避免重复代码

  2. 自动处理 loading:不需要手动管理 loading 状态

  3. 防重复点击:在请求过程中自动禁用按钮或阻止重复请求

  4. 类型安全:使用 TypeScript 提供类型检查

  5. 灵活性:可以通过 options 配置成功/失败的回调函数

  6. 可复用性:可以在任何组件中重用这个 Hook



useAsyncButton直接帮你进行了try catch,你不用再单独去做异常处理。



是不是很简单?有的人可能有疑问了,为什么下方不就能拿到接口请求以后的数据吗?为什么还需要onSuccess呢?


async () => {
// 这里是你的接口请求
const response = await fetch('your-api-endpoint');
const data = await response.json();
return data;
}

3. onSuccess


确实我们可以直接在调用 run() 后通过 .then()await 来获取数据。提供 onSuccess 回调主要有以下几个原因:



  1. 关注点分离


// 不使用 onSuccess
const { run } = useAsyncButton(async () => {
const response = await fetch('/api/data');
return response.json();
});

const handleClick = async () => {
const data = await run();
// 处理数据的逻辑和请求逻辑混在一起
setData(data);
message.success('请求成功');
doSomethingElse(data);
};

// 使用 onSuccess
const { run } = useAsyncButton(async () => {
const response = await fetch('/api/data');
return response.json();
}, {
onSuccess: (data) => {
// 数据处理逻辑被清晰地分离出来
setData(data);
message.success('请求成功');
doSomethingElse(data);
}
});

const handleClick = () => {
run(); // 更清晰的调用方式
};


  1. 统一错误处理


// 不使用 callbacks
const handleClick = async () => {
try {
const data = await run();
setData(data);
} catch (error) {
// 每个地方都需要写错误处理
message.error('请求失败');
}
};

// 使用 callbacks
const { run } = useAsyncButton(fetchData, {
onSuccess: (data) => setData(data),
onError: (error) => message.error('请求失败')
// 错误处理被集中管理
});


  1. 自动重试场景


const { run } = useAsyncButton(fetchData, {
onSuccess: (data) => setData(data),
onError: (error) => {
if (retryCount < 3) {
retryCount++;
run(); // 可以在失败时自动重试
}
}
});


  1. 状态联动


const { run } = useAsyncButton(fetchData, {
onSuccess: (data) => {
setData(data);
// 可能需要触发其他请求
refetchRelatedData();
// 或更新其他状态
setOtherState(true);
}
});

所以,虽然你完全可以不使用 onSuccess 回调,但它能帮助你:



  • 更好地组织代码结构

  • 统一管理成功/失败处理逻辑

  • 方便进行状态联动

  • 在需要扩展功能时更加灵活


选择使用与否取决于你的具体需求,如果是简单的场景,直接使用 await run() 也完全可以。


4. 禁止一段时间内点击


评论区有人说了,我要是想在某一段时间内防止重复点击怎么整?


我们可以扩展 useAsyncButton 的功能,添加一个防冷却时间(cooldown)的特性。这在一些特定场景下很有用,比如发送验证码按钮需要等待 60 秒才能再次点击:


import { useState, useCallback, useRef } from 'react';

interface AsyncButtonOptions {
onSuccess?: (data: any) => void;
onError?: (error: any) => void;
cooldown?: number; // 冷却时间(毫秒)
}

export function useAsyncButton<T>(
requestFn: (...args: any[]) => Promise<T>,
options: AsyncButtonOptions = {}
) {
const [loading, setLoading] = useState(false);
const [cooldownRemaining, setCooldownRemaining] = useState(0);
const timerRef = useRef<NodeJS.Timeout>();

const startCooldown = useCallback(() => {
if (!options.cooldown) return;

setCooldownRemaining(options.cooldown / 1000);
const startTime = Date.now();

timerRef.current = setInterval(() => {
const elapsed = Date.now() - startTime;
const remaining = Math.ceil((options.cooldown! - elapsed) / 1000);

if (remaining <= 0) {
clearInterval(timerRef.current);
setCooldownRemaining(0);
} else {
setCooldownRemaining(remaining);
}
}, 1000);
}, [options.cooldown]);

const run = useCallback(
async (...args: any[]) => {
if (loading || cooldownRemaining > 0) return;

try {
setLoading(true);
const data = await requestFn(...args);
options.onSuccess?.(data);
startCooldown();
return data;
} catch (error) {
options.onError?.(error);
throw error;
} finally {
setLoading(false);
}
},
[loading, cooldownRemaining, requestFn, options, startCooldown]
);

return {
loading,
cooldownRemaining,
run,
disabled: loading || cooldownRemaining > 0
};
}

使用示例:


import { useAsyncButton } from '../hooks/useAsyncButton';

const SendCodeButton = () => {
const { loading, cooldownRemaining, disabled, run } = useAsyncButton(
async () => {
// 发送验证码的接口请求
const response = await fetch('/api/send-code');
return response.json();
},
{
cooldown: 60000, // 60秒冷却时间
onSuccess: () => {
console.log('验证码发送成功');
},
onError: (error) => {
console.error('验证码发送失败', error);
}
}
);

return (
<button
onClick={() =>
run()}
disabled={disabled}
>
{loading ? '发送中...' :
cooldownRemaining > 0 ? `${cooldownRemaining}秒后重试` :
'发送验证码'}
</button>

);
};

export default SendCodeButton;

作者:白哥学前端
来源:juejin.cn/post/7498646341460787211
收起阅读 »

为什么说不可信的Wi-Fi不要随便连接?

新闻中一直倡导的“不可信的Wi-Fi不要随便连接”,一直不知道里面的风险,即使是相关专业的从业人员,惭愧。探索后发现这里面的安全知识不少,记录下: 简单来说: 当用户连接上攻击者创建的伪造热点或者不可信的wifi后,实际上就进入了一个高度不可信的网络环境,攻击...
继续阅读 »

新闻中一直倡导的“不可信的Wi-Fi不要随便连接”,一直不知道里面的风险,即使是相关专业的从业人员,惭愧。探索后发现这里面的安全知识不少,记录下:


简单来说:


当用户连接上攻击者创建的伪造热点或者不可信的wifi后,实际上就进入了一个高度不可信的网络环境,攻击者可以进行各种信息窃取、欺骗和控制操作。


主要风险有:




🚨 1.中间人攻击(MITM)


攻击者拦截并转发你与网站服务器之间的数据,做到“你以为你连的是官网,其实中间有人”。



  • 可窃取账号密码、聊天记录、信用卡信息

  • 可篡改网页内容,引导你下载恶意应用


如果你和“正确”的网站之间是https,那么信息不会泄露,TLS能保证通信过程的安全,前提是你连接的这个https网站是“正确”的。正确的含义是:不是某些人恶意伪造的,不是一些不法份子通过DNS欺骗来重定向到的。




🪤 2.DNS欺骗 / 重定向


攻击者控制DNS,将合法网址解析到伪造网站。



  • 你访问的“http://www.bank.com” 其实是假的银行网站,DNS域名解析到恶意服务上,返回和银行一样的登录的界面,这样用户输入账号密码就被窃取到了。

  • 输入的账号密码被记录,后端没收到任何请求


这里多说一句:目前的登录方式中,采用短信验证码的方式,能避免真实的密码被窃取的风险,尽量用这种登录方式。




📥 3.强制HTTP连接,篡改内容


即使你访问的是HTTPS网站,攻击者可以强制降级为HTTP或注入恶意代码:



  • 注入广告、木马脚本

  • 启动钓鱼表单页面骗你输入账号密码


攻击者操作流程:



  • 用户访问 http://example.com(明文)

  • 攻击者拦截请求,阻止它跳转到 HTTPS

  • 返回伪造页面(比如仿登录页面),引导用户输入账号密码

  • 用户完全不知道自己并未进入 HTTPS 页面


这里“降级”的意思是,虽然你访问的是http网站,网站正常会转为https的访问方式,但是被阻止了,一直使用的是http协议访问,能实现这种降级的前提有两个:



  • 用户没有直接输入 https://baidu.com, 而是输入的http://baidu.com, 依赖浏览器自动跳转

  • 访问的网站没有开启 HSTS(HTTP Strict Transport Security)


搭建安全的网站的启示:



  • 网站访问用https,并且如果用户访问HTTP网站时被自动转到 HTTPS 网站

  • 网站要启用HSTS


HSTS 是一种告诉浏览器“以后永远都不要使用 HTTP 访问我”的机制。


如何开启HSTS?
添加响应头(核心方式)
。在你的网站服务端(如 Nginx、Apache、Spring Boot、Express 等)添加以下 HTTP 响应头:


Strict-Transport-Security: max-age=31536000; includeSubDomains; preload

各参数含义如下:


参数含义
max-age=31536000浏览器记住 HSTS 状态的时间(单位:秒,31536000 秒 = 1 年)
includeSubDomains所有子域名也强制使用 HTTPS(推荐)
preload提交到浏览器 HSTS 预加载列表(详见下文)

网站实现http访问转为了https访问:


1 网站服务器配置了自动重定向(HTTP to HTTPS)



  • 这是最常见的做法。网站后台(如 Nginx、Apache、Tomcat 等)配置了规则,凡是 HTTP 请求都会返回 301/302 重定向到 HTTPS 地址。

  • 目的是强制用户用加密的 HTTPS 访问,保障数据安全。


2 请求的http response中加入HSTS机制



  • 网站通过 HTTPS 响应头发送了 HSTS 指令。

  • 浏览器收到后会记住该网站在一定时间内只能用 HTTPS 访问。

  • 即使你输入 http://,浏览器也会自动用 https:// 访问,且不会发送 HTTP 请求。




📁 4.会话劫持(Session Hijacking)


如果你已登录某个网站(如微博/邮箱),攻击者可以窃取你与服务端之间的 Session Cookie,无需密码即可“冒充你”。


搭建web服务对于cookie泄密的安全启示:


1、开启 Cookie 的 Secure 和 HttpOnly


当一个 Cookie 设置了 Secure 标志后,它只会在 HTTPS 加密连接中发送,不会通过 HTTP 明文连接发送。


设置 HttpOnly 后,JavaScript 无法通过 document.cookie 访问该 Cookie,它只能被浏览器在请求时自动带上。如果站点存在跨站脚本漏洞(XSS),攻击者注入的 JS 可以读取用户的 Cookie。设置了 HttpOnly 后,即便 JS 被执行,也无法读取该 Cookie。


2、配合设置 SameSite=Strict 或 Lax 可进一步防止 CSRF 攻击。


Set-Cookie: sessionid=abc123; SameSite=Strict; Secure; HttpOnly

CSRF(Cross-Site Request Forgery) 攻击的原理示意


操作说明
用户登录 bank.com,浏览器存有 bank.com 的 CookieCookie 设置为非 HttpOnly 且未限制 SameSite 或 设置为 SameSite=Lax,正常携带
攻击网站 attacker.com,诱导用户访问 <img src="https://bank.com/transfer?to=attacker&amount=1000">这个请求是向 bank.com 发送的跨域请求
浏览器自动带上 bank.com 的 Cookie因为请求的目标是 bank.com,Cookie 会被自动携带
bank.com 服务器收到请求,认为是用户本人操作,执行转账服务器无法区分这个请求是不是用户主动发起的

重点是:你在浏览器中访问A网站,浏览器中存储A的cookie,此时你访问恶意的B网站,B网站向A网站发送请求,浏览器一般默认带上A网站的cookie,因此,相当于B网站恶意使用了你在A网站的身份,完成了攻击,比如获取信息,比如添加东西。设置SameSite=Strict能防止跨站伪造攻击,对A网站的请求只能在A网站下发送,在B网站发起对A网站请求的无法使用A的cookie


同源策略和Cookie的关系:
同源策略限制的是脚本访问另一个域的内容(比如 JS 不能读取别的网站 Cookie 或响应数据),但浏览器发送请求时,会自动携带目标域对应的 Cookie(只要该 Cookie 未被 SameSite 限制)。 也就是说,请求可以跨域发送,Cookie 也会随请求自动发送,但脚本无法读取响应。在没有设置SameSite时,B网站是可以直接往A网站发送请求并附带上A网站的cookie的。


关于SameSite三种取值详解:


说明是否防CSRF是否影响用户体验
Strict最严格:完全阻止第三方请求携带 Cookie(即使用户点击链接跳转也不带)✅ 完全防止❗️可能影响登录态保持等
Lax较宽松:阻止大多数第三方请求,但允许用户主动导航(点击链接)时携带 Cookie✅ 可防大部分场景✅ 用户体验良好
不限制跨站请求,所有请求都携带 Cookie❌ 不防CSRF⚠️ 必须配合 Secure 使用



🛡 SameSite使用建议(最佳实践)


场景建议配置
登录态/session CookieSameSite=Lax; Secure; HttpOnly ✅ 实用且安全
高安全需求(如金融后台)SameSite=Strict; Secure; HttpOnly ✅ 更强安全性
跨域 OAuth / 第三方登录等SameSite=; Secure ⚠️ 必须使用 HTTPS,否则被浏览器拒绝



🧬 5.恶意软件传播


伪造热点可提供假的软件下载链接、更新提示等方式传播病毒或木马程序。




📡 6.网络钓鱼 + 社会工程攻击


攻击者可能弹出“需登录使用Wi-Fi”的界面,其实是钓鱼网站:



  • 模拟常见的Wi-Fi登录界面(如酒店/机场门户)

  • 用户一旦输入账号、手机号、验证码等敏感信息就被窃取




🔎 7.MAC地址、设备指纹收集


哪怕你没主动上网,连接伪热点后,攻击者也可能收集:



  • 你的设备MAC地址、品牌型号

  • 操作系统、语言、浏览器等指纹信息

  • 用于后续追踪、精准广告投放,甚至诈骗定位




✅ 如何防范被伪热点攻击?


措施说明
关闭“自动连接开放Wi-Fi”阻止设备自动连接伪热点
避免输入账号密码、支付信息尤其在陌生Wi-Fi环境下
使用 VPN建立安全通道防止数据被截取
留意HTTPS证书异常浏览器地址栏变红或提示“不安全”要立刻断开连接
使用手机流量热点相对更可控安全
安装安全软件检测钓鱼网站和网络攻击行为



作者:星夜晚晚
来源:juejin.cn/post/7517468634194362387
收起阅读 »

瞧瞧别人家的判空,那叫一个优雅!

大家好,我是苏三,又跟大家见面了。 一、传统判空的血泪史 某互联网金融平台因费用计算层级的空指针异常,导致凌晨产生9800笔错误交易。 DEBUG日志显示问题出现在如下代码段: // 错误示例 BigDecimal amount = user.getWalle...
继续阅读 »

大家好,我是苏三,又跟大家见面了。


一、传统判空的血泪史


某互联网金融平台因费用计算层级的空指针异常,导致凌晨产生9800笔错误交易。


DEBUG日志显示问题出现在如下代码段:


// 错误示例
BigDecimal amount = user.getWallet().getBalance().add(new BigDecimal("100"));

此类链式调用若中间环节出现null值,必定导致NPE。


初级阶段开发者通常写出多层嵌套式判断:


if(user != null){
    Wallet wallet = user.getWallet();
    if(wallet != null){
        BigDecimal balance = wallet.getBalance();
        if(balance != null){
            // 实际业务逻辑
        }
    }
}

这种写法既不优雅又影响代码可读性。


那么,我们该如何优化呢?


最近准备面试的小伙伴,可以看一下这个宝藏网站:www.susan.net.cn,里面:面试八股文、面试真题、工作内推什么都有


二、Java 8+时代的判空革命


Java8之后,新增了Optional类,它是用来专门判空的。


能够帮你写出更加优雅的代码。


1. Optional黄金三板斧


// 重构后的链式调用
BigDecimal result = Optional.ofNullable(user)
    .map(User::getWallet)
    .map(Wallet::getBalance)
    .map(balance -> balance.add(new BigDecimal("100")))
    .orElse(BigDecimal.ZERO);

高级用法:条件过滤


Optional.ofNullable(user)
    .filter(u -> u.getVipLevel() > 3)
    .ifPresent(u -> sendCoupon(u)); // VIP用户发券

2. Optional抛出业务异常


BigDecimal balance = Optional.ofNullable(user)
    .map(User::getWallet)
    .map(Wallet::getBalance)
    .orElseThrow(() -> new BusinessException("用户钱包数据异常"));

3. 封装通用工具类


public class NullSafe {
    
    // 安全获取对象属性
    public static <T, R> R get(T target, Function<T, R> mapper, R defaultValue) {
        return target != null ? mapper.apply(target) : defaultValue;
    }
    
    // 链式安全操作
    public static <T> T execute(T root, Consumer<T> consumer) {
        if (root != null) {
            consumer.accept(root);
        }
        return root;
    }
}

// 使用示例
NullSafe.execute(user, u -> {
    u.getWallet().charge(new BigDecimal("50"));
    logger.info("用户{}已充值", u.getId());
});

三、现代化框架的判空银弹


4. Spring实战技巧


Spring中自带了一些好用的工具类,比如:CollectionUtils、StringUtils等,可以非常有效的进行判空。


具体代码如下:


// 集合判空工具
List<Order> orders = getPendingOrders();
if (CollectionUtils.isEmpty(orders)) {
    return Result.error("无待处理订单");
}

// 字符串检查
String input = request.getParam("token");
if (StringUtils.hasText(input)) {
    validateToken(input); 
}

5. Lombok保驾护航


我们在日常开发中的entity对象,一般会使用Lombok框架中的注解,来实现getter/setter方法。


其实,这个框架中也提供了@NonNull等判空的注解。


比如:


@Getter
@Setter
public class User {
    @NonNull // 编译时生成null检查代码
    private String name;
    
    private Wallet wallet;
}

// 使用构造时自动判空
User user = new User(@NonNull "张三", wallet);

四、工程级解决方案


6. 空对象模式


public interface Notification {
    void send(String message);
}

// 真实实现
public class EmailNotification implements Notification {
    @Override
    public void send(String message) {
        // 发送邮件逻辑
    }
}

// 空对象实现
public class NullNotification implements Notification {
    @Override
    public void send(String message) {
        // 默认处理
    }
}

// 使用示例
Notification notifier = getNotifier();
notifier.send("系统提醒"); // 无需判空

7. Guava的Optional增强


其实Guava工具包中,给我们提供了Optional增强的功能。


比如:


import com.google.common.base.Optional;

// 创建携带缺省值的Optional
Optional<User> userOpt = Optional.fromNullable(user).or(defaultUser);

// 链式操作配合Function
Optional<BigDecimal> amount = userOpt.transform(u -> u.getWallet())
                                    .transform(w -> w.getBalance());

Guava工具包中的Optional类已经封装好了,我们可以直接使用。


五、防御式编程进阶


8. Assert断言式拦截


其实有些Assert断言类中,已经做好了判空的工作,参数为空则会抛出异常。


这样我们就可以直接调用这个断言类。


例如下面的ValidateUtils类中的requireNonNull方法,由于它内容已经判空了,因此,在其他地方调用requireNonNull方法时,如果为空,则会直接抛异常。


我们在业务代码中,直接调用requireNonNull即可,不用写额外的判空逻辑。


例如:


public class ValidateUtils {
    public static <T> T requireNonNull(T obj, String message) {
        if (obj == null) {
            throw new ServiceException(message);
        }
        return obj;
    }
}

// 使用姿势
User currentUser = ValidateUtils.requireNonNull(
    userDao.findById(userId), 
    "用户不存在-ID:" + userId
);

最近就业形势比较困难,为了感谢各位小伙伴对苏三一直以来的支持,我特地创建了一些工作内推群, 看看能不能帮助到大家。


你可以在群里发布招聘信息,也可以内推工作,也可以在群里投递简历找工作,也可以在群里交流面试或者工作的话题。


添加苏三的私人微信:li_su223,备注:掘金+所在城市,即可加入。


9. 全局AOP拦截


我们在一些特殊的业务场景种,可以通过自定义注解 + 全局AOP拦截器的方式,来实现实体或者字段的判空。


例如:


@Aspect
@Component
public class NullCheckAspect {
    
    @Around("@annotation(com.xxx.NullCheck)")
    public Object checkNull(ProceedingJoinPoint joinPoint) throws Throwable {
        Object[] args = joinPoint.getArgs();
        for (Object arg : args) {
            if (arg == null) {
                throw new IllegalArgumentException("参数不可为空");
            }
        }
        return joinPoint.proceed();
    }
}

// 注解使用
public void updateUser(@NullCheck User user) {
    // 方法实现
}

六、实战场景对比分析


场景1:深层次对象取值


// 旧代码(4层嵌套判断)
if (order != null) {
    User user = order.getUser();
    if (user != null) {
        Address address = user.getAddress();
        if (address != null) {
            String city = address.getCity();
            // 使用city
        }
    }
}

// 重构后(流畅链式)
String city = Optional.ofNullable(order)
    .map(Order::getUser)
    .map(User::getAddress)
    .map(Address::getCity)
    .orElse("未知城市");

场景2:批量数据处理


List<User> users = userService.listUsers();

// 传统写法(显式迭代判断)
List<String> names = new ArrayList<>();
for (User user : users) {
    if (user != null && user.getName() != null) {
        names.add(user.getName());
    }
}

// Stream优化版
List<String> nameList = users.stream()
    .filter(Objects::nonNull)
    .map(User::getName)
    .filter(Objects::nonNull)
    .collect(Collectors.toList());

七、性能与安全的平衡艺术


上面介绍的这些方案都可以使用,但除了代码的可读性之外,我们还需要考虑一下性能因素。


下面列出了上面的几种在CPU消耗、内存只用和代码可读性的对比:


方案CPU消耗内存占用代码可读性适用场景
多层if嵌套★☆☆☆☆简单层级调用
Java Optional★★★★☆中等复杂度业务流
空对象模式★★★★★高频调用的基础服务
AOP全局拦截★★★☆☆接口参数非空验证

黄金法则



  • Web层入口强制参数校验

  • Service层使用Optional链式处理

  • 核心领域模型采用空对象模式


八、扩展技术


除了,上面介绍的常规判空之外,下面再给大家介绍两种扩展的技术。


Kotlin的空安全设计


虽然Java开发者无法直接使用,但可借鉴其设计哲学:


val city = order?.user?.address?.city ?: "default"

JDK 14新特性预览


// 模式匹配语法尝鲜
if (user instanceof User u && u.getName() != null) {
    System.out.println(u.getName().toUpperCase());
}

总之,优雅判空不仅是代码之美,更是生产安全底线。


本文分享了代码判空的10种方案,希望能够帮助你编写出既优雅又健壮的Java代码。


这5个项目,太炸裂了


最后说一句(求关注,别白嫖我)


如果这篇文章对您有所帮助,或者有所启发的话,帮忙关注一下我的同名公众号:苏三说技术,您的支持是我坚持写作最大的动力。


求一键三连:点赞、转发、在看。


关注公众号:【苏三说技术】,在公众号中回复:进大厂,可以免费获取我最近整理的50万字的面试宝典,好多小伙伴靠这个宝典拿到了多家大厂的offer。


作者:苏三说技术
来源:juejin.cn/post/7478221220074504233
收起阅读 »

什么语言最适合用来游戏开发?

什么语言最适合用来游戏开发? 游戏开发,是一项结合了图形渲染、性能优化、系统架构与玩家体验的综合艺术,而“选用什么编程语言”这个问题,往往是新手开发者迈入这片领域时面临的第一个技术岔路口。 一、从需求出发:游戏开发对语言的核心要求 在选择语言之前,我们先明确...
继续阅读 »

什么语言最适合用来游戏开发?


游戏开发,是一项结合了图形渲染、性能优化、系统架构与玩家体验的综合艺术,而“选用什么编程语言”这个问题,往往是新手开发者迈入这片领域时面临的第一个技术岔路口。




一、从需求出发:游戏开发对语言的核心要求


在选择语言之前,我们先明确一点:游戏类型不同,对语言的要求也大不一样。开发 3D AAA 大作和做一个像素风的休闲小游戏,使用的语言和引擎可能完全不同。


一般来说,语言选择需要考虑:


维度说明
性能需求是否要求极致性能(如大型 3D 游戏)?
跨平台能力是否要支持多个平台(Windows/Mac/Linux/iOS/Android/主机)?
引擎生态是否依赖成熟的游戏引擎(如 Unity、Unreal)?
开发效率团队大小如何?语言是否有丰富工具链、IDE 支持、调试便利性?
学习曲线是个人项目还是商业项目?是否有足够时间去掌握复杂语法或底层结构?



二、主流语言实战解析


C++:3A最常用的语言



  • 适合场景:大型 3D 游戏、主机平台、UE(Unreal Engine)项目

  • 特点



    • 几乎所有主流游戏引擎底层都是用 C++ 编写的(UE4/5、CryEngine 等)

    • 手动内存管理带来极致性能控制,但也带来更高的 bug 风险

    • 编译时间长、语法复杂,不适合快速原型开发




如果你追求的是性能边界、需要对引擎源码进行改造,或者准备进入 3A 游戏开发领域,C++ 是必修课。


C#:Unity 的生态核心



  • 适合场景:中小型游戏、独立游戏、跨平台移动/PC 游戏、Unity 项目

  • 特点



    • Unity 的脚本语言就是 C#,生态丰富、社区活跃、教程资源丰富

    • 开发效率高,语法现代,有良好的 IDE 支持(VS、Rider)

    • 在性能上不如 C++,但对大多数项目而言“够用”




如果你是个人开发者或小团队,C# + Unity 几乎是性价比最高的方案之一。


JavaScript/TypeScript:Web 游戏与轻量跨平台



  • 适合场景:H5 游戏、小程序游戏、跨平台 2D 游戏、快速迭代

  • 特点



    • 配合 Phaser、PixiJS、Cocos Creator 等框架,可以高效制作 Web 游戏

    • 原生支持浏览器平台,无需安装,天然适合传播

    • 性能不及原生语言,但足以支撑休闲游戏




Web 平台的红利尚未过去,JS/TS + WebGL 仍然是轻量化游戏开发的稳定选择。


Python/Lua:脚本语言发力



  • 适合场景:游戏逻辑脚本、AI 行为树、数据驱动配置、教学引擎

  • 特点



    • 并不适合用来开发整款游戏,但常作为内嵌脚本语言

    • Lua 广泛用于游戏脚本(如 WOW、GTA、Roblox),轻量、运行效率高

    • Python 适合教学、原型设计、AI 模块等场景




他们更多是游戏开发的一环,而非“用来开发整款游戏”的首选语言。




三、主流引擎使用的主语言和适用语言


游戏引擎主语言适用语言
Unreal EngineC++C++ / Blueprint(可视化脚本)
UnityC#C#
GodotGDScriptGDScript / C# / C++ / Python(部分支持)
Cocos CreatorTypeScript/JSTypeScript / JavaScript
PhaserJavaScriptJavaScript / TypeScript



四、总结:如何选对“你的语言”?



语言没有好坏,只有适不适合你的项目定位与资源情况。



如果你是:



  • 学习引擎开发/大作性能优化:优先掌握 C++,结合 Unreal 学习

  • 做跨平台独立游戏/商业项目:优先 C# + Unity

  • 做 Web 平台轻量游戏:TypeScript + Phaser/Cocos 是好选择

  • 研究 AI、教学、逻辑脚本:Python/Lua 脚本语言


写游戏不是目的,做出好玩的游戏才是!




如果你打算正式进军游戏开发领域,不妨从一个引擎 + 一门主语言开始,结合一个小项目落地,再去拓展更多语言和引擎的协作模式。


作者:Jooolin
来源:juejin.cn/post/7516784123693498378
收起阅读 »

被问到 NextTick 是宏任务还是微任务

NextTick 等待下一次 DOM 更新刷新的工具方法。 cn.vuejs.org/api/general… <https://cn.vuejs.org/api/general.html#nexttick> 从字面上看 就知道 肯定是个 异步的...
继续阅读 »

NextTick


等待下一次 DOM 更新刷新的工具方法。


cn.vuejs.org/api/general…


<https://cn.vuejs.org/api/general.html#nexttick>

从字面上看 就知道 肯定是个 异步的嘛。


然后面试官 那你来说说 js执行过程吧。 宏任务 微任务 来做做 宏任务 微任务输出的结果的题吧。


再然后 问问你 nextTick 既然几个异步的 那么他是 宏任务 还是个 微任务呀。


vue2 中


文件夹 src/core/util/next-tick.js 中


image-20240926175135221.png


promise --> mutationObserver -> setImmediate -> setTimeout


支持 哪个走哪个


vue3 中


image-20240926171628843.png


好吧 好吧 promise 了嘛


image-20240926174944844.png


全程 promise


作者:努力学基础的卡拉米
来源:juejin.cn/post/7418505553642291251
收起阅读 »

什么?localhost还能设置二级域名?

大家好,我是农村程序员,独立开发者,行业观察员,前端之虎陈随易。 我会在这里分享关于 独立开发、编程技术、思考感悟 等内容,欢迎关注。 个人网站 1️⃣:chensuiyi.me 个人网站 2️⃣:me.yicode.tech 技术群,搞钱群,闲聊群,自驾群...
继续阅读 »

大家好,我是农村程序员,独立开发者,行业观察员,前端之虎陈随易。


我会在这里分享关于 独立开发编程技术思考感悟 等内容,欢迎关注。



  • 个人网站 1️⃣:chensuiyi.me

  • 个人网站 2️⃣:me.yicode.tech

  • 技术群,搞钱群,闲聊群,自驾群,想入群的在我个人网站联系我。


如果你觉得本文有用,一键三连 (点赞评论转发),就是对我最大的支持~





网上冲浪看到一个有趣且违背常识的帖子,用了那么多年的 localhost,没想到 localhost 还能设置子域名。



而且还不需要修改 hosts 文件,直接就能使用,这真是离谱他妈给离谱开门,离谱到家了。


先说说应用场景:



  • 多用户/多会话隔离:在本地开发中模拟不同用户的 cookies 和 session storage,适合测试用户认证或个性化功能。

  • 跨域开发与测试:模拟真实多域环境 (如 API 和前端分离),用于调试 CORS、单点登录或微服务架构。

  • 简化开发流程:无需修改 hosts 文件即可快速创建子域名,适合快速原型设计或临时项目。

  • 工具与服务器集成:与本地开发工具 (如 localias) 结合,支持 HTTPS 和自定义端口,增强开发体验。

  • 灵活调试:通过自定义子域名和 IP (如 127.0.0.42) 进行高级调试或模拟复杂网络配置。


总得来说就是,localhsot 支持子域名比我们自己手动配置不同的域名并设置 hosts 文件方便多了。


接下来给大家实测一下。



请看,这是我直接在浏览器输入 test1.localhost:3020 后,就能请求到我本地启动的监听 3020 端口的后端接口返回的数据。


我没有配置 hosts 文件,没有做过任何多余的配置工作,直接就生效了。


那么我们可以直接在本地就能调试多服务器集群,跨域 cookie 共享,SSO 单点登录,微服务架构等功能,非常方便。


另外,本公众号是 前端之虎陈随易 专门分享技术的公众号,目前关注量不多,希望大家点点小手指,来个大大的关注哦~


作者:前端之虎陈随易
来源:juejin.cn/post/7521013717438758938
收起阅读 »

优雅!用了这两款插件,我成了整个公司代码写得最规范的码农

同事:你的代码写的不行啊,不够规范啊。 我:我写的代码怎么可能不规范,不要胡说。 于是同事打开我的 IDEA ,安装了一个插件,然后执行了一下,规范不规范,看报告吧。 这可怎么是好,这玩意竟然给我挑出来这么多问题,到底靠谱不。 同事潇洒的走掉了,只留下我在...
继续阅读 »

同事:你的代码写的不行啊,不够规范啊。



我:我写的代码怎么可能不规范,不要胡说。


于是同事打开我的 IDEA ,安装了一个插件,然后执行了一下,规范不规范,看报告吧。



这可怎么是好,这玩意竟然给我挑出来这么多问题,到底靠谱不。


同事潇洒的走掉了,只留下我在座位上盯着屏幕惊慌失措。我仔细的查看了这个报告的每一项,越看越觉得这插件指出的问题有道理,果然是我大意了,竟然还给我挑出一个 bug 来。



这是什么插件,review 代码无敌了。



这个插件就是 SonarLint,官网的 Slogan 是 clean code begins in your IDE with {SonarLint}。


作为一个程序员,我们当然希望自己写的代码无懈可击了,但是由于种种原因,有一些问题甚至bug都无法避免,尤其是刚接触开发不久的同学,也有很多有着多年开发经验的程序员同样会有一些不好的代码习惯。


代码质量和代码规范首先肯定是靠程序员自身的水平和素养决定的,但是提高水平的是需要方法的,方法就有很多了,比如参考大厂的规范和代码、比如有大佬带着,剩下的就靠平时的一点点积累了,而一些好用的插件能够时时刻刻提醒我们什么是好的代码规范,什么是好的代码。


SonarLint 就是这样一款好用的插件,它可以实时帮我们 review代码,甚至可以发现代码中潜在的问题并提供解决方案。


SonarLint 使用静态代码分析技术来检测代码中的常见错误和漏洞。例如,它可以检测空指针引用、类型转换错误、重复代码和逻辑错误等。这些都是常见的问题,但是有时候很难发现。使用 SonarLint 插件,可以在编写代码的同时发现这些问题,并及时纠正它们,这有助于避免这些问题影响应用程序的稳定性。


比如下面这段代码没有结束循环的条件设置,SonarLint 就给出提示了,有强迫症的能受的了这红下划线在这儿?



SonarLint 插件可以帮助我提高代码的可读性。代码应该易于阅读和理解,这有助于其他开发人员更轻松地维护和修改代码。SonarLint 插件可以检测代码中的代码坏味道,例如不必要的注释、过长的函数和变量名不具有描述性等等。通过使用 SonarLint 插件,可以更好地了解如何编写清晰、简洁和易于理解的代码。


例如下面这个名称为 hello_world的静态 final变量,SonarLint 给出了两项建议。



  1. 因为变量没有被使用过,建议移除;

  2. 静态不可变变量名称不符合规范;



SonarLint 插件可以帮助我遵循最佳实践和标准。编写符合标准和最佳实践的代码可以确保应用程序的质量和可靠性。SonarLint 插件可以检测代码中的违反规则的地方,例如不安全的类型转换、未使用的变量和方法、不正确的异常处理等等。通过使用 SonarLint 插件,可以学习如何编写符合最佳实践和标准的代码,并使代码更加健壮和可靠。


例如下面的异常抛出方式,直接抛出了 Exception,然后 SonarLint 建议不要使用 Exception,而是自定义一个异常,自定义的异常可能让人直观的看出这个异常是干什么的,而不是 Exception基本类型导出传递。



安装 SonarLint


可以直接打开 IDEA 设置 -> Plugins,在 MarketPlace中搜索SonarLint,直接安装就可以。



还可以直接在官网下载,打开页面http://www.sonarsource.com/products/so… EXPLORE即可到下载页面去下载了。虽然我们只是在 IDEA 中使用,但是它不只支持 Java 、不只支持 IDEA ,还支持 Python、PHP等众多语言,以及 Visual Studio 、VS Code 等众多 IDE。



在 IDEA 中使用


SonarLint 插件安装好之后,默认就开启了实时分析的功能,就跟智能提示的功能一样,随着你噼里啪啦的敲键盘,SonarLint插件就默默的进行分析,一旦发现问题就会以红框、红波浪线、黄波浪线的方式提示。


当然你也可以在某一文件中点击右键,也可在项目根目录点击右键,在弹出菜单中点击Analyze with SonarLint,对当前文件或整个项目进行分析。



分析结束后,会生成分析报告。



左侧是对各个文件的分析结果,右侧是对这个问题的建议和修改示例。


SonarLint 对问题分成了三种类型


类型说明Bug代码中的 bug,影响程序运行Vulnerability漏洞,可能被作为攻击入口Code smell代码意味,可能影响代码可维护性


问题按照严重程度分为5类


严重性说明BLOCKER已经影响程序正常运行了,不改不行CRITICAL可能会影响程序运行,可能威胁程序安全,一般也是不改不行MAJOR代码质量问题,但是比较严重MINOR同样是代码质量问题,但是严重程度较低INFO一些友好的建议


SonarQube


SonarLint 是在 IDE 层面进行分析的插件,另外还可以使用 SonarQube功能,它以一个 web 的形式展现,可以为整个开发团队的项目提供一个web可视化的效果。并且可以和 CI\CD 等部署工具集成,在发版前提供代码分析。



SonarQube是一个 Java 项目,你可以在官网下载项目本地启动,也可以以 docker 的方式启动。之后可以在 IDEA 中配置全局 SonarQube配置。



也可以在 SonarQube web 中单独配置一个项目,创建好项目后,直接将 mvn 命令在待分析的项目中执行,即可生成对应项目的分析报告,然后在 SonarQube web 中查看。



5


对于绝大多数开发者和开发团队来说,SonarQube 其实是没有必要的,只要我们每个人都解决了 IDE 中 SonarLint 给出的建议,当然最终的代码质量就是符合标准的。


阿里 Java 规约插件


每一个开发团队都有团队内部的代码规范,比如变量命名、注释格式、以及各种类库的使用方式等等。阿里一直在更新 Java 版的阿里巴巴开发者手册,有什么泰山版、终极版,想必各位都听过吧,里面的规约如果开发者都能遵守,那别人恐怕再没办法 diss 你的代码不规范了。


对应这个开发手册的语言层面的规范,阿里也出了一款 IDEA 插件,叫做 Alibaba Java Coding Guidelines,可以在插件商店直接下载。



比如前面说的那个 hello_world变量名,插件直接提示「修正为以下划线分隔的大写模式」。



再比如一些注释上的提示,不建议使用行尾注释。



image-20230314165107639


还有,比如对线程池的使用,有根据规范建议的内容,建议自己定义核心线程数和最大线程数等参数,不建议使用 Excutors工具类。



有了这俩插件,看谁还能说我代码写的不规范了。


作者:码小凡
来源:juejin.cn/post/7260314364876931131
收起阅读 »

本尊来!网易灰度发布系统揭秘:一天300次上线是怎么实现的?

你可能听过“网易每天上线几百次”, 但你是否知道:99%的发布都不是全量,而是按灰度批次推进。 今天从代码 + 场景双视角,拆解网易灰度发布的完整实现逻辑,让你真正搞懂: 发布是怎么分用户、分地域、分时间段的 如何回滚不影响线上用户 甚至如何模拟真实用户流量...
继续阅读 »

你可能听过“网易每天上线几百次”,
但你是否知道:99%的发布都不是全量,而是按灰度批次推进


今天从代码 + 场景双视角,拆解网易灰度发布的完整实现逻辑,让你真正搞懂:



  • 发布是怎么分用户、分地域、分时间段的

  • 如何回滚不影响线上用户

  • 甚至如何模拟真实用户流量进行 A/B 实验




一、网易灰度系统整体架构图(简化)


image.png


二、核心策略算法:如何选择灰度用户?


网易内部灰度用户分流引擎大致是这样:


interface User {
uid: string
region: string // 地域
isVip: boolean
loginTime: number // 最近登录时间
}

// 灰度策略配置
const strategy = {
percent: 10, // 灰度比例
regionInclude: ['华南'], // 地域包含
vipOnly: true // 只投放给 VIP
}

// 筛选函数
function filterUsers(users: User[], strategy) {
const filtered = users.filter(u =>
(!strategy.regionInclude || strategy.regionInclude.includes(u.region)) &&
(!strategy.vipOnly || u.isVip)
)
const count = Math.floor((strategy.percent / 100) * filtered.length)
return filtered.slice(0, count)
}



三、实际运行结果展示(模拟环境)


const users: User[] = Array.from({ length: 1000 }, (_, i) => ({
uid: `U${i}`,
region: ['华南', '华北', '华东'][i % 3],
isVip: i % 2 === 0,
loginTime: Date.now() - i * 10000,
}))

const selected = filterUsers(users, strategy)

console.log('灰度命中用户数:', selected.length)
console.log('前5个用户:', selected.slice(0, 5))

✅ 输出示例:


灰度命中用户数: 166
前5个用户: [
{ uid: 'U0', region: '华南', isVip: true, loginTime: 1717288879181 },
{ uid: 'U6', region: '华南', isVip: true, loginTime: 1717288819181 },
{ uid: 'U12', region: '华南', isVip: true, loginTime: 1717288759181 },
...
]



四、网易如何触发灰度?手动?自动?答案是:多触发源 + 策略组合



  1. ✅ 手动控制(管理员控制台)

  2. ✅ CI/CD 自动触发(合并主干自动上线)

  3. ✅ 实验平台触发(A/B 实验验证新功能)


示例:CI/CD 触发部署的逻辑(伪代码):


// Jenkinsfile 中执行灰度命令
steps {
script {
sh 'node deploy.js --env=prod --gray=10%'
}
}



五、监控数据如何决定“是否继续灰度”?


网易内部有自动指标监控,如:


指标名作用阈值
error_rate错误率异常自动中止>0.05
api_delay接口响应时间>300ms
login_success_ratio登录成功率<0.95

代码示例(灰度中控系统伪代码):


if (metrics.error_rate > 0.05 || metrics.login_success_ratio < 0.95) {
graySystem.stopDeployment()
graySystem.rollback()
console.log('灰度异常,中止并回滚')
} else {
graySystem.continue()
}



六、网易的灰度回滚机制非常丝滑,为什么?


他们采用了 “金丝雀版本+热切流量+自动恢复” 策略:


graySystem.deploy(version: '1.2.3', tag: 'canary')
// graySystem.rollback() 会回到上一个 tag=stable 的版本

而且每次发布都会打上 Git tag,并记录环境信息,回滚只需1行命令:


gray rollback --env prod --tag stable



七、你能学到什么?(总结)



  • 灰度不等于“发布慢一点”,而是可控可观测的发布策略

  • 用户维度灰度筛选逻辑要尽量结构化,避免硬编码

  • 数据指标必须“事前定义”,不能出了问题再想怎么止损

  • 所有灰度发布必须可回滚




彩蛋:



“上线不是勇气的象征,而是风控能力的体现。”



作者:前端付豪
来源:juejin.cn/post/7511150244576837684
收起阅读 »

解锁企业高效未来|上海飞络Synergy AI开启智能体协作新时代

他/她可以有自己的电脑,可以有自己的邮箱号,可以有自己的企业微信号。只要赋予权限,他/她可以替你完成各种日常工作,他/她可以随时随地和你沟通并完成你安排的任务,他/她永远高效!他/她永不抱怨!Synergy AI数字员工雇佣管理平台,以大语言模型驱动的AI A...
继续阅读 »

他/她可以有自己的电脑,可以有自己的邮箱号,可以有自己的企业微信号。只要赋予权限,他/她可以替你完成各种日常工作,他/她可以随时随地和你沟通并完成你安排的任务,他/她永远高效!他/她永不抱怨!

Synergy AI数字员工雇佣管理平台,以大语言模型驱动的AI Agent为核心,结合MCP工具集,并在数据安全、信息安全及行为安全的多维度监控下,为企业提供安全、合规、高效的“智能体员工”,重塑人机协作新范式!

为什么选择Synergy AI数字员工管理平台?

1、智能生产力升级

AI Agent数字员工深度融合语言理解、逻辑推理与工具调用能力,是能够自主感知环境、决策并执行任务的人工智能系统。它可以拥有自己的电脑、邮箱,微信号等所有员工的权限,同时也具备MCP工具集中的各种技能,能够像真人一样沟通,处理工作,但是能够实现更高的工作效率和更加低廉的成本!

2、根据职位定制AI员工工作流

通过“AIGC+Workflow”组合,实现任务自动化执行,响应速度大幅提升,成为企业降本增效的核心引擎。

同时基于企业人员、技能、文档、流程等六大核心信息库,AI数字员工可快速融入业务场景,提供从单职能支持、人机协同到多职能协作的全链路服务。

3、安全合规,全程可控

1)行为监测

实时检测AI数字员工是否存在权限越界、敏感数据操作,信息泄露,被黑客利用等安全合规隐患。

2)数据安全管控

智能识别、过滤、脱敏替换AI数字员工及大语言模型使用过程中触发的敏感数据,企业核心数据泄漏等风险。

3)效能可视化

通过工作流执行情况、人工干预度等指标,持续优化AI员工表现。

Synergy AI能实现什么效果?

1、AI销售助理

可协助销售管理日程、预约会议、统计CRM数字,甚至代替销售联络沟通回款问题。入职飞络销售部门后,内部数据显示客户响应效率提升3倍以上,人力成本降低60%,助力团队精准触达商机。

2、SOC安全及运维专员

在安全运营和运维场景中,AI员工可以迅速响应各个安全系统平台的告警,并根据制定的工作流程,进行下一步的沟通、交流、处置。让企业安全事件响应速度大幅提升,精准提高准确率,为企业筑牢数字防线。

3、更多AI人职位有待解锁

根据每家企业不同的场景需求,Synergy AI提供可以定制化的各种企业AI数字员工,让AI智能体真正能够匹配企业需求,为企业带来实际帮助。

Synergy AI如何落地实施?

1、分析岗位SOW/SOP

找到重复、需要与人互动的工作流,快速实现智能化并通过拟人化的AI员工来完成,逐步将AI工作流覆盖全业务。

2、无缝对接系统

支持OA、ERP、CRM、M365等主流平台MCP / API对接。

3、7×24小时护航

飞络安全运营中心全程监控,保障业务稳定运行。

企业的信息安全如何保护?

飞络基于自研发两大安全管理平台,为企业在使用AI的同时,极大限度保障企业的数据以及隐私安全:

企业AI安全事件监控管理平台

通过企业AI安全事件监控管理平台,我们可以实时提供AI系统以及AI Agents的运行状态,对于所发生的安全事件,实行7*24小时的安全监控及管理。

ASSA:企业AI数据过滤平台

通过ASSA,企业可以管理及管控企业内部信息传输到大语言模型上的数据,对于敏感信息、企业机密、个人信息等进行阻止、脱敏、模糊化等管理操作

7*24 SOC服务

基于飞络提供的7*24级别的SOC运营服务,可以协助客户一起实时监控及管理所有AI相关的安全事件,为企业的数据安全保驾护航!

Synergy AI数字员工雇佣管理平台,以自主研发技术为核心,为企业提供一站式智能解决方案。

收起阅读 »

给前端小白的科普,为什么说光有 HTTPS 还不够?为啥还要请求签名?

今天咱们聊个啥呢?先设想一个场景:你辛辛苦苦开发了一个前端应用,后端 API 也写得杠杠的。用户通过你的前端界面提交一个订单,比如说买1件商品。请求发出去,一切正常。但如果这时候,有个“不开眼”的黑客老哥,在你的请求发出后、到达服务器前,悄咪咪地把“1件”改成...
继续阅读 »

今天咱们聊个啥呢?先设想一个场景:你辛辛苦苦开发了一个前端应用,后端 API 也写得杠杠的。用户通过你的前端界面提交一个订单,比如说买1件商品。请求发出去,一切正常。但如果这时候,有个“不开眼”的黑客老哥,在你的请求发出后、到达服务器前,悄咪咪地把“1件”改成了“100件”,或者把你用户的优惠券给薅走了,那服务器收到的就是个被篡改过的“假”请求。更狠一点,如果他拿到了你某个用户的合法请求,然后疯狂重放这个请求,那服务器不就炸了?


是不是想想都后怕?别慌,今天咱就来聊聊怎么给咱们的API请求加一把“锁”,让这种“中间人攻击”和“重放攻击”无处遁形。这把锁,就是大名鼎鼎的 HMAC-SHA256 请求签名。学会了它,你就能给你的应用穿上“防弹衣”!


image.png


一、光有 HTTPS 还不够?为啥还要请求签名?


可能有机灵的小伙伴会问:“老张,咱不都有 HTTPS 了吗?数据都加密了,还怕啥?”


问得好!HTTPS 确实牛,它能保证你的数据在传输过程中不被窃听和篡改,就像给数据修了条“加密隧道”。但它主要解决的是传输层的安全。可如果:



  1. 请求在加密前就被改了:比如黑客通过某种手段(XSS、恶意浏览器插件等)在你的前端代码执行时就修改了要发送的数据,那 HTTPS 加密的也是被篡改后的数据。

  2. 请求被合法地解密后,服务器无法验证“我是不是我”:HTTPS 保证了数据从A点到B点没被偷看,但如果有人拿到了一个合法的、加密的请求包,他可以原封不动地发给服务器100遍(重放攻击),服务器每次都会认为是合法的。

  3. API Key/Secret 直接在前端暴露: 有些简单的 API 认证,可能会把 API Key 直接写在前端,这简直就是“裸奔”,分分钟被扒下来盗用。


请求签名,则是在应用层做的一道防线。它能确保:



  • 消息的完整性:数据没被篡改过。

  • 消息的身份验证:确认消息确实是你授权的客户端发来的。

  • 防止重放攻击:结合时间戳或 Nonce,让每个请求都具有唯一性。


它和 HTTPS 是好搭档,一个负责“隧道安全”,一个负责“货物安检”,双保险!


二、主角登场:HMAC-SHA256 是个啥?


HMAC-SHA256,听起来挺唬人,拆开看其实很简单:



  • HMAC:Hash-based Message Authentication Code,翻译过来就是“基于哈希的消息认证码”。它是一种使用密钥(secret key)来生成消息摘要(MAC)的方法。

  • SHA256:Secure Hash Algorithm 256-bit,一种安全的哈希算法,能把任意长度的数据转换成一个固定长度(256位,通常表示为64个十六进制字符)的唯一字符串。相同的输入永远得到相同的输出,输入有任何微小变化,输出都会面目全非。


所以,HMAC-SHA256 就是用一个共享密钥 (Secret Key),通过 SHA256 算法,给你的请求数据生成一个独一无二的“签名”。


三、签名的艺术:请求是怎么被“签”上和“验”货的?


整个流程其实不复杂,咱们用个图来说明一下:


sequenceDiagram
participant C as 前端 (Client)
participant S as 后端 (Server)

C->>C: 1. 准备请求参数 (如 method, path, query, body)
C->>C: 2. 加入时间戳 (timestamp) 和/或 随机数 (nonce)
C->>C: 3. 将参数按约定规则排序、拼接成一个字符串 (stringToSign)
C->>C: 4. 使用共享密钥 (Secret Key) 对 stringToSign 进行 HMAC-SHA256 运算,生成签名 (signature)
C->>S: 5. 将原始请求参数 + timestamp + nonce + signature 一起发送给后端

S->>S: 6. 接收到所有数据
S->>S: 7. 校验 timestamp/nonce (检查是否过期或已使用,防重放)
S->>S: 8. 从接收到的数据中,按与客户端相同的规则,提取参数、排序、拼接成 stringToSign'
S->>S: 9. 使用自己保存的、与客户端相同的 Secret Key,对 stringToSign' 进行 HMAC-SHA256 运算,生成 signature'
S->>S: 10. 比对客户端传来的 signature 和自己生成的 signature'
alt 签名一致
S->>S: 11. 验证通过,处理业务逻辑
S-->>C: 响应结果
else 签名不一致
S->>S: 11. 验证失败,拒绝请求
S-->>C: 错误信息 (如 401 Unauthorized)
end

简单来说,就是:



  1. 客户端:把要发送的数据(比如请求方法、URL路径、查询参数、请求体、时间戳等)按照事先约定好的顺序和格式拼成一个长长的字符串。然后用一个只有你和服务器知道的“秘密钥匙”(Secret Key)和 HMAC-SHA256 算法,给这个字符串算出一个“指纹”(签名)。最后,把原始数据、时间戳、签名一起发给服务器。

  2. 服务器端:收到请求后,用完全相同的规则和完全相同的“秘密钥匙”,对收到的原始数据(不包括客户端传来的签名)也算一遍“指纹”。然后比较自己算出来的指纹和客户端传过来的指纹。如果一样,说明数据没被改过,而且确实是知道秘密钥匙的“自己人”发的;如果不一样,那对不起,这请求有问题,拒收!


四、Talk is Cheap, Show Me The Code!


光说不练假把式,咱们来点实在的。


前端签名 (JavaScript - 通常使用 crypto-js 库)


// 假设你已经安装了 crypto-js: npm install crypto-js
import CryptoJS from 'crypto-js';

function generateSignature(params, secretKey) {
// 1. 准备待签名数据
const method = 'GET'; // 请求方法
const path = '/api/user/profile'; // 请求路径
const timestamp = Math.floor(Date.now() / 1000).toString(); // 时间戳 (秒)
const nonce = CryptoJS.lib.WordArray.random(16).toString(); // 随机数,可选

// 2. 构造待签名字符串 (规则很重要,前后端要一致!)
// 通常会对参数名按字典序排序
const sortedKeys = Object.keys(params).sort();
const queryString = sortedKeys.map(key => `${key}=${params[key]}`).join('&');

const stringToSign = `${method}\n${path}\n${queryString}\n${timestamp}\n${nonce}`;
console.log("String to Sign:", stringToSign); // 调试用

// 3. 使用 HMAC-SHA256 生成签名
const signature = CryptoJS.HmacSHA256(stringToSign, secretKey).toString(CryptoJS.enc.Hex);
console.log("Generated Signature:", signature); // 调试用

return {
signature,
timestamp,
nonce
};
}

// --- 使用示例 ---
const mySecretKey = "your-super-secret-key-dont-put-in-frontend-directly!"; // 强调:密钥不能硬编码在前端!
const requestParams = {
userId: '123',
role: 'user'
};

const { signature, timestamp, nonce } = generateSignature(requestParams, mySecretKey);

// 实际发送请求时,把 signature, timestamp, nonce 放在请求头或请求体里
// 例如:
// fetch(`${path}?${queryString}`, {
// method: method,
// headers: {
// 'X-Signature': signature,
// 'X-Timestamp': timestamp,
// 'X-Nonce': nonce,
// 'Content-Type': 'application/json'
// },
// // body: JSON.stringify(requestBody) // 如果是POST/PUT等
// })
// .then(...)

划重点! 上面代码里的 mySecretKey 绝对不能像这样直接写在前端代码里!这只是个演示。真正的 Secret Key 需要通过安全的方式分发和存储,比如在构建时注入,或者通过更安全的认证流程动态获取(但这又引入了新的复杂性,通常 Secret Key 是后端持有,客户端动态获取一个有时效性的 token)。对于纯前端应用,更常见的做法是后端生成签名所需参数,或者整个流程由 BFF (Backend For Frontend) 层处理。如果你的应用是 App,可以把 Secret Key 存储在原生代码中,相对安全一些。


后端验签 (Node.js - 使用内置 crypto 模块)


const crypto = require('crypto');

function verifySignature(requestData, clientSignature, clientTimestamp, clientNonce, secretKey) {
// 0. 校验时间戳 (例如,请求必须在5分钟内到达)
const serverTimestamp = Math.floor(Date.now() / 1000);
if (Math.abs(serverTimestamp - parseInt(clientTimestamp, 10)) > 300) { // 5分钟窗口
console.error("Timestamp validation failed");
return false;
}

// (可选) 校验 Nonce 防止重放,需要存储已用过的 Nonce,可以用 Redis 等
// if (isNonceUsed(clientNonce)) {
// console.error("Nonce replay detected");
// return false;
// }
// markNonceAsUsed(clientNonce, clientTimestamp); // 标记为已用,并设置过期时间

// 1. 从请求中提取参与签名的参数
const { method, path, queryParams } = requestData; // 假设已解析好

// 2. 构造待签名字符串 (规则必须和客户端完全一致!)
const sortedKeys = Object.keys(queryParams).sort();
const queryString = sortedKeys.map(key => `${key}=${queryParams[key]}`).join('&');

const stringToSign = `${method}\n${path}\n${queryString}\n${clientTimestamp}\n${clientNonce}`;
console.log("Server String to Sign:", stringToSign);

// 3. 使用 HMAC-SHA256 生成签名
const expectedSignature = crypto.createHmac('sha256', secretKey)
.update(stringToSign)
.digest('hex');
console.log("Server Expected Signature:", expectedSignature);
console.log("Client Signature:", clientSignature);

// 4. 比对签名 (使用 crypto.timingSafeEqual 防止时序攻击)
if (clientSignature.length !== expectedSignature.length) {
return false;
}
return crypto.timingSafeEqual(Buffer.from(clientSignature), Buffer.from(expectedSignature));
}

// --- Express 示例中间件 ---
// app.use((req, res, next) => {
// const clientSignature = req.headers['x-signature'];
// const clientTimestamp = req.headers['x-timestamp'];
// const clientNonce = req.headers['x-nonce'];
// // 实际项目中,secretKey 应该从环境变量或配置中读取
// const API_SECRET_KEY = process.env.API_SECRET_KEY || "your-super-secret-key-dont-put-in-frontend-directly!";

// // 构造 requestData 对象,包含 method, path, queryParams
// // 注意:如果是 POST/PUT 请求,请求体 (body) 通常也需要参与签名
// // 且 body 如果是 JSON,建议序列化后参与签名,而不是原始对象
// const requestDataForSig = {
// method: req.method.toUpperCase(),
// path: req.path,
// queryParams: req.query, // 对于GET;POST/PUT可能还需包含body
// // bodyString: req.body ? JSON.stringify(req.body) : "" // 如果body参与签名
// };

// if (!verifySignature(requestDataForSig, clientSignature, clientTimestamp, clientNonce, API_SECRET_KEY)) {
// return res.status(401).send('Invalid Signature');
// }
// next();
// });

五、细节是魔鬼:实施过程中的注意事项



  1. 密钥管理 (Secret Key)



    • 绝对保密:这是最重要的!密钥泄露,签名机制就废了。

    • 不要硬编码在前端:再次强调!对于B端或内部系统,可以考虑通过安全的构建流程注入。对于C端开放应用,通常结合用户登录后的 session token 或 OAuth token 来做,或者使用更复杂的 API Gateway 方案。

    • 定期轮换:为了安全,密钥最好能定期更换。



  2. 时间戳 (Timestamp)



    • 防止重放攻击:服务器会校验收到的时间戳与当前服务器时间的差值,如果超过一定阈值(比如5分钟),就认为是无效请求。

    • 时钟同步:客户端和服务器的时钟要尽量同步,不然很容易误判。



  3. 随机数 (Nonce)



    • 更强的防重放:Nonce 是一个只使用一次的随机字符串。服务器需要记录用过的 Nonce,在一定时间内(同时间戳窗口)不允许重复。可以用 Redis 等缓存服务来存。



  4. 哪些内容需要签名?



    • HTTP 方法 (GET, POST, etc.)

    • 请求路径 (Path, e.g., /users/123)

    • 查询参数 (Query Parameters, e.g., ?name=zhangsan&age=18):参数名需要按字典序排序,确保客户端和服务端拼接顺序一致。

    • 请求体 (Request Body):如果是 application/x-www-form-urlencodedmultipart/form-data,处理方式同 Query Parameters。如果是 application/json,通常是将整个 JSON 字符串作为签名内容的一部分。注意空 body 和有 body 的情况。

    • 关键的请求头:比如 Content-Type,以及自定义的一些重要 Header。

    • 时间戳和 Nonce:它们本身也要参与签名,防止被篡改。



  5. 一致性是王道:客户端和服务端在选择哪些参数参与签名、参数的排序规则、拼接格式等方面,必须严格一致,一个空格,一个换行符不同,签名结果就天差地别。


六、HMAC-SHA256 vs. 其他方案?


方案优点缺点适用场景
仅 HTTPS传输层加密,防止窃听无法防止应用层篡改(加密前)、无法验证发送者身份(应用层)、无法防重放基础数据传输安全
简单摘要 (如MD5)实现简单若无密钥,容易被伪造;MD5本身已不安全文件完整性校验(非安全敏感)
HMAC-SHA256消息完整性、身份验证(基于共享密钥)、可防重放(结合时间戳/Nonce)密钥管理是关键和难点;签名和验签有一定计算开销需要保障API接口安全、防止未授权访问和篡改的场景
JWT (JSON Web Token)无状态、可携带用户信息、标准化Token 可能较大;吊销略麻烦;主要用于用户认证和授权用户登录、单点登录、API授权

HMAC-SHA256 更侧重于请求本身的完整性和来源认证,而 JWT 更侧重于用户身份的认证和授权。它们可以结合使用。


好啦,今天关于 HMAC-SHA256 请求签名的唠嗑就到这里。这玩意儿看起来步骤多,但一旦理解了原理,实现起来其实就是细心活儿。给你的 API 加上这把锁,晚上睡觉都能踏实点!


我是老码小张,一个喜欢研究技术原理,并且在实践中不断成长的技术人。希望今天的分享对你有帮助,咱们下回再聊!欢迎大家留言交流你的看法和经验哦!


作者:老码小张
来源:juejin.cn/post/7502641888970670080
收起阅读 »

聊聊四种实时通信技术:长轮询、短轮询、WebSocket 和 SSE

这篇文章,我们聊聊 四种实时通信技术:短轮询、长轮询、WebSocket 和 SSE 。 1 短轮询 浏览器 定时(如每秒)向服务器发送 HTTP 请求,服务器立即返回当前数据(无论是否有更新)。 优点:实现简单,兼容性极佳 缺点:高频请求浪费资源,实时性...
继续阅读 »

这篇文章,我们聊聊 四种实时通信技术:短轮询、长轮询、WebSocket 和 SSE 。


1 短轮询


浏览器 定时(如每秒)向服务器发送 HTTP 请求,服务器立即返回当前数据(无论是否有更新)。




  • 优点:实现简单,兼容性极佳

  • 缺点:高频请求浪费资源,实时性差(依赖轮询间隔)

  • 延迟:高(取决于轮询频率)

  • 适用场景:兼容性要求高,延迟不敏感的简单场景。


笔者职业生涯印象最深刻的短轮询应用场景是比分直播



如图所示,用户进入比分直播界面,浏览器定时查询赛事信息(比分变动、黄红牌等),假如数据有变化,则重新渲染页面。


这种方式实现起来非常简单可靠,但是频繁的调用后端接口,会对后端性能会有影响(主要是 CPU)。同时,因为依赖轮询间隔,页面数据变化有延迟,用户体验并不算太好。


2 长轮询


浏览器发送 HTTP 请求后,服务器 挂起连接 直到数据更新或超时,返回响应后浏览器立即发起新请求。




  • 优点:减少无效请求,比短轮询实时性更好

  • 缺点:服务器需维护挂起连接,高并发时资源消耗大

  • 延迟:中(取决于数据更新频率)

  • 适用场景:需要较好实时性且无法用 WebSocket/SSE 的场景(如消息通知)


长轮询最常见的应用场景是:配置中心,我们耳熟能详的注册中心 Nacos 、阿波罗都是依赖长轮询机制。


nacos长轮询



客户端发起请求后,Nacos 服务端不会立即返回请求结果,而是将请求挂起等待一段时间,如果此段时间内服务端数据变更,立即响应客户端请求,若是一直无变化则等到指定的超时时间后响应请求,客户端重新发起长链接。



3 WebSocket


基于 TCP 的全双工协议,通过 HTTP 升级握手(Upgrade: websocket)建立持久连接,双向实时通信。 image.png



  • 优点:最低延迟,支持双向交互,节省带宽

  • 缺点:实现复杂,需单独处理连接状态

  • 延迟:极低

  • 适用场景:聊天室、在线游戏、协同编辑等 高实时双向交互 需求


笔者曾经服务于北京一家电商公司,参与直播答题功能的研发。



直播答题整体架构见下图:



Netty TCP 网关的技术选型是:Netty、ProtoBuf、WebSocket ,选择 WebSocket 是因为它支持双向实时通信,同时 Netty 内置了 WebSocket 实现类,工程实现起来相对简单。


4 Server Send Event(SSE)


基于 HTTP 协议,服务器可 主动推送 数据流(如Content-Type: text/event-stream),浏览器通过EventSource API 监听。




  • 优点:原生支持断线重连,轻量级(HTTP协议)

  • 缺点:不支持浏览器向服务器发送数据

  • 延迟:低(服务器可即时推送)

  • 适用场景:股票行情、实时日志等 服务器单向推送 需求。


SSE 最经典的应用场景是 : DeepSeek web 聊天界面 ,如图所示:



当在 DeepSeek 对话框发送消息后,浏览器会发送一个 HTTP 请求 ,服务端会通过 SSE 方式将数据返回到浏览器。



5 总结


特性短轮询长轮询SSEWebSocket
通信方向浏览器→服务器浏览器→服务器服务器→浏览器双向通信
协议HTTPHTTPHTTPWebSocket(基于TCP)
实时性极高
资源消耗高(频繁请求)中(挂起连接)低(长连接)

选择建议



  • 需要 简单兼容性 → 短轮询

  • 需要 中等实时性 → 长轮询

  • 只需 服务器推送 → SSE

  • 需要 全双工实时交互 → WebSocket




作者:勇哥Java实战
来源:juejin.cn/post/7496375493329174591
收起阅读 »

BOE(京东方)第6代新型半导体显示器件生产线全面量产 打造全球显示产业新引擎

2025年5月26日,BOE(京东方)成功举办主题为“屏启未来 智显无界”的量产交付活动,开启第6代新型半导体显示器件生产线由建设转向运营的崭新篇章。这不仅标志着BOE(京东方)在LTPO、LTPS、Mini LED等高端显示领域实现跨越式突破,也为我国半导体...
继续阅读 »

2025年5月26日,BOE(京东方)成功举办主题为“屏启未来 智显无界”的量产交付活动,开启第6代新型半导体显示器件生产线由建设转向运营的崭新篇章。这不仅标志着BOE(京东方)在LTPO、LTPS、Mini LED等高端显示领域实现跨越式突破,也为我国半导体显示产业注入强劲动能,加速助力北京打造国际科技创新中心。作为全球技术最先进、产能最大的VR用LCD生产基地,该生产线将充分发挥技术引领和产业集聚优势,进一步巩固BOE(京东方)行业龙头地位,加速全球虚拟现实产业和数字经济发展。BOE(京东方)科技集团董事长陈炎顺,BOE(京东方)首席执行官冯强,BOE(京东方)首席运营官王锡平,行业专家及生态伙伴出席现场仪式,共同见证这一荣耀时刻。

活动现场,BOE(京东方)科技集团董事长陈炎顺发表致辞,他表示,BOE(京东方)以“BOE速度”打造新型显示产业基地建设标杆,成功实现开工当年封顶、次年产品点亮的关键目标。与此同时,技术研发与产品准备也在同步推进,多款产品已完成客户送样并推进交付。BOE(京东方)特别感谢战略合作伙伴们对技术创新的追求和坚持,这也推动着BOE(京东方)不断超越自我,取得一个又一个新的突破。BOE(京东方)将始终以战略客户伙伴的前沿需求和技术标准为指引,在“屏之物联”战略指导下,用踏实奋斗和持续创新回馈各界支持。

作为全球技术最先进的液晶显示屏生产基地,BOE(京东方)第6代新型半导体显示器件生产线总投资290亿元,占地面积42万平方米,设计月产能达5万片。该生产线以LTPO(低温多晶氧化物)和LTPS(低温多晶硅)技术为核心,聚焦聚焦 VR 显示面板、中小尺寸高附加值 IT 显示面板、车载显示面板等高端产品研发与生产,采用1500mm×1850mm的6代线玻璃基板,配备当前最先进的生产设备,并整合京东方多条成熟产线的先进经验,大幅提升生产效率和产品精度。在技术创新方面,BOE(京东方)LTPO技术融合了LTPS的高迁移率和Oxide的低功耗优势,可实现1500PPI以上的超高像素密度,并大幅度降低面板功耗,为显示设备提供更流畅、更清晰的动态画面。

值得一提的是,BOE(京东方)第6代新型半导体显示器件生产线还充分赋能多元化的场景应用,多款产品凭借极具竞争力的产品性能和领先的技术优势,获得全球一线知名客户的高度认可。其中,BOE(京东方)自主设计开发的超高2117PPI Real RGB显示屏实现成功点亮,达到当前LCD行业最高分辨率。在此次交付活动上,BOE(京东方)展示了已具备量产条件的2.24英寸1500PPI以及2.24英寸1700PPI VR显示模组,16英寸240Hz电竞笔记本屏幕(分辨率2560×1600,100% DCI-P3色域),以及14.6英寸窄边框高端车载中控屏等产品,全面满足“元宇宙”、高端消费电子、智能出行等领域的需求。

更加值得关注的是,BOE(京东方)第6代新型半导体显示器件生产线还在可持续发展方面走在世界前列。通过洁净室气流集控、AI分区温湿度自调、用电集控等创新技术,BOE(京东方)实现供热回收使用率100%、实现纯水回用率达80%、污染物排放均值小于标准50%。此外,在“双碳”目标引领下,BOE(京东方)将绿色理念贯穿于研发、生产与回收全生命周期。例如,生产线生产的产品在提升画质的同时更加注重产品低功耗性能,为设备的长时间使用提供可持续支持。这些实践不仅呼应了全球绿色低碳转型趋势,更展现了BOE(京东方)作为行业领军者的责任担当。同时,依托AI赋能,BOE(京东方)第6代新型半导体显示器件生产线还实现了智能排产、预测性维护、智能缺陷管理等全流程优化,设备综合效率(OEE)提升0.5%,工艺稳定性提升20%,良率分析效率提升20%,为行业树立了绿色生产与智能制造的双重标杆,也有力地回应了BOE(京东方)“Open Next Earth”的可持续发展品牌内涵。

在虚实交融的数字文明浪潮中,屏幕已从信息媒介跃升为跨越现实与虚拟、链接当下与未来的纽带。BOE(京东方)将持续以“屏之物联”战略为核心,加速显示技术与物联网、人工智能等前沿技术的深度融合,深刻践行“科技创新+绿色发展”之道。面向未来,BOE(京东方)将与更多合作伙伴携手,以协同创新之力探寻合作路径,全力赋能万物互联的未来智能生态体系,共同迎接一个更智慧、更互联、更美好、更绿色的全新时代。


收起阅读 »

生产环境到底能用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

收起阅读 »