判题机的开发(代码沙箱、三种模式、工厂模式、策略模式优化、代理模式)

判题机模块预开发

  1. 梳理判题模块和代码沙箱的关系
    判题模块:调用代码沙箱,把代码和输入交给代码沙箱去执行。
    代码沙箱:只负责接收代码和输入,返回编译运行的结果,不负责判题。(可以作为独立的项目/服务,提供给其他的需要执行代码的项目去使用)
    这两个模块完全解耦。
    思考:为什么代码沙箱要接受和输出一组运行用例?
    每道题有多组用例,如果每条用例都单独调用一次代码沙箱,会调用多个接口、需要多次网络传输、程序要多次编译、记录程序的执行状态(重复代码不重复编译)

代码沙箱开发

  1. 定义代码沙箱的接口,提高通用性(之后我们的项目代码只调用接口,不调用具体实现类,这样在调用其他代码沙箱实现类,就不用去修改名称了,便于扩展)
    扩展思路:代码可以增加一个查看代码沙箱状态的接口
  2. 定义多种不同代码沙箱实现:
    示例代码沙箱、远程代码沙箱、第三方代码沙箱

LomBok Builder注解:

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
  1. 编写单元测试,验证单个代码沙箱的执行

    @Test
    void executeCode() {
    CodeSandbox codeSandbox=new ExampleCodeSandbox();
    String userCode="int main(){}";
    String codeLanguage= QuestionSubmitLanguageEnum.JAVA.getValue();
    List<String>inputList= Arrays.asList("1 2","2 3");
    ExecuteCodeRequest executeCodeRequest= ExecuteCodeRequest.builder()
    .userCode(userCode)
    .codeLanguage(codeLanguage)
    .inputList(inputList)
    .build();
    ExecuteCodeResponse executeCodeResponse=codeSandbox.executeCode(executeCodeRequest);
    Assertions.assertNotNull(executeCodeResponse);
    }

存在问题:new某个沙箱代码写死了,若要改用其他沙箱,需要改动很多处代码

  1. 使用工厂模式,根据用户传入的字符串参数来生成对应代码沙箱实现类

    public static void main(String[] args) {
    Scanner scanner = new Scanner(System.in);
    while (scanner.hasNext()) {
    String type = scanner.next();
    CodeSandbox codeSandbox = CodeSandboxFactory.newInstance(type);
    String userCode = "int main(){}";
    String codeLanguage = QuestionSubmitLanguageEnum.JAVA.getValue();
    List<String> inputList = Arrays.asList("1 2", "2 3");
    ExecuteCodeRequest executeCodeRequest = ExecuteCodeRequest.builder()
    .userCode(userCode)
    .codeLanguage(codeLanguage)
    .inputList(inputList)
    .build();
    ExecuteCodeResponse executeCodeResponse = codeSandbox.executeCode(executeCodeRequest);
    }
    }

只需要根据字符串来判断然后生成沙箱,无需再手动创建。

  1. 参数配置化,把项目中一些可选项交给用户去自定义选项或字符串,写到配置文件中。这样只需改变配置文件,而无需看代码内容,就可以更方便自由的自定义使用项目更多功能。

    #代码沙箱配置
    codesandbox:
    type: example

    @Value("${codesandbox.type}")
    private String type;

  2. 代码沙箱能力增强:比如在调用代码沙箱前,输出请求参数;在代码沙箱调用后,输出响应结果日志,便于管理员分析
    ------>使用代理模式,提供一个Proxy,来增强代码沙箱的能力
    原本需要用户自己多次调用日志,使用代理后,调用者只需要调用代理类,代理类调用沙箱类(代理类可以完成一些额外的功能)。优点:不仅不需要改变原本沙箱,对调用者来说,调用方式几乎没有改变,无需在每个调用沙箱的代码上方再去调用日志

    @Slf4j
    public class CodeSandboxProxy implements CodeSandbox{
    private final CodeSandbox codeSandbox;
    public CodeSandboxProxy(CodeSandbox codeSandbox){
    this.codeSandbox=codeSandbox;
    }
    @Override
    public ExecuteCodeResponse executeCode(ExecuteCodeRequest executeCodeRequest) {
    log.info("请求信息"+executeCodeRequest.toString());
    ExecuteCodeResponse executeCodeResponse= codeSandbox.executeCode(executeCodeRequest);
    log.info("响应信息"+executeCodeResponse.toString());
    return executeCodeResponse;
    }
    }

    CodeSandbox codeSandbox=CodeSandboxFactory.newInstance(type);
    codeSandbox=new CodeSandboxProxy(codeSandbox);

    /**

    • 示例代码沙箱(单纯跑通业务流程)
      */
      public class ExampleCodeSandbox implements CodeSandbox {
      @Override
      public ExecuteCodeResponse executeCode(ExecuteCodeRequest executeCodeRequest) {
      List<String> inputList = executeCodeRequest.getInputList();
      ExecuteCodeResponse executeCodeResponse=new ExecuteCodeResponse();
      executeCodeResponse.setOutputList(inputList);
      executeCodeResponse.setMessage("测试成功!");
      executeCodeResponse.setStatus(QuestionSubmitStatusEnum.SUCCEED.getValue());
      JudgeInfo judgeInfo=new JudgeInfo();
      judgeInfo.setTime(100L);
      judgeInfo.setMemory(100L);
      judgeInfo.setMessage(JudgeInfoMessageEnum.ACCEPTED.getText());
      executeCodeResponse.setJudgeInfo(judgeInfo);
      return executeCodeResponse;
      }
      }

判题服务完整业务流程实现

判题服务业务流程

  1. 获取题目id,获取对应题目提交信息(代码,编程语言)

  2. 如果提交状态不为等待中就不用重复执行

  3. 更改题目提交状态"判题中",防止重复执行,也能让用户看到判题状态

  4. 调用沙箱,获取执行结果

  5. 根据执行结果,设置题目判题状态和信息
    判断逻辑:1、先判断沙箱执行的结果输出数量是否和预期数量相等;2、判断每一项输出和预期输出是否相等;3、判断题目的限制是否符合要求;4、还可能有其他异常情况

    @Service
    public class JudegeServiceImpl implements JudegeService {
    @Value("${codesandbox.type}")
    private String type;
    @Resource
    private QuestionService questionService;
    @Resource
    private QuestionSubmitService questionSubmitService;
    /**
    * 1. 获取题目id,获取对应题目提交信息(代码,编程语言)
    * 2. 如果提交状态不为等待中就不用重复执行
    * 3. 更改题目提交状态"判题中",防止重复执行,也能让用户看到判题状态
    * 4. 调用沙箱,获取执行结果
    * 5. 根据执行结果,设置题目判题状态和信息
    * @param questionSubmitId
    * @return
    */
    @Override
    public QuestionSubmitVO doJudege(long questionSubmitId) {
    // 1. 获取题目id,获取对应题目提交信息(代码,编程语言)
    QuestionSubmit questionSubmit=questionSubmitService.getById(questionSubmitId);
    if(questionSubmit==null){
    throw new BusinessException(ErrorCode.NOT_FOUND_ERROR,"提交信息不存在!");
    }
    Long questionId = questionSubmit.getQuestionId();
    Question question=questionService.getById(questionId);
    if(question==null){
    throw new BusinessException(ErrorCode.NOT_FOUND_ERROR,"题目不存在!");
    }
    // 2. 如果提交状态不为等待中就不用重复执行
    if(!questionSubmit.getCodeStatus().equals(QuestionSubmitStatusEnum.WAITING.getValue())){
    throw new BusinessException(ErrorCode.OPERATION_ERROR,"正在判题中!");
    }
    //3. 更改题目提交状态"判题中",防止重复执行,也能让用户看到判题状态
    QuestionSubmit questionSubmitUpdate=new QuestionSubmit();
    questionSubmitUpdate.setId(questionSubmitId);
    questionSubmitUpdate.setCodeStatus(QuestionSubmitStatusEnum.RUNNING.getValue());
    boolean update=questionSubmitService.updateById(questionSubmitUpdate);
    if(!update){
    throw new BusinessException(ErrorCode.SYSTEM_ERROR,"题目状态更新错误!");
    }
    // 4. 调用沙箱,获取执行结果
    CodeSandbox codeSandbox= CodeSandboxFactory.newInstance(type);
    codeSandbox=new CodeSandboxProxy(codeSandbox);
    String codeLanguage = questionSubmit.getCodeLanguage();
    String userCode = questionSubmit.getUserCode();
    //获取输入用例
    String judegeCaseStr=question.getJudgeCase();
    List<JudgeCase>judgeCaseList=JSONUtil.toList(judegeCaseStr, JudgeCase.class);
    List<String>inputList=judgeCaseList.stream().map(JudgeCase::getInput).collect(Collectors.toList());
    ExecuteCodeRequest executeCodeRequest= ExecuteCodeRequest.builder()
    .userCode(userCode)
    .codeLanguage(codeLanguage)
    .inputList(inputList)
    .build();
    ExecuteCodeResponse executeCodeResponse=codeSandbox.executeCode(executeCodeRequest);

    // 5. 根据执行结果,设置题目判题状态和信息
    JudgeInfoMessageEnum judgeInfoMessageEnum=JudgeInfoMessageEnum.WAITING;
    List<String> outputList = executeCodeResponse.getOutputList();
    // 依次判断每一项输出和预期是否相等
    if(outputList.size()!=inputList.size()){
    judgeInfoMessageEnum=JudgeInfoMessageEnum.WRONG_ANSWER;
    return null;
    }
    for (int i=0;i<judgeCaseList.size();i++){
    if(!judgeCaseList.get(i).getOutput().equals(outputList.get(i))){
    judgeInfoMessageEnum=JudgeInfoMessageEnum.WRONG_ANSWER;
    return null;
    }
    }
    //判断题目限制
    JudgeInfo judgeInfo = executeCodeResponse.getJudgeInfo();
    Long time = judgeInfo.getTime();
    Long memory = judgeInfo.getMemory();
    String judgeConfigStr = question.getJudgeConfig();
    JudgeConfig judgeConfig = JSONUtil.toBean(judgeConfigStr, JudgeConfig.class);
    Long needMemoryLimit = judgeConfig.getMemoryLimit();
    Long needTimeLimit= judgeConfig.getTimeLimit();
    if(memory>needMemoryLimit){
    judgeInfoMessageEnum=JudgeInfoMessageEnum.MEMORY_LIMIT_EXCEEDED;
    return null;
    }
    if(time>needTimeLimit){
    judgeInfoMessageEnum=JudgeInfoMessageEnum.TIME_LIMIT_EXCEEDED;
    return null;
    }
    return null;
    }
    }


策略模式

思考:我们的代码沙箱本身执行时间,对于不同的编程语言是不同的。所以我们可以采用策略模式。针对不同的情况,定义独立的策略,而不是把所有的判题逻辑全部混在一起。

首先编写默认判题模块,如果所有的选择判题策略都写在判题服务代码中,代码会过于复杂,产生很多if-else,建议单独编写判断策略的方法。-->定义JudgeManager,尽量简化对判题功能的调用,

/**
 * 判题管理,简化调用
 *
 * @Author Adellle
 * @Date 2024/12/18 19:44
 * @Version 1.0
 */
 @Service
public class JudgeManager {
    JudgeInfo doJudge(JudgeContext judgeContext) {
        QuestionSubmit questionSubmit = judgeContext.getQuestionSubmit();
        String codeLanguage = questionSubmit.getCodeLanguage();
        JudgeStrategy judgeStrategy = new DefaultJudgeStrategy();
        if ("java".equals(codeLanguage)) {
            judgeStrategy = new JavaLanguageDefaultJudgeStrategy();
        }
        return judgeStrategy.doJudge(judgeContext);
    }
}

代码逻辑梳理

 @Resource
    @Lazy
    private JudgeService judgeService;

    /**
     * 提交题目
     *
     * @param questionSubmitAddRequest
     * @param loginUser
     * @return
     */
    @Override
    public long doQuestionSubmit(QuestionSubmitAddRequest questionSubmitAddRequest, User loginUser) {
        // 校验编程语言是否合法(校验合法性)
        String language = questionSubmitAddRequest.getCodeLanguage();
        QuestionSubmitLanguageEnum languageEnum = QuestionSubmitLanguageEnum.getEnumByValue(language);
        if (languageEnum == null) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR, "编程语言错误");
        }
        Long questionId = questionSubmitAddRequest.getQuestionId();
        // 判断实体是否存在,根据类别获取实体
        Question question = questionService.getById(questionId);
        if (question == null) {
            throw new BusinessException(ErrorCode.NOT_FOUND_ERROR);
        }
        // 是否已提交题目
        long userId = loginUser.getId();
        // 每个用户串行提交题目
        QuestionSubmit questionSubmit = new QuestionSubmit();
        questionSubmit.setUserId(userId);
        questionSubmit.setQuestionId(questionId);
        questionSubmit.setUserCode(questionSubmitAddRequest.getUserCode());
        questionSubmit.setCodeLanguage(questionSubmitAddRequest.getCodeLanguage());
        // 设置初始状态
        questionSubmit.setCodeStatus(QuestionSubmitStatusEnum.WAITING.getValue());
        questionSubmit.setJudgeInfo("{}");
        boolean save = this.save(questionSubmit);
        if (!save) {
            throw new BusinessException(ErrorCode.SYSTEM_ERROR, "插入数据失败!");
        }
        long questionSubmitId=questionSubmit.getId();
        //  (执行判题服务(异步执行))
        //作用:调用 judgeService.doJudege 方法来执行判题操作,使用CompletableFuture.runAsync() 来异步执行判题任务。
        CompletableFuture.runAsync(() -> {
            judgeService.doJudege(questionSubmitId);
        });
        return questionSubmitId;
    }

注:CompletableFuture.runAsync 会在独立的线程中异步执行判题操作,不会阻塞主线程。需要确保 judgeService.doJudege(questionSubmitId) 方法能够正确处理判题逻辑,并且判题是一个耗时操作,因此采用了异步方式。这里使用异步执行来提高系统性能,避免主线程被阻塞,确保用户能够快速得到提交的反馈。

开始判题

public interface JudgeService {
    /**
     * 判题服务
     *
     * @param questionSubmitId
     * @return
     */
    QuestionSubmit doJudege(long questionSubmitId);
}

@Service
public class JudgeServiceImpl implements JudgeService {
    @Value("${codesandbox.type}")
    private String type;
    @Resource
    private QuestionService questionService;
    @Resource
    private QuestionSubmitService questionSubmitService;
    @Resource
    private JudgeManager judgeManager;

    /**
     * 1. 获取题目id,获取对应题目提交信息(代码,编程语言)
     * 2. 如果提交状态不为等待中就不用重复执行
     * 3. 更改题目提交状态"判题中",防止重复执行,也能让用户看到判题状态
     * 4. 调用沙箱,获取执行结果
     * 5. 根据执行结果,设置题目判题状态和信息
     *
     * @param questionSubmitId
     * @return
     */
    @Override
    public QuestionSubmit doJudege(long questionSubmitId) {
//        1. 获取题目id,获取对应题目提交信息(代码,编程语言)
        QuestionSubmit questionSubmit = questionSubmitService.getById(questionSubmitId);
        if (questionSubmit == null) {
            throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "提交信息不存在!");
        }
        Long questionId = questionSubmit.getQuestionId();
        Question question = questionService.getById(questionId);
        if (question == null) {
            throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "题目不存在!");
        }
//        2. 如果提交状态不为等待中就不用重复执行
        if (!questionSubmit.getCodeStatus().equals(QuestionSubmitStatusEnum.WAITING.getValue())) {
            throw new BusinessException(ErrorCode.OPERATION_ERROR, "正在判题中!");
        }
        //3. 更改题目提交状态"判题中",防止重复执行,也能让用户看到判题状态
        QuestionSubmit questionSubmitUpdate = new QuestionSubmit();
        questionSubmitUpdate.setId(questionSubmitId);
        questionSubmitUpdate.setCodeStatus(QuestionSubmitStatusEnum.RUNNING.getValue());
        boolean update = questionSubmitService.updateById(questionSubmitUpdate);
        if (!update) {
            throw new BusinessException(ErrorCode.SYSTEM_ERROR, "题目状态更新错误!");
        }
//        4. 调用沙箱,获取执行结果
        CodeSandbox codeSandbox = CodeSandboxFactory.newInstance(type);
        codeSandbox = new CodeSandboxProxy(codeSandbox);
        String codeLanguage = questionSubmit.getCodeLanguage();
        String userCode = questionSubmit.getUserCode();
        //获取输入用例
        String judegeCaseStr = question.getJudgeCase();
        List<JudgeCase> judgeCaseList = JSONUtil.toList(judegeCaseStr, JudgeCase.class);
        List<String> inputList = judgeCaseList.stream().map(JudgeCase::getInput).collect(Collectors.toList());
        ExecuteCodeRequest executeCodeRequest = ExecuteCodeRequest.builder()
                .userCode(userCode)
                .codeLanguage(codeLanguage)
                .inputList(inputList)
                .build();
        ExecuteCodeResponse executeCodeResponse = codeSandbox.executeCode(executeCodeRequest);
        List<String> outputList = executeCodeResponse.getOutputList();
//        5. 根据执行结果,设置题目判题状态和信息
        JudgeContext judgeContext = new JudgeContext();
        judgeContext.setJudgeInfo(executeCodeResponse.getJudgeInfo());
        judgeContext.setInputList(inputList);
        judgeContext.setOutputList(outputList);
        judgeContext.setQuestion(question);
        judgeContext.setJudgeCaseList(judgeCaseList);
        judgeContext.setQuestionSubmit(questionSubmit);
        JudgeInfo judgeInfo = judgeManager.doJudge(judgeContext);
        //6. 修改数据库中的判题结果
        questionSubmitUpdate = new QuestionSubmit();
        questionSubmitUpdate.setId(questionSubmitId);
        questionSubmitUpdate.setCodeStatus(QuestionSubmitStatusEnum.SUCCEED.getValue());
        questionSubmitUpdate.setJudgeInfo(JSONUtil.toJsonStr(judgeInfo));
        update = questionSubmitService.updateById(questionSubmitUpdate);
        if (!update) {
            throw new BusinessException(ErrorCode.SYSTEM_ERROR, "题目状态更新错误!");
        }
        QuestionSubmit res = questionSubmitService.getById(questionId);
        return res;
    }
}
  • 使用策略模式,不把所有的if-else都放在判题服务中,减少对判题功能的调用

    @Service
    public class JudgeManager {
    JudgeInfo doJudge(JudgeContext judgeContext) {
    QuestionSubmit questionSubmit = judgeContext.getQuestionSubmit();
    String codeLanguage = questionSubmit.getCodeLanguage();
    JudgeStrategy judgeStrategy = new DefaultJudgeStrategy();
    if ("java".equals(codeLanguage)) {
    judgeStrategy = new JavaLanguageDefaultJudgeStrategy();
    }
    return judgeStrategy.doJudge(judgeContext);
    }
    }

    public class DefaultJudgeStrategy implements JudgeStrategy {

      @Override
      public JudgeInfo doJudge(JudgeContext judgeContext) {
          JudgeInfo judgeInfo = judgeContext.getJudgeInfo();
          Long time = judgeInfo.getTime();
          Long memory = judgeInfo.getMemory();
          JudgeInfo judgeInfoResponse=new JudgeInfo();
          judgeInfoResponse.setTime(time);
          judgeInfoResponse.setMemory(memory);
          List<String> inputList = judgeContext.getInputList();
          List<String> outputList = judgeContext.getOutputList();
          Question question = judgeContext.getQuestion();
          List<JudgeCase> judgeCaseList = judgeContext.getJudgeCaseList();
          JudgeInfoMessageEnum judgeInfoMessageEnum=JudgeInfoMessageEnum.ACCEPTED;
    

    // 依次判断每一项输出和预期是否相等
    if(outputList.size()!=inputList.size()){
    judgeInfoMessageEnum=JudgeInfoMessageEnum.WRONG_ANSWER;
    judgeInfoResponse.setMessage(judgeInfoMessageEnum.getValue());
    return judgeInfoResponse;
    }
    for (int i=0;i<judgeCaseList.size();i++){
    if(!judgeCaseList.get(i).getOutput().equals(outputList.get(i))){
    judgeInfoMessageEnum=JudgeInfoMessageEnum.WRONG_ANSWER;
    judgeInfoResponse.setMessage(judgeInfoMessageEnum.getValue());
    return judgeInfoResponse;
    }
    }
    //判断题目限制
    String judgeConfigStr = question.getJudgeConfig();
    JudgeConfig judgeConfig = JSONUtil.toBean(judgeConfigStr, JudgeConfig.class);
    Long needMemoryLimit = judgeConfig.getMemoryLimit();
    Long needTimeLimit= judgeConfig.getTimeLimit();
    if(memory>needMemoryLimit){
    judgeInfoMessageEnum=JudgeInfoMessageEnum.MEMORY_LIMIT_EXCEEDED;
    judgeInfoResponse.setMessage(judgeInfoMessageEnum.getValue());
    return judgeInfoResponse;
    }
    if(time>needTimeLimit){
    judgeInfoMessageEnum=JudgeInfoMessageEnum.TIME_LIMIT_EXCEEDED;
    judgeInfoResponse.setMessage(judgeInfoMessageEnum.getValue());
    return judgeInfoResponse;
    }
    judgeInfoResponse.setMessage(judgeInfoMessageEnum.getValue());
    return judgeInfoResponse;

      }
    

    }

    public class JavaLanguageDefaultJudgeStrategy implements JudgeStrategy {

      @Override
      public JudgeInfo doJudge(JudgeContext judgeContext) {
          JudgeInfo judgeInfo = judgeContext.getJudgeInfo();
          Long time = judgeInfo.getTime();
          Long memory = judgeInfo.getMemory();
          JudgeInfo judgeInfoResponse = new JudgeInfo();
          judgeInfoResponse.setTime(time);
          judgeInfoResponse.setMemory(memory);
          List<String> inputList = judgeContext.getInputList();
          List<String> outputList = judgeContext.getOutputList();
          Question question = judgeContext.getQuestion();
          List<JudgeCase> judgeCaseList = judgeContext.getJudgeCaseList();
          JudgeInfoMessageEnum judgeInfoMessageEnum = JudgeInfoMessageEnum.ACCEPTED;
    

    // 依次判断每一项输出和预期是否相等
    if (outputList.size() != inputList.size()) {
    judgeInfoMessageEnum = JudgeInfoMessageEnum.WRONG_ANSWER;
    judgeInfoResponse.setMessage(judgeInfoMessageEnum.getValue());
    return judgeInfoResponse;
    }
    for (int i = 0; i < judgeCaseList.size(); i++) {
    if (!judgeCaseList.get(i).getOutput().equals(outputList.get(i))) {
    judgeInfoMessageEnum = JudgeInfoMessageEnum.WRONG_ANSWER;
    judgeInfoResponse.setMessage(judgeInfoMessageEnum.getValue());
    return judgeInfoResponse;
    }
    }
    //判断题目限制
    String judgeConfigStr = question.getJudgeConfig();
    JudgeConfig judgeConfig = JSONUtil.toBean(judgeConfigStr, JudgeConfig.class);
    Long needMemoryLimit = judgeConfig.getMemoryLimit();
    Long needTimeLimit = judgeConfig.getTimeLimit();
    if (memory > needMemoryLimit) {
    judgeInfoMessageEnum = JudgeInfoMessageEnum.MEMORY_LIMIT_EXCEEDED;
    judgeInfoResponse.setMessage(judgeInfoMessageEnum.getValue());
    return judgeInfoResponse;
    }
    //Java程序本身需要额外执行10s
    long JAVA_PROGRAM_TIME_COST = 10000L;
    if ((time - JAVA_PROGRAM_TIME_COST) > needTimeLimit) {
    judgeInfoMessageEnum = JudgeInfoMessageEnum.TIME_LIMIT_EXCEEDED;
    judgeInfoResponse.setMessage(judgeInfoMessageEnum.getValue());
    return judgeInfoResponse;
    }
    judgeInfoResponse.setMessage(judgeInfoMessageEnum.getValue());
    return judgeInfoResponse;
    }
    }

相关推荐
工业互联网专业2 分钟前
基于springboot+vue的高校社团管理系统的设计与实现
java·vue.js·spring boot·毕业设计·源码·课程设计
九圣残炎4 分钟前
【ElasticSearch】 Java API Client 7.17文档
java·elasticsearch·搜索引擎
随心Coding6 分钟前
【零基础入门Go语言】错误处理:如何更优雅地处理程序异常和错误
开发语言·后端·golang
m0_748234527 分钟前
【Spring Boot】Spring AOP动态代理,以及静态代理
spring boot·后端·spring
m0_748251521 小时前
Ubuntu介绍、与centos的区别、基于VMware安装Ubuntu Server 22.04、配置远程连接、安装jdk+Tomcat
java·ubuntu·centos
咸甜适中1 小时前
go语言gui窗口应用之fyne框架-动态添加、删除一行控件(逐行注释)
开发语言·后端·golang
Bro_cat1 小时前
深入浅出JSON:数据交换的轻量级解决方案
java·ajax·java-ee·json
梁雨珈1 小时前
Groovy语言的安全开发
开发语言·后端·golang
等一场春雨1 小时前
Java设计模式 五 建造者模式 (Builder Pattern)
java·设计模式·建造者模式
hunzi_11 小时前
Java和PHP开发的商城系统区别
java·php