When trying to implement a sandbox in a plugin-like environment, I came accross https://stackoverflow.com/a/5580239/2057294 which seems to be exactly what I want, however I am unable to get it working, what am I doing wrong?
I have the following setup:
final class ModURLClassLoader extends URLClassLoader {
ModURLClassLoader(final URL[] urls) {
super(urls);
}
ModURLClassLoader(final URL[] urls, final ClassLoader parent) {
super(urls, parent);
}
ModURLClassLoader(final URL[] urls, final ClassLoader parent, final URLStreamHandlerFactory factory) {
super(urls, parent, factory);
}
#Override
protected PermissionCollection getPermissions(final CodeSource codesource) {
PermissionCollection permissionCollection = super.getPermissions(codesource);
//give no permissions to the codesource
return permissionCollection;
}
#Override
protected Class<?> loadClass(final String name, final boolean resolve) throws ClassNotFoundException {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
int i = name.lastIndexOf('.');
if (i != -1) {
sm.checkPackageAccess(name.substring(0, i));
}
}
return super.loadClass(name, resolve);
}
}
The idea here is that code loaded through the ModURLClassLoader may have no permissions at all. I do want to be able to add extra permissions statically in case I block some permissions which a mod would generally want to have.
Then I load up my classes via the following:
public class JavaMod extends LoadableMod {
private ECSMod ecsMod;
private ModURLClassLoader modUrlClassLoader;
JavaMod(final Path modDirectory) throws ModNotLoadableException {
super(modDirectory);
}
#Override
protected void load0() throws ModNotLoadableException {
try {
Properties properties = ModLoaderHelper.getConfiguration(modDirectory);
String jarName = properties.getProperty("jar");
String entryPoint = properties.getProperty("entryPoint");
Path jarPath = modDirectory.resolve(jarName);
try {
modUrlClassLoader = AccessController.doPrivileged((PrivilegedExceptionAction<ModURLClassLoader>)() -> new ModURLClassLoader(new URL[] { jarPath.toUri().toURL() }, getClass().getClassLoader()));
} catch (PrivilegedActionException ex) {
throw new ModNotLoadableException(ex);
}
Class<?> clazz = Class.forName(entryPoint, false, modUrlClassLoader);
if (!ECSMod.class.isAssignableFrom(clazz)) {
throw new ModNotLoadableException(clazz + " does not implement ECSMod");
}
this.ecsMod = (ECSMod)clazz.newInstance();
} catch (Exception ex) {
throw new ModNotLoadableException(ex);
}
}
#Override
protected void unload0() {
try {
modUrlClassLoader.close();
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
#Override
protected ECSGame createGame0() {
ECSGame ecsGame = new ECSGame();
ecsMod.setupGame(ecsGame);
return ecsGame;
}
}
Which loads an untrusted JAR that has a class that implements ECSMod, I have confirmed that both the clazz and ecsMod belong to an instance of ModURLClassLoader.
Then the malicious code:
public final class UntrustedEvilMod implements ECSMod {
#Override
public void setupGame(final ECSGame game) {
System.exit(0);
}
}
Where ECSMod and ECSGame are classes that belong to the plugin (also called mod) API and reside in the original classloader.
I'm running this without a security manager, as I believe that this would be unnecessary in this context? As the ModURLClassLoader defines the security.
Why is this not working, meaning that it still causes the program to exit, whereas I would've wanted an AccessControlException?
Related
I am migrating an app from java 8 to java 11. I did most of the upgrade but got stuck with the following exception.
class com.teamhub.plugins.sitemaps.jobs.SitemapUpdateJob cannot be cast to class org.quartz.Job (com...SitemapUpdateJob is in unnamed module of loader com...utils.ChildFirstClassLoader #bb39957; org.quartz.Job is in unnamed module of loader org.akhikhl.gretty.FilteringClassLoader #247667dd)
The application uses the normal context class loader but it loads plugins into the classpath using the ChildFirstClassLoader which on java 8 worked like a charm. In Java 11 though, it fails.
Looks like the issue is that the parent classloader loaded Job, and the child, ChildFirstClassLoader failes to use that to cast an implemented job.
public class ChildFirstClassLoader extends URLClassLoader {
private static final Set<String> FORCE_IN_PARENT = new HashSet<String>(); //ImmutableSet.of("");
private ClassLoader system;
public ChildFirstClassLoader(URL[] classpath, ClassLoader parent) {
super(classpath, parent);
system = getSystemClassLoader();
}
#Override
protected synchronized Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
// checking local
c = findClass(name);
} catch (ClassNotFoundException | SecurityException e) {
c = loadClassFromParent(name, resolve);
}
}
if (resolve)
resolveClass(c);
return c;
}
#Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
for (String pkg : FORCE_IN_PARENT){
if (name.startsWith(pkg)) {
throw new ClassNotFoundException();
}
}
return super.findClass(name);
}
private Class<?> loadClassFromParent(String name, boolean resolve) throws ClassNotFoundException {
// checking parent
// This call to loadClass may eventually call findClass
// again, in case the parent doesn't find anything.
Class<?> c;
try {
c = super.loadClass(name, resolve);
} catch (ClassNotFoundException | SecurityException e) {
c = loadClassFromSystem(name);
}
return c;
}
private Class<?> loadClassFromSystem(String name) throws ClassNotFoundException{
Class<?> c = null;
if (system != null) {
// checking system: jvm classes, endorsed, cmd classpath,
// etc.
c = system.loadClass(name);
}
return c;
}
#Override
public URL getResource(String name) {
URL url = findResource(name);
if (url == null)
url = super.getResource(name);
if (url == null && system != null)
url = system.getResource(name);
return url;
}
#Override
public Enumeration<URL> getResources(String name) throws IOException {
/**
* Similar to super, but local resources are enumerated before parent
* resources
*/
Enumeration<URL> systemUrls = null;
if (system != null) {
systemUrls = system.getResources(name);
}
Enumeration<URL> localUrls = findResources(name);
Enumeration<URL> parentUrls = null;
if (getParent() != null) {
parentUrls = getParent().getResources(name);
}
final List<URL> urls = new ArrayList<URL>();
if (localUrls != null) {
while (localUrls.hasMoreElements()) {
URL local = localUrls.nextElement();
urls.add(local);
}
}
if (systemUrls != null) {
while (systemUrls.hasMoreElements()) {
urls.add(systemUrls.nextElement());
}
}
if (parentUrls != null) {
while (parentUrls.hasMoreElements()) {
urls.add(parentUrls.nextElement());
}
}
return new Enumeration<URL>() {
Iterator<URL> iter = urls.iterator();
public boolean hasMoreElements() {
return iter.hasNext();
}
public URL nextElement() {
return iter.next();
}
};
}
public InputStream getResourceAsStream(String name) {
URL url = getResource(name);
try {
return url != null ? url.openStream() : null;
} catch (IOException e) {
}
return null;
}
#Override
public String toString() {
return String.format("ChildFirstClassLoader [system=%s, getURLs()=%s, getParent()=%s]",
system, Arrays.toString(getURLs()), getParent());
}
I made a java project, the project only contais this class:
package test.processor;
public abstract class Processor {
public abstract void loadData(String objectId);
public abstract void processData();
public abstract void saveData(String objectId);
}
The project is exported as a jar file (processor.jar)
Then I made another project that imports processor.jar and there is a class that extends Processor:
package test.process;
import test.processor.Processor;
public class Process extends Processor{
#Override
public void loadData(String objectId) {
System.out.println("LOAD DATAAAAAAAAAAAA");
}
#Override
public void processData() {
System.out.println("PROCESS DATAAAAAAAAAAAA");
}
#Override
public void saveData(String objectId) {
System.out.println("SAVE DATAAAAAAAAAAAA");
}
}
This project is also exported as jar (plugin.jar).
Finally, I coded something to load the plugins dynamically:
import test.processor.Processor;
public class Test {
public void testPlugins(){
Processor plugin = (Processor) loadJar(
"C:\\Users\\...\\Desktop\\plugin.jar",
"test.process.Process");
processor.loadData("dada");
}
private Object loadJar(String jar, String className){
File jarFile = new File(jar);
Object instance = null;
try {
URL jarpath = jarFile.toURI().toURL();
String jarUrl = "jar:" + jarpath + "!/";
URL urls[] = { new URL(jarUrl) };
URLClassLoader child = new URLClassLoader(urls);
Class classToLoad = Class.forName(nomeClasse, true, child);
instance = classToLoad.newInstance();
} catch (Exception ex) {
ex.printStackTrace();
}
return instance;
}
}
If I run that code inside a main method it works correctly, once I try to run it in the server there is a problem when loading the class, I get a ClassNotFoundException (Processor).
I tried putting the jar in the tomcat/lib, project/WEB-INF/lib and nothing changed.
Any idea of what Im doing wrong?
I didn't solve it the way I wanted, but I solved it:
First I tried loading the process.jar manually:
private Object loadJars(String processJar, String pluginJar, String className){
File processJarFile = new File(processJar);
File pluginJarFile = new File(pluginJar);
Object instance = null;
try {
URL processJarPath = processJarFile.toURI().toURL();
String processJarUrl = "jar:" + processJarPath + "!/";
URL pluginJarPath = pluginJarFile.toURI().toURL();
String pluginJarUrl = "jar:" + pluginJarPath + "!/";
URL urls[] = { new URL(processJarUrl), new URL(pluginJarUrl) };
URLClassLoader child = new URLClassLoader(urls);
Class classToLoad = Class.forName(nomeClasse, true, child);
instance = classToLoad.newInstance();
} catch (Exception ex) {
ex.printStackTrace();
}
return instance;
}
That loads the Process class correctly, the problem happens in the testPlugins mehod, once it tries to cast to Processor (ClassCastException, can't cast Process to Processor):
public void testPlugins(){
Processor plugin = (Processor) loadJars("C:\\Users\\...\\Desktop\\processor.jar",
"C:\\Users\\...\\Desktop\\plugin.jar",
"test.process.Process");
processor.loadData("dada");
}
Still need to read a lot about classloading but I guess the problem is that it doesn't recognize the Processor loaded from C:\Users\...\Desktop\processor.jar as the same as the Processor loaded from the webapp context or it "forgets" Process extends Processor.
I was in a hurry so I didn't have time to research, to solve the problem I invoked the methods using reflection:
public void modifiedTestPlugins(){
Object plugin = loadJar("C:\\Users\\...\\Desktop\\processor.jar",
"C:\\Users\\...\\Desktop\\plugin.jar",
"test.process.Process");
try {
Method processData = findMethod(obj.getClass(), "processData");
//here I invoke the processData method, it prints: PROCESS DATAAAAAAAAAAAA
loadData.invoke(processData, new Object[]{});
} catch (Exception e) {
e.printStackTrace();
}
}
private static Method findMethod(Class clazz, String methodName) throws Exception {
Method[] methods = clazz.getMethods();
for (int i = 0; i < methods.length; i++) {
if (methods[i].getName().equals(methodName))
return methods[i];
}
return null;
}
I am trying to compile and load a class dynamically during runtime using the JavaCompiler API. I store the compiled bytecode in memory. So I use a custom class loader to load the class.
public class CompilerAPITest {
static String sourceCode = "package in.test;" +
"public class DynamicCompilationHelloWorld implements TestInterface{" +
"public void test (){" +
"System.out.println (\"Hello, dynamic compilation world!\");" +
"new in.test.another.SomeClass().fun();" +
"}" +
"}" ;
public void doCompilation (){
SimpleJavaFileObject fileObject = new DynamicJavaSourceCodeObject ("in.test.DynamicCompilationHelloWorld", sourceCode) ;
JavaFileObject javaFileObjects[] = new JavaFileObject[]{fileObject} ;
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
JavaFileManager stdFileManager = new
CompilerAPITest.ClassFileManager(compiler
.getStandardFileManager(null, null, null));
Iterable<? extends JavaFileObject> compilationUnits = Arrays.asList(javaFileObjects);
DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<JavaFileObject>();
List<String> options = Arrays.asList("-cp", System.getProperty("java.class.path")
+ ":" + getPath(CompilerAPITest.class));
CompilationTask compilerTask = compiler.getTask(null, stdFileManager, diagnostics, options, null, compilationUnits) ;
boolean status = compilerTask.call();
if (!status){//If compilation error occurs
/*Iterate through each compilation problem and print it*/
for (Diagnostic<? extends JavaFileObject> diagnostic : diagnostics.getDiagnostics()){
System.out.format("Error on line %d in %s", diagnostic.getLineNumber(), diagnostic);
}
}
try {
stdFileManager.close() ;//Close the file manager
} catch (IOException e) {
e.printStackTrace();
}
try {
stdFileManager.getClassLoader(null)
.loadClass("in.test.DynamicCompilationHelloWorld").asSubclass(TestInterface.class).newInstance().test();
} catch (ClassNotFoundException e) {
e.printStackTrace();
//This does nothing.
} catch (InstantiationException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalAccessException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public static void main(String args[]){
new CompilerAPITest ().doCompilation() ;
}
class DynamicJavaSourceCodeObject extends SimpleJavaFileObject{
private String qualifiedName ;
private String sourceCode ;
protected DynamicJavaSourceCodeObject(String name, String code) {
super(URI.create("string:///" +name.replaceAll("\\.", "/") + Kind.SOURCE.extension), Kind.SOURCE);
this.qualifiedName = name ;
this.sourceCode = code ;
}
#Override
public CharSequence getCharContent(boolean ignoreEncodingErrors)
throws IOException {
return sourceCode ;
}
public String getQualifiedName() {
return qualifiedName;
}
public void setQualifiedName(String qualifiedName) {
this.qualifiedName = qualifiedName;
}
public String getSourceCode() {
return sourceCode;
}
public void setSourceCode(String sourceCode) {
this.sourceCode = sourceCode;
}
}
private static class ClassFileManager extends
ForwardingJavaFileManager<JavaFileManager> {
private JavaClassObject jclassObject;
public ClassFileManager(StandardJavaFileManager
standardManager) {
super(standardManager);
}
#Override
public ClassLoader getClassLoader(Location location) {
return new java.security.SecureClassLoader() {
#Override
protected Class<?> findClass(String name)
throws ClassNotFoundException {
byte[] b = jclassObject.getBytes();
return super.defineClass(name, jclassObject
.getBytes(), 0, b.length);
}
};
}
#Override
public JavaFileObject getJavaFileForOutput(Location location,
String className, Kind kind, FileObject sibling)
throws IOException {
jclassObject = new JavaClassObject(className, kind);
return jclassObject;
}
}
private static class JavaClassObject extends SimpleJavaFileObject {
protected final ByteArrayOutputStream bos =
new ByteArrayOutputStream();
public JavaClassObject(String name, Kind kind) {
super(URI.create("string:///" + name.replace('.', '/')
+ kind.extension), kind);
}
public byte[] getBytes() {
return bos.toByteArray();
}
#Override
public OutputStream openOutputStream() throws IOException {
return bos;
}
}
}
This works fine when run in a standalone setup. However when i call doCompilation() in my production setup, which runs on JBoss, i get the following exception.
java.lang.NoClassDefFoundError:
in/test/TestInterface(wrong name:
in/test/DynamicCompilationHelloWorld)
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:621)
at java.lang.ClassLoader.defineClass(ClassLoader.java:466)
at in.test.CompilerAPITest$ClassFileManager$1.findClass(CompilerAPITest.java:126)
What could be the problem here?
Found the issue after some googling.
Parent class loader needs to be set for the custom class loader used.
Class loading hierarchy is slightly different in JBoss. Here UnifiedClassLoader is used, which is used for checking even the peer class loaders, in addition to the parents, before throwing ClassNotFoundException. So when a custom class loader is used, it needs to delegate the defineClass calls to the UnifiedClassLoader, when it can not load the class.
Here is an example code snippet for a custom class loader.
private static class ByteClassLoader extends ClassLoader{
private Map<String, JavaFileObject> store = new HashMap<String, JavaFileObject>();
public ByteClassLoader(Map<String, JavaFileObject> str)
{
super( ByteClassLoader.class.getClassLoader() ); // set parent
store = str;
}
protected Class<?> findClass(String name)
throws ClassNotFoundException{
JavaFileObject jfo = store.get(name);
if (jfo == null){
throw new ClassNotFoundException(name);
}
byte[] bytes = ((JavaClassObject)jfo).getBytes();
Class<?> cl = defineClass(name, bytes, 0, bytes.length);
if (cl == null){
throw new ClassNotFoundException(name);
}
return cl;
}
}
And for loading the class,
ByteClassLoader cl = new ByteClassLoader(store);
cl.loadClass(className);
needs to be used.
Here is an excellent link on dynamic compilation and class loading.
http://fivedots.coe.psu.ac.th/~ad/jg/javaArt1/onTheFlyArt1.pdf
I want to have my persistence.xml in conf folder of my app. How can I tell Persistence.createEntityManagerFactory that it should read it from there?
If you are using EclipseLink you can set the persistence.xml location with the persistence unit property, "eclipselink.persistencexml".
properties.put("eclipselink.persistencexml", "/org/acme/acme-persistence.xml");
EntityManagerFactory factory = Persistence.createEntityManagerFactory("acme", properties);
This solution worked for me
Thread.currentThread().setContextClassLoader(new ClassLoader() {
#Override
public Enumeration<URL> getResources(String name) throws IOException {
if (name.equals("META-INF/persistence.xml")) {
return Collections.enumeration(Arrays.asList(new File("conf/persistence.xml")
.toURI().toURL()));
}
return super.getResources(name);
}
});
Persistence.createEntityManagerFactory("test");
The createEntityManagerFactory methods search for persistence.xml files within the META-INF directory of any CLASSPATH element.
if your CLASSPATH contains the conf directory, you could place an EntityManagerFactory definition in conf/META-INF/persistence.xml
The ClassLoader may be a URLClassLoader, so try it this way:
final URL alternativePersistenceXmlUrl = new File("conf/persistence.xml").toURI().toURL();
ClassLoader output;
ClassLoader current = Thread.currentThread().getContextClassLoader();
try{
URLClassLoader parent = (URLClassLoader)current;
output = new URLClassLoader(parent.getURLs(), parent){
#Override
public Enumeration<URL> getResources(String name) throws IOException {
if (name.equals("META-INF/persistence.xml")) {
return Collections.enumeration(Arrays.asList(alternativePersistenceXmlUrl));
}
return super.getResources(name);
}
};
}catch(ClassCastException ignored) {
output = new ClassLoader() {
#Override
public Enumeration<URL> getResources(String name) throws IOException {
if (name.equals("META-INF/persistence.xml")) {
return Collections.enumeration(Arrays.asList(alternativePersistenceXmlUrl));
}
return super.getResources(name);
}
};
}
It should work. Works for me under certain test etc conditions.
Please this is a hack and should not be used in production.
My solution is for EclipseLink 2.7.0 and Java 9 and it is modified and detailed version of #Evgeniy Dorofeev answer.
In org.eclipse.persistence.internal.jpa.deployment.PersistenceUnitProcessor on line 236 we see the following code:
URL puRootUrl = computePURootURL(descUrl, descriptorPath);
This code is used by EclipseLink to compute root url of the persistence.xml path. That's very important because final path will be made by adding descriptorPath to puRootUrl.
So, let's suppose we have file on /home/Smith/program/some-folder/persistence.xml, then we have:
Thread currentThread = Thread.currentThread();
ClassLoader previousClassLoader = currentThread.getContextClassLoader();
Thread.currentThread().setContextClassLoader(new ClassLoader(previousClassLoader) {
#Override
public Enumeration<URL> getResources(String name) throws IOException {
if (name.equals("some-folder/persistence.xml")) {
URL url = new File("/home/Smith/program/some-folder/persistence.xml").toURI().toURL();
return Collections.enumeration(Arrays.asList(url));
}
return super.getResources(name);
}
});
Map<String, String> properties = new HashMap<>();
properties.put("eclipselink.persistencexml", "some-folder/persistence.xml");
try {
entityManagerFactory = Persistence.createEntityManagerFactory("unit-name", properties);
} catch (Exception ex) {
logger.error("Error occured creating EMF", ex);
} finally {
currentThread.setContextClassLoader(previousClassLoader);
}
Details:
Pay attention that when creating new class loader I pass there previous classloader otherwise it doesn't work.
We set property eclipselink.persistencexml. If we don't do that then default descriptorPath will be equal to META-INF/persistence.xml and we would need to keep our persistence.xml on /home/Smith/program/META-INF/persistence.xml to be found.
I tried these ways when the program is starting (at first line of main function):
Write your persistence.xml to the resources/META-INF/persistence.xml of the jar
I had problem with this way: Java write .txt file in resource folder
Create META-INF folder in the jar directory and put your persistence.xml into it, then execute this command:
jar uf $jarName META-INF/persistence.xml
This command will replace META-INF/persistence.xml (your file) in the jar
private fun persistence() {
val fileName = "META-INF/persistence.xml"
val jarName: String?
val done = try {
jarName = javaClass.protectionDomain.codeSource.location.path
if (File(fileName).exists() && !jarName.isNullOrBlank()
&& jarName.endsWith(".jar") && File(jarName).exists()) {
Command().exec("jar uf $jarName META-INF/persistence.xml", timeoutSec = 30)
true
} else false
} catch (e: Exception) {
false
}
if (done) {
logger.info { "$fileName exist and will be loaded :)" }
} else {
logger.info {
"$fileName not exist in current folder so it will be read from .jar :(" +
" you can run: jar uf jarName.jar META-INF/persistence.xml"
}
}
}
Running Command Line in Java
A solution by creating tweaked PersistenceUnitDescriptor.
import org.hibernate.jpa.boot.internal.ParsedPersistenceXmlDescriptor;
import org.hibernate.jpa.boot.internal.PersistenceXmlParser;
import org.hibernate.jpa.boot.spi.Bootstrap;
import org.hibernate.jpa.boot.spi.EntityManagerFactoryBuilder;
public class HibernateEntityManagerFactoryBuilder {
public static final EntityManagerFactory build(URL xmlUrl) {
final ParsedPersistenceXmlDescriptor xmlDescriptor = PersistenceXmlParser.locateIndividualPersistenceUnit(xmlUrl);
final HibernatePersistenceUnitDescriptor hibernateDescriptor = new HibernatePersistenceUnitDescriptor(xmlDescriptor);
final EntityManagerFactoryBuilder builder = Bootstrap.getEntityManagerFactoryBuilder(hibernateDescriptor, Collections.emptyMap(), (ClassLoader) null);
final EntityManagerFactory factory = builder.build();
return factory;
}
public static final EntityManagerFactory build(URL xmlUrl, final String name) {
final ParsedPersistenceXmlDescriptor xmlDescriptor = PersistenceXmlParser.locateNamedPersistenceUnit(xmlUrl, name);
if(xmlDescriptor == null) throw new RuntimeException("Persistence unit with name '"+name+ "' not found.");
final HibernatePersistenceUnitDescriptor hibernateDescriptor = new HibernatePersistenceUnitDescriptor(xmlDescriptor);
final EntityManagerFactoryBuilder builder = Bootstrap.getEntityManagerFactoryBuilder(hibernateDescriptor, Collections.emptyMap(), (ClassLoader) null);
final EntityManagerFactory factory = builder.build();
return factory;
}
public static void main(String[] args) {
try {
final EntityManagerFactory factory = build(new File("D:/ini/persistence.xml").toURI().toURL());
} catch (Exception e) {e.printStackTrace();}
}
}
public class HibernatePersistenceUnitDescriptor implements PersistenceUnitDescriptor {
private final PersistenceUnitDescriptor descriptor;
public HibernatePersistenceUnitDescriptor(PersistenceUnitDescriptor descriptor) {
this.descriptor = descriptor;
}
#Override
public URL getPersistenceUnitRootUrl() {
return null;
}
#Override
public String getName() {
return descriptor.getName();
}
#Override
public String getProviderClassName() {
return descriptor.getProviderClassName();
}
#Override
public boolean isUseQuotedIdentifiers() {
return descriptor.isUseQuotedIdentifiers();
}
#Override
public boolean isExcludeUnlistedClasses() {
return descriptor.isExcludeUnlistedClasses();
}
#Override
public PersistenceUnitTransactionType getTransactionType() {
return descriptor.getTransactionType();
}
#Override
public ValidationMode getValidationMode() {
return descriptor.getValidationMode();
}
#Override
public SharedCacheMode getSharedCacheMode() {
return descriptor.getSharedCacheMode();
}
#Override
public List<String> getManagedClassNames() {
return descriptor.getManagedClassNames();
}
#Override
public List<String> getMappingFileNames() {
return descriptor.getMappingFileNames();
}
#Override
public List<URL> getJarFileUrls() {
return descriptor.getJarFileUrls();
}
#Override
public Object getNonJtaDataSource() {
return descriptor.getNonJtaDataSource();
}
#Override
public Object getJtaDataSource() {
return descriptor.getJtaDataSource();
}
#Override
public Properties getProperties() {
return descriptor.getProperties();
}
#Override
public ClassLoader getClassLoader() {
return descriptor.getClassLoader();
}
#Override
public ClassLoader getTempClassLoader() {
return descriptor.getTempClassLoader();
}
#Override
public void pushClassTransformer(EnhancementContext enhancementContext) {
descriptor.pushClassTransformer(enhancementContext);
}
}
Despite warnings to drop my present course of action, I currently see no better way to solve my problem. I must generate Java code at runtime, then compile it, load it and reference it.
Problem is that the generated code imports code that has already been loaded by the system class loader (I suppose) - that is, code present in one of the jars on my classpath.
(I run inside a Tomcat 6 web container over Java 6.) You may ask yourselves why that is a problem - well I sure don't know - but fact is that I get compilation errors:
/W:/.../parser/v0.5/AssignELParser.java:6:
package com.xxx.yyy.zzz.configuration
does not exist
Following some examples off the internet I have defined the following classes:
class MemoryClassLoader extends ChainedAction {
private static final Logger LOG = Logger.getLogger(MemoryClassLoader.class);
private LoaderImpl impl;
private class LoaderImpl extends ClassLoader {
// The compiler tool
private final JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
// Compiler options
private final Iterable<String> options = Arrays.asList("-verbose");
// DiagnosticCollector, for collecting compilation problems
private final DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<JavaFileObject>();
// Our FileManager
private final MemoryFileManager manager = new MemoryFileManager(this.compiler);
public LoaderImpl(File sourceDirectory) {
List<Source> list = new ArrayList<Source>();
File[] files = sourceDirectory.listFiles(new FilenameFilter() {
#Override
public boolean accept(File dir, String name) {
return name.endsWith(Kind.SOURCE.extension);
}
});
for (File file : files) {
list.add(new Source(file));
}
CompilationTask task = compiler.getTask(null, manager, diagnostics, options, null, list);
Boolean compilationSuccessful = task.call();
LOG.info("Compilation has " + ((compilationSuccessful) ? "concluded successfully" : "failed"));
// report on all errors to screen
for (Diagnostic<? extends JavaFileObject> diagnostic : diagnostics.getDiagnostics()) {
LOG.warn(diagnostic.getMessage(null));
}
}
#Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
synchronized (this.manager) {
Output output = manager.map.remove(name);
if (output != null) {
byte[] array = output.toByteArray();
return defineClass(name, array, 0, array.length);
}
}
return super.findClass(name);
}
}
#Override
protected void run() {
impl = new LoaderImpl(new File(/* Some directory path */));
}
}
class MemoryFileManager extends ForwardingJavaFileManager<JavaFileManager> {
final Map<String, Output> map = new HashMap<String, Output>();
MemoryFileManager(JavaCompiler compiler) {
super(compiler.getStandardFileManager(null, null, null));
}
#Override
public Output getJavaFileForOutput(Location location, String name, Kind kind, FileObject source) {
Output output = new Output(name, kind);
map.put(name, output);
return output;
}
}
class Output extends SimpleJavaFileObject {
private final ByteArrayOutputStream baos = new ByteArrayOutputStream();
Output(String name, Kind kind) {
super(URI.create("memo:///" + name.replace('.', '/') + kind.extension), kind);
}
byte[] toByteArray() {
return this.baos.toByteArray();
}
#Override
public ByteArrayOutputStream openOutputStream() {
return this.baos;
}
}
class Source extends SimpleJavaFileObject {
public Source(File file) {
super(file.toURI(), Kind.SOURCE);
}
#Override
public CharSequence getCharContent(boolean ignoreEncodingErrors) {
StringBuilder sb = new StringBuilder("");
try {
File file = new File(uri);
FileReader fr = new FileReader(file);
BufferedReader br = new BufferedReader(fr);
sb = new StringBuilder((int) file.length());
String line = "";
while ((line = br.readLine()) != null) {
sb.append(line);
sb.append("\n");
}
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return sb.toString();
}
}
It seems that the inner class LoaderImpl by extending the ClassLoader class and by not calling an explicit super constructor should reference as its parent class loader the system class loader.
If it does so then why do I get the "runtime" compilation error - above? Why does it not find the code for the imported class?
Not sure if it can help, but have you tried to specify classpath explicitly?
getClassPath()
{
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
URL[] urls = ((URLClassLoader) classLoader).getURLs();
StringBuilder buf = new StringBuilder(1000);
buf.append(".");
String separator = System.getProperty("path.separator");
for (URL url : urls) {
buf.append(separator).append(url.getFile());
}
}
classPath = buf.toString();
and then
options.add("-classpath");
options.add(getClassPath());
I also can't see where do you pass LoaderImpl instance to the compiler. Shouldn't it be done explicitly?