«1. Обзор

Когда мы создаем какое-то решение для управления контентом, нам нужно решить две проблемы. Нам нужно место для хранения самих файлов, и нам нужна какая-то база данных для их индексации.

Содержимое файлов можно хранить в самой базе данных, или мы можем хранить содержимое где-то еще и индексировать его в базе данных.

В этой статье мы собираемся проиллюстрировать оба этих метода с помощью базового приложения Image Archive. Мы также внедрим REST API для загрузки и выгрузки.

2. Пример использования

Наше приложение Image Archive позволит нам загружать и скачивать изображения 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 "[email protected]" 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. Отдельное содержимое и местоположение

Пока что мы можем загружать и скачивать изображения в базе данных.

Еще один хороший вариант — загрузить содержимое файла в другое место. Затем мы сохраняем только его расположение в файловой системе в БД.

Для этого нам нужно добавить новое поле в нашу сущность Image:


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-baeldung.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 "[email protected]" http://localhost:8080/file-system/image

1

И затем мы загружаем:

curl -v http://localhost:8080/file-system/image/1 -o image.jpeg

11. Заключение

В этой статье мы узнали, как сохранить информацию о файле в базе данных с содержимым файла либо в той же строке, либо во внешнем расположении.

Мы также создали и протестировали REST API, используя многокомпонентную загрузку, и мы предоставили функцию загрузки, используя Resource, чтобы разрешить потоковую передачу файла вызывающей стороне.

Как всегда, образцы кода можно найти на GitHub.