使用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);
});
相关推荐
我登哥MVP1 小时前
Spring Boot 从“会用”到“精通”:自动装配原理
java·spring boot·后端·spring·tomcat·maven·intellij-idea
小的~~1 小时前
Java线程及线程池的相关的问题
java·开发语言·多线程
爱吃羊的老虎1 小时前
【JAVA】Java微服务—网关Gateway
java·微服务·gateway
CTA终结者1 小时前
Python 写期货自动交易:行情下单与成交回报怎么组织
开发语言·python·区块链
倾一生爱恋换一世纯真1 小时前
接口自动化测试框架搭建流程
python
小雨下雨的雨1 小时前
近视度数模拟器鸿蒙PC Electron框架技术实现详解
前端·javascript·electron
TE-茶叶蛋1 小时前
Next.js中App Router 全部特殊文件一览
开发语言·javascript·网络
abcy0712132 小时前
python flask app.py里的接口放在别的目录下图文教程
python
小雨下雨的雨2 小时前
鸿蒙PC用Electron框架——Canvas蜡笔抖动效果实现技术深度解析
前端·javascript·华为·electron·鸿蒙系统