Spring Boot Starter 实践:将判题服务沙箱调用封装为 SDK

1. 背景与痛点

在我的编程平台项目中,判题服务(Judge Service)需要调用代码沙箱(Code Sandbox)来执行用户提交的代码。最初的实现方式是使用工厂模式硬编码创建沙箱实例,配置参数(如 URL、密钥)也分散在代码中。

这种方式存在明显的痛点:

  • 耦合度高:业务代码强依赖具体的沙箱实现,切换沙箱(如从本地切换到远程)需要修改代码。

  • 配置分散:鉴权参数、超时设置没有统一管理。

  • 复用性差:如果未来有其他微服务也需要调用沙箱,只能复制粘贴代码。

为了解决这些问题,我决定利用 Spring Boot 的 自动装配(Auto Configuration) 机制,将沙箱调用逻辑封装为一个通用的 Starter,实现“引入依赖、配置参数、开箱即用”。

2. 核心架构设计

  • CodeSandbox:定义统一的沙箱接口。

  • CodeSandboxProperties:定义统一的配置属性类(prefix = "codesandbox")。

  • CodeSandboxAutoConfiguration:核心自动配置类,根据配置动态加载具体的 Bean。

  • SPI 机制:通过 spring.factoriesimports 文件暴露自动配置。


3. 实现步骤

3.1 创建 Maven 模块与依赖

新建一个 Maven 模块 code-sandbox-spring-boot-starterpom.xml: 需要引入 spring-boot-autoconfigureconfiguration-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 定义配置属性类

使用 @ConfigurationPropertiesapplication.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,我们实现了:

  1. 业务解耦:判题服务不再关心沙箱的具体实现细节。

  2. 配置统一:所有沙箱相关的参数收敛到 CodeSandboxProperties

  3. 极简接入:新服务只需引入 Maven 依赖并添加几行配置,即可具备沙箱调用能力。

这正是 Spring Boot "约定大于配置" 设计理念的最佳实践。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值