Java Developer Vs. ChatGPT Part I: Writing a Spring Boot Microservice
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 MS 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! Can an AI generate a Java MS end-to-end?
Many developers have previously posted examples of how AI can save time by generating targetted 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:
A self-contained Spring Boot application (using the default embedded server) based on Java (17 or later) with Maven.
Have a resource controller (using the @RestController annotation) for our code generation API endpoints.
Talk with OpenAI API (Codex) to generate the code according to our users prompt.
Use a record class to represent the request and response payloads.
Allow basic persistence of the responses using Spring Data JPA.
Be able to spin up the project easily in IntelliJ.
Out of scope (for now):
Docker, K8s, and anything related to packaging and deployment.
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 itCodeGenerationService, usecom.aias 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:
|-- src
| |-- main
| | |-- java
| | | |-- com
| | | | |-- ai
| | |-- resources
|-- pom.xml
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 namedgenerateCodethat 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 yourpom.xml:
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 methodgenerateCodethat 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 theAPI_KEYproperty into externalapplication.propertiesfile and refactor the client to use the new config property
[chatGPT] >First, create a new file namedapplication.propertiesinside thesrc/main/resourcesfolder and add the following line:
openai.api-key=your_api_key
Replace your_api_key with your OpenAI API key.
Next, modify theOpenAIClientclass to read the API key from theapplication.propertiesfile. You can use the Spring Boot’s@Valueannotation to inject the property value into the class. Also, make the class a Spring-managed bean by adding the@Serviceannotation.
Now, the OpenAIClient class reads the API key from theapplication.propertiesfile 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 namedPromptRequestthat contains one String argument namedinputand place it under a new relative package calledpojos
[chatGPT] >To create a record class namedPromptRequestwith a single String argument namedinput, create a new package calledcom.ai.pojosand add the followingPromptRequest.javafile:
package com.ai.pojos;
public record PromptRequest(String input) {
}
[me] >Now create another record class under the same package, namedGeneratedCodeResult, that has one String argument namedresult
[chatGPT] >To create a record class namedGeneratedCodeResultwith a single String argument namedresult, add the followingGeneratedCodeResult.javafile to thecom.ai.pojospackage:
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 namedCodeGeneration. Use Spring conventions.
[chatGPT] >To create a REST controller namedCodeGenerationusing Spring conventions, follow these steps:
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);
}
}
TheCodeGenerationControllerclass is annotated with@RestController, which indicates that it is a RESTful web service. The@RequestMappingannotation sets the base URL path for this controller, which is/api/codegeneration.
ThegenerateCodemethod is mapped to the/generateendpoint and accepts a POST request with a JSON payload. It takes aPromptRequestobject as input and returns aGeneratedCodeResultobject. The@RequestBodyannotation tells Spring to deserialize the JSON payload into aPromptRequestobject.
TheOpenAIClientis injected into the controller via the constructor, and itsgenerateCodemethod 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?
Any thoughts? Connect with us: Here
Till the next time!
25 Best IntelliJ IDEA Plugins for Developers in 2023
We decided to compile a list of the best IntelliJ Plugins, which we think will boost any developer in 2023. This article explores some of our favorite, most innovative plugins and how they can enhance productivity and the developer experience.
To provide the best experiences, we use technologies like cookies to store and/or access device information. Consenting to these technologies will allow us to process data such as browsing behavior or unique IDs on this site. Not consenting or withdrawing consent, may adversely affect certain features and functions.
Functional Always active
The technical storage or access is strictly necessary for the legitimate purpose of enabling the use of a specific service explicitly requested by the subscriber or user, or for the sole purpose of carrying out the transmission of a communication over an electronic communications network.
Preferences
The technical storage or access is necessary for the legitimate purpose of storing preferences that are not requested by the subscriber or user.
Statistics
The technical storage or access that is used exclusively for statistical purposes.The technical storage or access that is used exclusively for anonymous statistical purposes. Without a subpoena, voluntary compliance on the part of your Internet Service Provider, or additional records from a third party, information stored or retrieved for this purpose alone cannot usually be used to identify you.
Marketing
The technical storage or access is required to create user profiles to send advertising, or to track the user on a website or across several websites for similar marketing purposes.