Practices Of Using CDI In ZK
Ian YT Tsai, Engineer, Potix Corporation
September 06, 2012
ZK 6
Introduction
In previous article: Starting A Web Application Based On ZK CDI JPA and Jetty, I introduced the configuration and an application stack which is development friendly. In this article, I want to go through our demo project to show you the programming practices and solutions for common scenarios.
About the Demo Application
Basically the demo application is modified from the demo application of ZK Essentials. The application is an online order management system.
This demo consists of 4 parts:
- Login, when it's the first time the user requests the URL: http://localhost:8080/backend_demo, the request will be redirected to the login page and ask user to perform the login process.
- product view, after login, user will be redirected back to main page which has three fragments, the first one is product view which displays all available products.
- shopping cart view, At the east part of main view which displays user's shopping cart content, user can add, remove and change quantity of products in cart.
- order view, The bottom of the main page displays user's order and order details of each order.
Entity Beans of This Appliation
In this demo application we have several entity beans all under package demo.model.bean
- Product, a product with several attributes like name, price, quantity
- CartItem, a shopping cart item which has attribute amount and pointed to a product.
- Order, an order which contains multiple order item.
- OrderItem, an item that has attributes which comes from cartitem
- User, a user with attributes user name and password
Persistence Layer
In the last section of previous article, we know how to define and get an Entity Manager instance in Java program:
EntityManagerFactory factory = Persistence.createEntityManagerFactory("breakfast");
EntityManager em = factory.createEntityManager();
Although getting an Entity Manager using programmatic approach is quite easy, we'd much prefer to use CDI's context injection ability to inject entity manager instance when needed. The sample code of our Data Access Object which requires an Entity manager might looks like this:
public class OrderDAO {
@Inject
EntityManager em;
And now we have a problem, Entity Manager is not a CDI managed bean, which means CDI container doesn't know how to instantiate it, so how to make this work? Or more precisely, how to ask CDI to create an EntityManager from corresponding persistence unit and inject it to all needed managed beans?
CDI Producer for Entity Manager
For beans that are already predefined and not designed as a managed bean presumably, CDI provids a solution named producer. Let's take a look at the sample code below to see how producer works:
@Singleton
public class EMFProducer implements Serializable{
private static EntityManagerFactory factory;
public EntityManagerFactory getEntityManagerFactory() {
if (factory == null) {
factory = Persistence.createEntityManagerFactory("breakfast");
}
return factory;
}
@Produces
@RequestScoped
public EntityManager produceEntityManager() {
System.out.println(">>>> EMFProducer: EntityManager created.");
return getEntityManagerFactory().createEntityManager();
}
}
In the code above, we have our EMFProducer being tagged with @Singleton which means there's only one instance of this class in CDI container. Then we have the produceEntityManager() method being annotated with @produces java annotation which tells CDI container that this is a producer method. Now, for any managed bean who requires an Entity Manager injection (such as the OrderDAO sample above), CDI container will call this producer method according to the given scope @RequestScoped and inject the result into the managed bean.
Data Access Object & Request Scope
Data Access Object is designed to work with entity beans, normally it is better to be request scope object. Here is the way to do it in CDI:
@RequestScoped
@Named(value = "cartitemDao")
public class CartitemDAO {
@Inject
EntityManager em;
public List<CartItem> findByUser(Long userId){
//...
}
Now for any managed bean who needs to have CartitemDAO injected, it will use a new CartitemDAO in every new request.
Logic Layer
Logic Layer is a layer for business objects of this demo, in this demo application, all object about this layer has been put in package demo.web.model.
Business Object Design practice
In this demo application, we have several main business objects which provides a set of logical API to interact with view. They are:
- demo.web.model.ProductManager
- demo.web.model.UserOrderManager
- demo.web.model.UserCredentialManager
- demo.web.model.ShoppingCart
Let's use them to see how to design your business object in some common scenario.
Application Scope Practice
In demo.web.model.ProductManager, as the scenario here is to provide a list of available products for view to display, the code is very simple:
@Named("productManager")
@ApplicationScoped
public class ProductManager {
@Inject
private ProductDAO productDao;
public List<Product> findAllAvailable() {
return productDao.findAllAvailable();
}
}
We give a @Named annotation which will tell CDI the bean name of ProductManager, then we set the scope of our ProductManager to be @ApplicationScoped as product manager is generic to every user.
Inconsistent Scope handling
Though our ProductManager is application scoped, the ProductDAO which CDI injected is RequestScoped, this causes a scope inconsistent problem and needs to be solved.
As figure shown above, to deal with this inconsistency, during injection, CDI created a proxy object which wrapped some code that will get correct instance of ProductDAO according to request scope .
Session Scope Practice
In web logical layer design, a business object which needs to keep state across multiple requests should be stored in session. In CDI, you can annotate a managed bean with @SessionScoped to make sure requests belongs to one session will access the same instance.
To identify if a managed bean should be session scoped or not, you can check if it contains unmanaged bean member fields. Normally a bean contains member fields which have a strong flavor of current user state could be session scoped, for example in our demo application while user try to request http://localhost:8080/backend_demo/index.zul we need to check if current user is authenticated. if not, well redirect him to login.zul for login process:
In order to do this, I designed demo.web.model.UserCredentialManager which is a session object maintaining the state of user credential:
@Named("userCredentialManager")
@SessionScoped
public class UserCredentialManager implements Serializable{
public UserCredentialManager(){}
private User user;
@Inject
private UserDAO userDao;
public synchronized void login(String name, String password) {
User tempUser = userDao.findUserByName(name);
if (tempUser != null && tempUser.getPassword().equals(password)) {
user = tempUser;
} else {
user = null;
}
}
//....
As you can see above, the UserCredentialManager has a login(name, password) method and stored a User bean inside. Here's one thing to be careful, in CDI, a session scoped object needs to be Serializable otherwise CDI will throw exception while parsing bean. To bean's member field, except managed bean(ex: UserDAO) every member field needs to be Serializable or with keyword transient .
You can also inject BO into another BO, for example, in demo.web.model.UserOrderManager we need userCredentialManager to get current user:
@Named("userOrderManager")
@SessionScoped
public class UserOrderManager implements Serializable{
@Inject
private OrderDAO orderDao;
@Inject
private UserCredentialManager userCredentialManager;
public List<Order> findAll() {
return orderDao.findByUser(userCredentialManager.getUser());
}
Presentation Layer
This layer is about how to use BO in ZK's view, the java classes that controlling zk views are all under demo.web.ui.ctrl. In this application we demonstrate how to use CDI bean in ZK MVC controller in:
- demo.web.ui.ctrl.LoginViewCtrl
- demo.web.ui.ctrl.ProductViewCtrl
and how to use CDI bean in ZK MVVM in:
- demo.web.ui.ctrl.OrderViewViewModel
- demo.web.ui.ctrl.ShoppingCartViewModel
Context Injection in ZK
Adopting CDI's context injection in ZK is very simple, it's all about ZK's org.zkoss.zkplus.cdi.DelegatingVariableResolver. Here we will discuss 3 circumstances you can use ZK's CDI DelegatingVariableResolver.
ZK's Listener
In the login process example above, we have a WorkbenchInit declared in index.zul, let's see how to get managed bean in ZK's listener such as org.zkoss.zk.ui.util.Initiator.
In ZK, we have a bunch of Listeners that you can do some interception or flow control. To get CDI managed bean from them, the easist way is to construct a new DelegatingVariableResolver instance:
public class WorkbenchInit implements Initiator {
private UserCredentialManager userCredentialManager;
public void doInit(Page page, @SuppressWarnings("rawtypes") Map arg) throws Exception {
if(userCredentialManager==null){
userCredentialManager = (UserCredentialManager)
new DelegatingVariableResolver().resolveVariable("userCredentialManager");
}
if (!userCredentialManager.isAuthenticated()) {
Executions.getCurrent().sendRedirect("login.zul");
}
}
//...
As illustrated above, we can get our session object userCredentialManager by calling resolveVariable(String) through DelegatingVariableResolver in an Initiator.
ZK MVC
In Listener scenario we use a programmatic approach to get managed bean. In ZK MVC controller, we can use annotations for variable wiring to save coding effort. For example, in demo.web.ui.ctrl.LoginViewCtrl:
@VariableResolver(org.zkoss.zkplus.cdi.DelegatingVariableResolver.class)
public class LoginViewCtrl extends SelectorComposer<Window> {
@Wire
private Textbox nameTxb, passwordTxb;
@Wire
private Label mesgLbl;
@WireVariable
private UserCredentialManager userCredentialManager;
@Listen("onClick=#confirmBtn; onOK=#passwordTxb")
public void doLogin() {
userCredentialManager.login(nameTxb.getValue(), passwordTxb.getValue());
if (userCredentialManager.isAuthenticated()) {
Executions.getCurrent().sendRedirect("index.zul");
} else {
mesgLbl.setValue("Your User Name or Password is invalid!");
}
}
//...
We use @VariableResolver(org.zkoss.zkplus.cdi.DelegatingVariableResolver.class) to annotate LoginViewCtrl which tells super class: SelectorComposer that this controller will based on CDI context to do variable wiring. Then in the member field declaration part, we can use @WireVariable to wire UserCredentialManager. As you can see, by default if the field's name is the name of that CDI bean, the instance will be wired automatically.
ZK MVVM
In ZK MVVM, the way to do variable wiring is very similar to ZK MVC, let's use demo.web.ui.ctrl.OrderViewViewModel for example:
@VariableResolver(org.zkoss.zkplus.cdi.DelegatingVariableResolver.class)
public class OrderViewViewModel {
@WireVariable
private UserOrderManager userOrderManager;
private Order selectedItem;
public Order getSelectedItem() {
return selectedItem;
}
@NotifyChange("selectedItem")
public void setSelectedItem(Order selectedItem) {
this.selectedItem = selectedItem;
}
public List<Order> getOrders() {
return userOrderManager.findAll();
}
@Command
@NotifyChange({"orders", "selectedItem"})
public void cancelOrder() {
if (getSelectedItem() == null) {
return;
}
userOrderManager.cancelOrder(getSelectedItem());
setSelectedItem(null);
}
@GlobalCommand
@NotifyChange("orders")
public void submitNewOrder(
@BindingParam("cartItems")List<CartItem> cartItems
,@BindingParam("orderNote") String orderNote){
userOrderManager.createOrder( cartItems, orderNote);
}
}
We reuse @VariableResolver and @WireVariable annotations here, which makes our View Model object becomes very clean to both ZK view and data.
Notification
By now(ZK 6.0.2) ZK's Variable Resolving approach can only work with named beans in CDI. If your CDI beans are based on Alternative or Producer you'll have to add an adoption layer(a facade or something) to make them accessible with Variable Resolver.
Downloads
- please download the demo project from here
- please follow the instructions from the previous article
Comments
Copyright © Potix Corporation. This article is licensed under GNU Free Documentation License. |