cancel
Showing results for 
Search instead for 
Did you mean: 

Isolating a portal component using custom classloaders (hard!)

Former Member
0 Kudos

Tough problem coming up.

If you haven't read the thread Inqmyxml.jar exclusing at https://forums.sdn.sap.com/thread.jspa?forumID=41&threadID=22479&messageID=202313#202313

please do that first.

Due to recent problems with webservices in early version of the portal, I'm currently doing a proof of concept on using AXIS to call webservices in the portal.

Of course the classloader problem shows up here, since the xml parser classes are allready loaded by through the classloader of the portal component.

I have however managed to successfully implement total isolation with two different methods, but I feel none of them are good enough for using in a production system.

1. The first thing I did was to place all the jar files I needed for AXIS (including xerces) in a folder AxisEPPoC\dist\PORTAL-INF\lib_internal.

This only so that the portal component classloader doesn't know of them

2. Then I made a fucntion which starts on the private resource path and retrieves all the jar files under it as URL's

3. This was then feed into my own custom parent last classloader (but it also works with the URLClassloader if you set the parent to null). My own custom parent last classloader, basically loads the class in the current classloader if it exist and the class is not a part of the java package, other wise it delegates up to the parent if any parent is set (but I used it without a parent)

4. Then I use the tip from Armin by calling


Thread thread = Thread.currentThread();		ClassLoader orgContextClassLoader =thread.getContextClassLoader();
//myClassloader is my custom parent last classloader
thread.setContextClassLoader(myClassLoader);

//in finally clause of the try I revert
} finally {			thread.setContextClassLoader(orgContextClassLoader);
}

I had hoped this was enough, but I still get an exception when I instansiate the DirectoryLocator of Axis indicating that it is using the xml parser of the portal

Caused by: java.lang.ExceptionInInitializerError: java.lang.ClassCastException: com.inqmy.lib.jaxp.SAXParserFactoryImpl

at javax.xml.parsers.SAXParserFactory.newInstance(Unknown Source)

at org.apache.axis.utils.XMLUtils.initSAXFactory(XMLUtils.java:218)

at org.apache.axis.utils.XMLUtils.<clinit>(XMLUtils.java:119)

at org.apache.axis.configuration.FileProvider.configureEngine(FileProvider.java:209)

at org.apache.axis.AxisEngine.init(AxisEngine.java:187)

at org.apache.axis.AxisEngine.<init>(AxisEngine.java:172)

at org.apache.axis.client.AxisClient.<init>(AxisClient.java:88)

at org.apache.axis.client.Service.getAxisClient(Service.java:143)

at org.apache.axis.client.Service.<init>(Service.java:152)

at org.tempuri.DirectoryLocator.<init>(DirectoryLocator.java:10)

at java.lang.Class.newInstance0(Native Method)

at java.lang.Class.newInstance(Class.java:232)

at com.company.portal.component.AxisEPPoC.doContent(AxisEPPoC.java:47)

5. Since the above failed, I implemented the solution using the java.reflection API on my custom classloader

Basically doing calls like


Class directoryLocatorClass = myClassLoader.loadClass("org.tempuri.DirectoryLocator") ; 
Object directoryLocatorObj =  directoryLocatorClass.newInstance();

Method getDirectorySoapMethod = directoryLocatorClass.getMethod("getDirectorySoap", null);
Object directorySoapObj = getDirectorySoapMethod.invoke(directoryLocatorObj, null);

This works without a problem.

But coding with reflection is not very practical, you lose compile time checking and there is no way to get results in any complex data type (only java.lang types).

6. So I tried one more thing.

This time I created a class which extends a thread and put the webservice call in the run method.

Then I used Armin's trick from 4. and afterwards created a new thread.


Thread axisThread = ((Thread)myClassLoader.loadClass("com.company.portal.component.AxisThread").newInstance());
axisThread.run();

And this worked!

But then it hit me that all this was to no avail, as communicating with the thread in any orderly fashion was impossible (as far as I could think of).

Anyone have any ideas how to solve this (or if it is solvable?)

Accepted Solutions (0)

Answers (1)

Answers (1)

detlev_beutner
Active Contributor
0 Kudos

Hi Dagfinn,

I have some questions, but maybe these start to help

To start, I've never worked with Axis, so I don't know anything about it's inner structure.

As far as I understand the aim is to kick out inqmyxml and let Axis use a different implementation (Xerces?). Right?

You wrote that you excluded the classes from the java package from your parent-last-classloader - does this also hold for javax.*? How about the idea - to make it generic - to look inside the found JARs and to apply the parent-last-strategy only to the found classes?

I don't know to what "tip of Armin" you are referring - can it be that you are talking about my hint using ThreadContextClassloader?

If so, my question is: Does Axis make use of the ThreadContextClassloader? (At a first glance, it does not seem to, at least not in the field we are discussing.)

The next thing is that when trying to instantiate DirectoryLocator (done by DefaultConstructor?), in XMLUtils.java:218 Axis tries to load a SaxParserFactory using SAXParserFactory.newInstance(); (see http://cvs.apache.org/viewcvs.cgi/ws-axis/java/src/org/apache/axis/utils/XMLUtils.java?rev=1.101&vie... ) (because the method is called from static initializer with null as factoryClassName). This means it uses the system property javax.xml.parsers.SAXParserFactory to load the class - this will be set to com.inqmy.lib.jaxp.SAXParserFactoryImpl. The problem is that you shouldn't set this property to some Xerces class for other threads using this property in parallel...

In the end, for using reflection it works, it seems that the ThreadContextClassloader does not get used?!

For the thread alternative, I don't know what communication you want; if you just want some return value within you "main" thread, you could do it this way:


ReturnValueBean rvb = new ReturnValueBean();
axisThread.setReturnValueBean(rvb);
axisThread.run();
axisThread.join();
SomeDetailedReturnValue = rvb.getSomeDetail();

and just write return values into the bean within run() method. But I'm quite unsure if that is enough for you and if the whole technique won't lead to other ClassCastException later on...

I hope we get closer...

Best regards

Detlev

Former Member
0 Kudos

I was wondering when you were going to reply

You are correct in that the aim is to kick out inqmyxml and let Axis use a different implementation Xerces.

I did a minor hack for the javax classes in myclassloader so that they were loaded from the classloader if they existed, but if not and there was no parent classloader it was loaded from the system classloader.

The load class method is defined as


public synchronized Class loadClass(String className) throws ClassNotFoundException
		{
			//all java.* are handled by the system classloader (but not javax classes)
			if (className.indexOf("java.") == 0)
			{
				return getSystemClassLoader().loadClass(className);
			}
			//check cache
			Class cachedClass = findLoadedClass(className);
			if (cachedClass == null)
			{
				try
				{
					Class loadedClass = findClass(className);
					System.out.println("Loaded class:"+className);
					return loadedClass;
				}
				catch (ClassNotFoundException e)
				{
					if(!useParent){
						//system classloader might have reference to javax classes
						//but we only ask if we don't have it ourself
						if(className.indexOf("javax.") == 0){
							return getSystemClassLoader().loadClass(className);
						}
						throw e;
					}else {
						//let our parent classloader handle it
						return getParent().loadClass(className);
					}
				}
			}else {
				System.out.println ("Returning cached class:" +className );
				return cachedClass;
			}
		}
	}

The Armin tip is the setContextClassLoader call in the Thread class. I would also belive that this would not need to be included, but if it isn't it called first, the solution fails with the same classcastexception (for both the reflection and the new thread solution). Why this is, I haven't figured out yet.

The tip about the system property is of great value. Perhaps I'll find a method of using this (although I can't change the system property). I'll probably continue looking more in this direction.

The thread return value solution might work, but I see two problems.

1. You cannot call the method setReturnValueBean() from the component , since it only nows the object as an instance of the Thread class. But I could use reflection for this one call.

I don't think I can make a new interface which the axisThread implements and that the component uses, since it will be loaded by different classloaders, and therefore it is not the same (unless the setContextClassLoader loads the interface in the custom classloader's context).

2. I can only return primitive type defined in java.lang, since otherwise I have to load those classes as well (see classloader discussion in point 1).

Right now, I am feeling the urge to use aspects to change the implementation of javax.xml.parsers.SAXParserFactory.newInstance() so that it returns the Xerces parser if the caller is from org.axis .

Thanks for your help so far

Former Member
0 Kudos

Actually, the best solution is probably to change the org.apache.axis.utils.XMLUtils.initSAXFactory method so that it always returns the Xerces parser (though this might not be enough if the rest of the javax.xml classes are not compatible between xerces and inqmy

detlev_beutner
Active Contributor
0 Kudos

Hi Dagfinn,

to start with cosmetics, className.indexOf("java.") == 0 reads much smoother as className.startsWith("java.").

Let's have a deeper look at the ClassCastException and why it only appears in the first case:

When calling SAXParserFactory.newInstance() from within XMLUtils, first the javax.xml.parsers.SAXParserFactory gets loaded by the portals classloader, because in the stacktrace part you sent the ContextClassloader does not get into the game. This loads com.inqmy.lib.jaxp.SAXParserFactoryImpl (this for looking at the system property), now using the thread context classloader (ie your classloader; see sources of SAXParserFactory and FactoryFinder by Sun). When now casting to SAXParserFactory and throwing a ClassCastException, the first classloader has been different. Crash.

For the reflection/thread solution, javax.xml.parsers.SAXParserFactory gets loaded by your classloader, so no ClassCastException in the end.

Anyhow, you don't want the inqmyml stuff...

> Why this is, I haven't figured out yet.

For the same reasons the ClassCastException reoccurs for the reflection/thread solutions when <i>not</i> setting the ThreadContextClassLoader. Because then javax.xml.parsers.SAXParserFactory gets loaded by your classloader and

com.inqmy.lib.jaxp.SAXParserFactoryImpl by the (not set) context classloader, but "If not set, the default is the ClassLoader context of the parent Thread." - that's the portal app classloader (so it's the same exception as the first, but just the other way round).

The problems you see I see too without going / checking in detail, that was my intuition " if the whole technique won't lead to other ClassCastException later on"...

So I hope to have shed some more light on to this issue... I also would suggest to work on org.apache.axis.utils.XMLUtils.

Hope it helps

Detlev

Former Member
0 Kudos

Hi Detlev,

To start with cosmetics , I actually though I used the startsWith method. Probably in another version. Luckily I can excuse myself by the fact that this is proof of concept code

Actually I think the solution might be easier than we thought (at least I hope) . I think my classloader loads the javax.xml.parser classes (as I have seen on my output), the only problem is that this class uses the system property. I haven't got the exact details , but I'll work at it some more tomorrow.

I see that the service class in Axis can be instansiated with a parser ( but then you require a QName as well). Hopefully, this will avoid having to tamper with the axis code, and only the generated proxy classes.

Cheers

Dagfinn

Turned some argument upside down

Former Member
0 Kudos

Arrggg,

I think I almost had it now with the call XMLUtils.initSAXFactory("org.apache.xerces.jaxp.SAXParserFactoryImpl" , true,false) ;

, but now apache commons logging is complaining:

Caused by: java.lang.ExceptionInInitializerError: org.apache.commons.logging.LogConfigurationException: org.apache.commons.logging.LogConfigurationException: org.apache.commons.logging.LogConfigurationException: Class org.apache.commons.logging.impl.Log4JLogger does not implement Log

at org.apache.commons.logging.impl.LogFactoryImpl.newInstance(LogFactoryImpl.java:532)

at org.apache.commons.logging.impl.LogFactoryImpl.getInstance(LogFactoryImpl.java:272)

at org.apache.commons.logging.LogFactory.getLog(LogFactory.java:414)

at org.apache.axis.components.logger.LogFactory.getLog(LogFactory.java:76)

at org.apache.axis.handlers.BasicHandler.<clinit>(BasicHandler.java:81)

at org.apache.axis.client.Service.getAxisClient(Service.java:143)

at org.apache.axis.client.Service.<init>(Service.java:152)

at org.tempuri.DirectoryLocator.<init>(DirectoryLocator.java:10)

This is due to their discovery mechanism (which doesn't use the context classloader)

http://www.qos.ch/logging/thinkAgain.jsp

detlev_beutner
Active Contributor
0 Kudos

Hi Dagfinn,

what a pity.

The article is great, I have always been one of these "Use the standard if possible in any way"-guys, and I prefer JDK1.4 logging, or if needed, log4j (but only if needed). Never really found commons-logging that helpful. Too bad anyway that EP6 SP2 / J2EE 6.20 remains on JDK 1.3.1 until it's death (and think of it, JDK 1.3.1 has started it's end-of-life period (see http://java.sun.com/j2se/1.3/ )... we are forced to work with an old, not very capable, almost dead candidate).

OK, many people are moving to NetWeaver, so we will do (are doing), but just have a look on EP5 questions here on SDN... shudder...

All these classloader issues are in the end really hard. It's just some weeks ago I struggled around to get Hibernate working for the whole J2EE engine - impossible on 6.20 for classloader problems; SAP promised to make it possible from SP9 on, didn't test it until now. What is really strange is that there are so many developers out there, "good" developers, but so(!) ignorant in what environments their software will run and what problems are caused.

Hope you don't get too frustrated

best regards

Detlev

Former Member
0 Kudos

Hi

I've actually managed to solve this by myself just as I was about to give up.

The key discovery is that "No ClassLoader is an island". You have to chain your own custom parent class loader with the portal classloader. Basically I have three classloaders

1. Which has all the generated axis classes from the wsdl file. Parent classloader is 2

2. Which has all the base axis files (all the jar files including xerces). It's parent is the "portal classloader"

3. The portal classloader, which loads your service/component

Since you can't change the classloader that loads a class, you need to seperate your service (it makes much more sense to use a service) in to two levels. The first one sets up the classloaders and uses one java reflection call to create an instance of the second level implementation via the new classloaders. In order for these classes to communicate properly you create an interface which is loaded by the "portal classloader"

Of course, solving this brings up some new questions:

1. Are we guaranteed that portal services are singletons (if not the overhead of axis could be too big)

2. Are all portal services loaded by the same classloader ? (if it is we can reuse the same classloaders between services)

I'll have a discussion with the customer about publishing the results at SDN, but at the moment I haven't got the time to do it.

Cheers

Dagfinn

Former Member
0 Kudos

Hi Dagfinn,

This post is good. Can you give your implementation code?

Thanks,

Wayne