jMint

Turning dirty hacks into charity

Overview

What is jMint?

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.

What is it for?

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:

How does it work?

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.

What is droplet?

Droplet (in the sense of injection) is an ordinary Java source code file that is used by jMint to find out 2 things:

  1. The target of modification - a place in the application where the modification must be applied;
  2. The modifying code itself.

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:

    /**
     * @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.

Download

The latest release alongside with its description is available on the Latest Release page.

Usage

Usage of jMint includes two steps:

  1. Create droplet(s);
  2. Attach jMint to target application for applying created droplet(s).

Step 1: Create droplet

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:

Approach 1. Creating droplet from target class

  1. Make a copy of the target class’s source code file with Droplet or _Droplet suffix (in any case) in the same package, for example com.example.coolapp.TheAppDroplet or com.example.coolapp.TheApp_droplet.
    ​ℹ​ If your target class name already ends with Droplet just add it again. Only the last one will be omitted.
    The suffix is needed to distinguish the original class from droplet. It will be omitted by jMint during parsing the droplet. The target type must not necessarily be a class, it can be an enum or an interface (but not annotation) as well as can be inner type on any level.
    ​💡​ In order to prevent accidental committing of droplet to your Version Control System (VCS) add droplets suffix into the list of exclusions. For instance in Git it can be achieved by adding the following line into .gitignore file: ` *Droplet.java`
  2. Find the target method(s) in the copied class and make sure it have javadoc description (a block comment starting with forward slash and double asterisk (/**) located just before the method definition). If not, add one.
  3. Add custom javadoc tag @cutpoint with one of values: BEFORE, INSTEAD, AFTER. In the simplest case the whole javadoc definition may look like:
    /** @cutpoint INSTEAD */
  4. In the body of target method write (or change) the code you want to be injected according to selected cutpoint.
    ​⚠​ Please remember about some limitations of modifying code (see corresponding section below). All other class members will be ignored by jMint.
  5. Repeat steps 2-4 for all the target methods of this class and then save the droplet anywhere you want.
    ​💡​ Note that in case of creating several droplets for the same purpose you can put them all into single ZIP or JAR archive and then use just like ordinary (separate) droplet.

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. ***

Approach 2. Creating a droplet from scratch

  1. Create a text file with name <TargetClassSimpleName>Droplet.java in any directory.
    ​ℹ​ This name format is just a kind of best practice; you may give the file any name.
  2. Inside the file specify the package of target class just like if you’d create it.
  3. Specify the target class (or enum, or interface) just like if you’d create it.
    Access modifiers as well as extends/implements clauses and annotations make no sense to droplets and thus may be omitted. Arbitrary combinations of inner types are supported by jMint.
  4. Define the target method just like if you’d create it.
    Access modifiers as well as throws clause and annotations make no sense to droplets and thus may be omitted.
  5. Prepend the target method with javadoc description.
    It’s a good practice to write detailed description of modifying method here but the droplet itself requires only one custom javadoc tag – @cutpoint followed by one of values: BEFORE, INSTEAD, AFTER.
  6. Write the body of the method according to selected cutpoint.
    ​⚠​ Please remember about some limitations of modifying code (see corresponding section below).
  7. Repeat steps 4-6 for all the methods you’d like to modify and then save the droplet.
    ​💡​ Note that in case of creating several droplets for the same purpose you can put them all into single ZIP or JAR archive and then use just like ordinary (separate) droplet.

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. ***

Step 2: Attach jMint to the app

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).

Java™ compatibility

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.

Limitations

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:

The reasons explanation is beyond this document; you may see Limitations chapter of Javassist Tutorial for more info.

Logging

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!");
  }

Under the hood

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.

License

jMint is distributed under MIT License (see LICENSE.txt).

Support & feedback

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! 🤗