第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项目吗?

对,没错。但我要强调的是:

  • 按业务模块分包,为将来拆分做准备
  • 事务边界要清晰,别把无关操作塞进去
  • 并发问题要提前考虑,别等出事了再补

下一章,我们会讨论单体应用遇到瓶颈时,该从哪里开始拆。

嗯,那才是真正有意思的部分。