2010-09-09

Dynamic method invocation in Java 6

I often read that people are waiting so much for dynamic method invocation promised for JDK 7 (which now is delayed - see Re-thinking JDK7).

What I wonder is: I already use dynamic class loading and dynamic method invocation in Java 6. Of course, it was a little work, but once done the dynamic class loading and object creation is a one-liner and so is the call to a method:

1:  /*  
2:   * DynTool.java  
3:   */  
4:  package at.mwildam.common;  
5:    
6:  import java.beans.Expression;  
7:  import java.io.File;  
8:  import java.lang.reflect.InvocationTargetException;  
9:  import java.lang.reflect.Method;  
10:  import java.net.MalformedURLException;  
11:  import java.net.URL;  
12:  import java.net.URLClassLoader;  
13:  import java.util.Date;  
14:  import java.util.logging.Level;  
15:  import java.util.logging.Logger;  
16:  import javax.tools.JavaCompiler;  
17:    
18:    
19:  /**  
20:   * Dynamic class loading and method invocation  
21:   * By Martin Wildam (http://www.google.com/profiles/mwildam)  
22:   * @author Martin Wildam  
23:   */  
24:  public class DynTool  
25:  {  
26:    /**  
27:     * Tries to instanciate an object of the given class and returns a pointer to it.  
28:     *   
29:     * Expects the class files in the directory given by the  
30:     * dynClassPath parameter. For each dot (.) in the class path there  
31:     * must be given a subfolder. If there exists a .java file instead of  
32:     * the expected .class file then the method tries to compile the .java file.  
33:     * It tries also a compile if both the .java and the .class file exists but  
34:     * the .class file is out of date (older than the .java file.  
35:     * The addClassPath parameter is only used if the attempt to  
36:     * compile is done.  
37:     *   
38:     * You can also specify a jar file in the dynClassPath parameter if  
39:     * you have a fully compiled and prepared package.  
40:     *  
41:     * Returns null if the object could not be created.  
42:     *   
43:     * Sample: getPluginInstance("/Work/Java/TestGUI/dist/TestGUI.jar", "testgui.TestPluginClass", "/Work/Java/TestGUI/dist/lib")  
44:     *   
45:     * Note: Dynamic class loading and calls evaluated during runtime  
46:     * using reflection is time consuming in general and therefore should be  
47:     * avoided for performance citrical operations (although already better  
48:     * since Java 5).  
49:     *   
50:     * We are returning null here in error case although not recommended  
51:     * because I do agree with Joel Spolsky (http://www.joelonsoftware.com/) on  
52:     * Exceptions at http://www.joelonsoftware.com/items/2003/10/13.html.  
53:     *  
54:     * @param  dynClassPath path to classes root dir where to search for the class to instantiate.  
55:     *     Can also be a .jar file.  
56:     * @param  className Full class name for the plugin class to use.  
57:     * @param  addClassPath is an additional option for classpath to search (;-separated)  
58:     *     which is only used when a compile attempt is done.  
59:     * @return Object or null in error case.  
60:     */  
61:    public static Object getPluginInstance(String dynClassPath, String className, String addClassPath)  
62:    {  
63:      if (!dynClassPath.toLowerCase().endsWith(".jar"))  
64:      {  
65:        String classFile = className.replace(".", "/");  
66:        classFile = dynClassPath + "/" + classFile;  
67:        String javaFile = classFile;  
68:        javaFile += ".java";  
69:        classFile += ".class";  
70:        if (addClassPath.length() != 0) addClassPath = ";" + addClassPath;  
71:    
72:        if ((!existsFile(classFile) && existsFile(javaFile))  
73:            || getFileDate(classFile).before(getFileDate(javaFile)))  
74:        {  
75:          JavaCompiler jc = javax.tools.ToolProvider.getSystemJavaCompiler();  
76:          int r = jc.run(null, null, null, "-classpath", dynClassPath + addClassPath, "-d", dynClassPath, javaFile);  
77:          if (r != 0) return null;  
78:        }  
79:      }  
80:      else  
81:        addFileToClassPath(dynClassPath);  
82:    
83:      try  
84:      {  
85:        if (addClassPath != null && addClassPath.length() > 0)  
86:          addFilesToClassPath(addClassPath);  
87:        URL url = new URL(DynTool.getUrlFromPath(dynClassPath));  
88:        URL[] clsList = new URL[1];  
89:        clsList[0] = url;  
90:        URLClassLoader ucl = new URLClassLoader(clsList);  
91:        Class cls = ucl.loadClass(className);  
92:        return cls.newInstance();  
93:      }  
94:      catch (NoClassDefFoundError ex)  
95:      {  
96:        Logger.getLogger(DynTool.class.getName()).log(Level.SEVERE, null, ex);  
97:        return null;  
98:      }  
99:      catch (ClassNotFoundException ex)  
100:      {  
101:        Logger.getLogger(DynTool.class.getName()).log(Level.SEVERE, null, ex);  
102:        return null;  
103:      }  
104:      catch (InstantiationException ex)  
105:      {  
106:        Logger.getLogger(DynTool.class.getName()).log(Level.SEVERE, null, ex);  
107:        return null;  
108:      }  
109:      catch (IllegalAccessException ex)  
110:      {  
111:        Logger.getLogger(DynTool.class.getName()).log(Level.SEVERE, null, ex);  
112:        return null;  
113:      }  
114:      catch (MalformedURLException ex)  
115:      {  
116:        Logger.getLogger(DynTool.class.getName()).log(Level.SEVERE, null, ex);  
117:        return null;  
118:      }  
119:    }  
120:    
121:    
122:    /**  
123:     * Late binding method call on an already instantiated object.  
124:     *   
125:     * Invokes the requested method of a given object instance where the  
126:     * object class is not specified at compile time.  
127:     *   
128:     * To call a method that does not have a parameter then pass null for the  
129:     * params parameters.  
130:     *   
131:     * What the method returns is routed to the caller of this method as Object  
132:     * so you have to cast the return type to something more specific if needed  
133:     * or just use .tostring. If the called method is declared void then this  
134:     * method returns null.  
135:     *  
136:     * Note: Dynamic class loading and calls evaluated during runtime  
137:     * using reflection is time consuming in general and therefore should be  
138:     * avoided for performance critical operations.  
139:     *   
140:     * We are returning null here in error case although not recommended (because  
141:     * of the earlier mentioned reasons).  
142:     *  
143:     * @param  instance A not well known object for that we hope to be able to  
144:     *     call the requested method.  
145:     * @param  methodName Name of the method to be called  
146:     * @param  params parameter objects to pass to the method (best matching declaration variant is searched)  
147:     * @return Returned object or null if method is declared void or an error occurred.  
148:     */  
149:    public static Object call(Object instance, String methodName, Object... params)  
150:    {  
151:      //Statement stmt = new Statement(obj, methodName, null);  
152:      //stmt.execute();  
153:    
154:      Expression expr = new Expression(instance, methodName, params);  
155:      //expr.execute(); //Not necessary, called automatically on getValue();  
156:      Object result = null;  
157:      try  
158:      {  
159:        result = expr.getValue();  
160:      }  
161:      catch (Exception ex)  
162:      {  
163:        Logger.getLogger(DynTool.class.getName()).log(Level.SEVERE, null, ex);  
164:      }  
165:      return result;  
166:    }  
167:    
168:    
169:    /**  
170:     * Adds a resource given as URL to the classpath.  
171:     *   
172:     * Adds the given url to the classpath dynamically.  
173:     *   
174:     * From antony_miguel at  
175:     * http://forums.sun.com/thread.jspa?threadID=300557&start=0&tstart=0:  
176:     * "  
177:     * I've seen a lot of forum posts about how to modify the  
178:     * classpath at runtime and a lot of answers saying it can't be done.  
179:     * I needed to add JDBC driver JARs at runtime so I figured out the  
180:     * following method.  
181:     *   
182:     * The system classloader (ClassLoader.getSystemClassLoader()) is a subclass  
183:     * of URLClassLoader. It can therefore be casted into a URLClassLoader and  
184:     * used as one.  
185:     *   
186:     * URLClassLoader has a protected method addURL(URL url), which you can use  
187:     * to add files, jars, web addresses - any valid URL in fact.  
188:     *   
189:     * Since the method is protected you need to use reflection to invoke it.  
190:     * "  
191:     *   
192:     * The class path change does not reflect in the system property  
193:     * "" because that property does not get modified any more after application  
194:     * start. So don't check success by checking  
195:     * System.getProperty("java.class.path");.  
196:     *  
197:     * @param  url Url to add to the class path.  
198:     * @return True if operation was successful  
199:     */  
200:    public static boolean addUrlToClassPath(URL url)  
201:    {  
202:      if (url == null)  
203:      {  
204:        Logger.getLogger(DynTool.class.getName()).log(Level.WARNING, "Missing url to add to classpath.");  
205:        return false;  
206:      }  
207:    
208:      boolean b = false;  
209:    
210:      URLClassLoader sysloader = (URLClassLoader) ClassLoader.getSystemClassLoader();  
211:      Class sysclass = URLClassLoader.class;  
212:      try  
213:      {  
214:    
215:        Class[] methodParams = new Class[1];  
216:        methodParams[0] = URL.class;  
217:        //There could be really different classes in the array.  
218:        @SuppressWarnings("unchecked")  
219:        Method method = sysclass.getDeclaredMethod("addURL", methodParams);  
220:        method.setAccessible(true);  
221:        method.invoke(sysloader, new Object[]  
222:            {  
223:              url  
224:            });  
225:        b = true;  
226:      }  
227:      catch (IllegalAccessException ex)  
228:      {  
229:        Logger.getLogger(DynTool.class.getName()).log(Level.WARNING, null, ex);  
230:      }  
231:      catch (IllegalArgumentException ex)  
232:      {  
233:        Logger.getLogger(DynTool.class.getName()).log(Level.WARNING, null, ex);  
234:      }  
235:      catch (InvocationTargetException ex)  
236:      {  
237:        Logger.getLogger(DynTool.class.getName()).log(Level.WARNING, null, ex);  
238:      }  
239:      catch (NoSuchMethodException ex)  
240:      {  
241:        Logger.getLogger(DynTool.class.getName()).log(Level.WARNING, null, ex);  
242:      }  
243:      catch (SecurityException ex)  
244:      {  
245:        Logger.getLogger(DynTool.class.getName()).log(Level.WARNING, null, ex);  
246:      }  
247:    
248:      return b;  
249:    }  
250:    
251:    
252:    /**  
253:     * Converts a file path to an url link.  
254:     *   
255:     * Returns the url link equivalent to the given path as string.  
256:     *  
257:     * @param  path  
258:     * @return Url string  
259:     */  
260:    public static String getUrlFromPath(String path)  
261:    {  
262:      try  
263:      {  
264:        return new File(path).toURI().toURL().toString();  
265:      }  
266:      catch (MalformedURLException ex)  
267:      {  
268:        return "";  
269:      }  
270:    }  
271:    
272:    
273:    /**  
274:     * Returns a boolean true if a "normal" file with the specified name exists.  
275:     *   
276:     * Note: Returns false for files that are part of the kernel system.  
277:     * So it returns true only for "normal" files.  
278:     *  
279:     * @param  fileFullName FQPN of the file to be searched for  
280:     * @return Boolean  
281:     */  
282:    public static Boolean existsFile(String fileFullName)  
283:    {  
284:      if (fileFullName == null)  
285:        return false;  
286:      else  
287:      {  
288:        File f = new File(fileFullName);  
289:        return f.exists() && f.isFile();  
290:      }  
291:    }  
292:    
293:    
294:    /**  
295:     * Returns the timestamp of the file with the given name if exists otherwise 0.  
296:     *   
297:     * Returns a Date of 0 if the file could not be found otherwise the last  
298:     * file modification date.  
299:     *  
300:     * @param  fileName Name of the file from which to read the date and timestamp  
301:     * @return Date  
302:     */  
303:    public static Date getFileDate(String fileName)  
304:    {  
305:      if (!existsFile(fileName))  
306:        return new Date(0);  
307:      else  
308:      {  
309:        File f = new File(fileName);  
310:        return new Date(f.lastModified());  
311:      }  
312:    }  
313:    
314:    
315:    /**  
316:     * Adds the given file dynamically to the class path.  
317:     *   
318:     * Further details see {@link #addUrlToClassPath(java.net.URL) }.  
319:     *  
320:     * @param  file File object to be added dynamically to the class path.  
321:     * @return True if operation was successful.  
322:     */  
323:    public static boolean addFileToClassPath(File file)  
324:    {  
325:      if (file == null)  
326:      {  
327:        Logger.getLogger(DynTool.class.getName()).log(Level.WARNING, "Missing file to add to classpath.");  
328:        return false;  
329:      }  
330:    
331:      URL url;  
332:      try  
333:      {  
334:        url = file.toURI().toURL();  
335:      }  
336:      catch (MalformedURLException ex)  
337:      {  
338:        Logger.getLogger(DynTool.class.getName()).log(Level.WARNING, null, ex);  
339:        return false;  
340:      }  
341:    
342:      return addUrlToClassPath(url);  
343:    }  
344:    
345:    
346:    /**  
347:     * Adds the given file dynamically to the class path.  
348:     *   
349:     * Further details see {@link #addUrlToClassPath(java.net.URL) }.  
350:     *  
351:     * @param  fileName FQPN of the file object to be added dynamically to the class path.  
352:     * @return True if operation was successful.  
353:     */  
354:    public static boolean addFileToClassPath(String fileName)  
355:    {  
356:      return addFileToClassPath(new File(fileName));  
357:    }  
358:    
359:    
360:    /**  
361:     * Adds files in the given folder dynamically to the class path.  
362:     *   
363:     * You can specify multiple paths separating them by ";".  
364:     * Further details see {@link #addUrlToClassPath(java.net.URL) }.  
365:     * Path must contain only jars and class files (subfolders not included).  
366:     *  
367:     * @param  path FQPN of the path to be added dynamically to the class path  
368:     *     or multiple paths separated by ";".  
369:     * @return Number of jars and class files added.  
370:     */  
371:    public static int addFilesToClassPath(String path)  
372:    {  
373:      if (path == null || path.length() == 0) return 0;  
374:      int n = 0;  
375:    
376:      if (path.contains(";"))  
377:      {  
378:        String[] subPaths = path.split(";");  
379:        for (String subPath : subPaths)  
380:        {  
381:          n = n + addFilesToClassPath(subPath);  
382:        }  
383:      }  
384:      else  
385:      {  
386:        File dir = new File(path);  
387:        File[] contents = dir.listFiles(); //Path Must contain jars and class files only  
388:        for (int i = 0; i < contents.length; i++)  
389:        {  
390:          File file = contents[i];  
391:          if (addFileToClassPath(file))  
392:            n++;  
393:        }  
394:      }  
395:      return n;  
396:    }  
397:    
398:  }  
399:    
400:    

You might prefer looking at this code on pastebin.

Sample usage:
  1. Object obj = getPluginInstance(yourJarFile, yourPluginClassName, additionalLibPath);
  2. Object result = call(obj, "pluginMethod", param1, ...);
    // For no parameters use at least null for param1
I use this currently in two cases:
  • For loading plugins at runtime (in those cases I only need getPluginInstance because I cast the object to a known interface class).
  • To avoid fixed dependencies of common utility libraries (in those cases I only need the call method). What I mean with this? - To explain, a little example: I have a Swing utility class that runs through all elements of a JFrame or JDialog and returns a map with all widget names and values it can extract. This way, saving dialog inputs is a one-liner. However, I support a lot of different external widgets, most are not used in 80% of my projects, so I don't want to introduce the dependencies to all those external components not included in the Swing core. Using the dynamic call offers the support for those widgets when I find them in dialog contents (identified by class name) where they occur. And the appropriate dependencies are only needed in the particular projects where they are needed.
So I don't really understand, why so many people are arguing against Java, that dynamic calls are not possible - where my sample code either automatically compiles a class that is given as .java instead of packed into a compiled .class or .jar file. And if I have both I check the class file if it is older and compile only in that case. So I would say, this is dynamic Java language!

Related posts: Get reliable local IP address in Java, Popular Java myths.

2 comments:

Anonymous said...

You should read up a little on what JSR-292 actually does, it's not about making "dynamic calls from Java", it's about making dynamically typed languages running on the JVM simpler to implement and faster to run. Check out http://www.infoq.com/articles/invokedynamic for more info (slightly outdated; for example, there probably won't be any syntactic support for JSR-292 features in Java 7).

Martin Wildam said...

Thank you for mentioning this - of course there are optimizations planned for dynamic languages and of course it also gets easier to do dynamic calls.

But in many cases the way I showed in my post is sufficient to solve people's requirements (e.g. to dynamically load plugin classes).

It took me a while to get it right, although many example are around trying to show how it works. Therefore the discussions about invokedynamic and dynamic languages and dynamically calling methods I followed lately, were a good opportunity to write this.

And of course I am also looking forward to Java 7.