GitHub Packages 发布 Maven 库:从 Token 配置到 BOM 管理

GitHub Packages 发布 Maven 库:从 Token 配置到 BOM 管理 目标 把一个多模块 Maven 项目发布到 GitHub Packages,并让其他项目通过 BOM 统一引用。全程使用 GitHub 免费额度,不搭私有 Nexus。 前置条件 条件 说明 GitHub 账号 一个,免费套餐即可 Personal Access Token write:packages + read:packages 权限 Maven 3.6+ 构建工具 Java 17+ 运行时 环境搭建 第 1 步:生成 GitHub Token GitHub 右上角头像 → Settings → Developer settings → Personal access tokens → Tokens (classic) → Generate new token 勾选权限范围: ✅ write:packages (发布包) ✅ read:packages (下载包) ✅ repo (访问仓库,write:packages 自动依赖) Token 创建后只会显示一次,复制保存好。有效期的建议:如果用于本地开发,设 30 ~ 90 天;如果用于 CI/CD,设成永不过期并定期轮换。 ...

三月 10, 2023 · 3 分钟 · 553 字 · yaomingye

Spring Bean 生命周期与懒加载:从一次 Starter 踩坑说起

Spring Bean 生命周期与懒加载:从一次 Starter 踩坑说起 起因:一个 @PostConstruct 引发的血案 封装了一个 Redis 工具类的 Spring Boot Starter,里面有个组件叫 WorkIdAllocator ,它在 @PostConstruct 中做了这么一件事: @PostConstruct public void init() { setNextSnowFlaskWorkerId(); // 连接 Redis,分配一个雪花算法 WorkerId } 看起来没毛病——服务启动时自动分配到 WorkerId。但问题来了:Redis 的连接参数( spring.data.redis.host )放在 Nacos 配置中心,通过 shared-configs 加载。而 @PostConstruct 在 Bean 属性注入完成后就立刻执行,那时候 Redis 配置还没加载到 Spring 的 Environment 中。 结果: host = null → 默认 localhost:6379 → 连不上 → 启动失败。 修复方式也很简单——加个 @Lazy : @Lazy @Component public class WorkIdAllocator { // 第一次被调用时才执行 @PostConstruct } 问题虽然解决了,但某个开发者好奇心被勾起来了:Spring Bean 的生命周期到底分几个阶段?扩展点在什么时候执行?@Lazy 到底干了什么? ...

三月 8, 2023 · 6 分钟 · 1277 字 · yaomingye

单体拆分微服务:9 个服务踩出来的 7 个典型错误

单体拆微服务,这 7 个坑你踩过几个? 接手了一套从单体架构拆分为微服务的 Spring Cloud Alibaba 项目。9 个服务,Spring Boot 3.3.5,集齐了 Nacos、Gateway、Sentinel、RocketMQ、ShardingSphere、Elasticsearch 全家桶——看 POM 文件像一份微服务教科书。 实际跑起来就发现问题了:项目虽然拆成了 9 个模块,但在架构思维上仍然是个单体。 用了微服务的壳,没改掉单体时代的坏习惯。一顿排查下来,发现了 7 个典型错误。 错误一:全量包扫描——每个服务都在扫整个宇宙 第一个映入眼帘的就是各个 Application 类上的注解: @ComponentScan(basePackages = "cn.net.mall") 9 个服务里有好几个直接扫整个项目包树。这意味着什么? mall-pay 启动的时候,Spring 会去扫描 mall-common 下的所有类,包括 cn.net.mall.util.RedisUtil 。而 RedisUtil 又依赖 StringRedisTemplate ,这个类来自 spring-boot-starter-data-redis ,偏偏 mall-pay 的 POM 里没加这个依赖。 mall-pay 启动 → 扫描 cn.net.mall → 发现 RedisUtil → 尝试创建 → StringRedisTemplate 不在 classpath → ClassNotFoundException → 启动失败 flowchart LR subgraph SCAN["全量扫描 `cn.net.mall `"] PAY["mall-pay\n@ComponentScan"] COMMON["mall-common\nRedisUtil ← 依赖 → StringRedisTemplate"] end subgraph CLASSPATH["pay 的 classpath"] DEPS["mall-common.jar\n(但有 optional=true)"] MISSING["❌ StringRedisTemplate 不在"] end PAY -->|"扫描到"| COMMON COMMON -.->|"尝试创建 bean"| MISSING MISSING -->|"ClassNotFoundException"| CRASH["启动崩溃"] classDef process fill:#1e1e24,stroke:#6b7280,stroke-width:2px,color:#e5e7eb; classDef reject fill:#450a0a,stroke:#dc2626,stroke-width:2px,color:#fecaca; classDef highlight fill:#431407,stroke:#ea580c,stroke-width:2px,color:#fed7aa; class PAY,COMMON process; class MISSING,CRASH reject; class DEPS highlight; 更隐蔽的问题是:全量扫描让分仓库成为泡影。 微服务的核心理念之一就是独立开发、独立部署。如果每个服务都假设"所有模块在同一个 classpath 上",那一旦把服务拆到独立 Git 仓库,全量扫描就会漏掉其他服务的类——因为它根本不在 classpath 上。 ...

三月 6, 2023 · 8 分钟 · 1559 字 · yaomingye

Sentinel Dashboard 容器化部署踩坑全记录:从端口映射到认证配置的血泪史

被一个社区镜像折磨的 24 小时 目标说明 这篇博客的目标很朴素:让读者用 10 分钟把 Sentinel Dashboard 跑起来,而不是花一整天跟一个社区镜像死磕。 某开发者在搭建微服务治理平台时,需要部署 Sentinel Dashboard 作为流量治理控制台。本以为 docker run 一把梭,结果被 bladex/sentinel-dashboard:1.8.6 这个社区镜像折腾了整整一天——端口改不掉、环境变量传了等于没传、配置文件挂载不生效、启动就崩溃、浏览器打开 401……踩了个遍。 本文将完整记录这 7 个坑的根因、排查过程和最终解决方案。所有配置已在 Debian 13 + Docker 26+ 下验证通过。 📌 前置知识:需要了解基本 Docker 操作和 Spring Boot 配置文件概念。 前置条件 项目 要求 操作系统 Linux(本文基于 Debian 13 WSL2) Docker 26+ Docker Compose v2+ 目标端口 9903(按需调整) 验证命令: docker --version # Docker version 26.x.x docker compose version # Docker Compose version v2.x.x 环境搭建 创建一个部署目录,后续所有文件都在此目录下操作: mkdir -p ~/dev-env/sentinel && cd ~/dev-env/sentinel 先简单拉个镜像试试水: ...

三月 4, 2023 · 5 分钟 · 855 字 · yaomingye

Nacos 配置管理实战:config.import 与 bootstrap.yml 的取舍、多环境方案、模板标准化

Nacos 配置管理:一套标准化方案如何搞定 9 个微服务 接手了一套 Spring Cloud Alibaba 微服务项目。读完代码后先看了一遍它的配置管理——毕竟 9 个服务各有各的数据库、Redis、Token 密钥,还不用同一套连接机制,这不配置爆炸谁配置爆炸。 检查结果不出所料:有的服务用 bootstrap.yml 连 Nacos,有的用 config.import ,还有的既没 bootstrap.yml 也没 config.import ,全靠本地 application.yml 硬撑着。更离谱的是,9 个服务在 Nacos 上重复配置了 9 遍 Redis、9 遍 JWT 密钥,改一次密码要改 9 个 dataId。 分享一套标准化方案,从 9 个服务的混乱配置中理出了头绪。 bootstrap.yml 还是 config.import? Spring Cloud Alibaba 项目连接 Nacos 有两种做法。第一种是传统方案,在 bootstrap.yml 中配置 Nacos 参数: `` `yaml bootstrap.yml — 旧方案 spring: cloud: nacos: config: server-addr: ${NACOS_ADDR:localhost:8848} namespace: ${NACOS_NAMESPACE:mall} file-extension: yaml 需要额外引入 `spring-cloud-starter-bootstrap` 依赖才能生效。这是 Spring Cloud 2020 之前的标准做法。 第二种是新方案,直接在 `application.yml` 中通过 `spring.config.import` 指定: `` `yaml # application.yml — 新方案 spring: config: import: nacos:${spring.application.name}.yaml Spring Cloud 2023.x 已默认关闭 bootstrap 上下文,官方推荐使用 config.import 。 ...

三月 2, 2023 · 5 分钟 · 1030 字 · yaomingye

接手微服务项目的踩坑与重构:鉴权链路、补偿机制与 API 文档整理

接手了一套微服务项目,代码看起来挺像回事,仔细一看全是坑 这套项目表面上看骨架搭得不错——Spring Cloud Alibaba 全家桶、17 个 Maven 模块、9 个微服务、分库分表、ES 双写、SkyWalking 链路追踪,基础中间件能怼的全都怼上去了。 但真的跑起来、改起来、审计起来,才发现业务逻辑有不少问题:鉴权链路缺半截、下单不扣库存、@NoLogin 散落几十个地方、JWT 只存了 username 导致全链路 Redis 查重。基础设施搭得好,不等于项目能经受住实际业务场景的考验。 本文将项目里几个典型问题摆出来,聊聊踩坑过程和修复思路,同时配图说明关键流程的改造前后对比。 JWT claims:只放 username,剩下的全塞 Redis 第一个让人困惑的设计点在于 JWT 的使用方式。看下 Token 生成代码: // UserTokenHelper.java — 原实现 public String generateToken(String username, String json) { String token = Jwts.builder() .setSubject(username) // ← 只放了 username .setExpiration(generateExpired()) .signWith(SignatureAlgorithm.HS512, tokenSecret) .compact(); redisUtil.set(getTokenKey(username), token, 3600); // Redis 存一份 redisUtil.set(getUserKey(username), json, 3600); // 用户完整信息也存 Redis return token; } JWT 的 claims 字段本质上设计来承载结构化信息,签名保证不被篡改。但这里把它当成了一个随机字符串来用——JWT 里只放了 sub: "admin",userId、角色、权限全部丢进 Redis。 ...

二月 28, 2023 · 4 分钟 · 849 字 · yaomingye

Live2D 模型渲染崩溃:游戏提取动作文件的 TotalPointCount 元数据错误排查与修复

Live2D 调包侠踩坑记:一个「少算 44 个点」引发的渲染崩溃 故事的开端 某开发者最近在折腾网页 Live2D 看板娘。项目基于 naihe-live2d-widget-v3,它对标的是经典的 live2d-widget,但底层换成了最新的 Cubism SDK for Web v5,专门渲染 .moc3 格式的新版模型。 一切看起来都很顺利——直到把从游戏里提取的一个猫耳角色模型(编号 416)丢进去。 模型加载了,但动作一播放就崩。刷新,又崩。时好时坏,但大部分时间页面一片空白,控制台躺着一行红字: Uncaught (in promise) TypeError: Cannot set properties of undefined (setting 'time') 对调包侠来说,这可能就是那一刻想关电脑的信号。 定位问题:不是模型坏了,是 Meta 骗了 SDK 报错指向 live2d-sdk.js 中 parse 函数的某个 .time 赋值操作。顺着调用栈往上翻: parse → create → loadMotion → preLoadMotionGroup → setupModel → loadAssets ——动作文件解析时崩溃了。 📌 前置知识:Live2D 的 .motion3.json 文件描述了一条条动画曲线。每条曲线包含一串「段」(segments),每个段由若干个「控制点」(points)定义。Meta 段会预先声明总点数和总段数,方便 SDK 预分配内存。 某个开发者的直觉是:会不会是 Meta 里声明的数量跟实际数据对不上? 写了一段简单的验证脚本跑了一下: let calculatedPoints = 0; for (const curve of curves) { const segs = curve.Segments; let pos = 0, first = true; while (pos < segs.length) { if (first) { calculatedPoints++; pos += 2; first = false; } const segType = segs[pos]; switch (segType) { case 0: calculatedPoints++; pos += 3; break; // 线性 case 1: calculatedPoints += 3; pos += 7; break; // 贝塞尔 case 2: calculatedPoints++; pos += 3; break; // 步进 case 3: calculatedPoints++; pos += 3; break; // 反向步进 } } } 结果一看——全都对不上。 ...

二月 26, 2023 · 3 分钟 · 614 字 · yaomingye

B/S架构常见网络攻击与SpringBoot/Cloud防御实践——XSS、CSRF、SQL注入、DDoS与JWT安全的攻防图谱

B/S攻防:五类攻击与Spring体系的应对 📌 前置知识:本文假设读者用过 Spring Boot、知道 Cookie/Session/Token 的基本概念。Spring Security 的 Filter Chain 不熟没关系——每段防御代码会说明它在过滤器链中的位置。 XSS:当用户输入变成了可执行脚本 跨站脚本攻击(Cross-Site Scripting)的本质是:攻击者把 JavaScript 塞进用户输入,服务器原样输出到 HTML,浏览器执行了这段恶意脚本。 sequenceDiagram participant Attacker as 攻击者 participant Victim as 受害者浏览器 participant Server as 有漏洞的服务器 participant DB as 数据库 Attacker->>Server: "POST /comment 提交评论\n内容: script stealCookie()" Server->>DB: 存入评论(未做转义) Victim->>Server: GET /article?id=123 Server->>DB: 查询评论列表 DB-->>Server: 返回含脚本的评论 Server-->>Victim: "script stealCookie()\n浏览器解析并执行" Victim->>Attacker: "恶意脚本执行\nCookie 被发送到攻击者服务器" Spring Boot 的防御分三层: ① 输出转义——Thymeleaf 默认做 // Thymeleaf 模板中默认对变量做 HTML 转义 // <div th:text="${comment.content}"> → < 变成 &lt;,脚本失效 // 如果你用 JSP 或手动拼 HTML,务必用 escapeHtml: String safe = HtmlUtils.htmlEscape(userInput); ② 输入过滤——Spring 全局拦截 ...

二月 24, 2023 · 5 分钟 · 927 字 · yaomingye

JVM类加载与反射:从Class文件到运行时类的全景——Klass模型、类加载器、Metaspace与反射的桥接机制

反射凭什么认识你的类 某开发者写过这样一段代码,当时觉得平平无奇: Method method = obj.getClass().getDeclaredMethod("secretLogic", String.class); method.setAccessible(true); Object result = method.invoke(obj, "hacked!"); 运行完才反应过来——凭什么?一个 Class 对象拿在手里,连 private 方法都能翻出来调用,连参数签名都一清二楚。JVM 到底在背后存了什么东西,让反射可以"看见"一个类的全部内脏? 答案就在 HotSpot 的 Klass 模型 里。 📌 前置知识:本文假设读者知道 .class 文件是 javac 编译产物、JVM 基本内存分区(堆、栈、方法区)的常识。如果不清楚方法区和 Metaspace 的关系,后文有图。 全景先览:JVM 中的类在哪 在深入 Klass 之前,先看一张全局地图——一个类从 .class 文件到进入 JVM 运行时,到底经过了哪些区域。 这张图先有一个印象就行——关键记住一点:一个类被加载后,JVM 在 Metaspace 里存了一份"类模板"(InstanceKlass),在 Heap 里放了一个轻量的"Java 镜像"(Class 对象)。 反射读的元信息来自前者,你代码里 getClass() 拿到的是后者。 类模板:HotSpot 眼中的"类" 你写的每一个 Java 类,在 HotSpot 内部都有一个对应的 C++ 对象来描述它——这就是 Klass 模型。 Klass 继承链 Klass ← 所有类的抽象基类 ├── InstanceKlass ← 普通 Java 类(你写的 99% 的类) │ ├── InstanceMirrorKlass ← Class 对象本身(镜子) │ └── InstanceRefKlass ← 引用类型(软/弱/虚引用) ├── ArrayKlass ← 数组类 │ ├── TypeArrayKlass ← 基本类型数组(int[]) │ └── ObjArrayKlass ← 对象数组(String[]) ⚠️ 新手提示:Klass 不是 Class。Klass 是 C++ 层面的数据结构,存在 Metaspace; java.lang.Class 是 Java 层面的对象,存在 Heap。日常说"类模板"通常指 InstanceKlass。 ...

二月 22, 2023 · 3 分钟 · 590 字 · yaomingye

补偿机制:分布式系统中出事了我兜底的设计哲学——从本地回滚到Saga编排的完整实践

补偿:别等炸了才想兜底 某一天凌晨,运维群里弹出一条告警:订单服务返回码全是 500,错误日志里赫然写着 库存扣减失败,事务已提交。排查一圈发现——库存服务超时了,但订单服务的本地事务已经提交,用户钱扣了,货没发出去。 这不是什么玄幻剧情。只要你的系统一次操作涉及两个以上的外部依赖,它就一定会发生。 问题的根儿不在于某个服务挂了,而在于挂了之后没人善后。这就是补偿机制要解决的事。 📌 前置知识:本文假设读者已经知道数据库事务 ACID 的基本概念、分布式系统中"网络不可靠"的前提。如果对分布式事务的 2PC / TCC / Saga 还没概念,建议先翻一下本站的《分布式事务基础》和《TCC + Saga》两篇。 什么是补偿机制 先给个直接的定义: 补偿(Compensation) 是一系列操作,用于撤销一个已经部分执行或完全执行的业务流程,使系统回到业务上可接受的一致状态。 注意两个关键词: 撤销——不是 “取消”,是"对已经产生的副作用进行逆操作"。扣掉的库存加回去,冻结的额度解冻,发的优惠券标记作废。 业务上可接受——补偿之后的状态不一定等于执行之前的状态。比如退款流水里多了一条退款记录,这不是脏数据,这是业务可审计的中间态,本来就是设计的一部分。 补偿 ≠ 回滚(Rollback)。回滚是数据库层的物理操作,依赖 undo log,对业务透明;补偿是业务层的逻辑操作,需要开发者显式编写逆操作代码。 > ⚠️ 新手提示:把补偿理解成 Ctrl+Z 不准确。Ctrl+Z 是"回到上一步",补偿是"把已经造成的后果消弭掉"——相当于打翻了水杯,Ctrl+Z 是水自动回到杯子里(物理回滚),补偿是拿抹布擦干净桌子然后重新倒一杯(业务补救)。 补偿思维从本地就开始了 很多人觉得补偿是"分布式事务"才碰的东西,其实本地代码里到处都是补偿的影子,只是你没把它当成一个专门的概念。 场景一:文件操作的"撤销三部曲" public void processFile(String srcPath, String destPath) { File backupFile = null; File tempFile = null; try { // 步骤1:创建备份 backupFile = new File(srcPath + ".bak"); Files.copy(Path.of(srcPath), backupFile.toPath(), StandardCopyOption.REPLACE_EXISTING); // 步骤2:处理并写入临时文件 tempFile = new File(destPath + ".tmp"); try (var reader = new BufferedReader(new FileReader(srcPath)); var writer = new BufferedWriter(new FileWriter(tempFile))) { String line; while ((line = reader.readLine()) != null) { writer.write(transform(line)); writer.newLine(); } } // 步骤3:原子替换目标文件 Files.move(tempFile.toPath(), Path.of(destPath), StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); } catch (Exception e) { // 补偿逻辑:清掉所有中间产物 if (tempFile != null && tempFile.exists()) { tempFile.delete(); // 撤销步骤2 } if (backupFile != null && backupFile.exists()) { try { Files.move(backupFile.toPath(), Path.of(srcPath), StandardCopyOption.REPLACE_EXISTING); // 撤销步骤1 } catch (IOException ex) { log.error("连备份恢复都失败了,手动处理吧...", ex); } } throw new ProcessingException("文件处理失败,已尽力回滚", e); } } 这段代码没什么高深的,但仔细看它的结构——每执行一步,catch 里就有对应的逆操作。这就是补偿机制的最朴素形态: ...

二月 20, 2023 · 9 分钟 · 1854 字 · yaomingye
Cat Radio