![]() |
![]() |
![]() |
|||||
![]() |
Практическое применение аннотации в Java на примере создания Telegram-бота |
||||||
МЕНЮ Искусственный интеллект Поиск Регистрация на сайте Помощь проекту ТЕМЫ Новости ИИ Искусственный интеллект Разработка ИИГолосовой помощник Городские сумасшедшие ИИ в медицине ИИ проекты Искусственные нейросети Слежка за людьми Угроза ИИ ИИ теория Внедрение ИИКомпьютерные науки Машинное обуч. (Ошибки) Машинное обучение Машинный перевод Нейронные сети начинающим Реализация ИИ Реализация нейросетей Создание беспилотных авто Трезво про ИИ Философия ИИ Big data Работа разума и сознаниеМодель мозгаРобототехника, БПЛАТрансгуманизмОбработка текстаТеория эволюцииДополненная реальностьЖелезоКиберугрозыНаучный мирИТ индустрияРазработка ПОТеория информацииМатематикаЦифровая экономика
Генетические алгоритмы Капсульные нейросети Основы нейронных сетей Распознавание лиц Распознавание образов Распознавание речи Техническое зрение Чат-боты Авторизация |
2020-12-20 04:34 Рефлексия в Java — это специальное API из стандартной библиотеки, которая позволяет получить доступ к информации о программе во время выполнения. Большинство программ так или иначе пользуются рефлексией в различных его видах, ведь его возможности трудно уместить в одной статье. Многие ответы заканчиваются на этом, но что более важно, это понимание вообще концепции рефлексии. Мы гонимся за короткими ответами на вопросы, чтобы успешно пройти собеседование, но не понимаем основы — откуда это взялось и что именно понимать под рефлексией. В этой статье мы коснемся всех этих вопросов применительно к аннотациям и на живом примере увидим как использовать, находить и писать свою. ![]() Рефлексия Я считаю, что ошибочно будет думать, что рефлексия в Java ограничивается лишь каким-то пакетом в стандартной библиотеке. Поэтому предлагаю рассмотреть его как термин, не привязывая конкретному пакету. Reflection vs Introspection Наряду с рефлексией также есть понятие интроспекции. Интроспекция — это способность программы получить данные о типе и других свойствах объекта. Например, это if (obj instanceof Cat) { Cat cat = (Cat) obj; cat.meow(); } Это очень сильный метод, без чего Java не была бы такой, какая она есть. Тем не менее дальше получения данных он не уходит, и в дело вступает рефлексия. Некоторые возможности рефлексии Если говорить конкретнее, то рефлексия — это возможность программы исследовать себя во время выполнения и с помощью неё изменять своё поведение. Поэтому пример, показанный выше, является не рефлексией, а лишь интроспекцией типа объекта. Но что же тогда является рефлексией? Например, создание класса или вызов метода, но весьма своеобразным способом. Ниже приведу пример. Представим, что у нас нет никаких знаний о классе, который мы хотим создать, а лишь информация, где он находится. В таком случае мы не можем создать класс очевидным путём: Object obj = new Cat(); // а куда кошка пропала? Воспользуемся рефлексией и создадим экземпляр класса: Object obj = Class.forName("complete.classpath.MyCat").newInstance(); Давайте также через рефлексию вызовем его метод: Method m = obj.getClass().getDeclaredMethod("meow"); m.invoke(obj); От теории к практике: 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); } } Поиграть с ним можно в Jdoodle.Несмотря на простоту, в этом коде происходит довольно много сложных вещей, и зачастую программисту не хватает лишь просто использования Вопрос #1Почему в invoke методе в примере сверху мы должны передавать экземпляр объекта? Далее углубляться я не буду, так как мы уйдём далеко от темы. Вместо этого я оставлю ссылку на статью старшего коллеги Тагира Валеева. Аннотации Важной частью языка Java являются аннотации. Это некоторый дескриптор, который можно повесить на класс, поле или метод. Например, вы могли видеть аннотацию public abstract class Animal { abstract void doSomething(); } public class Cat extends Animal { @Override public void doSomething() { System.out.println("Meow"); } } Задумывались ли вы, как оно работает? Если не знаете, то, прежде чем читать дальше, попробуйте догадаться. Типы аннотаций Рассмотрим вышеприведённую аннотацию: @Target(ElementType.METHOD) @Retention(RetentionPolicy.SOURCE) public @interface Override { }
Если с первым и последним все более менее понятно (подробнее см. Эта аннотация может принимать три значения:
В первом случае аннотация запишется в байт-код вашего кода, но не должна сохраняться виртуальной машиной во время выполнения. Во втором случае аннотация будет доступна и во время выполнения, благодаря чему мы сможем её обработать, например получить все классы, которые имеют данную аннотацию. В третьем случае аннотация будет удалена компилятором (её не будет в байт-коде). Обычно это бывают аннотации, которые полезны только для компилятора. Возвращаясь к аннотации SuperCat Попробуем добавить свою аннотацию (это здорово нам пригодится во время разработки). 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!"); // <--- } } } Пусть у нас в доме будет два котика: Том и Алекс. Создадим аннотацию для суперкотика: @Target(ElementType.TYPE) // чтобы использовать для класса @Retention(RetentionPolicy.RUNTIME) // хотим чтобы наша аннотация дожила до рантайма @interface SuperCat { } // ... @SuperCat // <--- private class Alex extends Cat { @Override void meow() { System.out.println("Alex-style meow!"); } } // ... При этом Тома мы оставим обычным котом (мир несправедлив). Теперь попробуем получить классы, которые были аннотированы данным элементом. Было бы неплохо иметь такой метод у самого класса аннотации: Set<class<?>> classes = SuperCat.class.getAnnotatedClasses(); Но, к сожалению, такого пока метода нет. Тогда как нам найти эти классы? ClassPath Это параметр, который указывает на пользовательские классы. Надеюсь, вы с ними знакомы, а если нет, то спешите изучить это, так как это одна из фундаментальных вещей. Итак, узнав, где хранятся наши классы, мы сможем их загрузить через ClassLoader и проверить классы на наличие данной аннотации. Сразу приступим к коду: 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() ); } } } } Не советую использовать это в вашей программе. Код приведён только для ознакомительных целей! Этот пример показателен, но используется только для учебных целей из-за этого: Class<?> repoClass = Class.forName(classNamePath); Дальше мы узнаем, почему. А пока разберём по строчкам весь код сверху: // ... // пакет в котором мы сейчас находимся 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(); // ... Чтобы разобраться, откуда мы берём эти файлы, рассмотрим JAR-архив, который создаётся, когда мы запускаем приложение: ????com ? ????apploidxxx ? ????examples ? Cat.class ? Home$Alex.class ? Home$Tom.class ? Home.class ? Main.class ? SuperCat.class Таким образом, Поэтому загрузим каждый файл: 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); } Всё, что сделано ранее, было только для того, чтобы вызвать этот метод Class.forName, который загрузит необходимый нам класс. Итак, финальная часть — это получение всех аннотаций, использованных на класс repoClass, а затем проверка, являются ли они аннотацией 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 И готово! Теперь, когда у нас есть сам класс, то мы получаем доступ ко всем методам рефлексии. Рефлексируем Как и в примере сверху, мы можем просто создать новый экземпляр нашего класса. Но перед этим разберём несколько формальностей.
List<cat> superCats = new ArrayList<>(); final Home home = new Home(); // дом, где будут жить наши котики Итак, обработка обретает финальную форму: for (Annotation annotation : annotations) { if (annotation.annotationType() == SuperCat.class) { Object obj = repoClass .getDeclaredConstructor(Home.class) .newInstance(home); superCats.add((Cat) obj); } } И снова рубрика вопросов: Вопрос #2Что будет, если мы пометим класс, который не наследуется от ?Почему нам нужен конструктор, который принимает тип аргумента ?
Вопрос #3
Подумайте пару минут, а затем сразу разберём ответы: Ответ #2: Будет Вы можете проверить это, убрав Ответ #3: Кошкам нужен дом, потому что они являются внутренними классами. Всё в рамках спецификации The Java Language Specification глава 15.9.3. Тем не менее вы можете избежать этого, просто сделав эти классы статическими. Но при работе с рефлексией вы часто будете сталкиваться с такого рода вещами. И вам на самом деле не нужно для этого досконально знать спецификацию Java. Эти вещи достаточно логичны, и можно додуматься самому, почему мы должны передавать в конструктор экземпляр родительского класса, если он Подведём итоги и получим: 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! Так что не так с Сам он как раз-таки делает всё, что от него нужно. Тем не менее мы его используем неправильно. Представьте себе, что вы работаете над проектов в котором 1000 и больше классов (всё-таки на Java пишем). И представьте, что вы будете загружать каждый класс, который найдёте в classPath. Сами понимаете, что память и остальные ресурсы JVM не резиновые. Способы работы с аннотациями Если бы не было другого способа работать с аннотациями, то использование их в качестве меток класса, как, например, в Spring, было бы весьма и весьма спорным. Прямо в байт-код Все (надеюсь) так или иначе имеют представление, что такое байт-код. В нём хранится вся информация о наших классах и их метаданных (в том числе аннотаций). Reflections Reflections — библиотека с WTFPL лицензией, которая позволяет делать с ней всё, что вы захотите. Довольно быстрая библиотека для различной работы с classpath и метаданными. Полезным является то, что она может сохранять информацию о уже некоторых прочитанных данных, что позволяет сэкономить время. Можете покопаться внутри и найти класс 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(); } } } spring-context Я бы рекомендовал использовать библиотеку Reflections, так как внутри она работает через javassist, что свидетельствует о том, что используется чтение байт-кода, а не его загрузка. @Command(value = "hello", aliases = {"привет", "йоу"}) public class Hello implements Executable { public BotResponse execute(Message message) throws Exception { return BotResponseFactoryUtil.createResponse("hello-hello", message.peerId); } } Примеры кода с аннотацией Практическое применение аннотаций в создании Телеграм-бота Всё это было довольно длинным, но необходимым вступлением для работы с аннотациями. Далее, мы будем реализовывать бота, но цель статьи — это не мануал к его созданию. Это практическое применение аннотаций. Здесь могли быть что угодно: от консольных приложений до этих же самых ботов для вк, телеги и прочего. Reflections Первый бот на очереди — это бот, написанный на библиотеке reflections, без Spring. Будем разбирать не всё подряд, а лишь основные моменты, в особенности нас интересует обработка аннотаций. До разбора в статье вы сами можете разобраться в его работе в моём репозитории.Во всех примерах будем придерживаться того, что бот состоит из нескольких команд, причём эти команды мы не будем загружать вручную, а просто будем добавлять аннотации. Вот пример команды: @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(); } В этом случае параметр @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()); } } Но также мы можем добавить параметр, чтобы логгер срабатывал при определённых сообщениях: @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!"); } } Или срабатывал после обработки запроса: @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()); } } Или и там, и там: @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"); } } Мы можем делать такое, так как 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(); } } По сути, мы просто создаём мапу с именем команды, которую берём из значения 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()); } } На каждый паттерн приходится несколько логгеров. Остальное всё так же. 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(); } } } Лучше всего узнать код самому и посмотреть в репозитории, а ещё лучше открыть его через IDE. Этот репозиторий подходит для начала работы и ознакомления, но в качестве бота он недостаточно хорош. Спринговый бот Это приобретает больше смысла при работе с экосистемой спринга:
Вообще использование спринга в качестве каркаса для бота — это тема отдельного разговора. Ведь многие могут подумать, что это слишком тяжело для бота (хотя, скорее всего, они и на 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; } В отличие от прошлого примера здесь уже используется более высокий уровень абстракции для интерфейсов, что, конечно же, хорошо. Также нам не нужно самим создавать экземпляры команд. Подведём итоги Только вам решать, что лучше подойдёт под вашу задачу. Я разобрал условно три случая для примерно похожих ботов:
Тем не менее я могу дать вам совет, основываясь на своём опыте:
Реализация, например, JPA без Spring Data мне кажется довольно трудоёмкой задачей, хотя вы также можете посмотреть на альтернативы в виде micronaut или quarkus, но о них я только наслышан и не имею достаточного опыта, чтобы что-то советовать относительно этого. PreparedStatement stmt = connection.prepareStatement("UPDATE alias SET aliases=?::jsonb WHERE vkid=?"); stmt.setString(1, aliases.toJSON()); stmt.setInt(2, vkid); stmt.execute(); Но код имеет двухгодичную давность, так что не советую оттуда брать все паттерны. И вообще я бы не рекомендовал таким заниматься вовсе (работу через JDBC). Источник: habr.com Комментарии: |
||||||