# Conflicts: # .idea/workspace.xml # flash-extensions/flash-ext-view-jte/jte-classes/gg/jte/generated/ondemand/pages/JtehomeGenerated.class # flash-extensions/flash-ext-view-jte/src/main/java/dev/relism/flash/ext/view/jte/JteExtension.java
Flash
A high-performance HTTP/1.1 server library for Java 21, built around virtual threads and a zero-allocation FSM router.
Modules
| Module | Description |
|---|---|
flash |
Core server library — router, request parser, HTTP I/O transport |
flash-extensions/flash-ext-jackson |
Jackson JSON integration |
flash-extensions/flash-ext-openapi |
OpenAPI 3.0 spec + Swagger UI |
flash-extensions/flash-ext-oidc |
OIDC Authorization Code + PKCE flow |
flash-extensions/flash-ext-view-core |
Minimal shared SSR runtime primitives |
flash-extensions/flash-ext-view-jte |
Opinionated jte SSR extension |
flash-extensions/flash-ext-view-thymeleaf |
Opinionated Thymeleaf SSR extension |
flash-bench |
Demo harness (OIDC + OpenAPI + Jackson) |
Requirements
- Java 21+
- Maven 3.8+
Quick start
FlashApp.create(8080)
.get("/ping", (req, res) -> "pong")
.start();
With full configuration:
FlashApp.create(
FlashConfiguration.builder()
.port(8080)
.host("0.0.0.0")
.maxHeaderBufferSize(65536)
.build()
)
.get("/ping", (req, res) -> "pong")
.start();
Route registration
Lambda routes
FlashApp app = FlashApp.create(8080);
app.get("/hello", (req, res) -> "world");
app.post("/echo", (req, res) -> {
byte[] body = req.body().bytes();
return res.status(200).body(body);
});
app.get("/users/{id}", (req, res) -> {
String id = req.pathParam("id");
return "user:" + id;
});
Class-based handlers
Extend RequestHandler (or a subclass like JacksonHandler) and annotate with @Route:
@Route(method = HttpMethod.GET, path = "/api/users")
public class ListUsers extends JacksonHandler {
@Override
public Object handle(Request req, Response res) throws Exception {
return json(res, List.of("alice", "bob"));
}
}
// Register:
app.register(new ListUsers());
Middleware
Apply middleware via .with() on the RouteHandle returned by any registration call:
Middleware authCheck = next -> (req, res) -> {
if (req.header("Authorization") == null)
return res.status(401).body("Unauthorized");
return next.handle(req, res);
};
app.get("/secure", (req, res) -> "secret data")
.with(authCheck);
Multiple middlewares are composed outermost-first (left-to-right in the call):
app.get("/admin", handler).with(logging, auth, rateLimit);
// execution order: logging → auth → rateLimit → handler
Classpath scan
Scans a package for classes that extend RequestHandler and carry @Route. Each is
instantiated via its public no-arg constructor:
app.scan("dev.example.handlers");
Namespace mounting
Mount a scoped sub-router under a prefix. All routes registered inside the scope get the prefix prepended automatically. The scope inherits the parent's extension context (annotation processors, services):
app.mount("/api", scope -> {
scope.get("/health", (req, res) -> "ok"); // → GET /api/health
scope.register(new UserHandler()); // @Route(path="/users") → GET /api/users
scope.scan("dev.example.api");
});
Extensions
Extensions are installed before route registration. Each extension receives the FlashRegistrar
and FlashContext — it can register routes, expose services, and register annotation processors.
FlashApp.create(8080)
.install(new JacksonExtension())
.install(new OpenApiExtension("/openapi", "My API", "1.0.0"))
.install(new OidcExtension(oidcConfig))
.register(new MyHandler())
.start();
See extension-specific READMEs for full details:
Error handlers
app.onNotFound((req, res) -> res.status(404).body("Not found: " + req.path()));
app.onException((ex, req, res) -> {
if (ex instanceof IllegalArgumentException)
return res.status(400).body(ex.getMessage());
return res.status(500).body("Internal error");
});
FlashConfiguration
| Field | Default | Description |
|---|---|---|
port |
— | TCP port to bind |
host |
"0.0.0.0" |
Bind address |
maxHeaderBufferSize |
65536 |
Max size of the header buffer (bytes) |
Architecture
ServerSocket.accept()
→ RequestParser.parse() # zero-alloc header parsing, buffer reuse across keep-alive
→ GlobalRouter.route() # two-tier: mounted sub-routers (longest prefix) then FastPathRouterImpl
→ RequestHandler.handle() # user handler; return value sets body
→ Request.drain() # consume unread body for keep-alive
→ HttpServer writes response # status line, headers, then fixed or chunked body
→ loop or close socket # based on Connection header
- Virtual threads — each accepted socket runs on a virtual thread (
Executors.newVirtualThreadPerTaskExecutor()). Java 21 required. - Zero-allocation router —
FastPathRouterImplusesfpr-core, a byte-level FSM that matches onMETHOD + pathbytes with no per-request allocation. - Keep-alive —
RequestParserreuses its header buffer across requests on the same connection. - Chunked transfer — both chunked request bodies (decoded via
ChunkedInputStream) and chunked response bodies are supported.
Build & test
# Build all modules (skip tests)
mvn clean package -DskipTests
# Run all tests
mvn test
# Run a single test class
mvn test -pl flash -Dtest=RequestParserTest
# Run the benchmark demo server
java -jar flash-bench/target/flash-bench-1.0-SNAPSHOT.jar