In previous post we introduced you all with our new hero Micro Services but now let’s create our first application. Light Bend Inc. created Lagom framework for creating reactive micro services. Its really easy to build micro services applications using Lagom framework because of reduced boiler plate task and code.
Architecture overview of Lagom Application, our application is basically divided into 2 components
- application-api (here we define entity objects and api endpoints)
- application-impl (here we provide the implementation of our services)
Prerequisite
- Knowledge of Java 6 and basics of Java 8 functional programming
- Lagom framework for micro services, just have an overview
- Any IDE supporting Java Development (We are using IntellijIdea)
- Cassandra (Basic SQL queries, we are using embedded cassandra), Kafka is internally used by Lagom Framework for message posting.
- Lombok just basic idea, using for automated creation of immutable objects (Just to make code look cleaner and concise)
- Junit Testing Framework
Architecture Overview
Lagom application works in read and write segregated mode, read side operation which only reads from persistent storage system and doesn’t perform any write operation similarly write side operations makes changes into persistent storage (here we are using embedded cassandra provided by lagom framework)
- We have to create service interface first which contains entity objects and abstract methods which are service endpoints to be exposed to user and their mapping
- Next we have to create service implementation corresponding to abstract methods in service-api. This is the part where we have to focus as we have to define our services here and need to get our hands dirty with code
Below is a snapshot of Lagom project, this is how project structure looks like.
Book.java is Entity class here we are declaring out entity which is to be persisted into DB(we can use any relational database but we are using embedded cassandra), you must be curious as we have not defined getter-setter methods but look there is annotation @Builder lombok framework will create getter-setter on the fly.
package com.knoldus; | |
import com.fasterxml.jackson.databind.annotation.JsonDeserialize; | |
import lombok.AllArgsConstructor; | |
import lombok.Builder; | |
import lombok.Value; | |
import javax.annotation.concurrent.Immutable; | |
@Value | |
@Builder | |
@Immutable | |
@JsonDeserialize | |
@AllArgsConstructor | |
public final class Book { | |
String isbn; | |
String bookName; | |
String authorName; | |
float price; | |
} |
BookService.java extends Service interface of Lagom framework, here we provide abstract methods for each service. ServiceCall interface is there which provides implementation for how service is implemented and invoked by Lagom Framework which receives request and response type parameter. Inside descriptor we specify mapping of Call identifiers with corresponding method to be invoked, here we are using restCall method for identifying service call.
There are three types of Call identifiers
- Named Call : In this we directly provide same name or any alternative name corresponding to service method
- Path Call : As name suggests, here we can specify uri path of the resource mapped with service method.
- Rest Call : In this we provide url pattern to be mapped with service method with http methods (GET, POST, PUT, DELETE etc)
package com.knoldus; | |
import akka.Done; | |
import akka.NotUsed; | |
import com.lightbend.lagom.javadsl.api.Descriptor; | |
import com.lightbend.lagom.javadsl.api.Service; | |
import com.lightbend.lagom.javadsl.api.ServiceCall; | |
import java.util.List; | |
import java.util.Optional; | |
import static com.lightbend.lagom.javadsl.api.Service.named; | |
import static com.lightbend.lagom.javadsl.api.Service.restCall; | |
import static com.lightbend.lagom.javadsl.api.transport.Method.*; | |
public interface BookService extends Service { | |
/** | |
* @param isbn | |
* @return | |
*/ | |
ServiceCall<NotUsed, Optional<Book>> getBook(String isbn); | |
/** | |
* @return | |
*/ | |
ServiceCall<Book, Done> newBook(); | |
/** | |
* @return | |
*/ | |
ServiceCall<Book, Done> updateBook(); | |
/** | |
* @param isbn | |
* @return | |
*/ | |
ServiceCall<NotUsed, Done> deleteBook(String isbn); | |
ServiceCall<NotUsed, List<Book>> getAllBook(); | |
/** | |
* @return | |
*/ | |
@Override | |
default Descriptor descriptor() { | |
return named("book").withCalls( | |
restCall(GET, "/api/books/:isbn", this::getBook), | |
restCall(POST, "/api/books", this::newBook), | |
restCall(PUT, "/api/books", this::updateBook), | |
restCall(DELETE, "/api/books/:isbn", this::deleteBook), | |
restCall(GET, "/api/books", this::getAllBook) | |
).withAutoAcl(true); | |
} | |
} |
Here goes the implementation of BookServiceImpl.java where we provide implementation of abstract methods in service api. Lagom framework create and provide instance of cassandra. We create persistent entity registry and cassandra session which allows us to read and persist the data into cassandra.
import akka.Done; | |
import akka.NotUsed; | |
import com.knoldus.Book; | |
import com.knoldus.BookService; | |
import com.lightbend.lagom.javadsl.api.ServiceCall; | |
import com.lightbend.lagom.javadsl.persistence.PersistentEntityRef; | |
import com.lightbend.lagom.javadsl.persistence.PersistentEntityRegistry; | |
import com.lightbend.lagom.javadsl.persistence.ReadSide; | |
import com.lightbend.lagom.javadsl.persistence.cassandra.CassandraSession; | |
import commands.BookCommand; | |
import events.BookEventProcessor; | |
import javax.inject.Inject; | |
import java.util.List; | |
import java.util.Optional; | |
import java.util.concurrent.CompletionStage; | |
import java.util.stream.Collectors; | |
public class BookServiceImpl implements BookService { | |
private final PersistentEntityRegistry persistentEntityRegistry; | |
private final CassandraSession session; | |
/** | |
* @param registry | |
* @param readSide | |
* @param session | |
*/ | |
@Inject | |
public BookServiceImpl(final PersistentEntityRegistry registry, ReadSide readSide, CassandraSession session) { | |
this.persistentEntityRegistry = registry; | |
this.session = session; | |
persistentEntityRegistry.register(BookEntity.class); | |
readSide.register(BookEventProcessor.class); | |
} | |
/** | |
* @param isbn | |
* @return | |
*/ | |
@Override | |
public ServiceCall<NotUsed, Optional<Book>> getBook(String isbn) { | |
return request -> { | |
CompletionStage<Optional<Book>> bookFuture = | |
session.selectAll("SELECT * FROM book WHERE isbn = ?", isbn) | |
.thenApply(rows -> | |
rows.stream() | |
.map(row -> Book.builder().isbn(row.getString("isbn")) | |
.bookName(row.getString("bookName")) | |
.authorName(row.getString("authorName")) | |
.price(row.getFloat("price")) | |
.build() | |
) | |
.findFirst() | |
); | |
return bookFuture; | |
}; | |
} | |
/** | |
* @return | |
*/ | |
@Override | |
public ServiceCall<Book, Done> newBook() { | |
return book -> { | |
PersistentEntityRef<BookCommand> ref = bookEntityRef(book); | |
return ref.ask(BookCommand.CreateBook.builder().book(book).build()); | |
}; | |
} | |
/** | |
* @return | |
*/ | |
@Override | |
public ServiceCall<Book, Done> updateBook() { | |
return book -> { | |
PersistentEntityRef<BookCommand> ref = bookEntityRef(book); | |
return ref.ask(BookCommand.UpdateBook.builder().book(book).build()); | |
}; | |
} | |
/** | |
* @param isbn | |
* @return | |
*/ | |
@Override | |
public ServiceCall<NotUsed, Done> deleteBook(String isbn) { | |
return request -> { | |
Book book = Book.builder().isbn(isbn).build(); | |
System.out.println(book); | |
PersistentEntityRef<BookCommand> ref = bookEntityRef(book); | |
return ref.ask(BookCommand.DeleteBook.builder().book(book).build()); | |
}; | |
} | |
@Override | |
public ServiceCall<NotUsed, List<Book>> getAllBook() { | |
return request -> { | |
CompletionStage<List<Book>> bookFuture = | |
session.selectAll("SELECT * FROM book") | |
.thenApply(rows -> | |
rows.stream() | |
.map(row -> Book.builder().isbn(row.getString("isbn")) | |
.bookName(row.getString("bookName")) | |
.authorName(row.getString("authorName")) | |
.price(row.getFloat("price")) | |
.build() | |
).collect(Collectors.toList()) | |
); | |
return bookFuture; | |
}; | |
} | |
/** | |
* @param book | |
* @return | |
*/ | |
private PersistentEntityRef<BookCommand> bookEntityRef(Book book) { | |
return persistentEntityRegistry.refFor(BookEntity.class, book.getIsbn()); | |
} | |
} |
Testing Lagom Micro Services
For testing we are using JUnit framework and lagom-javadsl-integration-client for providing mock client for hitting the rest end points. We are creating mock data by calling @Before Junit annotation which fills mock data in cassandra, calling rest end points then verifying data and, at the end we are calling @After Junit annotation for deleting the mock data from cassandra.
package unit; | |
import static org.junit.Assert.assertTrue; | |
import java.net.URI; | |
import java.util.Optional; | |
import java.util.concurrent.CompletionStage; | |
import java.util.concurrent.TimeUnit; | |
import akka.Done; | |
import com.knoldus.Book; | |
import org.junit.After; | |
import org.junit.Before; | |
import org.junit.BeforeClass; | |
import org.junit.Test; | |
import com.lightbend.lagom.javadsl.client.integration.LagomClientFactory; | |
import akka.actor.ActorSystem; | |
import akka.stream.ActorMaterializer; | |
import akka.stream.Materializer; | |
import com.knoldus.BookService; | |
public class BookServiceTest { | |
/** | |
* Demo data for testing | |
*/ | |
private static final String SERVICE_LOCATOR_URI = "http://localhost:8000"; | |
private static LagomClientFactory clientFactory; | |
// private static CommerceContentService commerceContentService; | |
private static BookService bookService; | |
private static ActorSystem system; | |
private static Materializer mat; | |
@BeforeClass | |
public static void setup() { | |
clientFactory = LagomClientFactory.create("unit-test", BookService.class.getClassLoader()); | |
bookService = clientFactory.createDevClient(BookService.class, | |
URI.create(SERVICE_LOCATOR_URI)); | |
system = ActorSystem.create(); | |
mat = ActorMaterializer.create(system); | |
} | |
@Before | |
public void preLoadData(){ | |
Book book1 = Book.builder().isbn("1").bookName("Introduction to Algorithms").authorName("Thomas H. Cormen").price(499.99f).build(); | |
Book book2 = Book.builder().isbn("2").bookName("C Programming").authorName("Dennis Ritchie").price(199.99f).build(); | |
Book book3 = Book.builder().isbn("3").bookName("Operating System Concepts").authorName("Galvin").price(620).build(); | |
Book book4 = Book.builder().isbn("4").bookName("Compiler Design").authorName("Aho Ullman").price(340).build(); | |
try { | |
Done done; | |
done = await(bookService.newBook().invoke(book1)); | |
done = await(bookService.newBook().invoke(book2)); | |
done = await(bookService.newBook().invoke(book3)); | |
done = await(bookService.newBook().invoke(book4)); | |
}catch (Exception exc){ | |
System.out.println("Exception (preLoadData()) : Unable to load data."); | |
} | |
System.out.println("Data inserted successfully."); | |
} | |
@Test | |
public void getBook() throws Exception { | |
String isbn = "1"; | |
Optional<Book> response = await(bookService.getBook(isbn).invoke()); | |
assertTrue(response.isPresent()); | |
if (response.get().getBookName() == null || response.get().getAuthorName() == null || response.get().getPrice() == 0.0) | |
throw new AssertionError("getBook() service failed to return Book Object."); | |
String bookName = "Introduction to Algorithms"; | |
String authorName = "Thomas H. Cormen"; | |
float price = 199.99f; | |
if(bookName.equals(response.get().getBookName())){ | |
if(authorName.equals(response.get().getAuthorName())){ | |
if(price==response.get().getPrice()){ | |
System.out.println("Valid Input."); | |
}else{ | |
System.out.println("Price Mismatch."); | |
assert(true); | |
} | |
}else{ | |
System.out.println("Author Name mismatch."); | |
assert(true); | |
} | |
}else{ | |
System.out.println("Book Name mismatch."); | |
assert(true); | |
} | |
} | |
@After | |
public void postDeleteData(){ | |
try { | |
Done done; | |
done = await(bookService.deleteBook("1").invoke()); | |
done = await(bookService.deleteBook("2").invoke()); | |
done = await(bookService.deleteBook("3").invoke()); | |
done = await(bookService.deleteBook("4").invoke()); | |
}catch (Exception exc){ | |
System.out.println("Exception (preLoadData()) : Unable to delete data from cassandra."); | |
} | |
System.out.println("Data deleted from cassandra successfully."); | |
} | |
private <T> T await(CompletionStage<T> future) throws Exception { | |
return future.toCompletableFuture().get(10, TimeUnit.SECONDS); | |
} | |
} |
Github url of the application Lagom Application
Feel free to comment and post queries, Let me know in a comment if you guys want any topic/topics to be covered.
Gah, this is _almost_ RESTful, if only you used URIs properly: rather than e.g. “/api/get-book/:isbn”, your paths should just be e.g. “/api/books/:isbn”. Your URIs should identify the resource you are operating on, not the operation!
Thanks for your suggestion Chris, I have made changes into service url pattern.
Reblogged this on Bharat Singh.
Reblogged this on LearningPool.