Friends of OpenJDK Today

Java Developer vs. ChatGPT Part I: Writing a Spring Boot Microservice

April 27, 2023

Author(s)

  • Avatar photo
    Roni Dover

    Afflicted by an acute Product Manager/Developer split personality disorder that was never treated. Currently, CTO and co-founder of Digma (digma.ai), an IDE plugin for code runtime AI analysis to help ... Learn more

We pit a seasoned Java developer against the all-knowing generative AI to find out once and for all: Can an AI generate a Java microservice end-to-end?

There’s a lot of buzz around Generative AI tools for code generation. It’s no secret that the future of developer productivity goes alongside tools such as GitHub Copilot and OpenAI's ChatGPT to enable us, developers, to produce code better and faster. These tools can learn from our existing code and suggest new code based on the desired functionality, most importantly, they can even write documentation and tests!

Many developers have previously posted examples of how AI can save time by generating targeted pieces of code, cracking complicated regexes, or building some front-end scaffolds. As a lazy developer, my first instinct was to see just how far we can take such code. It's one thing to ask our loveable assistant to solve a specific coding problem, another to ask it to come up with an end-to-end solution.

Today, I want to tackle this very question by asking ChatGPT to write a complete, fully functional, Spring Boot Java microservice. The catch? Not a single line of code will be written by a human operator (me).

To make this experiment fun, we’re going to build a RESTful web service that generates code for a given prompt by using OpenAI API (Codex models) behind the scenes. Yes, it’s a code generation service produced by a code generation model. 😉

Before we start, let’s define the requirements for the capabilities of the expected outcome.

  1. A self-contained Spring Boot application (using the default embedded server) based on Java (17 or later) with Maven.
  2. Have a resource controller (using the @RestController annotation) for our code generation API endpoints.
  3. Talk with OpenAI API (Codex) to generate the code according to our users prompt.
  4. Use a record class to represent the request and response payloads.
  5. Allow basic persistence of the responses using Spring Data JPA.
  6. Be able to spin up the project easily in IntelliJ.

Out of scope (for now):

  1. Docker, K8s, and anything related to packaging and deployment.
  2. Full E2E test coverage.

Sounds reasonable right? Let’s get to it!

Disclaimer: I am using ChatGPT Plus with the latest GPT4 model to generate the code snippets. Some of the code generation prompt examples here might not work with the current free-to-use GPT3.5 model.

Step 1: Bootstrapping the initial Spring Boot skeleton

(The prompts and responses are copied as is - for the reader's benefit, the dialog is shared as text instead of screenshots. Prompts are shared as is without any modification)

[me] > Before we start, let's set the requirements for the project: use Java 17 or later as the language, use Maven as the build tool, and generate production-ready code. Sounds good?

[chatGPT] > I acknowledge the requirements for the project:

  • Use Java 17 or later as the programming language.
  • Use Maven as the build tool.
  • Generate production-ready code.

We can now proceed with creating the Spring Boot microservice based on these requirements.

[me] > Write the initial code skeleton for the project. Name it CodeGenerationService, use com.ai as the package name, and provide the relevant files (main class, pom.xml) with the folder structure.

[chatGPT] > Here's the initial code skeleton for the CodeGenerationService project. We will create a folder structure and the necessary files, including the main class and the pom.xml file.

Create the following folder structure:

CodeGenerationService

|-- src
|   |-- main
|   |   |-- java
|   |   |   |-- com
|   |   |   |   |-- ai
|   |   |-- resources
|-- pom.xml

Inside the com.ai package, create the main class file CodeGenerationServiceApplication.java with the following content:

package com.ai;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

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

Next, create the pom.xml file in the root directory of the project with the following content:

<?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         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.ai</groupId>
    <artifactId>CodeGenerationService</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>
    <name>CodeGenerationService</name>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <java.version>17</java.version>
        <spring.boot.version>2.6.2</spring.boot.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>${spring.boot.version}</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>${spring.boot.version}</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

This sets up a basic Spring Boot project using Maven as the build tool and Java 17 as the programming language. The project has a single CodeGenerationServiceApplication class, which is the entry point for the application. The pom.xml file includes the required dependencies and build configurations.

[me] > Provide a run command to start the application using Maven

[chatGPT] > To run the application using Maven, you can use the spring-boot:run command. Open a terminal or command prompt, navigate to the project's root directory (where the pom.xml file is located), and execute the following command:

mvn spring-boot:run

This command will start the Spring Boot application, and you should see the application logs in the terminal.


Okay. This was... pretty straightforward. We were able to generate a minimal required setup for a Spring Boot application. Let’s check it actually works:

$ mvn spring-boot:run 

[INFO] Attaching agents: []

  .   ____                      _ 
 /\\ / ___'_ __ (_)_    _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.6.2)

2023-03-21 21:05:38.369  INFO 19502 --- [           main] com.ai.CodeGenerationServiceApplication  : Starting CodeGenerationServiceApplication using Java 17.0.1 on just.local with PID 19502 (/Users/mar/Development/ChatGPT/target/classes started by mar in /Users/mar/Development/ChatGPT)
2023-03-21 21:05:38.371  INFO 19502 --- [           main] com.ai.CodeGenerationServiceApplication  : No active profile set, falling back to default profiles: default
2023-03-21 21:05:39.231  INFO 19502 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2023-03-21 21:05:39.244  INFO 19502 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2023-03-21 21:05:39.245  INFO 19502 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.56]
2023-03-21 21:05:39.335  INFO 19502 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2023-03-21 21:05:39.336  INFO 19502 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 918 ms
2023-03-21 21:05:39.666  INFO 19502 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2023-03-21 21:05:39.673  INFO 19502 --- [           main] com.ai.CodeGenerationServiceApplication  : Started CodeGenerationServiceApplication in 1.902 seconds (JVM running for 2.239)

Great success! Or at least a great start.

Step 2: Create OpenAI API client

[me] > In this project we're going to use the OpenAI API. Write a Java client that contains a method named generateCode that accepts a String input and returns the generated code by the API.

[chatGPT] > To use the OpenAI API in your Java project, you can use a library like Retrofit to make HTTP requests. First, add the necessary dependencies to your pom.xml:

<dependency>
    <groupId>com.squareup.retrofit2</groupId>
    <artifactId>retrofit</artifactId>
    <version>2.9.0</version>
</dependency>

<dependency>
    <groupId>com.squareup.retrofit2</groupId>
    <artifactId>converter-gson</artifactId>
    <version>2.9.0</version>
</dependency>

Next, create a new package called com.ai.openai and add the following files:

1. OpenAIApi.java:

package com.ai.openai;
import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.http.Body;
import retrofit2.http.Headers;
import retrofit2.http.POST;


public interface OpenAIApi {

    @Headers("Content-Type: application/json")
    @POST("v1/engines/davinci-codex/completions")
    Call<ResponseBody> generateCode(@Body String input);

}

2. OpenAIClient.java:

package com.ai.openai;

import okhttp3.OkHttpClient;
import okhttp3.ResponseBody;
import okhttp3.logging.HttpLoggingInterceptor;
import retrofit2.Call;
import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;

import java.io.IOException;

public class OpenAIClient {
    private static final String API_KEY = "your_api_key";
    private static final String BASE_URL = "https://api.openai.com/";

    private final OpenAIApi api;

    public OpenAIClient() {

        OkHttpClient.Builder httpClient = new OkHttpClient.Builder(); 
        HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
        logging.setLevel(HttpLoggingInterceptor.Level.BODY);
        httpClient.addInterceptor(logging);
        httpClient.addInterceptor(chain -> {    
            okhttp3.Request original = chain.request();
            okhttp3.Request request = original.newBuilder()
                .header("Authorization", "Bearer " + API_KEY)
                .method(original.method(), original.body())
                .build();

            return chain.proceed(request);
        });

        Retrofit retrofit = new Retrofit.Builder()
            .baseUrl(BASE_URL)
            .addConverterFactory(GsonConverterFactory.create())
            .client(httpClient.build())
            .build();

        api = retrofit.create(OpenAIApi.class);
    }

    public String generateCode(String input) {

        String code = "";
        Call<ResponseBody> call = api.generateCode(input);

        try {
            Response<ResponseBody> response = call.execute();
            if (response.isSuccessful()) {
                code = response.body().string();
            } 
            else {
                throw new IOException("Failed to generate code: " +                           response.errorBody().string());
        }

        } catch (IOException e) {
            e.printStackTrace();
        }
        return code;

    }
}

Replace your_api_key with your OpenAI API key. The OpenAIClient class has a method generateCode that accepts a String input and returns the generated code by the API. It uses Retrofit to make API requests and OkHttpClient to add the necessary headers.

Please note that this implementation uses synchronous calls to the OpenAI API. In a real-world application, it's recommended to use asynchronous calls to avoid blocking the main thread.


A few interesting observations on the generated code:

1. ChatGPT decides to use an external HTTP library for the client code implementation instead of using Spring’s very own RestTemplate. Weird.

2. It is still aware of the project scope and names the package according to our original prompt (`com.ai`). Nice!

3. The implementation is using the Codex model by default (`davinci-codex`) without mentioning that explicitly. Not bad!

4. The sync vs. async comment at the end: it seems that ChatGPT decided to regard the ‘production-ready code’ requirement we mentioned to it before in a manner of criticizing its own code rather than implementing it “properly” from the first try.


Let’s continue:

[me] > Please extract the API_KEY property into external application.properties file and refactor the client to use the new config property

[chatGPT] > First, create a new file named application.properties inside the src/main/resources folder and add the following line:

openai.api-key=your_api_key

Replace your_api_key with your OpenAI API key.

Next, modify the OpenAIClient class to read the API key from the application.properties file. You can use the Spring Boot's @Value annotation to inject the property value into the class. Also, make the class a Spring-managed bean by adding the @Service annotation.

Update the OpenAIClient.java file as follows:

package com.ai.openai;
import okhttp3.OkHttpClient;
import okhttp3.ResponseBody;
import okhttp3.logging.HttpLoggingInterceptor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import retrofit2.Call;
import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
import java.io.IOException;

@Service
public class OpenAIClient {

    @Value("${openai.api-key}")
    private String apiKey;
    private static final String BASE_URL = "https://api.openai.com/";
    private final OpenAIApi api;

    public OpenAIClient() {

        OkHttpClient.Builder httpClient = new OkHttpClient.Builder();
        HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
        logging.setLevel(HttpLoggingInterceptor.Level.BODY);
        httpClient.addInterceptor(logging);
        httpClient.addInterceptor(chain -> {
            okhttp3.Request original = chain.request();
            okhttp3.Request request = original.newBuilder()
                .header("Authorization", "Bearer " + apiKey)
                .method(original.method(), original.body())
                .build();
            return chain.proceed(request);
        });

        Retrofit retrofit = new Retrofit.Builder()
            .baseUrl(BASE_URL)
            .addConverterFactory(GsonConverterFactory.create())
            .client(httpClient.build())
            .build();
        api = retrofit.create(OpenAIApi.class);

    }

    public String generateCode(String input) {
        String code = "";
        Call<ResponseBody> call = api.generateCode(input);
        try {
            Response<ResponseBody> response = call.execute();
        if (response.isSuccessful()) {
            code = response.body().string();
        } else {
            throw new IOException("Failed to generate code: " + response.errorBody().string());
        }
        } catch (IOException e) {
        e.printStackTrace();
        }
        return code;
    }

}

Now, the OpenAIClient class reads the API key from the application.properties file and uses it for authentication when making requests to the OpenAI API.


We can see that ChatGPT made the change correctly using Spring’s proper annotations, understood where the application.properties should be located (under resources folder) and refactored the code accordingly.

Implicitly, I hoped for it to take one step further and create a relevant interface, a separate Service class, and take any DDD aspects in concern. We’ll get back to it later!

Step 3: POJOs to the rescue

[me] > Write a record class named PromptRequest that contains one String argument named input and place it under a new relative package called pojos

[chatGPT] > To create a record class named PromptRequest with a single String argument named input, create a new package called com.ai.pojos and add the following PromptRequest.java file:

package com.ai.pojos;

public record PromptRequest(String input) {

}

[me] > Now create another record class under the same package, named GeneratedCodeResult, that has one String argument named result

[chatGPT] > To create a record class named GeneratedCodeResult with a single String argument named result, add the following GeneratedCodeResult.java file to the com.ai.pojos package:

package com.ai.pojos;

public record GeneratedCodeResult(String result) {

}

Nothing complicated here. We created the request & response POJOs that will be used later for serving the API calls. ChatGPT delivered as desired.

Step 4: Write our first REST service endpoint (controller)

[me] > Write a REST controller named CodeGeneration. Use Spring conventions.

[chatGPT] > To create a REST controller named CodeGeneration using Spring conventions, follow these steps:

1. Create a new package called com.ai.controllers.

2. Create a new file named CodeGenerationController.java in the com.ai.controllers package.

3. Add the following content to the CodeGenerationController.java *file:

package com.ai.controllers;
import com.ai.openai.OpenAIClient;
import com.ai.pojos.GeneratedCodeResult;
import com.ai.pojos.PromptRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/codegeneration")
public class CodeGenerationController {

    private final OpenAIClient openAIClient;

    @Autowired
    public CodeGenerationController(OpenAIClient openAIClient) {
        this.openAIClient = openAIClient;
    }

    @PostMapping("/generate")
    public GeneratedCodeResult generateCode(@RequestBody PromptRequest     promptRequest) {

        String input = promptRequest.input();
        String generatedCode = openAIClient.generateCode(input);
        return new GeneratedCodeResult(generatedCode);

    }

}

The CodeGenerationController class is annotated with @RestController, which indicates that it is a RESTful web service. The @RequestMapping annotation sets the base URL path for this controller, which is /api/codegeneration.

The generateCode method is mapped to the /generate endpoint and accepts a POST request with a JSON payload. It takes a PromptRequest object as input and returns a GeneratedCodeResult object. The @RequestBody annotation tells Spring to deserialize the JSON payload into a PromptRequest object.

The OpenAIClient is injected into the controller via the constructor, and its generateCode method is called to get the generated code from the OpenAI API.


I wanted to take ChatGPT step by step in the Controller code generation, instead, it already included the implementation using the client and the POJOs we previously generated! I am shocked. This was a very unexpected outcome from my prompt and made me stare at the screen for a minute realizing how it inferred the scope immediately. I salute you, my AI friend.

Summary

We saw how using ChatGPT we managed to create a new Spring Boot microservice, implemented relevant classes (OpenAI API client and the request/response POJOs), and more importantly: generated our first Controller code that combines all the various components created by that point.

While on some occasions it decided to follow the original requirements and Spring conventions, on many others we see that they were ignored, resulting in future refactoring we’ll have to do. For a model that outshined at the last prompt, I would expect a more consistent behavior while nonetheless impressed by the outcome.

In the next article in the series, I will 'productionize' the generated code: add persistence for responses (using Spring Data JPA), refactor the code to apply best practices, add some observability and answer the question: can we actually do it?

Till the next time!

Related Articles

View All

Author(s)

  • Avatar photo
    Roni Dover

    Afflicted by an acute Product Manager/Developer split personality disorder that was never treated. Currently, CTO and co-founder of Digma (digma.ai), an IDE plugin for code runtime AI analysis to help ... Learn more

Comments (0)

Your email address will not be published. Required fields are marked *

Highlight your code snippets using [code lang="language name"] shortcode. Just insert your code between opening and closing tag: [code lang="java"] code [/code]. Or specify another language.

Save my name, email, and website in this browser for the next time I comment.

Subscribe to foojay updates:

https://foojay.io/feed/
Copied to the clipboard