| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.system.service.database; |
| | | |
| | | import cn.hutool.core.date.DateUtil; |
| | | import cn.hutool.core.io.FileUtil; |
| | | import cn.hutool.core.io.IoUtil; |
| | | import cn.hutool.core.util.HexUtil; |
| | | import cn.hutool.core.util.StrUtil; |
| | | import cn.iocoder.yudao.framework.common.exception.ServiceException; |
| | | import jakarta.annotation.Resource; |
| | | import jakarta.servlet.http.HttpServletResponse; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.beans.factory.annotation.Value; |
| | | import org.springframework.stereotype.Service; |
| | | |
| | | import javax.sql.DataSource; |
| | | import java.io.BufferedReader; |
| | | import java.io.BufferedWriter; |
| | | import java.io.File; |
| | | import java.io.FileInputStream; |
| | | import java.io.FileOutputStream; |
| | | import java.io.IOException; |
| | | import java.io.InputStream; |
| | | import java.io.InputStreamReader; |
| | | import java.io.OutputStream; |
| | | import java.io.OutputStreamWriter; |
| | | import java.net.URLEncoder; |
| | | import java.nio.charset.StandardCharsets; |
| | | import java.sql.Connection; |
| | | import java.sql.ResultSet; |
| | | import java.sql.ResultSetMetaData; |
| | | import java.sql.Statement; |
| | | import java.sql.Types; |
| | | import java.util.ArrayList; |
| | | import java.util.Date; |
| | | import java.util.List; |
| | | import java.util.concurrent.TimeUnit; |
| | | import java.util.regex.Matcher; |
| | | import java.util.regex.Pattern; |
| | | |
| | | @Service |
| | | @Slf4j |
| | | public class DatabaseBackupServiceImpl implements DatabaseBackupService { |
| | | |
| | | /** å¤ä»½è¶
æ¶æ¶é´ï¼åéï¼ */ |
| | | private static final long BACKUP_TIMEOUT_MINUTES = 10; |
| | | /** MySQL 容å¨å
é¨ç«¯å£ */ |
| | | private static final String DOCKER_MYSQL_PORT = "3306"; |
| | | |
| | | @Value("${spring.datasource.dynamic.datasource.master.url:}") |
| | | private String dbUrl; |
| | | |
| | | @Value("${spring.datasource.dynamic.datasource.master.username:root}") |
| | | private String dbUsername; |
| | | |
| | | @Value("${spring.datasource.dynamic.datasource.master.password:}") |
| | | private String dbPassword; |
| | | |
| | | /** å¤ä»½å½ä»¤ï¼å¯æ¾å¼æå®ï¼å¦ docker exec mysql8 mysqldumpï¼ï¼ç空åèªå¨æ£æµæ¬å° mysqldump æ Docker MySQL å®¹å¨ */ |
| | | @Value("${yudao.database-backup.command:}") |
| | | private String backupCommand; |
| | | |
| | | /** å¤ä»½ç®æ æ°æ®åº hostï¼ç空å仿°æ®æº JDBC URL è§£æ */ |
| | | @Value("${yudao.database-backup.host:}") |
| | | private String backupHost; |
| | | |
| | | /** å¤ä»½ç®æ æ°æ®åº portï¼ç空å仿°æ®æº JDBC URL è§£æ */ |
| | | @Value("${yudao.database-backup.port:}") |
| | | private String backupPort; |
| | | |
| | | /** å¤ä»½ç®æ æ°æ®åºåï¼ç空å仿°æ®æº JDBC URL è§£æ */ |
| | | @Value("${yudao.database-backup.database:}") |
| | | private String backupDatabase; |
| | | |
| | | /** å¤ä»½è´¦å·ï¼ç空åä½¿ç¨æ°æ®æºè´¦å· */ |
| | | @Value("${yudao.database-backup.username:}") |
| | | private String backupUsername; |
| | | |
| | | /** å¤ä»½å¯ç ï¼ç空åä½¿ç¨æ°æ®æºå¯ç */ |
| | | @Value("${yudao.database-backup.password:}") |
| | | private String backupPassword; |
| | | |
| | | /** ä¸»æ°æ®æºï¼ç¨äº JDBC 导åºå
åº */ |
| | | @Resource |
| | | private DataSource dataSource; |
| | | |
| | | @Override |
| | | public void backupDatabase(HttpServletResponse response) throws Exception { |
| | | if (StrUtil.isEmpty(dbUrl)) { |
| | | throw new ServiceException(500, "æªæ¾å°æ°æ®åºè¿æ¥é
ç½®"); |
| | | } |
| | | |
| | | // å¤ä»½è¿æ¥çæ°æ®åºä¿¡æ¯ï¼ä¼å
ä½¿ç¨ yudao.database-backup é
ç½®ï¼æªé
ç½®çé¨å仿°æ®æº JDBC URL è§£æå
åº |
| | | String dbHost = backupHost; |
| | | String dbPort = backupPort; |
| | | String dbName = backupDatabase; |
| | | if (StrUtil.isEmpty(dbHost) || StrUtil.isEmpty(dbPort) || StrUtil.isEmpty(dbName)) { |
| | | Matcher matcher = Pattern.compile("jdbc:mysql://([^:]+):(\\d+)/([^?]+)").matcher(dbUrl); |
| | | if (matcher.find()) { |
| | | if (StrUtil.isEmpty(dbHost)) { |
| | | dbHost = matcher.group(1); |
| | | } |
| | | if (StrUtil.isEmpty(dbPort)) { |
| | | dbPort = matcher.group(2); |
| | | } |
| | | if (StrUtil.isEmpty(dbName)) { |
| | | dbName = matcher.group(3); |
| | | } |
| | | } |
| | | } |
| | | if (StrUtil.isEmpty(dbHost) || StrUtil.isEmpty(dbPort) || StrUtil.isEmpty(dbName)) { |
| | | log.warn("æ æ³ä»é
ç½®æ JDBC URL è§£æå®æ´çæ°æ®åºä¿¡æ¯: host={}, port={}, db={}", dbHost, dbPort, dbName); |
| | | throw new ServiceException(500, "æ æ³è§£ææ°æ®åºå¤ä»½è¿æ¥ä¿¡æ¯ï¼è¯·å¨ yudao.database-backup ä¸é
ç½® host/port/database"); |
| | | } |
| | | // localhost ç»ä¸èµ° TCP 127.0.0.1ï¼mysqldump 对 localhost å¯è½èµ° socket/loopbackï¼ |
| | | // èä¸åç¯å¢ root@localhost ç认è¯è¦æ±å·®å¼å¤§ï¼å¼ºå¶ TCP æ´ç¨³å® |
| | | if ("localhost".equalsIgnoreCase(dbHost)) { |
| | | dbHost = "127.0.0.1"; |
| | | } |
| | | String username = StrUtil.isEmpty(backupUsername) ? dbUsername : backupUsername; |
| | | String password = StrUtil.isEmpty(backupPassword) ? dbPassword : backupPassword; |
| | | |
| | | // ç»è£
åéå¤ä»½å½ä»¤ï¼æä¼å
çº§ä¾æ¬¡å°è¯ï¼æå¨é
ç½® > æ¬æº mysqldump > Docker å®¹å¨ mysqldump |
| | | List<List<String>> candidates = new ArrayList<>(); |
| | | List<String> labels = new ArrayList<>(); |
| | | if (StrUtil.isNotEmpty(backupCommand)) { |
| | | candidates.add(splitCommand(backupCommand)); |
| | | labels.add("æå¨é
ç½®å½ä»¤[" + backupCommand + "]"); |
| | | } |
| | | if (isLocalMysqldumpAvailable()) { |
| | | candidates.add(buildLocalMysqldumpCommand(dbHost, dbPort, username, password, dbName)); |
| | | labels.add("æ¬æº mysqldump"); |
| | | } |
| | | String container = findMysqlDockerContainer(dbPort); |
| | | if (StrUtil.isNotEmpty(container)) { |
| | | candidates.add(buildDockerExecCommand(container, username, password, dbName)); |
| | | labels.add("Docker 容å¨[" + container + "] mysqldump"); |
| | | } |
| | | log.info("æ°æ®åºå¤ä»½åéæ¹å¼ï¼{}", labels); |
| | | |
| | | // 便¬¡å°è¯ææå¤é¨å½ä»¤æ¹å¼ |
| | | List<String> failures = new ArrayList<>(); |
| | | String timestamp = DateUtil.format(new Date(), "yyyyMMdd_HHmmss"); |
| | | String fileName = "backup_" + timestamp + ".sql"; |
| | | for (int i = 0; i < candidates.size(); i++) { |
| | | File tempFile = File.createTempFile("backup_", ".sql"); |
| | | try { |
| | | String error = runMysqldump(candidates.get(i), password, tempFile); |
| | | if (error == null) { |
| | | log.info("æ°æ®åºå¤ä»½æåï¼ä½¿ç¨æ¹å¼ï¼{}", labels.get(i)); |
| | | downloadAttachment(response, tempFile, fileName); |
| | | return; |
| | | } |
| | | log.warn("å¤ä»½æ¹å¼ [{}] 失败ï¼{}", labels.get(i), error); |
| | | failures.add(labels.get(i) + "ï¼" + error); |
| | | } finally { |
| | | FileUtil.del(tempFile); |
| | | } |
| | | } |
| | | |
| | | // å¤é¨å½ä»¤å
¨é¨å¤±è´¥æä¸å¯ç¨æ¶ï¼ä½¿ç¨ JDBC 导åºå
åºï¼ä¿è¯ä»»ä½é¨ç½²ç¯å¢ï¼å¦åºç¨å®¹å¨å
æ mysqldump/dockerï¼é½è½å¤ä»½ |
| | | File tempFile = File.createTempFile("backup_", ".sql"); |
| | | try { |
| | | String error = exportByJdbc(tempFile); |
| | | if (error == null) { |
| | | log.info("æ°æ®åºå¤ä»½æåï¼ä½¿ç¨æ¹å¼ï¼JDBC 导åºå
åº"); |
| | | downloadAttachment(response, tempFile, fileName); |
| | | return; |
| | | } |
| | | log.warn("JDBC 导åºå
åºå¤±è´¥ï¼{}", error); |
| | | failures.add("JDBC 导åºå
åºï¼" + error); |
| | | } finally { |
| | | FileUtil.del(tempFile); |
| | | } |
| | | throw new ServiceException(500, "æ°æ®åºå¤ä»½å¤±è´¥ï¼" + String.join(" ï½ ", failures)); |
| | | } |
| | | |
| | | /** |
| | | * éè¿ JDBC ç´è¿æ°æ®åºå¯¼åºå»ºè¡¨è¯å¥ä¸å
¨éæ°æ®ï¼ä½ä¸ºå¤é¨å½ä»¤ä¸å¯ç¨æ¶çå
åºå¤ä»½æ¹å¼ |
| | | * |
| | | * @return æ§è¡æåè¿å nullï¼å¤±è´¥è¿åé误æè¿° |
| | | */ |
| | | private String exportByJdbc(File outputFile) { |
| | | try (Connection conn = dataSource.getConnection(); |
| | | BufferedWriter writer = new BufferedWriter( |
| | | new OutputStreamWriter(new FileOutputStream(outputFile), StandardCharsets.UTF_8))) { |
| | | writer.write("/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;\n"); |
| | | writer.write("/*!40101 SET NAMES utf8mb4 */;\n"); |
| | | writer.write("SET TIME_ZONE='+00:00';\n"); |
| | | writer.write("SET FOREIGN_KEY_CHECKS=0;\n"); |
| | | writer.write("SET UNIQUE_CHECKS=0;\n"); |
| | | writer.write("SET SQL_MODE='NO_AUTO_VALUE_ON_ZERO';\n\n"); |
| | | try (Statement st = conn.createStatement(); |
| | | ResultSet rs = st.executeQuery("SHOW TABLES")) { |
| | | List<String> tables = new ArrayList<>(); |
| | | while (rs.next()) { |
| | | tables.add(rs.getString(1)); |
| | | } |
| | | for (String table : tables) { |
| | | exportTable(conn, writer, table); |
| | | } |
| | | } |
| | | writer.write("\nSET FOREIGN_KEY_CHECKS=1;\n"); |
| | | writer.write("SET UNIQUE_CHECKS=1;\n"); |
| | | writer.write("/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;\n"); |
| | | return null; |
| | | } catch (Exception e) { |
| | | log.error("JDBC 导åºå
åºå¤±è´¥", e); |
| | | return "JDBC 导åºå¼å¸¸: " + e.getMessage(); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 导åºåå¼ è¡¨ç建表è¯å¥ä¸å
¨é¨æ°æ® |
| | | */ |
| | | private void exportTable(Connection conn, BufferedWriter writer, String table) throws Exception { |
| | | String quoted = "`" + table.replace("`", "``") + "`"; |
| | | // 建表è¯å¥ |
| | | try (Statement st = conn.createStatement(); |
| | | ResultSet rs = st.executeQuery("SHOW CREATE TABLE " + quoted)) { |
| | | if (rs.next()) { |
| | | writer.write(rs.getString(2)); |
| | | writer.write(";\n\n"); |
| | | } |
| | | } |
| | | // å
¨éæ°æ®ï¼æµå¼è¯»åé¿å
大表å ç¨è¿å¤å
å |
| | | try (Statement st = conn.createStatement()) { |
| | | st.setFetchSize(Integer.MIN_VALUE); |
| | | try (ResultSet rs = st.executeQuery("SELECT * FROM " + quoted)) { |
| | | ResultSetMetaData meta = rs.getMetaData(); |
| | | int columnCount = meta.getColumnCount(); |
| | | int[] types = new int[columnCount]; |
| | | for (int i = 0; i < columnCount; i++) { |
| | | types[i] = meta.getColumnType(i + 1); |
| | | } |
| | | while (rs.next()) { |
| | | StringBuilder sb = new StringBuilder("INSERT INTO ").append(quoted).append(" VALUES ("); |
| | | for (int i = 0; i < columnCount; i++) { |
| | | if (i > 0) { |
| | | sb.append(','); |
| | | } |
| | | if (rs.getObject(i + 1) == null) { |
| | | sb.append("NULL"); |
| | | continue; |
| | | } |
| | | int type = types[i]; |
| | | if (type == Types.BIT) { |
| | | sb.append(rs.getBoolean(i + 1) ? '1' : '0'); |
| | | } else if (isBinaryType(type)) { |
| | | sb.append("X'").append(HexUtil.encodeHexStr(rs.getBytes(i + 1))).append("'"); |
| | | } else { |
| | | sb.append('\'').append(escapeSql(rs.getString(i + 1))).append('\''); |
| | | } |
| | | } |
| | | sb.append(");\n"); |
| | | writer.write(sb.toString()); |
| | | } |
| | | } |
| | | } |
| | | writer.write('\n'); |
| | | } |
| | | |
| | | /** |
| | | * æ¯å¦äºè¿å¶åç±»åï¼äºè¿å¶æ°æ®ä»¥åå
è¿å¶åé¢é导åºé¿å
ä¹±ç |
| | | */ |
| | | private boolean isBinaryType(int type) { |
| | | return type == Types.BINARY || type == Types.VARBINARY |
| | | || type == Types.LONGVARBINARY || type == Types.BLOB; |
| | | } |
| | | |
| | | /** |
| | | * SQL å符串åé¢éè½¬ä¹ |
| | | */ |
| | | private String escapeSql(String value) { |
| | | return value.replace("\\", "\\\\").replace("'", "''").replace("\0", "\\0"); |
| | | } |
| | | |
| | | /** |
| | | * æå»ºæ¬æº mysqldump å¤ä»½å½ä»¤ |
| | | */ |
| | | private List<String> buildLocalMysqldumpCommand(String host, String port, String username, String password, String dbName) { |
| | | List<String> command = new ArrayList<>(); |
| | | command.add("mysqldump"); |
| | | // MySQL 8 caching_sha2_password 认è¯é»è®¤ä¸è¯·æ±æå¡ç«¯å
¬é¥ï¼ä¼æ¥ Access denied (using password: YES) |
| | | command.add("--get-server-public-key"); |
| | | command.add("-h" + host); |
| | | command.add("-P" + port); |
| | | command.add("-u" + username); |
| | | if (StrUtil.isEmpty(password)) { |
| | | command.add("--skip-password"); |
| | | } |
| | | command.add(dbName); |
| | | return command; |
| | | } |
| | | |
| | | /** |
| | | * æå»ºéè¿ docker exec è°ç¨å®¹å¨å
mysqldump çå¤ä»½å½ä»¤ |
| | | */ |
| | | private List<String> buildDockerExecCommand(String container, String username, String password, String dbName) { |
| | | List<String> command = new ArrayList<>(); |
| | | command.add("docker"); |
| | | command.add("exec"); |
| | | // docker exec ä¸ä¼è½¬å宿主æºç¯å¢åéï¼å¯ç ééè¿ -e æ¾å¼ä¼ å
¥å®¹å¨ |
| | | if (StrUtil.isNotEmpty(password)) { |
| | | command.add("-e"); |
| | | command.add("MYSQL_PWD=" + password); |
| | | } |
| | | command.add(container); |
| | | command.add("mysqldump"); |
| | | command.add("--get-server-public-key"); |
| | | // 容å¨å
åºå®èµ° 127.0.0.1:3306 ç TCP è¿æ¥èªèº«ï¼ç»å¼ socket/loopback 认è¯å·®å¼ |
| | | command.add("-h127.0.0.1"); |
| | | command.add("-P" + DOCKER_MYSQL_PORT); |
| | | command.add("-u" + username); |
| | | if (StrUtil.isEmpty(password)) { |
| | | command.add("--skip-password"); |
| | | } |
| | | command.add(dbName); |
| | | return command; |
| | | } |
| | | |
| | | /** |
| | | * æ§è¡ mysqldump å½ä»¤ï¼å° stdout åå
¥ä¸´æ¶æä»¶ |
| | | * |
| | | * @return æ§è¡æåè¿å nullï¼å¤±è´¥è¿åé误æè¿° |
| | | */ |
| | | private String runMysqldump(List<String> command, String password, File outputFile) { |
| | | try { |
| | | ProcessBuilder pb = new ProcessBuilder(command); |
| | | // å¯ç éè¿ç¯å¢åéä¼ éï¼é¿å
åºç°å¨å½ä»¤è¡åè¿ç¨å表 |
| | | if (StrUtil.isNotEmpty(password)) { |
| | | pb.environment().put("MYSQL_PWD", password); |
| | | } |
| | | Process process = pb.start(); |
| | | |
| | | // åå°è¯»å stderrï¼é²æ¢ç®¡éç¼å²åºå满导è´è¿ç¨é»å¡ |
| | | StringBuilder stderr = new StringBuilder(); |
| | | Thread stderrThread = new Thread(() -> { |
| | | try (BufferedReader reader = new BufferedReader( |
| | | new InputStreamReader(process.getErrorStream(), StandardCharsets.UTF_8))) { |
| | | String line; |
| | | while ((line = reader.readLine()) != null) { |
| | | stderr.append(line).append(System.lineSeparator()); |
| | | } |
| | | } catch (IOException ignored) { |
| | | } |
| | | }); |
| | | stderrThread.start(); |
| | | |
| | | // mysqldump è¾åºåå
¥æ¬å°ä¸´æ¶æä»¶ï¼docker exec åºæ¯è¾åºä¸ä¼è½å¨å®¹å¨å
æä»¶ç³»ç»ï¼ |
| | | try (InputStream in = process.getInputStream(); |
| | | OutputStream out = new FileOutputStream(outputFile)) { |
| | | IoUtil.copy(in, out); |
| | | } |
| | | |
| | | boolean finished = process.waitFor(BACKUP_TIMEOUT_MINUTES, TimeUnit.MINUTES); |
| | | stderrThread.join(); |
| | | if (!finished) { |
| | | process.destroyForcibly(); |
| | | return "æ§è¡è¶
æ¶ï¼è¶
è¿ " + BACKUP_TIMEOUT_MINUTES + " åéï¼"; |
| | | } |
| | | if (process.exitValue() != 0) { |
| | | return "exitCode=" + process.exitValue() + "ï¼" + stderr.toString().trim(); |
| | | } |
| | | return null; |
| | | } catch (IOException e) { |
| | | return "å¯å¨å½ä»¤å¤±è´¥: " + e.getMessage(); |
| | | } catch (InterruptedException e) { |
| | | Thread.currentThread().interrupt(); |
| | | return "æ§è¡è¢«ä¸æ: " + e.getMessage(); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * å°å¤ä»½æä»¶ä½ä¸ºéä»¶è¾åºå°ååº |
| | | */ |
| | | private void downloadAttachment(HttpServletResponse response, File file, String fileName) throws IOException { |
| | | response.setContentType("application/octet-stream; charset=UTF-8"); |
| | | response.setHeader("Content-Disposition", "attachment; filename=\"" + URLEncoder.encode(fileName, "UTF-8") + "\""); |
| | | response.setContentLengthLong(file.length()); |
| | | try (FileInputStream fis = new FileInputStream(file); |
| | | OutputStream os = response.getOutputStream()) { |
| | | IoUtil.copy(fis, os); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * å°é
ç½®çå½ä»¤å符串æç©ºç½ååä¸ºåæ°å表 |
| | | */ |
| | | private List<String> splitCommand(String command) { |
| | | List<String> parts = new ArrayList<>(); |
| | | for (String token : command.trim().split("\\s+")) { |
| | | parts.add(token); |
| | | } |
| | | return parts; |
| | | } |
| | | |
| | | /** |
| | | * æ£æµæ¬æº mysqldump æ¯å¦å¯ç¨ |
| | | */ |
| | | private boolean isLocalMysqldumpAvailable() { |
| | | try { |
| | | Process process = new ProcessBuilder("mysqldump", "--version").redirectErrorStream(true).start(); |
| | | boolean finished = process.waitFor(5, TimeUnit.SECONDS); |
| | | if (!finished) { |
| | | process.destroyForcibly(); |
| | | return false; |
| | | } |
| | | return process.exitValue() == 0; |
| | | } catch (IOException e) { |
| | | return false; |
| | | } catch (InterruptedException e) { |
| | | Thread.currentThread().interrupt(); |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * æ¥æ¾è¿è¡ä¸ç MySQL Docker 容å¨ï¼ä¼å
éæ©ç«¯å£æ å°ä¸æ°æ®æºç«¯å£ä¸è´çå®¹å¨ |
| | | * |
| | | * @return 容å¨åï¼æªæ¾å°è¿å null |
| | | */ |
| | | private String findMysqlDockerContainer(String dbPort) { |
| | | try { |
| | | Process ps = new ProcessBuilder("docker", "ps", "--format", "{{.ID}}|{{.Names}}|{{.Image}}") |
| | | .redirectErrorStream(true).start(); |
| | | String output = IoUtil.read(ps.getInputStream(), StandardCharsets.UTF_8); |
| | | if (!ps.waitFor(10, TimeUnit.SECONDS) || ps.exitValue() != 0) { |
| | | return null; |
| | | } |
| | | List<String[]> containers = new ArrayList<>(); |
| | | for (String line : output.split("\n")) { |
| | | String[] parts = line.trim().split("\\|"); |
| | | if (parts.length == 3 && parts[2].startsWith("mysql")) { |
| | | containers.add(parts); |
| | | } |
| | | } |
| | | if (containers.isEmpty()) { |
| | | return null; |
| | | } |
| | | // ä¼å
éæ©ç«¯å£æ å°ä¸æ°æ®æºç«¯å£ä¸è´çå®¹å¨ |
| | | for (String[] container : containers) { |
| | | Process portProcess = new ProcessBuilder("docker", "port", container[0], "3306/tcp") |
| | | .redirectErrorStream(true).start(); |
| | | String portOutput = IoUtil.read(portProcess.getInputStream(), StandardCharsets.UTF_8); |
| | | if (portProcess.waitFor(10, TimeUnit.SECONDS) && portProcess.exitValue() == 0 |
| | | && portOutput.contains(dbPort)) { |
| | | return container[1]; |
| | | } |
| | | } |
| | | log.warn("æªå¹é
å°ç«¯å£æ å°ç MySQL 容å¨ï¼ä½¿ç¨ç¬¬ä¸ä¸ª: {}", containers.get(0)[1]); |
| | | return containers.get(0)[1]; |
| | | } catch (IOException e) { |
| | | log.warn("æ£æµ Docker ç¯å¢å¤±è´¥: {}", e.getMessage()); |
| | | return null; |
| | | } catch (InterruptedException e) { |
| | | Thread.currentThread().interrupt(); |
| | | return null; |
| | | } |
| | | } |
| | | } |