系列专栏:从Java到AI应用开发 | 第3篇

写在前面

前两篇我们搭了API、建了模型,代码能跑了。但你有没有发现一个问题——所有逻辑都堆在Controller里

学生选课、下单支付、库存扣减……全写在接口方法里,Controller越来越胖,越来越乱。改一个业务规则,要在路由代码里翻半天。

今天这篇就解决一件事:Service层怎么设计和组织业务逻辑。这也是Java项目里最核心的一层——Controller管接请求,Repository管存数据,中间这坨"业务到底怎么跑",全是Service的活。

一、为什么要分Service层?

先看一个反面教材:

java

99

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

// ❌ 全塞在Controller里

@RestController

@RequestMapping("/api/orders")

public class OrderController {

@PostMapping

public Order createOrder(@RequestBody CreateOrderRequest request) {

// 1. 查商品

Product product = productRepository.findById(request.getProductId());

// 2. 检查库存

if (product.getStock() < request.getQuantity()) {

throw new RuntimeException("库存不足");

}

// 3. 计算金额

BigDecimal amount = product.getPrice().multiply(BigDecimal.valueOf(request.getQuantity()));

// 4. 扣库存

product.setStock(product.getStock() - request.getQuantity());

productRepository.save(product);

// 5. 创建订单

Order order = new Order();

order.setProductId(product.getId());

order.setQuantity(request.getQuantity());

order.setAmount(amount);

order.setStatus(OrderStatus.PENDING);

orderRepository.save(order);

return order;

}

}

问题很明显:

  • 不可复用 → 别的地方要创建订单(比如定时任务、其他接口),得复制一遍
  • 不可测试 → 想单独测下单逻辑,还得模拟整个HTTP请求
  • 职责混乱 → Controller既管参数接收,又管业务执行

Service层就是把这些业务逻辑抽出来,让Controller只做转发。

二、Service层的基本结构

1. 接口 + 实现

Java项目的标准做法是定义接口,再写实现类:

java

9

1

2

3

4

5

6

7

// 接口:定义"能做什么"

public interface OrderService {

Order createOrder(String userId, String productId, int quantity);

Order payOrder(String orderId);

Order cancelOrder(String orderId);

}

java

99

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

// 实现:定义"怎么做"

@Service

public class OrderServiceImpl implements OrderService {

@Autowired

private ProductRepository productRepository;

@Autowired

private OrderRepository orderRepository;

@Override

public Order createOrder(String userId, String productId, int quantity) {

// 业务逻辑全在这里

Product product = productRepository.findById(productId)

.orElseThrow(() -> new RuntimeException("商品不存在"));

if (product.getStock() < quantity) {

throw new RuntimeException("库存不足");

}

product.setStock(product.getStock() - quantity);

productRepository.save(product);

Order order = new Order();

order.setUserId(userId);

order.setProductId(productId);

order.setQuantity(quantity);

order.setAmount(product.getPrice().multiply(BigDecimal.valueOf(quantity)));

order.setStatus(OrderStatus.PENDING);

order.setCreateTime(LocalDateTime.now());

return orderRepository.save(order);

}

@Override

public Order payOrder(String orderId) {

Order order = orderRepository.findById(orderId)

.orElseThrow(() -> new RuntimeException("订单不存在"));

if (order.getStatus() != OrderStatus.PENDING) {

throw new IllegalStateException("只有待支付订单才能付款");

}

// 调用支付(目前模拟)

order.setStatus(OrderStatus.PAID);

order.setPayTime(LocalDateTime.now());

return orderRepository.save(order);

}

@Override

public Order cancelOrder(String orderId) {

Order order = orderRepository.findById(orderId)

.orElseThrow(() -> new RuntimeException("订单不存在"));

if (order.getStatus() == OrderStatus.SHIPPED || order.getStatus() == OrderStatus.DELIVERED) {

throw new IllegalStateException("已发货/已签收订单不能取消");

}

// 归还库存

Product product = productRepository.findById(order.getProductId()).orElseThrow();

product.setStock(product.getStock() + order.getQuantity());

productRepository.save(product);

order.setStatus(OrderStatus.CANCELLED);

return orderRepository.save(order);

}

}

现在Controller变得非常清爽:

java

99

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

@RestController

@RequestMapping("/api/orders")

public class OrderController {

@Autowired

private OrderService orderService;

@PostMapping

public Order createOrder(@RequestBody CreateOrderRequest request) {

return orderService.createOrder(request.getUserId(), request.getProductId(), request.getQuantity());

}

@PostMapping("/{orderId}/pay")

public Order payOrder(@PathVariable String orderId) {

return orderService.payOrder(orderId);

}

@PostMapping("/{orderId}/cancel")

public Order cancelOrder(@PathVariable String orderId) {

return orderService.cancelOrder(orderId);

}

}

Controller只负责:接收请求 → 调Service → 返回结果。一行业务逻辑都不写。

2. 为什么要写接口?

有人觉得接口是多余的一层,直接写实现类不就行了?

写接口的好处:

  • 方便替换 → 后面换实现(比如从内存换成数据库),Controller不用改
  • 方便测试 → Mock接口比Mock类更干净
  • 团队协作 → 接口是契约,前后端、同事之间先对接口再开发

小项目可能觉得麻烦,但这是Java的工程规范,养成习惯比后面改成本低得多。

三、@Transactional:事务不是可选项

取消订单时要"改订单状态 + 归还库存",如果第一步成功第二步失败呢?订单取消了,库存没回来——数据不一致

事务就是解决这个问题的:要么全成功,要么全回滚。

java

99

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

@Service

public class OrderServiceImpl implements OrderService {

@Transactional // 这一行就够了

@Override

public Order cancelOrder(String orderId) {

Order order = orderRepository.findById(orderId)

.orElseThrow(() -> new RuntimeException("订单不存在"));

// 改状态

order.setStatus(OrderStatus.CANCELLED);

orderRepository.save(order);

// 归还库存(如果这里报错,上面的状态修改也会回滚)

Product product = productRepository.findById(order.getProductId()).orElseThrow();

product.setStock(product.getStock() + order.getQuantity());

productRepository.save(product);

return order;

}

}

事务的几个要点

表格

要点 说明
加在哪 加在Service方法上,不要加在Controller上
回滚条件 默认只回滚RuntimeException,checked异常不回滚
只读事务 查询方法加 @Transactional(readOnly = true),性能更好
传播机制 默认REQUIRED,方法间调用共用同一事务

java

99

1

2

3

4

5

6

7

8

9

10

11

12

// 查询用只读事务

@Transactional(readOnly = true)

public Order getOrder(String orderId) {

return orderRepository.findById(orderId).orElse(null);

}

// 需要回滚checked异常时

@Transactional(rollbackFor = Exception.class)

public void someBusinessMethod() throws IOException {

// ...

}

四、业务校验:在Service层做,不在Controller做

参数格式校验(非空、长度)放在Controller用 @Valid 就行。但业务规则校验必须在Service层,因为:

  • 同一个业务可能被多个入口调用
  • 校验逻辑和业务逻辑是一体的,分开容易漏

java

99

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

@Service

public class CourseService {

@Autowired

private StudentRepository studentRepository;

@Autowired

private CourseRepository courseRepository;

@Transactional

public boolean enrollCourse(String studentId, String courseCode) {

Student student = studentRepository.findById(studentId)

.orElseThrow(() -> new RuntimeException("学生不存在"));

Course course = courseRepository.findById(courseCode)

.orElseThrow(() -> new RuntimeException("课程不存在"));

// === 业务规则校验 ===

// 1. 不能重复选课

if (student.getEnrolledCourses().contains(courseCode)) {

throw new BusinessException("已选该课程,不能重复选课");

}

// 2. 学分上限(每学期不超过25学分)

int currentCredits = student.getEnrolledCourses().stream()

.map(code -> courseRepository.findById(code).orElseThrow())

.mapToInt(Course::getCredit)

.sum();

if (currentCredits + course.getCredit() > 25) {

throw new BusinessException("选课学分超过上限(25学分)");

}

// 3. 容量限制

int enrolled = (int) studentRepository.countByEnrolledCourse(courseCode);

if (enrolled >= course.getCapacity()) {

throw new BusinessException("课程已满");

}

// === 通过校验,执行业务 ===

student.getEnrolledCourses().add(courseCode);

studentRepository.save(student);

return true;

}

}

校验在前,执行在后,顺序不能乱。 校验失败抛业务异常,让全局异常处理器统一返回友好提示。

五、Service层设计原则总结

plaintext

9

1

2

3

4

Controller → 接收请求,参数校验(@Valid),调Service,返回结果

Service → 业务校验,业务逻辑,事务管理,调Repository

Repository → 只管数据存取,不管业务

表格

原则 说明
单一职责 一个Service方法只做一件事
接口优先 先定义接口,再写实现
事务在Service 涉及多步写操作的方法必须加@Transactional
校验在Service 业务规则校验属于Service,参数格式校验属于Controller
异常要语义化 抛BusinessException,别抛RuntimeException("库存不足")
Service之间可互调 复杂业务可以组合多个Service,但注意事务传播

和AI应用的关系

Service层的设计思想,在做AI应用时完全通用:

  • AI对话服务ChatService.chat() 内部要校验用户权限、检查调用额度、记录对话历史、调用大模型API——这和"下单"一样是多步业务
  • RAG检索服务RagService.search() 要查知识库、向量检索、重排序、拼上下文——每一步都可能失败,需要事务或补偿机制
  • AI Agent调度 → Agent的每个工具调用就是一个Service方法,有校验、有执行、有状态管理

Spring Boot + AI的本质,就是在Service层里把大模型当组件来调用。 模型是工具,业务逻辑才是灵魂。

思考题

如果一个Service方法里需要调用另一个Service的方法(比如下单时要调用InventoryService扣库存、PointService扣积分),事务该怎么传播?如果积分扣除失败,库存要不要回滚?

提示:看看 @Transactional(propagation = ...) 的几种传播机制。

下篇见!

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐