I'm working with a Java sort-of-application-server (Smartfox) which can run multiple applications ("extensions") but has a very inconvenient classpath setup to go along with it, along with issues when trying to use SLF4J.
To work around that I'd like to wrap my applications in their own classloaders. Such a containing classloader should be much like Tomcat's, in that it
Can load classes from a directory containing JARs.
Prefers classes from its own classpath over those from the parent
Is there a library somewhere that has such a classloader I can just "drag and drop" in my project? If not, would it be hard to create it myself? Any known pitfalls?
OSGi (and other module systems) are designed to handle exactly this kind of problem.
It might look like overkill at first, but I think you'll quickly re-implement significant parts of the things that OSGi alread does for you.
Equinox is the OSGi implementation used by Eclipse, for example.
Since I had trouble embedding an OSGi container and it was indeed a bit overkill, I rolled my own solution. But I'll learn to use OSGi one day, in a situation where I don't need to embed the framework.
If you somehow happen to want to use this code, it's under the "do whatever you want with it" license.
public class SmartfoxExtensionContainer extends AbstractExtension {
private AbstractExtension extension;
private void initRealExtension() {
final String zone = this.getOwnerZone();
System.out.println("[SmartfoxExtensionContainer] ========= Init extension for zone " + zone + " =========");
try {
// load properties
File propFile = new File("wext/" + zone + ".properties");
System.out.println("[SmartfoxExtensionContainer] Load config from " + propFile.getCanonicalPath());
Properties props = new Properties();
final FileInputStream ins = new FileInputStream(propFile);
try {
props.load(new InputStreamReader(ins, "UTF-8"));
} finally {
try {
ins.close();
} catch (IOException e) {}
}
// construct classloader
File jarDir = new File(props.getProperty("classpath", "wext/" + zone));
System.out.println("[SmartfoxExtensionContainer] Load classes from " + jarDir.getCanonicalPath());
if (!jarDir.isDirectory()) throw new RuntimeException("That is not an existing directory");
final File[] fs = jarDir.listFiles();
URL[] urls = new URL[fs.length];
for (int f = 0; f < fs.length; f++) {
System.out.println("[SmartfoxExtensionContainer] " + fs[f].getName());
urls[f] = fs[f].toURI().toURL();
}
SelfishClassLoader cl = new SelfishClassLoader(urls, SmartfoxExtensionContainer.class.getClassLoader());
// get real extension class
String mainClass = props.getProperty("mainClass", "Extension");
System.out.println("[SmartfoxExtensionContainer] Main class: " + mainClass);
#SuppressWarnings("unchecked")
Class<? extends AbstractExtension> extClass = (Class<? extends AbstractExtension>) cl.loadClass(mainClass);
// create extension and copy settings
extension = extClass.newInstance();
extension.setOwner(this.getOwnerZone(), this.getOwnerRoom());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/* ======================= DELEGATES ======================= */
#Override
public void init() {
initRealExtension();
extension.init();
}
#Override
public void destroy() {
extension.destroy();
}
#Override
public void handleRequest(String arg0, ActionscriptObject arg1, User arg2, int arg3) {
extension.handleRequest(arg0, arg1, arg2, arg3);
}
#Override
public void handleRequest(String arg0, String[] arg1, User arg2, int arg3) {
extension.handleRequest(arg0, arg1, arg2, arg3);
}
#Override
public void handleInternalEvent(InternalEventObject arg0) {
extension.handleInternalEvent(arg0);
}
#Override
public Object handleInternalRequest(Object params) {
return extension.handleInternalRequest(params);
}
#Override
public void handleRequest(String cmd, JSONObject jso, User u, int fromRoom) {
extension.handleRequest(cmd, jso, u, fromRoom);
}
/* ======================= CUSTOM CLASSLOADER ======================= */
private static class SelfishClassLoader extends URLClassLoader {
SelfishClassLoader(URL[] urls, ClassLoader parent) {
super(urls, parent);
}
// override default behaviour: find classes in local path first, then parent
#Override protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// First, check if the class has already been loaded
Class<?> clz = findLoadedClass(name);
if (clz == null) {
try {
clz = findClass(name);
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from current class loader
}
if (clz == null) {
// If still not found, then invoke parent.findClass in order
// to find the class.
clz = getParent().loadClass(name);
}
}
if (resolve) {
resolveClass(clz);
}
return clz;
};
}
}
Related
I've read a post https://frankkieviet.blogspot.com/2009/03/javalanglinkageerror-loader-constraint.html and I use the demo code to simulate LinkageError.
/**
* A self-first delegating classloader. It only loads specified classes self-first; other
* classes are loaded from the parent.
*/
private static class CustomCL extends ClassLoader {
private Set<String> selfFirstClasses;
private String label;
public CustomCL(String name, ClassLoader parent, String... selfFirsNames) {
super(parent);
this.label = name;
this.selfFirstClasses = new HashSet<String>(Arrays.asList(selfFirsNames));
}
public Class<?> findClass(String name) throws ClassNotFoundException {
if (selfFirstClasses.contains(name)) {
try {
byte[] buf = new byte[100000];
String loc = name.replace('.', '/') + ".class";
InputStream inp = Demo.class.getClassLoader().getResourceAsStream(loc);
int n = inp.read(buf);
inp.close();
System.out.println(label + ": Loading " + name + " in custom classloader");
return defineClass(name, buf, 0, n);
} catch (Exception e) {
throw new ClassNotFoundException(name, e);
}
}
// Is never executed in this test
throw new ClassNotFoundException(name);
}
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
if (findLoadedClass(name) != null) {
System.out.println(label + ": already loaded(" + name + ")");
return findLoadedClass(name);
}
// Override parent-first behavior into self-first only for specified classes
if (selfFirstClasses.contains(name)) {
return findClass(name);
} else {
System.out.println(label + ": super.loadclass(" + name + ")");
return super.loadClass(name, resolve);
}
}
public String toString() {
return label;
}
}
public static class User {
}
public static class LoginEJB {
static {
System.out.println("LoginEJB loaded");
}
public static void login(User u) {
}
}
public static class Servlet {
public static void doGet() {
User u = new User();
System.out.println("Logging in with User loaded in " + u.getClass().getClassLoader());
LoginEJB.login(u);
}
}
private static class EjbCL extends CustomCL {
public EjbCL(ClassLoader parent, String... selfFirsNames) {
super("Ejb", parent, selfFirsNames);
}
}
private static class SfWebCL extends CustomCL {
public SfWebCL(ClassLoader parent, String... selfFirsNames) {
super("SFWeb", parent, selfFirsNames);
}
}
public static void test1() throws Exception {
CustomCL ejbCL = new EjbCL(Demo.class.getClassLoader(), "com.test.zim.Demo$User",
"com.test.zim.Demo$LoginEJB");
CustomCL sfWebCL = new SfWebCL(ejbCL, "com.test.zim.Demo$User",
"com.test.zim.Demo$Servlet");
System.out.println("Logging in, self-first");
sfWebCL.loadClass("com.test.zim.Demo$Servlet", false).getMethod("doGet").invoke(null);
// sfWebCL.loadClass("com.test.zim.Demo$Servlet", false).getMethod("doGet");
// sfWebCL.loadClass("com.test.zim.Demo$User", false);
// sfWebCL.loadClass("com.test.zim.Demo$LoginEJB", false).getMethods();
System.out.println("Examining methods of LoginEJB");
ejbCL.loadClass("com.test.zim.Demo$LoginEJB", false).getMethods();
}
public static void main(String[] args) {
try {
test1();
} catch (Throwable e) {
e.printStackTrace(System.out);
}
}
After I run the test code, the output is:
Logging in, self-first
SFWeb: Loading com.test.zim.Demo$Servlet in custom classloader
SFWeb: super.loadclass(java.lang.Object)
Ejb: super.loadclass(java.lang.Object)
SFWeb: Loading com.test.zim.Demo$User in custom classloader
SFWeb: super.loadclass(java.lang.System)
Ejb: super.loadclass(java.lang.System)
SFWeb: super.loadclass(java.lang.StringBuilder)
Ejb: super.loadclass(java.lang.StringBuilder)
SFWeb: super.loadclass(java.lang.Class)
Ejb: super.loadclass(java.lang.Class)
SFWeb: super.loadclass(java.io.PrintStream)
Ejb: super.loadclass(java.io.PrintStream)
Logging in with User loaded in SFWeb
SFWeb: super.loadclass(com.test.zim.Demo$LoginEJB)
Ejb: Loading com.test.zim.Demo$LoginEJB in custom classloader
Ejb: super.loadclass(java.lang.Object)
Ejb: super.loadclass(java.lang.System)
Ejb: super.loadclass(java.io.PrintStream)
LoginEJB loaded
Examining methods of LoginEJB
Ejb: already loaded(com.test.zim.Demo$LoginEJB)
Ejb: Loading com.test.zim.Demo$User in custom classloader
java.lang.LinkageError: loader constraint violation: loader (instance of com/test/zim/Demo$EjbCL) previously initiated loading for a different type with name "com/test/zim/Demo$User"
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
at java.lang.ClassLoader.defineClass(ClassLoader.java:642)
at com.test.zim.Demo$CustomCL.findClass(Demo.java:39)
at com.test.zim.Demo$CustomCL.loadClass(Demo.java:57)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at java.lang.Class.getDeclaredMethods0(Native Method)
at java.lang.Class.privateGetDeclaredMethods(Class.java:2701)
at java.lang.Class.privateGetPublicMethods(Class.java:2902)
at java.lang.Class.getMethods(Class.java:1615)
at com.test.zim.Demo.test1(Demo.java:117)
at com.test.zim.Demo.main(Demo.java:146)
I really didn't get why it show the linkage error, and when I change the code
sfWebCL.loadClass("com.test.zim.Demo$Servlet", false).getMethod("doGet").invoke(null);
to
sfWebCL.loadClass("com.test.zim.Demo$Servlet", false).getMethod("doGet");
sfWebCL.loadClass("com.test.zim.Demo$User", false);
sfWebCL.loadClass("com.test.zim.Demo$LoginEJB", false).getMethods();
The error disappeared, I've got some questions:
Before I change the code, how is the linkageError happened? I think the class "com/test/zim/Demo$User" should coexist in two different classloader without any problem, since the two classloader invoke their own defineClass method? And why the error said ejbClassLoader load the User class with a different name, I think it's the first time EJBClassLoader load the User class?
After I change the code, I write some code to load the "User" and "LoginEJB" class manually, instead of invoke the "doGet()" method, what's the difference between the two code snippet, why there is no error in the latter code?
The error was occur in the ClassLoader.defineClass() phase, what does defineClass() really mean?
The ClassLoader.findLoadedClass() method, is it mean find the class (eg Foo.class) the ClassLoader ever load? If its parent ClassLoader have loaded the Foo.class before, it should return true. But if the child classloader load the Foo.class firstly, the parent classloader should load it again, and there would be two Foo.class, and they will coexist without problem?
Here is what I would like to achieve. We have an application that is running as a servlet on an IBM Domino server.
The application uses resource bundle to get translated messages and labels according to the browser language.
We want to enable customers to override some of the values.
We cannot modify the bundle_lang.properties files in the .jar at runtime.
So the idea was to provide additional bundleCustom_lang.properties files along with the .jar
This bundle could be loaded at runtime using
private static void addToClassPath(String s) throws Exception {
File file = new File(s);
URLClassLoader cl = (URLClassLoader) ClassLoader.getSystemClassLoader();
java.lang.reflect.Method m = URLClassLoader.class.getDeclaredMethod("addURL", new Class[] { URL.class });
m.setAccessible(true);
m.invoke(cl, new Object[] { file.toURI().toURL() });
}
So far, so good, this works in Eclipse. Here I had the bundleCustom files in a directory outside the workspace ( /volumes/DATA/Temp/ )
Once the addition ResourceBundle is available, We check this bundle for the key first. If it returns a value than this value is being used for the translation. If no value is returned, or the file does not exist, the value from the bundle inside the .jar is used.
My full code is here
public class BundleTest2 {
static final String CUSTOM_BUNDLE_PATH = "/volumes/DATA/Temp/";
static final String CUSTOM_BUNDLE_MODIFIER = "Custom";
public static void main(String[] args) {
try {
addToClassPath(CUSTOM_BUNDLE_PATH);
System.out.println(_getTranslation("LabelBundle", "OutlineUsersAllVIP"));
} catch (Exception e) {
}
}
private static String _getTranslation(String bundle, String translation) {
return _getTranslation0(bundle, new Locale("de"), translation);
}
private static String _getTranslation0(String bundle, Locale locale, String key) {
String s = null;
try {
try {
ResourceBundle custom = ResourceBundle.getBundle(bundle + CUSTOM_BUNDLE_MODIFIER, locale);
if (custom.containsKey(key)) {
s = custom.getString(key);
}
} catch (MissingResourceException re) {
System.out.println("CANNOT FIND CUSTOM RESOURCE BUNDLE: " + bundle + CUSTOM_BUNDLE_MODIFIER);
}
if (null == s || "".equals(s)) {
s = ResourceBundle.getBundle(bundle, locale).getString(key);
}
} catch (Exception e) {
}
return s;
}
private static void addToClassPath(String s) throws Exception {
File file = new File(s);
URLClassLoader cl = (URLClassLoader) ClassLoader.getSystemClassLoader();
java.lang.reflect.Method m = URLClassLoader.class.getDeclaredMethod("addURL", new Class[] { URL.class });
m.setAccessible(true);
m.invoke(cl, new Object[] { file.toURI().toURL() });
}
}
When I try the same from inside the servlet, I get a MissingResourceException.
I also tried to put the .properties files into a customization.jar and provide the full path ( incl. the .jar ) when invoking addToClassPath().
Apparently, the customization.jar is loaded ( it is locked in the file system ), but I still get the MissingResourceException.
We already use the same code in addToClassPath to load a Db2 driver and this is working as expected.
What am I missing?
Why don't you use Database to store the overriden translations? Persisting something crated by client in the local deployment of application is generally not a good idea, what will happen if you redeploy the app, will these resources be deleted? What if you have to run another node of your app, how will you replicate the custom properties file?
I'm currently developing an Eclipse Neon editor plug-in. At the moment I'm trying to be able to open files from the filesystem, which weren't created inside of Eclipse. To accomplish that, I need to get an IProject in the following method:
public static IProject getProject(IStorage storage) {
if (storage instanceof IFile) {
return ((IFile) storage).getProject();
}
else if (storage instanceof IJarEntryResource) {
return ((IJarEntryResource) storage).getPackageFragmentRoot().getJavaProject().getProject();
}
else if (storage instanceof FileStorage) {
// ????
}
else {
throw new IllegalArgumentException("Unknown IStorage implementation");
}
}
Here FileStorage is an own implementation of the IStorage interface. And looks like that:
public class FileStorage implements IStorage {
private final FileStoreEditorInput editorInput;
FileStorage( FileStoreEditorInput editorInput ) {
this.editorInput = editorInput;
}
#Override
public <T> T getAdapter( Class<T> adapter ) {
return Platform.getAdapterManager().getAdapter( this, adapter );
}
#Override
public boolean isReadOnly() {
return false;
}
#Override
public String getName() {
return editorInput.getName();
}
#Override
public IPath getFullPath() {
return new Path( URIUtil.toFile( editorInput.getURI() ).getAbsolutePath() );
}
#Override
public InputStream getContents() {
try {
return editorInput.getURI().toURL().openStream();
} catch( IOException e ) {
throw new UncheckedIOException( e );
}
}
}
Is there any way to get an IProject from this FileStorage?
You can try and get an IFile for a file using:
IPath path = ... absolute path to the file
IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
IFile file = root.getFileForLocation(path);
if (file != null) {
IProject project = file.getProject();
...
}
But this will only work for a file inside the workspace. For anything outside of the workspace you can't have a project.
In short: No. The FileStorage class is meant to represent IStorage instances for files that are located outside of the workspace. Therefore they are not contained in any workspace project and it is not possible to obtain an IProject for them.
I've tested on three windows machines, and two linux VPSes, on different versions of Java, both on the OpenJDK & Oracle JDK. It functioned perfectly, and then all of a sudden, it only works in my IDE, though I haven't changed any relevant code, and I can't imagine what can cause this.
Prevalent code in system:
Class<?> cls = (session == null ? secjlcl : session.getJLCL()).loadClass(name);
Logger.log(JavaLoader.class.isAssignableFrom(cls) + " - " + cls + " - " + cls.getSuperclass().getName());
if (JavaLoader.class.isAssignableFrom(cls)) {
And my ClassLoader:
public class JavaLoaderClassLoader extends URLClassLoader {
public JavaLoaderClassLoader(URL[] url, ClassLoader parent) {
super(url);
}
private HashMap<String, Class<?>> javaLoaders = new HashMap<String, Class<?>>();
public String addClass(byte[] data) throws LinkageError {
Class<?> cls = defineClass(null, data, 0, data.length);
javaLoaders.put(cls.getName(), cls);
return cls.getName();
}
public Class<?> loadClass(String name, boolean resolve) {
if (javaLoaders.containsKey(name)) return javaLoaders.get(name);
try {
Class<?> see = super.loadClass(name, resolve);
if (see != null) return see;
}catch (ClassNotFoundException e) {
Logger.logError(e);
}
return null;
}
public void finalize() throws Throwable {
super.finalize();
javaLoaders = null;
}
}
One note, I expect many classloaders to load a different file in the same name/package, so I use separate classloaders to keep them separate, however in testing, that was NOT tested.
Now, this had worked flawlessly in the past, and I have zero clue why it stopped. I would assume I broke something, but the code still works in my IDE?
This appears to be your bug:
public JavaLoaderClassLoader(URL[] url, ClassLoader parent) {
super(url);
}
You aren't installing parent as the parent class loader through the super constructor.
I have the following problem.
A HashMap is used to set properties and the key is a ClassLoader.
The code that sets a property is the following (AxisProperties):
public static void setProperty(String propertyName, String value, boolean isDefault){
if(propertyName != null)
synchronized(propertiesCache)
{
ClassLoader classLoader = getThreadContextClassLoader();
HashMap properties = (HashMap)propertiesCache.get(classLoader);
if(value == null)
{
if(properties != null)
properties.remove(propertyName);
} else
{
if(properties == null)
{
properties = new HashMap();
propertiesCache.put(classLoader, properties);
}
properties.put(propertyName, new Value(value, isDefault));
}
}
}
One of these values is cached somewhere and I need to reset this hashmap but the problem is I don't know how to do this.
I thought to load the class (delegating to axis using a URLClassLoader) but I see that the code does getThreadContextClassLoader(); which is:
public ClassLoader getThreadContextClassLoader()
{
ClassLoader classLoader;
try
{
classLoader = Thread.currentThread().getContextClassLoader();
}
catch(SecurityException e)
{
classLoader = null;
}
return classLoader;
}
So I think it will use the classloader of my current thread not the one that I used to load the class to use (i.e. axis).
So is there a way around this?
Note: I already have loaded axis as part of my application. So the idea would be to reload it via a different classloader
If you know the class loader in question, you can set the context class loader before making the call into axis:
ClassLoader key = ...;
ClassLoader oldCtx = Thread.currentThread().getContextClassLoader();
try {
Thread.currentThread().setContextClassLoader(key);
// your code here.
}
finally {
Thread.currentThread().setContextClassLoader(oldCtx);
}
You often have to do this in cases when you are outside a servlet container, but the library assumes that you are in one. For instance, you have to do this with CXF in an OSGi container, where the semantics of context class loader are not defined. You can use a template like this to keep things clean:
public abstract class CCLTemplate<R>
{
public R execute( ClassLoader context )
throws Exception
{
ClassLoader oldCtx = Thread.currentThread().getContextClassLoader();
try {
Thread.currentThread().setContextClassLoader(context);
return inContext();
}
finally {
Thread.currentThread().setContextClassLoader(oldCtx);
}
}
public abstract R inContext() throws Exception;
}
And then do this when interacting with Axis:
ClassLoader context = ...;
new CCLTemplate<Void>() {
public Void inContext() {
// your code here.
return null;
}
}.execute(context);