ServiceLoader: встроенный DI-фреймворк, о котором вы, возможно, никогда не слышали

Автор оригинала: Erik Englund
  • Перевод
Салют, друзья. Уже в эту пятницу пройдет первое занятие в новой группе курса «Разработчик Java». Именно этому курсу и будет посвящена текущая публикация.



Многие из java-разработчиков для внедрения зависимостей используют Spring. Некоторые, возможно, пробовали Google Guice или даже OSGi Services. Но многие не знают, что в Java уже есть встроенный DI. Думаете это появилось в Java 11 или 12? Нет, он доступен с Java 6.

ServiceLoader предоставляет возможность поиска и создания зарегистрированных экземпляров интерфейсов или абстрактных классов. Если вы знакомы со Spring, то это очень похоже на аннотации Bean и Autowired. Давайте посмотрим на примеры использования Spring и ServiceLoader. И обсудим сходства и различия.

Spring


Сначала давайте посмотрим, как сделать простой DI в Spring. Создадим простой интерфейс:

public interface SimpleService {
   String echo(String value);
}

И реализацию интерфейса:

import org.springframework.stereotype.Component;

@Component
public class SimpleServiceImpl implements SimpleService {
   public String echo(final String value) {
       return value;
   }
}

Обратите внимание на @Component. Эта аннотация зарегистрирует наш класс как бин в Spring-контексте.

И наш main-класс.

@SpringBootApplication
public class SpringExample implements CommandLineRunner {
    private static final Logger log = 
            LoggerFactory.getLogger(SpringExample.class);

   @Autowired
   List<SimpleService> simpleServices;

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

   }

   public void run(final String... strings) throws Exception {
       for (SimpleService simpleService : simpleServices) {
           log.info("Echo: " + simpleService.echo(strings[0]));
       }
   }
}

Обратите внимание на аннотацию @Autowired на поле со списком SimpleService. Аннотация @SpringBootApplication предназначена для автоматического поиска бинов в пакете. Потом при запуске они автоматически инжектятся в SpringExample.

ServiceLoader


Мы будем использовать тот же интерфейс, что и в примере со Spring, поэтому не будем повторять его здесь. Вместо этого сразу посмотрим на реализацию сервиса:

import com.google.auto.service.AutoService;

@AutoService(SimpleService.class)
public class SimpleServiceImpl implements SimpleService {
   public String echo(final String value) {
       return value;
   }
}

В реализации мы “регистрируем” экземпляр сервиса, используя аннотацию @AutoService. Эта аннотация нужна только во время компиляции, так как javac использует ее для автоматического генерирования файла регистрации сервисов (Примечание переводчика: для maven-зависимости, содержащей @AutoService, указываем scope — provided):

META-INF/services/io.github.efenglu.serviceLoader.example.SimpleService

Этот файл содержит список классов, которые реализуют сервис:

io.github.efenglu.serviceLoader.example.SimpleServiceImpl

Имя файла должно быть полным именем сервиса (интерфейса). В файле может быть любое количество реализаций, каждая на отдельной строке.

В реализациях ДОЛЖЕН быть конструктор без параметров. Можно создать такой файл вручную, но использовать аннотацию гораздо проще. И main-класс:

public class ServiceLoaderExample {

   public static void main(String [] args) {
       final ServiceLoader<SimpleService> services = ServiceLoader.load(SimpleService.class);
       for (SimpleService service : services) {
           System.out.println("Echo: " + service.echo(args[0]));
       }
   }
}

Метод ServiceLoader.load вызывается для получения ServiceLoader, который можно использовать для получения экземпляров сервиса. Экземпляр ServiceLoader реализует интерфейс Iterable для типа сервиса, следовательно, переменную services можно использовать в цикле for each.

И что?


Оба способа относительно небольшие. Оба могут использоваться с аннотациями и поэтому довольно просты в использовании. Так зачем использовать ServiceLoader вместо Spring?

Зависимости


Давайте посмотрим на дерево зависимостей нашего простого примера со Spring:

[INFO] -----------< io.github.efenglu.serviceLoader:spring-example >-----------
[INFO] Building spring-example 1.0.X-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- maven-dependency-plugin:3.1.1:tree (default-cli) @ spring-example ---
[INFO] io.github.efenglu.serviceLoader:spring-example:jar:1.0.X-SNAPSHOT
[INFO] +- org.slf4j:slf4j-api:jar:1.7.25:compile
[INFO] +- org.springframework:spring-context:jar:4.3.22.RELEASE:compile
[INFO] |  +- org.springframework:spring-aop:jar:4.3.22.RELEASE:compile
[INFO] |  +- org.springframework:spring-core:jar:4.3.22.RELEASE:compile
[INFO] |  |  \- commons-logging:commons-logging:jar:1.2:compile
[INFO] |  \- org.springframework:spring-expression:jar:4.3.22.RELEASE:compile
[INFO] +- org.springframework.boot:spring-boot-autoconfigure:jar:1.5.19.RELEASE:compile
[INFO] +- org.springframework.boot:spring-boot:jar:1.5.19.RELEASE:compile
[INFO] \- org.springframework:spring-beans:jar:4.3.22.RELEASE:compile

И сравним с ServiceLoader:

[INFO] io.github.efenglu.serviceLoader:serviceLoader-example:jar:1.0.X-SNAPSHOT
## Only provided dependencies for the auto service annotation
[INFO] \- com.google.auto.service:auto-service:jar:1.0-rc4:provided
[INFO]    +- com.google.auto:auto-common:jar:0.8:provided
[INFO]    \- com.google.guava:guava:jar:23.5-jre:provided
[INFO]       +- com.google.code.findbugs:jsr305:jar:1.3.9:provided
[INFO]       +- org.checkerframework:checker-qual:jar:2.0.0:provided
[INFO]       +- com.google.errorprone:error_prone_annotations:jar:2.0.18:provided
[INFO]       +- com.google.j2objc:j2objc-annotations:jar:1.1:provided
[INFO]       \- org.codehaus.mojo:animal-sniffer-annotations:jar:1.14:provided

Если мы не будем обращать внимания на provided-зависимости, то у ServiceLoader НЕТ зависимостей. Правильно, ему нужна только Java.

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

Скорость


Для консольных приложений время запуска ServiceLoader НАМНОГО меньше, чем Spring Boot App. Это благодаря меньшему количеству загружаемого кода, отсутствию сканирования, отсутствию рефлекшена, отсутствию больших фреймворков.

Память


Spring не славится тем, что экономит память. Если вам важно расходование памяти, то следует рассмотреть возможность использования ServiceLoader для DI.

Модули Java


Одним из ключевых аспектов Java-модулей была возможность полностью защитить классы в модуле от кода вне модуля. ServiceLoader — это механизм, который позволяет внешнему коду «обращаться» к внутренним реализациям. Модули Java позволяют регистрировать сервисы для внутренних реализаций, сохраняя при этом границу.

Фактически, это единственный официально одобренный механизм поддержки внедрения зависимостей для Java-модулей. Spring и большинство других DI-фреймворков используют reflection для поиска и подключения своих компонент. Но это не совместимо с Java-модулями. Даже reflection не может заглянуть в модули (если вы это не разрешите, но зачем вам разрешать).

Заключение


Spring — отличная штука. В нем гораздо больше функционала, чем когда-либо будет в ServiceLoader. Но бывают случаи, когда ServiceLoader будет правильным выбором. Он простой, маленький, быстрый и всегда доступен.

Полный исходный код примеров в моем Git Repo.

На этом все. До встречи на курсе!
  • +11
  • 3,1k
  • 5
OTUS. Онлайн-образование
593,74
Цифровые навыки от ведущих экспертов
Поделиться публикацией

Комментарии 5

    +1

    Собственно, нам показали как загрузить сервис без зависимостей.


    А зависимости-то как инжектить?

      –1

      Похоже на сравнение теплого с мягким, даже немного неполное. То, что про ServiceLoader могли не знать — конечно зря, но вот мне это не помогло, это не замена Spring для более-менее навороченного приложения

        0

        С таким же успехом можно использовать Map<Class, Supplier>. ServiceLoader служит для поиска имплементации сервиса в classpath. Кроме того, в Java 9 это также стандартный способ для связи между модулями. В сравнении с нормальным DI он не обеспечивает:

        • транзитивные зависимости
        • AOP и проксирование
        • выбор имплементации сервиса

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

          0
          Одним из ключевых аспектов Java-модулей была возможность полностью защитить классы в модуле от кода вне модуля.

          Есть, правда, один нюанс. При прогоне юнит тестов Intellij IDEA склеивает все модули в один и весь код, который был написан с рассчётом на то, что модулей много, работать не будет.

            –1

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

            Самое читаемое