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.
Last active
December 20, 2015 07:39
-
-
Save ato/6095065 to your computer and use it in GitHub Desktop.
ShotgunServlet - A nimble code reloader for Java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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