Hello, Habrahabr! I want to talk about one library that I developed as part of my previous project and it turned out that it got into OpenSource.
To begin with, I will say a few words about why the need for this library has appeared. The project had to work with a complex domain bidirectional tree-like structure, i.e. the objects graph can be walked 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 the MongoDB document. In order to solve this problem, we smashed the composite object across different collections (although in MongoDB it’s 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 a lazy loading mechanism (lazy loading). That is, the root object was not loaded with all the nested objects, but only with top-level, its child elements were loaded on demand. The repository, which gave the main object, was used in custom tags (Java Custom Tag), and the tags in turn on FTL pages. During performance testing, we noticed that there are many lazy-load calls on the pages. Began to revise the pages and found non-optimal calls of the form:
rootObject.getObjectA().getObjectB().getName()
getObjectA () results in loading an object from another collection, the same situation with getObjectB (). But since the rootObject has an objectBName field, the string above can be rewritten as follows:
rootObject.getObjectBName()
This approach does not lead to the loading of child objects and works much faster.
')
There was a question: "
How to find all the pages where there are such non-optimal calls and eliminate them? ". A simple search by code took a lot of time and we decided to implement something like 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. And so the idea of creating JMSpy.
The library is available in maven central, so all you need is to specify the dependency in your build tool.
Example for maven:
<dependency> <groupId>com.github.dmgcodevil</groupId> <artifactId>jmspy-core</artifactId> <version>1.1.2</version> </dependency>
jmspy-core is a module that contains the main features of the library. There is also a 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 with which the end user interacts.
ProxyFactory is a factor that uses cglib to create a proxy. ProxyFactory is a singleton that takes Configuration as a parameter, so you can customize the factory to your needs, see below.
ContextExplorer is an interface that provides methods for obtaining information about the context of the execution of a method. For example,
jmspy-ext-freemarker is the implementation of
ContextExplorer in order to receive information about the page on which the object method was called (bean'a or pojo, as you prefer)
ProxyFactoryFactory allows you to create proxies for objects. There is an opportunity for configuration of factories, this can be useful in the case of complex cases, although it should be enough for simple objects of the default configuration. In order to create an instance of the factory, you need to use the getInstance method and pass the Configiration instance to it, for example:
Configuration.Builder builder = Configuration.builder() .ignoreType(DataLoader.class)
ContextExplorerContextExplorer is an interface whose implementations must provide information about the execution context. Jmspy provides a ready implementation for Freemarker (FreemarkerContextExplorer), which is supplied by a separate jar module jmspy-ext-freemarker. This implementation provides information about the page, address of the request, etc. You can create your own implementation and register it in 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 call context, 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 calling the method
MethodInvocationRecorder#record(java.lang.reflect.Method, Object)}
or
MethodInvocationRecorder#record(Object)}
getCurrentContextInfo - provides more detailed information, such as the name of the FTL page, JSP, etc. This method is called when a method has been called on an object obtained from
MethodInvocationRecorder # record , for example:
User user = new User(); MethodInvocationRecorder methodInvocationRecorder = new MethodInvocationRecorder(); MethodInvocationRecorder.record(user).getName();
MethodInvocationRecorderAs you may have guessed, this is the main class with which to work. Its main function is to start the process of spying on calls to methods. 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 the jmspy-viewer.
RestrictionsSince the library uses CGLIB to create proxies, it has a number of limitations that are based on 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 object type for which the proxy is created. Java has a number of some limitations provided to the inheritance mechanism, namely:
1. CGLIB cannot create proxies for final classes, as final classes cannot be inherited;
2. final methods cannot be intercepted, since the inherited class cannot override the final method.
In order to circumvent these restrictions, you can use two approaches:
1. Create a wrapper for the class (works only if your class implements a certain interface with which you work)
Example:
Interface
public interface IFinalClass { String getId(); }
Class:
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<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(); } }
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())
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-xyzjar=[parameter]
The parameter is a list of classes or packages that need to be instructed. Jmspy-agent will change classes if needed: remove final modifiers from types and methods, thus it can create a proxy without problems.
JMSpy Viewer Viewer for viewing and analyzing jmspy snapshots.
UI is not rich, but it is quite enough to get the necessary information, however, as long as there is only an assembly for windows. Below is a screenshot of the main window:

Documentation for the viewer is still in process, but ui is simple and intuitive.
I would be glad if this article and the library itself will be 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.