Публичные сервисы

Публичные сервисы #

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

Использование Google Guice #

В качестве механизма для внедрения зависимостей при разработке плагинов и сервисов 1C:EDT использует Google Guice. Это позволяет обеспечить слабую связанность.

Google Guice — это универсальный фреймворк с открытым исходным кодом для Java-платформы. Он разработан компанией Google под лицензией Apache 2.0. Фреймворк обеспечивает поддержку внедрения зависимостей при помощи аннотаций для конфигурирования объектов Java.
Внедрение зависимостей — это паттерн проектирования. Его основная задача заключается в том, чтобы отделить поведение объекта от управления его зависимостями. Google Guice позволяет классам реализаций программно привязываться к интерфейсу и затем внедряться в конструкторы, методы или поля, помеченные аннотацией @Inject.

Почему мы используем Google Guice?

На данный момент существует множество фреймворков, позволяющих реализовать внедрение зависимостей. Преимущества Google Guice заключаются в следующем:

  • чистые и простые реализация и использование,
  • сравнительная легковесность,
  • зависимости и реализации описываются на языке Java, что гарантирует защиту от несоответствия типов и от ошибок инициализации после рефакторинга кода.

Также мы используем в 1C:EDT технологию Eclipse Xtext, которая уже внутри себя использует Google Guice. Таким образом, использование внутри плагинов 1C:EDT единого фреймворка внедрения зависимостей дает более чистое и прозрачное решение. С помощью Google Guice связывается внутренняя инфраструктура плагинов.

Публичные сервисы #

В 1C:EDT присутствует такое понятие, как публичные сервисы. В качестве публичных сервисов используются OSGi-сервисы.

OSGi (Open Services Gateway Initiative) — это спецификация динамической модульной системы и сервисной платформы для Java-приложений, разрабатываемая консорциумом OSGi Alliance. Она дает модель для построения приложения из компонентов, которые связаны посредством сервисов. Преимущество этой модели заключается в возможности динамически переустановить компоненты и составные части приложения без необходимости остановки и запуска самого приложения.

1C:EDT является приложением, построенным на базе Eclipse Platform, поэтому оно представляет собой модульную OSGi-систему. По этой причине мы выбрали OSGi-сервисы в качестве публичных сервисов. Публичные сервисы используются для сервисной связи между плагинами в 1C:EDT.

Общая структура сервисного взаимодействия в плагинах 1C:EDT выглядит следующим образом:

OSGI

В качестве публичных сервисов в 1C:EDT мы используем OSGi-сервисы, поэтому они могут быть доступны и могут быть получены стандартными средствами или любыми сторонними средствами работы с OSGi-сервисами. Обратите внимание, что при этом использование простых Java-классов или библиотечных классов одного плагина в другом идет напрямую без каких-либо сервисов. Нет нужды делать сервисы из классов или плагинов, представляющих собой библиотеку.

В рамках 1C:EDT мы реализовали удобную инфраструктуру использования публичных OSGi-сервисов, которые можно подключать или регистрировать в любом плагине, в том числе и сторонним разработчикам. Рассмотрим на примерах возможности использования публичных сервисов.

Использование публичных сервисов внутри Google Guice #

Для импорта OSGi-сервисов в Guice-модули в 1C:EDT реализован класс AbstractServiceAwareModule. Далее показаны несколько примеров:

Импорт публичных сервисов

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public class MyModule extends AbstractServiceAwareModule
{
    public MyModule(Plugin plugin)
    {
        super(plugin);
    }
    @Override
    protected void doConfigure()
    {
        // Связывание локальных сервисов плагина
        bind(IInternalService.class).to(InternalService.class).in(Singleton.class);
        bind(IOtherInternalService.class).to(OtherInternalService.class);

        // Импорт нужных OSGi-сервисов
        bind(IPublicService.class).toService();
        bind(IPublicQualifiedService.class).annotatedWith(Names.named("MdImplementation"))
            .toService(IPublicQualifiedService.class, "qualifier", "MdImplementation");
    }
}

Реализация внутреннего сервиса

С помощью таких модулей возможно простое внедрение (@Inject) публичных OSGi-сервисов в нужные места, включая внутренние сервисы плагина в этом модуле:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class InternalService extends IInternalService
{
    @Inject
    private IPublicService publicService;

    @Inject
    @Named("MdImplementation");
    private IPublicQualifiedService publicQualifiedService;
    
    ...
}

Импорт публичных сервисов

У класса AbstractServiceAwareModule имеется конструктор с параметром org.osgi.framework.BundleContext. Его можно использовать в тестах, где экземпляр плагина может быть недоступен:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Injector injector = Guice.createInjector(new AbstractServiceAwareModule(FrameworkUtil.getBundle(getClass()).getBundleContext())
{
    @Override
    protected void doConfigure()
    {
        bind(IConfigurationProvider.class).toService();
    }
});

IConfigurationProvider configurationProvider = injector.getInstance(IConfigurationProvider.class);

Пример плагина #

Напишите плагин, который будет дополнять 1C:EDT редактором. Этот редактор сможет открывать файлы с расширением *.example.

Класс редактора

Создайте проект плагина, после чего создайте в нем редактор. Вы будете использовать в нем сервис общего назначения IConfigurationProvider. Этот сервис даст доступ к конфигурации проекта, который будет содержать файлы с расширением *.example. Для этого в классе редактора используйте аннотацию @Inject с именем сервиса, который вы хотите внедрить при помощи Google Guice:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package com.myplugin.example.editor;
 
import com._1c.g5.v8.dt.core.platform.IConfigurationProvider;
import com._1c.g5.v8.dt.metadata.mdclass.Configuration;

import com.google.common.base.Preconditions;
import com.google.inject.Inject;

...
 
/**
 * This is example editor that edits {@code *.example} files.
 */
public class ExampleEditor extends EditorPart
{
    @Inject
    private IConfigurationProvider configurationProvider ;

    @Override
    public void init(IEditorSite site, IEditorInput input)
        throws PartInitException
    {
        Preconditions.checkArgument(input instanceof IFileEditorInput,
            "IFileEditorInput expected, but actual: %s", input);
 
        setSite(site);
        setInput(input);

        IProject project = ((FileEditorInput)input).getFile().getProject();
        Configuration configuration = configurationProvider.getConfiguration(project);
 
        workWithConfiguration(configuration);
    }

    ...
}

Guice-модуль плагина

Чтобы ваш класс редактора ExampleEditor создавался в нужном Guice-окружении, вы должны сделать следующее:

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

Создайте Guice-модуль и подключите в нем сервис общего назначения IConfigurationProvider (список сервисов общего назначения можно найти в соответствующем документе):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package com.myplugin.example;
 
import com._1c.g5.wiring.AbstractServiceAwareModule;
import com._1c.g5.v8.dt.core.platform.IConfigurationProvider;
 
/**
 * My plugin Guice module that is aware of 1C:EDT Services.
 */
public class MyPluginExternalDependenciesModule
    extends AbstractServiceAwareModule
{
    /**
     * Constructor of {@link MyPluginExternalDependenciesModule}.
     *
     * @param bundle the parent bundle, cannot be {@code null}
     */
    public MyPluginExternalDependenciesModule(Plugin bundle)
    {
        super(bundle);
    }
    @Override
    protected void configure()
    {
        // Связываем сервис общего назначения IConfigurationProvider
        bind(IConfigurationProvider.class).toService();
    }
}

В этом примере вы унаследовали свой модуль от специального класса-модуля AbstractServiceAwareModule. Он знает обо всех сервисах общего назначения 1C:EDT и может легко подключать необходимые из них (о том, как подключать свои сервисы, будет рассказано ниже).

В методе doConfigure() вы описали, что хотите использовать сервис общего назначения IConfigurationProvider.

Активатор плагина

Модуль готов: теперь вы должны зарегистрировать свой редактор. Для этого платформа Eclipse предоставляет механизм IExecutableExtensionFactory, а в 1C:EDT реализован абстрактный класс AbstractGuiceAwareExecutableExtensionFactory. Этому классу нужно просто указать, какой модуль использовать при создании и инициализации расширений.

Для начала определитесь с местоположением инжектора вашего Guice-модуля (инжектор Guice-модуля — это сущность, которая позволяет использовать описанные в модуле зависимости. Подробнее — здесь). Мы рекомендуем использовать для этого сам класс плагина (активатора):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
package com.myplugin.example;

import org.eclipse.ui.plugin.AbstractUIPlugin;
import org.osgi.framework.BundleContext;
import com.google.inject.Guice;
import com.google.inject.Injector;

/**
 * The activator class controls the plug-in life cycle.
 */
public class MyPlugin
    extends AbstractUIPlugin
{
    // The plug-in ID
    public static final String PLUGIN_ID = "com.myplugin.example";

    // The shared instance
    private static MyPlugin plugin;

    private Injector injector;
 
    /**
     * Returns the shared instance
     *
     * @return the shared instance
     */
    public static MyPlugin getDefault()
    {
        return plugin;
    }

    @Override
    public void start(BundleContext context) throws Exception
    {
        super.start(context);
        plugin = this;
    }

    @Override
    public void stop(BundleContext context) throws Exception
    {
        plugin = null;
        super.stop(context);
    }

    /**
     * Returns Guice injector for this plugin.
     *
     * @return Guice injector for this plugin, never {@code null}
     */
    public synchronized Injector getInjector()
    {
        if (injector == null)
            return injector = createInjector();
        return injector;
    }

    private Injector createInjector()
    {
        try
        {
            return Guice.createInjector(new MyPluginExternalDependenciesModule(this));
        }
        catch (Exception e)
        {
            throw new RuntimeException("Failed to create injector for " + getBundle().getSymbolicName(), e);
        }
    }
}

Фабрика для создания расширений

Далее определите свою реализацию IExecutableExtensionFactory:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.myplugin.example;

import org.osgi.framework.Bundle;
import com._1c.g5.wiring.AbstractGuiceAwareExecutableExtensionFactory;
import com.google.inject.Injector;

/**
 * Extension Factory for my plugin.
 */
public class MyPluginExecutableExtensionFactory
    extends AbstractGuiceAwareExecutableExtensionFactory
{
    @Override
    protected Bundle getBundle()
    {
        return MyPlugin.getDefault().getBundle();
    }
    @Override
    protected Injector getInjector()
    {
        return MyPlugin.getDefault().getInjector();
    }
}

Фрагмент plugin.xml

Теперь укажите в описании своего расширения с редактором, что вы хотите создавать его, используя свою фабрику MyPluginExecutableExtensionFactory:

<extension point="org.eclipse.ui.editors">
   <editor
         class="com.myplugin.example.MyPluginExecutableExtensionFactory:com.myplugin.example.editor.ExampleEditor"
         default="false"
         extensions="example"
         id="com.myplugin.example.editor"
         name="My Editor">
   </editor>
</extension>

Фрагмент активатора плагина

Теперь ваш редактор будет создаваться в нужном вам Guice-окружении согласно описанию вашего модуля. В него вы можете подключать необходимые вам сервисы общего назначения, а также описывать свои сервисы. Однако мы рекомендуем отделять модули-потребители внешних сервисов от модулей, описывающих свои внутренние сервисы плагина, а при создании инжектора в плагине — объединять их:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
private Injector createInjector()
{
    try
    {
        return Guice.createInjector(new MyPluginServicesModule(), new MyPluginExternalDependenciesModule(this));
    }
    catch (Exception e)
    {
        throw new RuntimeException("Failed to create injector for " + getBundle().getSymbolicName(), e);
    }
}

Использование публичных сервисов вне Guice-окружения #

При создании плагинов вам может потребоваться работа с публичными OSGi-сервисами вне Guice-окружения. Мы рекомендуем следующий способ работы с публичным OSGi-сервисами:

Получение сервиса мы рекомендуем включать во внутренний метод. Остальной код класса будет взаимодействовать с методом-аксессором сервиса. Такой вариант даст возможность в дальнейшем гибко настраивать получение сервиса, если это потребуется.
Мы рекомендуем писать код так, чтобы клиент не хранил ссылку на сервис: тогда код останется актуальным в различных ситуациях доступности и изменения сервисов. Другими словами, клиент должен всегда вызывать get сервиса перед вызовом методов сервиса.

Далее приведен пример следования этим рекомендациям:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class MyClass
{
    private ServiceSupplier<IConfigurationProvider> configurationProviderSupplier = 
        ServiceAccess.supplier(IConfigurationProvider.class, MyClassPlugin.getPlugin());

    public void method(EObject modelObject)
    {
        ...

        // Пользуемся сервисом, когда он необходим
        Configuration configuration = getConfigurationProvider().getConfiguration(modelObject);

        ...

        Configuration configuration = getConfigurationProvider().getConfiguration(otherModelObject);

        ...
    }

    public void dispose()
    {
        // Закрываем доступ, когда он больше не нужен
        configurationProviderSupplier.close();
    }

    private IConfigurationProvider getConfigurationProvider()
    {
        return configurationProviderSupplier.get();
    }
}

Кроме показанного выше способа в 1C:EDT реализован и другой, нерекомендуемый способ работы. Он может использоваться либо как временное решение в коде, которое будет в дальнейшем переработано, либо там, где некритична производительность, например в тестах. Обратите внимание, что такой способ доступа медленнее.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public final class MyUtil
{
    public static void method(EObject modelObject)
    {
        ....

        Configuration configuration = ServiceAccess.get(IConfigurationProvider.class).getConfiguration(modelObject);

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

Предоставление публичных сервисов #

В 1C:EDT реализована точка расширения com._1c.g5.wiring.serviceProvider. Плагины, предоставляющие публичные сервисы, могут зарегистрировать расширение, после чего быть активированы при старте приложения специальным служебным плагином, который будет стартовать автоматически:

Фрагмент plugin.xml

1
2
3
4
5
<extension point="com._1c.g5.wiring.serviceProvider">
   <bundle
          symbolicName="com.myplugin">
   </bundle>
</extension>

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/**
 * Activator of the plug-in that produces some OSGi services.
 */
public class ServicesProducerPlugin
    extends Plugin
{
    private static ServicesProducerPlugin plugin;
    private ServiceRegistrator registrator;

    public static ServicesProducerPlugin getDefault()
    {
        return plugin;
    }
 
    @Override
    public void start(BundleContext context) throws Exception
    {
        super.start(context);
        plugin = this;
        registrator = new ServiceRegistrator(context);

        ServiceInitialization.schedule(() -> {
            // register instance
            registrator.service(ISomeInstanceService.class).registerInstance(new ISomeInstanceService()
            {
                @Override
                public void method()
                {
                    // do nothing
                }
            });
            // register other instance
            registrator.service(ISomeOtherInstanceService.class).registerInstance(new SomeOtherInstanceServiceImpl());
        });
    }

    @Override
    public void stop(BundleContext context) throws Exception
    {
        // unregister all exported services
        registrator.unregisterServices();
 
        plugin = null;
        super.stop(context);
    }

    ...
}
Инициализацию и регистрацию сервисов плагина мы рекомендуем выполнять в отдельном потоке, т. к. платформа OSGi чувствительна к времени запуска плагинов. Время запуска должно быть коротким. Для удобства реализован служебный класс ServiceInitialization с методом ServiceInitialization#schedule(Runnable).

Если требуется, чтобы в качестве выставляемых реализаций сервисов были доступны сервисы из IoC-контейнера на основе Google Guice, вы можете воспользоваться реализацией-расширением InjectorAwareServiceRegistrator:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
/**
 * Activator of the plug-in that produces some OSGi services.
 */
public class ServicesProducerPlugin
    extends Plugin
{
    private static ServicesProducerPlugin plugin;
    private InjectorAwareServiceRegistrator registrator ; 
    /**
     * Returns the shared instance
     *
     * @return the shared instance
     */
    public static ServicesProducerPlugin getDefault()
    {
        return plugin;
    }
 
    @Override
    public void start(BundleContext context) throws Exception
    {
        super.start(context);
        plugin = this;
        registrator = new InjectorAwareServiceRegistrator(context, this::getInjector);
     
        ServiceInitialization.schedule(() -> {     
            // register services from injector
            registrator.service(ISomeService.class).registerInjected();
      
            // register qualified service from injector
            registrator.service(ISomeQualifiedService.class).withProperty("qualifier", "MdImplementation")
                .registerInjected(ISomeQualifiedService.class, Names.named("MdImplementation"));
     
            // register instance
            registrator.service(ISomeInstanceService.class).registerInstance(new ISomeInstanceService()
            {
                @Override
                public void method()
                {
                    // do nothing
                }
            });
        });
    }

    @Override
    public void stop(BundleContext context) throws Exception
    {
        // unregister all registered services
        registrator.unregisterServices();
 
        plugin = null;
        super.stop(context);
    }

    ...
}

Если есть потребность выставлять публичные сервисы с жизненным циклом, отличным от жизненного цикла плагина, это можно сделать аналогичным образом и вне активатора:

1
2
3
4
5
6
7
8
// Регистрируем экземпляр
registrator = new ServiceRegistrator(bundleContext);
registrator.service(ISomeService.class).registerInstance(new SomeServiceImpl());
 
...
 
// Вы сами ответственны за то, чтобы дерегистрировать его, когда вы сочтете нужным
registrator.unregisterServices();

Разработка сервисов, требующих активации и деактивации #

Иногда имеется потребность в сервисах, которые инициализируются вместе со стартом плагина и деинициализируются с остановкой плагина. В 1C:EDT существует интерфейс IManagedService, и есть возможность встраивать сервисы, реализующие данный интерфейс, в жизненный цикл плагина c помощью ServiceRegistrator или InjectorAwareServiceRegistrator. Далее приведен пример активатора плагина с публичными сервисами, имеющими жизненный цикл:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
/**
 * Activator of the plug-in that produces some OSGi services.
 */
public class ServicesProducerPlugin
    extends Plugin
{
    private static ServicesProducerPlugin plugin;
    private InjectorAwareServiceRegistrator registrator ; 
    /**
     * Returns the shared instance
     *
     * @return the shared instance
     */
    public static ServicesProducerPlugin getDefault()
    {
        return plugin;
    }
 
    @Override
    public void start(BundleContext context) throws Exception
    {
        super.start(context);
        plugin = this;
        registrator = new InjectorAwareServiceRegistrator(context, this::getInjector);
     
        ServiceInitialization.schedule(() -> {  
            // register services from injector
            registrator.service(ISomeService.class).registerInjected();
      
            // register qualified service from injector
            registrator.service(ISomeQualifiedService.class).withProperty("qualifier", "MdImplementation")
                .registerInjected(ISomeQualifiedService.class, Names.named("MdImplementation"));
     
            // register instance
            registrator.service(ISomeInstanceService.class).registerInstance(new ISomeInstanceService()
            {
                @Override
                public void method()
                {
                    // do nothing
                }
            });
        });
 
        // activate all managed services
        ServiceInitialization.schedule(() -> {
            registrator.managedService(ISomeManagedService.class).activateBeforeRegistration().registerInjected();
        });
    }

    @Override
    public void stop(BundleContext context) throws Exception
    {
        // unregister all registered services
        registrator.unregisterServices();
        
        // deactivate all managed services
        registrator.deactivateManagedServices(this);
 
        plugin = null;
        super.stop(context);
    }

    ...
}
Инициализацию управляемых сервисов мы рекомендуем выносить в отдельный поток, если в процессе инициализации предполагается, например, чтение Extension Registry. Причина заключается в следующем: в расширениях, которые будут читаться при инициализации этого сервиса, могут быть попытки доступа к другим публичным сервисам, которые регистрируются в этом плагине. Это возможно потому, что, строго говоря, расширения могут определяться и разработчиками расширений 1C:EDT, которые могут использовать там все, что захотят. Поэтому в процессе инициализации могут возникать взаимные блокировки из-за очередности регистрации / активации управляемых сервисов. Если при активации управляемых сервисов они не читают Extension Registry, то выносить их в отдельный поток не обязательно.

Общие рекомендации #

  • Отделяйте плагин с интерфейсами публичных сервисов и внешним API от плагина с их реализацией и регистрацией.
  • Не делайте сервисы из классов или плагинов, представляющих собой библиотеку.
  • Публичные сервисы на данный момент реализованы в единственном объеме (scope) — как синглтоны. Поэтому внедрение их через Google Guice с расчетом получать каждый раз новый экземпляр сервиса невозможно. В случае такой потребности мы рекомендуем реализовать фабрику и зарегистрировать ее в качестве публичного сервиса. Эта фабрика уже сама будет каждый раз возвращать новый экземпляр необходимого сервиса.

Полезные ссылки (на английском языке) #