Showing posts with label Java SE 8. Show all posts
Showing posts with label Java SE 8. Show all posts

Tuesday, August 23, 2016

Java 8'de Optional Kullanımı


Java'da iki tür tip bulunuyor: basit tipler ve sınıflar. Değişkeni bellekte bir kutu olarak düşünebiliriz. Basit tipten bir değişken için bu kutunun içinde tanımlandığı basit tipten bir değer yer alır. Java'daki basit tipler fazla değil: byte, short, int, long, float, double, char ve boolean. Bunun dışındaki her tip bir sınıftır. Sınıf tipinden bir değişken ise referans değişkeni olarak adlandırılır. Referans tipinden bir değişken için ise kutunun içinde bir değer değil, bir adres yer alır. Bu adres ilgili sınıftan bir nesnenin adresidir. Java'da tüm nesneler Heap'de olmak zorundadır. Bu nedenle nesnelere bazen Heap nesnesi dendiği olur. Ancak referans tipinden bir değişken için kutunun içi boş olabilir. Kutunun içinin boş olduğunu, geçerli bir nesneyi göstermediğini tanımlamak için Java dilindeki bir sözcükten yararlanıyoruz: null. Eğer kutunun içinde null değeri var ise bu değişkeni kullanmasanız iyi olur. Eğer referans değişkenimiz null değeri taşıyorsa, bu referansı kullanarak bir metot çağrısı yapmanız, yürütme zamanında NullPointerException (NPE) tipinde bir hata almanıza neden olur. 


NPE bir RuntimeException olduğu için en azından derleyici sizi bu tür bir hatayı ele almak zorunda bırakmaz. Ele almadığınız durumda ise uygulamanız istenmeyen bir şekilde sona erecektir. Kutunun içinin boş olması, referans değişkeninin değerinin null olması, bazen mantıksal bir hatanın sonucudur. Bu hatayı mutlaka gidermeniz gerekir. Kutunun içinin boş olması, bazen de işin doğası gereği olabilir. İşin doğası gereği null değeri taşıyacak ise bu referans üzerinden erişimden önce mutlaka önlem alınması gerekir:
public class MovieSearchService {
. . .
    Collection<Movie> search(Criteria criteria){ . . . }
. . .
}

MovieSearchService içindeki search() metodu verilen bir kritere göre sorgu sonucu dönüyor ve sorgu sonucu boş olabilir. Bu durumda metot null dönüyorsa sorgu sonucuna erişimden önce mutlaka önlem almak gerekir:
MovieSearchService serchSrv= InMemeoryMovieSearchService.getInstance();
Criteria someCriteria= new Criteria(/* irrelavent */); 
Collection<Movie> searchResult= searchSrv.search(someCriteria);
if (searchResult != null){
   System.out.println(String.format("There are %d movies found:",searchResult.size()));
   searchResult.stream().forEach(System.out::println)    
} 

Eğer bir metot torba dönüyorsa, uygulama programcısını NPE'ye neden olmayacak ancak yukarıdaki gibi bir önlem almaya zorlamayacak bir çözüm sunabiliriz. Bunun için eğer arama kriterine uygun hiç bir film yoksa basitçe içi boş bir liste dönebiliriz: Collections.emptyList(). Bunun yanında, Java 8 ile birlikte doğrudan torba dönmek yerine Stream<> ya da CompletableFuture<> dönülebilir:
public interface ManagedServerRepository extends Repository<ManagedServer,Long> {
     Optional<S extends ManagedServer> findOne(Long id);
     <S extends ManagedServer> S save(S server);
     <S extends ManagedServer> S delete(S server);
     <S extends ManagedServer> S delete(Long id);
     @Query("select m from ManagedServer m")
     Stream<ManagedServer> streamAllServers();
     Stream<ManagedServer> findByServerType(ServerType serverType);
     @Async
     CompletableFuture<List<ManagedServer>> findAll();
}

Stream<> ile uyuşuk işleme (=lazy processing) ve CompletableFuture<> ile reaktif fonksiyonel programlama (=reactive functional programming) yapılabilir. Yukarıdaki kodda Spring Data JPA kullanılmaktadır. Spring Data JPA tabanlı kalıcılık çözümünde, sadece kalıcılık işlemlerinin tanımlandığı bir -arayüz tanımlanır, gerçekleme verilmez. Tüm kalıcılık detayları ile Spring Data JPA çatısı ilgilenecektir. Spring Data JPA çatısı Java Persistence API (JPA) çatısı üzerine kuruludur.
Burada tartışılabilecek diğer bir çözüm, arama kriterine uyan hiç bir film bulunamadığında, bunu bir hata durumu olarak kabul edip hatayı açıklayan bir Exception nesnesi fırlatmak olabilir. Bu durumda metot kritere uyan en az bir film bulduğunda bir torba döner ya da sorgu sonucu boş ise Exception nesnesi fırlatır. Her iki çözüm de herhangi bir önlem alınmış olunsun ya da olunmasın NPE'ye neden olmaz. Bu çözümden hangisinin tercih edileceği işin sahibinin vermesi gereken bir karardır. Eğer genel bir API tasarlanıyor ise API'nin her iki çözümü de içermesi tercih edilir. Kararı bırakın çağıran taraf kendi ihtiyacına göre versin.

Nesneyi Optional ile Sarmallamak

Java 8 ile birlikte üçüncü bir çözüm daha geldi: Optional. Optional sınıfı bir nesneyi saklama ve erişmek için bir çözüm sunuyor. Eğer saklayacağı nesne null olabiliyor ise Optional.ofNullable metodu ile yaratıyoruz:
@Override
public Optional<Country> findCountryByCode(String code) {
 Country country = countries.get(code);
 return Optional.ofNullable(country);
}

Eğer nesnenin null olmaması gerekiyorsa değeri Optional.of() metodu ile sarmallıyoruz:
Optional<Double> average= Optional.of(Double.NaN);

Optional.of() metodu null kontrolü yapar ve parametre null ise NPE fırlatır:
public final class Optional<T> {

   . . .

   public static <T> Optional<T> of(T value) {
        return new Optional<>(value);
   }

    private Optional(T value) {
        this.value = Objects.requireNonNull(value);
    }

    . . .

}

 
public final class Objects {

    . . .

   public static <T> T requireNonNull(T obj) {
        if (obj == null)
            throw new NullPointerException();
        return obj;
    }   

    . . .

}

Eğer sarmallanacak değer null ise bunu doğrudan Optional.empty() çağrısı ile oluşturabiliriz:
Optional<Double> average= Optional.empty(); 

Optional ile Nesneye Ulaşmak

Optional içinde sakladığımız nesneye ulaşmak için yukarıda tartıştığımız şekillerde tasarlanmış farklı davranışlara sahip metotlar bulunuyor. get() metodu ile Optional'da saklanan nesnenin referansına ulaşırsınız. Ancak Optional içinde nesne yoksa NoSuchElementException() alırsınız:
public final class Optional<T> {

    . . .

    public T get() {
        if (value == null) {
            throw new NoSuchElementException("No value present");
        }
        return value;
    }

    . . .

}

NoSuchElementException yine bir RuntimeException türünde bir hatadır. Collection API içindeki Iterator arayüzündeki next() metodu da eğer torbada ziyaret edilecek başka bir eleman yoksa NoSuchElementException fırlatır. Bu hatayı almamak için Optional içinde bir değer olup olmadığını sorgulayabilirsiniz:
Optional<Country> country= countryDao.findCountryByCode("KLM");
if (country.isPresent())
    System.out.println(country.get().getName()); 

Optional içinde yukarıdaki çözümü daha yalın bir şekilde karşılayacak  başka bir metot yer alıyor:
Consumer<Country> countryNameConsumer = c -> System.out.println(c.getName());
country.ifPresent(countryNameConsumer);

Yukarıdaki çözümü daha da yalın ifade edebiliriz:
country.map(Country::getName).ifPresent(System.out::println);

Eğer Optional içinde değer yoksa alternatif bir değerin kullanılmasını isteyebiliriz:
String countryName= country.map(Country::getName).orElse("Not found!");

Şimdi orElse() metodunun Optional sınıfındaki tanımına bakalım:
public final class Optional<T> {
    . . .
    public T orElse(T other) {
        return value != null ? value : other;
    }
    . . .
}

Eğer aradığımız kodda bir ülke yoksa yeni bir ülke yaratmak için Optional sınıfındaki orElseGet() çağrısını kullanıyoruz:
CountryDao countryDao= InMemoryWorldDao.getInstance();
Optional<Country> country= countryDao.findCountryByCode("KLM");
Country found= country.orElseGet(Country::new);

Eğer aradığımız kodda bir ülke yoksa Exception fırlatmak istersek Optional sınıfındaki orElseThrow() çağrısını kullanabilirsiniz:
CountryDao countryDao= InMemoryWorldDao.getInstance();
Optional<Country> country= countryDao.findCountryByCode("KLM");
Country existing= country.orElseThrow(NoSuchElementException::new);

Son olarak Optional içindeki fonksiyonel programlamaya yönelik flatMap() metodunu çalışacağız:
CountryDao countryDao= InMemoryWorldDao.getInstance();
Optional<Country> country= countryDao.findCountryByCode("TUR");
country.flatMap(Country::getCities).ifPresent(System.out::println);

Yukarıdaki örnekte ülke kodu ile aradığımız ülkenin şehirlerini listelemek istiyoruz. Yukarıdaki kodu anlamak için Country sınıfındaki getCities() metodunun tanımına göz atmamız gerekir:
public class Country {

     . . .

     public Optional<List<City>> getCities() {
         return Optional.ofNullable(cities);
     }

     . . .

}

Dikkat ederseniz getCities() metodu Optional<List<City>> tipinden bir değer dönüyor. Böylelikle flatMap() metodu Optional<Country>'yi Optional<List<City>>'ye dönüştürüyor.

Modellemede Optional Kullanımı

Nesneye dayalı tasarımda Optional<> ile neleri sarmallamalıyız? Sınıf özniteliklerini (=attributes, fields) ve metod parametrelerini olduğu gibi bırakın, Optional<> içinde sarmallamaya çalışmayın. Optional sınıfının en doğru kullanımı, yukarıdaki örneklerde olduğu gibi metod dönüş değerleri olacaktır. Çağıran tarafta ise Optional ile sarmallanan değerlere get() yerine orElse() ile ulaşmaya çalışın.

Java 9'da Optional'daki Yenilikler

Önümüzdeki yıl Mart ayında Java SE'nin yeni sürümü 9'un çıkması planlanıyor. Java 9Optional sınıfına yeni üç metot tanıştırıyor. Şimdi bu yenilikleri örneklerle çalışalım.  TUR, USA, ITA ve FRA kodlu ülkelerin bilgilerini ekrana yazmaya çalışalım. Java 9 öncesinde bunu aşağıdaki kodla gerçekleştirebiliriz:
Stream.of("TUR","USA","ITA","FRA")
        .map(countryDao::findCountryByCode)
        .map(c -> c.isPresent() ? Stream.of(c.get()) : Stream.empty())
        .forEach(System.out::println);

Java 9'daki ilk yenilik yukarıdaki c -> c.isPresent() ? Stream.of(c.get()) : Stream.empty() ifadesinin kısa yolunu oluşturan stream() çağrısı:
Stream.of("TUR","USA","ITA","FA")
        .map(countryDao::findCountryByCode)
        .map(Optional::stream)
        .forEach(System.out::println);

Gerçekten de Optional sınıfında stream çağrısının gerçeklemesine baktığımızda bu kısa yolu görüyoruz:
/**
 * If a value is present return a sequential {@link Stream} containing only
 * that value, otherwise return an empty {@code Stream}.
 *
 * @apiNote This method can be used to transform a {@code Stream} of
 * optional elements to a {@code Stream} of present value elements:
 *
 * <pre>{@code
 *     Stream<Optional<T>> os = ..
 *     Stream<T> s = os.flatMap(Optional::stream)
 * }</pre>
 *
 * @return the optional value as a {@code Stream}
 * @since 1.9
 */
public Stream<T> stream() {
 if (!isPresent()) {
  return Stream.empty();
 } else {
  return Stream.of(value);
 }
}

Diğer yeni gelen metot ise ifPresentOrElse(). Aşağıdaki örnekte hatalı ülke kodlarını saymak, hatasız ülke kodlarına karşı gelen ülke bilgilerini ise ekrana yazmak istiyoruz:
AtomicInteger counter= new AtomicInteger(0);
Stream.of("TUR","USA","ITA","FA")
      .map(countryDao::findCountryByCode)
      .forEach(
           c -> c.ifPresentOrElse(
                   System.out::println,
                   () -> { counter.incrementAndGet(); } 
                )
       );
System.out.println(counter.get());

Bu uygulama çalıştırıldığında aşağıdaki ekran ekran görüntüsü ile karşılaşılır:
Country{gnp=210721.0, code='TUR', name='Turkey', continent='Asia', surfaceArea=774815.0, population=66591000}
Country{gnp=8510700.0, code='USA', name='United States', continent='North America', surfaceArea=9363520.0, population=278357000}
Country{gnp=1161755.0, code='ITA', name='Italy', continent='Europe', surfaceArea=301316.0, population=57680000}
1

Sonuncu ve daha çok ihtiyaç duyulan metot ise or(). Şimdi, TUR kodlu ülkeyi önce cep (=cache) servisinde , orada yoksa InMemoryWorldDao servisinde arayalım, orada da yoksa en son JdbcWorldDao servisinde arayalım:
Optional<Country> turkey= cache.findCountryByCode("TUR")
                                .or(() -> countryDao.findCountryByCode("TUR"))
                                .or(() -> jdbcDao.findCountryByCode("TUR"));

Wednesday, September 16, 2015

Spring MVC Çatısı Kullanarak Web Uygulaması Geliştirmek

1. Web Mimarileri

Web uygulamalarını, değişimi yönetmek, sorumlulukları biri birinden ayırmak üzere katmanlı mimaride tasarlıyoruz. Örneğin, Java Enterprise Edition (Java EE)'de bu katmanlar, İstemci, Sunum, İş Mantığı, Tümleştirme ve Kaynak katmanlarıdır (Şekil-1). Her bir katman kendinden sonraki katmandan hizmet alırken, kendinden önceki katmana hizmet verir. İstemci çoğu zaman, bir Web Tarayıcısıdır, ama bunun dışında bir masaüstü uygulaması, ya da konsol uygulaması da olabilir. Sunum katmanı uygulamanın kullanıcıya dönük yüzünün oluşturulduğu yerdir. İş Mantığı katmanı işin asıl yapıldığı yerdir. Uygulama muhtemelen kurumun diğer uygulamaları ile birlikte çalışması gerekecektir. Tümleştirme katmanı kurumun bu diğer uygulamaları ile konuşmasını sağlamaktan sorumludur. Bu diğer uygulamalara, kurumun kaynakları gözü ile bakılabilir. Kaynaklar arasında İlişkisel veritabanları, NoSQL veritabanları (MongoDB, Neo4j, Apache Cassandra gibi), Kullanıcı ve şirket bilgilerinin saklandığı katalog sunucuları (OpenLDAP, Active Directory, Apache Directory Project gibi), Java EE, .Net, PHP, Python gibi farklı dil ve platformlarda yazılmış web uygulamaları yer alabilir.

Şekil-1 Web uygulamalarında katmanlar
Klasik Web uygulamalarında, tüm sorumluluk sunucu taraftadır. Her türlü işlem sunucu tarafta gerçekleştirilir. İstemcinin (Web tarayıcısının) herhangi bir sorumluluğu bulunmaz. Bu nedenle Web tarayıcısına herhangi bir eklenti kurulmasına da gerek bulunmaz. Bu nedenle web tarayıcısı, süper ince istemci olarak isimlendirilir:

Şekil-2 Klasik Web mimarisi
Bu mimaride, istemcideki arayüzde gerçekleşen her değişilik için bir isteğin (HTTP İsteği) sunucuya gitmesi, bu isteğin işlenmesi ve tarayıcıda gözükecek arayüzün üretilmesi ve cevabın (HTTP Cevabı) istemciye gönderilmesi gerekir. HTTP cevabı içinde temel olarak HTML ve CSS yer alır. Bu çalışma şekli, sunucunun üzerinde çok fazla yük yaratır ve sunucunun ölçeklenebilirliği üzerindeki en önemli kısıtı oluşturur. Burada Ajax teknolojisi sayesinde sunucu üzerindeki yük biraz azalmıştır. Ajax kullanıldığında, arayüzdeki değişiklik için yine bir istek gidecektir. Ancak istek asenkron olarak gönderilirken, cevap gelinceye kadar geçen sürede kullanıcı uygulama ile etkileşmeye devam edecektir. Ayrıca, sunucu arayüzün tamamını değil, değişikliği oluşturmak için vakit harcayacaktır:

Şekil-3 Ajax teknolojisinin kullanıldığı durum

Sunum katmanında uygulama geliştirirken, Model-View-Controller (MVC) mimari kalıbından yararlanıyoruz. MVC uzun süredir bildiğimiz mimari bir çözüm. Modeli, alan verisini saklayan bir Java sınıfı olarak kodluyoruz. Bu modelin arayüzde nasıl görüntüleneceğini View kontrol ediyor. Kullanıcı ise Controller ile etkileşim kuruyor. Controller, modeli oluşturmak ya da güncellemek, doğrulama yapmak, işi asıl yapacak olan İş Mantığı katmanına ihale etmek ve sunumu yapmak üzere View bileşenine aktarmaktan sorumludur. MVC mimarisinin web uygulamalarında gerçeklerken, dikkat edilmesi gereken ve masaüstü uygulamalarından farklılık gösteren yönleri bulunmaktadır. Model genellikle sunucu tarafta saklanır ve model değiştiğinde sunumun değişmesini sağlayacak doğrudan bir çözüm yoktur. Bunun için örneğin gözlemci kalıbından faydalanılabilir. Ancak gözlemci kalıbını web uygulamalarında gerçeklerken göz önünde bulundurulması gereken önemli bir durum var: HTTP protokolü sadece tek yönlü bir iletişime izin verir. WebSoket protokolü istemci ile sunucu arasında çift taraflı haberleşmeye izin verir ve bu nedenle gerçek MVC'yi web uygulamalarında gerçekleştirmemize olanak sağlar. WebSocket ile birlikte istemci bir istekte bulunmasa bile sunucu istemciye veri gönderebilir. Sunucu tarafta MVC yapmakla ilgili Java platformunda çok sayıda çözüm bulunur: Java Server Pages (JSP)+ Context and Dependency Injection (CDI), Java Server Faces (JSF), MVC 1.0 (Java EE 8), Spring MVC, Apache Wicket, Apache Tapestry, Apache Struts. Bu yazının devamında, Spring MVC kullanarak Şekil-2'de verilen mimarideki gibi bir Web uygulamasının nasıl geliştirileceğinin detaylarına bakacağız.

Son beş yılda istemci tarafta önemli gelişmeler yaşandı. Artık masaüstü ve dizüstü makinalar dışında, akıllı telefon ve tablet gibi cihazlar ile hareket halindeyken bile uygulamalara uzaktan erişebiliyoruz. Bu yeni cihazların hem işlemci yetenekleri, bellek kapasiteleri hem de ekran çözünürlükleri neredeyse masaüstü sistemlerdeki kadar iyi durumda. Örneğin, Samsung T800 marka tablet kullanıyorum. Bu cihazın bir çifti 1.9 GHz'de diğer bir çifti ise 1.3 GHz'de çalışan toplam dört çekirdeği var. Uygulamaların ihtiyaç duyması durumunda hızlı olan 1.9 GHz'de çalışan çift çekirdeği kullanırken, elbette daha fazla akım çekiyor. Eğer işlemci gereksinimi daha düşük olan hafif uygulamalar kullanıyorsam, 1.3 GHz'de çalışan çift çekirdeğe geçiş yapıyor. 10.5" boyutundaki ekranı ise  2560x1600'lük bir çözünürlük sunuyor. Artık İnternet erişimimiz, HD kalitesinde bir filmi kiralayarak ağ üzerinden izlemeye olanak verecek bant genişliğini bize sunuyor. Bu gelişmeler, daha fazla kullanıcı ve saniyede çok daha fazla sayıda HTTP isteği anlamına gelmektedir. Bu gelişmelere paralel olarak uygulamalardan beklentilerde de bazı değişiklikler oldu. Artık uygulamaların hem bağlantılı (=online) hem de bağlantısız (=offline) durumda çalışması isteniyor. Uygulamaların değişken bant genişliklerinde de çalışması bekleniyor. Kullanıcıya zengin kullanıcı arayüzü deneyimi yaşatacak içeriği sunması arzulanıyor.  Bu isteklerin Şekil-3'de verilen web mimarisi ile karşılanması mümkün değildir. Sunum katmanının, kullanıcı arayüzü mantığını sunucudan istemciye, web tarayıcısına taşınması gerekir. Bu değişikliği olanaklı hale getiren gelişme HTML 5 standardı olmuştur. HTML 5 ile birlikte web tarayıcısı uygulama geliştirebileceğimiz bir platforma dönüşmüştür. Bu platformun programlama dili Javascript (JS)'dir. HTML 5 sadece verilerin tarayıcıda nasıl gözükeceğini, sayfanın yapısını kontrol eden takılardan oluşan bir takı teknolojisi değildir. HTML 5 iki önemli yenilik sunuyor:


Şekil-4 İstemci tarafta uygulama geliştirmemizi sağlayan HTML 5 ve tombul istemci mimarisi

İstemci tarafta JS kullanarak uygulama geliştirmek zordur. JS alt düzey bir programlama dilidir. Örneğin JS kullanarak DOM (Document Object Model) üzerinde değişiklik yapmak ve karmaşık bir sayfanın arayüz mantığını kodlamak hem zor hem de oluşan kodun bakımını yapmak neredeyse imkansızdır. O yüzden çok sayıda MV* (MVC, MVVM, MVP) tabanlı JS çatısı geliştirilmiştir: AngularJS, BackboneJS, EmberJS, KnockoutJS gibi. Bu çatıların hepsinde Model sunucu tarafta, genellikle veri tabanında yer alır. Sunucunun yeni görevi bu veriyi istemciye taşımaktır:

Şekil-5 Yeni nesil web uygulama mimarisi

Temel olarak, veri servisi yapan sunucuyu, REpresentational State Transfer (REST) mimarisinde gerçekleştiriyoruz. Bu durumda, ön yüzü, veri servisinin hangi dil ve platformda geliştirildiğinden bağımsız olarak tasarlayabiliriz.

Şekil-6 Veri Servisi ve REST mimarisi

REST mimarisinde sunucu tarafta yönettiğimiz modele Kaynak (=Resource) adını veriyoruz. Ön yüzde Model, sunucuda Kaynak ve veri tabanında Veri olarak adlandırdığımız bu varlıkların hepsi aynı. Bu model/kaynak/veri üzerinde sorgulamak, silmek, güncellemek ve yaratmak gibi işlemler yapmak istiyoruz. REST mimari bir çözüm sunar. Bu mimaride gerçekleme yaparken çoğu zaman HTTP protokolünden ve onun metotlarından yararlanıyoruz: GET, PUT, POST ve DELETE. Temel olarak veri servisi, eğer ilişkisel veri tabanı kullanıyorsa, bu HTTP metotlarını uygun SQL cümlelerine dönüştürür:


REST mimarisinde kaynak farklı şekillerde kodlanabilir. Hatta aynı kaynağa erişen farklı iki istemciye farklı formatta sunum yapılabilir: birine JavaScript Object Notation (JSON), diğerine XML gibi.

Şekil-7 REST mimarisinde farklı istemcilere farklı sunum yapılabilir

Java platformunda, Şekil-7'de verilen mimaride uygulama geliştirilmek istenirse, farklı çözümler oluşturulabilinir:
1. Java EE 6/7
Java EE 6 ile birlikte gelen JAX-RS API'si kullanarak RESTful servisler kodlayabiliyoruz:

Şekil-8 JAX-RS tabanlı çözüm
2. Spring MVC

Spring platformunda Spring MVC kullanarak RESTful servis kodlayabiliyoruz:

Şekil-9 Spring MVC tabanlı çözüm

Son dönemde, büyük ölçekli uygulamaları, yekpare (=monolithic) web uygulaması yerine çeşitli kazanımları nedeni ile mikro servis mimarisinde gerçeklemeyi yeğliyoruz. Spring Boot projesi bu mimaride Spring tabanlı uygulama geliştirmeyi kolaylaştıracak bir çok yenilik getiriyor:

Şekil-10 Spring MVC tabanlı çözüm
Bu yazıda, hem Spring MVC kullanarak Şekil-3'de verilen mimaride hem de Spring Boot ve Spring MVC kullanılarak Şekil-10'da verilen mimaride uygulama geliştirilecektir.

2. Spring MVC

Bu bölümde, sunum katmanında Spring MVC kullanarak Şekil-2'de verilen mimaride uygulama geliştirilecektir. Spring MVC, Spring çatısının temel bileşenlerinden biridir. Spring MVC'de, Spring'in sıradan Java sınıfları olarak kodlanan, biri birine gevşek bağlı ve bileşen tabanlı uygulama geliştirme yaklaşımını, Web uygulaması geliştirirken kullanmaya devam ediyoruz. Spring MVC kendisini hem iletişim protokolünden (HTTP) hem de sunum teknolojisinden (Apache Velocity, JSP, JSF, Freemaker gibi) yalıtmaya çalışır. Şekil-11'de Spring MVC'nin mimarisi verilmiştir. Tüm HTTP istekleri, Spring MVC çatısının bize sağladığı DispatcherServlet Servlet bileşeni tarafından karşılanıyor. HTTP isteği beş adımda karşılanır. HTTP isteği içinde HTTP metodu (GET, POST gibi) ve isteğin hangi URL için yapıldığı bilgisi bulunur. Spring MVC'de Controller sınıflarını kodlarken @RequestMapping damgası kullanılarak Controller sınıfı metodları ile URL eşleştirilir. Handler Mapper, uygulama açılırken, tüm Controller sınıflarımızı tarayıp, bu eşleştirmelerin haritasını çıkarır. Loglardan bu eşleştirmeleri takip etmek mümkündür. DispatcherServlet, Handler Mapper'dan istek paketindeki URL ve metod için çağırması gereken Controller sınıfı ve metodunu öğrenir. Ardından da uygun parametrelerle, bu metodu çağırır. Controller metodu hem modeli hem de sunumu yapacak olan bileşenin lojik adını öğrenir. Spring MVC kendini belirli bir sunum teknolojisinden yalıttığı için bu lojik ismin fiziksel isme dönüştürülmesi gerekir. ViewResolver bu lojik-fiziksel isim dönüşümünden sorumludur. Her bir sunum teknolojisi için ayrı birer ViewResolver sınıfı yer alır. Bunu yapılandırma sırasında belirtiyoruz. DispatcherServlet, son olarak sunumu yapmak üzere isteği ViewResolver'dan öğrendiği fiziksel bileşene iletir. 

Şekil-11 Spring MVC'nin mimarisi
Şimdi, öncelikle örnek uygulamanın alan modeline bir bakalım:

package com.example.stockmarket.domain;

import java.util.Base64;

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name = "stocks")
public class Stock {
 @Id
 private String symbol;
 private String company;
 private double price;
 private byte[] image;

 public Stock() {
 }

 public Stock(String symbol, String company, double price) {
  this.symbol = symbol;
  this.company = company;
  this.price = price;
 }

 public String getSymbol() {
  return symbol;
 }

 public void setSymbol(String symbol) {
  this.symbol = symbol;
 }

 public double getPrice() {
  return price;
 }

 public void setPrice(double price) {
  this.price = price;
 }

 public String getCompany() {
  return company;
 }

 public void setCompany(String company) {
  this.company = company;
 }

 public byte[] getImage() {
  return image;
 }

 public String getImageSrc() {
  return "data:image/png;base64," + Base64.getEncoder().encodeToString(image);
 }

 public void setImage(byte[] image) {
  this.image = image;
 }

 @Override
 public boolean equals(Object o) {
  if (this == o)
   return true;
  if (o == null || getClass() != o.getClass())
   return false;

  Stock stock = (Stock) o;

  return symbol.equals(stock.symbol);

 }

 @Override
 public int hashCode() {
  return symbol.hashCode();
 }

}

Stock sınıfı hisse senetleri ile ilgili bilgiler saklıyor: sembol, şirket adı, fiyatı ve şirket logosu. Geliştireceğimiz uygulama, Stock ekleme, silme, güncelleme, sorgulama ve listeleme işlemlerine izin veren bir arayüze sahip olacaktır:


Uygulamayı Spring Boot uygulaması olarak geliştireceğiz. Uygulama sınıfı MvcApplication.java:

package com.example.stockmarket.application;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Import;

import com.example.stockmarket.config.MvcConfiguration;
import com.example.stockmarket.config.RepositoryConfig;

/**
 * @author Binnur Kurt (binnur.kurt@gmail.com)
 */
@SpringBootApplication()
@Import({RepositoryConfig.class,MvcConfiguration.class})
public class MvcApplication {
    public static void main(String[] args) {
        SpringApplication.run(MvcApplication.class, args);
    }
}

Şimdi yapılandırma sınıflarına (RepositoryConfig.java ve MvcConfiguration.java) bakalım:

RepositoryConfig.java:
package com.example.stockmarket.config;

import org.springframework.boot.orm.jpa.EntityScan;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;


@EnableJpaRepositories(basePackages="com.example.stockmarket.repository")
@EntityScan(basePackages="com.example.stockmarket.domain")
/**
 * 
 * @author Binnur Kurt (binnur.kurt@gmail.com)
 *
 */
public class RepositoryConfig {
}

MvcConfiguration.java:
package com.example.stockmarket.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.view.InternalResourceViewResolver;

/**
 * 
 * @author Binnur Kurt (binnur.kurt@gmail.com)
 *
 */
@Configuration
@EnableWebMvc
public class MvcConfiguration {
      @Bean
      public ViewResolver getInternalResourceViewResolver(){
           InternalResourceViewResolver resolver= new InternalResourceViewResolver();
           resolver.setPrefix("/jsp/");
           resolver.setSuffix(".jsp");
           return resolver;
      }
}

Yapılandırma ile ilgili olarak Spring Boot'un config dizininde beklediği bir application.properties dosyası bulunuyor:

server.port=80
server.servlet-path=/market
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/stockmarket
spring.datasource.username=root
spring.datasource.password=root

Görüldüğü gibi Spring Boot yapılandırma ile ilgili tanımlamaları en aza indiriyor. XML tabanlı herhangi bir yapılandırma dosyasına ihtiyaç duymadık. Veri tabanına erişimde ise Spring Data'yı tercih ettim:

package com.example.stockmarket.repository;

import java.util.Collection;

import org.springframework.data.repository.CrudRepository;

import com.example.stockmarket.domain.Stock;

public interface StockRepository extends CrudRepository<Stock, String> {
 Collection<Stock> findAll();
}

Böylelikle kalıcılıkla ilgili herhangi bir kod yazmak zorunda kalmadım, sadece kalıcılık işlemlerimi arayüzde listeledim. Şimdi, en önemli bileşene bakabiliriz: 

StockController.java
package com.example.stockmarket.controller;

import java.io.IOException;
import java.util.Optional;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;

import com.example.stockmarket.domain.Stock;
import com.example.stockmarket.model.StockForm;
import com.example.stockmarket.repository.StockRepository;

@Controller
@Scope("request")
@RequestMapping("/stockoperation")
public class StockController {
 @Autowired
 private StockRepository stockRepository;

 @RequestMapping(method = RequestMethod.GET)
 public String home() {
  return "home";
 }

 @RequestMapping(method = RequestMethod.POST)
 public String post(Model model, StockForm form, BindingResult bindingResult,
   @RequestParam(value = "image", required = false) MultipartFile image) {
  switch (form.getOperation()) {
  case "Find":
   Stock found = stockRepository.findOne(form.getSymbol());
   if (found != null) {
    model.addAttribute("stock", found);
    model.addAttribute("status", "Found.");
   } else {
    model.addAttribute("status", "Not found!");
   }
   break;
  case "Add":
   Stock newStock = createNewStock(form, image);
   stockRepository.save(newStock);
   model.addAttribute("status", "Added.");
   break;
  case "Delete":
   stockRepository.delete(form.getSymbol());
   model.addAttribute("status", "Deleted.");
   break;
  case "Update":
   found = stockRepository.findOne(form.getSymbol());
   if (found != null) {
    updateStock(form, image, found);
    stockRepository.save(found);
    model.addAttribute("stock", found);
    model.addAttribute("status", "Updated.");
   } else {
    model.addAttribute("status", "Not found!");
   }
   break;
  case "Find All":
   model.addAttribute("stocks", stockRepository.findAll());
   model.addAttribute("status", "Stocks are retrieved.");
   break;
  default:
   model.addAttribute("status", "Undefined operation!");
   break;
  }
  return "home";
 }

 private void updateStock(StockForm form, MultipartFile image, Stock found) {
  found.setCompany(form.getCompany());
  found.setPrice(form.getPrice());
  found.setCompany(form.getCompany());
  found.setImage(getImageBytes(image).orElse(found.getImage()));
 }

 private Optional<byte[]> getImageBytes(MultipartFile image) {
  try {
   return Optional.of(image.getBytes());
  } catch (IOException e) {
   return Optional.empty();
  }
 }

 private Stock createNewStock(StockForm form, MultipartFile image) {
  Stock newStock = new Stock();
  newStock.setSymbol(form.getSymbol());
  newStock.setCompany(form.getCompany());
  newStock.setPrice(form.getPrice());
  newStock.setImage(getImageBytes(image).get());
  return newStock;
 }
}

Son olarak, sunum bileşenini inceleyelim:

home.jsp
<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
 pageEncoding="ISO-8859-1"%>
<%@ taglib prefix="util" tagdir="/WEB-INF/tags"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>Stock Market</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Stockmarket</title>
<style type="text/css">
@import url('/css/bootstrap.css');

@import url('/css/bootstrap-theme.css');
</style>
<script type="text/javascript" src="/js/lib/jquery/jquery.min.js"></script>
<script type="text/javascript" src="/js/lib/bootstrap/bootstrap.js"></script>
</head>
<body>
 <div class="container" role="main">
    <form action="/market/stockoperation" enctype="multipart/form-data" method="post">
  <div class="panel panel-primary">
   <div class="panel-heading">
    <h3 class="panel-title">Stock Information</h3>
   </div>

   <div class="panel-body">

     <div class="form-group">
      <label for="symbol">Symbol:</label> 
      <input type="text" value="${stock.symbol}" class="form-control" name="symbol" id="symbol" />
     </div>
     <div class="form-group">
      <label for="company">Company:</label> 
      <input type="text" value="${stock.company}" class="form-control" name="company" id="company" />
     </div>
     <div class="form-group">
      <label for="price">Price:</label> 
      <input type="text" value="${stock.price}" class="form-control" name="price" id="price" />
     </div>
     <div class="form-group">
      <label for="logo">Logo:</label>
      <c:if test="${not empty stock}">
       <img src="${stock.getImageSrc()}" />
      </c:if>
      <input type="file" name="image" accept="image/*" id="logo" />
     </div>
    <div class="form-group">
     <input class="btn btn-success" type="submit" name="operation" value="Find" /> 
     <input class="btn btn-success" type="submit" name="operation" value="Add" /> 
     <input class="btn btn-success" type="submit" name="operation" value="Delete" /> 
     <input class="btn btn-success" type="submit" name="operation" value="Find All" /> 
     <input class="btn btn-success" type="submit" name="operation" value="Update" />
    </div>
    <div class="form-group">
     <h3>
      <span class="label label-info">${status}</span>
     </h3>
    </div>
   </div>
  </div>
    </form>
  <div class="panel panel-success" data-bind="visible: stocks().length > 0">
   <div class="panel-heading">
    <h3 class="panel-title">Stocks</h3>
   </div>
   <div class="panel-body">
    <table class="table table-striped">
     <thead>
      <tr>
       <th>Logo</th>
       <th>Symbol</th>
       <th>Company</th>
       <th>Price</th>
      </tr>
     </thead>
     <tbody>
        <c:forEach items="${stocks}" var="stock">
      <tr data-bind="event: { mouseover: $parent.displayStock}">
       <td><img src="${stock.getImageSrc()}"></img></td>
       <td>${stock.symbol}</td>
       <td>${stock.company}</td>
       <td>${stock.price}</td>
      </tr>
        </c:forEach>
     </tbody>
    </table>
   </div>
  </div>

 </div>
</body>
</html>

Şimdi çalışan koddan birkaç örnek ekran çıktısına bakalım:

Find butonunun kullanıldığı bir ekran görüntüsü

Find All butonunun kullanıldığı bir ekran görüntüsü

3. Spring MVC ve REST Servis

Bu bölümde, yine Spring MVC kullanarak Şekil-10'da verilen mimaride uygulama geliştirilecektir. Geliştireceğimiz uygulama, 2. Bölüm'de geliştirilen uygulama ile aynı işlevlere ve aynı görünüme sahip olacaktır. Uygulamayı yine Spring Boot uygulaması olarak geliştireceğiz. 

Uygulama sınıfı RestApplication.java:

package com.example.stockmarket.application;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Import;

import com.example.stockmarket.config.RepositoryConfig;
import com.example.stockmarket.config.RestConfiguration;

/**
 * @author Binnur Kurt (binnur.kurt@gmail.com)
 */
@SpringBootApplication()
@Import({RepositoryConfig.class,RestConfiguration.class})
public class RestApplication {
    public static void main(String[] args) {
        SpringApplication.run(RestApplication.class, args);
    }
}

Şimdi yapılandırma sınıfına (RestConfiguration.java) bakalım:

package com.example.stockmarket.config;

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

@Configuration
@ComponentScan(basePackages = {
        "com.example.stockmarket.controller"
})
/**
 * 
 * @author Binnur Kurt (binnur.kurt@gmail.com)
 *
 */
public class RestConfiguration {
}

RESTful servisi kodladığımız StockRestController.java sınıfını inceleyelim:

package com.example.stockmarket.controller;

import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;

import java.util.Collection;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import com.example.stockmarket.domain.Stock;
import com.example.stockmarket.repository.StockRepository;

@RestController
public class StockRestController {
 @Autowired
 private StockRepository stockRepository;

 @RequestMapping(value = "/stock/{symbol}", produces = APPLICATION_JSON_VALUE, method = RequestMethod.GET)
 public Stock find(@PathVariable("symbol") String symbol) {
  return stockRepository.findOne(symbol);
 }

 @RequestMapping(value = "/stock/{symbol}", produces = APPLICATION_JSON_VALUE, method = RequestMethod.DELETE)
 public Stock delete(@PathVariable("symbol") String symbol) {
  Stock found = stockRepository.findOne(symbol);
  if (found != null)
   stockRepository.delete(found);
  return found;
 }

 @RequestMapping(value = "/stocks", produces = APPLICATION_JSON_VALUE, method = RequestMethod.GET)
 public Collection<Stock> findAll() {
  return stockRepository.findAll();
 }

 @RequestMapping(value = "/stock", produces = APPLICATION_JSON_VALUE, consumes = APPLICATION_JSON_VALUE, method = RequestMethod.PUT)
 public Stock add(@RequestBody Stock stock) {
  return stockRepository.save(stock);
 }

 @RequestMapping(value = "/stock", produces = APPLICATION_JSON_VALUE, consumes = APPLICATION_JSON_VALUE, method = RequestMethod.POST)
 public Stock update(@RequestBody Stock stock) {
  Stock found = stockRepository.findOne(stock.getSymbol());
  if (found != null) {
   found.setPrice(stock.getPrice());
   found.setCompany(stock.getCompany());
   found.setImage(stock.getImage());
  }
  stockRepository.save(found);
  return found;
 }

}

Uygulamamız artık Tek Sayfa Uygulaması (=Single Page Application) olarak çalışıyor:

<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Stockmarket</title>
<style type="text/css">
@import url('css/bootstrap.css');

@import url('css/bootstrap-theme.css');
</style>
<title>Stock Market</title>
<script type="text/javascript" src="js/lib/jquery/jquery.min.js"></script>
<script type="text/javascript" src="js/lib/ko/knockout-3.3.0.js"></script>
<script type="text/javascript" src="js/lib/bootstrap/bootstrap.js"></script>
<script type="text/javascript" src="js/lib/ko/ko-file.js"></script>
<script type="text/javascript" src="js/stockmarket.js"></script>
</head>
<body>
<div class="container" role="main">
 <div class="panel panel-primary">
  <div class="panel-heading">
   <h3 class="panel-title">Stock Information</h3>
  </div>
  <div class="panel-body">
   <div class="form-group"
    data-bind="visible: fileData().dataUrl != null">
    <img style="height: 64px;"
     data-bind="attr: { src: fileData().dataUrl }" /> <input
     type="file" data-bind="fileInput: fileData" accept="image/*" />
   </div>
   <div class="form-group">
    <label for="symbol">Symbol:</label> <input type="text"
     data-bind="value: symbol" class="form-control" id="symbol" />
   </div>
   <div class="form-group">
    <label for="company">Company:</label> <input type="text"
     data-bind="value: company" class="form-control" id="company" />
   </div>
   <div class="form-group">
    <label for="price">Price:</label> <input type="text"
     data-bind="value: price" class="form-control" id="price" />
   </div>
   <div class="form-group">
    <button class="btn btn-success" data-bind="click: find">Find</button>
    <button class="btn btn-success" data-bind="click: findAll">Find
     All</button>
    <button class="btn btn-success" data-bind="click: add">Add</button>
    <button class="btn btn-success" data-bind="click: remove">Delete</button>
    <button class="btn btn-success" data-bind="click: update">Update</button>
   </div>
  </div>
 </div>
 <div class="panel panel-success"
  data-bind="visible: stocks().length > 0">
  <div class="panel-heading">
   <h3 class="panel-title">Stocks</h3>
  </div>
  <div class="panel-body">
   <table class="table table-striped">
    <thead>
     <tr>
      <th>No</th>
      <th>Logo</th>
      <th>Symbol</th>
      <th>Company</th>
      <th>Price</th>
     </tr>
    </thead>
    <tbody data-bind="foreach: stocks">
     <tr data-bind="event: { mouseover: $parent.displayStock}">
      <td data-bind="text: $index() + 1"></td>
      <td><img
       data-bind="attr: { src: 'data:image/png;base64,' + image }"></td>
      <td data-bind="text: symbol"></td>
      <td data-bind="text: company"></td>
      <td data-bind="price: price"></td>
     </tr>
    </tbody>
   </table>
  </div>
 </div>
</div>
</body>
</html>

Burada MVC'yi istemci tarafta, web tarayıcısında KnockoutJS kullanılarak gerçekleştirildi. Model sınıfı stockmarket.js dosyasında, elbette javascript kullanarak kodlandı:

ko.bindingHandlers.price = {
 update : function(element, valeuAccessor) {
  var amount = valeuAccessor(), formattedAmount = amount != null ? '$'
    + Number(amount).toFixed(2) : '';
  $(element).text(formattedAmount);
 }
};

var StockMarketViewModel = function() {
 var self = this;

 self.symbol = ko.observable();
 self.company = ko.observable();
 self.price = ko.observable();
 self.image = ko.observable(null);
 self.stocks = ko.observableArray([]);

 self.noicon = ko
   .observable('...');

 self.fileData = ko.observable({
  dataUrl : ko.observable('data:image/png;base64,' + self.noicon())
 });
 
 self.toJson = function() {
  return {
   symbol : self.symbol(),
   company : self.company(),
   price : self.price(),
   image : self.fileData().dataUrl().split(',')[1]
  };
 }
 

 self.find = function() {
  $.ajax({
   method : "GET",
   cache : false,
   url : "http://localhost/market/stock/" + self.symbol(),
   success : function(response) {
    if (response != undefined) {
     self.price(response.price);
     self.company(response.company);
     self.fileData().dataUrl(
       "data:image/png;base64," + response.image);
    }
   }
  });
 }

 self.add = function() {
  console.log(JSON.stringify(self.toJson()));
  $.ajax({
   method : "PUT",
   cache : false,
   url : "http://localhost/market/stock",
   data : JSON.stringify(self.toJson()),
   contentType : 'application/json',
   success : function(response) {
    alert('success');
   }
  });
 }

 self.remove = function() {
  $.ajax({
   method : "DELETE",
   cache : false,
   url : "http://localhost/market/stock/" + self.symbol(),
   success : function(response) {
    if (response.stock != undefined) {
     self.price(response.stock.price);
     self.company(response.stock.company);
    }
   }
  });
 }

 self.update = function() {
  $.ajax({
   method : "POST",
   cache : false,
   url : "http://localhost/market/stock",
   data : JSON.stringify(self.toJson()),
   contentType : 'application/json',
   success : function(response) {
    self.findAll();
   }
  });
 }

 self.findAll = function() {
  $.ajax({
   method : 'GET',
   cache : false,
   url : 'http://localhost/market/stocks',
   success : function(response) {
    self.stocks(response);
   }
  });
 }

 self.displayStock = function(stock) {
  self.symbol(stock.symbol);
  self.company(stock.company);
  self.price(stock.price);
  self.fileData().dataUrl('data:image/png;base64,' + stock.image);
 }
}

var vm = new StockMarketViewModel()
$(document).ready(function() {
 ko.applyBindings(vm);
});

Hem 2. Bölüm'de hem de 3. Bölüm'de geliştirilen uygulamaların koduna bu bağlantıdan Maven projesi olarak erişebilirsiniz. Spring ve yeni nesil web mimarileri ile ilgili daha fazla detay bilgi edinmek için bu adresteki eğitimlere göz atabilirsiniz.