Iqbal´s DLQ Help

JBang Meets Spring Boot & LangChain4j: A Powerhouse for Java Scripting and AI Pipelines

When was the last time you used Java in a pipeline script? Today, we'll explore how to use Java for scripting by building a tooling script for this blog.

I was waiting for a good use case for JBang, and then I realized it's not limited to simple single-file use cases. You can actually bring along Spring Boot, and usually, that's more than enough for me. Just give me that Spring context, and I'm good to go.

Use Case

I don't always have time to be meticulous in the articles I write here, as I am building everything from scratch. In this case, you are both the developer and the reporter.

I think it's good to leverage AI, learn scripting with Java through JBang, and explore the possibilities of LangChain4j, which was the driver for this article.

The plan is to run inferences against the markdown files in this blog using DeepSeek while respecting the original markdown structure and the author's intent.

AI, when used to augment your capabilities, is wonderful. It opens the door to a perpetually improving machine that doesn't tire.

I'll share some of the PR suggestions that DeepSeek came up with for the articles shared here.

Remember, you are in control. You can always choose to ignore the suggestions)

JBang

Introduction

JBang has done a lot for Java when it comes to quickly bootstrapping a project in a minute or less. It's a great tool for scripting in Java.

Although I believe I am slightly abusing the original intent of the tool by bringing in Spring Boot and packaging this script as a complete project, I am happy with the power this brings to the table. I can see Java in CI/CD pipelines in the future as a valid option that brings type safety and the rich standard API that Java is known for in a traditional pipeline.

What a time to be alive: NIO 2, Streams, in what traditionally was a bash script.

My understanding is that this can shift the general feeling that pipelines are second-class citizens. Once they are established, we rarely venture to improve them. In your project, this can power what I would call Pipelines As Code (PaC?).

Getting Started

As mentioned before, JBang aims to get you started with Java rather quickly:

jbang init myscript.java

This will create a script that you can run with JBang:

jbang myscript.java

I didn't want to cram everything into a single file, and JBang has support for multi-source files, so I went with that.

Let's explore how to set up a project with JBang, IntelliJ, and Spring here.

Since this blog is not a Java project, I am creating a folder to hold the future scripts:

jbang-spring-boot-project.png
└── scripts └── writerside-deepseek-expert └── src ├── com │ └── iqbalaissaoui │ ├── JBangSpringBootApp.java │ ├── application.properties │ ├── assistants │ │ ├── WriterSideExpert.java │ │ └── WriterSideParentTopicMaintainer.java │ ├── banner.txt │ ├── output.md │ ├── runners │ │ ├── AllFilesMarkdownRefinerRunner.java │ │ └── SingleFileMarkdownRefinerRunner.java │ └── services │ └── MarkDownRefinerService.java

To get full support from IntelliJ, you'll have to create a module this time since this is a regular folder and not a Maven project.

new-module-manual.png

You can initialize JBang within the src folder and use package names as you would in a regular Java project.

There is also a JBang plugin for IntelliJ that you'll need for a better experience.

JBang Plugin for IntelliJ

Worth noting that the dependency imports need you to run this action to be added to the project module.

You can run it on the entry point file JBangSpringBootApp.java.

Walkthrough the Code

Let's go through the files you see in the tree above:

Starting with the entry point JBangSpringBootApp.java:

/// usr/bin/env jbang "$0" "$@" ; exit $? //DEPS org.springframework.boot:spring-boot-dependencies:3.4.3@pom //DEPS org.springframework.boot:spring-boot-starter //DEPS dev.langchain4j:langchain4j-spring-boot-starter:0.36.2 //DEPS dev.langchain4j:langchain4j-open-ai-spring-boot-starter:0.36.2 //DEPS org.apache.commons:commons-lang3:3.17.0 //FILES application.properties //FILES banner.txt //SOURCES assistants/WriterSideExpert.java //SOURCES runners/AllFilesMarkdownRefinerRunner.java //SOURCES runners/SingleFileMarkdownRefinerRunner.java //SOURCES services/MarkDownRefinerService.java package com.iqbalaissaoui; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class JBangSpringBootApp { public static void main(String[] args) { SpringApplication.run(JBangSpringBootApp.class, args); } }
  • //DEPS are the dependencies that JBang will download for you, and you can use them in your script.

  • //FILES are the files that JBang will include in the build. We are using it here for Spring Boot's typical application.properties and banner.txt.

  • //SOURCES are the source files JBang will compile and include in the build.

We will be using:

  • Directly the Spring Boot starter as we don't need the embedded web server.

  • LangChain4j Spring integration for its easy-to-bootstrap high-level API for AI inference (AI Services).

  • Command Line Runners loaded conditionally by profiles for two use cases: all files in the blog and a single file.

The AI interface leveraging LangChain4j is in WriterSideExpert.java:

package com.iqbalaissaoui.assistants; import dev.langchain4j.service.SystemMessage; import dev.langchain4j.service.spring.AiService; @AiService public interface WriterSideExpert { String SYSTEM_PROMPT = """ You are a WriterSide Technical Expert, a highly skilled AI assistant specializing in technical writing and documentation. Your task is to review, refine, and improve the content of Markdown (MD) files while adhering to the following guidelines: Preserve Structure: Maintain the original structure, headings, code blocks, lists, and formatting of the MD file. Do not alter the hierarchy or break the file's layout. Improve Clarity: Enhance the clarity, readability, and flow of the content. Fix grammatical errors, awkward phrasing, and inconsistencies without changing the intended meaning. Technical Accuracy: Ensure technical terms, code snippets, and explanations are accurate and up-to-date. Suggest improvements to technical descriptions if they are unclear or incomplete. Conciseness: Remove redundancy and wordiness while retaining all essential information. Make the content more concise and to the point. Consistency: Ensure consistent use of terminology, tone, and style throughout the document. Follow standard technical writing best practices. Minor Changes Only: Avoid making major changes to the content. Focus on incremental improvements that enhance the quality of the document without altering its core message or structure. Respect Original Intent: Do not introduce new ideas, concepts, or sections unless they are minor and directly improve the existing content. Output Format: Return the improved content in the same MD format, ensuring it is ready for use and compatible with Markdown parsers. Your goal is to provide a polished, professional, and technically sound version of the input MD file while respecting its original structure and intent. Proceed with the improvements only after carefully analyzing the content."""; String WRITERSIDE_TOPICS_BASE_FOLDER = "../../../../../Writerside/topics/"; @SystemMessage(SYSTEM_PROMPT) String chat(String userMessage); }

MarkDownRefinerService.java is the service that will use the AI interface to refine the markdowns:

package com.iqbalaissaoui.services; import com.iqbalaissaoui.assistants.WriterSideExpert; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component public class MarkDownRefinerService { @Autowired private WriterSideExpert assistant; public String refine(String input) { return assistant.chat(input); } }

Let's take a look at the runners next:

SingleFileMarkdownRefinerRunner.java to target a single markdown file for improvements:

package com.iqbalaissaoui.runners; import com.iqbalaissaoui.assistants.WriterSideExpert; import com.iqbalaissaoui.services.MarkDownRefinerService; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.CommandLineRunner; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Component; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Optional; @Component @Profile("single-file") public class SingleFileMarkdownRefinerRunner implements CommandLineRunner { @Autowired private MarkDownRefinerService assistant; @Override public void run(String... varargs) throws IOException { System.out.println("MarkdownRefinerRunner.run"); // Check args that the first arg is an md file or throw illegalarg Optional.of(varargs) .filter(args -> args.length == 0) .ifPresent(args -> { throw new IllegalArgumentException("Please provide a markdown file as an argument"); }); // Check the argument is String topic = WriterSideExpert.WRITERSIDE_TOPICS_BASE_FOLDER + varargs[0]; // Check if the file exists or throw illegalarg Optional.of(Files.exists(Paths.get(topic))) .filter(Boolean.FALSE::equals) .ifPresent(b -> { throw new IllegalArgumentException("The file does not exist"); }); Path p = Path.of(topic); String input = Files.readString(p); String output = assistant.refine(input); Optional.of(StringUtils.difference(input, output)) .ifPresent(System.out::println); Files.writeString(p, output); } }

The rich standard API of Java here shines:

We can use the Files class from NIO 2 to easily read and write files, and StringUtils from Apache Commons to compare the differences between the original and the improved markdown.

We will also leverage the Stream API in the all-files processor.

AllFilesMarkdownRefinerRunner.java:

package com.iqbalaissaoui.runners; import com.iqbalaissaoui.assistants.WriterSideExpert; import com.iqbalaissaoui.services.MarkDownRefinerService; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.CommandLineRunner; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Component; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; import java.util.Optional; @Component @Profile("all-files") public class AllFilesMarkdownRefinerRunner implements CommandLineRunner { @Autowired private MarkDownRefinerService assistant; @Override public void run(String... varargs) throws Exception { System.out.println("AllFilesMarkdownRefinerRunner.run"); Files.find( Paths.get(WriterSideExpert.WRITERSIDE_TOPICS_BASE_FOLDER), Integer.MAX_VALUE, (filePath, fileAttr) -> fileAttr.isRegularFile() && filePath.toString().endsWith(".md") ) .filter(p -> { try { return Files.readString( Paths.get(WriterSideExpert.WRITERSIDE_TOPICS_BASE_FOLDER, "../hi.tree")).contains(p.getFileName().toString()); } catch (IOException e) { throw new RuntimeException(e); } }) .peek(p -> System.out.println("Processing file: " + p)) .forEach(p -> { try { String input = Files.readString(p); String output = assistant.refine(input); Optional.of(StringUtils.difference(input, output)) .ifPresent(System.out::println); Files.writeString(p, output); } catch (IOException e) { e.printStackTrace(); } }); } }

This runner reads all markdown files that belong to the specific WriterSide instance (hi.tree filter) and processes them with the AI service.

Finally, let's go through the application.properties:

# Active profile spring.profiles.active=single-file langchain4j.open-ai.chat-model.base-url=https://api.deepseek.com # Possible to expand like ${OPENAI_API_KEY} and set it as an environment variable langchain4j.open-ai.chat-model.api-key=${DEEPSEEK_API_KEY} langchain4j.open-ai.chat-model.model-name=deepseek-chat langchain4j.open-ai.chat-model.log-requests=true langchain4j.open-ai.chat-model.log-responses=true langchain4j.open-ai.chat-model.timeout=360s

I am only using these properties:

langchain4j.open-ai.chat-model.api-key=${DEEPSEEK_API_KEY} langchain4j.open-ai.chat-model.model-name=deepseek-chat langchain4j.open-ai.chat-model.log-requests=true langchain4j.open-ai.chat-model.log-responses=true langchain4j.open-ai.chat-model.timeout=360s

In particular, it's worth mentioning that the default timeout was not enough for some of the inferences, so I had to increase it.

Running the Script

Simply run the script with JBang from the same folder where the entry point file is (here is from within com.iqbalaissaoui):

For the single file runner, pass the file name as an argument (without the path as I included a base folder in the runner):

SPRING_PROFILES_ACTIVE=single-file jbang JBangSpringBootApp.java my-markdown-file.md

For the all files runner:

SPRING_PROFILES_ACTIVE=all-files jbang JBangSpringBootApp.java

And that's it! here's a sneak peek at the suggested output:

example ai suggestions.png

Where to Go from Here

Now the sky is the limit, you can chain AI Services that handle different scenarios with Java's UnaryOperator to keep the setup simple:

package com.iqbalaissaoui.services; import com.iqbalaissaoui.assistants.WriterSideExpert; import com.iqbalaissaoui.assistants.WriterSideReferenceRefiner; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.stereotype.Service; import java.util.function.UnaryOperator; @Service public class MarkDownRefinerService { @Autowired private WriterSideExpert writerSideExpert; @Autowired private WriterSideReferenceRefiner writerSideReferenceRefiner; public String refine(String input) { UnaryOperator<String> opWriterSideExpert = writerSideExpert::chat; UnaryOperator<String> opWriterSideReferenceRefiner = writerSideReferenceRefiner::chat; return opWriterSideExpert.andThen(opWriterSideReferenceRefiner).apply(input); } }
Last modified: 14 March 2025