繁体   English   中英

我可以从另一个 spring boot 应用程序中编译并运行 spring boot jar 吗?

[英]Can I compile and run a spring boot jar from inside another spring boot application?

如此快速的澄清是因为我已经阅读了之前的一些类似问题:

  1. 我正在寻找从正在运行的 spring 应用程序编译和运行 spring boot 代码库。
  2. 我不想将多个 spring boot jar 嵌套或打包在一个 jar 中。 第二个 spring boot 代码库在外面。 也许甚至在 github 上。

我已经看过https://www.toptal.com/spring-boot/spring-boot-application-programmatic-launch 这非常有帮助,但我不确定如何编译和加载 spring boot 应用程序。

我有一个暗示,这是在 Tomcat TomcatServletWebServerFactory 级别完成的 - 基本上 spring boot“助手”应用程序将触发 tomcat 加载外部 jar 并部署。 我不是 100% 确定这是否正确。

您不能简单地在从主 Spring Boot 应用程序中启动的外部进程中构建和运行辅助 Spring Boot 应用程序吗?

我刚刚在一个非常简单的概念验证中尝试了这个。 对于此 POC,我创建了两个虚拟 Spring Boot 应用程序,一个称为outer ,一个称为inner 后者应该由前者建造和运行。

这是目录结构(为简洁起见,在两个 Gradle 项目中省略Gradle 7.6 Wrapper文件):

├── inner
│   ├── build.gradle
│   ├── settings.gradle
│   └── src
│       └── main
│           ├── java
│           │   └── com
│           │       └── example
│           │           └── demo
│           │               └── DemoApplication.java
│           └── resources
│               └── application.properties
└── outer
    ├── build.gradle
    ├── settings.gradle
    └── src
        └── main
            └── java
                └── com
                    └── example
                        └── demo
                            └── DemoApplication.java

这两个settings.gradle文件都是空的。 这两个build.gradle文件也具有相同的内容:

plugins {
    id 'java'
    id 'org.springframework.boot' version '2.7.6'
    id 'io.spring.dependency-management' version '1.1.0'
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
}

“内部”应用程序是Spring Quickstart Guide中的演示应用程序,即inner/src/main/java/com/example/demo/DemoApplication.java如下所示:

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
@RestController
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

    @GetMapping("/hello")
    public String hello(@RequestParam(value = "name", defaultValue = "World") String name) {
        return String.format("Hello %s!", name);
    }
}

inner/src/main/resources/application.properties文件还包含server.port=8081以便其 Web 服务器运行在与“外部”端口不同的端口上。

这给我们留下了outer/src/main/java/com/example/demo/DemoApplication.java ,它定义了以下(原始)应用程序:

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.File;
import java.io.IOException;

@SpringBootApplication
@RestController
public class DemoApplication {

    private Process otherAppProcess = null;

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

    @PostMapping("/run")
    public String run() throws IOException, InterruptedException {
        synchronized (this) {
            if (otherAppProcess != null) {
                stop();
            }
            var processBuilder = new ProcessBuilder("./gradlew", "bootRun");
            processBuilder.directory(new File("../inner"));
            otherAppProcess = processBuilder.start();
        }
        return "Done.";
    }

    @PostMapping("/stop")
    public String stop() {
        synchronized (this) {
            if (otherAppProcess != null) {
                otherAppProcess.destroy();
                otherAppProcess = null;
            }
        }
        return "Ok.";
    }
}

您现在可以在outer/中运行./gradlew bootRun来启动“外部”Spring Boot 应用程序——一个 Tomcat Web 服务器。 该服务器响应 POST 请求,启动“内部”Spring Boot 应用程序的 Gradle 构建,该应用程序也运行该应用程序(一旦构建完成)。 例如,您现在可以尝试以下交互:

$ curl -X GET http://localhost:8081/hello
curl: (7) Failed to connect to localhost port 8081 after 0 ms: Connection refused
$ curl -X POST http://localhost:8080/run
Done.
$ curl -X GET http://localhost:8081/hello
Hello World!
$ curl -X POST http://localhost:8080/stop
Ok.

嗯——我知道你想要示例代码,但我目前没有时间自己尝试,这可能不是一个真正的答案——但对于评论来说太大了:

我以前从未做过这样的事情,但也许我可以在这里提出我的想法 - 如果它很糟糕,请随意忽略它。

我不知道您的“Main Spring Boot Project”是否使用了 Maven Wrapper,但让我们假设它使用了。

因此,让我们尝试以下概念:

  • 您启动“主”Spring Boot 应用程序,它实际上可以构建和启动多个 Spring Boot 应用程序。
  • 主要的 Spring Boot 应用程序从 GitHub / GitLab 中检查出 X 不同的 Spring Boot 应用程序(使用git clone )在它选择的某些目录中。 (你可以用 JGit 或用Runtime.getRuntime().exec("your git command")或你想到的任何东西来做到这一点)
  • 知道 Maven Wrapper 存在于此文件夹中,您基本上可以在目标文件夹中构建 Spring Boot JAR,(或者像在执行gradlew bootJar时使用 Gradle 一样)
  • 在“Shell 命令”成功执行后,您可以通过执行类似java -jar path/to/your/mySpringBoot.jar fully.qualified.package.Application的命令来启动 Spring Boot Jar

从概念上讲,这听起来有点您想做吗? 最后,如果我们考虑一下 - 它是相同的,当您手动签出您的项目时,构建 JAR 并启动它 - 不是吗?

您可以使用JavaCompiler

 public static void main(String[] args) {
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);
        Iterable<? extends JavaFileObject> fileObjects = fileManager.getJavaFileObjectsFromStrings(Arrays.asList("src/main/java/com/example/app2/Application.java"));
        compiler.getTask(null, fileManager, null, null, null, fileObjects).call();
        fileManager.close();
        
        // you can run jar file from this host application
        String[] newArgs = {"--spring.config.name=externalApp", "--spring.config.additional-location=file:/etc/externalApp/"};
        SpringApplication.run(com.example.externalApp.Application.class, newArgs);
    }

另请查看文档 -> Java 编译器

以 Chriki 的回答为例,我可以从所谓的 OuterApp 运行另一个 spring boot 应用程序。 内部应用程序将在端口号上运行,例如可以是 8081。

import org.springframework.boot.SpringApplication;

public class OuterApplication {
    public static void main(String[] args) {
      
        SpringApplication.run(OuterApplication.class, args);
        
        SpringApplication innerApp = new SpringApplication(InnerApplication.class);
        innerApp.setDefaultProperties(new HashMap<String, Object>() {{
            put("server.port", "8081");
        }});
        innerApp.run(args);
    }
}

您可以通过使用 SpringApplication.run() 方法在主应用程序中启动内部应用程序来执行此操作。

    @SpringBootApplication
public class MainApplication {

    public static void main(String[] args) {
        SpringApplication.run(MainApplication.class, args);
    }

    @Bean
    public CommandLineRunner commandLineRunner(ApplicationContext ctx) {
        return args -> {
            // Start your inner application here
            SpringApplication.run(InnerApplication.class, args);
        };
    }
}

你看过“Maven Invoker”了吗? 您不仅可以做到,您可以做任何您可以用 Maven 做的事情。

这是我的 github中的示例(它使用 TestContainers,但不是必须的)。

package com.example.demo;

import org.apache.maven.shared.invoker.DefaultInvocationRequest;
import org.apache.maven.shared.invoker.DefaultInvoker;
import org.apache.maven.shared.invoker.InvocationResult;
import org.apache.maven.shared.invoker.MavenInvocationException;
import org.junit.jupiter.api.Test;
import org.springframework.util.DigestUtils;
import org.testcontainers.DockerClientFactory;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.utility.LazyFuture;

import java.io.File;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.Future;

public class OCIwithTCCTest {
    private static final Future<String> IMAGE_FUTURE = new LazyFuture<>() {
        @Override
        protected String resolve() {
            // Find project's root dir
            File cwd;
            for (
                    cwd = new File(".");
                    !new File(cwd, "mvnw").isFile();
                    cwd = cwd.getParentFile()
            );

            // Make it unique per folder (for caching)
            var imageName = String.format(
                    "local/demo-%s:%s",
                    DigestUtils.md5DigestAsHex(cwd.getAbsolutePath().getBytes()),
                    System.currentTimeMillis()
            );

            var properties = new Properties();
            properties.put("spring-boot.build-image.imageName", imageName);
            properties.put("skipTests", "true");

            var request = new DefaultInvocationRequest()
                    .addShellEnvironment("DOCKER_HOST", DockerClientFactory.instance().getTransportConfig().getDockerHost().toString())
                    .setPomFile(new File(cwd, "pom.xml"))
                    .setGoals(List.of("spring-boot:build-image"))
                    .setMavenExecutable(new File(cwd, "mvnw"))
                    .setProperties(properties);

            InvocationResult invocationResult;
            try {
                invocationResult = new DefaultInvoker().execute(request);
            } catch (MavenInvocationException e) {
                throw new RuntimeException(e);
            }

            if (invocationResult.getExitCode() != 0) {
                throw new RuntimeException(invocationResult.getExecutionException());
            }

            return imageName;
        }
    };


    static final GenericContainer<?> APP = new GenericContainer<>(IMAGE_FUTURE)
            .withExposedPorts(8080);
    
    @Test
    void containerStartupTest() {
        APP.start();
    }
}

依赖性:

<dependency>
    <groupId>org.apache.maven.shared</groupId>
    <artifactId>maven-invoker</artifactId>
    <version>3.2.0</version>
    <scope>test</scope>
</dependency>

此示例存储库使用 spring-boot:build-image 将应用程序构建到 OCI 映像中,然后使用 Maven Invoker 在测试中针对该映像运行测试。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM