AspectJ Load time weaver не обнаруживает всех классов

Я использую декларативные транзакции Spring (аннотация @Transactional) в режиме "aspectj". Он работает в большинстве случаев точно так, как должен, но для одного он этого не делает. Мы можем назвать его Lang (потому что это то, что он на самом деле называл).

Я смог точно определить проблему для ткача времени. Включив отладку и подробное ведение журнала в файле aop.xml, он перечисляет все сотканные классы. Проблематичный класс Lang вообще не упоминается в журналах.

Затем я помещаю точку останова в начало Lang, заставляя Eclipse приостанавливать поток при загрузке класса Lang. Эта точка останова поражается, когда LTW переплетает другие классы! Поэтому я угадываю, что он либо пытается переплетать Lang, и терпит неудачу, и не выводит его, либо какой-либо другой класс имеет ссылку, которая заставляет его загружать Lang, прежде чем он действительно получит возможность переплетать его.

Однако я не уверен, как продолжать отлаживать это, так как я не могу воспроизвести его в меньших масштабах. Любые предложения о том, как продолжить?


Обновление. Другие подсказки также приветствуются. Например, как работает LTW? Кажется, что происходит много магии. Есть ли какие-либо опции для получения еще более отладочной информации от LTW? В настоящее время у меня есть:

<weaver options="-XnoInline -Xreweavable -verbose -debug -showWeaveInfo">

Я забыл упомянуть об этом раньше: spring -agent используется для разрешения LTW, т.е. InstrumentationLoadTimeWeaver.


Основываясь на предложениях Энди Клемента, я решил проверить, прошел ли какой-либо трансформатор AspectJ даже класс. Я поставил точку останова в ClassPreProcessorAgent.transform(..), и кажется, что класс Lang никогда не достигает этого метода, несмотря на то, что он загружается одним и тем же загрузчиком классов, как и другие классы (экземпляр Jetty WebAppClassLoader).

Затем я включил точку останова в InstrumentationLoadTimeWeaver$FilteringClassFileTransformer.transform(..). Даже для этого не выбрано значение Lang. И я считаю, что этот метод следует вызывать для всех загруженных классов, независимо от того, какой класс загрузчик они используют. Это начинает выглядеть так:

  • Проблема с моей отладкой. Возможно, Lang не загружается в то время, когда Eclipse сообщает, что он
  • Ошибка Java? Надуманный, но я предполагаю, что это произойдет.

Следующий ключ: я включил -verbose:class, и кажется, что Lang загружается преждевременно - возможно, до того, как трансформатор будет добавлен в Инструментарий. Как ни странно, моя точка останова Eclipse не поймает эту нагрузку.

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


Эти строки в ConfigurationClassBeanDefinitionReader приводят к чтению класса Lang:

else if (metadata.isAnnotated(Component.class.getName()) ||
        metadata.hasAnnotatedMethods(Bean.class.getName())) {
    beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_LITE);
    return true;
}

В частности, metadata.hasAnnotatedMethods() вызывает getDeclaredMethods() в классе, который загружает все классы параметров всех методов в этом классе. Я предполагаю, что это, возможно, не конец проблемы, потому что я думаю, что классы должны быть выгружены. Может ли JVM кэшировать экземпляр класса по непознаваемым причинам?

Ответ 1

Хорошо, я решил проблему. По сути, это проблема Spring в сочетании с некоторыми пользовательскими расширениями. Если кто-то встретит что-то подобное, я попытаюсь объяснить шаг за шагом, что происходит.

Прежде всего, у нас есть пользовательский BeanDefintionParser в нашем проекте. Этот класс имел следующее определение:

private static class ControllerBeanDefinitionParser extends AbstractSingleBeanDefinitionParser {

    protected Class<?> getBeanClass(Element element) {
        try {
            return Class.forName(element.getAttribute("class"));
        } catch (ClassNotFoundException e) {
            throw new RuntimeException("Class " + element.getAttribute("class") + "not found.", e);
        }
    }

// code to parse XML omitted for brevity

}

Теперь проблема возникает после того, как все определение bean было прочитано, и BeanDefinitionRegistryPostProcessor начинает пинать. На этом этапе класс под названием ConfigurationClassPostProcessor начинает просматривать все определения bean, чтобы искать bean > классы, аннотированные с помощью @Configuration или имеющие методы с @Bean.

В процессе чтения аннотаций для bean он использует интерфейс AnnotationMetadata. Для большинства обычных beans используется подкласс под названием AnnotationMetadataVisitor. Однако при анализе определений bean, если вы переопределили метод getBeanClass() для возврата экземпляра класса, как и у нас, вместо этого используется экземпляр StandardAnnotationMetadata. Когда вызывается StandardAnnotationMetadata.hasAnnotatedMethods(..), он вызывает Class.getDeclaredMethods(), что в свою очередь заставляет загрузчик классов загружать все классы, используемые в качестве параметров этого класса. Занятые таким образом классы неправильно выгружаются и, таким образом, никогда не сотканы, так как это происходит до того, как зарегистрирован трансформатор AspectJ.

Теперь моя проблема заключалась в том, что у меня был класс:

public class Something {
    private Lang lang;
    public void setLang(Lang lang) {
        this.lang = lang;
    }
}

Затем у меня был bean класса Something, который был проанализирован с использованием нашего пользовательского ControllerBeanDefinitionParser. Это вызвало неправильную процедуру обнаружения аннотаций, которая вызвала неожиданную загрузку классов, что означало, что AspectJ никогда не получал возможность переплетать Lang.

Решение заключалось в том, чтобы не переопределять getBeanClass(..), а вместо этого переопределять getBeanClassName(..), который согласно документации предпочтительнее:

private static class ControllerBeanDefinitionParser extends AbstractSingleBeanDefinitionParser {

    protected String getBeanClassName(Element element) {
        return element.getAttribute("class");
    }

// code to parse XML omitted for brevity

}

Урок дня: не переопределяйте getBeanClass, если вы на самом деле не имеете в виду это. Собственно, не пытайтесь писать свой собственный BeanDefinitionParser, если не знаете, что делаете.

Fin.

Ответ 2

Если ваш класс не упоминается в выводе -verbose/-debug, это говорит о том, что он не загружается загрузчиком, который, по вашему мнению, является. Можете ли вы быть на 100% уверены, что "Ланг" не находится на пути к классу загрузчика классов выше в иерархии? Какой загрузчик классов загружает Lang в момент времени, когда вы запускаете свою точку останова?

Кроме того, вы не указываете версию AspectJ - если вы на 1.6.7, у которой были проблемы с ltw для чего-либо, кроме тривиального aop.xml. Вы должны быть на 1.6.8 или 1.6.9.

Как это работает на самом деле?

Проще говоря, для каждого загрузчика классов, который может захотеть переплетать код, создается ткацкий станок AspectJ. AspectJ спрашивает, хочет ли он изменить байты для класса, прежде чем он будет определен для виртуальной машины. AspectJ просматривает любые файлы aop.xml, которые он может "видеть" (как ресурсы) через рассматриваемый класс загрузчика и использует их для настройки. После настройки он сплетает аспекты как указано, принимая во внимание все include/exclude clauses.

Энди Клемент
AspectJ Project Lead

Ответ 3

Вариант 1) Аспект J является открытым исходным кодом. Взломайте его и посмотрите, что происходит.

Вариант 2) Переименуйте свой класс в Bang, посмотрите, начнет ли он работать

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

Изменить -

Просмотр кода, подобного этому в источнике

        if (superclassnameIndex > 0) { // May be zero -> class is java.lang.Object
            superclassname = cpool.getConstantString(superclassnameIndex, Constants.CONSTANT_Class);
            superclassname = Utility.compactClassName(superclassname, false);

} else {
            superclassname = "java.lang.Object";
        }

Похоже, они пытаются пропустить плетение java.lang.stuff.... ничего не видят только для "lang", но он может быть там (или ошибка)