-1

I'm working on spring as part of my classwork and using Javapoet for code generation.

Is there a way I can create Spring Beans via JavaPoet?

I have a use case where I wish to create Java classes based on the configuration and load them up into spring context as beans.

I am aware I can use the @Profile to achieve the same, but I have been asked to do it via Javapoet because the annotation would be too easy.

Update - Here is my main class

    package com.example.PhotoAppDiscoveryService;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.javapoet.AnnotationSpec;
import org.springframework.javapoet.JavaFile;
import org.springframework.javapoet.MethodSpec;
import org.springframework.javapoet.TypeSpec;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.lang.model.element.Modifier;
import java.io.IOException;
import java.nio.file.Paths;

@SpringBootApplication
public class PhotoAppDiscoveryServiceApplication {

    public static void main(String[] args) throws IOException {
        generateRestController();
        SpringApplication.run(PhotoAppDiscoveryServiceApplication.class, args);
    }

    private static void generateRestController() throws IOException {
        MethodSpec main = MethodSpec.methodBuilder("getMapper")
                .addModifiers(Modifier.PUBLIC)
                .addAnnotation(
                        AnnotationSpec.builder(RequestMapping.class)
                                .addMember("value", "$S", "/api")
                                .build()
                )
                .returns(String.class)
                .addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!")
                .addStatement("return \"Response from Generated code!\"")
                .build();

        TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
                .addAnnotation(RestController.class)
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                .addMethod(main)
                .build();

        JavaFile javaFile = JavaFile.builder("com.example.PhotoAppDiscoveryService", helloWorld)
                .build();

        javaFile.writeTo(System.out);
        System.out.println("\n");
        try {
            javaFile.writeTo(Paths.get("src/main/java/"));
        } catch (Exception e){
            System.out.println("Exception occurred!");
            e.printStackTrace();
        }
    }
}
  • There's nothing special about a spring bean. It's usually just an annotated class. If you can generate a class, you can generate a class with an annotation. – Michael Jul 04 '23 at 12:20
  • Creating a class is not a problem, how do I tell spring it's a spring-managed component? – Sherlock Holmes Jul 05 '23 at 10:39
  • The same way you do it if you're not generating the class: an annotation (or historically an XML config file). – Michael Jul 05 '23 at 11:11
  • I tried that. Spring does not pick it up even though I have added the annotation. I believe the problem is with when I'm generating the class. Should this class already be available as my application starts up for spring to pick it up? if so, then how can I trigger the creation of these classes at compile time? – Sherlock Holmes Jul 05 '23 at 16:01
  • @Michael I have updated my original question with my source code. The first time I build my project, it gives a 404 response when I hit the "/api" endpoint. However, on the second build (even after I run mvn clean), spring detects this controller and registers it with the context to serve the static response. – Sherlock Holmes Jul 05 '23 at 16:10
  • "*Should this class already be available as my application starts up for spring to pick it up?*" Yes. The JVM works on compiled bytecode. If you are generating a source code file when your app starts up, the JVM can't do anything with that. It needs to be compiled (i.e. by javac). Your code generation step should happen at build-time, e.g. as a Maven plugin. – Michael Jul 06 '23 at 10:12
  • Alternatively, you can use a different library which generates *bytecode* directly (rather than source code), skipping the need for a compiler. [Bytebuddy](https://bytebuddy.net/#/) can do that. – Michael Jul 06 '23 at 10:13
  • Thank you for the responses @Michael, now it makes sense to me why it is not being picked up by spring. let me try this out. – Sherlock Holmes Jul 07 '23 at 07:23
  • @Michael how can I run a generator class in the generate-sources phase in maven? Is there a way to achieve this? – Sherlock Holmes Jul 07 '23 at 07:52

1 Answers1

0

I was able to create spring beans using JavaPoet by following the steps below.

  1. Create a Class that will generate new Java classes using javaPoet.
  2. Call this class in the "process-classes" phase.
  3. Now, a new Java class will be generated as per your specification.
  4. What's left is to compile this newly generated class and ask Maven to include this while creating an artifact jar.
  5. We can achieve this by running the compile goal of the maven-compiler-plugin in the "prepare-package" phase.
  6. This way, the generated class (Java Bean in my case), will be picked up by the application during startup.

Dynamic_Controller_service.java

package com.example.PhotoAppDiscoveryService.Dynamic_Controller;

import org.springframework.javapoet.AnnotationSpec;
import org.springframework.javapoet.JavaFile;
import org.springframework.javapoet.MethodSpec;
import org.springframework.javapoet.TypeSpec;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.lang.model.element.Modifier;
import java.io.IOException;
import java.nio.file.Paths;

// This class is responsible for generating a dynamic controller
public class Dynamic_Controller_service {

    public static void main(String[] args) {
        try {
            generateRestController();
        } catch (IOException | InstantiationException | IllegalAccessException e) {
            System.out.println("Exception occured durung class generation: " + e.getMessage());
            e.printStackTrace();
        }
    }

    public static boolean generateRestController() throws IOException, InstantiationException, IllegalAccessException {
        System.out.println("Begin Generating!");
        MethodSpec main = MethodSpec.methodBuilder("getMapper")
                .addModifiers(Modifier.PUBLIC)
                .addAnnotation(
                        AnnotationSpec.builder(RequestMapping.class)
                                .addMember("value", "$S", "/api")
                                .build()
                )
                .returns(String.class)
                .addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!")
                .addStatement("return \"Response from Generated code!\"")
                .build();

        TypeSpec controller = TypeSpec.classBuilder("HelloWorld")
                .addAnnotation(RestController.class)
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                .addMethod(main)
                .build();

        JavaFile javaFile = JavaFile.builder("com.example.PhotoAppDiscoveryService.Dynamic_Controller.generated", controller)
                .build();

        javaFile.writeTo(System.out);
        System.out.println("\n");
        try {
            javaFile.writeTo(Paths.get("src/main/java"));
        } catch (Exception e) {
            System.out.println("Exception occurred!");
            e.printStackTrace();
            return false;
        }
        return true;
    }
}
  1. This class needs to have a main method.
  2. The main method is invoked by the exec plugin during the specified phase.

Pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.1.1</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>PhotoAppDiscoveryService</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>PhotoAppDiscoveryService</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>17</java.version>
        <spring-cloud.version>2022.0.3</spring-cloud.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.squareup</groupId>
            <artifactId>javapoet</artifactId>
            <version>1.10.0</version>
        </dependency>
        <dependency>
            <groupId>net.minidev</groupId>
            <artifactId>accessors-smart</artifactId>
            <version>2.4.11</version>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <!--    Run the compiled Generator class        -->
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>exec-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <id>generate-sources</id>
                        <phase>process-classes</phase>
                        <goals>
                            <goal>java</goal>
                        </goals>
                        <configuration>
                            <mainClass>com.example.PhotoAppDiscoveryService.Dynamic_Controller.Dynamic_Controller_service</mainClass>
                            <addOutputToClasspath>true</addOutputToClasspath>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
            <!--    Now, compile the Generated Java POJO after running the above main class     -->
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <executions>
                    <execution>
                        <id>compile-generator</id>
                        <phase>prepare-package</phase>
                        <goals>
                            <goal>compile</goal>
                        </goals>
                        <configuration>
<!--                            <compilePath>${project.build.sourceDirectory}/com/example/PhotoAppDiscoveryService/Dynamic_Controller</compilePath>-->
<!--                            <compilePath>${project.build.sourceDirectory}/com.example.PhotoAppDiscoveryService.Dynamic_Controller</compilePath>-->
                            <includes>${project.build.sourceDirectory}/com.example.PhotoAppDiscoveryService.Dynamic_Controller.generated.*.java</includes>
<!--                            <outputDirectory>target/generated-sources</outputDirectory>-->
                        </configuration>
                    </execution>
                </executions>
            </plugin>
            <!--            -->
        </plugins>
    </build>
</project>
  1. The generated classes will be located under - target/classes/