如何在Spring Boot中向Amazon S3上传文件
亚马逊简单存储(Amazon S3)是亚马逊网络服务提供的一项服务,提供可扩展、安全和性能良好的对象存储。本文将介绍如何使用Spring Boot向Amazon S3上传文件。
前提条件
- 对[Java]有一定了解。
- 在你的电脑上安装了Java开发工具包。
- 你喜欢的IDE。我使用[Intellij社区版],它是免费的。
亚马逊网络服务账户
在我们开始创建我们的应用程序之前,请到[亚马逊控制台],并创建一个账户。你将获得12个月的免费访问各种亚马逊网络服务的机会,你可以用它来测试各种亚马逊服务。
注册后,前往[亚马逊控制台],在控制台提供的搜索框中搜索Amazon S3。
![Amazon search console]
亚马逊S3桶
从上面的步骤中选择Amazon S3后,创建一个新的S3桶,我们将用它来存储我们将从我们的应用程序上传的文件。
将该桶命名为spring-amazon-storage
,并将所有其他设置保留为默认,然后创建该桶。
访问和密匙
从My Security Credentials
导航菜单创建一个新的访问密钥,如下图所示。复制生成的访问权限和密匙,因为我们将使用它们从我们将要创建的应用程序中访问该桶。
创建应用程序
我们将使用spring initializr来创建我们的应用程序。前往spring initializr并创建一个新的Spring Boot应用程序,添加h2
,spring boot dev tools
,spring data jpa
和spring web
作为依赖,然后生成该项目。
解压缩下载的项目,并在你最喜欢的IDE中打开它。
添加Amazon SDK的依赖性
亚马逊SDK使我们能够从我们的应用程序与各种亚马逊服务进行交互。在pom.xml
文件中添加Amazon SDK的依赖关系,如下所示。
<!-- https://mvnrepository.com/artifact/com.amazonaws/aws-java-sdk -->
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk</artifactId>
<version>1.11.931</version>
</dependency>
项目结构
config/
|--- AmazonConfig.java
|--- BucketName.java
controllers/
|--- TodoController.java
domain/
|--- Todo.java
repositories/
|--- TodoRepository.java
service/
|--- FileStore.java
|--- TodoService.java
|--- TodoServiceImpl.java
SpringAmazonApplication.java
配置包
在配置包中,我们有两个Java文件,一个用来验证Amazon S3,另一个包含了桶的名称。
import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AmazonConfig {
@Bean
public AmazonS3 s3() {
AWSCredentials awsCredentials =
new BasicAWSCredentials("accessKey", "secretKey");
return AmazonS3ClientBuilder
.standard()
.withRegion("ap-south-1")
.withCredentials(new AWSStaticCredentialsProvider(awsCredentials))
.build();
}
}
上面的AmazonConfig
类被注解为@Configuration
注解,以使其作为一个配置类提供给Spring上下文。利用我们之前从亚马逊控制台得到的亚马逊凭证,我们将使用我们之前添加到pom.xml
中的Amazon-SDK中的AmazonS3ClientBuilder
,对S3进行认证。
import lombok.AllArgsConstructor;
import lombok.Getter;
@AllArgsConstructor
@Getter
public enum BucketName {
TODO_IMAGE("spring-amazon-storage");
private final String bucketName;
}
在上面的BucketName
枚举中,我们传入我们先前在亚马逊控制台创建的桶的名称。这个桶将被用来存储我们所有的文件上传。
@AllArgsConstructor
注释生成了一个构造函数,其中包含枚举中的 变量。bucketName
@Getter
注释为枚举中的 变量生成一个getter。bucketName
领域包
在这个包中,我们有Todo
模型,在数据库中代表我们的Todo
。
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Entity
public class Todo {
@Id
@GeneratedValue
private Long id;
private String title;
private String description;
private String imagePath;
private String imageFileName;
}
@Data
注释为 类生成了 , , 和 方法。Todo
getters
setters
toString
equals
@AllArgsConstructor
注释为 类生成一个带有所有参数的构造函数。Todo
@NoArgsConstructor
注释为 类生成一个没有参数的构造函数。Todo
@Builder
注释为 类创建了一个构建器模式。Todo
@Entity
注释使 类成为一个数据库实体。Todo
@Id
注释将 字段标记为数据库中的主键。id
@GeneratedValue
注释使 字段在新的 被保存到数据库中时自动增加。id
todo
仓库包
在这个包中,我们有一个存储库类,它扩展了JPA的CrudRepository
接口,使得它可以执行各种数据库查询。
public interface TodoRepository extends CrudRepository<Todo, Long> {
Todo findByTitle(String title);
}
服务包
@AllArgsConstructor
@Service
public class FileStore {
private final AmazonS3 amazonS3;
public void upload(String path,
String fileName,
Optional<Map<String, String>> optionalMetaData,
InputStream inputStream) {
ObjectMetadata objectMetadata = new ObjectMetadata();
optionalMetaData.ifPresent(map -> {
if (!map.isEmpty()) {
map.forEach(objectMetadata::addUserMetadata);
}
});
try {
amazonS3.putObject(path, fileName, inputStream, objectMetadata);
} catch (AmazonServiceException e) {
throw new IllegalStateException("Failed to upload the file", e);
}
}
public byte[] download(String path, String key) {
try {
S3Object object = amazonS3.getObject(path, key);
S3ObjectInputStream objectContent = object.getObjectContent();
return IOUtils.toByteArray(objectContent);
} catch (AmazonServiceException | IOException e) {
throw new IllegalStateException("Failed to download the file", e);
}
}
}
在上面的FileStore
类中,我们有用于从Amazon S3上传和下载文件的逻辑。
在upload
方法中,我们传入。
path
是存储文件的Amazon S3桶的路径。fileName
是被上传文件的实际名称。当从S3下载文件时,它将被用作密钥。optionalMetaData
地图包含文件的详细信息,如文件类型和文件大小。inputStream
包含应该被保存到Amazon S3的实际文件。
ObjectMetadata objectMetadata = new ObjectMetadata();
optionalMetaData.ifPresent(map -> {
if (!map.isEmpty()) {
map.forEach(objectMetadata::addUserMetadata);
}
});
上面的代码块在optionalMetaData
map中循环,将所有的文件信息添加到S3的objectMetaData
。
amazonS3.putObject(path, fileName, inputStream, objectMetadata);
将文件保存到Amazon S3桶中。
在download
方法中。
S3Object object = amazonS3.getObject(path, key);
从传入的路径下载文件,文件名与 方法中传入的键相似。getObject
S3ObjectInputStream objectContent = object.getObjectContent();
从Amazon S3返回的对象中获得一个inputStream。IOUtils.toByteArray(objectContent)
将输入流转换为可通过Restful APIs发送的 。byteArray
public interface TodoService {
Todo saveTodo(String title, String description, MultipartFile file);
byte[] downloadTodoImage(Long id);
List<Todo> getAllTodos();
}
上面的TodoService
接口包含了我们要实现的各种方法,以便能够保存和获取todos
。
@Service
@AllArgsConstructor
public class TodoServiceImpl implements TodoService {
private final FileStore fileStore;
private final TodoRepository repository;
@Override
public Todo saveTodo(String title, String description, MultipartFile file) {
//check if the file is empty
if (file.isEmpty()) {
throw new IllegalStateException("Cannot upload empty file");
}
//Check if the file is an image
if (!Arrays.asList(IMAGE_PNG.getMimeType(),
IMAGE_BMP.getMimeType(),
IMAGE_GIF.getMimeType(),
IMAGE_JPEG.getMimeType()).contains(file.getContentType())) {
throw new IllegalStateException("FIle uploaded is not an image");
}
//get file metadata
Map<String, String> metadata = new HashMap<>();
metadata.put("Content-Type", file.getContentType());
metadata.put("Content-Length", String.valueOf(file.getSize()));
//Save Image in S3 and then save Todo in the database
String path = String.format("%s/%s", BucketName.TODO_IMAGE.getBucketName(), UUID.randomUUID());
String fileName = String.format("%s", file.getOriginalFilename());
try {
fileStore.upload(path, fileName, Optional.of(metadata), file.getInputStream());
} catch (IOException e) {
throw new IllegalStateException("Failed to upload file", e);
}
Todo todo = Todo.builder()
.description(description)
.title(title)
.imagePath(path)
.imageFileName(fileName)
.build();
repository.save(todo);
return repository.findByTitle(todo.getTitle());
}
@Override
public byte[] downloadTodoImage(Long id) {
Todo todo = repository.findById(id).get();
return fileStore.download(todo.getImagePath(), todo.getImageFileName());
}
@Override
public List<Todo> getAllTodos() {
List<Todo> todos = new ArrayList<>();
repository.findAll().forEach(todos::add);
return todos;
}
}
在上面的TodoServiceImpl
,我们提供了用于保存和获取所有todos
的方法的实现。
控制者包
在这个包中,我们有TodoController
类,它处理传入的HTTP请求。
@RestController
@RequestMapping("api/v1/todo")
@AllArgsConstructor
@CrossOrigin("*")
public class TodoController {
TodoService service;
@GetMapping
public ResponseEntity<List<Todo>> getTodos() {
return new ResponseEntity<>(service.getAllTodos(), HttpStatus.OK);
}
@PostMapping(
path = "",
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE
)
public ResponseEntity<Todo> saveTodo(@RequestParam("title") String title,
@RequestParam("description") String description,
@RequestParam("file") MultipartFile file) {
return new ResponseEntity<>(service.saveTodo(title, description, file), HttpStatus.OK);
}
@GetMapping(value = "{id}/image/download")
public byte[] downloadTodoImage(@PathVariable("id") Long id) {
return service.downloadTodoImage(id);
}
}
测试我们从S3桶上传和下载的情况
总结
恭喜你!现在你已经学会了如何上传和下载。现在你已经学会了如何从Amazon S3上传和下载文件,继续实现向Amazon S3上传多个文件的逻辑。