AWS Open Source Blog
Alternative JAR Entry Points Using Java Dependency Injection Frameworks
In this post, we introduce a simple new pattern that allows dependency injection to take place before an alternative entry point is executed in a Java application. This pattern also has the advantage of working with different popular open source Java dependency injection frameworks, such as Spring Boot, Micronaut, and Guice with Java Spark.
Background
There are often times when application business logic is needed in multiple contexts. An example of this might be a series of tax calculations.
Consider an existing web application: Whenever a user makes a purchase, the application’s complex logic is run to determine how much tax to charge on an order. However, what happens when this same logic is required in a different context? For example, perhaps the business wants to execute the same logic to calculate periodic verification that all tax has been collected correctly. How can this be achieved?
One approach to handle this situation is to create a shared library, or a common service that can be externally invoked. However, many existing applications have evolved over years, and can’t easily be reverse engineered to factor out common code. In those situations, an alternate approach is required.
The pattern described in this post allows an application to leverage the same code base to handle both scenarios. Key to this pattern are two concepts:
Alternative Java Entry Points
Alternative Java entry points are useful to allow us to access a JAR’s logic through a different main
method than the main
method specified in the JAR’s manifest. This allows us to reuse logic in a code base by executing it in different ways.
Dependency Injection
Dependency injection involves instantiating dependent objects and passing them into other objects’ constructors. For example, instead of instantiating all our class fields with the new
keyword in our applications’ code bases, we have a class constructor which requires an instance as a parameter. Moreover, it is a best practice that these constructor parameters should be interfaces or abstract classes, because it decouples the application from specific implementations – making the application more flexible. This also allows us to mock objects and pass the mock into the class constructor during unit testing.
Open source dependency injection frameworks, such as Spring, Micronaut, and Guice go one step further and remove the need to instantiate any objects using the new
keyword. Using annotations (in the case of Spring and Micronaut) or a mapping class (in the case of Guice’s AbstractModule
), we can hand off object instantiation and injection to one of these frameworks, which can save software engineers time during development.
The Example
To elaborate upon this approach, a sample application will be updated to support multiple entry points. In this fictitious sample, a service exists which can perform a series of complex tax calculations.
The existing behavior calculates a web user’s tax for a particular purchase. The sample service is outlined in the following code:
@Service class TaxService { public double calculateTax(double amountToTax) { return Math.round(amountToTax * .07 * 100.0) / 100.0; } }
Note: For simplicity and readability, the complex tax details are omitted, and a simple calculation is used instead. A real-world application would have a far more complex service.
When the application is run, the existing main
method is executed, and the service is instantiated via dependency injection as shown here:
@RestController public class SpringController { private final TaxService taxService; @Autowired public SpringController(TaxService taxService) { this.taxService = taxService; } @GetMapping("/") public ResponseEntity<Double> getTax(@RequestParam double amountToTax) { double tax = this.taxService.calculateTax(amountToTax); return ResponseEntity.ok(tax); } }
@SpringBootApplication public class SpringBootEntryPoint { public static void main(String... args) { SpringApplication.run(SpringBootEntryPoint.class, args); } }
The Problem: When Alternative Entry Points meet Dependency Injection Frameworks
Alternative entry points can be difficult to use if your JAR relies on a dependency injection framework because dependency injection logic needs to execute before the alternative entry point can run.
So, how can we give dependency injection frameworks the time they need to configure the application before we run the alternative entry point?
The Solution
I found that using 1) a single main
method, 2) environment variables, and 3) Optional
creates a simple pattern that allows dependency injection logic to run before an alternative entry point is executed. This pattern also has the advantage of working with different popular open source dependency injection frameworks. I will show the pattern using Spring Boot, Micronaut, and Guice with Java Spark.
Spring Boot
Here is the pattern if the JAR uses Spring Boot as its dependency injection framework:
@SpringBootApplication public class SpringBootEntryPoint { public static void main(String[] args) { ConfigurableApplicationContext applicationContext = SpringApplication.run(SpringBootEntryPoint.class, args); /* * If an alternative entry point property exists, then determine if there is business logic that is mapped to * that property. If so, run the logic and exit. If an alternative entry point property does not exist, then * allow the spring application to run as normal. */ Optional.ofNullable(System.getenv("ALTERNATIVE_ENTRY_POINT")) .ifPresent( arg -> { int exitCode = 0; try(applicationContext) { if (arg.equals("periodicRun")) { double amountToTax = Double.parseDouble(System.getenv("AMOUNT_TO_TAX")); double tax = applicationContext.getBean(TaxService.class).calculateTax(amountToTax); System.out.println("Tax is " + tax); } else { throw new IllegalArgumentException( String.format("Did not recognize ALTERNATIVE_ENTRY_POINT, %s", arg) ); } } catch (Exception e) { exitCode = 1; e.printStackTrace(); } finally { System.out.println("Closing application context"); } /* If there is an alternative entry point listed, then we always want to exit the JVM so the spring app does not throw an exception after we close the applicationContext. Both the applicationContext and JVM should be closed/exited to prevent exceptions. */ System.out.println("Exiting JVM"); System.exit(exitCode); }); } }
We instantiate a Spring ConfigurableApplicationContext
in a single main
method, which contains all instantiated beans, including the TaxService
that we annotated with @Service
. After the ConfigurableApplicationContext
object is instantiated, we use Optional
to check if an environment variable with the name ALTERNATIVE_ENTRY_POINT
exists. If it does, we check if its value is periodicRun
. If so, we get the TaxService
bean from the ConfigurableApplicationContext
, invoke the calculateTax
method, and print the returned value.
Micronaut
Here is the pattern if the JAR uses Micronaut as its dependency injection framework:
public class MicronautEntryPoint { public static void main(String[] args) { ApplicationContext applicationContext = Micronaut.run(MicronautEntryPoint.class, args); /* * If an alternative entry point property exists, then determine if there is business logic that is mapped to * that property. If so, run the logic and exit. If an alternative entry point property does not exist, then * allow the spring application to run as normal. */ Optional.ofNullable(System.getenv("ALTERNATIVE_ENTRY_POINT")) .ifPresent( arg -> { int exitCode = 0; try(applicationContext) { if (arg.equals("periodicRun")) { double amountToTax = Double.parseDouble(System.getenv("AMOUNT_TO_TAX")); double tax = applicationContext.getBean(TaxService.class).calculateTax(amountToTax); System.out.println("Tax is " + tax); } else { throw new IllegalArgumentException( String.format("Did not recognize ALTERNATIVE_ENTRY_POINT, %s", arg) ); } } catch (Exception e) { exitCode = 1; e.printStackTrace(); } finally { System.out.println("Closing application context"); } /* If there is an alternative entry point listed, then we always want to exit the JVM so the spring app does not throw an exception after we close the applicationContext. Both the applicationContext and JVM should be closed/exited to prevent exceptions. */ System.out.println("Exiting JVM"); System.exit(exitCode); }); } }
The only major difference between the Spring Boot and Micronaut code samples is that the Micronaut class used previously does not need an annotation and the Micronaut equivalents of Spring methods are used (ex: Micronaut#run)
.
Guice with Java Spark
Here is the pattern if the JAR uses Guice as its dependency injection framework:
public class GuiceEntryPoint { private static Injector injector; public static void main(String[] args) { GuiceEntryPoint.injector = Guice.createInjector(new GuiceModule()); /* * If an alternative entry point property exists, then determine if there is business logic that is mapped to * that property. If so, run the logic and exit. If an alternative entry point property does not exist, then * allow the spring application to run as normal. */ Optional.ofNullable(System.getenv("ALTERNATIVE_ENTRY_POINT")) .ifPresent( arg -> { int exitCode = 0; try { if (arg.equals("periodicRun")) { double amountToTax = Double.parseDouble(System.getenv("AMOUNT_TO_TAX")); double tax = injector.getInstance(TaxService.class).calculateTax(amountToTax); System.out.println("Tax is " + tax); } else { throw new IllegalArgumentException( String.format("Did not recognize ALTERNATIVE_ENTRY_POINT, %s", arg) ); } } catch (Exception e) { exitCode = 1; e.printStackTrace(); } finally { System.out.println("Closing application context"); } /* If there is an alternative entry point listed, then we always want to exit the JVM so the spring app does not throw an exception after we close the applicationContext. Both the applicationContext and JVM should be closed/exited to prevent exceptions. */ System.out.println("Exiting JVM"); System.exit(exitCode); }); /* Run the Java Spark RESTful API. */ injector.getInstance(GuiceEntryPoint.class) .run(8080); } void run(final int port) { final TaxService taxService = GuiceEntryPoint.injector.getInstance(TaxService.class); port(port); get("/", (req, res) -> { String amountToTaxString = req.queryParams("amountToTax"); double amountToTax = Double.parseDouble(amountToTaxString); return taxService.calculateTax(amountToTax); }); } }
The main differences when using Guice with Java Spark are 1) you retrieve the beans from the Guice Injector
rather than from an ApplicationContext
object like in Spring and Micronaut and 2) there is a run
method which contains all the Java Spark controller endpoints rather than there being a controller class like in Spring Boot and Micronaut.
In each of these examples, you’ll notice that we control whether the alternative entry point’s logic is invoked by checking if an environment variable exists and, if it does exist, what its value is. If the environment variable does not exist or its value is not what we expect, then the TaxService
bean will not be retrieved from the ApplicationContext
or Injector
(depending on the framework being used) and executed. Instead, the application will run as it was originally intended to run. As a result, there is no need to extract common code and either copy it or refactor it into a common service.
Note that when using Spring and Micronaut, the applicationContext
is closed using try-with-resources
regardless of whether the service method call executes successfully or throws an Exception
. This guarantees that if an alternative entry point is specified, it will always result in the application exiting. This will prevent the Spring Boot application from continuing to run to service HTTP requests with the controller API endpoints. Lastly, we always exit the JVM if an alternative entry point environment variable is detected. This prevents Spring Boot from throwing an Exception
because the ApplicationContext
is closed, but the JVM is still running.
Additional if
statements can be added below the if (arg.equals("periodicRun"))
statement if we have other entry points that we want to specify.
Hands On
You can run the code samples from this post by cloning this repository and following the instructions in the README.
Conclusion
When developing brand new applications, it is often valuable to extract common code and create a series of services or shared libraries which can be used in different contexts. When it comes to existing applications, however, it may not be possible to make wholesale changes.
This article has demonstrated how to elegantly reuse existing application functionality without requiring significant changes. As a result, this approach can breathe new life into existing systems, and unlock new capabilities without requiring a major financial investment.