When is something not ready for Prime-Time?

When you can't do something simple with it.

Check out this absurdly simple Groovy script:

import javax.management.ObjectName
import javax.management.MBeanServerConnection
import javax.management.remote.JMXConnectorFactory as JMXFactory
import javax.management.remote.JMXServiceURL as JMXUrl

def serverUrl = 'service:jmx:rmi:///jndi/rmi://'
def serv = JMXFactory.connect(new JMXUrl(serverUrl))
def on = new ObjectName('Catalina:type=Server')
def gmb = new GroovyMBean(serv, new ObjectName('Catalina:type=Server')).serverInfo

For those of you not up on your JMX, this is a simple connection via an RMI connector to the JMX server running at port 9004 (which happens to be my local Tomcat installation). This is straight off the Groovy-JMX documentation page, but slimmed down because the GroovyMBean constructor throws an exception, claiming that the desired constructor cannot be found:

C:\Projects\Exploration\Groovy>groovy GroovyJMX.groovy
Caught: groovy.lang.GroovyRuntimeException: Could not find matching constructor
for: groovy.util.GroovyMBean(javax.management.remote.rmi.RMIConnector, javax.management.ObjectName)
at GroovyJMX.run(GroovyJMX.groovy:9)
at GroovyJMX.main(GroovyJMX.groovy)

C'mon, folks. I have no idea what the problem is, and debugging this is a nightmare. I've looked around various Groovy forums, and nobody appears to have any real idea what's going on. Either nobody is really using Groovy as a JMX client (in which case, just remove the GroovyMBean from the library), or else Groovy has a bug within it (thus reducing its efficicacy as a production-ready language).

I'm fully willing to accept that the problem is with me or my environment somehow. The challenge, however, is for somebody to take a stock JDK 1.6 and Groovy 1.0 download (oh, and 1.1-rc2 fails with the same error, so that's not the issue, either), run the above 8-line script, and tell me why mine isn't working. (I've already done the suggested step of removing the mx4j jar out of the groovy-1.0/lib directory, so that doesn't help, either.)

Oh, and if you're going to write in claiming that this is a ClassLoader issue or something, you'd be wrong--all of the JMX types being loaded are coming out of rt.jar (which I verified using -verbose:class, doing which required me to edit the Groovy launcher scripts, which I find to be just silly--don't make it hard for me to use the basic management & monitoring facilities of the JVM). The only ClassLoader player I don't know for certain is the org.codehaus ClassLoader that's established by Groovy itself, so if the problem is in ClassLoaders, it's inside of the Groovy implementation, which means it's a Groovy problem, not mine.

I've even taken the step to compile the Groovy code into .class files, and run those:

C:\Projects\Exploration\Groovy>groovyc -d compiled GroovyJMX.groovy
C:\Projects\Exploration\Groovy>cd compiled

C:\Projects\Exploration\Groovy\compiled>java -classpath .;libg\groovy-1.0.jar;li
bg\asm-2.2.jar GroovyJMX
Exception in thread "main" groovy.lang.GroovyRuntimeException: Could not find ma
tching constructor for: groovy.util.GroovyMBean(javax.management.remote.rmi.RMIC
onnector, javax.management.ObjectName)
at groovy.lang.MetaClassImpl.invokeConstructor(MetaClassImpl.java:776)
at groovy.lang.MetaClassImpl.invokeConstructor(MetaClassImpl.java:688)
at org.codehaus.groovy.runtime.Invoker.invokeConstructorOf(Invoker.java:
at org.codehaus.groovy.runtime.InvokerHelper.invokeConstructorOf(Invoker
at org.codehaus.groovy.runtime.ScriptBytecodeAdapter.invokeNewN(ScriptBy
at GroovyJMX.run(GroovyJMX.groovy:9)
at gjdk.GroovyJMX_GroovyReflector.invoke(Unknown Source)
at groovy.lang.MetaMethod.invoke(MetaMethod.java:115)
at org.codehaus.groovy.runtime.MetaClassHelper.doMethodInvoke(MetaClassH
at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:560)
at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:450)
at org.codehaus.groovy.runtime.Invoker.invokeMethod(Invoker.java:131)
at org.codehaus.groovy.runtime.InvokerHelper.invokeMethod(InvokerHelper.
at org.codehaus.groovy.runtime.InvokerHelper.runScript(InvokerHelper.jav
at gjdk.org.codehaus.groovy.runtime.InvokerHelper_GroovyReflector.invoke
(Unknown Source)
at groovy.lang.MetaMethod.invoke(MetaMethod.java:115)
at org.codehaus.groovy.runtime.MetaClassHelper.doMethodInvoke(MetaClassH
at groovy.lang.MetaClassImpl.invokeStaticMethod(MetaClassImpl.java:664)
at org.codehaus.groovy.runtime.Invoker.invokeMethod(Invoker.java:111)
at org.codehaus.groovy.runtime.InvokerHelper.invokeMethod(InvokerHelper.
at org.codehaus.groovy.runtime.ScriptBytecodeAdapter.invokeMethodN(Scrip
at GroovyJMX.main(GroovyJMX.groovy)


And here we can obviously see, by the light of the .jars passed in, that the mx4j jars are nowhere to be found (unless, of course, Groovy is explicitly searching the hard drive for them, because I deleted them entirely out of Groovy's lib directory).

I've asked a couple of the Groovy heavyweights (not mentioning anybody by name) if they know what's up. Silence. Not a good sign.

I'm about to go off and try the same thing using JRuby. If it works, out of the box, then the problem is with Groovy, not with me. If you're a Groovy expert, or know someone who is, then have them email me the solution (assuming they can find one), because this is a point where I'm about to close the door on Groovy forever: if you can't do something simple with it, it's not ready for prime-time. It may be OK to whip up dirt-simple websites where 90% of the stuff is pre-generated, but if I can't use it for something like being a JMX client, then it ain't worth my time.

'Nuff said.


Update: OK, I may have to eat my words.

Playing around with some JRuby/JMX stuff (which is absurdly simple, even without jmx4r, which I'm trying to get gem to install right now), I went back and decided to try a little JMX without using GroovyMBean:

import javax.management.*
import javax.management.remote.*
import java.lang.management.*

def serverUrl = 'service:jmx:rmi:///jndi/rmi://'
def connector = JMXConnectorFactory.connect(new JMXServiceURL(serverUrl))
def mbsc = connector.mBeanServerConnection

def memory_mbean =
ManagementFactory.newPlatformMXBeanProxy(mbsc, "java.lang:type=Memory", MemoryMXBean.class)

This, by the way, is almost a straight port of Jeff's corresponding JRuby/JMX example code. Works just fine. From there, I thought, "Let's see if I can get past the problem I was having with GroovyMBean a few minutes ago." So I add the one-line GroovyMBean constructor (taking the mBeanServerConnection as the first parameter):

def gmb = new GroovyMBean(mbsc, new ObjectName('Catalina:type=Server')).serverInfo

Voila! Success! A quick execution test verifies that I'm all good:

def query = new ObjectName('Catalina:*')
String[] allNames = mbsc.queryNames(query, null)
def modules = allNames.findAll{ name ->
}.collect{ new GroovyMBean(mbsc, it) }

println "Found ${modules.size()} web modules. Processing ..."

modules.each{ m ->
    println "Found ${m.name()} at ${m.path} (${m.processingTime})"


C:\Projects\Exploration\Groovy>groovy GroovyJMX.groovy
Found 5 web modules. Processing ...
Found Catalina:j2eeType=WebModule,name=//localhost/docs,J2EEApplication=none,J2E
EServer=none at /docs (0)
Found Catalina:j2eeType=WebModule,name=//localhost/host-manager,J2EEApplication=
none,J2EEServer=none at /host-manager (0)
Found Catalina:j2eeType=WebModule,name=//localhost/examples,J2EEApplication=none
,J2EEServer=none at /examples (0)
Found Catalina:j2eeType=WebModule,name=//localhost/,J2EEApplication=none,J2EESer
ver=none at (0)
Found Catalina:j2eeType=WebModule,name=//localhost/manager,J2EEApplication=none,
J2EEServer=none at /manager (0)

Now if that ain't cool, I don't know what is.

Now for the hard part: What happened? Why'd my earlier code fail?

Looking back at my example script above, I clearly see that somewhere along the way in my debugging/exploration, I accidentally left out the call to obtain the MBeanServerConnection from the Connector and pass that in to the GroovyMBean constructor. Ugh.

Formal apologies to the Groovy crowd. However, I stipulate, for your own consideration, that

  1. Originally my script was not so--I copied it line-for-line from the Groovy/JMX page, and I got the GroovyCastException that led me down this path. (You decide whether you believe me or not. :-) )
  2. The GroovyMBean constructor could (and should) be overloaded to take a Connector and extract the MBeanServerConnection from it and proceed. In fact, I'd suggest that the GroovyMBean should be written to take a JMXServiceURL as a parameter and handle all the details of connecting to the remote server.
  3. Exploration tests would have yielded a better record of my efforts, and maybe found the point where my coding got led astray. It would also help track down the problem for others, if there was a language issue at stake here. (There may be--one noticeable difference is that in the working version, I don't use the import-redeclaration feature, whereas in the non-working version I did. More research is required.) At the time, I was just trying to debug a simple script problem, but at some point, greater rigor on my part should have kicked in so I could have better results to work from. Sigh.
  4. Debugging this was way too hard. As we move into an era of more languages-atop-VMs, better debugging/spelunking facilities has to be a top priority.
  5. Groovy (and JRuby) need to make sure they honor--somehow--the various flags I want to pass to the Java VM, such as -verbose or -client/-server, or even -ms or -mx. Java does have the JAVA_TOOLS_OPTIONS environment variable that will be picked up by the Java launcher (java.exe), but that only works if the language in question uses the launcher itself as part of its startup script. Both Groovy and JRuby seem to fail this test (though I admit I didn't look super-hard for undocumented/underdocumented ways to do it).