In JEE environment it is useful to know, where a particular class is loaded from.
For example I have an instance of org.slf4j.Logger provided by a some black box library. Is it possible to find the responsible classloader? If the class of the instance comes from JDK, Application Server, EAR or Web Application classloader?
It turns out to be quite simple. The name of the classloader is returned by:
object.getClass().getClassLoader().getName()
It returns something like "app" or "platform". Be careful - classloader is null, if the class belongs to the bootstrap classloader, like the JUL logger java.util.logging.Logger does.
WebLogic has a long chain of classloaders without names. WebLogic's classloaders contain a useful field annotation. One can read it to find the JEE application, the classloader belongs to:
public Object getAnnotation(ClassLoader classloader) {
try {
Method amethod = classloader.getClass().getMethod("getAnnotation");
return amethod.invoke(classloader);
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
return "";
}
}
If you want to print an overview of all available classloader without digging for annotations, the hashcode of the classloader instance can be used. Here is a small JSP page. Put it into the webapp directory of your web project to get a quick overview.
<%
response.setContentType("text/plain");
List<Class<?>> clazzes = List.of(Logger.class, object.getClass());
out.println("CLASSLOADERS:\n");
ClassLoader classloader = new Object() {}.getClass().getClassLoader();
while (classloader != null) {
out.println(
String.format("%-18s", classloader.getName()) + " " +
String.format("%-10s", Integer.toHexString(classloader.hashCode())) + " " +
classloader.getClass().getName() + " / " + getAnnotation(classloader));
classloader = classloader.getParent();
}
out.println("\nCLASSES:\n");
for (Class<?> clazz : clazzes) {
ClassLoader cloader = clazz.getClassLoader();
URL location = Optional.of(clazz.getProtectionDomain()).map(x->x.getCodeSource()).map(x->x.getLocation()).orElse(null);
out.println(
clazz + " \n " +
(cloader != null ? Integer.toHexString(cloader.hashCode()) : "<bootstrap>") + "\t" +
location);
}
%>
Related
I am in the process of migrating existing VMSS tomcat project to AKS. In VM, we use server.xml to provide environment variables using <Environment .../> tags. For Kubernetes, I am using env: field to replace these values.
In the project, there is code Config.java:
private Object getObject(String key) {
try {
log.debug("looking up key: " + key + " in environment context.");
Object rtn = getEnvContext().lookup(key);
log.debug("Found property for " + key + ": " + rtn);
return rtn;
} catch (NamingException e) {
log.debug("Unable to find key: " + key);
return null;
}
}
private Context getEnvContext() throws NamingException {
Context initCtx = new InitialContext();
Context envCtx = (Context) initCtx.lookup("java:comp/env");
return envCtx;
}
From what is java:comp/env?, it says that java:comp/env is the node in the JNDI tree where you can find properties for the current Java EE component.
Here are my questions.
When tomcat starts, does it use server.xml <Environment .../> tag to create properties in the node in the JNDI tree? (thus, when .getObject(someString) is called, it can get the value defined in server.xml)
If I want to replace server.xml with Helm chart env field, will these properties (defined in Helm) be defined in the node in the JNDI tree? or do I need to define these beans in jndi.xml as JndiObjectFactoryBean?
I am sorry if I am not making sense at all here. I am very new to tomcat and concept of JNDI and JNDI tree. Please let me know if I need to clarify myself.
I have written a simple Plugin system so that I can include extensions from external sources. The Plugin Manager loads plugins from predetermined application and user plugins directories. I use a custom URLClassLoader so that I can sandbox all Plugins. Here's the general approach:
Locate the Plugin's JAR file in one of the predetermined directories.
If the Plugin has already been loaded, return the instance of the Plugin that has already been created.
If an instance of the custom URLClassLoader has not been created for the directory containing the Plugin, create one.
Loop through each class in the Plugin's JAR file looking for Classes that implement the PluginInterface by loading the Class using Class.forName( pluginName, false, PluginClassLoader ) and then testing to see if the PluginInterface isAssignableFrom the loaded Class.
If the Class implements the PluginInterface then a new instance of the Class is created, initialized, and saved for later use.
All of this works great when I run it from within the Eclipse IDE. But, when I run it outside of the IDE, the check to see if the Plugin implements the PluginInterface fails. I believe this is because under Eclipse, both the Plugin and the Interface have a related (parent or child) ClassLoader (namely sun.misc.Launcher$AppClassLoader#73d16e93) and outside of Eclipse the PluginInterface has an unrelated ClassLoader (namely java.net.URLClassLoader#14ae5a5).
Here's the code:
The custom ClassLoader:
public class PROD_PluginClassLoader extends URLClassLoader {
protected PROD_PluginClassLoader( URL pluginFileUrl ) {
super( new URL[] { pluginFileUrl } );
}
protected PROD_PluginClassLoader( String pluginFilePath ) throws MalformedURLException {
super( new URL[] { new File( pluginFilePath ).toURI().toURL() } );
}
protected PROD_PluginClassLoader( URL[] pluginFileUrls ) {
super( pluginFileUrls );
}
}
The PluginLoader:
private static List< String > loadedPlugins = new ArrayList<>();
private static List< PROD_PluginInterface > plugins = new ArrayList<>();
private static Map< String, PROD_PluginClassLoader > pluginClassLoaders = new HashMap<>();
protected static void getPluginInstance( String pluginName, String pluginFilePath ) {
try {
PROD_Utilities.printDebug( "Loading plugin name(" + pluginName + ") from(" + pluginFilePath + ")" );
if ( !pluginClassLoaders.containsKey( pluginFilePath ) ) pluginClassLoaders.put( pluginFilePath, new PROD_PluginClassLoader( pluginFilePath ) );
PROD_PluginClassLoader pLoader = pluginClassLoaders.get( pluginFilePath );
boolean pluginLoaded = false;
for ( String n : PROD_Utilities.getClassNamesFromJarFile( pluginFilePath ) ) {
Class<?> pClass = Class.forName( n, false, pLoader );
String interfaces = "";
for ( Class<?> c : pClass.getInterfaces() ) interfaces += "," + c.getName();
if ( !interfaces.isEmpty() ) interfaces = interfaces.substring( 1 );
PROD_Utilities.printDebug( String.format( "Plugin name(%s) object class(%s) super(%s) interfaces(%s) isPlugin(%b)", pluginName, pClass.getName(), pClass.getSuperclass().getName(), interfaces, PROD_PluginInterface.class.isAssignableFrom( pClass ) ) );
if ( pClass.getInterfaces().length > 0 )
PROD_Utilities.printDebug(
String.format(
"pClass loader(%s) parent(%s) pClass interface loader(%s) parent(%s) PROD_PluginInterface loader(%s) parent(%s)"
,pClass.getClassLoader()
,pClass.getClassLoader().getParent()
,pClass.getInterfaces()[0].getClassLoader()
,pClass.getInterfaces()[0].getClassLoader().getParent()
,PROD_PluginInterface.class.getClassLoader()
,PROD_PluginInterface.class.getClassLoader().getParent()
));
if ( PROD_PluginInterface.class.isAssignableFrom( pClass ) ) {
Class<? extends PROD_Plugin> newClass = pClass.asSubclass( PROD_Plugin.class );
Constructor<?> constructor = newClass.getConstructor();
setPluginSandbox();
plugins.add( ( PROD_PluginInterface ) constructor.newInstance() );
plugins.get( plugins.size()-1 ).pluginInitialization();
unsetPluginSandbox();
pluginLoaded = true;
}
}
if ( pluginLoaded ) loadedPlugins.add( pluginName.toLowerCase() );
else PROD_Utilities.printError( "Plugin (" + pluginName + ") is not a valid PROD plugin." );
} catch ( InstantiationException | IllegalAccessException | IllegalArgumentException
| InvocationTargetException | ClassNotFoundException | NoSuchMethodException
| SecurityException | MalformedURLException e ) {
PROD_Utilities.printError( "Could not load plugin (" + pluginName + ").", e.getMesprod() );
}
}
The debug information when running under Eclipse:
Debug: PROD_PluginManager.getPluginInstance().line(138): Loading plugin name(proddb) from(C:\Users\userid\eclipse-workspace\prod\plugins\proddb.jar)
Debug: PROD_PluginManager.getPluginInstance().line(147): Plugin name(proddb) object class(com.company.prod.proddb.PRODDB) super(com.company.prod.PROD_Plugin) interfaces(com.company.prod.PROD_PluginInterface) isPlugin(true)
Debug: PROD_PluginManager.getPluginInstance().line(149): pClass loader(com.company.prod.PROD_PluginClassLoader#5ec0a365) parent(sun.misc.Launcher$AppClassLoader#73d16e93) pClass interface loader(sun.misc.Launcher$AppClassLoader#73d16e93) parent(sun.misc.Launcher$ExtClassLoader#55f96302) PROD_PluginInterface loader(sun.misc.Launcher$AppClassLoader#73d16e93) parent(sun.misc.Launcher$ExtClassLoader#55f96302)
The debug information when running outside of Eclipse:
Debug: PROD_PluginManager.getPluginInstance().line(138): Loading plugin name(proddb) from(C:\Users\userid\eclipse-workspace\prod\plugins\proddb.jar)
Debug: PROD_PluginManager.getPluginInstance().line(147): Plugin name(proddb) object class(com.company.prod.proddb.PRODDB) super(com.company.prod.PROD_Plugin) interfaces(com.company.prod.PROD_PluginInterface) isPlugin(false)
Debug: PROD_PluginManager.getPluginInstance().line(149): pClass loader(com.company.prod.PROD_PluginClassLoader#12405818) parent(sun.misc.Launcher$AppClassLoader#55f96302) pClass interface loader(sun.misc.Launcher$AppClassLoader#55f96302) parent(sun.misc.Launcher$ExtClassLoader#3d3fcdb0) PROD_PluginInterface loader(java.net.URLClassLoader#14ae5a5) parent(null)
I find it very strange that the PluginInterface ClassLoader has changed to a URLClassLoader.
I believe the problem is that the PluginInterface and the Plugin don't share a related ClassLoader and thus the Plugin's PluginInterface is technically a different Java interface from the Application's PluginInterface. If that assessment is correct then my question is how do I fix this so that the PluginInterface and the Plugin do share a related ClassLoader?
Or, perhaps my assessment is incorrect. In which case, my question is why doesn't the Plugin appear to implement the PluginInterface?
I've been wrestling with this for several days now so thanks in advance for any and all answers.
Edit
How is my code (not the plugin) loaded?
From within Eclipse: using the Eclipse Run -> Run menu option.
From outside of Eclipse: java -jar prod.jar
Well after a VERY long time, I finally figured this out and am posting my findings here in case anyone else runs across a similar issue with isAssignableFrom.
When exporting the program from Eclipse using the Export Wizard for Java->Runable JAR file, I chose Library handling option Package required libraries into generated JAR and was getting the isAssignableFrom failure as described in the original post. After re-exporting using Library handling option Extract required libraries into generated JAR, everything worked as expected.
I wanted to retrieve values from my context.xml, and I've found this snippet of code to do so:
// Acquire an instance of our specified bean class
MyBean bean = new MyBean();
// Customize the bean properties from our attributes
Reference ref = (Reference) obj;
Enumeration addrs = ref.getAll();
while (addrs.hasMoreElements()) {
RefAddr addr = (RefAddr) addrs.nextElement();
String name = addr.getType();
String value = (String) addr.getContent();
if (name.equals("foo")) {
bean.setFoo(value);
} else if (name.equals("bar")) {
try {
bean.setBar(Integer.parseInt(value));
} catch (NumberFormatException e) {
throw new NamingException("Invalid 'bar' value " + value);
}
}
}
// Return the customized instance
return (bean);
I wanted to know if there was a method to do the exact same thing but with less steps
a web application on Tomcat 8.0
Tomcat 8.0 has reached End of Life. Do not use it. See "Migration Guide" at tomcat.apache.org to upgrade to Tomcat 8.5 or 9.0.
See "JDNI Resources" in Tomcat documentation. E.g. factory="org.apache.naming.factory.BeanFactory" can be used to create an arbitrary bean.
If you just need a set of configurable properties, defining them with "Parameter" element in Context will be easier. A web application will get those values via javax.servlet.ServletContext.getInitParameter(name) API.
When using a directory-expression for an <int-file:outbound-gateway> endpoint, the method below is called on org.springframework.integration.file.FileWritingMessageHandler:
private File evaluateDestinationDirectoryExpression(Message<?> message) {
final File destinationDirectory;
final Object destinationDirectoryToUse = this.destinationDirectoryExpression.getValue(
this.evaluationContext, message);
if (destinationDirectoryToUse == null) {
throw new IllegalStateException(String.format("The provided " +
"destinationDirectoryExpression (%s) must not resolve to null.",
this.destinationDirectoryExpression.getExpressionString()));
}
else if (destinationDirectoryToUse instanceof String) {
final String destinationDirectoryPath = (String) destinationDirectoryToUse;
Assert.hasText(destinationDirectoryPath, String.format(
"Unable to resolve destination directory name for the provided Expression '%s'.",
this.destinationDirectoryExpression.getExpressionString()));
destinationDirectory = new File(destinationDirectoryPath);
}
else if (destinationDirectoryToUse instanceof File) {
destinationDirectory = (File) destinationDirectoryToUse;
} else {
throw new IllegalStateException(String.format("The provided " +
"destinationDirectoryExpression (%s) must be of type " +
"java.io.File or be a String.", this.destinationDirectoryExpression.getExpressionString()));
}
validateDestinationDirectory(destinationDirectory, this.autoCreateDirectory);
return destinationDirectory;
}
Based on this code I see that if the directory to use evaluates to a String, it uses that String to create a new java.io.File object.
Is there a reason that a ResourceLoader couldn't/shouldn't be used instead of directly creating a new file?
I ask because my expression was evaluating to a String of the form 'file://path/to/file/' which of course is an invalid path for the java.io.File(String) constructor. I had assumed that Spring would treat the String the same way as it treats the directory attribute on <int-file:outbound-gateway> and pass it through a ResourceLoader.
Excerpt from my configuration file:
<int-file:outbound-gateway
request-channel="inputChannel"
reply-channel="updateTable"
directory-expression="
'${baseDirectory}'
+
T(java.text.MessageFormat).format('${dynamicPathPattern}', headers['Id'])
"
filename-generator-expression="headers.filename"
delete-source-files="true"/>
Where baseDirectory is a property that changes per-environment of the form 'file://hostname/some/path/'
There's no particular reason that this is the case, it probably just wasn't considered at the time of implementation.
The request sounds reasonable to me and will benefit others (even though you have found a work-around), by providing simpler syntax. Please open an 'Improvement' JIRA issue; thanks.
While not directly answering the question, I wanted to post the workaround that I used.
In my XML configuration, I changed the directory-expression to evaluate to a file through the DefaultResourceLoader instead of a String.
So this is what my new configuration looked like:
<int-file:outbound-gateway
request-channel="inputChannel"
reply-channel="updateTable"
directory-expression=" new org.springframework.core.io.DefaultResourceLoader().getResource(
'${baseDirectory}'
+
T(java.text.MessageFormat).format('${dynamicPathPattern}', headers['Id'])).getFile()
"
filename-generator-expression="headers.filename"
delete-source-files="true"/>
I need to access run-time information of running drools engine in my java web-app.
Things i need to know:
what are the active rules in run-time at any instant?
How much objects are inserted into session till now?
Are there some classes that let you access the information of drools run-time?
Thanks in advance
You just need to examine the KnowledgeBase and StatefulKnowledgeSession classes. The following methods demonstrate how to get hold of all rules in your knowledge base, and all facts in working memory.
/**
* Get a String showing all packages and rules in a knowledge base.
*/
public String kbString(KnowledgeBase kbase) {
StringBuilder sb = new StringBuilder();
for (KnowledgePackage p : kbase.getKnowledgePackages()) {
sb.append("\n Package : " + p.getName());
for (Rule r : p.getRules()) {
sb.append("\n Rule: " + r.getName());
}
}
return "Knowledge base built with packages: " + sb.toString();
}
/**
* Get a String showing the facts currently in working memory,
* and their details.
*
* #param session The session to search for facts.
*/
public String sessionFactsString(StatefulKnowledgeSession session) {
StringBuilder sb = new StringBuilder();
sb.append("\nThe following facts are currently in the system...");
for (Object fact : session.getObjects()) {
sb.append("\n\nFact: " + DroolsUtil.objectDetails(fact));
}
return sb.toString();
}
Edit for clarity - The objectDetails(Object) method above is a method for rendering any old Java bean as a String, using Apache Commons BeanUtils. It looks like this:
public static String objectDetails(Object o) {
StringBuilder sb = new StringBuilder(o.getClass().getSimpleName());
try {
#SuppressWarnings("unchecked")
Map<String, Object> objectProperties = BeanUtils.describe(o);
for (String k : objectProperties.keySet()) {
sb.append(", " + k + "=\"" + objectProperties.get(k) + "\"");
}
} catch (IllegalAccessException e) {
return "IllegalAccessException attempting to parse object.";
} catch (InvocationTargetException e) {
return "InvocationTargetException attempting to parse object.";
} catch (NoSuchMethodException e) {
return "NoSuchMethodException attempting to parse object.";
}
return sb.toString();
}
For debug purposes you can also add these listeners to the KnowledgeSession
Drools has an event model that exposes much of whats happening
internally, two default debug listeners are supplied
DebugAgendaEventListener and DebugWorkingMemoryEventListener which
print out debug event information to the err console, adding listeners
to a session is trivial and shown below. The WorkingMemoryFileLogger
provides execution auditing which can be viewed in a graphical viewer;
it's actually a specialised implementation built on the agenda and
working memory listeners, when the engine has finished executing
logger.writeToDisk() must be called.
ksession.addEventListener(new DebugAgendaEventListener()); // add 2 debug event listeners
ksession.addEventListener(new DebugWorkingMemoryEventListener());
// setup the audit logging
WorkingMemoryFileLogger logger = new WorkingMemoryFileLogger( session );
logger.setFileName( "log/helloworld" );