OJ 系统后端知识点梳理

2024 年 9 月 22 日 星期日(已编辑)
/ , , , , , , ,
22
摘要
对 OJ 后端代码进行知识点梳理
这篇文章上次修改于 2024 年 9 月 24 日 星期二,可能部分内容已经不适用,如有疑问可询问作者。

OJ 系统后端知识点梳理

ps:对 OJ 系统后端的进行梳理,梳理的知识点仅仅是针对作者本人有用的,以及添加了部分个人理解如果有理解错误或有其他更好的理解,麻烦给予指正,实在是感激不尽!!!若有其他侵权行为。请及时告知,会作以调整。

Controller 层

限制爬虫

  • 通过对分页查询接口,判断一页数据请求的大小(例如 size > 20),来判断是否被爬虫

model 层

  • dto 主要是对前端请求进行封装,在 Controller 层中使用 @RequestBody 来接收请求参数
  • 而 vo 是对 entity 或者 dto 进行再一次的封装,常用来返回给前端需要的字段(脱敏等等)

VO 层

  • 新增两个方法:objToVo 和 VoToObj (有助于转化)

service 层

新增方法 getQueryWrapper()

  • 返回值:QueryWrapper
  • 参数:DTO

作用:根据 DTO 拼接查询条件,返回 QueryWrapper

AOP + 自定义注解

本项目中 AOP 的作用:

  1. 权限校验(自定义注解)
  2. 请求响应日志记录

权限校验

  1. 自定义注解 AuthCheck
/**
 * 权限校验
 *
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AuthCheck {

    /**
     * 必须有某个角色
     *
     * @return
     */
    String mustRole() default "";

}
  • @Target 描述了此注解的作用域(要用在哪些地方),可取的值如下:

    • ElementType.METHOD 用于描述方法
    • ElementType.CONTRUCTOR 用于描述构造器
    • ElementType.FIELD 用于描述域
    • ElementType.LOCAL_VARIABLE 用于描述局部变量
    • ElementType.PACKAGE 用于描述包
    • ElementType.PARAMETER 用于描述参数
    • ElementType.TYPE 用于描述类,接口(注解类型等)或 enum 声明
  • @Retention 描述了此注解的生命周期(在什么范围有效),取值如下:

    • RententionPolicy.RUNTIME 运行时有效

    • RententionPolicy.CLASS 在 class 文件中有效

    • RententionPolicy.SOURCE 在源文件中有效

使用方法:

  • 直接在对应方法上面添加注解:@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
  • 表示只有管理员权限才能访问
  1. 自定义切面,围绕上述注解 @AuthCheck 进行鉴权
/**
 * 权限校验 AOP
 *
 */
@Aspect
@Component
public class AuthInterceptor {

    @Resource
    private UserService userService;

    /**
     * 执行拦截
     *
     * @param joinPoint
     * @param authCheck
     * @return
     */
    @Around("@annotation(authCheck)")
    public Object doInterceptor(ProceedingJoinPoint joinPoint, AuthCheck authCheck) throws Throwable {
        String mustRole = authCheck.mustRole();
        RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
        HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
        // 当前登录用户
        User loginUser = userService.getLoginUser(request);
        UserRoleEnum mustRoleEnum = UserRoleEnum.getEnumByValue(mustRole);
        // 不需要权限,放行
        if (mustRoleEnum == null) {
            return joinPoint.proceed();
        }
        // 必须有该权限才通过
        UserRoleEnum userRoleEnum = UserRoleEnum.getEnumByValue(loginUser.getUserRole());
        if (userRoleEnum == null) {
            throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
        }
        // 如果被封号,直接拒绝
        if (UserRoleEnum.BAN.equals(userRoleEnum)) {
            throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
        }
        // 必须有管理员权限
        if (UserRoleEnum.ADMIN.equals(mustRoleEnum)) {
            // 用户没有管理员权限,拒绝
            if (!UserRoleEnum.ADMIN.equals(userRoleEnum)) {
                throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
            }
        }
        // 通过权限校验,放行
        return joinPoint.proceed();
    }
}
  • @Aspect 定义了这个类是一个切面(使用 AOP 必用的一个类)
  • @Component 注册为 Bean 交给 Spring 管理
  • @Around 环绕通知(最强大的通知类型),通常方法返回值是 Object
  • joinPoint.proceed() 即执行原方法,通常返回一个 Object

请求响应日志

/**
 * 请求响应日志 AOP
 *
 **/
@Aspect
@Component
@Slf4j
public class LogInterceptor {

    /**
     * 执行拦截
     */
    @Around("execution(* com.yannqing.yanoj.controller.*.*(..))")
    public Object doInterceptor(ProceedingJoinPoint point) throws Throwable {
        // 计时
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        // 获取请求路径
        RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
        HttpServletRequest httpServletRequest = ((ServletRequestAttributes) requestAttributes).getRequest();
        // 生成请求唯一 id
        String requestId = UUID.randomUUID().toString();
        String url = httpServletRequest.getRequestURI();
        // 获取请求参数
        Object[] args = point.getArgs();
        String reqParam = "[" + StringUtils.join(args, ", ") + "]";
        // 输出请求日志
        log.info("request start,id: {}, path: {}, ip: {}, params: {}", requestId, url,
                httpServletRequest.getRemoteHost(), reqParam);
        // 执行原方法
        Object result = point.proceed();
        // 输出响应日志
        stopWatch.stop();
        long totalTimeMillis = stopWatch.getTotalTimeMillis();
        log.info("request end, id: {}, cost: {}ms", requestId, totalTimeMillis);
        return result;
    }
}

跨域配置

基于 Session 的跨域配置

/**
 * 全局跨域配置
 */
@Configuration
public class CorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        // 覆盖所有请求
        registry.addMapping("/**")
                // 允许发送 Cookie
                .allowCredentials(true)
                // 放行哪些域名(必须用 patterns,否则 * 会和 allowCredentials 冲突)
                // .allowedOriginPatterns("*")
                .allowedOrigins("https://yanoj.yannqing.com") // 这里改为你的前端地址
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                .allowedHeaders("*")
                .exposedHeaders("*");
    }
}

基于 token 的跨域配置

@Configuration
public class CorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowCredentials(true)
                .allowedOriginPatterns("*")
                .allowedMethods("GET","POST","PUT","DELETE")
                .allowedHeaders("*")
                .exposedHeaders("*");
    }
}

注意:

  • 现代浏览器出于安全考虑,不允许在使用 credentials: 'include' 时将 Access-Control-Allow-Origin 设置为 *。服务器必须指定具体的origin。因此,当前端设置了 credentials: 'include' 的时候,后端无论是使用 allowedOrigins(“ ”) 还是使用 allowedOriginPatterns(“ ”) 都不可以
  • allowedOrigins(“*”)allowCredentials(true) 不允许同时存在,可以使用 allowedOriginPatterns("*")allowCredentials(true)

equals 和 hashCode

ps:常用于 HashMap 和 HashSet 这种基于哈希表的集合

使用示例

  1. 在实体类中,自定义 hashCode () 和 equals() 方法,两个通常一起使用
import java.util.Objects;

public class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;
        Person person = (Person) obj;
        return age == person.age && Objects.equals(name, person.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }

    // Getters and setters omitted for brevity
}
  1. 使用实例:
import java.util.HashSet;

public class Main {
    public static void main(String[] args) {
        HashSet<Person> set = new HashSet<>();
        set.add(new Person("Alice", 30));
        set.add(new Person("Bob", 25));

        System.out.println(set.contains(new Person("Alice", 30))); // 输出 true
    }
}

解释:如果没有重写 hashCode() 方法。那么因为是 new 的一个新的 Person,输出结果为 false

使用 Lombok 注解实现

使用 @EqualsAndHashCode(callSuper = true)注解即可实现 equals 和 hashCode 方法

  • callSuper = true: 这个参数表示在生成的 equals()hashCode() 方法中,也会调用父类的 equals()hashCode() 方法。这对于类的继承结构非常重要,尤其是当父类中也有属性需要参与相等性和哈希码的计算时。

Mybatis-plus

对 Mybatis Plus 的使用

分页查询

步骤如下:

添加分页插件

/**
 * MyBatis Plus 配置
 *
 */
@Configuration
@MapperScan("com.yannqing.yanoj.mapper")
public class MyBatisPlusConfig {

    /**
     * 拦截器配置
     *
     * @return
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 分页插件
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }
}

查询的具体使用方法

  1. xxxxService.page() 方法

注意:xxxService 必须是继承了 IService 的接口

使用方式:

  • questionService.page(new Page<>(current, size), new QueryWrapper<Question>())
  • 第一个参数是 Page 对象 new Page<>(current, pageSize, totalCount)
  • 第二个参数是由 QueryWrapper 拼接的查询条件
  1. xxxxMapper.selectPage() 方法

注意:xxxxMapper 必须是继承了 BaseMapper 的接口

  • 使用方式和上述一致
  1. 手动分页(很少使用)

这种情况目前使用在:需要手动根据某一字段进行自定义排序

  • Page<QuestionSubmitVO> questionSubmitVOPage = new Page<>(current, pageSize, totalCount);
  • questionSubmitVOPage.setRecords(pageData);
// 1. 查询全部数据 List<QuestionSubmit>

// 2. 将所有的 List<QuestionSubmit> 转为 List<QuestionSubmitVO>
        
// 3. 对 QuestionSubmitVO 列表进行排序
questionSubmitVOList.sort((a, b) -> b.getCreatetime().compareTo(a.getCreatetime()));

// 4. 根据当前页和页大小进行分页
int totalCount = questionSubmitVOList.size();
int fromIndex = (current - 1) * pageSize;
int toIndex = Math.min(fromIndex + pageSize, totalCount);

List<QuestionSubmitVO> pageData = questionSubmitVOList.subList(fromIndex, toIndex);

// 5. 创建分页对象并返回
Page<QuestionSubmitVO> questionSubmitVOPage = new Page<>(current, pageSize, totalCount);
questionSubmitVOPage.setRecords(pageData);

DTO 请求参数分页结构

ps:当一个项目涉及的表过多的时候,分页查询也就多了起来,那么每一个分页查询的 DTO (前端向后端的请求参数)都要携带一些固定的参数(例如:current,size 等等)。因此可以写一个固定的 PageRequest 类,让所有的分页查询 DTO 继承 PageRequest 进行结构。

  1. 自定义 PageRequest 类
/**
 * 分页请求
 *
 */
@Data
public class PageRequest {

    /**
     * 当前页号
     */
    private int current = 1;

    /**
     * 页面大小
     */
    private int pageSize = 10;

    /**
     * 排序字段
     */
    private String sortField;

    /**
     * 排序顺序(默认升序)
     */
    private String sortOrder = CommonConstant.SORT_ORDER_ASC;
}
  1. 自定义 DTO
/**
 * 查询请求
 *
 */
@EqualsAndHashCode(callSuper = true)
@Data
public class QuestionQueryRequest extends PageRequest implements Serializable {

    /**
     * id
     */
    private Long id;

    /**
     * 标题
     */
    private String title;

    /**
     * 内容
     */
    private String content;

    /**
     * 标签列表
     */
    private List<String> tags;

    /**
     * 答案
     */
    private String answer;

    /**
     * 收藏数
     */
    private Integer favournum;

    /**
     * 创建用户 id
     */
    private Long userid;

    private static final long serialVersionUID = 1L;
}
  • 实现序列化
  • 继承 PageRequest
  • 实现 equals 和 hashCode 方法

QueryWrapper

QueryWrapper 的其他使用

eq() 方法

  • 第一个参数condition 条件,boolean 类型,用于判断是否添加此字段到拼接查询中。通常用来判断是否为空(可省)
  • 第二个参数:column 数据库列名,String 类型
  • 第三个参数:val 对应列的数据,Object 类型

sortBy() 方法

使用方式:orderBy(boolean condition, boolean isAsc, R... columns)

  • 第一个参数:是否使用这个条件(是否拼接)
  • 第二个参数:是否升序排序(true 为升序)
  • 第三个参数:要排序的列(可以多选,直接罗列即可)

排序规则(升序):

  • 对于数字,升序从小到大
  • 对于字符串,按字母表顺序从 A 到 Z
  • 对于日期,从最早到最新(即从最远到最近)

设计模式 + 模板方法

工厂模式

根据不同的情况,使用静态方法 newInstance 返回不同的实例,以此来决定使用不同的代码沙箱

工厂模式包含以下几个主要角色:

  • 抽象产品(Abstract Product):定义了产品的共同接口或抽象类。它可以是具体产品类的父类或接口,规定了产品对象的共同方法。下述的三个都是实现了 CodeSandBox 接口,这里的 CodeSandBox 就是抽象产品
  • 具体产品(Concrete Product):实现了抽象产品接口,定义了具体产品的特定行为和属性。如下述的 RemoteCodeSandBox,ThirdPartyCodeSandBox,ExampleCodeSandBox 都是具体产品
  • 抽象工厂(Abstract Factory):声明了创建产品的抽象方法,可以是接口或抽象类。它可以有多个方法用于创建不同类型的产品。这里没有使用这个角色
  • 具体工厂(Concrete Factory):实现了抽象工厂接口,负责实际创建具体产品的对象。这里的 CodeSandBoxFactory 就是具体工厂
/**
 * @description: 代码沙箱工厂(根据字符串参数创建对应的代码沙箱实例)
 * @author: yannqing
 * @create: 2024-08-06 17:47
 * @from: <更多资料:yannqing.com>
 **/
public class CodeSandBoxFactory {
    public static CodeSandBox newInstance(String type) {
        switch (type) {
            case "remote": {
                return new RemoteCodeSandBox();
            }
            case "thirdParty": {
                return new ThirdPartyCodeSandBox();
            }
            default: {
                return new ExampleCodeSandBox();
            }
        }
    }
}

工厂模式使用:

CodeSandBox codeSandBox = CodeSandBoxFactory.newInstance(type);

代理模式

ps:使用代理模式在执行方法的前后添加日志

代理模式通过引入一个代理对象来控制对原对象的访问。代理对象在客户端和目标对象之间充当中介,负责将客户端的请求转发给目标对象,同时可以在转发请求前后进行额外的处理

/**
 * 代码沙箱的代理模式(可以输出所有代码沙箱的请求和响应信息)
 */
@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;
    }
}

代理模式使用:

// 非代理模式使用
ExecuteCodeResponse executeCodeResponse = codeSandBox.executeCode(executeCodeRequest);

// 代理模式(在 CodeSandboxProxy 中的 executeCode 方法,可以做其他处理。然后 执行 原方法的 ExecuteCodeResponse executeCodeResponse = codeSandbox.executeCode(executeCodeRequest) 代码,并将响应信息返回即可
codeSandBox = new CodeSandboxProxy(codeSandBox);
ExecuteCodeResponse executeCodeResponse = codeSandBox.executeCode(executeCodeRequest);

策略模式

ps:使用 JudgeManager,执行不同的策略,使用 JudgeContext 封装代码沙箱的响应信息

  • 使判题服务进行解耦,代码不写死执行固定的策略(默认 / Java)

策略模式定义:

/**
 * 判题管理(简化调用)
 */
@Service
public class JudgeManager {

    /**
     * 执行判题
     *
     * @param judgeContext
     * @return
     */
    JudgeInfo doJudge(JudgeContext judgeContext) {
        QuestionSubmit questionSubmit = judgeContext.getQuestionSubmit();
        String language = questionSubmit.getLanguage();
        JudgeStrategy judgeStrategy = new DefaultJudgeStrategy();
        if ("java".equals(language)) {
            judgeStrategy = new JavaLanguageJudgeStrategy();
        }
        return judgeStrategy.doJudge(judgeContext);
    }
}

策略模式使用:

// 1. 使用策略模式 (解耦:代码不会固定使用哪一个判题策略)
JudgeInfo judgeInfo = judgeManager.doJudge(judgeContext);
// 2. 不使用策略模式
// JudgeStrategy judgeStrategy = new DefaultJudgeStrategy();
// JudgeInfo judgeInfo = judgeStrategy.doJudge(judgeContext);

模板方法

模板方法:定义一套通用的执行流程,让子类负责每个执行步骤的具体实现

模板方法的适用场景:适用于有规范的流程,且执行流程可以复用

作用:大幅节省重复代码量,便于项目扩展、更好维护

实现步骤:

  1. 创建一个抽象模板类 xxTemplate.java
  2. 把原来完整的一个方法抽离为一个个子方法
  3. 实际使用的时候,直接继承这个模板类
  4. 如果需要对某一方法进行覆盖,直接使用 Override 重写即可

定义

/**
 * @description: 模板方法实现
 * @author: yannqing
 * @create: 2024-08-23 17:51
 * @from: <更多资料:yannqing.com>
 **/
@Slf4j
public abstract class JavaCodeSandboxTemplate implements CodeSandBox{
        @Override
    public ExecuteCodeResponse executeCode(ExecuteCodeRequest executeCodeRequest) {
        List<String> inputList = executeCodeRequest.getInputList();
        String code = executeCodeRequest.getCode();
        String language = executeCodeRequest.getLanguage();

        // 1、保存代码文件
        File userCodeFile = saveCodeCodeToFile(code);

        // 2. 编译代码,得到 class 文件
        ExecuteMessage compileFileExecuteMessage = compileFile(userCodeFile);
        System.out.println(compileFileExecuteMessage);
        // 3. 执行代码,得到输出结果
        List<ExecuteMessage> executeMessageList = runFile(userCodeFile, inputList);

        // 4、整理输出, 封装结果
        ExecuteCodeResponse executeCodeResponse = getOutput(executeMessageList);


        // 5. 文件清理
        boolean result = deleteFile(userCodeFile);
        if (!result) {
            log.error("deleteFile error, userCodeFilePath: {}", userCodeFile.getAbsolutePath());
        }
        return executeCodeResponse;
    }
    // ... ... 下面是其他方法的实现,此处省略
    // 
    // getOutput,runFile,compileFile,saveCodeCodeToFile 等方法的实现
}

使用1:(重写 runFile方法)

/**
 * @description: java 原生代码沙箱操作Docker(直接复用模板方法)
 * @author: yannqing
 * @create: 2024-08-23 21:26
 * @from: <更多资料:yannqing.com>
 **/
@Component
public class JavaDockerCodeSandbox extends JavaCodeSandboxTemplate {

    @Override
    public List<ExecuteMessage> runFile(File userCodeFile, List<String> inputList) {
        
        // ... ... 其他逻辑
        
        return executeMessageList;
    }
}

使用2: (不做修改)

/**
 * @description: java 原生代码沙箱(直接复用模板方法)
 * @author: yannqing
 * @create: 2024-08-23 21:26
 * @from: <更多资料:yannqing.com>
 **/
@Component
public class JavaNativeCodeSandbox extends JavaCodeSandboxTemplate {


    @Override
    public ExecuteCodeResponse executeCode(ExecuteCodeRequest executeCodeRequest) {
        return super.executeCode(executeCodeRequest);
    }
}

进程类 Process

ps:执行原生的 cmd 命令

Process 抽象类

ps:包含 6 个抽象方法

// 获取子进程的输入流(是输入,不是输出!!下面也同理)
abstract public OutputStream getOutputStream();

// 获取子进程的输出
abstract public InputStream getInputStream();

// 获取子进程的错误输出
abstract public InputStream getErrorStream();

// 阻塞当前子进程的执行
abstract public int waitFor() throws InterruptedException;

// 获取子进程执行退出码(正常为 0)
abstract public int exitValue();

// 销毁当前子进程
abstract public void destroy();

两种实现方法

  1. 使用 Runtime 执行
String cmd = String.format("javac -encoding utf-8 %s", userCodeFile.getAbsolutePath());
Runtime runtime = Runtime.getRuntime();
Process p = runtime.exec(cmd);
  1. 使用 ProcessBuilder 执行
ProcessBuilder processBuilder = new ProcessBuilder("bash", "./finally.sh", voice);
Process p = processBuilder.start();

两者的区别

区别总述

ProcessBuilder:

  • 更灵活,支持更多的配置。

  • 可以单独设置命令和参数,例如设置环境变量、工作目录、重定向输入输出等。

  • 支持链式调用,可以逐步构建命令。

  • 允许将命令分开为多个部分,避免在执行时解析字符串命令时可能出现的问题。

Runtime:

  • 功能较简单,接受一个字符串来执行命令。

  • 无法直接设置环境变量或重定向 I/O(虽然可以通过 Process 对象处理 I/O,但需要手动操作)。

  • 命令参数必须作为一个完整的字符串,如果需要传递复杂参数,可能需要自己拼接字符串。

处理 I/O

  • ProcessBuilder 提供了更方便的 I/O 重定向支持,允许将标准输入、输出、错误流直接与文件或当前进程的流相关联:
ProcessBuilder processBuilder = new ProcessBuilder("cmd", "/c", "dir");
processBuilder.redirectOutput(new File("output.txt")); // 将输出重定向到文件
  • Runtime.exec() 没有内置的 I/O 重定向功能,必须手动操作 Process 的输入输出流

获取输出

使用 BufferReader 读取标准/错误输出流

// 创建线程来读取输入流和错误流
Thread inputThread = new Thread(() -> {
    try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
        String line;
        while ((line = reader.readLine()) != null) {
            System.out.println("Output: " + line);
        }
    } catch (IOException e) {
        throw new IllegalArgumentException(e.getMessage());
    }
});

Thread errorThread = new Thread(() -> {
    try (BufferedReader reader = new BufferedReader(new InputStreamReader(errorStream))) {
        String line;
        while ((line = reader.readLine()) != null) {
            System.err.println("Error: " + line);
        }
    } catch (IOException e) {
        throw new IllegalArgumentException(e.getMessage());

    }
});

// 启动输出流和错误流读取线程
inputThread.start();
errorThread.start();

// 等待外部进程执行完成
int exitCode = process.waitFor();
inputThread.join();  // 等待标准输出线程结束
errorThread.join();   // 等待错误输出线程结束

System.out.println("Exit code: " + exitCode);
if (exitCode != 0) {
    throw new IllegalArgumentException("脚本执行失败");
}

创建了两个线程来读取输出流和错误流

使用线程的优点:

  • 避免阻塞和死锁:标准输出和错误输出同时被读取,不会因为一个输出流填满缓冲区而导致另一个流阻塞或死锁。

  • 更高效的并发性:子进程的输出被并发处理,主线程也可以继续执行其他任务(如果有需要)。

不使用线程,两者同步读取输出的问题:

  • 阻塞问题:如果子进程在标准输出上输出大量数据,而错误输出暂时为空,程序会卡在读取标准输出的部分,直到标准输出结束,之后才开始读取错误输出。反之亦然,如果错误输出先有大量数据,而标准输出为空,程序会卡在读取错误输出上。这可能导致阻塞死锁问题,因为子进程的输出缓冲区可能会填满,阻止进程继续执行。

Docker Java

ps:使用 java 操作 docker 的类

  • DockerClientConfig:用于定义初始化 DockerClient 的配置(类比 MySQL 的连接、线程数配置)

  • DockerHttpClient:用于向 Docker 守护进程(操作 Docker 的接口)发送请求的客户端,低层封装(不推荐使用),你要自己构建请求参数(简单地理解成 JDBC)

  • DockerClient(推荐):才是真正和 Docker 守护进程交互的、最方便的 SDK,高层封装,对 DockerHttpClient 再进行了一层封装(理解成 MyBatis),提供了现成的增删改查

基本使用步骤

引入依赖

<!--        Docker Java 客户端的核心功能库-->
        <dependency>
            <groupId>com.github.docker-java</groupId>
            <artifactId>docker-java-core</artifactId>
            <version>3.3.6</version>
        </dependency>
<!--        使用 Apache HttpClient 5 来实现与 Docker 守护进程之间的 HTTP 通信-->
        <dependency>
            <groupId>com.github.docker-java</groupId>
            <artifactId>docker-java-transport-httpclient5</artifactId>
            <version>3.3.6</version>
        </dependency>

初始化 DockerClient

// 创建默认的 DockerClient 配置
DockerClientConfig config = DefaultDockerClientConfig.createDefaultConfigBuilder().build();
// 读取默认中的 docker 宿主机 ip(默认本机,否则是读取配置文件中的 ip)
DockerHttpClient httpClient = new ApacheDockerHttpClient.Builder()
        .dockerHost(config.getDockerHost())
        .build();
// 创建 DockerClient 实例
DockerClient dockerClient =  DockerClientImpl.getInstance(config, httpClient);

操作 Docker

  • 创建容器
  • 启动容器
  • 停止容器
  • 删除容器
  • 拉取镜像
  • 删除镜像
  • … …

远程连接

  1. 首先,保证远程防火墙放开端口:26665

  2. resources 目录下,新建文件 docker-java.properties (和 yml 配置文件是同级)
  3. 添加一行内容:DOCKER_HOST=tcp://xxx.xx.xx.xxx:26665

即可连接到远程 docker

操作 Docker

创建容器(启动)

  1. 可以使用 HonstConfig 指定容器的配置(内存,CPU,挂载,端口映射等等)
    1. withPortBindings 端口映射
    2. withBinds 挂载
    3. withMemory 指定内存限制
    4. withMemorySwap 减少内容到硬盘的写入
    5. withCpuCount 指定分配的 CPU
    6. withNetworkMode 指定网络模式(“bridge”)
  2. 使用 dockerClient.createContainerCmd() 方法链式构建创建容器的命令
    1. withName 指定容器名
    2. createContainerCmd 指定镜像
    3. withExposedPorts 暴露端口
    4. withCmd 创建容器时执行的命令
    5. withHostConfig hostConfig 配置,见上
    6. withEnv 设置环境变量
    7. withNetworkDisabled 设置是否禁用网络
    8. withReadonlyRootfs 设置根目录只读
    9. withAttachStdin 将标准输入附加到容器,用户可以向容器发送输入
    10. withAttachStderr 将标准错误输出附加到容器,容器中的错误信息可以实时显示在用户的终端中
    11. withAttachStdout 将标准输出附加到容器,允许容器的输出信息(如日志)实时显示在用户的终端
    12. withTty 创建一个伪终端(TTY),使得容器能够以交互方式运行
  3. 执行创建容器命令,并且获取到容器 id
  4. 根据 id 启动容器 dockerClient.startContainerCmd(containerId).exec();
// 端口映射
ExposedPort tcp1234 = ExposedPort.tcp(6080);
Ports portBindings = new Ports();
portBindings.bind(tcp1234, Ports.Binding.bindPort(port));
        
// 设置挂载
Bind[] binds = {
        Bind.parse("/tmp/test_docker:/home/ubuntu/shared"),
        Bind.parse("x11vnc_zh_CN_config:/home/ubuntu/.config"),
        Bind.parse("x11vnc_project:/home/ubuntu/project"),
        Bind.parse("/home/yuezi/.ssh:/home/ubuntu/.ssh")
};
// 创建容器配置
HostConfig hostConfig = HostConfig.newHostConfig()
    .withPortBindings(portBindings)        // 端口映射
    .withBinds(binds)                   // 挂载
    .withMemory(100 * 1000 * 1000L)      // 指定内存限制为 100M
    .withMemorySwap(0L)                  // 减少内容到硬盘的写入
    .withCpuCount(1L)                    // 指定分配的 CPU 为 1

// 构建创建容器命令
CreateContainerCmd createContainerCmd = dockerClient
        .createContainerCmd("amazoncorretto:17-alpine-jdk")
        .withHostConfig(hostConfig)
        .withName("containerName")     // 指定容器名
        .withNetworkDisabled(true)   // 禁止网络使用
        .withReadonlyRootfs(true)    // 设置根系统文件只读
        .withAttachStdin(true)
        .withAttachStderr(true)
        .withAttachStdout(true)
        .withTty(true);             // 创建可交互的窗口

// 执行创建容器命令
String containerId = createContainerCmd.exec().getId();
// 启动容器
dockerClient.startContainerCmd(containerId).exec();

判断容器状态

  1. 使用 dockerClient.inspectContainerCmd(containerId).exec(); 获取到容器的详细信息
  2. 可以使用 InspectContainerResponse 获取到容器的各种信息。相当于 docker inspect 命令
//判断容器状态
InspectContainerResponse containerResponse = dockerClient.inspectContainerCmd(containerId).exec();
if (Boolean.TRUE.equals(containerResponse.getState().getRunning())) {
    return false;
}

销毁容器

  1. 先判断容器是否在运行再进行销毁
  2. 销毁容器 dockerClient.removeContainerCmd(containerId).exec();

停止容器

  1. 先判断容器状态
  2. 停止容器 dockerClient.stopContainerCmd(containerId).exec();
StopContainerCmd stopContainerCmd = dockerClient.stopContainerCmd(containerId);
stopContainerCmd.exec();

重启容器

  1. 重启容器 dockerClient.restartContainerCmd(containerId).exec();

内存统计

final long[] maxMemory = {0L};

// 获取占用的内存
StatsCmd statsCmd = dockerClient.statsCmd(containerId);
ResultCallback<Statistics> statisticsResultCallback = statsCmd.exec(new ResultCallback<Statistics>() {
    @Override
    public void onStart(Closeable closeable) {

    }

    @Override
    public void onNext(Statistics statistics) {
        System.out.println("内存占用:" + statistics.getMemoryStats().getUsage());
        maxMemory[0] = Math.max(statistics.getMemoryStats().getUsage(), maxMemory[0]);
    }

    @Override
    public void onError(Throwable throwable) {

    }

    @Override
    public void onComplete() {

    }

    @Override
    public void close() throws IOException {

    }
});
statsCmd.exec(statisticsResultCallback);
// 记得关闭
try {
    stopWatch.start();
    dockerClient.execStartCmd(execId)
            .exec(execStartResultCallback)
            .awaitCompletion(TIME_OUT, TimeUnit.MICROSECONDS);
    stopWatch.stop();
    time = stopWatch.getLastTaskTimeMillis();
    statsCmd.close();
} catch (InterruptedException e) {
    System.out.println("程序执行异常");
    throw new RuntimeException(e);
}

时间统计

long time = 0L;

String execId =execCreateCmdResponse.getId();
// 判断是否超时
final boolean[] timeout = {true};
ExecStartResultCallback execStartResultCallback =new ExecStartResultCallback() {
    @Override
    public void onComplete() {
        timeout[0] = false;
        super.onComplete();
    }

    @Override
    public void onNext(
            Frame frame) {
        StreamType streamType = frame.getStreamType();
        if (StreamType.STDERR.equals(streamType)) {
            errorMessage[0] = new String(frame.getPayload());
            System.out.println("输出错误结果:" + errorMessage[0]);
        } else {
            message[0] = new String(frame.getPayload());
            System.out.println("输出结果:" + message[0]);
        }
        super.onNext(frame);
    }
};
// 记得关闭
try {
    stopWatch.start();
    dockerClient.execStartCmd(execId)
            .exec(execStartResultCallback)
            .awaitCompletion(TIME_OUT, TimeUnit.MICROSECONDS);
    stopWatch.stop();
    time = stopWatch.getLastTaskTimeMillis();
    statsCmd.close();
} catch (InterruptedException e) {
    System.out.println("程序执行异常");
    throw new RuntimeException(e);
}
  • Loading...
  • Loading...
  • Loading...
  • Loading...
  • Loading...