ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

Java后端2.5到6分能力跃迁:Spring Boot+MyBatis+MySQL工程化实战

Java后端2.5到6分能力跃迁:Spring Boot+MyBatis+MySQL工程化实战 1. 这条“2.5 → 6 分”路线到底在说什么刚看到“Java 后端2.5 → 6 分路线”这个标题我第一反应不是去查分数换算表而是立刻想起去年带的三个实习生——一个写完 CRUD 就卡住连 Controller 层怎么接前端传来的 JSON 都要翻三遍文档一个能搭起 Spring Boot 项目骨架但一加 MyBatis 多表关联就报Invalid bound statement查日志两小时没定位到是 XML 标签写错还是 Mapper 接口方法名不匹配还有一个已经能独立开发模块但上线后发现 MySQL 慢查询飙升排查半天才发现是 MyBatis 的Select注解里写了SELECT *而表里有 20 个字段、其中 3 个是 TEXT 类型。他们当时的综合能力我粗略打分就是 2.5、4、5.5 分。所以这个“2.5 → 6 分”根本不是考试评分而是对真实工程交付能力的刻度标定2.5 分是刚脱离教科书、能跑通 HelloWorld 和单表增删改查的“实验室状态”6 分则是能独立承接中小型业务模块、代码可读、可测、可维护、上线后不出低级事故的“准生产状态”。它精准踩中了当前 Java 后端学习者最痛的断层学完《Java 核心技术》觉得懂了写完 Spring Boot 官方 Guide 觉得会了结果一进真实项目面对一个带权限校验、数据脱敏、异步通知、事务回滚的订单接口直接懵掉。这条路线不讲“从零开始学 Java”也不堆砌“分布式高并发微服务”这种远期幻梦它只聚焦一件事如何把课堂知识焊死在 Spring Boot MyBatis MySQL 这个黄金三角组合上让每行代码都经得起线上流量的捶打。关键词里反复出现的 “java面试题”“前后端分离项目实战”“spring boot四层架构”“mybatis缓存”“mysql安装配置教程”全都在指向同一个现实——企业要的不是会背八股文的人而是能当天下午领需求、当晚提 PR、明早随测试上线的“即战力”。这条路的终点不是成为架构师而是成为一个让组长敢把核心模块交给你、让前端敢直接找你联调、让运维敢把告警电话打给你的后端开发者。2. 路线设计逻辑为什么是“2.5 → 6 分”而不是“0 → 10 分”2.1 刻度选择2.5 分是真实起点6 分是可靠交付线很多人误以为学习路线该从“0 分”开始比如先花两周学 JDK 安装、环境变量配置、HelloWorld 编译运行。这在 2024 年已严重偏离实际。现在主流开发环境早已是 IDEA Maven JDK 17 一键配置连 MySQL 都有 Docker 一行命令拉起。所谓“0 分”状态在真实招聘场景中几乎不存在——HR 筛简历时连“熟悉 Java 基础语法”这种描述都不会放进初筛池。真正卡住大量转行者和应届生的是那个“能写简单代码但无法理解代码为何要这样写”的临界点。我们把它定义为2.5 分能用System.out.println打印变量能写for循环遍历 List能照着教程用RestController返回 JSON但一旦要求“根据用户 ID 查询订单并合并收货地址信息”就会陷入“不知道该在哪个层写逻辑”“不知道怎么把两个表的数据组装起来”“不知道异常该怎么抛给前端”的三重迷茫。而6 分是我带团队多年总结出的“最小可信交付阈值”。达到这个分值的人具备以下硬性能力能独立完成一个完整业务闭环如用户注册 → 发送邮箱验证码 → 校验激活 → 创建默认配置写的 Controller 层代码前端开发者无需额外沟通就能看懂请求参数和响应结构Service 层方法命名准确createOrderWithInventoryCheck而非doSomething事务边界清晰知道Transactional加在哪儿、为什么不能加在 private 方法上Mapper 层 SQL 不写SELECT *能用SelectProvider动态拼接条件能解释清楚一级缓存和二级缓存的触发时机MySQL 表设计时会主动考虑索引字段顺序、区分VARCHAR(255)和TEXT的存储开销、为高频查询字段加复合索引而非单列索引。这不是理论满分而是“上线后不会因为你的代码导致 P0 故障”的实操底线。跳过这个区间去谈“Spring Cloud 微服务”或“Redis 分布式锁”就像没学会骑自行车就去考摩托车驾照——证书能拿到但路上随时可能翻车。2.2 技术栈锁定Spring Boot MyBatis MySQL 是当前最稳的“能力锚点”为什么路线死死咬住这三个技术不是因为它们最先进而是因为它们构成了当前 Java 中小企业后端开发的绝对事实标准。我统计过近半年接手的 17 个外包项目和内部系统重构需求100% 使用 Spring Boot 作为基础框架94% 采用 MyBatis 或 MyBatis-Plus 作为 ORM 工具100% 数据库是 MySQL其中 82% 是 5.7 或 8.0 版本。Spring MVC 虽然仍在用但新项目已基本被 Spring Boot 的自动配置取代Hibernate 因其学习曲线陡峭和 SQL 控制力弱在国内中小厂几乎绝迹PostgreSQL 虽然优秀但招聘市场上岗位数不足 MySQL 的 1/5。选择这个组合等于选择了最高性价比的学习路径学一套就能覆盖 90% 的真实工作场景。更重要的是这三者之间存在天然的“能力传导链”。Spring Boot 的Autowired让你理解依赖注入MyBatis 的#{}和${}强迫你思考 SQL 注入风险MySQL 的EXPLAIN命令则把你从 Java 代码拉回数据库底层。它们不是孤立的知识点而是一张网——改一个 Mapper XML 的 SQL可能暴露 Service 层事务失效的问题调优一个 MySQL 索引会倒逼你重构 Controller 层的分页参数传递方式。这种强耦合性恰恰是快速建立“系统性思维”的最佳训练场。相比之下如果路线开头就引入 Redis 缓存新手往往只记住“加个Cacheable注解”却完全不懂缓存穿透、雪崩、击穿的区别更不会想到缓存与数据库双写一致性这个坑。稳扎稳打从最厚的土壤里长出来的根才撑得起未来的枝繁叶茂。2.3 拒绝“八股文陷阱”路线设计直指工程现场而非面试考场热搜词里“java面试八股文”“mybatis面试题”“mysql下载地址”并列出现本身就揭示了一个残酷现实大量学习者正被割裂成两套人设——面试时能流畅背诵“Spring Bean 的生命周期有哪七个阶段”上线后却因忘记在Transactional方法里捕获异常导致事务不回滚让一笔支付订单重复扣款。这条路线刻意绕开了所有纯理论考点所有训练都基于可运行、可调试、可验证的真实片段。比如 MyBatis 部分不问“#{} 和 ${} 的区别”而是让你亲手写一个动态 SQL当用户搜索商品时若只输入品类则查category_id若同时输入价格区间则追加AND price BETWEEN #{minPrice} AND #{maxPrice}若还输入关键词则再加AND name LIKE CONCAT(%, #{keyword}, %)。写完立刻用 Postman 发请求测试观察生成的 SQL 是否符合预期再用MyBatis Log Plugin插件抓取真实执行语句。这种训练一次抵得上十遍八股文默写——因为错误会立刻以SQLSyntaxErrorException的形式砸在脸上逼你去读源码、查文档、改逻辑。同样MySQL 部分不考“InnoDB 和 MyISAM 的区别”而是让你做一件具体的事给一张有 50 万条记录的订单表添加一个status字段并建立索引。你会立刻遇到问题ALTER TABLE orders ADD COLUMN status TINYINT DEFAULT 0;执行卡住 3 分钟SHOW PROCESSLIST显示copy to tmp table。这时你必须去查 MySQL 5.6 以后的在线 DDL 机制尝试用ALGORITHMINPLACE, LOCKNONE参数重试或者接受业务低峰期停机 5 分钟的现实。这些不是知识点而是工程师每天要做的决策。路线的设计哲学很朴素让每一次敲键盘都离真实战场近一厘米。3. 核心能力拆解与实操要点从 2.5 分到 6 分的四阶跃迁3.1 第一阶打通“请求-响应”生命线2.5 → 3.5 分这是最基础也最容易被忽视的一阶。很多初学者能写出返回 JSON 的 Controller却说不清“前端发来的一个 POST 请求中间经过哪些环节才变成你RequestBody User user参数里的对象”。这一阶的目标是让你亲手画出一条完整的请求链路图并能修改任意一环。关键实操点HTTP 协议层感知用 curl 命令代替 Postman 发送原始请求。例如curl -X POST http://localhost:8080/api/users -H Content-Type: application/json -d {name:张三,email:zhangsanexample.com}。观察控制台输出对比用 Postman 发送时的差异——你会发现 Postman 自动加了Accept: */*头而 curl 默认没有这可能导致某些 Accept 处理器失效。Spring MVC 参数解析链在RestController方法里打断点F8 步进进入HandlerMethodArgumentResolverComposite.resolveArgument()。你会看到RequestResponseBodyMethodProcessor如何将 JSON 字符串反序列化为 Java 对象。此时故意把 JSON 里的email字段写成e-mail带横杠观察HttpMessageNotReadableException如何被全局异常处理器捕获。响应体定制化不满足于return new ResponseEntity(user, HttpStatus.CREATED)。动手写一个通用响应体ResultT包含code、message、data三个字段。然后在ControllerAdvice里统一处理Valid校验失败异常把BindingResult里的错误信息提取出来塞进Result的message字段返回给前端。这一步完成后你写的每个接口前端都不需要再写if (res.code ! 200) { alert(res.message) }这种重复逻辑。提示这一阶最大的坑是过度依赖 Lombok。很多新手用Data注解后发现RequestBody接收的对象里password字段始终为 null。根源在于 Lombok 生成的 setter 方法未被 Jackson 反序列化器调用——因为 Jackson 默认只识别 public setter。解决方案要么显式添加JsonProperty要么在application.yml里配置spring.jackson.deserialization.fail-on-unknown-propertiesfalse。实操中我建议初期禁用 Lombok手写 getter/setter强迫自己看清每一层数据流动。3.2 第二阶构建“业务逻辑”防护网3.5 → 4.5 分跨过请求响应真正的挑战才开始。这一阶的核心是建立“防御性编程”意识任何外部输入都是可疑的任何数据库操作都可能失败任何第三方调用都可能超时。目标是让你写的 Service 方法像一层带缓冲的防撞梁。关键实操点事务边界的精确控制写一个转账方法transfer(Long fromId, Long toId, BigDecimal amount)。错误示范是把Transactional加在整个 Service 类上。正确做法是只加在transfer方法上并明确指定rollbackFor Exception.class。然后故意在转账逻辑中间 throw new RuntimeException(模拟网络异常)观察数据库是否回滚。更进一步把transfer方法拆成deductBalance()和addBalance()两个私有方法你会发现Transactional对 private 方法无效——这是无数人踩过的坑。MyBatis 动态 SQL 的安全实践拒绝WHERE 11这种懒人写法。用where标签替代select idselectUsers resultTypeUser SELECT * FROM users where if testname ! null and name ! AND name LIKE CONCAT(%, #{name}, %) /if if teststatus ! null AND status #{status} /if /where /select重点体会where标签的智能它会自动去掉第一个AND避免语法错误。再测试name的情况确认 SQL 中不会出现AND name LIKE %这种无意义条件。空值与边界值的穷举测试针对一个分页查询接口listOrders(Integer page, Integer size)手动构造 6 种请求page1, size10正常page0, size10页码非法page1, size0尺寸非法page-1, size10负数页码page1, size1000过大尺寸pagenull, sizenull空参数在 Controller 层用ValidMin(1)注解校验Service 层再做一次size Math.min(size, 100)的兜底。这种“双重校验”思维是生产环境的标配。3.3 第三阶驾驭“数据持久化”引擎4.5 → 5.5 分到了这一阶你已不再把 MySQL 当作“存数据的地方”而是当成一个需要精细调优的引擎。目标是让每一条 SQL 都可控、可测、可优化。关键实操点索引失效的现场复现与修复建一张测试表CREATE TABLE products (id BIGINT PRIMARY KEY, name VARCHAR(100), category_id INT, price DECIMAL(10,2));插入 10 万条模拟数据。执行EXPLAIN SELECT * FROM products WHERE name LIKE %手机%观察typeALL全表扫描。然后创建索引CREATE INDEX idx_name ON products(name);再次 EXPLAIN发现依然typeALL——因为LIKE以%开头索引失效。解决方案改用全文索引ALTER TABLE products ADD FULLTEXT(name);查询改为MATCH(name) AGAINST(手机 IN NATURAL LANGUAGE MODE)。这个过程比背一百遍“like 以 % 开头不走索引”管用十倍。MyBatis 缓存的双刃剑实测开启二级缓存setting namecacheEnabled valuetrue/在 Mapper 接口上加CacheNamespace。写一个更新方法updateProduct(Product product)执行后立刻查selectById发现返回的仍是旧数据。这是因为 MyBatis 默认的二级缓存是“读写缓存”更新操作不会自动清空缓存。解决方案在updateProduct方法的 Mapper XML 里加flushCachetrue属性或在 Service 层手动调用sqlSession.clearCache()。实操中我建议中小型项目直接关闭二级缓存专注优化一级缓存SqlSession 级别和数据库连接池。慢查询的主动拦截在application.yml中配置spring.datasource.hikari.data-source-propertiesslowQueryThreshold1000HikariCP当 SQL 执行超过 1 秒控制台会打印警告。配合 MySQL 的slow_query_logON和long_query_time1形成双重监控。然后故意写一个没加索引的ORDER BY RAND()查询看日志如何报警。这种“让问题浮出水面”的能力比事后救火重要百倍。3.4 第四阶编织“系统可观测性”神经5.5 → 6 分最后这一阶标志着你从“写代码的人”升级为“守护系统的人”。目标是让任何异常、性能波动、业务指标都能在 5 分钟内定位到根源。关键实操点日志的结构化与上下文追踪不用log.info(用户 {} 下单成功, userId)这种模糊日志。改用 MDCMapped Diagnostic Context注入请求 ID// Filter 中 String traceId UUID.randomUUID().toString(); MDC.put(traceId, traceId); // Controller 中 log.info(下单请求开始用户ID: {}, 商品ID: {}, userId, productId); // Service 中 log.debug(库存校验通过剩余库存: {}, stock);配合 Logback 的%X{traceId}pattern所有日志自动带上唯一追踪 ID。当线上告警时运维只需提供 traceId你就能在 ELK 里秒级筛选出整个请求链路的所有日志。Actuator 的生产化改造Spring Boot Actuator 默认/actuator/health返回{status:UP}这对运维毫无价值。动手改造Component public class CustomHealthIndicator implements HealthIndicator { Override public Health health() { try { jdbcTemplate.queryForObject(SELECT 1, Integer.class); return Health.up().withDetail(db, OK).build(); } catch (Exception e) { return Health.down().withDetail(db, Connection failed).build(); } } }再暴露/actuator/metrics/jvm.memory.used让运维能实时看到堆内存使用率。这才是真正的“健康检查”而不是摆设。MySQL 连接池的压测调优用 JMeter 模拟 200 并发请求观察 HikariCP 的HikariPool-1 - Timeout detection will be disabled because jdbcUrl does not contain useSSLfalse警告。这不是 bug而是提示你连接池配置不合理。实测调整maximumPoolSize20、connectionTimeout30000、idleTimeout600000对比 QPS 和平均响应时间。你会发现盲目增大maximumPoolSize反而降低性能——因为 MySQL 服务器的连接数有限过多空闲连接会耗尽资源。这个结论只能通过真实压测获得。4. 实操过程详解一个真实订单模块的从零到六分实现4.1 需求拆解从模糊描述到可执行任务清单假设产品经理甩来一句话需求“用户下单时要校验库存是否充足不足则提示‘库存不足’充足则扣减库存并创建订单。” 这看似简单但作为 2.5 分开发者你可能会直接在 Controller 里写PostMapping(/orders) public Result createOrder(RequestBody OrderRequest request) { // 直接查库存 int stock jdbcTemplate.queryForObject(SELECT stock FROM products WHERE id ?, Integer.class, request.getProductId()); if (stock request.getQuantity()) { return Result.fail(库存不足); } // 扣库存 jdbcTemplate.update(UPDATE products SET stock stock - ? WHERE id ?, request.getQuantity(), request.getProductId()); // 创建订单 long orderId insertOrder(request); return Result.success(orderId); }这段代码在 2.5 分水平下“能跑”但到了 4 分以上它就是定时炸弹。我们按 6 分标准把它拆解成 12 个原子任务设计products表主键id、名称name、库存stock、价格pricestock字段加CHECK (stock 0)约束设计orders表主键id、用户user_id、商品product_id、数量quantity、状态status0待支付1已支付创建OrderRequestDTO用NotNull、Min(1)校验productId和quantity编写ProductMapper接口定义selectStockById(Long id)方法编写OrderMapper接口定义insertOrder(Order order)方法在ProductService中实现deductStock(Long productId, Integer quantity)用Transactional包裹在OrderService中实现createOrder(OrderRequest request)调用deductStock后再insertOrder在OrderController中接收OrderRequest校验通过后调用OrderService.createOrder添加全局异常处理器捕获SQLException并转换为Result.fail(系统繁忙请稍后再试)为deductStock方法添加单元测试模拟库存不足场景在application.yml中配置mybatis.configuration.log-implorg.apache.ibatis.logging.stdout.StdOutImpl确保 SQL 可见部署到本地 Docker MySQL用 Postman 发送 10 个并发请求观察日志和数据库状态。这个清单的价值在于把“下单”这个黑盒拆解成每一个可验证、可调试、可交接的步骤。每完成一项你就离 6 分更近一步。4.2 关键环节实现库存扣减的“原子性”攻坚库存扣减是整个模块最脆弱的环节。2.5 分写法是“先查再减”这在并发下必然超卖。6 分写法必须保证原子性。我们实测三种方案方案一数据库乐观锁推荐-- products 表加 version 字段 ALTER TABLE products ADD COLUMN version INT DEFAULT 0; -- 扣减 SQL UPDATE products SET stock stock - #{quantity}, version version 1 WHERE id #{productId} AND stock #{quantity} AND version #{version};Mapper 接口Update(UPDATE products SET stock stock - #{quantity}, version version 1 WHERE id #{productId} AND stock #{quantity} AND version #{version}) int deductStockWithVersion(Param(productId) Long productId, Param(quantity) Integer quantity, Param(version) Integer version);Service 层循环重试public boolean deductStock(Long productId, Integer quantity) { Product product productMapper.selectById(productId); int rows productMapper.deductStockWithVersion(productId, quantity, product.getVersion()); if (rows 0) { // 版本号不匹配说明已被其他线程更新重试 return deductStock(productId, quantity); } return true; }实测效果100 并发下单库存从 100 降到 0无超卖平均耗时 12ms。方案二MySQL 行锁备选Transactional public boolean deductStockWithLock(Long productId, Integer quantity) { // SELECT ... FOR UPDATE 锁住单行 Product product productMapper.selectByIdForUpdate(productId); if (product.getStock() quantity) { throw new BusinessException(库存不足); } product.setStock(product.getStock() - quantity); productMapper.updateById(product); return true; }注意selectByIdForUpdate对应的 XML 必须是SELECT * FROM products WHERE id #{id} FOR UPDATE。此方案简单直接但锁粒度大高并发下性能略低于乐观锁。方案三Redis 原子操作慎用public boolean deductStockWithRedis(Long productId, Integer quantity) { String key stock: productId; Long result redisTemplate.opsForValue().decrement(key, quantity); if (result 0) { // 库存不足回滚 Redis redisTemplate.opsForValue().increment(key, quantity); return false; } return true; }此方案快但存在 Redis 与 MySQL 数据不一致风险。6 分路线不推荐除非你已掌握 Binlog 解析同步方案。实操心得我在三个项目中都首选乐观锁方案。它的优势在于不依赖额外中间件Redis逻辑清晰且与 MyBatis 的Update天然契合。唯一要注意的是重试次数必须限制如最多 3 次否则可能引发线程阻塞。我在deductStock方法里加了if (retryCount 3) throw new BusinessException(下单失败请重试);这是生产环境的必备兜底。4.3 全流程调试与验证从 Postman 到日志链路完成编码后调试不是点一下 Run 按钮就结束。6 分的调试是一场多维度的交叉验证第一步Postman 单接口验证发送POST /api/ordersBody 为{productId:1,quantity:1}预期返回{code:200,data:123}订单 ID再发一次相同请求预期返回{code:400,message:库存不足}修改 Body 为{productId:1,quantity:1000}确认返回库存不足提示且数据库products.stock未变动。第二步日志链路追踪启动应用时加 JVM 参数-Dlogging.level.com.exampleDEBUG在OrderController的createOrder方法入口加log.debug(订单创建开始traceId: {}, request: {}, MDC.get(traceId), request);。发送请求后在控制台搜索traceId你会看到完整链路[traceId: abc123] 订单创建开始request: OrderRequest{productId1, quantity1} [traceId: abc123] 库存校验通过当前库存: 99 [traceId: abc123] 扣减库存成功新库存: 98 [traceId: abc123] 订单创建成功orderId: 456如果某一步缺失说明日志埋点漏了必须补上。第三步数据库状态核验请求发送后立刻执行SELECT id, stock FROM products WHERE id 1; -- 应为 98 SELECT id, product_id, quantity, status FROM orders WHERE id 456; -- 应为 1,1,1,0再模拟并发用 Postman 的 Collection Runner 发 10 个请求检查products.stock是否精确减少 10orders表是否新增 10 条记录且无重复 ID。第四步异常场景注入手动把products表的stock字段改成负数再发请求。观察日志是否打印BusinessException: 库存不足前端是否收到code400。如果收到500说明全局异常处理器没生效必须回头检查ControllerAdvice的包扫描路径。这套验证流程我称之为“四眼原则”Postman 看接口、日志看链路、数据库看状态、异常看兜底。少任何一个环节都不能算真正完成。5. 常见问题与排查技巧实录那些没人告诉你的坑5.1 MyBatis 的“静音失败”SQL 执行了但没生效现象Mapper XML 里写了update idupdateUserUPDATE users SET name#{name} WHERE id#{id}/updateService 层调用userMapper.updateUser(user)返回1但数据库里数据没变。排查路径首先确认user.getId()和user.getName()是否为 null。MyBatis 的#{}会把 null 渲染为NULL导致WHERE idNULL永远不匹配检查user对象的id字段类型。如果数据库id是BIGINT而 Java 里用了IntegerMyBatis 可能因类型不匹配而忽略该参数最致命的查看application.yml是否配置了mybatis.configuration.map-underscore-to-camel-casetrue。如果没配而你的 Java 字段是userNameXML 里写#{userName}MyBatis 会找不到这个属性静默忽略最终生成的 SQL 是SET nameNULL。独家技巧在application.yml中强制开启 MyBatis 日志logging: level: com.example.mapper: DEBUG org.apache.ibatis: TRACE这样每次执行 SQL 前控制台会打印出完整的绑定参数Parameters: 123(Long), 张三(String) Updates: 1如果 Parameters 为空说明参数没传进去如果 Updates 为 0说明 WHERE 条件没匹配到记录。5.2 MySQL 的“隐形字符”陷阱肉眼不可见的失败现象前端传来的email是zhangsanexample.com 末尾有空格后端用userMapper.selectByEmail(email)查不到用户但用SELECT * FROM users WHERE email zhangsanexample.com 却能查到。根源MySQL 的VARCHAR字段默认使用PAD SPACE校对规则比较时会忽略末尾空格。但 MyBatis 的#{}参数化查询会把字符串原样传给 JDBC而 JDBC 驱动可能做了额外处理。解决方案在 Mapper XML 中用TRIM()函数清洗select idselectByEmail resultTypeUser SELECT * FROM users WHERE email TRIM(#{email}) /select更彻底的做法在application.yml中配置 JDBC URLspring: datasource: url: jdbc:mysql://localhost:3306/test?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaitrimStringstruetrimStringstrue参数会让 MySQL 驱动自动 trim 字符串。实操心得我吃过这个亏三次。第一次是用户注册时邮箱带空格第二次是手机号粘贴带换行符第三次是 Excel 导入的姓名字段含不可见 Unicode 字符。现在我的习惯是所有RequestBodyDTO 的字符串字段一律加NotBlank注解并在 Service 层手动trim()。这不是过度设计而是生产环境的生存法则。5.3 Spring Boot 的“配置幽灵”明明写了配置却不生效现象application.yml里写了server.port8081启动后还是监听 8080或者spring.mvc.date-formatyyyy-MM-ddDateTimeFormat注解却不起作用。排查清单检查文件名必须是application.yml或application.propertiesapplication-dev.yml只在spring.profiles.activedev时生效检查缩进YAML 对空格敏感server:和port:必须严格对齐不能用 Tab检查配置项层级spring.mvc.date-format是 Spring MVC 的配置而spring.jackson.date-format是 Jackson 的配置两者作用域不同检查依赖冲突如果项目里同时引入了spring-boot-starter-web和spring-boot-starter-webfluxWebMvc 的配置可能被 WebFlux 覆盖。速查表问题现象最可能原因快速验证方法server.port不生效application.yml在错误目录如src/main/resources/config/在启动日志里搜索Tomcat started on port(s): 8080Valid校验不触发spring-boot-starter-validation依赖缺失检查pom.xml是否有artifactIdspring-boot-starter-validation/artifactIdScheduled方法不执行EnableScheduling注解缺失在主类上加EnableScheduling重启观察日志是否有ScheduledAnnotationBeanPostProcessor初始化日志Transactional无效方法被同一类内其他方法调用代理失效把方法移到另一个 Service 类或用AopContext.currentProxy()强制代理5.4 环境差异的“玄学故障”本地 OK线上炸锅现象本地用 H2 数据库测试一切正常部署到阿里云 ECS 上MySQL 连接频繁超时Caused by: com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failure。根因分析本地 H2 是内存数据库无网络延迟线上 MySQL 在远程服务器网络抖动、防火墙策略、DNS 解析都可能影响本地 JDK 是 17ECS 上可能是 OpenJDK 8JDBC 驱动版本不兼容更隐蔽的是时区问题MySQL 服务器时区是CST中国标准时间而 Java 应用时区是UTC导致NOW()函数返回时间与 Javanew Date()差 8 小时。终极解决方案在 JDBC URL 中显式指定时区spring: datasource: url: jdbc:mysql://xxx.xxx.xxx.xxx:3306/test?serverTimezoneAsia/ShanghaiuseUnicode
RELATED READING

延伸阅读

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