Tetapi bagaimana jika Anda bisa membuat antarmuka, misalnya, seperti ini:
@Service public interface GoogleSearchApi { @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:
@Service public interface GoogleSearchApi { @Uri("https://www.google.com") int mainPageStatus(); @Uri("https://www.google.com") HttpGet mainPageRequest(); @Uri("https://www.google.com/search?q={query}") CloseableHttpResponse searchSomething(String query); @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 {
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) {
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 { 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; @Component public class DynamicProxyInvocationHandlerDispatcher implements InvocationHandler { private final List<ProxyInvocationHandler> proxyHandlers; public DynamicProxyInvocationHandlerDispatcher(List<ProxyInvocationHandler> proxyHandlers) { this.proxyHandlers = proxyHandlers; } @Override public Object invoke(Object proxy, Method method, Object[] args) { switch (method.getName()) {
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:
- Baca Uri anotasi, dapatkan isinya
- Ganti placeholder Uri dalam string dengan nilai nyata
- Baca jenis metode pengembalian
- 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.