Статьи

Generics и Covariant Overriding нарушают обратную совместимость— Как это исправить?


Generics и Covariant Overriding — очень полезные функции, которые были добавлены в Java 5. Generics позволяет типу или методу работать с объектами различных типов, обеспечивая при этом безопасность типов во время компиляции, тогда как Covariant overriding позволяет изменять тип возвращаемого метода на подтип возвращаемого типа. метода суперкласса при переопределении его в подклассе. Обе эти функции помогают в разработке безопасных чистых API для типов. Всякий раз, когда вы разрабатываете новый API или переделываете свой унаследованный код, который в настоящее время никем не используется, вам не нужно беспокоиться о бинарной совместимости, поскольку никто не использует ее и ее изменение не нарушит бинарную совместимость. Но обычно есть ряд клиентов или потребителей вашего API, которые должны быть перекомпилированы для работы с новым кодом, иначе двоичная совместимость нарушится. Например,в устаревшем коде у вас есть следующий код

public class MyService{
public A getA(){
return new A();
}
}

и в новом переработанном коде вы изменили код так, чтобы он возвращал подтип A, называемый ASubtype.

public class MyService{
public ASubtype getA(){
return new ASubtype();
}
}

В приведенном выше фрагменте кода, если клиент использовал унаследованную версию метода getA (), который возвращал A, его необходимо перекомпилировать, чтобы он работал с новым методом getA (), который возвращает подтип A. То же самое верно, когда мы генерируем наш код. Например, предположим, у нас есть класс MyService, который реализует интерфейс службы, как показано ниже

public interface Service {

String getMessage(Object request);

}

public class ExampleService implements Service {

public String getMessage(Object request) {
return "Hello world!";
}

}

Когда мы генерируем код (как показано ниже), то есть мы добавляем параметр типа T в интерфейс Service, а у ExampleService, который реализует общий интерфейс Service, теперь есть метод getMessage (), который принимает аргумент String вместо Object. Теперь все клиенты API ExampleService должны быть перекомпилированы с новой подписью, иначе двоичная совместимость нарушится.

public interface Service<T> {
String getMessage(T request);
}

public class ExampleService implements Service {
public String getMessage(String request) {
return "Hello world!";
}
}

Это приводит к интересному вопросу, как это работает в стандартном коде Java. Та же проблема должна была возникнуть, когда были сгенерированы такие интерфейсы, как Comparable или Comparator и многие другие, потому что они также использовали объект Object в качестве аргументов и были сгенерированы для получения параметра типа T. Но такие классы, как Integer, String и т. Д., Которые реализуют эти интерфейсы, все еще остаются двоично-совместимыми. Я нашел ответ, как двоичная совместимость поддерживается в Java SDK при чтении книги обобщений и коллекций Java. Книга Java Generics and Collections — отличный справочник по изучению Generics. В Java SDK эта проблема решается путем добавления дополнительных файлов в файлы классов. Эти методы генерируются автоматически компилятором и называются мостами. Так,файл скомпилированного класса будет содержать две версии метода, одна из которых принимает параметр типа, заданный реализацией класса, т. е. Integer или String, а другая — объект Object в качестве аргумента. Компонент добавляет объект, принимающий Object в качестве аргумента. Вы можете найти то же самое, декомпилировав класс Integer или String. Декомпилированная версия класса Integer, как показано ниже, содержит две версии метода compareTo, как показано ниже.

public int compareTo(Integer integer){
int i = value;
int j = integer.value;
return i >= j ? ((int) (i != j ? 1 : 0)) : -1;
}

public volatile int compareTo(Object obj){
return compareTo((Integer)obj);
}

Как вы можете видеть выше, декомпилированная версия класса Integer имеет две версии метода compareTo (). Первый compareTo (Integer integer) — это тот, который существует в исходном коде класса Integer, но второй метод compareTo (Object obj) — это метод моста, который добавляется компилятором. Вот как двоичная совместимость поддерживается Java.

Но как мы можем поддерживать двоичную совместимость нашего кода?

Хорошо, что JDK поддерживает двоичную совместимость, но как мы можем поддерживать двоичную совместимость кода, который мы пишем. Есть ли способ генерировать методы моста для кода, который мы написали?

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

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

  1. Добавьте мавенскую зависимость метода-аннотации-моста
  2. Добавьте плагин bridge-method-injector maven, который будет выполнять постобработку байт-кода, чтобы внедрить необходимые методы моста
  3. Добавьте репозитории, из которых можно скачать плагины и зависимости.

Все вышеперечисленные три шага показаны ниже в примере pom.xml

<?xml version="1.0" encoding="UTF-8"?>
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<groupId>com.shekhar.jl</groupId>
<artifactId>bridge-method-injection-example</artifactId>
<version>1.0.0.CI-SNAPSHOT</version>
<packaging>jar</packaging>
<properties>
<maven.test.failure.ignore>true</maven.test.failure.ignore>
</properties>
<profiles>
<profile>
<id>strict</id>
<properties>
<maven.test.failure.ignore>false</maven.test.failure.ignore>
</properties>
</profile>
</profiles>

<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.7</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.infradna.tool</groupId>
<artifactId>bridge-method-annotation</artifactId>
<version>1.4</version>
<optional>true</optional>
</dependency>
</dependencies>

<build>
<pluginManagement>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<inherited>false</inherited>
<configuration>
<descriptorRefs>
<descriptorRef>project</descriptorRef>
</descriptorRefs>
</configuration>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.5</source>
<target>1.5</target>
</configuration>
</plugin>
<plugin>
<groupId>com.infradna.tool</groupId>
<artifactId>bridge-method-injector</artifactId>
<version>1.4</version>
<executions>
<execution>
<goals>
<goal>process</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

<repositories>
<repository>
<id>bridgeMethodInjection</id>
<name>bridgeMethodInjection</name>
<url>http://maven.dyndns.org/2/</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>bridgeMethodInjection</id>
<name>bridgeMethodInjection</name>
<url>http://maven.dyndns.org/2/</url>
</pluginRepository>
</pluginRepositories>
</project>

 Now lets apply this to our Covariant example. This is done by applying a @WithBridgeMethod annotation as shown below. This annotation tells the byte code processor to add the bridge method to your class file with return type as A.

public class MyService{
@WithBridgeMethods(A.class)
public ASubtype getA(){
return new ASubtype();
}
}

 The same can be done in case of Generics and you can refer to project documentation for more.