ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

输入验证与数据清洗的七道防御关卡

输入验证与数据清洗的七道防御关卡 1. 项目概述为什么一个“wwwwww”能暴露整个系统的脆弱性你有没有遇到过这样的场景用户在注册表单里随手敲了六个w——“wwwwww”然后点击提交结果页面直接报错500后台日志里堆满红色异常栈或者更隐蔽一点数据入库后变成一堆乱码下游报表统计时总数突然少了三成排查三天才发现是某条记录的手机号字段被截断成了“138****wwwwww”。这不是段子这是我去年帮一家本地生活平台做系统健康度审计时亲眼看到的真实案例。那个“wwwwww”就是压垮骆驼的最后一根稻草。这个标题里的“wwwwww”本质上是个输入污染探针——它不携带业务语义却能精准触发系统在边界处理、类型校验、长度控制、编码转换、异常传播等环节的薄弱点。而“数据清洗”在这里绝不是Excel里点几下“删除重复值”那么简单它是贯穿数据生命周期的防御体系从用户键盘敲下的第一个字符开始到最终进入分析模型的结构化字段为止每一道关卡都必须有明确的验证规则和兜底策略。我见过太多团队把数据清洗当成ETL阶段的“收尾工作”结果上游系统持续喂入脏数据清洗脚本越写越臃肿最后变成没人敢动的“祖传代码”。真正健壮的输入验证与异常处理核心不在技术多炫酷而在责任边界是否清晰、失败路径是否可控、错误信息是否可追溯。比如“wwwwww”提交后系统应该立刻告诉用户“手机号格式不正确”而不是抛出“java.lang.StringIndexOutOfBoundsException: String index out of range: -1”这种连开发都得查半天的错误更不该让这条脏数据流进数据库再等着数据分析师在月度复盘会上拍桌子说“上个月的转化率数据全不准”。这篇文章要拆解的就是如何用最小成本、最可持续的方式在代码里埋下这道“防洪堤”。它适用于所有需要接收外部输入的场景Web表单、API接口、文件导入、爬虫解析、IoT设备上报……只要你不是在写Hello World就绕不开这个问题。2. 整体设计思路三层防御模型与“失败即信号”原则2.1 为什么不能只靠后端校验——客户端、网关、服务层的职责划分很多团队的默认做法是前端简单做个正则判断后端再用框架自带的注解比如Spring Boot的Valid扫一遍觉得这就够了。我试过把这种方案直接上线结果两周内收到27个用户投诉说“明明填了正确的身份证号系统却说格式错误”。一查发现前端用的正则没覆盖15位老身份证号而后端校验器又把18位号码里的X转成了小写x导致校验失败。问题根源不是技术不行而是把防御责任全压给单一环节忽略了各层能力的天然差异。我现在的标准架构是三层防御模型每层只做自己最擅长的事客户端层浏览器/APP负责“友好拦截”。用轻量级规则如手机号正则^1[3-9]\d{9}$、邮箱基础格式快速反馈避免无效请求消耗服务器资源。关键原则是宁可放过不可错杀。比如身份证号校验前端只检查长度和基本数字格式复杂逻辑留给后端。网关层API Gateway承担“流量过滤器”角色。这里不做业务逻辑校验而是统一处理协议级异常JSON格式是否合法、Content-Type是否匹配、请求体大小是否超限比如限制POST Body ≤ 2MB、高频恶意请求如1秒内连续提交10次“wwwwww”。我们用Kong网关配置了自定义Lua插件对所有POST请求的body做UTF-8编码检测遇到字符直接返回400省得脏数据进到业务服务里再崩溃。服务层业务微服务执行“终极校验”。这是唯一有权决定数据是否合法的地方必须包含语义校验如“订单金额不能为负数”、“收货地址不能是火星”关联校验如“优惠券有效期必须晚于下单时间”幂等性校验防止重复提交导致数据错乱异常标准化封装所有错误必须转成统一格式含traceId便于追踪提示千万别在网关层做业务校验我见过有团队在Nginx里用Lua写了一套完整的手机号归属地查询逻辑结果运营商一更新号段整个网关就挂了——业务逻辑侵入基础设施是系统稳定性的最大敌人。2.2 “失败即信号”异常不是Bug而是系统状态的诚实反馈传统思维里异常程序出错了要赶紧try-catch吞掉或者打个日志就完事。但在我经手的30个数据管道项目里83%的数据质量问题根源都是异常被静默处理。比如一个CSV解析器遇到非法日期“2023-02-30”如果只是跳过这一行并记个log那下游的销售预测模型就会少算一天的订单量误差会像滚雪球一样放大。我的解决方案是推行“失败即信号”原则任何校验失败或解析异常都必须产生可量化、可追踪、可告警的信号。具体落地分三步分类分级把异常按影响程度分三级阻断型Critical如数据库连接失败、核心校验规则缺失。必须立即停止流程触发告警。降级型Warning如用户头像URL无法访问、非必填字段格式错误。记录详细上下文继续处理但标记该记录为“待人工复核”。忽略型Info如空格替换、全角转半角。仅记录操作痕迹不干预流程。上下文绑定每个异常必须携带5个关键字段traceId链路追踪IDsource来源web/api/filerawData原始输入片段截取前100字符ruleName触发的校验规则名如“mobile_format_v2”suggestion给运维的修复建议如“检查手机号正则是否支持19x号段”信号闭环所有Warning级异常自动写入Elasticsearch配置Kibana看板实时监控“异常率趋势”。当某类异常1小时内超过阈值如“身份证校验失败率0.5%”自动触发企业微信机器人推送附带TOP5失败样本和定位链接。实测下来这套机制让数据问题平均响应时间从4.2小时缩短到17分钟。最关键是它改变了团队心态——不再把异常当麻烦而是当系统发出的健康体检报告。2.3 为什么拒绝“万能清洗函数”——领域驱动的数据契约网络上流传着各种“一行代码搞定数据清洗”的神器比如Python里df[phone].str.replace(r\D, , regexTrue)。这类函数在demo里很酷但放到生产环境就是灾难。去年有个电商客户用类似函数批量清洗收货电话结果把“北京市朝阳区建国路8号”里的“8号”也替换成空地址变成“北京市朝阳区建国路号”。根本问题在于清洗不是字符串操作而是对业务语义的还原。一个“手机号”字段它的契约包含格式约束11位数字以1开头语义约束必须能通过运营商号段校验上下文约束同一用户的多个订单收货电话应高度一致所以我坚持用“领域驱动清洗”为每个核心字段定义独立的清洗器Cleaner比如MobileNumberCleaner类它内部包含parse()从原始文本提取可能的号码支持“138-1234-5678”、“86 13812345678”等多种格式validate()调用第三方号段API验证有效性缓存结果避免频繁调用normalize()统一输出标准格式“13812345678”fallback()当所有规则失败时返回预设的兜底值如“00000000000”并标记is_fallbacktrue这样做的好处是当运营商新增19x号段时只需更新MobileNumberCleaner里的号段库所有调用它的服务自动生效完全不影响其他字段清洗逻辑。比全局正则替换安全十倍。3. 核心细节解析从“wwwwww”到结构化数据的7道关卡3.1 第一道关HTTP请求体解析——别让编码问题毁掉第一印象“wwwwww”问题常始于最底层的编码解析。比如用户用手机输入法粘贴了一段带emoji的地址后端用new String(bytes, ISO-8859-1)解码结果得到一堆?字符后续所有校验都失效。这不是代码bug而是协议理解偏差。真实场景中HTTP请求体的编码声明有三个来源优先级从高到低Content-Type头里的charset参数如Content-Type: application/json; charsetutf-8JSON文档自身的BOM标记UTF-8 BOM为EF BB BF服务器默认编码通常是UTF-8但必须显式声明我在Spring Boot项目里强制要求// 在WebMvcConfigurer中统一设置 Bean public HttpMessageConverterString stringHttpMessageConverter() { StringHttpMessageConverter converter new StringHttpMessageConverter(StandardCharsets.UTF_8); converter.setWriteAcceptCharset(false); // 禁止在Response头写charset由前端控制 return converter; }同时所有Controller方法必须用RequestBody(required true)禁止使用String直接接收原始body——因为Spring默认用ISO-8859-1解码遇到中文就变乱码。正确姿势是PostMapping(/order) public ResultOrder createOrder(Valid RequestBody OrderRequest request) { // Spring会自动用UTF-8解析JSON并完成Bean Validation }注意如果你用的是FastJSON务必检查ParserConfig.getGlobalInstance().setAutoTypeSupport(true)是否开启——这是反序列化漏洞的高危开关生产环境必须设为false。3.2 第二道关JSON Schema校验——用标准协议代替手写if-else很多团队用一堆if (obj.get(phone) null || !obj.get(phone).matches(PHONE_REGEX))做校验既难维护又易漏。我推荐用JSON Schema作为契约语言它把校验规则从代码里抽离出来变成可版本管理、可自动化测试的配置。以用户注册接口为例schema文件user-register.json{ $schema: https://json-schema.org/draft/2020-12/schema, type: object, properties: { mobile: { type: string, pattern: ^1[3-9]\\d{9}$, minLength: 11, maxLength: 11, description: 中国大陆手机号11位纯数字 }, idCard: { type: string, pattern: ^\\d{15}|\\d{17}[\\dXx]$, description: 身份证号15位或18位末位X可大小写 } }, required: [mobile], additionalProperties: false }Java端用json-schema-validator库校验// 初始化一次复用Schema对象 SchemaFactory factory SchemaFactory.createDraft202012SchemaFactory(); Schema schema factory.createSchema(schemaJson); // 校验时 SetValidationMessage errors schema.validate(jsonNode); if (!errors.isEmpty()) { throw new BadRequestException(JSON校验失败: errors.stream() .map(ValidationMessage::toString).collect(Collectors.joining(; ))); }优势非常明显前端可以用同一份schema生成表单校验规则API文档工具如Swagger能自动生成字段说明新增字段时只需修改schema无需改Java代码所有校验错误都带标准错误码如invalid_string_pattern方便前端统一处理3.3 第三道关领域对象构建——用Builder模式隔离脏数据即使JSON校验通过“wwwwww”也可能藏在字段值里。比如{mobile:wwwwww}正则^1[3-9]\d{9}$当然不匹配但错误信息是“手机号格式错误”用户根本不知道自己输错了什么。更好的方式是在构建领域对象时就完成深度清洗。我坚持用Builder模式创建实体public class User { private final String mobile; private final String idCard; private User(Builder builder) { this.mobile MobileNumberCleaner.normalize(builder.mobile); // 自动清洗 this.idCard IdCardCleaner.normalize(builder.idCard); } public static class Builder { private String mobile; private String idCard; public Builder mobile(String mobile) { this.mobile mobile; return this; } public User build() { // 构建时强制校验 if (!MobileNumberCleaner.isValid(mobile)) { throw new IllegalArgumentException(手机号无效: mobile); } return new User(this); } } }这样调用时try { User user new User.Builder() .mobile(wwwwww) .build(); // 这里直接抛出IllegalArgumentException } catch (IllegalArgumentException e) { // 返回给前端{code:400,message:手机号无效: wwww} }关键点在于清洗和校验必须发生在对象构建的瞬间而不是在Service方法里。这样能保证User对象一旦创建成功其字段就一定是干净、合规的后续所有业务逻辑都无需再做二次校验。3.4 第四道关数据库写入——用约束代替应用层校验很多人把所有校验都放在Java代码里结果数据库成了“信任盲区”。我见过最典型的案例用户服务校验了手机号格式但订单服务直接INSERT时没校验结果数据库里存了“abc123”这种脏数据。解决方案是把核心约束下沉到数据库字段加NOT NULL非空字段手机号字段用CHECK (mobile ~ ^1[3-9]\d{9}$)PostgreSQL支持正则约束身份证号字段加唯一索引防重复注册金额字段用DECIMAL(10,2)而非FLOAT避免精度丢失Spring Data JPA配置示例Entity Table(name users, uniqueConstraints UniqueConstraint(columnNames mobile)) public class UserEntity { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; Column(name mobile, nullable false, length 11) Check(constraints mobile ~ ^1[3-9]\\d{9}$) // Hibernate 6支持 private String mobile; Column(name amount, precision 10, scale 2) private BigDecimal amount; }实操心得数据库约束是最后一道物理防线但它不能替代应用层校验。因为约束失败会抛出ConstraintViolationException错误信息是数据库方言的如MySQL的“Duplicate entry”前端无法友好展示。所以必须在应用层先做一次校验数据库约束只是兜底。3.5 第五道关异步任务中的异常隔离——别让一个失败拖垮整条队列数据清洗常在异步任务里执行如MQ消费、定时批处理。这时“wwwwww”可能引发连锁反应一个消息解析失败导致整个消费者线程卡死积压数千条消息。我的标准做法是每个消息独立try-catch并实现死信队列DLQRabbitListener(queues data_clean_queue) public void handleDataClean(Message message) { try { CleanTask task objectMapper.readValue(message.getBody(), CleanTask.class); cleanerService.clean(task); } catch (Exception e) { // 记录完整上下文 log.error(清洗任务失败 [taskId:{}], 原始消息: {}, task.getId(), new String(message.getBody()), e); // 发送到DLQ供人工复核 rabbitTemplate.send(data_clean_dlq, message); } }DLQ里的消息要包含原始消息体Base64编码避免特殊字符破坏失败堆栈触发时间、处理耗时关联的traceId我们还做了个增强当DLQ消息数1小时内超过100条自动触发告警并暂停主队列消费避免问题扩散。等运维确认后再手动重发DLQ里的消息。3.6 第六道关文件导入清洗——用流式解析对抗内存炸弹Excel或CSV文件导入是最容易被“wwwwww”攻击的场景。用户上传一个1GB的Excel里面第10万行是“wwwwww”如果用Apache POI一次性加载JVM直接OOM。正确姿势是流式解析分块校验// 使用EasyExcel的SAX模式内存占用10MB EasyExcel.read(inputStream, UserData.class, new UserDataListener()) .sheet() // 不指定sheet名读取第一个sheet .doRead(); // UserDataListener里逐行处理 public class UserDataListener extends AnalysisEventListenerUserData { private final ListUserData batch new ArrayList(1000); Override public void invoke(UserData data, AnalysisContext context) { try { // 每行独立清洗校验 UserData cleaned userCleaner.clean(data); batch.add(cleaned); if (batch.size() 1000) { saveBatch(batch); batch.clear(); } } catch (ValidationException e) { // 记录错误行号和原因 errorLog.warn(第{}行数据异常: {}, context.readRowHolder().getRowIndex(), e.getMessage()); } } }关键技巧用AnalysisEventListener而非Data注解避免反射开销错误行号用context.readRowHolder().getRowIndex()获取精确到行批量保存时用JDBC Batch Insert性能提升5倍以上对超大文件增加进度回调前端显示“已处理12,456/50,000行”3.7 第七道关日志与监控——让每个“wwwwww”都留下指纹没有监控的清洗系统就像没有刹车的汽车。我要求所有清洗环节必须输出结构化日志字段包括event_type: input_validation / data_clean / db_persiststatus: success / warning / errorduration_ms: 处理耗时毫秒raw_value: 原始输入截取防日志爆炸cleaned_value: 清洗后值仅success时输出rule_name: 触发的规则名用Logback配置JSON日志appender nameJSON classch.qos.logback.core.rolling.RollingFileAppender encoder classnet.logstash.logback.encoder.LogstashEncoder/ filelogs/app.json/file rollingPolicy classch.qos.logback.core.rolling.TimeBasedRollingPolicy fileNamePatternlogs/app.%d{yyyy-MM-dd}.%i.json/fileNamePattern /rollingPolicy /appender再用Prometheus抓取关键指标clean_errors_total{rulemobile_format}手机号格式错误次数clean_duration_seconds_bucket{le0.1}清洗耗时分布dlq_messages_total死信队列积压量当clean_errors_total突增时Grafana自动标红并关联展示TOP5错误样本。运维点一下就能看到“最近10分钟327次失败98%是‘wwwwww’输入来源全是iOS端注册页”。4. 实操过程详解手把手实现一个可落地的清洗管道4.1 环境准备与依赖选型——为什么选这些而不是别的项目用Spring Boot 3.2 Java 17依赖清单精简到极致!-- 核心校验 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency !-- JSON Schema -- dependency groupIdcom.networknt/groupId artifactIdjson-schema-validator/artifactId version1.4.3/version /dependency !-- 数据库约束 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-jpa/artifactId /dependency dependency groupIdorg.postgresql/groupId artifactIdpostgresql/artifactId /dependency !-- 异步消息 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-amqp/artifactId /dependency !-- 文件解析 -- dependency groupIdcom.alibaba/groupId artifactIdeasyexcel/artifactId version3.1.1/version /dependency选型理由放弃Hibernate Validator的Email/NotBlank这些注解太弱不支持手机号号段校验、身份证算法校验等业务需求且错误信息固定无法定制。不用Jackson的JsonFormat它只做序列化格式转换不做业务校验且无法处理“138-1234-5678”这种带分隔符的输入。不选Apache POI内存占用大解析10MB Excel就要500MB堆内存EasyExcel的SAX模式内存恒定在10MB内。RabbitMQ而非Kafka清洗任务是典型“点对点”场景不需要Kafka的分区、副本等复杂特性RabbitMQ的DLQ机制更成熟。实操心得所有依赖必须满足“单一职责”原则。比如json-schema-validator只负责校验不负责解析MobileNumberCleaner只负责手机号不处理邮箱。这样未来替换某个组件时影响范围可控。4.2 核心清洗器实现——以手机号清洗为例的完整代码MobileNumberCleaner.java是整个管道的基石它必须做到可测试、可扩展、可监控。Component public class MobileNumberCleaner { // 号段库缓存30分钟刷新一次 private final LoadingCacheString, Boolean carrierCache Caffeine.newBuilder() .expireAfterWrite(30, TimeUnit.MINUTES) .build(this::checkCarrier); /** * 清洗并标准化手机号 * 支持格式13812345678、86 138 1234 5678、138-1234-5678、(138)1234-5678 */ public String normalize(String raw) { if (StringUtils.isBlank(raw)) { return null; } // 步骤1移除所有非数字字符保留号用于国际号识别 String digitsOnly raw.replaceAll([^\\d], ); // 步骤2处理国际号前缀 if (digitsOnly.startsWith(86)) { digitsOnly digitsOnly.substring(3); } else if (digitsOnly.startsWith(86)) { digitsOnly digitsOnly.substring(2); } // 步骤3长度校验 if (digitsOnly.length() ! 11) { throw new ValidationException(手机号长度必须为11位当前 digitsOnly.length()); } // 步骤4号段校验调用缓存 if (!carrierCache.getIfPresent(digitsOnly.substring(0, 3))) { throw new ValidationException(未知号段 digitsOnly.substring(0, 3)); } return digitsOnly; } /** * 快速校验不清洗 */ public boolean isValid(String raw) { try { normalize(raw); return true; } catch (ValidationException e) { return false; } } /** * 号段校验逻辑模拟调用第三方API */ private Boolean checkCarrier(String prefix) { // 实际项目中调用运营商号段服务 // 这里用白名单模拟 SetString validPrefixes Set.of(130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 145, 147, 149, 150, 151, 152, 153, 155, 156, 157, 158, 159, 170, 171, 172, 173, 174, 175, 176, 177, 178, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 195, 196, 197, 198, 199); return validPrefixes.contains(prefix); } }单元测试必须覆盖所有边界Test void shouldNormalizeMobileWithHyphens() { String result cleaner.normalize(138-1234-5678); assertEquals(13812345678, result); } Test void shouldThrowOnInvalidPrefix() { assertThrowsValidationException(() - cleaner.normalize(12312345678)); } Test void shouldHandleInternationalFormat() { String result cleaner.normalize(86 138 1234 5678); assertEquals(13812345678, result); }注意normalize()方法里没有用try-catch吞异常而是让异常向上抛出。因为清洗失败本身就是业务事件必须被上层捕获并处理。4.3 API层集成——Controller如何优雅暴露清洗能力Controller不是业务逻辑容器而是协议适配器。它只做三件事接收请求、调用清洗器、返回标准化响应。RestController RequestMapping(/api/v1/clean) public class CleanController { private final MobileNumberCleaner mobileCleaner; private final IdCardCleaner idCardCleaner; public CleanController(MobileNumberCleaner mobileCleaner, IdCardCleaner idCardCleaner) { this.mobileCleaner mobileCleaner; this.idCardCleaner idCardCleaner; } PostMapping(/mobile) public ResultString cleanMobile(RequestBody CleanRequest request) { try { String cleaned mobileCleaner.normalize(request.getInput()); return Result.success(cleaned); } catch (ValidationException e) { return Result.fail(400, 手机号格式错误: e.getMessage()); } } PostMapping(/idcard) public ResultString cleanIdCard(RequestBody CleanRequest request) { try { String cleaned idCardCleaner.normalize(request.getInput()); return Result.success(cleaned); } catch (ValidationException e) { return Result.fail(400, 身份证号格式错误: e.getMessage()); } } }Result类是统一响应体public class ResultT { private int code; private String message; private T data; private long timestamp System.currentTimeMillis(); public static T ResultT success(T data) { ResultT r new Result(); r.code 200; r.message success; r.data data; return r; } public static T ResultT fail(int code, String message) { ResultT r new Result(); r.code code; r.message message; return r; } }关键设计每个清洗能力单独Endpoint不搞“万能清洗接口”错误码严格遵循HTTP语义400表示客户端错误输入问题500表示服务端错误系统故障响应体永远包含timestamp方便前端计算处理耗时4.4 数据库约束实战——PostgreSQL的CHECK约束写法在application.yml里配置Hibernate DDLspring: jpa: hibernate: ddl-auto: validate # 生产环境必须用validate禁止create/update properties: hibernate: format_sql: true实体类上的约束注解Entity Table(name users) public class UserEntity { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; Column(name mobile, nullable false, length 11) Check(constraints mobile ~ ^[1-9]\\d{10}$) // PostgreSQL正则 private String mobile; Column(name created_at, updatable false) CreationTimestamp private LocalDateTime createdAt; // getter/setter... }手动在数据库里加约束Hibernate不支持所有CHECK-- 添加手机号号段约束 ALTER TABLE users ADD CONSTRAINT chk_mobile_prefix CHECK (mobile ~ ^1[3-9]\d{9}$); -- 添加身份证号约束15位或18位 ALTER TABLE users ADD CONSTRAINT chk_id_card CHECK (id_card ~ ^\d{15}$|^\d{17}[\dXx]$);验证约束是否生效-- 测试插入非法数据 INSERT INTO users (mobile) VALUES (wwwwww); -- 报错new row for relation users violates check constraint chk_mobile_prefix提示PostgreSQL的~操作符支持PCRE正则比MySQL的REGEXP更强大。但注意不要在CHECK里调用函数如length(mobile) 11会影响性能。4.5 监控告警配置——Grafana看板的关键指标我们用Micrometer暴露指标Prometheus抓取Grafana展示。核心看板包含清洗成功率看板指标查询语句说明成功率100 - (rate(clean_errors_total[1h]) / rate(clean_requests_total[1h]) * 100)全局成功率阈值99.5%各规则失败率rate(clean_errors_total{rulemobile_format}[1h]) / rate(clean_requests_total{rulemobile_format}[1h])定位具体规则问题性能瓶颈看板指标查询语句说明P95耗时histogram_quantile(0.95, sum(rate(clean_duration_seconds_bucket[1h])) by (le, rule))查看慢规则内存占用jvm_memory_used_bytes{areaheap}防止EasyExcel内存泄漏DLQ监控看板指标查询语句说明DLQ积压rabbitmq_queue_messages{queuedata_clean_dlq}超过100条触发告警DLQ错误类型count by (error_type) (rabbitmq_queue_messages{queuedata_clean_dlq})分析错误分布告警规则示例Prometheus Alert Rules- alert: HighCleanErrorRate expr: 100 * (rate(clean_errors_total[1h]) / rate(clean_requests_total[1h])) 1 for: 5m labels: severity: warning annotations: summary: 清洗错误率过高 description: 过去1小时清洗错误率 {{ $value }}%超过阈值1% - alert: DLQBacklogHigh expr: rabbitmq_queue_messages{queuedata_clean_dlq} 100 for: 10m labels: severity: critical annotations: summary: 死信队列积压严重 description: DLQ消息数 {{ $value }}请立即处理5. 常见问题与排查技巧实录那些踩过的坑和血泪经验5.1 问题速查表从现象到根因的快速定位现象可能根因排查步骤解决方案用户提交“13812345678”报“格式错误”前端JS正则与后端Java正则引擎差异如$在Java里需双写$$1. 抓包看前端发送的原始值2. 在Controller入口打日志打印request.getBody()3. 对比前后端正则表达式统一用Java正则前端用new RegExp()动态生成CSV导入时中文变乱码Tomcat默认用ISO-8859-1解码multipart1. 检查server.tomcat.uri-encodingUTF-8配置2. 在MultipartConfigElement里设characterEncoding(UTF-8)Spring Boot 2.3默认已修复旧版本
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进