使用Java HttpServletResponse和JavaScript Fetch下载文件

Java端:

java 复制代码
@Autowired
private RestTemplate restTemplate;

@Override
public void download(HttpServletResponse response) {
	String baseUrl = "/contract/download";
	String fullUrl = SignApiUtil.BASE_DOMAIN + baseUrl;

	String timestamp = SignApiUtil.getTimestamp();
	String nonce = SignApiUtil.getNonce();
	String signature = SignApiUtil.getSignature(timestamp, nonce);

	// 请求头
	HttpHeaders headers = new HttpHeaders();
	headers.set("x-qys-accesstoken", SignApiUtil.APP_TOKEN);
	headers.set("x-qys-timestamp", timestamp);
	headers.set("x-qys-nonce", nonce);
	headers.set("x-qys-signature", signature);

	// 请求参数
	MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
	params.add("contractId", "3499343568015819654");

	UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(fullUrl).queryParams(params);

	HttpEntity<?> requestEntity = new HttpEntity<>(headers);

	restTemplate.setErrorHandler(new DefaultResponseErrorHandler() {
		@Override
		protected boolean hasError(HttpStatus statusCode) {
			return false;
		}
	});

	try {
		ResponseEntity<byte[]> responseEntity = restTemplate.exchange(
				builder.toUriString(),
				HttpMethod.GET,
				requestEntity,
				byte[].class
		);

		byte[] fileBytes = responseEntity.getBody();
		if (fileBytes == null) {
			throw new RuntimeException("文件内容为空");
		}

		String realFileName = "download";
		String contentDisposition = responseEntity.getHeaders().getFirst(HttpHeaders.CONTENT_DISPOSITION);

		if (contentDisposition != null && contentDisposition.trim().length() > 0) {
			// 解析文件名
			String fileNameEncoded = contentDisposition
					.replaceFirst("(?i)^.*filename=\"?", "")
					.replaceFirst("\"?$", "");

			// URL解码
			realFileName = java.net.URLDecoder.decode(fileNameEncoded, "UTF-8");
		}

		// 设置响应
		response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
		response.setCharacterEncoding(StandardCharsets.UTF_8.name());

		// 正确设置下载文件名
		String encodedFileName = URLEncoder.encode(realFileName, StandardCharsets.UTF_8.name());
		response.setHeader(HttpHeaders.CONTENT_DISPOSITION,
				"attachment;filename=" + encodedFileName +
				";filename*=UTF-8''" + encodedFileName);

		// 暴露给前端,让JS能读取到文件名
		response.setHeader("Access-Control-Expose-Headers", HttpHeaders.CONTENT_DISPOSITION);

		// 输出文件
		try (OutputStream os = response.getOutputStream()) {
			os.write(fileBytes);
			os.flush();
		}

	} catch (Exception e) {
		e.printStackTrace();
		throw new RuntimeException("文件下载失败:" + e.getMessage(), e);
	}
}

JavaScript端:

javascript 复制代码
fetch("/main/download", {
    method: "GET"
}).then(async (resp) => {

    if (!resp.ok) {
        throw new Error(`请求失败,状态码:${resp.status}`);
    }

    // 从响应头拿文件名
    let disposition = resp.headers.get("Content-Disposition");
    let fileName = "download.pdf";

    if (disposition) {
        const match = disposition.match(/filename\*?=(?:UTF-8'')?([^;]+)/);
        if (match && match[1]) {
            fileName = decodeURIComponent(match[1].replace(/"/g, ""));
        }
    }

    const blob = await resp.blob();

    const downloadUrl = window.URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = downloadUrl;
    a.download = fileName;
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);

    window.URL.revokeObjectURL(downloadUrl);
}).catch(error => {
    console.error("模板下载失败:", error);
});
相关推荐
meilindehuzi_a6 小时前
从零理解并实现 RAG:用 LangChain.js 构建语义检索问答系统
开发语言·javascript·langchain
wuqingshun3141596 小时前
什么是责任链模式,一般用在什么场景?
java·责任链模式
:-)7 小时前
算法-归并排序
java·开发语言·数据结构·算法·排序算法
鱼樱前端7 小时前
别再自己拼凑了!这款 Vue 3.5+ 严肃产品级组件库,把 AI 对话、工作流和复杂数据全包了!
javascript·vue.js·github
wuqingshun3141598 小时前
说一下消息队列的模型有哪些?
java
Listen·Rain8 小时前
Vue3:setup详解
前端·javascript·vue.js
fīɡЙtīиɡ ℡8 小时前
布隆过滤器
java
yaoxin5211239 小时前
462. Java 反射 - 获取声明类与封闭类
java·开发语言·python
kyriewen9 小时前
面试官让我优化一个卡死的表格,我打开 Claude 五秒重写,他放下咖啡问我要不要来当组长
前端·javascript·面试
中微极客9 小时前
解锁LLM开发全栈能力:Python + LangChain + RAG 工程实战指南
人工智能·python·langchain