La mayoría de los programas utilizan la reflexión de una forma u otra en sus diversas formas, porque sus capacidades son difíciles de encajar en un artículo.
Muchas respuestas terminan ahí, pero lo más importante es comprender el concepto general de reflexión. Buscamos respuestas breves a las preguntas para aprobar con éxito la entrevista, pero no entendemos los conceptos básicos: de dónde vino y qué se entiende exactamente por reflexión.
En este artículo tocaremos todos estos temas en relación con las anotaciones y con un ejemplo en vivo veremos cómo usar, encontrar y escribir el tuyo.
Reflexión
Creo que sería un error pensar que la reflexión de Java se limita a un solo paquete en la biblioteca estándar. Por tanto, propongo considerarlo como un término, sin vincularlo a un paquete específico.
Reflexión vs introspección
Junto con la reflexión, también existe el concepto de introspección. La introspección es la capacidad de un programa para obtener datos sobre el tipo y otras propiedades de un objeto. Por ejemplo, esto
instanceof
:
if (obj instanceof Cat) {
Cat cat = (Cat) obj;
cat.meow();
}
Esta es una técnica muy poderosa, sin la cual Java no sería lo que es. Sin embargo, no va más allá de recibir datos y entra en juego la reflexión.
Algunas posibilidades de reflexión
Más específicamente, la reflexión es la capacidad de un programa para examinarse a sí mismo en tiempo de ejecución y utilizarlo para cambiar su comportamiento.
Por lo tanto, el ejemplo que se muestra arriba no es un reflejo, sino solo una introspección del tipo de objeto. Pero, ¿qué es entonces la reflexión? Por ejemplo, creando una clase o llamando a un método, pero de una forma muy peculiar. A continuación se muestra un ejemplo.
Imaginemos que no tenemos ningún conocimiento sobre la clase que queremos crear, sino solo información sobre dónde está. En este caso, no podemos crear una clase de la forma obvia:
Object obj = new Cat(); // ?
Usemos la reflexión y creemos una instancia de la clase:
Object obj = Class.forName("complete.classpath.MyCat").newInstance();
Llamemos también a su método mediante reflexión:
Method m = obj.getClass().getDeclaredMethod("meow");
m.invoke(obj);
De la teoría a la práctica:
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);
}
}
Puedes jugar con él en Jdoodle .
A pesar de su simplicidad, hay muchas cosas complejas en este código y, a menudo, al programador le falta un uso simple
getDeclaredMethod and then invoke
.
Pregunta # 1
¿Por qué, en el método de invocación del ejemplo anterior, deberíamos pasar una instancia de objeto?
No iré más lejos, ya que nos alejaremos del tema. En cambio, dejaré un enlace a un artículo del colega principal Tagir Valeev .
Anotaciones
Las anotaciones son una parte importante del lenguaje Java. Es una especie de descriptor que se puede colgar en una clase, campo o método. Por ejemplo, es posible que haya visto la anotación
@Override
:
public abstract class Animal {
abstract void doSomething();
}
public class Cat extends Animal {
@Override
public void doSomething() {
System.out.println("Meow");
}
}
¿Te has preguntado alguna vez cómo funciona? Si no lo sabe, antes de seguir leyendo, intente adivinar.
Tipos de anotaciones
Considere la anotación anterior:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
@Target
- indica a qué se aplica la anotación. En este caso, al método.
@Retention
- la vida útil de la anotación en el código (no en segundos, por supuesto).
@interface
- es la sintaxis para crear anotaciones.
Si la primera y la última están más o menos claras (ver.
@Target
En la documentación ), entonces
@Retention
veamos ahora, ya que se dividirá en varios tipos de anotaciones, lo cual es muy importante de entender.
Esta anotación puede tomar tres valores:
En el primer caso, la anotación se escribirá en el código de bytes de su código, pero la máquina virtual no debe conservarla en tiempo de ejecución.
En el segundo caso, la anotación estará disponible en tiempo de ejecución, por lo que podemos procesarla, por ejemplo, obtener todas las clases que tienen esta anotación.
En el tercer caso, el compilador eliminará la anotación (no estará en el código de bytes). Suelen ser anotaciones que solo son útiles para el compilador.
Volviendo a la anotación
@Override
, vemos que tiene, lo
RetentionPolicy.SOURCE
que en general es lógico, dado que solo lo usa el compilador. En tiempo de ejecución, esta anotación realmente no proporciona nada útil.
Super gato
Intentemos agregar nuestra propia anotación (esto será útil durante el desarrollo).
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!"); // <---
}
}
}
Tengamos dos gatos en nuestra casa: Tom y Alex. Creemos una anotación para el súper gato:
@Target(ElementType.TYPE) //
@Retention(RetentionPolicy.RUNTIME) //
@interface SuperCat {
}
// ...
@SuperCat // <---
private class Alex extends Cat {
@Override
void meow() {
System.out.println("Alex-style meow!");
}
}
// ...
Al mismo tiempo, dejaremos a Tom como un gato corriente (el mundo es injusto). Ahora intentemos obtener las clases que fueron anotadas con este elemento. Sería bueno tener un método como este en la propia clase de anotación:
Set<class<?>> classes = SuperCat.class.getAnnotatedClasses();
Pero, lamentablemente, todavía no existe tal método. Entonces, ¿cómo encontramos estas clases?
ClassPath
Este es un parámetro que apunta a clases personalizadas.
Espero que los conozcas, y si no, apúrate a estudiar esto, ya que esta es una de las cosas fundamentales.
Entonces, después de haber descubierto dónde se almacenan nuestras clases, podemos cargarlas a través del ClassLoader y verificar las clases para esta anotación. Vayamos directamente al código:
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()
);
}
}
}
}
No recomiendo usar esto en su programa. ¡El código es solo para fines informativos!
Este ejemplo es indicativo, pero solo se usa con fines educativos debido a esto:
Class<?> repoClass = Class.forName(classNamePath);
Descubriremos por qué más tarde. Por ahora, echemos un vistazo a las líneas de arriba:
// ...
//
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();
// ...
Para averiguar de dónde obtenemos estos archivos, veamos el archivo JAR que se crea cuando ejecutamos la aplicación:
├───com │ └───apploidxxx │ └───examples │ Cat.class │ Home$Alex.class │ Home$Tom.class │ Home.class │ Main.class │ SuperCat.class
Por lo tanto,
classes
estos son solo nuestros archivos compilados como código de bytes. Sin embargo,
File
este aún no es un archivo descargado, solo sabemos dónde están, pero aún no podemos ver qué hay dentro de ellos.
Así que carguemos cada archivo:
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);
}
Todo lo que se hizo antes fue solo llamar a este método Class.forName, que cargará la clase que necesitamos. Entonces, la parte final es obtener todas las anotaciones utilizadas en repoClass y luego verificar si son anotaciones
@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
¡Y tu estas listo! Ahora que tenemos la clase en sí, tenemos acceso a todos los métodos de reflexión.
Reflexionando
Como en el ejemplo anterior, simplemente podemos crear una nueva instancia de nuestra clase. Pero antes de eso, veamos algunas formalidades.
- Primero, los gatos necesitan vivir en algún lugar, por eso necesitan un hogar. En nuestro caso, no pueden existir sin un hogar.
- En segundo lugar, creemos una lista de sobrecapas.
List<cat> superCats = new ArrayList<>();
final Home home = new Home(); // ,
Entonces el procesamiento toma su forma final:
for (Annotation annotation : annotations) {
if (annotation.annotationType() == SuperCat.class) {
Object obj = repoClass
.getDeclaredConstructor(Home.class)
.newInstance(home);
superCats.add((Cat) obj);
}
}
Y nuevamente el encabezado de preguntas:
Pregunta # 2
¿Qué pasa si marcamos una@SuperCat
clase que no heredaCat
?
Pregunta # 3
¿Por qué necesitamos un constructor que tome un tipo de argumentoHome
?
Piense por un par de minutos, y luego analice inmediatamente las respuestas:
Respuesta # 2 : Sí
ClassCastException
, porque la anotación en sí
@SuperCat
no garantiza que la clase marcada con esta anotación heredará o implementará algo.
Puedes comprobar esto quitando
extends Cat
de Alex. Al mismo tiempo, verá lo útil que puede ser la anotación
@Override
.
Respuesta # 3 : Los gatos necesitan un hogar porque son clases internas. Todo dentro del marco del capítulo 15.9.3 de la Especificación del lenguaje Java .
Sin embargo, puede evitar esto simplemente haciendo que estas clases sean estáticas. Pero cuando trabajas con la reflexión, a menudo te encontrarás con este tipo de cosas. Y realmente no necesita conocer la especificación de Java a fondo para eso. Estas cosas son bastante lógicas, y puede pensar en usted mismo por qué deberíamos pasar una instancia de la clase padre al constructor, si es así
non-static
.
Resumamos y obtengamos: 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!
Entonces, ¿qué pasa con
Class.forName
?
Él mismo hace exactamente lo que se le pide. Sin embargo, lo estamos usando incorrectamente.
Imagina que estás trabajando en proyectos con 1000 o más clases (después de todo, escribimos en Java). E imagina cargar todas las clases que encuentres en classPath. Usted mismo comprende que la memoria y otros recursos de JVM no son de goma.
Formas de trabajar con anotaciones
Si no hubiera otra forma de trabajar con anotaciones, entonces usarlas como etiquetas de clase, como, por ejemplo, en Spring, sería muy, muy controvertido.
Pero Spring parece funcionar. ¿Mi programa es tan lento debido a ellos? Desafortunadamente o afortunadamente, no. Spring funciona bien (en este sentido) porque usa una forma ligeramente diferente de trabajar con ellos.
Directo al código de bytes
Todo el mundo (espero) de alguna manera tiene una idea de lo que es un código de bytes. Almacena toda la información sobre nuestras clases y sus metadatos (incluidas las anotaciones).
Es hora de recordar el nuestro
RetentionPolicy
. En el ejemplo anterior, pudimos encontrar esta anotación porque indicamos que es una anotación en tiempo de ejecución. Por lo tanto, debe almacenarse en bytecode.
Entonces, ¿por qué no lo leemos (sí, desde el código de bytes)? Pero aquí no implementaré un programa para leerlo desde bytecode, ya que merece un artículo aparte. Sin embargo, puede hacerlo usted mismo; será una gran práctica que consolidará el material del artículo.
Para familiarizarse con el código de bytes, puede comenzar con mi artículo... Allí describo las cosas básicas del código de bytes con Hello World! El artículo será útil incluso si no va a trabajar directamente con código de bytes. Describe los puntos fundamentales que ayudarán a responder la pregunta: ¿por qué exactamente?
Después de eso, bienvenido a la especificación oficial de JVM . Si no desea analizar el código de bytes manualmente (por bytes), busque bibliotecas como ASM y Javassist .
Reflexiones
Reflections es una biblioteca con licencia WTFPL que le permite hacer lo que quiera con ella. Una biblioteca bastante rápida para varios trabajos con classpath y metadatos. Lo útil es que puede guardar información sobre algunos de los datos ya leídos, lo que ahorra tiempo. Puedes excavar dentro y encontrar la clase Tienda.
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();
}
}
}
contexto de primavera
Recomendaría usar la biblioteca Reflections, ya que internamente funciona a través de javassist, lo que indica que está leyendo el código de bytes en lugar de cargarlo.
Sin embargo, hay muchas otras bibliotecas que funcionan de manera similar. Hay muchos de ellos, pero ahora quiero desmontar solo uno de ellos: este
spring-context
. Quizás sea mejor que el primero cuando está desarrollando un bot en el marco de Spring. Pero aquí también hay un par de matices.
Si sus clases son esencialmente beans administrados, es decir, están en un contenedor Spring, entonces no necesita volver a escanearlos. Simplemente puede acceder a estos frijoles desde el propio contenedor.
Otra cosa es que si desea que sus clases etiquetadas sean beans, puede hacerlo manualmente a través de
ClassPathScanningCandidateComponentProvider
que funciona a través de ASM.
Nuevamente, es bastante raro que necesite utilizar este método, pero vale la pena considerarlo como una opción.
Escribí un bot para VK en él. Aquí hay un repositorio con el que te puedes familiarizar, pero lo escribí hace mucho tiempo, y cuando fui a buscar para insertar un enlace en el artículo, vi que a través del VK-Java-SDK recibo mensajes con campos sin inicializar, aunque todo funcionaba antes.
Lo curioso es que ni siquiera he cambiado la versión del SDK, así que si encuentras cuál fue el motivo, te lo agradeceré. Sin embargo, cargar los comandos en sí funciona bien, que es exactamente lo que puede ver si desea ver un ejemplo de cómo trabajar
spring-context
.
Los comandos que contiene son los siguientes:
@Command(value = "hello", aliases = {"", ""})
public class Hello implements Executable {
public BotResponse execute(Message message) throws Exception {
return BotResponseFactoryUtil.createResponse("hello-hello",
message.peerId);
}
}
SuperCat
Puede encontrar ejemplos de código anotado en este repositorio .
Aplicación práctica de anotaciones en la creación de un bot de Telegram
Todo esto fue una introducción bastante larga, pero necesaria, para trabajar con anotaciones. A continuación, implementaremos un bot, pero el propósito del artículo no es un manual para crearlo. Esta es una aplicación práctica de anotaciones. Aquí podría haber cualquier cosa: desde aplicaciones de consola hasta los mismos bots para VK, cart y otras cosas.
Además, aquí no se realizarán deliberadamente algunas comprobaciones complejas. Por ejemplo, antes de eso, los ejemplos no tenían ninguna verificación para el manejo de errores nulo o correcto, sin mencionar su registro.
Todo esto se hace para simplificar el código. Por lo tanto, si toma el código de los ejemplos, no sea perezoso en modificarlo, así lo entenderá mejor y lo personalizará para que se adapte a sus necesidades.
Usaremos la biblioteca de TelegramBots con una licencia MITpara trabajar con la API de Telegram. Puede utilizar cualquier otro. Lo elegí porque podía funcionar tanto "c" (tiene una versión con motor de arranque) como "sin" arranque de resorte.
En realidad, tampoco quiero complicar el código agregando algún tipo de abstracción, si lo desea, puede hacer algo universal, pero piense si vale la pena, así que para este artículo usaremos a menudo clases concretas de estas bibliotecas, vinculando nuestro código para ellos.
Reflexiones
El primer bot en línea es un bot escrito en la biblioteca de reflexiones, sin Spring. No analizaremos todo, sino solo los puntos principales, en particular, nos interesa el procesamiento de anotaciones. Antes de analizarlo en el artículo, usted mismo puede averiguar cómo funciona en mi repositorio .
En todos los ejemplos, nos adheriremos al hecho de que el bot consta de varios comandos, y no cargaremos estos comandos manualmente, sino que simplemente agregaremos anotaciones. Aquí hay un comando de ejemplo:
@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();
}
En este caso, el parámetro
/hello
se escribirá
value
en la anotación. value es algo así como la anotación predeterminada. Eso es
@Handler("/hello")
=
@Handler(value = "/hello")
.
También agregaremos registradores. Los llamaremos antes de procesar la solicitud o después, y también los combinaremos:
@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());
}
}
Pero también podemos agregar un parámetro para activar el registrador para ciertos mensajes:
@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!");
}
}
O se activa después de procesar la solicitud:
@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());
}
}
O tanto allí como allá:
@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");
}
}
Podemos hacer esto porque
executionTime
toma una matriz de valores. El principio de funcionamiento es simple, así que comencemos a procesar estas anotaciones:
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();
}
}
De hecho, simplemente creamos un mapa con el nombre del comando, que tomamos del valor
value
en la anotación. El código fuente está aquí .
Hacemos lo mismo con Log, solo que puede haber varios registradores con los mismos patrones, por lo que cambiamos ligeramente nuestra estructura de datos:
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());
}
}
Hay varios registradores para cada patrón. El resto es igual.
Ahora, en el propio bot, necesitamos configurar
executionTime
y redirigir las solicitudes a estas clases:
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();
}
}
}
Es mejor encontrar el código usted mismo y buscar en el repositorio, o incluso mejor abrirlo a través del IDE. Este repositorio es bueno para empezar y empezar, pero no lo suficientemente bueno como bot.
Primero, no hay suficiente abstracción entre equipos. Es decir, solo puede regresar de cada comando
SendMessage
. Esto puede superarse mediante el uso de un mayor nivel de abstracción, por ejemplo
BotApiMethodMessage
, pero ni siquiera eso resuelve todos los problemas.
En segundo lugar, la biblioteca en sí
TelegramBots
, como me parece, no está particularmente enfocada en tal trabajo (arquitectura) de un bot. Si está desarrollando un bot usando esta biblioteca en particular, puede usar
Ability Bot
que aparece en la wiki de la propia biblioteca. Pero realmente quiero ver una biblioteca completa con esa arquitectura. ¡Para que puedas empezar a escribir tu biblioteca!
Bot de primavera
Esto tiene más sentido cuando se trabaja con el ecosistema de primavera:
- Trabajar a través de anotaciones no viola el concepto general del contenedor de resorte.
- No podemos crear comandos nosotros mismos, sino obtenerlos del contenedor, marcando nuestros comandos como beans.
- Obtenemos una excelente DI de la primavera.
En general, usar un resorte como marco para un bot es un tema para una conversación separada. Después de todo, muchos pueden pensar que esto es demasiado difícil para un bot (aunque lo más probable es que tampoco escriban bots en Java).
Pero creo que Spring es un buen entorno no solo para aplicaciones empresariales / web. Solo contiene muchas bibliotecas oficiales y de usuario para su ecosistema (en primavera, me refiero a Spring Boot).
Y lo más importante, le permite implementar una gran cantidad de patrones de diferentes formas proporcionadas por el contenedor.
Implementación
Bueno, vayamos al bot en sí.
Dado que escribimos en la pila de primavera, no podemos crear nuestro propio contenedor de comandos, sino usar el existente en la primavera. No se pueden escanear, pero se obtienen del contenedor de IoC .
Los desarrolladores más independientes pueden comenzar a leer código de inmediato .
Aquí analizaré solo los comandos de lectura, aunque hay un par de puntos interesantes en el propio repositorio que puede considerar por su cuenta.
La implementación es muy similar al bot a través de Reflections, por lo que las anotaciones son las mismas.
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; }
A diferencia del ejemplo anterior, este ya utiliza un nivel más alto de abstracción para las interfaces, lo que, por supuesto, es bueno. Tampoco necesitamos crear instancias de comando nosotros mismos.
Resumamos
Depende de usted decidir qué es lo mejor para su tarea. He analizado tres casos de bots aproximadamente similares:
- Reflexiones.
- Contexto de primavera (sin primavera).
- ApplicationContext de Spring.
Sin embargo, puedo darte consejos basados en mi experiencia:
- Considere si necesita Spring. Proporciona un potente contenedor de IoC y capacidades de ecosistema, pero todo tiene un precio. Normalmente pienso así: si necesita una base de datos y un inicio rápido, entonces necesita Spring Boot. Si el bot es lo suficientemente simple, entonces puede prescindir de él.
- Si no necesita dependencias complejas, no dude en utilizar Reflections.
Implementar, por ejemplo, JPA sin Spring Data me parece una tarea bastante laboriosa, aunque también puedes buscar alternativas en forma de micronauta o quarkus, pero solo he oído hablar de ellas y no tengo la experiencia suficiente para aconsejar algo al respecto.
Si eres partidario de un enfoque más limpio desde cero, incluso sin JPA, mira este bot, que funciona a través de JDBC a través de VK y Telegram.
Allí verá muchas entradas del formulario:
PreparedStatement stmt = connection.prepareStatement("UPDATE alias SET aliases=?::jsonb WHERE vkid=?");
stmt.setString(1, aliases.toJSON());
stmt.setInt(2, vkid);
stmt.execute();
Pero el código tiene dos años, por lo que no recomiendo tomar todos los patrones a partir de ahí. Y en general, no recomendaría hacer esto en absoluto (trabajar a través de JDBC).
Además, personalmente, no me gusta trabajar directamente con Hibernate. Ya tuve la triste experiencia de escribir
DAO
y
HibernateSessionFactoryUtil
(los que escribieron entenderán a qué me refiero).
En cuanto al artículo en sí, traté de ser breve, pero lo suficiente para que con solo este artículo en la mano, puedas comenzar a desarrollar. Aún así, este no es un capítulo de un libro, sino un artículo sobre Habré. Puede estudiar las anotaciones y la reflexión en general más profundamente usted mismo, por ejemplo, creando el mismo bot.
¡Buena suerte a todos! Y no te olvides del código promocional HABR, que otorga un 10% de descuento adicional al indicado en el banner.