← Back to Projects

WEBRock: Architecting a Custom Java Servlet Framework from Scratch

Welcome to the official developer and user documentation for the **WebRock Framework**—a custom-built, lightweight replica of the Spring Boot framework running on standard Java Servlet containers. It is not complete replica, but having multiple features with different inline architecture than actual Spring Boot.

View on GitHub ↗

Overview

WEBRock is a lightweight, custom-built MVC web framework built entirely on top of the standard Java Servlet API. I embarked on this project to pull back the curtain on enterprise Java frameworks like Spring Boot and Spring MVC. Instead of treating these powerful tools as black boxes, I wanted to understand their internal mechanics by building them myself.

This framework is the culmination of my deep dive into Java Reflection, the Servlet Container lifecycle, and Inversion of Control (IoC). By abstracting away the boilerplate of raw HttpServletRequest and HttpServletResponse handling, WEBRock provides a modern, annotation-driven developer experience. It goes beyond simple routing, offering automated dependency injection, runtime generation of JavaScript client code, and dynamic PDF API documentation.

This case study breaks down the complete architecture and the core engineering challenges I solved to bring WEBRock to life.

Problem Statement

In classic J2EE web applications, the development process is heavily constrained by boilerplate configuration. Every new endpoint requires:

  1. Creating a new class that extends HttpServlet.
  2. Overriding doGet, doPost, etc.
  3. Manually mapping the Servlet to a URL in a massive web.xml file.
  4. Writing repetitive code to parse HTTP parameters, convert them into Java objects, and serialize the response back to JSON.

This results in tightly coupled code that is difficult to maintain and test. WEBRock was designed to solve this by providing a centralized Front Controller that dynamically maps requests to standard Java methods using custom annotations (@PATH, @POJO, @AutoWired), entirely eliminating the need for web.xml servlet mappings.

Architecture

WEBRock relies on a centralized dispatch mechanism and an in-memory routing table built at startup.

+---------------------------------------------------+
|               HTTP Request / Response             |
+---------------------------------------------------+
|            WebRock (Front Controller)             |
|  - Intercepts all '/*' requests                   |
|  - Looks up route in memory                       |
|  - Parses parameters / JSON payload               |
+---------------------------------------------------+
|            Reflection & Annotation Engine         |
|  - @PATH: Locates the target method               |
|  - @AutoWired: Injects dependencies (IoC)         |
|  - @SecuredAccess: Validates sessions/auth        |
+---------------------------------------------------+
|              Developer's Java Services            |
|  (Standard POJOs without Servlet boilerplate)     |
+---------------------------------------------------+

Traditional J2EE web development requires writing multiple servlet classes and explicitly mapping their URL paths in a web.xml deployment descriptor. As an application grows, this approach leads to a large volume of boilerplate code and configuration files.

WebRock addresses this issue by introducing a reflection-driven model. By using custom Java annotations, developers can turn plain old Java objects (POJOs) into active controllers, services, and DTOs.


Core Architectural Patterns

WebRock operates on three fundamental patterns:

  1. Front Controller Pattern (WebRock.java) Rather than mapping separate servlets to individual endpoints, WebRock registers a single routing servlet (WebRock) to catch wildcard patterns (e.g., /studentService/*, /requestParameterTesting/*). This servlet acts as the centralized gateway (Front Controller), dynamically resolving path routes, verifying security, injecting dependencies, and invoking service endpoints via reflection.

  2. Bootstrapping & Classpath Scanning (WebRockStarter.java) On application startup, WebRock searches the classpath for packages matching a configured prefix. It discovers annotated classes, registers endpoint metadata, runs prioritized startup tasks, compiles ES6 JavaScript clients for client-side use, and generates a PDF directory listing of the API.

  3. Decoupled Context Scopes To avoid direct dependencies on servlet APIs (HttpServletRequest, HttpSession, ServletContext), WebRock wraps these resources in custom class scopes (RequestScope, SessionScope, ApplicationScope, ApplicationDirectory). These scope wrappers can then be automatically wired or injected into your services.


The Startup & Request Lifecycle

1. The Startup Lifecycle (Bootstrap)

When the servlet container (e.g., Apache Tomcat) initializes:

  1. Load-on-Startup: The container reads web.xml and initializes WebRockStarter.
  2. Package Scan: The starter locates classes matching SERVICE_PACKAGE_PREFIX (e.g., bobby).
  3. Registration: Services and DTOs are scanned. Endpoint definitions are saved in the singleton registry (WebRockModel).
  4. JS Generation: JS client code is generated dynamically and stored in /WEB-INF/js/.
  5. Auto-Documenting: A PDF file listing all endpoints (webrockDOC.pdf) is written to the root folder using iText.
  6. Task Run: Methods annotated with @OnStartup are executed in order of ascending priority.

2. The Request Lifecycle (Runtime)

When a client triggers an AJAX request or form submission:

  1. Routing: WebRock servlet intercepts the request, cleanses the site name prefix, and queries WebRockModel for a matching Service.
  2. Security Interception: If @SecuredAccess is declared, the designated security checkpost and guard methods are executed.
  3. Dependency Injection: @AutoWired fields and request parameter injections are resolved.
  4. Argument Resolution: Method arguments are populated (either by reading the request payload JSON or binding query strings).
  5. Execution: The service method is executed.
  6. Response Handling: The output is serialized to JSON or forwarded to another route if @FORWARD is present.

Project Package Layout

com.ashvin.web.rock (Amit - Framework Developer)
 ├── annotations/          # Routing, injection, security, and startup annotations
 ├── exceptions/           # Core framework runtime exceptions
 ├── model/                # Singleton WebRockModel route repository
 ├── pojo/                 # Configuration, mapping, and scope POJOs
 ├── utils/                # Gson serializers and primitive wrappers
 ├── WebRock.java          # Front Controller dispatcher servlet
 ├── WebRockStarter.java   # Startup loader & compiler servlet
 └── JSFileLoading.java    # Generated JavaScript provider servlet

bobby (Bobby - Framework User & Tester)
 ├── student/              # Database schema, DTOs, and DAO implementations
 └── test/                 # Test cases validating injection, routing, and guards

Section 2: Detailed Annotation Directory

WebRock leverages annotations to configure components, control routing, enable scoping, and perform auto-wiring. Below is an exhaustive directory of all custom annotations provided by the framework.


1. @PATH


2. @POJO


3. @GET & @POST


4. @AutoWired


5. @InjectRequestParameter


6. @RequestParameter


7. @SecuredAccess


8. @FORWARD


9. @OnStartup


10. Scope Injection Annotations (@InjectRequestScope, etc.)


Section 3: Scopes, Injection & Context Engine

In typical J2EE web applications, business logic is often tightly coupled with servlet APIs like HttpServletRequest, HttpSession, and ServletContext. WebRock decouples these using Context Scope Wrappers and a reflection-based Injection Engine.


1. Scope Wrapper Classes

WebRock wraps servlet scopes in simple helper classes located in com.ashvin.web.rock.pojo. These classes allow endpoints to read and write attributes without importing servlet APIs directly.

A. RequestScope

Wraps the raw HttpServletRequest object for the current thread request.

B. SessionScope

Wraps HttpSession to maintain client state across multiple requests.

C. ApplicationScope

Wraps ServletContext to store global data shared across all users and connections.

D. ApplicationDirectory

Provides service methods with access to the physical deployment directory of the web application.


2. Auto-Wiring Injection Mechanics (@AutoWired)

When a client triggers a mapped service URL, the front controller (WebRock.java) instantiates the target service class and performs field-level auto-wiring:

graph TD
    A[Scan Fields for @AutoWired] --> B[Retrieve field 'name' parameter]
    B --> C[Check Request attributes]
    C -->|Found| D[Match Type & Inject]
    C -->|Not Found| E[Check Session attributes]
    E -->|Found| D
    E -->|Not Found| F[Check Application attributes]
    F -->|Found| D
    F -->|Not Found| G[Convert type to non-primitive wrapper & Inject null]

The Lookup Hierarchy:

  1. Request Attributes: Checked first via request.getAttribute(name).
  2. Session Attributes: Checked second via request.getSession().getAttribute(name).
  3. Application Attributes: Checked last via request.getServletContext().getAttribute(name).

Type Matching & Wrapper Conversion:


3. Scope Injection via Class Setters

In addition to field wiring, classes can request full access to scope wrappers via class-level annotations.

Execution Flow:

  1. WebRock checks the service class for these class-level annotations.
  2. If present, it uses reflection to locate the matching setter method (e.g., setSessionScope).
  3. It instantiates/wraps the required scope object (e.g., new SessionScope()) and invokes the setter method using reflection.

4. Parameter Parsing Utility (WebRockUtils.parseTo)

WebRock includes a robust type-conversion engine to parse HTTP string inputs (query strings, form parameters) into Java types:


Section 4: Routing Engine & Request Lifecycle

This section describes how the WebRock front controller resolves incoming URLs, binds method arguments to client payloads, forwards contexts, and handles exceptions.


1. Front Controller Route Resolution

Every incoming request maps to the standard servlet lifecycle method doGet or doPost in WebRock.java, which delegates to the internal dispatcher: doIt(HttpServletRequest request, HttpServletResponse response, String type).

Path Resolution Steps:

  1. Retrieve Configurations: The engine reads the SITE_NAME deployment parameter (configured in web.xml).
  2. Cleanse URI: It extracts the relative route path by dropping the context path and site name: $$\text{fullPathToService} = \text{requestURI} - \text{siteName}$$ Example: If URL is /webrock/studentService/add and siteName is webrock, the resolved endpoint path is /studentService/add.
  3. Query Services Registry: It requests the mapped endpoint metadata from the WebRockModel singleton: Service service = webRockModel.getPathService(fullPathToService, type);
  4. 404 Handling: If the endpoint or the HTTP request verb (type) is not registered, the controller responds with a 404 Not Found JSON body: {"error": "Requested service not found."}

2. Method Argument Binding Rules

Once a routing match is found, WebRock analyzes the method signature of the endpoint. Arguments are parsed and populated dynamically based on their configuration:

A. Scope Auto-filling

If a method parameter class type matches any of the scope wrapper classes (ApplicationScope, SessionScope, RequestScope, or ApplicationDirectory), WebRock instantiates the wrapper for the current request context and passes it automatically.

B. Request Parameter Mapping (@RequestParameter("name"))

If the parameter is marked with a name attribute, WebRock extracts the value using request.getParameter(name). It then converts the string value to the method parameter type via WebRockUtils.parseTo.

C. JSON Request Payload Binding (Unnamed Objects)

If a parameter does not have a @RequestParameter name, it is treated as a complex object representation. WebRock assumes the client has sent a raw JSON payload in the request body.

[!IMPORTANT] Payload Mapping Constraints:

  1. You may have at most one unnamed object parameter representing the JSON body payload.
  2. Mixing query string arguments (using @RequestParameter with a name) and a JSON body payload in the same method signature is strictly prohibited. Doing so will cause the framework to throw a ServiceException: Cannot use @RequestParameter alongwith JSON data in request to process multiple parameter on service...

3. Route Forwarding & Redirects (@FORWARD)

Endpoints can route requests upon successful execution by declaring a @FORWARD annotation on the method.


4. Exception Mapping & Responses

WebRock captures service exceptions thrown during reflection:

  1. Root Cause Extraction: It intercepts reflection execution errors (InvocationTargetException), extracts the root cause via .getCause(), and matches it against the list of exceptions declared in the service method signature (method.getExceptionTypes()).
  2. Declared Exceptions: If the thrown exception matches a declared signature exception (e.g. DAOException), WebRock extracts the exception message and responds to the client with a formatted JSON payload and a 500 HTTP status: {"error": "exception_message"}
  3. Implicit WebRock Security Failures: Catching a com.ashvin.web.rock.exceptions.SecurityException forces a 401 Unauthorized status returning: {"error": "Unauthorized access."}
  4. General Unhandled Runtime Failures: Any other generic exceptions or system crashes return a standard internal server response with a 500 status: {"error": "Internal server error"}

5. Custom Framework Exceptions

WebRock defines two specialized classes inside com.ashvin.web.rock.exceptions to distinguish framework setup issues from client access issues:


Section 5: Security Guard Model

WebRock provides route-level authorization mapping via the @SecuredAccess annotation. It intercepts endpoints, validates request attributes, and isolates security verification from controller business logic.


1. The @SecuredAccess Interception Pattern

The @SecuredAccess annotation can be placed at the class level (protecting all endpoints in that class) or the method level (protecting a specific method).


2. Startup Verification & Path Binding

During bootstrapping (WebRockStarter.java), the framework validates all security declarations:

  1. Class Verification: WebRock searches for the checkpost class name using Class.forName(checkPost).
  2. Method Search: It matches the guard method name.
  3. Signature Rules Validation:
    • The guard method parameters can only accept scope wrapper classes (ApplicationScope, SessionScope, RequestScope, or ApplicationDirectory). If any other parameter types are present, the verification fails and the guard is flagged as invalid.
    • The guard method must return void. If the guard returns any value, it is considered invalid and throws a SecurityException during execution.
  4. Service Binding: WebRock matches the guard method to a registered path endpoint inside the registry and maps the reference within the Service metadata object: securityAccess.setServicePath(service2.getPath());

3. Runtime Security Execution Path

When a request triggers a secured service, WebRock executes the guard filter before routing to the target method:

sequenceDiagram
    participant Client
    participant WebRock as Front Controller
    participant Guard as Security CheckPost
    participant Service as Business Service

    Client->>WebRock: Request Endpoint
    rect rgb(200, 220, 255)
        note right of WebRock: Security Gate
        WebRock->>Guard: newInstance() & Inject Scopes
        WebRock->>Guard: Invoke Guard Method
        alt Guard throws Exception
            Guard-->>WebRock: Throw SecurityException
            WebRock-->>Client: 401 Unauthorized Response (JSON)
        else Guard executes cleanly
            Guard-->>WebRock: Returns void (Passed)
        end
    end
    WebRock->>Service: Instantiate & Execute Service
    Service-->>WebRock: Return result
    WebRock-->>Client: 200 OK Response (JSON)
  1. Instantiation: WebRock creates an instance of the checkpost class: Object checker = securityServiceClass.newInstance();
  2. Dependency Injection: It parses the checkpost class for @AutoWired fields and injects scope variables from Request, Session, or ServletContext attributes.
  3. Guard Invocation: It compiles the arguments (the required scope wrappers) and invokes the guard method: securityServiceMethod.invoke(checker, parametersValue);
  4. Evaluation:
    • If the guard method completes without throwing an exception, authorization succeeds, and WebRock proceeds to execute the target service.
    • If the guard throws a declared exception (e.g. new SecurityException("Invalid username")), WebRock catches it, sets the HTTP response status to 401 Unauthorized, and returns the message: {"error": "Unauthorized access."}

Section 6: Auto-Generated JavaScript Client

A key feature of WebRock is its code generation compiler. On startup, WebRock compiles Java classes into frontend JavaScript APIs, eliminating the need to write manual AJAX/fetch calls or matching client-side DTO properties.


1. Compilation Configuration & Storage


2. DTO Code Compilation (@POJO)

For every Java class annotated with @POJO, WebRock parses class fields and creates a matching ES6 class structure:

  1. Constructor Properties: The class constructor parameters map to the exact fields of the Java class.
  2. Encapsulation Accessors: CamelCase getters and setters are compiled for all properties.

Code Comparison:


3. API Client Compilation (@PATH)

For every Java class annotated with @PATH, WebRock generates a JS API wrapper containing client endpoints that map to Java methods:

  1. Class Mapping: The JS class shares the Java class name (e.g. class StudentDAO).
  2. Method Filtering:
    • Only methods marked with @PATH are generated.
    • Arguments that match scope wrapper classes (SessionScope, etc.) are stripped, as they are populated implicitly on the server.
  3. Payload Formatting:
    • Query Parameters: If arguments use @RequestParameter (such as primitive fields), the client uses ES6 URLSearchParams to format a query string appended to the URL:
      const queryString = new URLSearchParams({ rollNumber }).toString();
      finalUrl += `?${queryString}`;
    • JSON Body Payload: If an argument has no name, it is treated as the JSON payload. The client converts the object to a JSON string and maps the HTTP method to POST:
      const body = JSON.stringify(studentDTO);
      xhr.send(body);
  4. ES6 Promise Wrapper: All compiled client methods return a standard JavaScript Promise:
    • Resolve (200 range status): Resolves the parsed JSON object: resolve(JSON.parse(xhr.responseText))
    • Reject (Non-200 status): Rejects with status codes and the error message parsed from the response JSON: reject({ status: xhr.status, message: JSON.parse(xhr.responseText).error })
    • Reject (Network Failure): Returns a general network exception if request fails to dispatch: reject(new Error('Network Error'))

Compiled Service Client Example (StudentManager.js):

class StudentDAO {
    constructor() {}

    // POST request with JSON body payload mapping
    add(studentDTO) {
        return new Promise((resolve, reject) => {
            const xhr = new XMLHttpRequest();
            let finalUrl = 'StudentManager/add';
            xhr.open("POST", finalUrl);
            xhr.setRequestHeader('Content-Type', 'application/json');
            xhr.onload = () => {
                if (xhr.status >= 200 && xhr.status < 300) {
                    resolve(JSON.parse(xhr.responseText));
                } else {
                    reject({ status: xhr.status, message: JSON.parse(xhr.responseText).error });
                }
            };
            xhr.onerror = () => reject(new Error('Network Error'));
            const body = JSON.stringify(studentDTO);
            xhr.send(body);
        });
    }

    // GET request with query-string parameters mapping
    delete(rollNumber) {
        return new Promise((resolve, reject) => {
            const xhr = new XMLHttpRequest();
            let finalUrl = 'StudentManager/delete';
            const queryString = new URLSearchParams({ rollNumber }).toString();
            finalUrl += `?${queryString}`;
            xhr.open("GET", finalUrl);
            xhr.setRequestHeader('Content-Type', 'application/json');
            xhr.onload = () => {
                if (xhr.status >= 200 && xhr.status < 300) {
                    resolve(JSON.parse(xhr.responseText));
                } else {
                    reject({ status: xhr.status, message: JSON.parse(xhr.responseText).error });
                }
            };
            xhr.onerror = () => reject(new Error('Network Error'));
            xhr.send();
        });
    }
}

Section 7: Bootstrap & Auto-Doc Engine

WebRock handles application context bootstrapping dynamically. During initialization, it scans packages, launches priority background services, compiles client scripts, and exports an API reference catalog in PDF format.


1. Classpath Scanner & Package Traversal

When the servlet container boots WebRockStarter, the loader performs classpath analysis:

  1. Parameter Retrieval: It reads SERVICE_PACKAGE_PREFIX (which defines the user package, e.g. bobby).
  2. Absolute Path Resolution: The loader maps the package to its absolute system path: String pathToClassFolder = getServletContext().getRealPath("/WEB-INF/classes");
  3. Recursive Directory Walk: Using Java NIO Files.walk(rootPath), the scanner traverses the directory tree and gathers all file paths ending in .class.
  4. FQN Resolution: It converts the relative file paths into Fully Qualified Java Class Names:
    // Converts "/WEB-INF/classes/bobby/test/Student.class" to "bobby.test.Student"
    String relativePath = Paths.get(rootDir).relativize(classPath).toString();
    String className = relativePath.replace(File.separator, ".").replace(".class", "");
  5. Class Loading: Loads the class using reflection: Class.forName(className).

2. Prioritized Startup Tasks (@OnStartup)

Services can register background tasks to run immediately upon container initialization:

  1. Discovery: WebRock gathers all registered endpoints marked with @OnStartup.
  2. Sorting Execution Order: It filters out priority settings that are $\ge 0$ and sorts the startup list in ascending order: Collections.sort(startupServices, (left, right) -> left.getPriority() - right.getPriority());
  3. Reflection Invocation: The startup engine executes each task sequentially: serviceMethod.invoke(serviceClassObject, parametersValue);
    • Example: An endpoint marked with @OnStartup(priority=0) is executed before another marked with @OnStartup(priority=3).
  4. Limitations:
    • The startup method signature must return void (non-void methods are skipped).
    • Startup methods cannot take external query or payload arguments (only context scopes can be injected).

3. Automated PDF Catalog Generation (iText)

Upon completing the classpath scan, WebRock compiles a visual PDF handbook detailing the entire API framework. It is saved in the site root folder as [SITE_NAME]DOC.pdf (e.g. webrockDOC.pdf).

iText Document Structure:

Stack

Layer Technology
Language Java 8+
Core API Java Servlet API 4.0
Frontend JavaScript (ES6)
Build Tool Maven
Platform Apache Tomcat / Linux
PDF Engine iText
Data Format JSON (via Gson/Jackson equivalents)