Groovlets, Servlets & JMX
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Apache Groovy 6.0 (6.0.0, 2026-09-22) and the examples were run with Groovy 6.0.0 on JDK 26
(checked on 2026-10-14, on macOS). It covers two modules of the standard distribution. groovy-servlet turns .groovy files
into servlets (Groovlets) and renders template pages; groovy-jmx reads and exports Java Management Extensions (JMX) managed
beans with GroovyMBean and JmxBuilder. The servlet examples were run in an embedded Jetty 12.1.3 server fetched with
@Grab into a scratch directory, bound to a random port on localhost, and called with plain URL requests; nothing was
installed. Examples that need a separately installed servlet container (Tomcat, WebLogic, Spring) or a remote JVM are labelled
"not run here". The page follows the greenhouse running scenario: zones, a temperature and a heater.
Groovlets and GroovyServlet
A Groovlet is a Groovy script that a groovy.servlet.GroovyServlet runs for every request. The servlet compiles the
.groovy file on first use, caches the class, and recompiles it when the file changes, so editing a script takes effect
without redeploying. A Groovlet is a script, not a class: there is no class declaration and the request, response and
session are already available as variables.
| Variable | Bound to | Notes |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
a |
|
|
|
what |
|
|
for binary content |
|
|
see Builders |
|
|
see JSON |
These variables are bound lazily, on first access, and cannot be reassigned in the script, so a Groovlet can still set the
status code or content type on response before the first use of out.
A Groovlet run in embedded Jetty
The first script serves four Groovlets from a site directory and requests them. It is a plain Groovy script rather than a
servlet-container setup, so it runs with the groovy command. GroovyServlet is registered for *.groovy and
TemplateServlet (see below) for *.gsp. Groovy 6 targets Jakarta EE 11, so the Jetty ee11 module is used, and the
jakarta.servlet.jsp-api jar is added because groovy.servlet.ServletCategory, which gives session.counter its property
syntax, refers to a JSP class (without it Groovy logs a warning and the session.counter property access in hello.groovy
fails). The first Groovlet counts visits in the session:
// site/hello.groovy
if (!session) {
session = request.getSession(true)
}
if (!session.counter) {
session.counter = 1
}
response.contentType = 'text/plain'
println "zone=${params.zone ?: 'none'} visit=${session.counter} method=${request.method}"
session.counter = session.counter + 1
The others use the html and json implicit variables and throw an error on purpose:
// site/markup.groovy
html.html {
head { title('Greenhouse') }
body { p("Hello from ${params.zone ?: 'nowhere'}") }
}
// site/status.groovy
response.contentType = 'application/json'
json.call(zone: params.zone ?: 'north', temperature: 21.5)
// site/fail.groovy
throw new IllegalStateException('boom')
// serve.groovy (run with the groovy command; JAVA_OPTS sets -Dgrape.root to a scratch directory)
@GrabConfig(systemClassLoader = true)
@Grab('org.eclipse.jetty.ee11:jetty-ee11-servlet:12.1.3')
@Grab('jakarta.servlet.jsp:jakarta.servlet.jsp-api:4.0.0')
import groovy.servlet.GroovyServlet
import groovy.servlet.TemplateServlet
import org.eclipse.jetty.server.Server
import org.eclipse.jetty.ee11.servlet.ServletContextHandler
def server = new Server(0) // port 0: pick a free port
def handler = new ServletContextHandler('/', ServletContextHandler.SESSIONS)
handler.baseResourceAsString = new File('site').absolutePath
handler.addServlet(GroovyServlet, '*.groovy')
handler.addServlet(TemplateServlet, '*.gsp')
server.handler = handler
server.start()
def port = server.URI.port
def get = { String path, String cookie = null ->
def c = new URL("http://localhost:${port}${path}").openConnection()
if (cookie) c.setRequestProperty('Cookie', cookie)
def code = c.responseCode
def body = (code < 400 ? c.inputStream : c.errorStream)?.text ?: ''
[code: code, type: c.contentType, body: body, cookie: c.getHeaderField('Set-Cookie')?.split(';')?.first()]
}
def r1 = get('/hello.groovy?zone=south')
println "${r1.code} ${r1.type} -> ${r1.body.trim()}"
def r2 = get('/hello.groovy?zone=south', r1.cookie) // same session cookie
println "${r2.code} -> ${r2.body.trim()}"
def page = get('/markup.groovy?zone=east')
println "${page.code} ${page.type}"
println page.body.readLines().collect { it.trim() }.findAll { it }.join(' ')
def st = get('/status.groovy')
println "${st.code} ${st.type} -> ${st.body}"
// ... the TemplateServlet and error checks follow below
server.stop()
200 text/plain;charset=utf-8 -> zone=south visit=1 method=GET
200 -> zone=south visit=2 method=GET
200 text/html;charset=utf-8
<html> <head> <title>Greenhouse</title> </head> <body> <p>Hello from east</p> </body> </html>
200 application/json -> {"zone":"north","temperature":21.5}
The second request sends back the cookie of the first, so the counter in the session goes from 1 to 2. Two details are easy to miss:
-
The script file name becomes the class name, so a Groovlet called
json.groovyorhtml.groovymakesjsonorhtmlresolve to the script class instead of the implicit variable (the first attempt at the status Groovlet failed withNo signature of static method: call for class: jsonfor that reason). Name the files after what they do. -
json.call(map)writes a JSON object to the response; useresponse.contentTypeto label it.
Setting up Groovlets in a container
Not run here (needs a separately installed container). The traditional setup declares the servlet in web.xml and puts the
Groovy jars in WEB-INF/lib:
<servlet>
<servlet-name>Groovy</servlet-name>
<servlet-class>groovy.servlet.GroovyServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>Groovy</servlet-name>
<url-pattern>*.groovy</url-pattern>
</servlet-mapping>
The .groovy files go where static files would, for example the web root. In Tomcat a context entry such as
<Context path="/groovy" docBase="/path_to_servlet_base"/> in conf/server.xml publishes them under /groovy/. Servlet
engines that register servlets in code (as the Jetty script above does) need no web.xml at all.
The Jakarta EE line matters for the dependency:
-
From Groovy 6,
groovy-servlettargets Jakarta EE 11 (Servlet 6.1) and also works in Jakarta EE 10 (Servlet 6.0) containers, because it uses no Servlet 6.1-only APIs. The classes live in thejakarta.servletpackage. -
A
javaxclassifier of the artifact is published for pre-Jakarta containers such as Tomcat 9 and Jetty 9, which use thejavax.servletpackage. -
Choose the container and the artifact together; mixing the
javaxjar with a Jakarta container fails at class loading.
Error responses
When a Groovlet throws, GroovyServlet answers with a generic HTTP 500 and writes the details (script path, exception
message and a stack frame) to the servlet and application logs, so internal detail does not reach the caller. For
development you can restore detailed client responses with the groovy.servlet.verbose.errors system property
(-Dgroovy.servlet.verbose.errors=true), or by overriding isVerboseErrors() in a subclass of GroovyServlet. A request for
a script that does not exist gets a 404. The same script was run twice, without and with the property:
# default
fail: 500 message: false exception class: false local path: false
missing: 404
# JAVA_OPTS="-Dgroovy.servlet.verbose.errors=true"
fail: 500 message: true exception class: false local path: false
missing: 404
With the property on, the text of the exception (boom) is in the client response; by default it is only in the server log.
Leave the property off in production.
TemplateServlet and GSP pages
groovy.servlet.TemplateServlet runs one of Groovy’s template engines (see
Template Engines) for each request, which allows Groovy Server Pages
(GSPs): HTML with embedded ${...} expressions and <% ... %> scriptlets, similar to JSPs but with Groovy code. The request
parameters are available through request as in a Groovlet. The page below was served by the same Jetty instance:
// site/zone.gsp
<html>
<body>
<h1>Zone ${request.getParameter('zone') ?: 'none'}</h1>
<% 3.times { n -> %>
<p>Row ${n + 1}</p>
<% } %>
</body>
</html>
200 text/html;charset=utf-8
<html> <body> <h1>Zone west</h1> <p>Row 1</p> <p>Row 2</p> <p>Row 3</p> </body> </html> <!-- Generated by Groovy TemplateServlet [create/get=10 ms, make=3 ms] -->
The servlet appends an HTML comment with timing information to every page it generates. The module also contains
AbstractHttpServlet, a base class for writing traditional servlets in Groovy, and the helper classes ServletBinding and
ServletCategory that provide the implicit variables and the property syntax outside GroovyServlet. For a new
application a GSP or Groovlet is a convenient way to prototype a page; for anything larger a framework gives routing,
validation and security that these classes do not.
Monitoring the JVM with GroovyMBean
Every JVM publishes JMX managed beans (MBeans) about itself — memory, threads, garbage collection, the runtime — through
the platform MBean server. groovy.jmx.GroovyMBean wraps an MBeanServerConnection and an object name, and exposes the
MBean attributes as Groovy properties and its operations as methods. This script reads and writes the Memory MBean of its
own JVM:
import groovy.jmx.GroovyMBean
import java.lang.management.ManagementFactory
import javax.management.ObjectName
def server = ManagementFactory.platformMBeanServer
def memory = new GroovyMBean(server, 'java.lang:type=Memory')
println memory.name()
println memory.HeapMemoryUsage.class.name
println memory.HeapMemoryUsage.get('used') > 0
println memory.Verbose
def runtime = new GroovyMBean(server, 'java.lang:type=Runtime')
println runtime.SpecVersion
println runtime.VmName != null
// attribute and operation metadata
println memory.listAttributeNames().sort().take(4)
println memory.listOperationNames().sort()
// write an attribute and call an operation
memory.Verbose = true
println memory.Verbose
memory.Verbose = false
memory.gc()
println 'gc invoked'
// query names
def collectors = server.queryNames(new ObjectName('java.lang:type=GarbageCollector,*'), null)
println collectors.size() > 0
println server.getMBeanCount() > 10
java.lang:type=Memory
javax.management.openmbean.CompositeDataSupport
true
false
26
true
[HeapMemoryUsage, NonHeapMemoryUsage, ObjectName, ObjectPendingFinalizationCount]
[gc]
true
gc invoked
true
true
Points worth knowing:
-
Attribute names follow the JMX convention and start with a capital letter (
HeapMemoryUsage,Verbose). The property name is the attribute name, somemory.heapMemoryUsagewould not be found. -
A composite attribute such as
HeapMemoryUsageis aCompositeData, read withget('used'). -
name()returns the object name,listAttributeNames()andlistOperationNames()describe the MBean, andserver.queryNames(pattern, null)finds MBeans by anObjectNamepattern. -
java.lang:type=Runtimereports the specification version of the JVM (26 here), which is the same value as the running JDK.
The documentation applies the same idea to the MBeans that servers publish, for example Tomcat (with its own domain and
type= keys) and WebLogic, and to a Spring application through its MBeanExporter. Those pages need the product and are
not run here; the Groovy side is identical: connect to the MBean server, build a GroovyMBean from an object name and read
its attributes.
Reaching another JVM
Not run here (needs a second JVM started with remote management enabled, for example with
-Dcom.sun.management.jmxremote.port=9999). A client connects through a JMXConnector and passes the resulting
MBeanServerConnection to GroovyMBean:
import groovy.jmx.GroovyMBean
import javax.management.remote.JMXConnectorFactory
import javax.management.remote.JMXServiceURL
def url = new JMXServiceURL('service:jmx:rmi:///jndi/rmi://localhost:9999/jmxrmi')
def connector = JMXConnectorFactory.connect(url)
try {
def server = connector.MBeanServerConnection
def memory = new GroovyMBean(server, 'java.lang:type=Memory')
println memory.HeapMemoryUsage.get('used')
} finally {
connector.close()
}
JMX gives control over a running JVM, including invoking operations such as garbage collection and in some applications much
more. Remote access must use authentication and TLS, and the management port must not be reachable from untrusted networks.
A SecurityException with the message "Authentication failed! Credentials required" means the container’s JMX access is
password protected; the documentation shows how to pass credentials in the environment map given to
JMXConnectorFactory.connect (with the JMXConnector.CREDENTIALS key), taken from a secure source rather than a literal in
the script.
Exporting MBeans with JmxBuilder
groovy.jmx.builder.JmxBuilder is a builder (see Builders) that registers
your own objects as MBeans, and creates timers and listeners, without writing an interface and an MBean-suffixed class. By
default it works against the platform MBean server (jmx.server). The example exports a Greenhouse object, reads and
writes it through GroovyMBean, and uses a timer with a listener:
import groovy.jmx.GroovyMBean
import groovy.jmx.builder.JmxBuilder
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import javax.management.ObjectName
class Greenhouse {
double temperature = 21.5
String zone = 'north'
void ventilate(int minutes) { println "ventilating ${zone} for ${minutes} min" }
}
def jmx = new JmxBuilder()
def house = new Greenhouse() // not called "bean": that would shadow the bean() node
def beans = jmx.export {
bean(target: house, name: 'demo:type=Greenhouse',
attributes: [temperature: [writable: true], zone: [:]],
operations: ['ventilate'])
}
println beans.size()
def server = jmx.server // the platform MBean server
println server.isRegistered(new ObjectName('demo:type=Greenhouse'))
def mbean = new GroovyMBean(server, 'demo:type=Greenhouse')
println mbean.listAttributeNames().sort()
println mbean.Temperature
mbean.Temperature = 23.0d // 23.0 would be a BigDecimal and fail the double setter
println house.temperature
try { mbean.Zone = 'south' } catch (javax.management.AttributeNotFoundException e) { println 'Zone is read-only' }
mbean.ventilate(5)
println mbean.listOperationNames()
// a timer MBean plus a listener
def latch = new CountDownLatch(1)
def types = []
jmx.timer(name: 'demo:type=Timer', event: 'tick', period: '100')
jmx.listener(from: 'demo:type=Timer', event: 'tick', call: { e ->
types << e.getClass().simpleName << e.keySet().sort()
latch.countDown()
})
server.invoke(new ObjectName('demo:type=Timer'), 'start', null, null)
println latch.await(5, TimeUnit.SECONDS)
println types.take(2)
server.invoke(new ObjectName('demo:type=Timer'), 'stop', null, null)
1
true
[Temperature, Zone]
21.5
23.0
Zone is read-only
ventilating north for 5 min
[ventilate, getTemperature, setTemperature, getZone]
true
[HashMap, [data, event, sequenceNumber, source, timeStamp]]
What the run shows:
-
export { ... }returns the list of registered MBeans; thebean()node takes the object (target), the JMX name (name, asdomain:key=value), and whichattributes,operationsandconstructorsto expose. A wildcard'*'exposes all of them, but then also exposes internal members; the run withoperations: '*'listed a compiler-generated method among the operations, so list the operations you mean. -
Attributes are read-only unless the descriptor says
writable: true. Writing the read-onlyZonefailed withAttributeNotFoundException. -
The attribute names are capitalised on the MBean (
Temperature,Zone) even though the Groovy properties are not, and the setter receives a value of the declared type;23.0is aBigDecimaland was rejected for adoubleuntil written as23.0d. -
A local variable named
beanshadows the builder node: the first attempt failed withNo signature of method: call for class: Greenhousebecausebean(target: ...)called the variable. -
timer(...)creates a timer MBean that emits a notification of the giveneventeveryperiodmilliseconds (a unit suffix such as'1s'is also accepted), andlistener(from:, event:, call:)registers a closure for it. The listener closure does not take the usual builder nesting (listener { ... }failed with'listener' doesn't support nesting); the handler goes incall:. It receives aMapwith the keysdata,event,sequenceNumber,sourceandtimeStamp.
Attribute and operation events
The descriptors can carry closures. onChange runs when an attribute is written and receives the old and new value;
onCall runs when an operation is invoked and receives the event name:
import groovy.jmx.GroovyMBean
import groovy.jmx.builder.JmxBuilder
class Heater {
double target = 18.0
void boost() { println 'boosting' }
}
def jmx = new JmxBuilder()
def heater = new Heater()
def log = []
jmx.export {
bean(target: heater, name: 'demo:type=Heater',
attributes: [target: [writable: true,
onChange: { e -> log << "changed ${e.oldValue} -> ${e.newValue}" }]],
operations: [boost: [onCall: { e -> log << "called ${e.event}" }]])
}
def mbean = new GroovyMBean(jmx.server, 'demo:type=Heater')
mbean.Target = 20.5d
mbean.boost()
log.each { println it }
boosting
changed 18.0 -> 20.5
called jmx.operation.call.boost
The documentation also covers emitting your own JMX notifications with emitter and broadcasting them with send, listening
to arbitrary MBean notifications with listener, and exporting through descriptors with constructors and parameter
signatures. They follow the same node-and-map style as the nodes shown above; only the ones in this page were run.
A connector server and client on localhost
JmxBuilder can also start a connector server and open a client, which is how a management tool reaches an MBean server in
another process. This script runs both ends in one JVM, over RMI on localhost, with no authentication; that is acceptable
only for such a local experiment:
import groovy.jmx.GroovyMBean
import groovy.jmx.builder.JmxBuilder
def jmx = new JmxBuilder()
java.rmi.registry.LocateRegistry.createRegistry(18999) // the RMI registry the connector registers in
def server = jmx.connectorServer(protocol: 'rmi', host: 'localhost', port: 18999)
server.start()
println server.isActive()
def client = new JmxBuilder().connectorClient(protocol: 'rmi', host: 'localhost', port: 18999)
client.connect()
def conn = client.MBeanServerConnection
def runtime = new GroovyMBean(conn, 'java.lang:type=Runtime')
println runtime.VmName != null
println conn.getAttribute(new javax.management.ObjectName('java.lang:type=Runtime'), 'SpecVersion')
client.close()
server.stop()
println server.isActive()
System.exit(0)
true
true
26
false
Pitfalls
-
A Groovlet whose file is called
json.groovyorhtml.groovyhides the implicit variable of the same name. -
sessionisnulluntil a session exists; test it and callrequest.getSession(true), as the example does. -
Property syntax on
session(session.counter) depends onServletCategory, which refers to a JSP class; add the JSP API jar to a container that does not provide it, or usegetAttributeandsetAttribute. -
The Jakarta/javax split must match the container, and in Groovy 6 the default artifact is the Jakarta one.
-
Leave
groovy.servlet.verbose.errorsoff outside development: the exception message ends up in the client response. -
JMX attributes are capitalised and typed: a
BigDecimalis not adouble, and an attribute is read-only unless exported withwritable: true. -
Expose operations by name rather than with
'*', and never open a JMX port to untrusted networks.
Where the book is outdated
Books from the Groovy 1.x and 2.x era describe Groovlets with javax.servlet classes, the groovy.servlet.GroovyServlet
mapping in web.xml and the implicit variables, which are still valid. What they predate is the move of the module to the
Jakarta namespace (Groovy 6 targets Jakarta EE 11, with a javax classifier for pre-Jakarta containers), the safer default 500
response with the groovy.servlet.verbose.errors switch, and the json implicit variable. GroovyMBean and JmxBuilder
work as the book describes; JVM command-line flags for remote management have changed over JDK releases, so check the
documentation of the JDK in use rather than the flags in a book.