I’ve just released a @WithLog annotation to GitHub under RobertFischer/Groovy-WithLog. The goal of @WithLog is to reduce the boilerplate associated with using Log4J.
Normally, using Log4J looks like this:
import org.apache.log4j.Logger class Foo { static log = Logger.getLogger(Foo) /* ... */ }
I’ve now said Logger three times and my class name twice. And there’s absolutely no useful value to having it clogging up the body of my class. This burns my eyes, and needs to stop.
Now you can replace it with:
@WithLog class Foo { /* ... */ }
As an AST transform, the class itself is actually changed at compile time: no runtime cost is paid, no additional library needs to be schlepped around (except Log4J, obviously), and you don’t have to worry about what order your code executes in for fear that log won’t be defined soon enough.
I’m considering creating a super-powered version which defaults to inserting log properties onto the compiling classes, and you have to explicitly say @NoLog to avoid it. I’m still trying to decide if that’s evil: on the one hand, it’s basically changing the Groovy language; on another, the user has to put it onto their classpath during compile, so you’re basically asking for it. Another potential option is to enable [EDIT: I went ahead and implemented that approach. Old version was still too chatty.][EDIT 2: See Hamlet's fully-qualified class name pattern approach for another interesting take.]@WithLog with no import (I’ll add the import for you), which would basically stomp anyone else’s @WithLog, but it’d be sexy-short.
The biggest difficulties I encountered when coding this up was the overwhelming lack of information about how to deal with Groovy AST Transforms: they’re not for the faint of heart, and I basically had to go grepping through the Groovy language source code to figure it out. The documentation on Groovy’s Codehaus site (notably similar to Hamlet D’Arcy’s blog posts) was a start, but questions like “What do these compile phases mean, anyway?” are nowhere to be found.
Also encountered a fun problem where Groovy went checking for static fields at compile time. You can see the same issue here:
class Foo { static doLog() { log.info("This won't even compile!") } } Foo.metaClass.static.log = [info:{ println it }] // Enable the above code to work /* org.codehaus.groovy.control.MultipleCompilationErrorsException: startup failed, /Users/robert/dev/workspace/Groovy-WithLog/test/Bogus.groovy: 2: Apparent variable 'log' was found in a static scope but doesn't refer to a local variable, static field or class. Possible causes: You attemped to reference a variable in the binding or an instance variable from a static context. You mispelled a classname or statically imported field. Please check the spelling. You attempted to use a method 'log' but left out brackets in a place not allowed by the grammar. @ line 2, column 19. static doLog() { log.info("This won't even compile!") } ^ 1 error */
Note that ripping the word static out of that code makes it compile and run just fine.
Because of that issue, I couldn’t use a local AST transform, which is basically exactly what the situation called for. Instead, I had to use a global transform at the magic “CONVERSION” stage, which also meant effectively having to duplicate annotation hunting and class resolution logic. The silver lining of that particular WTF-Cloud is that I can go all kinds of crazy with how I apply things now (see above conversation on @NoLog and raw imports).
Additional conversation about implementing this AST Transform will probably be in an upcoming GroovyMag. Coming up in August is Part II of my “Goldilocks and Grails Logging”, which is the impetus for this code.
Thanks to Hamlet D’Arcy and Peter Niederwieser for their long-suffering assistance in this effort.
As with most of my open source contributions, it’s released under the WTFPL. Have a blast.
9 Comments
Looks great, but why did you choose log4j? Instead you should use slf4j, than people can use log4j or any other framework as an implementation.
I chose Log4J specifically because in every project where I’ve ever worked with a logging facade (including the SLF4J usage in Grails), it’s been a facade over Log4J on the basis that “the logging implementation might change someday”, but that change in implementation never, ever comes. In practice, the only practical use of SLF4J I’ve ever encountered is hijacking java.util.logging calls and routing them into Log4J.
Given that’s true, why use the weaker API of SLF4J when I can go straight to the source and provide a richer API at less runtime cost? I appreciate the theoretical sentiment of allowing additional choice, but there’s a line where you’re doing abstractions for abstraction’s sake, and I think SLF4J is on the wrong side of that line.
If someone wanted to take my code and modify it to use SLF4J, they’re more than welcome to: it’s under the WTFPL and easily forkable via GitHub. All they’d have to do is change lines 37 to 39 (in this version) to be the AST for the SLF4J logging fetch call.
1. Totally disagree with decision to do an automatic import. Why not just put @WithLog in the default package instead? It is equivalent and easier. But it doesn’t play well with others. No one else can create a WithLog annotation now, right? Small point.
2. Concerning @NoLog to avoid a logger… why not extend the log4J mechanism to specify which classes get a Logger weaved in? Declare somewhere that *.** gets @WithLog or maybe com.smokejumper.** to weave Logger into only your classes.
3. Concerning the documentation. I pasted my blog post into the Groovy wiki upon request. Not many read my blog; the wiki is a better place for it. BUT… Groovy is open source. That goes for documentation too. Why haven’t you added a page to the user manual on the CompilePhase object?
1. If it’s in the default package, it can’t be called from something in a non-default package. This is why Grails artifacts are all getting packages these days.
2. That’s an interesting approach: look for some kind of explicit pattern to match against. I’ll have to think about that—how would it be communicated to the AST Transform at compile time?
3. Because I have no idea what I’m doing with the CompilePhase object, so there’s not a lot of documentation I can do. “I used
CONVERSIONbecause Peter Niederwieser said I should, and the result was that I had to duplicate type resolution.”@Hamlet
More re: 1. I did code it so that I check for explicit imports of another ‘WithLog’, or something aliased to ‘WithLog’, so there’s a bit of a defense there. You could also fully-qualify the alternative WithLog annotation, or set the “sublog.withLog.disableRaw” JVM property to “true” during compile time. So there are lots of possible outs from a practical standpoint. And this gives you a much more succinct
@WithLog: it was bugging me that I simply exchangedimport org.apache.log4j.Loggerforimport com.smokejumperit.sublog.WithLog(or whatever my package was): given how frequently I attach a logger, it should really be a one-liner. If that.I like the idea of giving the class a logger by default, which renders the import statement issue a moot point.
Does the IDE recognize calls to the logger? I suppose it does.
With the AST Transform, the IDE should recognize the logger as long as it’s importing the AST Transform JAR onto the classpath at compile time. Which I’d be surprised if it didn’t.
In a Grails project, will this also add a logger to classes under src/java?
No: it’s not automatically added to anything at this point, so you have to explicitly add the annotation.
That feature that could be added to Sublog (the Grails plugin), though: an AST transform extending @WithLog could query the CodeSource, and if the location is a subdirectory of the Grails project, then the logging could be added.
That’ll take a pretty minor modification to
@WithLogto allow child classes to specify new “finder” behavior, and it’ll need to be packaged up in a JAR with an appropriately-filled-outorg.codehaus.groovy.transform.ASTTransformationfile.If you decide to implement that, then feel free to share the patch and I’ll integrate it into Sublog.
2 Trackbacks
[...] (more info here) should also be able to be assigned via an [...]
[...] just updated the @WithLog AST transform for Groovy. In case you don’t know, that’s my project over at GitHub which allows you to turn: [...]