Maven Surefire 测试中的 MalformedInputException:当 JVM 默认编码与 Nacos 配置编码不一致时

又见编码问题:mvn test 报 MalformedInputException,IDE 却好好的 某开发者最近在项目中遇到一个奇怪的现象:mvn test 跑 Spring Boot 集成测试时,应用上下文启动失败,控制台抛出一串 MalformedInputException: Input length = 1。但同样的代码,在 IDE 里直接点"运行"按钮,启动得丝般顺滑。 这看起来像是 Nacos 配置中心的问题——因为错误信息里提到了 nacos:mall-auth-api-dev.yaml 配置文件找不到。但用 curl 请求 Nacos API,配置明明存在,内容也是合法的 YAML。 到底是哪里出了问题? 问题复现 执行命令: mvn test -pl mall-auth 控制台输出类似: *************************** APPLICATION FAILED TO START *************************** Description: Config data resource 'NacosConfigDataResource{...}' via location 'nacos:mall-auth-api-dev.yaml' does not exist 但此刻如果打开浏览器访问 Nacos 控制台,或者用 curl 直接拉取: curl "http://localhost:8848/nacos/v1/cs/configs?dataId=..." 配置内容完整返回,HTTP 状态码 200。所以配置是存在的,但 Nacos 客户端在 Spring Boot 中读取失败了。 翻到堆栈深处,真正的异常是: Caused by: org.yaml.snakeyaml.error.YAMLException: java.nio.charset.MalformedInputException: Input length = 1 at com.alibaba.cloud.nacos.parser.NacosDataParserHandler.parseNacosData(...) at com.alibaba.cloud.nacos.configdata.NacosConfigDataLoader.pullConfig(...) Caused by: java.nio.charset.MalformedInputException: Input length = 1 at java.base/java.nio.charset.CoderResult.throwException(...) at java.base/sun.nio.cs.StreamDecoder.implRead(...) 这就清楚了——不是配置"不存在",而是配置内容解析时遇到了字符编码问题。SnakeYAML 在读取 YAML 时,接收到了一个无法解码的字节序列。 ...

六月 19, 2023 · 4 分钟 · 726 字 · yaomingye

把 React + Vite 项目搬到 Electron 去:改造、踩坑与调试

把 React 项目搬到 Electron,顺便让 PDF 导出不再闹心 某开发者手头有个 Vite + React + TypeScript 的简历编辑器项目,浏览器里跑得好好的,但每次要导出 PDF 都得先开新窗口、再唤出浏览器打印对话框、再手动取消「页眉和页脚」——这套操作重复多了真的会烦躁。于是决定把它改成 Electron 桌面应用。 这篇文章记录了改造全过程,顺便解决了国内下载 Electron 二进制的问题,以及几个常用的调试命令。 第 1 步:先确认你从什么起点出发 这次改造的对象是一个标准的 Vite + React + TypeScript 前端项目,没有用任何奇怪的自定义配置。如果你也是类似的项目结构,可以直接照着操作。 改造前后的技术栈对比: 改造前 改造后 Vite 8 开发服务器 Vite 8 + Electron 43 React 18 + TypeScript 不变 MUI v9 组件库 不变 window.print() 导出 PDF webContents.printToPDF() 原生导出 浏览器 localStorage 持久化 不变 + 原生「另存为」对话框 验证入口: 项目根目录应该有一个 package.json,内含 "scripts": { "dev": "vite" },项目本身能通过 npm run dev 正常启动。 ...

四月 5, 2023 · 5 分钟 · 974 字 · yaomingye

今日日报:文档分组、接口聚合、配置扫描

今日日报 干了什么 Swagger 文档三层分组 给 8 个微服务分了三个文档组——前端接口、后台接口、内部微服务接口。加了 emoji 前缀区分,各服务不再相互干扰。所有内部接口统一加了 /v1/internal/ 路径前缀。 BFF 聚合 之前 BFF 层基本是透传,今天把管理后台的控制器补全了。系统管理、商品管理、订单管理、营销管理、基础数据都有了对应的 BFF 控制器和 Swagger 文档。小程序那边原本就做了首页和商品详情的聚合,今天补了用户中心。 前端接口映射 扫了两个前端项目(小程序和管理后台),列了 50 多个 API 调用,逐个去后端代码里 grep 确认接口路径。发现了 4 个前端写了但后端不存在的接口,也确认了一批属于另一个系统的接口。 DTO 注解补齐 一天下来给 103 个 DTO 和 Entity 加了 @Schema 注解,类级和字段级都有,Swagger 文档不再是黑盒了。 Nacos 扫描 拉到了全部 10 个服务的 Nacos 配置,更新了配置文档,补了 BFF 服务的 template 文件。 README 瘦身 把 README 里超长的内容拆到 docs 目录下,加了架构改进建议文档,整理了 28 项待改进问题。 明日计划 继续推进遗留任务。

三月 18, 2023 · 1 分钟 · 64 字 · yaomingye

微服务文档重构:API 分组、BFF 聚合与文档拆迁

今天干了啥:分组、聚合、拆文档 今天没写啥牛逼的业务代码,大部分时间在跟文档和接口结构较劲。记个流水账。 一、发现 doc.html 不对劲 打开一个微服务的 Knife4j 文档页,下拉框里赫然列着七八个其他服务的分组。一点就 404,明摆着是隔壁服务跑这儿串门了。 第一反应是 knife4j 的配置问题,加了一堆 enableXxx: false ,重启——纹丝不动。后来发现 knife4j 基础 starter 压根没有跨服务聚合功能。真正的原因藏在 SpringDoc 的配置链路里,某些共享配置文件往 swagger-config 端点的 urls 字段里塞了外部的分组 URL。 最后在每个服务的本地 application.yml 把 springdoc 配置全写死,用本地覆盖干掉了注入。顺便把相关的配置说明写进系统设计文档了。 产出: 一篇踩坑博客 + 配置修复。 二、给接口文档分了三个层 之前所有微服务的接口都在一个组里,前端要看、后端也要看、微服务 Feign 调用也混在一起。这次给每个服务分了三个 Swagger 分组: 前端接口(给前端的) 后台接口(给管理后台的) 内部接口(给其他微服务调用的) 每个服务各自独立,不再相互干扰。 三个分组的接口在代码上也做了物理隔离——新建了 controller/internal/ 包,只给微服务间 Feign 调用用。顺便把原来跟前端接口冲突的方法也挪过去了,URL 统一加 /v1/internal/ 前缀,从根上避免路由冲突。 三、把前端需要的接口聚合到 BFF 层 之前前端写个页面经常要调好几个微服务,商品详情页调了 5 次接口。虽然 BFF 层之前就已经做了一些聚合(首页聚合、商品详情聚合、下单预览聚合),但管理后台这边基本还是透传状态。 给管理后台 BFF 加了两个聚合接口: 用户编辑页:一次查出用户信息 + 角色列表 + 部门树 + 岗位列表 商品编辑页:一次查出商品详情 + 分类树(品牌和单位的数据等后续补 Feign 客户端) 同时给两个 BFF 服务都加上了 Swagger 文档。以后前端重写,只看这两个 BFF 的文档就够了,不用再翻 8 个微服务的接口。 ...

三月 16, 2023 · 1 分钟 · 127 字 · yaomingye

Knife4j 文档聚合:微服务里那些「不请自来」的 API 分组

隔壁服务的 API,怎么跑我这儿来了? 某天启动 auth 服务,打开 http://localhost:8021/doc.html ,想看一眼自己刚调好的三个 API 分组——等等,下拉框里怎么还有 message、order 的分组?点过去全是 404,auth 服务上根本就没有这些接口。 flowchart LR subgraph USER["👤 开发者"] A(["打开 auth 的\ndoc.html"]) end subgraph ACTUAL["期望"] B([只显示自己\n3 个分组]) end subgraph REAL["现实"] C([显示了 8 个\n不同服务的分组]) end A --> B A --> C C --> D{other groups\ntap 404} D -->|yes| E[「这就很烦了」] 代码是同一套代码,springdoc 和 knife4j 版本都是统一管理的,为什么 auth 的文档页面里会出现其他服务的痕迹?某开发者决定,今天不修好不下班。 第一反应:去 knife4j 找配置 这种"在一个服务里看到另一个服务的 API",第一感觉就是 knife4j 的网关聚合功能 在作祟。毕竟 knife4j 有个专门的 knife4j-aggregation-spring-boot-starter ,专门用来在 gateway 上聚合所有微服务的文档。 二话不说,给每个服务的 application.yml 加上: knife4j: enableAggregation: false 重启,刷新——没变化,其他分组稳如泰山地挂在下拉框里。 ...

三月 14, 2023 · 3 分钟 · 607 字 · yaomingye

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

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
Cat Radio