springWebFlux 上传文件 的坑
先说说请求参数的坑
1,springmvc 中使用MultipartFile 当接受参数
2,但是使用webflux的时候 MultipartFile接受会报错415 转换异常的错误 这时候必须用
@RequestPart("file") FilePart file
主要是因为 底层容器不同了 mvc 用的是servlet 而webflux 用的是netty
Config类
这边通过cos的对接文档 直接将TransferManager 注入到spring 容器中
import com.qcloud.cos.COSClient;
import com.qcloud.cos.ClientConfig;
import com.qcloud.cos.auth.BasicCOSCredentials;
import com.qcloud.cos.auth.COSCredentials;
import com.qcloud.cos.http.HttpProtocol;
import com.qcloud.cos.region.Region;
import com.qcloud.cos.transfer.TransferManager;
import com.qcloud.cos.transfer.TransferManagerConfiguration;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.Resource;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 腾讯云存储
*/
@Configuration
public class CosConfiguration {
@Value("${cos.secretId}")
private String secretId;
@Value("${cos.secretKey}")
private String secretKey;
@Value("${cos.region}")
private String region;
@Value("${cos.account}")
private String account;
@Resource
private CosSupplierConfiguration cosSupplierConfiguration;
// 创建 TransferManager 实例,这个实例用来后续调用高级接口
@Bean
public TransferManager transferManager() {
// 创建一个 COSClient 实例,这是访问 COS 服务的基础实例。
// 详细代码参见本页: 简单操作 -> 创建 COSClient
COSClient cosClient = getCosClinet();
// 自定义线程池大小,建议在客户端与 COS 网络充足(例如使用腾讯云的 CVM,同地域上传 COS)的情况下,设置成16或32即可,可较充分的利用网络资源
// 对于使用公网传输且网络带宽质量不高的情况,建议减小该值,避免因网速过慢,造成请求超时。
ExecutorService threadPool = Executors.newFixedThreadPool(16);
// 传入一个 threadpool, 若不传入线程池,默认 TransferManager 中会生成一个单线程的线程池。
TransferManager transferManager = new TransferManager(cosClient, threadPool);
// 设置高级接口的配置项
// 分块上传阈值和分块大小分别为 5MB 和 1MB
TransferManagerConfiguration transferManagerConfiguration = new TransferManagerConfiguration();
transferManagerConfiguration.setMultipartUploadThreshold(1024 * 1024);
transferManagerConfiguration.setMinimumUploadPartSize(5 * 1024 * 1024);
transferManager.setConfiguration(transferManagerConfiguration);
return transferManager;
}
public COSClient getCosClinet() {
// 1 初始化用户身份信息(secretId, secretKey)。
// SECRETID和SECRETKEY请登录访问管理控制台 https://console.cloud.tencent.com/cam/capi 进行查看和管理
COSCredentials cred = new BasicCOSCredentials(cosSupplierConfiguration.getSecretId(), cosSupplierConfiguration.getSecretKey());
// 2 设置 bucket 的地域, COS 地域的简称请参照 https://cloud.tencent.com/document/product/436/6224
// clientConfig 中包含了设置 region, https(默认 http), 超时, 代理等 set 方法, 使用可参见源码或者常见问题 Java SDK 部分。
Region r = new Region(cosSupplierConfiguration.getRegion());
ClientConfig clientConfig = new ClientConfig(r);
// 这里建议设置使用 https 协议
// 从 5.6.54 版本开始,默认使用了 https
clientConfig.setHttpProtocol(HttpProtocol.https);
// 3 生成 cos 客户端。
return new COSClient(cred, clientConfig);
}
}
在appliaction 中添加cos配置
#腾讯云存储
cos:
secretId:
secretKey:
region:
account:
buck-name:
下面为映射配置值的实体类
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Data
@Component
@ConfigurationProperties(prefix = "cos")
public class CosSupplierConfiguration {
private String secretId;
private String secretKey;
private String region;
private String account;
private String buckName;
}
controller层:
通过请求头拿到图片大小,或者使用 ServerHttpRequest request 直接获取请求对象
@ApiOperation(value = "上传文件")
@PostMapping(value = "/upload", produces = MediaType.APPLICATION_JSON_VALUE)
public Mono<ApiEntity<String>> upload(@RequestPart("file") FilePart file, @RequestHeader(value = "Content-Length", required = false) long contentLength) {
if (contentLength > Constants.Cos.FILE_UPLOAD_MAX_SIZE) {
return Mono.error(new ApiException("文件大小不能超过10M"));
}
return fileService.upload(file).switchIfEmpty(Mono.error(new ApiException("文件上传失败"))).map(ApiEntity::ok);
}
service层:
这里没有使用 @Autowired 或者@Resource 获得bean 而是构造方法 加final 的方式
很坑的地方:FilePart 是一个接口 在网上搜了很多都是 都是先通过创建临时文件 然后再通过临时File 上传到远程仓库,要通过本地临时文件 中转一下 然后我就想用
FilePart.content() 拿到Inputstream 的方法直接上传到远程 但是上传的过程中 上传成功了但是 上传的文件一直损坏 然后我看了 content()方法 返回的是一个Flux 类型的DataBuffer 也就是 在接受的时候 文件已经被切块了,所以需要注意 在使用inputstream 的时候 一定要进行map,reduce操作 然后用SequenceInputStream 将流块 排序也就是对应代码下面的 map(DataBuffer::asInputStream).reduce(SequenceInputStream::new),不然上传的文件就只是一部分
import cn.hutool.core.io.file.FileNameUtil;
import cn.hutool.core.util.IdUtil;
import com.dotc.group.metaverse.sender.config.Constants;
import com.dotc.group.metaverse.sender.config.CosSupplierConfiguration;
import com.qcloud.cos.model.ObjectMetadata;
import com.qcloud.cos.model.PutObjectRequest;
import com.qcloud.cos.model.UploadResult;
import com.qcloud.cos.transfer.TransferManager;
import com.qcloud.cos.transfer.Upload;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.codec.multipart.FilePart;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
import javax.annotation.Resource;
import java.io.SequenceInputStream;
import java.util.Objects;
@Service
@AllArgsConstructor
@Slf4j
public class FileService {
private final CosSupplierConfiguration supplierConfiguration;
private final TransferManager transferManager;
public Mono<String> upload(FilePart file) {
String key = Constants.Cos.SIGNATURE + "/" + IdUtil.simpleUUID() + "." + FileNameUtil.getSuffix(file.filename());
return file.content().map(DataBuffer::asInputStream).reduce(SequenceInputStream::new).flatMap(inputStream -> {
ObjectMetadata objectMetadata = new ObjectMetadata();
PutObjectRequest putObjectRequest = new PutObjectRequest(supplierConfiguration.getBuckName(), key, inputStream, objectMetadata);
UploadResult uploadResult = null;
try {
// 高级接口会返回一个异步结果Upload
// 可同步地调用 waitForUploadResult 方法等待上传完成,成功返回UploadResult, 失败抛出异常
Upload upload = transferManager.upload(putObjectRequest);
uploadResult = upload.waitForUploadResult();
} catch (Exception e) {
log.info("文件上传出错", e);
return Mono.empty();
}
if (!Objects.isNull(uploadResult)) {
return Mono.just(String.format(Constants.Cos.COS_PATH_FORMAT, supplierConfiguration.getBuckName(), supplierConfiguration.getRegion(), uploadResult.getKey()));
}
return Mono.empty();
});
}
}