Skip to the content.

Веб-сервер на Java

Главная / Лекция 6. Веб-сервер на Java

Лекция 6. Веб-сервер на Java

Содержание

  1. Аннотации в Java
  2. Аспектно-ориентированное программирование (АОП)
  3. Библиотека Lombok в Java
  4. Record
  5. Работа с JSON
  6. TCP/IP протоколы
  7. HTTP и REST API
  8. Java Servlets
  9. Jetty Server
  10. REST API принципы

Аннотации в Java

Аннотации в java

Аннотации в Java — это специальные конструкции, которые предоставляют дополнительную информацию для компилятора, инструментов разработки и во время выполнения программы. Они не влияют на выполнение кода напрямую, но могут изменять процесс компиляции, генерации кода или поведение приложения в runtime.

Что такое аннотации?

Аннотации — это своего рода «метки» или «теги», которые можно применять к классам, методам, полям и другим элементам программы. Они начинаются с символа @ и могут содержать атрибуты, которые определяют их поведение.

Пример:

@Override
public void someMethod() {
    // тело метода
}

Зачем нужны аннотации?

  1. Упрощение разработки: аннотации помогают автоматизировать некоторые задачи, такие как генерация кода, проверка типов и т. д.
  2. Улучшение читаемости кода: они делают код более понятным, указывая на его назначение или особенности.
  3. Интеграция с инструментами разработки: многие IDE и инструменты сборки используют аннотации для предоставления дополнительных функций, таких как рефакторинг, анализ кода и т. п.
  4. Поддержка фреймворков и библиотек: аннотации часто используются в фреймворках и библиотеках для конфигурации и управления поведением компонентов.

Как написать свою аннотацию?

Для создания собственной аннотации необходимо использовать ключевое слово interface вместе с @interface. Внутри аннотации можно определить атрибуты, которые будут хранить дополнительную информацию.

Пример определения аннотации:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyAnnotation {
    String value() default "default value";
    int id();
}

Здесь:

С какими концепциями программирования связаны аннотации?

Аннотации в Java — это мощный инструмент для улучшения процесса разработки, повышения читаемости кода и интеграции с различными инструментами и фреймворками. Понимание того, как работают аннотации и как их использовать, позволяет разработчикам более эффективно создавать и поддерживать свои приложения.

Аспектно-ориентированное программирование (АОП)

Аспектно-ориентированное программирование (АОП) — это подход к написанию программ, который позволяет выделить и отдельно описать так называемые «поперечные concerns» или «аспекты». Это могут быть функции, которые пересекаются с основной логикой программы и применяются в разных её частях, например, логирование, обработка ошибок, управление транзакциями или безопасность.

Как работает АОП?

В АОП используются специальные конструкции — аннотации, которые указывают, где и как должен быть применён аспект. Например, вы можете определить аспект для логирования и указать, что он должен применяться ко всем методам, помеченным определённой аннотацией.

Это позволяет:

Пример

Рассмотрим простой пример, который иллюстрирует основные идеи аспектно-ориентированного программирования (АОП). Предположим, у нас есть программа, которая выполняет простые арифметические операции: сложение и умножение. Мы хотим добавить логирование времени выполнения каждой операции без изменения исходного кода этих операций.

public class Calculator {
    public int add(int a, int b) {
        return a + b;
    }

    public int multiply(int a, int b) {
        return a * b;
    }
}
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
@interface LogExecutionTime {
}

public class Calculator {

    @LogExecutionTime
    public int add(int a, int b) {
        return a + b;
    }

    @LogExecutionTime
    public int multiply(int a, int b) {
        return a * b;
    }
}

Сам аспект мы не будем реализовывать - его можно реализовать с помощью AspectJ. AspectJ — это расширение языка Java, которое позволяет реализовывать аспектно-ориентированное программирование (АОП).

Библиотека Lombok в Java

Библиотека Lombok — это инструмент, который упрощает разработку на Java, автоматизируя создание шаблонного кода. Она позволяет сократить количество строк кода и сделать его более читаемым, устраняя необходимость вручную реализовывать стандартные методы, такие как геттеры, сеттеры, конструкторы и т. д.

Что такое Lombok?

Lombok — это библиотека с открытым исходным кодом, которая использует аннотации для генерации дополнительного кода во время компиляции. Это означает, что вместо того, чтобы писать много строк шаблонного кода, разработчики могут использовать аннотации Lombok, и библиотека автоматически сгенерирует необходимый код.

Зачем нужна Lombok?

Примеры использования Lombok

Генерация геттеров и сеттеров без Lombok:

public class Person {
    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

С lombok

import lombok.Getter;
import lombok.Setter;

public class Person {
    @Getter @Setter
    private String name;
    @Getter @Setter
    private int age;
}

Как добавить в проект:

plugins {
    id 'io.franzbecker.gradle-lombok' version '5.0.0'
    id 'java'
}

lombok {
    version = '1.18.26'
    sha256 = ""
}

Возможности

@Getter @Setter

@Getter
@Setter
public class Author {
    private int id;
    private String name;
    @Setter(AccessLevel.PROTECTED)
    private String surname;
}

public class Author {
    private int id;
    private String name;
    private String surname;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getSurname() {
        return surname;
    }

    protected void setSurname(String surname) {
        this.surname = surname;
    }
}

@NoArgsConstructor - создает дефолтный конструктор @AllArgsConstructor - создает конструктор, где все поля класса - аргументы

@NoArgsConstructor
@AllArgsConstructor
public class Author {
    private int id;
    private String name;
    private String surname;
    private final String birthPlace;
}

public class Author {
    private int id;
    private String name;
    private String surname;
    private final String birthPlace;

    // @NoArgsConstructor
    public Author() {}

    // @AllArgsConstructor
    public Author(int id, String name, String surname, String birthPlace) {
        this.id = id
        this.name = name
        this.surname = surname
        this.birthPlace = birthPlace
    }

    // @RequiredArgsConstructor
    public Author(String birthPlace) {
        this.birthPlace = birthPlace
    }
}

@ToString - создает метод ToString

@ToString(includeFieldNames=true)
public class Author {
    private int id;
    private String name;
    private String surname;
}

public class Author {
    private int id;
    private String name;
    private String surname;

    @Override
    public String toString() {
        return "Author(id=" + this.id + ", name=" + this.name + ", surnname=" + this.surname + ")";
    }
}
@Getter
@Setter
@EqualsAndHashCode
public class Author {
    private int id;
    private String name;
    private String surname;
}

public class Author {

    // геттеры и сеттеры ...

    @Override
    public int hashCode() {
        final int PRIME = 31;
        int result = 1;
        result = prime * result + id;
        result = prime * result + ((name == null) ? 0 : name.hashCode());
        result = prime * result + ((surname == null) ? 0 : surname.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object o) {
        if (o == this) return true;
        if (!(o instanceof Author)) return false;
        Author other = (Author) o;
        if (!other.canEqual((Object)this)) return false;
        if (this.getId() == null ? other.getId() != null : !this.getId().equals(other.getId())) return false;
        if (this.getName() == null ? other.getName() != null : !this.getName().equals(other.getName())) return false;
        if (this.getSurname() == null ? other.getSurname() != null : !this.getSurname().equals(other.getSurname())) return false;
        return true;
    }
}

@Data - сокращенная аннотация сочетающая @ToString, @EqualsAndHashCode, @Getter @Setter и @RequiredArgsConstructor.

@Getter
@Builder
public class Student {
    private String firstName;
    private String lastName;
    private Long studentId;
    private String email;
    private String phoneNumber;
    private String address;
    private String country;
    private int age;
}

class Temp {
    Student john = Student.builder()
            .firstName("John")
            .lastName("Doe")
            .email("john@doe.com")
            .country("England")
            .age(20)
            .build();
}

Плюсы Lombok

Недостатки Lombok

Полезные ссылки

Статья Lombok. Полное руководство

Статья Lombok возвращает величие Java

Статья Lombok: хорошее и плохое применение

Record

Record — это относительно новая конструкция в языке Java, которая упрощает создание неизменяемых данных (immutable data). Она появилась в Java 16 как часть усилий по упрощению и улучшению языка.

Зачем нужны Record?

Когда появились Record?

Record были введены в Java 16 в марте 2021 года как часть проекта Amber, направленного на улучшение и упрощение языка Java.

Как раскрываются Record после компиляции?

После компиляции Record раскрывается в обычный класс с некоторыми автоматически сгенерированными методами:

Пример записи:

public record Person(String name, int age) {
    // автоматически генерируются геттеры, equals, hashCode и toString
}

Компилятор преобразует этот код в полноценный класс с соответствующими методами, что позволяет разработчикам сосредоточиться на логике приложения, а не на реализации базовых функциональностей.

Пример того, как будет выглядеть класс Person, созданный с использованием record, после компиляции в обычный класс Java:

import java.util.Objects;

public final class Person {
    private final String name;
    private final int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String name() {
        return name;
    }

    public int age() {
        return age;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return age == person.age && Objects.equals(name, person.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }

    @Override
    public String toString() {
        return "Person{" + "name=" + name + ", age=" + age + '}';
    }
}

Наследоваться от Record в Java нельзя, поскольку Record по умолчанию является final классом. Это сделано для обеспечения неизменяемости и простоты использования, что является одной из ключевых особенностей Record. Запрет на наследование помогает предотвратить потенциальные проблемы, связанные с нарушением инвариантов и изменением состояния объекта, что противоречит концепции неизменяемых данных.

Работа с JSON.

JSON (JavaScript Object Notation) — это формат обмена данными, который используется для хранения и передачи структурированной информации. Он основан на синтаксисе объектов JavaScript и представляет данные в виде текстовых объектов, что делает его лёгким для чтения как для человека, так и для машины. JSON широко применяется в веб-разработке для обмена данными между сервером и клиентом, а также между различными приложениями.

С помощью JSON можно отправлять данные с сервера на веб-страницу.

Пример:

{
  "name": "yandex",
  "id": 0
}

JSON обладает несколькими ключевыми особенностями:

В Java существует несколько подходов к работе с JSON:

Парсинг JSON:

Сериализация / десериализация объектов:

Json Simple

Json Simple — это библиотека для работы с JSON в Java, которая предоставляет простой и удобный способ парсить JSON-данные, а также создавать и манипулировать JSON-объектами.

Основные возможности Json Simple:

Конечно нам потребуется зависимость в gradle-файле (maven)

implementation 'com.googlecode.json-simple:json-simple:1.1'

Мы будем работать с 2-мя основным классами:

class Temp {
    public static String buildWeatherJson() {
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("name", "Yandex");
        jsonObject.put("id", "0");

        return jsonObject.toJSONString();
    }

    public static void parseCurrentWeatherJson(String resultJson) {
        try {
            // конвертируем строку с Json в JSONObject для дальнейшего его парсинга
            JSONObject companyJsonObject = (JSONObject) JSONValue.parseWithException(resultJson);

            // получаем название города, для которого смотрим погоду
            System.out.println("Название компании: " + companyJsonObject.get("name"));

            // получаем массив элементов для поля weather
            /* ... "weather": [
            {
                "id": 500,
                    "main": "Rain",
                    "description": "light rain",
                    "icon": "10d"
            }
            ], ... */
            JSONArray weatherArray = (JSONArray) weatherJsonObject.get("weather");
            // достаем из массива первый элемент
            JSONObject weatherData = (JSONObject) weatherArray.get(0);

            // печатаем текущую погоду в консоль
            System.out.println("Погода на данный момент: " + weatherData.get("main"));
            // и описание к ней
            System.out.println("Более детальное описание погоды: " + weatherData.get("description"));

        } catch (org.json.simple.parser.ParseException e) {
            e.printStackTrace();
        }
    }
}

Jackson

Jackson — это популярная библиотека для работы с JSON в Java, которая предоставляет мощные возможности для сериализации и десериализации объектов. Она позволяет преобразовывать Java-объекты в JSON-формат и обратно, что упрощает обмен данными между приложениями и хранение информации.

Основные возможности Jackson:

Подключение в gradle

implementation 'com.fasterxml.jackson.core:jackson-core:2.10.1'
implementation 'com.fasterxml.jackson.core:jackson-annotations:2.10.1'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.10.1'
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class Employee {
    private String firstName;
    private String lastName;
    private int age;
}

public class JacksonExample {
    static ObjectMapper objectMapper = new ObjectMapper();

    public static void main(String[] args) {
        Employee employee = new Employee("Mark", "James", 20);

        try {
            String json = objectMapper.writeValueAsString(employee);
            System.out.println(json);
        } catch (JsonProcessingException e) {
            throw new RuntimeException();
        }

        try {
            Employee employee1 = objectMapper.readValue(employeeJson, Employee.class);
            System.out.println(employee.getFirstName());
            System.out.println(employee.getLastName());
            System.out.println(employee.getAge());
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
    }
}

Иногда документ JSON представляет собой не объект, а список объектов. Давайте посмотрим, как можно его прочитать.

class Temp {
    public static void main(String[] args) {
        File file = new File("src/test/resources/employeeList.json");
        List<Employee> employeeList = objectMapper.readValue(file, new TypeReference<>(){});

        assertThat(employeeList).hasSize(2);
        assertThat(employeeList.get(0).getAge()).isEqualTo(33);
        assertThat(employeeList.get(0).getLastName()).isEqualTo("Simpson");
        assertThat(employeeList.get(0).getFirstName()).isEqualTo("Marge");
    }
}

У Jackson есть свои аннотации: @JsonSetter @JsonGetter @JsonIgnore

@JsonAnySetter

public class Car {  
    @JsonSetter("car_brand")  
    private String carBrand;  
    private Map<String, String> unrecognizedFields = new HashMap<>();  
  
    @JsonAnySetter  
    public void allSetter(String fieldName, String fieldValue) {  
        unrecognizedFields.put(fieldName, fieldValue);  
    }  
}

Предисловие 1. Немного про TCP-IP

Как же нам из приложения отправить запрос на сервер, использую эти ваши интернеты?

На примере доставки подарочной плитки шоколадки:

Какие же есть протоколы? Об этом вам подробнее расскажут на Вычислительных системах и компьютерных сетях. Если вкратце - есть различные протоколы, которые определяют различные действия на различных этапов передачи данных между программами.

Так сложилось, что есть две модели, описывающие уровни протоколов. Одна из них теоретическая – модель OSI, а другая практическая – TCP/IP.

Таким образом работа протоколов на пути нашего сообщения идет следующим образом:

Предисловие 2. Немного про HTTP и REST API

Протокол HTTP:

Запрос HTTP

Запрос HTTP состоит из следующих элементов:

Ответ HTTP

Методы HTTP

Коды ответов

Пример запроса

Java Servlets

Веб-контейнер - программа. В веб-контейнере развернут набор сервлетов.

После того как соединение установлено, веб-контейнер формирует 2 объекта: HttpServletReuest and HttpServletResponse.

Зависимости gradle:

    compileOnly 'jakarta.servlet:jakarta.servlet-api:5.0.0'

Пример простого сервлета (для демонстрации базовой структуры):

import java.io.IOException;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

@WebServlet("/ping")
public class PingServlet extends HttpServlet {
    /**
     * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
     */
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // Устанавливаем заголовки ответа ДО записи тела
        response.setContentType("text/html; charset=UTF-8");
        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setStatus(HttpServletResponse.SC_OK);
        
        // Теперь записываем тело ответа
        String reply = "<h1>pong</h1>";
        response.getOutputStream().write(reply.getBytes("UTF-8"));
    }
}

Пример сервлета с обработкой POST и DELETE запросов

Вот упрощенный пример сервлета, который демонстрирует обработку POST и DELETE запросов с парсингом JSON тела запроса:

import java.io.BufferedReader;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

import com.fasterxml.jackson.databind.ObjectMapper;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

// Пример класса для представления пользователя
class User {
    private Long id;
    private String name;
    private String email;
    
    // Конструкторы и геттеры/сеттеры
    // [replacement - остальная часть класса User]
}

@WebServlet("/api/users/*")
public class UserServlet extends HttpServlet {
    // Хранилище пользователей в памяти
    private static final Map<Long, User> users = new ConcurrentHashMap<>();
    private static final AtomicLong idGenerator = new AtomicLong(1);
    
    // Jackson ObjectMapper для работы с JSON
    private ObjectMapper objectMapper = new ObjectMapper();
    
    // Инициализация с тестовыми данными
    @Override
    public void init() throws ServletException {
        // [replacement - инициализация тестовых данных]
    }
    
    /**
     * Обработка GET запросов
     * [replacement - код метода doGet]
     */
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        // [replacement - обработка GET запросов]
    }
    
    /**
     * Обработка POST запросов для создания нового пользователя
     * - Читает тело запроса как JSON
     * - Создает нового пользователя
     * - Возвращает созданного пользователя с кодом 201
     */
    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        
        // Устанавливаем заголовки ответа
        response.setContentType("application/json; charset=UTF-8");
        
        try {
            // Читаем тело запроса
            StringBuilder sb = new StringBuilder();
            String line;
            BufferedReader reader = request.getReader();
            while ((line = reader.readLine()) != null) {
                sb.append(line);
            }
            
            // Проверяем, что тело запроса не пустое
            if (sb.length() == 0) {
                response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
                Map<String, String> error = new HashMap<>();
                error.put("error", "Тело запроса не может быть пустым");
                error.put("code", "EMPTY_REQUEST_BODY");
                objectMapper.writeValue(response.getOutputStream(), error);
                return;
            }
            
            // Преобразуем JSON в объект User
            User user = objectMapper.readValue(sb.toString(), User.class);
            
            // Проверяем обязательные поля
            if (user.getName() == null || user.getName().trim().isEmpty()) {
                response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
                Map<String, String> error = new HashMap<>();
                error.put("error", "Поле 'name' обязательно для заполнения");
                error.put("code", "MISSING_NAME");
                objectMapper.writeValue(response.getOutputStream(), error);
                return;
            }
            
            // Назначаем ID и сохраняем пользователя
            user.setId(idGenerator.getAndIncrement());
            users.put(user.getId(), user);
            
            // Возвращаем созданного пользователя с кодом 201
            response.setStatus(HttpServletResponse.SC_CREATED);
            objectMapper.writeValue(response.getOutputStream(), user);
            
        } catch (com.fasterxml.jackson.core.JsonProcessingException e) {
            // Ошибка парсинга JSON - возвращаем 400
            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
            Map<String, String> error = new HashMap<>();
            error.put("error", "Неверный формат JSON в теле запроса");
            error.put("code", "INVALID_JSON_FORMAT");
            objectMapper.writeValue(response.getOutputStream(), error);
        } catch (Exception e) {
            // В случае внутренней ошибки сервера - возвращаем 500
            response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            Map<String, String> error = new HashMap<>();
            error.put("error", "Внутренняя ошибка сервера");
            error.put("code", "INTERNAL_SERVER_ERROR");
            objectMapper.writeValue(response.getOutputStream(), error);
        }
    }
    
    /**
     * Обработка PATCH запросов для частичного обновления пользователя
     * [replacement - код метода doPatch]
     */
    @Override
    protected void doPatch(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        // [replacement - обработка PATCH запросов]
    }
    
    /**
     * Обработка DELETE запросов для удаления пользователя
     */
    @Override
    protected void doDelete(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        
        // Устанавливаем заголовки ответа
        response.setContentType("application/json; charset=UTF-8");
        
        try {
            // Получаем ID пользователя из URL
            String pathInfo = request.getPathInfo();
            
            if (pathInfo == null || pathInfo.equals("/")) {
                // ID не указан - возвращаем 400
                response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
                Map<String, String> error = new HashMap<>();
                error.put("error", "Не указан ID пользователя для удаления");
                error.put("code", "MISSING_USER_ID");
                objectMapper.writeValue(response.getOutputStream(), error);
                return;
            }
            
            // Извлекаем ID из пути
            String[] parts = pathInfo.split("/");
            if (parts.length != 2) {
                // Неверный формат пути - возвращаем 400
                response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
                Map<String, String> error = new HashMap<>();
                error.put("error", "Неверный формат ID пользователя");
                error.put("code", "INVALID_ID_FORMAT");
                objectMapper.writeValue(response.getOutputStream(), error);
                return;
            }
            
            try {
                Long userId = Long.parseLong(parts[1]);
                User removedUser = users.remove(userId);
                
                if (removedUser != null) {
                    // Пользователь успешно удален - возвращаем 204 No Content
                    response.setStatus(HttpServletResponse.SC_NO_CONTENT);
                    // Нет тела ответа для 204
                } else {
                    // Пользователь не найден - возвращаем 404
                    response.setStatus(HttpServletResponse.SC_NOT_FOUND);
                    Map<String, String> error = new HashMap<>();
                    error.put("error", "Пользователь не найден");
                    error.put("code", "USER_NOT_FOUND");
                    objectMapper.writeValue(response.getOutputStream(), error);
                }
                
            } catch (NumberFormatException e) {
                // Неверный формат ID - возвращаем 400
                response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
                Map<String, String> error = new HashMap<>();
                error.put("error", "Неверный формат ID пользователя");
                error.put("code", "INVALID_ID_FORMAT");
                objectMapper.writeValue(response.getOutputStream(), error);
            }
            
        } catch (Exception e) {
            // В случае внутренней ошибки сервера - возвращаем 500
            response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            Map<String, String> error = new HashMap<>();
            error.put("error", "Внутренняя ошибка сервера");
            error.put("code", "INTERNAL_SERVER_ERROR");
            objectMapper.writeValue(response.getOutputStream(), error);
        }
    }
}

Этот пример демонстрирует:

Экземпляры сервлета создает веб-контейнер в рантайме. После этого сервлет в рантайме может обрабатывать запросы. Веб-контейнер плодит по отдельному потоку на каждый запрос, приходящий к нему, и сервлеты соответственно эти запросы обрабатывают.

Jetty Server

Теперь нам нужен контейнер для наших сервлетов. Наиболее распространенные веб-сервера на java - Tom Cat, Jetty и др. На этой лекции для избежания взрыва мозга посмотрим на Jetty, тк он кажется чуть проще.

Jetty — свободный контейнер сервлетов, написанный полностью на Java.

Конечно нам потребуются зависимости в gradle:

implementation 'org.eclipse.jetty:jetty-servlet:11.0.14'
implementation 'org.eclipse.jetty:jetty-server:11.0.14'
implementation 'com.fasterxml.jackson.core:jackson-core:2.15.2'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.2'

Создадим стартовый класс для нашего сервера:

import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletContextHandler;

public class JettyServer {
    private Server server;

    public void start() throws Exception {
        int port = 8080;  // Определяем порт в переменной
        Server server = new Server(port);  // Используем переменную вместо хардкода
        ServletContextHandler handler = new ServletContextHandler(server, "/");
        
        // Регистрируем оба сервлета
        handler.addServlet(PingServlet.class, "/ping");
        handler.addServlet(UserServlet.class, "/api/users/*");

        try {
            server.start();
            System.out.println("Сервер запущен и слушает порт: " + port);
            System.out.println("Доступные эндпоинты:");
            System.out.println("  - GET /ping - простой пинг-понг");
            System.out.println("  - POST /api/users - создать нового пользователя");
            System.out.println("  - DELETE /api/users/{id} - удалить пользователя");
            
            server.join();  // Добавляем join() чтобы сервер продолжал работать
        } catch (Exception e) {
            System.out.println("Ошибка при запуске сервера:");
            e.printStackTrace();
        }
    }
}

Также создадим главный класс для запуска приложения:

public class Application {
    public static void main(String[] args) {
        JettyServer server = new JettyServer();
        try {
            server.start();
        } catch (Exception e) {
            System.err.println("Не удалось запустить сервер:");
            e.printStackTrace();
        }
    }
}

REST API

REST (Representational State Transfer) — это архитектурный стиль для построения распределённых систем, особенно веб-сервисов. REST API — это API, который следует принципам REST.

Основные принципы REST

  1. Клиент-серверная архитектура: Клиент и сервер разделены, что позволяет им развиваться независимо друг от друга.

  2. Отсутствие состояния (Stateless): Каждый запрос от клиента должен содержать всю информацию, необходимую для понимания и обработки запроса. Сервер не хранит информацию о состоянии клиента между запросами.

  3. Кэшируемость: Ответы от сервера должны быть явно помечены как кэшируемые или нет, что позволяет улучшить производительность.

  4. Единый интерфейс: Унифицированный интерфейс между компонентами упрощает архитектуру и позволяет каждой части развиваться независимо.

  5. Слоистая система: Архитектура может состоять из нескольких слоёв, каждый из которых выполняет свою функцию.

  6. Код по требованию (Code on Demand): Сервер может расширять функциональность клиента, передавая ему исполняемый код (необязательный принцип).

Ресурсы и URI

В REST всё является ресурсом. Каждый ресурс имеет уникальный идентификатор — URI (Uniform Resource Identifier).

Примеры URI для ресурсов:

HTTP методы в REST

REST использует стандартные HTTP методы для выполнения операций над ресурсами:

GET

Примеры:

GET /users              — получить всех пользователей
GET /users/123          — получить пользователя с ID 123
GET /users/123/orders   — получить заказы пользователя 123

POST

Примеры:

POST /users
{
  "name": "Ivan",
  "email": "ivan@example.com"
}
— создать нового пользователя

PUT

Примеры:

PUT /users/123
{
  "id": 123,
  "name": "Ivan Updated",
  "email": "ivan.updated@example.com"
}
— полностью обновить пользователя 123

PATCH

Примеры:

PATCH /users/123
{
  "email": "new.email@example.com"
}
— обновить только email пользователя 123

DELETE

Примеры:

DELETE /users/123  — удалить пользователя 123

Идемпотентность

Идемпотентность — это свойство операции, при котором многократное выполнение одной и той же операции даёт тот же результат, что и однократное выполнение.

Почему идемпотентность важна?

  1. Надёжность в сети: В распределённых системах запросы могут повторяться из-за сетевых проблем, таймаутов или ошибок. Идемпотентные операции гарантируют, что повторные запросы не приведут к нежелательным побочным эффектам.

  2. Безопасность: Предотвращает случайное дублирование операций (например, двойное списание денег с счёта).

  3. Предсказуемость: Поведение системы становится более предсказуемым и отлаживаемым.

Идемпотентность HTTP методов:

Метод Идемпотентный Объяснение
GET ✅ Да Множественные запросы возвращают одни и те же данные
POST ❌ Нет Каждый запрос создаёт новый ресурс
PUT ✅ Да Замена ресурса одним и тем же содержимым даёт тот же результат
PATCH ❌ Нет (обычно) Зависит от операции (например, инкремент не идемпотентен)
DELETE ✅ Да После первого удаления последующие не изменят состояние

Примеры идемпотентности:

Идемпотентная операция (PUT):

# Первый запрос
PUT /accounts/123
{
  "balance": 1000
}
# Результат: баланс = 1000

# Второй запрос (повтор)
PUT /accounts/123
{
  "balance": 1000
}
# Результат: баланс = 1000 (тот же результат)

Не идемпотентная операция (POST):

# Первый запрос
POST /orders
{
  "userId": 123,
  "amount": 100
}
# Результат: создан заказ с ID 1

# Второй запрос (повтор)
POST /orders
{
  "userId": 123,
  "amount": 100
}
# Результат: создан заказ с ID 2 (другой результат!)

Не идемпотентная операция (PATCH с инкрементом):

# Первый запрос
PATCH /accounts/123
{
  "operation": "increment",
  "value": 100
}
# Результат: баланс = 1100 (было 1000)

# Второй запрос (повтор)
PATCH /accounts/123
{
  "operation": "increment",
  "value": 100
}
# Результат: баланс = 1200 (другой результат!)

Коды ответов HTTP

REST API использует стандартные коды ответов HTTP для указания результата операции:

2xx — Успешные операции

4xx — Ошибки клиента

5xx — Ошибки сервера

Форматы данных

REST API обычно использует JSON для обмена данными:

Пример ответа (GET /users/123):

{
  "id": 123,
  "name": "Ivan",
  "email": "ivan@example.com",
  "createdAt": "2024-01-15T10:30:00Z",
  "_links": {
    "self": "/users/123",
    "orders": "/users/123/orders"
  }
}

Пример запроса (POST /users):

{
  "name": "Petr",
  "email": "petr@example.com"
}

Версионирование API

Существует несколько подходов к версионированию REST API:

  1. Версия в URL:
    /api/v1/users
    /api/v2/users
    
  2. Версия в заголовке:
    Accept: application/vnd.myapi.v1+json
    
  3. Версия в параметрах запроса:
    /users?version=1
    

Пример полного REST API

Рассмотрим пример REST API для управления пользователями:

GET    /users              — получить всех пользователей
GET    /users/{id}         — получить пользователя по ID
POST   /users              — создать нового пользователя
PUT    /users/{id}         — полностью обновить пользователя
PATCH  /users/{id}         — частично обновить пользователя
DELETE /users/{id}         — удалить пользователя

GET    /users/{id}/orders  — получить заказы пользователя
POST   /users/{id}/orders  — создать заказ для пользователя

Примеры запросов и ответов:

  1. Создание пользователя: ```bash POST /users Content-Type: application/json

{ “name”: “Anna”, “email”: “anna@example.com” }

Ответ: 201 Created Location: /users/456

{ “id”: 456, “name”: “Anna”, “email”: “anna@example.com”, “createdAt”: “2024-01-15T10:30:00Z” }


2. Получение пользователя:
```bash
GET /users/456

Ответ:
200 OK

{
  "id": 456,
  "name": "Anna",
  "email": "anna@example.com",
  "createdAt": "2024-01-15T10:30:00Z"
}
  1. Обновление пользователя: ```bash PUT /users/456 Content-Type: application/json

{ “id”: 456, “name”: “Anna Updated”, “email”: “anna.updated@example.com” }

Ответ: 200 OK

{ “id”: 456, “name”: “Anna Updated”, “email”: “anna.updated@example.com”, “createdAt”: “2024-01-15T10:30:00Z”, “updatedAt”: “2024-01-15T11:00:00Z” }


4. Удаление пользователя:
```bash
DELETE /users/456

Ответ:
204 No Content
  1. Ошибка при попытке получить несуществующего пользователя: ```bash GET /users/999

Ответ: 404 Not Found

{ “error”: “User not found”, “code”: “USER_NOT_FOUND”, “details”: “User with ID 999 does not exist” }


### Лучшие практики REST API

1. **Используйте существительные во множественном числе для ресурсов:**
   - ✅ `/users`, `/products`, `/orders`
   - ❌ `/user`, `/getUsers`, `/getAllUsers`

2. **Используйте правильные HTTP методы:**
   - GET для получения данных
   - POST для создания
   - PUT для полного обновления
   - PATCH для частичного обновления
   - DELETE для удаления

3. **Возвращайте правильные коды ответов:**
   - 201 для создания ресурса
   - 200/204 для успешных операций
   - 400 для ошибок клиента
   - 404 для несуществующих ресурсов
   - 500 для ошибок сервера

4. **Используйте пагинацию для больших коллекций:**

GET /users?page=1&limit=20


5. **Поддерживайте фильтрацию, сортировку и поиск:**

GET /users?name=Ivan&sort=createdAt:desc


6. **Включайте метаданные в ответы:**
   ```json
   {
     "data": [...],
     "meta": {
       "total": 100,
       "page": 1,
       "limit": 20
     }
   }
  1. Обеспечивайте идемпотентность там, где это возможно:
    • Используйте PUT вместо POST для обновлений
    • Добавляйте уникальные идентификаторы для операций
    • Реализуйте механизмы обработки повторных запросов
  2. Документируйте ваш API:
    • Используйте OpenAPI/Swagger
    • Описывайте все эндпоинты, параметры и ответы
    • Приводите примеры запросов и ответов

Полезные ссылки

Очень классная статья про TCP и UDP

Про сервлеты

Еще про сервлеты

Простая веб-служба со встроенным Jetty

Jetty tutorial for begginers

Парсинг JSON с помощью Jackson