Friday, November 22, 2013

ServiceLoader<S> Dynamic Reloading on JEE7 Web Application

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 the ServiceLoader, 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:

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 the META-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;
    }
}

Conclusion

If you need to add some additional functionality to a web application, and reload it on the fly, Give the ServiceLoader a try. You might just be impressed.

0 comments :

Popular Posts