
Skrip adalah salah satu cara paling umum untuk membuat aplikasi lebih fleksibel, dengan kemampuan untuk memperbaiki sesuatu saat dalam perjalanan. Tentu saja, pendekatan ini juga memiliki kelemahan, Anda harus selalu ingat keseimbangan antara fleksibilitas dan pengelolaan. Tetapi dalam artikel ini kami tidak akan membahas "secara umum" tentang pro dan kontra dari menggunakan skrip, kami akan mempertimbangkan cara praktis untuk menerapkan pendekatan ini, dan juga memperkenalkan perpustakaan yang menyediakan infrastruktur yang nyaman untuk menambahkan skrip ke aplikasi yang ditulis dalam Kerangka Kerja Musim Semi.
Beberapa kata pengantar
Ketika Anda ingin menambahkan kemampuan untuk mengubah logika bisnis dalam suatu aplikasi tanpa kompilasi dan penyebaran berikutnya, maka skrip adalah salah satu cara yang muncul di pikiran di tempat pertama. Seringkali, skrip muncul bukan karena dimaksudkan, tetapi karena itu terjadi. Misalnya, dalam spesifikasi ada bagian dari logika yang tidak sepenuhnya jelas sekarang, tetapi agar tidak menghabiskan beberapa hari ekstra (dan kadang-kadang lebih lama) untuk analisis, Anda dapat membuat titik ekstensi dan memanggil skrip - sebuah rintisan. Dan kemudian, tentu saja, skrip ini akan ditulis ulang ketika persyaratan menjadi jelas.
Metode ini bukan hal baru, dan kelebihan dan kekurangannya sudah diketahui: fleksibilitas - Anda dapat mengubah logika pada aplikasi yang sedang berjalan dan menghemat waktu untuk menginstal ulang, tetapi, di sisi lain, skrip lebih sulit untuk diuji, sehingga kemungkinan masalah dengan keamanan, kinerja, dll.
Trik-trik yang akan dibahas nanti dapat bermanfaat baik bagi pengembang yang sudah menggunakan skrip dalam aplikasi mereka, dan bagi mereka yang hanya memikirkannya.
Bukan masalah pribadi, hanya scripting
Dengan JSR-233, scripting di Java menjadi sangat sederhana. Ada cukup mesin skrip berdasarkan API ini (Nashorn, JRuby, Jython, dan lainnya), jadi menambahkan sedikit keajaiban skrip ke kode Anda bukan masalah:
Map<String, Object> parameters = createParametersMap(); ScriptEngineManager manager = new ScriptEngineManager(); ScriptEngine scriptEngine = manager.getEngineByName("groovy"); Object result = scriptEngine.eval(script.getScriptAsString("discount.groovy"), new SimpleBindings(parameters));
Jelas, jika kode seperti itu tersebar di seluruh aplikasi, maka itu akan berubah menjadi sesuatu yang tidak dapat dipahami. Dan, tentu saja, jika Anda memiliki lebih dari satu panggilan skrip di aplikasi Anda, Anda perlu membuat kelas terpisah untuk bekerja dengannya. Kadang-kadang Anda dapat melangkah lebih jauh dan membuat kelas khusus yang akan membungkus panggilan
evaluateGroovy()
dalam metode Java yang diketik secara reguler. Metode-metode ini akan memiliki kode utilitas yang cukup seragam, seperti dalam contoh:
public BigDecimal applyCustomerDiscount(Customer customer, BigDecimal orderAmount) { Map<String, Object> params = new HashMap<>(); params.put("cust", customer); params.put("amount", orderAmount); return (BigDecimal)scripting.evalGroovy(getScriptSrc("discount.groovy"), params); }
Pendekatan ini sangat meningkatkan transparansi saat memanggil skrip dari kode aplikasi - Anda dapat segera melihat parameter apa yang diterima skrip, apa jenisnya dan apa yang dikembalikan. Hal utama adalah jangan lupa untuk menambah standar penulisan kode larangan memanggil skrip bukan dari metode yang diketik!
Kami memompa skrip
Terlepas dari kenyataan bahwa skrip itu sederhana, jika Anda memiliki banyak skrip dan menggunakannya secara intensif, ada peluang nyata untuk mengalami masalah kinerja. Misalnya, jika Anda menggunakan sekelompok templat groovy untuk menghasilkan laporan dan Anda menjalankannya pada saat yang sama, cepat atau lambat ini akan menjadi salah satu hambatan dalam kinerja aplikasi.
Oleh karena itu, banyak kerangka kerja membuat berbagai tambahan pada API standar untuk meningkatkan kecepatan kerja, caching, pemantauan eksekusi, menggunakan bahasa skrip yang berbeda dalam satu aplikasi, dll.
Misalnya, mesin
skrip yang agak cerdik dibuat di CUBA yang mendukung fitur tambahan, seperti:
- Kemampuan untuk menulis skrip di Jawa dan Groovy
- Cache kelas agar tidak mengkompilasi ulang skrip
- Tempat JMX untuk mengontrol mesin
Semua ini, tentu saja, meningkatkan kinerja dan kegunaan, tetapi tetap saja mesin tingkat rendah tetap tingkat rendah, dan Anda masih perlu membaca teks skrip, meneruskan parameter dan memanggil API untuk menjalankan skrip. Jadi Anda masih perlu melakukan semacam pembungkus di setiap proyek untuk membuat pengembangan lebih efisien.
Dan tidak adil untuk tidak menyebutkan GraalVM - mesin eksperimental yang dapat menjalankan program dalam berbagai bahasa (JVM dan non-JVM) dan memungkinkan Anda untuk memasukkan
modul dalam bahasa ini ke dalam
aplikasi Java . Saya berharap bahwa Nashorn akan turun dalam sejarah cepat atau lambat, dan kita akan memiliki kesempatan untuk menulis bagian kode dalam berbagai bahasa dalam satu sumber. Tapi ini hanya mimpi.
Kerangka Kerja Musim Semi: tawaran yang sulit ditolak?
Spring memiliki dukungan pelaksanaan skrip bawaan yang dibangun di atas API JDK. Dalam
org.springframework.scripting.*
Paket, Anda dapat menemukan banyak kelas yang berguna - semua sehingga Anda dapat dengan mudah menggunakan API tingkat rendah untuk skrip dalam aplikasi Anda.
Selain itu, ada tingkat dukungan yang lebih tinggi, hal ini dijelaskan secara rinci dalam
dokumentasi . Singkatnya - Anda perlu membuat kelas dalam bahasa scripting (misalnya, Groovy) dan menerbitkannya sebagai kacang melalui deskripsi XML:
<lang:groovy id="messenger" script-source="classpath:Messenger.groovy"> <lang:property name="message" value="I Can Do The Frug" /> </lang:groovy>
Setelah kacang diterbitkan, kacang dapat ditambahkan ke kelasnya menggunakan IoC. Spring menyediakan pembaruan skrip otomatis saat mengubah teks dalam file, Anda dapat menggantung aspek pada metode, dll.
Ini terlihat bagus, tetapi Anda perlu membuat kelas "nyata" untuk mempublikasikannya, Anda tidak dapat menulis fungsi reguler dalam skrip. Selain itu, skrip hanya dapat disimpan dalam sistem file, untuk menggunakan database Anda harus naik di dalam Spring. Ya, dan banyak yang menganggap konfigurasi XML sudah ketinggalan zaman, terutama jika aplikasi sudah memiliki segalanya pada anotasi. Ini, tentu saja, rasanya, tetapi orang sering harus memperhitungkannya.
Script: Kesulitan dan Gagasan
Jadi, setiap solusi memiliki harganya sendiri, dan jika kita berbicara tentang skrip dalam aplikasi Java, maka ketika memperkenalkan teknologi ini, kita mungkin menghadapi beberapa kesulitan:
- Kelola. Seringkali, panggilan skrip tersebar di seluruh aplikasi, dan dengan perubahan kode, cukup sulit untuk melacak panggilan skrip yang diperlukan.
- Kemampuan untuk menemukan rekan panggilan. Jika terjadi kesalahan pada skrip tertentu, maka menemukan semua
evaluateGroovy()
panggilannya akan menjadi masalah, kecuali jika Anda menerapkan pencarian berdasarkan nama file atau panggilan metode seperti evaluateGroovy()
- Transparansi Menulis skrip bukanlah tugas yang mudah, dan bahkan lebih sulit bagi mereka yang menyebut skrip ini. Anda perlu mengingat apa yang disebut parameter input, tipe data apa yang mereka miliki dan apa hasil dari eksekusi. Atau lihat kode sumber skrip setiap kali.
- Menguji dan memperbarui - tidak selalu mungkin untuk menguji skrip di lingkungan kode aplikasi, dan bahkan setelah mengunggahnya ke server "pertempuran", Anda harus dapat memutar kembali semuanya jika terjadi kesalahan.
Tampaknya membungkus panggilan skrip dalam metode Java akan membantu menyelesaikan sebagian besar masalah di atas. Sangat bagus jika kelas seperti itu dapat dipublikasikan dalam wadah IoC dan metode panggilan dengan nama normal dan bermakna dalam layanan mereka, alih-alih memanggil
eval(“disc_10_cl.groovy”)
dari beberapa kelas utilitas. Kelebihan lainnya adalah bahwa kode menjadi mendokumentasikan diri sendiri, pengembang tidak perlu memikirkan jenis algoritma apa yang tersembunyi di balik nama file.
Selain itu, jika setiap skrip akan dikaitkan dengan hanya satu metode, Anda dapat dengan cepat menemukan semua rekan panggilan dalam aplikasi menggunakan menu "Temukan Penggunaan" dari IDE dan memahami tempat skrip dalam setiap algoritma logika bisnis tertentu.
Pengujian disederhanakan - ini berubah menjadi pengujian kelas "normal", menggunakan kerangka kerja yang akrab, mengejek, dan banyak lagi.
Semua hal di atas sangat sesuai dengan ide yang disebutkan di awal artikel - kelas "khusus" untuk metode yang diterapkan oleh skrip. Tetapi bagaimana jika Anda mengambil satu langkah lagi dan menyembunyikan semua kode layanan dari jenis yang sama untuk memanggil mesin skrip dari pengembang sehingga ia bahkan tidak memikirkannya (well, hampir)?
Gudang naskah - konsep
Idenya cukup sederhana dan harus akrab bagi mereka yang setidaknya pernah bekerja dengan Spring, terutama dengan Spring JPA. Yang Anda butuhkan adalah membuat antarmuka Java dan memanggil skrip saat memanggil metode-metodenya. Di JPA, omong-omong, pendekatan yang identik digunakan - panggilan ke CrudRepository dicegat, berdasarkan nama metode dan parameter, permintaan dibuat, yang kemudian dieksekusi oleh mesin database.
Apa yang dibutuhkan untuk mengimplementasikan konsep tersebut?
Pertama, anotasi tingkat kelas sehingga Anda dapat menemukan antarmuka - repositori dan membuat bin berdasarkannya.
Juga, penjelasan tentang metode antarmuka ini mungkin akan berguna untuk menyimpan metadata yang diperlukan untuk memanggil metode. Misalnya - di mana mendapatkan teks skrip dan mesin mana yang digunakan.
Tambahan yang berguna adalah kemampuan untuk menggunakan metode dengan implementasi di antarmuka (alias default) - kode ini akan berfungsi sampai analis bisnis menampilkan versi algoritma yang lebih lengkap, dan pengembang membuat skrip berdasarkan
informasi ini. Atau biarkan analis menulis skrip, dan pengembang kemudian cukup menyalinnya ke server. Ada banyak pilihan :-)
Jadi, anggaplah bahwa untuk toko online Anda perlu membuat layanan untuk menghitung diskon berdasarkan profil pengguna. Tidak jelas sekarang bagaimana melakukan ini, tetapi analis bisnis bersumpah bahwa semua pengguna terdaftar berhak atas diskon 10%, ia akan mengetahui sisanya dari pelanggan dalam waktu seminggu. Servis dibutuhkan besok - musim tentunya. Seperti apa kode untuk kasus ini?
@ScriptRepository public interface PricingRepository { @ScriptMethod default BigDecimal applyCustomerDiscount(Customer customer, BigDecimal orderAmount) { return orderAmount.multiply(new BigDecimal("0.9")); } }
Dan kemudian algoritma itu sendiri, ditulis, misalnya, di asyik, akan tiba pada waktunya, di sana diskon akan sedikit berbeda:
def age = 50 if ((Calendar.YEAR - customer.birthday.year) >= age) { return orderAmount.multiply(0.75) } else { return orderAmount.multiply(0.9) }
Tujuan dari semua ini adalah untuk memberikan pengembang kemampuan untuk menulis hanya kode antarmuka dan kode skrip, dan tidak dipusingkan dengan semua panggilan ini untuk mendapatkan
getEngine
,
eval
, dan lainnya. Perpustakaan untuk bekerja dengan skrip harus melakukan semua keajaiban - mencegat permohonan metode antarmuka, mendapatkan teks skrip, mengganti nilai parameter, mendapatkan mesin skrip yang diinginkan, menjalankan skrip (atau memanggil metode default jika tidak ada teks skrip) dan mengembalikan nilai. Idealnya, selain kode yang telah ditulis, program harus memiliki sesuatu seperti ini:
@Service public class CustomerServiceBean implements CustomerService { @Inject private PricingRepository pricingRepository;
Tantangannya bisa dibaca, dimengerti, dan untuk membuatnya, seseorang tidak perlu memiliki keterampilan khusus.
Ini adalah ide-ide yang menjadi dasar dibuatnya perpustakaan kecil untuk mengerjakan skrip. Ini dimaksudkan untuk aplikasi Spring, kerangka kerja ini digunakan untuk membuat perpustakaan. Ini menyediakan API yang dapat diperluas untuk memuat skrip dari berbagai sumber dan mengeksekusinya, yang menyembunyikan pekerjaan rutin dengan mesin skrip.
Bagaimana cara kerjanya
Untuk semua antarmuka yang ditandai dengan
@ScriptRepository
, objek proxy dibuat selama inisialisasi konteks Spring menggunakan metode
newProxyInstance
dari kelas
Proxy
. Proxy ini diterbitkan dalam konteks Spring sebagai kacang singleton, sehingga Anda dapat mendeklarasikan bidang kelas dengan jenis antarmuka dan meletakkan
@Autowired
atau
@Inject
annotation di atasnya. Tepat seperti yang direncanakan.
Pemindaian dan pemrosesan antarmuka skrip diaktifkan menggunakan anotasi
@EnableSriptRepositories
, seperti di Spring, JPA atau repositori untuk MongoDB diaktifkan (masing-masing
@EnableJpaRepositories
dan
@EnableMongoRepositories
). Sebagai parameter anotasi, Anda perlu menentukan array dengan nama paket yang ingin Anda pindai.
@Configuration @EnableScriptRepositories(basePackages = {"com.example", "com.sample"}) public class CoreConfig {
Metode harus dijelaskan dengan
@ScriptMethod
(ada juga
@GroovyScript
dan
@JavaScript
, dengan spesialisasi yang sesuai) untuk menambahkan metadata untuk memanggil skrip. Tentu saja, metode standar di antarmuka didukung.
Struktur umum perpustakaan ditunjukkan dalam diagram. Komponen yang disorot biru perlu dikembangkan, putih - yang sudah ada di perpustakaan. Ikon Pegas menandai komponen yang tersedia dalam konteks Pegas.
Ketika metode antarmuka disebut (pada kenyataannya, objek proxy), penangan panggilan diluncurkan, yang dalam konteks aplikasi mencari dua kacang: penyedia, yang akan mencari teks skrip, dan pelaksana, yang pada kenyataannya, teks yang ditemukan akan mengeksekusi. Kemudian pawang mengembalikan hasilnya ke metode panggilan.
Nama
@ScriptMethod
penyedia dan pelaksana ditentukan dalam anotasi
@ScriptMethod
, di mana Anda juga dapat menetapkan batas waktu pada eksekusi metode. Di bawah ini adalah contoh kode penggunaan perpustakaan:
@ScriptRepository public interface PricingRepository { @ScriptMethod (providerBeanName = "resourceProvider", evaluatorBeanName = "groovyEvaluator", timeout = 100) default BigDecimal applyCustomerDiscount( @ScriptParam("cust") Customer customer, @ScriptParam("amount") BigDecimal orderAmount) { return orderAmount.multiply(new BigDecimal("0.9")); } }
Anda dapat melihat anotasi
@ScriptParam
- mereka diperlukan untuk menunjukkan nama parameter saat meneruskannya ke skrip, karena kompiler Java menghapus nama asli dari sumber (ada cara untuk membuatnya tidak melakukan ini, tetapi lebih baik tidak bergantung padanya). Anda dapat menghilangkan nama parameter, tetapi dalam kasus ini, Anda harus menggunakan "arg0", "arg1" dalam skrip, yang tidak sangat meningkatkan keterbacaan.
Secara default, perpustakaan memiliki penyedia untuk membaca file .groovy dan .js dari disk dan pelaksana yang terkait, yang merupakan pembungkus API JSR-233 standar. Anda dapat membuat kacang Anda sendiri untuk sumber skrip yang berbeda dan untuk mesin yang berbeda, untuk ini Anda perlu mengimplementasikan antarmuka yang sesuai:
ScriptProvider
dan
SpringEvaluator
. Antarmuka pertama menggunakan
org.springframework.scripting.ScriptSource
dan yang kedua adalah
org.springframework.scripting.ScriptEvaluator
. API Musim Semi digunakan sehingga kelas yang sudah jadi dapat digunakan jika sudah ada dalam aplikasi.
Penyedia dan artis dicari berdasarkan nama untuk fleksibilitas yang lebih besar - Anda dapat mengganti kacang standar dari perpustakaan di aplikasi Anda dengan memberi nama komponen Anda dengan nama yang sama.
Pengujian dan versi
Karena skrip sering berubah dan mudah, Anda perlu memiliki cara untuk memastikan bahwa perubahan tidak merusak apa pun. Perpustakaan kompatibel dengan JUnit, repositori dapat dengan mudah diuji sebagai kelas reguler sebagai bagian dari tes unit atau integrasi. Pustaka tiruan juga didukung, dalam tes untuk pustaka Anda dapat menemukan contoh bagaimana membuat tiruan pada metode repositori skrip.
Jika diperlukan versi, maka Anda dapat membuat penyedia yang akan membaca berbagai versi skrip dari sistem file, dari database, atau dari Git, misalnya. Jadi akan mudah untuk mengatur rollback ke versi script sebelumnya jika ada masalah di server utama.
Total
Perpustakaan yang disajikan akan membantu mengatur skrip dalam aplikasi Musim Semi:
- Pengembang akan selalu memiliki informasi tentang parameter apa yang dibutuhkan skrip dan apa yang dikembalikan. Dan jika metode antarmuka diberi nama secara bermakna, lalu apa yang dilakukan skrip.
- Penyedia dan pelaksana akan membantu menjaga kode untuk menerima skrip dan berinteraksi dengan mesin skrip di satu tempat dan panggilan ini tidak akan tersebar di seluruh kode aplikasi.
- Semua panggilan skrip dapat dengan mudah ditemukan menggunakan Temukan Penggunaan.
Konfigurasi semi Boot, pengujian unit, tiruan didukung. Anda bisa mendapatkan data tentang metode "skrip" dan parameternya melalui API. Dan Anda juga dapat membungkus hasil eksekusi dengan objek ScriptResult khusus, di mana akan ada hasil atau contoh pengecualian jika Anda tidak ingin repot dengan mencoba ... menangkap ketika memohon skrip. Konfigurasi XML didukung jika diperlukan karena satu dan lain alasan. Dan akhirnya - Anda dapat menentukan batas waktu untuk metode skrip, jika perlu.
Sumber perpustakaan ada di sini.