date: 2025-03-23 21:16:45 title: 图片上传至服务器和OSS author: zaqai tags:
- Java
- SpringBoot
图片上传
上传接口为image/upload
@RequestMapping(path = "image/")
@RestController
@Slf4j
public class ImageRestController {
@Autowired
private ImageService imageService;
@RequestMapping(path = "upload")
public ResVo<ImageVo> upload(HttpServletRequest request) {
ImageVo imageVo = new ImageVo();
try {
String imagePath = imageService.saveImg(request);
imageVo.setImagePath(imagePath);
} catch (Exception e) {
log.error("save upload file error!", e);
return ResVo.fail(StatusEnum.UPLOAD_PIC_FAILED);
}
return ResVo.ok(imageVo);
}
}
imageService.saveImg(request)的实现
- 先判断请求, 从请求中获取文件
- 验证文件类型是否为合法image
- 调用
imageUploader.upload
- 该
ImageUploader
接口可以有多个实现, 上传至本地, OSS, 或其他
- 该
@Override
public String saveImg(HttpServletRequest request) {
MultipartFile file = null;
if (request instanceof MultipartHttpServletRequest) {
file = ((MultipartHttpServletRequest) request).getFile("image");
}
if (file == null) {
throw ExceptionUtil.of(StatusEnum.ILLEGAL_ARGUMENTS_MIXED, "缺少需要上传的图片");
}
String fileType = validateStaticImg(file.getContentType());
if (fileType == null) {
throw ExceptionUtil.of(StatusEnum.ILLEGAL_ARGUMENTS_MIXED, "图片只支持png,jpg,gif");
}
try {
return imageUploader.upload(file.getInputStream(), fileType);
} catch (IOException e) {
log.error("Parse img from httpRequest to BufferedImage error! e:", e);
throw ExceptionUtil.of(StatusEnum.UPLOAD_PIC_FAILED);
}
}
上传至服务器(本地)
- 使用了
@ConditionalOnExpression
注解,表示只有在配置文件中的image.oss.type
属性值为 "local" 时才会实例化该类。 - 直接将
InputStream
保存在本地
@Slf4j
@ConditionalOnExpression(value = "#{'local'.equals(environment.getProperty('image.oss.type'))}")
@Component
public class LocalStorageWrapper implements ImageUploader {
@Autowired
private ImageProperties imageProperties;
private Random random;
public LocalStorageWrapper() {
random = new Random();
}
@Override
public String upload(InputStream input, String fileType) {
// 记录耗时分布
StopWatchUtil stopWatchUtil = StopWatchUtil.init("图片上传");
try {
if (fileType == null) {
// 根据魔数判断文件类型
InputStream finalInput = input;
byte[] bytes = stopWatchUtil.record("流转字节", () -> StreamUtils.copyToByteArray(finalInput));
input = new ByteArrayInputStream(bytes);
fileType = getFileType((ByteArrayInputStream) input, fileType);
}
String path = imageProperties.getAbsTmpPath() + imageProperties.getWebImgPath();
String fileName = genTmpFileName();
InputStream finalInput = input;
String finalFileType = fileType;
FileWriteUtil.FileInfo file = stopWatchUtil.record("存储", () -> FileWriteUtil.saveFileByStream(finalInput, path, fileName, finalFileType));
return imageProperties.buildImgUrl(imageProperties.getWebImgPath() + file.getFilename() + "." + file.getFileType());
} catch (Exception e) {
log.error("Parse img from httpRequest to BufferedImage error! e:", e);
throw ExceptionUtil.of(StatusEnum.UPLOAD_PIC_FAILED);
} finally {
log.info("图片上传耗时: {}", stopWatchUtil.prettyPrint());
}
}
}
上传至OSS
OSS自己申请
修改配置文件
image:
abs-tmp-path: /tmp/storage/
web-img-path: /forum/image/
tmp-upload-path: /tmp/forum/
# 可以放自己的OSS的自定义域名
cdn-host:
oss:
type: ali
prefix: paicoding/
endpoint: oss-cn-beijing.aliyuncs.com
ak:
sk:
bucket:
# 可以放自己的OSS的自定义域名
host:
添加OssProperties
JavaBean
@Data
public class OssProperties {
/**
* 上传文件前缀路径
*/
private String prefix;
/**
* oss类型
*/
private String type;
/**
* 下面几个是oss的配置参数
*/
private String endpoint;
private String ak;
private String sk;
private String bucket;
private String host;
}
Spring Boot 的 @ConfigurationProperties 注解使得类能够方便地将配置文件(如 application.yml)中的属性绑定到类的字段上。
@Setter
@Getter
@Component
@ConfigurationProperties(prefix = "image")
public class ImageProperties {
// oss已自动读取配置文件并赋值
private OssProperties oss;
public String buildImgUrl(String url) {
if (!url.startsWith(cdnHost)) {
return cdnHost + url;
}
return url;
}
}
upload()的实现
ByteArrayInputStream input = new ByteArrayInputStream(bytes);
fileName = properties.getOss().getPrefix() + fileName + "." + getFileType(input, fileType);
// 创建PutObjectRequest对象。
PutObjectRequest putObjectRequest = new PutObjectRequest(properties.getOss().getBucket(), fileName, input);
// 设置该属性可以返回response。如果不设置,则返回的response为空。
putObjectRequest.setProcess("true");
// 上传文件
PutObjectResult result = stopWatchUtil.record("文件上传", () -> ossClient.putObject(putObjectRequest));
if (SUCCESS_CODE == result.getResponse().getStatusCode()) {
return properties.getOss().getHost() + fileName;
} else {
log.error("upload to oss error! response:{}", result.getResponse().getStatusCode());
// Guava 不允许回传 null
return "";
}
判断应不应该上传
public boolean uploadIgnore(String fileUrl) {
if (StringUtils.isNotBlank(properties.getOss().getHost()) && fileUrl.startsWith(properties.getOss().getHost())) {
return true;
}
return !fileUrl.startsWith("http");
}
转链逻辑
在前端获取markdown源码, 使用正则const reg = /!\[(.*?)\]\((.*?)\)/mg;
匹配图片链接, 判断是否需要转链, 转的话就调用上传接口, 将图片链接传至后端
转链缓存
在上传或者转链之前, 先判断缓存里有没有对应的图片, 有的话直接返回图片的地址, 没有的话再上传
图片缓存
private Cache<String, String> imgReplaceCache = CacheBuilder
.newBuilder()
.maximumSize(300)
.expireAfterWrite(5, TimeUnit.MINUTES)
.build();
业务流程
public String saveImg(String img) {
if (imageUploader.uploadIgnore(img)) {
// 已经转存过,不需要再次转存;非http图片,不处理
return img;
}
try {
InputStream stream = FileReadUtil.getStreamByFileName(img);
URI uri = URI.create(img);
String path = uri.getPath();
int index = path.lastIndexOf(".");
String fileType = null;
if (index > 0) {
// 从url中获取文件类型
fileType = path.substring(index + 1);
}
String digest = calculateSHA256(stream);
String ans = imgReplaceCache.getIfPresent(digest);
if (StringUtils.isBlank(ans)) {
ans = imageUploader.upload(stream, fileType);
imgReplaceCache.put(digest, ans);
}
if (StringUtils.isBlank(ans)) {
return buildUploadFailImgUrl(img);
}
return ans;
} catch (Exception e) {
log.error("外网图片转存异常! img:{}", img, e);
return buildUploadFailImgUrl(img);
}
}
回复