1. 把 Spring Boot 放进你的技术栈:先搞清楚它到底解决了什么问题
如果你是刚接触 Java 后端,或者从其他语言转过来,可能已经听过不少人说“Spring Boot 很简单”“跟着文档敲一遍就会了”之类的话。这些说法没毛病,但往往让人忽略一个更重要的问题:Spring Boot 到底解决的是什么问题?它凭什么能成为绝大多数 Java 后端项目的起步框架?
我个人的理解是,Spring Boot 最大的价值不在于它引入了什么新概念,而在于它把 Java 后端开发里那些繁琐、重复、容易出错的基础设施工作给自动化了。以前写一个 Java Web 项目,你得手动配置一大堆 XML 文件,管理各种依赖的版本,再把 Tomcat 单独装到服务器上,把打好的 WAR 包丢进去。现在有了 Spring Boot,这些事绝大多数都不需要你操心了。你只需要引入一个依赖,写一个主类,就能把一个 Web 服务跑起来,内嵌的 Tomcat 已经帮你准备好了。
这意味着什么?意味着你从“Hello World”到“做一个能上线的服务”,中间那条路被大幅缩短了。你可以把更多的精力放在业务逻辑本身,而不是在环境配置上消耗掉一整天。
还有一点很重要的,是 Spring Boot 背后的“约定优于配置”思想。框架默认帮你做了一套最合理的配置,你不需要在项目初期就理解每一个细节,只需要按照它的约定去放代码,就能得到一个行为正确的服务。比如你的配置文件放在src/main/resources/application.properties,比如你的主类放在根包下面,它就能自动扫描到所有的组件。
对于零基础的同学,我的建议是:不要一上来就纠结底层原理,先把服务跑起来,再回头去理解为什么。你前期不需要搞懂自动配置到底是怎么生效的,不需要理解@SpringBootApplication这个注解背后那几百行源码。你只需要知道,照着标准姿势写,这个东西就能跑,靠着这份信心把第一个接口做出来,你后续的学习动力才会有。
我见过很多初学者倒在第一步的原因,往往不是学不会,而是在“看了一堆原理、却一行代码都没跑过”的状态里反复横跳。这就像背了一晚上游泳理论,第二天跳进水里照样呛水。Spring Boot 的学习曲线真正陡峭的地方,是你开始脱离教程、自己面对一个未知需求的时候,而前期上手反而是整个过程中最平滑的一段。
2. 从零到“Hello World”:用最小成本把第一个服务跑起来
2.1 项目搭建前必做的环境准备
先把话说清楚:这部分没有任何捷径,但你也不需要准备太多东西。你只需要 JDK(我推荐 8 或 11,稳定且生态最成熟)、一个 IDEA(社区版足够用),以及本地的 Maven 环境。
JDK 安装这个事,网上教程很多,我只提两个容易踩的坑。第一个是环境变量配置,JAVA_HOME必须指向 JDK 的安装目录,而不是 JRE 目录,弄反了会导致很多工具识别不到编译器。第二个是版本问题,如果你之前电脑上装过其他 Java 程序,可能有多个 JDK 混在一起,建议在命令行里执行java -version确认当前生效的版本,避免 IDEA 里跑的和你以为的不是同一个。
IDEA 方面,新手别一上来就去折腾破解、插件美化这些事。社区版已经够你学到微服务阶段的前半程了,无非就是少了部分企业版的功能,对单机开发和调试没有影响。至于 Maven,如果你用的是 IDEA 自带的绑定版本,正常情况下不用额外安装,只需要确认settings.xml里的镜像源配置好了。国内开发如果不配置镜像源,下载依赖的速度会非常折磨人,这个我相信每个经历过的人都懂。
2.2 用 Spring Initializr 生成基础工程
在 Spring 官网的 Spring Initializr 上,你可以像填表单一样生成一个基础工程。这一步的意义,很多教程没讲透:它帮你做的事,是把 Maven 的pom.xml写好,把所有依赖按正确版本组装好,再生成一个标准的目录结构。你不需要自己一个字一个字敲这些内容,但你应该去看看它们长什么样。
Generate 之后你会得到一个压缩包,包含下面这些核心内容:
src/main/java src/main/resources pom.xml在 IDEA 里选择 Open 定位到解压后的目录,等它把依赖索引建好,整个环境就算齐活了。第一次加载会慢,因为 Maven 要从远程仓库拉一大批 jar 包,这个时候去刷会儿手机是正常的,不用焦虑。
不过我要多说一句:生成的工程默认带了一个测试类,也就是XXXXApplicationTests。这个类在mvn test的时候会加载完整的 Spring 上下文。如果你后续改了很多配置,而某个配置写错了,跑测试时会直接失败。我建议新手不要一上来就删掉它,留着它其实是一个很好的健康检查工具,每次项目改动之后跑一下,至少能证明你的 Spring 容器是能正常启动的。
2.3 第一个接口背后到底发生了什么
新建一个包,比如叫controller,在里面写一个类:
@RestController public class HelloController { @GetMapping("/hello") public String sayHello() { return "Hello, Spring Boot!"; } }这时候你启动应用,浏览器里访问http://localhost:8080/hello,就能看到返回的字符串了。整个过程看起来无比简单,但如果你只停留在“能跑就行”的程度,后面会吃亏。我建议你把下面这个问题搞清楚:一个 HTTP 请求从进入你的服务,到返回结果,中间经历了什么?
路径大致是这样的:浏览器发起请求,到达内嵌的 Tomcat → Tomcat 把请求交给 Spring MVC 的分发器 → 分发器根据 URL 匹配到你的sayHello方法 → 方法执行完返回字符串 → Spring 帮你把字符串序列化写入 HTTP 响应 → 浏览器渲染显示。
这中间的每一步,Spring Boot 都用默认配置帮你兜底了。@RestController背后是@Controller加@ResponseBody,意味着这个类的每个方法返回值都会直接作为 HTTP 响应体写入返回内容,而不是跳转到一个界面。@GetMapping则是把 GET 请求和特定方法做绑定。理解了这层,你再去看那些五花八门的注解,就不会觉得它们是一个个孤立的魔法符号了。
2.4 新手最容易写错的路径匹配和参数类型
我见过不少初学者卡在同一个问题上:接口地址访问了,却报 404 或者 405。出现这种情况,绝大多数是下面两种原因:
第一种是路径大小写或拼写问题。/hello和/Hello是两个完全不同的路径,Spring MVC 的默认匹配是区分大小写的。
第二种是请求方式不匹配。你定义了@GetMapping,然后用 POST 去访问,自然报 405。尤其是很多同学喜欢用 Postman 测试接口,工具里默认请求方式是 GET,如果换了 POST 访问 GET 接口,就会一脸懵。
传递参数时,最常见的问题是类型转换异常。比如接口定义为@RequestParam Integer age,你传abc进去,Spring 在做字符串转 Integer 时直接抛异常,返回 400。严格来说是前端参数没传对,但后端如果没有任何校验和兜底,这个错误信息在日志里会非常难排查。所以从第一天写接口,就要养成“对参数做校验”的习惯。
新手阶段还有一个高频困惑:@RequestParam、@PathVariable、@RequestBody到底什么时候用哪个?我总结了一个最朴素的记忆方式:@PathVariable用在 RESTful 风格的路径里,比如/user/123中的123;@RequestParam用在 URL 问号后面的查询参数,比如/user?id=123;@RequestBody用来接收前端放在请求体里的 JSON 数据。三种方式没有绝对的好坏,看你的接口设计和团队规范来选,但区别一定要分清。
提示:哪怕是一个只有几行的 Hello 接口,也值得我们用 Postman 或者 curl 去测一遍完整的请求链路。这能帮你提前熟悉接口调试工具,后面写几十个接口时效率会高很多。
3. 工程结构设计与分层思路:决定你是否能走到“可上线”的关键节点
3.1 别再一包到底:包结构的可扩展性比你想象的更重要
很多人把 Spring Boot 工程弄成一个巨大的 controller 包,所有类都往里塞。写的时候当然爽,但等到接口数量超过 50 个、逻辑开始复杂,你会发现:找一个类要翻半天,改一个字段要全局搜,新来的同事根本不敢动代码。
我推荐从一开始就按“功能模块”分包,而不是按“技术层”分包。比如你做一个用户模块和一个订单模块,那就建user和order两个顶层包,每个包内部再按controller、service、repository去组织。这样做的好处是:模块之间的边界天然清晰,想拆分成独立服务时只需要把整个包复制出去即可。
Java 里包的访问权限控制也会因此受益。假如你在user包下定义了某个类,它默认只对同包下的其他类可见,这能帮你减少不小心的跨模块依赖。
不过这里也要提醒一句:分包只是手段,不是目的。过度分包一样会让你陷入“找一个类要翻五六层目录”的困境。我的判断标准很简单:如果两个类的变更原因高度相关,它们就应该靠得很近。比如UserController和UserService几乎总是同时修改,那把它们的包路径放在一起,比把它们按 controller/service 分开更合理。
3.2 Controller、Service、Repository 各司其职
分层架构这个概念,学的时候觉得抽象,真正写起来你就会发现,它的本质是“把代码按职责分离,让每一层只做一件事”。
Controller 负责接收请求、校验入参、调用 Service、封装返回。它不应该有任何业务逻辑。什么叫业务逻辑?例如“计算订单折扣后金额大于 100 才允许下单”这种判断,如果出现在 Controller 里,代码就会越写越乱。
Service 层负责编排业务规则和处理事务边界。一个典型的方法可能长这样:先查询用户是否存在,再校验余额是否足够,然后扣款、更新订单状态。步骤一多,你自然会把它们拆成多个private方法,保持单个方法的可读性。
Repository 层负责和数据库打交道。新手不用急着学 MyBatis-Plus,Spring Data JPA 对初学者更直观,因为它的方法名就能直接生成查询,比如findByUsername(String username)。但你心里要清楚,JPA 只是帮你省掉了写 SQL 的繁琐,不等于你可以不懂 SQL。数据库的索引、联合查询、事务隔离这些基本功,到后面仍然是要补的。
很多初学者会想:写一个小项目,有必要搞这么多层吗?我的答案是:有必要,但你不用从第一天就严格遵循。哪怕你的 Service 层只有一个方法,也请把 Controller 和 Service 分开写。因为这个分层习惯一旦养成,对于后续的项目扩展和代码维护,收益是指数级的。
3.3 一个完整的“增删改查”落地:以用户模块为例
口说无凭,我以一个完整的用户模块为例,带你看一遍标准的写法。这不是让你死记硬背代码,而是让你看清楚每一层大概负责什么。
先定义实体类:
@Entity public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String username; private String email; // 省略 getter/setter }Repository 接口:
public interface UserRepository extends JpaRepository<User, Long> { Optional<User> findByUsername(String username); }Service 里包含核心校验逻辑:
@Service public class UserService { private final UserRepository userRepository; public UserService(UserRepository userRepository) { this.userRepository = userRepository; } public User createUser(User user) { if (userRepository.findByUsername(user.getUsername()).isPresent()) { throw new RuntimeException("用户名已存在"); } return userRepository.save(user); } }Controller 只需要暴露 API 入口:
@RestController @RequestMapping("/users") public class UserController { private final UserService userService; public UserController(UserService userService) { this.userService = userService; } }我特别想强调一下 Service 层里那个“用户名是否已存在”的校验。它看起来不起眼,但这就是业务逻辑。如果你把这段代码写在 Controller 里,将来可能因为另一个入口(比如管理后台、批量导入)需要同样的逻辑,你就会被迫复制粘贴,然后埋下一个改一处漏三处的坑。放在 Service 层,所有入口都经过同一个校验,这才是分层的真正意义。
3.4 构造器注入的好处与“循环依赖”避坑
你可能在教程里见过两种注入方式:字段注入和构造器注入。字段注入写起来最方便:
@Autowired private UserService userService;但我推荐你从学习阶段就用构造器注入:
private final UserService userService; public UserController(UserService userService) { this.userService = userService; }原因有两个。第一,构造器注入能保证对象在创建时所有依赖就已就绪,不会出现运行到一半才发现某个依赖是 null 的情况。第二,它天然暴露了循环依赖问题。如果 A 依赖 B,B 又依赖 A,用字段注入时 Spring 可以通过三级缓存勉强解决,但用构造器注入会直接启动失败,逼你正视设计问题。对于零基础的同学,把这种报错看成是框架在保护你,比想办法绕过它更有价值。
顺便提醒一下:如果你在网上搜“构造器注入报错循环依赖怎么解决”,很多答案会教你用@Lazy或@Autowired字段注入绕过去。但新手阶段遇到循环依赖,更大概率是设计问题,应该重构而不是绕过。
4. 数据访问与事务管理:从写 SQL 到理解数据库行为
4.1 连接数据库前,先弄懂连接池和数据源的逻辑
你可能听过别人说“项目里配了 HikariCP”,一脸疑惑地问这玩意是干嘛的。简单说,数据库连接是非常昂贵的资源。每次访问数据库都从零建立 TCP 连接,再完成认证和会话初始化,这个过程的耗时经常占到一次请求时间的很大比例。连接池的作用,就是提前创建一批连接放在池子里,用完归还,需要就取,省去重复建连的开销。
Spring Boot 2.x 默认使用 HikariCP 作为数据源实现,你只需要在配置文件里加上数据源信息就行:
spring.datasource.url=jdbc:mysql://localhost:3306/your_db spring.datasource.username=root spring.datasource.password=your_password spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver很多新手配完这个,启动时报错说“Failed to determine a suitable driver class”。原因多数是你的依赖里没有引入 MySQL 驱动。在 Spring Boot 的场景里,通常你只需要在 Maven 里加一个依赖:
<dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency>把驱动放在 runtime scope,是因为编译代码时不需要直接引用它的类,只有运行时才需要。这个细节,看懂pom.xml就很容易理解。
4.2 JPA 和 MyBatis 到底怎么选
这是新手问得最多的一个问题:Spring Boot 里到底用 JPA 还是 MyBatis?如果只从“从零开始学”的角度,我站 JPA。原因是 Spring Data JPA 的方法名派生查询非常直观:你写一个方法名findByEmailAndStatus,它就能根据约定自动生成查询,不需要手写 SQL。对于前期理解“数据访问层到底在干嘛”,这个工具几乎零门槛。
但如果你将来要去互联网公司做业务系统,很可能遇到的是 MyBatis 或者 MyBatis-Plus。因为团队更倾向于用 SQL 精确控制查询语句,复杂报表和千万级数据量下,手写 SQL 的性能优化空间更大。所以更现实的建议是:JPA 帮你快速上手,但 SQL 的基本功必须同步学。等你能熟练写出带关联、索引、分页的 SQL 语句之后,再切换到 MyBatis 会非常轻松。
如果你愿意在初学者阶段就多花一点时间,也可以直接用 MyBatis-Plus 起步。它的门槛没有传说中那么高,它的BaseMapper已经帮你内置了几乎所有单表 CRUD 方法,简单场景不用写 XML。复杂 SQL 就退回到自定义 XML 里写。用这套组合,你在实战项目里会更顺手一些。
4.3 事务传播行为:为什么 10 个服务嵌套调用时,事务往往不生效
业务里有一个高频场景:某个 Service 方法调用另一个 Service 方法,两个方法都打了@Transactional,但数据仍然出现了不一致。这个问题在初学者身上尤其常见,很多人盯着@Transactional注解反复检查,却找不到原因。
关键在于 Spring 的事务是依赖 AOP 代理实现的。你调用userService.createUser()时,实际调用的是 Spring 生成的代理对象,它先开启事务,再执行你的方法,随后提交或回滚。可是当你在同一个类里写一个方法,直接通过this调用另一个@Transactional方法,由于走的是this而不是代理对象,第二个方法上的事务注解就不生效了。
我给你一个简单的规避原则:事务边界尽量放在最外层入口,且这个入口要写在独立的类里。如果你发现同一个类内部有多个方法互相调用的逻辑,并且各自身上的事务行为必须独立控制,先想想设计是不是出了问题,要不要把这些方法挪到不同 Service 里。
MySQL 的默认隔离级别是可重复读(Repeatable Read),Spring 的默认传播行为是 REQUIRED(存在事务就加入,不存在就新建)。这些默认值对绝大多数业务够用了,但如果你涉及资金、订单这类强一致性的场景,务必去系统学一遍事务隔离级别和传播机制。
5. 安全与认证授权:没有它,你的服务永远不能叫“可上线”
5.1 从“谁都能访问”到“只有你能访问”的最小改造
如果你照着前面的路径,把 CRUD 接口都写完了,此时部署到公网,等于在互联网上给所有人敞开了大门。任何一个会猜路径的人都能调用你的接口增删改查数据。可上线的第一步,就是给服务加上认证和授权。
Spring Security 是 Java 生态里的标配安全框架,但它同时也是初学者最容易头大的东西:概念多、过滤器链复杂、配置繁琐。我的建议是前期不急着在代码里配置复杂的过滤器链,先把最常用的登录流程跑通:用户登录成功后,服务端生成一个 Token 返回给前端,前端后续请求都带上这个 Token,服务端校验通过才放行。
5.2 JWT 登录流程的前因后果
JWT(JSON Web Token)是目前最流行的无状态认证方案之一。所谓无状态,意思是服务端不需要在内存或数据库里保存用户的会话信息,Token 本身包含了用户身份信息,服务端只需要验签确认它没有被篡改即可。
一个最简单的流程是:
- 用户提交用户名和密码
- 后端校验用户名密码是否正确
- 正确则用密钥生成一个 JWT,返回给前端
- 前端在后续请求的 Header 里加
Authorization: Bearer <token> - 后端通过过滤器拦截请求,解析并校验 Token,解析出用户身份
JWT 本身有三个部分:Header、Payload、Signature。Header 声明了算法类型,Payload 存用户信息和过期时间,Signature 用密钥对整个 Token 做签名。这套设计保证了:Token 内容可以被任何人用 Base64 解码看到,但签名无法伪造,因为签名需要密钥。
这里要特别注意一个安全常识:不要把密码类敏感信息放进 Payload。JWT 的 Payload 不做加密,只是编码,任何人拿到 Token 都能解出来看。保存用户 ID、用户名还好,放密码就等于裸奔。
5.3 加密方案:BCrypt 为什么比 MD5 靠谱
新手写注册功能时,很容易跳进一个坑:把用户密码明文存进数据库,或者用 MD5 做一次哈希就存进去。明文存储等于把用户的钱包放在大街上不管;用 MD5 则容易被彩虹表攻击,也就是攻击者用预计算的哈希值反查原文,常用弱密码几秒钟就能破解。
正确做法是使用 BCrypt 这类自适应哈希算法,在 Spring Boot 里最简单的引入方式是:
PasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); String encoded = passwordEncoder.encode("rawPassword"); boolean matches = passwordEncoder.matches("rawPassword", encoded);BCrypt 的核心理念是加盐和自适应成本因子。它会在每次编码时生成随机盐,导致同样的密码每次编码结果都不同;成本因子可以调高计算强度,让暴力破解的代价成倍上升。虽然计算变慢,但对单次登录请求来说完全在可接受范围内。
5.4 最小但完整的过滤链配置
网上关于 Spring Security 的配置,动辄几百行,新手很容易被吓到。这里我给你一个能用的最小配置思路:
@Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.csrf(csrf -> csrf.disable()) .authorizeHttpRequests(auth -> auth .requestMatchers("/api/auth/login").permitAll() .anyRequest().authenticated() ); return http.build(); } }这段配置做了三件事:关闭 CSRF 防护(前后端分离项目一般不需要)、放行登录接口、其余接口必须有身份认证才能访问。至于 JWT 过滤器的具体写法,你后续会遇到很多成熟方案,但核心思路都一样:在过滤器链里检查 Header 里的 Token,把用户身份放进 SecurityContext 供后续代码获取。
如果你用的是 Spring Boot 3.x,注意 HttpSecurity 的 API 是 lambda 风格的;如果是 2.x,则可能是http.csrf().disable()这种链式写法。两者不要混用,否则编译都过不去。
注意:安全是一个很深的话题,JWT 也不是万能药。Token 过期、刷新、单点登录、权限粒度设计,后面每一项都值得单独写一篇。但第一步先把认证跑通、把密码加密做好,你就已经甩开大多数玩具项目了。
6. 从单体到微服务:什么时候拆、怎么拆、拆完有什么坑
6.1 微服务不是银弹:先搞懂它解决的问题和引入的成本
很多人看到一个项目写的“基于微服务架构”,就觉得这是很高级的东西,然后自己也照着拆。但微服务不是把项目拆了就完事,它带来的一系列分布式问题,才是真正考验人的地方。
先说它解决的问题:独立部署、独立扩展、技术栈异构、故障隔离。比如你的订单模块流量特别大,可以单独给它多开几个实例,而不需要把整个应用都扩容;比如某个模块挂了,不至于整个系统不可用。
但它的成本也很大:服务间调用要走网络,网络延迟和故障需要额外处理;数据一致性更难保证,因为不同服务可能各自有自己的数据库;日志排查也变得麻烦,一次请求可能要跨多个服务追踪。对于新手来说,这些问题的复杂度是几何级数上升的。
所以我的建议很明确:单体优先,微服务后置。你先把一个单体应用做到逻辑清晰、模块边界分明、接口设计合理,后面真有性能瓶颈和独立部署需求了,再按模块边界拆分也不迟。
6.2 从“裸写接口”到“服务发现与注册”:理解核心组件
假设你已经决定拆微服务了,比如把订单服务和用户服务拆开。此时 A 服务要调用 B 服务,你当然可以在配置文件里写死 B 的地址。但问题来了:如果 B 服务有 5 个实例,或者某个实例挂了重新拉起后 IP 变了,写死地址就不行了。这就是服务发现与注册要解决的事。
服务注册中心的作用,是让每个服务启动时把自己的地址和端口注册上去,调用方通过注册中心去拿当前可用的实例列表。Nacos 是现在国内用得比较多的注册中心,它同时提供了配置管理的能力。
Spring Cloud 的体系里,Alibaba 这个分支(Nacos + OpenFeign + Sentinel)在国内非常主流。如果你是纯学习,哪怕只是本地把三个微服务通过 Nacos 注册起来、互相调用通,这个实验本身就很有价值,因为它能帮你把“服务发现”这个抽象概念落地成具体的请求链路。
6.3 OpenFeign 声明式调用与熔断
服务间调用,最初始的方式是直接用 RestTemplate 手动拼 URL、发请求、解析响应。代码一多,这样的写法会让你很崩溃。OpenFeign 的出现,就是为了让你像调用本地方法一样调用远程服务。
你只需要定义一个接口:
@FeignClient(name = "order-service") public interface OrderClient { @GetMapping("/orders/{id}") OrderDto getOrderById(@PathVariable("id") Long id); }然后在调用方代码里注入这个接口,就能直接调方法。OpenFeign 内部帮你处理了 URL 拼接、序列化反序列化、负载均衡等一堆事情。它的名字里的 Feign 没有任何隐藏含义,就是 Spring Cloud 生态的一个组件名称。
有了服务调用,就一定要考虑失败情况。如果订单服务挂了,用户服务调用它时等了 5 秒才超时,用户体验会非常差。熔断的本质就是:当一个服务的错误率达到阈值后,调用方直接快速失败,不再发起真实的远程调用,避免故障扩散。这在生产环境里几乎是必备机制。
6.4 分库后的事务与“分布式事务”的常识
拆成微服务后,一个最常见的业务场景是:下单时要扣库存、生成订单、扣余额,三个操作分别在三个服务里。这时候,你不能再靠本地数据库的@Transactional来保证一致性了,因为每个服务操作的是各自独立的数据库。
业界有一堆方案:比如本地消息表、事务消息、TCC、Seata 等。但我想先给新手泼一盆冷水:分布式事务没有银弹,能避免跨服务事务就尽量避免。很多所谓的分布式事务问题,实际上可以通过“合并服务”或者“事件驱动”来绕开。
如果你现在还在学习阶段,不需要把 TCC 的细节啃透。你只需要理解一件事:单体应用里用数据库事务很自然,微服务里跨服务保证数据一致性是一个极其复杂的工程问题。当你真正遇到它时,先问自己能不能从架构层面绕开它,而不是急着引入一个分布式事务框架。
7. 从“学习过”到“能上线”:测试、部署与项目沉淀
7.1 单元测试的价值:写得越早,后期越省事
很多初学者觉得写测试是浪费时间,项目嘛,能跑不就行了?有这个想法的人,几乎都会在项目迭代到几个版本之后后悔。测试不是给自己找麻烦,它是给你未来的自己省麻烦。
在 Spring Boot 里写一个最简单的单元测试其实非常容易:
@SpringBootTest class UserServiceTest { @Autowired private UserService userService; @Test void shouldRejectDuplicateUsername() { // 构造测试数据 // 调用 service 方法 // 断言抛出异常 } }写完核心业务逻辑后顺手补几个关键测试,在后续重构时你就能直接知道“改动有没有破坏原来的行为”,这种安全感是任何代码审查都无法替代的。
7.2 打包与部署:从 IDEA 里跑通到服务器上跑稳
只要你的pom.xml引入了spring-boot-maven-plugin,可以直接用:
mvn clean package -DskipTests在target目录下会生成一个可执行的 JAR 包。这个包内置了 Tomcat,在任何装有 JDK 的服务器上都能直接运行:
java -jar your-project.jar部署这件事,现在有更时髦的 Docker 方式:写一个 Dockerfile,把 JAR 包打进去,然后在任意支持 Docker 的机器上docker run就能启动。对于学习阶段,Docker 不一定要学得很深,但至少要动手构建一个镜像、把服务跑起来,感受一下“一次构建,到处运行”的差异。
打包时最常遇到的一个问题是:配置文件里写的数据库地址、Redis 地址都是本地地址,到了服务器上需要改成服务器对应的地址。比较合理的做法是使用 Spring Boot 的多环境配置:application-dev.yml、application-prod.yml,用spring.profiles.active指定激活环境。这样本地开发和生产环境的配置彻底分开,不会出现“在本地好好的,到线上就挂了”的玄学问题。
7.3 日志、监控和文档:让你的服务不是“黑盒”
很多人把服务跑起来就觉得完事了,直到某个接口出问题,才发现自己压根不知道服务内部发生了什么。日志就是帮你还原现场的黑匣子。
Spring Boot 默认用 Logback 作为日志实现,你只需要在application.yml里配好日志级别和输出格式。我建议你把业务日志习惯养成在代码里,尤其是接口入参、关键业务判断、异常捕获这几个位置。简单来说就是:执行到哪一步了,告诉我。后续排查问题时,你会感谢当初愿意多写几行日志的自己。
如果你想让日志结构化、方便集中收集,后续可以引入 Loki 或者 ELK 这类日志系统,但那是另一个课题了。学习阶段先把日志和异常之间的关系搞清楚,就已经领先很多人。
7.4 学习路径的时间规划与心态建设
如果有人问我:按这个路径走完,大概需要多久?我会给一个相对保守的时间:每天投入两小时左右,三到四周可以走完从环境配置到“单体应用部署上线”,再花一个月左右学习微服务相关组件并动手实现一个小的服务调用链。当然,这只是技术学习本身,后续你还需要补 SQL、数据结构、网络基础这些计算机核心功底,但是这条路已经能跑,剩下的是持续补充深度。
我的建议是不要贪多。很多人学 Spring Boot,今天看到 MyBatis 教程就学 MyBatis,明天看到 Redis 面试题就学 Redis,最后什么都学了一点,却没有任何一个项目能在本地完完整整跑通。你要做的是先选定一条主路线,比如“用户、商品、订单”这三个模块的电商类项目,把它从无到有部署到云服务器上,让它在公网能至少跑一天不崩。这个完整闭环带给你的信心和实战经验,胜过看二十篇教程。
8. 真实项目里踩过的 5 个高频坑(附排查思路)
一路写下来,如果不把踩坑记录整理出来,这篇内容总觉得缺了一半。下面这些坑是我在辅导新人、做小项目时反复遇到的,希望你能提前避开。
8.1 端口被占用
启动时提示Port 8080 was already in use。新手第一反应是重启电脑,其实大可不必。在命令行执行netstat -ano | findstr 8080找到占用端口的进程 PID,然后去任务管理器结束对应进程即可。更省事的方案是直接在配置文件里换一个端口:
server.port=80818.2 依赖版本冲突
Spring Boot 的依赖管理其实已经帮你解决了很多版本冲突问题,因为它有统一的依赖版本管理。真正让人头疼的是:手动引入第三方库时版本不对,导致启动报错。遇到这种问题,先去确认pom.xml中是否有明确的版本号与 Spring Boot 的版本不兼容。我的经验是,优先使用 Spring Boot 官方管理的依赖,不要手动指定版本,如果你非要指定,先去查一下它和当前 Spring Boot 版本的兼容性。
建议:把 IDEA 里的 Maven 面板打开,看Dependencies那一栏有没有红色波浪线,有就说明依赖解析有问题。这是排查依赖冲突最快的方式。
8.3 配置文件里的密码和地址信息写死
新手很常见的习惯是把数据库密码直接写在application.properties里,然后整个代码传到代码仓库,账号密码对所有人可见。这个问题的正确解法是:使用环境变量或者配置中心管理敏感信息。在 Spring Boot 里,你可以在配置文件中引用环境变量:
spring.datasource.password=${DB_PASSWORD}这样仓库里就不会出现真正的密码了。这个习惯,最好从写第一个项目开始养成。
8.4 接口返回的 JSON 结构不一致
其实很多接口设计上的混乱,是因为没有统一响应体。刚开始写接口时,有的人直接返回实体对象,有的直接返回字符串,有的返回一个 Map,前端对接时就非常痛苦。
一个比较规范的做法是定义一个统一响应体:
public class ApiResponse<T> { private int code; private String message; private T data; // 构造器和静态方法省略 }所有接口都返回ApiResponse,前端只需要解析一种结构。这个习惯值得从第一个接口就养成,而不必等到项目大了再统一重构。
8.5 本地一切正常,部署到服务器后跨域或时区异常
最常见的两个问题:前端页面部署在某个域名下,你的后端在另一个域名下,浏览器会有跨域限制。如果你是前后端分离项目,Spring Boot 里通常需要配置允许的跨域来源。另一个问题是服务器时区没设置对,导致数据库存的时间差 8 小时。在数据库连接 URL 里加上serverTimezone=Asia/Shanghai能大概率解决这个问题。
如果部署环境有问题,不要慌,先检查服务器的系统时区、JDK 版本、配置文件激活的环境,再去看应用日志,按顺序排查比瞎猜高效得多。
最后分享一个我个人的项目习惯:每次学完一个模块,不要急着进下一个,而是把学到的内容动手加进自己的项目里。比如你会了 JPA,就把原来用内存数据的接口改成读数据库;你会了 Spring Security,就把原来的所有接口加上认证。保持项目始终是“活”的,比什么都重要。我在实际带新人的过程中发现,那些能坚持把一个玩具电商项目从零写到部署上线的同学,哪怕中间走了很多弯路,最后的技术底子都更扎实。不是因为他们天赋多高,而是因为那条完整的路,他们在早期就走过了一遍。