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:

  1. Java 21+ installed
  2. Spring Boot project running on port 8585
  3. Admin credentials set up (from .env file)
  4. A JPA Entity you want to create a table for
  5. 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

  1. 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)
  2. auto-table.html - Generic Thymeleaf fragment
    • Renders ANY entity table
    • Dynamic columns based on TableConfig
    • Type-aware rendering (TEXT, CHECKBOX, LINK, etc.)
  3. 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:

  1. Stop the running application
  2. 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 .env file (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

  1. Click “Create New Game” button
  2. 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
  3. Click Save
  4. Redirected to table → new record appears!

Update an Existing Record

  1. Click Update button on any row
  2. Form pre-filled with current values
  3. Modify fields
  4. Click Save
  5. Changes reflected in table

Delete a Record

  1. Click Delete button on any row
  2. Record removed immediately
  3. 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.java for 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:

  1. Find old controller (e.g., GameMvcController.java)
  2. Either delete it or change its @RequestMapping path:
    @RequestMapping("/mvc/games-old") // Rename to avoid conflict
    

❌ Page Loads Forever (ERR_INCOMPLETE_CHUNKED_ENCODING)

Symptom: Browser shows “Loading…” indefinitely

Possible Causes:

  1. Template syntax error - Check Spring Boot console for Thymeleaf errors
  2. Missing fragment - Ensure fragments/auto-table.html exists
  3. 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

  1. Open GenerateTablePage.java

  2. 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";
    
  3. Run the generator:
    java GenerateTablePage.java
    
  4. Restart Spring Boot

  5. Visit: http://localhost:8585/mvc/resumes/read

  6. 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:

  • id
  • username
  • professionalSummary
  • experiences

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 the student_projects table in SQLite
  • Each field → becomes a column (titletitle, teamMembersteam_members, etc.)
  • @Id + @GeneratedValue → auto-incrementing primary key
  • spring.jpa.hibernate.ddl-auto=update in application.properties triggers the auto-creation on startup

What reflection will discover:

  • title, description, teamMembersTEXT columns
  • gradeTEXT (numeric)
  • completedCHECKBOX (boolean detected automatically)
  • repoLinkTEXT (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 endpoints
  • studentprojects/read.html — table view with column toggles
  • studentprojects/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:

  • completed is stored as 1/0 (SQLite doesn’t have a native boolean — JPA handles the conversion)
  • grade for the ML project is NULL because we passed null in 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_projects table from the @Entity class (no SQL written)
  • Reflection → Config: TableConfigBuilder discovered all 9 fields and correctly typed them
  • Generator → CRUD: GenerateTablePage.java produced 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)

  1. Write an entity → JPA auto-creates the database table (no SQL)
  2. Write a repository → Spring Data gives you CRUD operations (one-liner)
  3. Seed datainit() method + ModelInit populates the table
  4. Run the generator → 3 config lines produce controller + views
  5. Verify in SQLite.tables, .schema, SELECT prove it all works

Key Concepts

  1. Java Reflection — Runtime type inspection (discovers fields automatically)
  2. JPA/Hibernate — Entity classes become database tables without writing SQL
  3. Code GenerationGenerateTablePage.java eliminates boilerplate
  4. Generic Templates — One Thymeleaf fragment renders any entity
  5. 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

  1. Add Custom Field Labels
    • Modify TableConfigBuilder to read @Column(name=...) annotations
    • Use annotation names as display labels
  2. Add Data Validation
    • Generate form validation based on entity constraints
    • Show error messages for invalid inputs
  3. Custom Styling
    • Modify auto-table.html to use your own CSS classes
    • Add custom Bootstrap themes

Medium Challenges

  1. Support @ManyToOne Relationships
    • Detect relationship fields via reflection
    • Render as dropdown selects in edit form
    • Load options from related repository
  2. Add Pagination
    • Modify generated controller to accept page parameters
    • Update template to show pagination controls
  3. Export to CSV/Excel
    • Add export format options beyond JSON
    • Use Apache POI for Excel generation

Hard Challenges

  1. Generate REST API Controllers
    • Extend generator to create REST endpoints
    • Auto-generate @RestController with GET/POST/PUT/DELETE
  2. Add Search/Filter UI
    • Generate search fields based on entity types
    • Build dynamic SQL queries from user input
  3. Create Admin Dashboard
    • Auto-generate cards showing entity statistics
    • Link to all generated CRUD pages

Pick a challenge and level up your automation skills! 🚀