Uploading and downloading files using Spring Boot REST API
Handling uploading and downloading files are very common jobs in most of the web applications. Spring Boot provides the MultipartFile interface to handle HTTP multi-part requests for uploading files.
In this tutorial, we will learn the following:
- Create a Spring Boot web application that allows file uploads
- Upload single and multiple files using RESTful web services
- Download file using RESTful web service
- List all files uploaded on the server
- A simple Thymeleaf & HTML web interface to upload file(s) from browser
Tools you need to complete this tutorial:
Note: This article uses RESTful web services to upload and download files in Spring Boot. If you are using Thymeleaf and want to upload a file, check out this guide.
We only need spring-boot-starter-web and spring-boot-starter-thymeleaf starter dependencies for our example Spring Boot project. We do not need any extra dependency for file upload. Here is how our build.gradle file looks like:
I used Spring Initializr to generate the above Gradle configuration file. It is an easier and quicker way to create a Spring Boot application.
Before we start the actual work, let's first configure the location on the server where all the uploaded files will be stored. We'll also configure the maximum file size that can be uploaded in a single HTTP multi-part request. Spring Boot automatically enables multipart/form-data requests, so we do not need to do anything.
In above properties file, we have two multi-part settings:
- spring.servlet.multipart.max-file-size is set to 10MB, which means total files size cannot exceed 10MB.
- spring.servlet.multipart.max-request-size sets the maximum multipart/form-data request size to 10MB.
In simple words, we cannot upload files greater than 10MB in size given the above configuration.
In our application.properties file, we define the storage location. Now let's create a POJO class called StorageProperties and annotate it with @ConfigurationProperties to automatically bind the properties defined in application.properties file.
Notice the prefix= "storage" attribute in the above annotation. It instructs @ConfigurationProperties to bind all the properties that start with storage prefix to their corresponding attributes of POJO class when the application is started.
The next step is to enable the ConfigurationProperties feature by adding @EnableConfigurationProperties annotation to our main configuration class.
Let's now create a controller class called FileController for handling uploading and downloading files via RESTful web services. It also defines a route to list all the uploaded files.
As always, our controller class is annotated with @Controller to let the Spring MVC pick it up for routes. Each method is decorated with @GetMapping or @PostMapping to bind the path and the HTTP action with that particular method.
- GET / loads the current list of uploaded files and renders it into a Thymeleaf template called listFiles.html .
- POST /download/
resolves the resource if it exists, and sends it to the browser for download. HttpHeaders.CONTENT_DISPOSITION adds the "Content-Disposition" response header to indicate file attachment. - POST /upload-file & /upload-multiple-files routes handle HTTP multi-part requests and use StorageService for saving files on the server. Both these methods return an object of FileResponse after the upload is finished.
The FileResponse class is used to return a JSON response for RESTful web services.
The FileController class uses the StorageService interface for storing and resolving files in the file system. It is the most important class for handling files in our example. We'll define these classes in the next section.
In production, it's not advised to store the uploaded files in your application file system. You might lose all files if your application server is damaged. It also makes very difficult to move the application from one server to another. Therefore, it is a good practice to use external storage like AWS S3 for storing all the uploaded files. I'll write about this topic in the future.
Finally, it is time to create a storage service called StorageService for our controller to connect with a storage layer (e.g. file system in our case). This task involves several classes. We'll define these classes one-by-one.
The first step is to define an interface called StorageService as shown below:
The above interface declares several abstract methods for initializing, storing, removing and retrieving files. It only lists possible storage operations without their implementation. Now, it is up to you to decide how you want to implement them. In this example, we will use our file system for handling files. It can also be implemented to store the files on any external location.
Let's create a concrete class FileSystemStorageService that implements the StorageService interface.
The above implementation class is taken from Spring Boot official files uploading example with few modifications done by me. The important change I made is the addition of @PostConstruct annotation on the init() method. It guarantees that the init() method is only called once the bean is fully initialized with all the dependencies injected.
The FileSystemStorageService class throws exceptions in case of unexpected scenarios, for example, the file requested by the user might not exist.
The first exception is StorageException which is thrown when we are unable to create the storage directory or the uploaded file is empty etc.
The FileNotFoundException exception is thrown when a file is requested by the user but it does not exist on the server.
Notice the @ResponseStatus(HttpStatus.NOT_FOUND) annotation above. This annotation ensures that Spring Boot responds with a 404 (Not Found) HTTP status instead of 501 (Internal Server Error) when the exception is thrown.
We are almost done with our backend development. Since we created RESTful APIs for uploading and downloading files, we can test them via Postman. Let's run the application by typing the following command in your terminal from the root directory of the project:
Once the application is started, you can access it at http://localhost:8080.



We have tested our RESTful APIs and they are working fine. Now it is time to create a simple front-end interface using HTML & Thymeleaf that lists all the files uploaded so far. It will also allow users to upload files directly from the browser.
The above template has two forms that enable users to upload a single file as well as multiple files. At the bottom, it also shows a list of currently uploaded files on the server. Here is how it looks like:

Source code: Download the complete source code from GitHub available under MIT license.
That's all folks for uploading and downloading files in Spring Boot. We discussed strategies for handling single as well as multiple files via RESTful web services. We tested our REST APIs via Postman to confirm that they are working as expected. Finally, we created the simplest web interface in HTML and Thymeleaf for showing a list of all the uploaded files.
In the end, I really appreciate that you read this article and hope that you'd have learned how to handle files in Spring Boot today. If you have any questions or feedback, please feel free to send me a tweet.
Happy learning Spring Boot 😍
✌️ Like this article? Follow me on Twitter and LinkedIn. You can also subscribe to RSS Feed.
Сохранение файлов в приложение и данных о них на БД

Смотрим контроллер: Из интересного: 9 — Принимаем файл в виде MultipartFile . Можно принимать и в виде массива байтов, но этот вариант мне нравится больше, так как мы с MultipartFile можем вытягивать различные свойства переданного файла. 10 — 14 — оборачиваем наши действия в try catch , чтобы если на более низком уровне возникнет исключение, мы его пробросили выше и отправили 400 ошибку ответом. Далее — уровень сервиса: Смотрим реализацию: 8 — в случае падения IOException, все наши сохранения в БД откатятся. 11 — генерируем ключ, который будет уникальным для файла, когда он будет соохранен (даже если будут сейвиться два файла с одинаковыми именами, путаницы не возникнет). 12 — строим сущность для сохранения в БД. 17 — загоняем сущность с инфой в БД. 18 — сохраняем файл с за хешированным именем. 20 — возвращаем созданую сущность FileInfo , но со сгенерированным id в БД (об этом речь пойдёт чуть ниже) и датой создания. Метод генерации ключа к файлу: Здесь мы хешируем имя + дата создания, что и обеспечит нам уникальность. Интерфейс dao слоя: Его имплементация: 11 — создаём дату которую и сохраним. 12 — 21 — сохраняем сущность, но более сложным путем, с явным созданием объекта PreparedStatement , чтобы можно было вытащить сгенерированный id (он его вытягивает не отдельным запросом, а в виде ответных метаданных). 22 — 26 — достраиваем нашу многострадальную сущность и отдаем наверх (на самом деле он его не достраивает, а создаёт новый объект, заполняя переданные поля и копируя остальные с изначального). Давайте посмотрим, как будут сохраняться наши файлы в FileManager : 1 — принимаем файл в виде массива байтов и отдельно имя, под которым он будет сохранен (наш сгенерированный ключ). 2 — 3 — создаем путь (а в пути прописываем путь плюс наш ключ) и файл по нему. 6 — 7 — создаем поток и пишем туда наши байты (и оборачиваем это все добро в try-finally чтобы быть уверенными, что поток точно закроется). Тем не менее, многие из методов могут нам выкинуть IOException. В таком случае, благодаря прописанной в шапке метода проброске, мы прокинем его в контроллер и отдадим 400 статус. Давайте протестируем все это дело в Postman: Как видим, все отлично, ответ 201, ответный JSON пришел в виде нашей сохраняемой сущности в БД, и если заглянем в наше хранилище: БД: alt=»Сохранение файлов в приложение и данных о них на БД — 6″ width=»650″ />увидим, что у нас появилось новое значение. (=*;*=)
Download

Controller: Реализация: Тут особо интересного ничего нет: метод поиска сущности по id и загрузка файла, разве что 46 — помечаем, что транзакция у нас для чтения. Уровень dao: Имплементация: 4 — поиск по id c использованием jdbcTemplate и RowMapper . 8 — 15 — реализация RowMapper для нашего конкретного случая, для сопоставления данных из БД и полей модели. Идем в FileManager и смотрим, как загружается наш файл: Возвращаем файл в виде объекта Resource , а искать будем по ключу. 3 — создаем Resource по пути + ключ. 4 — 8 — проверяем, что файл по заданному пути не пуст и читаем. Если всё ОК, возвращаем его, а если нет, прокидываем IOException наверх. Проверяем наш метод в Postman: Как видим, он отработал на ОК))
Delete
Тут ничего особенного: также возвращаем 404 в случае неудачи с помощью try-catch . Интерфейс сервиса: Implementation: 1 — также откат изменения данных (удаления) при падении IOException. 5 — удаляем информацию о файле из БД. 6 — удаляем сам файл из нашего “хранилища”. Интерфейс dao: Реализация: Ничего такого — просто delete. Удаление самого файла: Юзаем в Postman:
Смотрим в хранилище:
Пусто 🙂 Теперь в БД:
Видим, что все good)) 

Давайте попробуем написать тест под наш FileManager . Для начала взглянем на структуру тестовой части: mockFile.txt — это файл, с помощью которого мы будем тестить наши операции с file storage. testFileStorage будет заменой нашего хранилища. FileManagerTest : Здесь мы видим задание тестовых данных. Тест сохранения файла: 3 — с помощью тестовой рефлексии меняем нашу константу в сервисе для задания пути сохранения файла. 5 — вызываем проверяемый метод. 7 — 10 — проверяем правильность исполнения сохранения. 11 — удаляем сохраненный файл (мы не должны оставить никаких следов). Тест загрузки файла: 3 — опять же, меняем путь для нашего FileManager . 5 — юзаем проверяемый метод. 7 — 9 — проверяем результат исполнения. Тест удаления файла: 9 — 3 — 4 — задаем путь и создаем файл. 5 — 6 — проверяем его существование. 9 — используем проверяемый метод. 77 — проверяем, что обьекта уже нет. И смотрим, что там у нас по зависимостям: На этом у меня сегодня всё))
Работа с БД в Spring Boot на примере postgresql
Данная статья является продолжением Spring Boot Restful Service, где была бы раскрыта тема работы с БД в Spring Boot. Давайте рассмотрим эту тему подробнее на примере СУБД postgresql, а в качестве основы возьмём проект, который мы делали в той статье.
Напомню, что проект представляет из себя простой restful-service, который принимает GET-запрос по HTTP и возвращает профиль пользователя по его id. Сам профиль содержит кроме id также имя, фамилию и возраст. Поэтому создадим таблицу profiles в базе данных.
CREATE TABLE public.profiles
(
id serial ,
first_name character varying ( 50 ) NOT NULL ,
last_name character varying ( 50 ) NOT NULL ,
age integer NOT NULL ,
CONSTRAINT profile_id_pk PRIMARY KEY (id)
);
insert into profiles (first_name, last_name, age) values ( 'Иван' , 'Петров' , 23 );
Для поля id можно использовать тип serial. Он представляет собой целое число, которое инкрементируется (увеличивается на 1) автоматически при вставке новой записи в таблицу.
При работе с БД нужно использовать пул подключений к БД, чтобы не создавать их заново при каждом новом sql-запросе, иначе выполнение запроса будет занимать продолжительное время. В качестве пула предлагаю использовать один из наиболее производительных в настоящий момент HikariCP. Также нам нужна поддержка работы с БД со стороны Spring Boot и драйвер для работы с СУБД postgresql. Добавим все эти зависимости в наш проект.
При инициализации пула требуется указать параметры подключения к БД, такие как логин, пароль и т.п. Поскольку данные параметры являются изменяемыми и доступ к ним должен быть ограничен, вынесем их в отдельный текстовый файл и назовём его application.config. Пример содержимого такого файла:
Чтобы Spring Boot увидел данные настройки, абсолютный путь к файлу следует указывать через параметр командной строки —spring.config.location=/путь/до/файла/application.config. Если запускаете проект при помощи Idea, указывайте данный параметр в строке Program Arguments.
Для удобства работы с этими настройками создадим класс ConnectionSettings, в который Spring автоматически подставит все настройки с префиксом «mainPool» в соответствующие поля, благодаря аннотации @ConfigurationProperties. Вообще это очень хорошая практика — группировать связанные настройки через префикс.
@Component
@ConfigurationProperties (prefix = "mainPool" )
public class ConnectionSettings <
private static int DEFAULT_MAX_POOL_SIZE = 5 ;
private String jdbcDriver;
private String jdbcString;
private String jdbcUser;
private String jdbcPassword;
private int jdbcMaxPoolSize = DEFAULT_MAX_POOL_SIZE ;
>
Для каждого из этих полей нужно создать геттер и сеттер, но я для краткости не стал их здесь приводить.
Наш пул подключений максимум может хранить до 5 объектов, однако это значение может быть переопределено через файл настроек.
Теперь создадим ещё один компонент, в котором будем инициализировать сам пул.
@Configuration
public class DatabaseConfig <
private final ConnectionSettings connectionSettings;
@Autowired
public DatabaseConfig(ConnectionSettings connectionSettings) <
this .connectionSettings = connectionSettings;
>
@Bean
public DataSource dataSource() <
HikariConfig hikariConfig = new HikariConfig();
hikariConfig.setDriverClassName(connectionSettings.getJdbcDriver());
hikariConfig.setJdbcUrl(connectionSettings.getJdbcString());
hikariConfig.setUsername(connectionSettings.getJdbcUser());
hikariConfig.setPassword(connectionSettings.getJdbcPassword());
hikariConfig.setMaximumPoolSize(connectionSettings.getJdbcMaxPoolSize());
hikariConfig.setPoolName( "main" );
return new HikariDataSource(hikariConfig);
>
>
Аннотация @Bean позволяет нам вручную создавать бины, которые Spring потом сможет подставлять в другие компоненты.
Для работы с БД принято выделять отдельной слой dao (data access object — объект доступа к данным). Как и для сервиса из предыдущей статьи, здесь будет удобно выделить интерфейс, который будет выглядеть так:
public interface ProfileDao <
<>
Optional<Profile> getProfileById( int id);
>
Обратите внимание, что при поиске по id здесь мы будем возвращать типизированный Optional. То есть объект может быть в базе, а может и не быть. И в зависимости от кейса это может трактоваться как ошибка, так и нормальное поведение. Решение о том, ошибка это или нет, будет принимать сервисный слой, который мы рассмотрим далее.
Реализация класса Profile предельно проста. Его единственное назначение — это отображать поля таблицы в поля класса на Java. Для краткости не буду приводить код всего класса, ибо он достаточно прост.
public class Profile <
<>
private int id;
private String firstName;
private String lastName;
private int age;
public int getId() <
return id;
>
public void setId( int id) <
this .id = id;
>
// далее идут остальные get- и set-методы.
@Repository
public class ProfileDaoImpl implements ProfileDao <
private static final String SQL_GET_PROFILE_BY_ID =
"select id, first_name, last_name, age from profiles where >;
private final ProfileMapper profileMapper;
private final NamedParameterJdbcTemplate jdbcTemplate;
@Autowired
public ProfileDaoImpl(
ProfileMapper profileMapper,
NamedParameterJdbcTemplate jdbcTemplate
) <
this .profileMapper = profileMapper;
this .jdbcTemplate = jdbcTemplate;
>
@Override
public Optional<Profile> getProfileById( int id) <
MapSqlParameterSource params = new MapSqlParameterSource();
params.addValue( "id" , id);
try <
return Optional.ofNullable(
jdbcTemplate.queryForObject(
SQL_GET_PROFILE_BY_ID ,
params,
profileMapper
)
);
> catch (EmptyResultDataAccessException e) <
return Optional.empty();
>
>
>
Обратите внимание, что ВСЕ dao-компоненты снабжаются аннотацией @Repository, которая является частным случаем @Component. Она обеспечивает маппинг ошибок, специфичных для СУБД, в стандартные исключения JDBC.
Сам SQL-запрос для выборки профиля пользователя здесь вынесен в качестве константы в начало класса. Для подстановки целевого id используется именованный параметр с двоеточием в начале, а не простая конкатенация строки и числа. Это позволяет нам сделать запрос более устойчивым к хакерским атакам типа sql injection с одной стороны и более производительным с другой, т.к. СУБД сможет закешировать шаблон данного запроса.
NamedParameterJdbcTemplate — стандартный компонент, предоставляющий методы для взаимодействия с БД. Как видно из названия, он поддерживает именованные параметры. ProfileMapper преобразует данные, полученные из БД в объект Profile. То есть он хранит в себе логику маппинга полей таблицы на поля класса. Более подробно мы рассмотрим его чуть ниже.
Реализация нашего целевого метода getProfileById() предельно проста. Сначала подставляем требуемый id в sql-запрос через именованный параметр благодаря классу MapSqlParameterSource. Затем вызываем метод queryForObject, передавая ему сам sql-запрос, именованные параметры и маппер полей таблицы. В качестве результата получаем объект Profile или исключение EmptyResultDataAccessException если объект не найден. Исходя из того, что id является первичным ключом в таблице и его значение уникально, мы можем здесь использовать метод queryForObject(). Если бы искали не по уникальному значению, то использовали бы метод query(), который возвращает список объектов. Результат оборачиваем в Optional.
Сам ProfileMapper не хранит внутреннего состояния и всего лишь реализует интерфейс RowMapper, типизированный нашим объектом Profile.
@Component
public class ProfileMapper implements RowMapper<Profile> <
@Override
public Profile mapRow(ResultSet rs, int rowNum) throws SQLException <
Profile profile = new Profile();
profile.setId(rs.getInt( "id" ));
profile.setFirstName(rs.getString( "first_name" ));
profile.setLastName(rs.getString( "last_name" ));
profile.setAge(rs.getInt( "age" ));
return profile;
>
>
На вход он получает ResultSet, представляющий собой результат выборки. Из этого ResultSet мы извлекаем значения полей благодаря методам getInt() и getString() по имени колонки в таблице.
Теперь осталось только внедрить наш ProfileDao в сервисный слой. В предыдущей статье мы уже создавали реализацию сервисного слоя ProfileServiceMock, которая является заглушкой и на самом деле ни в какую базу не ходит. Сейчас мы создадим другую реализацию того же сервиса:
@Primary
@Service
public class ProfileServiceImpl implements ProfileService <
private final ProfileDao profileDao;
@Autowired
public ProfileServiceImpl(ProfileDao profileDao) <
this .profileDao = profileDao;
>
@Override
public Profile getProfile( int personId) <
return profileDao.getProfileById(personId)
.orElseThrow(() -> new ProfileNotFoundException(personId));
>
>
Обратите внимание на аннотацию @Primary. Если её не указывать, то спринг не сможет заинжектить в ProfileController нужную нам реализацию сервиса, т.к. по факту у нас их две. Чтобы указать, что по умолчанию нам нужна именно эта реализация, мы и используем данную аннотацию.
Как я уже говорил, именно сервисный слой находится в контексте выполнения запроса и может правильно трактовать пустой результат из dao. В данном случае это ошибка и здесь Optional предоставляет очень удобный метод orElseThrow(), в который мы передаём наше исключение через лямбда-выражение.
На этом примере с двумя реализациями одного интерфейса хорошо виден принцип модульности, которого стоит придерживаться при разработке любых приложений на Spring.
ProfileService, в свою очередь, вызывается из контроллера. Таким образом, вырисовывается типичная трёхслойная архитектура: контроллер (с аннотацией @Controller) -> сервис (@Service) -> dao (@Repository). Контроллер отвечает за маппинг входящих http-запросов, сервисный слой реализует бизнес-логику, а dao работает непосредственно с БД.
Теперь если вы запустите приложение и выполните GET-запрос по адресу http://localhost:8080/profile/1, то получите профиль с >
Если же выполнить запрос с другим id, то наш ErrorController корректно обработает исключение ProfileNotFoundException и выдаст пользователю json с описанием ошибки:
Итоги
В результате мы добавили в наше приложение слой dao, который ходит в БД, а также создали новую реализацию сервисного слоя, который вместо заглушки теперь использует это dao.