In my another posts I explained ways to turn your Jakarta EE application decoupled. In this post I will cover how to decouple the application configuration from our application.
Software delivery have many steps that need be done and one of these step is make the configuration of our application, such as configuration about file system and directories, configuration about paths to other services or resource, configuration about database and others configuration about environment.
Generally, we works with development environment, testing environment, staging environment and production environment. The application need be configured in each of these environment that have its values of configuration — each environment have the same properties but the value of these properties can be different — that allow the application to work. Many developers make the configuration using a configuration file (.properties, .xml or another) inside application to configure them, but this way turn the package of the application coupled with the environment, and the developer need generate one package to each environment. Each package need know the details of the environment that it will running. It is a bad practices and increase the complexity of the delivery of application both to application with monolithic architecture and to application with microservice architecture.
External configuration store pattern is an operational pattern (some literatures define as architecture pattern or cloud design pattern) that decouple the configuration details from application. With this, the application don’t know the value of configuration properties, but only know which properties it need to be read from configuration store. Below we have the figure showing the diference between application using external configuration store pattern and application don’t using external configuration store pattern.
Benefits of using external configuration store pattern
The use of external configuration store pattern have several benefits, but I will talk about some. The main benefit is that we can update any configuration values without shall rebuild our application. Once the package was generated, then this package will running on any environment except if another problem that is not related with configuration occur or if the environment is with a wrong configuration. Furthermore, we allow another team (infrastructure team or middleware team) manage the configuration without need a help of developer, because the package of application don’t need be updated.
Another benefit of external configuration store pattern is that it can make all configuration centralized and many applications can read the configuration properties from the same location.
Implementing external configuration store pattern using Jakarta EE
The implementation of external configuration store pattern can be done with the following ways:
- Using the application server as configuration server by system properties
- Using an external file or a set of external files.
- Using a datasource (relational database, NoSQL or other)
- Using a custom configuration server
In this post I’ll show you how to implemente external configuration store pattern using the application server as configuration server by system properties and using an external file or a set of external files.
In our scenario, we’ll have three JAX-RS resources, one to return a welcome message, other to upload files and other to download files. On resource that return a welcome message we’ll have one method to get message from system properties of application server and another method to get message from an external file. To implement that we will use CDI producer to read the properties both from application server and from external file. Furthermore, we’ll create the qualifier @Property to be used by producer at moment of injection.
Creating the configurationStore.properties
This file is used when the application uses a external file. This is the only configuration that is configured inside application, to permit the application know where is the configuration store.
path=${path_to_configuration_store}
Implementing Qualifier
The code below has the implementation of qualifier used to configure the injection and permit the producer product the value injected.
@Qualifier @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER}) public @interface Property { @Nonbinding String key() default ""; @Nonbinding boolean container() default true; @Nonbinding String fileName() default ""; }
Note that the qualifier have three attributes, key, container and fileName. The attribute key is used to pass the key of property. The attribute container is used to define if the attribute will be read from Jakarta EE container. If the attribute container is true, the application will search the property on the application server. The attribute fileName is used to pass the file path when we are using an external file. When the fileName is passed and the attribute container is true, then the application will search first on the Jakarta EE container and if the property is not found on the Jakarta EE container, then is find on external file.
Implementing Producer
The code below has implementation of PropertyProducer, the producer used to inject the properties.
public class PropertyProducer { @Property @Produces public String readProperty(InjectionPoint point){ String key = point .getAnnotated() .getAnnotation(Property.class) .key(); if( point .getAnnotated() .getAnnotation(Property.class) .container() ){ String value = System.getProperty(key); if( Objects.nonNull(value) ){ return value; } } return readFromPath(point .getAnnotated() .getAnnotation(Property.class) .fileName(), key); } private String readFromPath(String fileName, String key){ try(InputStream in = new FileInputStream( readPathConfigurationStore() + fileName)){ Properties properties = new Properties(); properties.load( in ); return properties.getProperty( key ); } catch ( Exception e ) { e.printStackTrace(); throw new PropertyException("Error to read property."); } } private String readPathConfigurationStore(){ Properties configStore = new Properties(); try( InputStream stream = PropertyProducer.class .getResourceAsStream("/configurationStore.properties") ) { configStore.load(stream); } catch ( Exception e ) { e.printStackTrace(); throw new PropertyException("Error to read property."); } return configStore.getProperty("path"); } }
Implementing the Config
This class is the core of this post, because it class contain the configurations of application that was read from both application server and external file. This class is a singleton and all configuration properties injected are centralized in this class.
@Singleton public class Config { @Inject @Property(key="message.welcome") public String WELCOME; @Inject @Property(key="message.welcome", container = false, fileName = "config.properties") public String WELCOME_EXTERNAL_FILE; @Inject @Property(key="path.download") public String PATH_DOWNLOAD; @Inject @Property(key="path.upload") public String PATH_UPLOAD; }
Implementing the WelcomeResource
The code below has the implementation of a JAX-RS resource that has two methods, one method to return a welcome message defined into system properties of application server, and other method to return a welcome message defined into external file.
@Path("/welcome") public class WelcomeResource { @Inject private Config config; @GET @Produces(MediaType.APPLICATION_JSON) public Response message(){ Map map = new HashMap(); map.put("message", config.WELCOME); return Response .status( Response.Status.OK ) .entity( map ) .build(); } @GET @Path("/external") @Produces(MediaType.APPLICATION_JSON) public Response messageExternalFile(){ Map map = new HashMap(); map.put("message", config.WELCOME_EXTERNAL_FILE); return Response .status( Response.Status.OK ) .entity( map ) .build(); } }
Implementing FileDao
The code below have the FileDao implementation, the class to read and write file. FileDao is used by UploadResource and DownloadResource.
@Stateless public class FileDao { @Inject private Config config; public boolean save( File file ){ File fileToSave = new File(config.PATH_UPLOAD + "/" + file.getName()); try (InputStream input = new FileInputStream( file )) { Files.copy( input, fileToSave.toPath() ); } catch (Exception e) { e.printStackTrace(); return false; } return true; } public File find( String fileName ){ File file = new File(config.PATH_DOWNLOAD + "/" + fileName); if( file.exists() && file.isFile() ) { return file; } return null; } }
Implementing UploadResource
The code below has the implementation of a JAX-RS resource process an upload of file. Note that we have the FileDao class used to read and write the file.
@Path("/upload") public class UploadResource { @Inject private FileDao fileDao; @POST public Response upload(@NotNull File file){ if( fileDao.save( file ) ){ return Response .created(URI.create("/download?fileName="+ file.getName())) .build(); } return Response.serverError().build(); } }
Implementing DownloadResource
The code below has the implementation of a JAX-RS resource process a download of file. Note that we have the FileDao class used to read and write the file.
@Path("/download") public class DownloadResource { @Inject private FileDao fileDao; @GET public Response download(@NotNull @QueryParam("fileName") String fileName){ File file = fileDao.find( fileName ); if( Objects.isNull( file ) ){ return Response.status(Response.Status.NOT_FOUND).build(); } return Response.ok(file) .header("Content-Disposition", "attachment; filename=\"" + fileName + "\"") .build(); } }
Eclipse MicroProfile Config
The Jakarta EE is a new project and is based on Java EE 8, but many peoples talk about a possible merge between MicroProfile and Jakarta EE. For me, these projects will be increasingly closer. The MicroProfile project has a solution called Eclipse MicroProfile Config, today on 1.3 version, that permit us implementing the external configuration store pattern. If you want to know more about Eclipse MicroProfile Config, access: https://microprofile.io/project/eclipse/microprofile-config
Conclusion
Using external configuration store pattern, we decouple configurations from application. Thus, we can update some configurations without rebuild our application. Furthermore, other teams may managing the configuration without a developer intervention and we can share the same set of configurations with any applications.
The use of this pattern is a good practice, mainly if the application was done with a microservice architecture, because it promote a better delivery and an easier maintenance.
If you wanna see the full code of this example, access the github in this link: https://github.com/rhuan080/jakartaee-example-external-conf
Leave a Reply