Java实现文件自动下载,XXL-Job定时任务中的HTTP文件下载最佳实践
目录
- 1、前言
- 2、业务背景
- 3、核心实现
- 1. XXL-Job任务入口
- 2. URL生成与中文编码处理
- 3. HTTP下载核心实现
- 4、技术要点分析
- 1. 中文URL编码处理
- 2. 重试机制设计
- 3. HTTP连接优化
- 4. 异常处理与日志记录
- 5、常见问题与解决方案
- 1. 文件下载为空
- 2. 连接超时
- 3. 中文文件名乱码
- 6、总结
## 更多学习内容:https://www.processon.com/mindmap/60504b5ff346fb348a93b4fa
1、前言
在企业级应用开发中,定时任务是一个常见的需求场景。特别是在气象、农业等数据密集型行业,需要定期从外部系统下载最新的数据文件。本文将基于一个真实的项目案例,详细介绍如何在XXL-Job框架中实现可靠的HTTP文件下载功能。
2、业务背景
在农业气象系统中,我们需要定期下载中长期天气预报文件,这些文件通常存储在内网文件服务器上,文件名包含中文字符和日期信息。系统需要具备以下特性:
- 自动重试机制:如果当天文件不存在,自动尝试前几天的文件
- 中文文件名支持:正确处理URL中的中文字符编码
- 详细的日志记录:便于问题排查和监控
- 异常处理:优雅处理各种网络异常情况
3、核心实现
1. XXL-Job任务入口
@XxlJob("downloadMediumAndLongTermForecast")
public ReturnT<String> downloadMediumAndLongTermForecast(String param) throws Exception {XxlJobLogger.log("获取最新的周预报文件");// 示例模板 URL(注意:文件名中有中文)String templateUrl = "http://172.18.152.109:10003/Files/Weather/SWP/Original/zqtqyb/{yyyy}/{MM}/{dd}/{yyyyMMdd}中期天气预报.docx";LocalDate currentDate = LocalDate.now();boolean success = false;final int MAX_RETRY_DAYS = 3;for (int i = 0; i <= MAX_RETRY_DAYS; i++) {String fullUrl = generateFullUrl(templateUrl, currentDate);try {XxlJobLogger.log("尝试下载:" + fullUrl);// 构建目标文件路径File targetDir = new File(dirInfo.getRoot() + "Product/");if (!targetDir.exists()) {targetDir.mkdirs();}File targetFile = new File(targetDir, "WeatherForecastNew.doc");// 下载并记录结果boolean result = downloadUsingHttpWithDebug(fullUrl, targetFile);if (result) {XxlJobLogger.log("✅ 成功下载文件:" + targetFile.getAbsolutePath());success = true;break;} else {XxlJobLogger.log("❌ 下载失败,尝试前一日...");currentDate = currentDate.minusDays(1);}} catch (Exception e) {XxlJobLogger.log("❌ 下载失败(" + e.getClass().getSimpleName() + ": " + e.getMessage() + "),尝试前一日...");currentDate = currentDate.minusDays(1);}}if (!success) {XxlJobLogger.log("❌ 在过去 " + MAX_RETRY_DAYS + " 天内均未找到有效的文件");return ReturnT.FAIL;}XxlJobLogger.log("获取最新的周预报文件完成");return ReturnT.SUCCESS;
}
2. URL生成与中文编码处理
处理中文文件名是这个实现的关键难点之一:
public static String generateFullUrl(String templateUrl, LocalDate date) {DateTimeFormatter pathFormatter = DateTimeFormatter.ofPattern("yyyy/MM/dd");DateTimeFormatter filePrefixFormatter = DateTimeFormatter.ofPattern("yyyyMMdd");String formattedDatePath = date.format(pathFormatter);String formattedDatePrefix = date.format(filePrefixFormatter);String url = templateUrl.replace("{yyyy}", String.valueOf(date.getYear())).replace("{MM}", String.format("%02d", date.getMonthValue())).replace("{dd}", String.format("%02d", date.getDayOfMonth())).replace("{yyyyMMdd}", formattedDatePrefix);// 对中文文件名进行URL编码try {// 只对文件名部分进行编码,保持路径部分不变int lastSlashIndex = url.lastIndexOf('/');if (lastSlashIndex != -1) {String pathPart = url.substring(0, lastSlashIndex + 1);String fileNamePart = url.substring(lastSlashIndex + 1);String encodedFileName = URLEncoder.encode(fileNamePart, "UTF-8").replace("+", "%20"); // 空格用%20而不是+url = pathPart + encodedFileName;}} catch (UnsupportedEncodingException e) {XxlJobLogger.log("URL编码失败: " + e.getMessage());}return url;
}
3. HTTP下载核心实现
private boolean downloadUsingHttpWithDebug(String fileUrl, File outputFile) {try {// 添加URL编码日志XxlJobLogger.log("原始URL: " + fileUrl);URL url = new URL(fileUrl);HttpURLConnection httpConn = (HttpURLConnection) url.openConnection();// 设置更完整的请求头httpConn.setRequestMethod("GET");httpConn.setConnectTimeout(30000); // 增加到30秒httpConn.setReadTimeout(60000); // 增加到60秒httpConn.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");httpConn.setRequestProperty("Accept", "*/*");httpConn.setRequestProperty("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8");httpConn.setRequestProperty("Accept-Encoding", "identity"); // 不使用压缩httpConn.setRequestProperty("Connection", "keep-alive");// 处理重定向httpConn.setInstanceFollowRedirects(true);int responseCode = httpConn.getResponseCode();// 输出详细调试信息XxlJobLogger.log("HTTP 状态码:" + responseCode);XxlJobLogger.log("Content-Type: " + httpConn.getContentType());XxlJobLogger.log("Content-Length: " + httpConn.getContentLength());if (responseCode == HttpURLConnection.HTTP_MOVED_TEMP|| responseCode == HttpURLConnection.HTTP_MOVED_PERM|| responseCode == HttpURLConnection.HTTP_SEE_OTHER) {String newUrl = httpConn.getHeaderField("Location");XxlJobLogger.log("检测到重定向,新地址:" + newUrl);return downloadUsingHttpWithDebug(newUrl, outputFile); // 递归处理}if (responseCode != HttpURLConnection.HTTP_OK) {XxlJobLogger.log("服务器返回非 200 状态码: " + responseCode);// 尝试读取错误响应try (InputStream errorStream = httpConn.getErrorStream()) {if (errorStream != null) {byte[] errorBytes = new byte[1024];int bytesRead = errorStream.read(errorBytes);if (bytesRead > 0) {String errorResponse = new String(errorBytes, 0, bytesRead, "UTF-8");XxlJobLogger.log("错误响应: " + errorResponse);}}}return false;}// 开始下载try (InputStream inputStream = httpConn.getInputStream();FileOutputStream outputStream = new FileOutputStream(outputFile)) {byte[] buffer = new byte[4096];int bytesRead;while ((bytesRead = inputStream.read(buffer)) != -1) {outputStream.write(buffer, 0, bytesRead);}// 强制刷新确保写入完成outputStream.flush();// 检查文件大小是否为 0if (outputFile.length() == 0) {XxlJobLogger.log("❌ 下载文件为空(0字节)");return false;}return true;}} catch (Exception e) {XxlJobLogger.log("⚠️ 下载过程中发生异常:" + e.getClass().getName() + ": " + e.getMessage());e.printStackTrace(); // 打印完整堆栈跟踪return false;}
}
4、技术要点分析
1. 中文URL编码处理
问题:直接使用包含中文的URL会导致HTTP请求失败。
解决方案:
- 只对文件名部分进行URL编码,保持路径部分不变
- 使用UTF-8编码
- 将
+
替换为%20
(空格的正确编码)
2. 重试机制设计
策略:
- 从当前日期开始,逐日向前尝试
- 最多重试3天
- 每次失败后记录详细日志
3. HTTP连接优化
关键配置:
- 连接超时:30秒
- 读取超时:60秒
- User-Agent:模拟浏览器请求
- Accept-Encoding:设置为identity避免压缩问题
4. 异常处理与日志记录
最佳实践:
- 详细记录每个步骤的执行情况
- 区分不同类型的异常
- 提供足够的调试信息
5、常见问题与解决方案
1. 文件下载为空
原因:服务器返回200状态码但内容为空
解决:检查文件大小,如果为0字节则认为下载失败
2. 连接超时
原因:网络不稳定或服务器响应慢
解决:适当增加超时时间,添加重试机制
3. 中文文件名乱码
原因:URL编码问题
解决:正确使用URLEncoder进行UTF-8编码
6、总结
本文介绍了一个完整的HTTP文件下载解决方案,特别适用于定时任务场景。通过合理的重试机制、正确的中文编码处理和详细的日志记录,可以构建一个稳定可靠的文件下载系统。
在实际应用中,还需要根据具体的业务需求进行调整和优化。希望这个案例能为大家在类似场景下的开发工作提供参考。