Spring Boot - AJAX 跨域图片上传
Spring Boot 2.2.4.RELEASE
项目结构
后端
新建项目,引入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- hutool -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.1.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/commons-io/commons-io -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
logback-spring.xml 日志配置:
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
<!-- 定义日志文件的输出路径 -->
<property name="USER_HOME" value="G:/log" />
<!-- 输出到控制台 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} %-5level [%-24thread] %logger - %msg%n</pattern>
</encoder>
</appender>
<!-- 基于大小以及时间的轮转策略 -->
<!-- 参考:http://www.logback.cn/04%E7%AC%AC%E5%9B%9B%E7%AB%A0Appenders.html -->
<appender name="ROLLING" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 要写入文件的名称。如果文件不存在,则新建。 -->
<file>${USER_HOME}/logback.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${USER_HOME}/%d{yyyyMMdd}/logback-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>20KB</maxFileSize>
<!-- 最多保留多少数量的归档文件,将会异步删除旧的文件。 -->
<maxHistory>30</maxHistory>
<!-- 所有归档文件总的大小。当达到这个大小后,旧的归档文件将会被异步的删除。 -->
<totalSizeCap>1000MB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%d{HH:mm:ss.SSS} %-5level [%-24thread] %logger - %msg%n</pattern>
</encoder>
</appender>
<logger name="com.mk.controller" level="DEBUG" ></logger>
<!-- 日志输出级别 -->
<root level="info">
<appender-ref ref="ROLLING" />
<appender-ref ref="STDOUT" />
</root>
</configuration>
工具类,用于获取文件扩展名:
public class CommonUtils {
/**
* <p> 获取文件的类型名称,即扩展名。
* <p> 示例:
* <p> 执行 <code>CommonUtils.getTypeName("file.txt", true);</code>,返回 <code>".txt"</code>
* <p> 执行 <code>CommonUtils.getTypeName("file.txt", false);</code>,返回 <code>"txt"</code>
* @param filename 文件名称
* @param dot 是否保留类型名称前面的点(.),<code>true</code> 保留,否则去除。
* @return 如果存在,返回文件类型名称。否则返回 <code>null</code>
*/
public static String getTypeName(String filename, boolean dot) {
if ((filename == null) || (filename.equals(""))) {
return null;
}
int lastOccurrence = filename.lastIndexOf(".");
if (lastOccurrence != -1) {
if (!dot) {
lastOccurrence += 1;
}
String typeName = filename.substring(lastOccurrence);
return typeName;
}
return null; // 该文件不存在类型名称
}
}
附件控制器,用于处理图片的上传和查看:
import java.io.File;
import java.io.IOException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.io.FileUtils;
import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;
import com.mk.common.CommonUtils;
import cn.hutool.core.date.DateUtil;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@RestController
@RequestMapping("/attachment")
public class AttachmentController {
/**
* 存在文件的目录
*/
private String PATH = "G:" + File.separator + "20191108" + File.separator + "1";
/**
* 图片上传
* 注意,此处没有对上传的文件进行任何校验,请谨慎使用。
* @param file
* @return
*/
@CrossOrigin(origins = {"*"})
@PostMapping("image/upload")
public Map<String, Object> upload(@RequestParam(value = "file", required = false) MultipartFile file,
HttpServletRequest request) {
// ServletRequestAttributes servletRequestAttributes =
// (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
// HttpServletRequest request = servletRequestAttributes.getRequest();
String scheme = request.getScheme();
int serverPort = request.getServerPort();
String serverName = request.getServerName();
String path = this.PATH;
Map<String, Object> data = new HashMap<String, Object>();
data.put("message", "图片上传失败!");
data.put("url", "");
if ((file == null) || file.isEmpty()) {
return data;
}
String originalFilename = file.getOriginalFilename(); // 原文件名称
// 因为上传的文件可能出现同名,所以需要重新分配一个新文件名称,防止出现同名文件被覆盖的情况。
String dateFormatStr = DateUtil.format(new Date(), "yyyyMMddHHmmssSSS");
String newFilename = dateFormatStr
+ CommonUtils.getTypeName(originalFilename, true); // 文件类型(扩展名)
// 存放文件的临时目录
File directory = new File(path);
if (!directory.exists()) {
directory.mkdirs(); // 创建目录
}
// 保存文件到指定位置
File destination = new File(directory, newFilename);
try {
// Transfer the received file to the given destination file.
// file.transferTo(destination);
FileUtils.writeByteArrayToFile(destination, file.getBytes()); // 效果同上
} catch (IllegalStateException | IOException e) {
log.error("保存用户上传的文件异常:", e);
return data;
}
// 检查文件是否保存成功
if (destination.exists()) {
String url = scheme + "://" + serverName + ":" + serverPort + "/attachment/image/" + newFilename;
if (log.isDebugEnabled()) {
log.debug("url: " + url);
}
data.put("message", "图片上传成功!");
data.put("url", url);
}
if (log.isDebugEnabled()) {
log.debug(data.toString());
}
return data;
}
/**
* 根据传入的文件名称将对应文件写到响应输出流
* @param response
* @param filename
*/
@CrossOrigin(origins = {"*"})
@GetMapping("image/{filename}")
public void image(HttpServletResponse response, @PathVariable String filename) {
String path = this.PATH;
if (log.isDebugEnabled()) {
log.debug(filename);
}
// 参考:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Caching_FAQ
// Set standard HTTP/1.1 no-cache headers
response.setHeader("Cache-Control", "no-store, no-cache");
response.setContentType("image/jpeg"); // return a jpeg
// 将文件写到输出流
ServletOutputStream out = null;
try {
out = response.getOutputStream();
File file = new File(path , filename); // 存放图片的临时目录
if (file.isFile()) {
out.write(FileUtils.readFileToByteArray(file));
}
out.flush();
} catch (IOException e) {
log.error("查看图片文件异常:", e);
} finally {
IOUtils.closeQuietly(out);
}
}
}
前端
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>AJAX 跨域上传文件</title>
</head>
<body>
<form id="form" enctype="multipart/form-data">
<input id="file" type="file" name="file" />
<input type="text" value="" placeholder="用于对比,请忽略我" />
<br />
<button type="button" id="upload">上传文件</button>
</form>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js"></script>
<script type="text/javascript">
$(function() {
$("#upload").click(function() {
var form = $("#form")[0];
// var form = document.getElementById("form"); // 同上,二选一
console.log("form: ", form);
// 参考:FormData - https://developer.mozilla.org/zh-CN/docs/Web/API/FormData
// 参考:FormData用法详解 - https://blog.csdn.net/zqian1994/article/details/79635413
var formData = new FormData(form);
// 调试
// console.log(formData);
// var keys = formData.keys(); // 表单中所有键,即带有 name 属性的输入标签
// while (true) {
// var key = keys.next();
// if (key.done == true) {
// break;
// }
// console.log("key name: " + key.value);
// }
$.ajax({
type: 'post',
url: "http://localhost:8080/attachment/image/upload", // 图片上传接口
data: formData,
cache: false,
processData: false,
contentType: false,
success: function(data, textStatus, jqXHR) {
console.log(data);
console.log(textStatus);
console.log(jqXHR);
},
error: function(XMLHttpRequest, textStatus, errorThrown) {
console.log(XMLHttpRequest);
console.log(textStatus);
console.log(errorThrown);
}
});
});
});
</script>
</body>
</html>
注意:后端和前端是两个不同的项目。
测试
图片上传成功之后返回的数据:
点击链接查看图片: