Skip to content

Instantly share code, notes, and snippets.

@ato
Last active December 20, 2015 07:39
Show Gist options
  • Save ato/6095065 to your computer and use it in GitHub Desktop.
Save ato/6095065 to your computer and use it in GitHub Desktop.
ShotgunServlet - A nimble code reloader for Java

I've now turned this into a proper project called JShotgun. JShotgun is even faster as it uses Java 7 WatchService and reloads in a background thread.

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.security.SecureClassLoader;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* A nimble code reloader for Java servlets.
*
* Before each request it checks if any class files were changed and if so
* reloads them and reinitializes the wrapped servlet. Classes loaded from jars
* are excluded so this class automatically becomes a noop in most production
* deployments.
*
* You'll need away of automating the compilation but that could be anything
* from Eclipse's "build automatically" option to a shell script:
*
* <pre>
* while true; do inotifywait *.java; make; done
* </pre>
*
* Concurrency safe: waits for any outstanding requests to be completed before
* reloading.
*
* @author Alex Osborne
*/
public class ShotgunServlet extends HttpServlet {
private static final long serialVersionUID = -7847588451972338616L;
final ReadWriteLock lock = new ReentrantReadWriteLock();
SneakyClassLoader classLoader;
Class<HttpServlet> servletClass;
HttpServlet servlet;
void setUp() throws ServletException {
try {
lock.writeLock().lock();
if (servlet == null) {
Thread thread = Thread.currentThread();
classLoader = new SneakyClassLoader(thread.getContextClassLoader());
thread.setContextClassLoader(classLoader);
servlet = loadTarget(classLoader);
servlet.init(getServletConfig());
}
} finally {
lock.writeLock().unlock();
}
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
try {
lock.readLock().lock();
while (servlet == null || classLoader.hasChanges()) {
lock.readLock().unlock();
tearDown();
setUp();
lock.readLock().lock();
}
Thread.currentThread().setContextClassLoader(classLoader);
servlet.service(request, response);
} finally {
lock.readLock().unlock();
}
}
void tearDown() {
try {
lock.writeLock().lock();
if (servlet != null && classLoader.hasChanges()) {
servlet.destroy();
servlet = null;
}
} finally {
lock.writeLock().unlock();
}
}
@Override
public void destroy() {
super.destroy();
servlet.destroy();
}
public HttpServlet loadTarget(ClassLoader classLoader) throws ServletException {
String target = getServletConfig().getInitParameter("shotgun.targetClass");
if (target == null) {
throw new ServletException("shotgun.targetClass servlet parameter must be set in web.xml. eg\n" +
" <init-param><param-name>shotgun.targetClass</param-name>\n" +
" <param-value>org.example.MyServlet</param-value></init-param>");
}
try {
return (HttpServlet) classLoader.loadClass(target).newInstance();
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
throw new ServletException("unable to load " + target, e);
}
}
/**
* This ClassLoader loads local files without telling its parents. Classes
* loaded from jars or anywhere else are just delegated as normal.
*/
static class SneakyClassLoader extends SecureClassLoader {
final Map<String, Class<?>> classes = new ConcurrentHashMap<>();
final Map<String, Long> modifiedTimes = new ConcurrentHashMap<>();
boolean changes = false;
public SneakyClassLoader(ClassLoader parent) {
super(parent);
}
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
URL url = urlForClass(name);
if (url == null || !"file".equals(url.getProtocol())) {
return super.loadClass(name);
}
try {
return defineClass(name, url);
} catch (ClassFormatError | IOException e) {
throw new ClassNotFoundException(name, e);
}
}
Class<?> defineClass(String name, URL url) throws IOException, ClassFormatError {
URLConnection conn = url.openConnection();
modifiedTimes.put(name, conn.getLastModified());
byte[] b = slurpBytes(conn);
Class<?> c = defineClass(name, b, 0, b.length);
classes.put(name, c);
return c;
}
byte[] slurpBytes(URLConnection conn) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] b = new byte[8192];
try (InputStream stream = conn.getInputStream()) {
for (;;) {
int len = stream.read(b);
if (len < 0)
break;
baos.write(b, 0, len);
}
}
return baos.toByteArray();
}
URL urlForClass(String name) {
return getResource(name.replace('.', '/').concat(".class"));
}
boolean scanForChanges() {
for (Entry<String, Long> entry : modifiedTimes.entrySet()) {
URL url = urlForClass(entry.getKey());
if (url != null) {
try {
if (url.openConnection().getLastModified() > entry.getValue()) {
return true;
}
} catch (IOException | NullPointerException e) {
return true;
}
}
}
return false;
}
boolean hasChanges() {
if (changes) {
return true;
}
changes = scanForChanges();
return changes;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment