Статьи

Загрузка класса времени выполнения для поддержки меняющегося API

Я поддерживаю плагин IntelliJ, который улучшает опыт написания спецификаций Спока . Задача этого проекта — поддержка нескольких несовместимых версий IntelliJ API в одной кодовой базе. Решение простое в ретроспективе (это пример шаблона адаптера в дикой природе), но первоначально потребовалось немного размышлений и поиска примеров. Сегодня я снова был в коде, чтобы исправить поддержку новой версии , и я решил задокументировать, как я изначально решил проблему.

Основная проблема заключается в том, что мой скомпилированный код может быть загружен в среде выполнения JVM с любой из нескольких различных версий API. Моим решением было разделить проект на четыре части:

  • Основной проект, который не зависит от каких-либо различных вызовов API и поэтому совместим со всеми версиями API. Основной проект также имеет код, который загружает соответствующую реализацию адаптера на основе среды выполнения, в которой он находится. В этом случае я могу использовать IntelliJ PicoContainer для поиска службы, но API отражения или внедрение зависимостей также имеют что нужно
  • Набор абстрактных адаптеров, которые предоставляют API для основного проекта. Этот проект также не зависит от кода, который варьируется в зависимости от версии API.
  • Наборы классов, которые реализуют абстрактные адаптеры для каждой поддерживаемой версии API. Каждый набор адаптеров переносит изменяющиеся вызовы API и компилируется с определенной версией API.

Простейший случай, с которым нужно иметь дело, — это рефакторинг, в котором что-то в API перемещается. Это также то, что фактически сломало эту последнюю версию. Моему основному коду нужен Groovy-экземпляр com.intellij.lang.Language. Этот экземпляр перенесен в IntelliJ 14.

Этот код был постоянным до 14, поэтому в этом случае я добавляю новый адаптер. В модуле адаптера у меня есть абстрактный класс LanguageLookup.java :

01
02
03
04
05
06
07
08
09
10
11
package com.cholick.idea.spock;
 
import com.intellij.lang.Language;
import com.intellij.openapi.components.ServiceManager;
 
public abstract class LanguageLookup {
    public static LanguageLookup getInstance() {
        return ServiceManager.getService(LanguageLookup.class);
    }
    public abstract Language groovy();
}

Самая низкая версия IntelliJ API, которую я поддерживаю, — 11. Поиск экземпляра языка Groovy постоянен для 11-13, поэтому первый конкретный адаптер живет в модуле, скомпилированном с IntelliJ 11 API. LanguageLookup11.java :

01
02
03
04
05
06
07
08
09
10
package com.cholick.idea.spock;
 
import com.intellij.lang.Language;
import org.jetbrains.plugins.groovy.GroovyFileType;
 
public class LanguageLookup11 extends LanguageLookup {
    public Language groovy() {
        return GroovyFileType.GROOVY_LANGUAGE;
    }
}

В новейшем API появилось критическое изменение, поэтому второй конкретный адаптер живет в модуле, скомпилированном с версией 14 их API. LanguageLookup14.java :

01
02
03
04
05
06
07
08
09
10
package com.cholick.idea.spock;
 
import com.intellij.lang.Language;
import org.jetbrains.plugins.groovy.GroovyLanguage;
 
public class LanguageLookup14 extends LanguageLookup {
    public Language groovy() {
        return GroovyLanguage.INSTANCE;
    }
}

Наконец, у основного проекта есть класс SpockPluginLoader.java, который регистрирует правильный класс адаптера на основе загруженного API времени выполнения (я опустил несколько методов, не относящихся конкретно к примеру):

01
02
03
04
05
06
07
08
09
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
package com.cholick.idea.spock.adapter;
 
import com.cholick.idea.spock.LanguageLookup;
import com.cholick.idea.spock.LanguageLookup11;
import com.cholick.idea.spock.LanguageLookup14;
import com.intellij.openapi.application.ApplicationInfo;
import com.intellij.openapi.components.ApplicationComponent;
import com.intellij.openapi.components.impl.ComponentManagerImpl;
import org.jetbrains.annotations.NotNull;
import org.picocontainer.MutablePicoContainer;
 
public class SpockPluginLoader implements ApplicationComponent {
    private ComponentManagerImpl componentManager;
 
    SpockPluginLoader(@NotNull ComponentManagerImpl componentManager) {
        this.componentManager = componentManager;
    }
 
    @Override
    public void initComponent() {
        MutablePicoContainer picoContainer = componentManager.getPicoContainer();
        registerLanguageLookup(picoContainer);
    }
 
    private void registerLanguageLookup(MutablePicoContainer picoContainer) {
        if(isAtLeast14()) {
            picoContainer.registerComponentInstance(LanguageLookup.class.getName(), new LanguageLookup14());
        } else {
            picoContainer.registerComponentInstance(LanguageLookup.class.getName(), new LanguageLookup11());
        }
    }
 
    private IntelliJVersion getVersion() {
        int version = ApplicationInfo.getInstance().getBuild().getBaselineVersion();
        if (version >= 138) {
            return IntelliJVersion.V14;
        } else if (version >= 130) {
            return IntelliJVersion.V13;
        } else if (version >= 120) {
            return IntelliJVersion.V12;
        }
        return IntelliJVersion.V11;
    }
 
    private boolean isAtLeast14() {
        return getVersion().compareTo(IntelliJVersion.V14) >= 0;
    }
 
    enum IntelliJVersion {
        V11, V12, V13, V14
    }
}

Наконец, в коде, где мне нужен Groovy com.intellij.lang.Language, я использую сервис LanguageLookup и вызываю его метод groovy:

1
2
3
4
...
Language groovy = LanguageLookup.getInstance().groovy();
if (PsiUtilBase.getLanguageAtOffset(file, offset).isKindOf(groovy)) {
...

Это решение позволяет одному и тому же скомпилированному плагину JAR поддерживать различные API IntelliJ в версиях 11-14. Я предполагаю, что разработчики Android обычно внедряют подобные решения, но мне никогда не приходилось писать это как разработчик веб-приложений.