Yandex Cloud Object Storage - это совместимое с AWS S3 облачное хранилище. В этой статье мы интегрируем его в Spring Boot приложение, используя SDK Амазона.
Создание бакета в Yandex Cloud
Для начала создадим папку, в которой позже будет создано объектное хранилище
Здесь в сервисах находим и создаем s3
Указываем необходимые параметры
И переходим в наше ведерко
Создание аккаунта для доступа к бакету
Как указано в документации, для использования s3 необходимо создать сервисный аккаунт.
Для этого возвращаемся в созданную папку, переходим в Service accounts и создаем новый аккаунт:
Далее возвращаемся в наш бакет и переходим на вкладку security, чтобы добавить роль именно для бакета
Нажимаем на вкладку Assign bindings и добавляем роль editor для нашего аккаунта
Переходим обратно в аккаунт и создаем static access key, сохраняем key id и secret key
Выбираем зависимость для работы с S3
Yandex Object Storage совместим с AWS S3, поэтому именно их sdk мы будем использовать, но есть несколько разных зависимостей, использующих эту sdk.
com.amazonaws:aws-java-sdk:1.x.x - устаревший sdk, который перестанет поддерживаться в этом году
software.amazon.awssdk:s3:2.x.x - актуальный sdk
io.awspring.cloud:spring-cloud-aws-starter-s3 - надстройка над sdk, поддерживаемая комьюинити и улучшающая интеграцию со спрингом
org.springframework.cloud.stream.app:aws-s3-app-starters-common - стартер из проекта spring cloud stream, использующийся для интеграции этого проекта с S3
В этой статье используем актуальную версию sdk (вариант 2), так как нам нужна только базовая функциональность, без лишних абстракций.
Создаем контроллер
Создайте новый spring boot проект и добавьте в следующие зависимости:
implementation("software.amazon.awssdk:aws-sdk-java:2.29.33")
implementation("software.amazon.awssdk:apache-client:2.29.33")
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0")
implementation("org.springframework.boot:spring-boot-starter-web")
Обратите внимание, что вместе с sdk нам так же необходимо добавить http-клиент амазона. Также, мы добавили springdoc-openapi для генерации Swagger UI, через который мы будем тестировать приложение.
Создадим контроллер с клиентом s3Client
для работы с S3. В настоящем приложении клиента стоит создать отдельным бином и вынести все константы в application.properties
, но для простоты я оставлю все в контроллере.
@RestController
@RequestMapping("/photos")
@Tag(name = "Photos")
@ApiResponses(@ApiResponse(responseCode = "200", useReturnTypeSchema = true))
public class PhotoController {
private static final String KEY_ID = "YOUR_KEY_ID";
private static final String SECRET_KEY = "YOUR_SECRET_KEY";
private static final String REGION = "ru-central1";
private static final String S3_ENDPOINT = "https://storage.yandexcloud.net";
private static final String BUCKET = "spring-boot-s3-exmaple";
private final S3Client s3Client;
public PhotoController() {
AwsCredentials credentials = AwsBasicCredentials.create(KEY_ID, SECRET_KEY);
s3Client = S3Client.builder()
.httpClient(ApacheHttpClient.create())
.region(Region.of(REGION))
.endpointOverride(URI.create(S3_ENDPOINT))
.credentialsProvider(StaticCredentialsProvider.create(credentials))
.build();
}
}
Добавим метод для загрузки фотографий в бакет:
@PutMapping(consumes = MULTIPART_FORM_DATA_VALUE)
public String uploadFile(@RequestParam MultipartFile photo) throws IOException {
String key = "photos/" + photo.getOriginalFilename();
PutObjectRequest putObjectRequest = PutObjectRequest.builder()
.bucket(BUCKET)
.key(key)
.contentType(photo.getContentType())
.build();
s3Client.putObject(putObjectRequest, RequestBody.fromBytes(photo.getBytes()));
return key;
}
И для получения их из него:
@GetMapping
public ResponseEntity<byte[]> downloadFile(@RequestParam String key) throws IOException {
GetObjectRequest objectRequest = GetObjectRequest.builder()
.bucket(BUCKET)
.key(key)
.build();
var inputStream = s3Client.getObject(objectRequest);
byte[] data = inputStream.readAllBytes();
var headers = new HttpHeaders();
headers.add(HttpHeaders.CONTENT_DISPOSITION, "inline");
headers.add(HttpHeaders.CONTENT_TYPE, inputStream.response().contentType());
return ResponseEntity.ok()
.headers(headers)
.body(data);
}
Вот как будет выглядеть контроллер полностью:
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.AwsCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.http.apache.ApacheHttpClient;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import java.io.IOException;
import java.net.URI;
import static org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE;
@RestController
@RequestMapping("/photos")
@Tag(name = "Photos")
@ApiResponses(@ApiResponse(responseCode = "200", useReturnTypeSchema = true))
public class PhotoController {
private static final String KEY_ID = "YOR_KEY_ID";
private static final String SECRET_KEY = "YOUR_SECRET_KEY";
private static final String REGION = "ru-central1";
private static final String S3_ENDPOINT = "https://storage.yandexcloud.net";
private static final String BUCKET = "spring-boot-s3-exmaple";
private final S3Client s3Client;
public PhotoController() {
AwsCredentials credentials = AwsBasicCredentials.create(KEY_ID, SECRET_KEY);
s3Client = S3Client.builder()
.httpClient(ApacheHttpClient.create())
.region(Region.of(REGION))
.endpointOverride(URI.create(S3_ENDPOINT))
.credentialsProvider(StaticCredentialsProvider.create(credentials))
.build();
}
@PutMapping(consumes = MULTIPART_FORM_DATA_VALUE)
public String uploadFile(@RequestParam MultipartFile photo) throws IOException {
String key = "photos/" + photo.getOriginalFilename();
PutObjectRequest putObjectRequest = PutObjectRequest.builder()
.bucket(BUCKET)
.key(key)
.contentType(photo.getContentType())
.build();
s3Client.putObject(putObjectRequest, RequestBody.fromBytes(photo.getBytes()));
return key;
}
@GetMapping
public ResponseEntity<byte[]> downloadFile(@RequestParam String key) throws IOException {
GetObjectRequest objectRequest = GetObjectRequest.builder()
.bucket(BUCKET)
.key(key)
.build();
var inputStream = s3Client.getObject(objectRequest);
byte[] data = inputStream.readAllBytes();
var headers = new HttpHeaders();
headers.add(HttpHeaders.CONTENT_DISPOSITION, "inline");
headers.add(HttpHeaders.CONTENT_TYPE, inputStream.response().contentType());
return ResponseEntity.ok()
.headers(headers)
.body(data);
}
}
Тестируем приложение
Переходим на http://localhost:8080/swagger-ui/index.html и загружаем фото
Далее проверяем, что оно появилось в бакете. Как видите, S3 понимает, что если путь содержит /
то нужно создать папку, поэтому наше фото добавилось в папку photos
Пробуем получить фото через наш эндпоинт:
Варианты локального S3 для тестовой среды
Возможно, вам нужен S3 только на проде, а при разработке вы хотели бы использовать локальный сервис. Два таких сервиса - MinIO и LocalStack. Я не буду разворачивать их в этой статье, но коротко сравню:
LocalStack — это инструмент для локальной разработки, который эмулирует облачные сервисы AWS. Используется только для разработки и тестирования. Если вы используете не только S3, то LocalStack вам подойдет.
MinIO — это объектное хранилище с открытым исходным кодом, совместимое с API Amazon S3. Можно использовать в продакшене.
👨💻 Джуниор