Веб-приложение на Java — это программа, которая работает на сервере в JVM, принимает HTTP-запросы от браузера или другого сервиса и возвращает ответ: HTML-страницу или данные, чаще всего JSON. Интерфейс в браузере и серверная часть на Java — две разные программы, которые общаются по HTTP.
Содержание
- Мини-словарь: кто за что отвечает
- Шаг 0. HTTP-сервер на чистом JDK
- Сервлет-контейнер или Spring Boot
- Первый REST-контроллер на Spring Boot
- Типовая ошибка: неверный код ответа
- Если не получилось
- Что еще нужно в production
- Что учить: дорожная карта до своего сервиса
- Выводы
- Где применяется / связь с практикой
- FAQ
Ниже — путь от десятка строк на чистом 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-образом.
- HTTP: методы, коды ответов, заголовки, JSON. Проверка — объяснить каждую строку ответа
curl -iвыше. - Java Core: классы, коллекции, исключения, records, лямбды.
- Сборка: Maven или Gradle, зависимости,
mvn package. - Spring Boot: контроллеры, внедрение зависимостей, конфигурация через
application.properties. - Данные: SQL и PostgreSQL, затем JDBC и Spring Data JPA.
- Тесты: JUnit, MockMvc, тесты с реальной БД через Testcontainers.
- Безопасность: Spring Security, валидация входа.
- Доставка: 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 — надстройка, которая дает автоконфигурацию, стартеры и встроенный сервер, чтобы приложение запускалось без ручной настройки.



