第2章:单体架构实践 — 用Spring Boot搭建标准单体应用
好,咱们直接动手。这一章,我会带你搭一个标准的单体应用。
包含三个核心模块:用户、订单、商品。
别小看这个结构。我在很多公司见过,业务量不大的时候,这套架子能撑好几年。
2.1 为什么选Spring Boot?
说实话,现在Java后端选型,Spring Boot几乎是默认选项。
它帮你省掉了繁琐的XML配置。内嵌Tomcat,打一个jar包就能跑。
我个人习惯是:新项目一律从Spring Boot起步。除非有特殊要求,否则不折腾。
核心优势:
- 自动配置,开箱即用
- 生态丰富,社区活跃
- 部署简单,运维友好
2.2 项目结构设计
先看目录结构。我建议按业务模块分包,而不是按技术层分包。
什么意思?就是别搞什么controller包、service包、dao包那种大杂烩。
而是按user、order、product这样分。每个模块内部再分层。
com.example.mall
├── MallApplication.java
├── common
│ ├── config
│ ├── exception
│ └── util
├── user
│ ├── controller
│ ├── service
│ ├── repository
│ └── entity
├── order
│ ├── controller
│ ├── service
│ ├── repository
│ └── entity
└── product
├── controller
├── service
├── repository
└── entity
这样设计的好处是:将来拆微服务时,每个模块直接拎出来就行。
我在项目中吃过亏,一开始按技术层分包,后来拆服务时改得想哭。
2.3 用户模块实现
用户模块是最基础的。一般包含注册、登录、信息查询。
先看实体类:
@Entity
@Table(name = "tb_user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String username;
@Column(nullable = false)
private String password;
private String phone;
private String email;
@Column(name = "create_time")
private LocalDateTime createTime;
// getter/setter 省略
}
嗯,这里要注意:密码绝对不能明文存储。
我建议用BCrypt加密。Spring Security自带这个工具。
避坑指南:
我曾经见过一个项目,密码用MD5存。结果数据库泄露后,用户信息全被破解。从那以后,我所有项目强制用BCrypt。
登录接口示例:
@RestController
@RequestMapping("/api/user")
public class UserController {
@Autowired
private UserService userService;
@PostMapping("/login")
public Result login(@RequestBody LoginRequest request) {
// 校验参数
// 调用service
// 返回token
}
}
2.4 商品模块实现
商品模块相对简单。主要是CRUD加上库存管理。
但这里有个坑:库存扣减的并发问题。
先看商品实体:
@Entity
@Table(name = "tb_product")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String description;
private BigDecimal price;
@Column(name = "stock")
private Integer stock;
@Column(name = "status")
private Integer status; // 0-下架 1-上架
// getter/setter
}
你想想看,如果两个用户同时下单,库存会不会扣成负数?
会的。我在项目中就遇到过,上线第一天就出现超卖。
我的做法:
用数据库乐观锁。在stock字段上加版本号,更新时校验。
或者直接用 UPDATE product SET stock = stock - 1 WHERE id = ? AND stock > 0
这种原子操作,简单有效。
2.5 订单模块实现
订单模块是最复杂的。因为它涉及多个模块的交互。
一个下单流程,要查商品、扣库存、生成订单、更新用户信息。
订单实体:
@Entity
@Table(name = "tb_order")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "order_no", unique = true)
private String orderNo;
@Column(name = "user_id")
private Long userId;
@Column(name = "total_amount")
private BigDecimal totalAmount;
@Column(name = "status")
private Integer status; // 0-待支付 1-已支付 2-已取消
@Column(name = "create_time")
private LocalDateTime createTime;
// 订单明细
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> items;
}
下单的核心逻辑:
@Service
@Transactional
public class OrderService {
public Order createOrder(Long userId, List<Long> productIds) {
// 1. 查询用户
// 2. 查询商品,校验库存
// 3. 扣减库存(注意并发)
// 4. 生成订单号
// 5. 保存订单和明细
// 6. 返回订单
}
}
关键点:
- 整个下单流程要加
@Transactional,保证事务一致性 - 订单号要唯一,我习惯用雪花算法生成
- 库存扣减和订单生成要在同一个事务里
2.6 模块间调用
单体应用里,模块间调用很简单。直接注入Service就行。
比如订单模块调用商品模块:
@Service
public class OrderService {
@Autowired
private ProductService productService;
public void createOrder(...) {
Product product = productService.findById(productId);
// 业务逻辑
}
}
说白了,就是本地方法调用。没有网络开销,性能好。
这也是单体应用的优势之一。
2.7 配置文件与启动
application.yml配置示例:
server:
port: 8080
spring:
datasource:
url: jdbc:mysql://localhost:3306/mall?useSSL=false
username: root
password: 123456
jpa:
hibernate:
ddl-auto: update
show-sql: true
启动类:
@SpringBootApplication
public class MallApplication {
public static void main(String[] args) {
SpringApplication.run(MallApplication.class, args);
}
}
然后运行 mvn spring-boot:run 或者直接运行main方法。
一个标准的单体应用就起来了。
2.8 本章小结
这一章,我们搭了一个完整的单体应用。
用户、商品、订单三个模块,各自独立又相互协作。
你可能会觉得:这不就是普通的Spring Boot项目吗?
对,没错。但我要强调的是:
- 按业务模块分包,为将来拆分做准备
- 事务边界要清晰,别把无关操作塞进去
- 并发问题要提前考虑,别等出事了再补
下一章,我们会讨论单体应用遇到瓶颈时,该从哪里开始拆。
嗯,那才是真正有意思的部分。