Sunday, February 1, 2015

CDI ile Olay Temelli Programlama

Dağıtık uygulama geliştirmek, geliştirilen uygulamayı çalışır halde tutmak, izlemek ve hata ayıklamak zordur. Java EE 6 ve 7 platformu bu zorluğun üstesinden gelmek için çeşitli çözümler sunmaktadır. Java EE 5 ile birlikte programlama modelinde önemli bazı değişiklikler gerçekleşmiştir. Programlama modelinin ve mimarinin temel özellikleri aşağıdaki gibi sıralanabilir:
  • Bileşen Tabanlı Programlama
  • Sıradan Java Sınıflarının Kullanımı
  • Betimsel Programlama
   Bileşen tabanlı programlamada bileşenleri yine nesneye dayalı programlamadaki sınıflar olarak kodluyoruz. Nesneye dayalı programlamada modellemeyi sınıflar aracılığı ile gerçekleştiriyoruz. Ardından bu sınıflardan nesneler yaratıyoruz. Java'da yaratılan tüm nesneler Heap alanına yerleşir. Bu nedenle nesnelere Heap nesneleri dendiği de olur. Bu nesneler, biz onlardan istekte bulunmadığımız sürece herhangi bir iş yapmazlar. Bu anlamda pasif olduklarını söyleyebiliriz. Bileşenler de birer nesnedir. Ancak bu nesneler geliştiriciler tarafından new operatörü kullanılarak yaratılmazlar. Bileşenler bir kap (=container) modeline ihtiyaç duyarlar. Kap, bileşenin yaşam döngüsünü yönetir: gerektiğinde bir tane yaratır, gerekli metotlarını çağırır ve gerektiğinde sonlandırır. Bileşenler kaptan çeşitli hizmetler alırlar. Bu hizmetler her kurumsal uygulama geliştirirken tekrar tekrar çözülmesi gereken güvenlik, kalıcılık, kaynak paylaşımı gibi problemlerin çözümleridir. Bu kap Java EE'de uygulama sunucusudur. Uygulama sunucusu bu problemlerin çözümüne bileşenler üzerinden erişmemizi sağlar. Üstelik bu çözüme kod yazarak değil betimsel programlama ile Java'ya Standard Edition 5 ile gelen notlar (=annotations) aracılığı ile erişiyoruz. Uygulama sunucusu iki kaptan oluşur: Web kabı ve EJB kabı. Her bir kabın bir dizi bileşen modeli bulunur. Web kabı Web bileşenlerini, EJB kabı EJB bileşenlerini yönetir. Servlet, JSP ve JSF sayfları birer Web bileşenidir. Yaşam döngüleri Web kabı tarafından yönetilir. Web bileşenlerinin görevi uygulamanın kullanıcıya dönük yüzünü oluşturmakla ilgili sorumlulukları yerine getirmektir. EJB kabında ise Stateless/Stateful/Singleton Session, Message-Driven Bean (MDB) bileşenleri yer alır. Bu bileşenlerin görevi ise iş mantığını çalıştırmaktır. Kısa süreli işleri Session Bean, uzun soluklu ya da yığın işleri MDB bileşenlerine yaptırıyoruz. Session Bean bileşenleri senkron, MDB ise asenkron çalışır. Senkron çalışmada isteği bileşene gönderdikten sonra cevabı bekliyoruz. Ancak cevabı aldıktan sonra çalışmamıza devam ediyoruz. 
   Sınıflar tek bir sorumluluğa sahip ya da tek bir konuda uzman olacak şekilde tasarlanır. Böylelikle sınıflar sadece uzmanlık alanlarında diğer sınıflara hizmet verirler. Bazen bir sınıf uzmanlığı dışında bir iş yapması gerektiğinde, konunun uzmanı sınıf hangisi ise ondan hizmet alır ve isteği bu sınıfa ihale eder. Bu şekilde sınıflar arasında hizmet alma-verme ilişkisi doğar. Bu ilişkinin gevşek bağlı olacak şekilde düzenlenmesi, yazılımdaki değişimin yönetilebilmesi için çok önemlidir. Bunun için hizmet alan ve hizmet veren sınıf arasına bir arayüz yerleştirilir. Bu arayüz bir sözleşme işlevi görür. Sınıflar biri birlerinden bu sözleşmeye uygun olarak hizmet alır ve hizmet verirler. Java programlama dilinde nesneler bu şekilde biri birlerinden hizmet alırlarken, istekler senkron olarak karşılanır. Hizmet alan sınıf cevap gelene kadar bekler. Ancak çok-lifli (=multi-threaded) programlama ile asenkron bir yapı kurulabilir. Hizmet alan ve hizmet veren sınıf arasında asenkron bir yapı kurmanın bir başka yöntemi araya bir mesajlaşma sistemi yerleştirmektir. Hizmet alan nesne isteğini bir mesaj olarak mesajlaşma sistemine iletir ve yoluna devam eder. Hizmet veren nesne mesajı alır ve işler. 
   Message-Driven Bean (MDB) kullanmak için JMS (Java Messaging System) sunucusuna ihtiyaç duyulur. İstemci isteğini bir mesaj olarak kuyruğa yerleştirir ve yoluna devam eder. Hizmet veren sınıf, MDB, vakit buldukça kendisine gelen mesajları alır ve işler. Eğer işlem mili saniye mertebesinde sürelerde tamamlanabilecek sürelerde ise Session Bean, saniye, dakika mertebesinde ya da daha da uzun soluklu ise MDB olarak gerçeklenir. Genellikle uygulama sunucuları üzerinde birer JMS sistemi bulunur. İstenirse JMS sunucusu ayrı olarak da kurulabilir. JMS üzerinden hizmet alan ve veren bileşenler böylelikle fiziksel olarak farklı makinalarda olabilir. Üstelik hizmet veren bileşen, Java EE'de bu MDB olacaktır, henüz ayakta olmasa bile sorun çıkmaz. Bir süre sonra MDB çalışmaya başlar ve kuyrukta biriken mesajları alarak gereğini yerine getirmeye ve iş mantığını çalıştırmaya başlar. 
   JMS üzerinden hem tasarımsal hem de fiziksel olarak gevşek bağlı yapılar oluşturulabilir. JMS tabanlı çözümün en önemli yitimi, asenkron olarak iletilen isteğin sonucu için hazırda bir çözümün olmayışıdır. İşlemin sonucunu veritabanı üzerinden ya da başka bir mesaj kuyruğu üzerinden alınabileceği çözümler kurulabilse de bu mimariyi biraz karmaşık hale getirecektir. 
   Java EE 6 ile birlikte yeni bir bileşen modelimiz daha var: CDI (Contexts and Dependency Injection) Bean. CDI en temelde hizmet alan ve veren bileşenler arasındaki bağımlılıkları yöneten bir çatı sunuyor. Tıpkı Spring, Guice, JBoss Seam gibi. Bunun dışında ilgiye dayalı programlama, olay temelli programlama ve dekoratör tasarım kalıbı gibi çözümlere hızlı bir şekilde ulaşmamızı sağlar. CDI'nın şartnamesine bu bağlantıdan erişebilirsiniz. CDI Java EE'deki diğer tüm API'lerle ve onların bileşenleriyle tümleşik çalışacak şekilde tasarlanmıştır.
   Bu yazıda CDI çatısının olay temelli programlama çözümünü inceleyeceğiz. İncelememizi Wildfly üzerinde çalışan bir Java EE 7 uygulaması üzerinde gerçekleştireceğiz. Öncelikle bir olay sınıfı tasarlamamız gerekiyor. Olay sınıfının hiç bir özelliği bulunmayan, yalın bir Java sınıfı olması istenir. Sadece özniteliklerden oluşan ve durumu değiştirilemez bir sınıf olması tercih edilir. Bu anlamda aykırı durumlar için tasarladığımız Exception sınıfları ile benzerlik gösterir. Exception sınıflarının da sadece hatayı açıklayan öznitelikler içermesini isteriz. Exception sınıfının kesinlikle hatayı çözmeye çalışan metodlar içermemelidir. Örnek uygulamamızda 1000 liranın üzerindeki para akışlarının bir web sayfasından izlenmesi sağlanıyor. Para transferi iş mantığı katmanında bir CDI bileşeni tarafından gerçekleniyor. Önce olay sınıfına bir bakalım:
package com.example.event;

import com.example.domain.Account;

public class OverdraftEvent {
 private Account account;
 private double amount;

 public OverdraftEvent(Account account, double amount) {
  this.account = account;
  this.amount = amount;
 }

 public Account getAccount() {
  return account;
 }

 public double getAmount() {
  return amount;
 }

 @Override
 public int hashCode() {
  final int prime = 31;
  int result = 1;
  result = prime * result + ((account == null) ? 0 : account.hashCode());
  return result;
 }

 @Override
 public boolean equals(Object obj) {
  if (this == obj)
   return true;
  if (obj == null)
   return false;
  if (getClass() != obj.getClass())
   return false;
  OverdraftEvent other = (OverdraftEvent) obj;
  if (account == null) {
   if (other.account != null)
    return false;
  } else if (!account.equals(other.account))
   return false;
  return true;
 }

 @Override
 public String toString() {
  return "OverdraftEvent [account=" + account + ", amount=" + amount
    + "]";
 }

}
Şimdi olayı yayınlayacak, iş mantığı katmanındaki bileşene ve arayüzüne göz atalım:
AccountService.java:
package com.example.service;

public interface AccountService {
 boolean transferMoney(String from, String to, double amount);
}
SimpleAccountService.java:
package com.example.service.impl;
import javax.enterprise.event.Event;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;

import com.example.dao.AccountDao;
import com.example.domain.Account;
import com.example.domain.InsufficientBalance;
import com.example.event.OverdraftEvent;
import com.example.service.AccountService;

@Named
@Singleton
public class SimpleAccountService implements AccountService {
 @Inject
 private Event<OverdraftEvent> event;
 @Inject
 private AccountDao accountDao;

 @Override
 public boolean transferMoney(String from, String to, double amount) {
  Account fromAccount = accountDao.findAccountByIban(from);
  if (fromAccount == null)
   return false;
  Account toAccount = accountDao.findAccountByIban(to);
  if (toAccount == null)
   return false;
  try {
   fromAccount.withdraw(amount);
   toAccount.deposit(amount);
   if (amount > 1_000) {
    OverdraftEvent oe = new OverdraftEvent(fromAccount, amount);
    System.err.println("Firing an event: " + oe);
    event.fire(oe);
   }
  } catch (InsufficientBalance e) {
   return false;
  }
  return true;
 }
}
   Olayı yayınlayacak bileşenin, basitçe Event tipinde bir nesneyi, öznitelik olarak @Inject etmesi ve daha sonra olay gerçekleştiğinde fire çağrısını kullanarak olayı tetiklemesi yeterli olur. Burada bileşenin, bu olayı hangi bileşenlerin dinlendiğini bilmez. Benzer şekilde bu olayın gözlemcileri de olayın hangi bileşen tarafından yayınlandığını bilmeleri gerekmez. Böylelikle bileşenler arasında JMS gibi bir ağır sıklet çözüm kullanılmaksızın gevşek bağlı bir yapı kurulur. Ama burada CDI API'sinin JMS'in yerini aldığı düşünülmemelidir. JMS'in belirgin olarak üstün olduğu noktalar aşağıdaki gibi sıralanabilir:

  • JMS ile kümeleme yapılabilir. CDI ise sadece aynı Java Sanal Makinasındaki bileşenler için yerel bir çözüm üretir.
  • JMS mesajların alındığını garanti eder. Hata oluşursa tekrar deneyebilir.
  • JMS sunucuları dağıtık atomik işlemlerde yer alabilir. 

   Gözlemci sınıf ise bu örnek uygulamada Websocket için tasarlanmış bir @ServerEndpoint bileşeni olacaktır:
package com.example.web.endpoint;

import java.io.IOException;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

import javax.enterprise.event.Observes;
import javax.inject.Singleton;
import javax.websocket.EncodeException;
import javax.websocket.OnClose;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

import com.example.event.OverdraftEvent;

@ServerEndpoint(value="/monitor",encoders=OverdraftEventEncoder.class)
@Singleton
public class AccountMonitor {
 private static Set<Session> peers = Collections
   .synchronizedSet(new HashSet<Session>());

 public AccountMonitor() {
  System.err.println("AccountMonitor is created!");
 }

 @OnOpen
 public void onOpen(Session peer) {
  System.err.println("Session is created: " + peer);
  peers.add(peer);
 }

 @OnClose
 public void onClose(Session peer) {
  System.err.println("Session is closed: " + peer);
  peers.remove(peer);
 }

 public void sendEvent(@Observes OverdraftEvent event) {
  System.err.println("Event has been received!");
  System.err.println(event);
  for (Session peer : peers) {
   try {
    peer.getBasicRemote().sendObject(event);
   } catch (IOException | EncodeException e) {
    e.printStackTrace();
   }
  }
 }
}
Websocket standardı HTML 5 ile gelen ve web tarayıcısı ile sunucu arasında tam çift yönlü (=Full Dublex) haberleşme sağlayan yeni bir protokoldür. Bu protokol yardımı ile istemci istekte bulunmasa bile sunucu web tarayıcısına veri gönderebilir. Bu örnek uygulamada, olay yaratıldığında, bu olayı sunucudan tarayıcılara göndererek, oturum açarak sunucuya bağlı bulunan kullanıcıları anında bilgilendirmek istiyoruz. sendEvent(@Observes OverdraftEvent event) metodunun imzasına baktığımızda önce olay tipinde (OverdraftEvent) bir parametre aldığını görüyoruz. Bu parametreye ayrıca @Observes notunun düşüldüğünü görüyoruz. Bu şekilde sendEvent metodunun OverdraftEvent olaylarına abone olmasını sağladık. Dergi aboneliğine benzer şekilde, derginin her yeni sayısı çıktığında tüm abonelere ulaştırılır. Her bir olay fırlatıldığında bu olayın gözlemcilerine haber verilir. Uygulamanın kullanımına ve çalışmasına ilişkin örnek bir çıktı aşağıdaki şekilde verilmiştir:
Tüm uygulamanın kaynak koduna bu bağlantıdan ulaşabilirsiniz. CDI ve Java EE 7 ile ilgili daha detaylı bilgi edinmek için "Developing Enterprise Applications on Java EE 7" ve "Contexts and Dependency Injection (CDI)" eğitimlerinin içeriklerini inceleyebilirsiniz.

No comments:

Post a Comment