├── .gitignore ├── README.md ├── app ├── build.gradle └── src │ └── main │ ├── java │ └── dev │ │ └── fumin │ │ └── sample │ │ └── eventdriven │ │ ├── Main.java │ │ ├── application │ │ ├── di │ │ │ └── ApplicationModule.java │ │ ├── event │ │ │ ├── EnqueueEvent.java │ │ │ ├── EnqueueEventInterceptor.java │ │ │ ├── EventDispatcher.java │ │ │ ├── EventQueue.java │ │ │ ├── EventRelay.java │ │ │ ├── EventStore.java │ │ │ └── StoredEvent.java │ │ ├── persistence │ │ │ └── Transactional.java │ │ └── usecase │ │ │ ├── column │ │ │ └── CreateColumnUseCase.java │ │ │ ├── note │ │ │ ├── CopyNoteToTaskUseCase.java │ │ │ ├── CreateNoteCommand.java │ │ │ └── CreateNoteUseCase.java │ │ │ └── project │ │ │ ├── CreateProjectUseCase.java │ │ │ └── DeactivateProjectUseCase.java │ │ ├── common │ │ └── function │ │ │ └── ThrowableConsumer.java │ │ ├── di │ │ ├── DependencyFilter.java │ │ └── DependencyListener.java │ │ ├── domain │ │ ├── common │ │ │ ├── ConcurrencyEntity.java │ │ │ └── Entity.java │ │ ├── event │ │ │ ├── DomainEvent.java │ │ │ ├── DomainEventPublisher.java │ │ │ ├── DomainEventSubscriber.java │ │ │ └── DomainResult.java │ │ └── model │ │ │ ├── cloumn │ │ │ ├── Column.java │ │ │ ├── ColumnId.java │ │ │ ├── ColumnRepository.java │ │ │ └── ColumnService.java │ │ │ ├── google │ │ │ └── tasks │ │ │ │ ├── Task.java │ │ │ │ └── TaskCreationService.java │ │ │ ├── note │ │ │ ├── Note.java │ │ │ ├── NoteCreated.java │ │ │ ├── NoteEvent.java │ │ │ ├── NoteId.java │ │ │ ├── NoteRepository.java │ │ │ └── NoteService.java │ │ │ └── project │ │ │ ├── Project.java │ │ │ ├── ProjectActivated.java │ │ │ ├── ProjectDeactivated.java │ │ │ ├── ProjectEvent.java │ │ │ ├── ProjectId.java │ │ │ └── ProjectRepository.java │ │ ├── infrastructure │ │ ├── di │ │ │ └── InfrastructureModule.java │ │ ├── event │ │ │ ├── DomainEventResetFilter.java │ │ │ ├── DomainEventSerializer.java │ │ │ ├── EventTranslator.java │ │ │ ├── MySqlConsumedEventStore.java │ │ │ ├── MySqlEventStore.java │ │ │ └── PubsubEventDispatcher.java │ │ ├── persistence │ │ │ ├── dao │ │ │ │ ├── ColumnDao.java │ │ │ │ ├── ConsumedEventDao.java │ │ │ │ ├── EventDao.java │ │ │ │ ├── NoteDao.java │ │ │ │ └── ProjectDao.java │ │ │ ├── doma │ │ │ │ ├── DomaConfig.java │ │ │ │ └── DomaTransactionInterceptor.java │ │ │ ├── entity │ │ │ │ ├── BaseDto.java │ │ │ │ ├── BaseDtoListener.java │ │ │ │ ├── ColumnDto.java │ │ │ │ ├── ConcurrencyDto.java │ │ │ │ ├── ConsumedEventDto.java │ │ │ │ ├── EventDto.java │ │ │ │ ├── NoteDto.java │ │ │ │ └── ProjectDto.java │ │ │ ├── repository │ │ │ │ ├── MySqlColumnRepository.java │ │ │ │ ├── MySqlNoteRepository.java │ │ │ │ └── MySqlProjectRepository.java │ │ │ └── translation │ │ │ │ ├── BaseTranslator.java │ │ │ │ ├── ColumnTranslator.java │ │ │ │ ├── ConcurrencyDataTranslator.java │ │ │ │ ├── DataTranslator.java │ │ │ │ ├── NoteTranslator.java │ │ │ │ └── ProjectTranslator.java │ │ ├── pubsub │ │ │ ├── LocalSafePublisher.java │ │ │ ├── RemoteSafePublisher.java │ │ │ └── SafePublisher.java │ │ ├── serialization │ │ │ └── DateTypeAdapter.java │ │ └── service │ │ │ └── DummyTaskCreationService.java │ │ └── presentation │ │ ├── api │ │ ├── ColumnApi.java │ │ ├── NoteApi.java │ │ └── ProjectApi.java │ │ ├── di │ │ └── PresentationModule.java │ │ └── event │ │ ├── ConsumedEvent.java │ │ ├── ConsumedEventStore.java │ │ ├── Event.java │ │ ├── EventProxy.java │ │ ├── EventReceiver.java │ │ └── receiver │ │ └── NoteCreatedReceiver.java │ └── resources │ └── META-INF │ └── dev │ └── fumin │ └── sample │ └── eventdriven │ └── infrastructure │ └── persistence │ └── dao │ ├── ColumnDao │ └── selectById.sql │ ├── ConsumedEventDao │ └── exists.sql │ ├── EventDao │ └── selectById.sql │ ├── NoteDao │ └── selectById.sql │ └── ProjectDao │ └── selectById.sql ├── build.gradle ├── buildSrc ├── build.gradle.kts └── src │ └── main │ └── java │ └── dependencies │ ├── Dep.kt │ └── Environment.kt ├── docker ├── Dockerfile-MySQL ├── docker-compose.yaml └── initdb.d │ └── ddl.sql ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── pubsub-emulator ├── build.gradle └── src │ └── main │ └── java │ └── dev │ └── fumin │ └── sample │ └── pubsub │ └── emulator │ └── EmulatorInitializer.java └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | **/build/ 3 | **/.gradle/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | **/.DS_Store 6 | 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | 17 | ### IntelliJ IDEA ### 18 | .idea/ 19 | *.iws 20 | *.iml 21 | *.ipr 22 | **/out/ 23 | 24 | ### NetBeans ### 25 | /nbproject/private/ 26 | /nbbuild/ 27 | /dist/ 28 | /nbdist/ 29 | /.nb-gradle/ 30 | 31 | docker/db*/ 32 | 33 | app/src/**/resources/*.json 34 | app/.credential/ 35 | 36 | .java-version -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ドメインイベントによるイベント駆動のサンプル 2 | 3 | ## 概要 4 | 5 | イベント駆動による非同期処理のサンプルコード。 6 | DDDのドメインイベントを用いてイベントの発生、配信を表現する。 7 | 8 | 詳しい解説は[こちら](https://zenn.dev/fuuuuumin65/articles/2c96e8f0b29c01) 9 | 10 | ## 環境 11 | 12 | + GAE/Java11 13 | + Cloud SQL for MySQL5.7 14 | + Cloud Pub/Sub 15 | 16 | ## アプリのローカル実行手順 17 | 18 | ローカルで動作を確認するために、Pub/Subのローカルエミュレータ、MySQLのローカル環境を使用します。 19 | GAEにデプロイする場合は各種設定を変更してください。 20 | 21 | ### MySQLの環境構築 22 | 23 | 開発用にローカルにDockerでMySQL環境を構築します。 24 | Dockerはインストール済み前提です。 25 | 26 | `docker`ディレクトリの直下で、以下の順序でコマンドを実行します。 27 | 28 | ``` 29 | docker-compose build 30 | docker-compose up -d 31 | ``` 32 | 33 | ### Pub/Subエミュレータの起動 34 | 35 | ``` 36 | gcloud beta emulators pubsub start --project=local-project 37 | ``` 38 | 39 | ### ローカル環境の初期化 40 | 41 | ``` 42 | $(gcloud beta emulators pubsub env-init) 43 | ./gradlew pubsub-emulator:run 44 | ``` 45 | 46 | ### アプリの実行 47 | 48 | 環境変数の都合があるので、初期化を実行したコンソール上で以下を実行。 49 | 50 | ``` 51 | ./gradlew app:runApp 52 | ``` 53 | 54 | 以下のリクエストを実行 55 | 56 | 1. POST /api/projects {"name":"My Project"} 57 | 1. POST /api/columns {"projectId":"上のレスポンスのIDを入れる", "columnName":"Todo"} 58 | 1. POST /api/notes {"projectId":"上のレスポンスのIDを入れる", "columnId":"上のレスポンスのIDを入れる", "description":"コードレビューをする"} 59 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | import dependencies.Dep 2 | import dependencies.Environment 3 | 4 | apply plugin: "com.google.cloud.tools.appengine" 5 | apply plugin: "war" 6 | 7 | sourceCompatibility = "11" 8 | targetCompatibility = "11" 9 | 10 | task copyDomaResources(type: Sync) { 11 | from sourceSets.main.resources.srcDirs 12 | into compileJava.destinationDir 13 | include "doma.compile.config" 14 | include "META-INF/**/*.sql" 15 | include "META-INF/**/*.script" 16 | } 17 | 18 | compileJava { 19 | dependsOn copyDomaResources 20 | options.encoding = "UTF-8" 21 | options.annotationProcessorPath = configurations.annotationProcessor 22 | } 23 | 24 | 25 | dependencies { 26 | compileOnly Dep.Lombok.core 27 | annotationProcessor Dep.Lombok.compiler 28 | 29 | implementation Dep.Database.Doma.core 30 | annotationProcessor Dep.Database.Doma.compiler 31 | implementation Dep.Database.hikariCp 32 | implementation Dep.Database.mysql 33 | 34 | implementation Dep.Gson.core 35 | implementation Dep.Guice.core 36 | implementation Dep.Guice.servlet 37 | 38 | implementation Dep.GCP.pubsub 39 | 40 | compileOnly Dep.Servlet.api 41 | 42 | providedCompile Dep.Jetty.server 43 | providedCompile Dep.Jetty.webapp 44 | providedCompile Dep.Jetty.util 45 | providedCompile Dep.Jetty.annotations 46 | } 47 | 48 | appengine { 49 | deploy { 50 | projectId = "GCLOUD_CONFIG" 51 | version = "GCLOUD_CONFIG" 52 | promote = true 53 | } 54 | } 55 | 56 | war { 57 | 58 | archivesBaseName = "app" 59 | 60 | from { 61 | configurations.providedCompile.collect { 62 | it.isDirectory() ? it : project.zipTree(it) 63 | } 64 | } 65 | // 同じモジュールにMainを持ちたい場合はMainを直下に展開する 66 | from fileTree(dir: 'build/classes/java/main', include: '**/Main.class') 67 | 68 | manifest.attributes('Main-Class': 'dev.fumin.sample.eventdriven.Main') 69 | } 70 | 71 | task runApp() { 72 | dependsOn war 73 | doLast { 74 | javaexec { 75 | environment("GOOGLE_CLOUD_PROJECT", Environment.googleCloudProject) 76 | main = "-jar" 77 | args(buildDir.path + "/libs/app.war") 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/Main.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven; 2 | 3 | import org.eclipse.jetty.server.Server; 4 | import org.eclipse.jetty.webapp.Configuration; 5 | import org.eclipse.jetty.webapp.WebAppContext; 6 | 7 | import java.net.URL; 8 | import java.security.ProtectionDomain; 9 | 10 | public class Main { 11 | 12 | public static void main(String[] args) throws Exception { 13 | System.setProperty("org.eclipse.jetty.util.log.class", "org.eclipse.jetty.util.log.StrErrLog"); 14 | System.setProperty("org.eclipse.jetty.LEVEL", "INFO"); 15 | 16 | int port = Integer.parseInt(System.getenv().getOrDefault("PORT", "8080")); 17 | Server server = new Server(port); 18 | 19 | WebAppContext webapp = new WebAppContext(); 20 | webapp.setContextPath("/"); 21 | ProtectionDomain domain = Main.class.getProtectionDomain(); 22 | URL warLocation = domain.getCodeSource().getLocation(); 23 | webapp.setWar(warLocation.toExternalForm()); 24 | 25 | Configuration.ClassList classList = Configuration.ClassList.setServerDefault(server); 26 | 27 | classList.addBefore( 28 | "org.eclipse.jetty.webapp.JettyWebXmlConfiguration", 29 | "org.eclipse.jetty.annotations.AnnotationConfiguration"); 30 | 31 | webapp.setInitParameter("org.eclipse.jetty.servlet.Default.dirAllowed", "false"); 32 | 33 | server.setHandler(webapp); 34 | server.start(); 35 | server.join(); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/application/di/ApplicationModule.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.application.di; 2 | 3 | import com.google.inject.matcher.Matchers; 4 | import com.google.inject.servlet.ServletModule; 5 | 6 | import dev.fumin.sample.eventdriven.application.event.EnqueueEvent; 7 | import dev.fumin.sample.eventdriven.application.event.EnqueueEventInterceptor; 8 | 9 | public class ApplicationModule extends ServletModule { 10 | 11 | @Override 12 | protected void configureServlets() { 13 | super.configureServlets(); 14 | 15 | EnqueueEventInterceptor enqueueEventInterceptor = new EnqueueEventInterceptor(); 16 | bindInterceptor(Matchers.any(), Matchers.annotatedWith(EnqueueEvent.class), 17 | enqueueEventInterceptor); 18 | requestInjection(enqueueEventInterceptor); 19 | 20 | } 21 | 22 | 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/application/event/EnqueueEvent.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.application.event; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | import dev.fumin.sample.eventdriven.domain.event.DomainEvent; 9 | 10 | @Retention(RetentionPolicy.RUNTIME) 11 | @Target(ElementType.METHOD) 12 | public @interface EnqueueEvent { 13 | 14 | Class[] value(); 15 | 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/application/event/EnqueueEventInterceptor.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.application.event; 2 | 3 | import org.aopalliance.intercept.MethodInterceptor; 4 | import org.aopalliance.intercept.MethodInvocation; 5 | 6 | import java.util.ArrayList; 7 | import java.util.Arrays; 8 | import java.util.List; 9 | 10 | import javax.inject.Inject; 11 | 12 | import dev.fumin.sample.eventdriven.domain.event.DomainEvent; 13 | import dev.fumin.sample.eventdriven.domain.event.DomainEventPublisher; 14 | import dev.fumin.sample.eventdriven.domain.event.DomainEventSubscriber; 15 | 16 | public class EnqueueEventInterceptor implements MethodInterceptor { 17 | 18 | @Inject 19 | private EventQueue eventQueue; 20 | 21 | @Inject 22 | private DomainEventPublisher publisher; 23 | 24 | @Override 25 | public Object invoke(MethodInvocation invocation) throws Throwable { 26 | EnqueueEvent annotation = invocation.getMethod().getAnnotation(EnqueueEvent.class); 27 | EventCapture captor = new EventCapture(); 28 | Arrays.stream(annotation.value()) 29 | .forEach(eventClass -> publisher.subscribe(eventClass, captor::handle)); 30 | Object o = invocation.proceed(); 31 | captor.pushAll(eventQueue); 32 | return o; 33 | } 34 | 35 | private static class EventCapture implements DomainEventSubscriber { 36 | 37 | private List events = new ArrayList<>(); 38 | 39 | @Override 40 | public void handle(DomainEvent event) { 41 | events.add(event); 42 | } 43 | 44 | public void pushAll(EventQueue queue) { 45 | events.forEach(queue::push); 46 | } 47 | 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/application/event/EventDispatcher.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.application.event; 2 | 3 | import dev.fumin.sample.eventdriven.domain.event.DomainEvent; 4 | 5 | public interface EventDispatcher { 6 | 7 | void dispatchToProxy(long eventId); 8 | 9 | void dispatch(long eventId, DomainEvent event); 10 | 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/application/event/EventQueue.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.application.event; 2 | 3 | import java.util.Optional; 4 | 5 | import javax.inject.Inject; 6 | 7 | import dev.fumin.sample.eventdriven.domain.event.DomainEvent; 8 | 9 | public class EventQueue { 10 | 11 | private final EventStore store; 12 | private final EventDispatcher dispatcher; 13 | 14 | @Inject 15 | public EventQueue(EventStore store, EventDispatcher dispatcher) { 16 | this.store = store; 17 | this.dispatcher = dispatcher; 18 | } 19 | 20 | public void push(DomainEvent event) { 21 | StoredEvent storedEvent = new StoredEvent<>(event); 22 | // DBへ保存 23 | storedEvent = store.save(storedEvent); 24 | // 保存したイベントのIDをプロクシへ送信 25 | dispatcher.dispatchToProxy(storedEvent.getId() 26 | .orElseThrow(() -> new IllegalStateException("event id is null."))); 27 | } 28 | 29 | public Optional pop(long id) { 30 | return store.fetchById(id).map(stored -> { 31 | // DBから削除することで、送信済みとして扱う。 32 | store.delete(stored); 33 | return stored.getEvent(); 34 | }); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/application/event/EventRelay.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.application.event; 2 | 3 | import javax.inject.Inject; 4 | 5 | import dev.fumin.sample.eventdriven.application.persistence.Transactional; 6 | import dev.fumin.sample.eventdriven.domain.event.DomainEvent; 7 | 8 | public class EventRelay { 9 | 10 | private final EventQueue queue; 11 | private final EventDispatcher dispatcher; 12 | 13 | @Inject 14 | public EventRelay(EventQueue queue, EventDispatcher dispatcher) { 15 | this.queue = queue; 16 | this.dispatcher = dispatcher; 17 | } 18 | 19 | @Transactional 20 | public void relay(long eventId) { 21 | DomainEvent event = queue.pop(eventId) 22 | .orElseThrow(() -> new IllegalStateException("Event is not yet ensured or lost.")); 23 | dispatcher.dispatch(eventId, event); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/application/event/EventStore.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.application.event; 2 | 3 | import java.util.Optional; 4 | 5 | import dev.fumin.sample.eventdriven.domain.event.DomainEvent; 6 | 7 | public interface EventStore { 8 | 9 | StoredEvent save(StoredEvent event); 10 | 11 | Optional> fetchById(long id); 12 | 13 | void delete(StoredEvent event); 14 | 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/application/event/StoredEvent.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.application.event; 2 | 3 | import java.util.Date; 4 | import java.util.Optional; 5 | 6 | import dev.fumin.sample.eventdriven.domain.event.DomainEvent; 7 | import lombok.AllArgsConstructor; 8 | import lombok.Getter; 9 | import lombok.Setter; 10 | 11 | @AllArgsConstructor 12 | public class StoredEvent { 13 | 14 | @Setter 15 | private Long id; 16 | 17 | @Getter 18 | private T event; 19 | 20 | @Getter 21 | private String eventName; 22 | 23 | @Getter 24 | private Date storedAt; 25 | 26 | @Getter 27 | private long version; 28 | 29 | public StoredEvent(T event) { 30 | this.event = event; 31 | this.storedAt = new Date(); 32 | this.eventName = event.getClass().getName(); 33 | } 34 | 35 | public Optional getId() { 36 | return Optional.ofNullable(id); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/application/persistence/Transactional.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.application.persistence; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Retention(RetentionPolicy.RUNTIME) 9 | @Target(ElementType.METHOD) 10 | public @interface Transactional { 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/application/usecase/column/CreateColumnUseCase.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.application.usecase.column; 2 | 3 | import javax.inject.Inject; 4 | 5 | import dev.fumin.sample.eventdriven.application.persistence.Transactional; 6 | import dev.fumin.sample.eventdriven.domain.model.cloumn.Column; 7 | import dev.fumin.sample.eventdriven.domain.model.cloumn.ColumnService; 8 | import dev.fumin.sample.eventdriven.domain.model.project.ProjectId; 9 | 10 | public class CreateColumnUseCase { 11 | 12 | private final ColumnService columnService; 13 | 14 | @Inject 15 | public CreateColumnUseCase(ColumnService columnService) { 16 | this.columnService = columnService; 17 | } 18 | 19 | @Transactional 20 | public String handle(String projectId, String name) { 21 | Column column = columnService.create(new ProjectId(projectId), name); 22 | return column.getId().getValue(); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/application/usecase/note/CopyNoteToTaskUseCase.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.application.usecase.note; 2 | 3 | import javax.inject.Inject; 4 | 5 | import dev.fumin.sample.eventdriven.application.persistence.Transactional; 6 | import dev.fumin.sample.eventdriven.domain.model.google.tasks.TaskCreationService; 7 | import dev.fumin.sample.eventdriven.domain.model.note.NoteId; 8 | import dev.fumin.sample.eventdriven.domain.model.note.NoteRepository; 9 | import dev.fumin.sample.eventdriven.domain.model.project.ProjectId; 10 | 11 | public class CopyNoteToTaskUseCase { 12 | 13 | private final NoteRepository noteRepository; 14 | private final TaskCreationService taskCreationService; 15 | 16 | @Inject 17 | public CopyNoteToTaskUseCase(NoteRepository noteRepository, TaskCreationService taskCreationService) { 18 | this.noteRepository = noteRepository; 19 | this.taskCreationService = taskCreationService; 20 | } 21 | 22 | @Transactional 23 | public void handle(String projectId, String noteId) { 24 | ProjectId pId = new ProjectId(projectId); 25 | NoteId nId = new NoteId(noteId); 26 | noteRepository.noteFrom(pId, nId) 27 | .map(note -> note.copyToTasks(taskCreationService)) 28 | .ifPresent(task -> System.out.println("Task created : " + task.getId())); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/application/usecase/note/CreateNoteCommand.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.application.usecase.note; 2 | 3 | import lombok.Builder; 4 | import lombok.Value; 5 | 6 | @Builder 7 | @Value 8 | public class CreateNoteCommand { 9 | 10 | String projectId; 11 | String columnId; 12 | String description; 13 | 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/application/usecase/note/CreateNoteUseCase.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.application.usecase.note; 2 | 3 | import javax.inject.Inject; 4 | 5 | import dev.fumin.sample.eventdriven.application.event.EnqueueEvent; 6 | import dev.fumin.sample.eventdriven.application.persistence.Transactional; 7 | import dev.fumin.sample.eventdriven.domain.model.cloumn.ColumnId; 8 | import dev.fumin.sample.eventdriven.domain.model.note.Note; 9 | import dev.fumin.sample.eventdriven.domain.model.note.NoteCreated; 10 | import dev.fumin.sample.eventdriven.domain.model.note.NoteService; 11 | import dev.fumin.sample.eventdriven.domain.model.project.ProjectId; 12 | 13 | public class CreateNoteUseCase { 14 | 15 | private final NoteService noteService; 16 | 17 | @Inject 18 | public CreateNoteUseCase(NoteService noteService) { 19 | this.noteService = noteService; 20 | } 21 | 22 | @Transactional 23 | @EnqueueEvent(NoteCreated.class) 24 | public String handle(CreateNoteCommand command) { 25 | ProjectId projectId = new ProjectId(command.getProjectId()); 26 | ColumnId columnId = new ColumnId(command.getColumnId()); 27 | Note note = noteService.createNote(projectId, columnId, command.getDescription()); 28 | return note.getId().getValue(); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/application/usecase/project/CreateProjectUseCase.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.application.usecase.project; 2 | 3 | import javax.inject.Inject; 4 | 5 | import dev.fumin.sample.eventdriven.domain.model.project.Project; 6 | import dev.fumin.sample.eventdriven.domain.model.project.ProjectId; 7 | import dev.fumin.sample.eventdriven.domain.model.project.ProjectRepository; 8 | import dev.fumin.sample.eventdriven.application.persistence.Transactional; 9 | 10 | public class CreateProjectUseCase { 11 | 12 | private final ProjectRepository projectRepository; 13 | 14 | @Inject 15 | public CreateProjectUseCase(ProjectRepository projectRepository) { 16 | this.projectRepository = projectRepository; 17 | } 18 | 19 | @Transactional 20 | public String handle(String name) { 21 | ProjectId id = projectRepository.newId(); 22 | Project project = Project.create(id, name); 23 | projectRepository.create(project); 24 | return project.getId().getValue(); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/application/usecase/project/DeactivateProjectUseCase.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.application.usecase.project; 2 | 3 | import javax.inject.Inject; 4 | 5 | import dev.fumin.sample.eventdriven.application.event.EventQueue; 6 | import dev.fumin.sample.eventdriven.domain.model.project.Project; 7 | import dev.fumin.sample.eventdriven.domain.model.project.ProjectId; 8 | import dev.fumin.sample.eventdriven.domain.model.project.ProjectRepository; 9 | import dev.fumin.sample.eventdriven.application.persistence.Transactional; 10 | 11 | public class DeactivateProjectUseCase { 12 | 13 | private final ProjectRepository projectRepository; 14 | private final EventQueue eventQueue; 15 | 16 | @Inject 17 | public DeactivateProjectUseCase( 18 | ProjectRepository projectRepository, 19 | EventQueue eventQueue 20 | ) { 21 | this.projectRepository = projectRepository; 22 | this.eventQueue = eventQueue; 23 | } 24 | 25 | @Transactional 26 | public void handle(String projectId) { 27 | projectRepository.projectOf(new ProjectId(projectId)) 28 | .flatMap(Project::deactivate) 29 | .ifPresent(eventQueue::push); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/common/function/ThrowableConsumer.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.common.function; 2 | 3 | public interface ThrowableConsumer { 4 | void accept(T t) throws Exception; 5 | } 6 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/di/DependencyFilter.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.di; 2 | 3 | import com.google.inject.servlet.GuiceFilter; 4 | 5 | import javax.servlet.annotation.WebFilter; 6 | 7 | 8 | @WebFilter(filterName = "DependencyFilter", urlPatterns = "/*") 9 | public class DependencyFilter extends GuiceFilter { 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/di/DependencyListener.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.di; 2 | 3 | import com.google.inject.Guice; 4 | import com.google.inject.Injector; 5 | import com.google.inject.servlet.GuiceServletContextListener; 6 | 7 | import javax.servlet.annotation.WebListener; 8 | 9 | import dev.fumin.sample.eventdriven.application.di.ApplicationModule; 10 | import dev.fumin.sample.eventdriven.infrastructure.di.InfrastructureModule; 11 | import dev.fumin.sample.eventdriven.presentation.di.PresentationModule; 12 | 13 | @WebListener 14 | public class DependencyListener extends GuiceServletContextListener { 15 | 16 | @Override 17 | protected Injector getInjector() { 18 | return Guice.createInjector( 19 | new InfrastructureModule(), 20 | new PresentationModule(), 21 | new ApplicationModule() 22 | ); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/domain/common/ConcurrencyEntity.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.domain.common; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public abstract class ConcurrencyEntity extends Entity { 9 | 10 | private long version; 11 | 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/domain/common/Entity.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.domain.common; 2 | 3 | import java.util.Date; 4 | 5 | import lombok.Setter; 6 | 7 | @Setter 8 | public abstract class Entity { 9 | 10 | private Date createdAt = new Date(); 11 | private Date updatedAt = new Date(); 12 | 13 | public Date getCreatedAt() { 14 | return new Date(createdAt.getTime()); 15 | } 16 | 17 | public Date getUpdatedAt() { 18 | return new Date(updatedAt.getTime()); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/domain/event/DomainEvent.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.domain.event; 2 | 3 | public interface DomainEvent { 4 | } 5 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/domain/event/DomainEventPublisher.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.domain.event; 2 | 3 | import java.util.ArrayList; 4 | import java.util.HashMap; 5 | import java.util.List; 6 | import java.util.Map; 7 | 8 | public class DomainEventPublisher { 9 | 10 | private static final ThreadLocal, List>>> 11 | SUBSCRIBERS = ThreadLocal.withInitial(HashMap::new); 12 | 13 | public void subscribe( 14 | Class subscribeTo, 15 | DomainEventSubscriber subscriber 16 | ) { 17 | // ドメインイベントの型をキーにサブスクライバを登録する 18 | List> domainEventSubscribers = SUBSCRIBERS.get() 19 | .computeIfAbsent(subscribeTo, key -> new ArrayList<>()); 20 | domainEventSubscribers.add(subscriber); 21 | } 22 | 23 | @SuppressWarnings("unchecked") 24 | public void publish(DomainEvent event) { 25 | Class key = event.getClass(); 26 | List> subscribers = SUBSCRIBERS.get().get(key); 27 | 28 | if (subscribers == null) { 29 | return; 30 | } 31 | 32 | subscribers.forEach(subscriber -> ((DomainEventSubscriber) subscriber) 33 | .handle(event)); 34 | } 35 | 36 | public void reset() { 37 | SUBSCRIBERS.get().clear(); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/domain/event/DomainEventSubscriber.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.domain.event; 2 | 3 | @FunctionalInterface 4 | public interface DomainEventSubscriber { 5 | void handle(T event); 6 | } 7 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/domain/event/DomainResult.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.domain.event; 2 | 3 | import lombok.Value; 4 | 5 | @Value 6 | public class DomainResult { 7 | R result; 8 | E event; 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/domain/model/cloumn/Column.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.domain.model.cloumn; 2 | 3 | import dev.fumin.sample.eventdriven.domain.common.ConcurrencyEntity; 4 | import dev.fumin.sample.eventdriven.domain.model.project.ProjectId; 5 | import lombok.Getter; 6 | 7 | @Getter 8 | public final class Column extends ConcurrencyEntity { 9 | 10 | private final ColumnId id; 11 | private final ProjectId projectId; 12 | private String name; 13 | private boolean active; 14 | 15 | public static Column create(ProjectId projectId, ColumnId id, String name) { 16 | return new Column(id, projectId, name, true); 17 | } 18 | 19 | public Column(ColumnId id, ProjectId projectId, String name, boolean active) { 20 | this.id = id; 21 | this.projectId = projectId; 22 | this.name = name; 23 | this.active = active; 24 | } 25 | 26 | public void deactivate() { 27 | active = false; 28 | } 29 | 30 | public void activate() { 31 | active = true; 32 | } 33 | 34 | public void setName(String name) { 35 | if (name == null || name.isBlank()) { 36 | throw new IllegalArgumentException("name must not be null or blank."); 37 | } 38 | this.name = name; 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/domain/model/cloumn/ColumnId.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.domain.model.cloumn; 2 | 3 | import lombok.Value; 4 | 5 | @Value 6 | public class ColumnId { 7 | String value; 8 | } 9 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/domain/model/cloumn/ColumnRepository.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.domain.model.cloumn; 2 | 3 | import java.util.Optional; 4 | 5 | import dev.fumin.sample.eventdriven.domain.model.project.ProjectId; 6 | 7 | public interface ColumnRepository { 8 | 9 | ColumnId newId(); 10 | 11 | void create(Column column); 12 | 13 | void update(Column column); 14 | 15 | Optional columnFrom(ProjectId projectId, ColumnId columnId); 16 | 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/domain/model/cloumn/ColumnService.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.domain.model.cloumn; 2 | 3 | import javax.inject.Inject; 4 | 5 | import dev.fumin.sample.eventdriven.domain.model.project.Project; 6 | import dev.fumin.sample.eventdriven.domain.model.project.ProjectId; 7 | import dev.fumin.sample.eventdriven.domain.model.project.ProjectRepository; 8 | 9 | public class ColumnService { 10 | 11 | private final ProjectRepository projectRepository; 12 | private final ColumnRepository columnRepository; 13 | 14 | @Inject 15 | public ColumnService( 16 | ProjectRepository projectRepository, 17 | ColumnRepository columnRepository 18 | ) { 19 | this.projectRepository = projectRepository; 20 | this.columnRepository = columnRepository; 21 | } 22 | 23 | public Column create(ProjectId projectId, String name) { 24 | Project project = projectRepository.projectOf(projectId) 25 | .orElseThrow(() -> new IllegalArgumentException("specified project is not found.")); 26 | 27 | if (!project.isActive()) { 28 | throw new IllegalStateException("specified project is not active."); 29 | } 30 | 31 | ColumnId columnId = columnRepository.newId(); 32 | Column column = Column.create(projectId, columnId, name); 33 | 34 | columnRepository.create(column); 35 | 36 | return column; 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/domain/model/google/tasks/Task.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.domain.model.google.tasks; 2 | 3 | import lombok.Value; 4 | 5 | @Value 6 | public class Task { 7 | String id; 8 | String title; 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/domain/model/google/tasks/TaskCreationService.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.domain.model.google.tasks; 2 | 3 | public interface TaskCreationService { 4 | 5 | Task createTask(String title); 6 | 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/domain/model/note/Note.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.domain.model.note; 2 | 3 | import java.util.Objects; 4 | 5 | import dev.fumin.sample.eventdriven.domain.common.ConcurrencyEntity; 6 | import dev.fumin.sample.eventdriven.domain.model.google.tasks.TaskCreationService; 7 | import dev.fumin.sample.eventdriven.domain.model.project.ProjectId; 8 | import dev.fumin.sample.eventdriven.domain.event.DomainResult; 9 | import dev.fumin.sample.eventdriven.domain.model.cloumn.ColumnId; 10 | import dev.fumin.sample.eventdriven.domain.model.google.tasks.Task; 11 | import lombok.Getter; 12 | 13 | @Getter 14 | public final class Note extends ConcurrencyEntity { 15 | 16 | private final NoteId id; 17 | private final ProjectId projectId; 18 | private ColumnId columnId; 19 | private String description; 20 | 21 | public static DomainResult create( 22 | NoteId id, 23 | ProjectId projectId, 24 | ColumnId columnId, 25 | String description 26 | ) { 27 | Note note = new Note(id, projectId, columnId, description); 28 | NoteCreated event = new NoteCreated(projectId, columnId, id); 29 | return new DomainResult<>(note, event); 30 | } 31 | 32 | public Note(NoteId id, ProjectId projectId, ColumnId columnId, String description) { 33 | this.id = Objects.requireNonNull(id, "id must not be null"); 34 | this.projectId = Objects.requireNonNull(projectId, "projectId must not be null"); 35 | moveTo(columnId); 36 | setDescription(description); 37 | } 38 | 39 | public void moveTo(ColumnId columnId) { 40 | this.columnId = Objects.requireNonNull(columnId, "columnId must not be null"); 41 | } 42 | 43 | public void setDescription(String description) { 44 | if (description == null || description.isBlank()) { 45 | throw new IllegalArgumentException("description must not be null or blank."); 46 | } 47 | this.description = description; 48 | } 49 | 50 | public Task copyToTasks(TaskCreationService service) { 51 | return service.createTask(description); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/domain/model/note/NoteCreated.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.domain.model.note; 2 | 3 | import dev.fumin.sample.eventdriven.domain.model.cloumn.ColumnId; 4 | import dev.fumin.sample.eventdriven.domain.model.project.ProjectId; 5 | import lombok.Value; 6 | 7 | @Value 8 | public class NoteCreated implements NoteEvent { 9 | 10 | ProjectId projectId; 11 | ColumnId columnId; 12 | NoteId noteId; 13 | 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/domain/model/note/NoteEvent.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.domain.model.note; 2 | 3 | import dev.fumin.sample.eventdriven.domain.event.DomainEvent; 4 | 5 | public interface NoteEvent extends DomainEvent { 6 | } 7 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/domain/model/note/NoteId.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.domain.model.note; 2 | 3 | import lombok.Value; 4 | 5 | @Value 6 | public class NoteId { 7 | String value; 8 | } 9 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/domain/model/note/NoteRepository.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.domain.model.note; 2 | 3 | import java.util.Optional; 4 | 5 | import dev.fumin.sample.eventdriven.domain.model.project.ProjectId; 6 | 7 | public interface NoteRepository { 8 | 9 | NoteId newId(); 10 | 11 | void create(Note note); 12 | 13 | void update(Note note); 14 | 15 | Optional noteFrom(ProjectId projectId, NoteId noteId); 16 | 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/domain/model/note/NoteService.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.domain.model.note; 2 | 3 | import javax.inject.Inject; 4 | 5 | import dev.fumin.sample.eventdriven.domain.model.project.Project; 6 | import dev.fumin.sample.eventdriven.domain.model.project.ProjectId; 7 | import dev.fumin.sample.eventdriven.domain.model.project.ProjectRepository; 8 | import dev.fumin.sample.eventdriven.domain.event.DomainEventPublisher; 9 | import dev.fumin.sample.eventdriven.domain.event.DomainResult; 10 | import dev.fumin.sample.eventdriven.domain.model.cloumn.Column; 11 | import dev.fumin.sample.eventdriven.domain.model.cloumn.ColumnId; 12 | import dev.fumin.sample.eventdriven.domain.model.cloumn.ColumnRepository; 13 | 14 | public class NoteService { 15 | 16 | private final ProjectRepository projectRepository; 17 | private final ColumnRepository columnRepository; 18 | private final NoteRepository noteRepository; 19 | private final DomainEventPublisher eventPublisher; 20 | 21 | @Inject 22 | public NoteService( 23 | ProjectRepository projectRepository, 24 | ColumnRepository columnRepository, 25 | NoteRepository noteRepository, 26 | DomainEventPublisher eventPublisher 27 | ) { 28 | this.projectRepository = projectRepository; 29 | this.columnRepository = columnRepository; 30 | this.noteRepository = noteRepository; 31 | this.eventPublisher = eventPublisher; 32 | } 33 | 34 | public Note createNote(ProjectId projectId, ColumnId columnId, String description) { 35 | 36 | Project project = projectRepository.projectOf(projectId) 37 | .orElseThrow(() -> new IllegalArgumentException("specified project is not found.")); 38 | 39 | if (!project.isActive()) { 40 | throw new IllegalStateException("specified project is not active."); 41 | } 42 | 43 | Column column = columnRepository.columnFrom(projectId, columnId) 44 | .orElseThrow(() -> new IllegalArgumentException("specified column is not found.")); 45 | 46 | if (!column.isActive()) { 47 | throw new IllegalStateException("specified column is not active."); 48 | } 49 | 50 | NoteId noteId = noteRepository.newId(); 51 | DomainResult domainResult = Note.create( 52 | noteId, projectId, columnId, description); 53 | 54 | Note note = domainResult.getResult(); 55 | 56 | noteRepository.create(note); 57 | eventPublisher.publish(domainResult.getEvent()); 58 | 59 | return note; 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/domain/model/project/Project.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.domain.model.project; 2 | 3 | import java.util.Optional; 4 | 5 | import dev.fumin.sample.eventdriven.domain.common.ConcurrencyEntity; 6 | import dev.fumin.sample.eventdriven.domain.model.cloumn.Column; 7 | import dev.fumin.sample.eventdriven.domain.model.cloumn.ColumnId; 8 | import lombok.Getter; 9 | 10 | @Getter 11 | public final class Project extends ConcurrencyEntity { 12 | 13 | private final ProjectId id; 14 | private String name; 15 | private boolean active; 16 | 17 | public static Project create(ProjectId id, String name) { 18 | return new Project(id, name, true); 19 | } 20 | 21 | public Project(ProjectId id, String name, boolean active) { 22 | this.id = id; 23 | this.active = active; 24 | setName(name); 25 | } 26 | 27 | public Optional deactivate() { 28 | if (isActive()) { 29 | active = false; 30 | return Optional.of(new ProjectDeactivated(this.id)); 31 | } 32 | return Optional.empty(); 33 | } 34 | 35 | public Optional activate() { 36 | if (!isActive()) { 37 | active = true; 38 | return Optional.of(new ProjectActivated(this.id)); 39 | } 40 | return Optional.empty(); 41 | } 42 | 43 | public void setName(String name) { 44 | if (name == null || name.isBlank()) { 45 | throw new IllegalArgumentException("name must not be null or blank."); 46 | } 47 | this.name = name; 48 | } 49 | 50 | public Column addColumn(ColumnId columnId, String name) { 51 | if (!isActive()) { 52 | throw new IllegalStateException("project is not active"); 53 | } 54 | return Column.create(this.id, columnId, name); 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/domain/model/project/ProjectActivated.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.domain.model.project; 2 | 3 | import lombok.Value; 4 | 5 | @Value 6 | public class ProjectActivated implements ProjectEvent { 7 | ProjectId projectId; 8 | } 9 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/domain/model/project/ProjectDeactivated.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.domain.model.project; 2 | 3 | import lombok.Value; 4 | 5 | @Value 6 | public class ProjectDeactivated implements ProjectEvent { 7 | 8 | ProjectId projectId; 9 | 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/domain/model/project/ProjectEvent.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.domain.model.project; 2 | 3 | import dev.fumin.sample.eventdriven.domain.event.DomainEvent; 4 | 5 | public interface ProjectEvent extends DomainEvent { 6 | } 7 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/domain/model/project/ProjectId.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.domain.model.project; 2 | 3 | import lombok.Value; 4 | 5 | @Value 6 | public class ProjectId { 7 | 8 | String value; 9 | 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/domain/model/project/ProjectRepository.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.domain.model.project; 2 | 3 | import java.util.Optional; 4 | 5 | public interface ProjectRepository { 6 | 7 | ProjectId newId(); 8 | 9 | void create(Project project); 10 | 11 | void update(Project project); 12 | 13 | Optional projectOf(ProjectId id); 14 | 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/infrastructure/di/InfrastructureModule.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.infrastructure.di; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.GsonBuilder; 5 | import com.google.inject.matcher.Matchers; 6 | import com.google.inject.servlet.ServletModule; 7 | 8 | import java.util.Date; 9 | 10 | import dev.fumin.sample.eventdriven.application.event.EventDispatcher; 11 | import dev.fumin.sample.eventdriven.application.event.EventStore; 12 | import dev.fumin.sample.eventdriven.application.persistence.Transactional; 13 | import dev.fumin.sample.eventdriven.domain.model.cloumn.ColumnRepository; 14 | import dev.fumin.sample.eventdriven.domain.model.google.tasks.TaskCreationService; 15 | import dev.fumin.sample.eventdriven.domain.model.note.NoteRepository; 16 | import dev.fumin.sample.eventdriven.domain.model.project.ProjectRepository; 17 | import dev.fumin.sample.eventdriven.infrastructure.event.MySqlConsumedEventStore; 18 | import dev.fumin.sample.eventdriven.infrastructure.event.PubsubEventDispatcher; 19 | import dev.fumin.sample.eventdriven.infrastructure.persistence.doma.DomaConfig; 20 | import dev.fumin.sample.eventdriven.infrastructure.persistence.doma.DomaTransactionInterceptor; 21 | import dev.fumin.sample.eventdriven.presentation.event.ConsumedEventStore; 22 | import dev.fumin.sample.eventdriven.infrastructure.event.DomainEventResetFilter; 23 | import dev.fumin.sample.eventdriven.infrastructure.event.MySqlEventStore; 24 | import dev.fumin.sample.eventdriven.infrastructure.persistence.dao.ColumnDao; 25 | import dev.fumin.sample.eventdriven.infrastructure.persistence.dao.ColumnDaoImpl; 26 | import dev.fumin.sample.eventdriven.infrastructure.persistence.dao.ConsumedEventDao; 27 | import dev.fumin.sample.eventdriven.infrastructure.persistence.dao.ConsumedEventDaoImpl; 28 | import dev.fumin.sample.eventdriven.infrastructure.persistence.dao.EventDao; 29 | import dev.fumin.sample.eventdriven.infrastructure.persistence.dao.EventDaoImpl; 30 | import dev.fumin.sample.eventdriven.infrastructure.persistence.dao.NoteDao; 31 | import dev.fumin.sample.eventdriven.infrastructure.persistence.dao.NoteDaoImpl; 32 | import dev.fumin.sample.eventdriven.infrastructure.persistence.dao.ProjectDao; 33 | import dev.fumin.sample.eventdriven.infrastructure.persistence.dao.ProjectDaoImpl; 34 | import dev.fumin.sample.eventdriven.infrastructure.persistence.repository.MySqlColumnRepository; 35 | import dev.fumin.sample.eventdriven.infrastructure.persistence.repository.MySqlNoteRepository; 36 | import dev.fumin.sample.eventdriven.infrastructure.persistence.repository.MySqlProjectRepository; 37 | import dev.fumin.sample.eventdriven.infrastructure.pubsub.LocalSafePublisher; 38 | import dev.fumin.sample.eventdriven.infrastructure.pubsub.RemoteSafePublisher; 39 | import dev.fumin.sample.eventdriven.infrastructure.pubsub.SafePublisher; 40 | import dev.fumin.sample.eventdriven.infrastructure.serialization.DateTypeAdapter; 41 | import dev.fumin.sample.eventdriven.infrastructure.service.DummyTaskCreationService; 42 | 43 | public class InfrastructureModule extends ServletModule { 44 | 45 | @Override 46 | protected void configureServlets() { 47 | super.configureServlets(); 48 | 49 | Gson gson = new GsonBuilder() 50 | .registerTypeAdapter(Date.class, new DateTypeAdapter()) 51 | .create(); 52 | bind(Gson.class).toInstance(gson); 53 | 54 | // pubsub 55 | String port = System.getenv("PUBSUB_EMULATOR_HOST"); 56 | if (port != null) { 57 | System.out.println("emulator port : " + port); 58 | bind(SafePublisher.class).toInstance(new LocalSafePublisher(port)); 59 | } else { 60 | bind(SafePublisher.class).to(RemoteSafePublisher.class); 61 | } 62 | 63 | // dao 64 | bind(ProjectDao.class).toInstance(new ProjectDaoImpl(DomaConfig.getInstance())); 65 | bind(ColumnDao.class).toInstance(new ColumnDaoImpl(DomaConfig.getInstance())); 66 | bind(NoteDao.class).toInstance(new NoteDaoImpl(DomaConfig.getInstance())); 67 | bind(EventDao.class).toInstance(new EventDaoImpl(DomaConfig.getInstance())); 68 | bind(ConsumedEventDao.class).toInstance(new ConsumedEventDaoImpl(DomaConfig.getInstance())); 69 | 70 | // repository 71 | bind(ProjectRepository.class).to(MySqlProjectRepository.class); 72 | bind(ColumnRepository.class).to(MySqlColumnRepository.class); 73 | bind(NoteRepository.class).to(MySqlNoteRepository.class); 74 | 75 | // interceptor 76 | bindInterceptor(Matchers.any(), Matchers.annotatedWith(Transactional.class), 77 | new DomaTransactionInterceptor(DomaConfig.getInstance())); 78 | 79 | // event 80 | bind(EventStore.class).to(MySqlEventStore.class); 81 | bind(EventDispatcher.class).to(PubsubEventDispatcher.class); 82 | bind(ConsumedEventStore.class).to(MySqlConsumedEventStore.class); 83 | 84 | // service 85 | bind(TaskCreationService.class).to(DummyTaskCreationService.class); 86 | 87 | 88 | // filter 89 | filter("/*").through(DomainEventResetFilter.class); 90 | 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/infrastructure/event/DomainEventResetFilter.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.infrastructure.event; 2 | 3 | 4 | import java.io.IOException; 5 | 6 | import javax.inject.Inject; 7 | import javax.inject.Singleton; 8 | import javax.servlet.Filter; 9 | import javax.servlet.FilterChain; 10 | import javax.servlet.FilterConfig; 11 | import javax.servlet.ServletException; 12 | import javax.servlet.ServletRequest; 13 | import javax.servlet.ServletResponse; 14 | import javax.servlet.annotation.WebFilter; 15 | 16 | import dev.fumin.sample.eventdriven.domain.event.DomainEventPublisher; 17 | 18 | @Singleton 19 | @WebFilter(urlPatterns = "/*") 20 | public class DomainEventResetFilter implements Filter { 21 | 22 | @Inject 23 | private DomainEventPublisher publisher; 24 | 25 | @Override 26 | public void init(FilterConfig filterConfig) throws ServletException { 27 | 28 | } 29 | 30 | @Override 31 | public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { 32 | // スレッドがプールされ、別のリクエストによってスレッドが再利用されるので、リクエスト毎にPublisherをリセットする。 33 | publisher.reset(); 34 | chain.doFilter(request, response); 35 | } 36 | 37 | @Override 38 | public void destroy() { 39 | 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/infrastructure/event/DomainEventSerializer.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.infrastructure.event; 2 | 3 | import com.google.gson.Gson; 4 | 5 | import javax.inject.Inject; 6 | 7 | import dev.fumin.sample.eventdriven.domain.event.DomainEvent; 8 | 9 | public class DomainEventSerializer { 10 | 11 | private final Gson gson; 12 | 13 | @Inject 14 | public DomainEventSerializer(Gson gson) { 15 | this.gson = gson; 16 | } 17 | 18 | public String serialize(DomainEvent event) { 19 | return gson.toJson(event); 20 | } 21 | 22 | @SuppressWarnings("unchecked") 23 | public T deserialize(String source, String eventName) { 24 | try { 25 | Class clazz = Class.forName(eventName); 26 | return (T) gson.fromJson(source, clazz); 27 | } catch (ClassNotFoundException e) { 28 | throw new RuntimeException(e); 29 | } 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/infrastructure/event/EventTranslator.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.infrastructure.event; 2 | 3 | import java.sql.Timestamp; 4 | import java.util.Date; 5 | 6 | import javax.inject.Inject; 7 | 8 | import dev.fumin.sample.eventdriven.application.event.StoredEvent; 9 | import dev.fumin.sample.eventdriven.infrastructure.persistence.entity.EventDto; 10 | import dev.fumin.sample.eventdriven.domain.event.DomainEvent; 11 | 12 | public class EventTranslator { 13 | 14 | private final DomainEventSerializer serializer; 15 | 16 | @Inject 17 | public EventTranslator(DomainEventSerializer serializer) { 18 | this.serializer = serializer; 19 | } 20 | 21 | public StoredEvent toModel(EventDto dto) { 22 | T event = serializer.deserialize(dto.getEvent(), dto.getEventName()); 23 | return new StoredEvent<>( 24 | dto.getId(), 25 | event, 26 | dto.getEventName(), 27 | new Date(dto.getStoredAt().getTime()), 28 | dto.getVersion() 29 | ); 30 | } 31 | 32 | public EventDto toDto(StoredEvent storedEvent) { 33 | EventDto dto = new EventDto(); 34 | storedEvent.getId().ifPresent(dto::setId); 35 | dto.setEvent(serializer.serialize(storedEvent.getEvent())); 36 | dto.setEventName(storedEvent.getEventName()); 37 | dto.setStoredAt(new Timestamp(storedEvent.getStoredAt().getTime())); 38 | dto.setVersion(storedEvent.getVersion()); 39 | return dto; 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/infrastructure/event/MySqlConsumedEventStore.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.infrastructure.event; 2 | 3 | import java.sql.Timestamp; 4 | 5 | import javax.inject.Inject; 6 | 7 | import dev.fumin.sample.eventdriven.infrastructure.persistence.dao.ConsumedEventDao; 8 | import dev.fumin.sample.eventdriven.presentation.event.ConsumedEvent; 9 | import dev.fumin.sample.eventdriven.presentation.event.ConsumedEventStore; 10 | import dev.fumin.sample.eventdriven.infrastructure.persistence.entity.ConsumedEventDto; 11 | 12 | public class MySqlConsumedEventStore implements ConsumedEventStore { 13 | 14 | private final ConsumedEventDao consumedEventDao; 15 | 16 | @Inject 17 | public MySqlConsumedEventStore(ConsumedEventDao consumedEventDao) { 18 | this.consumedEventDao = consumedEventDao; 19 | } 20 | 21 | @Override 22 | public boolean exists(long eventId, String receiver) { 23 | return consumedEventDao.exists(eventId, receiver); 24 | } 25 | 26 | @Override 27 | public void insert(ConsumedEvent event) { 28 | ConsumedEventDto dto = new ConsumedEventDto(); 29 | dto.setId(event.getEventId()); 30 | dto.setReceiver(event.getReceiver()); 31 | dto.setReceivedAt(new Timestamp(event.getReceivedAt().getTime())); 32 | consumedEventDao.insert(dto); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/infrastructure/event/MySqlEventStore.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.infrastructure.event; 2 | 3 | import java.util.Optional; 4 | 5 | import javax.inject.Inject; 6 | 7 | import dev.fumin.sample.eventdriven.application.event.EventStore; 8 | import dev.fumin.sample.eventdriven.application.event.StoredEvent; 9 | import dev.fumin.sample.eventdriven.infrastructure.persistence.dao.EventDao; 10 | import dev.fumin.sample.eventdriven.infrastructure.persistence.entity.EventDto; 11 | import dev.fumin.sample.eventdriven.domain.event.DomainEvent; 12 | 13 | public class MySqlEventStore implements EventStore { 14 | 15 | private final EventDao eventDao; 16 | private final EventTranslator translator; 17 | 18 | @Inject 19 | public MySqlEventStore(EventDao eventDao, EventTranslator translator) { 20 | this.eventDao = eventDao; 21 | this.translator = translator; 22 | } 23 | 24 | @Override 25 | public StoredEvent save(StoredEvent event) { 26 | EventDto dto = translator.toDto(event); 27 | eventDao.insert(dto); 28 | event.setId(dto.getId()); 29 | return event; 30 | } 31 | 32 | @Override 33 | public Optional> fetchById(long id) { 34 | return eventDao.selectById(id).map(translator::toModel); 35 | } 36 | 37 | @Override 38 | public void delete(StoredEvent event) { 39 | eventDao.delete(translator.toDto(event)); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/infrastructure/event/PubsubEventDispatcher.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.infrastructure.event; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.protobuf.ByteString; 5 | import com.google.pubsub.v1.PubsubMessage; 6 | import com.google.pubsub.v1.TopicName; 7 | 8 | import java.util.HashMap; 9 | import java.util.Map; 10 | 11 | import javax.inject.Inject; 12 | 13 | import dev.fumin.sample.eventdriven.application.event.EventDispatcher; 14 | import dev.fumin.sample.eventdriven.domain.model.note.NoteCreated; 15 | import dev.fumin.sample.eventdriven.domain.model.project.ProjectActivated; 16 | import dev.fumin.sample.eventdriven.domain.model.project.ProjectDeactivated; 17 | import dev.fumin.sample.eventdriven.domain.event.DomainEvent; 18 | import dev.fumin.sample.eventdriven.infrastructure.pubsub.SafePublisher; 19 | 20 | public class PubsubEventDispatcher implements EventDispatcher { 21 | 22 | private static final String PROJECT_ID = System.getenv("GOOGLE_CLOUD_PROJECT"); 23 | private static final Map, String> TOPICS = new HashMap<>() { 24 | { 25 | put(NoteCreated.class, "note_created"); 26 | put(ProjectDeactivated.class, "project_deactivated"); 27 | put(ProjectActivated.class, "project_activated"); 28 | } 29 | }; 30 | 31 | private final SafePublisher safePublisher; 32 | private final Gson gson; 33 | 34 | @Inject 35 | public PubsubEventDispatcher(SafePublisher safePublisher, Gson gson) { 36 | this.safePublisher = safePublisher; 37 | this.gson = gson; 38 | } 39 | 40 | @Override 41 | public void dispatchToProxy(long eventId) { 42 | TopicName topicName = TopicName.of(PROJECT_ID, "proxy"); 43 | safePublisher.publish(topicName, publisher -> { 44 | PubsubMessage message = PubsubMessage.newBuilder() 45 | .putAttributes("eventId", "" + eventId) 46 | .build(); 47 | publisher.publish(message).get(); 48 | }); 49 | } 50 | 51 | @Override 52 | public void dispatch(long eventId, DomainEvent event) { 53 | String name = TOPICS.get(event.getClass()); 54 | TopicName topicName = TopicName.of(PROJECT_ID, name); 55 | safePublisher.publish(topicName, publisher -> { 56 | String payload = gson.toJson(event); 57 | ByteString data = ByteString.copyFromUtf8(payload); 58 | PubsubMessage message = PubsubMessage.newBuilder() 59 | .putAttributes("eventId", "" + eventId) 60 | .setData(data) 61 | .build(); 62 | publisher.publish(message).get(); 63 | }); 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/infrastructure/persistence/dao/ColumnDao.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.infrastructure.persistence.dao; 2 | 3 | import org.seasar.doma.Dao; 4 | import org.seasar.doma.Insert; 5 | import org.seasar.doma.Select; 6 | import org.seasar.doma.Update; 7 | 8 | import java.util.Optional; 9 | 10 | import dev.fumin.sample.eventdriven.infrastructure.persistence.entity.ColumnDto; 11 | 12 | @Dao 13 | public interface ColumnDao { 14 | 15 | @Insert 16 | int insert(ColumnDto dto); 17 | 18 | @Update 19 | int update(ColumnDto dto); 20 | 21 | @Select 22 | Optional selectById(String id, String projectId); 23 | 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/infrastructure/persistence/dao/ConsumedEventDao.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.infrastructure.persistence.dao; 2 | 3 | import org.seasar.doma.Dao; 4 | import org.seasar.doma.Insert; 5 | import org.seasar.doma.Select; 6 | 7 | import dev.fumin.sample.eventdriven.infrastructure.persistence.entity.ConsumedEventDto; 8 | 9 | @Dao 10 | public interface ConsumedEventDao { 11 | 12 | @Select 13 | boolean exists(long id, String receiver); 14 | 15 | @Insert 16 | int insert(ConsumedEventDto dto); 17 | 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/infrastructure/persistence/dao/EventDao.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.infrastructure.persistence.dao; 2 | 3 | import org.seasar.doma.Dao; 4 | import org.seasar.doma.Delete; 5 | import org.seasar.doma.Insert; 6 | import org.seasar.doma.Select; 7 | 8 | import java.util.Optional; 9 | 10 | import dev.fumin.sample.eventdriven.infrastructure.persistence.entity.EventDto; 11 | 12 | @Dao 13 | public interface EventDao { 14 | 15 | @Insert 16 | int insert(EventDto dto); 17 | 18 | @Delete 19 | int delete(EventDto dto); 20 | 21 | @Select 22 | Optional selectById(long id); 23 | 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/infrastructure/persistence/dao/NoteDao.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.infrastructure.persistence.dao; 2 | 3 | import org.seasar.doma.Dao; 4 | import org.seasar.doma.Insert; 5 | import org.seasar.doma.Select; 6 | import org.seasar.doma.Update; 7 | 8 | import java.util.Optional; 9 | 10 | import dev.fumin.sample.eventdriven.infrastructure.persistence.entity.NoteDto; 11 | 12 | @Dao 13 | public interface NoteDao { 14 | 15 | @Insert 16 | int insert(NoteDto dto); 17 | 18 | @Update 19 | int update(NoteDto dto); 20 | 21 | @Select 22 | Optional selectById(String id, String projectId); 23 | 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/infrastructure/persistence/dao/ProjectDao.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.infrastructure.persistence.dao; 2 | 3 | import org.seasar.doma.Dao; 4 | import org.seasar.doma.Insert; 5 | import org.seasar.doma.Select; 6 | import org.seasar.doma.Update; 7 | 8 | import java.util.Optional; 9 | 10 | import dev.fumin.sample.eventdriven.infrastructure.persistence.entity.ProjectDto; 11 | 12 | @Dao 13 | public interface ProjectDao { 14 | 15 | @Insert 16 | int insert(ProjectDto dto); 17 | 18 | @Update 19 | int update(ProjectDto dto); 20 | 21 | @Select 22 | Optional selectById(String id); 23 | 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/infrastructure/persistence/doma/DomaConfig.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.infrastructure.persistence.doma; 2 | 3 | import com.zaxxer.hikari.HikariConfig; 4 | import com.zaxxer.hikari.HikariDataSource; 5 | 6 | import org.seasar.doma.jdbc.Config; 7 | import org.seasar.doma.jdbc.dialect.Dialect; 8 | import org.seasar.doma.jdbc.dialect.MysqlDialect; 9 | import org.seasar.doma.jdbc.tx.LocalTransactionDataSource; 10 | import org.seasar.doma.jdbc.tx.LocalTransactionManager; 11 | import org.seasar.doma.jdbc.tx.TransactionManager; 12 | 13 | import javax.sql.DataSource; 14 | 15 | public class DomaConfig implements Config { 16 | 17 | private static final DomaConfig INSTANCE; 18 | 19 | private final Dialect dialect; 20 | 21 | private final LocalTransactionDataSource dataSource; 22 | 23 | private final TransactionManager transactionManager; 24 | 25 | static { 26 | synchronized (DomaConfig.class) { 27 | INSTANCE = new DomaConfig(); 28 | } 29 | } 30 | 31 | private DomaConfig() { 32 | dialect = new MysqlDialect(); 33 | dataSource = createDataSource(); 34 | transactionManager = new LocalTransactionManager(dataSource.getLocalTransaction(getJdbcLogger())); 35 | } 36 | 37 | public static DomaConfig getInstance() { 38 | return INSTANCE; 39 | } 40 | 41 | @Override 42 | public DataSource getDataSource() { 43 | return dataSource; 44 | } 45 | 46 | @Override 47 | public Dialect getDialect() { 48 | return dialect; 49 | } 50 | 51 | @Override 52 | public TransactionManager getTransactionManager() { 53 | return transactionManager; 54 | } 55 | 56 | private LocalTransactionDataSource createDataSource() { 57 | String appId = System.getenv("GAE_APPLICATION"); 58 | HikariConfig config = new HikariConfig(); 59 | 60 | if (appId == null) { 61 | // for local 62 | config.setJdbcUrl("jdbc:mysql://localhost:3306/eventdriven_db"); 63 | config.setUsername("myuser"); 64 | config.setPassword("mypassword"); 65 | try { 66 | Class.forName("com.mysql.jdbc.Driver"); 67 | } catch (ClassNotFoundException e) { 68 | throw new RuntimeException(e); 69 | } 70 | } else { 71 | // for CloudSQL 72 | config.setJdbcUrl("set your cloud sql jdbc url"); 73 | config.setUsername("set your db user name"); 74 | config.setPassword("set your db user password"); 75 | config.addDataSourceProperty("socketFactory", 76 | "com.google.cloud.sql.mysql.SocketFactory"); 77 | config.addDataSourceProperty("cloudSqlInstance", "set your instance name"); 78 | config.addDataSourceProperty("useSSL", "false"); 79 | } 80 | 81 | return new LocalTransactionDataSource(new HikariDataSource(config)); 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/infrastructure/persistence/doma/DomaTransactionInterceptor.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.infrastructure.persistence.doma; 2 | 3 | import org.aopalliance.intercept.MethodInterceptor; 4 | import org.aopalliance.intercept.MethodInvocation; 5 | import org.seasar.doma.jdbc.Config; 6 | import org.seasar.doma.jdbc.tx.TransactionManager; 7 | 8 | public class DomaTransactionInterceptor implements MethodInterceptor { 9 | 10 | private final Config config; 11 | 12 | public DomaTransactionInterceptor(Config config) { 13 | this.config = config; 14 | } 15 | 16 | @Override 17 | public Object invoke(MethodInvocation invocation) throws Throwable { 18 | TransactionManager tm = config.getTransactionManager(); 19 | try { 20 | return tm.required(() -> { 21 | try { 22 | return invocation.proceed(); 23 | } catch (Throwable t) { 24 | throw new RuntimeException(t); 25 | } 26 | }); 27 | } catch (Exception e) { 28 | throw e.getCause(); 29 | } 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/infrastructure/persistence/entity/BaseDto.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.infrastructure.persistence.entity; 2 | 3 | import org.seasar.doma.Entity; 4 | import org.seasar.doma.jdbc.entity.NamingType; 5 | 6 | import java.sql.Timestamp; 7 | 8 | import lombok.Getter; 9 | import lombok.Setter; 10 | 11 | @Entity(naming = NamingType.SNAKE_LOWER_CASE) 12 | @Getter 13 | @Setter 14 | public abstract class BaseDto { 15 | 16 | private Timestamp createdAt; 17 | 18 | private Timestamp updatedAt; 19 | 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/infrastructure/persistence/entity/BaseDtoListener.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.infrastructure.persistence.entity; 2 | 3 | import org.seasar.doma.jdbc.entity.EntityListener; 4 | import org.seasar.doma.jdbc.entity.PreInsertContext; 5 | import org.seasar.doma.jdbc.entity.PreUpdateContext; 6 | 7 | import java.sql.Timestamp; 8 | 9 | public class BaseDtoListener implements EntityListener { 10 | 11 | @Override 12 | public void preInsert(T t, PreInsertContext context) { 13 | Timestamp now = new Timestamp(System.currentTimeMillis()); 14 | t.setCreatedAt(now); 15 | t.setUpdatedAt(now); 16 | } 17 | 18 | @Override 19 | public void preUpdate(T t, PreUpdateContext context) { 20 | Timestamp now = new Timestamp(System.currentTimeMillis()); 21 | t.setUpdatedAt(now); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/infrastructure/persistence/entity/ColumnDto.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.infrastructure.persistence.entity; 2 | 3 | import org.seasar.doma.Entity; 4 | import org.seasar.doma.Id; 5 | import org.seasar.doma.Table; 6 | import org.seasar.doma.jdbc.entity.NamingType; 7 | 8 | import lombok.Getter; 9 | import lombok.Setter; 10 | 11 | @Entity(naming = NamingType.SNAKE_LOWER_CASE, listener = BaseDtoListener.class) 12 | @Table(name = "`column`") 13 | @Getter 14 | @Setter 15 | public class ColumnDto extends ConcurrencyDto { 16 | 17 | @Id 18 | private String id; 19 | private String projectId; 20 | private String name; 21 | private boolean active; 22 | 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/infrastructure/persistence/entity/ConcurrencyDto.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.infrastructure.persistence.entity; 2 | 3 | import org.seasar.doma.Entity; 4 | import org.seasar.doma.Version; 5 | import org.seasar.doma.jdbc.entity.NamingType; 6 | 7 | import lombok.Getter; 8 | import lombok.Setter; 9 | 10 | @Entity(naming = NamingType.SNAKE_LOWER_CASE) 11 | @Getter 12 | @Setter 13 | public abstract class ConcurrencyDto extends BaseDto { 14 | 15 | @Version 16 | private long version; 17 | 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/infrastructure/persistence/entity/ConsumedEventDto.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.infrastructure.persistence.entity; 2 | 3 | import org.seasar.doma.Entity; 4 | import org.seasar.doma.Id; 5 | import org.seasar.doma.Table; 6 | import org.seasar.doma.jdbc.entity.NamingType; 7 | 8 | import java.sql.Timestamp; 9 | 10 | import lombok.Getter; 11 | import lombok.Setter; 12 | 13 | @Entity(naming = NamingType.SNAKE_LOWER_CASE) 14 | @Table(name = "consumed_event") 15 | @Getter 16 | @Setter 17 | public class ConsumedEventDto { 18 | 19 | @Id 20 | long id; 21 | 22 | @Id 23 | String receiver; 24 | 25 | Timestamp receivedAt; 26 | 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/infrastructure/persistence/entity/EventDto.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.infrastructure.persistence.entity; 2 | 3 | import org.seasar.doma.Entity; 4 | import org.seasar.doma.GeneratedValue; 5 | import org.seasar.doma.GenerationType; 6 | import org.seasar.doma.Id; 7 | import org.seasar.doma.Table; 8 | import org.seasar.doma.Version; 9 | import org.seasar.doma.jdbc.entity.NamingType; 10 | 11 | import java.sql.Timestamp; 12 | 13 | import lombok.Getter; 14 | import lombok.Setter; 15 | 16 | @Entity(naming = NamingType.SNAKE_LOWER_CASE) 17 | @Table(name = "event") 18 | @Getter 19 | @Setter 20 | public class EventDto { 21 | 22 | @Id 23 | @GeneratedValue(strategy = GenerationType.IDENTITY) 24 | private long id = -1; 25 | 26 | private String event; 27 | 28 | private String eventName; 29 | 30 | private Timestamp storedAt; 31 | 32 | @Version 33 | private long version; 34 | 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/infrastructure/persistence/entity/NoteDto.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.infrastructure.persistence.entity; 2 | 3 | import org.seasar.doma.Entity; 4 | import org.seasar.doma.Id; 5 | import org.seasar.doma.Table; 6 | import org.seasar.doma.jdbc.entity.NamingType; 7 | 8 | import lombok.Getter; 9 | import lombok.Setter; 10 | 11 | @Entity(naming = NamingType.SNAKE_LOWER_CASE, listener = BaseDtoListener.class) 12 | @Table(name = "note") 13 | @Getter 14 | @Setter 15 | public class NoteDto extends ConcurrencyDto { 16 | 17 | @Id 18 | private String id; 19 | private String projectId; 20 | private String columnId; 21 | private String description; 22 | 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/infrastructure/persistence/entity/ProjectDto.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.infrastructure.persistence.entity; 2 | 3 | import org.seasar.doma.Entity; 4 | import org.seasar.doma.Table; 5 | import org.seasar.doma.jdbc.entity.NamingType; 6 | 7 | import lombok.Getter; 8 | import lombok.Setter; 9 | 10 | @Entity(naming = NamingType.SNAKE_LOWER_CASE, listener = BaseDtoListener.class) 11 | @Table(name = "project") 12 | @Getter 13 | @Setter 14 | public class ProjectDto extends ConcurrencyDto { 15 | 16 | private String id; 17 | private String name; 18 | private boolean active; 19 | 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/infrastructure/persistence/repository/MySqlColumnRepository.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.infrastructure.persistence.repository; 2 | 3 | import java.util.Optional; 4 | import java.util.UUID; 5 | 6 | import javax.inject.Inject; 7 | 8 | import dev.fumin.sample.eventdriven.domain.model.cloumn.Column; 9 | import dev.fumin.sample.eventdriven.domain.model.cloumn.ColumnId; 10 | import dev.fumin.sample.eventdriven.domain.model.cloumn.ColumnRepository; 11 | import dev.fumin.sample.eventdriven.domain.model.project.ProjectId; 12 | import dev.fumin.sample.eventdriven.infrastructure.persistence.dao.ColumnDao; 13 | import dev.fumin.sample.eventdriven.infrastructure.persistence.translation.ColumnTranslator; 14 | 15 | public class MySqlColumnRepository implements ColumnRepository { 16 | 17 | private final ColumnDao dao; 18 | private final ColumnTranslator translator; 19 | 20 | @Inject 21 | public MySqlColumnRepository(ColumnDao dao, ColumnTranslator translator) { 22 | this.dao = dao; 23 | this.translator = translator; 24 | } 25 | 26 | @Override 27 | public ColumnId newId() { 28 | return new ColumnId(UUID.randomUUID().toString()); 29 | } 30 | 31 | @Override 32 | public void create(Column column) { 33 | dao.insert(translator.toDto(column)); 34 | } 35 | 36 | @Override 37 | public void update(Column column) { 38 | dao.update(translator.toDto(column)); 39 | } 40 | 41 | @Override 42 | public Optional columnFrom(ProjectId projectId, ColumnId columnId) { 43 | return dao.selectById(columnId.getValue(), projectId.getValue()).map(translator::toModel); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/infrastructure/persistence/repository/MySqlNoteRepository.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.infrastructure.persistence.repository; 2 | 3 | import java.util.Optional; 4 | import java.util.UUID; 5 | 6 | import javax.inject.Inject; 7 | 8 | import dev.fumin.sample.eventdriven.domain.model.note.Note; 9 | import dev.fumin.sample.eventdriven.domain.model.note.NoteId; 10 | import dev.fumin.sample.eventdriven.domain.model.note.NoteRepository; 11 | import dev.fumin.sample.eventdriven.domain.model.project.ProjectId; 12 | import dev.fumin.sample.eventdriven.infrastructure.persistence.dao.NoteDao; 13 | import dev.fumin.sample.eventdriven.infrastructure.persistence.translation.NoteTranslator; 14 | 15 | public class MySqlNoteRepository implements NoteRepository { 16 | 17 | private final NoteDao dao; 18 | private final NoteTranslator translator; 19 | 20 | @Inject 21 | public MySqlNoteRepository(NoteDao dao, NoteTranslator translator) { 22 | this.dao = dao; 23 | this.translator = translator; 24 | } 25 | 26 | @Override 27 | public NoteId newId() { 28 | return new NoteId(UUID.randomUUID().toString()); 29 | } 30 | 31 | @Override 32 | public void create(Note note) { 33 | dao.insert(translator.toDto(note)); 34 | } 35 | 36 | @Override 37 | public void update(Note note) { 38 | dao.update(translator.toDto(note)); 39 | } 40 | 41 | @Override 42 | public Optional noteFrom(ProjectId projectId, NoteId noteId) { 43 | return dao.selectById(noteId.getValue(), projectId.getValue()).map(translator::toModel); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/infrastructure/persistence/repository/MySqlProjectRepository.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.infrastructure.persistence.repository; 2 | 3 | import java.util.Optional; 4 | import java.util.UUID; 5 | 6 | import javax.inject.Inject; 7 | 8 | import dev.fumin.sample.eventdriven.domain.model.project.Project; 9 | import dev.fumin.sample.eventdriven.domain.model.project.ProjectId; 10 | import dev.fumin.sample.eventdriven.domain.model.project.ProjectRepository; 11 | import dev.fumin.sample.eventdriven.infrastructure.persistence.dao.ProjectDao; 12 | import dev.fumin.sample.eventdriven.infrastructure.persistence.translation.ProjectTranslator; 13 | 14 | public class MySqlProjectRepository implements ProjectRepository { 15 | 16 | private final ProjectDao dao; 17 | private final ProjectTranslator translator; 18 | 19 | @Inject 20 | public MySqlProjectRepository(ProjectDao dao, ProjectTranslator translator) { 21 | this.dao = dao; 22 | this.translator = translator; 23 | } 24 | 25 | @Override 26 | public ProjectId newId() { 27 | return new ProjectId(UUID.randomUUID().toString()); 28 | } 29 | 30 | @Override 31 | public void create(Project project) { 32 | dao.insert(translator.toDto(project)); 33 | } 34 | 35 | @Override 36 | public void update(Project project) { 37 | dao.update(translator.toDto(project)); 38 | } 39 | 40 | @Override 41 | public Optional projectOf(ProjectId id) { 42 | return dao.selectById(id.getValue()).map(translator::toModel); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/infrastructure/persistence/translation/BaseTranslator.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.infrastructure.persistence.translation; 2 | 3 | import java.sql.Date; 4 | import java.sql.Timestamp; 5 | 6 | import dev.fumin.sample.eventdriven.domain.common.Entity; 7 | import dev.fumin.sample.eventdriven.infrastructure.persistence.entity.BaseDto; 8 | 9 | public abstract class BaseTranslator 10 | implements DataTranslator { 11 | 12 | protected void attachToDto(M model, D dto) { 13 | dto.setCreatedAt(new Timestamp(model.getCreatedAt().getTime())); 14 | dto.setUpdatedAt(new Timestamp(model.getUpdatedAt().getTime())); 15 | } 16 | 17 | protected void attachToModel(D dto, M model) { 18 | model.setCreatedAt(new Date(dto.getCreatedAt().getTime())); 19 | model.setUpdatedAt(new Date(dto.getUpdatedAt().getTime())); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/infrastructure/persistence/translation/ColumnTranslator.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.infrastructure.persistence.translation; 2 | 3 | import dev.fumin.sample.eventdriven.domain.model.cloumn.Column; 4 | import dev.fumin.sample.eventdriven.domain.model.cloumn.ColumnId; 5 | import dev.fumin.sample.eventdriven.domain.model.project.ProjectId; 6 | import dev.fumin.sample.eventdriven.infrastructure.persistence.entity.ColumnDto; 7 | 8 | public class ColumnTranslator extends ConcurrencyDataTranslator { 9 | 10 | @Override 11 | public Column toModel(ColumnDto dto) { 12 | Column column = new Column( 13 | new ColumnId(dto.getId()), 14 | new ProjectId(dto.getProjectId()), 15 | dto.getName(), 16 | dto.isActive() 17 | ); 18 | attachToModel(dto, column); 19 | return column; 20 | } 21 | 22 | @Override 23 | public ColumnDto toDto(Column column) { 24 | ColumnDto dto = new ColumnDto(); 25 | dto.setId(column.getId().getValue()); 26 | dto.setProjectId(column.getProjectId().getValue()); 27 | dto.setName(column.getName()); 28 | dto.setActive(column.isActive()); 29 | attachToDto(column, dto); 30 | return dto; 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/infrastructure/persistence/translation/ConcurrencyDataTranslator.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.infrastructure.persistence.translation; 2 | 3 | import dev.fumin.sample.eventdriven.domain.common.ConcurrencyEntity; 4 | import dev.fumin.sample.eventdriven.infrastructure.persistence.entity.ConcurrencyDto; 5 | 6 | public abstract class ConcurrencyDataTranslator 7 | extends BaseTranslator { 8 | 9 | @Override 10 | protected void attachToDto(M model, D dto) { 11 | super.attachToDto(model, dto); 12 | dto.setVersion(model.getVersion()); 13 | } 14 | 15 | @Override 16 | protected void attachToModel(D dto, M model) { 17 | super.attachToModel(dto, model); 18 | model.setVersion(dto.getVersion()); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/infrastructure/persistence/translation/DataTranslator.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.infrastructure.persistence.translation; 2 | 3 | public interface DataTranslator { 4 | 5 | Model toModel(Dto dto); 6 | 7 | Dto toDto(Model model); 8 | 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/infrastructure/persistence/translation/NoteTranslator.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.infrastructure.persistence.translation; 2 | 3 | import dev.fumin.sample.eventdriven.domain.model.cloumn.ColumnId; 4 | import dev.fumin.sample.eventdriven.domain.model.note.Note; 5 | import dev.fumin.sample.eventdriven.domain.model.note.NoteId; 6 | import dev.fumin.sample.eventdriven.domain.model.project.ProjectId; 7 | import dev.fumin.sample.eventdriven.infrastructure.persistence.entity.NoteDto; 8 | 9 | public class NoteTranslator extends ConcurrencyDataTranslator { 10 | 11 | @Override 12 | public Note toModel(NoteDto dto) { 13 | Note note = new Note( 14 | new NoteId(dto.getId()), 15 | new ProjectId(dto.getProjectId()), 16 | new ColumnId(dto.getColumnId()), 17 | dto.getDescription() 18 | ); 19 | attachToModel(dto, note); 20 | return note; 21 | } 22 | 23 | @Override 24 | public NoteDto toDto(Note note) { 25 | NoteDto dto = new NoteDto(); 26 | dto.setId(note.getId().getValue()); 27 | dto.setProjectId(note.getProjectId().getValue()); 28 | dto.setColumnId(note.getColumnId().getValue()); 29 | dto.setDescription(note.getDescription()); 30 | attachToDto(note, dto); 31 | return dto; 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/infrastructure/persistence/translation/ProjectTranslator.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.infrastructure.persistence.translation; 2 | 3 | import dev.fumin.sample.eventdriven.domain.model.project.Project; 4 | import dev.fumin.sample.eventdriven.domain.model.project.ProjectId; 5 | import dev.fumin.sample.eventdriven.infrastructure.persistence.entity.ProjectDto; 6 | 7 | public class ProjectTranslator extends ConcurrencyDataTranslator { 8 | 9 | @Override 10 | public Project toModel(ProjectDto dto) { 11 | Project project = new Project( 12 | new ProjectId(dto.getId()), 13 | dto.getName(), 14 | dto.isActive() 15 | ); 16 | attachToModel(dto, project); 17 | return project; 18 | } 19 | 20 | @Override 21 | public ProjectDto toDto(Project project) { 22 | ProjectDto dto = new ProjectDto(); 23 | dto.setId(project.getId().getValue()); 24 | dto.setName(project.getName()); 25 | dto.setActive(project.isActive()); 26 | attachToDto(project, dto); 27 | return dto; 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/infrastructure/pubsub/LocalSafePublisher.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.infrastructure.pubsub; 2 | 3 | import com.google.api.gax.core.CredentialsProvider; 4 | import com.google.api.gax.core.NoCredentialsProvider; 5 | import com.google.api.gax.grpc.GrpcTransportChannel; 6 | import com.google.api.gax.rpc.FixedTransportChannelProvider; 7 | import com.google.api.gax.rpc.TransportChannelProvider; 8 | import com.google.cloud.pubsub.v1.Publisher; 9 | import com.google.pubsub.v1.TopicName; 10 | 11 | import io.grpc.ManagedChannel; 12 | import io.grpc.ManagedChannelBuilder; 13 | import dev.fumin.sample.eventdriven.common.function.ThrowableConsumer; 14 | 15 | public class LocalSafePublisher implements SafePublisher { 16 | 17 | private final String port; 18 | 19 | public LocalSafePublisher(String port) { 20 | this.port = port; 21 | } 22 | 23 | @Override 24 | public void publish(TopicName topicName, ThrowableConsumer publisherConsumer) { 25 | ManagedChannel channel = ManagedChannelBuilder.forTarget(port).usePlaintext().build(); 26 | TransportChannelProvider channelProvider = FixedTransportChannelProvider.create( 27 | GrpcTransportChannel.create(channel)); 28 | CredentialsProvider credentialsProvider = NoCredentialsProvider.create(); 29 | try { 30 | Publisher publisher = Publisher.newBuilder(topicName) 31 | .setChannelProvider(channelProvider) 32 | .setCredentialsProvider(credentialsProvider) 33 | .build(); 34 | publisherConsumer.accept(publisher); 35 | } catch (Exception e) { 36 | throw new RuntimeException(e); 37 | } finally { 38 | channel.shutdown(); 39 | } 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/infrastructure/pubsub/RemoteSafePublisher.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.infrastructure.pubsub; 2 | 3 | import com.google.cloud.pubsub.v1.Publisher; 4 | import com.google.pubsub.v1.TopicName; 5 | 6 | import dev.fumin.sample.eventdriven.common.function.ThrowableConsumer; 7 | 8 | public class RemoteSafePublisher implements SafePublisher { 9 | 10 | @Override 11 | public void publish(TopicName topicName, ThrowableConsumer publisherConsumer) { 12 | Publisher publisher = null; 13 | try { 14 | publisher = Publisher.newBuilder(topicName).build(); 15 | publisherConsumer.accept(publisher); 16 | } catch (Exception e) { 17 | throw new RuntimeException(e); 18 | } finally { 19 | if (publisher != null) { 20 | publisher.shutdown(); 21 | } 22 | } 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/infrastructure/pubsub/SafePublisher.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.infrastructure.pubsub; 2 | 3 | import com.google.cloud.pubsub.v1.Publisher; 4 | import com.google.pubsub.v1.TopicName; 5 | 6 | import dev.fumin.sample.eventdriven.common.function.ThrowableConsumer; 7 | 8 | public interface SafePublisher { 9 | 10 | void publish(TopicName topicName, ThrowableConsumer publisherConsumer); 11 | 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/infrastructure/serialization/DateTypeAdapter.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.infrastructure.serialization; 2 | 3 | import com.google.gson.TypeAdapter; 4 | import com.google.gson.stream.JsonReader; 5 | import com.google.gson.stream.JsonToken; 6 | import com.google.gson.stream.JsonWriter; 7 | 8 | import java.io.IOException; 9 | import java.util.Date; 10 | 11 | /** 12 | * Gson用Date変換アダプタ。 13 | * デフォルトのアダプタではミリ秒が落ちてしまうので独自に実装している。 14 | */ 15 | public class DateTypeAdapter extends TypeAdapter { 16 | 17 | @Override 18 | public void write(JsonWriter out, Date value) throws IOException { 19 | if (value == null) { 20 | out.nullValue(); 21 | } else { 22 | out.value(value.getTime()); 23 | } 24 | } 25 | 26 | @Override 27 | public Date read(JsonReader in) throws IOException { 28 | if (in.peek() == JsonToken.NULL) { 29 | in.nextNull(); 30 | return null; 31 | } 32 | return new Date(in.nextLong()); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/infrastructure/service/DummyTaskCreationService.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.infrastructure.service; 2 | 3 | import java.util.UUID; 4 | 5 | import dev.fumin.sample.eventdriven.domain.model.google.tasks.Task; 6 | import dev.fumin.sample.eventdriven.domain.model.google.tasks.TaskCreationService; 7 | 8 | public class DummyTaskCreationService implements TaskCreationService { 9 | 10 | @Override 11 | public Task createTask(String title) { 12 | return new Task(UUID.randomUUID().toString(), title); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/presentation/api/ColumnApi.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.presentation.api; 2 | 3 | import com.google.gson.Gson; 4 | 5 | import java.io.IOException; 6 | import java.util.stream.Collectors; 7 | 8 | import javax.inject.Inject; 9 | import javax.inject.Singleton; 10 | import javax.servlet.ServletException; 11 | import javax.servlet.annotation.WebServlet; 12 | import javax.servlet.http.HttpServlet; 13 | import javax.servlet.http.HttpServletRequest; 14 | import javax.servlet.http.HttpServletResponse; 15 | 16 | import dev.fumin.sample.eventdriven.application.usecase.column.CreateColumnUseCase; 17 | import lombok.Data; 18 | import lombok.Value; 19 | 20 | @Singleton 21 | @WebServlet(value = "/api/columns") 22 | public class ColumnApi extends HttpServlet { 23 | 24 | @Inject 25 | private Gson gson; 26 | 27 | @Inject 28 | private CreateColumnUseCase useCase; 29 | 30 | @Override 31 | protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { 32 | String body = req.getReader().lines().collect(Collectors.joining()); 33 | ReqBody reqBody = gson.fromJson(body, ReqBody.class); 34 | 35 | String columnId = useCase.handle(reqBody.projectId, reqBody.columnName); 36 | 37 | ResBody resBody = new ResBody(reqBody.projectId, columnId); 38 | resp.getWriter().println(gson.toJson(resBody)); 39 | resp.setContentType("application/json"); 40 | resp.setStatus(HttpServletResponse.SC_OK); 41 | } 42 | 43 | @Data 44 | public static class ReqBody { 45 | private String projectId; 46 | private String columnName; 47 | } 48 | 49 | @Value 50 | public static class ResBody { 51 | String projectId; 52 | String columnId; 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/presentation/api/NoteApi.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.presentation.api; 2 | 3 | import com.google.gson.Gson; 4 | 5 | import java.io.IOException; 6 | import java.util.stream.Collectors; 7 | 8 | import javax.inject.Inject; 9 | import javax.inject.Singleton; 10 | import javax.servlet.ServletException; 11 | import javax.servlet.annotation.WebServlet; 12 | import javax.servlet.http.HttpServlet; 13 | import javax.servlet.http.HttpServletRequest; 14 | import javax.servlet.http.HttpServletResponse; 15 | 16 | import dev.fumin.sample.eventdriven.application.usecase.note.CreateNoteCommand; 17 | import dev.fumin.sample.eventdriven.application.usecase.note.CreateNoteUseCase; 18 | import lombok.Data; 19 | import lombok.Value; 20 | 21 | @Singleton 22 | @WebServlet(value = "/api/notes") 23 | public class NoteApi extends HttpServlet { 24 | 25 | @Inject 26 | private Gson gson; 27 | 28 | @Inject 29 | private CreateNoteUseCase useCase; 30 | 31 | @Override 32 | protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { 33 | String body = req.getReader().lines().collect(Collectors.joining()); 34 | ReqBody reqBody = gson.fromJson(body, ReqBody.class); 35 | 36 | CreateNoteCommand command = CreateNoteCommand.builder() 37 | .projectId(reqBody.projectId) 38 | .columnId(reqBody.columnId) 39 | .description(reqBody.description) 40 | .build(); 41 | String noteId = useCase.handle(command); 42 | 43 | ResBody resBody = new ResBody(reqBody.projectId, reqBody.columnId, noteId); 44 | resp.getWriter().println(gson.toJson(resBody)); 45 | resp.setContentType("application/json"); 46 | resp.setStatus(HttpServletResponse.SC_OK); 47 | } 48 | 49 | @Data 50 | public static class ReqBody { 51 | private String projectId; 52 | private String columnId; 53 | private String description; 54 | } 55 | 56 | @Value 57 | public static class ResBody { 58 | String projectId; 59 | String columnId; 60 | String noteId; 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/presentation/api/ProjectApi.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.presentation.api; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.inject.Singleton; 5 | 6 | import java.io.IOException; 7 | import java.util.stream.Collectors; 8 | 9 | import javax.inject.Inject; 10 | import javax.servlet.ServletException; 11 | import javax.servlet.annotation.WebServlet; 12 | import javax.servlet.http.HttpServlet; 13 | import javax.servlet.http.HttpServletRequest; 14 | import javax.servlet.http.HttpServletResponse; 15 | 16 | import dev.fumin.sample.eventdriven.application.usecase.project.CreateProjectUseCase; 17 | import lombok.Data; 18 | import lombok.Value; 19 | 20 | @Singleton 21 | @WebServlet(value = "/api/projects") 22 | public class ProjectApi extends HttpServlet { 23 | 24 | @Inject 25 | private Gson gson; 26 | 27 | @Inject 28 | private CreateProjectUseCase useCase; 29 | 30 | @Override 31 | protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { 32 | String body = req.getReader().lines().collect(Collectors.joining()); 33 | ReqBody reqBody = gson.fromJson(body, ReqBody.class); 34 | 35 | String projectId = useCase.handle(reqBody.name); 36 | 37 | ResBody resBody = new ResBody(projectId); 38 | resp.getWriter().println(gson.toJson(resBody)); 39 | resp.setContentType("application/json"); 40 | resp.setStatus(HttpServletResponse.SC_OK); 41 | } 42 | 43 | @Data 44 | public static class ReqBody { 45 | private String name; 46 | } 47 | 48 | @Value 49 | public static class ResBody { 50 | String projectId; 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/presentation/di/PresentationModule.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.presentation.di; 2 | 3 | import com.google.inject.servlet.ServletModule; 4 | 5 | import dev.fumin.sample.eventdriven.presentation.api.ColumnApi; 6 | import dev.fumin.sample.eventdriven.presentation.api.ProjectApi; 7 | import dev.fumin.sample.eventdriven.presentation.event.EventProxy; 8 | import dev.fumin.sample.eventdriven.presentation.api.NoteApi; 9 | import dev.fumin.sample.eventdriven.presentation.event.receiver.NoteCreatedReceiver; 10 | 11 | public class PresentationModule extends ServletModule { 12 | 13 | @Override 14 | protected void configureServlets() { 15 | super.configureServlets(); 16 | 17 | // api 18 | serve("/api/projects").with(ProjectApi.class); 19 | serve("/api/columns").with(ColumnApi.class); 20 | serve("/api/notes").with(NoteApi.class); 21 | 22 | // event receiver 23 | serve("/event/proxy").with(EventProxy.class); 24 | serve("/event/receiver/note-created").with(NoteCreatedReceiver.class); 25 | 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/presentation/event/ConsumedEvent.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.presentation.event; 2 | 3 | import java.util.Date; 4 | 5 | import lombok.Value; 6 | 7 | @Value 8 | public class ConsumedEvent { 9 | long eventId; 10 | String receiver; 11 | Date receivedAt; 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/presentation/event/ConsumedEventStore.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.presentation.event; 2 | 3 | public interface ConsumedEventStore { 4 | boolean exists(long eventId, String receiver); 5 | void insert(ConsumedEvent event); 6 | } 7 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/presentation/event/Event.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.presentation.event; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | import lombok.Getter; 7 | import lombok.Value; 8 | 9 | @Value 10 | public class Event { 11 | 12 | long eventId; 13 | Message message; 14 | String subscription; 15 | 16 | public static class Message { 17 | 18 | @Getter 19 | private final String messageId; 20 | 21 | @Getter 22 | private final String data; 23 | 24 | @Getter 25 | private final Map attributes = new HashMap<>(); 26 | 27 | public Message(String messageId, String data) { 28 | this.messageId = messageId; 29 | this.data = data; 30 | } 31 | 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/presentation/event/EventProxy.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.presentation.event; 2 | 3 | import javax.inject.Inject; 4 | import javax.inject.Singleton; 5 | import javax.servlet.annotation.WebServlet; 6 | 7 | import dev.fumin.sample.eventdriven.application.event.EventRelay; 8 | 9 | @Singleton 10 | @WebServlet(value = "/event/proxy") 11 | public class EventProxy extends EventReceiver { 12 | 13 | @Inject 14 | private EventRelay eventRelay; 15 | 16 | @Override 17 | protected void onReceive(Event event) { 18 | // イベントリレーに受信したイベントのIDを渡し、本来のチャンネルに送信させる。 19 | eventRelay.relay(event.getEventId()); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/presentation/event/EventReceiver.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.presentation.event; 2 | 3 | import com.google.gson.JsonObject; 4 | import com.google.gson.JsonParser; 5 | 6 | import java.io.IOException; 7 | import java.util.Base64; 8 | import java.util.Date; 9 | import java.util.stream.Collectors; 10 | 11 | import javax.inject.Inject; 12 | import javax.servlet.ServletException; 13 | import javax.servlet.http.HttpServlet; 14 | import javax.servlet.http.HttpServletRequest; 15 | import javax.servlet.http.HttpServletResponse; 16 | 17 | import dev.fumin.sample.eventdriven.application.persistence.Transactional; 18 | 19 | public abstract class EventReceiver extends HttpServlet { 20 | 21 | private final JsonParser parser = new JsonParser(); 22 | 23 | @Inject 24 | private ConsumedEventStore consumedEventStore; 25 | 26 | @Override 27 | @Transactional 28 | public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { 29 | String body = req.getReader().lines().collect(Collectors.joining()); 30 | 31 | System.out.println(body); 32 | 33 | JsonObject json = parser.parse(body).getAsJsonObject(); 34 | JsonObject messageObj = json.get("message").getAsJsonObject(); 35 | JsonObject attributesObj = messageObj.get("attributes").getAsJsonObject(); 36 | 37 | Event.Message message = new Event.Message( 38 | messageObj.get("messageId").getAsString(), 39 | new String(Base64.getDecoder().decode(messageObj.get("data").getAsString())) 40 | ); 41 | attributesObj.entrySet() 42 | .forEach(entry -> message.getAttributes() 43 | .put(entry.getKey(), entry.getValue().getAsString())); 44 | 45 | long eventId = Long.parseLong(message.getAttributes().get("eventId")); 46 | Event event = new Event(eventId, message, json.get("subscription").getAsString()); 47 | 48 | // 消費済みのイベントは無視する 49 | if (!consumedEventStore.exists(eventId, event.getSubscription())) { 50 | // Pub/Subから同一のメッセージが重複して送信される可能性があるので 51 | // 冪等性をもたせるために消費したイベントを保存する 52 | ConsumedEvent consumedEvent = 53 | new ConsumedEvent(eventId, event.getSubscription(), new Date()); 54 | consumedEventStore.insert(consumedEvent); 55 | onReceive(event); 56 | } 57 | 58 | resp.setStatus(HttpServletResponse.SC_OK); 59 | } 60 | 61 | // 未処理の場合、サブクラスに処理を委譲する 62 | protected abstract void onReceive(Event event); 63 | 64 | } 65 | -------------------------------------------------------------------------------- /app/src/main/java/dev/fumin/sample/eventdriven/presentation/event/receiver/NoteCreatedReceiver.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.eventdriven.presentation.event.receiver; 2 | 3 | import com.google.gson.JsonObject; 4 | import com.google.gson.JsonParser; 5 | 6 | import javax.inject.Inject; 7 | import javax.inject.Singleton; 8 | import javax.servlet.annotation.WebServlet; 9 | 10 | import dev.fumin.sample.eventdriven.application.usecase.note.CopyNoteToTaskUseCase; 11 | import dev.fumin.sample.eventdriven.presentation.event.Event; 12 | import dev.fumin.sample.eventdriven.presentation.event.EventReceiver; 13 | 14 | @Singleton 15 | @WebServlet(value = "/event/receiver/note-created") 16 | public class NoteCreatedReceiver extends EventReceiver { 17 | 18 | private final JsonParser jsonParser = new JsonParser(); 19 | 20 | @Inject 21 | private CopyNoteToTaskUseCase useCase; 22 | 23 | @Override 24 | protected void onReceive(Event event) { 25 | JsonObject root = jsonParser.parse(event.getMessage().getData()).getAsJsonObject(); 26 | String projectId = root.get("projectId").getAsJsonObject().get("value").getAsString(); 27 | String noteId = root.get("noteId").getAsJsonObject().get("value").getAsString(); 28 | useCase.handle(projectId, noteId); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/resources/META-INF/dev/fumin/sample/eventdriven/infrastructure/persistence/dao/ColumnDao/selectById.sql: -------------------------------------------------------------------------------- 1 | SELECT * 2 | FROM `column` 3 | WHERE id = /* id */'id' 4 | AND project_id = /* projectId */'pId'; -------------------------------------------------------------------------------- /app/src/main/resources/META-INF/dev/fumin/sample/eventdriven/infrastructure/persistence/dao/ConsumedEventDao/exists.sql: -------------------------------------------------------------------------------- 1 | SELECT EXISTS( 2 | SELECT 1 FROM consumed_event WHERE id = /* id */'id' AND receiver = /* receiver */'receiver' 3 | ) -------------------------------------------------------------------------------- /app/src/main/resources/META-INF/dev/fumin/sample/eventdriven/infrastructure/persistence/dao/EventDao/selectById.sql: -------------------------------------------------------------------------------- 1 | SELECT * 2 | FROM event 3 | WHERE id = /* id */'id' -------------------------------------------------------------------------------- /app/src/main/resources/META-INF/dev/fumin/sample/eventdriven/infrastructure/persistence/dao/NoteDao/selectById.sql: -------------------------------------------------------------------------------- 1 | SELECT * 2 | FROM note 3 | WHERE id = /* id */'id' 4 | AND project_id = /* projectId */'pId' -------------------------------------------------------------------------------- /app/src/main/resources/META-INF/dev/fumin/sample/eventdriven/infrastructure/persistence/dao/ProjectDao/selectById.sql: -------------------------------------------------------------------------------- 1 | SELECT * 2 | FROM project 3 | WHERE id = /* id */'id' -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | import dependencies.Dep 2 | 3 | buildscript { 4 | dependencies { 5 | classpath Dep.GradlePlugin.appEngine 6 | classpath Dep.GradlePlugin.kotlin 7 | classpath Dep.GradlePlugin.shadow 8 | } 9 | 10 | repositories { 11 | maven { 12 | url 'https://maven-central.storage.googleapis.com' 13 | } 14 | maven { 15 | url "https://plugins.gradle.org/m2/" 16 | } 17 | mavenCentral() 18 | jcenter() 19 | } 20 | } 21 | 22 | allprojects { 23 | repositories { 24 | maven { 25 | url 'https://maven-central.storage.googleapis.com' 26 | } 27 | mavenCentral() 28 | jcenter() 29 | } 30 | } 31 | 32 | task clean(type: Delete) { 33 | delete rootProject.buildDir 34 | } -------------------------------------------------------------------------------- /buildSrc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `kotlin-dsl` 3 | } 4 | 5 | repositories { 6 | jcenter() 7 | } 8 | 9 | kotlinDslPluginOptions { 10 | experimentalWarning.set(false) 11 | } -------------------------------------------------------------------------------- /buildSrc/src/main/java/dependencies/Dep.kt: -------------------------------------------------------------------------------- 1 | package dependencies 2 | 3 | object Dep { 4 | 5 | private const val kotlinVersion = "1.3.61" 6 | 7 | object GradlePlugin { 8 | const val appEngine = "com.google.cloud.tools:appengine-gradle-plugin:2.2.0" 9 | const val kotlin = "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" 10 | const val shadow = "com.github.jengelman.gradle.plugins:shadow:5.0.0" 11 | } 12 | 13 | object Jetty { 14 | private const val version = "9.4.29.v20200521" 15 | const val server = "org.eclipse.jetty:jetty-server:$version" 16 | const val webapp = "org.eclipse.jetty:jetty-webapp:$version" 17 | const val util = "org.eclipse.jetty:jetty-util:$version" 18 | const val annotations = "org.eclipse.jetty:jetty-annotations:$version" 19 | } 20 | 21 | object Servlet { 22 | const val api = "javax.servlet:javax.servlet-api:4.0.1" 23 | } 24 | 25 | object Lombok { 26 | const val core = "org.projectlombok:lombok:1.18.12" 27 | const val compiler = core 28 | } 29 | 30 | object Gson { 31 | const val core = "com.google.code.gson:gson:2.8.5" 32 | } 33 | 34 | object Guice { 35 | private const val version = "4.2.3" 36 | const val core = "com.google.inject:guice:$version" 37 | const val servlet = "com.google.inject.extensions:guice-servlet:$version" 38 | } 39 | 40 | object Database { 41 | const val mysql = "mysql:mysql-connector-java:8.0.16" 42 | const val hikariCp = "com.zaxxer:HikariCP:3.3.1" 43 | 44 | object Doma { 45 | private const val version = "2.24.0" 46 | const val core = "org.seasar.doma:doma:$version" 47 | const val compiler = "org.seasar.doma:doma:$version" 48 | } 49 | 50 | } 51 | 52 | object GCP { 53 | const val pubsub = "com.google.cloud:google-cloud-pubsub:1.108.0" 54 | } 55 | 56 | } -------------------------------------------------------------------------------- /buildSrc/src/main/java/dependencies/Environment.kt: -------------------------------------------------------------------------------- 1 | package dependencies 2 | 3 | object Environment { 4 | 5 | const val googleCloudProject = "local-project" 6 | 7 | } -------------------------------------------------------------------------------- /docker/Dockerfile-MySQL: -------------------------------------------------------------------------------- 1 | # Dockerfile_MySQL 2 | FROM mysql:5.7 3 | 4 | # Set debian default locale to ja_JP.UTF-8 5 | RUN apt-get update && \ 6 | apt-get install -y locales && \ 7 | rm -rf /var/lib/apt/lists/* && \ 8 | echo "ja_JP.UTF-8 UTF-8" > /etc/locale.gen && \ 9 | locale-gen ja_JP.UTF-8 10 | ENV LC_ALL ja_JP.UTF-8 11 | 12 | # Set MySQL character 13 | RUN { \ 14 | echo '[mysqld]'; \ 15 | echo 'character-set-server=utf8mb4'; \ 16 | echo 'collation-server=utf8mb4_general_ci'; \ 17 | echo '[client]'; \ 18 | echo 'default-character-set=utf8mb4'; \ 19 | } > /etc/mysql/conf.d/charset.cnf -------------------------------------------------------------------------------- /docker/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | eventdriven-db: 2 | build: . 3 | container_name: event_driven_mysql 4 | dockerfile: Dockerfile-MySQL 5 | environment: 6 | MYSQL_ROOT_PASSWORD: root 7 | MYSQL_USER: myuser 8 | MYSQL_PASSWORD: mypassword 9 | MYSQL_DATABASE: eventdriven_db 10 | TZ: "Asia/Tokyo" 11 | ports: 12 | - 3306:3306 13 | volumes: 14 | - ./initdb.d:/docker-entrypoint-initdb.d -------------------------------------------------------------------------------- /docker/initdb.d/ddl.sql: -------------------------------------------------------------------------------- 1 | -- Create syntax for TABLE 'column' 2 | CREATE TABLE `column` ( 3 | `id` varchar(36) NOT NULL, 4 | `project_id` varchar(36) NOT NULL, 5 | `name` varchar(256) NOT NULL, 6 | `active` tinyint(1) NOT NULL, 7 | `created_at` datetime NOT NULL, 8 | `updated_at` datetime NOT NULL, 9 | `version` int(10) unsigned NOT NULL, 10 | PRIMARY KEY (`id`,`project_id`) 11 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 12 | 13 | -- Create syntax for TABLE 'event' 14 | CREATE TABLE `event` ( 15 | `id` bigint(20) NOT NULL AUTO_INCREMENT, 16 | `event` text NOT NULL, 17 | `event_name` varchar(256) NOT NULL, 18 | `stored_at` datetime NOT NULL, 19 | `version` int(10) unsigned NOT NULL, 20 | PRIMARY KEY (`id`) 21 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 22 | 23 | -- Create syntax for TABLE 'consumed_event' 24 | CREATE TABLE `consumed_event` ( 25 | `id` bigint(20) NOT NULL, 26 | `receiver` varchar(100) NOT NULL, 27 | `received_at` datetime NOT NULL, 28 | PRIMARY KEY (`id`, `receiver`) 29 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 30 | 31 | -- Create syntax for TABLE 'note' 32 | CREATE TABLE `note` ( 33 | `id` varchar(36) NOT NULL, 34 | `project_id` varchar(36) NOT NULL, 35 | `column_id` varchar(36) NOT NULL, 36 | `description` text NOT NULL, 37 | `created_at` datetime NOT NULL, 38 | `updated_at` datetime NOT NULL, 39 | `version` int(10) unsigned NOT NULL, 40 | PRIMARY KEY (`id`,`project_id`) 41 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 42 | 43 | -- Create syntax for TABLE 'project' 44 | CREATE TABLE `project` ( 45 | `id` varchar(36) NOT NULL, 46 | `name` varchar(256) NOT NULL, 47 | `active` tinyint(1) NOT NULL, 48 | `created_at` datetime NOT NULL, 49 | `updated_at` datetime NOT NULL, 50 | `version` int(10) unsigned NOT NULL, 51 | PRIMARY KEY (`id`) 52 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 53 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fumin65/domain-event-driven-sample/8c30e565b302bb95c3cc4bcd813cf43f113adda2/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.2.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS='"-Xmx64m"' 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS="-Xmx64m" 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /pubsub-emulator/build.gradle: -------------------------------------------------------------------------------- 1 | import dependencies.Dep 2 | import dependencies.Environment 3 | 4 | apply plugin: "java" 5 | apply plugin: "application" 6 | 7 | sourceCompatibility = 1.8 8 | 9 | dependencies { 10 | implementation Dep.GCP.pubsub 11 | } 12 | 13 | run { 14 | environment("GOOGLE_CLOUD_PROJECT", Environment.googleCloudProject) 15 | } 16 | 17 | mainClassName = "dev.fumin.sample.pubsub.emulator.EmulatorInitializer" 18 | -------------------------------------------------------------------------------- /pubsub-emulator/src/main/java/dev/fumin/sample/pubsub/emulator/EmulatorInitializer.java: -------------------------------------------------------------------------------- 1 | package dev.fumin.sample.pubsub.emulator; 2 | 3 | import com.google.api.gax.core.CredentialsProvider; 4 | import com.google.api.gax.core.NoCredentialsProvider; 5 | import com.google.api.gax.grpc.GrpcTransportChannel; 6 | import com.google.api.gax.rpc.FixedTransportChannelProvider; 7 | import com.google.api.gax.rpc.NotFoundException; 8 | import com.google.api.gax.rpc.TransportChannelProvider; 9 | import com.google.cloud.pubsub.v1.SubscriptionAdminClient; 10 | import com.google.cloud.pubsub.v1.SubscriptionAdminSettings; 11 | import com.google.cloud.pubsub.v1.TopicAdminClient; 12 | import com.google.cloud.pubsub.v1.TopicAdminSettings; 13 | import com.google.pubsub.v1.ProjectSubscriptionName; 14 | import com.google.pubsub.v1.PushConfig; 15 | import com.google.pubsub.v1.Subscription; 16 | import com.google.pubsub.v1.Topic; 17 | import com.google.pubsub.v1.TopicName; 18 | 19 | import java.util.HashMap; 20 | import java.util.Map; 21 | 22 | import io.grpc.ManagedChannel; 23 | import io.grpc.ManagedChannelBuilder; 24 | 25 | public class EmulatorInitializer { 26 | 27 | public static void main(String[] args) throws Exception { 28 | String port = System.getenv("PUBSUB_EMULATOR_HOST"); 29 | System.out.println("emulator port : " + port); 30 | ManagedChannel channel = ManagedChannelBuilder.forTarget(port) 31 | .usePlaintext() 32 | .build(); 33 | 34 | TransportChannelProvider channelProvider = FixedTransportChannelProvider.create( 35 | GrpcTransportChannel.create(channel)); 36 | CredentialsProvider credentialsProvider = NoCredentialsProvider.create(); 37 | 38 | try { 39 | TopicAdminClient topicAdminClient = TopicAdminClient.create( 40 | TopicAdminSettings.newBuilder() 41 | .setTransportChannelProvider(channelProvider) 42 | .setCredentialsProvider(credentialsProvider) 43 | .build()); 44 | SubscriptionAdminClient subscriptionAdminClient = SubscriptionAdminClient.create( 45 | SubscriptionAdminSettings.newBuilder() 46 | .setTransportChannelProvider(channelProvider) 47 | .setCredentialsProvider(credentialsProvider) 48 | .build()); 49 | 50 | String projectId = System.getenv("GOOGLE_CLOUD_PROJECT"); 51 | TopicName proxyTopicName = TopicName.of(projectId, "proxy"); 52 | TopicName noteCreatedTopicName = TopicName.of(projectId, "note_created"); 53 | 54 | Map subscriptions = new HashMap<>(); 55 | subscriptions.put(proxyTopicName, "http://localhost:8080/event/proxy"); 56 | subscriptions.put(noteCreatedTopicName, "http://localhost:8080/event/receiver/note-created"); 57 | 58 | System.out.println("check topic"); 59 | try { 60 | topicAdminClient.getTopic(proxyTopicName); 61 | System.out.println("topic already exists."); 62 | } catch (NotFoundException e) { 63 | subscriptions.forEach((key, value) -> { 64 | Topic topic = topicAdminClient.createTopic(key); 65 | System.out.println("Topic is created: " + topic); 66 | 67 | PushConfig pushConfig = PushConfig.newBuilder() 68 | .setPushEndpoint(value) 69 | .build(); 70 | 71 | String[] paths = value.split("/"); 72 | String name = paths[paths.length - 1]; 73 | ProjectSubscriptionName subscriptionName = 74 | ProjectSubscriptionName.of(key.getProject(), name); 75 | Subscription subscription = subscriptionAdminClient.createSubscription( 76 | subscriptionName, key, pushConfig, 60); 77 | 78 | System.out.println("Subscription is created : " + subscription); 79 | }); 80 | } 81 | 82 | } finally { 83 | channel.shutdown(); 84 | } 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'domain-event-driven' 2 | include 'app' 3 | include 'pubsub-emulator' 4 | 5 | --------------------------------------------------------------------------------