See 2024 JetBrains Java Developer Survey and Spring Boot Statistics for context on the Java ecosystem.
Note: The Community Toolkit provides Java/Spring Boot hosting. Aspire 13 elevated Python and JavaScript to first-class citizens with polyglot connection properties including JDBC format. This proposal outlines bringing Java to the same first-class status.
Java is the second most popular enterprise language after JavaScript/TypeScript, with Spring Boot dominating the web framework space (~70% of Java web applications). With Aspire 13's rebrand from ".NET Aspire" to "Aspire" and first-class Python/JavaScript support, Java is the natural next step. This document outlines a Spring Boot-centric approach while maintaining flexibility for other Java frameworks.
There are several ways to launch Java applications:
- Maven:
mvn spring-boot:runormvn exec:java - Gradle:
./gradlew bootRunor./gradlew run - Executable JAR:
java -jar application.jar - Exploded application:
java -cp "lib/*" com.example.Main
Aspire should support all methods, with the ability to configure JVM arguments, system properties, and environment variables.
Aspire should auto-detect the build system:
| File Present | Build System | Default Command |
|---|---|---|
pom.xml |
Maven | mvn spring-boot:run |
build.gradle or build.gradle.kts |
Gradle | ./gradlew bootRun |
*.jar |
Pre-built | java -jar <app>.jar |
Tip
On Windows, use mvnw.cmd and gradlew.bat if wrapper scripts are present. This ensures consistent build tool versions across teams.
Spring Boot should be the primary focus given its market dominance. Aspire integration should leverage existing Spring Boot capabilities:
Spring Boot profiles allow environment-specific configuration. Aspire should:
- Set
SPRING_PROFILES_ACTIVE=aspire(or configurable profile name) - Allow developers to define Aspire-specific configuration in
application-aspire.yml
# application-aspire.yml
spring:
datasource:
url: ${CATALOG_JDBC}
username: ${CATALOG_USERNAME}
password: ${CATALOG_PASSWORD}
data:
redis:
url: ${REDIS_URL}Spring Boot's auto-configuration automatically configures beans based on classpath and properties. Aspire should:
- Provide a
spring-boot-starter-aspirethat auto-configures:- OpenTelemetry instrumentation
- Service discovery
- Health check endpoints
- Connection string parsing
Spring Boot Actuator provides production-ready features. Aspire should leverage:
| Actuator Endpoint | Aspire Use Case |
|---|---|
/actuator/health |
Health checks in dashboard |
/actuator/info |
Service metadata |
/actuator/metrics |
Metrics collection |
/actuator/loggers |
Dynamic log level changes |
/actuator/env |
Environment inspection |
Spring Boot DevTools provides:
- Automatic restart on code changes
- LiveReload support
- Development-time property defaults
Aspire should detect and support DevTools for local development scenarios.
While Spring Boot is the priority, Aspire should support:
| Framework | Priority | Notes |
|---|---|---|
| Spring Boot | P0 | Primary target |
| Quarkus | P1 | Growing in cloud-native space, has dev mode |
| Micronaut | P2 | Compile-time DI, fast startup |
| Jakarta EE | P3 | Enterprise deployments |
| Plain Java | P3 | Console apps, utilities |
Like Python frameworks, Java web applications have distinct modes:
| Mode | Spring Boot | Quarkus |
|---|---|---|
| Development | mvn spring-boot:run |
mvn quarkus:dev |
| Production | java -jar app.jar |
java -jar app-runner.jar |
Aspire should:
- Default to development mode for local runs
- Support production mode configuration
- Handle graceful transitions between modes
Spring Boot supports multiple embedded servers:
- Tomcat (default)
- Jetty
- Undertow
- Netty (for reactive applications)
Aspire should be agnostic to the embedded server choice.
Aspire 13 introduced polyglot connection properties that expose database connections in multiple formats:
| Format | Example | Use Case |
|---|---|---|
| JDBC | jdbc:postgresql://host:port/dbname |
Java applications |
| URI | postgresql://user:pass@host:port/dbname |
Python, Node.js |
| Individual properties | DB_HOST, DB_PORT, DB_USERNAME, DB_PASSWORD, DB_DATABASENAME |
Any language |
Note: JDBC connection strings do not include credentials for security best practices. Username and password are provided as separate environment variables.
var postgres = builder.AddPostgres("postgres")
.AddDatabase("catalog");
builder.AddSpringApp("catalog-api", "../CatalogApi")
.WithReference(postgres); // Provides JDBC URL + credentialsThe Java application receives environment variables using the resource name:
CATALOG_JDBC=jdbc:postgresql://localhost:5432/catalog
CATALOG_USERNAME=postgres
CATALOG_PASSWORD=...
This can be used directly in Spring Boot:
# application-aspire.yml
spring:
datasource:
url: ${CATALOG_JDBC}
username: ${CATALOG_USERNAME}
password: ${CATALOG_PASSWORD}Or programmatically:
String jdbcUrl = System.getenv("CATALOG_JDBC");
String username = System.getenv("CATALOG_USERNAME");
String password = System.getenv("CATALOG_PASSWORD");| Database | JDBC Driver | Spring Boot Starter |
|---|---|---|
| PostgreSQL | postgresql |
spring-boot-starter-data-jpa |
| MySQL/MariaDB | mysql |
spring-boot-starter-data-jpa |
| SQL Server | mssql-jdbc |
spring-boot-starter-data-jpa |
| MongoDB | mongodb-driver |
spring-boot-starter-data-mongodb |
| Redis | jedis/lettuce |
spring-boot-starter-data-redis |
For pom.xml-based projects:
- Detect Maven wrapper (
mvnw) presence - Run
./mvnw dependency:resolveto download dependencies - Run
./mvnw spring-boot:runor configured goal
<!-- Aspire could inject a profile -->
<profile>
<id>aspire</id>
<dependencies>
<dependency>
<groupId>io.aspire</groupId>
<artifactId>spring-boot-starter-aspire</artifactId>
</dependency>
</dependencies>
</profile>For build.gradle-based projects:
- Detect Gradle wrapper (
gradlew) presence - Run
./gradlew dependenciesto resolve - Run
./gradlew bootRunor configured task
// build.gradle.kts - Aspire could add this
dependencies {
if (System.getenv("ASPIRE_ENABLED") != null) {
implementation("io.aspire:spring-boot-starter-aspire")
}
}For executable JARs:
- Locate the JAR file (configurable path)
- Run with
java -jar <app>.jar - Pass JVM arguments and environment variables
Aspire should detect and respect:
JAVA_HOMEenvironment variable.java-versionfile (SDKMAN convention).tool-versionsfile (asdf convention)pom.xml<maven.compiler.source>propertybuild.gradlesourceCompatibilitysetting
| LTS Version | Support Status | Notes |
|---|---|---|
| Java 8 | Maintenance | Legacy, declining usage |
| Java 11 | Supported | Enterprise standard |
| Java 17 | Recommended | Current LTS, Spring Boot 3 minimum |
| Java 21 | Supported | Latest LTS, virtual threads |
Warning
Spring Boot 3.x requires Java 17+. Spring Boot 2.x (maintenance mode) supports Java 8+. Aspire should validate compatibility and warn appropriately.
Java applications support remote debugging via JDWP. Aspire should:
-
Enable debug agent in development mode:
-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 -
Expose debug port in dashboard
-
Provide IDE connection instructions
Spring Boot DevTools provides automatic restart. For frameworks without DevTools:
- JRebel (commercial)
- DCEVM + HotswapAgent
- Spring Loaded (deprecated)
Aspire should document integration with these tools but not require them.
Java applications should receive SIGTERM for graceful shutdown. Spring Boot handles this via:
@Bean
public GracefulShutdown gracefulShutdown() {
return new GracefulShutdown();
}Or via server.shutdown=graceful in application.properties.
Aspire should:
- Send
SIGTERMfirst - Wait configurable timeout (default 30s)
- Send
SIGKILLif process doesn't terminate
Aspire injects standard OpenTelemetry environment variables that Java applications can use directly:
| Variable | Description | Default |
|---|---|---|
OTEL_EXPORTER_OTLP_ENDPOINT |
OTLP collector endpoint | http://localhost:4318 |
OTEL_SERVICE_NAME |
Service name for telemetry | Resource name |
OTEL_TRACES_EXPORTER |
Traces exporter | otlp |
OTEL_METRICS_EXPORTER |
Metrics exporter | otlp |
OTEL_LOGS_EXPORTER |
Logs exporter | otlp |
OTEL_RESOURCE_ATTRIBUTES |
Additional resource attributes | - |
This means Java applications with OpenTelemetry configured will automatically export telemetry to Aspire's dashboard without additional configuration.
The OpenTelemetry Java Agent provides automatic instrumentation for 150+ libraries including Spring, JDBC, HTTP clients, and messaging systems.
java -javaagent:opentelemetry-javaagent.jar -jar application.jarThe agent automatically reads the OTEL_* environment variables injected by Aspire. No code changes required.
Aspire should:
- Automatically download and cache the OTEL Java agent
- Inject the
-javaagentJVM argument - Set all required
OTEL_*environment variables
The OpenTelemetry Spring Boot Starter (stable since 2024) provides native Spring Boot integration.
<dependency>
<groupId>io.opentelemetry.instrumentation</groupId>
<artifactId>opentelemetry-spring-boot-starter</artifactId>
</dependency>The starter automatically picks up OTEL_* environment variables. It also supports Spring-style configuration:
# application.yml - these are read automatically from OTEL_* env vars
otel:
exporter:
otlp:
endpoint: ${OTEL_EXPORTER_OTLP_ENDPOINT}
service:
name: ${OTEL_SERVICE_NAME}Advantages over Java agent:
- Lighter weight (no bytecode manipulation)
- Better IDE debugging support
- Spring-native configuration
- Easier custom instrumentation
Spring Boot 3.x includes native observability via Micrometer, which can export to OTLP:
# application.yml
management:
otlp:
tracing:
endpoint: ${OTEL_EXPORTER_OTLP_ENDPOINT}/v1/traces
metrics:
export:
endpoint: ${OTEL_EXPORTER_OTLP_ENDPOINT}/v1/metrics
tracing:
sampling:
probability: 1.0Note: Spring Boot's native OpenTelemetry auto-configuration uses
spring.application.nameas the service name by default, notOTEL_SERVICE_NAME. The OpenTelemetry Spring Boot Starter respectsOTEL_SERVICE_NAME.
For first-class Java support, Aspire should:
- Detect OpenTelemetry setup - Check if the project has OTEL dependencies
- If no OTEL detected - Automatically attach the Java agent
- If OTEL starter detected - Just inject environment variables (already works)
- If Micrometer OTLP detected - Inject environment variables (already works)
builder.AddSpringApp("catalog-api", "../CatalogApi")
.WithOpenTelemetry(); // Auto-detect and configure appropriately
// Or explicitly choose approach
builder.AddSpringApp("catalog-api", "../CatalogApi")
.WithOpenTelemetryAgent(); // Force Java agent
builder.AddSpringApp("catalog-api", "../CatalogApi")
.WithOpenTelemetry(agent: false); // Rely on app's own OTEL setupAll three approaches support log export to OTLP:
| Approach | Log Configuration |
|---|---|
| Java Agent | Automatic (captures SLF4J/Logback/Log4j2) |
| Spring Starter | Add opentelemetry-logback-appender dependency |
| Micrometer | Not supported natively, use starter or agent |
For the Spring Boot Starter with Logback:
<!-- logback-spring.xml -->
<configuration>
<appender name="OTEL" class="io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender"/>
<root level="INFO">
<appender-ref ref="OTEL"/>
<appender-ref ref="CONSOLE"/>
</root>
</configuration>Spring Boot Actuator with Micrometer automatically exports metrics when configured:
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
otlp:
metrics:
export:
enabled: true
endpoint: ${OTEL_EXPORTER_OTLP_ENDPOINT}/v1/metricsThe Java agent also captures JVM metrics automatically.
Aspire 13 introduced polyglot-friendly environment variables for service URLs:
services__basket-api__http__0=http://localhost:5001
services__basket-api__https__0=https://localhost:5002
# Simplified format (Aspire 13+)
BASKET_API_HTTP=http://localhost:5001
BASKET_API_HTTPS=https://localhost:5002
This simplifies Spring Boot configuration:
# application-aspire.yml
app:
services:
basket-api: ${BASKET_API_HTTP}
catalog-api: ${CATALOG_API_HTTP}For teams already using Spring Cloud, Aspire could provide a DiscoveryClient implementation:
// Aspire could provide a DiscoveryClient implementation
@Configuration
@ConditionalOnClass(DiscoveryClient.class)
public class AspireDiscoveryConfiguration {
@Bean
public DiscoveryClient aspireDiscoveryClient() {
return new AspireDiscoveryClient();
}
}This would allow:
@Autowired
private DiscoveryClient discoveryClient;
// Discovers "basket-api" service registered in Aspire
ServiceInstance instance = discoveryClient
.getInstances("basket-api")
.get(0);However, the simplified environment variable approach is recommended for most use cases.
var builder = DistributedApplication.CreateBuilder(args);
var postgres = builder.AddPostgres("postgres")
.AddDatabase("catalog");
builder.AddSpringApp("catalog-api", "../CatalogApi")
.WithReference(postgres)
.WithHttpEndpoint(port: 8080);
builder.Build().Run();builder.AddSpringApp("catalog-api", "../CatalogApi")
.WithMavenGoal("spring-boot:run")
.WithProfile("aspire")
.WithJvmArgs("-Xmx512m", "-Xms256m")
.WithReference(postgres);builder.AddJavaApp("worker", "./workers/processor.jar")
.WithJavaVersion(21)
.WithJvmArgs("-Xmx1g")
.WithEnvironment("WORKER_THREADS", "4");builder.AddSpringApp("api", "../ApiProject")
.WithGradleTask("bootRun")
.WithProfile("aspire");// Quarkus
builder.AddQuarkusApp("quarkus-api", "../QuarkusProject")
.WithDevMode();
// Plain Java
builder.AddJavaApp("processor", "../JavaProcessor")
.WithMainClass("com.example.Main")
.WithClasspath("lib/*", "classes");Core utilities for any Java application:
// Connection string parsing
String jdbcUrl = AspireConnectionStrings.getJdbcUrl("postgres");
Map<String, String> props = AspireConnectionStrings.parse("redis");
// Service discovery
String basketApiUrl = AspireServices.getUrl("basket-api");
// Environment helpers
boolean isDevelopment = AspireEnvironment.isDevelopment();Auto-configuration for Spring Boot:
// Just add the dependency - everything auto-configures
@SpringBootApplication
public class CatalogApiApplication {
public static void main(String[] args) {
SpringApplication.run(CatalogApiApplication.class, args);
}
}Auto-configured features:
- DataSource from
{RESOURCE}_JDBC,{RESOURCE}_USERNAME,{RESOURCE}_PASSWORD - Redis from
{RESOURCE}_URL - Health check endpoints
- Service discovery client
Note: OpenTelemetry configuration is not needed in this starter. Aspire injects standard
OTEL_*environment variables that the OpenTelemetry Spring Boot Starter or Java agent read automatically.
Quarkus extension with similar functionality.
The existing Community Toolkit integration should continue working. Proposed migration:
| Phase | Timeline | Changes |
|---|---|---|
| Phase 1 | - | Add spring-boot-starter-aspire |
| Phase 2 | - | Enhance AddSpringApp with auto-detection |
| Phase 3 | - | Automatic OTEL agent management |
| Phase 4 | - | First-party support consideration |
-
Should Aspire manage the JDK installation?
- Pro: Consistent environment
- Con: Complexity, existing tools (SDKMAN, Jabba) do this well
-
Should Aspire auto-attach the Java agent for OTEL?
- Pro: Zero-code telemetry for any Java app
- Con: Adds startup overhead, may conflict with existing OTEL setup
- Recommendation: Auto-detect and only attach if no OTEL dependencies found
-
How to handle multi-module Maven/Gradle projects?
- Detect module structure
- Allow specifying target module
-
Should we support GraalVM native images?
- Faster startup, lower memory
- Requires additional build configuration
-
What about Kotlin?
- Runs on JVM, same infrastructure
- Spring Boot fully supports Kotlin
- Should work out of the box
- Aspire 13 - What's New - Polyglot support, JDBC connection strings
- Aspire Community Toolkit - Java
- Spring Boot Documentation
- Spring Boot Profiles
- Spring Boot Actuator
- Spring Boot DevTools
- OpenTelemetry Java
- Quarkus Dev Mode
- Micronaut
This document should be reviewed by Java Champions and experts:
- Manfred Riem (Microsoft)
- Bruno Borges (Microsoft)
- Jonathan Giles (Microsoft)
Feedback welcome! Please comment or open issues for discussion.