Spring Boot上手记:从零到上线的60分钟实战
上周五晚上九点半,我刚从公司挤完一小时地铁回到北京回龙观的出租屋,耳机里放着Lo-fi beats,正准备打开IntelliJ写点Python脚本处理明天要交的Spark作业数据。突然钉钉弹出一条消息:“老张,明天运营那边要搞个促销活动报名页面,简单表单就行,后端你搭个服务支持下?”
我差点一口老血喷出来——这都快十点了,明天还要早起通勤,结果临时塞需求?但转念一想,反正也是个简单接口,用Spring Boot搓个脚手架也就半小时的事儿。而且……最近在刷面试题挑战,刚好练练手。
于是,这篇“60分钟快速上手Spring Boot”的实战笔记就这么诞生了。
为啥一个大数据开发要碰Spring Boot?
可能有人会问:你不是天天和Spark、Hive、Flink打交道吗?怎么突然搞起Java后端了?
说实话,在我们这种偏数据中台的团队,前后端边界早就模糊了。去年双11期间,运营部门临时要一个实时报名人数看板,前端同学排期排到明年,产品经理直接拍桌子:“你们数据组不是会写代码吗?自己搞!”
于是,我这个“全栈民工”被迫捡起了尘封三年的Spring Boot技能。虽然平时写Python写得飞起(Pandas + Flask三行搞定API),但公司技术栈统一要求Java,说是为了“稳定性”和“运维一致性”——懂的都懂。
不过话说回来,Spring Boot这几年确实稳得一批。自动配置、内嵌Tomcat、starter机制,对像我这种只想快速交付、不想折腾部署的人来说简直是福音。
环境准备:别在环境上浪费时间
首先确认你装了:
- JDK 17(公司强制升级,别问为啥不用8)
- Maven 3.8+
- 一个能跑的IDE(IntelliJ IDEA,别跟我提Eclipse)
然后去 start.spring.io 快速生成项目。我一般选这些依赖:
Spring Web
Spring Data JPA
H2 Database(开发用)
Lombok
Validation
小贴士:生产环境别用H2!我们线上用的是MySQL 8.0,但本地开发用H2能省掉数据库连接配置的麻烦,特别适合快速验证逻辑。
生成完下载zip,解压导入IDEA。整个过程不超过3分钟——比等地铁还快。
第一步:写个Hello World,但别止步于此
很多人教程到这里就结束了:“看,你的Spring Boot跑起来了!” 但现实哪有这么简单?
我直接上一个带参数校验、日志记录、返回统一封装的Controller:
@RestController
@RequestMapping("/api/v1/activity")
@RequiredArgsConstructor
@Slf4j
public class ActivityController {
private final ActivityService activityService;
@PostMapping("/register")
public ResponseEntity<Result<Void>> register(@Valid @RequestBody RegisterRequest request) {
log.info("收到报名请求: phone={}, activityId={}", request.getPhone(), request.getActivityId());
try {
activityService.register(request);
return ResponseEntity.ok(Result.success());
} catch (IllegalArgumentException e) {
log.warn("报名失败: {}", e.getMessage());
return ResponseEntity.badRequest().body(Result.fail(e.getMessage()));
}
}
}
这里有几个最佳实践:
- 用
@Valid做参数校验(配合@NotBlank,@Pattern等注解) - 返回统一封装的
Result<T>,避免前端拿到裸露的500错误 - 打日志时脱敏(手机号只打后四位更安全,这里为演示简化了)
- 用 Lombok 的
@RequiredArgsConstructor自动注入Service,干净利落
数据库设计:别让运营随便改字段
运营同事的需求永远是:“先做个简单的,后面再加字段”。但如果你真信了,等着被线上事故教育吧。
这次的报名表,我预设了扩展性:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | BIGINT | 主键 |
| phone | VARCHAR(11) | 手机号(唯一索引) |
| activity_id | BIGINT | 活动ID |
| extra_info | JSON | 扩展字段,存问卷答案等 |
| created_at | DATETIME | 创建时间 |
关键点:用 extra_info 存JSON。这样下次运营说“要加个身份证号”或“用户选了哪个套餐”,不用改表结构,直接往JSON里塞就行。MySQL 5.7+ 支持JSON类型,查询也方便。
Entity代码长这样:
@Entity
@Table(name = "activity_registration", indexes = {
@Index(name = "idx_phone", columnList = "phone"),
@Index(name = "idx_activity", columnList = "activity_id")
})
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ActivityRegistration {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank
@Pattern(regexp = "^1[3-9]\\d{9}$")
private String phone;
private Long activityId;
@JdbcTypeCode(SqlTypes.JSON)
private Map<String, Object> extraInfo;
private LocalDateTime createdAt;
}
注意:
- 加了手机号格式校验(正则别写错,不然测试同事会找你喝茶)
- 两个常用查询字段建了索引
extraInfo用@JdbcTypeCode(SqlTypes.JSON)标记为JSON类型(Spring Boot 3.x写法)
Service层:防重、限流、事务一个不能少
你以为保存个数据就完了?Too young。
运营搞活动最怕什么?重复报名 和 刷量。所以我在Service层加了双重保障:
@Service
@Transactional
@RequiredArgsConstructor
public class ActivityServiceImpl implements ActivityService {
private final ActivityRegistrationRepository repository;
private final RedisTemplate<String, String> redisTemplate;
@Override
public void register(RegisterRequest request) {
String lockKey = "register:lock:" + request.getPhone();
// 分布式锁防并发重复提交
Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", Duration.ofSeconds(10));
if (!Boolean.TRUE.equals(locked)) {
throw new IllegalArgumentException("操作太频繁,请稍后再试");
}
try {
// 检查是否已报名
if (repository.existsByPhoneAndActivityId(request.getPhone(), request.getActivityId())) {
throw new IllegalArgumentException("您已报名过该活动");
}
// 保存
ActivityRegistration registration = new ActivityRegistration();
registration.setPhone(request.getPhone());
registration.setActivityId(request.getActivityId());
registration.setExtraInfo(request.getExtraInfo());
registration.setCreatedAt(LocalDateTime.now());
repository.save(registration);
} finally {
redisTemplate.delete(lockKey); // 释放锁
}
}
}
这里踩过坑:
- 最初没加分布式锁,压力测试时出现重复数据
- Redis锁的key必须带业务维度(比如手机号),否则会阻塞其他用户
finally里删锁,避免异常导致死锁
血泪教训:上次有个活动没做防重,运营导出数据发现同一个手机号报了200次,差点背锅到年终奖都没了。
配置文件:开发vs生产要分开
很多新手把数据库密码写死在 application.yml 里,然后 commit 到 Git —— 这种操作建议直接送走。
我们用 profile 分离环境:
# application.yml
spring:
profiles:
active: dev
---
# application-dev.yml
spring:
datasource:
url: jdbc:h2:mem:testdb
driver-class-name: org.h2.Driver
jpa:
hibernate:
ddl-auto: create-drop
show-sql: true
---
# application-prod.yml
spring:
datasource:
url: ${DB_URL}
username: ${DB_USER}
password: ${DB_PASSWORD}
jpa:
hibernate:
ddl-auto: validate # 生产严禁自动建表!
启动命令:
# 本地开发
./mvnw spring-boot:run
# 生产部署
java -jar app.jar --spring.profiles.active=prod
记住:生产环境 ddl-auto 必须是 validate 或 none!自动建表/更新表结构是线上事故高发区。
和Python对比:为什么这次没用Flask?
我知道很多人会问:你不是Python老手吗?为啥不用Flask快速搭个API?
原因很现实:
- 公司规范:所有对外服务必须Java栈,便于统一监控(Arthas、SkyWalking)
- 运维友好:Spring Boot Actuator 提供
/actuator/health,/actuator/metrics,运维同学能直接对接Prometheus - 性能考量:虽然Python够快,但高并发场景下Java的线程模型更稳(别杠,这是生产经验)
不过私下我确实用Python写了数据同步脚本——每天凌晨把报名数据从MySQL抽到Hive,供运营分析用。这才是我的舒适区 😌
面试题挑战:Spring Boot高频考点
最近在刷面试题,发现几个和本文强相关的考点,分享一下:
| 问题 | 回答要点 |
|---|---|
| Spring Boot自动配置原理? | @EnableAutoConfiguration + spring.factories 加载 XXXAutoConfiguration |
| 如何自定义Starter? | 写 autoconfigure 模块 + starter 模块,暴露 @EnableXXX 注解 |
| JPA和MyBatis怎么选? | 快速开发选JPA,复杂SQL/性能敏感选MyBatis(我们数据服务用MyBatis) |
| 如何保证接口幂等? | 本文用Redis锁 + 唯一索引,也可用Token机制 |
如果你正在准备跳槽,建议动手写个类似的小项目,比背八股文有用多了。
上线之后:别以为万事大吉
周五晚上十一点,我把服务打包成jar,扔到测试环境。运维同学看了一眼,幽幽地说:“记得加健康检查,不然K8s把你Pod干掉。”
赶紧补上 application-prod.yml:
management:
endpoints:
web:
exposure:
include: health,info,metrics
endpoint:
health:
show-details: always
第二天早上,运营小姐姐开心地说:“页面跑起来了!数据也进去了!”
我松了口气,心想终于可以安心写Spark作业了……
结果中午又来消息:“能不能加个导出Excel功能?”
我默默关掉音乐,打开了Apache POI文档……
总结:60分钟,不止是入门
回头看这60分钟,其实做了远超“入门”的事情:
- 安全:参数校验、防重、日志脱敏
- 可维护:统一返回、JSON扩展字段
- 可运维:Actuator、Profile分离
- 可扩展:预留运营后续需求
Spring Boot 的魅力就在于:让你在“快速交付”和“工程规范”之间找到平衡点。它不会替你思考业务,但能帮你避开90%的基础设施坑。
最后给新人一句忠告:别只照着教程敲Hello World。试着把它当成真实需求来做——加校验、加日志、考虑并发、设计表结构。这样,你写的就不是demo,而是能扛住运营暴击的生产代码。
哦对了,现在我耳机里换成了《Spring Boot in Action》有声书(开玩笑的)。继续调我的Spark作业去了,北京地铁又要晚点……

评论 0