TinyServer — A lightweight Java HTTP server

A lightweight Java HTTP server with servlets, JSP, JSON, and dependency injection.

TinyServer is a lightweight Java HTTP server that supports GET and POST requests, servlet-based request handling, minimal JSP templating, JSON parsing and serialization, dependency injection, and pluggable logging. It requires Java 21+ as it uses virtual threads.


1. Starting the server

From the command line

# With a config file (recommended)
java -cp server.jar:web.jar com.tinyserver.Main /path/to/config.txt

# Without a config file — starts on port 8080 with defaults
java -cp server.jar:web.jar com.tinyserver.Main

See the buid_jars.sh shell script on how to build the above mentioned jars.

Shutdown

The server installs a JVM shutdown hook. Press Ctrl-C or send SIGTERM — the log thread is flushed before the process exits.


2. Configuration file

The config file is a plain-text file. Each line is key value (whitespace-separated). Blank lines and lines beginning with # are ignored.

# ExampleWebApp/src/config.txt

port 8080
max_req_len 1000000
use_virtual_threads true
log_file_dir /Users/agent07/server

# Static HTML files
html_dir /Users/agent07/server/html

# JSP files
jsp_dir /Users/agent07/server/jsp
jsp_class_path server.jar:web.jar
always_compile_jsps true

# Servlet mappings  (map <url-path> <fully-qualified-class>)
map /testjson com.example.web.JsonServlet
map /TestServlet com.example.web.TestServlet

All config keys

KeyTypeDefaultDescription
portint8080TCP port to listen on
max_req_lenint100 000 000Maximum POST body size in bytes
use_virtual_threadsboolfalseUse Java 21 virtual threads per request
log_file_dirpathDirectory for daily rotating log files. If absent, logs go to stdout
html_dirpathDirectory for static files. Requests that don't match a servlet are served from here
jsp_dirpathRoot directory for .jsp files
jsp_class_pathstringserver.jarClasspath passed to the JSP compiler (colon-separated jars)
always_compile_jspsboolfalseRecompile the JSP on every request instead of caching the compiled class
mapMaps a URL path to a servlet class (see §3)
injectRegisters a dependency injection binding (see §9)
static_injectLike inject but creates a single shared instance (see §9)
janitor_schedule_minutesint720How often the janitor thread runs (see §11)
anything elsestringStored in the servlet context as key → value, readable via Servlet.getFromContext(key)

3. Writing a servlet

All servlets extend com.tinyserver.servlet.Servlet and implement a single method:

public void service(HTTPRequest request, HTTPResponse response)

Minimal example — JSON response

// ExampleWebApp/src/com/example/web/JsonServlet.java

package com.example.web;

import com.tinyserver.servlet.*;
import com.tinyserver.servlet.HTTPResponse.HTTPRetCode;
import java.io.File;

class RuntimeValues {
    public long maxMemoryM;
    public long usedMemoryM;
    public long noThreads;
    public long noVirtualThreads;
    public long queriesPerMinute;
    public long freeDiskM;
}

public class JsonServlet extends Servlet {

    @Override
    public void service(HTTPRequest request, HTTPResponse response) {
        response.setContentType("application/json");
        try {
            RuntimeValues varz = new RuntimeValues();
            varz.maxMemoryM  = Runtime.getRuntime().maxMemory()  / 1_000_000;
            varz.usedMemoryM = Runtime.getRuntime().totalMemory()/ 1_000_000;
            varz.noThreads   = Thread.activeCount();
            varz.freeDiskM   = new File("/").getUsableSpace() / 1_000_000;

            response.getOutputStream().writeBytes(JsonSerializer.toJson(varz).getBytes());
        } catch (Exception e) {
            Log.error(e.toString());
            response.getOutputStream().writeBytes(e.toString().getBytes());
            response.setReturnCode(HTTPRetCode._500);
        }
    }
}

Register it in config.txt:

map /testjson com.example.web.JsonServlet

Now GET /testjson returns a JSON object.

Minimal example — form handler that forwards to a JSP

// ExampleWebApp/src/com/example/web/TestServlet.java

public class TestServlet extends Servlet {

    @Override
    public void service(HTTPRequest request, HTTPResponse response) {
        String name = request.parameters.get("name");           // form field
        request.attributes.put("msg", "Hello, " + name + "!"); // pass data to JSP
        request.forwardTo("/jsp/Test.jsp", response);           // render JSP
    }
}

Register it:

map /TestServlet com.example.web.TestServlet

4. Writing a JSP

JSP files live in jsp_dir and are served under /jsp/<filename>. The compiler supports three tag types:

TagPurpose
<%! ... %>Declaration — Java method or field placed outside service()
<%= ... %>Expression — value written to the response with out.print(...)
<% ... %>Scriptlet — raw Java statements inside service()

The compiled class has access to request (HTTPRequest) and response (HTTPResponse) automatically.

Example — Test.jsp

<%!
  // Declaration: helper method available to all scriptlets/expressions on this page
  java.lang.String getItem(int indx) {
      return new com.example.web.TestClass().getMessage() + indx;
  }
%>

<!DOCTYPE html>
<html>
<head><title>Hello Tiny JSP</title></head>
<body>

  <%-- Expression: evaluated and written inline --%>
  <p>Server time: <%= (new java.util.Date()).toLocaleString() %></p>

  <%-- Reading a request attribute set by a servlet --%>
  <b><%= request.attributes.get("msg") == null ? "" : request.attributes.get("msg") %></b>

  <%-- Scriptlet: loop --%>
  <% for (int i = 0; i < 3; i++) { %>
      <p><%= getItem(i) %></p>
  <% } %>

</body>
</html>

JSPs are compiled on first request and cached unless always_compile_jsps true is set in config.


5. Forwarding between servlets and JSPs

A servlet can hand off to another servlet or JSP using request.forwardTo(path, response). The same HTTPRequest object is reused, so attributes set before the forward are available in the target.

// Set data in the originating servlet
request.attributes.put("msg", "Hello from TestServlet " + name + "!");

// Forward to a JSP — note the leading /jsp/ prefix
request.forwardTo("/jsp/Test.jsp", response);

Inside the JSP, read the attribute:

<%= request.attributes.get("msg") %>

6. HTTPRequest reference

com.tinyserver.servlet.HTTPRequest

MemberTypeDescription
headersHashtable<String,String>HTTP request headers (lower-cased keys)
parametersHashtable<String,String>Query-string (GET) or form (POST application/x-www-form-urlencoded) parameters
attributesHashtable<String,String>Arbitrary key/value pairs for inter-servlet communication
contentchar[]Raw POST body
filesHashtable<String, Hashtable<String,byte[]>>Uploaded files (multipart — reserved, not yet implemented)
getContent()Stringcontent as a String
getMethod()StringThe URL path, e.g. /TestServlet
forwardTo(path, response)voidDispatch to another servlet or JSP
getServlet(path)ServletResolve a path to its Servlet instance (rarely called directly)

7. HTTPResponse reference

com.tinyserver.servlet.HTTPResponse

MethodDescription
getOutputStream()ByteArrayOutputStream — write the response body here
setContentType(String)Default is "text/html". Use "application/json" for JSON responses
setCharacterEncoding(String)Default is "utf-8"
setReturnCode(HTTPRetCode)Set HTTP status. Default is _200
addHeader(String name, String value)Add an arbitrary response header

Available status codes (HTTPRetCode enum)

_200, _400, _404, _411, _413, _429, _500, _501, _505


8. JSON serialization

com.tinyserver.servlet.JsonSerializer converts plain Java objects to/from JSON using reflection. All fields must be one of: String, int, long, BigInteger, or List<String | Integer | Long | T> where T is another serializable type.

Serializing

RuntimeValues varz = new RuntimeValues();
varz.maxMemoryM = 512L;

String json = JsonSerializer.toJson(varz);
// → {"object":"RuntimeValues","maxMemoryM":512,...}

The "object" field is always emitted first and contains the simple class name. It is used during deserialization for type checking.

Deserializing

MyClass obj = new MyClass();
boolean ok = JsonSerializer.getObjFrom(jsonString, obj);

Returns false if any field could not be set.

Limitations


9. Dependency injection

TinyServer has a simple field-level DI system. Annotate a field in your servlet with @Inject:

import com.tinyserver.servlet.Inject;

public class MyServlet extends Servlet {

    @Inject
    MyRepository repo;   // injected before service() is called

    @Override
    public void service(HTTPRequest request, HTTPResponse response) {
        repo.doSomething();
    }
}

Register the concrete implementation in config.txt:

# inject <concrete-class> <interface-or-base-class>
inject com.myapp.SqlRepository com.myapp.MyRepository

# static_inject — same but creates one shared instance (singleton)
static_inject com.myapp.SqlRepository com.myapp.MyRepository

10. Logging

com.tinyserver.servlet.Log is a static, async logger.

Log.debug("Detailed diagnostic info");
Log.info("Mapped /foo to com.example.FooServlet");
Log.error("Something went wrong: " + e.getMessage());
Log.printStackTrace(exception);

Log output

Log format

2026-04-06 10:23:45,123    INFO    HTTPRequest method: /TestServlet

Input sanitization

Log.sanitize(msg) strips \n and \r from log messages to prevent log injection. It is called automatically for all Log.info() / Log.debug() / Log.error() calls.


11. Janitor thread

The janitor thread lets you run periodic background work (database cleanup, cache expiry, etc.). Implement com.tinyserver.servlet.JanitorThread and register it as a static_inject:

public class MyJanitor extends JanitorThread {
    @Override
    public void doRecurringWork() {
        Log.info("Janitor: cleaning up...");
        // delete old records, compact caches, etc.
    }
}
static_inject com.myapp.MyJanitor com.tinyserver.servlet.JanitorThread
janitor_schedule_minutes 60

doRecurringWork() is called immediately at startup and then every janitor_schedule_minutes minutes.


12. Async requests from the browser (JSP example)

Fetch a servlet response asynchronously and show it in the page, or in an alert:

<!-- Show raw response text in an alert (fetches /varz) -->
<button onclick="fetchVarzAlert()">Async</button>

<!-- Render JSON response as a table -->
<button onclick="fetchVarz()">Get JSON</button>

<script>
  async function fetchVarzAlert() {
    try {
      const response = await fetch('/varz', {
        method: 'GET',
        headers: { 'Accept': 'application/json' }
      });
      const text = await response.text();
      alert(text);
    } catch (error) {
      alert(`Error fetching /varz:\n${error.message}`);
    }
  }

  async function fetchVarz() {
    try {
      const response = await fetch('/testjson', {
        method: 'GET',
        headers: { 'Accept': 'application/json' }
      });
      if (!response.ok) throw new Error(`HTTP ${response.status}`);
      const data = await response.json();
      renderTable(data);
    } catch (error) {
      alert(`Error: ${error.message}`);
    }
  }
</script>

Note: unless the header is already set via response.getHeaders().put(ACCESS_CONTROL_ALLOW_ORIGIN, "whatever"), TinyServer sends Access-Control-Allow-Origin: * on all servlet responses automatically, so cross-origin fetch calls from the same host always succeed.


13. Project layout

TinyServer/src/
  com/tinyserver/
    Main.java               Entry point — reads config, starts server, installs shutdown hook
    TinyServer.java         Concrete server — dispatches requests to servlets / static files
    ServerEngine.java       Abstract base — HTTP parsing (GET/POST), socket accept loop
    Config.java             Config file parser, servlet registry, DI container
    MimeUtils.java          Maps file extensions to MIME types

  com/tinyserver/servlet/
    Servlet.java            Base class for all servlets (also holds servlet context)
    HTTPRequest.java        Parsed request: headers, parameters, attributes, body
    HTTPResponse.java       Response builder: status, content-type, headers, body stream
    JspCompiler.java        Compiles .jsp files to Servlet subclasses at runtime
    JsonSerializer.java     Reflection-based JSON serializer / deserializer
    JsonParser.java         Low-level JSON stream parser
    JsonObj.java            JSON value types (string, number, array, object)
    JsonListener.java       Callback interface for JsonParser
    Inject.java             @Inject field annotation for DI
    JanitorThread.java      Abstract base for periodic background tasks
    Log.java                Async logger with daily-rotating file output
    MimeUtils.java          (servlet package copy)
    MySStream.java          Internal byte-stream helper
    ReadWriteLock.java      Reentrant read/write lock used internally

ExampleWebApp/src/
  com/example/web/
    JsonServlet.java        Example: returns runtime metrics as JSON (maps to /testjson)
    TestServlet.java        Example: reads a form field, sets an attribute, forwards to JSP
    TestClass.java          Helper used by Test.jsp

ExampleWebApp/jsp/
  Test.jsp                  Example JSP: date expression, form submit, async fetch, scriptlet loop

ExampleWebApp/src/config.txt  Canonical example config file
SecBot Support
Questions about TinyServer? Ask me anything!