In this guide, we will use the Python library vaderSentiment from a Spring Boot application written in Java.

To complete this guide, you will need the following:
-
Some time on your hands
-
A decent text editor or IDE
-
A supported JDK1, preferably the latest GraalVM JDK
We recommend that you follow the instructions in the next sections and create the application step by step. However, you can go right to the completed example.
You can use Spring Initializr project generator to generate a basic Spring Boot project skeleton. To generate a project:
- Navigate to https://start.spring.io/
- GraalPy currently provides plugins for Maven and Gradle. You can select either one as the build system. Note: in the Gradle case, this guide uses Groovy.
- Click on Dependencies and select Spring Web
- Click on Dependencies and select Thymeleaf
- Click on Generate. Download and extract the generated ZIP file
The generated Spring Boot application will contain the file DemoApplication.java, which is used when running the application via Maven or via deployment. You can also run the main class directly within your IDE if it is configured correctly.
src/main/java/com/example/demo/DemoApplication.java
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}Add the required dependencies for GraalPy in the dependency section of the POM build script. For Gradle, the GraalPy Gradle plugin that we will add in the next section will inject these dependencies automatically.
pom.xml
<dependency>
<groupId>org.graalvm.python</groupId>
<artifactId>python</artifactId> <!-- ① -->
<version>25.0.2</version>
<type>pom</type> <!-- ② -->
</dependency>
<dependency>
<groupId>org.graalvm.python</groupId>
<artifactId>python-embedding</artifactId> <!-- ③ -->
<version>25.0.2</version>
</dependency>❶ The python dependency is a meta-package that transitively depends on all resources and libraries to run GraalPy.
❷ Note that the python package is not a JAR - it is simply a pom that declares more dependencies.
❸ The python-embedding dependency provides the APIs to manage and use GraalPy from Java.
Most Python packages are hosted on PyPI and can be installed via the pip tool.
The Python ecosystem has conventions about the filesystem layout of installed packages that need to be kept in mind when embedding into Java.
You can use the GraalPy plugins for Gradle and Maven to manage Python packages for you.
Add the graalpy-maven-plugin configuration into the plugins section of the POM or the org.graalvm.python plugin dependency and a graalPy block to your Gradle build:
pom.xml
<plugin>
<groupId>org.graalvm.python</groupId>
<artifactId>graalpy-maven-plugin</artifactId>
<version>25.0.2</version>
<configuration>
<packages> <!-- ① -->
<package>vader-sentiment==3.2.1.1</package> <!-- ② -->
<package>requests</package> <!-- ③ -->
</packages>
</configuration>
<executions>
<execution>
<goals>
<goal>process-graalpy-resources</goal>
</goals>
</execution>
</executions>
</plugin>build.gradle
plugins {
id 'org.graalvm.python' version '25.0.2'
// ...
build.gradle
graalPy {
packages = [ // ①
'vader-sentiment==3.2.1.1', // ②
'requests' // ③
]
}
❶ The packages section lists all Python packages optionally with requirement specifiers.
❷ Python packages and their versions can be specified as if used with pip.
Install and pin the vader-sentiment package to version 3.2.1.1.
❸ The vader_sentiment package does not declare requests as a dependency so it has to done so manually at this place.
GraalPy provides APIs to make setting up a context to load Python packages from Java as easy as possible.
Create a Java class which will serve as a wrapper bean for the GraalPy context:
src/main/java/com/example/demo/GraalPyContext.java
package com.example.demo;
import jakarta.annotation.PreDestroy;
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Value;
import org.graalvm.python.embedding.GraalPyResources;
import org.springframework.stereotype.Component;
@Component // ①
public class GraalPyContext {
static final String PYTHON = "python";
private final Context context;
public GraalPyContext() {
context = GraalPyResources.contextBuilder().build(); // ②
context.initialize(PYTHON); // ③
}
public Value eval(String source) {
return context.eval(PYTHON, source); // ④
}
@PreDestroy
public void close() {
context.close(true); // ⑤
}
}❶ Eagerly initialize as a singleton component.
❷ The created GraalPy context will serve as a single access point to GraalPy for the whole application.
❸ Initializing a GraalPy context isn't cheap, so we do so already at creation time to avoid delayed response time.
❹ Evaluate Python code in the GraalPy context.
❺ Close the GraalPy context at application shutdown.
After reading the vaderSentiment docs, you can now write the Java interface that matches the Python type and function you want to call.
GraalPy makes it easy to access Python objects via these interfaces. Java method names are mapped directly to Python method names. Return values and arguments are mapped according to a set of generic rules as well as the Target type mapping. The names of the interfaces can be chosen freely, but it makes sense to base them on the Python types, as we do below.
Your application will call the Python function polarity_scores(text) from the vader_sentiment.SentimentIntensityAnalyzer Python class.
In oder to do so, create a Java interface with a method matching that function:
src/main/java/com/example/demo/SentimentIntensityAnalyzer.java
package com.example.demo;
import java.util.Map;
public interface SentimentIntensityAnalyzer {
Map<String, Double> polarity_scores(String text); // ①
}❶ The Java method to call into SentimentIntensityAnalyzer.polarity_scores(text).
The Python return value is a dict and can be directly converted to a Java Map on the fly.
The same applies to the argument, which is a Python String and therefore also a String on Java side.
Using this Java interface and the GraalPy context, you can now construct a bean which calls the SentimentIntensityAnalyzer.polarity_scores(text) Python function:
src/main/java/com/example/demo/SentimentAnalysisService.java
package com.example.demo;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.context.annotation.ImportRuntimeHints;
import org.springframework.stereotype.Service;
import java.util.Map;
@Service
@ImportRuntimeHints(SentimentAnalysisService.SentimentIntensityAnalyzerRuntimeHints.class)
public class SentimentAnalysisService {
private final SentimentIntensityAnalyzer sentimentIntensityAnalyzer;
public SentimentAnalysisService(GraalPyContext context) {
var value = context.eval("""
from vader_sentiment.vader_sentiment import SentimentIntensityAnalyzer
SentimentIntensityAnalyzer() # ①
""");
sentimentIntensityAnalyzer = value.as(SentimentIntensityAnalyzer.class); // ②
}
public Map<String, Double> getSentimentScore(String text) {
return sentimentIntensityAnalyzer.polarity_scores(text); // ③
}
static class SentimentIntensityAnalyzerRuntimeHints implements RuntimeHintsRegistrar { // ④
@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
hints.proxies().registerJdkProxy(SentimentIntensityAnalyzer.class);
}
}
}❶ The executed Python snippet imports the vader_sentiment.SentimentIntensityAnalyzer Python class into the GraalPy context and returns a new instance of it.
Note that the GraalPy context preserves its state and an eventual subsequent eval call accessing SentimentIntensityAnalyzer would not require an import anymore.
❷ Map the obtained org.graalvm.polyglot.Value to the SentimentIntensityAnalyzer type.
❸ Return the sentimentIntensityAnalyzer object.
❹ Register interface as proxy for Value.as() (see 7.1. for more details).
The application will have a simple chat-like view, which takes text as input and return its sentiment value in form of an emoticon.
Create a html file, which uses jQuery to query an API endpoint for the sentiment value.
src/main/resources/templates/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<title>Demo App</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
crossorigin="anonymous"></script>
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script language="JavaScript">
$(document).ready(() => {
$("#form").on("submit", (event) => {
event.preventDefault();
let messageInput = $("#message");
let text = messageInput.val();
messageInput.val("");
$("#responses").append(`<div class="row py-3"><div class="col-12"><div class="message-query">${text}</div></div></div>`);
$.getJSON(
"/analyze",
{text: text},
data => {
let reactions = [
[0.5, "\u{1F604}"],
[0.05, "\u{1F642}"],
[-0.05, "\u{1F610}"],
[-0.5, "\u{1F641}"],
];
let selectedReaction = "\u{1F620}";
for (let [threshold, reaction] of reactions) {
if (data["compound"] > threshold) {
selectedReaction = reaction;
break;
}
}
$("#responses").append(`<div class="row py-3"><div class="col-12"><div class="message-reaction display-6">${selectedReaction}</div></div></div>`);
}
);
});
});
</script>
<style>
@media (min-width: 768px) {
.main-container {
width: 600px;
}
}
.message-query, .message-reaction {
width: fit-content;
padding: 2%;
border-radius: 1.2em;
color: white;
}
.message-query {
background-color: #5dade2;
}
.message-reaction {
margin-left: auto;
background-color: #d5dbdb;
}
</style>
</head>
<body>
<div class="container py-5 main-container">
<div class="row align-items-center rounded-3 border shadow-lg">
<div class="row p-5">
<h2>Sentiment analysis demo</h2>
<p class="lead">Input messages and the application will visualize the sentiment.</p>
<div id="responses"></div>
<form id="form">
<div class="row py-3">
<div class="col-12">
<div class="input-group">
<input type="text" id="message" class="form-control" placeholder="Message" required>
<button type="submit" class="btn btn-primary btn-lg px-4 me-md-2 fw-bold">Send</button>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</body>
</html>To create a microservice that provides a simple sentiment analysis, you also need a controller:
src/main/java/com/example/demo/DemoController.java
package com.example.demo;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@Controller
public class DemoController {
private final SentimentAnalysisService sentimentAnalysisService; // ①
public DemoController(SentimentAnalysisService sentimentAnalysisService) {
this.sentimentAnalysisService = sentimentAnalysisService;
}
@GetMapping("/")
public String answer() { // ②
return "index";
}
@GetMapping(value = "/analyze", produces = "application/json")
@ResponseBody
public Map<String, Double> answer(@RequestParam String text) { // ③
return sentimentAnalysisService.getSentimentScore(text); // ④
}
}❶ Inject our service commponent
❷ Serve the main page
❸ Serve the sentiment analysis endpoint
❹ Use the SentimentAnalysisService component to call the SentimentIntensityAnalyzer.polarity_scores(text) Python function.
Create a test to verify that when you make a GET request to /analyze you get the expected sentiment score response:
src/test/java/com/example/demo/DemoApplicationTests.java
package com.example.demo;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.web.servlet.MockMvc;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.lessThan;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringBootTest
@AutoConfigureMockMvc
@DirtiesContext // Necessary to shut down GraalPy context
class DemoApplicationTests {
@Autowired
private MockMvc mockMvc; // ①
@Test
void testIndex() throws Exception { // ②
mockMvc.perform(get("/")).andExpect(status().isOk());
}
@Test
void testSentimentAnalysis() throws Exception { // ③
mockMvc.perform(get("/analyze").param("text", "I'm happy"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.compound", greaterThan(0.1)));
mockMvc.perform(get("/analyze").param("text", "This sucks"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.compound", lessThan(-0.1)));
}
}❶ Use Spring MVC Test framework to make requests to the controller.
❷ Use MVC mock to make request to the index page and verify it returned an Ok response.
❸ Use MVC mock to make requests to the /analyze endpoint and verify the compound sentiment values are within expected ranges.
To run the tests:
./mvnw testor
./gradlew testTo run the application:
./mvnw spring-boot:runor
./gradlew bootRunThis will start the application on port 8080.
The GraalVM Native Image compilation requires metadata to properly run code that uses dynamic proxies.
To compile the application into a native executable, SentimentAnalysisService is annotated with @ImportRuntimeHints (see 4.5.) and the corresponding SentimentIntensityAnalyzerRuntimeHints register SentimentIntensityAnalyzer for proxy access.
We will use GraalVM, the polyglot embeddable virtual machine, to generate a native executable of our Spring Boot application.
Compiling native executables ahead of time with GraalVM improves startup time and reduces the memory footprint of JVM-based applications.
First, add the org.graalvm.buildtools plugin for your build system.
For Maven:
pom.xml
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
</plugin>For Gradle, add this to the plugins block:
build.gradle
id 'org.graalvm.buildtools.native' version '0.11.4'
Make sure that the JAVA_HOME environmental variable is set to the location of a GraalVM installation.
We recommend using a GraalVM 24.1.
To generate and run a native executable using Maven, run:
./mvnw -Pnative native:compile
./target/graalpy-springbootTo generate a native executable using Gradle, run:
./gradlew nativeCompile
./build/native/nativeCompile/demo-
Use GraalPy in a Java SE application
-
Use GraalPy with Micronaut
-
Install and use Python packages that rely on native code, e.g. for data science and machine learning
-
Follow along how you can manually install Python packages and files if the Maven plugin gives not enough control
-
Freeze transitive Python dependencies for reproducible builds
-
Migrate from Jython to GraalPy
-
Learn more about the GraalPy Maven plugin
-
Learn more about the Polyglot API for embedding languages
-
Explore in depth with GraalPy reference manual
Footnotes
-
Oracle JDK 17 and OpenJDK 17 are supported with interpreter only. GraalVM JDK 21, Oracle JDK 21, OpenJDK 21 and newer with JIT compilation. Note: GraalVM for JDK 17 is not supported. ↩