-
-
Notifications
You must be signed in to change notification settings - Fork 341
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add support for transformation at load-time in the JVM (#2645)
- Loading branch information
Showing
6 changed files
with
439 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | ||
} | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
185
src/main/java/spoon/decompiler/SpoonClassFileTransformer.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
Oops, something went wrong.