Пример standalone-приложения на Spring

Вернуться назад

14.01.2018

Рассмотрим базовые возможности dependency injection (внедрения зависимостей), которые открывает нам Spring.

Создадим обычный maven-проект, где в pom.xml добавим сам Spring (артефакт spring-context) и секцию build со стандартным плагином maven-compiler-plugin, в котором указываем версию java в source и target.

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>4.3.13.RELEASE</version>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <configuration>
                <source>1.8</source>
                <target>1.8</target>
            </configuration>
        </plugin>
    </plugins>
</build>

Теперь создадим главный класс TestApp, который будет точкой запуска приложения.

public class TestApp {

    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("app-config.xml");
        MainService service = context.getBean(MainService.class);
        service.doWork();
    }
}

Здесь мы создаём контекст на основе конфигурационного файла Spring, который после сборки maven'ом окажется внутри jar-файла. В данном случае мы используем ClassPathXmlApplicationContext, но есть и много других реализаций. Затем из контекста получаем главный бин MainService и вызываем у него целевой метод.

Поскольку все бины мы будем конфигурировать прямо в коде при помощи аннотаций (секция annotation-config), содержимое app-config.xml будет минимальным. Мы лишь указываем базовый пакет (секция component-scan), внутри которого Spring автоматически будет искать все бины.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

    <context:annotation-config />
    <context:component-scan base-package="ru.devmark.test"/>
</beans>

Теперь определим интерфейс нашего главного бина MainService. В связи с особенностями работы Spring, инициализация бина происходит быстрее, если у него есть интерфейс. Да и вообще, с точки зрения ООП всегда хорошо выделять интерфейс. Просто возьмите это за правило.

public interface MainService {

    void doWork();
}

Его реализация MainServiceImpl, помеченная аннотацией @Service.

@Service
public class MainServiceImpl implements MainService {

    private Handler handler;

    @Autowired
    public MainServiceImpl(Handler handler) {
        this.handler = handler;
    }

    @Override
    public void doWork() {
        System.out.println(handler.getString());
    }
}

Все spring-бины помечаются одной из четырёх аннотаций:

  1. Controller - бины, содержащие маппинг входящих http-запросов
  2. Service - бины, реализующие бизнес-логику приложения
  3. Repository - бины, работающие с БД
  4. Component - все остальные бины

Component - наиболее обобщённая аннотация и три другие являются её частными случаями. По большому счёту, разницы между ними нет и почти любой бин будет работать с любой аннотацией, но с точки зрения логики приложения лучше придерживаться такого деления.

Также обратите внимание на аннотацию @Autowired. Она указывает, что в данный конструктор в качестве аргумента нужно поместить бин с подходящим интерфейсом. В нашем примере это некий интерфейс Handler.

Сам интерфейс Handler предельно прост. В нём только один метод, который возвращает строку.

public interface Handler {

    String getString();
}

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

@Component
public class DateHandler implements Handler {

    @Override
    public String getString() {
        return LocalDateTime.now().toString();
    }
}

Обратите внимание, что данный бин DateHandler помечен аннотацией @Component.

Хорошей практикой является создание бинов таким образом, чтобы они не имели внутреннего состояния. Например, не создавать поля класса, которые могут менять значения между вызовами методов. Зачем это нужно? Дело в том, что по умолчанию Spring создаёт каждый бин в одном экземпляре, который затем будет передан во все бины, которые в нём нуждаются. И если ваш бин будет хранить состояние, то неизвестно, как он будет себя вести, если его состояние будут одновременно менять различные компоненты.

Теперь, если вы запустите приложение, то увидите на экране текущее время.

Давайте теперь рассмотрим более интересный вариант, когда один бин от другого отличается незначительно. Например, пусть один из них возвращает всё время одну строку, а второй - другую. Создавать ещё два класса было бы излишне. Поэтому сделаем такую реализацию, в которую константное значение будем передавать при инициализации бина. И при этом НЕ будем снабжать её какой-либо аннотацией.

public class NameHandler implements Handler {

    private final String name;

    public NameHandler(String name) {
        this.name = name;
    }

    @Override
    public String getString() {
        return name;
    }
}

Теперь нам потребуется специальный конфигурационный класс с аннотацией @Configuration, который позволяет более гибко инициализировать бины.

@Configuration
public class AppConfiguration {

    @Bean
    public Handler firstNameHandler() {
        return new NameHandler("Alice");
    }

    @Bean
    public Handler secondNameHandler() {
        return new NameHandler("Bob");
    }
}

Здесь всё просто: один метод с аннотацией @Bean - один бин. В конструктор мы передаём ему константу. В итоге Spring создаст два бина, один из который всё время будет возвращать «Alice», а другой - «Bob».

Теперь для вызова новых обработчиков нам нужно внедрить их в наш сервис MainServiceImpl. Можно конечно прописать ещё два поля, но и тут Spring облегчает нам задачу. Достаточно создать одно поле, представляющее из себя коллекцию List из интерфейсов Handler, в которую Spring автоматически добавит ВСЕ бины, имеющие этот интерфейс. Вот ещё одна причина, по которой удобно использовать интерфейсы.

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

@Service
public class MainServiceImpl implements MainService {

    private List<Handler> handlers;

    @Autowired
    public MainServiceImpl(List<Handler> handlers) {
        this.handlers = handlers;
    }

    @Override
    public void doWork() {
        handlers.forEach(h -> System.out.println(h.getString()));
    }
}

В итоге в нашей коллекции обработчиков будет ровно три бина (DateHandler и два экземпляра NameHandler).

Для отображения значения каждого из них воспользуемся Stream API из Java 8, что равносильно обычному циклу foreach и вызову метода getString() у каждого из элементов.

Тэги: Java, maven, Java 8, Stream API, Spring, ООП.


Исходники