Groovify Your Java Servlets (Part 1) : Getting Started
Get Groovy with it!
Join the DZone community and get the full member experience.
Join For FreeThis article is not about Groovlets, which are Groovy scripts executed by a servlet. They are run on request, having the whole web context (request, response, etc.) bound to the evaluation context. Groovlets are much more suitable for smaller web applications. Compared to Java Servlets, coding in Groovy can be much simpler.
You may also like: From Java to Groovy in a Few Easy Steps
This post provides a simple example demonstrating the kinds of things you can do with a Groovlet. It has a couple of implicit variables we can use, for example, request, response to access the HttpServletRequest,
and HttpServletResponse
objects. We have access to the HttpSession
with the session variable. If we want to output data, we can use out
, sout
, and HTML. This is more like a script as it does not have a class wrapper.
Groovlet
if (!session) {
session = request.getSession(true)
}
if (!session.counter) {
session.counter = 1
}
html.html { // html is implicitly bound to new MarkupBuilder(out)
head {
title('Groovy Servlet')
}
body {
p("Hello, ${request.remoteHost}: ${session.counter}! ${new Date()}")
}
}
session.counter = session.counter + 1
In this first part of this series, we are going to set the building blocks of how to bring the same logic to enhance the Servlet API. The goal is to write our artifacts (servlets, filters, listeners) with the Groovy language, which is in constant evolution in the Tiobe Index of language popularity, coming in this month at No. 11.
Next, we will register them in the ServletContext
class. We will consider a live development as a mandatory feature, even if it is well-known that the ServletContext
class will throw an IllegalStateException
when adding a new Servlet
, Filter
, or Listener
after its initialization. Also, updating the artifacts at runtime is another challenging task.
To lay things down, I will first uncover the final design. In the next articles, I will cover more ground to show you, in detail, how things are achieved technically. The main vision is to use the Groovy language and its provided modules (JSON, SQL, etc.) to simplify Servlet API web development while waiting to apply the same principles to the JAX-RS API.
It is worth it to mention that we are going to build a non-intrusive API that will not affect the current structure of your Java EE project, and you are free to groovify your existing Java Servlets over time. Let's get started!
New Servlet Signature
import org.gservlet.annotation.Servlet
@Servlet("/customers")
class CustomerServlet {
void get() {
def customers = []
customers << [firstName : "John", lastName : "Doe"]
customers << [firstName : "Kate", lastName : "Martinez"]
customers << [firstName : "Allisson", lastName : "Becker"]
json(customers)
}
void post() {
def customer = request.body // get the json request payload as object
json(customer)
}
void put() {
def customer = request.body // get the json request payload as object
json(customer)
}
void delete() {
def param = request.param // shortcut to request.getParameter("param")
def attribute = request.attribute // shortcut to request.getAttribute("attribute")
}
void head() {
}
void trace() {
}
void options() {
}
}
The @WebServlet
annotation, which is used to define a Servlet component in a web application, will be reduced to our own @Servlet
annotation with the same attributes. In the same spirit, and as read above, the name of the HTTP request method handlers (doGet
, doPost
, doPut
, and so on) will be shortened and they will take no arguments since the request and the response are now implicit variables.
package org.gservlet.annotation;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Servlet {
String name() default "";
String[] value() default {};
String[] urlPatterns() default {};
int loadOnStartup() default -1;
WebInitParam [] initParams() default {};
boolean asyncSupported() default false;
String smallIcon() default "";
String largeIcon() default "";
String description() default "";
String displayName() default "";
}
We will no longer explicitly extend the HttpServlet
class. When a script is loaded by the GroovyScriptEngine
, we will use javassist
, a library for dealing with Java bytecode, to extend dynamically our own derived HttpServlet
class within a BytecodeProcessor instance, which is set anonymously to the Groovy CompilerConfiguration
object.
Servlet Base Class
package org.gservlet;
import java.lang.reflect.Method;
import java.util.logging.Logger;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public abstract class HttpServlet extends javax.servlet.http.HttpServlet {
protected HttpServletRequest request;
protected HttpServletResponse response;
protected final Logger logger = Logger.getLogger(HttpServlet.class.getName());
public void doGet(HttpServletRequest request, HttpServletResponse response) {
this.request = request;
this.response = response;
invoke("get");
}
public void doPost(HttpServletRequest request, HttpServletResponse response) {
this.request = request;
this.response = response;
invoke("post");
}
public void doPut(HttpServletRequest request, HttpServletResponse response) {
this.request = request;
this.response = response;
invoke("put");
}
public void doDelete(HttpServletRequest request, HttpServletResponse response) {
this.request = request;
this.response = response;
invoke("delete");
}
public void doHead(HttpServletRequest request, HttpServletResponse response) {
this.request = request;
this.response = response;
invoke("head");
}
public void doTrace(HttpServletRequest request, HttpServletResponse response) {
this.request = request;
this.response = response;
invoke("trace");
}
public void doOptions(HttpServletRequest request, HttpServletResponse response) {
this.request = request;
this.response = response;
invoke("options");
}
private void invoke(String methodName) {
try {
Method method = getClass().getDeclaredMethod(methodName);
method.invoke(this);
} catch (NoSuchMethodException e) {
logger.info("no method " + methodName + " has been declared for the servlet " + this.getClass().getName());
} catch (Exception e) {
e.printStackTrace();
}
}
public void json(Object object) throws IOException {
response.setHeader("Content-Type", "application/json");
write(groovy.json.JsonOutput.toJson(object));
}
public void write(String content) throws IOException {
response.getWriter().write(content);
}
}
The code of the HttpServlet
base class above is not complete. I'm going to wait for the next articles to show you how we are going to make the HttpServletRequest
, the HttpServletResponse
, and so on, to become implicit variables.
As stated in the style guide, in Groovy, a getter and a setter form what we call a property. Instead of the Java-way of calling getters/setters, we can use a field-like access notation for accessing and setting such properties, but there is more to write about.
Bytecode Processing
protected GroovyScriptEngine createScriptEngine(File folder) throws Exception {
URL[] urls = { folder.toURI().toURL()};
GroovyScriptEngine engine = new GroovyScriptEngine(urls);
CompilerConfiguration configuration = new CompilerConfiguration();
final ClassPool classPool = ClassPool.getDefault();
classPool.insertClassPath(new LoaderClassPath(engine.getParentClassLoader()));
configuration.setBytecodePostprocessor(new BytecodeProcessor() {
public byte[] processBytecode(String name, byte[] original) {
ByteArrayInputStream stream = new ByteArrayInputStream(original);
try {
CtClass clazz = classPool.makeClass(stream);
clazz.detach();
Object[] annotations = clazz.getAnnotations();
for (Object annotation : annotations) {
String value = annotation.toString();
if (value.indexOf("Servlet") != -1) {
clazz.setSuperclass(classPool.get(HttpServlet.class.getName()));
return clazz.toBytecode();
}
}
} catch (Exception e) {
e.printStackTrace();
}
return original;
}
});
engine.setConfig(configuration);
return engine;
}
To route an HTTP request to our new handler methods (get
, post
, put
, and so on), we will register our servlets in the ServletContext
as dynamic proxies with an invocation handler instance. A dynamic proxy can be thought of as a kind of Facade as it allows one single class with one single method to service multiple method calls to arbitrary classes with an arbitrary number of methods. When a method is invoked on a proxy instance, the method invocation is encoded and dispatched to the invoke method of its invocation handler.
InvocationHandler class
package org.gservlet;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class DynamicInvocationHandler implements InvocationHandler {
protected Object target;
public DynamicInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return method.invoke(target, args);
}
public Object getTarget() {
return target;
}
public void setTarget(Object target) {
this.target = target;
}
}
Servlet Registration
protected void addServlet(ServletContext context, Servlet annotation, Object object) {
String name = annotation.name().trim().equals("") ? object.getClass().getName() : annotation.name();
ServletRegistration registration = context.getServletRegistration(name);
if (registration == null) {
DynamicInvocationHandler handler = new DynamicInvocationHandler(object);
handlers.put(name, handler);
Object servlet = Proxy.newProxyInstance(this.getClass().getClassLoader(),
new Class[] { javax.servlet.Servlet.class }, handler);
registration = context.addServlet(name, (javax.servlet.Servlet) servlet);
if (annotation.value().length > 0) {
registration.addMapping(annotation.value());
}
if (annotation.urlPatterns().length > 0) {
registration.addMapping(annotation.urlPatterns());
}
} else {
String message = "The servlet with the name " + name
+ " has already been registered. Please use a different name or package";
throw new RuntimeException(message);
}
}
To reload an artifact means to update the corresponding target object of the proxy's invocation handler once our file watcher can detect a file change. The java.nio.file package provides a file change notification API, called the Watch Service API. This API enables us to register a directory (or directories) with the watch service.
When registering, we tell the service which types of events we are interested in: file creation, file deletion, or file modification. When the service detects an event of interest, it is forwarded to the registered process and handled as needed. The WatchService API is fairly low level, allowing us to customize it. We can use it as is, or we can choose to create a high-level API on top of this mechanism.
public void loadScripts(File folder) throws Exception {
if (folder.exists()) {
File[] files = folder.listFiles();
if (files != null) {
for (File file : files) {
if (file.isFile()) {
loadScript(file);
} else {
loadScripts(file);
}
}
}
watch(folder);
}
}
public void loadScript(File script) throws Exception {
Object object = scriptManager.loadScript(script);
register(object);
}
public void register(Object object) throws Exception {
Annotation[] annotations = object.getClass().getAnnotations();
for (Annotation annotation : annotations) {
if (annotation instanceof Servlet) {
addServlet(context, (Servlet) annotation, object);
}
if (annotation instanceof Filter) {
addFilter(context, (Filter) annotation, object);
}
if (annotation instanceof ContextListener) {
addListener(context, annotation, object);
}
if (annotation instanceof RequestListener) {
addListener(context, annotation, object);
}
if (annotation instanceof ContextAttributeListener) {
addListener(context, annotation, object);
}
if (annotation instanceof RequestAttributeListener) {
addListener(context, annotation, object);
}
if (annotation instanceof SessionListener) {
addListener(context, annotation, object);
}
if (annotation instanceof SessionAttributeListener) {
addListener(context, annotation, object);
}
}
}
protected void watch(File folder) {
boolean reload = Boolean.parseBoolean(System.getenv(Constants.RELOAD));
if (reload) {
new FileWatcher().addListener(new FileAdapter() {
public void onCreated(String fileName) {
reload(new File(folder + "/" + fileName));
}
}).watch(folder);
}
}
New Filter Signature
import org.gservlet.annotation.Filter
@Filter("/*")
class CORSFilter {
void filter() {
response.addHeader("Access-Control-Allow-Origin", "*")
response.addHeader("Access-Control-Allow-Methods","GET, OPTIONS, HEAD, PUT, POST, DELETE")
if (request.method == "OPTIONS") {
response.status = response.SC_ACCEPTED
return
}
next()
}
}
Like for a Listener, the same design is applied to a Filter and we will extend another base class dynamically at runtime. To perceive the difference in terms of simplicity and clarity, the Groovy class above is a complete rewriting of the Java CORS Filter example published at the HowToDoInJava
website. Below is the Java Filter
base class used to achieve such transformation through inheritance.
Filter Base Class
package org.gservlet;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.logging.Logger;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
public abstract class Filter implements javax.servlet.Filter {
protected FilterConfig config;
protected FilterChain chain;
protected ServletRequest request;
protected ServletResponse response;
protected Logger logger = Logger.getLogger(Filter.class.getName());
@Override
public void init(FilterConfig config) throws ServletException {
this.config = config;
try {
Method method = getClass().getDeclaredMethod("init");
method.invoke(this);
} catch (NoSuchMethodException e) {
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
this.request = request;
this.response = response;
this.chain = chain;
try {
Method method = getClass().getDeclaredMethod("filter");
method.invoke(this);
} catch (NoSuchMethodException e) {
logger.info("no method filter has been declared for the filter " + this.getClass().getName());
} catch (Exception e) {
e.printStackTrace();
}
}
public void next() throws IOException, ServletException {
chain.doFilter(request, response);
}
@Override
public void destroy() {
}
}
Filter Registration
protected void addFilter(ServletContext context, Filter annotation, Object object) {
String name = object.getClass().getName();
FilterRegistration registration = context.getFilterRegistration(name);
if (registration == null) {
DynamicInvocationHandler handler = new DynamicInvocationHandler(object);
handlers.put(name, handler);
Object filter = Proxy.newProxyInstance(this.getClass().getClassLoader(),
new Class[] { javax.servlet.Filter.class }, handler);
registration = context.addFilter(name, (javax.servlet.Filter) filter);
if (annotation.value().length > 0) {
registration.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST, DispatcherType.FORWARD), true,
annotation.value());
}
if (annotation.urlPatterns().length > 0) {
registration.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST, DispatcherType.FORWARD), true,
annotation.urlPatterns());
}
} else {
String message = "The filter with the name " + name
+ " has already been registered. Please use a different name or package";
throw new RuntimeException(message);
}
}
Without an explicit inheritance, we might think that we will lose the benefits of code completion in our IDEs but such is not the case since the Groovy language is also an excellent platform for the easy creation of domain-specific languages (DSLs). Using a DSLD, which is a DSL descriptor, it is possible to teach the editor some of the semantics behind these custom DSLs. For a while now, it has been possible to write an Eclipse plugin to extend Groovy-Eclipse, which requires specific knowledge of the Eclipse APIs. This is no longer necessary.
DSLD (DSL Descriptor)
// this is a Custom DSL Descriptor for the GServlet API
import org.gservlet.annotation.Servlet
import org.gservlet.annotation.Filter
contribute(currentType(annos: annotatedBy(Servlet))) {
property name : 'logger', type : java.util.Logger, provider : 'org.gservlet.HttpServlet', doc : 'the logger'
property name : 'request', type : javax.servlet.http.HttpServletRequest, provider : 'org.gservlet.HttpServlet', doc : 'the request'
property name : 'response', type : javax.servlet.http.HttpServletResponse, provider : 'org.gservlet.HttpServlet', doc : 'the response'
property name : 'session', type : javax.servlet.http.HttpSession, provider : 'org.gservlet.HttpServlet', doc : 'the session'
property name : 'context', type : javax.servlet.ServletContext, provider : 'org.gservlet.HttpServlet', doc : 'the context'
}
contribute(currentType(annos: annotatedBy(Servlet))) {
delegatesTo type : org.gservlet.HttpServlet, except : ['doGet','doPost','doHead','doPut','doTrace','doOptions','doDelete']
}
contribute(currentType(annos: annotatedBy(Filter))) {
property name : 'logger', type : java.util.Logger, provider : 'org.gservlet.Filter', doc : 'the logger'
property name : 'request', type : javax.servlet.http.HttpServletRequest, provider : 'org.gservlet.Filter', doc : 'the request'
property name : 'response', type : javax.servlet.http.HttpServletResponse, provider : 'org.gservlet.Filter', doc : 'the response'
property name : 'session', type : javax.servlet.http.HttpSession, provider : 'org.gservlet.Filter', doc : 'the session'
property name : 'context', type : javax.servlet.ServletContext, provider : 'org.gservlet.Filter', doc : 'the context'
property name : 'config', type : javax.servlet.FilterConfig, provider : 'org.gservlet.Filter', doc : 'the filter config'
property name : 'chain', type : javax.servlet.FilterChain, provider : 'org.gservlet.Filter', doc : 'the filter chain'
}
contribute(currentType(annos: annotatedBy(Filter))) {
delegatesTo type : org.gservlet.Filter, except : ['init','doFilter']
}
Stay tuned for the next articles and for this upcoming GServlet open source project.
Further Reading
Opinions expressed by DZone contributors are their own.
Comments