Skip to content

Commit

Permalink
Add guide applications for spring-boot-eventeria (#29)
Browse files Browse the repository at this point in the history
- add spring-boot-eventeria application in guide-projects directory
- introduce FunctionalBindingSupports
  • Loading branch information
chanhyeong authored Jul 1, 2024
1 parent af6eac3 commit 63ca5b0
Show file tree
Hide file tree
Showing 22 changed files with 1,052 additions and 0 deletions.
18 changes: 18 additions & 0 deletions guide-projects/spring-boot-eventeria-guide/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
plugins {
id "eventeria.java-conventions"
id "eventeria.verification-conventions"
id "eventeria.spring-dependency-management-conventions"
}

dependencies {
implementation project(":spring-boot-eventeria")
implementation("org.springframework.boot:spring-boot-starter-web")

compileOnly("org.projectlombok:lombok")
annotationProcessor("org.projectlombok:lombok")

// for embedded kafka
implementation("org.springframework.kafka:spring-kafka-test")

testImplementation("org.springframework.boot:spring-boot-starter-test")
}
3 changes: 3 additions & 0 deletions guide-projects/spring-boot-eventeria-guide/lombok.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
lombok.anyConstructor.addConstructorProperties=true
lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Qualifier
lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Value
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.navercorp.eventeria.guide.boot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringBootEventeriaGuideApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootEventeriaGuideApplication.class, args);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.navercorp.eventeria.guide.boot.config;

import java.util.List;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.kafka.KafkaException;
import org.springframework.kafka.test.EmbeddedKafkaBroker;
import org.springframework.kafka.test.EmbeddedKafkaZKBroker;

public final class EmbeddedKafkaHolder implements InitializingBean {

private static final EmbeddedKafkaBroker embeddedKafka = new EmbeddedKafkaZKBroker(1, false)
.brokerListProperty("spring.kafka.bootstrap-servers");

private static boolean started;

private final List<String> topics;

EmbeddedKafkaHolder(List<String> topics) {
super();
this.topics = topics;
}

public static EmbeddedKafkaBroker getEmbeddedKafka() {
if (!started) {
try {
embeddedKafka.afterPropertiesSet();
} catch (Exception e) {
throw new KafkaException("Embedded broker failed to start", e);
}
started = true;
}
return embeddedKafka;
}

@Override
public void afterPropertiesSet() {
embeddedKafka.afterPropertiesSet();
started = true;

embeddedKafka.addTopics(
topics.toArray(String[]::new)
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.navercorp.eventeria.guide.boot.config;

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

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class KafkaConfig {

@Bean("topicsMap")
@ConfigurationProperties(prefix = "topics")
Map<String, String> topicsMap() {
return new HashMap<>();
}

@Bean
EmbeddedKafkaHolder embeddedKafkaHolder(
@Qualifier("topicsMap") Map<String, String> topicsMap
) {
return new EmbeddedKafkaHolder(List.copyOf(topicsMap.values()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package com.navercorp.eventeria.guide.boot.config;

import java.util.function.Function;

import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.cloud.stream.binding.SubscribableChannelBindingTargetFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.fasterxml.jackson.databind.ObjectMapper;

import io.cloudevents.core.provider.EventFormatProvider;
import io.cloudevents.jackson.JsonFormat;
import lombok.RequiredArgsConstructor;

import com.navercorp.eventeria.guide.boot.domain.AfterPostCreationCommand.NotifyToSubscribers;
import com.navercorp.eventeria.guide.boot.domain.PostCreatedEvent;
import com.navercorp.eventeria.messaging.contract.Message;
import com.navercorp.eventeria.messaging.contract.cloudevents.serializer.CloudEventMessageReaderWriter;
import com.navercorp.eventeria.messaging.contract.serializer.MessageSerializerDeserializer;
import com.navercorp.eventeria.messaging.jackson.serializer.JacksonMessageSerializer;
import com.navercorp.eventeria.messaging.spring.cloud.stream.binding.ChannelBindable;
import com.navercorp.eventeria.messaging.spring.cloud.stream.binding.ChannelBinder;
import com.navercorp.eventeria.messaging.spring.cloud.stream.binding.DefaultChannelBinder;
import com.navercorp.eventeria.messaging.typealias.CloudEventMessageTypeAliasMapper;
import com.navercorp.spring.boot.eventeria.support.FunctionalBindingSupports;

@Configuration
@RequiredArgsConstructor
public class MessageConfig {

private final ObjectMapper objectMapper;

static {
EventFormatProvider.getInstance()
.registerFormat(
new JsonFormat()
.withForceExtensionNameLowerCaseDeserialization()
.withForceIgnoreInvalidExtensionNameDeserialization()
);
}

@Bean
MessageSerializerDeserializer messageSerializerDeserializer() {
return new JacksonMessageSerializer(objectMapper);
}

@Bean
CloudEventMessageTypeAliasMapper cloudEventMessageTypeAliasMapper() {
CloudEventMessageTypeAliasMapper typeAliasMapper = new CloudEventMessageTypeAliasMapper();

typeAliasMapper.addCompatibleTypeAlias(
PostCreatedEvent.class,
"com.navercorp.eventeria.guide.boot.domain.PostCreatedEvent"
);

typeAliasMapper.addCompatibleTypeAlias(
NotifyToSubscribers.class,
"com.navercorp.eventeria.guide.boot.domain.AfterPostCreationCommand$NotifyToSubscribers"
);

return typeAliasMapper;
}

@Bean
ChannelBindable channelBindable() {
return new ChannelBindable();
}

@Bean
@ConditionalOnMissingBean
ChannelBinder channelBinder(
SubscribableChannelBindingTargetFactory bindingTargetFactory,
ChannelBindable channelBindable
) {
return new DefaultChannelBinder(bindingTargetFactory, channelBindable);
}

/**
* Consume and converts messages from messaging system to {@link Message}
*
* @param cloudEventMessageReaderWriter
* @see #cloudEventMessageTypeAliasMapper()
* @see spring.cloud.function.definition application.yml
* @return type of {@link Message}
*/
@Bean
Function<org.springframework.messaging.Message<byte[]>, Message> transformCloudEventToMessage(
CloudEventMessageReaderWriter cloudEventMessageReaderWriter
) {
return FunctionalBindingSupports.convertToMessage(cloudEventMessageReaderWriter);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.navercorp.eventeria.guide.boot.controller;

import java.time.Instant;

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.RestController;

import lombok.Builder;
import lombok.RequiredArgsConstructor;

import com.navercorp.eventeria.guide.boot.domain.CreatePostService;
import com.navercorp.eventeria.guide.boot.domain.Post;

@RestController
@RequiredArgsConstructor
public class PostApiController {

private final CreatePostService createPostService;

@PostMapping("/posts/{postId}")
public PostResponse createPost(
@PathVariable long postId,
@RequestBody CreatePostRequest request
) {
Post post = createPostService.create(
Post.builder()
.id(postId)
.writerId(request.writerId())
.content(request.content())
.createdAt(Instant.now())
.build()
);

return PostResponse.builder()
.id(post.id())
.writerId(post.writerId())
.content(post.content())
.createdAt(post.createdAt())
.build();
}

public record CreatePostRequest(
long id,

String writerId,

String content
) {
}

@Builder
public record PostResponse(
long id,

String writerId,

String content,

Instant createdAt
) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.navercorp.eventeria.guide.boot.domain;

import org.springframework.stereotype.Component;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import com.navercorp.eventeria.guide.boot.domain.AfterPostCreationCommand.ApplySearchIndex;
import com.navercorp.eventeria.guide.boot.domain.AfterPostCreationCommand.RefreshPostRanking;
import com.navercorp.eventeria.guide.boot.domain.AfterPostCreationCommand.UpdateUserStatistic;

@Slf4j
@Component
@RequiredArgsConstructor
public class AfterPostCommandHandler {

public void updateUserStatistic(UpdateUserStatistic command) {
log.info("[CONSUME][UpdateUserStatistic] {}", command);
}

public void refreshPostRanking(RefreshPostRanking command) {
log.info("[CONSUME][RefreshPostRanking] {}", command);
}

public void applySearchIndex(ApplySearchIndex command) {
log.info("[CONSUME][ApplySearchIndex] {}", command);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.navercorp.eventeria.guide.boot.domain;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

import com.navercorp.eventeria.messaging.contract.command.AbstractCommand;
import com.navercorp.eventeria.messaging.contract.command.Command;

public sealed interface AfterPostCreationCommand extends Command {

@Override
default String getSourceType() {
return Post.class.getName();
}

// functional binding, concurrent
@Getter
@RequiredArgsConstructor
final class NotifyToSubscribers extends AbstractCommand implements AfterPostCreationCommand {
private final long postId;
}

// programmatic binding
@Getter
@RequiredArgsConstructor(staticName = "of")
final class UpdateUserStatistic extends AbstractCommand implements AfterPostCreationCommand {
private final long postId;
private final String writerId;
}

// programmatic binding
@Getter
@RequiredArgsConstructor(staticName = "from")
final class RefreshPostRanking extends AbstractCommand implements AfterPostCreationCommand {
private final long postId;
}

// programmatic binding
@Getter
@RequiredArgsConstructor(staticName = "from")
final class ApplySearchIndex extends AbstractCommand implements AfterPostCreationCommand {
private final long postId;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.navercorp.eventeria.guide.boot.domain;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;

import lombok.RequiredArgsConstructor;

import com.navercorp.eventeria.guide.boot.publisher.ProgrammaticBindingEventPublisher;
import com.navercorp.eventeria.messaging.spring.integration.channel.SpringMessagePublisher;

@Service
@RequiredArgsConstructor
public class CreatePostService {

@Qualifier(ProgrammaticBindingEventPublisher.OUTBOUND_BEAN_NAME)
private final SpringMessagePublisher springMessagePublisher;

/**
* publish {@link PostCreatedEvent} and return request parameter.
*
* @param post
* @return
*/
public Post create(Post post) {
springMessagePublisher.publish(
PostCreatedEvent.from(post)
);

return post;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.navercorp.eventeria.guide.boot.domain;

import java.time.Instant;

import lombok.Builder;

@Builder
public record Post(
long id,

String writerId,

String content,

Instant createdAt
) {
}
Loading

0 comments on commit 63ca5b0

Please sign in to comment.