Monday, March 16, 2009

Java annotation, scala object and javassist don't get along well

[Update: in the next paragraph, "showstopper" is clearly too strong - put it in my bad english level. The title is perhaps better, I wanted to say that things didn't came along as well as I hoped initially. In no case I wanted to sound over-pessimistic. Actually, the workaround is rather simple : I can use a direct call to the look-up service of T5 IOC each time I need a named service in place of the direct injection of that service.]

In my quest of using Tapestry 5 with a Scala back-end, I believe I fall on a showstopper. I seems that Javassist doesn't see annotation on method parameter in Scala object, although the annotation (being marked with Runtime retention) is seen with the Java reflexion API.

I tested with both Scala 2.7.3 and 2.8.0-nigtly, and both Javassist 3.9.0.GA and 3.10 - the behaviour is consistent in all combinations.

In fact, what seems to happens is that when one defines a Scala object "Foo.scala", scalac generates two classes : "Foo.class" that is a kind of singleton facade and initializer, and "Foo$.class" that is the actual class.

When the java reflexion API is used, I think that scala delegates the call in the rigth way. The problem seems to be that Javassist directly read the bytecode of "Foo.class", and doesn't see the annotation - it seems to be only on the method in "Foo$.class".

You can you see this behaviour like that:

Make a simple Java annotation, and compile it, something like @InjectService in Tapestry5-annotation package:

public @interface InjectService
String value();

Create a scala class that make use of that annotation and test it visibility thanks to Java reflexion API and Javassist :

import org.apache.tapestry5.ioc.annotations.InjectService
import javassist._
import javassist.runtime._

object ObjectTestAnnotation {
def methodWithParamAnnot(@InjectService("service id") s:String) = s

class ClassTestAnnotation {
def methodWithParamAnnot(@InjectService("service id") s:String) = s

object Main {

def main(args: Array[String]) {
* Using Java reflexion API
val methodWithParamAnnot = ObjectTestAnnotation.
getClass.getMethod("methodWithParamAnnot", classOf[String])
val annotations = methodWithParamAnnot.getParameterAnnotations
println("* with java reflexion API, on ObjectTestAnnotation")
annotations.foreach(_.foreach( x => println(x)))
// print:
// * with java reflexion API, on ObjectTestAnnotation
// @org.apache.tapestry5.ioc.annotations.InjectService(value=service id)

val methodWithParamAnnot1 = classOf[ClassTestAnnotation].
getMethod("methodWithParamAnnot", classOf[String])
val annotations1 = methodWithParamAnnot1.getParameterAnnotations
println("* with java reflexion API, on ClassTestAnnotation")
annotations1.foreach(_.foreach( x => println(x)))
// print:
// * with java reflexion API, on ClassTestAnnotation
// @org.apache.tapestry5.ioc.annotations.InjectService(value=service id)

* Using Javassist
val cp = ClassPool.getDefault()
val ctString = cp.get("java.lang.String")
val methodWithParamAnnot2 = cp.get("test.ObjectTestAnnotation")
val annotations2 = methodWithParamAnnot2.getParameterAnnotations
println("* with javassist, on ObjectTestAnnotation")
annotations2.foreach(_.foreach( x => println(x)))

// print (AND THAT'S BAD, the annotation is not seen !)

// * with javassist, on ObjectTestAnnotation

val methodWithParamAnnot2_bis = cp.get("test.ObjectTestAnnotation$")
val annotations2_bis = methodWithParamAnnot2_bis.getParameterAnnotations
println("* with javassist, on ObjectTestAnnotation$")
annotations2_bis.foreach(_.foreach( x => println(x)))
// print:
// * with javassist, on ObjectTestAnnotation$
// @org.apache.tapestry5.ioc.annotations.InjectService(value="service id")

val methodWithParamAnnot3 = cp.get("test.ClassTestAnnotation")
val annotations3 = methodWithParamAnnot3.getParameterAnnotations
println("* with javassist, on ClassTestAnnotation")
annotations3.foreach(_.foreach( x => println(x)))
// print:
// * with javassist, on ClassTestAnnotation
// @org.apache.tapestry5.ioc.annotations.InjectService(value="service id")




Cry ! Because, of course, Tapestry 5 uses these annotations in such a way that a bug here make the whole thing unusable - the core of T5-IOC container relies on them.
I would have to see if I can use a class in place of the object...


James Iry

Fanf...Java doesn't have top level objects so it's not surprising that Java tools don't always understand them. As you say, the "real" class is in Foo$.class.

It seems likely that this "show stopper" can be worked around by mimicking the conventional approach of Java singletons.

// private constructor
class Foo private() {// methods with annotations, etc }

// lazy instance
object Foo {lazy val instance = new Foo}

Also, from now on, before posting a "sky is falling" blog post, please try the mailing list or irc channel. We're willing to help.

In the meantime, file an enhancement request. It seems possible that there would be a way for forwarders to get annotations.


James, thanks for your answer.

Actually, I didn't want to post a "the sky is falling" blog message, I believe that I don't understand English as well as I hoped.

Clearly, the behaviour wasn't a "showstopper" (that seems to be a rather definitive word) since I worked around the problem by calling directly the IOC registry look-up service, but it's somewhat annoying - or perhaps not T5 idiomatic.

For the IRC/ml remark, I did try irc several times, but you're right, the ml would have been better for that.

For the enhancement request, I'm writing it - I just wanted to make a clean test file before posting it.

And for the record: your proposed solution would not have worked, because T5 uses javassist to parse the class and is waiting for precise "public static" method definition, not for a singleton implementation.

Thanks again for you comment, I will do use the mailing list.


@JamesIry : I'm just noticing that about 80% of my blog's comments are from you or their answers: I owe you a beer ;)

  © Blogger template 'Minimalist G' by 2008

Back to TOP