Showing posts with label Event Driven Programming. Show all posts
Showing posts with label Event Driven Programming. Show all posts

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.

Monday, February 10, 2014

Spring çatısında Olaya Dayalı Programlama

Tasarım yaparken, değişiklikleri daha iyi yönetebilmek amacıyla, sınıflar arasında gevşek bağlı bir yapı oluşturulmaya çalışılır. Bu amaçla öncelikle arayüz (=interface) tanımlanır. Bu arayüz bir tür sözleşme işlevi görür. Hizmet alan ve veren sınıflar biri birlerini doğrudan görmezler. Hizmet sağlayan sınıf bu arayüze ya da sözleşmeye  uygun olarak hizmet verir, istemci sınıf ise yine bu sözleşmeye uygun olarak hizmet alır. Böylelikle, ileride servis sağlayıcı sınıf değiştiğinde ya da başka bir sınıftan hizmet alındığında istemci sınıfta bir değişiklik yapmak gerekmez. Dört kafadarın (=GoF, Gang of Four) yayınladığı tasarım kalıpları incelendiğinde, bu şablon hemen hemen tüm kalıplarda karşımıza çıkar. Gevşek bağlı yapı oluşturmak için düşünülen mimari çözümlerden biri de Olaya Dayalı Programlamadır (Event Driven Programming).  Burada öncelikle olay tanımlanır. Sınıflardan biri belirli bir koşul gerçekleştiğinde olay nesnesini yayınlar. Bu olayın gerçekleşmesini gözlemlemek isteyen sınıflar ise olay için abone olurlar ve olay gerçekleştiğinde kayıtlı tüm nesnelere haber verilir. Bu yapıyı Spring çatısında gerçeklemek oldukça kolaydır. Öncelikle olay sınıfı, LargeAmountTransferEvent, oluşturulur:

package com.example.events;

import org.springframework.context.ApplicationEvent;

public class LargeAmountTransferEvent extends ApplicationEvent {
 private double amount;

 public LargeAmountTransferEvent(double amount, Object source) {
  super(source);
  this.amount = amount;
 }

 public double getAmount() {
  return amount;
 }

}

Gözlemci sınıfı oluşturmak için ilgili sınıfta Spring'in ApplicationListener arayüzünü gerçeklemek yeterli olur:

package com.example.events;

import java.sql.Date;
import java.sql.Timestamp;

import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;

@Component
public class LargeAmountTransferListener implements ApplicationListener<LargeAmountTransferEvent>{

 @Override
 public void onApplicationEvent(LargeAmountTransferEvent event) {
  System.out.println("Large amount transfer has occured: "+event.getAmount()+" at "+new Timestamp(event.getTimestamp()));
 }

}

com.example.service.AccountService.java:
package com.example.service;

import com.example.domain.Account;

public interface AccountService {
 boolean transferMoney(Account from,Account to,double amount);
}

Olayın yayınlanacağı sınıfın ise Spring'in ApplicationEventPublisherAware arayüzünü gerçeklemesi gerekir.
Bu arayüzde setApplicationEventPublisher isimli ek bir metot yer alır. Olay ise Spring IoC kabı tarafından sağlanan ApplicationEventPublisher sınıfından bir nesne aracılığı ile yayınlanır:

com.example.service.AccountServiceImpl.java:
package com.example.service;

import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.stereotype.Service;

import com.example.domain.Account;
import com.example.domain.DeficitException;
import com.example.events.LargeAmountTransferEvent;

@Service("accountsrv")
public class AccountServiceImpl implements AccountService,
  ApplicationEventPublisherAware {
 private ApplicationEventPublisher publisher;

 public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
  this.publisher = publisher;
 }

 public boolean transferMoney(Account from, Account to, double amount) {
  if (amount >= 100_000_000)
   publisher.publishEvent(new LargeAmountTransferEvent(amount, this));
  boolean withdraw = false;
  boolean deposit = false;
  try {
   from.withdraw(amount);
   withdraw = true;
   to.deposit(amount);
   deposit = true;
  } catch (Exception e) {
   if (withdraw)
    from.deposit(amount);
   if (deposit)
    try {
     to.withdraw(amount);
    } catch (DeficitException e1) {
     return false;
    }
  }
  return true;
 }
}

com.example.domain.Account.java:
package com.example.domain;

public class Account {
 private String accountId;
 private double balance;

 public Account() {
 }

 public Account(String accountId, double balance) {
  this.accountId = accountId;
  this.balance = balance;
 }

 public String getAccountId() {
  return accountId;
 }

 public double getBalance() {
  return balance;
 }

 public void withdraw(double amount) throws DeficitException {
  if (amount <= 0.0)
   throw new IllegalArgumentException(
     "Amount cannot be negative or zero!");
  if (amount > balance)
   throw new DeficitException(
     "Your balance dows not cover your expenses!", amount
       - balance);
  balance -= amount;
 }

 public void deposit(double amount) {
  if (amount <= 0.0)
   throw new IllegalArgumentException(
     "Amount cannot be negative or zero!");
  balance += amount;
 }

 @Override
 public int hashCode() {
  final int prime = 31;
  int result = 1;
  result = prime * result
    + ((accountId == null) ? 0 : accountId.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;
  Account other = (Account) obj;
  if (accountId == null) {
   if (other.accountId != null)
    return false;
  } else if (!accountId.equals(other.accountId))
   return false;
  return true;
 }

 @Override
 public String toString() {
  return "Account [accountId=" + accountId + ", balance=" + balance + "]";
 }
}

com.example.domain.DeficitException.java:
package com.example.domain;

public class DeficitException extends Exception {
 private double deficit;

 public DeficitException(String message, double deficit) {
  super(message);
  this.deficit = deficit;
 }

 public double getDeficit() {
  return deficit;
 } 
}

com.example.config.AppConfig.java:
package com.example.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

import com.example.domain.Account;

@Configuration
@ComponentScan({"com.example.service","com.example.events"})
public class AppConfig {
 @Bean
 public Account acc1(){
  return new Account("TR123456789", 100_000_000);
 }
 @Bean
 public Account acc2(){
  return new Account("TR987654321", 250_000_000);
 }
}

com.example.TestAppConfig.java:
package com.example;

import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import com.example.config.AppConfig;
import com.example.domain.Account;
import com.example.service.AccountService;

public class TestAppConfig {
 public static void main(String[] args) {
  ConfigurableApplicationContext context=
   new AnnotationConfigApplicationContext(AppConfig.class);
  AccountService accountSrv=
    (AccountService) context.getBean("accountsrv");
  Account account1= context.getBean("acc1",Account.class);
  Account account2= context.getBean("acc2",Account.class);
  accountSrv.transferMoney(account1, account2, 102500000);
  context.close();    
 }
}

Uygulamayı çalıştığımızda aşağıdaki gibi ekran görüntüsü oluşmaktadır:

Feb 10, 2014 12:19:15 AM org.springframework.context.support.AbstractApplicationContext prepareRefresh
INFO: Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@33d4f6b4: startup date [Mon Feb 10 00:19:15 EET 2014]; root of context hierarchy
Feb 10, 2014 12:19:15 AM org.springframework.beans.factory.support.DefaultListableBeanFactory preInstantiateSingletons
INFO: Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@4a2c1b1c: defining beans [org.springframework.context.annotation.internalConfigurationAnnotationProcessor,org.springframework.context.annotation.internalAutowiredAnnotationProcessor,org.springframework.context.annotation.internalRequiredAnnotationProcessor,org.springframework.context.annotation.internalCommonAnnotationProcessor,appConfig,org.springframework.context.annotation.ConfigurationClassPostProcessor.importAwareProcessor,accountsrv,largeAmountTransferListener,acc1,acc2]; root of factory hierarchy
Large amount transfer has occured: 1.025E8 at 2014-02-10 00:19:15.99
Feb 10, 2014 12:19:15 AM org.springframework.context.support.AbstractApplicationContext doClose
INFO: Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@33d4f6b4: startup date [Mon Feb 10 00:19:15 EET 2014]; root of context hierarchy
Feb 10, 2014 12:19:15 AM org.springframework.beans.factory.support.DefaultSingletonBeanRegistry destroySingletons
INFO: Destroying singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@4a2c1b1c: defining beans [org.springframework.context.annotation.internalConfigurationAnnotationProcessor,org.springframework.context.annotation.internalAutowiredAnnotationProcessor,org.springframework.context.annotation.internalRequiredAnnotationProcessor,org.springframework.context.annotation.internalCommonAnnotationProcessor,appConfig,org.springframework.context.annotation.ConfigurationClassPostProcessor.importAwareProcessor,accountsrv,largeAmountTransferListener,acc1,acc2]; root of factory hierarchy

Uygulamanın kaynak koduna bu adresten maven projesi olarak erişebilirsiniz.