Отключение проверки зависимостей во время компиляции при компиляции классов Java - PullRequest
12 голосов
/ 08 октября 2009

Рассмотрим следующие два класса Java:

a.) class Test { void foo(Object foobar) { } }

b.) class Test { void foo(pkg.not.in.classpath.FooBar foobar) { } }

Кроме того, предположим, что pkg.not.in.classpath.FooBar не найден в пути к классам.

Первый класс будет хорошо скомпилирован с использованием стандартного javac.

Однако второй класс не скомпилируется, и javac выдаст вам сообщение об ошибке "package pkg.not.in.classpath does not exist".

Сообщение об ошибке в общем случае приятно, поскольку проверка ваших зависимостей позволяет компилятору сообщать вам, если вы неправильно указали какой-либо аргумент метода, и т. Д.

Хотя проверка и проверка зависимостей во время компиляции приятна и полезна, AFAIK не строго , необходимый для создания файла класса Java в приведенном выше примере.

  1. Можете ли вы привести какой-либо пример, для которого было бы технически невозможно сгенерировать допустимый файл класса Java без выполнения проверки зависимостей времени компиляции?

  2. Знаете ли вы какой-либо способ дать команду javac или любому другому компилятору Java пропустить проверку зависимости времени компиляции?

Пожалуйста, убедитесь, что ваш ответ отвечает на оба вопроса.

Ответы [ 8 ]

19 голосов
/ 08 октября 2009

Можете ли вы привести какой-либо пример, для которого было бы технически невозможно сгенерировать допустимый файл класса Java без выполнения проверки зависимостей во время компиляции?

Рассмотрим этот код:

public class GotDeps {
  public static void main(String[] args) {
    int i = 1;
    Dep.foo(i);
  }
}

Если целевой метод имеет подпись public static void foo(int n), будут сгенерированы следующие инструкции:

public static void main(java.lang.String[]);
  Code:
   0:   iconst_1
   1:   istore_1
   2:   iload_1
   3:   invokestatic    #16; //Method Dep.foo:(I)V
   6:   return

Если целевой метод имеет подпись public static void foo(long n), то int будет повышен до long до вызова метода:

public static void main(java.lang.String[]);
  Code:
   0:   iconst_1
   1:   istore_1
   2:   iload_1
   3:   i2l
   4:   invokestatic    #16; //Method Dep.foo:(J)V
   7:   return

В этом случае невозможно будет сгенерировать инструкции вызова или как заполнить структуру CONSTANT_Methodref_info, указанную в пуле констант класса, числом 16. См. Формат файла class в спецификация VM для более подробной информации.

5 голосов
/ 08 октября 2009

Я не думаю, что есть такой способ - компилятор должен знать о классе аргумента, чтобы создать соответствующий байт-код. Если он не может найти класс Foobar, он не может скомпилировать класс Test.

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

Так что ваша предпосылка - что компилятору не нужно находить класс для компиляции в этом случае - неверна.

Редактировать - ваш комментарий, похоже, спрашивает: «Может ли компилятор просто пропустить факт и сгенерировать байт-код, который будет уместным в любом случае?»

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

Это означает, что, хотя механически было бы довольно просто создать компилятор, который бы делал то, что вы просите, он нарушил бы JLS и, таким образом, технически не был бы компилятором Java. Кроме того, обход безопасности при компиляции для меня не очень выгодно ...: -)

2 голосов
/ 08 октября 2009

Было бы нарушением JLS компилировать класс, не глядя на сигнатуры типов классов, от которых он зависит. Никакой совместимый Java-компилятор не позволит вам сделать это.

Однако ... можно сделать что-то похожее. В частности, если у нас есть класс A и класс B, который зависит от A, можно сделать следующее:

  1. Компиляция A.java
  2. Скомпилируйте B.java против A.class.
  3. Отредактируйте A.java, чтобы изменить его несовместимым способом.
  4. Скомпилируйте A.java, заменив старый класс A.
  5. Запустите приложение Java с использованием B.class и нового (несовместимого) A.class.

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

На самом деле, это показывает, почему компиляция игнорирующих зависимостей была бы плохой идеей. Если вы запускаете приложение с несовместимыми файлами байт-кода, (только) будет сообщено о первом обнаруженном несоответствии. Поэтому, если у вас много несоответствий, вам придется много раз запускать приложение, чтобы «обнаружить» их все. Действительно, если в приложении или какой-либо из его зависимостей есть какая-либо динамическая загрузка классов (например, с использованием Class.forName()), то некоторые из этих проблем могут появиться не сразу.

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

2 голосов
/ 08 октября 2009

Я не вижу, как вы могли бы разрешить это, не нарушая проверку типов Java. Как бы вы использовали ссылочный объект в вашем методе? Чтобы расширить на вашем примере,

class test {
   void foo (pkg.not.in.classpath.FooBar foobar) { 
       foobar.foobarMethod(); //what does the compiler do here?
  } 
}

Если вы находитесь в ситуации, когда вам нужно скомпилировать (и вызвать метод) что-то, что работает с библиотекой, у вас нет доступа к ближайшему, что вы можете получить - получить метод с помощью отражения, что-то вроде (вызов метода из памяти, может быть неточным)

 void foo(Object suspectedFoobar)
     {
       try{
        Method m = suspectedFoobar.getClass().getMethod("foobarMethod");
        m.invoke(suspectedFoobar);
       }
       ...
     }

Хотя я не вижу смысла делать это. Можете ли вы предоставить больше информации о проблеме, которую вы пытаетесь решить?

1 голос
/ 08 октября 2009

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

Что можно сделать (и, например, для драйверов JDBC), - это отложить проверку зависимостей с помощью отражения. Вы можете получить класс из Class.forName без знания компилятором класса во время компиляции. В целом, однако, это означает, что код записывается в интерфейс, и во время выполнения загружается класс, который реализует интерфейс.

0 голосов
/ 08 октября 2009

Я создал два класса: Caller и Callee

public class Caller {
    public void doSomething( Callee callee) {
        callee.doSomething();
    }

    public void doSame(Callee callee) {
        callee.doSomething();
    }

    public void doSomethingElse(Callee callee) {
        callee.doSomethingElse();
    }
}

public class Callee {
    public void doSomething() {
    }
    public void doSomethingElse() {
    }
}

Я скомпилировал эти классы, а затем разобрал их с помощью javap -c Callee > Callee.bc и javap -c Caller > Caller.bc. Это произвело следующее:

Compiled from "Caller.java"
public class Caller extends java.lang.Object{
public Caller();
Code:
0: aload_0
1: invokespecial #1; //Method java/lang/Object."<init>":()V
4: return

public void doSomething(Callee);
Code:
0: aload_1
1: invokevirtual #2; //Method Callee.doSomething:()V
4: return

public void doSame(Callee);
Code:
0: aload_1
1: invokevirtual #2; //Method Callee.doSomething:()V
4: return

public void doSomethingElse(Callee);
Code:
0: aload_1
1: invokevirtual #3; //Method Callee.doSomethingElse:()V
4: return

}

Compiled from "Callee.java"
public class Callee extends java.lang.Object{
public Callee();
Code:
0: aload_0
1: invokespecial #1; //Method java/lang/Object."<init>":()V
4: return

public void doSomething();
Code:
0: return

public void doSomethingElse();
Code:
0: return

}

Компилятор сгенерировал сигнатуру метода и вызов типов invokevirtual для вызовов метода для вызываемого объекта - он знает, какой класс и какой метод вызывается здесь. Если этот класс недоступен, как компилятор сгенерирует сигнатуру метода или «invokevirtual»?

Существует JSR ( JSR 292 ) для добавления кода операции invokedynamic, который будет поддерживать динамический вызов, однако в настоящее время это не поддерживается JVM.

0 голосов
/ 08 октября 2009

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

Нет ничего в грамматике Java для использования pkg.not.in.classpath.FooBar, чтобы различать это:

 package pkg.not.in.classpath;
 public class FooBar { }

из этого:

 package pkg.not.in.classpath;
 class FooBar { }

Так что только ваше слово разрешать использовать FooBar там законно.

Существует также двусмысленность между классами области пакета и внутренними классами в источнике:

class pkg {
    static class not {
        static class in {
            static class classpath {
                static class FooBar {}
            }
        }
    }
}

Внутренний класс также называется pkg.not.in.classpath.FooBar в исходном коде, но в файле класса будет называться pkg$not$in$classpath$FooBar, а не pkg/not/in/classpath/FooBar. Javac не может определить, что вы имеете в виду, не ища его в пути к классам.

0 голосов
/ 08 октября 2009

Извлечение интерфейса

pkg.in.classpath.IFooBar

составляют FooBar implements IFooBar и

class Test { void foo(pkg.in.classpath.IFooBar foobar) {} }

Ваш тестовый класс будет скомпилирован. Просто подключите правильную реализацию, т.е. FooBar во время выполнения, используя фабрики и конфигурацию. Ищите несколько контейнеров МОК .

...