SpringBoot图片上传的奇妙冒险
S3和OSS的核心区别:
- 价格:OSS在国内的价格通常比S3低20%-30%(特别是流量费用)
- 速度:OSS在国内的访问速度明显快于S3(毕竟服务器在国内)
- 合规性:OSS更符合中国的数据合规要求
- 集成:OSS与阿里云其他服务(如CDN)的集成更顺畅
与本地上传相比的优势:
- 不用操心服务器磁盘空间问题
- 天生具备高可用性和灾备能力
- 轻松实现CDN加速
- 无需自己处理图片缩略图等常见需求
下面就来开启我们的”本土化”奇妙冒险吧!
第一步:导入依赖
1. 创建Bucket
登录阿里云控制台,进入OSS服务,创建一个Bucket。记住这几个关键配置:
- 地域:选离你用户最近的(如华东1)
- 存储类型:标准存储就行
- 读写权限:先设私有,后面用STS临时令牌更安全
2. 获取AccessKey
在RAM访问控制中创建用户,记得保存:
- AccessKey ID
- AccessKey Secret
第二步:SpringBoot装备库
1. 添加依赖
1 2 3 4 5 6 7 8 9 10 11 12 13
| <dependency> <groupId>com.aliyun.oss</groupId> <artifactId>aliyun-sdk-oss</artifactId> <version>3.13.0</version> </dependency>
<dependency> <groupId>net.coobird</groupId> <artifactId>thumbnailator</artifactId> <version>0.4.14</version> </dependency>
|
2. 配置参数
application.yml
中加入:
1 2 3 4 5 6 7 8 9
| aliyun: oss: endpoint: https://oss-cn-hangzhou.aliyuncs.com bucket-name: your-bucket-name access-key-id: your-access-key-id access-key-secret: your-access-key-secret folder: images/ expire-time: 3600 cdn-domain: https://your-cdn-domain.com
|
第三步:编写代码
1. 配置类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @Configuration public class OssConfig {
@Value("${aliyun.oss.endpoint}") private String endpoint;
@Value("${aliyun.oss.access-key-id}") private String accessKeyId;
@Value("${aliyun.oss.access-key-secret}") private String accessKeySecret;
@Bean public OSS ossClient() { return new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret); } }
|
2. 服务类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
| @Service @Slf4j public class OssService {
@Autowired private OSS ossClient;
@Value("${aliyun.oss.bucket-name}") private String bucketName;
@Value("${aliyun.oss.folder}") private String folder;
@Value("${aliyun.oss.expire-time}") private Integer expireTime;
@Value("${aliyun.oss.cdn-domain}") private String cdnDomain;
public String upload(MultipartFile file) { if (file.isEmpty()) { throw new RuntimeException("上传文件不能为空"); }
String originalFilename = file.getOriginalFilename(); String fileExt = originalFilename.substring(originalFilename.lastIndexOf(".")); String fileName = folder + UUID.randomUUID() + fileExt;
try { ossClient.putObject(bucketName, fileName, file.getInputStream());
return StringUtils.isNotBlank(cdnDomain) ? cdnDomain + "/" + fileName : getFileUrl(fileName); } catch (IOException e) { log.error("文件上传失败", e); throw new RuntimeException("文件上传失败"); } }
public String uploadThumbnail(MultipartFile file, int width, int height) { try { BufferedImage thumbnail = Thumbnails.of(file.getInputStream()) .size(width, height) .asBufferedImage();
ByteArrayOutputStream os = new ByteArrayOutputStream(); ImageIO.write(thumbnail, "jpg", os); byte[] bytes = os.toByteArray();
String originalFilename = file.getOriginalFilename(); String fileName = folder + "thumb_" + UUID.randomUUID() + ".jpg";
ossClient.putObject(bucketName, fileName, new ByteArrayInputStream(bytes));
return StringUtils.isNotBlank(cdnDomain) ? cdnDomain + "/" + fileName : getFileUrl(fileName); } catch (IOException e) { log.error("缩略图生成失败", e); throw new RuntimeException("缩略图生成失败"); } }
public String getFileUrl(String fileName) { Date expiration = new Date(System.currentTimeMillis() + expireTime * 1000); URL url = ossClient.generatePresignedUrl(bucketName, fileName, expiration); return url.toString(); }
public void delete(String fileName) { ossClient.deleteObject(bucketName, fileName); } }
|
控制器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| @RestController @RequestMapping("/api/oss") public class OssController {
@Autowired private OssService ossService;
@PostMapping("/upload") public Result<String> upload(@RequestParam("file") MultipartFile file) { String url = ossService.upload(file); return Result.success(url); }
@PostMapping("/upload-thumbnail") public Result<String> uploadThumbnail(@RequestParam("file") MultipartFile file, @RequestParam(defaultValue = "200") int width, @RequestParam(defaultValue = "200") int height) { String url = ossService.uploadThumbnail(file, width, height); return Result.success(url); }
@DeleteMapping public Result<Void> delete(@RequestParam String fileName) { ossService.delete(fileName); return Result.success(); } }
|
第五步:前端小贴士
前端上传可以使用阿里云提供的Plupload或直接表单上传:
1 2 3 4 5 6 7 8 9 10 11 12 13
| <form action="/api/oss/upload" method="post" enctype="multipart/form-data"> <input type="file" name="file"> <button type="submit">上传</button> </form>
<form action="/api/oss/upload-thumbnail" method="post" enctype="multipart/form-data"> <input type="file" name="file"> <input type="number" name="width" value="200"> <input type="number" name="height" value="200"> <button type="submit">上传缩略图</button> </form>
|
高级技巧:更安全的STS令牌方式
上面的方式直接用了AccessKey,生产环境更推荐使用STS临时令牌:
1. 添加依赖
1 2 3 4 5
| <dependency> <groupId>com.aliyun</groupId> <artifactId>aliyun-java-sdk-sts</artifactId> <version>3.0.0</version> </dependency>
|
2. STS服务类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| @Service public class StsService {
@Value("${aliyun.oss.access-key-id}") private String accessKeyId;
@Value("${aliyun.oss.access-key-secret}") private String accessKeySecret;
@Value("${aliyun.oss.role-arn}") private String roleArn;
@Value("${aliyun.oss.bucket-name}") private String bucketName;
@Value("${aliyun.oss.expire-time}") private Integer expireTime;
public StsToken getStsToken() { DefaultProfile profile = DefaultProfile.getProfile("cn-hangzhou", accessKeyId, accessKeySecret); IAcsClient client = new DefaultAcsClient(profile);
AssumeRoleRequest request = new AssumeRoleRequest(); request.setRoleArn(roleArn); request.setRoleSessionName("oss-upload-session"); request.setDurationSeconds(expireTime.longValue());
try { AssumeRoleResponse response = client.getAcsResponse(request); return new StsToken( response.getCredentials().getAccessKeyId(), response.getCredentials().getAccessKeySecret(), response.getCredentials().getSecurityToken(), expireTime, bucketName ); } catch (ClientException e) { throw new RuntimeException("获取STS令牌失败", e); } }
@Data @AllArgsConstructor public static class StsToken { private String accessKeyId; private String accessKeySecret; private String securityToken; private Integer expireTime; private String bucketName; } }
|
3. 前端直传OSS
有了STS令牌后,前端可以直接上传到OSS,减轻服务器压力:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| fetch('/api/oss/sts-token') .then(res => res.json()) .then(token => { const client = new OSS({ region: 'oss-cn-hangzhou', accessKeyId: token.accessKeyId, accessKeySecret: token.accessKeySecret, stsToken: token.securityToken, bucket: token.bucketName });
client.put('filename.jpg', file).then(result => { console.log('上传成功', result); }); });
|
性能优化小技巧
- 使用CDN加速:OSS可以无缝对接阿里云CDN
- 图片处理:OSS原生支持图片处理参数,如
?x-oss-process=image/resize,w_200
- 分片上传:大文件使用分片上传API
- 客户端直传:使用STS令牌让客户端直接上传到OSS