Tuesday, June 2, 2015

CDI için Eklenti Yazımı

Java EE platformu üzerinde uygulama geliştirme modeli için aşağıdaki özelliklerin geçerli olduğunu söyleyebiliriz:
1. Yalın Java sınıflarının kullanıldığı bileşen tabanlı programlama
Bileşenler aynı sınıflardan yarattığımız nesneler gibidir. Ancak onlardan farklı olarak yaşayabilmeleri, var olabilmeleri için bir barınağa ya da kaba (=container) ihtiyaç duyarlar. Barınak bileşenlerin yaşam döngülerini yönetir ve bazı sık kullanılan hizmetler sunar. Kurumsal Java uygulamalarındaki uygulama sunucuları Web kabı ve EJB kabı olmak üzere iki kap ve bileşen modeli içerir. Web kabında Servlet ve JSP bileşen modelleri yer alır. EJB kabında ise Session Bean, Message Driven Bean bileşen modelleri bulunur.
Tarihsel nedenlerden dolayı Java EE'de bir kaç tane bileşen modeli ile karşılaşıyoruz:
  i. EJB kabında çalışan ağır sıklet bileşen modeli: EJB
 ii. Web kabında çalışan hafif sıklet bileşen modeli: EJB Lite
iii. Hafif sıklet bileşen modeli: CDI
     Java EE 6 ile birlikte yeni bir bileşen modelimiz daha var: CDI (Contexts and Dependency InjectionBeanCDI en temelde hizmet alan ve veren bileşenler arasındaki bağımlılıkları yöneten bir çatı sunuyor. Tıpkı SpringGuiceJBoss 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, Java EE'deki diğer tüm API'lerle ve onların bileşenleriyle tümleşik çalışacak şekilde tasarlanmıştır.
2. Java SE 5 ile gelen notlar (=Annotations) kullanılarak sağlanan bildirimsel programlama. Örneğin, CDI bileşenlerini @Bean damgasını kullanarak bildiriyoruz:
package com.example.lottery.web.model;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;

import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.inject.Named;

import com.example.lottery.service.LotteryService;

@Named("lottery")
@RequestScoped
public class LotteryViewModel  {
 private Collection<Integer> numbers;
 @Inject
 private LotteryService lotteryService;

 public LotteryViewModel() {
 }

 public Collection<Integer> getNumbers() {
  numbers = lotteryService.draw();
  return numbers;
 }

}
Her CDI bileşeni bir ada ve erime sahiptir. LotteryViewModel bileşenin adı "lottery" ve erimi ise @RequestScoped damgası ile bildirilmiş. Burada LotteryViewModel, adından anlaşılacağı gibi, sunumda kullanılmak üzere tanımlanmış bir model sınıfı. Sayısal Loto oynamak için biri birinden farklı 1-49 aralığında, 6 tane sıralı sayıya ihtiyaç vardır. Sayısal Loto kuponu için her sayıya ihtiyaç duyduğumuzda getNumbers() metodunu çağırmamız yeterli olur. Ancak sayısal loto sayılarını üretme sorumluluğu LotteryViewModel sınıfına vermedik. Bu sorumluluk, başka bir bileşene ait, ancak biz bu bileşeni bilmiyoruz, bilmek de istemiyoruz. Ne kadar az şey bilirsek o kadar iyi! Bilirsek, bağımlı oluyoruz, biz ise bağımlı olmak istemiyoruz. Belki hizmet alacağımız sınıfa bağımlı olmak istemiyoruz ama kaçınılmaz olarak bir arayüze bağımlıyız: 
package com.example.lottery.service;

import java.util.Set;

public interface LotteryService {
 Set<Integer> draw();
}
Bu sözleşme üzerinden hizmet almaya razıyız. Belki ileride hizmet alacağımız sınıfı değiştirmek isteriz. Hizmet veren sınıf da detaylarını bize göstermek istemez. Bu arayüz ya da sözleşmeye bağlı kalarak hizmet alıp vermek bize hiç yabancı bir durum değil. Oturduğumuz evin, elektrik, su ve doğal gaz aboneliği için sözleşmenin maddelerini okumamış olsak da her biri için ayrı birer sözleşme imzaladık. Artık ülkemizde sözde bir rekabet var. Elektriği daha rekabetçi bir dağıtım şirketinden almayı tercih ederiz. Bu arada elektrik tesisatını değiştirmesek iyi olur. Sözleşmenin maddelerinde değişiklik yapmadan herkes iç işleyişinde değişiklik yapmakta serbest! Bağımlılığı ise @Inject notunu düşerek bildiriyoruz. Uygulama sunucusunun görevi, bu bağımlılığı çözmek. Nasıl olsa tüm bileşenler uygulama sunucusunun yönetiminde, o halde bu görevi kolaylıkla yerine getirebilir! Şimdi bu arayüz üzerinden hizmet veren sınıfa bir göz atalım:
package com.example.lottery.service.impl;

import java.util.Set;
import java.util.TreeSet;

import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;

import com.example.lottery.service.LotteryService;
import com.example.lottery.service.RandomNumberGenerator;

@Named
@Singleton
public class SimpleLotteryService implements LotteryService {
 @Inject
 private RandomNumberGenerator randomNumberGenerator;

 @Override
 public Set<Integer> draw() {
  Set<Integer> number = new TreeSet<>();
  while (number.size() < 6)
   number.add(randomNumberGenerator.generate(1, 49));
  return number;
 }

}
Bu sınıfın da bir başka arayüz üzerinden bağımlılığı bulunuyor:
public interface RandomNumberGenerator {
 int generate(int max);
 int generate(int min,int max);
}
Neyse ki bu arayüzü gerçekleyen bir bileşenimiz bulunuyor:
package com.example.lottery.service.impl;

import java.util.Random;

import javax.inject.Named;
import javax.inject.Singleton;

import com.example.lottery.service.RandomNumberGenerator;

@Named
@Singleton
public class SimpleRandomNumberGenerator implements RandomNumberGenerator {
 private final Random random = new Random();

 @Override
 public int generate(int max) {
  return random.nextInt(max);
 }

 @Override
 public int generate(int min, int max) {
  return random.nextInt(max - min + 1) + min;
 }

}
Şimdi LotteryViewModel'i görünür hale getirmek için bir jsp kodlayalım:
lottery.jsp:
<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
 pageEncoding="ISO-8859-1"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>Lottery Page</title>
</head>
<body>
 <form>
  <input type="submit" value="Draw" />
 </form>
 <h2>
  <ul>
   <c:forEach items="${lottery.numbers}" var="number">
    <li>${number}</li>
   </c:forEach>
  </ul>
 </h2>

</body>
</html>
Modelimize "lottery" ismi vermiştik. JSP sayfasından modele erişirken aynı etiketi kullandık: ${lottery.numbers}.
Peki LotteryService arayüzünü gerçekleyen birden fazla bileşen varsa, oluşan belirsizliği nasıl gidereceğiz? Eğer belirsizliği ortandan kaldırmaz isek uygulamayı sunucuya yayınlayamayız. Çözüm için @Qualifier notundan yararlanacağız. Önce @Qualifier ile damgalanmış notlar oluşturacağız:
Cheap.java
package com.example.lottery.service;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import javax.inject.Qualifier;

@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.TYPE })
@Qualifier
public @interface Cheap {

}
Fast.java
package com.example.lottery.service;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import javax.inject.Qualifier;

@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.TYPE })
@Qualifier
public @interface Fast {

}
Good.java
package com.example.lottery.service;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import javax.inject.Qualifier;

@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.TYPE })
@Qualifier
public @interface Good {

} 

Hızlı, ucuz ve iyi. Hiçbir gerçekleme bunların üçünü de birden sağlamıyor. İkisini seç:


@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD,
      ElementType.METHOD,
      ElementType.TYPE})
@Qualifier
@Fast
@Cheap
public @interface FastCheap {

}

package com.example.lottery.service.impl;

import java.util.Set;
import java.util.TreeSet;

import javax.inject.Named;
import javax.inject.Singleton;

import com.example.lottery.service.FastCheap;
import com.example.lottery.service.LotteryService;

@Named
@Singleton
@FastCheap
public class FastCheapLotteryService implements LotteryService {
 private static int counter = 0;

 @Override
 public Set<Integer> draw() {
  Set<Integer> number = new TreeSet<>();
  while (number.size() < 6) {
   counter = (counter % 49) + 1;
   number.add(counter);
  }
  return number;
 }

}

package com.example.lottery.service;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import javax.inject.Qualifier;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD,
      ElementType.METHOD,
      ElementType.TYPE})
@Qualifier
@Fast
@Good
public @interface FastGood {

}

package com.example.lottery.service.impl;

import java.util.Set;
import java.util.TreeSet;

import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;

import com.example.lottery.service.FastGood;
import com.example.lottery.service.LotteryService;
import com.example.lottery.service.RandomNumberGenerator;

@Named
@Singleton
@FastGood
public class FastGoodLotteryService implements LotteryService {
 @Inject
 private RandomNumberGenerator randomNumberGenerator;

 @Override
 public Set<Integer> draw() {
  Set<Integer> number = new TreeSet<>();
  while (number.size() < 6)
   number.add(randomNumberGenerator.generate(1, 49));
  return number;
 }

}

package com.example.lottery.service;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import javax.inject.Qualifier;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD,
      ElementType.METHOD,
      ElementType.TYPE})
@Qualifier
@Good
@Cheap
public @interface GoodCheap {

}

package com.example.lottery.service.impl;

import java.util.Set;
import java.util.TreeSet;

import javax.inject.Named;
import javax.inject.Singleton;

import com.example.lottery.service.GoodCheap;
import com.example.lottery.service.LotteryService;

@Named
@Singleton
@GoodCheap
public class GoodCheapLotteryService implements LotteryService {
 private static int counter = 0;

 @Override
 public Set<Integer> draw() {
  Set<Integer> number = new TreeSet<>();
  while (number.size() < 6) {
   counter = ( 13 * counter +  43 ) % 49 + 1;
   number.add(counter);
  }
  return number;
 }

}
Hangi gerçeklemeyi tercih edeceğimizi ise @Inject ile bağımlılığı bildirdiğimiz yerde, yukarıdaki @Qualifier ile tanımladığımız notlardan birini kullanarak bildiriyoruz:
@Named("lottery")
@SessionScoped
public class LotteryViewModel implements Serializable {
 private Collection<Integer> numbers;
 @Inject
 @FastCheap
 private transient LotteryService lotteryService;

 public LotteryViewModel() {
  numbers = new ArrayList<>();
 }

 public Collection<Integer> getNumbers() {
  numbers = lotteryService.draw();
  return numbers;
 }

}
Bu örnekte, FastCheapLotteryService, FastGoodLotteryService ve GoodCheapLotteryService sınıfları aslında, sayısal loto için sayı üretirken kullanılabilecek birer strateji sunuyorlar. Biz ise @FastCheap, @FastGood, @GoodCheap notlarından birini kullanarak bu stratejilerden hangisini kullanacağımıza karar veriyoruz. Ya stratejimizi çalışma zamanında belirli bir algoritmaya göre sürekli değiştirmeye karar verirsek ne yapacağız?
İşte yanıtı:
package com.example.lottery.service.impl;

import javax.enterprise.context.RequestScoped;
import javax.enterprise.inject.Produces;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;

import com.example.lottery.service.Fast;
import com.example.lottery.service.FastCheap;
import com.example.lottery.service.FastGood;
import com.example.lottery.service.GoodCheap;
import com.example.lottery.service.LotteryService;

@Named
@Singleton
public class LotteryStrategy {
 private int count = 0;
 @Inject
 @FastGood
 private LotteryService fastGoodLotteryService;
 @Inject
 @FastCheap
 private LotteryService fastCheapLotteryService;
 @Inject
 @GoodCheap
 private LotteryService goodCheapLotteryService;
 @Inject
 @Fast
 private LotteryService fastLotteryService;

 @Produces
 @RequestScoped
 public LotteryService strategy() {
  count++;
  switch (count % 4) {
  case 0:
   return fastGoodLotteryService;
  case 1:
   return fastCheapLotteryService;
  case 2:
   return fastLotteryService;
  default:
   return goodCheapLotteryService;
  }
 }

}
Gerçi bu örnekte pek işe yarar bir strateji geliştiremedik. Stratejileri sırayla değiştirerek kullanmak da bir strateji sayılır! Sayısal loto için kazanma olasılığımızı arttıracak bir strateji var mı? Ben bilmiyorum. Bilsem de zaten söylemem!
Şimdi konuyu başka bir noktaya çekmek istiyorum: LotteryViewModel sınıfının bağımlılığını biraz daha gevşetebilir miyiz? Aşağıdaki örneğe bir bakalım:
@Named("lottery")
@RequestScoped
public class LotteryViewModel {
 @RandomNumber(min = 1, max = 49, size = 6, distinct = true, sorted = true, containerType = ArrayList.class)
 private Collection<Integer> numbers;
 @RandomNumber(min = 1, max = 100, size = 1)
 private int secret;

 public LotteryViewModel() {
 }

 public Collection<Integer> getNumbers() {
  return numbers;
 }

 public int getSecret() {
  return secret;
 }

}
Evet, şimdi hizmet aldığımız arayüzü bilmiyoruz. Tek bilmemiz gereken basit bir not:
@Random:
package com.example.service;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RandomNumber {
 int min() default 1;
 int max() default 100;
 int size() default 1;
 boolean distinct() default false;
 boolean sorted() default false;
 Class<?> containerType() default Void.class;
}
Bu notu kullanarak alacağımız hizmet ile ilgili tanımlamalar yapabiliyoruz:
  • Kaç tane rastgele sayı üretilecek? 
  • Hangi aralıkta değerlere ihtiyaç var? 
  • Bu sayılar tekil mi olsun? 
  • Sıralansın mı?
Peki, Collection numbers ve int secret değişkenlerinin değerleri nasıl belirlenecek? @Inject notunu düştüğümüzde, bağımlılık otomatik olarak CDI kabı tarafından çözülüyordu. @RandomNumber notu ise CDI'ın bildiği ve eyleme geçtiği bir not değil. Ama CDI genişletilebilir:
package com.example.cdi.extension;

import java.lang.reflect.Field;
import java.util.Set;

import javax.enterprise.context.spi.CreationalContext;
import javax.enterprise.event.Observes;
import javax.enterprise.inject.spi.AnnotatedType;
import javax.enterprise.inject.spi.Extension;
import javax.enterprise.inject.spi.InjectionPoint;
import javax.enterprise.inject.spi.InjectionTarget;
import javax.enterprise.inject.spi.ProcessInjectionTarget;

import com.example.service.RandomNumber;

public class RandomNumberExtension implements Extension {
 public RandomNumberExtension() {  
 }

 public <T> void initializeRandomNumberExtension(
    final @Observes ProcessInjectionTarget<T> pit) {  
  AnnotatedType<T> at = pit.getAnnotatedType();
  for (Field field : at.getJavaClass().getDeclaredFields())
   if (field.isAnnotationPresent(RandomNumber.class)) {
    final InjectionTarget<T> it = pit.getInjectionTarget();
    InjectionTarget<T> wrapped = new InjectionTarget<T>() {
     
     @Override
     public void dispose(T instance) {
      it.dispose(instance);
     }
     
     @Override
     public Set<InjectionPoint> getInjectionPoints() {
      return it.getInjectionPoints();
     }
     
     @Override
     public T produce(CreationalContext<T> ctx) {
      return it.produce(ctx);
     }
     
     @Override
     public void inject(T instance, CreationalContext<T> ctx) {
      try {
       RandomNumberGenerator.generate(instance);
      } catch (InstantiationException e) {
       e.printStackTrace();
      }
     }
     
     @Override
     public void postConstruct(T instance) {
      it.postConstruct(instance);
     }
     
     @Override
     public void preDestroy(T instance) {
      it.preDestroy(instance);
     }
     
    };
    pit.setInjectionTarget(wrapped);
      
   }
 }

}
CDI'ın bu ekletiyi görebilmesi için META-INF/services dizininde javax.enterprise.inject.spi.Extension isimli bir dosya oluşturmamız gerekir. Bu dosya içinde her bir eklenti için bir satır yer alır:
com.example.cdi.extension.RandomNumberExtension
Reflection API kullanarak hizmet alan nesnenin tip bilgisine erişmek mümkün:
package com.example.cdi.extension;

import java.lang.reflect.Field;

public class RandomNumberGenerator {
 private static final Random random = new Random();

 public static void generate(Object o) throws InstantiationException {
  Class<?> clazz = o.getClass();
  for (Field field : clazz.getDeclaredFields()) {
   if (field.isAnnotationPresent(RandomNumber.class)) {
    try {
     RandomNumber rn = field.getAnnotation(RandomNumber.class);
     Class<?> containerType = rn.containerType();
     if (rn.size() > 1) {
      generateCollection(o, field, rn);
     } else if (rn.size() == 1) {
      generateScalar(o, field, rn);
     }
    } catch (IllegalArgumentException | IllegalAccessException e) {
     e.printStackTrace();
    }
   }
  }
 }

 private static void generateCollection(Object o, Field field,
   RandomNumber rn) throws InstantiationException,
   IllegalAccessException {
  Collection<Integer> randomValues = (Collection<Integer>) rn
    .containerType().newInstance();
  int min = rn.min();
  int max = rn.max();
  for (int i = 0; i < rn.size(); ++i)
      randomValues.add(random.nextInt(max - min + 1) + min);
  field.setAccessible(true);
  field.set(o, randomValues);
 }

 private static void generateScalar(Object o, Field field, RandomNumber rn)
   throws InstantiationException, IllegalAccessException {
  int min = rn.min();
  int max = rn.max();
  int randomValue = random.nextInt(max - min + 1) + min;
  field.setAccessible(true);
  field.set(o, randomValue);
 }
}
CDI, Java EE'nin bir API'si olsa da Java SE uygulamalarında da kullanılabilir. Uygulama bir maven projesi olarak geliştirilmek istendiğinde, pom.xml dokümanına aşağıdaki bağımlılığı eklemek gerekir:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 <modelVersion>4.0.0</modelVersion>

 <groupId>com.example</groupId>
 <artifactId>imdb-cdi-javase</artifactId>
 <version>0.0.1-SNAPSHOT</version>
 <packaging>jar</packaging>

 <name>imdb-cdi-javase</name>
 <url>http://maven.apache.org</url>

 <properties>
  <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
 </properties>

 <dependencies>
  <dependency>
   <groupId>org.jboss.weld.se</groupId>
   <artifactId>weld-se-core</artifactId>
   <version>1.1.10.Final</version>
  </dependency>
 </dependencies>
</project>
Uygulama kodunu ise aşağıdaki gibi yazıyoruz:
package com.example.console;

import java.util.ArrayList;
import java.util.Collection;

import javax.enterprise.event.Observes;

import org.jboss.weld.environment.se.events.ContainerInitialized;

import com.example.service.RandomNumber;

public class TestCDI {

 @RandomNumber(min = 1, max = 49, size = 6, distinct = true, sorted = true, containerType = ArrayList.class)
 private Collection<Integer> numbers;
 @RandomNumber(min = 1, max = 100, size = 1)
 private int secret;
  
 public void main(@Observes ContainerInitialized event){
      System.err.println(numbers);
      System.err.println(secret);
 }

}
Kodun tamamına bu bağlantıdan erişebilirsiniz. CDI ile ilgili daha detaylı bilgi edinmek isteyenlere Context and Dependency Injection (OMEGA-376) eğitimini tavsiye ederim.