使用 Java 依赖注入框架的替代 JAR 入口点

作者:克里斯·琼斯 |

在这篇文章中,我们介绍了一种简单的新模式,它允许在 Java 应用程序中执行替代入口点之前进行依赖注入。这种模式还有一个优点,那就是可以与不同的流行的开源Java依赖注入框架一起使用,例如Spring Boot、Micronaut和带有Java Spark的Guice。

背景

有时在多个上下文中需要应用程序业务逻辑。这方面的一个例子可能是一系列的税收计算。

以现有的 Web 应用程序为例:每当用户进行购买时,都会运行应用程序的复杂逻辑来确定对订单收取多少税。但是,当在不同的上下文中需要相同的逻辑时会发生什么?例如,也许企业想要执行相同的逻辑来计算定期验证所有税款是否已正确征收。如何实现这一目标?

处理这种情况的一种方法是创建共享库或可以外部调用的公共服务。但是,许多现有应用程序已经发展了多年,不容易进行逆向工程以排除常用代码。在这种情况下,需要另一种方法。

这篇文章中描述的模式允许应用程序利用相同的代码库来处理这两种情况。这种模式的关键有两个概念:

备用 Java 入口点

备用 Java 入口点 对于允许我们通过与 JAR 清单中指定的 主 方法不同的主方法访问 JAR 的逻辑很有用。 这允许我们通过以不同的方式执行代码库中的逻辑来重用它。

依赖注入

依赖注入涉及实例化依赖对象并将其传递给其他对象的构造函数。例如,我们没有在应用程序的代码库中使用关键字实例化所有类字段,而是使用一个需要实例作为参数的类构造函数。此外,最佳做法是这些构造函数参数应该是接口或抽象类,因为它可以将应用程序与特定实现分离,从而使应用程序更加灵活。这也允许我们在单元测试期间模拟对象并将模拟传递给类构造函数。

Spr ing 、Micr onaut 和 Guic e 等开源依赖注入框架更进一步,无需使用新关键字实例化任何对象。 使用注释(以 Spring 和 Micronaut 为例)或映射类(以 Guice 的 Abst ractMod ule 为例 ),我们可以将对象实例化和注入交给其中一个框架,这可以节省软件工程师在开发过程中的时间。

这个例子

为了详细说明这种方法,将更新示例应用程序以支持多个入口点。在这个虚构的样本中,存在一项可以进行一系列复杂税收计算的服务。

现有行为会计算网络用户针对特定购买的税费。以下代码概述了示例服务:

@Service
class TaxService {

    public double calculateTax(double amountToTax) {
        return Math.round(amountToTax * .07 * 100.0) / 100.0;
    }

}

注意 :为了简单易读,省略了复杂的税收明细,而是使用了简单的计算方法。现实世界中的应用程序的服务要复杂得多。

运行应用程序时,将执行现有 的主 方法,并通过依赖注入对服务进行实例化,如下所示:

@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); 
   }

}

问题:当替代入口点遇到依赖注入框架时

如果您的 JAR 依赖依赖项注入框架,则可能很难使用替代入口点,因为依赖注入逻辑需要在备用入口点运行之前执行。

那么,在我们运行替代入口点之前,我们怎样才能给依赖注入框架留出配置应用程序所需的时间呢?

解决方案

我发现使用 1) 单一方法、2) 环境变量和 3) Optional 会创建一种简单的模式,允许依赖注入逻辑在执行替代入口点之前运行。这种模式还有一个优点,那就是可以与不同的流行的开源依赖注入框架一起使用。我将使用 Spring Boot、Micronaut 和 Guice 和 Java Spark 来展示这个图案。

春靴

以下是 JAR 使用 Spring Boot 作为其依赖注入框架时的模式:

@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);
                        });
    }

}

我们在单个方法 中实例化 Spring ConfigurableApp licationContext,该方法包含所有实例化的 bean,包括我们用 @Service 注解的 TaxService 。 ConfigurableApplicat ionConte xt 对象被实例化后,我们使用 可选 来检查名为 ALTERNATIVE_ENTRY_POINT 的环境变量是否存在。 如果是,我们会检查它的值是否为 P eriodic Run。如果是这样,我们会从 C onfigableApp licationConte xt 中获取 TaxSer vice bean ,调用 c alculateTax 方法,然后打印返回值。

Micronaut

如果 JAR 使用 Micronaut 作为其依赖注入框架,则模式如下:

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);
                        });
    }

}

Spring Boot 和 Micronaut 代码样本之间的唯一主要区别是,之前使用的 Micronaut 类不需要注释,而是使用了 Spring 方法的 Micronaut 等效项(例如:Micronaut #run)。

Java Spark 指南

如果 JAR 使用 Guice 作为其依赖注入框架,则模式如下:

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);
        });
    }

}

将 Guice 与 Java Spark 配合使用时,主要区别在于 1) 你从 Guice Injector 中检索 bean, 而不是像 Spring 和 Micronaut 那样从 A pp licationContext 对象中检索 bean;2) 运行 方法包含所有 Java Spark 控制器端点,而不是像 Spring Boot 和 Micronaut 那样使用控制器类。

在每个示例中,您都会注意到,我们通过检查环境变量是否存在以及其值是多少,来控制是否调用替代入口点的逻辑。如果环境变量不存在或其值不符合我们的预期,则不会从 A pp licationConte xt 或 Injector (取决于所使用的框架)中检索并执行 TaxSer vice bean。相反,应用程序将按原计划运行。因此,无需提取常用代码并将其复制或重构为通用服务。

请注意,在使用 Spring 和 Micronaut 时, 无论服务方法调 用成功执行还是抛出异 常,都会使用 try-with-resou rces 关闭 ApplicationContext。 这可以保证,如果指定了备用入口点,它将始终导致应用程序退出。这将阻止 Spring Boot 应用程序继续运行以使用控制器 API 端点为 HTTP 请求提供服务。最后,如果检测到替代入口点环境变量,我们总是会退出 JVM。这可以防止 Spring Boot 抛出异, 因为 A pp licat ionContext 已关闭,但是 JVM 仍在运行。

如果我们要 指定其他入口点,则可以在 i f (arg.equals (“periodicRun”)) 语句下添加其他 if 语句。

动手实践

你可以通过克隆这个 存储库 并按照 README 中的说明运行这篇文章中的代码示例。

结论

在开发全新的应用程序时,提取常用代码并创建一系列可在不同环境中使用的服务或共享库通常很有价值。但是,当涉及到现有应用程序时,可能无法进行全面的更改。

本文演示了如何在无需进行重大更改的情况下优雅地重用现有应用程序功能。因此,这种方法可以在不需要大量财务投资的情况下为现有系统注入新的活力,并解锁新功能。