Saturday, October 21, 2006

Using Jython to call Java

I have been writing standalone Java applications lately, which are run from within a shell script, something like the snippet below. Calling Java standalones in this way is a fairly standard approach. However, being a Python fan(atic?), I would like to write these shell scripts using Python. This article describes how to call Java programs using Jython, which is a Java port of the Python programming language.

1
2
3
#!/bin/bash
LIBDIR=/path/to/my/jar/repository
java -classpath $LIBDIR/commons-lang-2.1.jar:  $LIBDIR/com-mycompany-4.9.jar com.mycompany.mypackage.MyClass $*

In MyClass.java, I have a static main(String[] argv) method which gets invoked as a result of the call above. All it does is instantiate an instance of MyClass and pass some parameters to it, and then call a method on it that does the work. Here is an example, basically yet another implementation of HelloWorld.java, but it will do for illustrating the pattern.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.mycompany.mypackage;

import org.apache.commons.lang.StringUtils;

public class MyClass {

  private String name;

  public MyClass() {
    super();
  }

  public void setName(String name) {
    this.name = name;
  }

  public String greet() {
    if (StringUtils.isEmpty(name)) {
      return "Hi there";
    } else {
      return "Hi " + name;
    }
  }

  public static void main(String[] argv) {
    MyClass myclass = new MyClass();
    myclass.setName(argv[0]);
    System.out.println(myclass.greet());
  }
}

Arguably, classes in the real world have more code in their main() methods, but the approach I take when I see that I have more than one object being invoked in my main method, is to factor out the logic into private methods of that class, or into a Main.java class in that package if multiple classes are involved.

Anyway, the Jython script to call the main method of MyClass with a single string parameter that is passed in from the command line is shown below. Notice how the Jython script instantiates the MyClass object. The classpath is passed in to Jython using sys.path.append() calls. Resource files, such as .properties files or other XML configuration files need to be baked into the JAR file and should be accessible from within the Java code using getResourceAsStream().

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#!/usr/bin/env jython
import sys

def setClassPath():
  libDir = "/path/to/my/jar/files/"
  classPaths = [
    "commons-lang-2.1.jar",
    "com-mycompany-4.9.jar"
  ]
  for classPath in classPaths:
    sys.path.append(libDir + classPath)

def runJavaClass(name):
  from com.mycompany.mypackage import MyClass
  mc = MyClass()
  if (name != None):
    mc.setName(name)
  print mc.greet()

def main():
  if (len(sys.argv) < 2):
    name = None
  else:
    name = sys.argv[1]
  setClassPath()
  runJavaClass(name)

if __name__ == "__main__":
  main()

As you can see, invoking Java programs from Jython leads to cleaner but more verbose scripts, but does seem to be overkill in our case. However, this approach can really help when you have to design more complicated scripts that deal with a large number of classes. One approach, and my preferred one, is to put this logic in Java in another abstraction layer. However, sometimes, this is not the most practical approach, perhaps because the rules are rather fluid, and you would like to be able to change the rules without having to go through a big release cycle. In such cases, and depending on the complexity of the rules, I think I would be more productive using Python than Shell Scripts. Even if the rules were simple enough to be encoded within a Shell Script to start out with, it may still be more practical to use Jython, since the rules can get more complicated, and you may be faced with having to rewrite the script.

I don't think it is possible to set JVM parameters (such as minimum and maximum JVM sizes) to Jython from within the Jython script, something that is possible using a Shell Script. Jython itself is a shell script and calls the org.python.util.jython class, so these parameters can be passed into the Java invocation in the jython shell script, although this does not seem to be very clean. Running the class through Jython also seems a little slower than running it through a Shell Script, but I haven't done any benchmarks to say this conclusively.

12 comments (moderated to prevent spam):

Alex Kochnev said...

Sujit,
have you tested the code that you have on the blog ? In my experience, adding jar files to sys.path at runtime does not make the classes loadable - you either have to add the jars to the classpath before starting the app, or you have to work with loadSets to make the jar load..

Sujit Pal said...

Hi Alex,

Yes, I have, but not any more than the code that is in the blog. I was able to invoke the MyClass.greet() method (and I checked that the CLASSPATH was unset). I also read that this was not possible, and did look at loadSets before trying this, but I could not make it work. I was surprised (and elated) that just adding the jar files to sys.path ended up working.

However, I did not investigate further, because (a) I think Jython is a little slower than a shell script java call and (b) it does not lead to less code-writing. So in the absence of a clear benefit, I would have a hard time convincing my operations guy to give up shell scripts and learn python just because I think its cool to call java from python :-).

-sujit

Sujit Pal said...

The first comment from Alex asked if I tested the code I put in this blog. I did test it, but unfortunately, I did not have the code handy when the comment came in, so I was not able to verify that it indeed worked.

I recently used Jython again, and I am happy to verify that adding JAR file paths to the sys.path does indeed work for Jython. Here is the link to the blog post that describes it.

Siva Reddy said...

Thanks dude.... I was able to write my first complex program in jython by reading your post

Sujit Pal said...

Cool, glad it helped.

Thijs said...

Thanks, that's a useful snippet.

Sujit Pal said...

Thank Thijs -- however, I had trouble doing database access through JDBC using Jython by adding the JDBC jar file to my libdir as shown here -- adding to the classpath in the jython call works like a charm though...so I guess this approach doesn't work all the time.

CrazyCarl said...

Mr. Sujit,

This post is precisely what I'm after, but I'm having issues implementing.

It seems as if the jython code isn't being referenced anywhere? ( I'm rather new to java ) I guess I'd expect to see it called within the shell script?

CrazyCarl said...

On second look I'm wondering about the file structure of these directories...

Also It looks like this can accept in a name for the greeting... Where would that go?

Sujit Pal said...

Hi CrazyCarl, sorry, I guess the post isn't clear, but the script snippet is how one would call a Java main method in the absence of jython. With the jython setup described, you would call the last script by name from the command line (it already has the #! header to use Jython to execute the script) with the name as a parameter, something like this:

prompt$ ./my_jython_script my_name

The main() method will pull "my_name" from sys.argv[1] and setName() on the Java class and then call its greet() method.

CrazyCarl said...

Sujit,

In examining the code I'm unsure if a particular redundancy is necessary or what function it performs.

Specifically the calls made to setName(). They're made in two places

1) line:27 of the java
2) line:17 of the jython

It seems redundant? Are both calls necessary?

Sujit Pal said...

Hi CrazyCarl, yes, they are redundant. This was a phase where I was in love with Spring and used the setter injection pattern almost everywhere I could :-). So yes, one really doesn't need to call setName() in two places. The same effect could be had by declaring a constructor MyClass(String) in the Java code, and in line 15 of the Jython code, do mc = MyClass(name).