1. 背景与痛点
在我的编程平台项目中,判题服务(Judge Service)需要调用代码沙箱(Code Sandbox)来执行用户提交的代码。最初的实现方式是使用工厂模式硬编码创建沙箱实例,配置参数(如 URL、密钥)也分散在代码中。
这种方式存在明显的痛点:
-
耦合度高:业务代码强依赖具体的沙箱实现,切换沙箱(如从本地切换到远程)需要修改代码。
-
配置分散:鉴权参数、超时设置没有统一管理。
-
复用性差:如果未来有其他微服务也需要调用沙箱,只能复制粘贴代码。
为了解决这些问题,我决定利用 Spring Boot 的 自动装配(Auto Configuration) 机制,将沙箱调用逻辑封装为一个通用的 Starter,实现“引入依赖、配置参数、开箱即用”。
2. 核心架构设计
-
CodeSandbox:定义统一的沙箱接口。
-
CodeSandboxProperties:定义统一的配置属性类(
prefix = "codesandbox")。 -
CodeSandboxAutoConfiguration:核心自动配置类,根据配置动态加载具体的 Bean。
-
SPI 机制:通过
spring.factories或imports文件暴露自动配置。
3. 实现步骤
3.1 创建 Maven 模块与依赖
新建一个 Maven 模块 code-sandbox-spring-boot-starter。 pom.xml: 需要引入 spring-boot-autoconfigure 和 configuration-processor(用于生成配置元数据,编写配置文件时会有提示)。
XML
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.25</version>
</dependency>
</dependencies>
3.2 定义配置属性类
使用 @ConfigurationProperties 将 application.yml 中的配置映射为 Java 对象,实现配置的统一管理。
Java
package com.xqc.sandbox;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
@Data
@ConfigurationProperties(prefix = "codesandbox")
public class CodeSandboxProperties {
/**
* 沙箱类型: example (默认), remote, thirdparty
*/
private String type = "example";
/**
* 远程沙箱地址
*/
private String url;
/**
* 鉴权账号 / AccessKey (部分沙箱需要)
*/
private String accessKey;
/**
* 鉴权密钥 / SecretKey (部分沙箱需要)
*/
private String secretKey;
/**
* 超时时间 (毫秒),默认 5000
*/
private Integer timeout = 5000;
}
3.3 定义核心接口
Java
package com.xqc.sandbox;
import com.xqc.sandbox.model.ExecuteRequest;
import com.xqc.sandbox.model.ExecuteResponse;
public interface CodeSandbox {
/**
* 执行代码
* @param request 请求参数
* @return 执行结果
*/
ExecuteResponse executeCode(ExecuteRequest request);
}
3.4 编写具体实现类
远程沙箱实现 (RemoteCodeSandbox): 这里体现了 Starter 的优势——直接注入配置对象,无需手动传参。
Java
package com.xqc.sandbox.impl;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONUtil;
import cn.hutool.core.util.StrUtil;
import com.xqc.sandbox.CodeSandbox;
import com.xqc.sandbox.CodeSandboxProperties;
import com.xqc.sandbox.model.ExecuteRequest;
import com.xqc.sandbox.model.ExecuteResponse;
public class RemoteCodeSandbox implements CodeSandbox {
private final CodeSandboxProperties properties;
// 构造器注入配置对象
public RemoteCodeSandbox(CodeSandboxProperties properties) {
this.properties = properties;
}
@Override
public ExecuteResponse executeCode(ExecuteRequest executeRequest) {
String url = properties.getUrl();
String secretKey = properties.getSecretKey();
if (StrUtil.isBlank(url)) {
throw new RuntimeException("RemoteCodeSandbox needs a valid URL");
}
String json = JSONUtil.toJsonStr(executeRequest);
// 发起 HTTP 请求
String responseStr = HttpUtil.createPost(url)
.header("auth", secretKey) // 鉴权
.body(json)
.timeout(properties.getTimeout()) // 超时控制
.execute()
.body();
if (StrUtil.isBlank(responseStr)) {
throw new RuntimeException("Remote sandbox no response");
}
return JSONUtil.toBean(responseStr, ExecuteResponse.class);
}
}
默认示例沙箱 (ExampleCodeSandbox): 用于在未配置任何沙箱时的兜底策略。
Java
package com.xqc.sandbox.impl;
public class ExampleCodeSandbox implements CodeSandbox {
@Override
public ExecuteResponse executeCode(ExecuteRequest executeRequest) {
System.out.println("示例代码沙箱被调用");
// 返回模拟数据...
return new ExecuteResponse();
}
}
3.5 编写自动配置类 (核心)
这是 Starter 的灵魂。通过 @ConditionalOnProperty 根据配置文件动态决定加载哪个 Bean。
Java
package com.xqc.sandbox;
import com.xqc.sandbox.impl.ExampleCodeSandbox;
import com.xqc.sandbox.impl.RemoteCodeSandbox;
import com.xqc.sandbox.impl.ThirdPartyCodeSandbox;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableConfigurationProperties(CodeSandboxProperties.class) // 开启配置类注入
public class CodeSandboxAutoConfiguration {
/**
* 策略1:当配置 codesandbox.type = remote 时加载
*/
@Bean
@ConditionalOnProperty(name = "codesandbox.type", havingValue = "remote")
public CodeSandbox remoteCodeSandbox(CodeSandboxProperties properties) {
return new RemoteCodeSandbox(properties);
}
/**
* 策略2:当配置 codesandbox.type = thirdparty 时加载
*/
@Bean
@ConditionalOnProperty(name = "codesandbox.type", havingValue = "thirdparty")
public CodeSandbox thirdPartyCodeSandbox(CodeSandboxProperties properties) {
return new ThirdPartyCodeSandbox(properties);
}
/**
* 兜底策略:当没有配置 type 或没有其他 Bean 时加载
*/
@Bean
@ConditionalOnMissingBean(CodeSandbox.class)
public CodeSandbox exampleCodeSandbox() {
return new ExampleCodeSandbox();
}
}
3.6 注册自动配置 (SPI)
为了让 Spring Boot 启动时能扫描到我们的自动配置类,需要在资源目录下创建描述文件。
Spring Boot 2.7 及以下版本: 文件路径:src/main/resources/META-INF/spring.factories
Properties
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.xqc.sandbox.CodeSandboxAutoConfiguration
Spring Boot 3.x 版本 : 文件路径:src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
Plaintext
com.xqc.sandbox.CodeSandboxAutoConfiguration
4. 使用效果
完成 Starter 开发并 mvn install 后,在业务项目中引入依赖。
4.1 配置文件 (application.yml)
现在切换沙箱只需要改一行配置,甚至可以利用 Nacos 动态刷新。
YAML
codesandbox:
type: remote # 切换为 example 即可使用本地模式
url: http://192.168.1.100:8090/execute
secret-key: xxxx-xxxx-xxxx
timeout: 10000
4.2 业务代码重构
原来的工厂模式代码完全移除,直接利用 Spring 的依赖注入:
Java
@Service
public class JudgeServiceImpl implements JudgeService {
// 自动装配:Spring 会根据 yml 配置自动注入对应的实现类
@Resource
private CodeSandbox codeSandbox;
public QuestionSubmit doJudge(long questionSubmitId) {
// ... 前置逻辑
ExecuteRequest request = ExecuteRequest.builder()
.code(code)
.language(language)
.build();
// 核心调用:完全解耦,无需关心内部是如何创建的
ExecuteResponse executeResponse = codeSandbox.executeCode(request);
// ... 后置逻辑
}
}
5. 总结
通过封装 Spring Boot Starter,我们实现了:
-
业务解耦:判题服务不再关心沙箱的具体实现细节。
-
配置统一:所有沙箱相关的参数收敛到
CodeSandboxProperties。 -
极简接入:新服务只需引入 Maven 依赖并添加几行配置,即可具备沙箱调用能力。
这正是 Spring Boot "约定大于配置" 设计理念的最佳实践。
701

被折叠的 条评论
为什么被折叠?



