
Недавно я опубликовал свою первую статью на Хабре. И первый блин прилетел мне прямо в голову. 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

Я обещал без магии? Итак, по порядку:
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. Там будет о чем рассказать.
Не пишите лишнего, удачи!
