Java业务层实战:Service到底干什么?——把逻辑从Controller里救出来
系列专栏:从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 = ...) 的几种传播机制。
下篇见!
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)