Spring Boot 项目中常见的 controller、service、mapper 并不是 Spring Boot 强制规定的目录,而是一种便于职责分离的工程组织方式。Spring Boot 本身不要求固定代码结构,但建议把启动类放在根包下,使组件扫描能够覆盖下面的业务包。
最核心的调用链理解为:
Vue 前端
↓ HTTP 请求
Controller
↓
Service
↓
Mapper / Repository 或外部服务 Client
↓
SQL Server / bof-ml-service
↓
结果逐层返回
一个更加完整的调用链是:
浏览器
↓
Filter
↓
Spring Security
↓
DispatcherServlet
↓
Interceptor
↓
Controller
↓
Service
├─ Mapper → SQL Server
├─ Client → bof-ml-service
└─ 其他 Service
↓
DTO / VO 转换
↓
Controller
↓
前端
一、完整的常见分层
1. Controller:接口接入层
Controller 是后端系统接收 HTTP 请求的入口。
例如:
@RestController
@RequestMapping("/api/v1/predict")
@RequiredArgsConstructor
public class EndpointPredictionController {
private final EndpointPredictionService predictionService;
@PostMapping("/endpoint")
public ApiResponse<EndpointPredictionVO> predict(
@Valid @RequestBody EndpointPredictionRequest request) {
EndpointPredictionVO result =
predictionService.predict(request);
return ApiResponse.success(result);
}
}
它主要负责:
接收请求
解析 URL 和 JSON
执行基础参数校验
调用 Service
把结果转换为 HTTP 响应
它不应该负责:
直接写 SQL
直接操作 Mapper
构造复杂模型特征
处理大段业务判断
直接加载机器学习模型
@RestController 会将方法返回值直接写入响应体,@PostMapping、@GetMapping 等注解负责把 HTTP 请求映射到具体方法。(Home)
Controller 可以理解为:
后端系统的业务接口接待窗口。
2. Request DTO:请求参数对象
前端传来的 JSON 通常不要直接映射到数据库实体,而是映射为专门的请求对象。
例如:
@Data
public class EndpointPredictionRequest {
@NotBlank(message = "炉次号不能为空")
private String heatId;
}
前端发送:
{
"heatId": "H20260510-001"
}
Spring 会将它转换成:
EndpointPredictionRequest
Request DTO 的职责是:
定义接口允许接收哪些字段
进行参数校验
隔离前端接口与数据库结构
常见命名包括:
EndpointPredictionRequest
EndpointPredictionDTO
CreateHeatRequest
HeatQueryRequest
ModelActivateCommand
推荐在项目中把不同用途区分清楚:
Request:Controller 接收的请求
Response / VO:Controller 返回的数据
DTO:不同层之间传递的数据
Entity / DO:数据库表映射对象
3. Service:业务逻辑层
Service 是整个业务系统中最重要的一层。
例如:
public interface EndpointPredictionService {
EndpointPredictionVO predict(
EndpointPredictionRequest request);
}
实现类:
@Service
@RequiredArgsConstructor
public class EndpointPredictionServiceImpl
implements EndpointPredictionService {
private final HeatService heatService;
private final MlServiceClient mlServiceClient;
private final ModelResultMapper modelResultMapper;
private final EndpointPredictionConverter converter;
@Override
@Transactional
public EndpointPredictionVO predict(
EndpointPredictionRequest request) {
HeatEntity heat =
heatService.requireHeat(request.getHeatId());
EndpointMlResponse mlResponse =
mlServiceClient.predictEndpoint(
request.getHeatId());
ModelResultEntity result =
converter.toEntity(heat, mlResponse);
modelResultMapper.insert(result);
return converter.toVO(result);
}
}
Service 主要负责:
业务流程编排
业务规则判断
调用多个 Mapper
调用其他 Service
调用外部 Python 服务
事务控制
异常转换
结果组装
在你的终点预测场景中,Service 要表达的是:
1. 检查炉次是否存在
2. 检查炉次状态是否允许预测
3. 调用 bof-ml-service
4. 判断模型服务是否成功
5. 保存预测结果
6. 返回前端需要的数据
Service 不应该关注:
HTTP 状态如何映射
SQL 具体怎么写
JSON 如何序列化
XGBoost 模型怎样执行
为什么经常有 Service 和 ServiceImpl
常见结构是:
EndpointPredictionService
EndpointPredictionServiceImpl
其中:
Service 接口:定义系统能做什么
ServiceImpl:定义具体怎么做
接口形式适用于:
-
可能存在多个实现;
-
需要便于测试;
-
需要使用代理或扩展;
-
希望降低调用方与实现的耦合。
小型项目也可以直接使用:
@Service
public class EndpointPredictionService {
}
不一定必须拆成接口和实现类。
4. Mapper:数据库访问层
如果项目使用 MyBatis,Mapper 通常是数据库操作接口。
例如:
@Mapper
public interface HeatMapper {
HeatEntity selectByHeatId(String heatId);
int insert(HeatEntity entity);
int updateStatus(
@Param("heatId") String heatId,
@Param("status") String status);
}
MyBatis Spring Boot Starter 默认会扫描带有 @Mapper 的接口,也可以使用 @MapperScan 统一扫描 Mapper 包。(mybatis.org)
Mapper 对应的 XML:
<mapper namespace="com.converter.heat.mapper.HeatMapper">
<select id="selectByHeatId"
resultType="com.converter.heat.entity.HeatEntity">
SELECT
heat_id,
steel_grade,
converter_no,
blow_start_time,
main_blow_end_time
FROM bof_heat
WHERE heat_id = #{heatId}
</select>
</mapper>
Mapper 负责:
执行 SQL
查询数据库
插入数据
更新数据
删除数据
将查询结果映射为 Java 对象
Mapper 不应该负责:
判断炉次能不能预测
决定使用哪个模型
组织多步骤业务流程
调用 Python 模型服务
拼装前端页面数据
可以把 Mapper 理解为:
Java 代码访问 SQL Server 的直接通道。
5. Mapper XML:SQL 定义文件
MyBatis 项目中,较复杂 SQL 通常放在:
src/main/resources/mapper/
例如:
resources/
└── mapper/
├── HeatMapper.xml
├── ModelResultMapper.xml
└── ModelRegistryMapper.xml
配置:
mybatis:
mapper-locations: classpath*:mapper/**/*.xml
type-aliases-package: com.converter.system
Mapper XML 负责:
SQL 语句
结果字段映射
动态查询条件
分页条件
批量插入
复杂 JOIN
例如:
<select id="selectLatestPrediction"
resultType="ModelResultEntity">
SELECT TOP 1
*
FROM bof_model_result
WHERE heat_id = #{heatId}
AND task_key = #{taskKey}
AND model_version = #{modelVersion}
AND input_hash = #{inputHash}
ORDER BY created_at DESC
</select>
二、数据库对象相关的几个概念
6. Entity / DO / PO:数据库实体
这几个名字经常表示相似概念。
例如:
@Data
@TableName("bof_heat")
public class HeatEntity {
private Long id;
private String heatId;
private String steelGrade;
private String converterNo;
private LocalDateTime blowStartTime;
private LocalDateTime mainBlowEndTime;
}
常见名称:
Entity:实体对象
DO:Data Object
PO:Persistent Object
它们通常表示:
与数据库表结构接近的 Java 对象。
例如数据库表:
bof_heat
对应:
HeatEntity
数据库字段:
heat_id
steel_grade
main_blow_end_time
对应 Java 字段:
heatId
steelGrade
mainBlowEndTime
Entity 不应该直接承担:
前端请求结构
前端响应结构
复杂业务计算结果
模型服务的完整响应
因为数据库结构一旦发生变化,不应该强迫前端接口一起改变。
7. DTO:数据传输对象
DTO 是不同层之间传递数据的对象。
例如,从 Java 发往 Python 模型服务:
@Data
@Builder
public class EndpointMlRequestDTO {
private String requestId;
private String heatId;
private String schemaVersion;
}
从 Python 返回:
@Data
public class EndpointMlResponseDTO {
private String requestId;
private String modelName;
private String modelVersion;
private PredictionDTO prediction;
}
其中:
@Data
public class PredictionDTO {
private BigDecimal carbon;
private BigDecimal manganese;
private BigDecimal phosphorus;
private BigDecimal sulfur;
private BigDecimal temperature;
}
DTO 的特点是:
为一次数据传递服务
不必与数据库表一致
可以组合多个来源的数据
可以只保留调用所需字段
8. VO / Response:返回前端的对象
VO 通常表示:
View Object
即专门提供给前端页面的数据。
例如:
@Data
@Builder
public class EndpointPredictionVO {
private String heatId;
private String steelGrade;
private BigDecimal carbon;
private BigDecimal manganese;
private BigDecimal phosphorus;
private BigDecimal sulfur;
private BigDecimal temperature;
private String modelName;
private String modelVersion;
private Boolean fallbackUsed;
private LocalDateTime predictedAt;
}
最终返回:
{
"heatId": "H20260510-001",
"steelGrade": "Q235B",
"carbon": 0.052,
"manganese": 0.183,
"phosphorus": 0.016,
"sulfur": 0.009,
"temperature": 1662,
"modelName": "endpoint-xgb",
"modelVersion": "v2",
"fallbackUsed": false
}
为什么不直接返回 Entity?
因为数据库 Entity 可能包含:
数据库主键
内部状态
输入哈希
内部错误信息
创建者
逻辑删除标记
不适合向前端暴露的字段
VO 只保留页面真正需要的数据。
三、对象转换层
9. Converter / Assembler
Entity、DTO、VO 之间需要转换。
例如:
@Component
public class EndpointPredictionConverter {
public ModelResultEntity toEntity(
String heatId,
EndpointMlResponseDTO response) {
ModelResultEntity entity =
new ModelResultEntity();
entity.setHeatId(heatId);
entity.setTaskKey("endpoint_prediction");
entity.setModelName(response.getModelName());
entity.setModelVersion(response.getModelVersion());
entity.setCarbon(
response.getPrediction().getCarbon());
entity.setTemperature(
response.getPrediction().getTemperature());
return entity;
}
public EndpointPredictionVO toVO(
ModelResultEntity entity) {
return EndpointPredictionVO.builder()
.heatId(entity.getHeatId())
.carbon(entity.getCarbon())
.temperature(entity.getTemperature())
.modelName(entity.getModelName())
.modelVersion(entity.getModelVersion())
.predictedAt(entity.getCreatedAt())
.build();
}
}
常见名称包括:
Converter
Assembler
Mapper
StructMapper
这里要注意,"Mapper"有两种含义:
MyBatis Mapper
HeatMapper
ModelResultMapper
负责数据库操作。
对象转换 Mapper
HeatStructMapper
EndpointPredictionStructMapper
负责 Java 对象转换,常见于 MapStruct。
为了避免混淆,建议你的项目中:
数据库访问统一叫 Mapper
对象转换统一叫 Converter
四、外部服务调用层
10. Client / Gateway / Adapter
你的 Java 系统需要调用:
bof-ml-service
因此可以专门建立外部服务调用层。
例如:
public interface MlServiceClient {
EndpointMlResponseDTO predictEndpoint(
String heatId);
}
实现:
@Component
@RequiredArgsConstructor
public class MlServiceHttpClient
implements MlServiceClient {
private final WebClient mlWebClient;
@Override
public EndpointMlResponseDTO predictEndpoint(
String heatId) {
EndpointMlRequestDTO request =
EndpointMlRequestDTO.builder()
.heatId(heatId)
.build();
return mlWebClient.post()
.uri("/api/v1/predict/endpoint")
.bodyValue(request)
.retrieve()
.bodyToMono(
EndpointMlResponseDTO.class)
.block();
}
}
它负责:
构造 HTTP 请求
调用 Python 服务
设置超时时间
解析响应
处理网络异常
它不负责:
判断炉次是否允许预测
保存业务结果
决定页面显示什么
可以有:
MlServiceClient
MesClient
LaboratoryClient
OpcClient
FileStorageClient
11. Adapter:适配层
Adapter 通常用于屏蔽不同系统之间的数据格式差异。
例如 Python 返回:
{
"prediction": {
"C": 0.052,
"temperature": 1662
}
}
Java 内部希望使用:
EndpointPrediction
Adapter 可以完成:
Python 字段格式
↓
Java 领域对象格式
例如:
@Component
public class MlPredictionAdapter {
public EndpointPrediction adapt(
EndpointMlResponseDTO response) {
return new EndpointPrediction(
response.getPrediction().getCarbon(),
response.getPrediction().getTemperature(),
response.getModelVersion());
}
}
五、异常处理相关层
12. Exception:业务异常
例如炉次不存在:
public class HeatNotFoundException
extends RuntimeException {
public HeatNotFoundException(String heatId) {
super("炉次不存在:" + heatId);
}
}
没有激活模型:
public class ModelNotActiveException
extends RuntimeException {
public ModelNotActiveException(String taskKey) {
super("任务没有激活模型:" + taskKey);
}
}
模型服务超时:
public class MlServiceTimeoutException
extends RuntimeException {
}
13. GlobalExceptionHandler:全局异常处理
不要在每个 Controller 中重复写:
try {
...
} catch (Exception e) {
...
}
可以统一处理:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(HeatNotFoundException.class)
public ResponseEntity<ApiResponse<Void>>
handleHeatNotFound(
HeatNotFoundException exception) {
return ResponseEntity.status(404)
.body(ApiResponse.failure(
"HEAT_NOT_FOUND",
exception.getMessage()));
}
@ExceptionHandler(MlServiceTimeoutException.class)
public ResponseEntity<ApiResponse<Void>>
handleMlTimeout(
MlServiceTimeoutException exception) {
return ResponseEntity.status(504)
.body(ApiResponse.failure(
"ML_SERVICE_TIMEOUT",
"模型服务响应超时"));
}
}
这样所有 Controller 的错误结构保持一致。
六、通用响应结构
14. ApiResponse
常见统一返回结构:
@Data
@AllArgsConstructor
public class ApiResponse<T> {
private int code;
private String message;
private T data;
private String requestId;
public static <T> ApiResponse<T> success(T data) {
return new ApiResponse<>(
200,
"success",
data,
RequestContext.getRequestId());
}
public static <T> ApiResponse<T> failure(
String code,
String message) {
return new ApiResponse<>(
500,
message,
null,
RequestContext.getRequestId());
}
}
前端收到:
{
"code": 200,
"message": "success",
"data": {
"heatId": "H20260510-001",
"temperature": 1662
},
"requestId": "req-20260717-001"
}
七、配置相关层
15. Config:配置类
用于创建和配置 Spring Bean。
例如模型服务 HTTP 客户端:
@Configuration
public class MlServiceConfig {
@Bean
public WebClient mlWebClient(
MlServiceProperties properties) {
return WebClient.builder()
.baseUrl(properties.getBaseUrl())
.build();
}
}
配置文件:
bof:
ml-service:
base-url: http://127.0.0.1:18080
connect-timeout: 2s
read-timeout: 10s
属性类:
@Data
@ConfigurationProperties(prefix = "bof.ml-service")
public class MlServiceProperties {
private String baseUrl;
private Duration connectTimeout;
private Duration readTimeout;
}
常见 Config:
WebConfig
MyBatisConfig
SecurityConfig
JacksonConfig
CorsConfig
WebClientConfig
RedisConfig
TransactionConfig
八、安全相关层
16. Security
Security 负责:
用户登录
Token 验证
接口权限
角色判断
管理员操作控制
例如:
@PreAuthorize("hasRole('OPERATOR')")
@PostMapping("/endpoint")
public ApiResponse<EndpointPredictionVO> predict(...) {
}
模型激活接口:
@PreAuthorize("hasRole('MODEL_ADMIN')")
@PostMapping("/models/{modelId}/activate")
public ApiResponse<Void> activate(...) {
}
普通操作人员可以:
执行预测
查看预测结果
模型管理员可以:
上传模型
验证模型
激活模型
回滚模型
下架模型
九、请求预处理层
17. Filter
Filter 工作在 Servlet 层,通常早于 Controller。
常见用途:
生成 request_id
读取 Token
跨域处理
请求日志
编码处理
请求体包装
例如:
@Component
public class RequestIdFilter
extends OncePerRequestFilter {
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
String requestId =
UUID.randomUUID().toString();
MDC.put("requestId", requestId);
response.setHeader(
"X-Request-Id",
requestId);
try {
filterChain.doFilter(
request,
response);
} finally {
MDC.remove("requestId");
}
}
}
18. Interceptor
Interceptor 位于 Spring MVC 内部,主要围绕 Controller 工作。
常见用途:
登录检查
权限补充检查
操作日志
接口耗时
炉次上下文
重复提交限制
例如:
@Component
public class ApiTimingInterceptor
implements HandlerInterceptor {
@Override
public boolean preHandle(
HttpServletRequest request,
HttpServletResponse response,
Object handler) {
request.setAttribute(
"startTime",
System.currentTimeMillis());
return true;
}
@Override
public void afterCompletion(
HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) {
long elapsed =
System.currentTimeMillis()
- (long) request.getAttribute("startTime");
}
}
19. AOP / Aspect
AOP 用于处理横切逻辑,即多个业务方法都会出现、但不属于核心业务的内容。
例如:
操作日志
接口耗时
权限注解
幂等处理
数据权限
模型调用统计
示例:
@Aspect
@Component
public class ModelOperationAspect {
@Around("@annotation(ModelOperationLog)")
public Object record(ProceedingJoinPoint point)
throws Throwable {
long start = System.currentTimeMillis();
try {
return point.proceed();
} finally {
long elapsed =
System.currentTimeMillis() - start;
}
}
}
十、事务层
20. Transaction
事务通常放在 Service 层。
例如激活模型时,需要同时完成:
旧模型 active → inactive
新模型 staged → active
更新 bof_model_route
写入部署事件
这些操作应该作为一个整体成功或失败:
@Service
@RequiredArgsConstructor
public class ModelActivationService {
private final ModelCatalogMapper catalogMapper;
private final ModelRouteMapper routeMapper;
private final DeploymentEventMapper eventMapper;
@Transactional
public void activate(Long modelId) {
ModelCatalogEntity model =
catalogMapper.selectById(modelId);
routeMapper.switchActiveModel(
model.getTaskKey(),
modelId);
catalogMapper.updateStatus(
modelId,
"active");
eventMapper.insert(
DeploymentEventEntity.activate(modelId));
}
}
Spring 提供统一的事务抽象,并支持声明式事务;@Transactional 通常用于 Service 层的业务边界。(Home)
不要把事务主要放在 Controller:
@PostMapping(...)
@Transactional // 不推荐作为主要业务事务边界
更合理的是:
Controller
↓
@Transactional Service
↓
多个 Mapper 操作
十一、其他常见目录
21. Enum
保存固定枚举值:
public enum ModelStatus {
UPLOADED,
VALIDATING,
STAGED,
ACTIVE,
INACTIVE,
RETIRED,
VALIDATION_FAILED,
LOAD_FAILED
}
优点是避免代码中到处出现:
"active"
"inactive"
"staged"
22. Constant
保存固定常量:
public final class ModelTaskKeys {
public static final String ENDPOINT_PREDICTION =
"endpoint_prediction";
public static final String SLAG_PREDICTION =
"slag_prediction";
private ModelTaskKeys() {
}
}
23. Util
用于无状态、通用、确实可以复用的工具方法:
HashUtils
JsonUtils
TimeUtils
FileUtils
例如:
public final class HashUtils {
public static String sha256(String content) {
// 计算 SHA-256
}
}
不要把所有不知道放哪里的业务代码都放进 util。
错误示例:
EndpointPredictionUtil
ModelActivateUtil
HeatBusinessUtil
如果里面包含业务规则,它更适合放在:
Service
DomainService
Converter
24. Validator
复杂业务校验可以单独提取:
@Component
public class EndpointPredictionValidator {
public void validate(HeatEntity heat) {
if (heat == null) {
throw new HeatNotFoundException();
}
if (heat.getMainBlowEndTime() == null) {
throw new BusinessException(
"主吹尚未结束,不能执行终点预测");
}
}
}
Service 中调用:
validator.validate(heat);
25. Event / Listener
当一个业务完成后,需要触发附属操作,可以使用事件。
例如模型激活成功后:
publisher.publishEvent(
new ModelActivatedEvent(modelId));
监听器:
@Component
public class ModelActivatedListener {
@EventListener
public void handle(ModelActivatedEvent event) {
// 更新缓存、发送通知或刷新状态
}
}
如果事件必须在数据库事务提交成功之后执行,可以使用事务事件监听。Spring 支持通过 @TransactionalEventListener 将事件监听绑定到事务阶段。(Home)
26. Scheduler / Job
用于定时任务:
定时检查 Worker 健康状态
定时清理过期缓存
定时同步炉次数据
定时检查模型漂移
例如:
@Component
@RequiredArgsConstructor
public class WorkerHealthJob {
private final WorkerHealthService healthService;
@Scheduled(fixedDelay = 60_000)
public void checkWorkers() {
healthService.checkAll();
}
}
十二、常见错误分层方式
错误一:Controller 写全部逻辑
@PostMapping("/endpoint")
public Object predict(...) {
HeatEntity heat =
heatMapper.selectByHeatId(...);
if (heat == null) {
...
}
Object response =
webClient.post()...;
modelResultMapper.insert(...);
return response;
}
问题是:
Controller 过重
难以测试
难以复用
事务边界不清楚
错误二:Service 直接写 SQL
@Service
public class EndpointService {
public Object predict() {
String sql = "SELECT ...";
}
}
SQL 应放在 Mapper 或 Repository 层。
错误三:Mapper 包含业务判断
@Mapper
public interface ModelMapper {
boolean canActivateModel(Long modelId);
}
如果 canActivateModel 涉及:
状态判断
健康检查
是否存在替代模型
Schema 是否兼容
Worker 是否已预热
这不是简单数据库访问,应放在 Service。
错误四:Entity 直接返回前端
@GetMapping
public ModelResultEntity getResult() {
return mapper.selectById(...);
}
容易把数据库内部字段暴露给前端。
错误五:ServiceImpl 只是转发
public HeatEntity getHeat(String heatId) {
return heatMapper.selectByHeatId(heatId);
}
简单查询可以存在,但如果所有 Service 都只是机械转发,说明分层可能过度。
十五、最容易记住的总结
Controller:接口入口
Request:前端传了什么
Service:业务应该怎么执行
Mapper:数据库怎么读写
Entity:数据库中的数据长什么样
DTO:层与层之间传什么
VO:最终给前端展示什么
Converter:对象之间怎么转换
Client:怎么调用外部系统
Config:组件怎么配置
Validator:业务参数是否合法
Exception:失败时是什么问题
Filter:请求进入系统前做什么
Interceptor:进入 Controller 前后做什么
Aspect:公共横切逻辑怎么统一处理
Security:谁能访问接口
Transaction:多项数据库操作如何保证一致
Job:定时执行什么
Event:业务完成后触发什么
放到你的项目中,就是:
前端提出预测请求
Controller 接收请求
Service 组织预测流程
Mapper 查询和保存业务数据
Client 调用 bof-ml-service
Python 完成特征构造和模型推理
Java Converter 整理结果
VO 返回前端
最核心的分层原则是:
Controller 不写业务,Service 不写具体 SQL,Mapper 不做业务决策,Entity 不直接代替接口对象,外部系统调用单独放在 Client 或 Adapter 层。