Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright 2026 The Dapr Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
limitations under the License.
*/

package io.dapr.spring.boot.autoconfigure.actors;

import io.dapr.actors.runtime.ActorEventSubscription;
import io.dapr.actors.runtime.ActorRuntime;
import io.dapr.spring.boot.autoconfigure.client.DaprClientAutoConfiguration;
import io.dapr.spring.boot.properties.actors.DaprActorProperties;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;

/**
* Auto-configuration for Dapr actor hosting.
*/
@AutoConfiguration(after = DaprClientAutoConfiguration.class)
@ConditionalOnClass(ActorRuntime.class)
@EnableConfigurationProperties(DaprActorProperties.class)
public class DaprActorAutoConfiguration {

/**
* Starts the gRPC actor event stream after all application runners have registered their actor types.
*
* @param actorRuntime Dapr actor runtime.
* @param applicationContext Spring application context.
* @return The lifecycle that owns the event subscription.
*/
@Bean
@ConditionalOnProperty(prefix = "dapr.actors", name = "transport", havingValue = "grpc")
@ConditionalOnMissingBean(ActorEventSubscription.class)
DaprActorEventSubscriptionLifecycle daprActorEventSubscriptionLifecycle(
ActorRuntime actorRuntime, ConfigurableApplicationContext applicationContext) {
return new DaprActorEventSubscriptionLifecycle(actorRuntime, applicationContext);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Copyright 2026 The Dapr Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
limitations under the License.
*/

package io.dapr.spring.boot.autoconfigure.actors;

import io.dapr.actors.runtime.ActorEventSubscription;
import io.dapr.actors.runtime.ActorRuntime;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.ConfigurableApplicationContext;

/**
* Owns the gRPC actor event subscription for the lifetime of a Spring Boot application.
*/
final class DaprActorEventSubscriptionLifecycle
implements ApplicationListener<ApplicationReadyEvent>, DisposableBean {

static final String BEAN_NAME = "daprActorEventSubscription";

private final ActorRuntime actorRuntime;

private final ConfigurableApplicationContext applicationContext;

private ActorEventSubscription subscription;

DaprActorEventSubscriptionLifecycle(
ActorRuntime actorRuntime, ConfigurableApplicationContext applicationContext) {
this.actorRuntime = actorRuntime;
this.applicationContext = applicationContext;
}

@Override
public synchronized void onApplicationEvent(ApplicationReadyEvent event) {
if (event.getApplicationContext() != applicationContext || subscription != null) {
return;
}

ActorEventSubscription started = actorRuntime.subscribeActorEvents();
try {
// Register only after ApplicationReadyEvent so actor types registered by application runners are included.
applicationContext.getBeanFactory().registerSingleton(BEAN_NAME, started);
subscription = started;
} catch (RuntimeException e) {
started.close();
throw e;
}
}

@Override
public synchronized void destroy() {
if (subscription != null) {
subscription.close();
subscription = null;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
io.dapr.spring.boot.autoconfigure.client.DaprJackson3AutoConfiguration
io.dapr.spring.boot.autoconfigure.client.DaprClientAutoConfiguration
io.dapr.spring.boot.autoconfigure.actors.DaprActorAutoConfiguration
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright 2026 The Dapr Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
limitations under the License.
*/

package io.dapr.actors.runtime;

import java.time.Duration;

/** Test helper that creates an event subscription without opening a Dapr connection. */
public final class ActorEventSubscriptionTestFactory {

private ActorEventSubscriptionTestFactory() {
}

/**
* Creates a subscription that can be used to verify lifecycle management without starting its stream.
*
* @param actorRuntime Actor runtime used when the subscription is closed.
* @return An unstarted event subscription.
*/
public static ActorEventSubscription newUnstartedSubscription(ActorRuntime actorRuntime) {
return new ActorEventSubscription(actorRuntime, null, null, Duration.ZERO);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* Copyright 2026 The Dapr Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
limitations under the License.
*/

package io.dapr.spring.boot.autoconfigure.actors;

import io.dapr.actors.runtime.ActorEventSubscription;
import io.dapr.actors.runtime.ActorEventSubscriptionTestFactory;
import io.dapr.actors.runtime.ActorRuntime;
import io.dapr.actors.runtime.ActorRuntimeConfig;
import io.dapr.spring.boot.properties.actors.DaprActorProperties;
import org.junit.jupiter.api.Test;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.ConfigurableApplicationContext;

import java.time.Duration;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

class DaprActorAutoConfigurationTest {

private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(DaprActorAutoConfiguration.class));

@Test
void doesNotStartSubscriptionForDefaultHttpTransport() {
ActorRuntime actorRuntime = mock(ActorRuntime.class);

contextRunner.withBean(ActorRuntime.class, () -> actorRuntime).run(context -> {
assertThat(context.getBean(DaprActorProperties.class).getTransport())
.isEqualTo(DaprActorProperties.Transport.HTTP);
assertThat(context).doesNotHaveBean(DaprActorEventSubscriptionLifecycle.class);
assertThat(context).doesNotHaveBean(ActorEventSubscription.class);
});

verify(actorRuntime, never()).subscribeActorEvents();
}

@Test
void startsAndExposesSubscriptionWhenApplicationIsReady() {
ActorRuntime actorRuntime = mock(ActorRuntime.class);
ActorRuntimeConfig actorRuntimeConfig = mock(ActorRuntimeConfig.class);
when(actorRuntime.getConfig()).thenReturn(actorRuntimeConfig);
ActorEventSubscription subscription = ActorEventSubscriptionTestFactory.newUnstartedSubscription(actorRuntime);
when(actorRuntime.subscribeActorEvents()).thenReturn(subscription);

contextRunner.withPropertyValues("dapr.actors.transport=grpc")
.withBean(ActorRuntime.class, () -> actorRuntime)
.run(context -> {
assertThat(context).hasSingleBean(DaprActorEventSubscriptionLifecycle.class);
assertThat(context).doesNotHaveBean(ActorEventSubscription.class);
assertThat(context.getBean(DaprActorProperties.class).getTransport())
.isEqualTo(DaprActorProperties.Transport.GRPC);

ConfigurableApplicationContext applicationContext = context.getSourceApplicationContext();
context.publishEvent(new ApplicationReadyEvent(
new SpringApplication(), new String[0], applicationContext, Duration.ZERO));

assertThat(context).hasSingleBean(ActorEventSubscription.class);
assertThat(context.getBean(ActorEventSubscription.class)).isSameAs(subscription);
});

verify(actorRuntime).subscribeActorEvents();
assertThat(subscription.isRunning()).isFalse();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright 2026 The Dapr Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
limitations under the License.
*/

package io.dapr.spring.boot.properties.actors;

import org.springframework.boot.context.properties.ConfigurationProperties;

/**
* Configuration properties for hosting Dapr actors from a Spring Boot application.
*/
@ConfigurationProperties(prefix = "dapr.actors")
public class DaprActorProperties {

private Transport transport = Transport.HTTP;

/**
* Returns the transport used by the application to host actors.
*
* @return The actor hosting transport. Defaults to HTTP.
*/
public Transport getTransport() {
return transport;
}

/**
* Sets the transport used by the application to host actors.
*
* @param transport The actor hosting transport.
*/
public void setTransport(Transport transport) {
this.transport = transport;
}

/**
* Supported transports for hosting actors.
*/
public enum Transport {
/** Host callbacks through the existing HTTP app channel. */
HTTP,
/** Host callbacks through the app-initiated gRPC actor event stream. */
GRPC
}
}
21 changes: 21 additions & 0 deletions examples/src/main/java/io/dapr/examples/actors/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,27 @@ dapr run --resources-path ./components/actors --app-id demoactorservice -- java

The stream reconnects if Dapr restarts. For an actor service that is idle for long periods, enable gRPC keepalive with `-Ddapr.grpc.enable.keep.alive=true`.

### Hosting actors over gRPC with Spring Boot

The Dapr Spring Boot starter can start the actor event subscription after application startup, once actor types registered by application beans and runners are available. Set the transport property:

```properties
dapr.actors.transport=grpc
```

Register actor types before the application is ready, for example from an `ApplicationRunner`:

```java
@Bean
ApplicationRunner registerActors(ActorRuntime actorRuntime) {
return args -> actorRuntime.registerActor(DemoActorImpl.class);
}
```

When the application is ready, the starter exposes the running `ActorEventSubscription` as a bean and closes it with the application context. A health indicator can look it up through `ObjectProvider<ActorEventSubscription>` and check `isRunning()`.

The default transport remains HTTP. gRPC hosting requires Dapr to run without an HTTP app channel: omit `--app-port`, or configure `--app-protocol grpc` when an app port is present. If the Spring Boot application does not serve HTTP itself, set `spring.main.web-application-type=none` so it does not start an unrelated web server.

### Running the Actor client

The actor client is a simple java class with a main method that uses the Dapr Actor capabilities in order to create the actors and execute the different methods based on the Actor pattern.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Copyright 2026 The Dapr Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
limitations under the License.
*/

package io.dapr.it.actors;

import io.dapr.actors.ActorId;
import io.dapr.actors.client.ActorClient;
import io.dapr.actors.client.ActorProxy;
import io.dapr.actors.client.ActorProxyBuilder;
import io.dapr.it.actors.app.SpringBootGrpcActorService;
import io.dapr.it.containers.BaseContainerIT;
import io.dapr.testcontainers.DaprContainer;
import io.dapr.testcontainers.DaprProtocol;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

import java.util.UUID;

import static org.junit.jupiter.api.Assertions.assertEquals;

/** Verifies Spring Boot can host actors over the gRPC stream without an app port. */
public class SpringBootActorGrpcIT extends BaseContainerIT {

private static DaprContainer dapr;

private static ActorClient actorClient;

@BeforeAll
public static void start() throws Exception {
var pair = startActorHostAndAttach(
"springboot-actor-grpc-it",
SpringBootGrpcActorService.class,
() -> daprBuilder("springboot-actor-grpc-it")
.withAppProtocol(DaprProtocol.GRPC)
.withComponent(redisStateStore(STATE_STORE_NAME)));
dapr = pair.dapr();
actorClient = newActorClient(dapr);
waitForActorsReady(dapr);
}

@Test
public void invokesActorHostedBySpringBootOverGrpcWithoutAppPort() {
ActorProxy proxy = new ActorProxyBuilder<>("MyActorTest", ActorProxy.class, actorClient)
.build(new ActorId(UUID.randomUUID().toString()));

assertEquals("olleh", proxy.invokeMethod("say", "hello", String.class).block());
}
}
Loading
Loading