Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved demo capabilities (Generating traffic, Event Publication Registry, README) #122

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 84 additions & 5 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,40 @@ mvn clean package
java -jar target/*.jar
```

The application ships with the [HAL browser](https://github.com/mikekelly/hal-browser) embedded, so simply browsing to [http://localhost:8080/browser/index.html](http://localhost:8080/browser/index.html) will allow you to explore the web service.
To send requests to the server application, you have several choices:

**1. Run the `order_and_pay.sh` script.**

This script will create several orders and then immediately submit payment for all of them.
This script uses "valid" and "invalid" credit card numbers at random, with a valid number being used approximately 80% of the time.
The other 20% of payments will fail with different errors, depending on the reason the card is rejected (card not in DB vs invalid format).
```
./order_and_pay.sh
```

**2. Use the embedded [HAL browser](https://github.com/mikekelly/hal-browser).**

The application ships with [HAL browser](https://github.com/mikekelly/hal-browser) embedded, so simply browsing to [http://localhost:8080/explorer/index.html#uri=/](http://localhost:8080/explorer/index.html#uri=/) will allow you to explore the web service.

To get or create an order via HAL Explorer, you can start by either getting existing orders or placing a new order:
- To get existing orders, click on the HTTP Get Request button for the `restbucks:orders` relation under `Links`. In the pop-up, click `Go!`, then expand any of the orders listed under `Embedded Resources` and click on `self`.
- To place a new order, click on the HTTP Request button under the template titled `Place an order` under `HAL-FORMS Template Elements`.

Once you have an order displayed in HAL Explorer, you can submit payment:
- Click on the HTTP Request for the template titled `Go to checkout` under `HAL-FORMS Template Elements`. This template appears for all orders with status `Payment expected`.
- Enter any 16-digit number into the `Credit card nuumber` field and click `Go!`
- You will see an error, because the card number is not in the database.
- Enter `1234123412341234` and the payment will succeed. [Ollie has got you covered :)](server/src/main/java/org/springsource/restbucks/payment/PaymentInitializer.java)

> Note, that the curie links in the representations are currently not backed by any documents served but they will be in the future. Imagine simple HTML pages being served documenting the individual relation types.

## IDE setup
**3. Use the Android client.**

Refer to the section `The Android client` below for more information.

## IDE setup notes

### Eclipse
For the usage inside an IDE do the following:

1. Make sure you have an Eclipse with m2e installed (preferably [STS](http://spring.io/sts)).
Expand All @@ -32,6 +60,11 @@ For the usage inside an IDE do the following:

3. Import the checked out code through *File > Import > Existing Maven Project…*

### IntelliJ

1. After opening the project in IntelliJ, right click on `server/pom.xml` and select `Add as Maven Project`.
2. If you would like to execute tests in the `Run` panel (activated by right-clicking on `server/src/test/java/org/springsource/restbucks` and selecting `Run Tests in 'restbucks'`), you will need to edit Maven plugin configurations. Open the Maven tool window and find the list of plugins included in the project. Expand `byte-buddy`, right click on `transform-extended`, and select `Execute After Build` and `Execute After Rebuild`.

## Project description

The project uses:
Expand All @@ -44,6 +77,7 @@ The project uses:
- [Spring Plugin](http://github.com/spring-projects/spring-plugin)
- [Spring Security](http://github.com/spring-projects/spring-security)
- [Spring Session](http://github.com/spring-projects/spring-session)
- [Spring Modulith](http://github.com/spring-projects/spring-modulith)

The implementation consists of mainly two parts, the `order` and the `payment` part. The `Orders` are exposed as REST resources using Spring Data RESTs capability to automatically expose Spring Data JPA repositories contained in the application. The `Payment` process and the REST application protocol described in the book are implemented manually using a Spring MVC controller (`PaymentController`).

Expand Down Expand Up @@ -99,6 +133,15 @@ Content-Type: application/hal+json;charset=UTF-8
```

### Spring Modulith

Spring Modulith supports modular architecture in monoliths. It treats each root-level package as a separate module with top-level public members exposed as API and all other code protected as module-internal (unless explicitly specified otherwise). Through tests, it verifies compliance to modular architecture rules and also generates documentation for architecturally relevant components of the system. See `DocumentationTest` and also `target/spring-modulith-docs/`, once the test has been executed.

Additionally, we are using Spring Modulith to isolate a single module for integration testing in `OrdersIntegrationTest`. We are also using Spring Modulith, together with Spring Framework's in-memory messaging capabilities, to handle event-driven interactions between modules with additional support for transaction management. See the use of `ApplicationModuleListener` in `Engine`.

Finally, Spring Modulith is being used to extract distributed tracing from module interactions. To enable this functionality, see the `Observability` section below.


### Documentation / Client Stub Generation

The [`restdocs`](https://github.com/odrotbohm/spring-restbucks/tree/restdocs) branch contains the test for the order payment process augmented with setup to generate Asciidoctor snippets documenting the executed requests and expectations on the responses.
Expand All @@ -120,21 +163,57 @@ The project uses [Lombok](http://projectlombok.org) to reduce the amount of boil

## Observability

The project uses [Spring Moduliths'](https://github.com/spring-projects-experimental/spring-modulith) observability features to inspect the runtime interaction between the logical modules of RESTBucks.
The project uses [Spring Modulith's](https://github.com/spring-projects-experimental/spring-modulith) observability features to inspect the runtime interaction between the logical modules of RESTBucks.
To use and see this, run the application with the `observability` Maven profile enabled:

```
$ mvn spring-boot:run -Pobservability
```

That profile adds some dependencies, like Spring Cloud Sleuth as well as its integration with Zipkin.
Make sure you have Zipkin running as described [here](https://zipkin.io/pages/quickstart.html).
Interactions with the system will now expose the logical module invocation and their choreography
It also includes docker-compose support and will start Zipkin for you when the application starts.
Interactions with the system will now expose the logical module invocation and their choreography.

![A sample Zipkin trace](server/docs/images/observability.png "A sample Zipkin trace")

See how the triggering of the payment for an order changes the order state, kicks of the preparation engine and tweaks the order's state in turn at the start and end of the process.

## Event Publication Registry

The project uses [Spring Modulith's Event Publication Registry](https://docs.spring.io/spring-modulith/reference/events.html#publication-registry) to persist events in-flight between modules of RESTbucks.
After an event is processed, the completion timestamp of the persisted event is updated.

The project is also configured to republish pending events when the application restarts.
See `application.properties`.

To use and see this, run the application with the `observability` Maven profile enabled:

```
$ mvn spring-boot:run -Pobservability
```

This profile adds necessary dependencies and uses docker-compose support to start a postgres database automatically when the application starts.

Events are generated when an order is paid (see `OrderPaid`).
The `Engine` uses Spring Modulith's `@ApplicationModuleListener` annotation as a shortcut for the recommended configuration for integration of modules via events.
However, to avoid the potential of losing messages if the application fails, it is advisable to use the Event Publication Registry.

Generate payment traffic for the application.
You may use, for example, the script `order_and_pay.sh`.

Use your IDE or a SQL client to view the events in the database.
```sql
select * from event_publication;
```

Stop the application while events are still pending.
> Tip: If you need more time to stop the application, use the `restbucks.engine.processing` property in `application.properties`.

![Event Publication Registry with pending events](server/docs/images/eventPubReg-pending.png "A sample Zipkin trace")

Restart the application and the events will be republished and completed.
![Event Publication Registry with all completed events](server/docs/images/eventPubReg-completed.png "A sample Zipkin trace")

## Hypermedia

A core focus of this sample app is to demonstrate how easy resources can be modeled in a hypermedia driven way. There are two major aspects to this challenge in Java web-frameworks:
Expand Down
16 changes: 16 additions & 0 deletions server/compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
version: '2'
services:
zipkin:
image: openzipkin/zipkin
container_name: zipkin
ports:
- 9411:9411
postgres:
image: 'postgres:latest'
container_name: events
environment:
- 'POSTGRES_DB=db'
- 'POSTGRES_PASSWORD=pw'
- 'POSTGRES_USER=user'
ports:
- '5432:5432'
Binary file added server/docs/images/eventPubReg-completed.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added server/docs/images/eventPubReg-pending.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
114 changes: 114 additions & 0 deletions server/order_and_pay.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
#!/bin/bash

##### CREATE ORDERS
# Creates orders for each location for each drink and for combinations of 2 drinks.

# Define locations
locations=("To go" "In store")

error_flag=false

# Get drink options
drinks_http_response=$(http :8080/drinks/by-name)
declare -a drinks
declare -a drink_names
index=0
while true; do
value=$(echo "${drinks_http_response}" | jq --argjson idx "$index" '._embedded.drinks[$idx].value')
# If the value is null, exit the loop
if [ -z "$value" ] || [ "$value" = "null" ]; then break; fi
drinks+=($(echo "${drinks_http_response}" | jq --argjson idx "$index" '._embedded.drinks[$idx].value'))
drink_names+=("$(echo "${drinks_http_response}" | jq -r --argjson idx "$index" '._embedded.drinks[$idx].prompt')")

((index++))
done

num_locations=${#locations[@]}
num_drinks=${#drinks[@]}
expected_num_orders=$((num_drinks * (num_drinks + 1) / 2 * num_locations))
echo "Generating orders based on $num_locations locations and $num_drinks drinks"

# Place orders
declare -a payment_links

# Outer loop: locations
num_orders=0
location_index=0
echo "Order counters: [order:location:drinks]"
for location in "${locations[@]}"; do
((location_index++))
drinks_index=0

# Order all single drink combinations
for ((i = 0; i < ${#drinks[@]}; i++)); do
# Order all single drink combinations
((drinks_index++))
((num_orders++))
drink_order="${drinks[i]}"
echo "Order $num_orders:$location_index:$drinks_index - $location ${drink_names[i]}"
order=$(echo "{ \"drinks\": [${drink_order}], \"location\": \"${location}\" }" | http http://localhost:8080/orders)
payment_links+=($(echo ${order} | jq -r '._links["restbucks:payment"].href'))
done

# Order all two drink combinations
for ((i = 0; i < ${#drinks[@]} - 1; i++)); do
for ((j = i + 1; j < ${#drinks[@]}; j++)); do
((drinks_index++))
((num_orders++))
drink_order="${drinks[i]},${drinks[j]}"
echo "Order $num_orders:$location_index:$drinks_index - $location ${drink_names[i]}, ${drink_names[j]}"
order=$(echo "{ \"drinks\": [${drink_order}], \"location\": \"${location}\" }" | http http://localhost:8080/orders)
payment_links+=($(echo ${order} | jq -r '._links["restbucks:payment"].href'))
done
done

done

if [ "$num_orders" -ne "$expected_num_orders" ]; then
error_flag=true
echo "ERROR: Expected to generate $expected_num_orders, but generated $num_orders"
else
echo "Generated $num_orders orders"
fi

##### MAKE PAYMENTS

echo "Submitting payments"
creditCardNumber=""
expectedResult=""
num_payments_submitted=0
num_payments_accepted=0
for payment_link in "${payment_links[@]}"; do
# Conditional logic to set the credit card number based on the random number
random_number=$(( RANDOM % 10 )) # Generate a random number between 0 and 99
if [ "$random_number" -lt 8 ]; then
creditCardNumber="1234123412341234" # 80% of the time, card is valid
expectedResult="201"
elif [ "$random_number" -lt 9 ]; then
creditCardNumber="1111222233334444" # 10% of the time, card not found in db
expectedResult="500"
else
creditCardNumber="abcdefghijklmnop" # 10% of the time, invalid format
expectedResult="400"
fi

status_code=$(echo "{ \"number\": \"${creditCardNumber}\" }" | http PUT ${payment_link} --print=h | grep HTTP | awk '{print $2}')
if [ $status_code == $expectedResult ]; then
((num_payments_submitted++))
echo "OK: HTTP status $status_code for card $creditCardNumber at $payment_link"
if [ $status_code == "201" ]; then
((num_payments_accepted++))
fi
else
error_flag=true
echo "ERROR: Expected HTTP status $expectedResult, but got $status_code for card $creditCardNumber at $payment_link"
fi
done
echo "$num_payments_accepted of $num_payments_submitted payments passed credit card validation"

echo
if [[ $error_flag == "true" ]]; then
echo "ERROR!! Expected to process $num_orders payments, but processed $num_payments_submitted"
else
echo "SUCCESS: Expected and actual results match"
fi
26 changes: 23 additions & 3 deletions server/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,18 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/ma
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.0-M3</version>
<version>3.3.0-RC1</version>
</parent>

<properties>
<bytebuddy.version>1.14.13</bytebuddy.version>
<datasourceMicrometerSpringBootVersion>1.0.2</datasourceMicrometerSpringBootVersion>
<hibernate-entitymanager.version>6.5.0.RC1</hibernate-entitymanager.version>
<java.version>22</java.version>
<jmolecules.version>2023.2.0-SNAPSHOT</jmolecules.version>
<spring-modulith.version>1.2.0-M3</spring-modulith.version>
<lombok.version>1.18.32</lombok.version>
<jmolecules.version>2023.1.3</jmolecules.version>
<spring-framework.version>6.2.0-M2</spring-framework.version>
<spring-modulith.version>1.2.0-RC1</spring-modulith.version>
</properties>

<dependencyManagement>
Expand Down Expand Up @@ -256,6 +258,24 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/ma
<version>${datasourceMicrometerSpringBootVersion}</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-docker-compose</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>

<!-- For Event Publication Registry, also leveraging Docker Compose-->
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-starter-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>

</dependencies>
</profile>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
*/
package org.springsource.restbucks.order;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.ToString;
Expand All @@ -40,7 +42,8 @@ public class OrderPaid implements DomainEvent {
*
* @param orderId the id of the order that just has been payed
*/
public OrderPaid(OrderIdentifier orderId) {
@JsonCreator(mode = JsonCreator.Mode.PROPERTIES) // Needed for republishing events from registry
public OrderPaid(@JsonProperty OrderIdentifier orderId) {
this.orderId = orderId;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,15 @@
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springsource.restbucks.order.Order;
import org.springsource.restbucks.order.Order.OrderIdentifier;
import org.springsource.restbucks.payment.Payment.PaymentIdentifier;

/**
* Repository interface to manage {@link Payment} instances.
*
* @author Oliver Gierke
*/
interface Payments extends CrudRepository<Payment<?>, Long>, PagingAndSortingRepository<Payment<?>, Long> {
interface Payments
extends CrudRepository<Payment<?>, PaymentIdentifier>, PagingAndSortingRepository<Payment<?>, PaymentIdentifier> {

/**
* Returns the payment registered for the given {@link Order}.
Expand Down
7 changes: 7 additions & 0 deletions server/src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,11 @@ spring.application.name=restbucks
management.tracing.enabled=true
management.tracing.sampling.probability=1.0

# Event Publication Registry
spring.modulith.events.jdbc-schema-initialization.enabled=true
spring.modulith.republish-outstanding-events-on-restart=true
spring.docker.compose.lifecycle-management=start-only
#restbucks.engine.processing-time=5s

spring.threads.virtual.enabled=true

Loading