Как не писать лишнего. Без магии

    img
    Недавно я опубликовал свою первую статью на Хабре. И первый блин прилетел мне прямо в голову. 12к просмотров и плюс 4 звезды на гитхабе… Ладно, сам виноват, не надо было заниматься ерундой на уроках русского языка и литературы. Если я правильно понял, то проблема заключалась в том, что я сразу перешел к сути. Вывалил все в лоб. Не познакомился с родителями, так сказать. А что за Jeta такая, как она работает, что происходит за сценой? Магия какая я то… Никому ведь не нужна магия в проектах, так?


    "От куда у тебя уверенность, что твоя библиотека вообще кому-то нужна?" спросит среднестатистический хаброчанин. Оттуда, что каждый день, вешая очередную аннотацию или просто смотря на код, я думаю "Боже, это прекрасно!". Кто от такого откажется?


    Ладно, давайте сначала и по порядку.


    Jeta — фреймворк для генерации исходного кода по аннотациям, построенный на javax.annotation.processing. Что из себя представляет Annotation Processing можно почитать, например, тут или тут. Если вкратце — это плохо задокументированная технология, доступная с Java 1.5 (но лучше 1.6), которая позволяет пройтись по AST вашего проекта, вашим же процессором, обработать ваши аннотации угодным Вам способом. И сделать все это непосредственно перед компиляцией. На этом построены такие монстры как dagger, dagger 2, jeta, android annotations и другие. По моему мнению, Java Annotation Processing сильно недооцененная технология, а для таких рефлекшн-фобов как я — так вообще единственный способ пометапрограммировать. Благо, с появлением Android, ситуация начала меняться. Самое время приобщиться к прекрасному!


    Вот какие цели я преследовал во время работы над проектом:


    • Удобство написания кастомных annotation-процессоров.
    • Нахождение ошибок во время компиляции (по возможности).
    • Каждый компонент фреймворка должен быть заменяем. Не нравится как работает FooController? Напиши свою реализацию! И не забудь поделиться с сообществом, pull request-ы приветствуются!
    • Скорость работы. Хотелось минимизировать overhead насколько это возможно — тем самым лично забить гвоздь в голову крышку гроба Java Reflection.

    А ведь это еще не всё, "батарейки в комплекте"! Все что надо для комфортной работы — уже написано: Dependecy Injection, Event Bus, Validators и др. Все в соответствии с принципами описанными выше. А еще, из коробки доступны Collectors. Именно на их примере мы будем разбираться с тем, как устроен фреймворк.


    Spherical cow


    Предположим, в нашем проекте есть обработчик событий. Сейчас не важно, что за события, это могут быть push-сообщения, состояния state-machine-ы или команды от пользователя. О! а давайте это будут команды от пользователя. Тем более, что тема написания чат-ботов сейчас актуальна.


    Итак, нам нужны обработчики:


    public interface CommandHandler {
        void handle();
    }
    
    public class GreetingCommand implements CommandHandler {
        @Override
        public void handle() {
            System.out.print("Hi! How are you?");
        }
    }
    
    public class ExitCommand implements CommandHandler {
        @Override
        public void handle() {
            System.out.print("Bye!");
            System.exit(0);
        }
    }

    Процессор:


    public class CommandProcessor {
       private Map<String, CommandHandler> handlers = new HashMap<>();
    
       public void loop() {
            System.out.println("Input command. Type 'exit' to finish.");
            Scanner input = new Scanner(System.in);
            while (true) {
                String command = input.next();
                CommandHandler handler = handlers.get(command);
                if(handler == null)
                    System.out.println("Unknown command '" + command + "'. Try again");
                else
                    handler.handle();
            }
        }
    
        public static void main(String[] args) {
            new CommandProcessor().loop();
        }
    }

    Теперь нам нужно связать команды пользователя с соответствующими обработчиками. Я знаю, что мода на XML поутихла, но тем не менее, именно с помощью XML большинство программистов решают подобные задачи. Что ж, XML так XML..


    <?xml version="1.0" encoding="utf-8" ?>
    <handlers>
        <handler command="greet">org.brooth.jeta.samples.command_handler.commands.GreetingCommand</hanler>
        <handler command="exit">org.brooth.jeta.samples.command_handler.commands.ExitCommand</hanler>
    </handlers>

    парсим!


    public class CommandProcessor {
        private Map<String, CommandHandler> handlers = new HashMap<>();
    
        public CommandProcessor() {
            parseHandlers();
        }
    
        private void parseHandlers() {
            try {
                DocumentBuilder documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
                Document document = documentBuilder.parse("handlers.xml");
                NodeList nodes = document.getDocumentElement().getElementsByTagName("handler");
                for (int i = 0; i < nodes.getLength(); i++) {
                    Node node = nodes.item(i);
                    handlers.put(node.getAttributes().getNamedItem("command").getTextContent(),
                            (CommandHandler) Class.forName(node.getTextContent()).newInstance());
                }
            } catch (Exception e) {
                throw new RuntimeException("Failed to parse handlers.xml", e);
            }
        }
    
        public void loop() {...}
        public static void main(String[] args) {...}
    }

    Запускаем, проверяем!


    Input command. Type 'exit' to finish.
    greet
    Hi! How are you?
    fine!
    Unknown command 'fine!'. Try again
    exit
    Bye!

    Работает, отлично! Давайте еще что-нибудь напишем! Будем выводить текущее время с помощью команды time!


    public class TimeCommand implements CommandHandler {
        @Override
        public void handle() {
            System.out.println("It's " + new SimpleDateFormat("HH:mm").format(new Date()));
        }
    }

    Запускаем..


    Input command. Type 'exit' to finish.
    time
    Unknown command 'time'. Try again

    Чёрт! Ладно, нет необходимости нервничать. Сейчас быстро добавлю новый хендлер в handers.xml и перезапущу. Делов то! Это же не реальный Enterprise проект, который собирается 5 минут и еще столько же запускается! Ну вы поняли...


    Jeta in action


    И что нам предлагает Jeta? Jeta предлагает collectors! Хендлеры будут автоматически находиться во время компиляции, я гарантирую это!


    Подключаем библиотеку (build.gradle):


    buildscript {
        repositories {
            maven {
                url 'https://plugins.gradle.org/m2/'
            }
        }
        dependencies {
            classpath 'net.ltgt.gradle:gradle-apt-plugin:0.9'
        }
    }
    
    apply plugin: 'java'
    apply plugin: 'net.ltgt.apt'
    
    repositories {
        jcenter()
    }
    
    dependencies {
        apt 'org.brooth.jeta:jeta-apt:+'
        compile 'org.brooth.jeta:jeta:+'
    }

    Создаем аннотацию Command и вешаем на наши хендлеры:


    @Target(TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Command {
        String value();
    }
    
    @Command("exit")
    public class ExitCommand implements CommandHandler {...}
    
    @Command("greet")
    public class GreetingCommand implements CommandHandler {...}
    
    @Command("time")
    public class TimeCommand implements CommandHandler {...}

    Дорабатываем CommandProcessor:


    @TypeCollector(Command.class)
    public class CommandProcessor {
        private Map<String, CommandHandler> handlers = new HashMap<>();
    
        public CommandProcessor() {
            //parseHandlers();
            collectHandlers();
        }
    
        private void collectHandlers() {
            Metasitory metasitory = new MapMetasitory("");
            List<Class<?>> types = new TypeCollectorController(metasitory, getClass()).getTypes(Command.class);
            for (Class handlerClass : types) {
                try {
                    Command command = (Command) handlerClass.getAnnotation(Command.class);
                    handlers.put(command.value(), (CommandHandler) handlerClass.newInstance());
                } catch (Exception e) {
                    throw new RuntimeException("Failed to collect handlers", e);
                }
            }
        }
    
        private void parseHandlers() {...}
        public void loop() {...}
        public static void main(String[] args) {...}
    }

    И...


    Input command. Type 'exit' to finish.
    greet
    Hi! How are you?
    fine
    Unknown command 'fine'. Try again
    time
    It's 16:28
    exit
    Bye!


    Behind the scenes


    runtime
    Я обещал без магии? Итак, по порядку:


    Master


    Тут ничего сложного, в контексте фреймворка, мастер — это класс для которого генерируется метокод. В нашем случает — это CommandProcessor, т.к. он использует аннотацию @TypeCollector.


    Metacode


    Метакод — сгенерированный (для мастера) класс. Он располагается в том же пакете, где и его мастер (спокойствие, не физически), и имеет составное имя: <Master name> + "_Metacode". В нашем примере это CommandProcessor_Metacode:


    public class CommandProcessor_Metacode implements Metacode<CommandProcessor>, TypeCollectorMetacode {
        @Override
        public Class<CommandProcessor> getMasterClass() {
            return CommandProcessor.class;
        }
    
        @Override
        public List<Class<?>> getTypeCollection(Class<? extends Annotation> annotation) {
            if(annotation == org.brooth.jeta.samples.command_handler.Command.class) {
                List<Class<?>> result = new ArrayList<Class<?>>(3);
                result.add(org.brooth.jeta.samples.command_handler.commands.ExitCommand.class);
                result.add(org.brooth.jeta.samples.command_handler.commands.TimeCommand.class);
                result.add(org.brooth.jeta.samples.command_handler.commands.GreetingCommand.class);
                return result;
            }
            return null;
        }
    }

    Metasitory


    Странное название, знаю. Но говорить каждый раз "Metacode Repository" тоже не хочется.


    Metasitory, как не трудно догадаться, хранилище ссылок на метакод. Хотя, на рисунке это и выглядит как DB2, не стоит бояться, по умолчанию это — IdentityHashMap (впрочем, как упоминалось в начале, вы можете написать реализацию на DB2. Только пожалуйста, без pull request-ов). Если точнее, дефолтная Metasitory реализация — MapMetasitory. Это вы могли заметить в исходном коде CommandProcessor-а. MapMetasitory использует так называемые MetasitoryContainer-ы, которые, как и Metacode, генерируются автоматически во время компиляции. А вот они уже хранят контексты с метакодом в IdentityHashMap:


    public class MetasitoryContainer implements MapMetasitoryContainer {
      @Override
      public Map<Class<?>, MapMetasitoryContainer.Context> get() {
        Map<Class<?>, MapMetasitoryContainer.Context> result = new IdentityHashMap<>();
        result.put(org.brooth.jeta.samples.command_handler.CommandProcessor.class,
            new MapMetasitoryContainer.Context(
                org.brooth.jeta.samples.command_handler.CommandProcessor.class,
                new org.brooth.jeta.Provider<org.brooth.jeta.samples.command_handler.CommandProcessor_Metacode>() {
                    public org.brooth.jeta.samples.command_handler.CommandProcessor_Metacode get() {
                        return new org.brooth.jeta.samples.command_handler.CommandProcessor_Metacode();
                }},
                new Class[] {
                    org.brooth.jeta.collector.TypeCollector.class
                }));
        return result;
      }
    }

    Контекст состоит из трех полей: Класс мастера, Metacode Provider (создает экземпляры метакода) и список используемых аннотаций. Такого набора достаточно для поиска по Criteria:


    Criteria


    Тут все понятно, с помощью Criteria описывается запрос к Metasitory. В текущей версии (2.3) поддерживается поиск по следующим критериям:


    • masterEq(Class<?> masterClass) — поиск метакода по его мастеру (а класс мастера является ключом IdentityHashMap, т.е. быстро).
    • masterEqDeep(Class<?> masterClass) — поиск метакода не только для мастера но и для его потомков (вызвали один раз в базовом классе и забиыли).
    • usesAny(Set<Class<? extends Annotation>> annotationList) — мастер использует любую аннотацию из списка.
    • usesAll(Set<Class<? extends Annotation>> annotationList) — мастер использует все аннотации из списка.

    В нашем примере достаточно masterEq — т.к. нам интересен только CommandProcessor_Metacode.


    Controller


    Последний элемент (и кстати говоря необязательный) — контроллер. Вы обращаетесь к нужному контроллеру, он, с помощью Criteria, запрашивает у Metasitory соответствующий Metacode и "дергает" необходимые методы. Возможно делает еще какие-нибудь преобразования или проверки, все зависит от реализации. В нашем примере мы использовали TypeCollectorController (также фигурирует в исходном коде CommandProcessor-а):


    public class TypeCollectorController {
        private Collection<Metacode<?>> metacodes;
    
        public TypeCollectorController(Metasitory metasitory, Class<?> masterClass) {
            metacodes = metasitory.search(new Criteria.Builder()
                    .masterEq(masterClass)
                    .build());
        }
    
        public List<Class<?>> getTypes(Class<? extends Annotation> annotation) {
            assert annotation != null;
            if (metacodes.isEmpty())
                return null;
            return ((TypeCollectorMetacode) metacodes.iterator().next()).getTypeCollection(annotation);
        }
    }

    Nuff Said


    P.S.


    Если на этот раз к библиотеке проявится интерес, следующая статья будет о Jeta Dependency Injection. Там будет о чем рассказать.


    Не пишите лишнего, удачи!


    Исходный код примера
    Официальный сайт
    Jeta на GitHub

    • +11
    • 10.7k
    • 8
    Share post

    Similar posts

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 8

      –3
      Я правильно понял что вместо того чтобы использовать Map<String, CommandHandler> вы изобрели свой велосипед?
        0
        Думаю, что интерес не появится.
        Лично я пишу, что-то подобное ради удовольствия, когда немного хочу отвлечься от основной работы.

        Обычно среди «библиотек» есть 2 типа плохих методов: слишком сложные (чтобы в них разобраться) или сильно простые (которые проще написать самому). У вас второй случай.

        Когда-то, лет 10 назад я написал свой набор методов на javascript, и думал, что теперь весь мир изменится. Я показывал их преподавателям, друзьям, сокурсникам со словами «вот, берите и используйте». И знаете что? Эта библиотека нафиг никому не была нужна.

        Вы сильно переоценили свой «фреймворк». Эпитеты типа «никакой Магии» явно не для такого уровня статей. Посмотрите на свой творение более самокритично.

        То, как организовали классы (архитектуру, концепцию) — это удобно вам, но может быть не удобно другим (мы все думаем немного по-разному). И одно дело, когда перед тобой признанный фреймворк, который знают все и под который подстроены другие фреймворки — это одно, здесь можно и потерпеть. И совсем другое дело, это ваша библиотека.
          +1
          Вы сильно переоценили свой «фреймворк». Эпитеты типа «никакой Магии» явно не для такого уровня статей. Посмотрите на свой творение более самокритично.

          Соглашусь с автором, такой подход (annotation processors) — это вполне нормальный вариант кодогенерации. И сказать "никакой магии" вполне нормально. Если хочется посмотреть, что такой с магией — загляните в kryo или что-нибудь ещё из использующих библиотеку asm и генерирующих байткод в рантайме (и да, here be dragons).


          Вероятно, вы просто неподходящая аудитория для статей такого плана. Мне такие статьи на хабре куда более импонируют, чем тонны рассказов "как запустить mvn" и "как вписать свою первую зависимость в pom.xml" (с крайне глубокомысленными выводами типа не пишите addiction вместо dependency).

            0

            Спасибо вам, правда. Я бы и рад критики, помогла бы подумать, может что-то поменять, доработать, улучшить. Но вот читаю такие комментарии и думаю — эти люди вообще не читали статью? или может читали как то поверхностно? или я просто суть не могу донести? Но пока мне ваша версия с неподходящей аудиторией нравится больше)

          0

          Пока не заглянул в dependency:tree думал вам написать о разделении рантайм части и annotation processor'а.


          Почему вы, кстати, не опубликовали его в maven central, но только в jcenter? Bintray, вроде, позволяет публиковать и в central (по крайней мере, они рекламируют эту возможность).


          Также неплохо было бы добавить в документацию про использование с maven'ом:


          <dependency>
            <groupId>org.brooth.jeta</groupId>
            <artifactId>jeta</artifactId>
            <version>2.3</version>
          </dependency>
          <dependency>
            <groupId>org.brooth.jeta</groupId>
            <artifactId>jeta-apt</artifactId>
            <version>2.3</version>
            <scope>provided</scope>
          </dependency>

          У maven'а аудитория будет пошире (в том числе среди тех, кто им не пользуется, но понимает).


          В общем, выглядит интересно. Может попробую для плагинной архитектуры в одном из проектов.

            0

            С maven какая то странная бюрократия — нужно создать тикет в jira, описать библиотеку, они подумают, примут. Не то чтобы для меня это критично, просто сейчас Jeta развивается только на моих собственных проектах и jcenter меня пока устраивает. Если будет интерес со стороны сообщества и потребность в maven, тогда я заморочусь.

            0
            Не увидел не в прошлом посте, ни в этом, ни на сайте ответа: Jeta умеет как lombok генерировать «геттеры/сеттеры» из коробки?
              0
              Не совсем понятно зачем так все усложнять?
              Я подобное делал с помощью спринга.
              Помечаем Ваши команды не аннотацией Command, а
              @Component("greet")
              public class GreetingCommand implements CommandHandler {
              
                  @Override
                  public void handle() {
                      System.out.print("Hi! How are you?");
                  }
              }
              
              и инжектим все это куда-нибудь.
              public class CommandProcessor {
              
                  @Autowired
                  private Map<String, CommandHandler> handlers = new HashMap<>();
              }
              

              Можно и новую аннотацию сделать. Вариантов куча.
              В своих проектах использую lombok — рад, как слон.

              Only users with full accounts can post comments. Log in, please.