A maioria dos programas usa a reflexão de uma forma ou de outra em suas várias formas, porque seus recursos são difíceis de caber em um artigo.
Muitas respostas terminam aí, mas o mais importante é entender o conceito geral de reflexão. Estamos buscando respostas curtas a perguntas para passar com sucesso na entrevista, mas não entendemos o básico - de onde veio e o que exatamente significa reflexão.
Neste artigo, abordaremos todas essas questões em relação às anotações e, com um exemplo ao vivo, veremos como usar, localizar e escrever as suas.

Reflexão
Eu acredito que seria um erro pensar que a reflexão Java está limitada a apenas um pacote na biblioteca padrão. Portanto, proponho considerá-lo como um termo, sem amarrá-lo a um pacote específico.
Reflexão vs introspecção
Junto com a reflexão, existe também o conceito de introspecção. Introspecção é a capacidade de um programa de obter dados sobre o tipo e outras propriedades de um objeto. Por exemplo, este
instanceof
:
if (obj instanceof Cat) {
Cat cat = (Cat) obj;
cat.meow();
}
Esta é uma técnica muito poderosa, sem a qual Java não seria o que é. No entanto, ele não vai além do recebimento de dados, e a reflexão entra em jogo.
Algumas possibilidades de reflexão
Mais especificamente, reflexão é a capacidade de um programa de se examinar em tempo de execução e usá-lo para alterar seu comportamento.
Portanto, o exemplo mostrado acima não é reflexão, mas apenas introspecção do tipo de objeto. Mas o que é, então, reflexão? Por exemplo, criar uma classe ou chamar um método, mas de uma forma muito peculiar. Abaixo está um exemplo.
Vamos imaginar que não temos nenhum conhecimento sobre a classe que queremos criar, mas apenas informações sobre onde ela está localizada. Nesse caso, não podemos criar uma classe da maneira óbvia:
Object obj = new Cat(); // ?
Vamos usar reflexão e criar uma instância da classe:
Object obj = Class.forName("complete.classpath.MyCat").newInstance();
Vamos também chamar seu método por meio de reflexão:
Method m = obj.getClass().getDeclaredMethod("meow");
m.invoke(obj);
Da teoria à prática:
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);
}
}
Você pode brincar com ele no Jdoodle .
Apesar de sua simplicidade, há muitas coisas complexas acontecendo neste código e, frequentemente, o programador carece apenas de um uso simples
getDeclaredMethod and then invoke
.
Questão # 1
Por que, no método invoke no exemplo acima, devemos passar uma instância de objeto?
Não irei mais longe, pois nos distanciaremos do assunto. Em vez disso, deixarei um link para um artigo do colega sênior Tagir Valeev .
Anotações
As anotações são uma parte importante da linguagem Java. É algum tipo de descritor que pode ser colocado em uma classe, campo ou método. Por exemplo, você pode ter visto a anotação
@Override
:
public abstract class Animal {
abstract void doSomething();
}
public class Cat extends Animal {
@Override
public void doSomething() {
System.out.println("Meow");
}
}
Você já se perguntou como funciona? Se você não sabe, antes de continuar a leitura, tente adivinhar.
Tipos de anotações
Considere a anotação acima:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
@Target
- indica a que a anotação se aplica. Neste caso, o método.
@Retention
- o tempo de vida da anotação no código (não em segundos, é claro).
@interface
- é a sintaxe para criar anotações.
Se o primeiro e o último mais ou menos claro (veja.
@Target
Na documentação ), então
@Retention
vamos olhar agora, pois será dividido em vários tipos de anotações, o que é muito importante entender.
Essa anotação pode assumir três valores:
No primeiro caso, a anotação será escrita no bytecode do seu código, mas não deve ser mantida pela máquina virtual em tempo de execução.
No segundo caso, a anotação estará disponível em tempo de execução, graças ao qual poderemos processá-la, por exemplo, obter todas as classes que possuem esta anotação.
No terceiro caso, a anotação será removida pelo compilador (não estará no bytecode). Geralmente, são anotações úteis apenas para o compilador.
Voltando à anotação
@Override
, vemos que sim, o
RetentionPolicy.SOURCE
que geralmente é lógico, visto que é usado apenas pelo compilador. Em tempo de execução, essa anotação realmente não fornece nada de útil.
SuperCat
Vamos tentar adicionar nossa própria anotação (isso será útil para nós durante o desenvolvimento).
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!"); // <---
}
}
}
Vamos ter dois gatos em nossa casa: Tom e Alex. Vamos criar uma anotação para o supergato:
@Target(ElementType.TYPE) //
@Retention(RetentionPolicy.RUNTIME) //
@interface SuperCat {
}
// ...
@SuperCat // <---
private class Alex extends Cat {
@Override
void meow() {
System.out.println("Alex-style meow!");
}
}
// ...
Ao mesmo tempo, vamos deixar Tom como um gato comum (o mundo é injusto). Agora vamos tentar obter as classes que foram anotadas com este elemento. Seria bom ter um método como este na própria classe de anotação:
Set<class<?>> classes = SuperCat.class.getAnnotatedClasses();
Mas, infelizmente, esse método ainda não existe. Então, como encontramos essas classes?
ClassPath
Este é um parâmetro que aponta para classes personalizadas.
Espero que você os conheça e, se não estiver, aprenda a estudar isso, pois é uma das coisas fundamentais.
Assim, tendo descoberto onde nossas classes estão armazenadas, podemos carregá-las através do ClassLoader e verificar as classes para esta anotação. Vamos direto ao 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()
);
}
}
}
}
Eu não recomendo usar isso em seu programa. O código é apenas para fins informativos!
Este exemplo é indicativo, mas usado apenas para fins educacionais devido a isso:
Class<?> repoClass = Class.forName(classNamePath);
Vamos descobrir o porquê mais tarde. Por enquanto, vamos dar uma olhada nas linhas acima:
// ...
//
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 descobrir de onde obtemos esses arquivos, vamos examinar o arquivo JAR que é criado quando executamos o aplicativo:
├───com │ └───apploidxxx │ └───examples │ Cat.class │ Home$Alex.class │ Home$Tom.class │ Home.class │ Main.class │ SuperCat.class
Portanto,
classes
esses são apenas nossos arquivos compilados como bytecode. No entanto,
File
este ainda não é um arquivo baixado, sabemos apenas onde eles estão, mas ainda não podemos ver o que está dentro deles.
Então, vamos carregar cada arquivo:
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);
}
Tudo o que foi feito antes foi apenas chamar este método Class.forName, que carregará a classe que precisamos. Portanto, a parte final é obter todas as anotações usadas no repoClass e verificar se são anotações
@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
E pronto! Agora que temos a própria classe, temos acesso a todos os métodos de reflexão.
Refletindo
Como no exemplo acima, podemos simplesmente criar uma nova instância de nossa classe. Mas antes disso, vamos dar uma olhada em algumas formalidades.
- Primeiro, os gatos precisam viver em algum lugar, então precisam de um lar. No nosso caso, eles não podem existir sem um lar.
- Em segundo lugar, vamos criar uma lista de supercoats.
List<cat> superCats = new ArrayList<>();
final Home home = new Home(); // ,
Portanto, o processamento assume sua forma final:
for (Annotation annotation : annotations) {
if (annotation.annotationType() == SuperCat.class) {
Object obj = repoClass
.getDeclaredConstructor(Home.class)
.newInstance(home);
superCats.add((Cat) obj);
}
}
E novamente o título de perguntas:
Questão # 2
O que acontece se marcarmos uma@SuperCat
classe que não herda deCat
?
Questão # 3
Por que precisamos de um construtor que receba um tipo de argumentoHome
?
Pense por alguns minutos e então analise imediatamente as respostas:
Resposta # 2 : Sim
ClassCastException
, porque a anotação em si
@SuperCat
não garante que a classe marcada com esta anotação herdará ou implementará algo.
Você pode verificar isso removendo
extends Cat
de Alex. Ao mesmo tempo, você verá como as anotações podem ser úteis
@Override
.
Resposta # 3 : Os gatos precisam de um lar porque são classes internas. Tudo dentro da estrutura do capítulo 15.9.3 de Especificação da linguagem Java .
No entanto, você pode evitar isso simplesmente tornando essas classes estáticas. Mas ao trabalhar com reflexão, você frequentemente se deparará com esse tipo de coisa. E você realmente não precisa conhecer a especificação Java para isso. Essas coisas são bastante lógicas e você pode pensar por que devemos passar uma instância da classe pai para o construtor, se for o caso
non-static
.
Vamos resumir e obter: 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!
Então, o que há de errado
Class.forName
?
Ele mesmo faz exatamente o que é exigido dele. No entanto, estamos usando incorretamente.
Imagine que você esteja trabalhando em projetos com 1000 ou mais classes (afinal, escrevemos em Java). E imagine carregar todas as classes que você encontrar em classPath. Você mesmo entende que a memória e outros recursos JVM não são borracha.
Maneiras de trabalhar com anotações
Se não houvesse outra maneira de trabalhar com anotações, usá-las como rótulos de classe, como, por exemplo, no Spring, seria muito, muito controverso.
Mas a primavera parece funcionar. Meu programa é tão lento por causa deles? Infelizmente ou felizmente, não. O Spring funciona bem (nesse aspecto) porque usa uma maneira ligeiramente diferente de trabalhar com eles.
Direto para bytecode
Todos (espero) de alguma forma têm uma ideia do que é um bytecode. Ele armazena todas as informações sobre nossas classes e seus metadados (incluindo anotações).
É hora de lembrar o nosso
RetentionPolicy
. No exemplo anterior, conseguimos encontrar essa anotação porque indicamos que é uma anotação de tempo de execução. Portanto, deve ser armazenado em bytecode.
Então, por que não o lemos (sim, do bytecode)? Mas aqui não implementarei um programa para lê-lo a partir de bytecode, pois ele merece um artigo separado. Porém, você mesmo pode fazer isso - será uma ótima prática que consolidará o material do artigo.
Para se familiarizar com o bytecode, você pode começar com meu artigo... Lá eu descrevo as coisas básicas do bytecode com o Hello World! O artigo será útil mesmo se você não for trabalhar diretamente com bytecode. Descreve os pontos fundamentais que ajudarão a responder à pergunta: por que exatamente?
Depois disso, seja bem-vindo à especificação oficial da JVM . Se você não quiser analisar o bytecode manualmente (por bytes), procure bibliotecas como ASM e Javassist .
Reflexões
Reflections é uma biblioteca com licença WTFPL que permite que você faça o que quiser com ela. Uma biblioteca bastante rápida para vários trabalhos com classpath e metadados. O útil é que ele pode salvar informações sobre alguns dos dados já lidos, o que economiza tempo. Você pode cavar dentro e encontrar a classe 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();
}
}
}
contexto da primavera
Eu recomendaria usar a biblioteca Reflections, pois internamente ela funciona através do javassist, o que indica que está lendo bytecode, não carregando.
No entanto, existem muitas outras bibliotecas que funcionam de maneira semelhante. Existem muitos deles, mas agora quero desmontar apenas um deles - este
spring-context
. Talvez seja melhor do que o primeiro quando você está desenvolvendo um bot no framework Spring. Mas também existem algumas nuances aqui.
Se suas classes são essencialmente beans gerenciados, ou seja, estão em um contêiner Spring, então você não precisa examiná-los novamente. Você pode simplesmente acessar esses grãos do próprio contêiner.
Outra coisa é se você quiser que suas classes marcadas sejam beans, então você pode fazer isso manualmente
ClassPathScanningCandidateComponentProvider
através do ASM.
Novamente, é muito raro que você precise usar esse método, mas vale a pena considerá-lo como uma opção.
Eu escrevi um bot para VK nele. Aqui está um repositório com o qual você pode se familiarizar, mas eu o escrevi há muito tempo, e quando fui procurar inserir um link no artigo, vi que através do VK-Java-SDK recebo mensagens com campos não inicializados, embora tudo tenha funcionado antes.
O engraçado é que eu nem mesmo mudei a versão do SDK, então se você descobrir qual foi o motivo, ficarei grato. No entanto, carregar os comandos em si funciona bem, que é exatamente o que você pode ver se quiser ver um exemplo de como trabalhar
spring-context
.
Os comandos são os seguintes:
@Command(value = "hello", aliases = {"", ""})
public class Hello implements Executable {
public BotResponse execute(Message message) throws Exception {
return BotResponseFactoryUtil.createResponse("hello-hello",
message.peerId);
}
}
SuperCat
Você pode encontrar exemplos de códigos anotados neste repositório .
Aplicação prática de anotações na criação de um bot do Telegram
Tudo isso foi uma introdução bastante longa, mas necessária para trabalhar com anotações. A seguir, implementaremos um bot, mas o objetivo do artigo não é um manual para criá-lo. Esta é uma aplicação prática de anotações. Pode haver qualquer coisa aqui: de aplicativos de console aos mesmos bots para VK, carrinho e outras coisas.
Além disso, aqui algumas verificações complexas não serão executadas deliberadamente. Por exemplo, antes disso, os exemplos não tinham nenhuma verificação de tratamento de erro nulo ou correto, sem mencionar seu log.
Tudo isso é feito para simplificar o código. Portanto, se você pegar o código dos exemplos, não tenha preguiça de modificá-lo, então você irá entendê-lo melhor e personalizá-lo para atender às suas necessidades.
Usaremos a biblioteca TelegramBots com uma licença MITpara trabalhar com a API do telegrama. Você pode usar qualquer outro. Eu escolhi porque ele poderia funcionar tanto "c" (tem uma versão com um starter) ou "sem" bota de mola.
Na verdade, eu também não quero complicar o código adicionando algum tipo de abstração, se você quiser, você pode fazer algo universal, mas pense se vale a pena, portanto, para este artigo, muitas vezes usaremos classes concretas dessas bibliotecas, vinculando nosso código para eles.
Reflexões
O primeiro bot da linha é um bot escrito na biblioteca de reflexões, sem Spring. Não vamos analisar tudo, mas apenas os pontos principais, em particular, estamos interessados no processamento das anotações. Antes de analisá-lo no artigo, você mesmo pode descobrir como ele funciona no meu repositório .
Em todos os exemplos, respeitaremos o fato de que o bot consiste em vários comandos e não carregaremos esses comandos manualmente, mas simplesmente adicionaremos anotações. Aqui está um exemplo de comando:
@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();
}
Nesse caso, o parâmetro
/hello
será gravado
value
na anotação. valor é algo como a anotação padrão. Isso é
@Handler("/hello")
=
@Handler(value = "/hello")
.
Também adicionaremos loggers. Iremos chamá-los antes de processar a solicitação, ou depois, e também combiná-los:
@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());
}
}
Mas também podemos adicionar um parâmetro para acionar o logger para certas mensagens:
@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!");
}
}
Ou acionado após o processamento da solicitação:
@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());
}
}
Ou tanto ali como ali:
@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 fazer isso porque
executionTime
requer uma série de valores. O princípio de operação é simples, então vamos começar a processar estas anotações:
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();
}
}
Na verdade, apenas criamos um mapa com o nome do comando, que tiramos do valor
value
na anotação. O código-fonte está aqui .
Fazemos o mesmo com o Log, mas pode haver vários registradores com os mesmos padrões, então mudamos ligeiramente nossa estrutura de dados:
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());
}
}
Existem vários registradores para cada padrão. O resto é o mesmo.
Agora, no próprio bot, precisamos configurar
executionTime
e redirecionar solicitações para estas classes:
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();
}
}
}
É melhor descobrir o código por si mesmo e olhar no repositório, ou ainda melhor abri-lo por meio do IDE. Este repositório é bom para começar e começar, mas não é bom o suficiente como um bot.
Primeiro, não há abstração suficiente entre as equipes. Ou seja, você só pode retornar de cada comando
SendMessage
. Isso pode ser superado usando um nível mais alto de abstração, por exemplo
BotApiMethodMessage
, mas nem isso realmente resolve todos os problemas.
Em segundo lugar, a própria biblioteca
TelegramBots
, ao que me parece, não está particularmente focada nesse trabalho (arquitetura) do bot. Se você está desenvolvendo um bot usando esta biblioteca particular, você pode usar
Ability Bot
que está listado no wiki da própria biblioteca. Mas eu realmente quero ver uma biblioteca completa com essa arquitetura. Então você pode começar a escrever sua biblioteca!
Bot de primavera
Isso faz mais sentido ao trabalhar com o ecossistema de primavera:
- Trabalhar por meio de anotações não viola o conceito geral do contêiner de mola.
- Não podemos criar comandos nós mesmos, mas obtê-los do contêiner, marcando nossos comandos como beans.
- Obtemos um excelente DI da primavera.
Em geral, o uso de uma mola como estrutura para um bot é um assunto para outra conversa. Afinal, muitos podem pensar que isso é muito difícil para um bot (embora, provavelmente, eles também não escrevam bots em Java).
Mas acho que a primavera é um bom ambiente não apenas para aplicativos corporativos / web. Ele apenas contém muitas bibliotecas oficiais e de usuário para seu ecossistema (na primavera, quero dizer Spring Boot).
E o mais importante, ele permite que você implemente vários padrões de maneiras diferentes fornecidas pelo contêiner.
Implementação
Bem, vamos descer ao próprio bot.
Como escrevemos na pilha do spring, não podemos criar nosso próprio contêiner de comandos, mas usar o existente na primavera. Eles não podem ser digitalizados, mas obtidos do contêiner IoC .
Desenvolvedores mais independentes podem começar a ler o código imediatamente .
Aqui, analisarei apenas comandos de leitura, embora haja alguns pontos interessantes no repositório em si que você pode considerar por conta própria.
A implementação é muito semelhante ao bot através do Reflections, então as anotações são as mesmas.
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; }
Ao contrário do exemplo anterior, este já usa um nível mais alto de abstração para interfaces, o que, claro, é bom. Também não precisamos criar instâncias de comando.
Vamos resumir
Cabe a você decidir o que é melhor para sua tarefa. Analisei três casos de bots praticamente semelhantes:
- Reflexões.
- Contexto da Primavera (sem Primavera).
- ApplicationContext do Spring.
No entanto, posso dar conselhos com base na minha experiência:
- Considere se você precisa da Primavera. Ele fornece um poderoso contêiner IoC e recursos de ecossistema, mas tudo tem um preço. Normalmente penso assim: se você precisa de um banco de dados e um início rápido, precisa do Spring Boot. Se o bot for simples o suficiente, você pode passar sem ele.
- Se você não precisa de dependências complexas, sinta-se à vontade para usar o Reflections.
Implementar, por exemplo, JPA sem Spring Data parece-me uma tarefa bastante demorada, embora você também possa olhar para alternativas na forma de micronauta ou quarkus, mas eu apenas ouvi falar deles e não tenho experiência suficiente para aconselhar algo sobre isso.
Se você é adepto de uma abordagem mais limpa desde o início, mesmo sem JPA, então dê uma olhada neste bot, que funciona por meio de JDBC via VK e Telegram.
Lá você verá muitas entradas do formulário:
PreparedStatement stmt = connection.prepareStatement("UPDATE alias SET aliases=?::jsonb WHERE vkid=?");
stmt.setString(1, aliases.toJSON());
stmt.setInt(2, vkid);
stmt.execute();
Mas o código tem dois anos, então não recomendo pegar todos os padrões de lá. E, em geral, eu não recomendaria fazer isso de forma alguma (trabalhe com JDBC).
Além disso, pessoalmente, não gosto de trabalhar diretamente com o Hibernate. Já tive a triste experiência de escrever
DAO
e
HibernateSessionFactoryUtil
(quem escreveu vai entender o que quero dizer).
Quanto ao artigo em si, tentei ser curto, mas o suficiente para que apenas com este artigo em mãos você possa começar a desenvolver. Mesmo assim, este não é um capítulo de um livro, mas um artigo sobre Habré. Você pode estudar anotações e reflexões em geral mais profundamente, por exemplo, criando o mesmo bot.
Boa sorte a todos! E não se esqueça do código promocional HABR, que dá um desconto adicional de 10% ao indicado no banner.
