1. Обзор
Когда мы создаем какое-то решение для управления контентом, нам нужно решить две проблемы. Нам нужно место для хранения самих файлов, и нам нужна какая-то база данных для их индексации.
Содержимое файлов можно хранить в самой базе данных, или мы можем хранить содержимое где-то еще и индексировать его в базе данных.
В этой статье мы собираемся проиллюстрировать оба этих метода с помощью базового приложения Image Archive. Мы также внедрим REST API для загрузки и выгрузки.
2. Вариант использования
Наше приложение архива изображений позволит нам загружать и скачивать изображения в формате JPEG .
Когда мы загружаем изображение, приложение создаст для него уникальный идентификатор. Затем мы можем использовать этот идентификатор для его загрузки.
Мы будем использовать реляционную базу данных с Spring Data JPA и Hibernate .
3. Хранение базы данных
Начнем с нашей базы данных.
3.1. Объект изображения
Во-первых, давайте создадим нашу сущность изображения :
@Entity
class Image {
@Id
@GeneratedValue
Long id;
@Lob
byte[] content;
String name;
// Getters and Setters
}
Поле id
снабжено аннотацией @GeneratedValue
. Это означает, что база данных создаст уникальный идентификатор для каждой добавляемой нами записи. Индексируя изображения с этими значениями, нам не нужно беспокоиться о нескольких загрузках одного и того же изображения, конфликтующих друг с другом.
Во-вторых, у нас есть аннотация Hibernate @Lob
. Так мы сообщаем JPA о своем намерении сохранить потенциально большой двоичный файл .
3.2. Репозиторий изображений
Далее нам нужен репозиторий для подключения к базе данных .
Мы будем использовать Spring JpaRepository
:
@Repository
interface ImageDbRepository extends JpaRepository<Image, Long> {}
Теперь мы готовы сохранить наши изображения. Нам просто нужен способ загрузить их в наше приложение.
4. Контроллер REST
Мы будем использовать MultipartFile
для загрузки наших изображений. Загрузка вернет imageId
, который мы можем использовать для загрузки изображения позже.
4.1. Загрузка изображения
Давайте начнем с создания нашего ImageController
для поддержки загрузки:
@RestController
class ImageController {
@Autowired
ImageDbRepository imageDbRepository;
@PostMapping
Long uploadImage(@RequestParam MultipartFile multipartImage) throws Exception {
Image dbImage = new Image();
dbImage.setName(multipartImage.getName());
dbImage.setContent(multipartImage.getBytes());
return imageDbRepository.save(dbImage)
.getId();
}
}
Объект MultipartFile
содержит содержимое и исходное имя файла. Мы используем это для создания нашего объекта изображения
для хранения в базе данных.
Этот контроллер возвращает сгенерированный идентификатор в качестве тела своего ответа.
4.2. Загрузка изображения
Теперь добавим маршрут загрузки :
@GetMapping(value = "/image/{imageId}", produces = MediaType.IMAGE_JPEG_VALUE)
Resource downloadImage(@PathVariable Long imageId) {
byte[] image = imageRepository.findById(imageId)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND))
.getContent();
return new ByteArrayResource(image);
}
Переменная пути imageId
содержит идентификатор, сгенерированный при загрузке. Если указан недопустимый идентификатор, мы используем ResponseStatusException
для возврата кода ответа HTTP 404 (не найдено). В противном случае мы упаковываем сохраненные байты файла в ByteArrayResource
, что позволяет их загружать.
5. Тест архива образов базы данных
Теперь мы готовы протестировать наш архив изображений.
Сначала создадим наше приложение:
mvn package
Во-вторых, давайте начнем:
java -jar target/image-archive-0.0.1-SNAPSHOT.jar
5.1. Тест загрузки изображения
После запуска нашего приложения мы будем использовать инструмент командной строки curl
для загрузки нашего изображения :
curl -H "Content-Type: multipart/form-data" \
-F "image=@foreach.jpeg" http://localhost:8080/image
Поскольку ответом службы загрузки является imageId
,
и это наш первый запрос, вывод будет таким:
1
5.2. Тест загрузки изображения
Затем мы можем загрузить наше изображение:
curl -v http://localhost:8080/image/1 -o image.jpeg
Параметр -o image.jpeg
создаст файл с именем image.jpeg
и сохранит в нем содержимое ответа:
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET /image/1 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 200
< Accept-Ranges: bytes
< Content-Type: image/jpeg
< Content-Length: 9291
Мы получили HTTP/1.1 200, что означает, что наша загрузка прошла успешно.
Мы также можем попробовать загрузить изображение в браузере, нажав http://localhost:8080/image/1
.
6. Разделите контент и местоположение
Пока что мы можем загружать и скачивать изображения в базе данных.
Еще один хороший вариант — загрузить содержимое файла в другое место. Затем мы сохраняем только его расположение
в файловой системе в БД `` .
Для этого нам нужно добавить новое поле в наш объект изображения :
String location;
Это будет содержать логический путь к файлу в каком-то внешнем хранилище. В нашем случае это будет путь в файловой системе нашего сервера.
Однако мы можем в равной степени применить эту идею к разным магазинам. Например, мы могли бы использовать облачное хранилище — Google Cloud Storage или Amazon S3 . Местоположение также может использовать формат URI, например, s3://somebucket/path/to/file
.
Наша служба загрузки вместо того, чтобы записывать байты файла в базу данных, сохранит файл в соответствующей службе — в данном случае в файловой системе — и затем поместит местоположение файла в базу данных.
7. Хранилище файловой системы
Давайте добавим в наше решение возможность хранить изображения в файловой системе .
7.1. Сохранение в файловой системе
Во-первых, нам нужно сохранить наши изображения в файловой системе:
@Repository
class FileSystemRepository {
String RESOURCES_DIR = FileSystemRepository.class.getResource("/")
.getPath();
String save(byte[] content, String imageName) throws Exception {
Path newFile = Paths.get(RESOURCES_DIR + new Date().getTime() + "-" + imageName);
Files.createDirectories(newFile.getParent());
Files.write(newFile, content);
return newFile.toAbsolutePath()
.toString();
}
}
Одно важное замечание: нам нужно убедиться, что каждое из наших изображений имеет уникальное местоположение
, определенное на стороне сервера во время загрузки . В противном случае наши загрузки могут перезаписать друг друга.
Это же правило применимо к любому облачному хранилищу, где мы должны создавать уникальные ключи. В этом примере мы добавим текущую дату в формате миллисекунд к имени изображения:
/workspace/archive-achive/target/classes/1602949218879-foreach.jpeg
7.2. Получение из файловой системы
Теперь давайте реализуем код для извлечения нашего изображения из файловой системы:
FileSystemResource findInFileSystem(String location) {
try {
return new FileSystemResource(Paths.get(location));
} catch (Exception e) {
// Handle access or file not found problems.
throw new RuntimeException();
}
}
Здесь мы ищем изображение, используя его местоположение
. Затем мы возвращаем FileSystemResource
.
Кроме того, мы перехватываем любые исключения, которые могут возникнуть при чтении нашего файла. Мы также можем генерировать исключения с определенными HTTP-статусами .
7.3. Потоковая передача данных и ресурс Spring
Наш метод findInFileSystem
возвращает FileSystemResource ,
реализацию интерфейса ресурсов
Spring . ``
Он начнет читать наш файл только тогда, когда мы его используем . В нашем случае это будет при отправке клиенту через RestController
. Кроме того, он будет передавать содержимое файла из файловой системы пользователю, избавляя нас от загрузки всех байтов в память .
Этот подход является хорошим общим решением для потоковой передачи файлов клиенту. Если мы используем облачное хранилище вместо файловой системы, мы можем заменить FileSystemResource
реализацией другого ресурса, например InputStreamResource
или ByteArrayResource
.
8. Соединение содержимого и местоположения файла
Теперь, когда у нас есть FileSystemRepository,
нам нужно связать его с нашим ImageDbRepository.
8.1. Сохранение в базе данных и файловой системе
Давайте создадим FileLocationService
, начиная с нашего потока сохранения:
@Service
class FileLocationService {
@Autowired
FileSystemRepository fileSystemRepository;
@Autowired
ImageDbRepository imageDbRepository;
Long save(byte[] bytes, String imageName) throws Exception {
String location = fileSystemRepository.save(bytes, imageName);
return imageDbRepository.save(new Image(imageName, location))
.getId();
}
}
Сначала мы сохраняем изображение в файловой системе . Затем мы сохраняем запись, содержащую его местоположение
в базе данных .
8.2. Получение из базы данных и файловой системы
Теперь давайте создадим метод для поиска нашего изображения по его идентификатору
:
FileSystemResource find(Long imageId) {
Image image = imageDbRepository.findById(imageId)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
return fileSystemRepository.findInFileSystem(image.getLocation());
}
Сначала ищем наше изображение в базе данных . Затем мы получаем его местоположение и получаем его из файловой системы .
Если мы не находим imageId
в базе данных, мы используем ResponseStatusException для возврата ответа HTTP Not Found .
9. Загрузка и загрузка файловой системы
Наконец, давайте создадим FileSystemImageController:
@RestController
@RequestMapping("file-system")
class FileSystemImageController {
@Autowired
FileLocationService fileLocationService;
@PostMapping("/image")
Long uploadImage(@RequestParam MultipartFile image) throws Exception {
return fileLocationService.save(image.getBytes(), image.getOriginalFilename());
}
@GetMapping(value = "/image/{imageId}", produces = MediaType.IMAGE_JPEG_VALUE)
FileSystemResource downloadImage(@PathVariable Long imageId) throws Exception {
return fileLocationService.find(imageId);
}
}
Во- первых, мы сделали наш новый путь начинающимся с «/ file-system
».
Затем мы создали маршрут загрузки, аналогичный маршруту в нашем ImageController
, но без объекта dbImage
.
Наконец, у нас есть маршрут загрузки, который использует FileLocationService
для поиска изображения и возвращает FileSystemResource
в качестве ответа HTTP.
10. Тест архива образов файловой системы
Теперь мы можем протестировать нашу версию файловой системы так же, как мы это делали с версией нашей базы данных, хотя пути теперь начинаются с « file-system
»:
curl -H "Content-Type: multipart/form-data" \
-F "image=@foreach.jpeg" http://localhost:8080/file-system/image
1
И далее скачиваем:
curl -v http://localhost:8080/file-system/image/1 -o image.jpeg
11. Заключение
В этой статье мы узнали, как сохранить информацию о файле в базе данных с содержимым файла либо в той же строке, либо во внешнем расположении.
Мы также создали и протестировали REST API, используя многокомпонентную загрузку, и предоставили функцию загрузки с помощью Resource
, чтобы разрешить потоковую передачу файла вызывающей стороне.
Как всегда, образцы кода можно найти на GitHub .