-
Notifications
You must be signed in to change notification settings - Fork 12
Service Discovery and Service Binding
The first part of the documentation introduced how libraries can be published and consumed using Astrix. This part introduces another type of api, the heart of all service based architectures, namely the service.
In this context, a service is something that is typically provided by a different process. In order to consume a service, Astrix must first bind to the service which is done using a ServiceComponent
. The ServiceComponent
interface is the extension point to implement new service-transports in Astrix. As api-developer or consumer you never use the ServiceComponent
directly. Rather it is used behind the scenes by Astrix to bind to a provider of a given service. However, even if you don't intend to implement you own ServiceComponent
it's good to have knowledge about the inner workings of service binding.
Service binding is done in two steps:
- Lookup the service properties associated with the given service. This involves identifying what
ServiceComponent
to use and find all properties required by theServiceComponent
to bind to the service. This step is known as "service discovery". - Use the
ServiceComponent
to bind to the given service.
Each step uses a plugin architecture. New service discovery mechanisms can be plugged into the framework by implementing the ServiceDiscoveryFactoryPlugin
spi. Out of the box Astrix supports two mechanisms for service discovery. One using configuration, @AstrixConfigDiscovery
, and another using the service-registry, @AstrixServiceRegistryDiscovery
, which will be introduced later. New binding mechanisms can be implemented by implementing the ServiceComponent
directly.Astrix comes with a number of ServiceComponent
implementations which will be introduced throughout the documentation.
The first ServiceComponent
covered is the DirectComponent
which is a useful tool to support testing. It allows binding to a service within the same process, i.e. an ordinary object within the same jvm. The true power of the DirectComponent
comes when using it in combination with the service-registry, which we will see later in the documentation when covering the service-registry. This example introduces the DirectComponent
in combination with a @AstrixConfigDiscovery
.
In this example the api i split into one library, LunchSuggester
, and one service, LunchRestaruantFinder
. A service is defined in a similar way as a library, but by using the @Service
annotation to define a provided service. Each exported service must also define what discovery-strategy to use to locate the provided services. The method exporting LunchRestaurantFinder
is annotated with @AstrixConfigDiscovery
indicating that the properties required to bind to the provided service should be discovered by reading configuration.
public interface LunchSuggester {
String randomLunchRestaurant();
}
@AstrixApiProvider
public class LunchLibraryProvider {
@Library
public LunchSuggester lunchSuggester(LunchRestaurantFinder restaurantFinder) {
return new LunchSuggesterImpl(restaurantFinder);
}
}
public interface LunchRestaurantFinder {
List<String> getAllRestaurants();
}
@AstrixApiProvider
public interface LunchServiceProvider {
@AstrixConfigDiscovery("restaurantFinderUri")
@Service
LunchRestaurantFinder lunchRestaurantFinder();
}
A unit test for the library api might look like this:
public class LunchLibraryTest {
private AstrixContext astrix;
@After
public void after() {
astrix.destroy();
}
@Test
public void astrixDirectComponentAllowsBindingToObjectsInSameProcess() throws Exception {
LunchRestaurantFinder restaurantFinderStub = Mockito.mock(LunchRestaurantFinder.class);
String serviceUri = DirectComponent.registerAndGetUri(LunchRestaurantFinder.class, restaurantFinderStub);
AstrixConfigurer configurer = new AstrixConfigurer();
configurer.set("restaurantFinderUri", serviceUri);
configurer.setBasePackage("tutorial.p2");
astrix = configurer.configure();
LunchSuggester lunchSuggester = astrix.getBean(LunchSuggester.class);
Mockito.stub(restaurantFinderStub.getAllRestaurants()).toReturn(Arrays.asList("Pontus!"));
assertEquals("Pontus!", lunchSuggester.randomLunchRestaurant());
}
}
In the test we want to stub out the LunchRestaurantFinder
using Mockito. This can easily be done using the TestAstrixConfigurer
introduced in part 1, but for the sake of illustration we will use the DirectComponent
. We register the mock with the DirectComponent
which returns a serviceUri. A serviceUri contains all the service-properties required to bind to a given service. The first part identifies the name of the service component, in this case "direct"
. The second part is service-component specific and contains all properties required by the service-component to bind to the given service. DirectComponent
requires an identifier for the target instance which is generated when we register the instance with the DirectComponent
. Therefore a serviceUri identifying a service provided using the DirectComponent
might look like this: "direct:21"
.
When we configure Astrix we provide a setting, "restaurantFinderUri"
with a value that contains the serviceUri to the LunchRestaurantFinder
mock instance. When Astrix creates an instance of LunchRestaurantFinder
(which is done indirectly when we create the LunchSuggester
bean in the test) the process goes like this:
- Astrix sees that it is a service (the ApiProvider exports the bean as
@Service
) and that the service properties should be discovered using configuration (defined by the@AstrixConfigDiscovery
annotation) - Astrix queries the configuration for the entry name defined by the
@AstrixConfigDiscovery
annotation ("restaurantFinderUri") to get the serviceUri, lets say that its value is"direct:21"
- Astrix parses the serviceUri to find what
ServiceComponent
to use for binding, in this case"direct"
- Astrix delegates service binding to
DirectComponent
, passing in all component-specific properties, in this case"21"
- The
DirectComponent
queries its internal registry of objects and finds our mock instance and returns it
The previous example uses the configuration mechanism to discover the ServiceProperties required to bind to LunchRestaurantFinder
. Astrix ships with a small standalone configuration framework called DynamicConfig. A configuration property is resolved in the following order:
- Custom ConfigurationSource's
- System properties
- Programmatic configuration set on
AstrixConfigurer
META-INF/astrix/settings.properties
- Default values
Astrix will use the first value found for a given setting. Hence the Custom ConfigurationSource's takes precedence over the Programatic configuration and so on. The custom configuration is plugable by implementing the ConfigSource
and/or DynamicConfigSource
spi. By default Astrix will not use any external configuration. The settings.properties
provides a convenient way to override the default values provided by Astrix. It could be used to set corporate wide default-values by sharing a single settings.properties
file. For instance it could be used to say that "com.mycorp"
should be scanned for api-providers, avoiding the need to dupplicate such configurations on every instance of AstrixConfigurer throughout an enterprise.
TODO: configuration example?
Every service-bean in astrix (any bean bound using an ServiceComponent
) will be a "stateful" bean. Astrix.getBean(BeanType.class)
always returns a proxy for a stateful bean (provided that there exists an AstrixServiceProvider
exporting the given api). However, if the bean can't be bound the proxy will be in UNBOUND
state in which it will throw a ServiceUnavailableException
on each invocation. Astrix will periodically attempt to bind an UNBOUND
bean until successful.
The following example illustrates how a service-bean proxy goes from UNBOUND
state to BOUND
when the target service becomes available. It also illustrates usage of AstrixSettings as an external configuration provider which can be useful in testing.
public class AstrixBeanStateManagementTest {
private AstrixSettings settings = new AstrixSettings();
private AstrixContext astrix;
@After
public void after() {
astrix.destroy();
}
@Test
public void astrixManagesStateForEachServiceBean() throws Exception {
AstrixConfigurer configurer = new AstrixConfigurer();
configurer.set(AstrixSettings.BEAN_BIND_ATTEMPT_INTERVAL, 10); // 1.
configurer.set(AstrixSettings.ASTRIX_CONFIG_URI, settings.getExternalConfigUri()); // 2.
configurer.setBasePackage("tutorial.p2");
astrix = configurer.configure();
LunchSuggester lunchSuggester = astrix.getBean(LunchSuggester.class);
try {
lunchSuggester.randomLunchRestaurant(); // 3.
} catch (ServiceUnavailableException e) {
// LunchRestaurantFinder is UNBOUND
}
LunchRestaurantFinder restaurantFinder = Mockito.mock(LunchRestaurantFinder.class);
String serviceUri = DirectComponent.registerAndGetUri(LunchRestaurantFinder.class, restaurantFinder); // 4.
settings.set("restaurantFinderUri", serviceUri); // 5.
astrix.waitForBean(LunchSuggester.class, 2000); // 6.
Mockito.stub(restaurantFinder.getAllRestaurants()).toReturn(Arrays.asList("Pontus!"));
assertEquals("Pontus!", lunchSuggester.randomLunchRestaurant()); // 7.
}
}
- The
BEAN_BIND_ATTEMPT_INTERVAL
determines how often Astrix will attempt to bind a given bean (millis). - The
ASTRIX_CONFIG_URI
is a service-like uri to the external configuration source, in this case an instance ofAstrixSettings
within the same process. - The
LunchSuggester
usesLunchRestaurantFinder
in background. Since the configuration contains no entry for"restarurantFinderUri"
theLunchRestaurantFinder
will be in stateUNBOUND
- This registers a mock instance for
LunchRestaurantFinder
in the direct-component, which returns a serviceUri that can be used to bind to the mock instance. - A
"restaurantFinderUri"
pointing to the mock is added to the configuration. - Astrix allows us to wait for a bean to be bound. Note that we are waiting for a Library. Astrix is clever and detects that the library uses the `LunchRest
- aurantFinder
and therefore waits until the
LunchRestaurantFinder` is bound. - The
LunchSuggester
library is invoked, which in turn invokes theLunchRestaurantFinder
service which will beBOUND
at this time.