In our last blog post we focused on the Angular side of the generated application. This blog post is all about the Spring Boot server side part. In this post we’ll be adding some service to our HelloBeerTM app.
We’ll be developing on the app we’ve built in our previous JHipster blogs. Code can be found here.
But first let’s take a look at what’s in the server side part of our JHipster app.
Spring Boot architecture
All the entities we’ve added to our domain model will be exposed via REST operations. JHipster generates a layered architecture that corresponds to the hamburger in the picture.
The domain (or entity) object will be placed in the domain package. The corresponding repository will serve as the DAO and is placed in the repository package. Now if you’ve stuck to the defaults during generation, like I did, there will be no service and DTO layer for your entities (you can override this during generation). JHipster’s makers have the philosophy of omitting redundant layers. The service (and DTO) layers should be used for building complex (or composite) services that – for example – combine multiple repositories. The REST controllers by default just expose the domain objects directly and are placed in the web.rest package. JHipster calls them resources.
JHipster – Adding an OrderService
Eventually we wanna push our HelloBeer enterprise to the next level and start selling beers over the internet. So we need a REST service our customers can use for placing orders.
Order Service
So let us begin by adding an Order Service. The JHipster CLI has a command for this:
jhipster spring-service Order
Accept the defaults and in no time JHipster has generated the skeleton for our new OrderService:
package nl.whitehorses.hellobeer.service; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service @Transactional public class OrderService { private final Logger log = LoggerFactory.getLogger(OrderService.class); }
Order DTO
We also need an OrderDTO object to capture the JSON supplied to the exposed REST controller (which we’ll build in one of the next steps). Let’s keep it simple for now. Our OrderDTO contains an order reference (order id), a reference to our customer (customerId), and a list of inventory items (inventoryItemId and quantity) that the customer wants to order.
public class OrderDTO { private Long orderId; private Long customerId; private List<OrderItemDTO> orderItems; .... } public class OrderItemDTO { private Long inventoryItemId; private Long quantity; .. }
Order Service implementation
For our OrderService implementation, we’re just gonna add a few checks making sure our inventory levels won’t run in the negative. If all checks are passed, our item stock levels are updated (i.e. new levels with new stock dates are inserted) according to the order.
@Service @Transactional public class OrderService { private final ItemStockLevelRepository itemStockLevelRepository; private final InventoryItemRepository inventoryItemRepository; private static final String ENTITY_NAME = "order"; private final Logger log = LoggerFactory.getLogger(OrderService.class); public OrderService(ItemStockLevelRepository itemStockLevelRepository, InventoryItemRepository inventoryItemRepository) { this.itemStockLevelRepository = itemStockLevelRepository; this.inventoryItemRepository = inventoryItemRepository; } public void registerOrder(OrderDTO order) throws InvalidOrderException { // Map to store new item stock levels List<ItemStockLevel> itemStockLevelList = new ArrayList<>(); for (OrderItemDTO orderItem : order.getOrderItems()) { ItemStockLevel itemStockLevelNew = processOrderItem(orderItem.getInventoryItemId(), orderItem.getQuantity()); itemStockLevelList.add(itemStockLevelNew); } itemStockLevelRepository.save(itemStockLevelList); log.debug("Order processed"); } // validate order items before processing // - assuming there are no multiple entries for one inventory item in the order // - if one order item entry fails, the whole order fails. private ItemStockLevel processOrderItem(Long inventoryItemId, Long qtyOrdered) { final InventoryItem inventoryItem = inventoryItemRepository.findOne(inventoryItemId); if (inventoryItem == null) { throw new InvalidOrderException("Invalid order", ENTITY_NAME, "invalidorder"); } // find item stock level final Optional<ItemStockLevel> itemStockLevel = itemStockLevelRepository.findTopByInventoryItemOrderByStockDateDesc(inventoryItem); if (!itemStockLevel.isPresent()) { throw new InvalidOrderException("Invalid order", ENTITY_NAME, "invalidorder"); } // check if quantity available Long qtyCurrent = itemStockLevel.get().getQuantity(); Long newqty = qtyCurrent - qtyOrdered; if (newqty < 0L) { throw new InvalidOrderException("Invalid order", ENTITY_NAME, "invalidorder"); } // construct new item stock level ItemStockLevel itemStockLevelNew = new ItemStockLevel(); itemStockLevelNew.setInventoryItem(inventoryItem); itemStockLevelNew.setQuantity(newqty); itemStockLevelNew.setStockDate(ZonedDateTime.now(ZoneId.systemDefault())); return itemStockLevelNew; } }
The code hopefully speaks for itself. In a nutshell: for every order item we first get the inventory item belonging to the inventory item id and check if it exists. Next we get the current item stock level for the inventory item. For this we’ve had to add the findTopByInventoryItemOrderByStockDateDesc method to the ItemStockLevelRepository first:
@SuppressWarnings("unused") @Repository public interface ItemStockLevelRepository extends JpaRepository<ItemStockLevel, Long> { Optional<ItemStockLevel> findTopByInventoryItemOrderByStockDateDesc(InventoryItem inventoryItem); }
This gets us the item stock level at the most recent stock date (note that we get the implementation for free thanks to Spring). If such a level exists, we deduct the quantity ordered from the current quantity and if the current quantity is sufficient, we construct a new item stock level entry. After all order items are processed without validation errors, we store the new set of stock levels.
Not shown here are the OrderServiceIntTest in the nl.whitehorses.hellobeer.service package to test the new service and the new InvalidOrderException. Please check the GitHub code for the details.
Order Controller
Now let us add the controller for the Order Service. The JHipster CLI also has a command for this one:
jhipster spring-controller Order
Just add one POST action called processOrder, and see this controller (and a corresponding test class) being generated:
package nl.whitehorses.hellobeer.web.rest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * Order controller */ @RestController @RequestMapping("/api/order") public class OrderResource { private final Logger log = LoggerFactory.getLogger(OrderResource.class); /** * POST processOrder */ @PostMapping("/process-order") public String processOrder() { return "processOrder"; } }
Order Controller implementation
Alright. Find the code for the implementation below. Not much going on in here. The OrderService will be doing the heavy lifting and the controller just delegates to it.
We let the POST method return the supplied Order object after processing along with a status 200 code:
@RestController @RequestMapping("/api/order") public class OrderResource { private final Logger log = LoggerFactory.getLogger(OrderResource.class); private static final String ENTITY_NAME = "order"; private final OrderService orderService; public OrderResource (final OrderService orderService) { this.orderService = orderService; } /** * POST processOrder */ @PostMapping("/process-order") @Timed public ResponseEntity<OrderDTO> processOrder(@Valid @RequestBody OrderDTO order) { log.debug("REST request to process Order : {}", order); if (order.getOrderId() == null) { throw new InvalidOrderException("Invalid order", ENTITY_NAME, "invalidorder"); } orderService.registerOrder(order); return ResponseEntity.ok(order); } }
I’ve also implemented the generated test class OrderResourceTest. I’ve renamed it from OrderResourceIntTest. Since I’m integration testing the OrderService, I can suffice by unit testing the controller and just mocking the OrderService dependency. See the GitHub code for the test class implementation.
Beer tasting
All right. All pieces are in place to test our new service. If you fire up the application again and check the API menu (admin login required!), you’ll see the Swagger definition of our new service being displayed quite nicely:
You can test the APIs by using the API page, but I prefer to user Postman for testing. You can get the urls from the Swagger definition. Please note that all APIs are secured and you need to send a header parameter along with the request. The parameter (called Authorization) can be found in the CURL section of the definition (click the Try it out! button first to see it):
Note that this parameter will change every time you reboot the server part of the application.
Now let’s first check our initial item stock levels:
As you can see, there is a stock level for inventory item 1 of 100 and a stock level for inventory item 2 of 100.
Let’s order 5 item 1 items and 10 item 2 items and see what happens!
Looking good so far: a response with a 200 Status Code containing the Order we put in our request.
Now for the final check: call the item stock level GET again:
And there you have it: two new item stock level entries with quantities of 95 for inventory item 1 and 90 for inventory item 2.
For completeness, I’ll also show you what happens when you try to order to much of one inventory item:
Great! So our validation also kicks in when needed.
JHipster – Contract-First
Now what about API-First development? As it turns out, JHipster supports that as well. If you paid attention during setup, you will have noticed the API-First option you can select. I didn’t select it when I generated the hb-jhipster application, but if I did, I would have seen a familiar entry in the generated Maven pom:
<plugin> <!-- Plugin that provides API-first development using swagger-codegen to generate Spring-MVC endpoint stubs at compile time from a swagger definition file --> <groupId>io.swagger</groupId> <artifactId>swagger-codegen-maven-plugin</artifactId> <version>${swagger-codegen-maven-plugin.version}</version> <executions> <execution> <goals> <goal>generate</goal> </goals> <configuration> <inputSpec>${project.basedir}/src/main/resources/swagger/api.yml</inputSpec> <language>spring</language> <apiPackage>nl.whitehorses.hellobeer.web.api</apiPackage> <modelPackage>nl.whitehorses.hellobeer.web.api.model</modelPackage> <generateSupportingFiles>false</generateSupportingFiles> <configOptions> <interfaceOnly>true</interfaceOnly> <java8>true</java8> </configOptions> </configuration> </execution> </executions> </plugin>
Yes that’s right: the swagger-codegen-maven-plugin. If you put your Swagger definition in a src/main/resources/swagger/api.yml file, you can generate the Spring Boot code according to the contract by running the following Maven command:
./mvnw generate-sources
See the JHipster documentation for more information.
Feel free to experiment, I’ve already done that in one of my previous blogs here.
The main difference between mine and the JHipster approach is that they’re using the interfaceOnly option, while I used the delegate option.
It’s a subtle difference and mainly a matter of taste. With the interfaceOnly option, you need to implement the API interface and build a controller class complete with annotations. With the delegateOnly option, the ApiController is generated for you and you only need to implement a simple interface. See the picture for the difference (the yellow objects are generated, the blue ones you need to implement yourself).
Summary
In this blog post we’ve explored the server layer of our JHipster app a bit. We used the CLI to generate a new Service and Controller for our HelloBeerTM application. It’s nice to see JHipster helping us as much as possible along the way.
Before exploring the glorious world of microservices and Spring Cloud, I’ll put out 1 last JHipster blog for now (I’ll probably come back to check out their microservice options). In that blog I’m gonna check out the Kafka integration option JHipster provides. So for now: happy drinking and stay tuned!