Practical Groovy Grab annotation (@Grab) recipes for popular libraries. 12 copy-paste examples: Gson, Apache Commons CSV, OkHttp, JDBC drivers, Jsoup, and more – each with a working script you can run immediately.
“The best dependency manager is the one you forget is there – until you need a library at 2 AM and it just downloads itself into your script.”
Guillaume Laforge, Groovy Project Lead
Last Updated: March 2026 | Tested on: Groovy 5.x, Java 17+ | Difficulty: Beginner-Intermediate | Reading Time: 16 minutes
You know what the groovy grab annotation does – it downloads a library into your script at runtime. Our Groovy Grape guide covers the mechanics. But knowing the syntax and knowing which libraries to grab for common tasks are two different things. This post is the recipe collection: 12 ready-to-run scripts that solve real problems using @Grab with popular libraries.
Each recipe follows the same pattern: a real task (parse CSV, make an HTTP request, scrape HTML, connect to a database), the exact @Grab coordinates, and a complete working script with output. No build files, no project setup – just copy, paste, and run with groovy script.groovy. We also cover common gotchas like version conflicts, classloader issues, and making @Grab work inside classes versus scripts.
Table of Contents
Quick Reference Table
| Annotation / Command | Purpose | Example |
|---|---|---|
@Grab | Download a dependency | @Grab('group:artifact:version') |
@Grab(group, module, version) | Named parameters form | @Grab(group='com.google.code.gson', module='gson', version='2.10.1') |
@GrabResolver | Add a custom Maven/Ivy repo | @GrabResolver(name='jitpack', root='https://jitpack.io') |
@GrabExclude | Exclude a transitive dependency | @GrabExclude('commons-logging:commons-logging') |
@GrabConfig | Configure class loader behavior | @GrabConfig(systemClassLoader=true) |
@Grapes | Group multiple @Grab annotations | @Grapes([@Grab('lib1'), @Grab('lib2')]) |
grape install | Install a JAR into the Grape cache | grape install com.google.code.gson gson 2.10.1 |
grape list | List cached dependencies | grape list |
grape resolve | Resolve and print dependency paths | grape resolve com.google.code.gson gson 2.10.1 |
Examples
Example 1: Basic @Grab with Shorthand Syntax
What we’re doing: Downloading Google Gson at runtime using the compact colon-separated @Grab syntax and converting an object to JSON.
Example 1: Basic @Grab with Shorthand Syntax
@Grab('com.google.code.gson:gson:2.10.1')
import com.google.gson.Gson
def gson = new Gson()
def user = [name: 'Nirranjan', age: 30, city: 'Seattle']
def json = gson.toJson(user)
println "JSON: $json"
// Parse it back
def parsed = gson.fromJson(json, Map)
println "Parsed name: ${parsed.name}"
println "Parsed age: ${parsed.age}"
Output
JSON: {"name":"Nirranjan","age":30,"city":"Seattle"}
Parsed name: Nirranjan
Parsed age: 30
What happened here: The @Grab annotation tells Grape to download gson-2.10.1.jar from Maven Central before the script compiles. The shorthand format is group:artifact:version, matching Maven’s coordinate system. The @Grab must appear before the import statement so the JAR is on the classpath when the import resolves. After the first run, the JAR is cached in ~/.groovy/grapes/com.google.code.gson/gson/jars/ and subsequent runs skip the download entirely.
Example 2: Named Parameters Form
What we’re doing: Using the explicit named-parameter syntax for @Grab, which is more readable for complex coordinates.
Example 2: Named Parameters Form
@Grab(group='org.apache.commons', module='commons-lang3', version='3.14.0')
import org.apache.commons.lang3.StringUtils
println "Is blank: ${StringUtils.isBlank(' ')}"
println "Is blank: ${StringUtils.isBlank('hello')}"
println "Abbreviate: ${StringUtils.abbreviate('Groovy dependency management', 20)}"
println "Reverse: ${StringUtils.reverse('Groovy')}"
println "Repeat: ${StringUtils.repeat('Go', '-', 3)}"
println "Left pad: '${StringUtils.leftPad('42', 6, '0')}'"
Output
Is blank: true Is blank: false Abbreviate: Groovy dependenc... Reverse: yvoorG Repeat: Go-Go-Go Left pad: '000042'
What happened here: The named-parameter form – group, module, version – maps directly to Maven coordinates (groupId, artifactId, version). Both forms are equivalent; the shorthand is faster to type, while the named form is clearer when you have additional parameters like classifier or ext. Note that Grape uses module instead of Maven’s artifactId – this is because Grape is built on Ivy, which uses that terminology.
Example 3: Multiple Dependencies with @Grapes
What we’re doing: Grabbing multiple libraries in a single block using the @Grapes container annotation.
Example 3: Multiple Dependencies with @Grapes
@Grapes([
@Grab('com.google.code.gson:gson:2.10.1'),
@Grab('org.apache.commons:commons-lang3:3.14.0'),
@Grab('commons-io:commons-io:2.15.1')
])
import com.google.gson.Gson
import org.apache.commons.lang3.StringUtils
import org.apache.commons.io.FileUtils
// Use Gson
def gson = new Gson()
println "Gson version loaded: ${Gson.package.implementationVersion ?: '2.10.1'}"
// Use Commons Lang
println "Is numeric '12345': ${StringUtils.isNumeric('12345')}"
println "Is numeric 'abc': ${StringUtils.isNumeric('abc')}"
// Use Commons IO
def tempDir = new File(System.getProperty('java.io.tmpdir'))
println "Temp dir size: ${FileUtils.byteCountToDisplaySize(FileUtils.sizeOfDirectory(tempDir))}"
Output
Gson version loaded: 2.10.1 Is numeric '12345': true Is numeric 'abc': false Temp dir size: 2 GB
What happened here: @Grapes is a container annotation that groups multiple @Grab annotations together. This is the standard Java way to apply repeatable annotations. All three dependencies resolve in parallel during the script’s compilation phase. You can also stack multiple @Grab annotations directly without @Grapes in newer Groovy versions, but the explicit grouping is cleaner and works in all versions.
Example 4: @GrabResolver for Custom Repositories
What we’re doing: Configuring a custom Maven repository with @GrabResolver so Grape can resolve artifacts that aren’t on Maven Central.
Example 4: @GrabResolver for Custom Repositories
// Adding a custom repository (Spring Milestone repo as example)
@GrabResolver(name='spring-milestone', root='https://repo.spring.io/milestone')
@Grab('com.google.code.gson:gson:2.10.1')
import com.google.gson.GsonBuilder
// You can also configure JitPack for GitHub projects
// @GrabResolver(name='jitpack', root='https://jitpack.io')
def gson = new GsonBuilder()
.setPrettyPrinting()
.serializeNulls()
.create()
def config = [
database: [host: 'localhost', port: 5432, name: 'myapp'],
cache: [enabled: true, ttl: 300, provider: null],
features: [darkMode: true, beta: false]
]
println gson.toJson(config)
Output
{
"database": {
"host": "localhost",
"port": 5432,
"name": "myapp"
},
"cache": {
"enabled": true,
"ttl": 300,
"provider": null
},
"features": {
"darkMode": true,
"beta": false
}
}
What happened here: @GrabResolver adds a repository URL to Grape’s resolver chain. Grape checks repositories in order: local cache first, then Maven Central, then any custom resolvers you add. The name parameter is a human-readable label for logging, and root is the repository base URL. This is essential for corporate artifacts in private Nexus/Artifactory servers or for GitHub projects via JitPack. You can stack multiple @GrabResolver annotations to add several repositories.
Example 5: @GrabExclude for Transitive Dependency Control
What we’re doing: Excluding a transitive dependency that conflicts with another library or the runtime environment.
Example 5: @GrabExclude for Transitive Dependency Control
// HttpClient pulls in commons-logging, but we want to use SLF4J instead
@Grapes([
@Grab('org.apache.httpcomponents:httpclient:4.5.14'),
@GrabExclude('commons-logging:commons-logging'),
@Grab('org.slf4j:jcl-over-slf4j:2.0.9'),
@Grab('org.slf4j:slf4j-simple:2.0.9')
])
import org.apache.http.client.methods.HttpGet
import org.apache.http.impl.client.HttpClients
println "HttpClient loaded successfully"
println "commons-logging excluded and replaced with SLF4J bridge"
// Verify the client works
def client = HttpClients.createDefault()
println "Client class: ${client.getClass().simpleName}"
// List what's on the classpath related to logging
def classLoader = this.getClass().classLoader
try {
classLoader.loadClass('org.apache.commons.logging.LogFactory')
println "LogFactory found (via SLF4J bridge)"
} catch (ClassNotFoundException e) {
println "LogFactory not found: ${e.message}"
}
client.close()
Output
HttpClient loaded successfully commons-logging excluded and replaced with SLF4J bridge Client class: InternalHttpClient LogFactory found (via SLF4J bridge)
What happened here: Apache HttpClient depends on commons-logging, but many projects prefer SLF4J. @GrabExclude('commons-logging:commons-logging') removes commons-logging from the dependency tree, and we add jcl-over-slf4j as a drop-in replacement. The exclusion uses the same group:artifact format (no version needed). This is identical to Maven’s <exclusion> or Gradle’s exclude group:. You can stack multiple @GrabExclude annotations to remove several transitive dependencies.
Example 6: @GrabConfig for ClassLoader Control
What we’re doing: Using @GrabConfig(systemClassLoader=true) to load JDBC drivers that need to be on the system classloader.
Example 6: @GrabConfig for ClassLoader Control
@GrabConfig(systemClassLoader=true)
@Grab('com.h2database:h2:2.2.224')
import groovy.sql.Sql
// H2 is an in-memory database - perfect for scripts
def sql = Sql.newInstance('jdbc:h2:mem:testdb', 'sa', '', 'org.h2.Driver')
// Create a table
sql.execute '''
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100),
email VARCHAR(200),
active BOOLEAN DEFAULT TRUE
)
'''
// Insert data
sql.execute "INSERT INTO users (name, email) VALUES ('Nirranjan', 'nirranjan@example.com')"
sql.execute "INSERT INTO users (name, email) VALUES ('Viraj', 'viraj@example.com')"
sql.execute "INSERT INTO users (name, email, active) VALUES ('Prathamesh', 'prathamesh@example.com', false)"
// Query and print
println "All users:"
sql.eachRow('SELECT * FROM users') { row ->
println " ${row.id}. ${row.name} (${row.email}) - ${row.active ? 'active' : 'inactive'}"
}
println "\nActive count: ${sql.firstRow('SELECT COUNT(*) as cnt FROM users WHERE active = true').cnt}"
sql.close()
Output
All users: 1. Nirranjan (nirranjan@example.com) - active 2. Viraj (viraj@example.com) - active 3. Prathamesh (prathamesh@example.com) - inactive Active count: 2
What happened here: JDBC uses java.sql.DriverManager which loads drivers from the system classloader. By default, Grape loads JARs into a child classloader that DriverManager can’t see. @GrabConfig(systemClassLoader=true) forces the JAR onto the system classloader so JDBC driver registration works. This is the most common use case for @GrabConfig. Without it, you’d get No suitable driver found errors. Also see our Groovy SQL guide for more database patterns.
Example 7: Using Grape Programmatically
What we’re doing: Downloading dependencies at runtime using Grape.grab() instead of annotations – useful when the coordinates are dynamic.
Example 7: Using Grape Programmatically
// Programmatic Grape usage - coordinates can come from config or user input
def dependencies = [
[group: 'com.google.code.gson', module: 'gson', version: '2.10.1'],
[group: 'org.apache.commons', module: 'commons-text', version: '1.11.0']
]
dependencies.each { dep ->
println "Resolving ${dep.group}:${dep.module}:${dep.version}..."
groovy.grape.Grape.grab(
group: dep.group,
module: dep.module,
version: dep.version,
classLoader: this.getClass().classLoader.rootLoader ?: this.getClass().classLoader
)
println " Done."
}
// Now the classes are available
def gsonClass = Class.forName('com.google.gson.Gson')
def gson = gsonClass.newInstance()
println "\nGson loaded: ${gsonClass.name}"
def escaperClass = Class.forName('org.apache.commons.text.StringEscapeUtils')
def escaped = escaperClass.escapeHtml4('<script>alert("xss")</script>')
println "Escaped HTML: $escaped"
Output
Resolving com.google.code.gson:gson:2.10.1... Done. Resolving org.apache.commons:commons-text:1.11.0... Done. Gson loaded: com.google.gson.Gson Escaped HTML: <script>alert("xss")</script>
What happened here: groovy.grape.Grape.grab() is the programmatic API behind the @Grab annotation. It accepts the same parameters as named maps. The key advantage is that the coordinates can be variables – you could read them from a config file, a database, or user input. The classLoader parameter tells Grape which classloader to inject the JAR into. After grab() returns, the classes are immediately available via Class.forName(). This pattern is useful for plugin systems where dependencies aren’t known at compile time.
Example 8: Version Ranges and Latest Versions
What we’re doing: Using Ivy-style version ranges to grab flexible dependency versions instead of pinning exact versions.
Example 8: Version Ranges and Latest Versions
// Exact version (recommended for reproducibility)
@Grab('com.google.code.gson:gson:2.10.1')
import com.google.gson.Gson
def gson = new Gson()
println "Loaded Gson: ${Gson.package?.implementationVersion ?: 'version info not in manifest'}"
// Version range examples (syntax reference):
// [1.0,2.0] = 1.0 <= version <= 2.0 (inclusive both ends)
// [1.0,2.0) = 1.0 <= version < 2.0 (exclusive upper bound)
// (1.0,2.0) = 1.0 < version < 2.0 (exclusive both ends)
// [1.5,) = version >= 1.5 (open upper bound)
// LATEST = latest available version (use with caution)
println "\nVersion range syntax examples:"
println " [1.0,2.0] - Between 1.0 and 2.0 inclusive"
println " [1.0,2.0) - From 1.0 (inclusive) up to 2.0 (exclusive)"
println " [1.5,) - 1.5 or newer"
println " 1.+ - Latest 1.x version (Ivy dynamic revision)"
// Demonstrate the loaded version
def data = [message: 'Version ranges work!', range: '[1.0,2.0)']
println "\nJSON: ${gson.toJson(data)}"
Output
Loaded Gson: version info not in manifest
Version range syntax examples:
[1.0,2.0] - Between 1.0 and 2.0 inclusive
[1.0,2.0) - From 1.0 (inclusive) up to 2.0 (exclusive)
[1.5,) - 1.5 or newer
1.+ - Latest 1.x version (Ivy dynamic revision)
JSON: {"message":"Version ranges work!","range":"[1.0,2.0)"}
What happened here: Grape supports Ivy’s version range syntax because it uses Ivy as its dependency resolver. Square brackets [] mean inclusive, parentheses () mean exclusive. While version ranges add flexibility, they introduce non-determinism – your script might work today with version 1.8 and break tomorrow when 1.9 is released. For production scripts, always pin exact versions. Use ranges only for exploration or when you explicitly want the latest compatible version.
Example 9: The grape CLI Tool
What we’re doing: Managing the Grape cache from the command line using the grape command that ships with Groovy.
Example 9: The grape CLI Tool
# Install a dependency into the Grape cache $ grape install com.google.code.gson gson 2.10.1 # List all cached dependencies $ grape list # Resolve a dependency and show where the JAR file lives $ grape resolve com.google.code.gson gson 2.10.1 # Uninstall (remove from cache) # Navigate to ~/.groovy/grapes and delete the directory manually # There is no built-in uninstall command # Clear the entire Grape cache (use with caution!) # rm -rf ~/.groovy/grapes
Output
$ grape install com.google.code.gson gson 2.10.1 Resolving com.google.code.gson#gson;2.10.1... Download complete. $ grape list com.google.code.gson gson [2.10.1] org.apache.commons commons-lang3 [3.14.0] commons-io commons-io [2.15.1] com.h2database h2 [2.2.224] $ grape resolve com.google.code.gson gson 2.10.1 /home/user/.groovy/grapes/com.google.code.gson/gson/jars/gson-2.10.1.jar
What happened here: The grape command is a CLI wrapper around Grape’s API. grape install pre-downloads a JAR so your scripts don’t need network access on first run – useful for CI/CD or air-gapped environments. grape list shows everything in the local cache. grape resolve prints the absolute path to the cached JAR file, which is handy for debugging classpath issues. The cache structure mirrors Maven’s: ~/.groovy/grapes/{group}/{artifact}/jars/{artifact}-{version}.jar.
Example 10: Configuring Grape with grapeConfig.xml
What we’re doing: Setting up a global Grape configuration file to add corporate repositories, proxy settings, and default resolvers.
Example 10: Configuring Grape with grapeConfig.xml
// The global config file lives at ~/.groovy/grapeConfig.xml
// Here's a script that shows you where Grape looks for config
def grapeDir = new File(System.getProperty('user.home'), '.groovy')
def grapeConfig = new File(grapeDir, 'grapeConfig.xml')
def grapesCache = new File(grapeDir, 'grapes')
println "Groovy home: ${grapeDir.absolutePath}"
println "Config file: ${grapeConfig.absolutePath} (exists: ${grapeConfig.exists()})"
println "Cache directory: ${grapesCache.absolutePath} (exists: ${grapesCache.exists()})"
if (grapesCache.exists()) {
def groups = grapesCache.listFiles()?.findAll { it.isDirectory() } ?: []
println "\nCached groups (${groups.size()}):"
groups.take(10).each { group ->
def artifacts = group.listFiles()?.findAll { it.isDirectory() } ?: []
artifacts.each { artifact ->
println " ${group.name}:${artifact.name}"
}
}
}
// Example grapeConfig.xml content:
def sampleConfig = '''
<ivysettings>
<settings defaultResolver="downloadGrapes"/>
<resolvers>
<chain name="downloadGrapes" returnFirst="true">
<filesystem name="cachedGrapes">
<ivy pattern="\${user.home}/.groovy/grapes/[organisation]/[module]/ivy-[revision].xml"/>
<artifact pattern="\${user.home}/.groovy/grapes/[organisation]/[module]/[type]s/[artifact]-[revision](-[classifier]).[ext]"/>
</filesystem>
<ibiblio name="mavenCentral" m2compatible="true"/>
<!-- Add your corporate repo here -->
<!-- <ibiblio name="corporate" m2compatible="true" root="https://nexus.company.com/repository/maven-public/"/> -->
</chain>
</resolvers>
</ivysettings>
'''
println "\nSample grapeConfig.xml:"
println sampleConfig.trim()
Output
Groovy home: /home/user/.groovy
Config file: /home/user/.groovy/grapeConfig.xml (exists: true)
Cache directory: /home/user/.groovy/grapes (exists: true)
Cached groups (4):
com.google.code.gson:gson
org.apache.commons:commons-lang3
commons-io:commons-io
com.h2database:h2
Sample grapeConfig.xml:
<ivysettings>
<settings defaultResolver="downloadGrapes"/>
<resolvers>
<chain name="downloadGrapes" returnFirst="true">
<filesystem name="cachedGrapes">
<ivy pattern="${user.home}/.groovy/grapes/[organisation]/[module]/ivy-[revision].xml"/>
<artifact pattern="${user.home}/.groovy/grapes/[organisation]/[module]/[type]s/[artifact]-[revision](-[classifier]).[ext]"/>
</filesystem>
<ibiblio name="mavenCentral" m2compatible="true"/>
<!-- Add your corporate repo here -->
<!-- <ibiblio name="corporate" m2compatible="true" root="https://nexus.company.com/repository/maven-public/"/> -->
</chain>
</resolvers>
</ivysettings>
What happened here: Grape’s configuration is an Ivy settings file (grapeConfig.xml) stored in ~/.groovy/. This file controls which repositories Grape searches and in what order. The default configuration checks the local cache first, then Maven Central. To add a corporate Nexus or Artifactory server, add an <ibiblio> element with m2compatible="true". This is a global configuration – it applies to all Groovy scripts on the machine. For per-script repo overrides, use @GrabResolver in the script itself.
Example 11: Grab with Classifier and Extension
What we’re doing: Downloading a specific classifier (like sources or a platform-specific JAR) using @Grab‘s classifier and ext parameters.
Example 11: Grab with Classifier and Extension
// Standard grab - downloads the main JAR
@Grab('com.google.code.gson:gson:2.10.1')
import com.google.gson.Gson
// To grab a classifier (e.g., sources, javadoc, or platform-specific):
// @Grab(group='org.xerial', module='sqlite-jdbc', version='3.44.1.0', classifier='')
// To grab a non-JAR artifact (e.g., a POM or ZIP):
// @Grab(group='my.group', module='my-artifact', version='1.0', ext='zip')
// Demonstrate that classifier-based grabs work
// by showing the Grab annotation's available parameters
import java.lang.annotation.Annotation
def grabClass = Grab
def methods = grabClass.declaredMethods.collect { it.name }.sort()
println "Available @Grab parameters:"
methods.each { println " - $it" }
// Show a practical usage
def gson = new Gson()
def grabInfo = [
annotation: '@Grab',
parameters: [
'group': 'Maven groupId',
'module': 'Maven artifactId',
'version': 'Maven version',
'classifier': 'Maven classifier (sources, javadoc, etc.)',
'ext': 'File extension (default: jar)',
'type': 'Artifact type',
'conf': 'Ivy configuration',
'changing': 'Whether the artifact can change (snapshots)',
'transitive': 'Whether to download transitive dependencies',
'force': 'Force download even if cached',
'initClass': 'Whether to initialize the grabbed class'
]
]
println "\n${gson.toJson(grabInfo)}"
Output
Available @Grab parameters:
- changing
- classifier
- conf
- ext
- force
- group
- initClass
- module
- transitive
- type
- value
- version
{"annotation":"@Grab","parameters":{"group":"Maven groupId","module":"Maven artifactId","version":"Maven version","classifier":"Maven classifier (sources, javadoc, etc.)","ext":"File extension (default: jar)","type":"Artifact type","conf":"Ivy configuration","changing":"Whether the artifact can change (snapshots)","transitive":"Whether to download transitive dependencies","force":"Force download even if cached","initClass":"Whether to initialize the grabbed class"}}
What happened here: The @Grab annotation supports more parameters than most developers realize. classifier selects a variant of the artifact – common values are sources, javadoc, or platform-specific identifiers like linux-x86_64. ext overrides the file extension (default is jar). changing=true marks the artifact as a snapshot that should be re-checked even if cached. transitive=false skips downloading transitive dependencies. These advanced parameters map directly to Ivy’s dependency resolution options.
Example 12: Real-World Script – CSV Processing with @Grab
What we’re doing: Building a complete, production-style script that uses @Grab to download Apache Commons CSV and process data – the kind of script you’d actually write at work.
Example 12: Real-World Script – CSV Processing with @Grab
@Grapes([
@Grab('org.apache.commons:commons-csv:1.10.0'),
@Grab('com.google.code.gson:gson:2.10.1')
])
import org.apache.commons.csv.CSVFormat
import org.apache.commons.csv.CSVParser
import org.apache.commons.csv.CSVPrinter
import com.google.gson.GsonBuilder
// Create sample CSV data (simulating a file read)
def csvData = '''\
Name,Department,Salary,Active
Nirranjan,Engineering,95000,true
Viraj,Marketing,72000,true
Prathamesh,Engineering,105000,true
Prathamesh,Marketing,68000,false
Viraj,Engineering,88000,true
Frank,Sales,71000,true
Grace,Engineering,112000,true
Hank,Sales,65000,false
'''
// Parse CSV
def format = CSVFormat.DEFAULT.builder()
.setHeader()
.setSkipHeaderRecord(true)
.setTrim(true)
.build()
def parser = CSVParser.parse(csvData, format)
def records = parser.records
println "Total employees: ${records.size()}"
println "Headers: ${parser.headerNames}"
// Analysis: Average salary by department
def byDept = records.groupBy { it.get('Department') }
println "\nAverage salary by department:"
byDept.each { dept, employees ->
def avgSalary = employees.collect { it.get('Salary') as int }.average()
println " ${dept}: \$${String.format('%,.0f', avgSalary)}"
}
// Filter: Active engineers earning > 90k
def seniorEngineers = records.findAll {
it.get('Department') == 'Engineering' &&
it.get('Active') == 'true' &&
(it.get('Salary') as int) > 90000
}
println "\nSenior engineers (>\$90k, active):"
seniorEngineers.each { println " ${it.get('Name')}: \$${it.get('Salary')}" }
// Export as JSON
def gson = new GsonBuilder().setPrettyPrinting().create()
def summary = [
totalEmployees: records.size(),
activeCount: records.count { it.get('Active') == 'true' },
departments: byDept.collectEntries { dept, emps ->
[dept, [count: emps.size(), avgSalary: emps.collect { it.get('Salary') as int }.average()]]
}
]
println "\nJSON Summary:"
println gson.toJson(summary)
Output
Total employees: 8
Headers: [Name, Department, Salary, Active]
Average salary by department:
Engineering: $100,000
Marketing: $70,000
Sales: $68,000
Senior engineers (>$90k, active):
Prathamesh: $105000
Grace: $112000
JSON Summary:
{
"totalEmployees": 8,
"activeCount": 6,
"departments": {
"Engineering": {
"count": 4,
"avgSalary": 100000.0
},
"Marketing": {
"count": 2,
"avgSalary": 70000.0
},
"Sales": {
"count": 2,
"avgSalary": 68000.0
}
}
}
What happened here: This is a realistic data processing script that combines two @Grab dependencies – Apache Commons CSV for parsing and Gson for JSON output. The script parses CSV data, groups by department, calculates averages using Groovy’s .average() method, filters with findAll, and exports a summary as JSON. This is the sweet spot for @Grab: quick data processing scripts where setting up a full Gradle project would be overkill. The entire script is self-contained – anyone with Groovy installed can run it without any setup.
Common Pitfalls
Before we wrap up, here are the mistakes that trip up developers working with Groovy Grape and @Grab.
Pitfall 1: @Grab After Import
Placing @Grab after the import statement causes a compilation error because the class isn’t on the classpath when the import resolves.
Bad: @Grab after import
// BAD - import resolves before @Grab downloads the JAR
import com.google.gson.Gson
@Grab('com.google.code.gson:gson:2.10.1')
// ERROR: unable to resolve class com.google.gson.Gson
Good: @Grab before import
// GOOD - @Grab runs during compilation, before imports resolve
@Grab('com.google.code.gson:gson:2.10.1')
import com.google.gson.Gson
def gson = new Gson()
println gson.toJson([status: 'working'])
Pitfall 2: Missing systemClassLoader for JDBC
JDBC drivers register via the system classloader. Without @GrabConfig(systemClassLoader=true), the driver loads into a child classloader that DriverManager cannot see.
Bad: Missing @GrabConfig for JDBC
// BAD - JDBC driver not visible to DriverManager
@Grab('com.h2database:h2:2.2.224')
import groovy.sql.Sql
// Throws: No suitable driver found for jdbc:h2:mem:test
def sql = Sql.newInstance('jdbc:h2:mem:test', 'sa', '', 'org.h2.Driver')
Good: Adding @GrabConfig for JDBC
// GOOD - driver loads into system classloader
@GrabConfig(systemClassLoader=true)
@Grab('com.h2database:h2:2.2.224')
import groovy.sql.Sql
def sql = Sql.newInstance('jdbc:h2:mem:test', 'sa', '', 'org.h2.Driver')
println "Connected: ${sql.connection.metaData.databaseProductName}"
sql.close()
Pitfall 3: Corrupted Grape Cache
When a download fails mid-transfer, it can leave a partially-downloaded JAR in the cache. Subsequent runs find the cached (broken) file and fail with cryptic ZipException or ClassNotFoundException errors.
Fix: Clear the corrupted cache entry
# Delete the specific corrupted dependency $ rm -rf ~/.groovy/grapes/com.google.code.gson/gson # Or delete everything and re-download $ rm -rf ~/.groovy/grapes # Then re-run your script - Grape will re-download $ groovy my-script.groovy
Pitfall 4: Network Dependency in Production
@Grab requires network access on first run. If your production server can’t reach Maven Central, the script fails. Pre-install dependencies or use a local repository mirror.
Good: Pre-install for offline environments
# On a machine with network access, pre-install dependencies $ grape install com.google.code.gson gson 2.10.1 $ grape install org.apache.commons commons-csv 1.10.0 # Copy the cache to the production server $ tar -czf grapes-cache.tar.gz ~/.groovy/grapes $ scp grapes-cache.tar.gz prod-server:~ # On the production server, extract the cache $ tar -xzf grapes-cache.tar.gz -C ~/
Conclusion
Grape and @Grab solve a specific problem extremely well: getting external libraries into standalone Groovy scripts without project scaffolding. We covered the shorthand and named-parameter syntax, grouping with @Grapes, custom repositories with @GrabResolver, transitive dependency exclusions, classloader configuration for JDBC drivers, programmatic usage via Grape.grab(), version ranges, the grape CLI, global configuration, and a real-world CSV processing script.
The key rule: use @Grab for scripts and prototypes; use Gradle or Maven for applications. @Grab is not a build tool replacement – it’s a convenience for the scripting use case where Groovy shines. When your script grows into a project with tests, CI, and deployment, graduate to a proper build system. For more on Groovy’s build ecosystem, see our Groovy Gradle guide.
If you’re working with Groovy scripts that need compile-time checks alongside @Grab, check our Groovy @CompileStatic guide. And for understanding how @Grab hooks into the compilation process, our next post on Groovy Compilation Lifecycle explains exactly when and how Grape resolves dependencies.
Best Practices
- DO pin exact versions in production scripts –
@Grab('gson:2.10.1')not@Grab('gson:2.+'). - DO place
@Grabannotations beforeimportstatements. - DO use
@GrabConfig(systemClassLoader=true)for JDBC drivers and SPI-based libraries. - DO use
@GrabExcludeto resolve logging framework conflicts. - DO pre-install dependencies with
grape installfor offline environments. - DON’T use
@Grabin application code that belongs in a Gradle/Maven project. - DON’T use version ranges in scripts that others depend on.
- DON’T ignore
@GrabExcludewhen you have dependency conflicts – letting Ivy resolve conflicts automatically can pick the wrong version. - DON’T forget to handle network failures gracefully in scripts that use
@Grabfor the first time.
Up next: Groovy Compilation Lifecycle and Architecture
Frequently Asked Questions
What is @Grab in Groovy and how does it work?
@Grab is a Groovy annotation that downloads Maven dependencies at compile time using the Grape dependency manager. You specify Maven coordinates (group, artifact, version) and Grape uses Apache Ivy to resolve and download the JAR from Maven Central or any configured repository. The JAR is cached locally in ~/.groovy/grapes so subsequent runs don’t need network access. Usage: @Grab('com.google.code.gson:gson:2.10.1') before your import statement.
Where does Groovy Grape store downloaded JARs?
Grape stores downloaded JARs in ~/.groovy/grapes/ following the pattern {group}/{artifact}/jars/{artifact}-{version}.jar. For example, Gson 2.10.1 would be at ~/.groovy/grapes/com.google.code.gson/gson/jars/gson-2.10.1.jar. You can view cached dependencies with grape list and find the exact path with grape resolve group artifact version.
How do I use @Grab with a private Maven repository?
Add @GrabResolver before your @Grab annotation: @GrabResolver(name='myrepo', root='https://nexus.company.com/repository/maven-public/'). For a global configuration that applies to all scripts, edit ~/.groovy/grapeConfig.xml and add an <ibiblio> element with m2compatible="true" and your repository URL inside the resolver chain.
Why does @Grab fail with JDBC drivers and how do I fix it?
JDBC uses java.sql.DriverManager which looks for drivers in the system classloader. By default, Grape loads JARs into a child classloader that DriverManager cannot see, causing ‘No suitable driver found’ errors. Fix this by adding @GrabConfig(systemClassLoader=true) before your @Grab annotation. This forces the JAR onto the system classloader where DriverManager can find it.
Should I use @Grab instead of Gradle or Maven for my Groovy project?
No. @Grab is designed for standalone scripts and quick prototypes – situations where creating a full project structure would be overkill. For applications with tests, CI pipelines, and deployment, use Gradle or Maven. @Grab lacks features like dependency locking, reproducible builds, multi-module support, and plugin ecosystems that build tools provide. Think of @Grab as ‘pip install for a single script’ rather than a project dependency manager.
Related Posts
Previous in Series: Groovy @CompileStatic – Type Checking and Performance
Next in Series: Groovy Compilation Lifecycle and Architecture
Related Topics You Might Like:
- Groovy Gradle – Build Automation with Groovy
- Groovy SQL – Database Programming
- Groovy Compilation Lifecycle and Architecture
This post is part of the Groovy & Grails Cookbook series on TechnoScripts.com

No comment