1.1 Zielsetzung und Abgrenzung zum Architekturteil
Im ersten Teil dieser Serie stand die Theorie im Vordergrund: Wir haben geklärt, warum ein URL-Shortener nicht nur ein Komfortwerkzeug, sondern ein sicherheitsrelevantes Element digitaler Infrastruktur ist. Wir haben Modelle zur Kollisionserkennung, Entropieverteilung und Weiterleitungslogik diskutiert sowie architekturelle Varianten analysiert – von zustandslosen Redirect-Services bis hin zu domänenspezifischen Validierungsmechanismen.
Dieser zweite Teil wendet sich nun der konkreten Umsetzung zu. Wir entwickeln eine erste lauffähige Version eines URL-Shorteners in Java 24 – bewusst ohne den Einsatz von Frameworks wie Spring Boot oder Jakarta EE. Ziel ist eine transparente, modular strukturierte Lösung, die alle Kernfunktionen bereitstellt: das Verkürzen von URLs, das sichere Speichern von Mappings, das Weiterleiten über HTTP und die optionale Darstellung über eine Vaadin-basierte Benutzeroberfläche.
Table of Contents
1.2 Technologische Leitplanken: WAR, Vaadin, Core JDK
Die Implementierung orientiert sich an einem modernen, aber bewusst schlanken Technologiestack. Zum Einsatz kommen ausschließlich die Bordmittel des JDK sowie Vaadin Flow als UI-Framework. Die Entscheidung für Vaadin basiert auf dem Anspruch, interaktive Verwaltungsoberflächen ohne zusätzliches JavaScript oder separate Frontend-Logik umsetzen zu können – vollständig in Java.
Das Projekt wird als Multi-Modul-Struktur aufgesetzt, wobei die Trennung zwischen Kernlogik, API-Schicht und UI auch im Code sichtbar und wartbar bleibt. Als Build-Tool wird Maven eingesetzt, ergänzt um ein WAR-Packaging-Plugin, das eine klassische Servlet-Deployment-Struktur erzeugt. Die Verwendung von Java 24 erlaubt es, moderne Sprachmittel wie Records, pattern matching, Sequenced Collections und Virtual Threads zu nutzen.
Ziel ist eine produktionsnahe, nachvollziehbare Umsetzung, die sich sowohl als Lernressource eignet als auch als Ausgangspunkt für weiterführende Produktentwicklungen dienen kann.
1.3 Übersicht über die Komponenten
Die Anwendung besteht aus folgenden Kernkomponenten:
- einem Base62-Encoder, der fortlaufende IDs in URL-taugliche Kurzformen transformiert
- einem Mapping-Store, der die Zuordnung zwischen Kurzlink und Original-URL verwaltet
- einem REST-Service, der das Verkürzen von URLs und die Auflösung via Redirect ermöglicht
- einer optionalen UI auf Basis von Vaadin Flow zur manuellen Verwaltung der Mappings
und einem konfigurierbaren WAR-Deployment, das alle Komponenten integriert
Die Architektur folgt dabei dem Prinzip: „So wenig wie möglich, so viel wie nötig.“ Jeder Teil der Anwendung ist modular gehalten und erlaubt bei Bedarf spätere Aufspaltung – etwa in separate Dienste für Lesen, Schreiben oder Analyse.
Im nächsten Kapitel widmen wir uns der konkreten Projektstruktur und dem Aufbau der Module.
2. Projektstruktur und Modulorganisation
2.1 Aufbau eines modularen WAR-Projekts
Die erste lauffähige Version des URL-Shorteners wird als monolithische Java-Anwendung realisiert, die in Form einer klassischen WAR-Datei ausgerollt wird. Dabei orientiert sich der Aufbau des Projekts an einer klaren Schichtenarchitektur, die auf eine spätere Zerlegung vorbereitet ist. Das Projekt ist modular organisiert, wobei zwischen Kernlogik, HTTP-Schnittstelle und Benutzeroberfläche unterschieden wird. Diese Trennung erlaubt nicht nur eine bessere Wartbarkeit, sondern bildet auch die Grundlage für die in Teil III geplante Service-Dekomposition.
Das Projekt besteht aus drei Hauptmodulen:
- shortener-core: Enthält sämtliche fachliche Logik, inklusive URL-Encoding, Datenmodell und Store-Interfaces.
- shortener-api: Implementiert die REST-API auf Basis des Java-HTTP-Servers (com.sun.net.httpserver.HttpServer).
- shortener-ui-vaadin: Optionales UI-Modul mit Vaadin Flow zur Verwaltung und Visualisierung von Mappings.
Diese Module werden über ein zentrales WAR-Projekt (shortener-war) zusammengeführt, das die Auslieferungskonfiguration übernimmt und alle Abhängigkeiten vereint. Das WAR-Projekt ist dabei das einzige, das Servlet-spezifische Aspekte (z. B. web.xml, VaadinServlet) kennt – die übrigen Module bleiben vollständig unabhängig davon.
2.2 Trennung von Domänen-, API- und UI-Code
Die Modularisierung des Projekts orientiert sich an dem Prinzip der technologischen Isolierung: Die fachliche Kernlogik darf nichts über HTTP, Servlet-Container oder UI-Frameworks wissen. So bleibt sie vollständig testbar, austauschbar und in sich wiederverwendbar – beispielsweise für künftige CLI- oder Event-basierte Varianten des Shorteners.
- Das core-Modul definiert alle zentralen Schnittstellen (UrlMappingStore, ShortCodeEncoder) sowie die Basisklassen (ShortUrlMapping, Base62Encoder). Diese Komponenten enthalten keinerlei I/O-Logik.
- Das api-Modul ist zuständig für das Parsen von HTTP-Anfragen, das Routing sowie die Erzeugung von Redirects und JSON-Antworten. Es greift intern auf die core-Logik zu, bleibt jedoch losgelöst von UI-Aspekten.
Das ui-vaadin-Modul verwendet Vaadin Flow zur Realisierung einer webbasierten Oberfläche, bindet die core-Logik direkt ein und wird im WAR über eine dedizierte Servlet-Definition initialisiert.
Optional lassen sich weitere Module hinzufügen – etwa für Persistenz, Monitoring oder Analyse –, ohne dass die Struktur dabei an Kohärenz verliert.
2.3 Tooling und Build (Maven, JDK 24, WAR-Plugin)
Das Build-System basiert auf Maven in aktueller Version. Jedes Modul wird als eigenständiges Maven-Projekt mit eigener pom.xml geführt, wobei shortener-war als übergeordnete WAR-Applikation konfiguriert ist. Das WAR-Packaging erfolgt über das Standard-Servlet-Modell, sodass die resultierende Datei problemlos in Tomcat, Jetty oder einem beliebigen Servlet 4.0+ kompatiblen Container betrieben werden kann.
Zur Laufzeit wird Java 24 vorausgesetzt, was insbesondere für moderne Sprachmittel wie record, pattern matching und SequencedMap relevant ist. Als Zielplattform wird –release 21 oder höher empfohlen, um Kompatibilität mit modernen Runtimes zu gewährleisten.
Die Integration von Vaadin Flow erfolgt rein serverseitig über das Vaadin Servlet und benötigt keine separate Frontend-Build-Pipeline. Ressourcen wie Themes oder Icons werden vollständig aus dem Classpath geladen.
3. URL-Encoding: Base62 und ID-Erzeugung
3.1 Entwurf eines stabilen Kurzlink-Schemas
Die zentrale Anforderung an einen URL-Shortener ist die Erzeugung von eindeutigen, möglichst kurzen Zeichenketten, die als Schlüssel für den Zugriff auf die ursprüngliche URL dienen. Um diese Anforderung zu erfüllen, bedient sich die erste Implementierung eines sequenziellen ID-Schemas, das jede neue URL mit einer fortlaufenden numerischen ID versieht. Diese ID wird anschließend in ein URL-kompatibles Format überführt – konkret: Base62.
Base62 umfasst die 26 Großbuchstaben, die 26 Kleinbuchstaben sowie die 10 Dezimalziffern. Im Gegensatz zu Base64 enthält Base62 keine Sonderzeichen wie +, / oder =, was es ideal für URLs macht: Die erzeugten Strings sind lesbar, systemfreundlich und in allen Kontexten problemlos übertragbar.
Das entstehende Schema basiert somit auf einem zweistufigen Verfahren:
- Vergabe einer eindeutigen numerischen ID (z. B. 1, 2, 3, …)
- Umwandlung dieser ID in einen Base62-String (z. B. 1 → b, 2 → c, …)
Diese Methode garantiert eindeutige und nicht erratbare Codes – insbesondere, wenn die ID-Zählung nicht bei 0 beginnt oder Codes zusätzlich gemischt werden.
3.2 Implementierung eines Base62-Encoders
Der Base62-Encoder wird als eigenständige Utility-Klasse im core-Modul implementiert. Er enthält zwei statische Methoden:
- encode(long value): wandelt eine positive Ganzzahl in einen Base62-String um
- decode(String input): wandelt einen Base62-String zurück in eine Ganzzahl
Dabei wird das Alphabet intern als konstante Zeichenkette definiert und der Umwandlungsprozess rein rechnerisch durchgeführt – vergleichbar mit der Darstellung einer Zahl in einem anderen Stellenwertsystem.
Durch diese Implementierung entsteht ein deterministischer, stabiler und threadsicherer Encoder, der ohne externe Bibliotheken auskommt. Die resultierenden Codes sind deutlich kürzer als die zugrunde liegende Dezimalzahl und enthalten keine Sonderzeichen – ein zentraler Vorteil für eingebettete oder getippte Links.
Für Spezialfälle – etwa benutzerdefinierte Aliase – bleibt der Encoder optional, da solche Aliase als separate Strings direkt gespeichert werden können. Im Standardfall stellt der Base62-Encoder jedoch die bevorzugte Methode dar.
3.3 Alternativen: Zufall, Hashing, benutzerdefinierte Aliase
Neben dem sequentiellen Ansatz existieren weitere Verfahren zur Erzeugung von Kurzlinks, die in späteren Ausbaustufen berücksichtigt werden können:
- Zufallsbasierte Tokens (z. B. UUID, SecureRandom) erhöhen die Nichtvorhersagbarkeit, benötigen jedoch Kollisionserkennung und zusätzlichen Speicheraufwand.
- Hashing-Verfahren (z. B. SHA-1 der Ziel-URL) garantieren Stabilität, sind jedoch anfällig für Kollisionen bei hoher Last oder identischen Zieladressen.
- Benutzerdefinierte Aliase ermöglichen lesbare Kurzlinks (z. B. /helloMax), erfordern jedoch zusätzliche Prüfung auf Kollisionen, syntaktische Gültigkeit und Schutz reservierter Begriffe.
Für die erste Version konzentrieren wir uns auf das sequentielle Modell mit Base62-Transformation – einfach und stabil.
3.4 Implementierung: Base62Encoder.java
Ziel ist es, eine einfache Utility-Klasse bereitzustellen, die Ganzzahlen in Base62-Strings umwandelt und umgekehrt. Diese Klasse ist thread-safe, stateless und ohne jegliche externe Abhängigkeiten implementiert.
Zunächst der vollständige Quelltext:
package shortener.core.util;
public final class Base62Encoder {
private static final String ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
private static final int BASE = ALPHABET.length();
private Base62Encoder() {
}
public static String encode(long number) {
if (number < 0) {
throw new IllegalArgumentException("Only non-negative values supported");
}
StringBuilder result = new StringBuilder();
do {
int remainder = (int) (number % BASE);
result.insert(0, ALPHABET.charAt(remainder));
number = number / BASE;
} while (number > 0);
return result.toString();
}
public static long decode(String input) {
if (input == null || input.isEmpty()) {
throw new IllegalArgumentException("Input must not be null or empty");
}
long result = 0;
for (char c : input.toCharArray()) {
int index = ALPHABET.indexOf(c);
if (index == -1) {
throw new IllegalArgumentException("Invalid character in Base62 string: " + c);
}
result = result * BASE + index;
}
return result;
}
}
3.5 Was passiert hier – und warum?
Diese Klasse kapselt das gesamte Verhalten für das Base62-Encoding in zwei statische Methoden. Der Zeichenvorrat besteht aus Ziffern (0–9), Kleinbuchstaben (a–z) und Großbuchstaben (A–Z), was genau 62 verschiedene Zeichen ergibt.
- Die Methode encode(long number) zerlegt eine Ganzzahl in eine umgekehrte Darstellung im Stellenwertsystem zur Basis 62. Dabei wird sukzessive der Rest bei Division durch 62 berechnet und das entsprechende Zeichen eingefügt. Das Ergebnis ist ein kurzer, URL-freundlicher String.
- Die Methode decode(String input) kehrt diesen Vorgang um: Sie wandelt einen Base62-String wieder zurück in seine numerische Repräsentation. Jedes Zeichen wird durch seinen Index im Alphabet ersetzt und entsprechend gewichtet.
Diese Implementierung ist robust gegen ungültige Eingaben, arbeitet vollständig im Speicher und lässt sich direkt in der ID-Erzeugung oder in URL-Mappings einsetzen.
Implementierung: ShortCodeGenerator.java
Die Klasse abstrahiert die ID-Erzeugung vom konkreten Speichermechanismus. Sie eignet sich sowohl für die In-Memory-Version als auch für künftige persistente Varianten. Der Generator ist thread-sicher und verwendet ausschließlich JDK-Bordmittel.
package shortener.core.encode;
public final class ShortCodeGenerator {
private final AtomicLong counter;
public ShortCodeGenerator(long initialValue) {
this.counter = new AtomicLong(initialValue);
}
public String nextCode() {
long id = counter.getAndIncrement();
return Base62Encoder.encode(id);
}
public long currentId() {
return counter.get();
}
}
Erläuterung
Diese Klasse kapselt einen sequenziellen Zähler, der bei jedem Aufruf von nextCode() einen neuen, eindeutigen Kurzcode generiert. Die Ausgabe basiert auf einer monoton wachsenden ID, die in einen Base62-String kodiert wird.
Die Methode getAndIncrement() der AtomicLong ist nicht blockierend und damit hochperformant, auch unter starker Nebenläufigkeit. Die erzeugten Codes sind eindeutig, kompakt und deterministisch – Eigenschaften, die sich für Auditing, Logging und spätere Analyse hervorragend eignen.
Der Konstruktor erlaubt es, den Startwert zu konfigurieren. Das ist nützlich, wenn z. B. nach einem Neustart ein persistierter Zähler fortgesetzt werden soll.
Beispielnutzung (z. B. im Mapping Store)
ShortCodeGenerator generator = new ShortCodeGenerator(1L);
String shortCode = generator.nextCode(); // z. B. “b”, “c”, “d”, …
Diese Klasse ist bewusst einfach gehalten und kann im nächsten Kapitel direkt in den Mapping Store eingebunden werden.
4. Mapping Store: Speicherung der Zuordnung
4.1 Interface-Design: UrlMappingStore
Der Mapping Store bildet das Herzstück des URL-Shorteners. Er verwaltet die Zuordnung zwischen einem Kurzcode (z. B. kY7zD) und der zugehörigen Ziel-URL (https://example.org/foo/bar). Gleichzeitig übernimmt er die Kontrolle über Mehrfachverwendungen, Ablaufzeiten und potenzielle Aliase.
In der ersten Ausbaustufe wird eine rein in-memory-basierte Lösung verwendet. Diese ist schnell, einfach und ideal für den Start – auch wenn sie bei einem Neustart verloren geht. Persistenz wird bewusst auf eine spätere Phase verschoben.
Der Store wird über ein einfaches Interface abstrahiert. Dieses Interface ermöglicht eine spätere Substitution (z. B. durch eine Datei- oder Datenbank-basierte Version), ohne dass die API- oder UI-Komponenten davon beeinflusst werden.
4.2 In-Memory-Implementierung mit ConcurrentHashMap
Die erste konkrete Implementierung nutzt eine ConcurrentHashMap, um den Zugriff auch unter Last zuverlässig zu ermöglichen. Jeder Mapping-Eintrag wird durch ein einfaches Record-Objekt (ShortUrlMapping) repräsentiert, das Ziel-URL, Erstellungszeitpunkt und optional Ablaufinformationen enthält.
Die Kombination aus ConcurrentHashMap und ShortCodeGenerator erlaubt eine deterministische und threadsichere ID-Vergabe, ohne dass explizite Synchronisation notwendig wird. So entsteht eine performante Lösung, die bereits unter hoher Last stabil arbeitet.
4.3 Erweiterbarkeit für spätere Persistenz
Alle Einträge sind durch ein zentrales Interface erreichbar. Dieses Interface wird nicht nur zur Speicherung und Abfrage genutzt, sondern bildet auch die Basis für spätere Erweiterungen: etwa eine Persistenzschicht mit Flatfile oder EclipseStore, eine Ablaufkontrolle über TTL oder gar Event-getriebene Backends.
Die Datenstruktur lässt sich um Metriken, Gültigkeitslogik oder Audit-Felder erweitern, ohne dass die API verändert werden muss – ein klassischer Ansatz der Schnittstellenorientierung im Sinne der Open/Closed-Prinzipien.
4.4 Implementierung
Datenmodell: ShortUrlMapping.java
package shortener.core.model;
public record ShortUrlMapping(
String shortCode,
String originalUrl,
Instant createdAt,
Optional<Instant> expiresAt
) {}
Diese Struktur bildet die grundlegende Zuordnung ab und erlaubt die optionale Angabe eines Ablaufzeitpunkts.
Store-Interface: UrlMappingStore.java
package shortener.core.store;
public interface UrlMappingStore {
ShortUrlMapping createMapping(String originalUrl);
Optional<ShortUrlMapping> findByShortCode(String shortCode);
boolean exists(String shortCode);
List<ShortUrlMapping> findAll();
boolean delete(String shortCode);
int mappingCount();
}
Das Interface ist bewusst schlank gehalten und abstrahiert die zwei Kernoperationen: Einfügen (mit Erzeugung) und Lookup.
Implementierung: InMemoryUrlMappingStore.java
package shortener.core.store;
public class InMemoryUrlMappingStore
implements UrlMappingStore, HasLogger {
private final ConcurrentHashMap<String, ShortUrlMapping> store
= new ConcurrentHashMap<>();
private final ShortCodeGenerator generator;
public InMemoryUrlMappingStore() {
this.generator = new ShortCodeGenerator(1L);
}
@Override
public ShortUrlMapping createMapping(String originalUrl) {
logger().info("originalUrl: {} ->", originalUrl);
String code = generator.nextCode();
ShortUrlMapping shortMapping = new ShortUrlMapping(
code,
originalUrl,
Instant.now(),
Optional.empty()
);
store.put(code, shortMapping);
return shortMapping;
}
@Override
public Optional<ShortUrlMapping> findByShortCode(String shortCode) {
return Optional.ofNullable(store.get(shortCode));
}
@Override
public boolean exists(String shortCode) {
return store.containsKey(shortCode);
}
@Override
public List<ShortUrlMapping> findAll() {
return new ArrayList<>(store.values());
}
@Override
public boolean delete(String shortCode) {
return store.remove(shortCode) != null;
}
@Override
public int mappingCount() {
return store.size();
}
}
4.5 Warum so?
Die Verwendung einer ConcurrentHashMap stellt sicher, dass gleichzeitige Schreib- und Lesevorgänge konsistent und performant abgewickelt werden können. Die Kombination mit AtomicLong in ShortCodeGenerator verhindert Kollisionen. Über das Interface lässt sich später eine persistente Implementierung einführen, ohne das API- oder UI-Verhalten anzupassen.
5. HTTP-API mit Bordmitteln von Java
5.1 HTTP-Server mit com.sun.net.httpserver.HttpServer
Anstatt auf schwergewichtige Frameworks wie Spring oder Jakarta EE zurückzugreifen, verwenden wir für den ersten Prototyp die leichtgewichtige HTTP-Server-Implementierung, die das JDK bereits im Paket com.sun.net.httpserver mitliefert. Diese API ist zwar rudimentär, aber performant, stabil und für unseren Anwendungsfall vollkommen ausreichend.
Der Server ist in wenigen Zeilen konfiguriert, benötigt keine XML- oder Annotation-basierten Mappings und lässt sich vollständig programmatisch steuern. Für jeden Pfad definieren wir einen eigenen HttpHandler, der die Anfrage entgegennimmt, verarbeitet und eine strukturierte HTTP-Antwort zurückliefert.
5.2 POST /shorten: URL verkürzen
Der erste Endpunkt erlaubt es, eine lange URL über einen HTTP POST an den Shortener zu senden. Als Antwort liefert der Server die erzeugte Kurzform – im einfachsten Fall als JSON-Objekt mit dem shortCode.
Beispiel-Anfrage:
POST /shorten
Content-Type: application/json
{
"url": "https://example.com/some/very/long/path"
}
Antwort:
200 OK
Content-Type: application/json
{
"shortCode": "kY7zD"
}
Fehlende oder ungültige Eingaben werden mit 400 Bad Request beantwortet.
5.3 GET /{code}: Weiterleitung zur Original-URL
Beim Aufruf eines Shortcodes (z. B. GET /kY7zD) prüft der Server, ob ein Mapping existiert. Wenn ja, wird eine HTTP 302-Weiterleitung zur Originaladresse durchgeführt. Ist der Code unbekannt oder abgelaufen, folgt eine 404 Not Found-Antwort.
Diese Weiterleitung ist zustandslos und ermöglicht eine spätere Isolation in einen read-only Redirect-Service.
5.4 Implementierung
Startpunkt: ShortenerServer.java
package shortener.api;
public class ShortenerServer
implements HasLogger {
private HttpServer server;
public static void main(String[] args)
throws IOException {
new ShortenerServer().init();
}
public void init()
throws IOException {
var store = new InMemoryUrlMappingStore();
this.server = HttpServer.create(new InetSocketAddress(8080), 0);
server.createContext("/shorten", new ShortenHandler(store));
server.createContext("/", new RedirectHandler(store));
server.setExecutor(null); // default executor
server.start();
System.out.println("URL Shortener server running at http://localhost:8080");
}
public void shutdown() {
if (server != null) {
server.stop(0);
System.out.println("URL Shortener server stopped");
}
}
}
POST-Handler: ShortenHandler.java
package shortener.api;
public class ShortenHandler
implements HttpHandler, HasLogger {
private final UrlMappingStore store;
public ShortenHandler(UrlMappingStore store) {
this.store = store;
}
@Override
public void handle(HttpExchange exchange)
throws IOException {
if (!"POST".equalsIgnoreCase(exchange.getRequestMethod())) {
exchange.sendResponseHeaders(405, -1);
return;
}
InputStream body = exchange.getRequestBody();
Map<String, String> payload = parseJson(body);
String originalUrl = payload.get("url");
logger().info("Received request to shorten url: {}", originalUrl);
if (originalUrl == null || originalUrl.isBlank()) {
exchange.sendResponseHeaders(400, -1);
return;
}
ShortUrlMapping mapping = store.createMapping(originalUrl);
logger().info("Created mapping for {} -> {}", originalUrl, mapping.shortCode());
byte[] response = toJson(Map.of("shortCode", mapping.shortCode())).getBytes(StandardCharsets.UTF_8);
exchange.getResponseHeaders().add("Content-Type", "application/json");
exchange.sendResponseHeaders(200, response.length);
try (OutputStream os = exchange.getResponseBody()) {
os.write(response);
}
}
}
GET-Handler: RedirectHandler.java
package shortener.api;
public class RedirectHandler
implements HttpHandler , HasLogger {
private final UrlMappingStore store;
public RedirectHandler(UrlMappingStore store) {
this.store = store;
}
@Override
public void handle(HttpExchange exchange)
throws IOException {
var requestURI = exchange.getRequestURI();
var fullPath = requestURI.getPath();
logger().info("Full path: {}", fullPath);
String path = fullPath.substring(1); // strip leading '/'
logger().info("Path: {}", path);
if (path.isEmpty()) {
exchange.sendResponseHeaders(400, -1);
return;
}
Optional<String> target = store
.findByShortCode(path)
.map(ShortUrlMapping::originalUrl);
if (target.isPresent()) {
exchange.getResponseHeaders().add("Location", target.get());
exchange.sendResponseHeaders(302, -1);
} else {
exchange.sendResponseHeaders(404, -1);
}
}
}
JsonUtils.java (Minimal-Java-JSON ohne externe Libraries)
Da wir im Rahmen dieser ersten Implementierung keine externen Abhängigkeiten wie Jackson oder Gson verwenden wollen, benötigen wir eine eigene Utility-Klasse, um einfache JSON-Objekte zu verarbeiten – konkret:
- String → Map<String, String>: zur Verarbeitung von HTTP-POST-Payloads (/shorten)
- Map<String, String> → JSON-String: zur Antworterzeugung (z. B. { “shortCode”: “abc123” })
Diese Klasse genügt für einfache key-value-Strukturen, wie sie im Shortener gebraucht werden. Sie ist nicht für verschachtelte Objekte oder Arrays gedacht, sondern als pragmatische Lösung für den Start.
Implementierung: JsonUtils.java
package shortener.api.util;
public final class JsonUtils {
private JsonUtils() { }
public static Map<String, String> parseJson(InputStream input)
throws IOException {
String json = readInputStream(input).trim();
return parseJson(json);
}
@NotNull
public static Map<String, String> parseJson(String json)
throws IOException {
if (!json.startsWith("{") || !json.endsWith("}")) {
throw new IOException("Invalid JSON object");
}
Map<String, String> result = new HashMap<>();
// Entferne geschweifte Klammern
String body = json.substring(1, json.length() - 1).trim();
if (body.isEmpty()) {
return Collections.emptyMap();
}
// Trenne key-value-Paare anhand von Kommas
String[] entries = body.split(",");
Arrays.stream(entries)
.map(entry -> entry.split(":", 2))
.filter(parts -> parts.length == 2)
.forEachOrdered(parts -> {
String key = unquote(parts[0].trim());
String value = unquote(parts[1].trim());
result.put(key, value);
});
return result;
}
public static String toJson(Map<String, String> map) {
StringBuilder sb = new StringBuilder();
sb.append("{");
boolean first = true;
for (Map.Entry<String, String> entry : map.entrySet()) {
if (!first) {
sb.append(",");
}
sb.append("\"").append(escape(entry.getKey())).append("\":");
sb.append("\"").append(escape(entry.getValue())).append("\"");
first = false;
}
sb.append("}");
return sb.toString();
}
private static String readInputStream(InputStream input)
throws IOException {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(input, StandardCharsets.UTF_8))) {
return reader.lines().collect(joining());
}
}
private static String unquote(String s) {
if (s.startsWith("\"") && s.endsWith("\"") && s.length() >= 2) {
return s.substring(1, s.length() - 1);
}
return s;
}
private static String escape(String s) {
// einfache Escape-Logik für Anführungszeichen
return s.replace("\"", "\\\"");
}
}
Eigenschaften und Grenzen
Diese Implementierung ist:
- vollständig JDK-basiert (keine Drittanbieter-Bibliotheken)
- für flache JSON-Objekte geeignet, d. h. { “key”: “value” }
- robust gegen triviale Parsing-Fehler, aber ohne JSON-Schema-Validierung
- bewusst minimalistisch, um im Rahmen des Prototyps zu bleiben
Sie reicht aus für:
- POST /shorten (Client sendet { “url”: “…” })
- Antwort auf diesen POST (Server sendet { “shortCode”: “…” })
Beispielhafte Verwendung
InputStream body = exchange.getRequestBody();
Map<String, String> input = JsonUtils.parseJson(body);
String shortCode = "abc123";
String response = JsonUtils.toJson(Map.of("shortCode", shortCode));
Für produktive Systeme empfiehlt sich perspektivisch:
- Gson (leichtgewichtig, idiomatisch)
- Jackson (umfangreich, auch für DTO-Bindung)
- Json-B (Standard-API, Jakarta konform)
Für unsere erste Implementierung im Core-JDK bleibt jedoch die oben gezeigte Lösung bewusst der passende Mittelweg.
5.6 Core Java Client Implementierung
Die Klasse URLShortenerClient erfüllt die Funktion eines minimalistischen HTTP-Clients zur Interaktion mit einem URL-Shortener-Dienst. Ihr Aufbau erlaubt die Anbindung an einen konfigurierbaren oder lokal laufenden Server, wobei die Standardadresse auf http://localhost:8080/ gesetzt ist. Dies ermöglicht eine einfache Integration in lokale Entwicklungsumgebungen, Testläufe oder automatisierte Systemtests, ohne dass zusätzliche Konfiguration erforderlich wäre.
Im Zentrum der Funktionalität steht die Methode shortenURL(String originalUrl). Sie initiiert einen HTTP-POST-Aufruf gegen den Server-Endpunkt /shorten, überträgt die zu verkürzende URL in einem einfachen JSON-Dokument und wertet die Antwort des Servers unmittelbar aus. Die erfolgreiche Rückmeldung wird ausschließlich durch einen Statuscode 200 OK signalisiert. In diesem Fall extrahiert die Methode den enthaltenen shortCode mittels der statischen Hilfsmethode extractShortCode() aus der Klasse JsonUtils. Sollte der Server stattdessen einen anderen HTTP-Code liefern, so wird der Vorgang mit einer entsprechenden IOException abgebrochen, was eine explizite Fehlerbehandlung auf Anwendungsebene erzwingt. Dadurch wird eine klare semantische Trennung zwischen regulärer Nutzung und Ausnahmesituation gewahrt.
Die zweite zentrale Methode, resolveShortcode(String shortCode), dient dem expliziten Auflösen einer Kurz-URL. Sie sendet einen GET-Request direkt an den Root-Kontext des Servers, ergänzt um den übergebenen Code. Das Verhalten dieser Methode entspricht weitgehend dem eines Web-Browsers, mit dem Unterschied, dass automatische Weiterleitungen bewusst deaktiviert wurden. Auf diese Weise kann die Methode die tatsächliche Zieladresse – sofern vorhanden – aus dem HTTP-Headerfeld Location extrahieren und als Ergebnis zurückgeben. Sie unterscheidet dabei sauber zwischen gültiger Weiterleitung (Status 301 oder 302), nicht vorhandenen Codes (Status 404) und sonstigen unerwarteten Antworten. In letzterem Fall wird analog zum Kurzungsprozess eine IOException geworfen.
Technisch betrachtet verwendet der URLShortenerClient ausschließlich Komponenten der Java SE API, namentlich HttpURLConnection, URI und Stream-basierte Ein- und Ausgaberoutinen. Die gesamte Kommunikation erfolgt UTF-8-kodiert, was eine hohe Interoperabilität mit modernen JSON-basierten REST-Schnittstellen sicherstellt. Die Klasse implementiert darüber hinaus das Interface HasLogger, was auf eine projektweite Logging-Infrastruktur schließen lässt und implizit eine gute Nachvollziehbarkeit bei der Serverkommunikation unterstützt.
Die Verwendung dieses Clients empfiehlt sich insbesondere im Kontext von Integrationstests, Kommandozeilenwerkzeugen oder administrativen Scripten, bei denen gezielt einzelne URLs verkürzt oder geprüft werden sollen. Aufgrund ihrer schlanken Struktur eignet sich die Klasse auch als Ausgangspunkt für weitere Abstraktionen, beispielsweise für eine serviceorientierte Kapselung in größeren Architekturen.
public class URLShortenerClient
implements HasLogger {
protected static final String DEFAULT_SERVER_PORT = "8080";
protected static final String DEFAULT_SERVER_URL = "http://localhost:" + DEFAULT_SERVER_PORT;
protected static final String SHORTEN_URL_ENDPOINT = "/shorten";
protected static final String REDIRECT_URL_ENDPOINT = "/";
private final URI serverBase;
public URLShortenerClient(String serverBaseUrl) {
this.serverBase = URI.create(serverBaseUrl.endsWith("/") ? serverBaseUrl : serverBaseUrl + "/");
}
public URLShortenerClient() {
this.serverBase = URI.create(DEFAULT_SERVER_URL);
}
/**
* String originalUrl = "https://svenruppert.com";
*
* @param originalUrl
* @return
* @throws IOException
*/
public String shortenURL(String originalUrl)
throws IOException {
var serverURL = serverBase.toURL();
// --- Schritt 1: POST zum /shorten Endpunkt mit gültiger URL ---
URL shortenUrl = URI.create(serverURL + SHORTEN_URL_ENDPOINT).toURL();
HttpURLConnection connection = (HttpURLConnection) shortenUrl.openConnection();
connection.setRequestMethod("POST");
connection.setDoOutput(true);
connection.setRequestProperty("Content-Type", "application/json");
String body = "{\"url\":\"" + originalUrl + "\"}";
try (OutputStream os = connection.getOutputStream()) {
os.write(body.getBytes());
}
int status = connection.getResponseCode();
if (status == 200) {
try (InputStream is = connection.getInputStream()) {
String jsonResponse = new String(is.readAllBytes(), UTF_8);
String extractedShortCode = JsonUtils.extractShortCode(jsonResponse);
logger().info("extractedShortCode .. {}", extractedShortCode);
return extractedShortCode;
}
} else {
throw new IOException("Server returned status " + status);
}
}
public String resolveShortcode(String shortCode)
throws IOException {
logger().info("Resolving shortCode: {}", shortCode);
URL url = URI.create(DEFAULT_SERVER_URL + REDIRECT_URL_ENDPOINT + shortCode).toURL();
logger().info("url .. {}", url);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setInstanceFollowRedirects(false);
int responseCode = connection.getResponseCode();
logger().info("responseCode .. {}", responseCode);
if (responseCode == 302 || responseCode == 301) {
var location = connection.getHeaderField("Location");
logger().info("location .. {}", location);
return location;
} else if (responseCode == 404) {
return null;
} else {
throw new IOException("Unexpected response: " + responseCode);
}
}
}
5.7 Zusammenfassung
Mit wenigen Zeilen lässt sich eine funktionale HTTP-API realisieren, die sich hervorragend als Testbett und Proof-of-Concept eignet. Die Struktur ist minimal, aber offen für Erweiterungen wie Fehlerobjekte, Ratelimiting oder Logging. Besonders bemerkenswert ist: Die gesamte API kommt ohne Servlet-Container, externe Frameworks oder Reflection aus – ideal für eingebettete Anwendungen und leichtgewichtige Deployments.
In dem nächsten Teil werden wir uns eine grafische Oberfläche erstellen um die Interaktionen mit den Usern abzubilden.
Happy Coding
Sven