Turning dirty hacks into charity
jMint is a tool for modifying methods of a running Java™ application without changing its source code. jMint key features are:
ℹ In a nutshell jMint is a wrapper around Javassist byte code manipulation library. The latter is used to compile source code and modify byte code whilst jMint itself exposes developer-friendly interface and preprocesses the input data.
⚠ jMint is not a hacking tool and therefore doesn’t contain any facilities to break the protection of classes (if any). It also knows nothing about legal aspects so that before deploying a modified application or library please make sure you do not violate its license.
Typical use cases of jMint include (but not restricted to) testing stage when some custom behavior should not (or even can not) be included into the source code. For example:
jMint operates as Java agent – special kind of application that is launched by (usually HotSpotJVM and is able to modify byte code of classes being loaded by JVM.
Droplet (in the sense of injection) is an ordinary Java source code file that is used by jMint to find out 2 things:
Droplets look like all other Java type definitions (classes, interfaces, enums) but in fact they are not. Their classes’ and methods’ signatures do not define anything new, instead they just specify the target of modification.
💡 Example. If you have class com.example.coolapp.Saver
with method private void save(BusinessEntity entity)
and you want to prepend it with dumping of argument to standard output then your droplet may look like this (Saver.java):
package com.example.coolapp;
import com.example.coolapp.model.BusinessEntity;
class Saver {
/** @cutpoint BEFORE */
void save(BusinessEntity entity) {
System.out.println("Entity to save: " + entity.toString());
}
}
This droplet doesn’t define a new method save
. Instead it instructs jMint to find method com.example.coolapp.Saver#save(com.example.coolapp.model.BusinessEntity)
during class loading and to inject the specified code right before the body of the method.
The place of insertion relative to target method body is called cutpoint and is specified via custom javadoc tag @cutpoint
which can take one of 3 values:
BEFORE
to inject the code right before the target method body.INSTEAD
to completely replace the target method body with a new one.AFTER
to append some code to the end of the target method body.AFTER
cutpoint there is synthetic variable $_
available which holds current result value. If the result type is void
, then the type of $_
is Object
and the value of $_
is null
. There is also $r
variable which represents the result type (return type) of the method. It is intended to be used as the cast type in a cast expression.AFTER
cutpoint behaves like ordinary code appended to the end of target method body. This means that in case of exception such code won’t be executed. If you want it to be executed anyway (like finally
block) just add asFinally
parameter right after the cutpoint declaration: /**
* @cutpoint AFTER AS_FINALLY
*/
public void actionPerformed(ActionEvent event) {
Note that the parameter may be specified in various forms: asFinally
, AS_FINALLY
, as-finally
or even as finally
.
💡 There is also auxiliary IGNORE
cutpoint which is applied by default to all methods with no explicit cutpoint tag. This cutpoint may be applied explicitly to the methods left in the droplet in order to maintain its semantic correctness.
🚧 Dedicated CATCH
cutpoint for certain exceptions is planned to be implemented in one of upcoming releases. Please feel free to send feedback (via email or issues) if you’d like it to be released sooner.
For more info on droplets see Usage section.
The latest release alongside with its description is available on the Latest Release page.
Usage of jMint includes two steps:
There are 2 general approaches to create a droplet: from source code of target class and from scratch. You are free to choose any of them. Here are some hints that may help:
Droplet
or _Droplet
suffix (in any case) in the same package, for example com.example.coolapp.TheAppDroplet
or com.example.coolapp.TheApp_droplet
.Droplet
just add it again. Only the last one will be omitted..gitignore
file:
` *Droplet.java`/**
) located just before the method definition). If not, add one.@cutpoint
with one of values: BEFORE
, INSTEAD
, AFTER
.
In the simplest case the whole javadoc definition may look like:/** @cutpoint INSTEAD */
Example. Here’s a sample droplet created from copy of its target class (FooDroplet.java):
package com.example.coolapp;
import com.example.coolapp.model.*;
import java.lang.util.*;
import com.example.coolapp.util.Monitored;
@Monitored
public class FooDroplet extends FooBase implements Fooable {
private static final Logger log = LoggerFactory.getLogger(Foo.class);
/* other fields left from original class */
/**
* Performs fooing with given entity.
*
* @param entity an entity to fooify
* @cutpoint BEFORE
*/
@Override
public void fooify(BusinessEntity entity) throws Exception {
System.out.println("Entity to fooify: " + entity.toString());
}
/* other constructors and methods left from original class */
}
As you can see there is plenty of code that doesn’t concerns the droplet. Compare it with code from the next approach. ***
<TargetClassSimpleName>Droplet.java
in any directory.extends
/implements
clauses and annotations make no sense to droplets and thus may be omitted. Arbitrary combinations of inner types are supported by jMint.
throws
clause and annotations make no sense to droplets and thus may be omitted.
@cutpoint
followed by one of values: BEFORE
, INSTEAD
, AFTER
.
Example. Here’s a sample droplet created from scratch (FooDroplet.java):
package com.example.coolapp;
import com.example.coolapp.model.BusinessEntity;
class FooDroplet {
/** @cutpoint BEFORE */
void fooify(BusinessEntity entity) {
System.out.println("Entity to fooify: " + entity.toString());
}
}
Writing such droplet might took some time but it is free of redundancy inherent to the first approach. ***
Because jMint ships as Java agent, it is attached to JVM through its startup argument -javaagent
. The absolute or relative path to jMint jar is specified after colon (:
) following the argument name. Then, followed by equal sign goes a list of droplets paths separated by semicolon (;
). As a whole the command line may look like:
java -javaagent:path/to/jmint.jar=a/long/way/to/droplets/FirstDroplet.java;a/long/way/to/droplets/SecondDroplet.java com.example.coolapp.Main
💡 To shorten the record you may introduce a couple of variables in the launch script to hold the prefix paths:
JMINT_PATH=path/to/jmint.jar
DROPLETS_HOME=a/long/way/to/droplets
java -javaagent:$JMINT_PATH=$DROPLETS_HOME/FirstDroplet.java;$DROPLETS_HOME/SecondDroplet.java com.example.coolapp.Main
or
JMINT=path/to/jmint.jar
DROPLETS=a/long/way/to/droplets/FirstDroplet.java
DROPLETS=$DROPLETS;a/long/way/to/droplets/SecondDroplet.java
java -javaagent:$JMINT=$DROPLETS com.example.coolapp.Main
Started with such arguments JVM will launch jMint and let it modify byte code of classes being loaded.
⚠ Note that being unable to load an agent JVM will not start at all.
ℹ javaagent
is not singleton option for JVM. You may add as many agents as you want declaring them as separate javaagent
arguments on the JVM launch command.
To ensure that your target methods have been modified correctly look for messages from class tech.toparvion.jmint.DropletsInjector
in the log (see Logging section).
While jMint itself builds on JDK 8, its source code and compilation target are both set to JDK 6. This is to make jMint compatible with legacy applications where Side Effect Injection approach is often the only solution.
Nonetheless, you can use jMint to instrument bytecode of modern applications including those on JDK 11 and higher.
Unfortunately, source code of droplets’ methods (the modifying code) can not be as rich and diverse as usual one. The modifying code must not contain:
T <T> nvl(T t)
);java.lang.Double.*
; it should be replaced with separate single imports);The reasons explanation is beyond this document; you may see Limitations chapter of Javassist Tutorial for more info.
jMint emits some log messages about its work using Java Util Logging (JUL) façade. By default, all log messages are directed to error output, but this can be changed by leveraging either JUL configuration or a bridge to another logging system, for example jul-to-slf4j bridge.
Here’s some sample log output emitted by jMint during its initialization and injection stages:
... (at the start of JVM) ...
[main] INFO tech.toparvion.jmint.JMintAgent - jMint started (version: 1.5-beta).
...
[main] INFO tech.toparvion.jmint.JMintAgent - Droplets loading took: 1167 ms
... (later, at runtime) ...
[main] INFO tech.toparvion.jmint.DropletsInjector - Method 'sampleapp.standalone.painter.Painter.buildContent()' has been modified at AFTER.
...
[main] INFO tech.toparvion.jmint.DropletsInjector - Method 'sampleapp.standalone.painter.Painter#main' is skipped due to IGNORE.
💡 Also note that you can use target application’s logging system right from your droplets to emit log messages on behalf of modified class. For example, if class to modify has SLF4J logger attached like:
private static final Logger log = LoggerFactory.getLogger(MyClass.class);
then your droplet may use it to emit its own messages like:
/**
* @cutpoint BEFORE
*/
public void checkAuthorized() {
log.warn("DROPLET: checkAuthorized method is stubbed, no actual check performed!");
}
jMint is built upon three great tools: Java Byte Code Instrumentation API, Javassist byte code manipulating library and ANTLR4 language recognition tool. The latter (created by professor of genius Terence Parr) is used by jMint during startup to parse droplets and extract all the required information from them. Then with the help of Instrumentation API jMint registers itself as an interceptor for all the class loadings happening in JVM. When loading of some target class is detected jMint transforms its byte code by means of Javassist library (created by incredibly talented Shigeru Chiba) and returns it back to JVM.
jMint is distributed under MIT License (see LICENSE.txt).
jMint is being developed by single person (@Toparvion) in spare time as a helpful tool for day-to-day tasks. It is not considered feature-completed yet so that new features (alongside with bug fixes) are expected in the foreseeable future. The priorities in choosing features to implement (and bugs to fix) depend heavily on the feedback going from the tool’s users. You’re welcome to post issues or contact the author directly: toparvion[at]gmx[dot]com
.
Development contributions as well as testing assistance are also extremely appreciated! 🤗