- PVSM.RU - https://www.pvsm.ru -
Недавно я опубликовал свою первую статью [1] на Хабре. И первый блин прилетел мне прямо в голову. 12к просмотров и плюс 4 звезды на гитхабе… Ладно, сам виноват, не надо было заниматься ерундой на уроках русского языка и литературы. Если я правильно понял, то проблема заключалась в том, что я сразу перешел к сути. Вывалил все в лоб. Не познакомился с родителями, так сказать. А что за Jeta [2] такая, как она работает, что происходит за сценой? Магия какая я то [3]… Никому ведь не нужна магия в проектах, так?
"От куда у тебя уверенность, что твоя библиотека вообще кому-то нужна?" спросит среднестатистический хаброчанин [4]. Оттуда, что каждый день, вешая очередную аннотацию или просто смотря на код, я думаю "Боже, это прекрасно!". Кто от такого откажется?
Ладно, давайте сначала и по порядку.
Jeta [2] — фреймворк для генерации исходного кода по аннотациям, построенный на javax.annotation.processing
. Что из себя представляет Annotation Processing можно почитать, например, тут [5] или тут [6]. Если вкратце — это плохо задокументированная технология, доступная с Java 1.5 (но лучше 1.6), которая позволяет пройтись по AST [7] вашего проекта, вашим же процессором, обработать ваши аннотации угодным Вам способом. И сделать все это непосредственно перед компиляцией. На этом построены такие монстры как dagger [8], dagger 2 [9], jeta [2], android annotations [10] и другие. По моему мнению, Java Annotation Processing сильно недооцененная технология, а для таких рефлекшн [11]-фобов как я — так вообще единственный способ пометапрограммировать. Благо, с появлением Android, ситуация начала меняться. Самое время приобщиться к прекрасному!
Вот какие цели я преследовал во время работы над проектом:
FooController
? Напиши свою реализацию! И не забудь поделиться с сообществом, pull request-ы приветствуются!А ведь это еще не всё, "батарейки в комплекте"! Все что надо для комфортной работы — уже написано: Dependecy Injection [12], Event Bus [13], Validators [14] и др [15]. Все в соответствии с принципами описанными выше. А еще, из коробки доступны Collectors [16]. Именно на их примере мы будем разбираться с тем, как устроен фреймворк.
Предположим, в нашем проекте есть обработчик событий. Сейчас не важно, что за события, это могут быть 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? Jeta предлагает collectors [16]! Хендлеры будут автоматически находиться во время компиляции, я гарантирую это!
Подключаем [17] библиотеку (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!
Я обещал без магии? Итак, по порядку:
Тут ничего сложного, в контексте фреймворка, мастер — это класс для которого генерируется метокод. В нашем случает — это CommandProcessor
, т.к. он использует аннотацию @TypeCollector
.
Метакод — сгенерированный (для мастера) класс. Он располагается в том же пакете, где и его мастер (спокойствие, не физически), и имеет составное имя: <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;
}
}
Странное название, знаю. Но говорить каждый раз "Metacode Repository" тоже не хочется.
Metasitory, как не трудно догадаться, хранилище ссылок на метакод. Хотя, на рисунке это и выглядит как DB2, не стоит бояться, по умолчанию это — IdentityHashMap
(впрочем, как упоминалось в начале, вы можете написать реализацию на DB2. Только пожалуйста, без pull request-ов). Если точнее, дефолтная Metasitory реализация — MapMetasitory [18]. Это вы могли заметить в исходном коде 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 описывается запрос к 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.
Последний элемент (и кстати говоря необязательный) — контроллер. Вы обращаетесь к нужному контроллеру, он, с помощью 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
Если на этот раз к библиотеке проявится интерес, следующая статья будет о Jeta Dependency Injection. Там будет о чем рассказать.
Не пишите лишнего [19], удачи!
→ Исходный код примера [20]
→ Официальный сайт [2]
→ Jeta на GitHub [21]
Автор: brooth
Источник [22]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/java/247341
Ссылки в тексте:
[1] первую статью: https://habrahabr.ru/post/317970
[2] Jeta: http://jeta.brooth.org
[3] Магия какая я то: https://projectlombok.org
[4] среднестатистический хаброчанин: https://habrahabr.ru/post/317880
[5] тут: https://habrahabr.ru/company/e-Legion/blog/206208
[6] тут: https://habrahabr.ru/post/200354
[7] AST: https://en.wikipedia.org/wiki/Abstract_syntax_tree
[8] dagger: http://square.github.io/dagger
[9] dagger 2: https://google.github.io/dagger
[10] android annotations: http://androidannotations.org
[11] рефлекшн: https://docs.oracle.com/javase/tutorial/reflect
[12] Dependecy Injection: http://jeta.brooth.org/guide/inject.html
[13] Event Bus: http://jeta.brooth.org/guide/event-bus.html
[14] Validators: http://jeta.brooth.org/guide/validator.html
[15] др: http://jeta.brooth.org/guide.html
[16] Collectors: http://jeta.brooth.org/guide/collector.html
[17] Подключаем: http://jeta.brooth.org/guide/install.html
[18] MapMetasitory: https://github.com/brooth/jeta/blob/master/jeta/src/main/java/org/brooth/jeta/metasitory/MapMetasitory.java
[19] Не пишите лишнего: https://habrahabr.ru/post/317970/
[20] Исходный код примера: https://github.com/brooth/jeta-samples/tree/master/command-handler
[21] Jeta на GitHub: https://github.com/brooth/jeta
[22] Источник: https://habrahabr.ru/post/318020/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best
Нажмите здесь для печати.