JMSpy — шпион за вызовами методов

в 10:23, , рубрики: java
image

Здравствуй! Хочу рассказать об одной библиотеке, которую я разработал в рамках моего прошлого проекта и так вышло, что она попала в OpenSource.

Для начала скажу пару слов о том, почему появилась необходимость в данной библиотеке. В рамках проекта приходилось работать со сложной доменной bidirectional tree-like структурой, т.е. по графу объектов можно ходить сверху вниз (от родителя до ребенка) и наоборот. Поэтому объекты получились объемные. В качестве хранилища мы использовали MongoDB, а так как объекты были объемные, то некоторые из них превышали максимальный размер MongoDB документа. Для того, чтобы решить эту проблему мы разнесли композитный объект по разным коллекциям (хотя в MongoDB лучше все хранить цельными документами). Таким образом, дочерние объекты сохранялись в отдельные коллекции, а документ, который являлся родителем, содержал ссылки на них. Используя данный подход мы реализовали механизм ленивой загрузки (lazy loading). То есть рутовый объект загружался не со всеми вложенными объектами, а только с top-level, его дочерние элементы грузились по требованию. Репозиторий, который отдавал основной объект, использовался в кастомных тэгах (Java Custom Tag), а теги в свою очередь на FTL страницах. В ходе performance тестирования мы заметили, что на страницах происходит много lazy-load вызовов. Начали пересматривать страницы и обнаружили неоптимальные вызовы вида:

rootObject.getObjectA().getObjectB().getName()

getObjectA() приводит к загрузке объекта из другой коллекции, та же ситуация и с getObjectB(). Но так как в rootObject есть поле objectBName то строку выше можно переписать следующим образом:

rootObject.getObjectBName()

такой подход не приводит к загрузке дочерних объектов и работает намного быстрее.

Встал вопрос: "Как найти все страницы, где есть такие неоптимальные вызовы, и устранить их?". Простым поиском по коду это занимало много времени и мы решили реализовать что-то вроде debug мода. Включаем debug mode, запускаем UI тесты, а по окончанию получаем информацию о том, какие методы нашего парент объекта вызывались и где. Так и появилась идея создания JMSpy.

Библиотека доступна в maven central, таким образом все, что вам нужно, это указать зависимость в вашем build tool.
Пример для maven:

<dependency>
    <groupId>com.github.dmgcodevil</groupId>
    <artifactId>jmspy-core</artifactId>
    <version>1.1.2</version>
</dependency>

jmspy-core — это модуль, который содержит основные возможности библиотеки. Так же есть jmspy-agent и jmspy-ext-freemarker, но об этом позже. JMspy позволяет записывать вызовы любой вложенности, например:

object.getCollection().iterator().next().getProperty()

Для начала рассмотрим основные компоненты библиотеки и их предназначение.

MethodInvocationRecorder — это основной класс, с которым взаимодействует конечный пользователь.
ProxyFactory — фэктори, который использует cglib для создания прокси. ProxyFactory это синглетон, принимающий Configuration в качестве параметра, таким образом, можно настроить фэктори под свои нужды, об этом ниже.
ContextExplorer — интерфейс, который предоставляет методы для получения информации о контексте исполнения метода. Например, jmspy-ext-freemarker — это реализация ContextExplorer для того, чтобы получать информацию о странице, на которой вызвался метод объекта (bean'a или pojo, как вам удобнее)

ProxyFactory

Фэктори позволяет создавать прокси для объектов. Есть возможность для конфигурации фэктори, это может быть полезно в случае сложных кейсов, хотя для простых объектов дефолтной конфигурации должно хватить. Для того, чтобы создать экземпляр фэктори нужно воспользоваться методом getInstance и передать туда инстанс конфигруции (Configiration), например так:

Configuration.Builder builder = Configuration.builder()
                .ignoreType(DataLoader.class) // objects with type DataLoader for which no proxy should be created
                .ignoreType(java.util.logging.Logger.class) // ignore objects with type DataLoader
                .ignorePackage("com.mongodb");  // ignore objects with types exist in specified package
ProxyFactory proxyFactory = ProxyFactory.getInstance(builder.build());

ContextExplorer

ContextExplorer это интерфейс, реализации которого должны предоставлять информацию о контексте выполнения. Jmspy предоставляет готовую реализацию для Freemarker (FreemarkerContextExplorer), которая поставляется отдельным jar модулем jmspy-ext-freemarker. Эта реализация предоставляет информацию о странице, адресе запроса и т.д. Вы можете создать свою реализацию и зарегистрировать ее в MethodInvocationRecorder. Можно зарегистрировать только одну реализацию для MethodInvocationRecorder. Интерфейс ContextExplorer содержит два метода, ниже немного о каждом из них.

getRootContextInfo — возвращает базовую информацию о контексте вызова, такую, как рутовый метод, название приложения, информацию о запросе, url и т.д. Этот метод вызывается сразу после того, как будет создан InvocationRecord, т.е. сразу после вызова метода

MethodInvocationRecorder#record(java.lang.reflect.Method, Object)} 

или

MethodInvocationRecorder#record(Object)} 

getCurrentContextInfo — предоставляет более детальную иформацию, такую как имя страницы FTL, JSP и т.д. Этот метод вызывается в то время, когда какой-либо метод был вызван на объекте, полученном из MethodInvocationRecorder#record, например:

User user = new User();
MethodInvocationRecorder methodInvocationRecorder = new MethodInvocationRecorder();
MethodInvocationRecorder.record(user).getName(); // в это время будет вызван getCurrentContextInfo()

MethodInvocationRecorder

Как вы уже догадались, это основной класс, с которым придется работать. Его основной функцией является запуск процесса шпионажа за вызовами методов. MethodInvocationRecorder предоставляет конструкторы, в которые можно передать экземпляр ProxyFactory и ContextExplorer.
В этом классе есть еще один важный метод: makeSnapshot(). Этот метод сохраняет текущий граф вызовов для последующего анализа при помощи jmspy-viewer.

Ограничения
Так как библиотека использует CGLIB для создания прокси, она имеет ряд ограничений, которые исходят из природы CGLIB. Известно, что CGLIB использует наследование и может создавать прокси для типов, которые не реализуют никаких интерфейсов. Т.е. CGLIB наследует сгенерированный прокси класс от целевого типа объекта, для которого создается прокси. Java имеет ряд некоторых ограничений предоставляемых к механизму наследования, а именно:

1. CGLIB не может создать прокси для final классов, так как финальные классы не могут наследоваться;
2. final методы не могут быть перехвачены, так как наследуемый класс не может переопределить финальный метод.
Для того что бы обойти эти ограничение можно воспользоваться двумя подходами:

1. Создать wrapper для класса (работает только в том случае, если ваш класс реализует некий интерфейс, с которым вы работаете)
Пример:

Интерфейс

public interface IFinalClass {
    String getId();
}

Класс:

public final class FinalClass implements IFinalClass {

    private String id;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }
}

Создаем wrapper

public class FinalClassWrapper implements IFinalClass, Wrapper<IFinalClass> {

    private IFinalClass target;

    public FinalClassWrapper() {
    }

    public FinalClassWrapper(IFinalClass target) {
        this.target = target;
    }

    @Override
    public Wrapper create(IFinalClass target) {
        return new FinalClassWrapper(target);
    }

    @Override
    public void setTarget(IFinalClass target) {
        this.target = target;
    }

    @Override
    public IFinalClass getTarget() {
        return target;
    }

    @Override
    public Class<? extends Wrapper<IFinalClass>> getType() {
        return FinalClassWrapper.class;
    }

    @Override
    public String getId() {
        return target.getId();
    }
}

Теперь нужно зарегистрировать враппер FinalClassWrapper используя метод registerWrapper.

    public static void main(String[] args) {
        Configuration conf = Configuration.builder()
                .registerWrapper(FinalClass.class, new FinalClassWrapper()) //register our wrapper
                .build();
        ProxyFactory proxyFactory = ProxyFactory.getInstance(conf);
        MethodInvocationRecorder invocationRecorder = new MethodInvocationRecorder(proxyFactory);
        IFinalClass finalClass = new FinalClass(); 
        IFinalClass proxy = invocationRecorder.record(finalClass); 
        System.out.println(isCglibProxy(proxy));
    }

2. Использовать jmspy-agent.

Jmspy-agent — это простой java agent. Для того, что бы использовать агент, его нужно указать в строке запуска приложения используя параметр -javaagent, например:

-javaagent:{path_to_jar}/jmspy-agent-x.y.z.jar=[parameter]

В качестве параметра задается список классов или пакетов, которые нужно инструментировать. Jmspy-agent изменит классы если нужно: уберет final модификаторы с типов и методов, таким образом сможет создавать прокси без проблем.

JMSpy Viewer Вьювер для просмотра и анализа jmspy снэпшотов.
UI не богатый, но его вполне достаточно для того, что бы получить необходимую информацию, правда, пока есть только сборка для windows. Ниже приведен скриншот основного окна:

image

Документация по вьюверу еще в процессе, но ui простой и интуитивно понятный.

Буду рад, если это статья и сама библиотека окажутся полезными. Хотелось бы услышать ваши комментарии, что бы понять, стоит ли улучшать и развивать библиотеку дальше.

Проект на github.

Спасибо за внимание.

Автор: dmgcodevil

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js