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:
- Object obj = getPluginInstance(yourJarFile, yourPluginClassName, additionalLibPath);
- 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.