Search code examples
springspring-bootcode-generationspring-bean

Using JavaPoet to create Sping Beans


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

Solution

  • 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/