enhanced HTTP server configuration and response handling; added acceptorThreads, improved header management, and refined error page titles

This commit is contained in:
Relism
2026-03-19 12:44:33 +01:00
parent 94d029a631
commit 96afbf665d
30 changed files with 1250 additions and 521 deletions
+16 -38
View File
@@ -1,4 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
@@ -11,10 +10,11 @@
</parent>
<artifactId>flash-bench</artifactId>
<packaging>jar</packaging>
<properties>
<jmh.version>1.37</jmh.version>
<maven.compiler.release>21</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<ahc.version>2.12.3</ahc.version>
</properties>
<dependencies>
@@ -26,39 +26,20 @@
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>${jmh.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.asynchttpclient</groupId>
<artifactId>async-http-client</artifactId>
<version>${ahc.version}</version>
</dependency>
</dependencies>
<build>
<finalName>flash-bench</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
<path>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>${jmh.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<!--
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
@@ -68,7 +49,12 @@
<phase>package</phase>
<goals><goal>shade</goal></goals>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>dev.relism.bench.Main</mainClass>
</transformer>
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
</transformers>
<filters>
<filter>
<artifact>*:*</artifact>
@@ -79,18 +65,10 @@
</excludes>
</filter>
</filters>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>dev.relism.bench.Main</mainClass>
</transformer>
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
-->
</plugins>
</build>
</project>
</project>
@@ -0,0 +1,176 @@
package dev.relism.bench;
import dev.relism.HttpServer;
import dev.relism.http.ContentType;
import dev.relism.http.HttpStatus;
import lombok.extern.slf4j.Slf4j;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.zip.GZIPOutputStream;
@Slf4j
public class StaticResourceManager {
private static final int GZIP_THRESHOLD = 1024;
private static final Map<String, ContentType> MIME_TYPES = new HashMap<>();
static {
MIME_TYPES.put("html", ContentType.TEXT_HTML);
MIME_TYPES.put("css", ContentType.TEXT_CSS);
MIME_TYPES.put("js", ContentType.TEXT_JAVASCRIPT);
MIME_TYPES.put("mjs", ContentType.TEXT_JAVASCRIPT);
MIME_TYPES.put("png", ContentType.IMAGE_PNG);
MIME_TYPES.put("jpg", ContentType.IMAGE_JPEG);
MIME_TYPES.put("jpeg", ContentType.IMAGE_JPEG);
MIME_TYPES.put("svg", ContentType.IMAGE_SVG);
MIME_TYPES.put("json", ContentType.JSON);
MIME_TYPES.put("ico", ContentType.BINARY);
MIME_TYPES.put("webp", ContentType.BINARY);
}
public record BakedAsset(byte[] raw, byte[] gzipped, ContentType contentType, boolean immutable, String etag) {}
private final Map<String, BakedAsset> cache = new ConcurrentHashMap<>();
private final String mountPath;
private BakedAsset indexFallback;
// Campi per le statistiche del report
private long totalRawSize = 0, totalCompressedSize = 0;
private int compressedCount = 0;
public StaticResourceManager(Path rootPath, String mountPath) {
this.mountPath = normalizeMountPath(mountPath);
long start = System.currentTimeMillis();
bakeAll(rootPath);
long duration = System.currentTimeMillis() - start;
// Richiamo del report qui
printReport(duration);
}
private String normalizeMountPath(String path) {
String p = path.startsWith("/") ? path : "/" + path;
return p.endsWith("/") ? p.substring(0, p.length() - 1) : p;
}
private void bakeAll(Path rootPath) {
if (!Files.exists(rootPath)) return;
try (var stream = Files.walk(rootPath)) {
stream.filter(Files::isRegularFile).forEach(file -> {
try {
String relative = rootPath.relativize(file).toString().replace("\\", "/");
byte[] raw = Files.readAllBytes(file);
byte[] gzipped = compressIfBeneficial(raw, relative);
String etag = "\"" + Integer.toHexString(Arrays.hashCode(raw)) + "-" + raw.length + "\"";
boolean isImmutable = relative.matches(".*\\.[a-f0-9]{8,}\\..*");
BakedAsset asset = new BakedAsset(raw, gzipped, resolveContentType(relative), isImmutable, etag);
cache.put(relative, asset);
// Aggiornamento statistiche
totalRawSize += raw.length;
totalCompressedSize += (gzipped != null) ? gzipped.length : raw.length;
if (gzipped != null) compressedCount++;
if ("index.html".equals(relative)) indexFallback = asset;
} catch (IOException ignored) {}
});
} catch (IOException ignored) {}
}
// --- IL METODO PRINTREPORT ---
private void printReport(long duration) {
double saved = totalRawSize > 0 ? (1.0 - (double)totalCompressedSize / totalRawSize) * 100 : 0;
long immutableCount = cache.values().stream().filter(BakedAsset::immutable).count();
System.out.println("\n" + "=".repeat(45));
System.out.printf("🚀 FLASH ASSET BAKE COMPLETE [%dms]\n", duration);
System.out.println("-".repeat(45));
System.out.printf("📦 Total Assets: %d\n", cache.size());
System.out.printf("🤐 Gzipped (On-Disk): %d\n", compressedCount);
System.out.printf("💾 RAM Usage: %.2f MB\n", (double)totalRawSize / (1024 * 1024));
System.out.printf("📉 Bandwidth Saving: %.1f%%\n", saved);
System.out.println("-".repeat(45));
System.out.println("🛡️ STRATEGIES ENABLED:");
System.out.println(" • ETag Validation: [ACTIVE] (304 Not Modified)");
System.out.printf(" • Immutable Assets: [%d files] (Cache: 1 year)\n", immutableCount);
System.out.println(" • SPA Fallback: [ENABLED] (Route -> index.html)");
System.out.println("=".repeat(45) + "\n");
}
public void register(HttpServer server) {
server.get(mountPath + "/**", (req, res) -> {
String path = req.getRequestLine().getPath().toString();
String subPath = path.substring(mountPath.length()).replaceFirst("^/", "");
BakedAsset asset = subPath.isEmpty() ? indexFallback : cache.get(subPath);
if (asset == null) {
if (!subPath.contains(".") && indexFallback != null) {
return serve(res, indexFallback, req);
}
return res.status(HttpStatus.NOT_FOUND);
}
return serve(res, asset, req);
});
}
private Object serve(dev.relism.models.Response res, BakedAsset asset, dev.relism.models.Request req) {
// Caching condizionale (ETag)
String ifNoneMatch = req.header("If-None-Match");
if (asset.etag().equals(ifNoneMatch)) {
return res.status(HttpStatus.NOT_MODIFIED);
}
res.type(asset.contentType());
res.header("ETag", asset.etag());
// Cache-Control Strategy
if (asset.contentType() == ContentType.TEXT_HTML) {
res.header("Cache-Control", "no-cache, must-revalidate");
} else if (asset.immutable()) {
res.header("Cache-Control", "public, max-age=31536000, immutable");
} else {
res.header("Cache-Control", "public, max-age=3600");
}
if (asset.gzipped() != null && acceptsGzip(req)) {
res.header("Content-Encoding", "gzip");
res.header("Vary", "Accept-Encoding");
return res.body(asset.gzipped());
}
return res.body(asset.raw());
}
private boolean acceptsGzip(dev.relism.models.Request req) {
String enc = req.header("Accept-Encoding");
return enc != null && enc.contains("gzip");
}
private byte[] compressIfBeneficial(byte[] data, String name) {
if (data.length < GZIP_THRESHOLD || isAlreadyCompressed(name)) return null;
try (var baos = new ByteArrayOutputStream(); var gzip = new GZIPOutputStream(baos)) {
gzip.write(data);
gzip.finish();
byte[] compressed = baos.toByteArray();
return compressed.length < (data.length * 0.9) ? compressed : null;
} catch (IOException e) { return null; }
}
private boolean isAlreadyCompressed(String n) {
n = n.toLowerCase();
return n.endsWith(".png") || n.endsWith(".jpg") || n.endsWith(".jpeg") || n.endsWith(".ico") || n.endsWith(".webp");
}
private ContentType resolveContentType(String filename) {
int dot = filename.lastIndexOf('.');
String ext = (dot > 0) ? filename.substring(dot + 1).toLowerCase() : "";
return MIME_TYPES.getOrDefault(ext, ContentType.BINARY);
}
}