Skip to content

Commit 24fc7e0

Browse files
authored
Add example for Spring Modulith (#222)
1 parent 1fdd45c commit 24fc7e0

File tree

66 files changed

+1906
-61
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

66 files changed

+1906
-61
lines changed

.github/workflows/build-and-publish-antora.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ on:
44
push:
55
branches:
66
- master
7-
- docs/**
87
paths:
98
- 'docs/**'
109
- 'antora-playbook.yml'

README.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,4 +62,5 @@ All tutorials are documented in AsciiDoc format and published as an https://anto
6262
|link:test-rest-assured[Spring Test: Integration with RestAssured] | Implement Behaviour Driven Development with https://rest-assured.io/[RestAssured]
6363
|link:test-slice-tests-rest[Spring Test: Implementing Slice Tests for REST application] | Dive into available options to implement tests with Spring Boot's test components
6464
|link:web-rest-client[Spring Web: REST Clients for calling Synchronous API] | Implement REST client to perform synchronous API calls
65+
|link:modulith[Spring Modulith: Building Modular Monolithic Applications] | Structure Spring Boot applications into well-defined modules with clear boundaries
6566
|===

docs/modules/ROOT/nav.adoc

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
** xref:data-rest-validation.adoc[REST Validation]
1919
* Spring GraphQL
2020
** xref:graphql.adoc[GraphQL Server]
21+
* Spring Modulith
22+
** xref:modulith.adoc[Building Modular Monolithic Applications]
2123
* jOOQ
2224
** xref:jooq.adoc[jOOQ]
2325
* Spring Test
@@ -26,4 +28,4 @@
2628
** xref:test-rest-assured.adoc[Integration with RestAssured]
2729
** xref:test-slice-tests-rest.adoc[Implementing Slice Tests for REST application]
2830
* Spring Web
29-
** xref:web-rest-client.adoc[REST Clients for calling Synchronous API]
31+
** xref:web-rest-client.adoc[REST Clients for calling Synchronous API]

docs/modules/ROOT/pages/batch-rest-repository.adoc

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
= Spring Batch: Working With REST Resources
22
:source-highlighter: highlight.js
3+
Rashidi Zin <rashidi@zin.my>
4+
2.0, September 27, 2023
35
:nofooter:
46
:icons: font
57
:url-quickref: https://github.com/rashidi/spring-boot-tutorials/tree/master/batch-rest-repository
@@ -22,39 +24,48 @@ consists of `ItemReader` and `ItemWriter`. We will implement all of them in link
2224
----
2325
@Configuration
2426
class UserJobConfiguration {
27+
2528
private final JobRepository jobRepository;
2629
private final PlatformTransactionManager transactionManager;
2730
private final MongoOperations mongo;
31+
2832
UserJobConfiguration(JobRepository jobRepository, PlatformTransactionManager transactionManager, MongoOperations mongo) {
2933
this.jobRepository = jobRepository;
3034
this.transactionManager = transactionManager;
3135
this.mongo = mongo;
3236
}
37+
3338
@Bean
3439
public Job userJob() throws MalformedURLException {
3540
return new JobBuilder("userJob", jobRepository).start(step()).build();
3641
}
42+
3743
private Step step() throws MalformedURLException {
3844
return new StepBuilder("userStep", jobRepository)
3945
.<User, User>chunk(10, transactionManager)
4046
.reader(reader())
4147
.writer(writer())
4248
.build();
4349
}
50+
4451
private JsonItemReader<User> reader() throws MalformedURLException {
4552
JacksonJsonObjectReader<User> jsonObjectReader = new JacksonJsonObjectReader<>(User.class);
53+
4654
jsonObjectReader.setMapper(new ObjectMapper());
55+
4756
return new JsonItemReaderBuilder<User>()
4857
.name("userReader")
4958
.jsonObjectReader(jsonObjectReader)
5059
.resource(new UrlResource("https://jsonplaceholder.typicode.com/users"))
5160
.build();
5261
}
62+
5363
private MongoItemWriter<User> writer() {
5464
return new MongoItemWriterBuilder<User>()
5565
.template(mongo)
5666
.build();
5767
}
68+
5869
}
5970
----
6071

@@ -64,15 +75,19 @@ From the code above, we can see that a `URL` form of `Resource` is assigned to `
6475
----
6576
@Configuration
6677
class UserJobConfiguration {
78+
6779
private JsonItemReader<User> reader() throws MalformedURLException {
6880
JacksonJsonObjectReader<User> jsonObjectReader = new JacksonJsonObjectReader<>(User.class);
81+
6982
jsonObjectReader.setMapper(new ObjectMapper());
83+
7084
return new JsonItemReaderBuilder<User>()
7185
.name("userReader")
7286
.jsonObjectReader(jsonObjectReader)
7387
.resource(new UrlResource("https://jsonplaceholder.typicode.com/users"))
7488
.build();
7589
}
90+
7691
}
7792
----
7893

@@ -88,27 +103,37 @@ Once completed then we will verify that the database contains the expected numbe
88103
@SpringBatchTest
89104
@SpringBootTest(classes = { BatchTestConfiguration.class, MongoTestConfiguration.class, UserJobConfiguration.class }, webEnvironment = NONE)
90105
class UserBatchJobTests {
106+
91107
@Container
92108
@ServiceConnection
93109
private final static MySQLContainer<?> MYSQL_CONTAINER = new MySQLContainer<>("mysql:latest")
94110
.withInitScript("org/springframework/batch/core/schema-mysql.sql");
111+
95112
@Container
96113
@ServiceConnection
97114
private final static MongoDBContainer MONGO_DB_CONTAINER = new MongoDBContainer("mongo:latest");
115+
98116
@Autowired
99117
private JobLauncherTestUtils launcher;
118+
100119
@Autowired
101120
private MongoOperations mongoOperations;
121+
102122
@Test
103123
@DisplayName("Given there are 10 users returned from REST Service When the job is COMPLETED Then all users should be saved to MongoDB")
104124
void launch() {
125+
105126
await().atMost(ofSeconds(30)).untilAsserted(() -> {
106127
var execution = launcher.launchJob();
128+
107129
assertThat(execution.getExitStatus()).isEqualTo(COMPLETED);
108130
});
131+
109132
var persistedUsers = mongoOperations.findAll(User.class);
133+
110134
assertThat(persistedUsers).hasSize(10);
111135
}
136+
112137
}
113138
----
114139

docs/modules/ROOT/pages/batch-skip-step.adoc

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
= Spring Batch: Skip Specific Data
22
:source-highlighter: highlight.js
3+
Rashidi Zin <rashidi@zin.my>
4+
2.0, October 1, 2023
35
:nofooter:
46
:icons: font
57
:url-quickref: https://github.com/rashidi/spring-boot-tutorials/tree/master/batch-skip-step
68

79
Skip processing specific data through business logic in Spring Batch.
810

9-
1011
== Background
12+
1113
Spring Batch is a framework for batch processing – execution of a series of jobs. A job is composed of a series of steps.
1214
Each step consists of a reader, a processor, and a writer. The reader reads data from a data source, the processor
1315
processes the data, and the writer writes the processed data to a data source.
@@ -21,25 +23,30 @@ Our application will process data from link:{url-quickref}/src/main/resources/us
2123
into MySQL database. Content of `users.json` is taken from link:https://jsonplaceholder.typicode.com/users[JSONPlaceholder].
2224

2325
== Implement Logics to Skip Data
26+
2427
=== Returning `null`
28+
2529
First approach is to return `null` from `ItemProcessor` implementation. This approach is straight forward and does not
2630
require additional configuration.
2731

2832
[source,java]
2933
----
3034
@Configuration
3135
class UserJobConfiguration {
36+
3237
private ItemProcessor<UserFile, User> processor() {
3338
return item -> switch (item.username()) {
3439
case "Elwyn.Skiles" -> null;
3540
};
3641
}
42+
3743
}
3844
----
3945

4046
With that, when the `Job` detected that the `ItemProcessor` returns `null`, it will skip the data and continue to the next.
4147

4248
=== Throwing `RuntimeException`
49+
4350
Second approach is to throw `RuntimeException` from `ItemProcessor` implementation. This approach requires additional
4451
configuration to be done when defining `Step`.
4552

@@ -49,15 +56,19 @@ Implementation in `ItemProcessor` is as follows:
4956
----
5057
@Configuration
5158
class UserJobConfiguration {
59+
5260
private ItemProcessor<UserFile, User> processor() {
5361
return item -> switch (item.username()) {
5462
case "Maxime_Nienow" -> throw new UsernameNotAllowedException(item.username());
5563
};
5664
}
65+
5766
static class UsernameNotAllowedException extends RuntimeException {
67+
5868
public UsernameNotAllowedException(String username) {
5969
super("Username " + username + " is not allowed");
6070
}
71+
6172
}
6273
}
6374
----
@@ -68,6 +79,7 @@ Next is to inform `Step` to skip `UsernameNotAllowedException`:
6879
----
6980
@Configuration
7081
class UserJobConfiguration {
82+
7183
private Step step(JobRepository jobRepository, PlatformTransactionManager transactionManager, DataSource dataSource) {
7284
return new StepBuilder("userStep", jobRepository)
7385
.<UserFile, User>chunk(10, transactionManager)
@@ -76,14 +88,15 @@ class UserJobConfiguration {
7688
.skipLimit(1)
7789
.build();
7890
}
91+
7992
}
8093
----
8194

8295
With that, when the `Job` detected that the `ItemProcessor` throws `UsernameNotAllowedException`, it will skip the data.
83-
8496
Full definition of the `Job` can be found in link:{url-quickref}/src/main/java/zin/rashidi/boot/batch/user/UserJobConfiguration.java[UserJobConfiguration.java].
8597

8698
== Verification
99+
87100
We will implement integration tests to verify that our implementation is working as intended whereby `Elwyn.Skiles` and
88101
`Maxime_Nienow` are skipped thus will not be available in the database.
89102

@@ -104,25 +117,33 @@ We will implement integration tests to verify that our implementation is working
104117
statements = "CREATE TABLE IF NOT EXISTS users (id BIGINT PRIMARY KEY, name text, username text)"
105118
)
106119
class UserBatchJobTests {
120+
107121
@Container
108122
@ServiceConnection
109123
private final static MySQLContainer<?> MYSQL_CONTAINER = new MySQLContainer<>("mysql:latest");
124+
110125
@Autowired
111126
private JobLauncherTestUtils launcher;
127+
112128
@Autowired
113129
private JdbcTemplate jdbc;
130+
114131
@Test
115132
@DisplayName("Given the username Elwyn.Skiles and Maxime_Nienow are skipped, When job is executed, Then users are not inserted into database")
116133
void findAll() {
134+
117135
await().atMost(10, SECONDS).untilAsserted(() -> {
118136
var execution = launcher.launchJob();
119137
assertThat(execution.getExitStatus()).isEqualTo(COMPLETED);
120138
});
139+
121140
var users = jdbc.query("SELECT * FROM users", (rs, rowNum) ->
122141
new User(rs.getLong("id"), rs.getString("name"), rs.getString("username"))
123142
);
143+
124144
assertThat(users).extracting("username").doesNotContain("Elwyn.Skiles", "Maxime_Nienow");
125145
}
146+
126147
}
127148
----
128149

0 commit comments

Comments
 (0)