[Из песочницы] JMSpy — шпион за вызовами методов
Здравствуй, Хабрахабр! Хочу рассказать об одной библиотеке, которую я разработал в рамках моего прошлого проекта и так вышло, что она попала в 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:
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 extends Wrapper
@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. Ниже приведен скриншот основного окна:
Документация по вьюверу еще в процессе, но ui простой и интуитивно понятный.
Буду рад, если это статья и сама библиотека окажутся полезными. Хотелось бы услышать ваши комментарии, что бы понять, стоит ли улучшать и развивать библиотеку дальше.
Проект на github.
Спасибо за внимание.