Mikroservislerin uygulaması üzerine bu yazı dizisindeki ilk 4 yazıyı takip ettiyseniz şu anda elinizde kullanıma (yani consume edilmeye) hazır web servis altyapınız bulunacaktır. Mikroservis tasarımımızda küçük iş parçalarını gerçekleştiren ve JWT ile güvenliği sağlanan işlemler tasarladık. Bu servisleri Eureka 'ya kaydettik ve aralarında load balance yaptık. Elimzdeki gateway ise mikroservislerimizi çağıracağımız kapımız olacak. Böylece sırada MVC (client) uygulamamız geliyor. Bu uygulama ile gerek business açısından gerekse transaction management açısından garantilememiz gereken tutarlılıkları işleyişe koyacağız.
Buradaki MVC uygulamamız bir mobil uygulama da olabilirdi. Sistemimiz gateway sayesinde dışarıdan erişime zaten hazır durumda. Bu da mikroservis mimarimiz ile bir anlamda teknoloji bağımsızlığını sağladığımızı da gösteriyor. Servisler veya bunları çağıran client 'lar http request 'leri üzerinden ilerlediği için ortak bir noktada buluşabiliyor. Bizim yapımızda esnek olmayan ana yapımız Gateway, Eureka ve Config server. Bu da zaten mikroservisin mikroservis olmasını sağlayan omurga. Bu parçaların her birisi için de spring cloud dışında da çözümler mevuttur diye düşünüyorum.
Daha sonra uzunca yazacağım bir konuyu kısaca belirtmem lazım burada. Mikroservisleri araştırdığınızda message queue veya event sourcing gibi kavramlar da karşınıza çıkacaktır. Bunlar da kendi içinde farklı çözümleri veya araçları olan kavramlardır ve genelde küçük servislerin iş gereksinimlerini kendi aralarında yönetmesi için kullanılır. Bizim yapımızda servisleri iş gereksinimlerinden bağımsız tasarlayıp, bunların çalıştırılmasını MVC uygulamasına bıraktık.
Bu kararı en başta servisleri küçük iş parçalarına bölerek verdik aslında. Business kodlarını servislere karıştırmadık. Alternatifi var mıdır? Tabi ki evet. Fakat "herşeyi olan bir mikroservis" yazmaya çalıştığınızda yan binadaki komşunuza kasede yemek ikram etmek için kargo uçağı göndermeniz gibi bir durum oluşur. Biletleme uygulamamız için farklı senaryolarda farklı olası çözümleri serinin son yazısında düşüneceğiz. Bütün proje kodları zaten github 'da mecvuttu. Genel sistem mimarimiz yine burada dursun.
Yazı dizisinde Spring Cloud ile bütün servisleri geliştirmişken spring boot ile MVC uygulaması hazırlamamak mantıksız olacaktır. Bu uygulamanın bütün kodlarını burada yazmayacağım. Zaten konumuz bir MVC projesinin nasıl geliştirileceği değil. Mikroservisi kullanırken belli seviyede veri tutarlılığını sağlayabilecek altyapıyı oluşturacağız. Sonrası istediğiniz gibi şekillenebilir bir uygulama iskeletine dönüşecek. Mikroservisler üzerine "bu sorunlar için bu çözümler veya tool 'lar var" seviyesinde kalmak yerine çalışan bir projemiz olacak bu sayede. Aşağıdaki bağımlılıklarla projeyi oluşturarak başlayalım. Spring MVC projeleri konusunda deneyiminiz yoksa bu yazımdan faydalanabilirsiniz.
Bu projede MVC yapısından faydalanmak için Spring Web bağımlılığı ekledik. Controller 'lar, session 'lar, request 'ler, response 'lar, model ve view gibi sınflar bu dependency sayesinde geliyor. İkinci sıradaki Thymeleaf sayesinde html arayüzleri MVC yapısına kolayca bağlayabileceğiz. Lombok ile model sınıflarında tekrar eden getter setter gibi kodları kısaltmış olacağız. DevTools ile uygulamada bir değişiklik yapıldığında otomatik deploy ve restart olmasını sağlayacağız. POM dosyasında "spring-cloud.version" bilgisine de dikkat etmeniz gerekiyor. 2020... ve sonrası olması gerekiyor.
Bu ekranda görünmeyen bir bağımlılığımız daha var. Servislerden gelen verileri tutmak için aracı POJO sınıflarını ve sabit değerleri tanımladığımız ab-util projesi. Bu projeyi de yazı dizisini takip ettiyseniz indirmişsinizdir. Dependency olarak eklemek için aşağıdaki ifadeyi ekleyebilirsiniz.
<dependency>
<groupId>com.aldimbilet</groupId>
<artifactId>util</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
Bu projede dikkat ederseniz JPA veya Security yok. Security kısmını küçük servisler içinde JWT token 'ları üreten ve kontrol eden kodlarla gerçekleştirdik. Kullanıcı giriş için JWT token 'ını userservice 'den alıyor olacağız. Sonrasında ilgili servis metodlarına JWT header bilgisini göndereceğiz. Bu projede hangi sayfalara kim nasıl ulaşacak konusu için de JWT kullanabilirdik veya spring security ile kolayca güvenliği sağlayabilirdik.
Bir de OpenFeign var. Bu dependency 'nin amacı şu: Mikroservislerde tanımlı restful endpoint 'lerin tüketilmesini sadeleştirmek. Bir restful web servisi çağırmak için bir connection açmanız, get veya post yapmanız ve dönen stream 'i okuyup ilgili veriye çevirmeniz gerekir. Bu işlemleri tekrar tekrar yazmak ve kodu çoğaltmak yerine OpenFeign kullanılıyor. Eskiden Feign olarak geçen bu kütüphane spring cloud bünyesine geçmiş ve openfeign olarak adlandırılmış. Yanlış dependency 'yi kullanmamaya dikkat edin.
Bu kütüpane ile javada metod çağırır gibi "userservice.login()" benzeri kodlar yazarak bütün web servis işlemlerini halledebileceğiz. FAKAT !! Az kod yoktur, görünmeyen kod vardır. Bu işlemlerde belli hatalar aldığınızda Feign bunları exception olarak fırlatır ve MVC uygulaması çöker. Bu yüzden kodlarda try-catch 'ler göreceksiniz. Aşağıda Feign kodlarında incelememiz devam edecek.
Kodlara application.properties ile başlayalım. Bu uygulamanın Eureka 'ya kaydolmak veya config server 'dan konfigürasyonları almak gibi bir işlevi yok tabi. Burası consumer. Bu yüzden bootstrap ihtiyacı da duymadık. Aşağıdaki bilgileri yazmamız yeterli olacaktır.
server.port=80
spring.application.name=ab-webservice
gateway.adress.userservice=http://localhost:4441/user/
gateway.adress.activityservice=http://localhost:4441/act/
gateway.adress.paymentservice=http://localhost:4441/pay/
Burada port belirtmezseniz default olarak 8080 oluyor diye hatırlıyorum. application.name bilgisi de gerekli değil normalde ama property dosyaları çoğalınca hangisiydi bu diye düşünmek zorunda kalmıyorum. Fakat sonraki enteresan property 'ler de neyin nesi? Şöyle ki, spring boot sizin istediğiniz isimde property 'ler tanımlamanıza izin veriyor. Normalde "gateway.adress" şeklinde property 'ler spring bünyesinde yok. Bunları 4441 portunda çalışan gateway 'in nerede olduğunu bilmek için yazdık. Linklerin sonundaki /user, /act ve /pay path 'leri de hatırlarsanız routelocator 'da yönlendirme yaptığımız path 'lerdi ve ilgili servislere yönlendiriliyordu. Yani hangi servisin hangi route 'da olduğunu bilmemiz gerekiyor bu noktada. Sebebini (Feign yüzünden) sonra açıklayacağım.
Main sınıfta Feign ile bu servisleri çağırabilmek için bir Annotation kullanmamız gerekiyor. @EnableFeignClients. Bu şekilde proje ayağa kalkarken bu servisleri çağıracak olan feign client sınıfları da hazırlıyor. Resttemplate oluşturup get ve post 'ları yapacak kodları hazırlıyor arka planda. Bu annotation kritiktir. Ununutursanız FeignClient sınıflar bean 'e çevirilmez ve inject etmek istediğiniz yerde "Consider defining a bean of type '...UserClient' in your configuration." gibi bir hata alırsınız.
@SpringBootApplication
@EnableFeignClients
public class AldimBiletMvcWebClientApplication
{
public static void main(String[] args)
{
SpringApplication.run(AldimBiletMvcWebClientApplication.class, args);
}
}
Sonrasında @FeignClient 'lar oluşturacağım. Çünkü bir önceki yazıdan servislerimi bilyorum, parametrelerini, path 'lerini ve dönüş değerlerini de biliyorum. O zaman bu servisleri kullanacak client 'ları rahatlıkla yazabilirim. Userservice için örnek client kodunu yazarak açıklayayım.
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import com.aldimbilet.pojos.UserRegisterPojo;
@FeignClient(url = "${gateway.adress.userservice}", name = "UserClient")
// Burada bir URL vermemiz gerekiyor, aksi takdirde Feign çağırdığı servisi kendi içinde load balance etmeye çalışıyor
// Bunun yerine Eureka 'da gömülü Ribbon ile (lb:// linklerini hatırlayın) load balance zaten yapıyoruz
// URL yazmamanız durumunda "did you forget load balancer?" gibi bir hata alırsınız
// @FeignClient yazmamanız durumunda bu interface 'den bean oluşmaz ve "consider defining bean" hatası alırsınız
// gateway.adress property 'si application.proeprties içerisinde tanımlı idi
// Metodların içerisini Feign dolduruyor arka planda
public interface UserClient
{
// Javada metod çağırmak gibi burada da dönüş değeri, ismi ve parametreleri aynı olan bir metod yazıyoruz
// path = localhost:4441/user/hello oluyor
@GetMapping(path = "hello")
ResponseEntity sayHello(@RequestHeader(value = Constants.HEADER_STRING) String token);
// Spring security ile login yapabilmek için bir json verisini String olarak göndermemiz yeterli oluyor
// Spring security default olarak bu yapıyı kullanıyor ve ResponseEntity döndürüyor
// Arka planda Jackson isimli kütüphane bu dönüşümleri hallediyor
// path = localhost:4441/user/login oluyor
@PostMapping(path = "login", consumes = MediaType.APPLICATION_JSON_VALUE)
ResponseEntity login(@RequestBody String user);
// Register işlemi için ABUser sınıfı gönderemeyiz, o sınıf userservice 'a ait
// Bu yüzden aracı bir pojo 'muz var (UserRegisterPojo) veriyi taşıyan ve Jackson tarafından Json 'a çevirilen
// Post metodlarında @RequestBody parametresi zorunludur, neyi post ediyorsun anlamında
// path = localhost:4441/user/register oluyor
@PostMapping(path = "register", consumes = MediaType.APPLICATION_JSON_VALUE)
ResponseEntity register(@RequestBody UserRegisterPojo userInfo);
// Burada consumes bilgisi yazılmazsa Feign default olarak başka bir türe veriyi çevirmeye çalışabiliyor
// path = localhost:4441/user/getUserInfo oluyor
// Userservice 'deki getUserInfo metodunda token parametresi yok normalde ama @RequestHeader içinde gittiği için hata almıyoruz
@GetMapping(path = "getUserInfo", consumes = MediaType.TEXT_PLAIN_VALUE)
ResponseEntity getUserInfo(@RequestHeader(value = Constants.HEADER_STRING) String token, @RequestParam String username);
@GetMapping(path = "getUserCard", consumes = MediaType.APPLICATION_JSON_VALUE)
ResponseEntity getUserCard(@RequestHeader(value = Constants.HEADER_STRING) String token, @RequestParam Long userId);
}
Userserivce içerisinde tanımlı http metodlarının birebir karşılığını da burada yazmış olduk. Feign ile ilgili belirtmek istediğim bir özellik daha var. @FeignClient tanımında (configuration = FeignConfig.class) gibi bir konfigürasyon sınıfı yazabilirsiniz. Bu sınıf içerisinde hata yakalama için kodlar yazabilirsiniz, örneğin ErrorDecoder gibi. Veya giden gelen request 'ler üzerinde işlemler yapmak için RequestInterceptor ekleyebilirsiniz. Bu sınıf sayesinde FeignConfig sınıfınız daha da gelişecek fakat yan etkisi olarak muhtemelen iş gereksinimleri ile ilgilenmek zorunda kalacaktır.
Ayrıca feign client sınıfları Feign.Builder() yapısı ile de oluşturulabiliyor. Bu şekilde manuel oluşturduğunuzda da spring framework 'ün kendiliğinden hallettiği encoder - decoder (giden ve gelen veri türü), interceptor, target (client sınıfı) gibi özelliklerini de belirtmeniz gerekecektir. Bu bilgiler şu aşamada işinize yaramayabilir fakat daha fazla fonksiyonu olan bir feign client yazmak isterseniz karşılaşacağınız zorlukları da belirtmek istedim.
Sonunda geldik MVC uygulaması arayüzüne ve bunların controller metodlarına. Thymeleaf ile ilgili kodları burada yazmayacağım. Kodlar github 'da. Arayüz akışımız şu şekilde:
Sistem öncelikle ödemeyi alır. Sonra rezerve etme durumu olmadığı için seçilen etkinlikte yer olup olmadığı sorgulanır. Bilet alınmadan bir bot kullanıcının girip kilitlememesi için böyle bir gereksinim düşünülmüştür. Etkinlik bileti kalmadı ise para iadesi yapılır. Buradaki amacımız mantıklı bir senaryo kurgulamak değil. İş kurallarını işletmek adına servislere yük bindirmediğimiz için örnek bir iş akışını MVC uygulamasında yürütmüş olacağız. Bu akışlarla ilgili mantıksal tutarsızlıkları önleyerek consistency (tutarlılık) sağlayacağız. Bu da iş akışını fault tolerance ile beraber başı ve sonu olan bir hikayeye yani bir SAGA 'ya çevirmiş oluyor.
Programatik hatalar veya servis çökmeleri durumu için de hata yakalama işlemlerimiz olacak. Çünkü feign client sınıfları 200 dışındaki status kodlarını exception olarak görecektir. Feign metodları için ayrı ayrı ExceptionHandler 'lar yazılabilmekle birlikte bütün client 'ı kapsayan config class 'ı yazılabildiğinden bahsetmiştim. Bir başka yöntem de @RestControllerAdvice sınıfında @ExceptionHandler bean 'i tanımlayarak bütün hataları tek bir noktada yakalamaktır. Fakat biz bunların yerine kendi try-catch bloklarımız ile hataları ayıklayacağız. Controller sınıfındaki birkaç metodu açıklamaları ile yazayım.
import java.util.List;
import java.util.Random;
import javax.servlet.http.HttpServletRequest;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.servlet.ModelAndView;
import com.aldimbilet.pojos.ActivityPojo;
import com.aldimbilet.pojos.BasketPojo;
import com.aldimbilet.pojos.CardInfoPojo;
import com.aldimbilet.pojos.UserInfoPojo;
import com.aldimbilet.pojos.UserRegisterPojo;
import com.aldimbilet.util.Constants;
import com.aldimbilet.website.feign.ActivityClient;
import com.aldimbilet.website.feign.PaymentClient;
import com.aldimbilet.website.feign.UserClient;
import com.aldimbilet.website.util.SessionConstants;
import feign.FeignException;
import lombok.AllArgsConstructor;
@Controller
@AllArgsConstructor
public class MikroServiceClientController
{
private UserClient userClient;
private ActivityClient activityClient;
private PaymentClient paymentClient;
@PostMapping(path = "login")
public ModelAndView login(HttpServletRequest req)
{
String username = req.getParameter("username");
String password = req.getParameter("password");
// Spring Security 'ye User bilgisini Json olarak gönderiyoruz
// Kullanıcı adı şifre bilgileri doğru ise cevabında JWT token 'ı alıyoruz
// Spring security aşağıdaki formatta veri beklemektedir, bunu json objesi olarak oluşturabilir veya bir pojodan çevirebilirsiniz
String user = "{\"username\":\"" + username + "\",\"password\":\"" + password + "\"}";
String token = "";
try
{
ResponseEntity responseEntity = userClient.login(user);
token = responseEntity.getBody();
String bearer = token.substring(token.indexOf(" ") + 1);
user = token.substring(token.indexOf("(") + 1, token.indexOf(")"));
req.getSession().setAttribute(SessionConstants.BEARER, bearer);
req.getSession().setAttribute(SessionConstants.USERNAME, user);
return new ModelAndView("redirect:/index");
}
catch (FeignException e)
{
// Bu noktada servis ayakta olmayabilir veya hata fırlatabilir
if (e.status() == HttpStatus.SERVICE_UNAVAILABLE.value())
{
return new ModelAndView("redirect:/login?err=2");
}
else if (e.status() == HttpStatus.UNAUTHORIZED.value())
{
// Ayrıca kullanıcı adı ve şifre de yanlış olabilir
// Userserice içerisinde unsuccessfulAuthentication metodunda UNAUTHORIZED döndürülmüştü
return new ModelAndView("redirect:/login?err=3");
}
else
{
return new ModelAndView("redirect:/login");
}
}
}
@PostMapping(path = "signup")
public ModelAndView signup(@ModelAttribute(name = "userregisterpojo") UserRegisterPojo pojo)
{
try
{
ResponseEntity responseEntity = userClient.register(pojo);
// servis response 'u userservice tarafından belirlenmişti
// bu servisler için SWAGGER benzeri bir yapı ile dökümantasyon yazılması tavsiye edilir
return new ModelAndView("redirect:/login");
}
catch (FeignException e)
{
// Feign kullandığımız için bütün hatalar feign exception olarak wrap ediliyor
// Servis ayakta değil ise farklı bir işleyiş gösterilebilir
if (e.status() == HttpStatus.SERVICE_UNAVAILABLE.value())
{
// 503
return new ModelAndView("redirect:/signup?err=2");
}
else
{
// General errors
return new ModelAndView("redirect:/signup?err=1");
}
}
}
@GetMapping(path = "payment")
public ModelAndView payment(HttpServletRequest req)
{
ModelAndView payment = new ModelAndView("payment");
BasketPojo basket = (BasketPojo) req.getSession().getAttribute(SessionConstants.BASKET);
String bearer = (String) req.getSession().getAttribute(SessionConstants.BEARER);
String cardInfo = req.getSession().getAttribute(SessionConstants.CARD_NUMBER).toString();
Boolean sold = paymentClient.makePayment(Constants.TOKEN_PREFIX + bearer, cardInfo).getBody();
// makePayment metodu rastgele true false döndürüyor, kredi kartı reddedildi veya limit yetersizi simule etmek için
if (sold)
{
Boolean resp = activityClient.checkActivitySeatAvailable(Constants.TOKEN_PREFIX + bearer, basket.getActId()).getBody();
if (!resp)
{
payment.addObject("status", "Event is full, sorry");
// Ödemeyi geri iade etmek de bir fault tolerance işlemidir business açısından
paymentClient.returnPayment(Constants.TOKEN_PREFIX + bearer, cardInfo);
}
else
{
activityClient.sellSeat(Constants.TOKEN_PREFIX + bearer, basket.getActId());
payment.addObject("status", "Payment done, you bought the ticket");
}
}
else
{
payment.addObject("status", "Payment error, sorry");
}
}
}
Login, signup ve payment metodlarımızda basic hata yakalama ve tutarlılık kontrolleri yaptık. Bütün işlemlerde servisler ayakta değil ise failover 'lar tarafından fırlatılan 503 yani unavailable hatalarını yakaladık. Bu bir resilliance kontrolü. Login işleminde kullanıcı adı şifre bilgisinin tutmaması durumunda başka bir yol izleyip UNAUTHORIZED status 'ünü kontrol ettik. Payment sırasında ise yer ayırt etme fonksiyonu olmadığı için son adımda ödeme aldıktan sonra yer kalmaması durumunda para iadesi yaptık. Bu da bir business SAGA idi ve MVC tarafından işletilmiş oldu. Bu sayede consistency de sağlamış olduk. Biraz daha ileri gidip geçici bir tabloya rezerve koltuk bilgisi yazsaydık return payment 'ın hemen altında rezerve bilgisini silecek bir metod yazıp çağırabilirdik. Biz en basit hali ile tutarlılık ve güvenilirlik sağladık. Servislerin erişilemez olması durumu, hata vermesi durumu ve iş kurallarının ihlal edilmesi durumu gibi 3 çeşit fault tolerance gerçekleştirdik. Bu bence 3 farklı SAGA çeşidi idi. Bu konunun daha derinine serinin son yazısında ineceğim :)
Burada kendi hatalarımdan da bahsetmem lazım. Feign kütüphanesini ilk kullandığımda header bilgisini dinamik verememiştim. Yanlış annotation kullanmıştım. Bu yüzden ayrı bir feign config sınıf tanımlamıştım ve interceptor, encoder ve decoder ve target gibi bilgileri dışardan vermiştim. Bu durumda da giden gelen veri türleri (ResponseEntity
Thymeleaf ile ilgili kısa bir bilgi vereyim. Spring framework tarafından sağlanan modelandview nesnesi oluşturulurken thymeleaf engine 'i devreye girer. Örneğin "return new ModelAndView("redirect:/login");" şeklinde bir değer döndürüldüğünde thymeleaf template klasöründe login.html isimli sayfa arar. Default uzantımız html. Bu sayfa açılırken spring framework tarafından üretilen verilere ulaşabilir. Değişkenler, session veya requestparam gibi verilerle thymleaf içerisinde belli koşullar (if ifadeleri) yazabilirsiniz. Uygulamayı ayağa kaldırıp düzenleyeme çalışırsanız bu bilgiler işinize yarayacaktır.
MVC uygulaması için temel fonksiyonları gerçekleştiren kodları da görmüş olduk. Mikroservis yolculuğumuzun esas amacı MVC uygulaması olmasa da bu uygulama ile tasarladığımız mikroservis üzerinde iş kurallarına dair SAGA yapıları işletmek veya belli hata kontrolleri yapmak konusunda elimizde bir örneğimiz olmuş oldu. Tabi ki bu konularda uygulanabilecek farklı çözümler de mevcut ve mikroservisleri öğrenmeye çalışırken kafanızı karıştıracak en önemli sebeplerden birisi de bu. Serinin son yazısında bu noktadan sonra ne yapılabilir, ne elde ettik, nelerden feragat ettik gibi sorulara ve bazı alternatif teknolojilere göz atacağız ve biraz felsefe yapacağız.
Bu kadar kodu yazdıktan tabi olduğu gibi çeyizlik saklamayacağız. İsterseniz Eureka -> Config Server -> Gateway -> Servisler -> MVC sırası ile projeleri ayağa kaldırabilirsiniz. Bu işlemin detayları ve kolayca ayağa kaldırmanın yolu bir sonraki yazıda olacak. Eğer mikroservisler konusunda yeni iseniz, bu noktaya kadar hazırladığımız sistemi çalıştırmanız, üzerine kendi fikirlerinizi geliştirmeniz ve hatalarla karşılaşıp çözümler üretmeniz size çok şey katacaktır. Bir sonraki yazıda görüşmek üzere :)
Bir yorum yazabilirsiniz