ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

Spring Boot + MyBatis + Maven + MySQL:后端开发到部署完整链路实战

Spring Boot + MyBatis + Maven + MySQL:后端开发到部署完整链路实战 搞后端开发的但凡你经历过几个真实项目应该都有这种感觉框架层面的东西越学越多但真正能让你把项目从零跑起来、部署上线、并且保证线上不崩的往往是几个最基础的工具链。最近不少朋友在后台问我说自己看了很多Spring Boot和微服务的教程真到要动手做一个能用的后端项目时反而卡在环境配置、依赖冲突、数据库连不上这些“低级问题”上。这篇内容我就拿Spring Boot MyBatis Maven MySQL这套最经典的后端组合把从开发到部署的完整链路重新捋一遍把我实际项目中踩过的坑、用过的配置、以及为什么这么做的逻辑都讲透。这篇东西适合谁一个是刚学完Java基础、准备做第一个完整后端项目的初学者另一个是已经会写CRUD但没系统梳理过整套工具链的初中级开发者。我把这套技术栈当成一条完整流水线来讲Maven管依赖和构建Spring Boot管应用骨架MyBatis管数据库操作MySQL管数据存储最后再讲怎么打包部署。你会看到的不只是配置代码而是每个环节背后真实项目里会遇到的取舍和问题。1. 先搞明白一件事这套技术栈为什么至今还是后端主力很多新人在选技术栈的时候容易陷入一个误区觉得“别人都在用的一定是最先进的”。可实际上Spring Boot MyBatis Maven MySQL这套组合之所以到现在还是国内后端项目的主力配置不是因为它新而是因为它恰好卡在了一个“够用、可控、好招人、好维护”的平衡点上。1.1 四个组件各自扮演什么角色先把这个组合拆开来看每个组件解决的是明确的一层问题Maven是项目的基础设施管理者。它管着三件事项目依赖的下载与版本管理、项目的构建打包流程、多模块项目的工程结构。没有Maven你就要自己到处找jar包、手动丢进lib目录、还要自己拼classpath几乎等于回到了原始社会。Spring Boot是应用框架。它解决的是“如何快速构建一个可运行的Java应用”的问题。内嵌Tomcat、自动配置、约定优于配置这些特性让你不再需要为了跑起来一个Web应用去配置一堆XML。MyBatis是数据访问层框架。它解决的是“Java代码如何跟数据库交互”的问题。它把SQL映射、参数绑定、结果集映射做成了一套半自动化的机制既保留了你对SQL的完全控制权又帮你省掉了JDBC里最枯燥的样板代码。MySQL是数据存储层。稳定、开源、生态成熟、网上资料多对这个组合来说是最稳妥的选择。这套组合的一个特点就是“每层都有明确的边界和替代方案”。任何一层出了问题你都能清楚地定位到是依赖管理的问题、框架配置的问题、SQL映射的问题、还是数据库本身的问题。1.2 为什么不用JPA/HibernateMyBatis还值得选吗这是我在社区里被问得最多的问题。说实话 JPA/Hibernate的自动建表和对象关系映射确实省代码但我在实际项目里遇到的情况是一旦业务SQL变得复杂比如多表联查、动态条件拼接、复杂统计报表Hibernate的抽象层反而会变成阻碍。你要么去学一套复杂的HQL和Criteria API要么最终还是会被迫去写native SQL——那还不如一开始就用MyBatis。MyBatis的核心思路很朴素SQL还是你自己写框架只负责帮你把参数传进去、把结果封装回来。这意味着什么意味着任何数据库层面的优化手段比如分页SQL、条件索引利用、连表查询结构调整都完全在你的掌控范围之内。国内大量涉及财务、订单、报表类业务的项目选MyBatis就是因为它能让你在对SQL有绝对控制权的前提下依然保持开发效率。1.3 什么样的项目最适合用这套技术栈我从实际经验出发说三类典型场景第一类是传统的管理后台系统比如用户管理、权限管理、订单管理这种重CRUD的业务。这类系统SQL模式相对固定用MyBatis写起来很直接。第二类是业务规则复杂、SQL灵活多变的中小型项目。比如电商系统的商品筛选、多条件组合查询用MyBatis的动态SQL在XML里写条件拼接调试起来非常直观。第三类是团队技术栈统一、要求快速交付的创业项目。Spring Boot的快速启动能力加上MyBatis的低学习成本能让一个三人小组在一周内就把一个可演示的MVP跑起来。当然如果你的项目是纯数据模型驱动、实体关系特别多而且变化频繁JPA会更有优势。但就目前国内就业市场的需求和团队维护成本来说Spring Boot MyBatis依然是投入产出比最高的选择。2. 环境搭建与配置落地Maven和MySQL别急着写代码先把根基打牢我见过太多人项目跑不起来最后排查半天发现是Maven的settings.xml不对或者MySQL的时区配置有问题。这部分我按真实开发环境中“应该这样做”的标准来梳理。2.1 Maven的本地仓库与阿里云仓库镜像配置Maven默认的中央仓库在国外国内网络环境下拉依赖经常超时。所以拿到Maven之后第一件事就是配置镜像。打开Maven安装目录下conf/settings.xml在mirrors标签里加上mirror idaliyunmaven/id mirrorOfcentral/mirrorOf name阿里云公共仓库/name urlhttps://maven.aliyun.com/repository/public/url /mirrormirrorOf这里写central的意思是只对中央仓库走这个镜像其他第三方仓库不受影响。这个细节要注意有些人为了省事写成*结果把所有仓库请求都强制走阿里云反而会出问题。再说本地仓库。默认位置在~/.m2/repository我个人建议在settings.xml中显式配置到一个独立目录比如localRepositoryD:/dev/maven-repo/localRepository这样做的原因有两个一是系统盘空间有限jar包常年累积能有几十个G二是如果哪天需要重装系统或迁移环境直接把这个目录拷走就行不用重新下载一遍依赖。2.2 MySQL 5.7与8.0安装配置中容易翻车的细节MySQL的安装过程网上教程一大堆但有几个点教程经常不写清楚。第一个是选择合适的版本。如果是老项目5.7依然是很多企业生产环境的主流版本如果是新项目直接用8.0因为8.0的默认字符集是utf8mb4排序规则和JSON支持都更完善。第二个是安装模式。Windows环境我推荐用MySQL Installer按向导安装关键是在安装类型里选Server only不要装那些Example和Documentation省时间也没用。Linux环境直接用包管理器安装之后记得执行mysql_secure_installation做基础安全配置把匿名用户删掉、设置root密码、禁用root远程登录。第三个是时区和字符集问题。Spring Boot连接MySQL时报错The server time zone value Öйú±ê׼ʱ¼ä is unrecognized十有八九是时区没设置。可以在MySQL配置文件my.iniWindows或/etc/my.cnfLinux中加入[mysqld] character-set-serverutf8mb4 collation-serverutf8mb4_unicode_ci default-time-zone08:00这里character-set-serverutf8mb4是必须的它能正确存储四字节的Emoji字符default-time-zone设置成东八区避免代码里用serverTimezoneAsia/Shanghai这种硬编码方式依赖连接串参数。2.3 Spring Boot MyBatis整合的最小配置创建一个Spring Boot项目我习惯用2.7.x版本稳定且生态兼容性好如果你用3.x就得注意JDK版本必须17在pom.xml中引入核心依赖parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.2/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency /dependencies这里注意一点mybatis-spring-boot-starter的版本不能直接从Spring Boot的依赖管理里继承必须要显式声明版本号。我用的2.3.2是兼容Spring Boot 2.x最后几个版本的最佳选择。然后是application.yml中的核心配置spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/your_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: your_password mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.demo.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImplmap-underscore-to-camel-case: true这一行很关键它能把数据库的user_name字段自动映射成Java对象里的userName省掉大量resultMap手写映射。2.4 依赖报错与本地仓库混乱的处理思路Maven依赖报错是日常开发里最烦的问题之一。我总结一个排查顺序先看IDEA的Maven面板有没有红色波浪线然后看本地仓库里对应的jar包目录是否完整再检查settings.xml里有没有配置了冲突的mirror或者proxy。还有一个特殊情况如果你机器上有多个本地仓库目录比如公司电脑上原有C:/Users/name/.m2/repository新配置了D:/dev/maven-repo你会发现不少项目依赖仍然尝试从旧仓库拉取。这时候最简单的做法不是手动去合并两个仓库而是把旧的repository目录里的_remote.repositories和.lastUpdated文件清理掉然后执行mvn clean让它重新解析。如果实在依赖混乱得厉害干脆删除旧仓库目录重新下载一劳永逸。3. MyBatis核心机制拆解SQL映射、缓存与SQL排查这些才是面试和实战的分水岭很多人用MyBatis只停留在“照着模板写Mapper接口和XML”的程度一旦遇到缓存脏数据、SQL拼接报错、或者性能问题就抓瞎。这部分我把MyBatis内部的工作原理讲透。3.1 Mapper接口与XML绑定的底层原理Mapper接口本身没有实现类那Spring容器在启动的时候是怎么给这个接口创建出代理对象的核心机制是MapperProxy。MyBatis启动时通过MapperScan扫描指定包下的接口然后为每个接口生成MapperProxyFactory这个工厂会创建一个基于JDK动态代理的代理对象。当你调用接口方法时代理对象会根据方法名去查找对应的MappedStatement这个MappedStatement就是从XML文件解析出来的SQL模板。所以有个很常见的错误值得说一下接口方法名必须和XML中select标签的id完全一致包括命名空间前缀否则启动或调用时会报Invalid bound statement (not found)。我看到很多人用IDEA的快捷键自动生成Mapper接口后XML中namespace写错了包路径结果就是所有查询全部报错。排查时优先检查两处XML文件的namespace是否是接口的全限定名、mapper-locations配置是否覆盖到了XML文件所在目录。3.2 一级缓存与二级缓存什么时候有用、什么时候是坑这是MyBatis面试题里少不了的内容但实战中的理解更重要。一级缓存是SqlSession级别的默认开启。同一个SqlSession里执行两次相同的查询第二次会直接返回缓存对象不会查数据库。但在Spring整合环境下每次请求通常会创建新的SqlSession所以一级缓存的实际作用范围很有限。它真正要注意的坑是如果你在一个SqlSession里先查了用户列表然后往用户表插入了数据再查同一个用户列表MyBatis默认不会自动清缓存你拿到的可能还是旧数据。Spring整合环境下因为SqlSession生命周期短这个问题不太明显但还是要建立这个意识。二级缓存是namespace级别的默认关闭。开启方式是Mapper XML中加cache/标签。二级缓存能跨SqlSession共享查询结果但坑非常大如果你的Mapper里有插入、更新操作默认情况下二级缓存会失效反而导致频繁的缓存清空重建如果是多表关联查询关联表的数据更新了本表的二级缓存不会被清掉就会产生脏数据。我的建议是默认不要开二级缓存。项目中的数据一致性要求普遍较高二级缓存带来的性能提升在中小型项目里几乎感知不到但脏数据的风险随时可能炸。真要缓存热点数据用Redis做应用层缓存可控性高得多。3.3 SQL打印与慢SQL定位的实操方法配置里加log-impl: org.apache.ibatis.logging.stdout.StdOutImpl就能把SQL打印到控制台。但要注意生产环境别这样配太吵了而且会泄露SQL结构。更推荐的做法是只打印参数占位符的日志logging: level: com.example.demo.mapper: debug这样MyBatis会打印 Preparing:和 Parameters:两行前者是SQL模板后者是实际参数值方便你在本地排查参数绑定问题。遇到慢SQL我的排查套路是先打开MySQL慢查询日志或者直接在Navicat里EXPLAIN执行计划分析。重点看type列如果是ALL就是全表扫描必须加索引如果是range或ref说明索引利用得还行还要留意Extra列里有没有Using filesort有的话大概率排序字段没建索引。3.4 动态SQL的常见细节#{}和${}的区别永远要记住动态SQL是MyBatis最强大的能力没有之一。if、where、foreach、set这些标签组合起来可以胜任绝大多数复杂查询。但有个原则性问题必须刻在脑子里#{}是预编译占位符MyBatis会把它替换成?然后通过PreparedStatement传参能防SQL注入${}是字符串直接拼接相当于直接把值拼进SQL里存在注入风险。所以规则就是所有用户传入的值一律用#{}只有表名、列名这种数据库结构对象才考虑用${}并且要经过白名单校验。还有一种容易翻车的情况动态SQL里判断参数等于某个值时如果参数是数字类型直接用teststatus 1没问题如果是字符串类型必须写成teststatus 1单双引号顺序搞反会导致OGNL表达式直接报错。另外我强烈推荐在XML里使用where标签而不是手动写where 11。虽然where 11也能工作但这会导致SQL永远有一个恒真的条件一旦影响查询优化器对索引的选择效率就下来了。where标签会自动去掉第一个条件前面的AND或OR既干净又安全。4. 从单机Demo到前后端分离项目接口设计、跨域、分层与工程化很多人做练习项目时没有工程化意识Controller里直接注入Mapper、所有逻辑堆在Service里、返回结果没有统一封装。真实项目里这样写代码基本撑不过第一个迭代。4.1 后端项目的典型分层结构与包组织我的标准分层是四层Controller接口层、Service业务逻辑层、Mapper数据访问层、Entity/VO/DTO数据模型层。包结构大概长这样com.example.project ├── controller ├── service │ └── impl ├── mapper ├── entity ├── dto ├── vo ├── config └── commonController只负责接收参数、调用Service、返回结果里面不写任何业务逻辑。Service层写业务规则如果业务复杂就拆成service接口和service.impl实现类。Mapper接口只做数据访问。common包放统一返回结果Result、异常处理器、工具类。好处是清晰的职责边界让团队协作不会冲突出了问题也能快速定位是入参问题、业务问题还是SQL问题。我见过很多初级项目把所有代码堆在Controller里初始开发快但后续每次需求变更都在同一个类里打补丁最后变成一个两千行的怪物类。4.2 前后端分离项目里跨域问题怎么处理最稳妥前后端分离后前端跑在http://localhost:8080后端跑在http://localhost:9090浏览器的同源策略就会拦截跨域请求这就是热词里“后端跨域”问题的来源。后端解决跨域的方式有几种CrossOrigin注解、CorsFilter过滤器和全局CORS配置。最佳实践是全局配置因为一个项目通常有几十个Controller给每个都加注解太啰嗦。我一般这么做Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }注意allowedOriginPatterns在Spring Boot 2.4及以上版本替代了allowedOrigins因为后者在allowCredentials(true)时会拒绝带来源的跨域请求。另外跨域预检请求OPTIONS一定要允许否则前端会报“CORS preflight did not succeed”。还有一种情况前后端最终会部署在同一个域名的不同路径下比如前端在/、后端在/api这种就可以完全不用跨域配置靠Nginx反向代理解决。4.3 业务实战示例基于Spring Boot的大学生就业推荐系统最近热搜词里有个很典型的场景——基于Spring Boot的大学生就业推荐系统这类项目从技术角度看就是一套标准的多表关联推荐筛选逻辑。我拿它来演示如何把前面这些技术落进真实业务。用户表user、简历表resume、职位表position、投递记录表application就是四个基础表。职位推荐的核心SQL是一个多条件动态查询根据学生的专业方向、技能标签、期望城市、薪资范围来查职位。对应的Mapper XML大概长这样select idselectRecommendPositions resultTypecom.example.project.vo.PositionVO SELECT p.id, p.title, p.city, p.salary_min, p.salary_max, p.industry, (SELECT COUNT(*) FROM application a WHERE a.position_id p.id) AS apply_count FROM position p where if testmajor ! null and major ! AND p.major_required LIKE CONCAT(%, #{major}, %) /if if testcity ! null and city ! AND p.city #{city} /if if testminSalary ! null AND p.salary_max gt; #{minSalary} /if /where ORDER BY p.create_time DESC /select这个例子里有几个细节值得说和在XML里需要转义成gt;和lt;不然XML解析会直接报错LIKE CONCAT(%, #{major}, %)比直接写LIKE %${major}%安全得多既防注入又能利用索引优化。这个案例也说明所谓“推荐系统”在后端起步阶段往往不是复杂算法而是多条件组合筛选和排序先把数据查对、查快比什么都重要。4.4 事务、异常与统一返回这些工程细节决定了系统能不能撑住Service层方法上加上Transactional注解可以让方法内多个数据库操作处于同一个事务中任何一个失败都会整体回滚。但要注意三点一是事务只对RuntimeException默认回滚checked exception需要显式配置rollbackFor Exception.class二是Transactional只对public方法生效Spring AOP切不到private方法三是不要在同一个类内部调用带事务注解的方法因为代理对象调用链问题会导致事务失效。统一返回结果我也建议从一开始就做好。定义一个ResultT类里面包含code、message、data三个字段Controller把所有接口返回都包成Result。前端拿到响应后先判断code是否为200再处理业务数据。这个习惯养成之后接口联调和前端对接都能省不少事。全局异常处理器用RestControllerAdvice实现把参数校验异常、业务异常、未知异常分别处理前端看到的就是结构统一的错误信息而不是异常堆栈。5. 部署上线打包、配置分离与线上故障排查代码写完只是第一步真正考验功底的是把项目安全部署到服务器上并且稳定运行。5.1 用Maven打包Spring Boot项目并正确启动jar包Maven打包只需要执行mvn clean package -DskipTests这里-DskipTests跳过测试用例编译和运行打包速度会快很多。打包完成后target目录下会生成两个文件your-project-0.0.1-SNAPSHOT.jar和your-project-0.0.1-SNAPSHOT.jar.original。前者是Spring Boot的可执行jar内含所有依赖和内嵌Tomcat后者是普通jar。部署用前者。启动命令我推荐写成nohup java -Xms256m -Xmx512m -jar your-project.jar --spring.profiles.activeprod app.log 21 解释一下这个命令nohup让进程在终端关闭后继续运行-Xms256m -Xmx512m设置JVM初始堆内存和最大堆内存这是根据服务器配置来的512M只是小项目起步值--spring.profiles.activeprod指定激活生产环境配置 app.log 21把日志输出重定向到文件最后的让命令在后台执行。5.2 环境配置分离开发环境别连生产数据库千万不要在生产环境里改同一个application.yml。正确做法是拆分多环境配置文件application.yml application-dev.yml application-prod.ymlapplication.yml里只放公共配置比如端口号、应用名。开发环境配置写进application-dev.yml放本地数据库账号和调试日志。生产配置文件application-prod.yml里放生产数据库连接串、生产日志级别、连接池参数。启动时通过--spring.profiles.activedev或prod指定用哪套配置。这样操作的好处是避免了开发过程中误连生产库的灾难性事故。这个坑我踩过一次当时在本地调试代码配置文件里连的是生产库结果测试数据全部写进了生产环境后面清理数据花了整整一下午。5.3 服务器部署环境的几个关键检查点服务器上部署Spring Boot项目的常见部署架构是Nginx做反向代理和静态资源服务后端jar通过systemd或者shell脚本管理。这里的几个关键点我提一下第一端口别被防火墙拦了。service iptables stop不是生产环境该做的事正确做法是只放行Nginx端口比如80/443后端端口8080只允许内网访问对外请求统一走Nginx代理到后端。第二数据库连接串上的服务器地址别写错。如果你把MySQL装在Docker容器里宿主机访问容器内MySQL的地址是localhost但需要映射端口另一个容器访问它就要用Docker网络内的服务名或者容器IP。热搜词里的“访问docker容器内的mysql”指的就是这类问题。解决方式无非就是端口映射和服务名解析关键是先确认网络模式。第三JVM参数要根据服务器内存设置。1核2G的云服务器给-Xmx512m差不多给到1G很容易因为系统内存不足导致OOM甚至服务被杀掉。5.4 上线后最常见的问题与快速定位手段服务部署完不代表结束我列举几个线上高频问题和处理经验。日志文件里看到Access denied for user第一反应不是改配置重试而是确认数据库账号是否有该库的权限以及密码是否包含特殊字符导致连接串里URL编码出问题。看到Communications link failure多半是数据库地址不通或者MySQL服务没起来。可以先在服务器上执行telnet 数据库IP 3306测端口通不通再考虑代码问题。如果接口响应慢先把SQL打印开起来看慢SQL然后确认是不是有大量慢查询堆积在数据库端。我惯用的排查手段是把MyBatis的SQL日志加上耗时统计定位是哪一条SQL拖慢了响应。还有一个常见问题Bean named xxxMapper is expected to be of type之类的报错。这通常是Mapper接口和XML的绑定问题或者是MapperScan扫描了多个包导致Bean冲突。排查方式是把MapperScan的包范围缩小并且在启动类上加SpringBootApplication后确认包路径是否正确。写在最后的一点经验如果你现在正在照着教程搭建自己的Spring Boot MyBatis项目我个人最想给你的一条建议是遇到报错时不要急着去复制网上的配置。“为什么”比“怎么改”重要得多。比如MyBatis报找不到Mapper绑定你知道了它是通过动态代理加命名空间匹配去定位SQL的以后就再也不会犯这个错了。这套技术栈看起来简单但真正拉开差距的恰恰是对这些基础机制的熟悉程度。把这篇内容里的每个环节自己动手做一遍比刷二十个教程有效得多。
RELATED READING

延伸阅读

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