Groovy Goodness: Adding Extra Methods Using Extension Modules
Join the DZone community and get the full member experience.
Join For FreeGroovy 2.0 brought us extension modules. An extension module is a JAR
file with classes that provide extra methods to existing other classes
like in the JDK or third-party libraries. Groovy uses this mechanism to
add for example extra methods to the File
class. We can implement our own extension module to add new extension
methods to existing classes. Once we have written the module we can add
it to the classpath of our code or application and all the new methods
are immediately available.
We define the new extension methods in
helper classes, which are part of the module. We can create instance and
static extension methods, but we need a separate helper class for each
type of extension method. We cannot mix static and instance extension
methods in one helper class. First we create a very simple class with an
extension method for the String
class. The first argument
of the extension method defines the type or class we want the method to
be added to. The following code shows the method likeAPirate
. The extension method needs to be public
and static
even though we are creating an instance extension method.
// File: src/main/groovy/com/mrhaki/groovy/PirateExtension.groovy package com.mrhaki.groovy class PirateExtension { static String likeAPirate(final String self) { // List of pirate language translations. def translations = [ ["hello", "ahoy"], ["Hi", "Yo-ho-ho"], ['are', 'be'], ['am', 'be'], ['is', 'be'], ['the', "th'"], ['you', 'ye'], ['your', 'yer'], ['of', "o'"] ] // Translate the original String to a // pirate language String. String result = self translations.each { translate -> result = result.replaceAll(translate[0], translate[1]) } result } }
Next we need to create an extension module descriptor file. In this
file we define the name of the helper class, so Groovy will know how to
use it. The descriptor file needs to be placed in the META-INF/services
directory of our module archive or classpath. The name of the file is org.codehaus.groovy.runtime.ExtensionModule
.
In the file we define the name of our module, version and the name of
the helper class. The name of the helper class is defined with the
property extensionClasses
:
# File: src/main/resources/META-INF/services/org.codehaus.groovy.runtime.ExtensionModule moduleName = pirate-module moduleVersion = 1.0 extensionClasses = com.mrhaki.groovy.PirateExtension
Now are extension module is ready. The easiest way to distribute the module is by packaging the code and descriptor file in a JAR file and put it in a artifact repository manager. Other developers can then use build tools like Gradle or Maven to include the extension module in their projects and applications. If we use Gradle to create a JAR file we only needs this small build script:
/ File: build.gradle apply plugin: 'groovy' repositories.mavenCentral() dependencies { // Since Gradle 1.4 we don't use the groovy configuration // to define dependencies. We can simply use the // compile and testCompile configurations. compile 'org.codehaus.groovy:groovy-all:2.0.6' }
Now we can invoke $ gradle build
and we got ourselves an extension module.
Let's add a test for our new extension method. Because we use Gradle the test classpath already will contain our extension module helper class and descriptor file. In our test we can simply invoke the method and test the results. We are going to use Spock to write a simple specification:
// File: src/test/groovy/com/mrhaki/groovy/PirateExtensionSpec.groovy package com.mrhaki.groovy import spock.lang.Specification class PirateExtensionSpec extends Specification { def "likeAPirate method should work as instance method on a String value"() { given: final String originalText = "Hi, Groovy is the greatest language of the JVM." expect: originalText.likeAPirate() == "Yo-ho-ho, Groovy be th' greatest language o' th' JVM." } }
We add the dependency to Spock in our Gradle build file:
/ File: build.gradle apply plugin: 'groovy' repositories.mavenCentral() dependencies { // Since Gradle 1.4 we don't use the groovy configuration // to define dependencies. We can simply use the // compile and testCompile configurations. compile 'org.codehaus.groovy:groovy-all:2.0.6' testCompile 'org.spockframework:spock-core:0.7-groovy-2.0' }
We can run $ gradle test
to run the Spock specification and test our new extension method.
To
add a static method to an existing class we need to add an extra helper
class to our extension module and an extra property to our descriptor
file to register the helper class. The first argument of the extension
method define the type we want to add a static method to. In the
following helper class we add the extension method talkLikeAPirate()
to the String
class.
/ File: src/main/groovy/com/mrhaki/groovy/PirateStaticExtension.groovy package com.mrhaki.groovy class PirateStaticExtension { static String talkLikeAPirate(final String type) { "Arr, me hearty," }
We change the descriptor file and add the staticExtensionClasses
property:
# File: src/main/resources/META-INF/services/org.codehaus.groovy.runtime.ExtensionModule moduleName = pirate-module moduleVersion = 1.0 extensionClasses = com.mrhaki.groovy.PirateExtension staticExtensionClasses = com.mrhaki.groovy.PirateStaticExtension
In our Spock specification we add an extra test for our new static method talkLikeAPirate()
on the String
class:
// File: src/test/groovy/com/mrhaki/groovy/PirateExtensionSpec.groovy package com.mrhaki.groovy import spock.lang.Specification class PirateExtensionSpec extends Specification { def "likeAPirate method should work as instance method on a String value"() { given: final String originalText = "Hi, Groovy is the greatest language of the JVM." expect: originalText.likeAPirate() == "Yo-ho-ho, Groovy be th' greatest language o' th' JVM." } def "talkLikeAPirate method should work as static method on String class"() { expect: "Arr, me hearty, Groovy rocks!" == String.talkLikeAPirate() + " Groovy rocks!" } }
Written with Groovy 2.1
Published at DZone with permission of Hubert Klein Ikkink, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments