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

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

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

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

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

Ответы [ 7 ]

4 голосов
/ 20 сентября 2011

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

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

Вам потребуется либо:

  1. принудительно выполнить некоторый код в конструкторе (относительнолегко, если ваш слушатель является абстрактным классом, которым вы управляете)
  2. Как-нибудь проработайте код (лекарство кажется здесь хуже, чем болезнь).
  3. Сделайте некоторые предположения о том, как называются классы.
  4. Прочитайте jar (сделайте то же самое, что и javac -p)

For 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.Анонимные внутренние классы именуются в порядке их появления в файле, поэтому Actor $ 1 будет отображаться в файле перед Actor $ 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 содержит список номеров строк, которые применяются к методу.Итак, мой конструктор для Consumer начинается со строки 20. Но это первая строка конструктора , а не первая строка класса.Это только та же строка, потому что я использую конструктор по умолчанию.Если я добавлю конструктор, то номера строк изменятся.компилятор не хранит строку, в которой объявлен класс.Таким образом, вы не можете найти, где объявлен класс, без разбора самой java.У вас просто нет доступной информации.

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

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

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

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

3 голосов
/ 20 сентября 2011

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

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

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

public synchronized native Throwable fillInStackTrace();

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

1 голос
/ 05 августа 2014

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

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();
    }

}

РЕДАКТИРОВАТЬ: Оглядываясь назад, я бы, вероятно, использовал AOP, чтобы сделать это сейчас.На самом деле, некоторые незначительные изменения этого проекта , и я, вероятно, мог бы сделать это. Вот вопрос стека с подсказкой.

1 голос
/ 15 мая 2012

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

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

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

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

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

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

1 голос
/ 20 сентября 2011

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

0 голосов
/ 06 августа 2018

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

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

Вам потребуется обновить исходную папку (src) и выходную папку (bin) в соответствии с вашим соглашением.Вам нужно будет предоставить собственную реализацию

String searchReplace(old, new, inside)

Вот код

public static String sourceFileFromClass(Class<?> clazz) {
    String answer = null;
    try {
        if (clazz.getEnclosingClass() != null) {
            clazz = clazz.getEnclosingClass();
        }
        String simpleName = clazz.getSimpleName();

        // the .class file should exist from where it was loaded!
        URL url = clazz.getResource(simpleName + ".class");
        String sourceFile = searchReplace(".class", ".java", url.toString());
        sourceFile = searchReplace("file:/", "", sourceFile);
        sourceFile = searchReplace("/bin/", "/src/", sourceFile);
        if( new java.io.File(sourceFile).exists() ) {
            answer = sourceFile;
        }
    }
    catch (Exception ex) {
        // ignore
    }

    return answer;
}
0 голосов
/ 20 сентября 2011

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

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

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

Почему?

...