Przejdź do treści

Budowanie adresów URL i kodowanie parametrów

  • WWW i API
  • poziom: początkujący
  • aktualizacja: 16 sierpnia 2026

Ręczne sklejanie adresu operatorem + działa do pierwszej spacji, polskiej litery albo znaku & w szukanej frazie — potem serwer dostaje adres, którego nie da się rozebrać z powrotem. Ten skrypt pokazuje, jak to zrobić poprawnie: klucz i wartość kodowane osobno, a przede wszystkim różnicę, o którą potyka się większość osób. URLEncoder zamienia spację na + i w query stringu jest to prawidłowe, ale w ścieżce adresu + znaczy dosłownie plus — tam spacja musi być zapisana jako %20.

JAVA AdresyUrl.java 136 linii Pobierz .java
import java.net.URI;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * Budowanie adresów URL i kodowanie parametrów — skryptyJAVA.pl
 * Zapisz jako: AdresyUrl.java
 */
public class AdresyUrl {

    /**
     * Skleja adres bazowy z parametrami zapytania. Klucz i wartość kodowane są osobno —
     * dopiero wtedy znaki "=" oraz "&" wstawione przez nas pozostają separatorami,
     * a te same znaki wpisane przez użytkownika zamieniają się w %3D i %26.
     *
     * @param bazowy    adres bez znaku zapytania albo z gotowym początkiem query;
     *                  adres z kotwicą "#" się nie nada — parametry wylądowałyby za nią
     * @param parametry pary klucz-wartość; wpis o wartości null jest pomijany
     */
    public static String zbudujAdres(String bazowy, Map<String, String> parametry) {
        StringBuilder adres = new StringBuilder(bazowy);
        char separator = bazowy.contains("?") ? '&' : '?';
        for (Map.Entry<String, String> wpis : parametry.entrySet()) {
            if (wpis.getValue() == null) { continue; }  // null to nie to samo co pusty napis
            adres.append(separator)
                 .append(kodujParametr(wpis.getKey()))
                 .append('=')
                 .append(kodujParametr(wpis.getValue()));
            separator = '&';
        }
        return adres.toString();
    }

    /** Kodowanie do query stringu: spacja zamienia się na "+", dokładnie jak w formularzu HTML. */
    public static String kodujParametr(String tekst) {
        return URLEncoder.encode(tekst, StandardCharsets.UTF_8);
    }

    /**
     * Kodowanie POJEDYNCZEGO segmentu ścieżki. W ścieżce "+" znaczy dosłownie plus,
     * więc spacja musi być zapisana jako %20. Podmiana jest bezpieczna: prawdziwy plus
     * z tekstu został wcześniej zamieniony przez URLEncoder na %2B.
     */
    public static String kodujSegmentSciezki(String segment) {
        return URLEncoder.encode(segment, StandardCharsets.UTF_8).replace("+", "%20");
    }

    /** Skleja ścieżkę z osobnych segmentów; ukośniki wstawiamy my, więc nie znikną w kodowaniu. */
    public static String zbudujSciezke(String... segmenty) {
        StringBuilder sciezka = new StringBuilder();
        for (String segment : segmenty) {
            sciezka.append('/').append(kodujSegmentSciezki(segment));
        }
        return sciezka.toString();
    }

    /**
     * Rozbija surowy query string na mapę. Parametr bez znaku "=" dostaje pusty napis,
     * tak samo jak parametr z pustą wartością — w adresie te dwa zapisy wyglądają inaczej,
     * ale w praktyce znaczą to samo. LinkedHashMap zachowuje kolejność z adresu.
     * Powtórzony klucz (tag=a&tag=b) nadpisuje poprzedni — w mapie zostaje ostatnia wartość.
     */
    public static Map<String, String> rozbijQuery(String query) {
        Map<String, String> parametry = new LinkedHashMap<>();
        if (query == null || query.isEmpty()) { return parametry; }
        String tresc = query.startsWith("?") ? query.substring(1) : query;
        for (String para : tresc.split("&")) {
            if (para.isEmpty()) { continue; }
            int rownosc = para.indexOf('=');
            String klucz = rownosc < 0 ? para : para.substring(0, rownosc);
            String wartosc = rownosc < 0 ? "" : para.substring(rownosc + 1);
            parametry.put(URLDecoder.decode(klucz, StandardCharsets.UTF_8),
                          URLDecoder.decode(wartosc, StandardCharsets.UTF_8));
        }
        return parametry;
    }

    /** Rozkłada adres na części przez java.net.URI; getRaw... oddaje wersję jeszcze zakodowaną. */
    public static String opiszAdres(String adres) {
        URI uri = URI.create(adres);
        int port = uri.getPort();  // -1 oznacza brak portu w adresie, czyli 80 albo 443
        return "schemat=" + uri.getScheme()
                + " | host=" + uri.getHost()
                + " | port=" + (port == -1 ? "domyślny" : String.valueOf(port))
                + " | ścieżka=" + uri.getRawPath()
                + " | query=" + uri.getRawQuery();
    }

    /** Dokleja adres względny, ale odrzuca zmianę schematu lub serwera bazowego. */
    public static String doklejSciezke(String bazowy, String wzgledny) {
        URI baza = URI.create(bazowy);
        URI wynik = baza.resolve(wzgledny);
        if (!baza.isAbsolute() || baza.getRawAuthority() == null
                || !baza.getScheme().equalsIgnoreCase(wynik.getScheme())
                || !baza.getRawAuthority().equalsIgnoreCase(wynik.getRawAuthority())) {
            throw new IllegalArgumentException("Adres względny prowadzi poza serwer bazowy.");
        }
        return wynik.toString();
    }

    // ── PRZYKŁADY ───────────────────────────────────────────
    public static void main(String[] args) {
        Map<String, String> parametry = new LinkedHashMap<>();
        parametry.put("fraza", "żółw ćma");
        parametry.put("miasto", "Łódź");
        parametry.put("sortuj", "cena rosnąco");
        parametry.put("strona", "2");

        String adres = zbudujAdres("https://przyklad.pl/szukaj", parametry);
        System.out.println("adres:    " + adres);
        System.out.println("dopisany: " + zbudujAdres(adres, Map.of("format", "json")));

        // Ten sam tekst w ścieżce wygląda inaczej: %20 zamiast "+"
        System.out.println("parametr: " + kodujParametr("raport marzec 2026.pdf"));
        System.out.println("segment:  " + kodujSegmentSciezki("raport marzec 2026.pdf"));
        System.out.println("ścieżka:  " + zbudujSciezke("pliki", "Łódź", "raport marzec.pdf"));

        // Do rozbierania bierzemy getRawQuery(), czyli wersję jeszcze nie zdekodowaną
        String query = URI.create(adres).getRawQuery() + "&debug&pusty=";
        System.out.println("parametry z adresu:");
        for (Map.Entry<String, String> wpis : rozbijQuery(query).entrySet()) {
            System.out.println("    " + wpis.getKey() + " -> [" + wpis.getValue() + "]");
        }

        System.out.println(opiszAdres("https://api.przyklad.pl:8443/v1/klienci?limit=20&od=2026-01-01"));
        System.out.println(opiszAdres("https://przyklad.pl/szukaj"));

        String katalog = "https://przyklad.pl/dokumenty/2026/";
        System.out.println("względny: " + doklejSciezke(katalog, "raport.pdf"));
        System.out.println("od korzenia: " + doklejSciezke(katalog, "/cennik.pdf"));
        System.out.println("poziom wyżej: " + doklejSciezke(katalog, "../2025/raport.pdf"));
    }
}

Jak użyć tego skryptu

  1. Wklej kod do pliku AdresyUrl.java — nazwa musi być identyczna jak nazwa klasy publicznej — i uruchom poleceniem java AdresyUrl.java. Wydruk pokaże gotowy adres z frazą „żółw ćma” i miastem „Łódź”, ten sam tekst zakodowany raz dla parametru, a raz dla ścieżki, oraz komplet parametrów odczytanych z powrotem.
  2. We własnym projekcie wywołuj AdresyUrl.zbudujAdres(bazowy, parametry), przekazując LinkedHashMap<String, String> — zwykła HashMap ustawi parametry w przypadkowej kolejności, przez co ten sam zestaw filtrów da za każdym razem inny adres i rozjedzie Ci się cache. Wpis o wartości null jest pomijany, więc opcjonalne filtry możesz wrzucać do mapy bez sprawdzania warunkiem.
  3. Do ścieżek używaj zbudujSciezke("pliki", "Łódź", "raport marzec.pdf") i podawaj każdy segment osobno. Nie przepuszczaj przez tę metodę gotowego kawałka ścieżki z ukośnikami — URLEncoder zamieni każdy ukośnik na %2F i zamiast trzech katalogów dostaniesz jedną dziwną nazwę pliku.
  4. Parametry z gotowego adresu odczytasz przez rozbijQuery(URI.create(adres).getRawQuery()). Sięgaj po getRawQuery(), a nie po getQuery() — ta druga metoda oddaje tekst już zdekodowany, więc wartość zawierająca & albo = rozpadnie się przy dzieleniu na osobne, fałszywe parametry. Adresy względne z odpowiedzi API doklejaj metodą doklejSciezke(...): po URI.resolve sprawdza ona schemat i pełne authority, więc odrzuci także zapis //obcy-serwer.pl/plik.

Warto wiedzieć

  • Najczęstszy błąd: URLEncoder koduje spację jako +. W query stringu to poprawne, bo obowiązuje tam format application/x-www-form-urlencoded, ale w ścieżce adresu + jest zwykłym znakiem plus i serwer poszuka pliku o nazwie raport+marzec.pdf. Dlatego kodujSegmentSciezki(...) podmienia + na %20 — bezpiecznie, bo prawdziwy plus z tekstu został już wcześniej zamieniony na %2B.
  • Nie przepuszczaj przez URLEncoder całego adresu. Ta klasa koduje wszystko, czego nie zna, więc z https://przyklad.pl/a b zrobi https%3A%2F%2Fprzyklad.pl%2Fa+b — razem z :// i ukośnikami. Koduj wyłącznie pojedyncze wartości, a szkielet adresu składaj sam.
  • Query string wycieka nagłówkiem Referer: gdy z Twojej strony użytkownik kliknie link na obcą witrynę, tamten serwer dostanie w prezencie komplet parametrów adresu, na którym stał. Dlatego do zbudujAdres(...) nie wkładaj haseł, tokenów resetu hasła ani kluczy API — nawet jednorazowych. Takie dane przekazuje się nagłówkiem żądania albo w ciele metody POST.
  • rozbijQuery(...) opiera się na URLDecoder, a ten przy uszkodzonym zapisie procentowym rzuca IllegalArgumentException — wywróci go zarówno ?rabat=100% (urwany znak procenta), jak i ?a=%ZZ. To wyjątek niesprawdzany, więc kompilator o nim nie przypomni, a query string prawie zawsze pochodzi od użytkownika. Jeśli podajesz tej metodzie cudzy adres, otocz wywołanie blokiem try i odpowiadaj kodem 400 zamiast pozwolić, żeby wyjątek ubił żądanie.
#http #api #parsowanie #polskie znaki #utf-8

Podobne skrypty