JMSpy - Method Call Spy

Bild

Hallo Habrahabr! Ich möchte über eine Bibliothek sprechen, die ich im Rahmen meines letzten Projekts entwickelt habe, und so kam es, dass sie in OpenSource aufgenommen wurde.

Zunächst möchte ich ein paar Worte darüber sagen, warum diese Bibliothek benötigt wurde. Innerhalb des Projekts musste ich mit einer komplexen bidirektionalen baumartigen Domänenstruktur arbeiten, d. H. In der Grafik der Objekte können Sie von oben nach unten (von Eltern zu Kindern) und umgekehrt gehen. Daher erwiesen sich die Objekte als voluminös. Wir haben MongoDB als Speicher verwendet, und da die Objekte umfangreich waren, haben einige von ihnen die maximale Größe eines MongoDB-Dokuments überschritten. Um dieses Problem zu lösen, haben wir das zusammengesetzte Objekt in verschiedene Sammlungen aufgeteilt (obwohl es in MongoDB besser ist, alles mit festen Dokumenten zu speichern). Daher wurden die untergeordneten Objekte in separaten Sammlungen gespeichert, und das übergeordnete Dokument enthielt Verweise auf sie. Mit diesem Ansatz haben wir den Lazy-Loading-Mechanismus implementiert. Das heißt, das Stammobjekt wurde nicht mit allen verschachtelten Objekten geladen, sondern nur mit der obersten Ebene. Die untergeordneten Objekte wurden bei Bedarf geladen. Das Repository, das das Hauptobjekt angegeben hat, wurde in benutzerdefinierten Tags (Java Custom Tag) und Tags auf FTL-Seiten verwendet. Während der Leistungstests haben wir festgestellt, dass auf den Seiten viele Aufrufe zum verzögerten Laden vorhanden sind. Sie begannen die Seiten zu überarbeiten und fanden suboptimale Aufrufe des Formulars:

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

getObjectA () führt zum Laden eines Objekts aus einer anderen Sammlung, genau wie bei getObjectB (). Da es in rootObject jedoch ein objectBName-Feld gibt, kann die obige Zeile wie folgt umgeschrieben werden:

rootObject.getObjectBName()

Dieser Ansatz lädt keine untergeordneten Objekte und arbeitet viel schneller.

Es stellte sich die Frage: " Wie finde ich alle Seiten, auf denen es solche suboptimalen Aufrufe gibt, und eliminiere sie? ". Bei einer einfachen Codesuche dauerte dies lange und wir entschieden uns, so etwas wie einen Debug-Modus zu implementieren. Wir aktivieren den Debug-Modus, führen UI-Tests aus und erhalten am Ende Informationen darüber, welche Methoden unseres übergeordneten Objekts wo aufgerufen wurden. So entstand die Idee, JMSpy zu erstellen.

Die Bibliothek ist in Maven Central verfügbar. Sie müssen also nur die Abhängigkeit in Ihrem Build-Tool angeben.
Beispiel für Maven:

com.github.dmgcodeviljmspy-core1.1.2

jmspy-core ist ein Modul, das die Hauptfunktionen der Bibliothek enthält. Es gibt auch jmspy-agent und jmspy-ext-freemarker, aber dazu später mehr. Mit JMspy können Sie Anrufe von Verschachtelungen aufzeichnen, zum Beispiel:

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

Betrachten Sie zunächst die Hauptkomponenten der Bibliothek und ihren Zweck.

MethodInvocationRecorder ist die Hauptklasse, mit der der Endbenutzer interagiert.
ProxyFactory ist eine Factory, die cglib zum Erstellen von Proxys verwendet. ProxyFactory ist ein Singleton, der die Konfiguration als Parameter verwendet, sodass Sie die Fabriken so konfigurieren können, dass sie Ihren Anforderungen entsprechen. Weitere Informationen hierzu finden Sie weiter unten.
ContextExplorer - Eine Schnittstelle, die Methoden zum Abrufen von Informationen über den Kontext der Ausführung einer Methode bereitstellt. Zum Beispiel ist jmspy-ext-freemarker eine Implementierung von ContextExplorer , um Informationen über die Seite abzurufen, auf der die Methode des Objekts aufgerufen wurde (bean'a oder pojo, wie Sie es bevorzugen).

ProxyFactory

Mit Factor können Sie Proxys für Objekte erstellen. Es ist möglich, einen Faktor zu konfigurieren. Dies kann bei komplexen Fällen hilfreich sein, obwohl die Standardeinstellungen für einfache Objekte ausreichen sollten. Um eine Instanz einer Factory zu erstellen, müssen Sie die Methode getInstance verwenden und die Konfigurationsinstanz dort übergeben, beispielsweise wie folgt:

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 ist eine Schnittstelle, deren Implementierungen Informationen zum Ausführungskontext bereitstellen müssen. Jmspy bietet eine vorgefertigte Implementierung für Freemarker (FreemarkerContextExplorer), die mit einem separaten jms-Modul jmspy-ext-freemarker geliefert wird. Diese Implementierung enthält Informationen zur Seite, Anforderungsadresse usw. Sie können Ihre Implementierung erstellen und bei MethodInvocationRecorder registrieren. Sie können nur eine Implementierung für MethodInvocationRecorder registrieren. Die ContextExplorer-Oberfläche enthält zwei Methoden. Nachfolgend finden Sie einige Informationen zu jeder dieser Methoden.

getRootContextInfo- Gibt grundlegende Informationen zum Kontext des Aufrufs zurück, z. B. die Root-Methode, den Anwendungsnamen, Anforderungsinformationen, die URL usw. Diese Methode wird unmittelbar nach dem Erstellen des InvocationRecord aufgerufen, d. H. unmittelbar nach dem Methodenaufruf

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

oder

MethodInvocationRecorder#record(Object)} 

getCurrentContextInfo - bietet detailliertere Informationen wie FTL-Seitenname, JSP usw. Diese Methode wird aufgerufen, wenn eine Methode für ein Objekt aufgerufen wurde, das aus dem Datensatz MethodInvocationRecorder # abgerufen wurde. Beispiel:

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

MethodInvocationRecorder

Wie Sie vielleicht vermutet haben, ist dies die Hauptklasse, mit der Sie arbeiten müssen. Seine Hauptfunktion besteht darin, den Prozess des Ausspionierens von Methodenaufrufen zu starten. MethodInvocationRecorder bietet Konstruktoren, an die Sie eine Instanz von ProxyFactory und ContextExplorer übergeben können.
In dieser Klasse gibt es eine weitere wichtige Methode: makeSnapshot (). Diese Methode speichert das aktuelle Anrufdiagramm für eine spätere Analyse mit jmspy-viewer.

Einschränkungen
Da die Bibliothek CGLIB zum Erstellen eines Proxys verwendet, gibt es eine Reihe von Einschränkungen, die sich aus der Natur von CGLIB ergeben. Es ist bekannt, dass CGLIB Vererbung verwendet und Proxys für Typen erstellen kann, die keine Schnittstellen implementieren. Das heißt, CGLIB erbt die generierte Proxy-Klasse vom Zieltyp des Objekts, für das der Proxy erstellt wird. Java

unterliegt einer Reihe von Einschränkungen für den Vererbungsmechanismus: 1. CGLIB kann keine Proxys für endgültige Klassen erstellen, da endgültige Klassen nicht vererbt werden können.
2. endgültige Methoden können nicht abgefangen werden, da die geerbte Klasse die endgültige Methode nicht überschreiben kann.
Um diese Einschränkungen zu umgehen, können Sie zwei Ansätze verwenden:

1. Erstellen Sie einen Wrapper für die Klasse(Funktioniert nur, wenn Ihre Klasse eine bestimmte Schnittstelle implementiert, mit der Sie arbeiten.)
Beispiel:

Schnittstelle
public interface IFinalClass {
    String getId();
}

Note:
public final class FinalClass implements IFinalClass {
    private String id;
    public String getId() {
        return id;
    }
    public void setId(String id) {
        this.id = id;
    }
}

Erstellen Sie einen 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();
    }
}

Jetzt müssen Sie den FinalClassWrapper-Wrapper mit der registerWrapper-Methode registrieren.

    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. Verwenden Sie jmspy-agent .

Jmspy-Agent ist ein einfacher Java-Agent. Um den Agenten verwenden zu können, muss er in der Anwendungsstartzeile mit dem Parameter -javaagent angegeben werden. Beispiel:

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

Als Parameter wird eine Liste der zu instrumentierenden Klassen oder Pakete angegeben. Jmspy-agent ändert die Klassen bei Bedarf: Entfernen Sie die endgültigen Modifikatoren aus den Typen und Methoden, damit Sie problemlos Proxys erstellen können.

JMSpy Viewer Viewer zum Anzeigen und Analysieren von jmspy-Snapshots.
Die Benutzeroberfläche ist nicht reichhaltig, aber es reicht völlig aus, um die erforderlichen Informationen abzurufen, während es nur eine Assembly für Windows gibt. Unten sehen Sie einen Screenshot des Hauptfensters: Die

Bild

Viewer-Dokumentation ist noch in Bearbeitung, aber die Benutzeroberfläche ist einfach und intuitiv.

Ich würde mich freuen, wenn dieser Artikel und die Bibliothek selbst nützlich sind. Ich würde gerne Ihre Kommentare hören, um zu verstehen, ob es sich lohnt, die Bibliothek zu verbessern und weiterzuentwickeln.

Projekt auf Github .

Vielen Dank für Ihre Aufmerksamkeit.

Jetzt auch beliebt: