簡體   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