← Назад к вопросам

Как отдавать 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) — говорит браузеру, что это PDF
  • Content-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();
    }
}

Лучшие практики

  1. Используйте правильный Content-Type: application/pdf
  2. Устанавливайте Content-Disposition: attachment или inline
  3. Указывайте имя файла: filename=\"report.pdf\"
  4. Проверяйте права доступа перед отправкой
  5. Используйте потоковую отправку для больших файлов
  6. Кэшируйте если возможно
  7. Обрабатывайте ошибки корректно
  8. Логируйте скачивания для аудита

Вывод

Для отправки PDF:

  • Используйте ResponseEntity<byte[]> для простых случаев
  • Используйте InputStreamResource для больших файлов
  • Генерируйте PDF на лету с iText для динамических документов
  • Проверяйте права доступа и обрабатывайте ошибки
  • Кэшируйте если возможно
  • Для очень больших/долгих операций используйте очереди и асинхронность