Kebanyakan program menggunakan refleksi dalam satu atau lain cara dalam berbagai bentuknya, karena kemampuannya sulit untuk dimasukkan ke dalam satu artikel.
Banyak jawaban berakhir di sana, tetapi yang lebih penting adalah memahami konsep umum refleksi. Kami mengejar jawaban singkat atas pertanyaan agar berhasil lulus wawancara, tetapi kami tidak memahami dasar-dasarnya - dari mana asalnya dan apa sebenarnya yang dimaksud dengan refleksi.
Pada artikel ini kami akan menyentuh semua masalah ini dalam kaitannya dengan anotasi dan dengan contoh langsung kami akan melihat cara menggunakan, menemukan, dan menulis milik Anda.
Refleksi
Saya percaya itu akan menjadi kesalahan untuk berpikir bahwa refleksi Java hanya terbatas pada satu paket di pustaka standar. Oleh karena itu, saya mengusulkan untuk menganggapnya sebagai istilah, tanpa mengikat pada paket tertentu.
Refleksi vs Introspeksi
Selain refleksi, ada pula konsep introspeksi. Introspeksi adalah kemampuan suatu program untuk memperoleh data tentang tipe dan properti lain dari suatu objek. Misalnya, ini
instanceof
:
if (obj instanceof Cat) {
Cat cat = (Cat) obj;
cat.meow();
}
Ini adalah teknik yang sangat kuat, tanpanya Java tidak akan menjadi seperti itu. Namun demikian, dia tidak melangkah lebih jauh dari menerima data, dan refleksi mulai dimainkan.
Beberapa kemungkinan refleksi
Lebih khusus lagi, refleksi adalah kemampuan program untuk memeriksa dirinya sendiri pada saat runtime dan menggunakannya untuk mengubah perilakunya.
Oleh karena itu, contoh yang ditunjukkan di atas bukanlah refleksi, tetapi hanya introspeksi tipe objek. Namun, apakah refleksi itu? Misalnya, membuat kelas atau memanggil metode, tetapi dengan cara yang sangat aneh. Berikut ini contohnya.
Mari kita bayangkan bahwa kita tidak memiliki pengetahuan tentang kelas yang ingin kita buat, tetapi hanya informasi tentang di mana kelas itu. Dalam kasus ini, kita tidak dapat membuat kelas dengan cara yang jelas:
Object obj = new Cat(); // ?
Mari gunakan refleksi dan buat instance kelas:
Object obj = Class.forName("complete.classpath.MyCat").newInstance();
Mari kita juga memanggil metodenya melalui refleksi:
Method m = obj.getClass().getDeclaredMethod("meow");
m.invoke(obj);
Dari teori ke praktek:
import java.lang.reflect.Method;
import java.lang.Class;
public class Cat {
public void meow() {
System.out.println("Meow");
}
public static void main(String[] args) throws Exception {
Object obj = Class.forName("Cat").newInstance();
Method m = obj.getClass().getDeclaredMethod("meow");
m.invoke(obj);
}
}
Anda dapat memainkannya di Jdoodle .
Terlepas dari kesederhanaannya, ada cukup banyak hal kompleks yang terjadi dalam kode ini, dan seringkali programmer hanya kekurangan penggunaan sederhana
getDeclaredMethod and then invoke
.
Pertanyaan # 1
Mengapa, dalam metode pemanggilan pada contoh di atas, kita harus meneruskan sebuah instance objek?
Saya tidak akan melangkah lebih jauh, karena kita akan jauh dari topik. Sebagai gantinya, saya akan meninggalkan tautan ke artikel oleh kolega senior Tagir Valeev .
Anotasi
Anotasi adalah bagian penting dari bahasa Java. Ini adalah semacam deskripsi yang dapat digantung di kelas, bidang, atau metode. Misalnya, Anda mungkin pernah melihat anotasi
@Override
:
public abstract class Animal {
abstract void doSomething();
}
public class Cat extends Animal {
@Override
public void doSomething() {
System.out.println("Meow");
}
}
Pernahkah Anda bertanya-tanya bagaimana cara kerjanya? Jika Anda tidak tahu, maka sebelum membaca lebih lanjut, coba tebak.
Jenis anotasi
Pertimbangkan anotasi di atas:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
@Target
- menunjukkan untuk apa anotasi tersebut diterapkan. Dalam hal ini, metode.
@Retention
- umur anotasi dalam kode (bukan dalam hitungan detik, tentu saja).
@interface
- adalah sintaks untuk membuat anotasi.
Jika yang pertama dan terakhir kurang lebih jelas (lihat.
@Target
Dalam dokumentasi ), maka
@Retention
mari kita lihat sekarang, karena akan dibagi menjadi beberapa jenis anotasi, yang sangat penting untuk dipahami.
Anotasi ini dapat mengambil tiga nilai:
Dalam kasus pertama, anotasi akan ditulis ke dalam bytecode kode Anda, tetapi tidak boleh dipertahankan oleh mesin virtual pada waktu proses.
Dalam kasus kedua, anotasi akan tersedia saat runtime, berkat itu kita dapat memprosesnya, misalnya, mendapatkan semua kelas yang memiliki anotasi ini.
Dalam kasus ketiga, anotasi akan dihapus oleh kompiler (tidak akan ada di bytecode). Ini biasanya merupakan anotasi yang hanya berguna untuk kompilator.
Kembali ke anotasi
@Override
, kita melihat bahwa anotasi memiliki,
RetentionPolicy.SOURCE
yang umumnya logis, karena hanya digunakan oleh kompilator. Dalam waktu proses, anotasi ini benar-benar tidak memberikan sesuatu yang berguna.
SuperCat
Mari kita coba menambahkan anotasi kita sendiri (ini akan berguna selama pengembangan).
abstract class Cat {
abstract void meow();
}
public class Home {
private class Tom extends Cat {
@Override
void meow() {
System.out.println("Tom-style meow!"); // <---
}
}
private class Alex extends Cat {
@Override
void meow() {
System.out.println("Alex-style meow!"); // <---
}
}
}
Mari kita punya dua kucing di rumah kita: Tom dan Alex. Mari buat anotasi untuk kucing super:
@Target(ElementType.TYPE) //
@Retention(RetentionPolicy.RUNTIME) //
@interface SuperCat {
}
// ...
@SuperCat // <---
private class Alex extends Cat {
@Override
void meow() {
System.out.println("Alex-style meow!");
}
}
// ...
Pada saat yang sama, kita akan meninggalkan Tom sebagai kucing biasa (dunia ini tidak adil). Sekarang mari kita coba mendapatkan kelas yang dianotasi dengan elemen ini. Alangkah baiknya memiliki metode seperti ini pada kelas anotasi itu sendiri:
Set<class<?>> classes = SuperCat.class.getAnnotatedClasses();
Namun sayangnya belum ada cara seperti itu. Lalu bagaimana kita menemukan kelas-kelas ini?
ClassPath
Ini adalah parameter yang mengarah ke kelas khusus.
Saya harap Anda sudah familiar dengan mereka, dan jika belum, segera pelajari ini, karena ini adalah salah satu hal mendasar.
Jadi, setelah mengetahui di mana kelas kita disimpan, kita dapat memuatnya melalui ClassLoader dan memeriksa kelas untuk anotasi ini. Mari langsung ke kode:
public static void main(String[] args) throws ClassNotFoundException {
String packageName = "com.apploidxxx.examples";
ClassLoader classLoader = Home.class.getClassLoader();
String packagePath = packageName.replace('.', '/');
URL urls = classLoader.getResource(packagePath);
File folder = new File(urls.getPath());
File[] classes = folder.listFiles();
for (File aClass : classes) {
int index = aClass.getName().indexOf(".");
String className = aClass.getName().substring(0, index);
String classNamePath = packageName + "." + className;
Class<?> repoClass = Class.forName(classNamePath);
Annotation[] annotations = repoClass.getAnnotations();
for (Annotation annotation : annotations) {
if (annotation.annotationType() == SuperCat.class) {
System.out.println(
"Detected SuperCat!!! It is " + repoClass.getName()
);
}
}
}
}
Saya tidak menyarankan penggunaan ini dalam program Anda. Kode ini hanya untuk tujuan informasi!
Contoh ini bersifat indikatif, tetapi hanya digunakan untuk tujuan pendidikan karena ini:
Class<?> repoClass = Class.forName(classNamePath);
Kami akan mencari tahu alasannya nanti. Untuk saat ini, mari kita lihat garis-garis dari atas:
// ...
//
String packageName = "com.apploidxxx.examples";
// , -
ClassLoader classLoader = Home.class.getClassLoader();
// com.apploidxxx.examples -> com/apploidxxx/examples
String packagePath = packageName.replace('.', '/');
URL urls = classLoader.getResource(packagePath);
File folder = new File(urls.getPath());
//
File[] classes = folder.listFiles();
// ...
Untuk mengetahui dari mana kita mendapatkan file-file ini, mari kita lihat arsip JAR yang dibuat saat kita menjalankan aplikasi:
├───com │ └───apploidxxx │ └───examples │ Cat.class │ Home$Alex.class │ Home$Tom.class │ Home.class │ Main.class │ SuperCat.class
Jadi,
classes
ini hanyalah file yang kami kompilasi sebagai bytecode. Meskipun demikian,
File
ini belum merupakan file yang diunduh, kami hanya tahu di mana mereka berada, tetapi kami masih tidak dapat melihat apa yang ada di dalamnya.
Jadi, mari muat setiap file:
for (File aClass : classes) {
// , , Home.class, Home$Alex.class
// .class
// Java
int index = aClass.getName().indexOf(".");
String className = aClass.getName().substring(0, index);
String classNamePath = packageName + "." + className;
// classNamePath = com.apploidxxx.examples.Home
Class<?> repoClass = Class.forName(classNamePath);
}
Semua yang telah dilakukan sebelumnya hanya memanggil metode ini Class.forName, yang akan memuat kelas yang kita butuhkan. Jadi bagian terakhir adalah mendapatkan semua anotasi yang digunakan pada kelas repoClass dan kemudian memeriksa apakah itu anotasi
@SuperCat
:
Annotation[] annotations = repoClass.getAnnotations();
for (Annotation annotation : annotations) {
if (annotation.annotationType() == SuperCat.class) {
System.out.println(
"Detected SuperCat!!! It is " + repoClass.getName()
);
}
}
output: Detected SuperCat!!! It is com.apploidxxx.examples.Home$Alex
Dan Anda Selesai! Sekarang kita memiliki kelas itu sendiri, kita mendapatkan akses ke semua metode refleksi.
Bercermin
Seperti pada contoh di atas, kita cukup membuat instance baru dari kelas kita. Namun sebelum itu, mari kita lihat beberapa formalitas.
- Pertama, kucing perlu tinggal di suatu tempat, jadi mereka membutuhkan rumah. Dalam kasus kami, mereka tidak dapat hidup tanpa rumah.
- Kedua, mari buat daftar supercoats.
List<cat> superCats = new ArrayList<>();
final Home home = new Home(); // ,
Jadi, pemrosesan mengambil bentuk akhirnya:
for (Annotation annotation : annotations) {
if (annotation.annotationType() == SuperCat.class) {
Object obj = repoClass
.getDeclaredConstructor(Home.class)
.newInstance(home);
superCats.add((Cat) obj);
}
}
Dan lagi judul pertanyaan:
Pertanyaan # 2
Apa yang terjadi jika kita menandai@SuperCat
kelas yang tidak diwarisiCat
?
Pertanyaan # 3
Mengapa kita membutuhkan konstruktor yang menggunakan tipe argumenHome
?
Pikirkan sejenak, lalu segera analisis jawabannya:
Jawaban # 2 : Ya
ClassCastException
, karena anotasi itu sendiri
@SuperCat
tidak menjamin bahwa kelas yang ditandai dengan anotasi ini akan mewarisi atau menerapkan sesuatu.
Anda dapat memeriksanya dengan menghapus
extends Cat
dari Alex. Pada saat yang sama, Anda akan melihat betapa bergunanya anotasi
@Override
.
Jawaban # 3 : Kucing membutuhkan rumah karena mereka adalah kelas batin. Semuanya dalam kerangka Spesifikasi Bahasa Java bab 15.9.3 .
Namun, Anda dapat menghindari ini hanya dengan membuat kelas-kelas ini menjadi statis. Namun saat bekerja dengan refleksi, Anda akan sering menjumpai hal semacam ini. Dan Anda tidak perlu mengetahui spesifikasi Java secara menyeluruh untuk itu. Hal-hal ini cukup logis, dan Anda dapat memikirkan sendiri mengapa kita harus meneruskan turunan kelas induk ke konstruktor, jika ya
non-static
.
Mari kita meringkas dan mendapatkan: Home.java
package com.apploidxxx.examples;
import java.io.File;
import java.lang.annotation.*;
import java.lang.reflect.InvocationTargetException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface SuperCat {
}
abstract class Cat {
abstract void meow();
}
public class Home {
public class Tom extends Cat {
@Override
void meow() {
System.out.println("Tom-style meow!");
}
}
@SuperCat
public class Alex extends Cat {
@Override
void meow() {
System.out.println("Alex-style meow!");
}
}
public static void main(String[] args) throws Exception {
String packageName = "com.apploidxxx.examples";
ClassLoader classLoader = Home.class.getClassLoader();
String packagePath = packageName.replace('.', '/');
URL urls = classLoader.getResource(packagePath);
File folder = new File(urls.getPath());
File[] classes = folder.listFiles();
List<Cat> superCats = new ArrayList<>();
final Home home = new Home();
for (File aClass : classes) {
int index = aClass.getName().indexOf(".");
String className = aClass.getName().substring(0, index);
String classNamePath = packageName + "." + className;
Class<?> repoClass = Class.forName(classNamePath);
Annotation[] annotations = repoClass.getAnnotations();
for (Annotation annotation : annotations) {
if (annotation.annotationType() == SuperCat.class) {
Object obj = repoClass
.getDeclaredConstructor(Home.class)
.newInstance(home);
superCats.add((Cat) obj);
}
}
}
superCats.forEach(Cat::meow);
}
}
output: Alex-style meow!
Jadi ada apa dengan
Class.forName
?
Dia sendiri melakukan apa yang diminta darinya. Namun, kami menggunakannya secara tidak benar.
Bayangkan Anda sedang mengerjakan proyek dengan 1000 atau lebih kelas (bagaimanapun, kami menulis di Java). Dan bayangkan memuat setiap kelas yang Anda temukan di classPath. Anda sendiri memahami bahwa memori dan sumber daya JVM lainnya bukanlah karet.
Cara bekerja dengan anotasi
Jika tidak ada cara lain untuk bekerja dengan anotasi, maka menggunakannya sebagai label kelas, seperti, misalnya, di Spring, akan sangat, sangat kontroversial.
Tapi Spring sepertinya berhasil. Apakah program saya sangat lambat karena mereka? Sayangnya atau untungnya, tidak. Musim semi bekerja dengan baik (dalam hal ini) karena menggunakan cara yang sedikit berbeda untuk bekerja dengannya.
Langsung ke bytecode
Setiap orang (saya harap) entah bagaimana memiliki gagasan tentang apa itu bytecode. Ini menyimpan semua informasi tentang kelas kami dan metadatanya (termasuk anotasi).
Saatnya mengingat milik kita
RetentionPolicy
. Dalam contoh sebelumnya, kami dapat menemukan anotasi ini karena kami menunjukkan bahwa itu adalah anotasi runtime. Oleh karena itu, harus disimpan dalam bytecode.
Jadi mengapa kita tidak membacanya saja (ya, dari bytecode)? Tetapi di sini saya tidak akan menerapkan program untuk membacanya dari bytecode, karena program ini layak mendapat artikel terpisah. Namun, Anda dapat melakukannya sendiri - ini akan menjadi praktik hebat yang akan mengkonsolidasikan materi artikel.
Untuk membiasakan diri dengan bytecode, Anda bisa mulai dengan artikel saya... Di sana saya menjelaskan hal-hal bytecode dasar dengan Hello World! Artikel ini akan berguna bahkan jika Anda tidak akan bekerja langsung dengan bytecode. Ini menjelaskan poin-poin fundamental yang akan membantu menjawab pertanyaan: mengapa tepatnya?
Setelah itu selamat datang di spesifikasi resmi JVM . Jika Anda tidak ingin mengurai bytecode secara manual (bytes), lihatlah library seperti ASM dan Javassist .
Refleksi
Reflections adalah pustaka dengan lisensi WTFPL yang memungkinkan Anda melakukan apa pun yang Anda inginkan. Perpustakaan yang cukup cepat untuk berbagai pekerjaan dengan classpath dan metadata. Hal yang berguna adalah dapat menyimpan informasi tentang beberapa data yang sudah dibaca, yang menghemat waktu. Anda dapat menggali ke dalam dan menemukan kelas Store.
package com.apploidxxx.examples;
import org.reflections.Reflections;
import java.lang.reflect.InvocationTargetException;
import java.util.Optional;
import java.util.Set;
public class ExampleReflections {
private static final Home HOME = new Home();
public static void main(String[] args) {
Reflections reflections = new Reflections("com.apploidxxx.examples");
Set<Class<?>> superCats = reflections
.getTypesAnnotatedWith(SuperCat.class);
for (Class<?> clazz : superCats) {
toCat(clazz).ifPresent(Cat::meow);
}
}
private static Optional<Cat> toCat(Class<?> clazz) {
try {
return Optional.of((Cat) clazz
.getDeclaredConstructor(Home.class)
.newInstance(HOME)
);
} catch (InstantiationException |
IllegalAccessException |
InvocationTargetException |
NoSuchMethodException e)
{
e.printStackTrace();
return Optional.empty();
}
}
}
konteks pegas
Saya akan merekomendasikan menggunakan pustaka Refleksi, karena secara internal bekerja melalui javassist, yang menunjukkan bahwa ia membaca bytecode, bukan memuatnya.
Namun, ada banyak pustaka lain yang bekerja dengan cara serupa. Ada banyak, tetapi sekarang saya hanya ingin membongkar salah satunya - ini
spring-context
. Ini mungkin lebih baik daripada yang pertama saat Anda mengembangkan bot dalam framework Spring. Tetapi ada juga beberapa nuansa di sini.
Jika kelas Anda pada dasarnya adalah kacang yang dikelola, yaitu, mereka berada dalam wadah Spring, maka Anda tidak perlu memindai ulang. Anda cukup mengakses kacang ini dari wadahnya sendiri.
Hal lainnya adalah jika Anda ingin kelas yang diberi tag menjadi kacang, maka Anda dapat melakukannya secara manual melalui
ClassPathScanningCandidateComponentProvider
yang bekerja melalui ASM.
Sekali lagi, sangat jarang Anda perlu menggunakan metode ini, tetapi ini layak dipertimbangkan sebagai opsi.
Saya menulis bot untuk VK di atasnya. Ini adalah repositori yang dapat Anda kenali, tetapi saya sudah lama menulisnya, dan ketika saya pergi untuk melihat untuk memasukkan tautan ke artikel, saya melihat bahwa melalui VK-Java-SDK saya menerima pesan dengan bidang yang tidak diinisialisasi, meskipun semuanya berfungsi sebelumnya.
Lucunya, saya bahkan tidak mengubah versi SDK, jadi jika Anda menemukan alasannya, saya akan berterima kasih. Namun, memuat perintah itu sendiri berfungsi dengan baik, itulah yang dapat Anda lihat jika Anda ingin melihat contoh bekerja dengannya
spring-context
.
Perintah di dalamnya adalah sebagai berikut:
@Command(value = "hello", aliases = {"", ""})
public class Hello implements Executable {
public BotResponse execute(Message message) throws Exception {
return BotResponseFactoryUtil.createResponse("hello-hello",
message.peerId);
}
}
SuperCat
Anda dapat menemukan contoh kode beranotasi di repositori ini .
Aplikasi praktis dari anotasi dalam membuat bot Telegram
Ini semua adalah pengenalan yang agak panjang, tapi perlu untuk bekerja dengan anotasi. Selanjutnya, kami akan menerapkan bot, tetapi tujuan artikel tersebut bukanlah manual untuk membuatnya. Ini adalah aplikasi praktis dari anotasi. Mungkin ada apa saja di sini: dari aplikasi konsol hingga bot yang sama untuk VK, keranjang, dan hal lainnya.
Selain itu, di sini beberapa pemeriksaan rumit tidak akan dilakukan dengan sengaja. Misalnya, sebelumnya, contoh tidak memiliki pemeriksaan untuk null atau penanganan kesalahan yang benar, belum lagi loggingnya.
Semua ini dilakukan untuk menyederhanakan kode. Oleh karena itu jika anda mengambil kodenya dari contoh, maka jangan malas untuk memodifikasinya, agar anda lebih memahaminya dan menyesuaikannya dengan kebutuhan anda.
Kami akan menggunakan perpustakaan TelegramBots dengan lisensi MITuntuk bekerja dengan API telegram. Anda dapat menggunakan yang lain. Saya memilihnya karena dapat bekerja baik "c" (memiliki versi dengan starter) atau "tanpa" pegas.
Sebenarnya saya juga tidak ingin mempersulit kode dengan menambahkan semacam abstraksi, jika mau, Anda dapat melakukan sesuatu yang universal, tetapi pikirkan apakah itu layak, jadi untuk artikel ini kita akan sering menggunakan kelas konkret dari perpustakaan ini, mengikat kita kode untuk mereka.
Refleksi
Bot pertama dalam baris adalah bot yang ditulis di pustaka refleksi, tanpa Spring. Kami tidak akan menganalisis semuanya, tetapi hanya poin utama, khususnya kami tertarik pada pemrosesan anotasi. Sebelum menganalisis artikel, Anda sendiri dapat mengetahui cara kerjanya di repositori saya .
Dalam semua contoh, kami akan mematuhi fakta bahwa bot terdiri dari beberapa perintah, dan kami tidak akan memuat perintah ini secara manual, tetapi hanya akan menambahkan anotasi. Berikut contoh perintahnya:
@Handler("/hello")
public class HelloHandler implements RequestHandler {
private static final Logger log = LoggerFactory
.getLogger(HelloHandler.class);
@Override
public SendMessage execute(Message message) {
log.info("Executing message from : " + message.getText());
return SendMessage.builder()
.text("Yaks")
.chatId(String.valueOf(message.getChatId()))
.build();
}
}
@Retention(RetentionPolicy.RUNTIME)
public @interface Handler {
String value();
}
Dalam hal ini, parameter
/hello
akan ditulis ke
value
dalam anotasi. nilai adalah sesuatu seperti anotasi default. Yaitu
@Handler("/hello")
=
@Handler(value = "/hello")
.
Kami juga akan menambahkan penebang. Kami akan memanggil mereka sebelum memproses permintaan, atau setelahnya, dan juga menggabungkannya:
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
String value() default ".*"; // regex
ExecutionTime[] executionTime() default ExecutionTime.BEFORE;
}
default` , , `value
@Log
public class LogHandler implements RequestLogger {
private static final Logger log = LoggerFactory
.getLogger(LogHandler.class);
@Override
public void execute(Message message) {
log.info("Just log a received message : " + message.getText());
}
}
Tapi kita juga bisa menambahkan parameter sehingga logger dipicu untuk pesan tertentu:
@Log(value = "/hello")
public class HelloLogHandler implements RequestLogger {
public static final Logger log = LoggerFactory
.getLogger(HelloLogHandler.class);
@Override
public void execute(Message message) {
log.info("Received special hello command!");
}
}
Atau dipicu setelah memproses permintaan:
@Log(executionTime = ExecutionTime.AFTER)
public class AfterLogHandler implements RequestLogger {
private static final Logger log = LoggerFactory
.getLogger(AfterLogHandler.class);
@Override
public void executeAfter(Message message, SendMessage sendMessage) {
log.info("Bot response >> " + sendMessage.getText());
}
}
Atau keduanya di sana dan di sana:
@Log(executionTime = {ExecutionTime.AFTER, ExecutionTime.BEFORE})
public class AfterAndBeforeLogger implements RequestLogger {
private static final Logger log = LoggerFactory
.getLogger(AfterAndBeforeLogger.class);
@Override
public void execute(Message message) {
log.info("Before execute");
}
@Override
public void executeAfter(Message message, SendMessage sendMessage) {
log.info("After execute");
}
}
Kita bisa melakukan ini karena
executionTime
membutuhkan array nilai. Prinsip operasinya sederhana, jadi mari kita mulai memproses anotasi ini:
Set<Class<?>> annotatedCommands =
reflections.getTypesAnnotatedWith(Handler.class);
final Map<String, RequestHandler> commandsMap = new HashMap<>();
final Class<RequestHandler> requiredInterface = RequestHandler.class;
for (Class<?> clazz : annotatedCommands) {
if (LoaderUtils.isImplementedInterface(clazz, requiredInterface)) {
for (Constructor<?> c : clazz.getDeclaredConstructors()) {
//noinspection unchecked
Constructor<RequestHandler> castedConstructor =
(Constructor<RequestHandler>) c;
commandsMap.put(extractCommandName(clazz),
OBJECT_CREATOR.instantiateClass(castedConstructor));
}
} else {
log.warn("Command didn't implemented: "
+ requiredInterface.getCanonicalName());
}
}
// ...
private static String extractCommandName(Class<?> clazz) {
Handler handler = clazz.getAnnotation(Handler.class);
if (handler == null) {
throw new
IllegalArgumentException(
"Passed class without Handler annotation"
);
} else {
return handler.value();
}
}
Faktanya, kami hanya membuat peta dengan nama perintah, yang kami ambil dari nilai
value
di anotasi. Kode sumbernya ada di sini .
Kami melakukan hal yang sama dengan Log, hanya ada beberapa logger dengan pola yang sama, jadi kami sedikit mengubah struktur data kami:
Set<Class<?>> annotatedLoggers = reflections.getTypesAnnotatedWith(Log.class);
final Map<String, Set<RequestLogger>> commandsMap = new HashMap<>();
final Class<RequestLogger> requiredInterface = RequestLogger.class;
for (Class<?> clazz : annotatedLoggers) {
if (LoaderUtils.isImplementedInterface(clazz, requiredInterface)) {
for (Constructor<?> c : clazz.getDeclaredConstructors()) {
//noinspection unchecked
Constructor<RequestLogger> castedConstructor =
(Constructor<RequestLogger>) c;
String name = extractCommandName(clazz);
commandsMap.computeIfAbsent(name, n -> new HashSet<>());
commandsMap
.get(extractCommandName(clazz))
.add(OBJECT_CREATOR.instantiateClass(castedConstructor));
}
} else {
log.warn("Command didn't implemented: "
+ requiredInterface.getCanonicalName());
}
}
Ada beberapa penebang untuk setiap pola. Selebihnya sama saja.
Sekarang, di bot itu sendiri, kita perlu mengonfigurasi
executionTime
dan mengalihkan permintaan ke kelas-kelas ini:
public final class CommandService {
private static final Map<String, RequestHandler> commandsMap
= new HashMap<>();
private static final Map<String, Set<RequestLogger>> loggersMap
= new HashMap<>();
private CommandService() {
}
public static synchronized void init() {
initCommands();
initLoggers();
}
private static void initCommands() {
commandsMap.putAll(CommandLoader.readCommands());
}
private static void initLoggers() {
loggersMap.putAll(LogLoader.loadLoggers());
}
public static RequestHandler serve(String message) {
for (Map.Entry<String, RequestHandler> entry : commandsMap.entrySet()) {
if (entry.getKey().equals(message)) {
return entry.getValue();
}
}
return msg -> SendMessage.builder()
.text(" ")
.chatId(String.valueOf(msg.getChatId()))
.build();
}
public static Set<RequestLogger> findLoggers(
String message,
ExecutionTime executionTime
) {
final Set<RequestLogger> matchedLoggers = new HashSet<>();
for (Map.Entry<String, Set<RequestLogger>> entry:loggersMap.entrySet()) {
for (RequestLogger logger : entry.getValue()) {
if (containsExecutionTime(
extractExecutionTimes(logger), executionTime
))
{
if (message.matches(entry.getKey()))
matchedLoggers.add(logger);
}
}
}
return matchedLoggers;
}
private static ExecutionTime[] extractExecutionTimes(RequestLogger logger) {
return logger.getClass().getAnnotation(Log.class).executionTime();
}
private static boolean containsExecutionTime(
ExecutionTime[] times,
ExecutionTime executionTime
) {
for (ExecutionTime et : times) {
if (et == executionTime) return true;
}
return false;
}
}
public class DefaultBot extends TelegramLongPollingBot {
private static final Logger log = LoggerFactory.getLogger(DefaultBot.class);
public DefaultBot() {
CommandService.init();
log.info("Bot initialized!");
}
@Override
public String getBotUsername() {
return System.getenv("BOT_NAME");
}
@Override
public String getBotToken() {
return System.getenv("BOT_TOKEN");
}
@Override
public void onUpdateReceived(Update update) {
try {
Message message = update.getMessage();
if (message != null && message.hasText()) {
// run "before" loggers
CommandService
.findLoggers(message.getText(), ExecutionTime.BEFORE)
.forEach(logger -> logger.execute(message));
// command execution
SendMessage response;
this.execute(response = CommandService
.serve(message.getText())
.execute(message));
// run "after" loggers
CommandService
.findLoggers(message.getText(), ExecutionTime.AFTER)
.forEach(logger -> logger.executeAfter(message, response));
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
Cara terbaik adalah mencari sendiri kodenya dan melihat di repositori, atau bahkan lebih baik membukanya melalui IDE. Repositori ini bagus untuk memulai dan memulai, tetapi tidak cukup baik sebagai bot.
Pertama, tidak ada cukup abstraksi antar tim. Artinya, Anda hanya dapat kembali dari setiap perintah
SendMessage
. Ini dapat diatasi dengan menggunakan tingkat abstraksi yang lebih tinggi, misalnya
BotApiMethodMessage
, tetapi ini tidak benar-benar menyelesaikan semua masalah.
Kedua, perpustakaan itu sendiri
TelegramBots
, menurut saya, tidak terlalu fokus pada pekerjaan (arsitektur) bot semacam itu. Jika Anda mengembangkan bot menggunakan pustaka khusus ini, Anda dapat menggunakan
Ability Bot
yang terdaftar di wiki perpustakaan itu sendiri. Tapi saya benar-benar ingin melihat perpustakaan yang lengkap dengan arsitektur seperti itu. Jadi Anda bisa mulai menulis perpustakaan Anda!
Bot musim semi
Ini lebih masuk akal saat bekerja dengan ekosistem pegas:
- Bekerja melalui anotasi tidak melanggar konsep umum wadah pegas.
- Kita tidak dapat membuat perintah sendiri, tetapi mendapatkannya dari wadah, menandai perintah kita sebagai kacang.
- Kami mendapatkan DI yang sangat baik dari musim semi.
Secara umum, penggunaan pegas sebagai kerangka kerja bot merupakan topik pembicaraan lain. Lagi pula, banyak yang mungkin berpikir bahwa ini terlalu sulit untuk bot (meskipun, kemungkinan besar, mereka juga tidak menulis bot di Java).
Tapi menurut saya musim semi adalah lingkungan yang baik tidak hanya untuk aplikasi perusahaan / web. Itu hanya berisi banyak pustaka resmi dan khusus untuk ekosistemnya (pada musim semi maksud saya Spring Boot).
Dan, yang terpenting, ini memungkinkan Anda menerapkan banyak pola dengan cara berbeda yang disediakan oleh penampung.
Penerapan
Nah, mari kita ke bot itu sendiri.
Karena kita menulis pada tumpukan pegas, kita tidak dapat membuat wadah perintah kita sendiri, tetapi menggunakan yang sudah ada di pegas. Mereka tidak dapat dipindai, tetapi diperoleh dari wadah IoC .
Pengembang yang lebih independen dapat langsung mulai membaca kode .
Di sini saya akan menganalisis dengan tepat pembacaan perintah, meskipun ada beberapa poin yang lebih menarik dalam repositori itu sendiri yang dapat Anda pertimbangkan sendiri.
Implementasinya sangat mirip dengan bot melalui Reflections, jadi penjelasannya sama.
ObjectLoader.java
@Service
public class ObjectLoader {
private final ApplicationContext applicationContext;
public ObjectLoader(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
public Collection<Object> loadObjectsWithAnnotation(
Class<? extends Annotation> annotation
) {
return applicationContext.getBeansWithAnnotation(annotation).values();
}
}
CommandLoader.java
public Map<String, RequestHandler> readCommands() { final Map<String, RequestHandler> commandsMap = new HashMap<>(); for (Object obj : objectLoader.loadObjectsWithAnnotation(Handler.class)) { if (obj instanceof RequestHandler) { RequestHandler handler = (RequestHandler) obj; commandsMap.put(extractCommandName(handler.getClass()), handler); } } return commandsMap; }
Berbeda dengan contoh sebelumnya, ini sudah menggunakan level abstraksi yang lebih tinggi untuk antarmuka, yang, tentu saja, bagus. Kami juga tidak perlu membuat contoh perintah sendiri.
Mari kita simpulkan
Terserah Anda untuk memutuskan apa yang terbaik untuk tugas Anda. Saya telah mengurai tiga kasus untuk bot yang kurang lebih serupa:
- Refleksi.
- Konteks Pegas (tanpa Pegas).
- ApplicationContext dari Spring.
Namun, saya dapat memberi Anda saran berdasarkan pengalaman saya:
- Pertimbangkan jika Anda membutuhkan Spring. Ini menyediakan wadah IoC dan kemampuan ekosistem yang kuat, tetapi semuanya memiliki harga. Saya biasanya berpikir seperti ini: jika Anda membutuhkan database dan quick start, maka Anda memerlukan Spring Boot. Jika bot cukup sederhana, maka Anda dapat melakukannya tanpanya.
- Jika Anda tidak membutuhkan dependensi yang kompleks, silakan gunakan Refleksi.
Menerapkan, misalnya, JPA tanpa Data Musim Semi menurut saya tugas yang agak memakan waktu, meskipun Anda juga dapat melihat alternatif dalam bentuk mikronaut atau quarkus, tetapi saya hanya mendengar tentang mereka dan tidak memiliki cukup pengalaman untuk memberi saran tentang hal ini.
Jika Anda penganut pendekatan yang lebih bersih dari awal, bahkan tanpa JPA, lihat bot ini , yang bekerja melalui JDBC melalui VK dan Telegram.
Di sana Anda akan melihat banyak entri formulir:
PreparedStatement stmt = connection.prepareStatement("UPDATE alias SET aliases=?::jsonb WHERE vkid=?");
stmt.setString(1, aliases.toJSON());
stmt.setInt(2, vkid);
stmt.execute();
Tetapi kodenya berumur dua tahun, jadi saya tidak menyarankan mengambil semua pola dari sana. Dan secara umum, saya tidak akan merekomendasikan melakukan ini sama sekali (bekerja melalui JDBC).
Juga secara pribadi, saya tidak terlalu suka bekerja langsung dengan Hibernate. Saya sudah mengalami pengalaman menyedihkan dalam menulis
DAO
dan
HibernateSessionFactoryUtil
(mereka yang menulis akan mengerti maksud saya).
Sedangkan untuk artikelnya sendiri, saya berusaha untuk membuatnya tetap singkat, tapi cukup sehingga hanya dengan artikel ini anda bisa mulai mengembangkannya. Namun, ini bukanlah satu bab dalam buku itu, tapi sebuah artikel tentang Habré. Anda sendiri dapat mempelajari lebih lanjut tentang anotasi dan refleksi secara umum, misalnya, dengan membuat bot yang sama.
Semoga beruntung untuk semuanya! Dan jangan lupa kode promo HABR nya yang memberikan tambahan diskon 10% dari yang tertera di banner.