Home > front end >  Can I compile and run a spring boot jar from inside another spring boot application?
Can I compile and run a spring boot jar from inside another spring boot application?

Time:12-18

So quick clarifications because I have read some of the previous similar questions:

  1. I'm looking to compile and run a spring boot codebase from a running spring application.
  2. I'm not looking to nest or package multiple spring boot jars inside one. The second spring boot codebase is outside. Maybe even on github.

I have already looked at https://www.toptal.com/spring-boot/spring-boot-application-programmatic-launch. It was super helpful, but I'm not sure how to compile and load a spring boot application.

I have an inkling that this is done at the Tomcat TomcatServletWebServerFactory level - basically the spring boot "helper" application will trigger tomcat to load the external jar and deploy. I'm not 100% sure if this is correct.

CodePudding user response:

Can’t you simply build and run your secondary Spring Boot applications in external processes that are started from within your primary Spring Boot application?

I’ve just tried this in a very simple proof-of-concept. For this POC, I have created two dummy Spring Boot applications, one called outer and one called inner. The latter is supposed to be built and run by the former.

Here’s the directory structure (ommitting Gradle 7.6 Wrapper files in each of the two Gradle projects for brevity):

├── 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

The two settings.gradle files are both empty. The two build.gradle files have the same content, too:

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'
}

The “inner” application is the demo applicatation from the Spring Quickstart Guide, i.e., inner/src/main/java/com/example/demo/DemoApplication.java looks as follows:

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

The inner/src/main/resources/application.properties file additionally contains server.port=8081 so that its web server is run on a different port than the one of “outer”.

That leaves us with outer/src/main/java/com/example/demo/DemoApplication.java which defines the following (crude) application:

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.";
    }
}

You can now run ./gradlew bootRun in outer/ to start the “outer” Spring Boot application – a Tomcat web server. That server reacts to a POST request wich starts a Gradle build of the “inner” Spring Boot application which also runs that application (once the build is complete). For example, you can now try the following interaction:

$ 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.

CodePudding user response:

Hmm - I know you wanted sample Code, but I lack the time currently to try it out by myself and this here might not be a real answer - but too big for a comment:

I never done something like this before, but maybe I could throw my idea in here - if it sucks, feel free to ignore it.

I don't know if your "Main Spring Boot Project" uses the Maven Wrapper, but lets assume it does.

So let's try the following concept:

  • You start your "Main" Spring Boot Application, which actually could Build and Start multiple Spring Boot Applications.
  • The Main Spring Boot Application checksout X different Spring Boot Applications from GitHub / GitLab whatever (with git clone) in some directories of its choice. (you could do this with JGit or with Runtime.getRuntime().exec("your git command") or whatever comes in your mind)
  • Knowing that the Maven Wrapper exists in this folder, you could basically build the Spring Boot JAR in the target Folder, (or like with Gradle when you do gradlew bootJar)
  • After the "Shell Command" did execute successfully, you could start the Spring Boot Jar by executing something like java -jar path/to/your/mySpringBoot.jar fully.qualified.package.Application

Does it sound conceptually somewhat you want to do? In the end, if we think about it - it is the same, when you checkout your Project manually, build the JAR and start it - ain't it?

CodePudding user response:

You can use 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);
    }

Also take a look at the documentation -> Java Compiler

  • Related