OJ 系统后端知识点梳理
ps:对 OJ 系统后端的进行梳理,梳理的知识点仅仅是针对作者本人有用的,以及添加了部分个人理解。如果有理解错误或有其他更好的理解,麻烦给予指正,实在是感激不尽!!!若有其他侵权行为。请及时告知,会作以调整。
Controller 层
限制爬虫
- 通过对分页查询接口,判断一页数据请求的大小(例如 size > 20),来判断是否被爬虫
model 层
- dto 主要是对前端请求进行封装,在 Controller 层中使用 @RequestBody 来接收请求参数
- 而 vo 是对 entity 或者 dto 进行再一次的封装,常用来返回给前端需要的字段(脱敏等等)
VO 层
- 新增两个方法:objToVo 和 VoToObj (有助于转化)
service 层
新增方法 getQueryWrapper()
- 返回值:QueryWrapper
- 参数:DTO
作用:根据 DTO 拼接查询条件,返回 QueryWrapper
AOP + 自定义注解
本项目中 AOP 的作用:
- 权限校验(自定义注解)
- 请求响应日志记录
权限校验
- 自定义注解 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)
- 表示只有管理员权限才能访问
- 自定义切面,围绕上述注解 @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 这种基于哈希表的集合
使用示例
- 在实体类中,自定义 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
}
- 使用实例:
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;
}
}
查询的具体使用方法
- xxxxService.page() 方法
注意:xxxService 必须是继承了 IService
的接口
使用方式:
questionService.page(new Page<>(current, size), new QueryWrapper<Question>())
- 第一个参数是 Page 对象
new Page<>(current, pageSize, totalCount)
- 第二个参数是由 QueryWrapper 拼接的查询条件
- xxxxMapper.selectPage() 方法
注意:xxxxMapper 必须是继承了 BaseMapper
的接口
- 使用方式和上述一致
- 手动分页(很少使用)
这种情况目前使用在:需要手动根据某一字段进行自定义排序
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 进行结构。
- 自定义 PageRequest 类
/**
* 分页请求
*
*/
@Data
public class PageRequest {
/**
* 当前页号
*/
private int current = 1;
/**
* 页面大小
*/
private int pageSize = 10;
/**
* 排序字段
*/
private String sortField;
/**
* 排序顺序(默认升序)
*/
private String sortOrder = CommonConstant.SORT_ORDER_ASC;
}
- 自定义 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);
模板方法
模板方法:定义一套通用的执行流程,让子类负责每个执行步骤的具体实现
模板方法的适用场景:适用于有规范的流程,且执行流程可以复用
作用:大幅节省重复代码量,便于项目扩展、更好维护
实现步骤:
- 创建一个抽象模板类
xxTemplate.java
- 把原来完整的一个方法抽离为一个个子方法
- 实际使用的时候,直接继承这个模板类
- 如果需要对某一方法进行覆盖,直接使用 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();
两种实现方法
- 使用 Runtime 执行
String cmd = String.format("javac -encoding utf-8 %s", userCodeFile.getAbsolutePath());
Runtime runtime = Runtime.getRuntime();
Process p = runtime.exec(cmd);
- 使用 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
- 创建容器
- 启动容器
- 停止容器
- 删除容器
- 拉取镜像
- 删除镜像
- … …
远程连接
首先,保证远程防火墙放开端口:
26665
- 在
resources
目录下,新建文件docker-java.properties
(和 yml 配置文件是同级) - 添加一行内容:
DOCKER_HOST=tcp://xxx.xx.xx.xxx:26665
即可连接到远程 docker
操作 Docker
创建容器(启动)
- 可以使用
HonstConfig
指定容器的配置(内存,CPU,挂载,端口映射等等)withPortBindings
端口映射withBinds
挂载withMemory
指定内存限制withMemorySwap
减少内容到硬盘的写入withCpuCount
指定分配的 CPUwithNetworkMode
指定网络模式(“bridge”)
- 使用
dockerClient.createContainerCmd()
方法链式构建创建容器的命令withName
指定容器名createContainerCmd
指定镜像withExposedPorts
暴露端口withCmd
创建容器时执行的命令withHostConfig
hostConfig 配置,见上withEnv
设置环境变量withNetworkDisabled
设置是否禁用网络withReadonlyRootfs
设置根目录只读withAttachStdin
将标准输入附加到容器,用户可以向容器发送输入withAttachStderr
将标准错误输出附加到容器,容器中的错误信息可以实时显示在用户的终端中withAttachStdout
将标准输出附加到容器,允许容器的输出信息(如日志)实时显示在用户的终端withTty
创建一个伪终端(TTY),使得容器能够以交互方式运行
- 执行创建容器命令,并且获取到容器 id
- 根据 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();
判断容器状态
- 使用
dockerClient.inspectContainerCmd(containerId).exec();
获取到容器的详细信息 - 可以使用
InspectContainerResponse
获取到容器的各种信息。相当于docker inspect
命令
//判断容器状态
InspectContainerResponse containerResponse = dockerClient.inspectContainerCmd(containerId).exec();
if (Boolean.TRUE.equals(containerResponse.getState().getRunning())) {
return false;
}
销毁容器
- 先判断容器是否在运行再进行销毁
- 销毁容器
dockerClient.removeContainerCmd(containerId).exec();
停止容器
- 先判断容器状态
- 停止容器
dockerClient.stopContainerCmd(containerId).exec();
StopContainerCmd stopContainerCmd = dockerClient.stopContainerCmd(containerId);
stopContainerCmd.exec();
重启容器
- 重启容器
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);
}