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.

4 comments:

  1. Çok güzel bir yazı olmuş elinize sağlık

    ReplyDelete
  2. Banner hocam elinize kolunuza sağlık yaklaşık 1 haftadır arayıpda bulamadığım tarzda bir örneği anlatmışsınız başarılarınızın ve yazılarınızın devamını dilerim

    ReplyDelete
  3. Sayın hocam emeğinize sağlık güzel ama yazıları yazarken, paragraf, indentler, satırbaşları vs biraz daha okunaklı yazabilirseniz seviniriz. Aksi takdirde böyle güzel yazıların okuması zorlaşıyor.

    ReplyDelete
  4. Teşekkürler Hocam . Adım adım çok iyi anlatıyorsunuz.

    ReplyDelete