PDF export converts application data into a fixed-layout document that looks the same on every device and prints cleanly.In Spring Boot, a PDF library (here OpenPDF) builds the document in memory as a byte[], and the controller returns it with the application/pdf content type.Unlike CSV or Excel, a PDF is meant for reading and printing, not for editing or calculating.It is used for academic reports, certificates, invoices, receipts, and official letters.
Features
- Printable, fixed-layout output that looks identical everywhere
- Download as a file or open directly in the browser (
?inline=true) - Fonts, colors, tables, and alignment control with OpenPDF
- Table header row that repeats on every page
- Page numbers in the footer
- Alternating row colors and a colored PASS/FAIL result
- Built from database data, with calculations (total, percentage, grade) done in Java
- Subject-wise marks saved with the student through a one-to-many relationship
Hands-on Experiment: Creating a spring boot application to understand how application data can be transformed into printable PDF documents
The following steps are involved in developing this hands-on experiment.
Step 1: Generate the Project
Step 2: Project Structure
Step 3: pom.xml
Step 4: application.properties
Step 5: Create Entity Class
Step 6: Create Repository Interface
Step 7: Create Service Class
Step 8: Create Controller Class
Step 9: Main Application Class
Step 10: Run the Main Application Class
Step 1: Configure the Project on Spring Initializr
Go to start.spring.io and set:
- Project: Maven
- Language: Java
- Spring Boot version: 3.2.x
- Group: com.example
- Artifact: pdfexport
- Name: pdfexport
- Package name: com.example.pdfexport
- Packaging: Jar
- Java: 17
- Dependencies: Spring Web, Spring Data JPA, MySQL Driver.
Click Generate to download the ZIP, then extract and import it into your IDE as a Maven project.
Step 2: Generated Project Structure

Step 3: Explore pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>student-pdf-demo</artifactId>
<version>1.0.0</version>
<name>student-pdf-demo</name>
<description>PDF Export - Student academic report</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- OpenPDF: free library to create PDF documents (package com.lowagie.text) -->
<dependency>
<groupId>com.github.librepdf</groupId>
<artifactId>openpdf</artifactId>
<version>1.3.30</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Step 4: application.properties
spring.application.name=student-pdf-demo
# ---------- MySQL ----------
# Create the database first: CREATE DATABASE student_pdf_db;
spring.datasource.url=jdbc:mysql://localhost:3306/student_pdf_db
spring.datasource.username=root
spring.datasource.password=password
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
server.port=8082
# ---------- Report header ----------
app.college.name=Sunrise Institute of TechnologyCode language: PHP (php)
Step 5: Create Entity Class
package com.example.pdfexport.entity;
import jakarta.persistence.*;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
@Entity
@Table(name = "students")
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Column(nullable = false, unique = true)
private String email;
private String course;
private String department;
private LocalDate admissionDate;
// Subject-wise marks; saved together with the student (cascade)
@OneToMany(mappedBy = "student", cascade = CascadeType.ALL, orphanRemoval = true)
private List<SubjectMark> marks = new ArrayList<>();
public Student() {
}
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getCourse() { return course; }
public void setCourse(String course) { this.course = course; }
public String getDepartment() { return department; }
public void setDepartment(String department) { this.department = department; }
public LocalDate getAdmissionDate() { return admissionDate; }
public void setAdmissionDate(LocalDate admissionDate) { this.admissionDate = admissionDate; }
public List<SubjectMark> getMarks() { return marks; }
public void setMarks(List<SubjectMark> marks) { this.marks = marks; }
}
package com.example.pdfexport.entity;
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.*;
@Entity
@Table(name = "subject_marks")
public class SubjectMark {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String subjectName;
@Column(nullable = false)
private Integer marks;
// Defaults to 100 when not sent
private Integer maxMarks;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "student_id")
@JsonIgnore
private Student student;
public SubjectMark() {
}
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getSubjectName() { return subjectName; }
public void setSubjectName(String subjectName) { this.subjectName = subjectName; }
public Integer getMarks() { return marks; }
public void setMarks(Integer marks) { this.marks = marks; }
public Integer getMaxMarks() { return maxMarks; }
public void setMaxMarks(Integer maxMarks) { this.maxMarks = maxMarks; }
public Student getStudent() { return student; }
public void setStudent(Student student) { this.student = student; }
}
Step 6: Create below Repository Interface
package com.example.pdfexport.repository;
import com.example.pdfexport.entity.Student;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface StudentRepository extends JpaRepository<Student, Long> {
boolean existsByEmail(String email);
}
Step 7: Create below Service class
package com.example.pdfexport.service;
import com.example.pdfexport.entity.Student;
import com.example.pdfexport.entity.SubjectMark;
import com.example.pdfexport.repository.StudentRepository;
import com.lowagie.text.Document;
import com.lowagie.text.DocumentException;
import com.lowagie.text.Element;
import com.lowagie.text.Font;
import com.lowagie.text.FontFactory;
import com.lowagie.text.HeaderFooter;
import com.lowagie.text.PageSize;
import com.lowagie.text.Paragraph;
import com.lowagie.text.Phrase;
import com.lowagie.text.Rectangle;
import com.lowagie.text.pdf.PdfPCell;
import com.lowagie.text.pdf.PdfPTable;
import com.lowagie.text.pdf.PdfWriter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.awt.Color;
import java.io.ByteArrayOutputStream;
import java.time.LocalDate;
import java.util.List;
@Service
public class StudentService {
private static final Color PRIMARY = new Color(31, 78, 121);
private static final Color LIGHT = new Color(238, 241, 244);
private static final Color GREEN = new Color(30, 130, 76);
private static final Color RED = new Color(192, 57, 43);
private final StudentRepository studentRepository;
@Value("${app.college.name}")
private String collegeName;
public StudentService(StudentRepository studentRepository) {
this.studentRepository = studentRepository;
}
// ---------------- Student operations ----------------
@Transactional
public Student create(Student student) {
if (studentRepository.existsByEmail(student.getEmail())) {
throw new IllegalArgumentException("Email already registered: " + student.getEmail());
}
// Link every mark to its student and apply the default maximum
if (student.getMarks() != null) {
for (SubjectMark m : student.getMarks()) {
m.setStudent(student);
if (m.getMaxMarks() == null) {
m.setMaxMarks(100);
}
}
}
return studentRepository.save(student);
}
@Transactional(readOnly = true)
public List<Student> getAll() {
List<Student> students = studentRepository.findAll();
students.forEach(s -> s.getMarks().size()); // load marks before the transaction ends
return students;
}
@Transactional(readOnly = true)
public Student getById(Long id) {
Student student = studentRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Student not found: " + id));
student.getMarks().size();
return student;
}
// ---------------- PDF generation ----------------
@Transactional(readOnly = true)
public byte[] generateAcademicReport(Long id) throws DocumentException {
Student student = getById(id);
List<SubjectMark> marks = student.getMarks();
// ----- calculations -----
int total = 0;
int maxTotal = 0;
boolean allSubjectsPassed = true;
for (SubjectMark m : marks) {
total += m.getMarks();
maxTotal += m.getMaxMarks();
if (percent(m.getMarks(), m.getMaxMarks()) < 40) {
allSubjectsPassed = false;
}
}
double overallPercent = maxTotal == 0 ? 0 : percent(total, maxTotal);
boolean pass = !marks.isEmpty() && allSubjectsPassed;
// ----- fonts -----
Font collegeFont = FontFactory.getFont(FontFactory.HELVETICA_BOLD, 20, PRIMARY);
Font titleFont = FontFactory.getFont(FontFactory.HELVETICA_BOLD, 14, Color.DARK_GRAY);
Font labelFont = FontFactory.getFont(FontFactory.HELVETICA_BOLD, 10, Color.DARK_GRAY);
Font textFont = FontFactory.getFont(FontFactory.HELVETICA, 10, Color.BLACK);
Font headFont = FontFactory.getFont(FontFactory.HELVETICA_BOLD, 10, Color.WHITE);
Font smallFont = FontFactory.getFont(FontFactory.HELVETICA_OBLIQUE, 8, Color.GRAY);
ByteArrayOutputStream out = new ByteArrayOutputStream();
Document document = new Document(PageSize.A4, 40, 40, 50, 50);
PdfWriter.getInstance(document, out);
// page number in the footer
HeaderFooter footer = new HeaderFooter(new Phrase("Page ", smallFont), true);
footer.setAlignment(Element.ALIGN_CENTER);
footer.setBorder(Rectangle.NO_BORDER);
document.setFooter(footer);
document.open();
// ----- title -----
Paragraph college = new Paragraph(collegeName, collegeFont);
college.setAlignment(Element.ALIGN_CENTER);
document.add(college);
Paragraph title = new Paragraph("ACADEMIC REPORT", titleFont);
title.setAlignment(Element.ALIGN_CENTER);
title.setSpacingAfter(16);
document.add(title);
// ----- student details (2 label/value pairs per row) -----
PdfPTable details = new PdfPTable(new float[]{1.2f, 2.3f, 1.2f, 2.3f});
details.setWidthPercentage(100);
addDetail(details, "Student ID", String.valueOf(student.getId()), labelFont, textFont);
addDetail(details, "Name", student.getName(), labelFont, textFont);
addDetail(details, "Email", student.getEmail(), labelFont, textFont);
addDetail(details, "Course", nullSafe(student.getCourse()), labelFont, textFont);
addDetail(details, "Department", nullSafe(student.getDepartment()), labelFont, textFont);
addDetail(details, "Admission Date", String.valueOf(student.getAdmissionDate() == null ? "" : student.getAdmissionDate()),
labelFont, textFont);
document.add(details);
// ----- marks table -----
Paragraph marksTitle = new Paragraph("Subject-wise Performance", titleFont);
marksTitle.setSpacingBefore(18);
marksTitle.setSpacingAfter(8);
document.add(marksTitle);
PdfPTable table = new PdfPTable(new float[]{0.6f, 3f, 1.2f, 1.2f, 1.2f, 1f});
table.setWidthPercentage(100);
String[] heads = {"#", "Subject", "Marks", "Max", "Percent", "Grade"};
for (String h : heads) {
PdfPCell cell = new PdfPCell(new Phrase(h, headFont));
cell.setBackgroundColor(PRIMARY);
cell.setHorizontalAlignment(Element.ALIGN_CENTER);
cell.setPadding(6);
table.addCell(cell);
}
table.setHeaderRows(1); // header repeats on every page
int index = 1;
for (SubjectMark m : marks) {
Color rowColor = (index % 2 == 0) ? LIGHT : Color.WHITE;
double p = percent(m.getMarks(), m.getMaxMarks());
addBodyCell(table, String.valueOf(index), textFont, rowColor, Element.ALIGN_CENTER);
addBodyCell(table, m.getSubjectName(), textFont, rowColor, Element.ALIGN_LEFT);
addBodyCell(table, String.valueOf(m.getMarks()), textFont, rowColor, Element.ALIGN_CENTER);
addBodyCell(table, String.valueOf(m.getMaxMarks()), textFont, rowColor, Element.ALIGN_CENTER);
addBodyCell(table, String.format("%.1f%%", p), textFont, rowColor, Element.ALIGN_CENTER);
addBodyCell(table, grade(p), textFont, rowColor, Element.ALIGN_CENTER);
index++;
}
document.add(table);
// ----- summary -----
PdfPTable summary = new PdfPTable(new float[]{2f, 1f});
summary.setWidthPercentage(50);
summary.setHorizontalAlignment(Element.ALIGN_RIGHT);
summary.setSpacingBefore(14);
addSummary(summary, "Total Marks", total + " / " + maxTotal, labelFont, textFont);
addSummary(summary, "Percentage", String.format("%.2f%%", overallPercent), labelFont, textFont);
addSummary(summary, "Overall Grade", marks.isEmpty() ? "-" : grade(overallPercent), labelFont, textFont);
Font resultFont = FontFactory.getFont(FontFactory.HELVETICA_BOLD, 11, pass ? GREEN : RED);
addSummary(summary, "Result", marks.isEmpty() ? "NO MARKS" : (pass ? "PASS" : "FAIL"), labelFont, resultFont);
document.add(summary);
// ----- footer note -----
Paragraph note = new Paragraph(
"Pass mark: 40% in every subject. Report generated on " + LocalDate.now()
+ ". This is a system-generated document.", smallFont);
note.setSpacingBefore(30);
document.add(note);
document.close();
return out.toByteArray();
}
// ---------------- helpers ----------------
private double percent(int marks, int max) {
return max == 0 ? 0 : (marks * 100.0) / max;
}
private String grade(double percent) {
if (percent >= 90) return "A+";
if (percent >= 80) return "A";
if (percent >= 70) return "B";
if (percent >= 60) return "C";
if (percent >= 40) return "D";
return "F";
}
private String nullSafe(String value) {
return value == null ? "" : value;
}
private void addDetail(PdfPTable table, String label, String value, Font labelFont, Font valueFont) {
PdfPCell l = new PdfPCell(new Phrase(label, labelFont));
l.setBorder(Rectangle.BOTTOM);
l.setBorderColor(Color.LIGHT_GRAY);
l.setPadding(6);
table.addCell(l);
PdfPCell v = new PdfPCell(new Phrase(value, valueFont));
v.setBorder(Rectangle.BOTTOM);
v.setBorderColor(Color.LIGHT_GRAY);
v.setPadding(6);
table.addCell(v);
}
private void addBodyCell(PdfPTable table, String text, Font font, Color background, int align) {
PdfPCell cell = new PdfPCell(new Phrase(text, font));
cell.setBackgroundColor(background);
cell.setHorizontalAlignment(align);
cell.setPadding(6);
cell.setBorderColor(Color.LIGHT_GRAY);
table.addCell(cell);
}
private void addSummary(PdfPTable table, String label, String value, Font labelFont, Font valueFont) {
PdfPCell l = new PdfPCell(new Phrase(label, labelFont));
l.setBackgroundColor(LIGHT);
l.setPadding(6);
l.setBorderColor(Color.LIGHT_GRAY);
table.addCell(l);
PdfPCell v = new PdfPCell(new Phrase(value, valueFont));
v.setHorizontalAlignment(Element.ALIGN_CENTER);
v.setPadding(6);
v.setBorderColor(Color.LIGHT_GRAY);
table.addCell(v);
}
}
Step 8: Create Controller class
package com.example.pdfexport.controller;
import com.example.pdfexport.entity.Student;
import com.example.pdfexport.service.StudentService;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/students")
public class StudentController {
private final StudentService studentService;
public StudentController(StudentService studentService) {
this.studentService = studentService;
}
@PostMapping
public ResponseEntity<?> create(@RequestBody Student student) {
try {
return ResponseEntity.status(HttpStatus.CREATED).body(studentService.create(student));
} catch (IllegalArgumentException ex) {
return ResponseEntity.badRequest().body(Map.of("error", ex.getMessage()));
}
}
@GetMapping
public List<Student> getAll() {
return studentService.getAll();
}
@GetMapping("/{id}")
public ResponseEntity<?> getById(@PathVariable Long id) {
try {
return ResponseEntity.ok(studentService.getById(id));
} catch (IllegalArgumentException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(Map.of("error", ex.getMessage()));
}
}
// PDF report. Default = download. Add ?inline=true to open it inside the browser tab.
@GetMapping("/{id}/report/pdf")
public ResponseEntity<?> academicReport(@PathVariable Long id,
@RequestParam(defaultValue = "false") boolean inline) {
try {
byte[] pdf = studentService.generateAcademicReport(id);
String disposition = (inline ? "inline" : "attachment")
+ "; filename=\"student-report-" + id + ".pdf\"";
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, disposition)
.contentType(MediaType.APPLICATION_PDF)
.contentLength(pdf.length)
.body(pdf);
} catch (IllegalArgumentException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(Map.of("error", ex.getMessage()));
} catch (Exception ex) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Map.of("error", "PDF generation failed: " + ex.getMessage()));
}
}
}
Step 9: Main Application
package com.example.pdfexport;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class PdfExportApplication {
public static void main(String[] args) {
SpringApplication.run(PdfExportApplication.class, args);
}
}
Step 10: Run the StudentRestCurdApplication


