Недавно я опубликовал свою первую статью на Хабре. И первый блин прилетел мне прямо в голову. 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. Там будет о чем рассказать.
Не пишите лишнего, удачи!
→ Исходный код примера
→ Официальный сайт
→ Jeta на GitHub
Автор: brooth