[Из песочницы] JMSpy — шпион за вызовами методов

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:

com.github.dmgcodevil jmspy-core 1.1.2 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 ()); ContextExplorerContextExplorer это интерфейс, реализации которого должны предоставлять информацию о контексте выполнения. 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 {

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> 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.

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

© Habrahabr.ru