springboot調(diào)用python文件的詳細方案
介紹
后臺是用springboot技術(shù),其他同事做的算法是python,現(xiàn)在的需求是springboot調(diào)用python,python又需要調(diào)用其他的數(shù)據(jù)文件,比如dat文件,這個文件是app通過藍牙獲取智能戒指數(shù)據(jù)以后,保存到后臺,后臺需要將數(shù)據(jù)寫入到dat文件里,供python調(diào)用,本身難點并不大,主要本地環(huán)境死windows,服務器環(huán)境是ubuntu,并且是通過docker部署的,這樣文件的路徑就會產(chǎn)生問題,需要適配
實現(xiàn)思路
先處理一下文件路徑問題,路徑如下
如何能保證不同環(huán)境都能正確讀到文件呢
答案是:使用臨時文件復制
在 Spring Boot 調(diào)用 Python 腳本的場景中使用臨時文件的方式來實現(xiàn)跨平臺和 Docker 兼容,主要基于以下幾個關(guān)鍵原因:
1. 解決路徑一致性問題
問題:不同操作系統(tǒng)路徑格式不同(Windows: C:\path,Linux: /path),且 Docker 容器內(nèi)路徑與宿主機不同
方案:臨時目錄提供統(tǒng)一的絕對路徑基準
Path tempDir = Files.createTempDirectory("prefix"); // 自動適應各平臺路徑格式
2. 解決文件權(quán)限問題
問題:直接操作項目資源文件可能因權(quán)限不足失敗(尤其是 Docker 默認以非 root 用戶運行)
方案:臨時目錄確??蓪憴?quán)限
// 顯式設置權(quán)限(Linux/Unix需要) targetPath.toFile().setExecutable(true);
3. 解決資源打包問題
問題:Spring Boot 打包后,resources 下的文件存在于 JAR 內(nèi),無法直接通過文件系統(tǒng)訪問
方案:運行時復制到臨時目錄解壓
ClassPathResource resource = new ClassPathResource("python/script.py"); Files.copy(resource.getInputStream(), tempPath); // 從JAR內(nèi)解壓到文件系統(tǒng)
4. 解決 Docker 環(huán)境隔離
問題:Docker 容器有獨立文件系統(tǒng),無法直接訪問宿主機的項目資源
方案:構(gòu)建鏡像時復制資源,運行時使用臨時目錄
COPY src/main/resources/python /app/python # 構(gòu)建時固化路徑
5. 多線程安全寫入
問題:多線程并發(fā)寫入同一文件會導致沖突
方案:每個線程使用獨立臨時文件
Path threadSpecificFile = tempDir.resolve("thread_" + Thread.currentThread().getId() + ".dat");
6. 資源清理保障
問題:運行后殘留文件可能積累
方案:標準化清理流程
finally { deleteDirectory(tempDir.toFile()); // 確保刪除臨時文件 }
7. 調(diào)試與日志追蹤
問題:直接操作原始文件難以追蹤運行時狀態(tài)
方案:臨時文件提供獨立運行環(huán)境
System.out.println("臨時目錄: " + tempDir); // 明確顯示運行時文件位置
代碼部分的思路大致是,先將文件復制到臨時路徑,然后往臨時路徑的文件里寫內(nèi)容,這個臨時路徑是可變的,所以不能寫死,需要將路徑當入?yún)鹘opython文件
完整代碼
@Service public class PythonService { public String executePythonScript(String waveData) { Path tempDir = null; try { System.out.println("1:"+ DateUtils.dateTimeNow()); // 1. 創(chuàng)建臨時目錄(使用NIO API確??缙脚_兼容) tempDir = Files.createTempDirectory("python_workspace"); System.out.println("2:"+ DateUtils.dateTimeNow()); // 2. 復制資源 copyPythonResourcesToTemp(tempDir); System.out.println("3:"+ DateUtils.dateTimeNow()); //寫入數(shù)據(jù) Path tempFile = tempDir.resolve("python/raw_data/bp_106_63.dat"); try (BufferedWriter writer = new BufferedWriter(new FileWriter(tempFile.toFile()))) { writer.write("");//先清空數(shù)據(jù) writer.write(waveData); } // 3. 構(gòu)建命令(使用絕對路徑) String pythonScriptPath = tempDir.resolve("python/realTime_predict.py").toString(); String[] command = { "python3", pythonScriptPath }; String pythonPath = "C:\\xxxx\\Python\\Python311\\python.exe"; // 替換為你的實際路徑 boolean isWindows = System.getProperty("os.name").toLowerCase().contains("win"); if (isWindows) { command = new String[]{pythonPath, pythonScriptPath,tempFile.toString()}; } else { command = new String[]{"python3",pythonScriptPath,tempFile.toString()}; } System.out.println("4:"+ DateUtils.dateTimeNow()); // 4. 執(zhí)行命令 ProcessBuilder pb = new ProcessBuilder(command); pb.directory(tempDir.toFile()); pb.redirectErrorStream(true); System.out.println("5:"+ DateUtils.dateTimeNow()); Process process = pb.start(); String output = new BufferedReader( new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8)) .lines().collect(Collectors.joining("\n")); System.out.println("6:"+ DateUtils.dateTimeNow()); int exitCode = process.waitFor(); System.out.println("output:"+ output); if (exitCode != 0) { return "計算出錯"; } System.out.println("7:"+ DateUtils.dateTimeNow()); System.out.println("output:"+ output); String[] split = output.split("\n"); return split[split.length-1]; } catch (Exception e) { e.printStackTrace(); //throw new RuntimeException("執(zhí)行Python腳本出錯: " + e.getMessage(), e); } finally { // 生產(chǎn)環(huán)境建議保留日志,開發(fā)時可清理 if (tempDir != null) { deleteDirectory(tempDir.toFile()); } } return ""; } private void copyPythonResourcesToTemp(Path tempDir) throws IOException { // 創(chuàng)建python子目錄 Files.createDirectories(tempDir.resolve("python")); // 創(chuàng)建必要的子目錄結(jié)構(gòu) Files.createDirectories(tempDir.resolve("python/raw_data")); // 使用Spring的ResourceUtils復制所有文件 copyResourceToTemp("python/realTime_predict.py", tempDir.resolve("python/realTime_predict.py")); copyResourceToTemp("python/best_model.pth", tempDir.resolve("python/best_model.pth")); copyResourceToTemp("python/model.py", tempDir.resolve("python/model.py")); copyResourceToTemp("python/readData.py", tempDir.resolve("python/readData.py")); // 確保先創(chuàng)建raw_data目錄再復制數(shù)據(jù)文件 copyResourceToTemp("python/raw_data/bp_106_63.dat", tempDir.resolve("python/raw_data/bp_106_63.dat")); // 復制requirements.txt(如果存在) copyResourceIfExists("python/requirements.txt", tempDir.resolve("python/requirements.txt")); // 可選:復制requirements.txt ClassPathResource requirements = new ClassPathResource("python/requirements.txt"); if (requirements.exists()) { Files.copy(requirements.getInputStream(), tempDir.resolve("python/requirements.txt"), StandardCopyOption.REPLACE_EXISTING); } } // 新增方法:安全復制資源(僅當資源存在時) private void copyResourceIfExists(String resourcePath, Path targetPath) throws IOException { ClassPathResource resource = new ClassPathResource(resourcePath); if (resource.exists()) { try (InputStream in = resource.getInputStream()) { Files.copy(in, targetPath, StandardCopyOption.REPLACE_EXISTING); } } } // 專用資源復制方法(處理JAR內(nèi)資源) private void copyResourceToTemp(String resourcePath, Path targetPath) throws IOException { ClassPathResource resource = new ClassPathResource(resourcePath); try (InputStream in = resource.getInputStream()) { Files.copy(in, targetPath, StandardCopyOption.REPLACE_EXISTING); } // 設置可執(zhí)行權(quán)限(Linux/Unix需要) targetPath.toFile().setExecutable(true); } // 輔助方法:遞歸刪除目錄 private void deleteDirectory(File directory) { if (directory.exists()) { File[] files = directory.listFiles(); if (files != null) { for (File file : files) { if (file.isDirectory()) { deleteDirectory(file); } else { file.delete(); } } } directory.delete(); } } }
python部分
if __name__ == "__main__": # 測試預測 if len(sys.argv) < 2: print("請?zhí)峁?shù)據(jù)文件路徑作為參數(shù)") sys.exit(1) input_file = sys.argv[1] # 獲取Java傳遞的文件路徑參數(shù) try: sbp, dbp = predict_bp(input_file) print(f"{sbp}/{dbp} mmHg") except Exception as e: print(f"錯誤: {str(e)}", file=sys.stderr) sys.exit(1)
input_file就是
command = new String[]{“python3”,pythonScriptPath,tempFile.toString()};
里的tempFile.toString()
注意點
java調(diào)用python輸出中文是亂碼,在python里設置
from pathlib import Path # 強制設置標準輸出編碼為UTF-8 sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
python找不到python/raw_data里的文件
def get_data_file_path(self, filename): """獲取數(shù)據(jù)文件的絕對路徑""" # 1. 嘗試在同目錄下的raw_data文件夾查找 script_dir = os.path.dirname(os.path.abspath(__file__)) data_path = os.path.join(script_dir, "raw_data", filename) # 2. 如果在開發(fā)環(huán)境找不到,嘗試在上級目錄的raw_data查找 if not os.path.exists(data_path): parent_dir = os.path.dirname(script_dir) data_path = os.path.join(parent_dir, "raw_data", filename) # 3. 如果還是找不到,嘗試在Docker環(huán)境路徑查找 if not os.path.exists(data_path): data_path = os.path.join("/app/python/raw_data", filename) if not os.path.exists(data_path): raise FileNotFoundError(f"Data file not found at: {data_path}") return data_path
dockerFile注意點
dockerfile這個打包,困擾了我一天,主要遇到了庫拉取不下來,下載離線庫,又遇到了缺少其他依賴的問題
1.docker build報錯
# 第一階段:構(gòu)建Python環(huán)境(使用官方鏡像+國內(nèi)pip源) FROM python:3.9-slim AS python-builder
這個就遇到了
ERROR: failed to solve: python:3.9-slim: failed to resolve source metadata for docker.io/library/python:3.9-slim: failed commit on ref “unknown-sha256:b1fd1b5f83b18a7a7377874e3791c8104d5cf26c52677291a31d8805a9a3e5b0”: “unknown-sha256:b1fd1b5f83b18a7a7377874e3791c8104d5cf26c52677291a31d8805a9a3e5b0” failed size validation: 7630 != 7317: failed precondition
還有另一個庫openjdk:11-jre-slim,也報錯
ERROR: failed to solve: adoptopenjdk:11-jre-hotspot: failed to resolve source metadata for docker.io/library/adoptopenjdk:11-jre-hotspot: failed commit on ref “unknown-sha256:09a07bc840c63d79cfcc70a8960e0cead643b14cfdf6bdbca14a22bd6a9d3991”: “unknown-sha256:09a07bc840c63d79cfcc70a8960e0cead643b14cfdf6bdbca14a22bd6a9d3991” failed size validation: 7634 != 7377: failed precondition
解決的辦法是,先docker pull一下這些庫,因為比較大,docker build的時候可能會中斷
docker pull python:3.9 docker pull openjdk:11-jdk-slim
2.離線包的問題
構(gòu)建鏡像(禁用網(wǎng)絡訪問)
docker build --no-cache --pull=false --network=none -t ring_1.0.0 .
因為上邊的問題,我本來想著都用離線包,把requirements.txt里的依賴庫全下載到本地,然后copy到docker里,結(jié)果
6.811 ERROR: Could not find a version that satisfies the requirement nvidia-cuda-cupti-cu1212.1.105; platform_system == “Linux” and platform_machine == “x86_64” (from torch) (from versions: none)
6.812 ERROR: No matching distribution found for nvidia-cuda-cupti-cu1212.1.105; platform_system == “Linux” and platform_machine == “x86_64”
如果一個一個去找,非常麻煩,還不一定匹配,所以還是需要用在線打包
3.在線打包的注意點
一定一定注意,名稱是否正確
FROM python:3.9-slim as python-builder
我本來是這個,結(jié)果還是一直出錯,后來發(fā)現(xiàn)是名稱錯了,可以用
docker images查看一下鏡像名稱
結(jié)果我本地的python鏡像名是3.9不是3.9-slim,同樣的,F(xiàn)ROM openjdk:11-jre-slim 這個也要確認名稱
FROM python:3.9 as python-builder
Dockerfile文件內(nèi)容
# 第一階段:構(gòu)建Python環(huán)境 FROM python:3.9 as python-builder WORKDIR /app COPY ruoyi-admin/src/main/resources/python/requirements.txt . RUN pip install --user -r requirements.txt && \ mkdir -p /app/python/raw_data # 第二階段:構(gòu)建Java應用 FROM openjdk:11-jdk-slim # 安裝基礎Python環(huán)境 RUN apt-get update && \ apt-get install -y --no-install-recommends \ python3 \ python3-pip \ && rm -rf /var/lib/apt/lists/* # 從python階段復制依賴 COPY --from=python-builder /root/.local /root/.local ENV PATH=/root/.local/bin:$PATH # 設置工作目錄 WORKDIR /app # 復制應用文件 COPY ruoyi-admin/target/ring.jar /ring.jar COPY ruoyi-admin/src/main/resources/python /app/python # 設置權(quán)限 RUN chmod -R 755 /app/python && \ find /app/python -name "*.py" -exec chmod +x {} \; && \ chmod -R 777 /app/python/raw_data # 確保數(shù)據(jù)目錄可寫 # 環(huán)境變量 ENV PYTHON_SCRIPT_PATH=/app/python ENV PYTHONUNBUFFERED=1 EXPOSE 8080 ENTRYPOINT ["java", "-jar", "/ring.jar"]
以上就是springboot調(diào)用python文件的詳細方案的詳細內(nèi)容,更多關(guān)于springboot調(diào)用python的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Spring IoC學習之ApplicationContext中refresh過程詳解
這篇文章主要給大家介紹了關(guān)于Spring IoC學習之ApplicationContext中refresh過程的相關(guān)資料,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-09-09java實現(xiàn)自定義時鐘并實現(xiàn)走時功能
這篇文章主要為大家詳細介紹了java實現(xiàn)自定義時鐘并實現(xiàn)走時功能,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-06-06java+jsp+struts2實現(xiàn)發(fā)送郵件功能
這篇文章主要為大家詳細介紹了java+jsp+struts2實現(xiàn)發(fā)送郵件功能,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-03-03PostMan傳@RequestParam修飾的數(shù)組方式
這篇文章主要介紹了PostMan傳@RequestParam修飾的數(shù)組方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-08-08Java?ASM使用logback日志級別動態(tài)切換方案展示
這篇文章主要介紹了Java?ASM使用logback日志級別動態(tài)切換方案展示,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步早日升職加薪2022-04-04IDEA運行spring項目時,控制臺未出現(xiàn)的解決方案
文章總結(jié)了在使用IDEA運行代碼時,控制臺未出現(xiàn)的問題和解決方案,問題可能是由于點擊圖標或重啟IDEA后控制臺仍未顯示,解決方案提供了解決方法,包括通過右三角Run運行(快捷鍵:Alt+42)或以Debug運行(快捷鍵:Alt+5)來解決2025-01-01