Файлы, async, локализация и сессии¶
20. Работа с файлами¶
20.1. Загрузка файлов¶
Spring MVC обрабатывает multipart/form-data через MultipartResolver.
Boot автоконфигурирует StandardServletMultipartResolver.
Настройки (application.properties):
spring.servlet.multipart.enabled=true
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=20MB
20.2. MultipartFile¶
@PostMapping("/upload")
public ResponseEntity<String> upload(@RequestParam("file") MultipartFile file) {
if (file.isEmpty()) {
return ResponseEntity.badRequest().body("Empty file");
}
String filename = StringUtils.cleanPath(file.getOriginalFilename());
Path targetPath = uploadDir.resolve(filename);
Files.copy(file.getInputStream(), targetPath, StandardCopyOption.REPLACE_EXISTING);
return ResponseEntity.ok("Uploaded: " + filename);
}
В production нельзя доверять getOriginalFilename() напрямую: проверяйте path traversal (..), whitelist расширений/media types, лимиты размера и права доступа. Часто безопаснее генерировать свое имя файла и хранить оригинальное имя отдельно как metadata.
Несколько файлов:
@PostMapping("/upload-multiple")
public List<String> uploadMultiple(@RequestParam("files") List<MultipartFile> files) {
return files.stream()
.map(f -> storageService.store(f))
.toList();
}
20.3. multipart/form-data¶
HTML-форма для загрузки файла:
<form th:action="@{/upload}" method="post" enctype="multipart/form-data">
<input type="file" name="file"/>
<button type="submit">Upload</button>
</form>
enctype="multipart/form-data" — обязательно!
20.4. Отдача файлов клиенту¶
@GetMapping("/files/{filename:.+}")
public ResponseEntity<Resource> download(@PathVariable String filename) throws IOException {
Path filePath = uploadDir.resolve(filename).normalize();
Resource resource = new UrlResource(filePath.toUri());
if (!resource.exists()) {
return ResponseEntity.notFound().build();
}
String contentType = Files.probeContentType(filePath);
if (contentType == null) {
contentType = MediaType.APPLICATION_OCTET_STREAM_VALUE;
}
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType(contentType))
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + resource.getFilename() + "\"")
.body(resource);
}
Для скачивания пользовательских файлов обязательно проверяйте, что normalize() не вывел путь за пределы upload-директории:
Path root = uploadDir.toAbsolutePath().normalize();
Path filePath = root.resolve(filename).normalize();
if (!filePath.startsWith(root)) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid filename");
}
20.5. Resource¶
Resource — Spring-абстракция над файлами и потоками данных.
Реализации:
- FileSystemResource — файл на диске,
- ClassPathResource — файл из classpath,
- UrlResource — файл по URL,
- ByteArrayResource — данные в памяти.
20.6. Content-Disposition¶
Заголовок Content-Disposition контролирует поведение браузера:
// Скачать файл (браузер предложит сохранить):
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"report.pdf\"")
// Открыть в браузере (inline):
.header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"image.png\"")
21. Redirect и Forward¶
21.1. Что такое redirect¶
Redirect — сервер отвечает 3xx с заголовком Location. Браузер делает новый HTTP-запрос по новому URL.
- URL в браузере меняется.
- Создается новый request/response.
- Данные из первого request не доступны (только flash attributes).
- Два HTTP-запроса.
21.2. Что такое forward¶
Forward — сервер внутри себя передает обработку другому handler'у.
- URL в браузере не меняется.
- Тот же request/response.
- Данные request доступны в target.
- Один HTTP-запрос.
21.3. Разница между redirect и forward¶
| Redirect | Forward | |
|---|---|---|
| HTTP-запросов | 2 | 1 |
| URL меняется | да | нет |
| Request-объект | новый | тот же |
| Данные из оригинального request | нет | да |
| Используется для | POST -> redirect -> GET | внутренняя передача управления |
21.4. PRG pattern (Post-Redirect-Get)¶
Паттерн для предотвращения двойной отправки формы:
1. POST /register — обработка формы
2. redirect:/login — 302 Found, Location: /login
3. GET /login — браузер делает новый запрос
Без redirect: при обновлении страницы браузер предлагает повторить POST. С redirect: обновление страницы повторяет GET (безопасно).
@PostMapping("/register")
public String register(@Valid @ModelAttribute RegisterForm form, BindingResult result) {
if (result.hasErrors()) return "register";
userService.register(form);
return "redirect:/login?registered"; // PRG
}
21.5. Когда использовать каждый вариант¶
- Redirect: после успешного POST (PRG), перенаправление на другую страницу, смена URL.
- Forward: когда нужно передать управление другому контроллеру с тем же request (редко).
22. Асинхронная обработка в Spring MVC¶
22.1. Асинхронность в servlet stack¶
Spring MVC работает на Servlet API, где один запрос = один поток. Servlet 3.0+ поддерживает async processing: поток можно освободить, пока идет долгая операция, а затем вернуть результат.
Важно: Spring MVC async освобождает servlet-поток, но не делает блокирующий код неблокирующим. Если внутри Callable выполняется JDBC-запрос или blocking HTTP call, он всё равно занимает поток executor'а. Для настоящего non-blocking I/O нужен WebFlux/reactive stack или неблокирующие клиенты.
Для production обычно настраивают:
- AsyncTaskExecutor,
- timeout,
- propagation SecurityContext/MDC,
- обработку timeout/error callbacks.
22.2. Callable¶
Контроллер возвращает Callable<T>. Spring выполняет его в отдельном потоке (из TaskExecutor), освобождая servlet-поток.
@GetMapping("/heavy")
public Callable<String> heavyOperation() {
return () -> {
Thread.sleep(5000); // симуляция долгой работы
return "result"; // выполняется в другом потоке
};
}
22.3. DeferredResult¶
Результат устанавливается извне (из другого потока, по событию, из MQ):
@GetMapping("/subscribe")
public DeferredResult<String> subscribe() {
DeferredResult<String> result = new DeferredResult<>(10_000L); // timeout 10s
// Кто-то установит результат позже:
eventBus.subscribe(event -> result.setResult(event.getData()));
result.onTimeout(() -> result.setResult("timeout"));
return result;
}
22.4. WebAsyncTask¶
Как Callable, но с возможностью задать timeout и executor:
@GetMapping("/task")
public WebAsyncTask<String> asyncTask() {
return new WebAsyncTask<>(5000L, () -> heavyService.process());
}
22.5. StreamingResponseBody¶
Потоковая отдача больших данных без буферизации:
@GetMapping("/export")
public StreamingResponseBody export() {
return outputStream -> {
try (PrintWriter writer = new PrintWriter(outputStream)) {
for (int i = 0; i < 1_000_000; i++) {
writer.println("line " + i);
writer.flush();
}
}
};
}
22.6. SseEmitter¶
Server-Sent Events — сервер push'ит события клиенту через постоянное HTTP-соединение:
@GetMapping(value = "/events", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter streamEvents() {
SseEmitter emitter = new SseEmitter(30_000L);
executor.submit(() -> {
try {
for (int i = 0; i < 10; i++) {
emitter.send(SseEmitter.event()
.name("update")
.data("event " + i));
Thread.sleep(1000);
}
emitter.complete();
} catch (Exception e) {
emitter.completeWithError(e);
}
});
return emitter;
}
23. Статические ресурсы¶
23.1. Что считается static content¶
CSS, JavaScript, изображения, шрифты, favicon.ico — файлы, которые отдаются как есть, без обработки.
23.2. CSS / JS / images¶
Spring Boot по умолчанию раздает статику из:
- classpath:/static/
- classpath:/public/
- classpath:/resources/
- classpath:/META-INF/resources/
- root of ServletContext при war deployment
src/main/resources/static/
css/
main.css -> /css/main.css
js/
app.js -> /js/app.js
images/
logo.png -> /images/logo.png
23.3. Настройка resource handling¶
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/files/**")
.addResourceLocations("file:/opt/uploads/")
.setCacheControl(CacheControl.maxAge(7, TimeUnit.DAYS));
}
}
23.4. Раздача статических файлов¶
# Изменить базовый путь (по умолчанию /**)
spring.mvc.static-path-pattern=/static/**
# Изменить источник:
spring.web.resources.static-locations=classpath:/static/,classpath:/public/
В Thymeleaf:
<link rel="stylesheet" th:href="@{/css/main.css}"/>
<script th:src="@{/js/app.js}"></script>
<img th:src="@{/images/logo.png}" alt="Logo"/>
24. Локализация и интернационализация¶
24.1. Locale¶
Locale — объект, представляющий язык и регион пользователя (например, ru_RU, en_US).
Spring MVC определяет Locale через LocaleResolver.
24.2. LocaleResolver¶
LocaleResolver — интерфейс, определяющий текущий Locale запроса.
Реализации:
- AcceptHeaderLocaleResolver — из Accept-Language header (дефолт).
- CookieLocaleResolver — из cookie.
- SessionLocaleResolver — из HTTP-сессии.
- FixedLocaleResolver — фиксированный Locale.
@Bean
public LocaleResolver localeResolver() {
SessionLocaleResolver resolver = new SessionLocaleResolver();
resolver.setDefaultLocale(Locale.ENGLISH);
return resolver;
}
// Смена языка:
@Bean
public LocaleChangeInterceptor localeChangeInterceptor() {
LocaleChangeInterceptor interceptor = new LocaleChangeInterceptor();
interceptor.setParamName("lang"); // GET /page?lang=ru
return interceptor;
}
24.3. MessageSource¶
MessageSource — интерфейс для получения локализованных сообщений.
# messages.properties (дефолт):
user.greeting=Hello, {0}!
# messages_ru.properties:
user.greeting=Привет, {0}!
@Autowired
private MessageSource messageSource;
public String greet(String name, Locale locale) {
return messageSource.getMessage("user.greeting", new Object[]{name}, locale);
}
Настройка:
24.4. i18n в Spring MVC¶
@GetMapping("/greeting")
public String greeting(Locale locale, Model model) {
String msg = messageSource.getMessage("greeting", null, locale);
model.addAttribute("message", msg);
return "greeting";
}
24.5. Локализованные сообщения в представлениях¶
В Thymeleaf:
<!-- Простое сообщение: -->
<p th:text="#{user.greeting}"></p>
<!-- С параметром: -->
<p th:text="#{user.greeting(${user.name})}"></p>
<!-- Сообщения ошибок валидации тоже из MessageSource: -->
<span th:errors="*{email}"></span>
25. Session и Flash Attributes¶
25.1. Работа с HttpSession¶
@GetMapping("/cart/add/{id}")
public String addToCart(@PathVariable Long id, HttpSession session) {
Cart cart = (Cart) session.getAttribute("cart");
if (cart == null) cart = new Cart();
cart.add(productService.findById(id));
session.setAttribute("cart", cart);
return "redirect:/cart";
}
25.2. Хранение состояния между запросами¶
HTTP — stateless протокол. Для хранения состояния между запросами:
- Session — хранится на сервере, ID в cookie (JSESSIONID).
- Cookie — хранится в браузере.
- JWT/Token — в header или cookie, stateless.
- Flash attributes — одноразовые данные для redirect.
25.3. @SessionAttributes¶
Хранит атрибуты модели в сессии для использования между запросами в рамках одного контроллера:
@Controller
@SessionAttributes("wizard") // сохраняем объект "wizard" в сессии
public class WizardController {
@GetMapping("/step1")
public String step1(Model model) {
model.addAttribute("wizard", new WizardData()); // создаем и кладем в сессию
return "wizard/step1";
}
@PostMapping("/step2")
public String step2(@ModelAttribute WizardData wizard) { // берем из сессии
wizard.setStep2Data(...);
return "wizard/step2";
}
@PostMapping("/finish")
public String finish(@ModelAttribute WizardData wizard, SessionStatus status) {
wizardService.complete(wizard);
status.setComplete(); // убираем из сессии
return "redirect:/done";
}
}
25.4. Flash attributes¶
Flash attributes — данные, которые живут ровно один redirect (один запрос после redirect).
Используются для передачи сообщений об успехе/ошибке после POST-redirect-GET:
@PostMapping("/order/create")
public String createOrder(RedirectAttributes redirectAttributes) {
orderService.create(...);
redirectAttributes.addFlashAttribute("successMessage", "Order created!");
return "redirect:/orders";
}
@GetMapping("/orders")
public String orders(Model model) {
// "successMessage" автоматически появится в модели, если был установлен через flash
return "orders";
}
В Thymeleaf:
<div th:if="${successMessage}" class="alert alert-success">
<span th:text="${successMessage}"></span>
</div>
25.5. RedirectAttributes¶
Интерфейс для передачи данных через redirect:
// Flash attribute (в сессии, исчезает после одного запроса):
redirectAttributes.addFlashAttribute("message", "Saved!");
// Query param (добавляется к URL):
redirectAttributes.addAttribute("id", createdId); // redirect:/items?id=42
Аннотации и типы раздела:
| Аннотация / тип | Где используется | Что делает |
|---|---|---|
@SessionAttributes |
класс @Controller |
Сохраняет выбранные model attributes в HTTP session в рамках controller workflow. |
@ModelAttribute |
метод/параметр | Создает/читает model attribute; вместе с @SessionAttributes может доставать объект из session. |
SessionStatus |
параметр метода | Позволяет вызвать setComplete() и очистить session attributes текущего controller workflow. |
RedirectAttributes |
параметр метода | Добавляет query params (addAttribute) или flash attributes (addFlashAttribute) для redirect. |
HttpSession |
параметр метода | Низкоуровневый прямой доступ к server-side session. |
@SessionAttributes не стоит использовать как общий storage пользователя. Это инструмент для небольших MVC-flow, например wizard form. Для security/session management используйте Spring Security и явные сервисы.