Prosty serwer HTTP bez frameworka
- WWW i API
- poziom: średni
- aktualizacja: 16 sierpnia 2026
Do prostego webhooka, lokalnego panelu albo atrapy API nie zawsze potrzebujesz Springa. Moduł JDK jdk.httpserver zawiera klasę HttpServer, która obsługuje nasłuchiwanie, ścieżki i nagłówki. Poniższy program udostępnia stronę główną oraz endpoint /powitanie?imie=.... Demonstracja wiąże serwer wyłącznie z adresem 127.0.0.1, wybiera wolny port, wykonuje lokalne żądanie i zatrzymuje usługę w bloku finally, więc nie wymaga internetu ani nie pozostaje uruchomiona po teście. Uważaj na jedną pułapkę, którą większość tutoriali przemilcza: createContext dopasowuje ścieżki prefiksowo, więc kontekst / łapie każdy adres, którego nie przejął nikt inny — jeśli sam nie porównasz ścieżki, literówka w URL-u dostanie 200 zamiast 404.
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URLDecoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
/**
* Minimalny serwer HTTP z biblioteki standardowej — skryptyJAVA.pl
* Zapisz jako: ProstySerwerHttp.java
*/
public class ProstySerwerHttp {
/** Tworzy serwer na wskazanym porcie; wartość 0 oznacza automatyczny wybór wolnego portu. */
public static HttpServer utworz(int port) throws IOException {
// 127.0.0.1 ogranicza dostęp do tego komputera i jest bezpiecznym ustawieniem demonstracyjnym.
HttpServer serwer = HttpServer.create(new InetSocketAddress("127.0.0.1", port), 0);
serwer.createContext("/", ProstySerwerHttp::stronaGlowna);
serwer.createContext("/powitanie", ProstySerwerHttp::powitanie);
return serwer;
}
private static void stronaGlowna(HttpExchange wymiana) throws IOException {
// Kontekst "/" jest prefiksem awaryjnym, więc sam rozróżniamy stronę główną od 404.
if (!wymiana.getRequestURI().getPath().equals("/")) {
odpowiedz(wymiana, 404, "text/plain", "Nie znaleziono takiej strony.");
return;
}
if (!tylkoOdczyt(wymiana)) { return; }
String html = """
<!doctype html>
<html lang="pl"><meta charset="UTF-8"><title>Serwer Java</title>
<body><h1>Serwer działa</h1><p>Wypróbuj /powitanie?imie=Ola</p></body></html>
""";
odpowiedz(wymiana, 200, "text/html", html);
}
private static void powitanie(HttpExchange wymiana) throws IOException {
// Konteksty HttpServer działają prefiksowo: /powitanie/inne też trafiłoby do tej metody.
if (!wymiana.getRequestURI().getPath().equals("/powitanie")) {
odpowiedz(wymiana, 404, "text/plain", "Nie znaleziono takiej strony.");
return;
}
if (!tylkoOdczyt(wymiana)) { return; }
// Adres z błędnym zapisem procentowym (np. %ZZ) HttpServer odrzuca sam, kodem 400.
String imie = parametr(wymiana.getRequestURI(), "imie");
if (imie == null || imie.isBlank()) { imie = "świecie"; }
odpowiedz(wymiana, 200, "text/plain", "Cześć, " + imie.strip() + "!");
}
/** Przepuszcza GET i HEAD, a inne metody odrzuca z nagłówkiem dozwolonych metod. */
private static boolean tylkoOdczyt(HttpExchange wymiana) throws IOException {
String metoda = wymiana.getRequestMethod();
if (metoda.equalsIgnoreCase("GET") || metoda.equalsIgnoreCase("HEAD")) { return true; }
wymiana.getResponseHeaders().set("Allow", "GET, HEAD");
odpowiedz(wymiana, 405, "text/plain", "Dozwolone są tylko metody GET i HEAD.");
return false;
}
/** Odczytuje pojedynczy parametr query string i dekoduje zapis procentowy oraz znak +. */
private static String parametr(URI adres, String szukanaNazwa) {
String zapytanie = adres.getRawQuery();
if (zapytanie == null) { return null; }
for (String para : zapytanie.split("&")) {
String[] czesci = para.split("=", 2);
String nazwa = URLDecoder.decode(czesci[0], StandardCharsets.UTF_8);
if (nazwa.equals(szukanaNazwa)) {
return czesci.length == 2
? URLDecoder.decode(czesci[1], StandardCharsets.UTF_8) : "";
}
}
return null;
}
/** Wysyła odpowiedź UTF-8 i domyka jej strumień; dla HEAD wysyła same nagłówki. */
private static void odpowiedz(HttpExchange wymiana, int status, String typ, String tresc)
throws IOException {
byte[] bajty = tresc.getBytes(StandardCharsets.UTF_8);
wymiana.getResponseHeaders().set("Content-Type", typ + "; charset=UTF-8");
wymiana.getResponseHeaders().set("X-Content-Type-Options", "nosniff");
if (wymiana.getRequestMethod().equalsIgnoreCase("HEAD")) {
wymiana.sendResponseHeaders(status, -1); // HEAD wymaga długości -1, inaczej JDK ostrzega
wymiana.close();
return;
}
wymiana.sendResponseHeaders(status, bajty.length);
try (var wyjscie = wymiana.getResponseBody()) {
wyjscie.write(bajty);
}
}
// ── PRZYKŁADY ───────────────────────────────────────────
public static void main(String[] args) throws Exception {
HttpServer serwer = utworz(0);
serwer.start();
int port = serwer.getAddress().getPort();
System.out.println("Serwer działa lokalnie na porcie " + port);
try {
HttpClient klient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(2)).build();
HttpRequest zadanie = HttpRequest.newBuilder(
URI.create("http://127.0.0.1:" + port + "/powitanie?imie=Ola%20Nowak"))
.timeout(Duration.ofSeconds(3)).GET().build();
HttpResponse<String> wynik = klient.send(
zadanie, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
System.out.println("HTTP " + wynik.statusCode() + ": " + wynik.body());
} finally {
serwer.stop(0); // zatrzymanie jest konieczne, bo serwer utrzymuje wątek procesu
System.out.println("Serwer zatrzymany.");
}
}
}
Jak użyć tego skryptu
- Skopiuj kod do pliku
ProstySerwerHttp.java; nazwa pliku musi pokrywać się z nazwą klasy publicznej. Uruchom go poleceniemjava ProstySerwerHttp.java. Program wybierze wolny port, wykona żądanie do własnego endpointu i sam się zakończy — połączenie z internetem nie jest potrzebne. - W prawdziwej aplikacji podaj stały port, np.
HttpServer serwer = utworz(8080), wywołajserwer.start()i nie zatrzymuj go od razu. Endpoint będzie dostępny pod adresemhttp://127.0.0.1:8080/powitanie?imie=Ola. Zanim jednak wpuścisz tam więcej niż siebie, przeczytaj punkt 4 — domyślnie serwer obsługuje klientów pojedynczo. - Nową trasę dodasz przez
serwer.createContext("/status", Klasa::metoda). Metoda obsługi przyjmujeHttpExchange; odczytasz z niego metodę, URI, nagłówki i ciało żądania. - Bez
setExecutorserwer obsługuje wszystkie żądania po kolei w jednym wątku — jedno wolne żądanie każe czekać wszystkim pozostałym klientom. Jeżeli endpoint ma obsłużyć więcej niż jednego użytkownika naraz, ustaw przedstart()własną pulę, np.serwer.setExecutor(Executors.newFixedThreadPool(8)), a przy zamykaniu zatrzymaj i serwer, i pulę — inaczej jej wątki utrzymają proces przy życiu.
Warto wiedzieć
- Pakiet
com.sun.net.httpserverjest dostarczany z JDK 17 w modulejdk.httpserver, mimo prefiksucom.sun. Nie jest jednak pełnym serwerem produkcyjnym. Najważniejsze: bezsetExecutorwszystkie żądania idą po kolei przez jeden wątek, więc jeden handler czekający dwie sekundy na bazę zatrzymuje cały ruch. Do tego nie dostaniesz routingu z parametrami ścieżki, walidacji, limitowania ruchu ani integracji z TLS i logami tak wygodnej jak we frameworku. - Przykład nasłuchuje wyłącznie na
127.0.0.1, więc nie jest widoczny z innych komputerów. Zmiana na0.0.0.0wystawia usługę na wszystkie interfejsy — rób to dopiero po dodaniu uwierzytelniania, walidacji, limitów rozmiaru żądań i reguł zapory. - Zawsze ustawiaj poprawny kod odpowiedzi i
Content-Type. Kod 405 powinien zawierać nagłówekAllow, błędne dane klienta powinny dawać 400, brak zasobu 404, a nieobsłużony błąd serwera 500 — zwracanie 200 dla każdego przypadku utrudnia klientom poprawną obsługę. Skoro obsługujesz GET, obsłuż też HEAD: wysyłają go monitoring, health-checki i przeglądarki. WHttpServerpodaj wtedy wsendResponseHeadersdługość-1, bo przy podanej długości ciała JDK wypisuje ostrzeżenie na stderr. - Parametry URL są danymi niezaufanymi. Przed użyciem w HTML trzeba je zakodować jako tekst HTML, a przed zapytaniem SQL przekazać jako parametr przygotowanej instrukcji. Ten przykład zwraca imię jako
text/plain, dlatego nie interpretuje ono znaczników HTML. Adres z połamanym zapisem procentowym (np.?imie=%ZZ) odrzuci zresztą samHttpServerkodem 400, zanim żądanie dojdzie do Twojej metody.
Podobne skrypty
Pobieranie danych z API przez HttpClient
Klient HTTP na wbudowanym HttpClient z Javy 11: żądania GET i POST z ciałem JSON, limity czasu, sprawdzanie kodu odpowiedzi, pobieranie pliku prosto na dysk i wersja asynchroniczna — bez żadnych bibliotek.
średni Zobacz kod → WWW i APIPobieranie pliku z paskiem postępu
Strumieniowe pobieranie dużego pliku przez HttpClient z procentowym postępem, limitem czasu na połączenie i bezpiecznym plikiem tymczasowym. Samodzielny przykład uruchamia lokalny serwer, więc działa bez internetu.
średni Zobacz kod → WWW i APIBudowanie adresów URL i kodowanie parametrów
Sklejanie adresów URL bez błędów: kodowanie kluczy i wartości przez URLEncoder, osobna metoda dla segmentów ścieżki z %20 zamiast plusa, rozbieranie query stringu na mapę oraz odczyt części adresu przez URI.
początkujący Zobacz kod →