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