JMSpy - Method Call Spy

image

Hello, Habrahabr! I want to talk about one library that I developed as part of my last project and it so happened that it got into OpenSource.

First, I’ll say a few words about why there was a need for this library. Within the project, I had to work with a complex domain bidirectional tree-like structure, i.e. on the graph of objects you can walk from top to bottom (from parent to child) and vice versa. Therefore, the objects turned out to be voluminous. We used MongoDB as storage, and since the objects were voluminous, some of them exceeded the maximum size of a MongoDB document. In order to solve this problem, we split the composite object into different collections (although in MongoDB it is better to store everything with solid documents). Thus, the child objects were stored in separate collections, and the document that was the parent contained references to them. Using this approach, we implemented the lazy loading mechanism. That is, the root object was not loaded with all nested objects, but only with the top-level, its children were loaded on demand. The repository that gave the main object was used in custom tags (Java Custom Tag), and tags, in turn, on FTL pages. During performance testing, we noticed that there are a lot of lazy-load calls on the pages. They began to revise the pages and found suboptimal calls of the form:

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

getObjectA () results in loading an object from another collection, the same situation with getObjectB (). But since there is an objectBName field in rootObject, the line above can be rewritten as follows:

rootObject.getObjectBName()

this approach does not load child objects and works much faster.

The question arose: " How to find all the pages where there are such suboptimal calls, and eliminate them? ". With a simple code search, this took a long time and we decided to implement something like a debug mode. We turn on debug mode, run UI tests, and at the end we get information about which methods of our parent object were called and where. So the idea of ​​creating JMSpy came up.

The library is available in maven central, so all you need to do is specify the dependency in your build tool.
Example for maven:

com.github.dmgcodeviljmspy-core1.1.2

jmspy-core is a module that contains the main features of the library. There is also jmspy-agent and jmspy-ext-freemarker, but more on that later. JMspy allows you to record calls of any nesting, for example:

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

To begin, consider the main components of the library and their purpose.

MethodInvocationRecorder is the main class that the end user interacts with.
ProxyFactory is a factory that uses cglib to create proxies. ProxyFactory is a singleton that takes Configuration as a parameter, so you can configure the factories to fit your needs, more on that below.
ContextExplorer - an interface that provides methods for obtaining information about the context of the execution of a method. For example, jmspy-ext-freemarker is an implementation of ContextExplorer in order to receive information about the page on which the method of the object was called (bean'a or pojo, as you prefer).

ProxyFactory

Factor allows you to create proxies for objects. It is possible to configure a factor, this can be useful in the case of complex cases, although default settings should be enough for simple objects. In order to create an instance of a factory, you need to use the getInstance method and pass the Configiration instance there, for example like this:

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 is an interface whose implementations must provide information about the execution context. Jmspy provides a ready-made implementation for Freemarker (FreemarkerContextExplorer), which comes with a separate jms module jmspy-ext-freemarker. This implementation provides information about the page, request address, etc. You can create your implementation and register it with MethodInvocationRecorder. You can register only one implementation for MethodInvocationRecorder. The ContextExplorer interface contains two methods, below is a little about each of them.

getRootContextInfo- returns basic information about the context of the call, such as the root method, application name, request information, url, etc. This method is called immediately after the InvocationRecord is created, i.e. immediately after the method call

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

or

MethodInvocationRecorder#record(Object)} 

getCurrentContextInfo - provides more detailed information such as FTL page name, JSP, etc. This method is called when a method was called on an object obtained from MethodInvocationRecorder # record , for example:

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

MethodInvocationRecorder

As you may have guessed, this is the main class that you will have to work with. Its main function is to start the process of spying on method calls. MethodInvocationRecorder provides constructors to which you can pass an instance of ProxyFactory and ContextExplorer.
There is another important method in this class: makeSnapshot (). This method saves the current call graph for later analysis using jmspy-viewer.

Limitations
Since the library uses CGLIB to create a proxy, it has a number of limitations that come from the nature of CGLIB. It is known that CGLIB uses inheritance and can create proxies for types that do not implement any interfaces. Those. CGLIB inherits the generated proxy class from the target type of the object for which the proxy is being created. Java has a number of some restrictions provided to the inheritance mechanism, namely:

1. CGLIB cannot create proxies for final classes, since final classes cannot be inherited;
2. final methods cannot be intercepted, since the inherited class cannot override the final method.
In order to get around these restrictions, you can use two approaches:

1. Create a wrapper for the class(only works if your class implements a certain interface that you are working with)
Example:

Interface
public interface IFinalClass {
    String getId();
}

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

Create a 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();
    }
}

Now you need to register the FinalClassWrapper wrapper using the registerWrapper method.

    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. Use jmspy-agent .

Jmspy-agent is a simple java agent. In order to use the agent, it must be specified in the application launch line using the -javaagent parameter, for example:

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

As a parameter, a list of classes or packages to be instrumented is specified. Jmspy-agent will change the classes if necessary: ​​remove final modifiers from types and methods, thus being able to create proxies without problems.

JMSpy Viewer Viewer for viewing and analyzing jmspy snapshots.
The UI is not rich, but it is quite enough to get the necessary information, however, while there is only an assembly for windows. Below is a screenshot of the main window:

image

Viewer documentation is still in process, but ui is simple and intuitive.

I would be glad if this article and the library itself are useful. I would like to hear your comments in order to understand whether it is worth improving and developing the library further.

Project on github .

Thanks for attention.

Also popular now: