enhanced router middleware support; added pre-fused middleware handling and improved handler registration

This commit is contained in:
Relism
2026-03-19 21:35:54 +01:00
parent 16b5f8ac15
commit 9a30c5ab16
10 changed files with 204 additions and 85 deletions
+20 -17
View File
@@ -6,6 +6,7 @@ import dev.relism.http.HttpStatus;
import dev.relism.models.*;
import dev.relism.routing.AbstractRouter;
import dev.relism.routing.GlobalRouter;
import dev.relism.routing.Middleware;
import lombok.extern.slf4j.Slf4j;
@@ -29,7 +30,7 @@ import java.util.concurrent.TimeUnit;
public class HttpServer {
private final HttpServerConfiguration configuration;
private final ServerSocket serverSocket;
private final GlobalRouter globalRouter = new GlobalRouter();
private final GlobalRouter globalRouter;
private final ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor();
private final Set<Socket> activeSockets = ConcurrentHashMap.newKeySet();
private volatile boolean stopped = false;
@@ -52,9 +53,14 @@ public class HttpServer {
DIGITS[i] = String.valueOf(i).getBytes(StandardCharsets.UTF_8);
}
public HttpServer(HttpServerConfiguration configuration) throws IOException {
/**
* Creates the server with optional server-level middlewares applied to every registered route.
* Middlewares are pre-fused at construction time; the first element executes outermost.
*/
public HttpServer(HttpServerConfiguration configuration, Middleware... middlewares) throws IOException {
this.configuration = configuration;
this.serverSocket = new ServerSocket(configuration.getPort());
this.serverSocket = new ServerSocket(configuration.getPort());
this.globalRouter = new GlobalRouter(middlewares);
}
/** Returns a future that completes once the accept loop is running and the server is ready. */
@@ -119,21 +125,18 @@ public class HttpServer {
return this;
}
public HttpServer register(RequestHandler handler) {
globalRouter.register(handler);
return this;
}
public HttpServer register(RequestHandler handler, Middleware... m) { globalRouter.register(handler, m); return this; }
public HttpServer get(String path, SimpleHandler.FunctionalHandler h) { globalRouter.get(path, h); return this; }
public HttpServer post(String path, SimpleHandler.FunctionalHandler h) { globalRouter.post(path, h); return this; }
public HttpServer put(String path, SimpleHandler.FunctionalHandler h) { globalRouter.put(path, h); return this; }
public HttpServer delete(String path, SimpleHandler.FunctionalHandler h) { globalRouter.delete(path, h); return this; }
public HttpServer patch(String path, SimpleHandler.FunctionalHandler h) { globalRouter.patch(path, h); return this; }
public HttpServer options(String path, SimpleHandler.FunctionalHandler h) { globalRouter.options(path, h); return this; }
public HttpServer head(String path, SimpleHandler.FunctionalHandler h) { globalRouter.head(path, h); return this; }
public HttpServer trace(String path, SimpleHandler.FunctionalHandler h) { globalRouter.trace(path, h); return this; }
public HttpServer connect(String path, SimpleHandler.FunctionalHandler h) { globalRouter.connect(path, h); return this; }
public HttpServer purge(String path, SimpleHandler.FunctionalHandler h) { globalRouter.purge(path, h); return this; }
public HttpServer get (String path, SimpleHandler.FunctionalHandler h, Middleware... m) { globalRouter.get (path, h, m); return this; }
public HttpServer post (String path, SimpleHandler.FunctionalHandler h, Middleware... m) { globalRouter.post (path, h, m); return this; }
public HttpServer put (String path, SimpleHandler.FunctionalHandler h, Middleware... m) { globalRouter.put (path, h, m); return this; }
public HttpServer delete (String path, SimpleHandler.FunctionalHandler h, Middleware... m) { globalRouter.delete (path, h, m); return this; }
public HttpServer patch (String path, SimpleHandler.FunctionalHandler h, Middleware... m) { globalRouter.patch (path, h, m); return this; }
public HttpServer options(String path, SimpleHandler.FunctionalHandler h, Middleware... m) { globalRouter.options(path, h, m); return this; }
public HttpServer head (String path, SimpleHandler.FunctionalHandler h, Middleware... m) { globalRouter.head (path, h, m); return this; }
public HttpServer trace (String path, SimpleHandler.FunctionalHandler h, Middleware... m) { globalRouter.trace (path, h, m); return this; }
public HttpServer connect(String path, SimpleHandler.FunctionalHandler h, Middleware... m) { globalRouter.connect(path, h, m); return this; }
public HttpServer purge (String path, SimpleHandler.FunctionalHandler h, Middleware... m) { globalRouter.purge (path, h, m); return this; }
private void process(Socket socket) {
@@ -8,7 +8,10 @@ import java.util.List;
/**
* Lazy query parameter access ({@code ?key=value&...}). Backed by a zero-copy {@link ByteView}
* over the raw query string bytes, no parsing at construction, values decoded on demand.
* over the raw query string bytes, no parsing at construction, values percent-decoded on demand.
*
* <p>Percent-decoding ({@code %XX} → byte, {@code +} → space) follows the
* {@code application/x-www-form-urlencoded} convention used by all browsers.
*/
public class QueryParams {
public static final QueryParams EMPTY = new QueryParams(null);
@@ -22,10 +25,7 @@ public class QueryParams {
public String get(String name) {
long r = findFirst(name);
if (r < 0) return null;
int s = (int) (r >> 32), l = (int) (r & 0xFFFFFFFFL);
byte[] bytes = new byte[l];
for (int i = 0; i < l; i++) bytes[i] = raw.byteAt(s + i);
return new String(bytes, StandardCharsets.UTF_8);
return decode((int) (r >> 32), (int) (r & 0xFFFFFFFFL));
}
ByteView view(String name) {
@@ -51,11 +51,8 @@ public class QueryParams {
int valStart = i;
while (i < len && raw.byteAt(i) != '&') i++;
if (keyMatches(keyStart, keyLen, name)) {
int valLen = i - valStart;
byte[] bytes = new byte[valLen];
for (int j = 0; j < valLen; j++) bytes[j] = raw.byteAt(valStart + j);
if (result == null) result = new ArrayList<>();
result.add(new String(bytes, StandardCharsets.UTF_8));
result.add(decode(valStart, i - valStart));
}
}
if (i < len && raw.byteAt(i) == '&') i++;
@@ -63,6 +60,8 @@ public class QueryParams {
return result != null ? result : List.of();
}
// ── Internals ─────────────────────────────────────────────────────────────
/** Returns (valStart << 32) | valLen, or -1 if not found. */
private long findFirst(String name) {
if (raw == null) return -1L;
@@ -87,4 +86,39 @@ public class QueryParams {
for (int i = 0; i < len; i++) if (raw.byteAt(start + i) != (byte) name.charAt(i)) return false;
return true;
}
/**
* Percent-decodes a value slice from {@code raw} into a UTF-8 String.
* {@code %XX} triplets are decoded to their byte values; {@code +} decodes as space.
* Invalid {@code %} sequences are passed through as-is.
*/
private String decode(int start, int length) {
byte[] out = new byte[length]; // upper bound — decoded is never longer
int w = 0;
for (int i = 0; i < length; i++) {
byte b = raw.byteAt(start + i);
if (b == '+') {
out[w++] = ' ';
} else if (b == '%' && i + 2 < length) {
int hi = hexVal(raw.byteAt(start + i + 1));
int lo = hexVal(raw.byteAt(start + i + 2));
if (hi >= 0 && lo >= 0) {
out[w++] = (byte) ((hi << 4) | lo);
i += 2;
} else {
out[w++] = b; // not a valid escape — keep literal %
}
} else {
out[w++] = b;
}
}
return new String(out, 0, w, StandardCharsets.UTF_8);
}
private static int hexVal(byte b) {
if (b >= '0' && b <= '9') return b - '0';
if (b >= 'a' && b <= 'f') return b - 'a' + 10;
if (b >= 'A' && b <= 'F') return b - 'A' + 10;
return -1;
}
}
@@ -112,12 +112,6 @@ public class Response {
public boolean isStreaming() { return stream != null; }
/**
* Pre-encoded status bytes (e.g. {@code "200 OK"}), or {@code null} if the status was set
* via {@link #status(int)} — in which case {@link HttpStatus#bytesForCode} is used as fallback.
*/
public byte[] getStatusBytes() { return statusBytes; }
// -------------------------------------------------------------------------
// Internal setters used by HttpServer for handler return values
// -------------------------------------------------------------------------
@@ -12,9 +12,16 @@ import lombok.Getter;
import java.nio.charset.StandardCharsets;
/**
* Base router. Each router has a namespace prefix (default {@code "/"});
* Base router. Each router has a namespace prefix (default {@code "/"}).
* Error handlers are scoped to this router.
* The namespace is set automatically by {@link GlobalRouter#mount}.
*
* <p>Router-level {@link Middleware middlewares} are passed at construction time and
* pre-fused into a single wrapper applied to every handler registered on this router.
* Handler-level middlewares are passed to the {@link #register} / {@link #get} family of
* methods and wrapped <em>inside</em> the router-level chain.
*
* <p>All middleware composition happens once at boot time; the hot-path sees only a plain
* {@link RequestHandler} call with zero allocation and zero lookup overhead.
*/
public abstract class AbstractRouter {
@@ -24,6 +31,12 @@ public abstract class AbstractRouter {
@Getter(AccessLevel.PACKAGE)
protected byte[] namespaceBytes = new byte[]{ '/' };
/**
* Pre-fused router-level middleware, or {@code null} when none were registered.
* A single null-check in {@link #compile} is the only cost when no router middleware exists.
*/
private final Middleware routerMiddleware;
protected SimpleHandler notFoundHandler = new SimpleHandler((req, res) -> {
res.setStatusCode(404);
res.setContentType(ContentType.TEXT_HTML);
@@ -36,6 +49,20 @@ public abstract class AbstractRouter {
return ErrorPages.renderException(req, ex);
};
/**
* Constructs a router with optional router-level middlewares.
* Middlewares are pre-fused at construction time; the first element executes outermost.
*
* @param middlewares zero or more middlewares applied to every handler on this router
*/
protected AbstractRouter(Middleware... middlewares) {
this.routerMiddleware = middlewares.length == 0 ? null
: middlewares.length == 1 ? middlewares[0]
: Middleware.of(middlewares);
}
// ── Internal wiring ───────────────────────────────────────────────────────
SimpleHandler getNotFoundHandler() { return notFoundHandler; }
ExceptionHandler getExceptionHandler() { return exceptionHandler; }
void setNamespace(String namespace) {
@@ -43,6 +70,30 @@ public abstract class AbstractRouter {
this.namespaceBytes = namespace.getBytes(StandardCharsets.UTF_8);
}
/**
* Compiles a handler with its middleware chain. Called once per handler at registration.
*
* <p>Application order (innermost → outermost):
* <ol>
* <li>handler-level middlewares (passed at the call site)</li>
* <li>router-level middleware (set in the constructor)</li>
* </ol>
*/
private RequestHandler compile(RequestHandler handler, Middleware[] handlerMiddlewares) {
RequestHandler compiled = handler;
// handler-level: wrap from innermost outward; box FunctionalHandler → SimpleHandler once per layer
for (int i = handlerMiddlewares.length - 1; i >= 0; i--)
compiled = new SimpleHandler(handlerMiddlewares[i].wrap(compiled));
// router-level: outermost wrapper, executes first
if (routerMiddleware != null)
compiled = new SimpleHandler(routerMiddleware.wrap(compiled));
return compiled;
}
private static final Middleware[] NO_MIDDLEWARES = new Middleware[0];
// ── Error handler configuration ───────────────────────────────────────────
public AbstractRouter onNotFound(SimpleHandler.FunctionalHandler handler) {
this.notFoundHandler = new SimpleHandler(handler);
return this;
@@ -53,36 +104,54 @@ public abstract class AbstractRouter {
return this;
}
// ── Lambda handler registration ───────────────────────────────────────────
/**
* Registers a route relative to this router's namespace. Return a {@link Response} to replace
* it entirely, any other non-null value to set it as the body, or {@code null} to leave it unchanged.
* Registers a lambda handler with optional handler-level middlewares.
* Router-level middlewares are applied automatically on top.
*/
public AbstractRouter register(HttpMethod method, String path, SimpleHandler.FunctionalHandler handler) {
return addRoute(method, PathUtils.sanitize(path), new SimpleHandler(handler));
public AbstractRouter register(HttpMethod method, String path,
SimpleHandler.FunctionalHandler handler,
Middleware... middlewares) {
return addRoute(method, PathUtils.sanitize(path),
compile(new SimpleHandler(handler), middlewares));
}
public AbstractRouter get(String path, SimpleHandler.FunctionalHandler h) { return register(HttpMethod.GET, path, h); }
public AbstractRouter post(String path, SimpleHandler.FunctionalHandler h) { return register(HttpMethod.POST, path, h); }
public AbstractRouter put(String path, SimpleHandler.FunctionalHandler h) { return register(HttpMethod.PUT, path, h); }
public AbstractRouter delete(String path, SimpleHandler.FunctionalHandler h) { return register(HttpMethod.DELETE, path, h); }
public AbstractRouter patch(String path, SimpleHandler.FunctionalHandler h) { return register(HttpMethod.PATCH, path, h); }
public AbstractRouter options(String path, SimpleHandler.FunctionalHandler h) { return register(HttpMethod.OPTIONS, path, h); }
public AbstractRouter head(String path, SimpleHandler.FunctionalHandler h) { return register(HttpMethod.HEAD, path, h); }
public AbstractRouter trace(String path, SimpleHandler.FunctionalHandler h) { return register(HttpMethod.TRACE, path, h); }
public AbstractRouter connect(String path, SimpleHandler.FunctionalHandler h) { return register(HttpMethod.CONNECT, path, h); }
public AbstractRouter purge(String path, SimpleHandler.FunctionalHandler h) { return register(HttpMethod.PURGE, path, h); }
public AbstractRouter get (String path, SimpleHandler.FunctionalHandler h, Middleware... m) { return register(HttpMethod.GET, path, h, m); }
public AbstractRouter post (String path, SimpleHandler.FunctionalHandler h, Middleware... m) { return register(HttpMethod.POST, path, h, m); }
public AbstractRouter put (String path, SimpleHandler.FunctionalHandler h, Middleware... m) { return register(HttpMethod.PUT, path, h, m); }
public AbstractRouter delete (String path, SimpleHandler.FunctionalHandler h, Middleware... m) { return register(HttpMethod.DELETE, path, h, m); }
public AbstractRouter patch (String path, SimpleHandler.FunctionalHandler h, Middleware... m) { return register(HttpMethod.PATCH, path, h, m); }
public AbstractRouter options(String path, SimpleHandler.FunctionalHandler h, Middleware... m) { return register(HttpMethod.OPTIONS, path, h, m); }
public AbstractRouter head (String path, SimpleHandler.FunctionalHandler h, Middleware... m) { return register(HttpMethod.HEAD, path, h, m); }
public AbstractRouter trace (String path, SimpleHandler.FunctionalHandler h, Middleware... m) { return register(HttpMethod.TRACE, path, h, m); }
public AbstractRouter connect(String path, SimpleHandler.FunctionalHandler h, Middleware... m) { return register(HttpMethod.CONNECT, path, h, m); }
public AbstractRouter purge (String path, SimpleHandler.FunctionalHandler h, Middleware... m) { return register(HttpMethod.PURGE, path, h, m); }
/** Registers a class-based handler; the class must be annotated with {@link Route @Route}. */
public AbstractRouter register(RequestHandler handler) {
// ── Class-based handler registration ─────────────────────────────────────
/**
* Registers a class-based handler with optional handler-level middlewares.
* The class must carry a {@link Route @Route} annotation declaring method and path.
* Router-level middlewares are applied automatically on top.
*/
public AbstractRouter register(RequestHandler handler, Middleware... middlewares) {
Route annotation = handler.getClass().getAnnotation(Route.class);
if (annotation != null) {
addRoute(HttpMethod.valueOf(annotation.method()), annotation.path(), handler);
}
if (annotation != null)
addRoute(annotation.method(), annotation.path(),
compile(handler, middlewares));
return this;
}
// ── Routing ───────────────────────────────────────────────────────────────
public abstract RequestHandler route(Request request);
/**
* Stores a pre-compiled handler in the underlying routing structure.
* The handler passed here is already fully wrapped — implementations must not
* apply any additional middleware logic.
*/
protected abstract AbstractRouter addRoute(HttpMethod method, String path, RequestHandler handler);
protected static void setPathParams(Request request, String[] names, ByteView source, int[] starts, int[] lens) {
@@ -20,7 +20,14 @@ import java.util.Map;
public class GlobalRouter extends AbstractRouter {
private final Map<String, AbstractRouter> subRoutersMap = new HashMap<>();
private final List<AbstractRouter> sortedSubRouters = new ArrayList<>();
private final AbstractRouter internalRouter = new FastPathRouterImpl();
private final AbstractRouter internalRouter;
public GlobalRouter(Middleware... middlewares) {
super(middlewares);
this.internalRouter = new FastPathRouterImpl();
}
public GlobalRouter() { this(new Middleware[0]); }
public void mount(String namespace, AbstractRouter router) {
String sanitized = PathUtils.sanitize(namespace);
@@ -1,5 +1,7 @@
package dev.relism.routing;
import dev.relism.http.HttpMethod;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -17,6 +19,6 @@ import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Route {
String method() default "GET";
HttpMethod method() default HttpMethod.GET;
String path();
}
@@ -9,6 +9,7 @@ import dev.relism.http.HttpMethod;
import dev.relism.models.Request;
import dev.relism.models.RequestHandler;
import dev.relism.routing.AbstractRouter;
import dev.relism.routing.Middleware;
import dev.relism.routing.PathUtils;
/**
@@ -22,6 +23,9 @@ public class FastPathRouterImpl extends AbstractRouter {
private volatile FastPathRouter<ByteView, RequestHandler> router;
private String[] cachedParamNames;
public FastPathRouterImpl(Middleware... middlewares) { super(middlewares); }
public FastPathRouterImpl() { super(); }
private static final class FastPathRouterContext {
private static final ThreadLocal<MatchResult<RequestHandler>> RESULT_HOLDER =
ThreadLocal.withInitial(() -> new MatchResult<>(32, 128));