Buat pustaka gaya Spring Data Repository Anda sendiri dengan Dynamic Proxy dan Spring IoC

Tetapi bagaimana jika Anda bisa membuat antarmuka, misalnya, seperti ini:


@Service public interface GoogleSearchApi { /** * @return http status code for Google main page */ @Uri("https://www.google.com") int mainPageStatus(); } 

Dan kemudian menyuntikkannya dan memanggil metodenya:


 @SpringBootApplication public class App implements CommandLineRunner { private static final Logger LOG = LoggerFactory.getLogger(App.class); private final GoogleSearchApi api; public App(GoogleSearchApi api) { this.api = api; } @Override public void run(String... args) { LOG.info("Main page status: " + api.mainPageStatus()); } public static void main(String[] args) { SpringApplication.run(App.class, args); } } 

Ini sangat mungkin untuk diterapkan (dan tidak terlalu sulit). Selanjutnya saya akan menunjukkan bagaimana dan mengapa melakukannya.


Baru-baru ini, saya memiliki tugas untuk menyederhanakan interaksi pengembang dengan salah satu kerangka kerja yang digunakan. Itu perlu untuk memberi mereka cara yang lebih sederhana dan nyaman untuk bekerja dengannya daripada yang sudah diterapkan.


Properti yang ingin saya capai dari solusi seperti itu:


  • deskripsi deklaratif dari tindakan yang diinginkan
  • jumlah minimum kode yang dibutuhkan
  • integrasi dengan kerangka kerja injeksi ketergantungan yang digunakan (dalam kasus kami, Spring)

Ini diimplementasikan dalam pustaka Repositori dan Retrofit Data Musim Semi . Di dalamnya, pengguna menggambarkan interaksi yang diinginkan dalam bentuk antarmuka java, dilengkapi dengan penjelasan. Pengguna tidak perlu menulis sendiri implementasinya - pustaka menghasilkannya dalam runtime berdasarkan tanda tangan metode, anotasi dan tipe.


Ketika saya mempelajari topik itu, saya punya banyak pertanyaan, jawaban yang tersebar di Internet. Pada saat itu, artikel seperti ini tidak akan menyakiti saya. Karena itu, di sini saya mencoba mengumpulkan semua informasi dan pengalaman saya di satu tempat.


Dalam posting ini saya akan menunjukkan bagaimana Anda dapat mengimplementasikan ide ini, menggunakan contoh pembungkus untuk klien http. Contoh mainan, dirancang bukan untuk penggunaan nyata, tetapi untuk menunjukkan pendekatan. Kode sumber proyek dapat dipelajari di bitbucket .


Bagaimana cara mencari pengguna


Pengguna menggambarkan layanan yang ia butuhkan dalam bentuk antarmuka. Misalnya, untuk melakukan permintaan http di google:


 /** * Some Google requests */ @Service public interface GoogleSearchApi { /** * @return http status code for Google main page */ @Uri("https://www.google.com") int mainPageStatus(); /** * @return request object for Google main page */ @Uri("https://www.google.com") HttpGet mainPageRequest(); /** * @param query search query * @return result of search request execution */ @Uri("https://www.google.com/search?q={query}") CloseableHttpResponse searchSomething(String query); /** * @param query doodle search query * @param language doodle search language * @return http status code for doodle search result */ @Uri("https://www.google.com/doodles/?q={query}&hl={language}") int searchDoodleStatus(String query, String language); } 

Apa implementasi dari antarmuka ini pada akhirnya akan dilakukan ditentukan oleh tanda tangan. Jika jenis kembali int, permintaan http akan dieksekusi dan kode status akan dikembalikan. Jika jenis kembali adalah CloseableHttpResponse, maka seluruh respons akan dikembalikan, dan seterusnya. Di mana permintaan akan dibuat, kami akan mengambil Uri dari anotasi, menggantikan nilai yang ditransfer yang sama alih-alih placeholder dalam isinya.


Dalam contoh ini, saya membatasi diri saya untuk mendukung tiga tipe pengembalian dan satu anotasi. Anda juga dapat menggunakan nama metode, tipe parameter untuk memilih implementasi, menggunakan semua jenis kombinasi mereka, tetapi saya tidak akan membuka topik ini di posting ini.


Saat pengguna ingin menggunakan antarmuka ini, ia menyematkannya dalam kode menggunakan Spring:


 @SpringBootApplication public class App implements CommandLineRunner { private static final Logger LOG = LoggerFactory.getLogger(App.class); private final GoogleSearchApi api; public App(GoogleSearchApi api) { this.api = api; } @Override @SneakyThrows public void run(String... args) { LOG.info("Main page status: " + api.mainPageStatus()); LOG.info("Main page request: " + api.mainPageRequest()); LOG.info("Doodle search status: " + api.searchDoodleStatus("tesla", "en")); try (CloseableHttpResponse response = api.searchSomething("qweqwe")) { LOG.info("Search result " + response); } } public static void main(String[] args) { SpringApplication.run(App.class, args); } } 

Integrasi dengan Spring diperlukan dalam proyek kerja saya, tetapi itu, tentu saja, bukan satu-satunya yang mungkin. Jika Anda tidak menggunakan injeksi ketergantungan, Anda bisa mendapatkan implementasinya, misalnya, melalui metode pabrik statis. Tetapi dalam artikel ini saya akan mempertimbangkan Spring.


Pendekatan ini sangat mudah: cukup tandai antarmuka Anda sebagai komponen Spring (Layanan annotation dalam kasus ini), dan siap untuk implementasi dan penggunaan.


Cara mendapatkan Spring untuk mendukung sihir ini


Aplikasi khas Spring memindai classpath saat startup dan mencari semua komponen yang ditandai dengan anotasi khusus. Bagi mereka, ini mendaftarkan BeanDefinitions, resep yang dengannya komponen ini akan dibuat. Tetapi jika dalam kasus kelas konkret, Spring tahu bagaimana membuat mereka, apa yang memanggil konstruktor, dan apa yang harus lulus di dalamnya, maka untuk kelas dan antarmuka abstrak itu tidak memiliki informasi seperti itu. Karenanya, untuk GoogleSearchApi Spring kami tidak akan membuat BeanDefinition. Dalam hal ini dia akan membutuhkan bantuan dari kami.


Untuk menyelesaikan logika pemrosesan BeanDefinitions, ada antarmuka BeanDefinitionRegistryPostProcessor di musim semi. Dengan itu, kita dapat menambahkan BeanDefinitionRegistry definisi kacang yang kita inginkan.


Sayangnya, saya tidak menemukan cara untuk mengintegrasikan ke dalam logika Spring dari pemindaian classpath untuk memproses kacang biasa dan antarmuka kita dalam sekali jalan. Oleh karena itu, saya membuat dan menggunakan turunan kelas ClassPathScanningCandidateComponentProvider untuk menemukan semua antarmuka yang ditandai dengan anotasi Layanan:


Kode pindaian paket lengkap dan pendaftaran BeanDefinitions:


DynamicProxyBeanDefinitionRegistryPostProcessor
 @Component public class DynamicProxyBeanDefinitionRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor { // ,     private static final String[] SCAN_PACKAGES = {"com"}; private final InterfaceScanner classpathScanner; public DynamicProxyBeanDefinitionRegistryPostProcessor() { classpathScanner = new InterfaceScanner(); //   .      Service classpathScanner.addIncludeFilter(new AnnotationTypeFilter(Service.class)); } @Override public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { for (String basePackage : SCAN_PACKAGES) { createRepositoryProxies(basePackage, registry); } } @SneakyThrows private void createRepositoryProxies(String basePackage, BeanDefinitionRegistry registry) { for (BeanDefinition beanDefinition : classpathScanner.findCandidateComponents(basePackage)) { Class<?> clazz = Class.forName(beanDefinition.getBeanClassName()); //      bean definition BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(clazz); builder.addConstructorArgValue(clazz); //,          builder.setFactoryMethodOnBean( "createDynamicProxyBean", DynamicProxyBeanFactory.DYNAMIC_PROXY_BEAN_FACTORY ); registry.registerBeanDefinition(ClassUtils.getShortNameAsProperty(clazz), builder.getBeanDefinition()); } } @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { } private static class InterfaceScanner extends ClassPathScanningCandidateComponentProvider { InterfaceScanner() { super(false); } @Override protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) { return beanDefinition.getMetadata().isInterface(); } } } 

Selesai! Pada awal aplikasi, Spring akan menjalankan kode ini dan mendaftarkan semua antarmuka yang diperlukan, seperti kacang.


Membuat implementasi kacang yang ditemukan didelegasikan ke komponen terpisah dari DynamicProxyBeanFactory:


 @Component(DYNAMIC_PROXY_BEAN_FACTORY) public class DynamicProxyBeanFactory { public static final String DYNAMIC_PROXY_BEAN_FACTORY = "repositoryProxyBeanFactory"; private final DynamicProxyInvocationHandlerDispatcher proxy; public DynamicProxyBeanFactory(DynamicProxyInvocationHandlerDispatcher proxy) { this.proxy = proxy; } @SuppressWarnings("unused") public <T> T createDynamicProxyBean(Class<T> beanClass) { //noinspection unchecked return (T) Proxy.newProxyInstance(beanClass.getClassLoader(), new Class[]{beanClass}, proxy); } } 

Untuk membuat implementasi, mekanisme Dynamic Proxy yang lama digunakan. Implementasi dibuat dengan cepat menggunakan metode Proxy.newProxyInstance. Banyak artikel telah ditulis tentang dia, jadi saya tidak akan tinggal di sini secara detail.


Menemukan pawang yang tepat dan memproses panggilan


Seperti yang Anda lihat, DynamicProxyBeanFactory mengalihkan metode pemrosesan ke DynamicProxyInvocationHandlerDispatcher. Karena kami memiliki banyak potensi implementasi penangan (untuk setiap anotasi, untuk setiap jenis yang dikembalikan, dll.), Adalah logis untuk menetapkan beberapa tempat sentral untuk penyimpanan dan pencarian mereka.


Untuk menentukan apakah handler cocok untuk memproses metode yang disebut, saya memperluas antarmuka InvocationHandler standar dengan metode baru


 public interface HandlerMatcher { /** * @return {@code true} if handler is able to handle given method, {@code false} othervise */ boolean canHandle(Method method); } public interface ProxyInvocationHandler extends InvocationHandler, HandlerMatcher { } 

Hasilnya adalah antarmuka ProxyInvocationHandler, implementasi yang akan menjadi penangan kami. Juga, implementasi handler akan ditandai sebagai Komponen sehingga Spring dapat mengumpulkannya untuk kita ke dalam satu daftar besar di dalam DynamicProxyInvocationHandlerDispatcher:


DynamicProxyInvocationHandlerDispatcher
 package com.bachkovsky.dynproxy.lib.proxy; import lombok.SneakyThrows; import org.springframework.stereotype.Component; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.util.List; /** * Top level dynamic proxy invocation handler, which finds correct implementation based and uses it for method * invocation */ @Component public class DynamicProxyInvocationHandlerDispatcher implements InvocationHandler { private final List<ProxyInvocationHandler> proxyHandlers; /** * @param proxyHandlers all dynamic proxy handlers found in app context */ public DynamicProxyInvocationHandlerDispatcher(List<ProxyInvocationHandler> proxyHandlers) { this.proxyHandlers = proxyHandlers; } @Override public Object invoke(Object proxy, Method method, Object[] args) { switch (method.getName()) { // three Object class methods don't have default implementation after creation with Proxy::newProxyInstance case "hashCode": return System.identityHashCode(proxy); case "toString": return proxy.getClass() + "@" + System.identityHashCode(proxy); case "equals": return proxy == args[0]; default: return doInvoke(proxy, method, args); } } @SneakyThrows private Object doInvoke(Object proxy, Method method, Object[] args) { return findHandler(method).invoke(proxy, method, args); } private ProxyInvocationHandler findHandler(Method method) { return proxyHandlers.stream() .filter(h -> h.canHandle(method)) .findAny() .orElseThrow(() -> new IllegalStateException("No handler was found for method: " + method)); } } 

Dalam metode findHandler, kita melewati semua penangan dan mengembalikan yang pertama yang dapat menangani metode yang diteruskan. Mekanisme pencarian ini mungkin tidak terlalu efektif ketika ada banyak implementasi handler. Mungkin Anda perlu memikirkan struktur yang lebih cocok untuk menyimpannya daripada daftar.


Implementasi Handler


Tugas penangan meliputi membaca informasi tentang metode antarmuka yang dipanggil dan memproses panggilan itu sendiri.


Apa yang harus dilakukan pawang dalam kasus ini:


  1. Baca Uri anotasi, dapatkan isinya
  2. Ganti placeholder Uri dalam string dengan nilai nyata
  3. Baca jenis metode pengembalian
  4. Jika jenis pengembalian sesuai, proses metode dan mengembalikan hasilnya.

Tiga poin pertama diperlukan untuk semua tipe yang dikembalikan, jadi saya memasukkan kode umum ke dalam superclass abstrak
HttpInvocationHandler:


 public abstract class HttpInvocationHandler implements ProxyInvocationHandler { final HttpClient client; private final UriHandler uriHandler; HttpInvocationHandler(HttpClient client, UriHandler uriHandler) { this.client = client; this.uriHandler = uriHandler; } @Override public boolean canHandle(Method method) { return uriHandler.canHandle(method); } final String getUri(Method method, Object[] args) { return uriHandler.getUriString(method, args); } } 

Kelas pembantu UriHandler mengimplementasikan kerja dengan anotasi Uri: nilai membaca, menggantikan placeholder. Saya tidak akan memberikan kode di sini, karena itu cukup utilitarian.
Tetapi perlu dicatat bahwa untuk membaca nama parameter dari tanda tangan metode java, Anda perlu menambahkan opsi "-parameters" saat kompilasi .
HttpClient - pembungkus atas Apachevsky CloseableHttpClient, adalah backend untuk perpustakaan ini.


Sebagai contoh penangan tertentu, saya akan memberikan penangan yang mengembalikan kode respons status:


 @Component public class HttpCodeInvocationHandler extends HttpInvocationHandler { public HttpCodeInvocationHandler(HttpClient client, UriHandler uriHandler) { super(client, uriHandler); } @Override @SneakyThrows public Integer invoke(Object proxy, Method method, Object[] args) { try (CloseableHttpResponse resp = client.execute(new HttpGet(getUri(method, args)))) { return resp.getStatusLine().getStatusCode(); } } @Override public boolean canHandle(Method method) { return super.canHandle(method) && method.getReturnType().equals(int.class); } } 

Penangan lain dibuat serupa. Menambahkan penangan baru itu sederhana dan tidak memerlukan modifikasi kode yang ada - cukup buat penangan baru dan tandai sebagai komponen Pegas.


Itu saja. Kode ditulis dan siap untuk digunakan.


Kesimpulan


Semakin saya memikirkan desain seperti itu, semakin saya melihat kekurangan di dalamnya. Kelemahan yang saya lihat:


  • Ketik Keselamatan, yang tidak. Atur anotasi dengan salah - sebelum bertemu dengan RuntimeException. Digunakan kombinasi yang salah dari jenis kembali dan anotasi - hal yang sama.
  • Dukungan lemah dari IDE. Kurangnya penyelesaian otomatis. Pengguna tidak dapat melihat tindakan apa yang tersedia baginya dalam situasinya (seolah-olah ia meletakkan "titik" setelah objek dan melihat daftar metode yang tersedia)
  • Ada beberapa kemungkinan untuk aplikasi. Klien http yang telah disebutkan muncul di benak, dan klien pergi ke database. Tapi mengapa lagi ini bisa diterapkan?

Namun, dalam konsep kerja saya pendekatan ini telah mengakar dan populer. Kelebihan yang telah saya sebutkan - kesederhanaan, sejumlah kecil kode, deklaratif, memungkinkan pengembang berkonsentrasi pada penulisan kode yang lebih penting.


Apa pendapat Anda tentang pendekatan ini? Apakah ini sepadan dengan usaha? Masalah apa yang Anda lihat dalam pendekatan ini? Sementara saya masih mencoba untuk memahaminya, sementara itu sedang diputar di produksi kami, saya ingin mendengar apa yang dipikirkan orang lain tentang itu. Saya berharap materi ini bermanfaat bagi seseorang.

Source: https://habr.com/ru/post/id458548/


All Articles