Bildiğimiz üzere Feign projesinin amacı java http clientlarını kolayca oluşturmayı sağlamaktır. Ve bu amaçla Interface’ler üzerinden tanımlanan clientları kolayca kullanmayı sağlıyor.
https://github.com/OpenFeign/feign
interface GitHub { @RequestLine("GET /repos/{owner}/{repo}/contributors") List contributors(@Param("owner") String owner, @Param("repo") String repo); @RequestLine("POST /repos/{owner}/{repo}/issues") void createIssue(Issue issue, @Param("owner") String owner, @Param("repo") String repo); } public class MyApp { public static void main(String... args) { GitHub github = Feign.builder() .decoder(new GsonDecoder()) .target(GitHub.class, "https://api.github.com"); // Fetch and print a list of the contributors to this library. List contributors = github.contributors("OpenFeign", "feign"); for (Contributor contributor : contributors) { System.out.println(contributor.login + " (" + contributor.contributions + ")"); } } }
Peki Feign bu kolay kullanım fikrini nasıl implemente ediyor. Yapılan işlemlere baktığımızda; temelde Http request’i yapmak ve dönen response’u vermektir. Bu aşamada marshalling/request/response/unmarshalling işlemleri yapılarak http üzerinden gönderilen ve alınan veri java objelerine dönüştürülüyor.
Baktığımızda yapılan işlemler belirli bir pattern’i uyguluyor.
Benim buradaki amacım Feign kütüphanesi temelde javadaki hangi özelliği kullanarak bu standart işlemleri yaptığını ve belirli bir patterne uyan işlemlerde bu yöntemin kullanılabilirliğini göstermek istiyorum. Ve sonunda bu yöntemi spring ile nasıl kullanabileceğimizi göstereceğim.
Aslında mantık olarak karmaşık değil. Mantık Java reflection kütüphanesindeki Proxy class’ı kullanılarak tanımlanmış Interface’lere runtime’da InvocationHandler’lar ile bind edilerek dinamik davranış vermeye dayanmaktadır.
Öncelikle bir Interface’e ihtiyacımız var;
Parametrik yapıyı gösterebilmek için basit bir annotation tanımladım;
import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * @author dilaverd * @since 03.03.2021 */ @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface Gender { String name(); }
Interface’ler;
/** * @author dilaverdemirel * @since 28/07/2022 */ public interface MaleGenderInterface { @Gender(name = "Bay") String hello(String name); } /** * @author dilaverdemirel * @since 28/07/2022 */ public interface FemaleGenderInterface { @Gender(name = "Bayan") String hello(String name); }
İkinci olarak bir InvocationHandler’a ihtiyacımız var;
import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; /** * @author dilaverdemirel * @since 28/07/2022 */ public class CustomInvocationHandler implements InvocationHandler { @Override public Object invoke(Object proxy, Method method, Object[] args) { if (args != null) { for (Object arg : args) System.out.println("args = " + arg); } final var methodAnnotation = method.getAnnotation(Gender.class); System.out.println("Invoked method:" + method.getName()); return "Merhaba " + methodAnnotation.name() + " " + args[0] + "!"; } }
CustomInvocationHandler classına Method ve Method argument’leri parametre olarak iletilmektedir. Method referansı üzerinden annotation bilgilerine erişilerek invocation sırasından istenilen parametrik yapı oluşturulabiliyor.
Son olarak Proxy oluşturup InvocationHandler ile bind ederek DummyInterface içerisindeki hello metoduna davranış vermeye kalıyor.
import java.lang.reflect.Proxy; /** * @author dilaverdemirel * @since 28/07/2022 */ public class Tester { public static void main(String[] args) { MaleGenderInterface maleGenderInterface = (MaleGenderInterface) Proxy.newProxyInstance( MaleGenderInterface.class.getClassLoader(), new Class[]{MaleGenderInterface.class}, new CustomInvocationHandler()); System.out.println("result = " + maleGenderInterface.hello("Dilaver")); FemaleGenderInterface femaleGenderInterface = (FemaleGenderInterface) Proxy.newProxyInstance( FemaleGenderInterface.class.getClassLoader(), new Class[]{FemaleGenderInterface.class}, new CustomInvocationHandler()); System.out.println("result = " + femaleGenderInterface.hello("Derya")); } }
Yukarıda yaptığımız sadece Proxy.newProxyInstance metodunu kullanarak Interface’lere hangi InvocationHandler ile çalışacaklarını belirtiyoruz ve dönen instance’ın hello metodunu çağırıyoruz.
Çıktı aşağıdaki gibi oluşuyor;
args = Dilaver Invoked method:hello result = Merhaba Bay Dilaver! args = Derya Invoked method:hello result = Merhaba Bayan Derya!
Spring ile entegrasyon;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.lang.reflect.Proxy; /** * @author dilaverdemirel * @since 28/07/2022 */ @Configuration public class Config { @Autowired private ApplicationContext applicationContext; @Bean public MaleGenderInterface maleGenderInterface() { return (MaleGenderInterface) Proxy.newProxyInstance( MaleGenderInterface.class.getClassLoader(), new Class[]{MaleGenderInterface.class}, new CustomInvocationHandler()); } @Bean public FemaleGenderInterface femaleGenderInterface() { return (FemaleGenderInterface) Proxy.newProxyInstance( FemaleGenderInterface.class.getClassLoader(), new Class[]{FemaleGenderInterface.class}, new CustomInvocationHandler()); } }
Benzer mantıkla spring beanları tanımlayarak Proxy Interface’leri erişilebilir hale getirebiliriz.
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import tr.com.dilaverdemirel.handler.DemoBean; /** * @author dilaverdemirel * @since 28/07/2022 */ @SpringBootApplication public class DemoApplication implements CommandLineRunner { @Autowired private DemoBean demoBean; public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } @Override public void run(String... args) { demoBean.test(); } }
Çıktı;
args = Dilaver Invoked method:hello Merhaba Bay Dilaver! args = Derya Invoked method:hello Merhaba Bayan Derya!
Sonuç olarak;
Belirli bir patterni uygulayan problemlere esnek bir çözüm oluşturmanın bir mantığını paylaşmaya çalıştım. Proxy kullanılarak dynamic interface’ler ile farklı çözümler uygulanabilir.
Aşağıdaki repodan detaylı olarak inceleyebilirsiniz.