Как создать веб-приложение на Java: от HTTP-сервера до REST на Spring Boot

Как создать веб-приложение на Java: от HTTP-сервера до REST на Spring Boot Полезное

Веб-приложение на Java — это программа, которая работает на сервере в JVM, принимает HTTP-запросы от браузера или другого сервиса и возвращает ответ: HTML-страницу или данные, чаще всего JSON. Интерфейс в браузере и серверная часть на Java — две разные программы, которые общаются по HTTP.

Ниже — путь от десятка строк на чистом JDK до REST-сервиса на Spring Boot, который собирается в один jar. Все примеры проверены 23.09.2026 на Java 25.0.4 (LTS), Spring Boot 4.1.1 и Maven 3.9.16.

Мини-словарь: кто за что отвечает

Четыре термина постоянно смешивают, хотя это разные уровни.

Термин Что делает Примеры
Веб-сервер Принимает HTTP, отдает статику, проксирует запросы дальше Nginx, Apache HTTP Server
Сервлет-контейнер Запускает Java-код по спецификации Jakarta Servlet: разбирает запрос, вызывает сервлет, управляет потоками Apache Tomcat, Jetty
Сервер приложений Контейнер плюс полный набор Jakarta EE (транзакции, JPA, CDI, JMS) WildFly, Payara, Open Liberty
Фреймворк Дает модель программирования поверх контейнера: контроллеры, внедрение зависимостей, конфигурацию Spring Boot, Quarkus, Micronaut

В типичной схеме Nginx стоит впереди и держит HTTPS, а за ним работает Java-приложение со встроенным контейнером. Граница условная: Tomcat тоже умеет HTTPS, а Nginx не нужен для локальной разработки.

Шаг 0. HTTP-сервер на чистом JDK

Чтобы увидеть механику без магии фреймворков, начнем со встроенного в JDK пакета com.sun.net.httpserver. Это минимальный полный пример: сохраните его как HelloServer.java.

import com.sun.net.httpserver.HttpServer;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;

public class HelloServer {
    public static void main(String[] args) throws Exception {
        HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 8080), 0);
        server.createContext("/hello", exchange -> {
            if (!"GET".equals(exchange.getRequestMethod())) {
                exchange.sendResponseHeaders(405, -1); // метод не разрешен, тела нет
                exchange.close();
                return;
            }
            byte[] body = "{\"message\":\"Привет, веб!\"}".getBytes(StandardCharsets.UTF_8);
            exchange.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8");
            exchange.sendResponseHeaders(200, body.length);
            try (OutputStream out = exchange.getResponseBody()) {
                out.write(body);
            }
        });
        server.start();
        System.out.println("Слушаю http://127.0.0.1:8080/hello");
    }
}

Запуск без отдельной компиляции — java HelloServer.java. В консоли появится строка «Слушаю http://127.0.0.1:8080/hello». Проверяем из второго терминала:

curl -i http://127.0.0.1:8080/hello
curl -i -X POST http://127.0.0.1:8080/hello
HTTP/1.1 200 OK
Content-type: application/json; charset=utf-8
Content-length: 35

{"message":"Привет, веб!"}

HTTP/1.1 405 Method Not Allowed
Content-length: 0

Что здесь видно. Сервер слушает только 127.0.0.1, поэтому снаружи машины он недоступен — для учебы это безопаснее, чем 0.0.0.0. Длина тела 35 байт, хотя символов меньше: кириллица в UTF-8 занимает 2 байта на букву, и Content-Length считается в байтах. Путь, для которого нет контекста, получает 404.

Граница примера: такой сервер годится для учебы, тестов и маленьких утилит. Маршрутизации, разбора JSON, валидации и обработки ошибок в нем нет — все это пришлось бы писать вручную. Именно эту работу берут на себя контейнер и фреймворк.

Сервлет-контейнер или Spring Boot

Классический путь Java EE — собрать приложение в WAR и положить его во внешний Tomcat. Современный путь — встроить контейнер в приложение и запускать один jar.

Критерий WAR во внешнем контейнере Spring Boot со встроенным контейнером
Как запускается Tomcat запущен отдельно, WAR деплоится в него java -jar app.jar, Tomcat внутри
Кто управляет версией контейнера Администратор сервера Разработчик, через зависимости
Когда выбирают Существующая инфраструктура, несколько приложений в одном контейнере Новые сервисы, Docker, микросервисы
Порог входа Нужно понимать дескрипторы и настройку контейнера Автоконфигурация, старт за минуты

Важная деталь для старого кода: с Jakarta EE 9 пакеты javax.servlet переименованы в jakarta.servlet. Tomcat 10 и новее работает только с jakarta.*, поэтому учебники с javax.servlet без правок на актуальном стеке не соберутся.

Spring Boot не заменяет сервлеты, а строится на них: ниже в логе видно, что внутри работает Tomcat, а при первом запросе инициализируется DispatcherServlet — один сервлет Spring, который принимает все запросы и раздает их контроллерам.

Первый REST-контроллер на Spring Boot

Проект удобнее всего сгенерировать на start.spring.io, но минимальный вариант помещается в два файла. Файл pom.xml:

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>4.1.1</version>
    </parent>
    <groupId>ru.example</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1</version>
    <properties>
        <java.version>25</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webmvc</artifactId>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

Файл src/main/java/ru/example/demo/DemoApplication.java:

package ru.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;

@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

record Greeting(String message, int length) {}

@RestController
class GreetingController {
    @GetMapping("/api/greeting")
    Greeting greet(@RequestParam(defaultValue = "мир") String name) {
        if (name.isBlank() || name.length() > 50) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "name: от 1 до 50 символов");
        }
        return new Greeting("Привет, " + name + "!", name.length());
    }
}

Сборка и запуск:

mvn package
java -jar target/demo-0.0.1.jar

В логе (сокращено) видно встроенный Tomcat, время старта и, после первого запроса, инициализацию DispatcherServlet:

 :: Spring Boot ::                (v4.1.1)
Tomcat initialized with port 8080 (http)
Starting Servlet engine: [Apache Tomcat/11.0.24]
Tomcat started on port 8080 (http) with context path '/'
Started DemoApplication in 0.801 seconds (process running for 0.985)
Initializing Spring DispatcherServlet 'dispatcherServlet'

Запросы и ответы:

$ curl localhost:8080/api/greeting
{"message":"Привет, мир!","length":3}

$ curl "localhost:8080/api/greeting?name=Otus"
{"message":"Привет, Otus!","length":4}

$ curl -i -X POST localhost:8080/api/greeting
HTTP/1.1 405
Allow: GET
Content-Type: application/json

{"timestamp":"2026-09-23T15:52:41.367Z","status":405,"error":"Method Not Allowed","path":"/api/greeting"}

Что делает каждая часть

  • @SpringBootApplication включает автоконфигурацию и сканирование компонентов в пакете ru.example.demo и вложенных в него.
  • @RestController говорит, что возвращаемый объект нужно записать в тело ответа, а не искать HTML-шаблон.
  • @GetMapping связывает метод с путем и HTTP-методом: на POST Spring сам отвечает 405 с заголовком Allow: GET.
  • record Greeting превращается в JSON библиотекой Jackson, которая приходит вместе со стартером.
  • @RequestParam(defaultValue = "мир") берет параметр из строки запроса и подставляет значение по умолчанию. Оно срабатывает и на пустой параметр: ?name= вернет «Привет, мир!», поэтому проверка isBlank() ловит только строку из пробелов (?name=%20).

Сравните с шагом 0: маршрутизация, выбор метода, сериализация JSON и заголовки теперь делаются фреймворком.

Типовая ошибка: неверный код ответа

Новички часто сообщают об ошибке ввода обычным исключением.

Неверно:

if (name.isBlank() || name.length() > 50) {
    throw new IllegalArgumentException("name: от 1 до 50 символов");
}

Результат на ?name=%20 (пробел):

HTTP/1.1 500
{"timestamp":"2026-09-23T15:46:27.228Z","status":500,"error":"Internal Server Error","path":"/api/greeting"}

Клиент получает 500, как будто упал сервер, хотя ошибся он сам. Исправление — ResponseStatusException(HttpStatus.BAD_REQUEST, ...), как в рабочем примере выше. Результат после исправления:

HTTP/1.1 400
{"timestamp":"2026-09-23T15:48:11.592Z","status":400,"error":"Bad Request","path":"/api/greeting"}

Обратите внимание: текст сообщения в ответ не попал. По умолчанию Spring Boot не отдает клиенту сообщения исключений и стектрейсы — это защита от утечки внутренностей, отключать ее в production не стоит. Для большого API вместо проверок вручную используют Bean Validation (spring-boot-starter-validation, аннотации @Size, @NotBlank) и единый обработчик @RestControllerAdvice.

Если не получилось

Симптом Причина Что делать
Web server failed to start. Port 8080 was already in use. Порт занят другим процессом, часто старым экземпляром приложения Остановить процесс или задать server.port=8081 в application.properties
no main manifest attribute, in target/demo-0.0.1.jar В pom.xml нет spring-boot-maven-plugin, собрался обычный jar без зависимостей Вернуть плагин в <build> и пересобрать
404 на путь, который точно есть в контроллере Контроллер лежит вне пакета класса с @SpringBootApplication Перенести класс в тот же пакет или вложенный
Fatal error compiling: error: release version 25 not supported Версия JDK, которой запущен Maven, ниже, чем java.version в pom.xml Проверить java -version и mvn -v, поставить JDK 25 или понизить java.version

Все четыре случая воспроизведены прогоном, тексты ошибок — из реального вывода.

Что еще нужно в production

Пример выше — учебный. Перед тем как выставлять сервис наружу, нужны еще:

  • аутентификация и авторизация (Spring Security), защита от CSRF там, где есть cookie-сессии;
  • HTTPS — на обратном прокси или в самом контейнере;
  • секреты (пароли БД, ключи API) в переменных окружения или хранилище секретов, не в коде и не в git;
  • лимиты размера запросов и частоты обращений;
  • логи без персональных данных и паролей, метрики и health-check (Spring Boot Actuator, доступ к нему закрыт);
  • тесты контроллеров (MockMvc, @SpringBootTest) в сборке.

Что учить: дорожная карта до своего сервиса

Цель — не «понять веб», а собрать конкретный артефакт: REST-сервис с базой данных, тестами и Docker-образом.

  1. HTTP: методы, коды ответов, заголовки, JSON. Проверка — объяснить каждую строку ответа curl -i выше.
  2. Java Core: классы, коллекции, исключения, records, лямбды.
  3. Сборка: Maven или Gradle, зависимости, mvn package.
  4. Spring Boot: контроллеры, внедрение зависимостей, конфигурация через application.properties.
  5. Данные: SQL и PostgreSQL, затем JDBC и Spring Data JPA.
  6. Тесты: JUnit, MockMvc, тесты с реальной БД через Testcontainers.
  7. Безопасность: Spring Security, валидация входа.
  8. Доставка: Dockerfile, запуск контейнера, переменные окружения.

Срок сильно зависит от стартового уровня и часов в неделю, поэтому ориентир честнее ставить по артефактам: закрыт пункт, когда соответствующая часть работает в вашем сервисе.

Фронтенд — отдельная программа. Раньше HTML собирали на сервере через JSP; сегодня это legacy, а для серверных шаблонов в Spring берут Thymeleaf. Чаще страница на JavaScript сама вызывает JSON API через fetch, а для обмена в реальном времени есть WebSocket (схемы ws:// и wss://).

Выводы

  • Веб-приложение на Java — серверная программа в JVM, которая отвечает на HTTP-запросы HTML или JSON.
  • Веб-сервер, сервлет-контейнер, сервер приложений и фреймворк — разные уровни; Spring Boot работает поверх встроенного Tomcat.
  • Для старта хватает двух файлов: pom.xml со стартером spring-boot-starter-webmvc и класса с @RestController; результат — один jar.
  • Ошибку клиента возвращают кодом 4xx, а не 500; сообщения и стектрейсы наружу по умолчанию не уходят.
  • Учебный пример не готов к production без аутентификации, HTTPS, управления секретами и тестов.

Где применяется / связь с практикой

Освойте тему на практике

REST-сервисы на Spring Boot — основа бэкенда в банках, маркетплейсах, телекоме и внутренних системах компаний: мобильные и веб-клиенты, интеграции и микросервисы общаются именно через такие контроллеры. Следующий шаг после примера из статьи — подключить базу, безопасность и тесты. Системно это разбирается на курсе Разработчик на Spring Framework, а попробовать формат и задать вопросы практикующим разработчикам можно на открытых уроках Otus.

FAQ

Можно ли сделать веб-приложение на Java без Spring?
Да: подойдут Jakarta EE на сервере приложений, Quarkus, Micronaut, Javalin или чистые сервлеты в Tomcat. Spring Boot популярен из-за экосистемы и количества вакансий, но не обязателен.

Что выбрать для сборки: Maven или Gradle?
Оба инструмента поддерживаются Spring Boot и start.spring.io. Maven проще читать новичку благодаря декларативному XML, Gradle гибче и часто быстрее на больших проектах.

Чем Spring Boot отличается от Spring Framework?
Spring Framework — это сам фреймворк (контейнер зависимостей, Spring MVC, транзакции). Spring Boot — надстройка, которая дает автоконфигурацию, стартеры и встроенный сервер, чтобы приложение запускалось без ручной настройки.

OTUS Журнал
Скидка 5% 12-19 октября на курсы (popup)
Бесплатные открытые уроки (поп-ап)