FileController.java
· 1.6 KiB · Java
Originalformat
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
import com.demo.common.reply.Result;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
/**
* 文件
**/
@RestController
@RequestMapping("api/file")
@Tag(name = "文件")
public class FileController {
@GetMapping("download-image")
@Operation(summary = "下载")
@Parameter(name = "filename", description = "文件名称", in = ParameterIn.QUERY)
@ApiOperationSupport(order = 5)
public ResponseEntity<Object> download(String filename) {
Resource image = new ClassPathResource("static/" + filename);
if (!image.exists()) {
return ResponseEntity.ok(Result.error(404, "文件不存在"));
}
String fileName = URLEncoder.encode("网站图标.png", StandardCharsets.UTF_8);
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + fileName);
headers.add(HttpHeaders.CONTENT_TYPE, "image/png");
return ResponseEntity.ok().headers(headers).body(image);
}
}
| 1 | import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport; |
| 2 | import com.demo.common.reply.Result; |
| 3 | import io.swagger.v3.oas.annotations.Operation; |
| 4 | import io.swagger.v3.oas.annotations.Parameter; |
| 5 | import io.swagger.v3.oas.annotations.enums.ParameterIn; |
| 6 | import io.swagger.v3.oas.annotations.tags.Tag; |
| 7 | import org.springframework.core.io.ClassPathResource; |
| 8 | import org.springframework.core.io.Resource; |
| 9 | import org.springframework.http.HttpHeaders; |
| 10 | import org.springframework.http.ResponseEntity; |
| 11 | import org.springframework.web.bind.annotation.GetMapping; |
| 12 | import org.springframework.web.bind.annotation.RequestMapping; |
| 13 | import org.springframework.web.bind.annotation.RestController; |
| 14 | |
| 15 | import java.net.URLEncoder; |
| 16 | import java.nio.charset.StandardCharsets; |
| 17 | |
| 18 | /** |
| 19 | * 文件 |
| 20 | **/ |
| 21 | @RestController |
| 22 | @RequestMapping("api/file") |
| 23 | @Tag(name = "文件") |
| 24 | public class FileController { |
| 25 | |
| 26 | @GetMapping("download-image") |
| 27 | @Operation(summary = "下载") |
| 28 | @Parameter(name = "filename", description = "文件名称", in = ParameterIn.QUERY) |
| 29 | @ApiOperationSupport(order = 5) |
| 30 | public ResponseEntity<Object> download(String filename) { |
| 31 | Resource image = new ClassPathResource("static/" + filename); |
| 32 | if (!image.exists()) { |
| 33 | return ResponseEntity.ok(Result.error(404, "文件不存在")); |
| 34 | } |
| 35 | String fileName = URLEncoder.encode("网站图标.png", StandardCharsets.UTF_8); |
| 36 | HttpHeaders headers = new HttpHeaders(); |
| 37 | headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + fileName); |
| 38 | headers.add(HttpHeaders.CONTENT_TYPE, "image/png"); |
| 39 | return ResponseEntity.ok().headers(headers).body(image); |
| 40 | } |
| 41 | } |
Result.java
· 839 B · Java
Originalformat
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 响应结果
**/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Result<T> {
/**
* 状态码
*/
@Schema(description = "状态码 0-响应成功")
private int code;
/**
* 消息
*/
@Schema(description = "消息")
private String msg;
/**
* 响应数据
*/
@Schema(description = "响应数据")
private T data;
public static <T> Result<T> ok() {
return new Result<>(0, "ok", null);
}
public static <T> Result<T> ok(T data) {
return new Result<>(0, "ok", data);
}
public static <T> Result<T> error(Integer code, String msg) {
return new Result<>(code, msg, null);
}
}
| 1 | import io.swagger.v3.oas.annotations.media.Schema; |
| 2 | import lombok.AllArgsConstructor; |
| 3 | import lombok.Data; |
| 4 | import lombok.NoArgsConstructor; |
| 5 | |
| 6 | /** |
| 7 | * 响应结果 |
| 8 | **/ |
| 9 | @Data |
| 10 | @AllArgsConstructor |
| 11 | @NoArgsConstructor |
| 12 | public class Result<T> { |
| 13 | /** |
| 14 | * 状态码 |
| 15 | */ |
| 16 | @Schema(description = "状态码 0-响应成功") |
| 17 | private int code; |
| 18 | |
| 19 | /** |
| 20 | * 消息 |
| 21 | */ |
| 22 | @Schema(description = "消息") |
| 23 | private String msg; |
| 24 | |
| 25 | /** |
| 26 | * 响应数据 |
| 27 | */ |
| 28 | @Schema(description = "响应数据") |
| 29 | private T data; |
| 30 | |
| 31 | public static <T> Result<T> ok() { |
| 32 | return new Result<>(0, "ok", null); |
| 33 | } |
| 34 | |
| 35 | public static <T> Result<T> ok(T data) { |
| 36 | return new Result<>(0, "ok", data); |
| 37 | } |
| 38 | |
| 39 | public static <T> Result<T> error(Integer code, String msg) { |
| 40 | return new Result<>(code, msg, null); |
| 41 | } |
| 42 | } |
WebMvcConfig.java
· 702 B · Java
Originalformat
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowCredentials(true)
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.exposedHeaders("Content-Disposition") // 跨域时要开启此响应头 否则前端获取不到
.maxAge(3600);
}
}
| 1 | import org.springframework.context.annotation.Configuration; |
| 2 | import org.springframework.web.servlet.config.annotation.CorsRegistry; |
| 3 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; |
| 4 | |
| 5 | @Configuration |
| 6 | public class WebMvcConfig implements WebMvcConfigurer { |
| 7 | |
| 8 | @Override |
| 9 | public void addCorsMappings(CorsRegistry registry) { |
| 10 | registry.addMapping("/**") |
| 11 | .allowedOriginPatterns("*") |
| 12 | .allowCredentials(true) |
| 13 | .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") |
| 14 | .exposedHeaders("Content-Disposition") // 跨域时要开启此响应头 否则前端获取不到 |
| 15 | .maxAge(3600); |
| 16 | } |
| 17 | } |
| 18 |
demo.html
· 3.7 KiB · HTML
Originalformat
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>测试文件下载</title>
</head>
<body>
<button onclick="download('http://localhost:8080/api/file/download-image?filename=favicon.png')">下载文件</button>
<script>
/**
* 使用 Promise 链式写法下载文件
* 处理:1. URL编码的文件名 2. 200状态码下的JSON错误响应(文件不存在等)
* @param {string} url - 后端文件下载接口地址
* @returns {Promise<void>} - 无返回值,通过alert提示结果
*/
function download(url) {
// 返回Promise对象,开启链式调用
return fetch(url, {
method: 'GET',
headers: {
'Accept': '*/*' // 允许接收文件(blob)和JSON两种响应
}
})
// 第一步:解析响应(先判断Content-Type,区分文件/JSON)
.then(response => {
const contentType = response.headers.get('Content-Type');
// 场景1:响应为JSON(即使状态码200,也可能是错误信息)
if (contentType && contentType.includes('application/json')) {
return response.json().then(jsonData => {
// 抛出包含JSON错误信息的异常,交给catch处理
throw new Error(JSON.stringify(jsonData));
});
}
// 场景2:响应为文件(非JSON),直接返回response供后续处理
return response;
})
// 第二步:处理文件下载(仅当响应为文件时执行)
.then(fileResponse => {
// 解析URL编码的文件名
const disposition = fileResponse.headers.get('Content-Disposition');
let fileName = 'downloaded-file'; // 默认文件名
console.log('Content-Disposition:', disposition);
if (disposition && disposition.includes('filename=')) {
// 提取filename=后的内容,移除引号并解码URL编码
fileName = decodeURIComponent(
disposition.split('filename=')[1].replace(/["']/g, '')
);
}
// 转换为Blob并触发下载
return fileResponse.blob().then(blob => {
if (window.navigator.msSaveOrOpenBlob) {
navigator.msSaveBlob(blob, filename)
} else {
const blobUrl = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = blobUrl;
a.download = fileName;
document.body.appendChild(a);
a.click();
// 清理临时资源(避免内存泄漏)
setTimeout(() => {
document.body.removeChild(a);
URL.revokeObjectURL(blobUrl);
}, 100);
}
});
})
// 第三步:统一捕获所有错误(网络错误、JSON错误、业务异常等)
.catch(error => {
try {
// 尝试解析JSON格式的错误信息(文件不存在等后端自定义错误)
const errorData = JSON.parse(error.message);
// 匹配"文件不存在"的错误码(code=404)
if (errorData.code === 404) {
alert(`下载失败:${errorData.msg || '文件不存在'}`);
} else {
alert(`下载失败:${errorData.msg || '未知错误'}`);
}
} catch (parseErr) {
// 非JSON错误(网络异常、解析失败等)
alert(`下载出错:${error.message || '请检查网络或接口地址'}`);
}
console.error('文件下载完整错误信息:', error);
});
}
</script>
</body>
</html>
| 1 | <!DOCTYPE html> |
| 2 | <html lang="en"> |
| 3 | |
| 4 | <head> |
| 5 | <meta charset="UTF-8"> |
| 6 | <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| 7 | <title>测试文件下载</title> |
| 8 | </head> |
| 9 | |
| 10 | <body> |
| 11 | <button onclick="download('http://localhost:8080/api/file/download-image?filename=favicon.png')">下载文件</button> |
| 12 | <script> |
| 13 | /** |
| 14 | * 使用 Promise 链式写法下载文件 |
| 15 | * 处理:1. URL编码的文件名 2. 200状态码下的JSON错误响应(文件不存在等) |
| 16 | * @param {string} url - 后端文件下载接口地址 |
| 17 | * @returns {Promise<void>} - 无返回值,通过alert提示结果 |
| 18 | */ |
| 19 | function download(url) { |
| 20 | // 返回Promise对象,开启链式调用 |
| 21 | return fetch(url, { |
| 22 | method: 'GET', |
| 23 | headers: { |
| 24 | 'Accept': '*/*' // 允许接收文件(blob)和JSON两种响应 |
| 25 | } |
| 26 | }) |
| 27 | // 第一步:解析响应(先判断Content-Type,区分文件/JSON) |
| 28 | .then(response => { |
| 29 | const contentType = response.headers.get('Content-Type'); |
| 30 | // 场景1:响应为JSON(即使状态码200,也可能是错误信息) |
| 31 | if (contentType && contentType.includes('application/json')) { |
| 32 | return response.json().then(jsonData => { |
| 33 | // 抛出包含JSON错误信息的异常,交给catch处理 |
| 34 | throw new Error(JSON.stringify(jsonData)); |
| 35 | }); |
| 36 | } |
| 37 | // 场景2:响应为文件(非JSON),直接返回response供后续处理 |
| 38 | return response; |
| 39 | }) |
| 40 | // 第二步:处理文件下载(仅当响应为文件时执行) |
| 41 | .then(fileResponse => { |
| 42 | // 解析URL编码的文件名 |
| 43 | const disposition = fileResponse.headers.get('Content-Disposition'); |
| 44 | let fileName = 'downloaded-file'; // 默认文件名 |
| 45 | console.log('Content-Disposition:', disposition); |
| 46 | if (disposition && disposition.includes('filename=')) { |
| 47 | // 提取filename=后的内容,移除引号并解码URL编码 |
| 48 | fileName = decodeURIComponent( |
| 49 | disposition.split('filename=')[1].replace(/["']/g, '') |
| 50 | ); |
| 51 | } |
| 52 | |
| 53 | // 转换为Blob并触发下载 |
| 54 | return fileResponse.blob().then(blob => { |
| 55 | if (window.navigator.msSaveOrOpenBlob) { |
| 56 | navigator.msSaveBlob(blob, filename) |
| 57 | } else { |
| 58 | const blobUrl = URL.createObjectURL(blob); |
| 59 | const a = document.createElement('a'); |
| 60 | a.href = blobUrl; |
| 61 | a.download = fileName; |
| 62 | document.body.appendChild(a); |
| 63 | a.click(); |
| 64 | |
| 65 | // 清理临时资源(避免内存泄漏) |
| 66 | setTimeout(() => { |
| 67 | document.body.removeChild(a); |
| 68 | URL.revokeObjectURL(blobUrl); |
| 69 | }, 100); |
| 70 | } |
| 71 | }); |
| 72 | }) |
| 73 | // 第三步:统一捕获所有错误(网络错误、JSON错误、业务异常等) |
| 74 | .catch(error => { |
| 75 | try { |
| 76 | // 尝试解析JSON格式的错误信息(文件不存在等后端自定义错误) |
| 77 | const errorData = JSON.parse(error.message); |
| 78 | // 匹配"文件不存在"的错误码(code=404) |
| 79 | if (errorData.code === 404) { |
| 80 | alert(`下载失败:${errorData.msg || '文件不存在'}`); |
| 81 | } else { |
| 82 | alert(`下载失败:${errorData.msg || '未知错误'}`); |
| 83 | } |
| 84 | } catch (parseErr) { |
| 85 | // 非JSON错误(网络异常、解析失败等) |
| 86 | alert(`下载出错:${error.message || '请检查网络或接口地址'}`); |
| 87 | } |
| 88 | console.error('文件下载完整错误信息:', error); |
| 89 | }); |
| 90 | } |
| 91 | </script> |
| 92 | </body> |
| 93 | |
| 94 | </html> |