Урок 10 из 11 уверенный около 40 мин

Файлы и HTTP-клиент: читаем, пишем, запрашиваем

Files.readString и writeString, обход каталога, HttpClient из стандартной библиотеки и разбор ответа без внешних зависимостей.

Файлы: коротко о главном

Прочитать текстовый файл целиком в современной Java — одна строка. Класс Files появился ещё в Java 7, но методы readString и writeString добавили только в одиннадцатой версии, и во многих учебниках их до сих пор нет.

import java.nio.file.*;

var path = Path.of("notes.txt");
Files.writeString(path, """
        Первая строка
        Вторая строка
        """);

String content = Files.readString(path);
System.out.print(content);

Кодировка по умолчанию — UTF-8 независимо от системных настроек, поэтому русский текст читается одинаково на Windows и Linux. Класс Path собирает путь из частей без ручной склейки разделителей: Path.of("docs", "2026", "otchet.txt").

Когда файл большой

Читать целиком стоит только небольшие файлы: readString держит весь текст в памяти. Для журналов и выгрузок используйте построчный поток и обязательно закрывайте его через try-with-resources:

try (var lines = Files.lines(Path.of("access.log"))) {
    long errors = lines.filter(line -> line.contains(" 500 ")).count();
    System.out.println("Ошибок сервера: " + errors);
} catch (java.io.IOException e) {
    System.err.println("Не удалось прочитать журнал: " + e.getMessage());
}

Files.lines держит открытый файловый дескриптор, и без try он останется висеть. Это одно из немногих мест, где поток обязан закрываться.

Полезные операции с файловой системой

Files.exists(path);                       // существует ли
Files.createDirectories(Path.of("out/2026"));  // создать вместе с родителями
Files.copy(path, Path.of("copy.txt"), StandardCopyOption.REPLACE_EXISTING);
Files.size(path);                         // размер в байтах
Files.deleteIfExists(path);               // удалить, если есть

// Дописать в конец, а не перезаписать
Files.writeString(path, "ещё строка\n", StandardOpenOption.CREATE, StandardOpenOption.APPEND);

// Обойти каталог: walk рекурсивно, list — только верхний уровень
try (var files = Files.walk(Path.of("src"))) {
    files.filter(Files::isRegularFile)
         .filter(p -> p.toString().endsWith(".java"))
         .forEach(System.out::println);
}

Временные файлы создавайте через Files.createTempFile: он сам выберет каталог и права доступа, а имя не столкнётся с чужим.

HttpClient вместо сторонних библиотек

С Java 11 в стандартной библиотеке есть полноценный HTTP-клиент с поддержкой HTTP/2, редиректов и асинхронных запросов. Для большинства задач внешняя библиотека больше не нужна.

import java.net.URI;
import java.net.http.*;
import java.time.Duration;

var client = HttpClient.newBuilder()
        .connectTimeout(Duration.ofSeconds(10))
        .followRedirects(HttpClient.Redirect.NORMAL)
        .build();

var request = HttpRequest.newBuilder(URI.create("https://school-fortuna.ru/robots.txt"))
        .header("Accept", "text/plain")
        .timeout(Duration.ofSeconds(15))
        .GET()
        .build();

HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode());   // 200
System.out.println(response.body());

Клиент создают один раз на приложение и переиспользуют: он держит пул соединений. Два таймаута отвечают за разное: connectTimeout ограничивает установление соединения, timeout у запроса — весь обмен.

Отправка данных и заголовки

var body = """
        {"login":"anna","lesson":3}
        """;

var post = HttpRequest.newBuilder(URI.create("https://example.org/api/progress"))
        .header("Content-Type", "application/json; charset=utf-8")
        .POST(HttpRequest.BodyPublishers.ofString(body))
        .build();

var result = client.send(post, HttpResponse.BodyHandlers.ofString());
if (result.statusCode() / 100 != 2) {
    throw new IllegalStateException("Сервер ответил " + result.statusCode());
}

Проверять код ответа обязательно: клиент не считает 404 или 500 ошибкой и не бросает исключение — для него это нормальный ответ. Исключение будет только при сетевом сбое или таймауте.

Скачивание прямо в файл и асинхронный запрос

// Тело ответа сразу на диск, без промежуточной строки
var download = client.send(request, HttpResponse.BodyHandlers.ofFile(Path.of("robots.txt")));
System.out.println("Сохранено в " + download.body());

// Асинхронно: метод возвращает управление сразу
client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
        .thenApply(HttpResponse::body)
        .thenAccept(text -> System.out.println("Получено символов: " + text.length()))
        .join();   // в примере ждём результат, в приложении — нет

Три правила безопасности

  • Никогда не подставляйте пользовательский ввод в путь без проверки: Path.of(base).resolve(name).normalize().startsWith(base) защищает от выхода за каталог через «..».
  • Не логируйте заголовок Authorization и тело запроса с паролями — журналы живут дольше, чем кажется.
  • Ставьте таймауты всегда. Запрос без таймаута может ждать бесконечно и заблокировать поток.

Пример целиком: сохранить страницу и посчитать строки

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;

public class Fetch {
    public static void main(String[] args) throws Exception {
        var url = URI.create("https://school-fortuna.ru/sitemap.xml");
        var target = Path.of("sitemap.xml");

        var client = HttpClient.newHttpClient();
        var request = HttpRequest.newBuilder(url).timeout(Duration.ofSeconds(20)).GET().build();
        var response = client.send(request, HttpResponse.BodyHandlers.ofFile(target));

        if (response.statusCode() != 200) {
            System.err.println("Ответ " + response.statusCode());
            return;
        }
        try (var lines = Files.lines(target)) {
            System.out.println("Строк в файле: " + lines.count());
        }
    }
}

Запускается одной командой java Fetch.java — ни сборки, ни зависимостей.

Попробуйте сами

  1. Напишите программу, которая обходит каталог с исходниками и печатает пять самых больших файлов с их размерами.
  2. Запросите https://school-fortuna.ru/robots.txt и выведите только заголовки ответа: response.headers().map().
  3. Добавьте в пример Fetch обработку случая, когда сервер недоступен: поймайте java.io.IOException и напечатайте понятное сообщение вместо стека вызовов.