Auto-Generate CRUD Tables
Auto-Generate Admin CRUD Tables with Reflection
Stop writing boilerplate! This system uses Java reflection to automatically generate complete admin CRUD pages for any JPA entity in your Spring Boot backend.
What you get:
- Full CRUD: Create, Read, Update, Delete
- Auto-discovered fields via reflection
- Admin-only security
- Column visibility toggles
- Import/Export functionality
- Responsive Bootstrap UI
Time savings: From 60 minutes of manual coding → 10 seconds of automated generation!
Prerequisites
Before using the auto-generator, make sure you have:
- Java 21+ installed
- Spring Boot project running on port
8585 - Admin credentials set up (from
.envfile) - A JPA Entity you want to create a table for
- A JPA Repository for that entity
Example Entity Structure:
@Entity
@Table(name = "games")
public class Game {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String type;
private Long personId;
private Double amount;
// ... more fields
}
Example Repository:
public interface GameJPARepository extends JpaRepository<Game, Long> {
// Your custom queries
}
How It Works: The Magic of Reflection
The auto-generator uses Java Reflection to inspect your entity class at runtime and automatically discover all its fields.
What is Reflection?
Reflection is Java’s ability to examine and manipulate classes, methods, and fields at runtime. Instead of hardcoding each field name, the system asks the entity class: “What fields do you have?”
The Core Components
- TableConfigBuilder - Uses reflection to scan entity fields
- Skips: static fields, transients, collections
- Detects: String, Number, Boolean, Date types
- Auto-generates: column labels (camelCase → Title Case)
- auto-table.html - Generic Thymeleaf fragment
- Renders ANY entity table
- Dynamic columns based on
TableConfig - Type-aware rendering (TEXT, CHECKBOX, LINK, etc.)
- GenerateTablePage.java - One-file automation script
- Creates controller with all CRUD endpoints
- Generates
read.html(table view) - Generates
edit.html(create/update form)
Step 1: Configure the Generator
Open GenerateTablePage.java in your project root and edit 3 lines:
// ===================================================================
// EDIT THESE LINES - Everything else is automatic!
// ===================================================================
static final String ENTITY_NAME = "Game"; // Your entity class name
static final String ENTITY_PACKAGE = "com.open.spring.mvc.rpg.games"; // Where your entity class is
static final String PAGE_NAME = "Games"; // Creates /mvc/games/read
// ===================================================================
Configuration Guide
| Field | Example | Purpose |
|---|---|---|
ENTITY_NAME |
"Game" |
Name of your JPA entity class |
ENTITY_PACKAGE |
"com.open.spring.mvc.rpg.games" |
Full package path to entity |
PAGE_NAME |
"Games" |
Controls URL and file paths |
Result:
- Controller:
src/main/java/com/open/spring/mvc/games/GamesMvcController.java - Views:
src/main/resources/templates/games/read.html+edit.html - URL:
http://localhost:8585/mvc/games/read
Step 2: Run the Generator
Execute the generator from your project root:
java GenerateTablePage.java
Expected Output:
╔════════════════════════════════════════════════════════════╗
║ AUTO TABLE PAGE GENERATOR ║
╚════════════════════════════════════════════════════════════╝
Entity: Game
Entity Package: com.open.spring.mvc.rpg.games
Controller Pkg: com.open.spring.mvc.games
URL Path: /mvc/games/read
Generating files...
════════════════════════════════════════════════════════════
✓ Created: src/main/java/com/open/spring/mvc/games/GamesMvcController.java
✓ Created: src/main/resources/templates/games/read.html
✓ Created: src/main/resources/templates/games/edit.html
════════════════════════════════════════════════════════════
╔════════════════════════════════════════════════════════════╗
║ ✓ SUCCESS! Everything ready to use. ║
╚════════════════════════════════════════════════════════════╝
What Just Happened?
- ✅ Controller with 5 CRUD endpoints created
- ✅ Table view with column toggles generated
- ✅ Edit form with auto-discovered fields created
- ✅ All endpoints secured with admin-only access
Step 3: Restart Spring Boot
Since we generated new Java files, restart your Spring Boot application:
# Stop the current server (Ctrl+C in terminal)
./mvnw spring-boot:run
Or use VSCode:
- Stop the running application
- Click Run on
Main.java
Wait for:
Tomcat started on port(s): 8585 (http)
Started Main in X.XXX seconds
Step 4: Test Your New Admin Table!
Open the Table View
Navigate to: http://localhost:8585/mvc/games/read
Login Required:
- Username:
toby(or your admin username) - Password: From your
.envfile (ADMIN_PASSWORD)
What You Should See
Column Toggle Buttons at the top:
- Click any button to show/hide that column
- Active (green) = visible, Inactive (gray) = hidden
The Table:
- All entity fields automatically displayed
- Update/Delete buttons for each row
- “Create New Game” button at bottom
Import/Export Controls:
- Export All → Download as JSON
- Import JSON → Upload data
Step 5: Test CRUD Operations
Create a New Record
- Click “Create New Game” button
- Fill out the auto-generated form:
- All entity fields shown as form inputs
- Textareas for fields with “details”, “description”, “summary”
- Checkboxes for Boolean fields
- Regular inputs for everything else
- Click Save
- Redirected to table → new record appears!
Update an Existing Record
- Click Update button on any row
- Form pre-filled with current values
- Modify fields
- Click Save
- Changes reflected in table
Delete a Record
- Click Delete button on any row
- Record removed immediately
- Table refreshes
All operations are admin-only! Non-admin users see “Access denied” message.
Verification: Is It Actually Working?
Check 1: Controller Exists
Look for generated controller:
ls src/main/java/com/open/spring/mvc/games/GamesMvcController.java
Check 2: Views Exist
ls src/main/resources/templates/games/read.html
ls src/main/resources/templates/games/edit.html
Check 3: Database Has Data
If table shows 0 rows, check SQLite database:
# From project root
sqlite3 volumes/sqlite.db
# In SQLite shell
SELECT COUNT(*) FROM games;
SELECT * FROM games LIMIT 5;
If count is 0:
- Entity’s
init()method may not be called - Check
ModelInit.javafor seeding logic - Create test records via the UI
Check 4: Endpoints Are Accessible
Test URLs (while logged in as admin):
- ✅
http://localhost:8585/mvc/games/read - ✅
http://localhost:8585/mvc/games/new - ✅
http://localhost:8585/mvc/games/edit/1
Understanding the Generated Code
The Controller (5 Endpoints)
@Controller
@RequestMapping("/mvc/games")
public class GamesMvcController {
@Autowired
private GameJPARepository gameJPARepository;
// Checks if user is admin
private boolean isAdmin(Authentication authentication) { ... }
// Builds table config via reflection
private TableConfig buildTableConfig() {
return TableConfigBuilder.fromEntity(Game.class)
.withEntityName("games")
.withPaths("/mvc/games/edit", "/mvc/games/delete")
.build();
}
@GetMapping("/read") // List all records
@GetMapping("/new") // Show create form
@GetMapping("/edit/{id}") // Show edit form
@PostMapping("/save") // Create or update
@GetMapping("/delete/{id}") // Delete record
}
The Read View
One line renders the entire table using the auto-table fragment with dynamic columns!
The Edit View
Form fields auto-generated from tableConfig.columns:
- Loops through discovered fields
- Skips
id,ACTIONS,IMPORT_EXPORT - Renders appropriate input type based on field type
Troubleshooting Common Issues
❌ Error: “Ambiguous mapping”
Symptom:
Ambiguous mapping. Cannot map 'gamesMvcController' method
to {GET [/mvc/games/read]}: There is already 'gameMvcController'
Cause: Two controllers mapping to the same URL
Fix:
- Find old controller (e.g.,
GameMvcController.java) - Either delete it or change its
@RequestMappingpath:@RequestMapping("/mvc/games-old") // Rename to avoid conflict
❌ Page Loads Forever (ERR_INCOMPLETE_CHUNKED_ENCODING)
Symptom: Browser shows “Loading…” indefinitely
Possible Causes:
- Template syntax error - Check Spring Boot console for Thymeleaf errors
- Missing fragment - Ensure
fragments/auto-table.htmlexists - Repository not found - Check repository naming matches
REPO_NAME
Fix: Check console logs for specific error, restart Spring Boot
❌ Table Shows 0 Rows (But Entity Exists)
Symptom: Table renders but shows empty
Cause: Database table empty
Fix:
// Check ModelInit.java - entity seeding may be disabled
// Example:
Game[] games = Game.init();
for (Game g : games) {
gameJPARepository.save(g);
}
Or create records via UI: Click “Create New”
❌ Repository Not Found Error
Symptom:
Could not autowire. No beans of 'GameJPARepository' type found.
Cause: Repository naming doesn’t match REPO_NAME assumption
Fix: Generator assumes ENTITY_NAME + "JPARepository". If your repo has a different name (e.g., UnifiedGameRepository), manually edit generated controller to use correct repo name.
Advanced: How Reflection Works
Let’s peek under the hood at TableConfigBuilder.java:
Field Discovery
private List<Field> getAllFields(Class<?> clazz) {
List<Field> fields = new ArrayList<>();
// Add fields from current class
fields.addAll(Arrays.asList(clazz.getDeclaredFields()));
// Add fields from superclass (inheritance support)
if (clazz.getSuperclass() != null && clazz.getSuperclass() != Object.class) {
fields.addAll(getAllFields(clazz.getSuperclass()));
}
return fields;
}
Field Filtering
private boolean shouldSkipField(Field field) {
// Skip static fields
if (Modifier.isStatic(field.getModifiers())) return true;
// Skip @Transient fields
if (field.isAnnotationPresent(Transient.class)) return true;
// Skip collections/maps (avoid complex recursion)
if (Collection.class.isAssignableFrom(field.getType())) return true;
return false;
}
Type Detection
private TableColumn.ColumnType determineColumnType(Field field) {
Class<?> type = field.getType();
if (type == Boolean.class || type == boolean.class) {
return TableColumn.ColumnType.CHECKBOX;
}
if (type == String.class || Number.class.isAssignableFrom(type)) {
return TableColumn.ColumnType.TEXT;
}
return TableColumn.ColumnType.TEXT; // Default
}
Reflection Benefits:
- ✅ No hardcoding field names
- ✅ Automatically adapts to entity changes
- ✅ Works with ANY entity structure
- ✅ Supports inheritance
Try It Yourself: Generate a Resume Table
Let’s practice by generating a CRUD table for the Resume entity!
Exercise Instructions
-
Open
GenerateTablePage.java - Edit the configuration:
static final String ENTITY_NAME = "Resume"; static final String ENTITY_PACKAGE = "com.open.spring.mvc.resume"; static final String PAGE_NAME = "Resumes"; - Run the generator:
java GenerateTablePage.java -
Restart Spring Boot
-
Visit:
http://localhost:8585/mvc/resumes/read - Test CRUD:
- Create a resume record
- View it in the table
- Edit it
- Delete it
Expected Result
You should see a table showing Resume fields:
idusernameprofessionalSummaryexperiences
With full CRUD functionality, all auto-generated in 10 seconds!
Build & Test From Scratch: Prove It Works
The Resume exercise above uses an existing entity. Let’s do the real test — create a brand new entity that doesn’t exist yet, generate a table for it, and prove the entire pipeline works end-to-end: entity → database table → generated CRUD → working UI.
We’ll build a StudentProject entity to track class projects.
Part 1: Create the Entity (The Database Table)
Create a new file: src/main/java/com/open/spring/mvc/studentproject/StudentProject.java
This is the only Java class you write by hand. JPA automatically creates the student_projects database table from this.
package com.open.spring.mvc.studentproject;
import jakarta.persistence.*;
import lombok.*;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "student_projects")
public class StudentProject {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String description;
private String teamMembers;
private String language; // Java, Python, etc.
private Double grade; // 0.0 - 100.0
private Boolean completed; // true/false — renders as CHECKBOX
private String dueDate; // "2026-05-01"
private String repoLink; // GitHub URL
// Seed data — called by ModelInit to populate the DB on first run
public static StudentProject[] init() {
return new StudentProject[] {
new StudentProject(null, "Auto-Table Generator", "Reflection-based CRUD generator", "Yash, Toby", "Java", 95.0, true, "2026-04-15", "https://github.com/example/auto-table"),
new StudentProject(null, "Chat App", "Real-time messaging with WebSockets", "Alice, Bob", "JavaScript", 88.5, true, "2026-03-20", "https://github.com/example/chat-app"),
new StudentProject(null, "ML Image Classifier", "CNN-based image recognition", "Charlie, Dana", "Python", null, false, "2026-05-01", "https://github.com/example/ml-classifier")
};
}
}
What JPA does with this:
@Entity+@Table(name = "student_projects")→ creates thestudent_projectstable in SQLite- Each field → becomes a column (
title→title,teamMembers→team_members, etc.) @Id+@GeneratedValue→ auto-incrementing primary keyspring.jpa.hibernate.ddl-auto=updateinapplication.propertiestriggers the auto-creation on startup
What reflection will discover:
title,description,teamMembers→ TEXT columnsgrade→ TEXT (numeric)completed→ CHECKBOX (boolean detected automatically)repoLink→ TEXT (or LINK if configured)id→ shown but not editable
Part 2: Create the Repository
Create: src/main/java/com/open/spring/mvc/studentproject/StudentProjectJPARepository.java
package com.open.spring.mvc.studentproject;
import org.springframework.data.jpa.repository.JpaRepository;
public interface StudentProjectJPARepository extends JpaRepository<StudentProject, Long> {
// That's it. JPA gives you findAll(), save(), deleteById(), etc. for free.
}
This one-liner interface gives you all CRUD database operations automatically via Spring Data JPA.
Part 3: Seed the Database
Add seeding to your ModelInit.java (or wherever your project seeds data) so the table isn’t empty on first run:
@Autowired
private StudentProjectJPARepository studentProjectJPARepository;
// Inside your init method:
if (studentProjectJPARepository.count() == 0) {
for (StudentProject sp : StudentProject.init()) {
studentProjectJPARepository.save(sp);
}
System.out.println("StudentProject table seeded with " + studentProjectJPARepository.count() + " records");
}
Why seed? Without this, the table renders but shows 0 rows — which makes it look broken even though it works. Seed data proves the full pipeline: entity → DB table → data → UI.
Part 4: Run the Generator
Now use the auto-generator to create the controller and views — this is where the 3-line magic happens:
// In GenerateTablePage.java, change these 3 lines:
static final String ENTITY_NAME = "StudentProject";
static final String ENTITY_PACKAGE = "com.open.spring.mvc.studentproject";
static final String PAGE_NAME = "Studentprojects";
java GenerateTablePage.java
This generates:
StudentprojectsMvcController.java— controller with 5 CRUD endpointsstudentprojects/read.html— table view with column togglesstudentprojects/edit.html— create/update form
Then restart Spring Boot and navigate to: http://localhost:8585/mvc/studentprojects/read
Part 5: Prove It — Database Verification
This is the part that matters for testing. You need to prove the database table was created from scratch and that CRUD actually works at the data level, not just the UI level.
Test 1: Verify the Table Was Auto-Created
Before you created StudentProject.java, the student_projects table did not exist. JPA created it automatically on startup. Prove it:
sqlite3 volumes/sqlite.db
# Show all tables — student_projects should be in the list
.tables
# Show the table structure — columns should match your entity fields
.schema student_projects
Expected output:
CREATE TABLE student_projects (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT,
description TEXT,
team_members TEXT,
language TEXT,
grade REAL,
completed INTEGER,
due_date TEXT,
repo_link TEXT
);
Notice how JPA converted camelCase Java fields (teamMembers) to snake_case SQL columns (team_members). This happened automatically — you didn’t write any SQL.
Test 2: Verify Seed Data Loaded
SELECT COUNT(*) FROM student_projects;
-- Expected: 3
SELECT id, title, language, grade, completed FROM student_projects;
-- Expected:
-- 1 | Auto-Table Generator | Java | 95.0 | 1
-- 2 | Chat App | JavaScript | 88.5 | 1
-- 3 | ML Image Classifier | Python | NULL | 0
Key things to notice:
completedis stored as1/0(SQLite doesn’t have a native boolean — JPA handles the conversion)gradefor the ML project isNULLbecause we passednullin the seed data- IDs were auto-generated starting from 1
Test 3: CRUD at the Database Level
Test each operation through the UI, then verify it hit the database:
CREATE — Add a new project via the UI form, then:
SELECT * FROM student_projects ORDER BY id DESC LIMIT 1;
-- Should show your newly created record with id = 4
UPDATE — Edit a project’s grade via the UI, then:
SELECT id, title, grade FROM student_projects WHERE id = 1;
-- Grade should reflect your edit
DELETE — Delete a project via the UI, then:
SELECT COUNT(*) FROM student_projects;
-- Count should be one less than before
Test 4: Reflection Accuracy
Verify that what reflection discovered matches the actual database schema:
| Java Field | Java Type | Reflection Type | DB Column | DB Type |
|---|---|---|---|---|
id |
Long |
(skipped in form) | id |
INTEGER |
title |
String |
TEXT | title |
TEXT |
description |
String |
TEXT | description |
TEXT |
teamMembers |
String |
TEXT | team_members |
TEXT |
language |
String |
TEXT | language |
TEXT |
grade |
Double |
TEXT | grade |
REAL |
completed |
Boolean |
CHECKBOX | completed |
INTEGER |
dueDate |
String |
TEXT | due_date |
TEXT |
repoLink |
String |
TEXT | repo_link |
TEXT |
The auto-generator correctly mapped every field type — including detecting Boolean as CHECKBOX without any manual configuration.
What This Proves
- Entity → Table: JPA auto-created the
student_projectstable from the@Entityclass (no SQL written) - Reflection → Config:
TableConfigBuilderdiscovered all 9 fields and correctly typed them - Generator → CRUD:
GenerateTablePage.javaproduced a working controller + views from 3 config lines - UI → Database: Create, Update, Delete operations in the browser persist to SQLite
- Full pipeline: You went from zero to a working admin CRUD page with database by writing only 2 files (entity + repository) and editing 3 lines
Summary: What You Learned
The Full Pipeline (Proven End-to-End)
- Write an entity → JPA auto-creates the database table (no SQL)
- Write a repository → Spring Data gives you CRUD operations (one-liner)
- Seed data →
init()method +ModelInitpopulates the table - Run the generator → 3 config lines produce controller + views
- Verify in SQLite →
.tables,.schema,SELECTprove it all works
Key Concepts
- Java Reflection — Runtime type inspection (discovers fields automatically)
- JPA/Hibernate — Entity classes become database tables without writing SQL
- Code Generation —
GenerateTablePage.javaeliminates boilerplate - Generic Templates — One Thymeleaf fragment renders any entity
- Builder Pattern — Fluent configuration API via
TableConfigBuilder
Impact
- 97% code reduction: ~100 lines → 3 configuration lines per entity
- 180-360x faster: 60 minutes of manual work → 10 seconds
- Works with ANY entity: Unlimited scalability — just repeat the 5 steps above
Challenge: Extend the System
Want to go further? Try these enhancements:
Easy Challenges
- Add Custom Field Labels
- Modify
TableConfigBuilderto read@Column(name=...)annotations - Use annotation names as display labels
- Modify
- Add Data Validation
- Generate form validation based on entity constraints
- Show error messages for invalid inputs
- Custom Styling
- Modify
auto-table.htmlto use your own CSS classes - Add custom Bootstrap themes
- Modify
Medium Challenges
- Support @ManyToOne Relationships
- Detect relationship fields via reflection
- Render as dropdown selects in edit form
- Load options from related repository
- Add Pagination
- Modify generated controller to accept page parameters
- Update template to show pagination controls
- Export to CSV/Excel
- Add export format options beyond JSON
- Use Apache POI for Excel generation
Hard Challenges
- Generate REST API Controllers
- Extend generator to create REST endpoints
- Auto-generate
@RestControllerwith GET/POST/PUT/DELETE
- Add Search/Filter UI
- Generate search fields based on entity types
- Build dynamic SQL queries from user input
- Create Admin Dashboard
- Auto-generate cards showing entity statistics
- Link to all generated CRUD pages
Pick a challenge and level up your automation skills! 🚀