Как получить исходное имя файла/номер строки из объекта java.lang.Class

Возможно ли, учитывая объект java.lang.Class, получить имя исходного файла и номер строки, в которой был объявлен класс?

Данные должны быть доступны в информации об отладке файла .class. Единственное место, где я знаю, где JDK возвращает такую ​​информацию об отладке, находится в java.lang.StackTraceElement, но я не уверен, может ли заставить Java создать экземпляр java.lang.StackTraceElement для произвольного класса, потому что мы не выполняем метод в классе.

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

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

Ответ 1

Ответ на этот вопрос сводится к тому, сколько у вас контроля над кодом, реализующим Listener. Вы правы, что невозможно создать stacktrace, не будучи в методе.

Общая методика заключается в создании Exception() в конструкторе, но не бросайте его. Это содержит информацию о стеке, которую вы можете использовать, как вы хотите. Это даст вам номер строки конструктора, но не класса. Обратите внимание, что этот метод не особенно эффективен, потому что создание stacktrace является дорогостоящим.

Вам понадобится:

  • принудительно выполнить некоторый код в конструкторе (относительно легко, если ваш Listener является абстрактным классом, который вы контролируете)
  • Принесите код как-нибудь (лечение здесь хуже, чем заболевание).
  • Сделайте некоторые предположения о том, как названы классы.
  • Прочитайте банку (сделайте то же самое, что и javac -p)

Для 1) вы просто поместите создание исключения в абстрактный класс, и конструктор будет вызван подклассом:

class Top {
    Top() {
        new Exception().printStackTrace(System.out);
    }
}

class Bottom extends Top {
    public static void main(String[] args) {
        new Bottom();
    }
}

это создает что-то вроде:

java.lang.Exception
    at uk.co.farwell.stackoverflow.Top.<init>(Top.java:4)
    at uk.co.farwell.stackoverflow.Bottom.<init>(Bottom.java: 11)
    at uk.co.farwell.stackoverflow.Bottom.main(Bottom.java: 18)

В общем, есть некоторые правила именования, которые следуют: если у вас есть внешний класс под названием Actor и внутренний вызываемый Consumer, то скомпилированный класс будет называться Actor $Consumer. Анонимные внутренние классы называются в том порядке, в котором они появляются в файле, поэтому Актер $1 появится в файле до Актера $2. Я не думаю, что это на самом деле указано где угодно, так что это, вероятно, просто соглашение, и на него нельзя положиться, если вы делаете что-то сложное с несколькими jvms и т.д.

Возможно, как указывал jmg, вы можете определить несколько классов верхнего уровня в одном файле. Если у вас есть открытый класс Foo, это должно быть определено в Foo.java, но непубличный класс может быть включен в другой файл. Вышеупомянутый метод справится с этим.

Пояснение:

Если вы разобьете java (javap -c -verbose), вы увидите, что в отладочной информации есть номера строк, но они применяются только к методам. Используя следующий внутренний класс:

static class Consumer implements Runnable {
    public void run() {
        // stuff
    }
}

а выход javap содержит:

uk.co.farwell.stackoverflow.Actors$Consumer();
  Code:
   Stack=1, Locals=1, Args_size=1
   0:   aload_0
   1:   invokespecial   #10; //Method java/lang/Object."<init>":()V
   4:   return
  LineNumberTable: 
   line 20: 0

  LocalVariableTable: 
   Start  Length  Slot  Name   Signature
   0      5      0    this       Luk/co/farwell/stackoverflow/Actors$Consumer;

В строке LineNumberTable содержится список номеров строк, которые применяются к методу. Поэтому мой конструктор для Потребителя начинается с строки 20. Но это первая строка конструктора, а не первая строка класса. Это только одна и та же строка, потому что я использую конструктор по умолчанию. Если я добавлю конструктор, номера строк изменятся. компилятор не сохраняет строку, объявленную классом. Таким образом, вы не можете найти, где объявлен класс без разбора самой java. У вас просто нет информации.

Однако, если вы используете анонимный внутренний класс, например:

Runnable run = new Runnable() {
    public void run() {
    }
};

Тогда номер строки конструктора и класса будет соответствовать [*], поэтому это дает вам номер строки.

[*] За исключением случаев, когда "новый" и "Runnable()" находятся на разных строках.

Ответ 2

Вы можете найти текущую строку кода uut:

Throwable t = new Throwable();
System.out.println(t.getStackTrace()[0].getLineNumber());

Но похоже, что StackTraceElements создается встроенным методом JDK внутри Throwable.

public synchronized native Throwable fillInStackTrace();

Событие, если вы используете структуру манипулирования байтовым кодом, чтобы добавить метод к классу, который создает throwable, вы не получите правильную строку кода объявления класса.

Ответ 3

Вы можете получить трассировку стека из любого потока, вызвав getStackTrace(). Для текущего потока вы должны называть Thread.currentThread().getStackTrace().

Ответ 4

В ваших целях генерация исключения только для его трассировки стека является правильным ответом.

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

public boolean isScala(Class jvmClass) {
   JavaClass bpelClass = Repository.lookupClass(jvmClass.getName());
   return (bpelClass.getFileName().endsWith(".scala");
}

(Предупреждение: я не тестировал этот код.)

Другой вариант - создать пользовательский doclet для сбора соответствующих метаданных во время компиляции. Я использовал это в прошлом для приложения, которое необходимо было знать во время выполнения всех подклассов определенного суперкласса. Добавление их в метод factory было слишком громоздким, и это также позволяет мне напрямую ссылаться на javadoc.

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

Ответ 5

Вот как я это сделал:

import java.io.PrintWriter;
import java.io.StringWriter;

/**
 * This class is used to determine where an object is instantiated from by extending it. 
 */
public abstract class StackTracer {

    protected StackTracer() {
        System.out.println( shortenedStackTrace( new Exception(), 6 ) );
    }

    public static String shortenedStackTrace(Exception e, int maxLines) {
        StringWriter writer = new StringWriter();
        e.printStackTrace( new PrintWriter(writer) );
        String[] lines = writer.toString().split("\n");
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < Math.min(lines.length, maxLines); i++) {
            sb.append(lines[i]).append("\n");
        }
        return sb.toString();
    }

}

EDIT: Оглядываясь назад, я бы, вероятно, использовал АОП, чтобы сделать это сейчас. Фактически, некоторые незначительные изменения в этот проект, и я, вероятно, мог бы это сделать. Вот вопрос в стеке с подсказкой.

Ответ 6

Возможно ли, если экземпляр java.lang.Class получит имя исходного файла и номер строки, на которой был объявлен класс?

Исходный файл в большинстве случаев сильно связан с именем класса. В том файле, в котором объявлен класс, есть только одна строка. В информации об отладке нет необходимости в такой однозначной информации.

Данные должны быть доступны в информации об отладке файла .class

Почему?