HK2 - Dependency Injection Kernel

A light-weight and dynamic dependency injection framework

Configuration Example

This page shows an example of how to use the HK2 configuration service in order to inject values into HK2 services from a properties file. It is designed to show how end users would interact with the system, and as such does not demonstrate how the internals of the configuration system interact with each other.

The example itself is of a fake web server, which server has three ports it can open: an admin port, a SSL port and a normal HTTP port. The SSL port and HTTP port can be dynamically changed while the web server is up and running, but the admin port cannot. The web server can also be associated with one or more SSL certificates and private keys, which are themselves configured. In this specific example the web server is associated with two SSL certificates.

The Web Server

Lets first take a look at the Web Server. The Web Server service implements this HK2 Contract:

@Contract 
public interface WebServer {
    /**
     * Gets the name of this web server
     */
    public String getName();
    
    /**
     * Opens the admin port, and returns the number
     * of the port open
     */
    public int openAdminPort();
    
    /**
     * Opens the SSL port, and returns the number
     * of the port open
     */
    public int openSSLPort();
    
    /**
     * Opens the non-SSL port, and returns the number
     * of the port open
     */
    public int openPort();
    
    /**
     * Gets the current admin port, or -1
     * if the port is not open
     */
    public int getAdminPort();
    
    /**
     * Gets the current SSL port, or -1
     * if the port is not open
     */
    public int getSSLPort();
    
    /**
     * Gets the current HTTP port, or -1
     * if the port is not open
     */
    public int getPort();
    
    /**
     * Gets the list of certificates that are
     * used by this web server
     * 
     * @return A non-null but possibly empty set
     * of Files pointing to the public certificates
     * of the web server
     */
    public List<File> getCertificates(); 
}

Several fields of the WebServer are meant to be configured from an external source. In this example that source will be a properties file, which we will look at in detail later. All the configuration properties of the WebServer are encapsulated in a WebServerBean, which you can see here:

/**
 * This bean describes a WebServer
 */
public class WebServerBean {
    private String name;
    private String address;
    private int adminPort;
    private int sslPort;
    private int port;
    
    /**
     * @return the name
     */
    public String getName() {
        return name;
    }
    /**
     * @param name the name to set
     */
    public void setName(String name) {
        this.name = name;
    }
    /**
     * @return the address
     */
    public String getAddress() {
        return address;
    }
    /**
     * @param address the address to set
     */
    public void setAddress(String address) {
        this.address = address;
    }
    /**
     * @return the adminPort
     */
    public int getAdminPort() {
        return adminPort;
    }
    /**
     * @param adminPort the adminPort to set
     */
    public void setAdminPort(int adminPort) {
        this.adminPort = adminPort;
    }
    /**
     * @return the sslPort
     */
    public int getSSLPort() {
        return sslPort;
    }
    /**
     * @param sslPort the sslPort to set
     */
    public void setSSLPort(int sslPort) {
        this.sslPort = sslPort;
    }
    /**
     * @return the port
     */
    public int getPort() {
        return port;
    }
    /**
     * @param sshPort the port to set
     */
    public void setPort(int port) {
        this.port = port;
    }
}

The implementation of the WebServer contract is the WebServerImpl. The WebServerImpl is in the @ConfiguredBy scope, and injects several fields from the WebServerBean. The interesting parts of this code can be seen below. You can find the full source for this code here.

@Service @ConfiguredBy("WebServerBean")
public class WebServerImpl implements WebServer {
    @Configured
    private String name;
    
    @Configured
    private int adminPort;
    private int openAdminPort = -1;
    
    @Configured(dynamicity=Configured.Dynamicity.FULLY_DYNAMIC)
    private String address;
    
    private int sslPort;
    private int openSSLPort = -1;
    private int port;
    private int openPort = -1;
    
    private boolean opened = false;
    
    /**
     * These are configured services that can be used to get other
     * variable information about the WebServer.  In this case
     * it is getting information about the certificates that
     * this server can use for SSL
     */
    @Inject
    private IterableProvider<SSLCertificateService> certificates;
    
    /**
     * This method is called to set the port and sshPort.  It is guaranteed that
     * the server will not have these ports open at the time this method is called.
     * That is guaranteed since the ports are not open until the postConstruct method
     * is called on boot, and it is only called between the startDynamicConfiguration
     * and finishDynamicConfiguration methods when a dynamic configuration change is
     * made
     * 
     * @param sshPort The sshPort to use
     * @param port The port to use
     */
    @SuppressWarnings("unused")
    private void setUserPorts(
            @Configured(value="SSLPort", dynamicity=Configured.Dynamicity.FULLY_DYNAMIC) int sslPort,
            @Configured(value="port", dynamicity=Configured.Dynamicity.FULLY_DYNAMIC) int port) {
        this.sslPort = sslPort;
        this.port = port;
        
        if (opened) {
            openSSLPort = sslPort;
            openPort = port;
        }
    }
    
    @PostConstruct
    private void postConstruct() {
        opened = true;
    }
    
    @PreDestroy
    private void preDestroy() {
        openPort = -1;
        openSSLPort = -1;
        openAdminPort = -1;
    }
    
    // ... (uninteresting code)
    
    /* (non-Javadoc)
     * @see org.glassfish.examples.configuration.webserver.WebServer#getCertificates()
     */
    @Override
    public List<File> getCertificates() {
        LinkedList<File> retVal = new LinkedList<File>();
        
        for (SSLCertificateService certService : certificates) {
            retVal.add(certService.getCertificate());
        }
        
        return retVal;
    }
}

The above implementation of the WebServer contract is an HK2 Service, and it is in the ConfiguredBy scope. The ConfiguredBy annotation requires the name of the type upon which instances of the service are based, in this case "WebServerBean". This implies that for every instance of the type "WebServerBean" found in the HK2 management Hub a new instance of the WebServerImpl class will be created. Lets now look at the interesting fields and methods of the WebServerImpl.

The first configured parameter is a field called "name". In theory there could be several web servers configured, each with a different name. In this example we don’t do that, but the ability to do so is there. The admin port is also configured, like this:

    @Configured
    private int adminPort;

By putting @Configured on the field, that tells the HK2 configuration system to look for a getter named getAdminPort on the bean, and to get the value for the adminPort from that property. By the time the postConstruct method of the WebServerImpl has been called it is guaranteed that this field will have been filled in. Notice that the @Configured annotation did not take a value here, instead taking the name of the property from the name of the field. In contrast consider this method, which sets the ssl and http ports of the server:

    private void setUserPorts(
            @Configured(value="SSLPort", dynamicity=Configured.Dynamicity.FULLY_DYNAMIC) int sslPort,
            @Configured(value="port", dynamicity=Configured.Dynamicity.FULLY_DYNAMIC) int port) {
        this.sslPort = sslPort;
        this.port = port;
        
        if (opened) {
            openSSLPort = sslPort;
            openPort = port;
        }
    }

There are several interesting things to notice about this method. One thing is that we had to put @Configured on all the parameters that were to come from beans. If there had been other parameters on this method they would have been filled in with hk2 services like a normal hk2 initializer method. Another interesting thing is to notice that we set the dynamicity of the configured fields to FULLY_DYNAMIC. This means that if either of these two fields change, this method will get called with the new values. The dynamicity value of all @Configured annotated parameters on a method must be the same. One other subtle thing to notice is that unlike a normal HK2 intialization method, this method can now get called AFTER the postConstruct method has been called. In fact, this method works slightly different depending on whether it has been called before or after the postConstruct method.

Any HK2 service can also injected into WebServerImpl. This includes other configured services. For example, in the WebServerImpl there is this injection point:

    @Inject
    private IterableProvider<SSLCertificateService> certificates;

Let us look in detail at the SSLCertificateService to more fully understand the above usage.

The SSLCertificateService

SSLCertificateService is an HK2 service that deals with SSL certificates. This is the full implementation of the service:

@Service @ConfiguredBy("SSLCertificateBean")
public class SSLCertificateService {
    @Configured("$bean")
    private SSLCertificateBean certificateBean;
    
    /**
     * Returns the location of the public certificate
     * 
     * @return The public certificate for this SSL service
     */
    public File getCertificate() {
        return certificateBean.getCertificateLocation();
    }

}

Like the WebServerImpl this service is in the @ConfiguredBy scope. Instances of this service will be created for each instance of type "SSLCertificateBean" found in the Hub. Look at the @Configured injection point:

    @Configured("$bean")
    private SSLCertificateBean certificateBean;

In this case the @Configured annotation has a value of "$bean". When a @Configured annotation has a value of "$bean" it means to inject the whole bean rather than a value from the bean. The code for the SSLCertificateBean will be found below in the discussion about how the property file is translated into beans. Here is the injection point in the WebServerImpl:

    @Inject
    private IterableProvider<SSLCertificateService> certificates;

This injection point will be able to get all of the configured SSKCertificateService instances created. So if there are two SSLCertificateBeans in the Hub then this iterator will return two SSLCertificateServices.

Configuring the Services

This example has a specific configuration that it will read and then modify to ensure that the system is working properly. The initial property file for the example can be found here:

WebServerBean.Acme.name=Acme
WebServerBean.Acme.address=localhost
WebServerBean.Acme.adminPort=7070
WebServerBean.Acme.sslPort=81
WebServerBean.Acme.port=80

SSLCertificateBean.Corporate.certificateLocation=Corporatex509.cert
SSLCertificateBean.Corporate.secretKeyLocation=Corporate.pem

SSLCertificateBean.HR.certificateLocation=HRx509.cert
SSLCertificateBean.HR.secretKeyLocation=HR.pem

The way the HK2 Properties interpreter works is that property keys have three parts, which is the type name followed by the instance name followed by the parameter name. So the above properties file means to add an instance of the WebServerBean named "Acme" to the Hub with the properties given. Also add two instances of the SSLCertificateBean, one named "Corporate" and one named "HR" also with the given properties.

Properties are generally strings, but the HK2 Properties interpreter is able to translate those strings to the types found in the associated JavaBeans. This includes being able to translate the String if the type associated with that parameter name has a public constructor that takes a string. For example, consider the SSLCertificateBean:

public class SSLCertificateBean {
    private File certificateLocation;
    private File secretKeyLocation;
    /**
     * @return the certificateLocation
     */
    public File getCertificateLocation() {
        return certificateLocation;
    }
    /**
     * @param certificateLocation the certificateLocation to set
     */
    public void setCertificateLocation(File certificateLocation) {
        this.certificateLocation = certificateLocation;
    }
    /**
     * @return the secretKeyLocation
     */
    public File getSecretKeyLocation() {
        return secretKeyLocation;
    }
    /**
     * @param secretKeyLocation the secretKeyLocation to set
     */
    public void setSecretKeyLocation(File secretKeyLocation) {
        this.secretKeyLocation = secretKeyLocation;
    }
}

The SSLCertificateBean has File as the type for both the "secretKeyLocation" and "CertificateLocation" parameters. HK2 knows how to translate the strings in the properties file because File has a public constructor that takes a string, which is what it uses to fill in the fields of this bean.

In order to read the properties file into the Hub this example uses the PropertyFileService. The PropertyFileService is used to create PropertyFileHandle instances. This is how the test code reads the above property file into the Hub:

        Properties configuration = new Properties();
        
        URL configURL = getClass().getClassLoader().getResource("config.prop");
        InputStream configStream = configURL.openConnection().getInputStream();
        try {
            // Read the property file
            configuration.load(configStream);
        }
        finally {
            configStream.close();
        }
        
        // In order to read the Properties object into HK2 we need to get a PropertyFileHandle
        PropertyFileService propertyFileService = locator.getService(PropertyFileService.class);
        PropertyFileHandle propertyFileHandle = propertyFileService.createPropertyHandleOfAnyType();
        
        // Now read the configuration into hk2
        propertyFileHandle.readProperties(configuration);

The reason for using a PropertyFileHandle is because the handle keeps the state of the property file the last time it was read. In this way if the property file is modified and read in again the propertyFileHandle would know which values had changed, which instances were added and which instances were removed (which is the difficult part of the whole thing!).

Dynamic changes

In this example we simply modify the Properties object itself rather than reading a second file for simplicity. This is the test code that changes the properties and then tells hk2 about the changes:

        // Change the ports so that they look like this in the properties file:
        // adminPort = 8082
        // sslPort = 8081
        // port = 8080
        configuration.put("WebServerBean.Acme.adminPort", "8082");
        configuration.put("WebServerBean.Acme.sslPort", "8081");
        configuration.put("WebServerBean.Acme.port", "8080");
        
        // Tell hk2 about the change
        propertyFileHandle.readProperties(configuration);

The propertyFileHandle will notice the changes and inform the Hub, which will then notify the backing WebServerImpl about the modified dynamic properties. Even though the adminPort has changed the WebServerImpl will NOT be notified about that parameter changing because that field is NOT marked as being dynamic.

Initialization and setup

In order to use the various parts of the HK2 configuration subsystem they must be initialized. This is done via static methods on helper classes. This is the test initialization code (junit @Before block):

        // Enable HK2 service integration
        ConfigurationUtilities.enableConfigurationSystem(locator);
        
        // Enable Properties service, to get service properties from a Properties object
        PropertyFileUtilities.enablePropertyFileService(locator);

The PropertyFileService also needs the mapping from type name to Java Bean class. In order to do this we must configure the PropertyFileService with a PropertyFileBean. This is the code that maps the type named "SSLCertificateBean" to the SSLCertificateBean class and the type named "WebServerBean" to the WebServerBean class:

        // The propertyFileBean contains the mapping from type names to Java Beans
        PropertyFileBean propertyFileBean = new PropertyFileBean();
        propertyFileBean.addTypeMapping("WebServerBean", WebServerBean.class);
        propertyFileBean.addTypeMapping("SSLCertificateBean", SSLCertificateBean.class);
        
        // Add in the mapping from type name to bean classes
        PropertyFileService propertyFileService = locator.getService(PropertyFileService.class);
        propertyFileService.addPropertyFileBean(propertyFileBean);

The last thing to do in order to setup the environment is to tell HK2 about the backing HK2 services. This is done with a simple call to addClasses:

        // Add the test services themselves
        ServiceLocatorUtilities.addClasses(locator,
                SSLCertificateService.class,
                WebServerImpl.class);

The @Before block of the test code has now setup HK2 for the test run.

Running the test

This is the full test that shows that the WebServerImpl is properly running and gets the proper values from the properties file:

    /**
     * This test demonstrates adding and the modifying the http and
     * ssl ports of the web server
     */
    @Test // @org.junit.Ignore
    public void testDemonstrateWebServerConfiguration() throws IOException {
        // Before we add a configuration there is no web server
        WebServer webServer = locator.getService(WebServer.class);
        Assert.assertNull(webServer);
        
        Properties configuration = new Properties();
        
        // Gets the URL of the configuration property file.  This
        // file contains one web server and two SSL certificate
        // configuration objects
        URL configURL = getClass().getClassLoader().getResource("config.prop");
        InputStream configStream = configURL.openConnection().getInputStream();
        try {
            // Read the property file
            configuration.load(configStream);
        }
        finally {
            configStream.close();
        }
        
        // In order to read the Properties object into HK2 we need to get a PropertyFileHandle
        PropertyFileService propertyFileService = locator.getService(PropertyFileService.class);
        PropertyFileHandle propertyFileHandle = propertyFileService.createPropertyHandleOfAnyType();
        
        // Now read the configuration into hk2
        propertyFileHandle.readProperties(configuration);
        
        // We should now have a web server!
        webServer = locator.getService(WebServer.class);
        Assert.assertNotNull(webServer);
        
        // Lets open all the ports, and check that they have the expected values
        // In this case the ports are:
        // adminPort = 7070
        // sslPort = 81
        // port = 80
        Assert.assertEquals((int) 7070, webServer.openAdminPort());
        Assert.assertEquals((int) 81, webServer.openSSLPort());
        Assert.assertEquals((int) 80, webServer.openPort());
        
        // Now lets check that we have two SSL certificates
        List<File> certs = webServer.getCertificates();
        
        // The two certificates should be Corporatex509.cert and HRx509.cert
        Assert.assertEquals(2, certs.size());
        
        HashSet<String> foundCerts = new HashSet<String>();
        for (File cert : certs) {
            foundCerts.add(cert.getName());
        }
        
        Assert.assertTrue(foundCerts.contains("Corporatex509.cert"));
        Assert.assertTrue(foundCerts.contains("HRx509.cert"));
        
        // OK, we have verified that all of the parameters of the
        // webserver are as expected.  We are now going to dynamically
        // change all the ports.  In the webserver however only
        // the ssl and http ports are dynamic, so after the change
        // only the ssl and http ports should have their new values,
        // while the admin port should remain with the old value
        
        // Change the ports so that they look like this in the properties file:
        // adminPort = 8082
        // sslPort = 8081
        // port = 8080
        configuration.put("WebServerBean.Acme.adminPort", "8082");
        configuration.put("WebServerBean.Acme.sslPort", "8081");
        configuration.put("WebServerBean.Acme.port", "8080");
        
        // Tell hk2 about the change
        propertyFileHandle.readProperties(configuration);
        
        // Now lets check the web server, make sure the ports have been modified
        
        // The adminPort is NOT dynamic in the back end service, so it did not change
        Assert.assertEquals(7070, webServer.getAdminPort());
        
        // But the SSL and HTTP ports have changed dynamically
        Assert.assertEquals(8081, webServer.getSSLPort());
        Assert.assertEquals(8080, webServer.openPort());
    }

Final thoughts

This example is a simple demonstration of the HK2 configuration subsystem from an end-user perspective. The end-users write HK2 services such as WebServerImpl and SSLCertificateService. They annotate those services with ConfiguredBy and Configured. They can either be injected with parameters from beans or with the entire bean.

The mechanism demonstrated in this example used a properties file that was read in using the PropertyFileService. Any other mechanism can be used to add java beans to the Hub, making the persistence of the configuration data a pluggable mechanism. Dynamic changes to fields are also supported. More information about the HK2 configuration service can be found here.