Skip to content

Commit

Permalink
feat: add support for transformation at load-time in the JVM (#2645)
Browse files Browse the repository at this point in the history
  • Loading branch information
nharrand authored and monperrus committed Oct 16, 2018
1 parent e2af5a5 commit d9c26c6
Show file tree
Hide file tree
Showing 6 changed files with 439 additions and 0 deletions.
56 changes: 56 additions & 0 deletions doc/agent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
---
title: Agent
tags: [usage]
keywords: agent, usage, java, loadtime
---

# Spoon Agent

Spoon can also be used to transform classes at load time in the JVM. FOr this, `SpoonClassFileTransformer` provide an abstraction of `ClassFileTransformer`
where the user can define Spoon transformation.
Bytecode of classes will be decompiled on-the-fly when loaded, and the Spoon AST will be updated in consequence, and the code is recompiled on-the-fly.

The following example shows the definition of a basic JVM agent for inserting a tracing method call a the end of every method called `foo`.

Here is the agent:
```java
public class Agent {
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println( "Hello Agent" );

//Create a SpoonClassFileTransformer, that
// * excludes any classes not in our package from decompilation
// * adds the statement System.out.println("Hello <className>"); to the (first) method named "foo" of every classes
SpoonClassFileTransformer transformer = new SpoonClassFileTransformer(
cl -> cl.startsWith("org/my/package"),
new InsertPrintTransformer()
);
inst.addTransformer(transformer);

System.out.println( "Agent Done." );
}
}
```

```java
public class InsertPrintTransformer implements TypeTransformer {

@Override
public boolean accept(CtType type) {
if ((type instanceof CtClass) &&
type.getMethodsByName("foo").size() > 0) {
return true;
} else {
return false;
}
}

@Override
public void transform(CtType type) {
System.err.println("Transforming " + type.getQualifiedName());
CtMethod main = (CtMethod) type.getMethodsByName("foo").get(0);
main.getBody().addStatement(type.getFactory().createCodeSnippetStatement("System.out.println(\"Hello " + type.getQualifiedName() + "\");"));
System.err.println("Done transforming " + type.getQualifiedName());
}
}
```
58 changes: 58 additions & 0 deletions src/main/java/spoon/decompiler/MultiTypeTransformer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
* Copyright (C) 2006-2018 INRIA and contributors
* Spoon - http://spoon.gforge.inria.fr/
*
* This software is governed by the CeCILL-C License under French law and
* abiding by the rules of distribution of free software. You can use, modify
* and/or redistribute the software under the terms of the CeCILL-C license as
* circulated by CEA, CNRS and INRIA at http://www.cecill.info.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the CeCILL-C License for more details.
*
* The fact that you are presently reading this means that you have had
* knowledge of the CeCILL-C license and that you accept its terms.
*/
package spoon.decompiler;

import spoon.reflect.declaration.CtType;

import java.util.Collection;
import java.util.LinkedHashSet;

public class MultiTypeTransformer implements TypeTransformer {

protected LinkedHashSet<TypeTransformer> transformers;

public MultiTypeTransformer() {
transformers = new LinkedHashSet<>();
}

public void addTransformer(TypeTransformer transformer) {
transformers.add(transformer);
}

public void addTransformers(Collection<TypeTransformer> transformers) {
this.transformers.addAll(transformers);
}

@Override
public void transform(CtType type) {
for (TypeTransformer transformer: transformers) {
if (transformer.accept(type)) {
transformer.transform(type);
}
}
}

@Override
public boolean accept(CtType type) {
for (TypeTransformer transformer: transformers) {
if (transformer.accept(type)) {
return true;
}
}
return false;
}
}
185 changes: 185 additions & 0 deletions src/main/java/spoon/decompiler/SpoonClassFileTransformer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
/**
* Copyright (C) 2006-2018 INRIA and contributors
* Spoon - http://spoon.gforge.inria.fr/
*
* This software is governed by the CeCILL-C License under French law and
* abiding by the rules of distribution of free software. You can use, modify
* and/or redistribute the software under the terms of the CeCILL-C license as
* circulated by CEA, CNRS and INRIA at http://www.cecill.info.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the CeCILL-C License for more details.
*
* The fact that you are presently reading this means that you have had
* knowledge of the CeCILL-C license and that you accept its terms.
*/
package spoon.decompiler;

import org.benf.cfr.reader.Main;
import spoon.IncrementalLauncher;
import spoon.SpoonModelBuilder;
import spoon.reflect.CtModel;
import spoon.reflect.declaration.CtType;
import spoon.support.Experimental;

import java.io.File;
import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.nio.file.Files;
import java.security.ProtectionDomain;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Predicate;

@Experimental
public class SpoonClassFileTransformer implements ClassFileTransformer {

protected String pathToDecompiled;
//protected String pathToRecompile;
/*protected String pathToCache;*/

//Field filled by Constructor
protected File cache;
protected File recompileDir;
protected Set<String> classPath;
protected Set<File> inputSources;

protected TypeTransformer transformer;

protected Decompiler decompiler;

//Classes to exclude from decompilation
protected Predicate<String> classNameFilter;
//Exclude jvm classes
public static final Predicate<String> defaultFilter = s -> !(s.startsWith("java") || s.startsWith("sun"));


/**
* Default Constructor for SpoonClassFileTransformer
*
* @param typeTransformer Transformation to apply on loaded types.
*/
public SpoonClassFileTransformer(TypeTransformer typeTransformer) {
this(defaultFilter, typeTransformer, "spoon-decompiled", "spoon-cache", "spoon-recompiled", null);
}


/**
* Default Constructor for SpoonClassFileTransformer
*
* @param classNameFilter Filter for classname. If classeNameFilter.test(className) returns false,
* the class will be loaded without decompilation nor transformation.
* If null, a default filter will filter out typical jvm classes (starting with java* or sun*)
* Note @{SpoonClassFileTransformer.defaultFilter} may be used in conjunction of custom filter
* with `defaultFilter.and(classNameFilter)`.
* @param typeTransformer Transformation to apply on loaded types.
*/
public SpoonClassFileTransformer(Predicate<String> classNameFilter, TypeTransformer typeTransformer) {
this(classNameFilter, typeTransformer, "spoon-decompiled", "spoon-cache", "spoon-recompiled", null);
}

/**
* Default Constructor for SpoonClassFileTransformer
*
* @param classNameFilter Filter for classname. If classeNameFilter.test(className) returns false,
* the class will be loaded without decompilation nor transformation.
* If null, a default filter will filter out typical jvm classes (starting with java* or sun*)
* Note @{SpoonClassFileTransformer.defaultFilter} may be used in conjunction of custom filter
* with `defaultFilter.and(classNameFilter)`.
* @param typeTransformer Transformation to apply on loaded types.
* @param pathToDecompiled path to directory in which to put decompiled sources.
* @param pathToCache path to cache directory for IncrementalLauncher
* @param pathToRecompile path to recompiled classes
* @param decompiler Decompiler to use on classFile before building Spoon model. If null, default compiler (cfr) will be used.
*/
public SpoonClassFileTransformer(Predicate<String> classNameFilter,
TypeTransformer typeTransformer,
String pathToDecompiled,
String pathToCache,
String pathToRecompile,
Decompiler decompiler) {
if (classNameFilter == null) {
this.classNameFilter = defaultFilter;
} else {
this.classNameFilter = classNameFilter;
}

String classPathAr[] = System.getProperty("java.class.path").split(":");
classPath = new HashSet<>(Arrays.asList(classPathAr));
this.pathToDecompiled = pathToDecompiled;
recompileDir = new File(pathToRecompile);
cache = new File(pathToCache);

inputSources = new HashSet<>();
inputSources.add(new File(pathToDecompiled));

this.transformer = typeTransformer;

if (decompiler == null) {
this.decompiler = s -> Main.main(new String[]{s, "--outputdir", pathToDecompiled});
} else {
this.decompiler = decompiler;
}
}

@Override
public byte[] transform(
ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer
) throws IllegalClassFormatException {
try {

//If the class is not matched by user's filter, resume unmodified loading
if (!classNameFilter.test(className)) {
return classfileBuffer;
}

//Decompile classfile
String pathToClassFile = loader.getResource(className + ".class").getPath();
decompiler.decompile(pathToClassFile);

IncrementalLauncher launcher = new IncrementalLauncher(inputSources, classPath, cache);
launcher.addInputResource(pathToDecompiled);

//Get updated model
CtModel model = launcher.buildModel();
launcher.saveCache();

//Get class model
CtType toBeTransformed = model.getAllTypes().stream().filter(t -> t.getQualifiedName().equals(className.replace("/", "."))).findAny().get();

//If the class model is not modified by user, resume unmodified loading
if (!transformer.accept(toBeTransformed)) {
return classfileBuffer;
}
launcher.getEnvironment().debugMessage("[Agent] transforming " + className);
transformer.transform(toBeTransformed);

//Compile new class model
SpoonModelBuilder compiler = launcher.createCompiler();
compiler.setBinaryOutputDirectory(recompileDir);
compiler.compile(SpoonModelBuilder.InputType.CTTYPES);


File transformedClass = new File(compiler.getBinaryOutputDirectory(), className + ".class");
try {
//Load Modified classFile
byte[] fileContent = Files.readAllBytes(transformedClass.toPath());
launcher.getEnvironment().debugMessage("[Agent] loading transformed " + className);
return fileContent;
} catch (IOException e) {
launcher.getEnvironment().debugMessage("[ERROR][Agent] while loading transformed " + className);
e.printStackTrace();
}
} catch (Exception e) {
e.printStackTrace();
}
return classfileBuffer;
}
}
36 changes: 36 additions & 0 deletions src/main/java/spoon/decompiler/TypeTransformer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* Copyright (C) 2006-2018 INRIA and contributors
* Spoon - http://spoon.gforge.inria.fr/
*
* This software is governed by the CeCILL-C License under French law and
* abiding by the rules of distribution of free software. You can use, modify
* and/or redistribute the software under the terms of the CeCILL-C license as
* circulated by CEA, CNRS and INRIA at http://www.cecill.info.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the CeCILL-C License for more details.
*
* The fact that you are presently reading this means that you have had
* knowledge of the CeCILL-C license and that you accept its terms.
*/
package spoon.decompiler;

import spoon.reflect.declaration.CtType;

public interface TypeTransformer {

/**
* User's implementation of transformation to apply on type.
* @param type type to be transformed
*/
void transform(CtType type);

/**
* User defined filter to discard type that will not be transformed by the SpoonClassFileTransformer.
* @param type type considered for transformation
*/
default boolean accept(CtType type) {
return true;
}
}

0 comments on commit d9c26c6

Please sign in to comment.