Зарегистрируйтесь для доступа к 15+ бесплатным курсам по программированию с тренажером

Аннотации Java: Корпоративные приложения на Spring Boot

В Java аннотации встречаются часто, но особенно много их в Spring Boot. Чтобы понять, как работает фреймворк, нужно разобраться в устройстве аннотаций. В этом уроке мы познакомимся с ними и узнаем, как они работают.

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

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

package io.hexlet.spring;

import org.springframework.boot.SpringApplication;

// Определения аннотаций — это обычный код, который нужно импортировать
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;

@RestController // Аннотация на уровне класса
@RequestMapping("/api") // Аннотация на уровне класса
@SpringBootApplication
public class Application {

    @Autowired // Аннотация на уровне поля
    private UserRepository userRepository;

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

    @GetMapping("/hello")  // Аннотация на уровне метода
    public String sayHello(@RequestParam(name = "name", required = false, defaultValue = "Guest") String name) { // Аннотация на уровне параметров
        return "Hello, " + name + "!";
    }

    @PostMapping("/greet") // Аннотация на уровне метода
    public String greet(@RequestBody String name) {  // Аннотация на уровне параметров
        return "Greetings, " + name + "!";
    }
}

Некоторые аннотации выглядят как метка — @RestController, другие похожи на вызов метода с параметрами — @RequestMapping("/api"). Принцип работы от этого не меняется: аннотация не превращается в вызов метода, она остается меткой с дополнительными данными.

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

Встроенные аннотации

Подавляющее большинство аннотаций в реальных проектах написаны разработчиками библиотек, а еще буквально несколько аннотаций встроено прямо в Java. Например, аннотация @Deprecated позволяет отметить класс или метод как устаревший. Эту информацию затем можно увидеть в подсказках редактора. Такая аннотация помогает другим программистам при выборе классов и методов для реализации их задач:

@Deprecated
public void oldMethod() {
    // Дальше продолжается какой-то код
}

Самая часто используемая аннотация — это @Override. Она указывает, что помеченный метод должен переопределять метод наследуемого класса или реализовывать метод интерфейса. Сама аннотация не обязательна при переопределении, но она помогает избежать ошибок и сделать код проще для чтения:

// Пример из Spring Boot
public class User implements UserDetails {
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
}

Кастомные аннотации

С такими аннотациями мы будем встречаться чаще всего. Изучить их работу заранее невозможно — каждая конкретная аннотация обрабатывается своим образом и приводит к своим последствиям. Причем в большинстве случаев программист до конца не знает, что на самом деле происходит внутри.

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

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

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import jakarta.validation.Valid;

@RestController
@AllArgsConstructor
@RequestMapping("/api")
public class PostsController {

    @Autowired
    private final PostRepository repository;

    @PostMapping("/posts")
    @ResponseStatus(HttpStatus.CREATED)
    PostDTO create(@Valid @RequestBody PostDTO postData) throws JsonProcessingException {
        // Тут логика
    }

    @GetMapping("/posts/{id}")
    @ResponseStatus(HttpStatus.OK)
    PostDTO show(@PathVariable Long id) {
        // Тут логика
    }
}

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

Устройство кастомных аннотаций

Посмотрим, как определять и обрабатывать аннотации. Эти знания помогут разобраться в принципе работы аннотаций, а еще вопросы на эту тему часто задают на собеседованиях. Напишем аннотацию @LogExecutionTime, которая замеряет время выполнения помеченного ей метода:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface LogExecutionTime {
}

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

  • @interface определяет саму аннотацию
  • @Retention определяет жизненный цикл аннотации, то есть указывает, как долго аннотация должна оставаться с кодом. В этом случае аннотация должна быть доступна в рантайме, потому что именно так мы будем ее обрабатывать
  • @Target определяет, где мы будем применять аннотацию (например, в методах)

Аннотация готова, можно начинать применять ее. При этом в работе кода ничего не поменяется, потому что обработчик еще не написан:

// Мы должны импортировать нашу аннотацию
import <какой-то путь>.LogExecutionTime;

public class SomeService {
    @LogExecutionTime
    public void serve() throws InterruptedException {
        Thread.sleep(1500);  // Выполняем какую-то задачу
    }

    public void anotherMethod() {
        // Этот метод еще не отмечен аннотацией,
        // поэтому время выполнения метода не измеряется и не логируется
    }
}

Дальше мы напишем обработчик аннотации. Это обычный Java-код, поэтому нужно убедиться, что он выполняется до того, как исполнение дойдет до кода с аннотациями. В нашем случае обработчик выполняется в методе main():

import java.lang.reflect.Method;

public class Main {
    public static void main(String[] args) {
        var service = new SomeService();

        // Итерируем все методы класса
        for (Method method : SomeService.class.getDeclaredMethods()) {

            // Проверяем, есть ли у метода аннотация @LogExecutionTime
            if (method.isAnnotationPresent(LogExecutionTime.class)) {

                var startTime = System.currentTimeMillis();

                try {
                    // Выполняем метод с аннотацией LogExecutionTime
                    method.invoke(service);
                } catch (Exception e) {
                    e.printStackTrace();
                }

                long endTime = System.currentTimeMillis();
                long executionTime = endTime - startTime;

                System.out.println("Executed method: " + method.getName());
                System.out.println("Execution time: " + executionTime + " milliseconds");
            }
        }
    }
}

Здесь мы видим рефлексию — технику, которая отображает информацию о программе во время ее работы. Чтобы использовать ее, мы берем все методы класса SomeService, находим методы с аннотацией LogExecutionTime и вызываем их методы, проверяя время выполнения.

Параметры кастомных аннотаций

Добавим в @LogExecutionTime два параметра. Первый временно выключит логирование, а второй задаст минимальное время выполнения, ниже которого логировать не нужно:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface LogExecutionTime {
    boolean enabled();
    long threshold() default 0;  // Пороговое время в миллисекундах
}

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

public class SomeService {
    @LogExecutionTime(enabled = true)
    public void serve() throws InterruptedException {
        Thread.sleep(1500);  // Выполняем какую-то задачу
    }

    @LogExecutionTime(enabled = true, threshold = 100)
    public void anotherMethod() {
    }
}

Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты

Об обучении на Хекслете

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff
Рекомендуемые программы
профессия
от 6 300 ₽ в месяц
Разработка приложений на языке Java
10 месяцев
с нуля
Старт 2 мая

Используйте Хекслет по-максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»
Изображение Тото

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