Java is where the Cursor vs VS Code choice gets awkward fastest. The language server, the debugger, the Maven integration and the test runner all arrive as separate extensions, and the long-term runtime to target is JDK 25. Microsoft’s own release notes say its build of OpenJDK 25 carries no Microsoft-specific changes (see the Microsoft Learn OpenJDK release notes), so the runtime is the easy part. The editor wiring is where setups drift.
Both editors can open a pom.xml, so that is not the question. The question is whether the same extensions, settings and debug configs behave the same way in each, and what your AI assistant does with them. The extension side of that gap has its own write-up in the Cursor vs VS Code extension parity audit. Here we stay on Java and build one working service that runs, debugs, tests and gates identically in both.
Budget about 60 minutes. You finish with a Spring Boot 4.1.2 project on JDK 25, committed workspace config that both editors read, a gap-check script that tells you when one editor falls behind, matching AI rules for each editor, and a gate script that runs locally and in CI.
Cursor vs VSCode for Java in November 2026
Cursor is an AI coding product built around an editor derived from VS Code, and that lineage is why Java setup looks so similar in both. Your .vscode folder, your keybindings and most extension IDs carry over. What differs is who ships the AI layer, how extensions are sourced and what the subscription model looks like this month. Two of those moved in the last few weeks, so it is worth a short recap before touching a terminal.
What changed in VS Code
Microsoft announced a modernized VS Code UI in preview on October 15, 2026. The older design is now called the classic UI, and Microsoft said it plans to retire it without giving a removal date. On October 28 the team said the Agent Host Protocol reached version 1.0. GitHub Copilot remains a separate subscription-backed service inside the editor, and Copilot Free users receive next-edit suggestions. Paid Copilot users have had a unified inline-suggestions experience since September 23, as the VS Code team’s inline suggestions write-up explains.
None of that touches the Java toolchain. The Java extensions still install the same way, and the classic UI fallback means a theme or layout change will not break your debug configs.
What changed in Cursor
Cursor introduced The X Subscription on October 12, 2026, a combined plan that covers Cursor, Grok, Grok Bot and X Premium. On Premium and higher, third-party models stay available but are billed at API list prices through prepaid top-ups with spending caps. The Ultra tier is listed at $200 per month, or $2,000 per year. Cursor’s pricing policy says current fees live on its Models and Pricing page, so check there before you buy anything. Two product launches landed this week: live app building and deployment for cloud agents on November 5, and realtime voice chat with agents on November 6. The model picker now includes Claude Sonnet 5.5 and Claude Opus 5.5, with Claude Haiku 5.5 as an opt-in model.
The billing change matters more for Java than you might expect. A Maven build loop run by an agent can burn through paid model usage quickly, and we come back to that in the AI rules step. If you came from the Python version of this exercise, the Cursor vs VS Code Python setup covers the same drift problem with a different language server.
One more point on Java specifically. The language server that does the real work, Eclipse JDT Language Server wrapped by the Red Hat extension, is the same code in both editors. So the differences you will hit are packaging, licensing and AI behavior, not parsing or refactoring quality. The Cursor docs and release notes we checked do not say whether the Red Hat extension or Microsoft’s Extension Pack for Java ships in the box, so this tutorial never assumes it. Step 7 tests for it on your machine.
What You Will Build and the Time Budget
The project is a small stock-level service called stock-service. It exposes two endpoints, keeps levels in memory, rejects adjustments that would push stock below zero and ships with unit tests. It is deliberately boring, because the point is the editor setup around it. Every file below goes into one repository that you can open in VS Code, Cursor or both.
| Steps | What you do | Minutes |
|---|---|---|
| 1 to 3 | Install JDK 25, Maven and both editors with their shell commands | 15 |
| 4 and 5 | Write the pom, the service code, and run the first build | 9 |
| 6 and 7 | Install the Java extensions and run the gap check | 10 |
| 8 and 9 | Set the JDK per machine and share workspace settings | 9 |
| 10 and 11 | Debug the app and run tests from either editor | 9 |
| 12 | Write AI rules for Cursor and VS Code | 4 |
| 13 | Add a gate script and a CI workflow | 4 |
| Total | Thirteen steps | 60 |
The minutes assume a warm network and a laptop that already has git. If you are starting on a fresh machine, add 10 minutes for downloads. Windows readers can run every script in WSL or Git Bash, and the Java and Maven commands are identical in PowerShell.
Prerequisites and Versions to Pin
Pin versions before you write a line of code. Java projects fail in odd ways when the editor, the shell and the build tool each quietly pick a different JDK, and most of the troubleshooting later in this article traces back to that one problem.
- JDK 25 (LTS). Oracle identifies JDK 25 as a long-term-support release that shipped in September 2025, with commercial Premier Support planned through September 2030 and Extended Support through September 2033. Oracle JDK 25 is available at no cost under the Oracle No-Fee Terms and Conditions license. Any OpenJDK 25 build works for this tutorial, including Temurin or Microsoft’s build.
- Apache Maven 3.x. According to the Apache Maven download page, 3.10.0 is the latest recommended release. Maven 4 is still in release-candidate form and the project says it is not safe for production use, so stay on the 3.x line.
- Spring Boot 4.1.2. The stable maintenance release from October 23, 2026. Spring Boot is open source and distributed through Maven Central, so there is no license fee to plan for.
- VS Code and Cursor, current stable builds of each. We deliberately do not pin an editor version number, because both update on their own schedule. Step 3 has you record what you actually have.
- A shell with bash. macOS, Linux, WSL or Git Bash. Every script in this tutorial is plain bash.
- git 2.x and about 2 GB of free disk for the JDK, the Maven repository cache and language server indexes.
- A Cursor account is optional. Everything up to step 11 works without paying. Step 12 only needs the free tier to write the rules file, though the models you can call depend on your plan.
| Component | Use this | Status on November 7, 2026 |
|---|---|---|
| JDK | JDK 25 | LTS release, shipped September 2025 |
| Spring Boot | 4.1.2 | Stable maintenance release, October 23, 2026, 126 bug fixes |
| Spring Boot | 4.0.9 | Maintenance release, also October 23, 2026 |
| Spring Boot | 4.2.0-RC2 | Release candidate, November 6, 2026, 99 enhancements |
| Maven | 4.x | Release candidate, not production-ready per the Maven project |
The Spring team is already lining up the next train. Spring Cloud 2026.0.0-RC2, code-named Paddington, was announced on November 5 and is based on Spring Boot 4.2.0-RC2, according to the Spring blog. Treat anything labeled RC as a branch experiment. Your editor setup will not care which Boot version you use, but your teammates’ production services will.
One more prerequisite is a plan for user settings. VS Code and Cursor keep separate user-level settings files, and Java runtime paths live there. The Cursor vs VS Code settings sync guide covers keeping those files aligned across machines. For this tutorial, we keep machine-specific paths in user settings and everything else in the repository.
Steps 1 to 3: Install JDK 25, Maven and Both Editors
Step 1: Install JDK 25
Pick one install route and stick with it. On macOS and Linux, SDKMAN lets you switch JDKs per shell, which is handy if another project still needs an older release. List what is available, then install a Temurin 25 build.
curl -s "https://get.sdkman.io" | bash
source "$HOME/.sdkman/bin/sdkman-init.sh"
sdk list java | grep -i tem
sdk install java <the Temurin 25 identifier from the list>
sdk default java <the same identifier>
Package managers work too. On macOS, brew install --cask temurin@25 installs the Temurin cask. On Windows, winget search temurin shows the exact package ID for the 25 JDK. On Ubuntu, search apt for openjdk-25-jdk and install it if your release carries it. Then export JAVA_HOME so every tool agrees.
# macOS and Linux (add to ~/.zshrc or ~/.bashrc)
export JAVA_HOME="$(dirname "$(dirname "$(readlink -f "$(command -v java)")")")"
export PATH="$JAVA_HOME/bin:$PATH"
java -version
The expected output names a 25 build. Your patch number and vendor string will differ.
openjdk version "25.0.x" 20xx-xx-xx LTS
OpenJDK Runtime Environment Temurin-25.0.x+x (build 25.0.x+x-LTS)
OpenJDK 64-Bit Server VM Temurin-25.0.x+x (build 25.0.x+x-LTS, mixed mode, sharing)
Step 2: Install Maven and confirm it uses JDK 25
Install the current 3.x release from your package manager or from the Apache download page, then confirm Maven reports the same Java you just installed. This single check prevents the most common Java editor bug, a language server that compiles with one JDK while the terminal builds with another.
mvn -v
Apache Maven 3.x.y
Maven home: /home/you/.sdkman/candidates/maven/current
Java version: 25.0.x, vendor: Eclipse Adoptium, runtime: /home/you/.sdkman/candidates/java/current
If the Java version line shows anything other than 25, fix JAVA_HOME now. Do not move on until it matches.
Step 3: Install both editors and their shell commands
Download VS Code from code.visualstudio.com and Cursor from cursor.com, then install each editor’s command-line launcher from its Command Palette. In VS Code run Shell Command: Install 'code' command in PATH. In Cursor run Shell Command: Install 'cursor' command in PATH. Both launchers matter later, because the gap check script calls code --list-extensions and cursor --list-extensions. The two CLIs share a lot of surface but not all of it, and the Cursor CLI vs VS Code CLI comparison shows where they split.
code --version
cursor --version
Both commands print a version string. Write both down in your project notes. When something breaks after an update, those two lines tell you which editor moved.
Steps 4 and 5: Create the Spring Boot Project and Build It
Step 4: Lay out the project and write the pom
We write the project by hand so you can see every moving part. If you prefer Spring Initializr, generate a Maven project for Boot 4.1.2 with Java 25 and the Web and Actuator dependencies, then compare it to the file below. Boot 4 split its starters into narrower modules, so the web starter here is the MVC one. A pom copied from a Boot 3 tutorial will not resolve.
mkdir -p stock-service && cd stock-service
mkdir -p src/main/java/dev/techlab/stock src/test/java/dev/techlab/stock src/main/resources
mkdir -p .vscode .cursor/rules .github/workflows scripts
git init
<?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>4.1.2</version>
<relativePath/>
</parent>
<groupId>dev.techlab</groupId>
<artifactId>stock-service</artifactId>
<version>0.1.0</version>
<name>stock-service</name>
<properties>
<java.version>25</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
The java.version property is what the Spring Boot parent turns into the compiler release. It is also what the Java language server reads when it decides which runtime a project needs, so it is the single line that ties the build, the terminal and both editors to JDK 25.
Step 5: Write the service code and run the first build
Three small classes and one config file make the app. The service uses a sealed result type so the controller can use pattern matching, which gives the language server something real to analyze in both editors.
package dev.techlab.stock;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class InventoryApplication {
public static void main(String[] args) {
SpringApplication.run(InventoryApplication.class, args);
}
}
package dev.techlab.stock;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;
import org.springframework.stereotype.Service;
@Service
public class StockService {
public record StockItem(String sku, int quantity) {}
public sealed interface AdjustResult {
record Updated(StockItem item) implements AdjustResult {}
record Rejected(String reason) implements AdjustResult {}
}
private final Map<String, Integer> levels = new ConcurrentHashMap<>();
public StockItem find(String sku) {
return new StockItem(sku, levels.getOrDefault(sku, 0));
}
public AdjustResult adjust(String sku, int delta) {
var rejection = new AtomicReference<String>();
var next = levels.compute(sku, (key, current) -> {
int base = current == null ? 0 : current;
if (base + delta < 0) {
rejection.set("stock cannot go below zero for " + key);
return current;
}
return base + delta;
});
if (rejection.get() != null) {
return new AdjustResult.Rejected(rejection.get());
}
return new AdjustResult.Updated(new StockItem(sku, next));
}
}
package dev.techlab.stock;
import java.util.Map;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
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;
import dev.techlab.stock.StockService.AdjustResult;
import dev.techlab.stock.StockService.StockItem;
@RestController
@RequestMapping("/stock")
public class StockController {
public record AdjustRequest(int delta) {}
private final StockService service;
public StockController(StockService service) {
this.service = service;
}
@GetMapping("/{sku}")
public StockItem get(@PathVariable String sku) {
return service.find(sku);
}
@PostMapping("/{sku}/adjust")
public ResponseEntity<?> adjust(@PathVariable String sku, @RequestBody AdjustRequest request) {
return switch (service.adjust(sku, request.delta())) {
case AdjustResult.Updated updated -> ResponseEntity.ok(updated.item());
case AdjustResult.Rejected rejected ->
ResponseEntity.unprocessableEntity().body(Map.of("error", rejected.reason()));
};
}
}
spring.application.name=stock-service
server.port=8080
management.endpoints.web.exposure.include=health,info
Save the three Java files under src/main/java/dev/techlab/stock and the properties file under src/main/resources/application.properties. Now add the tests so the first build has something to prove.
package dev.techlab.stock;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
import dev.techlab.stock.StockService.AdjustResult;
class StockServiceTest {
private final StockService service = new StockService();
@Test
void unknownSkuStartsAtZero() {
assertThat(service.find("A-100").quantity()).isZero();
}
@Test
void adjustAddsStock() {
var result = service.adjust("A-100", 25);
assertThat(result).isInstanceOf(AdjustResult.Updated.class);
assertThat(service.find("A-100").quantity()).isEqualTo(25);
}
@Test
void adjustRejectsNegativeStock() {
service.adjust("A-100", 5);
var result = service.adjust("A-100", -6);
assertThat(result).isInstanceOf(AdjustResult.Rejected.class);
assertThat(service.find("A-100").quantity()).isEqualTo(5);
}
}
package dev.techlab.stock;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class InventoryApplicationTests {
@Test
void contextLoads() {
}
}
mvn -B -ntp verify
[INFO] Running dev.techlab.stock.InventoryApplicationTests
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO] Running dev.techlab.stock.StockServiceTest
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0
[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS
If the build passes in the terminal, any later red squiggle in an editor is an editor problem, not a code problem. That distinction saves hours. Commit now so you have a clean baseline to diff against.
Steps 6 and 7: Install the Java Extensions and Run the Gap Check
Step 6: Install the same extension set in both editors
The extension set is the same list in both editors. The Red Hat extension provides the language server, and Microsoft’s Extension Pack for Java bundles the debugger, test runner, Maven support and project manager around it. The Spring tooling comes from the VMware-published Spring Boot Extension Pack. The Extension Pack for Java listing on the Visual Studio Marketplace is free.
Write the list once, in scripts/java-extensions.txt, so both the installer and the checker read the same source of truth.
redhat.java
vscjava.vscode-java-debug
vscjava.vscode-java-test
vscjava.vscode-maven
vscjava.vscode-java-dependency
vmware.vscode-spring-boot
#!/usr/bin/env bash
# scripts/install-java-extensions.sh
set -uo pipefail
cd "$(dirname "$0")/.."
for editor in code cursor; do
if ! command -v "$editor" >/dev/null 2>&1; then
echo "SKIP $editor is not on PATH"
continue
fi
while read -r ext; do
[ -z "$ext" ] && continue
if "$editor" --install-extension "$ext" --force >/dev/null 2>&1; then
printf 'INSTALL %-8s %sn' "$editor" "$ext"
else
printf 'FAILED %-8s %sn' "$editor" "$ext"
fi
done < scripts/java-extensions.txt
done
Run it with bash scripts/install-java-extensions.sh. A FAILED line is not a bug in the script. It is the most useful signal in this whole tutorial, because it means that extension ID could not be fetched by that editor’s registry.
Installing extensions also raises a trust question. Language servers and debuggers execute code from your workspace, and an agent-driven editor can trigger that quickly. The Cursor vs VS Code workspace trust audit walks through what each editor runs on open, which is worth reading before you point either one at a repository you did not write.
Step 7: Write and run the gap check
Some extensions published by Microsoft are licensed for Microsoft’s own VS Code builds only, and Pylance is the best-known example. Java’s picture is less clear, which is exactly why we measure instead of assuming. This script compares each editor’s installed extensions to the required list and exits non-zero if anything is missing, so it can gate CI or a pre-commit hook later.
#!/usr/bin/env bash
# scripts/java-gap-check.sh
set -uo pipefail
cd "$(dirname "$0")/.."
status=0
for editor in code cursor; do
if ! command -v "$editor" >/dev/null 2>&1; then
echo "SKIP $editor is not on PATH"
continue
fi
installed="$(mktemp)"
"$editor" --list-extensions | tr 'A-Z' 'a-z' | sort > "$installed"
while read -r ext; do
[ -z "$ext" ] && continue
if grep -qx "$ext" "$installed"; then
printf 'OK %-8s %sn' "$editor" "$ext"
else
printf 'MISSING %-8s %sn' "$editor" "$ext"
status=1
fi
done < scripts/java-extensions.txt
rm -f "$installed"
done
exit "$status"
bash scripts/java-gap-check.sh
OK code redhat.java
OK code vscjava.vscode-java-debug
OK code vscjava.vscode-java-test
OK code vscjava.vscode-maven
OK code vscjava.vscode-java-dependency
OK code vmware.vscode-spring-boot
OK cursor redhat.java
OK cursor vscjava.vscode-java-debug
MISSING cursor vscjava.vscode-java-test
OK cursor vscjava.vscode-maven
OK cursor vscjava.vscode-java-dependency
OK cursor vmware.vscode-spring-boot
The sample output above is illustrative. Your run decides which row, if any, says MISSING. When one does, you have three options: find a replacement extension in the registry that editor uses, sideload a downloaded VSIX with cursor --install-extension path/to/file.vsix, or accept the gap and document it in the repository README so teammates know which editor gets the full experience.
Steps 8 and 9: Point Both Editors at JDK 25 and Share Workspace Settings
Step 8: Set the JDK in each editor’s user settings
Runtime paths are machine-specific, so they belong in user settings and never in the repository. Each editor has its own user settings file, and you need to edit both. The locations differ by operating system.
- Linux:
~/.config/Code/User/settings.jsonfor VS Code and~/.config/Cursor/User/settings.jsonfor Cursor. - macOS:
~/Library/Application Support/Code/User/settings.jsonand~/Library/Application Support/Cursor/User/settings.json. - Windows:
%APPDATA%CodeUsersettings.jsonand%APPDATA%CursorUsersettings.json.
Add the same two keys to both files. Replace the path with the output of echo $JAVA_HOME from step 1. The runtime name must be JavaSE-25 so the language server can map it to the java.version property in the pom.
{
"java.jdt.ls.java.home": "/home/you/.sdkman/candidates/java/current",
"java.configuration.runtimes": [
{
"name": "JavaSE-25",
"path": "/home/you/.sdkman/candidates/java/current",
"default": true
}
]
}
Reload each editor after saving. Open the Command Palette and run Java: Configure Java Runtime in each one. The panel should list JavaSE-25 as the default and show your path. If it lists an older JDK as the project runtime, the usual cause is a missing default flag or a typo in the runtime name.
Step 9: Commit shared workspace settings
Everything that is not machine-specific goes into .vscode/settings.json inside the repository. Cursor reads the same folder, so one file configures both editors. This is also the right place to tune how the language server treats Maven. If your real project is a multi-module build rather than this single service, the Cursor vs VS Code monorepo setup shows how to keep indexing fast across many modules.
{
"java.configuration.updateBuildConfiguration": "automatic",
"java.import.maven.enabled": true,
"java.compile.nullAnalysis.mode": "automatic",
"java.saveActions.organizeImports": true,
"java.jdt.ls.vmargs": "-XX:+UseParallelGC -Xmx2G -Xms256m",
"editor.formatOnSave": true,
"files.exclude": {
"**/.classpath": true,
"**/.project": true,
"**/.settings": true,
"**/.factorypath": true
},
"search.exclude": {
"**/target": true
}
}
{
"recommendations": [
"redhat.java",
"vscjava.vscode-java-debug",
"vscjava.vscode-java-test",
"vscjava.vscode-maven",
"vscjava.vscode-java-dependency",
"vmware.vscode-spring-boot"
]
}
Save the second block as .vscode/extensions.json. Both editors offer to install these when someone opens the repo for the first time. It duplicates scripts/java-extensions.txt on purpose. The text file drives automation, while this file drives the in-editor prompt, and the gap check in CI catches the two lists if they drift apart.
Open the project in each editor now. After a short import, the Java Projects view should show stock-service with its Maven dependencies. If you see a status item that says the language server is still importing, wait for it to finish before judging anything.
Steps 10 and 11: Debug the App and Run Tests from Either Editor
Step 10: Add launch and task configurations
Debug configs are plain JSON, and they are identical in both editors. The Java debugger extension handles launch and attach, so one file covers both. The deeper tour of breakpoints, logpoints and tasks lives in the Cursor vs VS Code debugging guide. This project only needs two configurations.
{
"version": "0.2.0",
"configurations": [
{
"type": "java",
"name": "Run stock-service",
"request": "launch",
"mainClass": "dev.techlab.stock.InventoryApplication",
"projectName": "stock-service",
"console": "integratedTerminal"
},
{
"type": "java",
"name": "Attach to port 5005",
"request": "attach",
"hostName": "localhost",
"port": 5005
}
]
}
{
"version": "2.0.0",
"tasks": [
{
"label": "verify",
"type": "shell",
"command": "bash scripts/verify.sh",
"group": { "kind": "test", "isDefault": true },
"problemMatcher": []
},
{
"label": "run with debug port",
"type": "shell",
"command": "mvn -B -ntp spring-boot:run -Dspring-boot.run.jvmArguments="-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005"",
"isBackground": true,
"problemMatcher": []
}
]
}
Set a breakpoint on the first line of StockService.adjust, pick Run stock-service in the Run and Debug view, and start it. In another terminal, send a request.
curl -s -X POST localhost:8080/stock/A-100/adjust
-H 'Content-Type: application/json' -d '{"delta": 25}'
The editor should stop on your breakpoint with the arguments visible in the Variables panel. Continue, and the shell prints the updated item.
{"sku":"A-100","quantity":25}
Now trigger the rejection path.
curl -s -i -X POST localhost:8080/stock/A-100/adjust
-H 'Content-Type: application/json' -d '{"delta": -30}'
HTTP/1.1 422
Content-Type: application/json
{"error":"stock cannot go below zero for A-100"}
The second configuration is for the case where the app already runs elsewhere, such as in a terminal task or a container. Start it with the run with debug port task, then attach on port 5005. Spring Boot’s Maven plugin passes the JVM arguments through, and both editors attach the same way.
Step 11: Run the tests from the Testing view and the terminal
Open the Testing view in each editor. With the Java test extension installed, the three tests in StockServiceTest and the context test appear in a tree, and you can run or debug any one of them from the gutter. If the tree is empty in one editor and populated in the other, your gap check from step 7 already told you why.
Whatever the editor does, the terminal is the source of truth. Run a single class to confirm it matches.
mvn -B -ntp -Dtest=StockServiceTest test
[INFO] Running dev.techlab.stock.StockServiceTest
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS
Break a test on purpose, for example by changing the expected quantity from 25 to 26, and run it from the gutter in both editors. The failure message should match. If it does not, the editor is running a stale compile, and the clean-workspace command in the troubleshooting section is your fix.
Step 12: Give Each Editor’s AI the Same Java Rules
This is the step where Cursor and VS Code genuinely diverge, because the assistants read different files. Cursor reads rules from .cursor/rules. GitHub Copilot in VS Code reads .github/copilot-instructions.md. If you write only one, one assistant will happily generate Java 8 style code with field injection and no tests.
---
description: Java 25 and Spring Boot conventions for stock-service
globs: src/**/*.java
alwaysApply: false
---
- Target Java 25. Prefer records, sealed interfaces and pattern matching over classic DTOs.
- Use constructor injection. Never use field injection.
- Add or update a JUnit 5 test with AssertJ for every behavior change.
- Do not add new dependencies to pom.xml without saying why.
- Before you say a task is done, run: bash scripts/verify.sh
- Never edit files under target/.
# Copilot instructions for stock-service
- Target Java 25. Prefer records, sealed interfaces and pattern matching.
- Use constructor injection only.
- Every behavior change needs a JUnit 5 test using AssertJ.
- Do not add dependencies to pom.xml without explaining the reason.
- Before declaring a task finished, run `bash scripts/verify.sh` and report the result.
- Never edit files under target/.
Save them as .cursor/rules/java.mdc and .github/copilot-instructions.md. Keep the two bullet lists in sync by hand or by script. The Cursor vs VS Code on one repo guide shows a drift check that automates the comparison.
The last rule in each file is the important one. It makes the agent run the same gate script you do, which turns an opinionated suggestion engine into a loop with an objective pass or fail. It also controls cost. Since the October 12 plan change, third-party models on Cursor’s Premium and higher plans are billed at API list prices from prepaid top-ups, so an agent that retries a broken Maven build ten times costs real money. Set a spending cap in your Cursor account settings and keep the verify script quiet and fast so the agent does not need many iterations.
Add a .cursorignore file so Cursor does not index build output.
target/
*.log
.env
VS Code’s Copilot has its own exclusion controls in its settings, and they are configured per account or organization rather than from a file in the repo. That is a real asymmetry, and it is the main reason to write down in your README which editor an agent session ran in.
Step 13: Add a Gate Script and a CI Workflow
Everything so far lives on your machine. The gate script puts the checks in one command, and the CI workflow runs the same command on every push. Start with the toolchain check, which fails fast when someone is on the wrong JDK.
#!/usr/bin/env bash
# scripts/check-java-toolchain.sh
set -euo pipefail
want=25
major="$(java -version 2>&1 | awk -F'[".]' '/version/ {print $2; exit}')"
if [ "$major" != "$want" ]; then
echo "FAIL java major version is $major, expected $want"
exit 1
fi
if ! mvn -v | grep -q "Java version: $want"; then
echo "FAIL Maven is not running on Java $want"
mvn -v | head -n 3
exit 1
fi
echo "OK java $major and Maven agree"
#!/usr/bin/env bash
# scripts/verify.sh
set -euo pipefail
cd "$(dirname "$0")/.."
bash scripts/check-java-toolchain.sh
mvn -B -ntp verify
if [ "${SKIP_EDITOR_CHECK:-0}" != "1" ]; then
bash scripts/java-gap-check.sh || echo "WARN editor extension gap, see output above"
fi
echo "VERIFY OK"
name: ci
on:
push:
pull_request:
jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '25'
cache: maven
- name: Verify
run: SKIP_EDITOR_CHECK=1 bash scripts/verify.sh
Save the last block as .github/workflows/ci.yml. The action versions shown are widely available, so bump them to the current major releases when you adopt this. CI skips the editor check because runners have neither editor installed.
Before you commit, run a secrets scan over what you are about to push. Agent-driven editors read broad context, and a stray credential in a config file is easy to miss. The Cursor vs VS Code secrets guard builds a canary-based test for exactly that. Then run the gate once locally.
bash scripts/verify.sh
OK java 25 and Maven agree
[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS
OK code redhat.java
...
VERIFY OK
Where Cursor and VS Code Differ for Java Work
After thirteen steps the pattern is clear. The Java mechanics are the same in both editors, and the differences cluster in four places. The table summarizes them from a Java developer’s point of view. These are practical observations from this setup, not benchmarks.
| Area | VS Code | Cursor |
|---|---|---|
| Language server | Red Hat extension running the Eclipse JDT Language Server | Same server and extension ID, installed through Cursor’s own registry |
| Debugger and tests | Microsoft Java debug and test extensions from the Marketplace | Same IDs, so run the gap check to confirm each one installs |
| Extension source | Microsoft Visual Studio Marketplace | Cursor’s registry, typically Open VSX based, with a different publisher mix |
| Shared config | .vscode/settings.json, launch.json, tasks.json |
Reads the same .vscode folder |
| AI instructions | .github/copilot-instructions.md for Copilot |
.cursor/rules/*.mdc plus Agent and Tab |
| AI billing | Copilot is a separate subscription-backed service | Plan-based, with third-party models billed at API list price on Premium and above |
| Command line | code |
cursor |
| Release cadence | Stable and Insiders channels | Frequent changelog updates, several per week lately |
If the AI row is the reason you are reading, remember that neither editor makes your Java better by itself. What makes the difference is a gate script, a rules file and a test suite. A model with no tests to run will invent confident code, and a model with a fast verify script will correct itself.
Cursor’s cloud agents and live app building, announced November 5, are aimed at web apps that ship with databases and authentication. They do not replace the local Maven loop you built here, so treat them as an addition for prototypes, not a different way to run your Spring Boot service.
8 Common Pitfalls With Java in Cursor and VS Code
- Two JDKs on one machine. The terminal uses one, the language server uses another, and builds pass or fail depending on where you click. Always run
mvn -vand compare it to the Java Configure panel. The toolchain script in step 13 exists for this reason. - Committing absolute JDK paths. A line like
/home/you/.sdkman/...in.vscode/settings.jsonbreaks every teammate who does not share your home directory. Keep paths in user settings. - Using a Boot 3 starter name in a Boot 4 pom. Boot 4 split its starters, so old copy-paste dependency blocks fail to resolve or pull in the wrong modules. When in doubt, generate a reference pom from Spring Initializr and diff it.
- Adopting Maven 4 early. The project labels it a release candidate and warns against production use. A new tool version is not the fix for a language server bug.
- Treating a Boot release candidate as production. Spring Boot 4.2.0-RC2 is a prerelease. Run it in a branch, and ship 4.1.2 until 4.2 goes final.
- Opening the same repo in both editors at once. Each editor starts its own language server, and both compile into
target/. You pay double memory and risk a half-written class directory. Close one before working in the other. - Letting the agent index build output. Without ignore rules, an AI assistant spends context on compiled classes and logs. Use
.cursorignorein Cursor and keeptarget/insearch.exclude. - Mixing runtime versions with preview libraries. If you add JavaFX or another runtime-coupled library, match its line to your JDK. Oracle’s JavaFX 25 release notes are the place to confirm the pairing with JDK 25.
A broader set of editor and tooling walkthroughs sits in the software coverage hub if you want to apply the same gap-check habit to another language.
Troubleshooting: 10 Fixes for Java Editor Problems
- Java runtime could not be located. The language server cannot find a JDK 21 or newer. Set
java.jdt.ls.java.homein user settings to your JDK 25 path and reload the window. - Project shows as a plain folder with no Maven import. You opened a parent directory. Use File, Open Folder on the directory that contains
pom.xml, then runJava: Clean Java Language Server Workspace. - An extension is missing in Cursor. Run
bash scripts/java-gap-check.sh. If the ID is missing, search Cursor’s extension panel for a close alternative, or sideload a VSIX withcursor --install-extension. - Error: release version 25 not supported. Maven is running on an older JDK. Run
mvn -v, fixJAVA_HOME, and restart the shell and both editors so they inherit it. - Debugger says main class not found. The
projectNameinlaunch.jsonmust match the Maven artifact ID, which is stock-service here. Runmvn compileonce so classes exist. - Breakpoints never hit when using spring-boot:run. Plain
spring-boot:rundoes not open a debug port. Use therun with debug porttask and the attach configuration. - Red squiggles after changing the pom. Run
Java: Reload Projects. If errors remain, delete.settings,.classpathand.project, then clean the language server workspace. - Language server is slow or restarts. It is probably short on memory. Raise
-Xmxinjava.jdt.ls.vmargs, close the other editor, and excludetargetfrom search. - Port already in use. A previous debug session is still running. Stop it from the Run and Debug toolbar, or change
server.portinapplication.properties. - cursor or code command not found. The shell launcher was never installed. Run the Shell Command install from each editor’s Command Palette, then open a new terminal.
If none of these match, capture the language server log from the Output panel in the failing editor, then reproduce the same action in the other editor. A problem that appears in only one points at packaging, and a problem that appears in both points at your project.
Advanced Tips for Java Teams Using Both Editors
- Speed up builds with the Maven Daemon. The Apache Maven download page lists Maven Daemon 1.0.6 as the current daemon release, according to the page we checked. Running
mvndin your tasks cuts JVM startup on repeat builds. - Try an incremental build cache. Apache Maven has a Maven Build Cache extension at version 1.3.0. It targets local and remote incremental caching and is worth a branch experiment on larger projects.
- Test Boot 4.2 safely. The first 4.2 release candidate, published October 23, added an Ubuntu 26.04-based default Buildpacks builder and executable-jar startup improvements. Create a branch, bump the parent version, run the gate, and compare results.
- Pin your tools in the repo. Add a Maven wrapper with
mvn wrapper:wrapperand commit the pinned version you tested. Then the build no longer depends on whatever Maven each laptop has. - Add a pre-push hook. Call
SKIP_EDITOR_CHECK=1 bash scripts/verify.shfrom.git/hooks/pre-pushso a failing build never leaves the machine. - Wire in tools through MCP carefully. Both editors can call external tools from an agent session. The Cursor MCP vs VS Code MCP walkthrough explains the config differences, and the audit habit from this tutorial applies there too.
A last tip on discipline. Once a month, run the gap check on a clean profile of each editor, not your daily one. Daily profiles accumulate extensions that mask what a new teammate would actually see.
The Complete Working Project
Here is the final layout. Every file appeared in a step above, and the whole tree is about 15 files.
stock-service/
pom.xml
.cursorignore
.cursor/rules/java.mdc
.github/copilot-instructions.md
.github/workflows/ci.yml
.vscode/settings.json
.vscode/launch.json
.vscode/tasks.json
.vscode/extensions.json
scripts/java-extensions.txt
scripts/install-java-extensions.sh
scripts/java-gap-check.sh
scripts/check-java-toolchain.sh
scripts/verify.sh
src/main/resources/application.properties
src/main/java/dev/techlab/stock/InventoryApplication.java
src/main/java/dev/techlab/stock/StockService.java
src/main/java/dev/techlab/stock/StockController.java
src/test/java/dev/techlab/stock/StockServiceTest.java
src/test/java/dev/techlab/stock/InventoryApplicationTests.java
To prove it all works end to end on a fresh clone, run these four commands in order. The first installs the extensions in whichever editors are on the PATH, the second runs the gate, the third starts the app and the fourth hits the health endpoint.
bash scripts/install-java-extensions.sh
bash scripts/verify.sh
mvn -B -ntp spring-boot:run &
curl -s localhost:8080/actuator/health
{"status":"UP"}
That health response is the finish line. You now have one repository that two editors, a CI runner and two AI assistants all treat the same way. When Cursor ships its next update or VS Code finishes retiring the classic UI, rerun the gap check and the gate, and you will know within a minute whether anything in the Java path moved.
Frequently Asked Questions
Cursor vs VSCode: which is better for Java?
Neither wins on Java mechanics, because both run the same language server and the same debug protocol. VS Code has the edge if you want Microsoft’s Java extensions from the first-party Marketplace and GitHub Copilot integration. Cursor has the edge if you want its agent workflow and rules system. Run the gap check on your machine and choose based on what installs.
Does Cursor support the Red Hat Java extension?
The Cursor documentation and release notes we reviewed do not state whether the Red Hat Java extension or Microsoft’s Extension Pack for Java ships in the box. Do not assume either way. Step 7 tests it directly with cursor --list-extensions and gives you a pass or fail per extension.
Which JDK should I use in 2026?
Use JDK 25. Oracle describes it as a long-term-support release with Premier Support planned through September 2030 and Extended Support through September 2033. Any OpenJDK 25 distribution works for this setup, including Temurin.
Should I use Spring Boot 4.2.0-RC2 for a new project?
Not for production. Release candidate 2 arrived on November 6, 2026. The stable choice today is Spring Boot 4.1.2, a maintenance release. Try 4.2 in a separate branch and promote it after the final release.
Do I need a paid Cursor plan to follow this tutorial?
No. Steps 1 through 11 and step 13 need no AI at all, and step 12 only writes a rules file. Paid plans start to matter when you want agents on third-party models, which on Premium and above are billed at API list prices from prepaid top-ups. Check Cursor’s Models and Pricing page before buying.
Can I use both editors on the same project?
Yes, with two caveats. Close one editor before using the other, since both write to the target directory. And keep the AI rules files in sync, because each assistant reads a different file.
Is Maven 4 ready for this setup?
No. The Apache Maven project calls Maven 4 a release candidate and says it is not safe for production while it remains under development. Stay on Maven 3.x and revisit when Maven 4 reaches a final release.
Will this work with Gradle instead of Maven?
The structure carries over. The Java debugger, test runner and language server do not care which build tool produced the classpath. Swap the pom for a Gradle build, replace the Maven extension with a Gradle one in scripts/java-extensions.txt, and change the verify script to call Gradle. Re-run the gap check to confirm the Gradle extension installs in both editors.