1. #1
    Nouveau membre du Club
    Homme Profil pro
    Étudiant
    Inscrit en
    juillet 2008
    Messages
    34
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 27
    Localisation : France

    Informations professionnelles :
    Activité : Étudiant

    Informations forums :
    Inscription : juillet 2008
    Messages : 34
    Points : 26
    Points
    26

    Par défaut Compilation "Personnalisé" avec JavaCompiler

    Les 2 choses que je souhaite faire :
    - compilation d'un String ( sans créer de .class pour des questions de performance d'accès disque ) pour savoir si le String est correct
    - compilation d'un String ( toujours sans créer de .class ) mais en récupérant cette fois-ci le bytecode généré ( pour pouvoir l'appeler ensuite avec Classloader.defineClass(...) sans toucher au disque dur)

    État actuel des choses :
    J'arrive à compiler&exécuter un String grâce à JavaCompiler et l'introspection ( lien ) mais cette compilation créer un .class

    Voie Process avec javac :
    J'ai beau cherché quels arguments entré à javac je ne trouve pas comment lui faire afficher le bytecode en sortie
    Pour ce qui est de lui envoyer un String :
    javac << cat Test.java
    Control + D
    fonctionne mais nécessite un .java ( ce qui n'est pas appréciable )
    >> impasse

    Voie JavaCompiler.run(...) :
    Je n'arrive pas à compiler de string avec, mais uniquement des fichiers
    >> impasse

    Alors je ne vois pas comment faire
    Voilà, vos idées sont les bienvenues.

  2. #2
    Membre éprouvé Avatar de Nudger
    Homme Profil pro
    Directeur de projet
    Inscrit en
    octobre 2010
    Messages
    498
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 41
    Localisation : France, Paris (Île de France)

    Informations professionnelles :
    Activité : Directeur de projet
    Secteur : High Tech - Produits et services télécom et Internet

    Informations forums :
    Inscription : octobre 2010
    Messages : 498
    Points : 1 042
    Points
    1 042

    Par défaut

    Bonsoir,

    Sans garantie.

    Javassist permet d'injecter du code dans des méthodes sans modifier le .class associé, il est probable qu'il permette également de créer une classe entière sans créer physiquement le .class.
    https://www.nudge-apm.com Surveillez vos applications Java et PHP et diagnostiquez facilement les problèmes de performance.

  3. #3
    Expert éminent sénior
    Avatar de adiGuba
    Homme Profil pro
    Développeur Java/Web
    Inscrit en
    avril 2002
    Messages
    13 822
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France

    Informations professionnelles :
    Activité : Développeur Java/Web
    Secteur : Transports

    Informations forums :
    Inscription : avril 2002
    Messages : 13 822
    Points : 22 737
    Points
    22 737
    Billets dans le blog
    1

    Par défaut

    Salut,


    C'est possible en gérant son propre JavaFileManager.
    Au passage il faut commencer par modifier la méthode getJavaSourceFromString() afin qu'elle prenne en compte le nom du fichier : comme on va devoir passer par un ClassLoader personnalisé on va avoir besoin de classe public (afin d'éviter des problème de droits d'accès). En clair au lieu de passer "code" au constructeur de la classe JavaSourceFromString, on va lui passer le vrai nom de la classe ("Test" dans l'exemple donnée.

    Au passage on peut éviter de s'implémenter l'Iteratable en passant via une collection. Cela donne donc ceci :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    	public static Iterable<JavaSourceFromString> getJavaSourceFromString(
    			String fileName, String code) {
    		return Collections.singletonList(new JavaSourceFromString(fileName, code));
    	}
     
    // la classe JavaSourceFromString ne change pas


    Ensuite, le compilateur gère les fichiers via la classe JavaFileObject. Il va donc falloir implémenter la notre propre implémentation. Heureusement l'API nous permet d'utiliser un Wrapper afin de ne pas avoir à tout redéfinir. On va donc se contenter de redéfinir openOutputStream() qui ouvre le flux d'écriture (ce qui se fera dans un byte[] dans notre cas) :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    class MyJavaFileObject extends ForwardingJavaFileObject<JavaFileObject> {
     
    	private ByteArrayOutputStream data;
     
    	public MyJavaFileObject(JavaFileObject object) {
    		super(object);
    	}
     
    	@Override
    	public OutputStream openOutputStream() throws IOException {
    		this.data = new ByteArrayOutputStream();
    		return data;
    	}
     
    	public byte[] getByteCode() {
    		return data.toByteArray(); // NPE si jamais ouvert
    	}
    }
    On ajoute également une méthode getByteCode() qui se chargera de retourner le bytecode ainsi généré...



    Maintenant, pour passer cela au compilateur, il faut passer par un JavaFileManager. Même chose ici en se contentant de modifier la méthode getJavaFileForOutput() :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class MyJavaFileManager extends ForwardingJavaFileManager<JavaFileManager> {
     
    	public MyJavaFileManager(JavaFileManager manager) {
    		super(manager);
    	}
     
    	@Override
    	public JavaFileObject getJavaFileForOutput(Location location,
    			String className, Kind kind, FileObject sibling) throws IOException {
    		JavaFileObject javaObject = super.getJavaFileForOutput(location, className, kind, sibling);
    		MyJavaFileObject myJavaObject = new MyJavaFileObject(javaObject);
    		return myJavaObject;
    	}
    }


    Le code de compilation devient donc ceci :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    	JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
    	String program = "public class Test{"
    			+ "   public static void main (String [] args){"
    			+ "      System.out.println (\"Hello, World\");"
    			+ "      System.out.println (args.length);" + "   }" + "}";
     
    	Iterable<? extends JavaFileObject> fileObjects = 
    		getJavaSourceFromString("Test", program);
     
    	MyJavaFileManager fileManager = new MyJavaFileManager(compiler.getStandardFileManager(null, null, null));
     
    	compiler.getTask(null, fileManager, null, null, null, fileObjects)
    			.call();
    Cela marche parfaitement sauf qu'on ne peut plus exécuter la classe.
    En effet Class.forName() ne peut plus la trouver puisqu'elle n'existe plus sur le disque !!!

    En effet il faut conserver une référence vers le bytecode généré (via une Map), et implémenter un classloader personnalisé qui se chargera de le charger en mémoire.

    L'implémentation d'un ClassLoader comme celui-là est trivial puisqu'il suffit d'implémenter findClass() de manière assez basique.

    Notre classe MyJavaFileManager s'étoffe donc un peu :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    class MyJavaFileManager extends ForwardingJavaFileManager<JavaFileManager> {
     
    	/** La map contenant l'association nom de classe / JavaFileObject */
    	private final Map<String, MyJavaFileObject> map = new HashMap<String, MyJavaFileObject>();
     
    	/** Le ClassLoader qui charge les classes depuis la Map: */
    	private final ClassLoader loader = new ClassLoader() {
    		protected Class<?> findClass(String name) throws ClassNotFoundException {
    			MyJavaFileObject javaObject = map.get(name);
    			if (javaObject == null)
    				throw new ClassNotFoundException(name);
    			byte[] bytes = javaObject.getByteCode();
    			return defineClass(name, bytes, 0, bytes.length);
    		}
    	};
     
    	public MyJavaFileManager(JavaFileManager manager) {
    		super(manager);
    	}
     
    	@Override
    	public JavaFileObject getJavaFileForOutput(Location location,
    			String className, Kind kind, FileObject sibling) throws IOException {
    		JavaFileObject javaObject = super.getJavaFileForOutput(location, className, kind, sibling);
    		MyJavaFileObject myJavaObject = new MyJavaFileObject(javaObject);
    		map.put(className, myJavaObject);
    		return myJavaObject;
    	}
     
    	public ClassLoader getClassLoader() {
    		return loader;
    	}
    }

    Désormais, on peut charger la classe via notre ClassLoader, et donc l'exécuter normalement :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    	JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
    	String program = "public class Test{"
    			+ "   public static void main (String [] args){"
    			+ "      System.out.println (\"Hello, World\");"
    			+ "      System.out.println (args.length);" + "   }" + "}";
     
    	Iterable<? extends JavaFileObject> fileObjects = 
    		getJavaSourceFromString("Test", program);
     
    	MyJavaFileManager fileManager = new MyJavaFileManager(compiler.getStandardFileManager(null, null, null));
     
    	compiler.getTask(null, fileManager, null, null, null, fileObjects)
    			.call();
     
     
    	ClassLoader loader = fileManager.getClassLoader();
     
    	Class<?> clazz = Class.forName("Test", true, loader);
     
    	Method m = clazz.getMethod("main", new Class[] { String[].class });
    	Object[] _args = new Object[] { new String[0] };
    	m.invoke(null, _args);

    Plus drôle : si on implémente une interface, on peut créer une instance et se passer d'une partie de l'introspection :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    	JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
    	String program = "public class Test implements Runnable {"
    			+ "   public void run (){"
    			+ "      System.out.println (\"Hello, World\");"
    			+ "   }"
    			+ "}";
     
    	Iterable<? extends JavaFileObject> fileObjects = 
    		getJavaSourceFromString("Test", program);
     
    	MyJavaFileManager fileManager = new MyJavaFileManager(compiler.getStandardFileManager(null, null, null));
     
    	compiler.getTask(null, fileManager, null, null, null, fileObjects)
    			.call();
     
    	ClassLoader loader = fileManager.getClassLoader();
     
    	Class<?> clazz = Class.forName("Test", true, loader);
     
    	Runnable r = Runnable.class.cast( clazz.newInstance() );
    	r.run();



    [edit] cela fonctionne pour des compilations simples comme cela. Maintenant si tu as besoin de compiler des codes plus complexe il faudra surement également implémenter les méthodes de lectures et d'autres méthode de JavaFileObject/JavaFileManager...



    Le code complet :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    import java.io.ByteArrayOutputStream;
    import java.io.IOException;
    import java.io.OutputStream;
    import java.net.URI;
    import java.util.Collections;
    import java.util.HashMap;
    import java.util.Map;
     
    import javax.tools.FileObject;
    import javax.tools.ForwardingJavaFileManager;
    import javax.tools.ForwardingJavaFileObject;
    import javax.tools.JavaCompiler;
    import javax.tools.JavaFileManager;
    import javax.tools.JavaFileObject;
    import javax.tools.SimpleJavaFileObject;
    import javax.tools.ToolProvider;
    import javax.tools.JavaFileObject.Kind;
     
    class MyJavaFileObject extends ForwardingJavaFileObject<JavaFileObject> {
     
    	private ByteArrayOutputStream data;
     
    	public MyJavaFileObject(JavaFileObject object) {
    		super(object);
    	}
     
    	@Override
    	public OutputStream openOutputStream() throws IOException {
    		this.data = new ByteArrayOutputStream();
    		return data;
    	}
     
    	public byte[] getByteCode() {
    		return data.toByteArray(); // NPE si jamais ouvert
    	}
    }
     
    class MyJavaFileManager extends ForwardingJavaFileManager<JavaFileManager> {
     
    	/** La map contenant l'association nom de classe / JavaFileObject */
    	private final Map<String, MyJavaFileObject> map = new HashMap<String, MyJavaFileObject>();
     
    	/** Le ClassLoader qui charge les classes depuis la Map: */
    	private final ClassLoader loader = new ClassLoader() {
    		protected Class<?> findClass(String name) throws ClassNotFoundException {
    			MyJavaFileObject javaObject = map.get(name);
    			if (javaObject == null)
    				throw new ClassNotFoundException(name);
    			byte[] bytes = javaObject.getByteCode();
    			return defineClass(name, bytes, 0, bytes.length);
    		}
    	};
     
    	public MyJavaFileManager(JavaFileManager manager) {
    		super(manager);
    	}
     
    	@Override
    	public JavaFileObject getJavaFileForOutput(Location location,
    			String className, Kind kind, FileObject sibling) throws IOException {
    		JavaFileObject javaObject = super.getJavaFileForOutput(location,
    				className, kind, sibling);
    		MyJavaFileObject myJavaObject = new MyJavaFileObject(javaObject);
    		map.put(className, myJavaObject);
    		return myJavaObject;
    	}
     
    	public ClassLoader getClassLoader() {
    		return loader;
    	}
    }
     
    class JavaSourceFromString extends SimpleJavaFileObject {
    	final String code;
     
    	JavaSourceFromString(String name, String code) {
    		super(URI.create("string:///" + name.replace('.', '/')
    				+ Kind.SOURCE.extension), Kind.SOURCE);
    		this.code = code;
    	}
     
    	public CharSequence getCharContent(boolean ignoreEncodingErrors) {
    		return code;
    	}
    }
     
    public class Main {
     
    	public static Iterable<JavaSourceFromString> getJavaSourceFromString(
    			String fileName, String code) {
    		return Collections.singletonList(new JavaSourceFromString(fileName,
    				code));
    	}
     
    	public static void main(String[] args) throws Exception {
    		JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
    		String program = "public class Test implements Runnable {"
    				+ "   public void run (){"
    				+ "      System.out.println (\"Hello, World\");"
    				+ "   }"
    				+ "}";
     
    		Iterable<? extends JavaFileObject> fileObjects = getJavaSourceFromString(
    				"Test", program);
     
    		MyJavaFileManager fileManager = new MyJavaFileManager(compiler
    				.getStandardFileManager(null, null, null));
     
    		compiler.getTask(null, fileManager, null, null, null, fileObjects)
    				.call();
     
    		ClassLoader loader = fileManager.getClassLoader();
     
    		Class<?> clazz = Class.forName("Test", true, loader);
     
    		Runnable r = Runnable.class.cast(clazz.newInstance());
    		r.run();
    	}
    }

    a++

  4. #4
    Nouveau membre du Club
    Homme Profil pro
    Étudiant
    Inscrit en
    juillet 2008
    Messages
    34
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 27
    Localisation : France

    Informations professionnelles :
    Activité : Étudiant

    Informations forums :
    Inscription : juillet 2008
    Messages : 34
    Points : 26
    Points
    26

    Par défaut

    @Nudger
    J'ai regardé rapidement, ça peut marcher effectivement mais adiGuba m'a donné une solution qui m'est plus familière et ne nécessitant pas de librairie supplémentaire. Merci quand-même.

    @adiGuba
    Merci beaucoup c'est exactement ce que je cherchais.
    Encore merci d'avoir pris le temps d'expliquer en détails tous les morceaux.

    Résolu.

  5. #5
    Futur Membre du Club
    Homme Profil pro
    Inscrit en
    mai 2012
    Messages
    11
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France

    Informations forums :
    Inscription : mai 2012
    Messages : 11
    Points : 9
    Points
    9

    Par défaut

    Juste une petit question: comment faire pour enregistrer cette classe fraîchement créée dans un fichier .class ?

+ Répondre à la discussion
Cette discussion est résolue.

Partager

Partager
  • Envoyer la discussion sur Viadeo
  • Envoyer la discussion sur Twitter
  • Envoyer la discussion sur Google
  • Envoyer la discussion sur Facebook
  • Envoyer la discussion sur Digg
  • Envoyer la discussion sur Delicious
  • Envoyer la discussion sur MySpace
  • Envoyer la discussion sur Yahoo