← Назад к вопросам
Как отдавать PDF файл пользователю
1.2 Junior🔥 161 комментариев
#REST API и микросервисы
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
# Как отдавать PDF файл пользователю
Этот вопрос касается REST API и работы с бинарными данными. Есть несколько подходов.
1. Простой способ: отправить файл напрямую
@RestController
@RequestMapping("/api/documents")
public class DocumentController {
@GetMapping("/{id}/pdf")
public ResponseEntity<byte[]> downloadPdf(@PathVariable Long id) {
// Получить PDF из хранилища
byte[] pdfContent = documentService.getPdfContent(id);
// Вернуть с правильными headers
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_PDF)
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=document.pdf")
.body(pdfContent);
}
}
Здесь:
.contentType(MediaType.APPLICATION_PDF)— говорит браузеру, что это PDFContent-Disposition: attachment— заставляет скачиваниеfilename=— имя файла при скачивании
2. Использование Resource (более оптимально)
@GetMapping("/{id}/pdf")
public ResponseEntity<Resource> downloadPdf(@PathVariable Long id) throws IOException {
// Способ 1: из файловой системы
Path filePath = Paths.get("/storage/documents/" + id + ".pdf");
Resource resource = new FileSystemResource(filePath);
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_PDF)
.contentLength(Files.size(filePath))
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + filePath.getFileName() + "\"")
.body(resource);
}
Или из classpath:
Resource resource = new ClassPathResource("templates/report.pdf");
3. Генерация PDF на лету (iText 7)
// Зависимость
// <dependency>
// <groupId>com.itextpdf</groupId>
// <artifactId>itext7-core</artifactId>
// <version>7.2.0</version>
// </dependency>
@Service
public class PdfGeneratorService {
public byte[] generateInvoicePdf(Invoice invoice) throws IOException {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
try (PdfWriter writer = new PdfWriter(outputStream);
PdfDocument pdf = new PdfDocument(writer);
Document document = new Document(pdf)) {
// Заголовок
document.add(new Paragraph("СЧЁТ ")
.setFontSize(20)
.setBold());
// Информация о счёте
document.add(new Paragraph("Номер: " + invoice.getNumber()));
document.add(new Paragraph("Дата: " + invoice.getDate()));
document.add(new Paragraph("Сумма: " + invoice.getAmount() + " RUB"));
// Таблица с товарами
Table table = new Table(UnitValue.createPercentArray(
new float[]{1, 1, 1, 1}));
table.addCell("Товар");
table.addCell("Количество");
table.addCell("Цена");
table.addCell("Сумма");
for (InvoiceItem item : invoice.getItems()) {
table.addCell(item.getName());
table.addCell(String.valueOf(item.getQuantity()));
table.addCell(String.valueOf(item.getPrice()));
table.addCell(String.valueOf(item.getTotal()));
}
document.add(table);
}
return outputStream.toByteArray();
}
}
@RestController
@RequestMapping("/api/invoices")
public class InvoiceController {
@Autowired
private PdfGeneratorService pdfGenerator;
@GetMapping("/{id}/pdf")
public ResponseEntity<byte[]> getInvoicePdf(@PathVariable Long id) throws IOException {
Invoice invoice = invoiceService.getInvoice(id);
byte[] pdfContent = pdfGenerator.generateInvoicePdf(invoice);
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_PDF)
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"invoice_" + id + ".pdf\"")
.body(pdfContent);
}
}
4. Потоковая отправка больших файлов
Для больших файлов используйте InputStreamResource:
@GetMapping("/{id}/pdf")
public ResponseEntity<InputStreamResource> downloadLargePdf(@PathVariable Long id)
throws IOException {
Path filePath = Paths.get("/storage/documents/" + id + ".pdf");
InputStream inputStream = Files.newInputStream(filePath);
InputStreamResource resource = new InputStreamResource(inputStream);
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_PDF)
.contentLength(Files.size(filePath))
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"document.pdf\"")
.body(resource);
}
5. Content-Disposition: inline vs attachment
// attachment — браузер СКАЧИВАЕТ файл
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"doc.pdf\"")
// inline — браузер ОТКРЫВАЕТ в новой вкладке (если есть plugin)
.header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"doc.pdf\"")
Выбирайте в зависимости от требований:
- Скачивание отчётов →
attachment - Просмотр документов →
inline
6. Кэширование PDF
@GetMapping("/{id}/pdf")
public ResponseEntity<byte[]> downloadPdf(@PathVariable Long id) {
byte[] pdfContent = documentService.getPdfContent(id);
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_PDF)
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=document.pdf")
.cacheControl(CacheControl.maxAge(7, TimeUnit.DAYS)) // Кэшировать на неделю
.body(pdfContent);
}
7. Обработка ошибок
@GetMapping("/{id}/pdf")
public ResponseEntity<?> downloadPdf(@PathVariable Long id) {
try {
Document doc = documentService.getDocument(id);
if (doc == null) {
return ResponseEntity.notFound().build();
}
// Проверка прав доступа
if (!currentUser.canAccess(doc)) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
}
byte[] pdfContent = pdfGenerator.generatePdf(doc);
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_PDF)
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + doc.getFileName() + ".pdf\"")
.body(pdfContent);
} catch (IOException e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("Error generating PDF");
}
}
8. Асинхронная генерация (для больших файлов)
Если PDF генерируется долго, используйте очередь:
@Service
public class PdfGenerationService {
private final MailService mailService;
@Async
public void generateAndEmail(Long invoiceId, String userEmail) {
try {
Invoice invoice = invoiceService.getInvoice(invoiceId);
byte[] pdf = pdfGenerator.generateInvoicePdf(invoice);
// Отправить на email
mailService.sendPdfAttachment(
userEmail,
"invoice_" + invoiceId + ".pdf",
pdf
);
} catch (Exception e) {
logger.error("Error generating PDF", e);
}
}
}
@RestController
public class InvoiceController {
@PostMapping("/{id}/send-pdf")
public ResponseEntity<String> sendPdfByEmail(@PathVariable Long id) {
pdfGenerationService.generateAndEmail(id, getCurrentUserEmail());
return ResponseEntity.ok("PDF will be sent to your email shortly");
}
}
9. Использование ByteArrayResource
@GetMapping("/{id}/pdf")
public ResponseEntity<ByteArrayResource> downloadPdf(@PathVariable Long id) {
byte[] pdfContent = documentService.getPdfContent(id);
ByteArrayResource resource = new ByteArrayResource(pdfContent);
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_PDF)
.contentLength(pdfContent.length)
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"document.pdf\"")
.body(resource);
}
10. Spring Data JPA + PDF
@Entity
@Table(name = "documents")
public class Document {
@Id
private Long id;
private String title;
@Lob // Large Object — для хранения бинарных данных
private byte[] pdfContent;
@CreationTimestamp
private LocalDateTime createdAt;
}
@Repository
public interface DocumentRepository extends JpaRepository<Document, Long> {}
@Service
public class DocumentService {
@Autowired
private DocumentRepository repository;
public byte[] getPdfContent(Long id) {
Document doc = repository.findById(id)
.orElseThrow(() -> new EntityNotFoundException("Document not found"));
return doc.getPdfContent();
}
}
Лучшие практики
- Используйте правильный Content-Type:
application/pdf - Устанавливайте Content-Disposition:
attachmentилиinline - Указывайте имя файла:
filename=\"report.pdf\" - Проверяйте права доступа перед отправкой
- Используйте потоковую отправку для больших файлов
- Кэшируйте если возможно
- Обрабатывайте ошибки корректно
- Логируйте скачивания для аудита
Вывод
Для отправки PDF:
- Используйте
ResponseEntity<byte[]>для простых случаев - Используйте
InputStreamResourceдля больших файлов - Генерируйте PDF на лету с iText для динамических документов
- Проверяйте права доступа и обрабатывайте ошибки
- Кэшируйте если возможно
- Для очень больших/долгих операций используйте очереди и асинхронность