📜 ⬆️ ⬇️

How not to write too much. No magic

Go img
Recently I published my first article on Habré. And the first pancake flew right to my head. 12k views and plus 4 stars on the githaba ... Well, I’m guilty myself, I shouldn’t have to do nonsense on the lessons of Russian language and literature. If I understood correctly, the problem was that I immediately got to the bottom. He poured everything in the forehead. Not met with parents, so to speak. And what about Jeta , how does it work, what happens behind the scenes? Magic is what I am ... Nobody needs magic in projects, right?


"From where do you have confidence that someone needs your library at all?" ask the average habrochanin . From there, that every day, hanging up another annotation or just looking at the code, I think, "God, this is great!" . Who will refuse this?


Okay, let's do it first and in order.


Jeta is a framework for generating source code from annotations, built on javax.annotation.processing . What is Annotation Processing can be read, for example, here or here . In short, this is a poorly documented technology, available with Java 1.5 (but better than 1.6), which allows you to walk through the AST of your project, with your processor, to process your annotations in a way that is pleasing to you. And do all this just before compiling. Monsters like dagger , dagger 2 , jeta , android annotations and others are built on this. In my opinion, Java Annotation Processing is undervalued technology, and for reflexes like me, this is the only way to program a programmer. Fortunately, with the advent of Android, the situation began to change. It's time to join the beautiful!


Here are the goals I pursued while working on the project:



But this is not all, "batteries included"! All that is necessary for comfortable work is already written: Dependecy Injection , Event Bus , Validators , etc. All in accordance with the principles described above. And yet, Collectors are available from the box. It is by their example that we will deal with how the framework works.


Spherical cow


Suppose there is an event handler in our project. Now it does not matter what kind of events, it can be push-messages, state-machine states or commands from the user. ABOUT! let's get these commands from the user. Moreover, the topic of writing chat bots is now relevant.


So, we need handlers:


 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); } } 

CPU:


 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(); } } 

Now we need to associate user commands with the appropriate handlers. I know that the fashion for XML has died down, but nevertheless, it is with the help of XML that most programmers solve such problems. Well, XML is so 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> 

parsim!


 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) {...} } 

Run, check!


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

Works great! Let's write something else! We will display the current time using the time command!


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

Run ..


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

Heck! Well, no need to be nervous. Now I’ll quickly add a new handler to handers.xml and restart. Delov then! Same not real Enterprise project which gathers 5 minutes and as much more is started! Well, you understand ...


Jeta in action


And what does Jeta offer us? Jeta offers collectors ! Handlers will be automatically compiled at any time, I guarantee it!


Connect the library ( 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:+' } 

Create a Command annotation and hang it on our handlers:


 @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 {...} 

We finish 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) {...} } 

AND...


 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
I promised without magic? So, in order:


Master


There is nothing complicated in the context of the framework, the master is the class for which the metocode is generated. In our CommandProcessor , this is the CommandProcessor , since it uses the @TypeCollector annotation.


Metacode


Metacode - generated (for the master) class. It is located in the same package as its master (calm, not physically), and has a <Master name> + "_Metacode" name: <Master name> + "_Metacode" . In our example, this is 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


Strange name, I know. But I don’t want to say “Metacode Repository” every time.


Metasitory , as it is not difficult to guess, is a repository of metacode references. Although, in the figure it looks like DB2 , you should not be afraid, by default it is IdentityHashMap (however, as mentioned at the beginning, you can write a implementation on DB2 . Only please, without pull request-s). More specifically, the default Metasitory implementation is MapMetasitory . This you might have noticed in the source code of CommandProcessor . MapMetasitory uses the so-called MetasitoryContainers , which, like Metacode , are generated automatically during compilation. And here they already store contexts with metacode in 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; } } 

The context consists of three fields: Wizard class, Metacode Provider (creates meta- code instances) and a list of annotations used. This set is enough to search by Criteria :


Criteria


Everything is clear, with the help of Criteria , a request to Metasitory is described . The current version (2.3) supports searching by the following criteria:



In our example, masterEq is enough - because we are only interested in CommandProcessor_Metacode .


Controller


The last element (and by the way optional) is the controller. You access the controller you need, using Criteria , it requests the appropriate Metacode from Metasitory and "pulls" the necessary methods. Perhaps he does some other transformations or checks, it all depends on the implementation. In our example, we used TypeCollectorController (also featured in the CommandProcessor -a source code):


 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 say


PS


If this time there is interest in the library, the next article will be about Jeta Dependency Injection . There will be something to tell.


Do not write too much , good luck!


Sample source code
Official website
Jeta on GitHub


')

Source: https://habr.com/ru/post/318020/


All Articles