Introduction
We had a technical discussion about updating an application on the fly in Java EE. There were a couple of issues that we were trying to resolve. One was a simple way to add functionality to a web application while deployed, and update the running application. It could be functionality like a new module, or service, or something like, the classic example for theServiceLoader
, codecs.Additionally, we needed to be able to add the functionality without adding another framework to make it happen. It needed to be something that was available in the existing Java SE/EE APIs. Again, the ServiceLoader seemed to be a possible solution.
I did a Proof of Concept (POC) for using a ServiceLoader to accomplish adding additional services to our application. That worked, but required a restart of the server, or a reload of the application at a minimum. This assumes that the application was NOT auto-deployed. It turns out that worked, but really was only a half-measure. I wanted to see if I could solve the dynamic reloading part, and I did.
Solution
Before we see the code, how does it work in general. We use the ServiceLoader which is part of the Service Provider Interface (SPI) functionality of Java. It is a hidden gem for those who need it, and framework creators can take advantage of this simple, easy to use technology. The ServiceLoader is managed by a Singleton that returns an instance of the ServiceLoader that will return our SPI implementations. In my example, I create an interface that is packaged separately in its own jar and is shared between the deployed web application and the service implementations. The ServiceLoader loads this interface and makes the implementations available. The cool part is that our Singleton class also has some cool NIO and NIO.2 help with the ZipFileSystemProvider to load the implementations from newly added jars. It also has some demo of how to use a URLClassLoader to add our new implementations and update the ServiceLoader.The code for the project can be found here:
- serviceloader-web-example (code)
-
example-log-spi-implementation (code)
example-log-spi-implementation-1.0-SNAPSHOT.jar (bin) -
example-log-spi (code)
example-log-spi-1.0-SNAPSHOT.jar (bin)
Log.java
Here is our interface that is common between the web service and the SPI implementations.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | package com.bluelotussoftware.service.spi; /** * Example logging interface. * * @author John Yeary * @version 1.0 */ public interface Log { void trace(String message); void debug(String message); void info(String message); void warn(String message); void severe(String message); void error(String message); void fatal(String message); } |
LogImpl.java
This simple interface will allow me to demonstrate the power of the SPI. Here is an implementation of the API.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | package com.bluelotussoftware.service.impl; import com.bluelotussoftware.service.spi.Log; /** * Example implementation of {@link Log}. * @author John Yeary * @version 1.0 */ public class LogImpl implements Log { @Override public void trace(String message) { log( "TRACE --> " + message); } @Override public void debug(String message) { log( "DEBUG --> " + message); } @Override public void info(String message) { log( "INFO --> " + message); } @Override public void warn(String message) { log( "WARN --> " + message); } @Override public void severe(String message) { log( "SEVERE --> " + message); } @Override public void error(String message) { log( "ERROR --> " + message); } @Override public void fatal(String message) { log( "FATAL --> " + message); } private void log(String message) { System.out.println(message); } } |
LogService
This class is the magic behind the SPI and allows us to dyanmically reload the new implementations as we add them to the WEB-INF/lib directory.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 | package com.bluelotussoftware.service; import com.bluelotussoftware.service.spi.Log; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; import java.nio.charset.Charset; import java.nio.file.DirectoryStream; import java.nio.file.FileSystem; import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.spi.FileSystemProvider; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.ServiceLoader; import javax.faces.context.FacesContext; import javax.servlet.ServletContext; /** * Singleton {@link Log} service with {@link ClassLoader} and * {@link ServiceLoader} reloading capabilities for use in a web application. * * @author John Yeary * @version 1.0 */ public class LogService { private static LogService service; private ServiceLoader serviceLoader; private LogService() { serviceLoader = ServiceLoader.load(Log. class ); } public static synchronized LogService getInstance() { if (service == null ) { service = new LogService(); } return service; } public List<log> getLoggers() { List<log> loggers = new ArrayList<>(); Iterator<log> it = serviceLoader.iterator(); while (it.hasNext()) { loggers.add(it.next()); } return loggers; } public Log getFirstAvailableLogger() { Log log = null ; Iterator<log> it = serviceLoader.iterator(); while (it.hasNext()) { log = it.next(); break ; } return log; } public void reload() throws MalformedURLException, IOException, ClassNotFoundException { // 1. Read the WEB-INF/lib directory and get a list of jar files. // 2. Examine the jar files and look for META-INF/services directories. // 3. If the directory has services, read the file names from each service entry. // 4. Store the class names in a list. // 5. Create a list of URLs for files. // 6. Create a URLClassLoader, add the parent ClassLoader and URLs. // 7. Call URLClassloader.loadClass(String name) using saved names. // 8. Create a new ServiceLoader instance with the URLClassLoader, and call load on the interface. ServletContext context = (ServletContext) FacesContext.getCurrentInstance().getExternalContext().getContext(); Path webInfLibDirectory = Paths.get(context.getRealPath( "WEB-INF/lib" )); URLClassLoader urlcl; List<url> jarURLs = new ArrayList<>(); FileSystemProvider provider = getZipFileSystemProvider(); List<string> implementationsToLoad = new ArrayList<>(); if (Files.exists(webInfLibDirectory, LinkOption.NOFOLLOW_LINKS)) { List<path> files = listJars(webInfLibDirectory); for (Path p : files) { info( "LOCATED JAR " + p.toFile().getName()); jarURLs.add(p.toUri().toURL()); FileSystem fs = provider.newFileSystem(p, new HashMap<String, Object>()); Path serviceDirectory = fs.getPath( "/META-INF" , "services" ); info( "SCANNING SERVICES" ); if (Files.exists(serviceDirectory)) { DirectoryStream<path> serviceListings = Files.newDirectoryStream(serviceDirectory); for (Path px : serviceListings) { List<string> services = Files.readAllLines(px, Charset.forName( "UTF-8" )); info(MessageFormat.format( "SERVICES FOUND: {0}" , Arrays.toString(services.toArray()))); implementationsToLoad.addAll(services); } } } urlcl = new URLClassLoader(jarURLs.toArray( new URL[jarURLs.size()]), context.getClassLoader()); load(implementationsToLoad, urlcl); serviceLoader = ServiceLoader.load(Log. class , urlcl); Iterator<log> it = serviceLoader.iterator(); while (it.hasNext()) { info(it.next().getClass().getName()); } } } private List<path> listJars(Path path) throws IOException { List<path> jars = new ArrayList<>(); DirectoryStream<path> ds = Files.newDirectoryStream(path, "*.jar" ); for (Path child : ds) { if (!Files.isDirectory(child)) { jars.add(child); } } return jars; } private void load( final List<string> FQCN, final ClassLoader classLoader) throws ClassNotFoundException { for (String s : FQCN) { info(MessageFormat.format( "LOAD CLASS {0}" , s)); Class<?> clazz = classLoader.loadClass(s); info(MessageFormat.format( "CLASS {0} LOADED" , clazz.getName())); } } private static FileSystemProvider getZipFileSystemProvider() { for (FileSystemProvider provider : FileSystemProvider.installedProviders()) { if ( "jar" .equals(provider.getScheme())) { return provider; } } return null ; } private void info( final String message) { getFirstAvailableLogger().info(message); } } |
com.bluelotussoftware.service.spi.Log
The SPI file located in theMETA-INF/services
directory of your jar file. This is one version, but each implementation would have its own. The file name is the same as the interface, and the listings on each line are concrete implementations.
1 2 | com.bluelotussoftware.service.impl.LogImpl #Default implementation. com.bluelotussoftware.service.impl.SecondaryLogger |
IndexBean.java
This bean has a cool NIO method of handling uploaded files. So I thought I would add it. Combined with PrimeFaces, it is functional and pretty.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | package com.bluelotussoftware.web; import com.bluelotussoftware.service.LogService; import com.bluelotussoftware.service.spi.Log; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.List; import javax.faces.application.FacesMessage; import javax.faces.bean.ManagedBean; import javax.faces.bean.RequestScoped; import javax.faces.context.FacesContext; import javax.servlet.ServletContext; import org.primefaces.event.FileUploadEvent; /** * Page bean for index.xhtml * * @author John Yeary * @version 1.0 */ @ManagedBean @RequestScoped public class IndexBean { private Log log; private LogService service; public IndexBean() { service = LogService.getInstance(); log = service.getFirstAvailableLogger(); log.info( "Constructor called." ); } public String getLoggerName() { log.info( "getLoggerName() called." ); return log.getClass().getName(); } public List<log> getLoggers() { return service.getLoggers(); } public void handleFileUpload(FileUploadEvent event) { Path path = Paths.get(((ServletContext) FacesContext.getCurrentInstance(). getExternalContext().getContext()) .getRealPath( "WEB-INF/lib" ), event.getFile().getFileName()); try { Files.copy(event.getFile().getInputstream(), path); } catch (IOException ex) { log.error(ex.getMessage()); } FacesMessage msg = new FacesMessage( "Succesful" , event.getFile().getFileName() + " is uploaded." ); FacesContext.getCurrentInstance().addMessage( null , msg); } public String reload() { try { service.reload(); log.info( "LogService reloaded." ); } catch (IOException | ClassNotFoundException ex) { log.error(ex.getMessage()); } return null ; } } |
0 comments :
Post a Comment