Skip to content

Commit

Permalink
support lazy, fail-fast and background initialization of asyncapi (#297)
Browse files Browse the repository at this point in the history
* support lazy, fail-fast and background initialization of asyncapi

* optimize imports.

* asyncapi creation fix order of invocation of channelservice and schemasservice.

* spotless format fixes

* fix SpringwolfAmqpProducerConfigurationIntegrationTest

* Apply suggestions from code review

Co-authored-by: Timon Back <[email protected]>

* rename LoadingMode into InitMode. InitModes knows only to modes 'background' and 'fail_fast'. Moved Initialization completley into AsyncApiInitApplicationListener. Fixed some tests.

Took 47 minutes

* feat(core): Ensure initMode=lazy stops application start

---------

Co-authored-by: Timon Back <[email protected]>
Co-authored-by: Timon Back <[email protected]>
  • Loading branch information
3 people authored Jul 28, 2023
1 parent 7158bd5 commit 06069cb
Show file tree
Hide file tree
Showing 17 changed files with 431 additions and 71 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,15 @@

import java.util.Map;

/**
* Service to detect AsyncAPI channels in the current spring context.
*/
public interface ChannelsService {

Map<String, ChannelItem> getChannels();
/**
* Detects all available AsyncAPI ChannelItem in the spring context.
*
* @return Map of channel names mapping to detected ChannelItems
*/
Map<String, ChannelItem> findChannels();
}
Original file line number Diff line number Diff line change
@@ -1,54 +1,100 @@
package io.github.stavshamir.springwolf.asyncapi;

import com.asyncapi.v2._6_0.model.channel.ChannelItem;
import io.github.stavshamir.springwolf.asyncapi.types.AsyncAPI;
import io.github.stavshamir.springwolf.asyncapi.types.Components;
import io.github.stavshamir.springwolf.configuration.AsyncApiDocket;
import io.github.stavshamir.springwolf.configuration.AsyncApiDocketService;
import io.github.stavshamir.springwolf.schemas.SchemasService;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Map;

@Slf4j
@Service
@RequiredArgsConstructor
public class DefaultAsyncApiService implements AsyncApiService {

/**
* Record holding the result of AsyncAPI creation.
*
* @param asyncAPI
* @param exception
*/
private record AsyncAPIResult(AsyncAPI asyncAPI, Throwable exception) {}

private final AsyncApiDocketService asyncApiDocketService;
private final ChannelsService channelsService;
private final SchemasService schemasService;
private final List<AsyncApiCustomizer> customizers;

private AsyncAPI asyncAPI;
private volatile AsyncAPIResult asyncAPIResult = null;

@PostConstruct
void buildAsyncApi() {
log.debug("Building AsyncAPI document");
@Override
public AsyncAPI getAsyncAPI() {
if (isNotInitialized()) {
initAsyncAPI();
}

AsyncApiDocket docket = asyncApiDocketService.getAsyncApiDocket();
if (asyncAPIResult.asyncAPI != null) {
return asyncAPIResult.asyncAPI;
} else {
throw new RuntimeException("Error occured during creation of AsyncAPI", asyncAPIResult.exception);
}
}

Components components =
Components.builder().schemas(schemasService.getDefinitions()).build();
/**
* Does the 'heavy work' of building the AsyncAPI documents once. Stores the resulting
* AsyncAPI document or alternativly a catched exception/error in the instance variable asyncAPIResult.
*
* @return
*/
protected synchronized void initAsyncAPI() {
if (this.asyncAPIResult != null) {
return; // Double Check Idiom
}

asyncAPI = AsyncAPI.builder()
.info(docket.getInfo())
.id(docket.getId())
.defaultContentType(docket.getDefaultContentType())
.servers(docket.getServers())
.channels(channelsService.getChannels())
.components(components)
.build();
try {
log.debug("Building AsyncAPI document");

for (AsyncApiCustomizer customizer : customizers) {
customizer.customize(asyncAPI);
AsyncApiDocket docket = asyncApiDocketService.getAsyncApiDocket();

// ChannelsService must be invoked before accessing SchemasService,
// because during channel scanning, all detected schemas are registered with
// SchemasService.
Map<String, ChannelItem> channels = channelsService.findChannels();

Components components = Components.builder()
.schemas(schemasService.getDefinitions())
.build();

AsyncAPI asyncAPI = AsyncAPI.builder()
.info(docket.getInfo())
.id(docket.getId())
.defaultContentType(docket.getDefaultContentType())
.servers(docket.getServers())
.channels(channels)
.components(components)
.build();

for (AsyncApiCustomizer customizer : customizers) {
customizer.customize(asyncAPI);
}
this.asyncAPIResult = new AsyncAPIResult(asyncAPI, null);
} catch (Throwable t) {
this.asyncAPIResult = new AsyncAPIResult(null, t);
}
}

@Override
public AsyncAPI getAsyncAPI() {
return asyncAPI;
/**
* checks whether asyncApi has internally allready been initialized or not.
*
* @return true if asyncApi has not allready been created and initialized.
*/
public boolean isNotInitialized() {
return this.asyncAPIResult == null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,31 @@
import com.asyncapi.v2._6_0.model.channel.ChannelItem;
import io.github.stavshamir.springwolf.asyncapi.scanners.channels.ChannelMerger;
import io.github.stavshamir.springwolf.asyncapi.scanners.channels.ChannelsScanner;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
* Service to detect AsyncAPI channels in the current spring context.
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class DefaultChannelsService implements ChannelsService {

private final List<? extends ChannelsScanner> channelsScanners;
private final Map<String, ChannelItem> channels = new HashMap<>();

@PostConstruct
void findChannels() {
/**
* Collects all AsyncAPI ChannelItems using the available {@link ChannelsScanner}
* beans.
* @return Map of channel names mapping to detected ChannelItems
*/
@Override
public Map<String, ChannelItem> findChannels() {
List<Map.Entry<String, ChannelItem>> foundChannelItems = new ArrayList<>();

for (ChannelsScanner scanner : channelsScanners) {
Expand All @@ -33,12 +38,6 @@ void findChannels() {
log.error("An error was encountered during channel scanning with {}: {}", scanner, e.getMessage());
}
}

this.channels.putAll(ChannelMerger.merge(foundChannelItems));
}

@Override
public Map<String, ChannelItem> getChannels() {
return channels;
return ChannelMerger.merge(foundChannelItems);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package io.github.stavshamir.springwolf.asyncapi;

import io.github.stavshamir.springwolf.configuration.properties.SpringWolfConfigProperties;
import io.github.stavshamir.springwolf.configuration.properties.SpringWolfConfigProperties.InitMode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.core.task.TaskExecutor;
import org.springframework.stereotype.Service;

/**
* Spring ApplicationListener listening on {@link ApplicationReadyEvent}. Triggers the AsyncAPI creation.
* If loadingMode equals {@link InitMode#BACKGROUND} AsyncAPI creation is triggered asynchronously using the
* provided Spring TaskExecutor.
*/
@Slf4j
@RequiredArgsConstructor
@Service
public class SpringWolfInitApplicationListener implements ApplicationListener<ApplicationReadyEvent>, InitializingBean {

private final TaskExecutor taskExecutor;
private final AsyncApiService asyncApiService;
private final SpringWolfConfigProperties configProperties;

@Override
public void onApplicationEvent(ApplicationReadyEvent event) {
if (configProperties.getInitMode() == InitMode.BACKGROUND) {
log.debug("triggering background asyncapi creation..");
taskExecutor.execute(this.asyncApiService::getAsyncAPI);
}
}

@Override
public void afterPropertiesSet() {
if (configProperties.getInitMode() == InitMode.FAIL_FAST) {
log.debug("triggering asyncapi creation..");
this.asyncApiService.getAsyncAPI();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import lombok.extern.slf4j.Slf4j;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.util.Optional;

Expand Down Expand Up @@ -63,7 +64,7 @@ private AsyncApiDocket parseApplicationConfigProperties(SpringWolfConfigProperti
}

private static Info buildInfo(@Nullable SpringWolfConfigProperties.ConfigDocket.Info info) {
if (info == null || info.getVersion() == null || info.getTitle() == null) {
if (info == null || !StringUtils.hasText(info.getVersion()) || !StringUtils.hasText(info.getTitle())) {
throw new IllegalArgumentException("One or more required fields of the info object (title, version) "
+ "in application.properties with path prefix " + SpringWolfConfigConstants.SPRINGWOLF_CONFIG_PREFIX
+ " is not set.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,30 @@
@Setter
public class SpringWolfConfigProperties {

public enum InitMode {
/**
* Default option.
* AsyncAPI detection on application startup. Exceptions interrupt the application start.
*
* Use for immediate feedback in case of misconfiguration.
*/
FAIL_FAST,

/**
* AsyncAPI detection after application startup in background thread (via Spring TaskExecutor).
*
* Use when your application context is large and initialization should be deferred to reduce start-up time.
*/
BACKGROUND
}

private boolean enabled = true;

/**
* Init mode for building AsyncAPI.
*/
private InitMode initMode = InitMode.FAIL_FAST;

@Nullable
private ConfigDocket docket;

Expand All @@ -46,15 +68,15 @@ public static class ConfigDocket {
/**
* Identifier of the application the AsyncAPI document is defining.
*
* @see com.asyncapi.v2._0_0.model.AsyncAPI#id
* @see com.asyncapi.v2._6_0.model.AsyncAPI#id
*/
@Nullable
private String id;

/**
* A string representing the default content type to use when encoding/decoding a message's payload.
*
* @see com.asyncapi.v2._0_0.model.AsyncAPI#defaultContentType
* @see com.asyncapi.v2._6_0.model.AsyncAPI#getdefaultContentType
*/
@Nullable
private String defaultContentType;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,19 @@

import com.asyncapi.v2._6_0.model.info.Info;
import io.github.stavshamir.springwolf.configuration.AsyncApiDocket;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;

@TestConfiguration
@ConditionalOnProperty(name = "test.springwolf.asyncapidocket", havingValue = "true", matchIfMissing = true)
class CustomBeanAsyncApiDocketConfiguration {
@Bean
public AsyncApiDocket docket() {
return AsyncApiDocket.builder()
.info(Info.builder()
.title("AsyncApiDocketConfiguration-title")
.version("AsyncApiDocketConfiguration-version")
.title("CustomBeanAsyncApiDocketConfiguration-title")
.version("CustomBeanAsyncApiDocketConfiguration-version")
.build())
.build();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public class SpringContextTest {
@Nested
@ContextConfiguration(
classes = {
SpringWolfConfigProperties.class,
CustomBeanAsyncApiDocketConfiguration.class, // user has defined an own AsyncApiDocket bean
DefaultAsyncApiDocketService.class,
DefaultAsyncApiService.class,
Expand All @@ -49,6 +50,8 @@ void testContextWithAsyncApiDocketBean() {
assertNotNull(context);

assertThat(asyncApiService.getAsyncAPI()).isNotNull();
assertThat(asyncApiService.getAsyncAPI().getInfo().getTitle())
.isEqualTo("CustomBeanAsyncApiDocketConfiguration-title");
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.Message;
import io.github.stavshamir.springwolf.configuration.AsyncApiDocket;
import io.github.stavshamir.springwolf.configuration.DefaultAsyncApiDocketService;
import io.github.stavshamir.springwolf.configuration.properties.SpringWolfConfigProperties;
import io.github.stavshamir.springwolf.schemas.DefaultSchemasService;
import io.github.stavshamir.springwolf.schemas.example.ExampleJsonGenerator;
import org.junit.jupiter.api.Test;
Expand All @@ -32,6 +33,7 @@
@ExtendWith(SpringExtension.class)
@ContextConfiguration(
classes = {
SpringWolfConfigProperties.class,
DefaultAsyncApiDocketService.class,
DefaultAsyncApiService.class,
DefaultChannelsService.class,
Expand Down
Loading

0 comments on commit 06069cb

Please sign in to comment.