DEV Community

sugaiketadao
sugaiketadao

Posted on

I built a lightweight Java framework for Japan's "SI" projects (third attempt in 10 years) #002 - URL Mapping

Introduction

In the previous article, I wrote about the background of creating the SIcore framework and how beginner-friendly design might also be AI-friendly (for tools like GitHub Copilot).

This time, I'll dive into one specific design choice: "URL maps directly to execution class name" — covering the concept through to the code-level implementation.

What This Article Covers

  • The URL mapping specification (rules)
  • What can be customized via configuration
  • Where and how the implementation works (Java code)
  • Benefits and considerations of this approach

Specification: URL Maps Directly to Class Name

In SIcore, we map URLs to execution classes (web services) through simple string replacement (plus root package prepending).

URL: http://localhost:8000/services/exmodule/ExampleListSearch
↓ Mapping
Execution class: com.example.app.service.exmodule.ExampleListSearch
Enter fullscreen mode Exit fullscreen mode

There are three key points:

  1. /services/ in the URL is the web service context path (configurable).
  2. The path after /services/ is converted to package and class names by replacing / with .
  3. A root package for web services (configurable) is prepended to form the fully qualified class name.

This eliminates the need for "routing configuration files," "annotation scanning," or "controller registration" — the URL alone determines the execution class.

Configuration: Context and Root Package

The configuration is in config/web.properties:

# JSON service context path
json.service.context=services
# JSON service root package
json.service.package=com.example.app.service
Enter fullscreen mode Exit fullscreen mode

By changing these settings, you can customize:

  • Change the /services part of the URL to a different context path
  • Change the base root package where execution classes are located

Implementation Overview: Where URLs Are Received

The web server uses JDK's standard com.sun.net.httpserver.HttpServer.
In src/com/onepg/web/StandaloneServer.java, we load the configuration and register JsonServiceHandler for the /services context.

// JSON service handler
final String jsonServiceContext = propMap.getString("json.service.context");
final String jsonServicePackage = propMap.getString("json.service.package");
this.server.createContext("/" + jsonServiceContext,
        new JsonServiceHandler(jsonServiceContext, jsonServicePackage));
Enter fullscreen mode Exit fullscreen mode

Implementation Details: URL → Class Name Generation

The method that builds the execution class name from the URL is in src/com/onepg/web/JsonServiceHandler.java:

private String buildClsNameByReq(final String reqPath) {
    return this.svcClsPackage + "."
            + reqPath.replace("/" + this.contextPath + "/", "").replace("/", ".");
}
Enter fullscreen mode Exit fullscreen mode

For example, when reqPath is /services/exmodule/ExampleListSearch:

  • replace("/services/", "")exmodule/ExampleListSearch
  • replace("/", ".")exmodule.ExampleListSearch
  • Prepend com.example.app.service.

This generates the fully qualified class name: com.example.app.service.exmodule.ExampleListSearch

Implementation Details: Execution via Reflection

Once the class name is determined, we instantiate and execute it via reflection:

final Class<?> cls = Class.forName(clsName);
final Object clsObj = cls.getDeclaredConstructor().newInstance();

if (!(clsObj instanceof AbstractWebService)) {
    throw new RuntimeException(
            "Classes not inheriting from web service base class (AbstractWebService) cannot be executed. ");
}

((AbstractWebService) clsObj).execute(io);
Enter fullscreen mode Exit fullscreen mode

By checking inheritance of AbstractWebService (base class), we ensure that "unrelated classes cannot be accidentally executed."

Request Handling: GET Uses Query, POST Uses JSON

The framework processes parameters differently based on the HTTP method:

if ("GET".equals(reqMethod)) {
    final String query = exchange.getRequestURI().getQuery();
    io.putAllByUrlParam(query);
} else if ("POST".equals(reqMethod)) {
    final String body = ServerUtil.getRequestBody(exchange);
    io.putAllByJson(body);
}
Enter fullscreen mode Exit fullscreen mode

On the browser side (JavaScript), we use HttpUtil.callJsonService('/services/...', req) for POST and HttpUtil.movePage('/services/...', req) for GET.
Note: For GET, req is appended as URL parameters (query string) because the server reads exchange.getRequestURI().getQuery().

Benefits of This Approach

  • No routing configuration needed → Fewer settings/annotations, easier for both writers and readers
  • URL alone reveals the execution class → Faster investigation and maintenance
  • Simple execution flow → Easier for beginners to trace
  • Clear conventions → Less confusion for AI (like GitHub Copilot) during code generation

Considerations (Future Improvements)

  • Reflection usage means optimization work is needed for high-frequency access paths (e.g., caching class resolution and constructor retrieval)
  • URL-accessible range ≈ public API, so careful package design and public/private boundaries are important

Conclusion

This article covered the internals of the "URL maps directly to class name" rule and its implementation (configuration → context registration → class resolution → execution).

Next time, I'll write about "JSON-centric design."

Links


Thank you for reading!
I'd appreciate it if you could give it a ❤️!

Top comments (0)