📜 ⬆️ ⬇️

How Ebean made friends with Gradle and made peace with IntelliJ Idea

I finally matured to start my web project. Another todo-manager who aggregates tasks from the sources I need. It was planned as a project for the soul, so clean and correct. No compromise in architecture and technology. Only best-practices, only hardcore. And, of course, all this is going to push a button in your favorite Intellij IDEA.

After 7 years of Java, the last two mixed with Scala, I wanted to try Groovy. For assembly, of course, Gradle is popular and convenient. The rails seemed too "hackneyed", so I decided to use Spring for the web, and moreover, using the modern, through the Spring Boot . And everything was just wonderful, only with ORM did not work out. At work, we sawed out Hibernate, the customer personally disliked (do not laugh, and it happens - a separate story) and replaced with his bicycle. Negative experience and unwillingness to pull a monster for the sake of a couple of entities made their own - there is no hard way for Hiberneyt! I wanted to try something completely different. By chance, stumbled upon Ebean , which was chosen.

After the final stack was collected, work began to boil. But here's a bad luck, the wagon with the functional has not yet moved from its place. Under the cut sincere excuse why.

Ebean


This opensource ORM framework, in my opinion, was created by the heaters of the classic JPA implementations, just to please the hipsters. Of the key features:

That is all that is needed for a regular, non-enterprise web application. The authors of the framework did a really great job. I think it was not for nothing that he was added as one of the regular ORM in Play! First, it scares that the site is somehow poorly updated, and the development has stopped. But no, GitHub has a very fresh version of avaje-ebeanorm 4.x. And thanks to Play's community and popularity, the project continues to evolve. As proof of activity on GitHub:
')


Here are some examples of what basic requests look like in Ebean:

// find an order by its id Order order = Ebean.find(Order.class, 12); // find all the orders List<Order> list = Ebean.find(Order.class).findList(); // find all the orders shipped since a week ago List<Order> list = Ebean.find(Order.class) .where() .eq("status", Order.Status.SHIPPED) .gt("shipDate", lastWeek) .findList(); 

Creating, saving and updating entities:
 Order order = new Order(); order.setOrderDate(new Date()); ... // insert the order Ebean.save(order); //If the bean was fetched it will be updated Order order = Ebean.find(Order.class, 12); order.setStatus("SHIPPED"); ... // update the order Ebean.save(order); 

Work with partial objects:
 // find order 12 // ... fetching the order id, orderDate and version property // .... nb: the Id and version property are always fetched Order order = Ebean.find(Order.class) .select("orderDate") .where().idEq(12) .findUnique(); // shipDate is not in the partially populated order // ... so it will lazy load all the missing properties Date shipDate = order.getShipDate(); // similarly if we where to set the shipDate // ... that would also trigger a lazy load order.setShipDate(new Date()); 

Inspired by the examples, the final decision was made to use the latest, actively supported version 4.1.8. Combat mood. Added a dependency to build.gradle:

 compile('org.avaje.ebeanorm:avaje-ebeanorm:4.1.8') 

Created configuration:

 @Configuration @PropertySource("config/db.properties") class EbeanConfig { @Autowired DbConfig dbConfig @Bean EbeanServer ebeanServer() { EbeanServerFactory.create(serverConfig()) } ServerConfig serverConfig() { new ServerConfig( dataSourceConfig: dataSourceConfig(), name: "main", ddlGenerate: false, ddlRun: false, defaultServer: true, register: true, namingConvention: new MatchingNamingConvention() ) } DataSourceConfig dataSourceConfig() { new DataSourceConfig(driver: dbConfig.driver, username: dbConfig.username, password: dbConfig.password, url: dbConfig.url) } } 

And the test entity:

 @Entity class TestEntity { @Id UUID id Integer value } 

I already rubbed my hands in anticipation of the beloved profit'a all. But ... there are no fairy tales, and of course, everything fell at the start with java.lang.IllegalStateException: Bean class xxxTestEntity is not enhanced?

Usually people read the documentation only when something goes wrong. And it is even good. It turns out that for normal operation, Ebean needs to expand the bytecode of .class files at the assembly stage, i.e. immediately after compilation. Why do you need it? Almost all ORMs are built into classes, just the majority get out differently. Hibernate, for example, creates a proxy runtime through cglib to intercept access to Lazy collections. Transparently honest lazy without such hacks just do not. Ebean, along with all, supports lazy loading, plus partial objects, and also tracks changes in each field in order not to send too much to the server during a save operation.

Early versions of the library supported two approaches to proxying: patching a .class file and instructing class code when loading via ClassLoader (it was required to connect an agent at the start of the JVM). Over time, for simplicity, support left only the first option.

Hmm ... Difficult, but ... People somehow live with Ebean in the Play Framework? It turns out that the ORM itself is supplied with a separate ebeanorm-agent library, which is able to expand the compiled .class file byte code, and the SBT on which Play keeps it successfully uses. Since in Play the assembly of the code is only internal, everything works like a clock. And no one, probably, doesn’t guess what is going on behind the scenes.

Gradle


But the question is, is there such a thing for Gradle? For Maven there is exactly a plugin. But for Gradle, I found absolutely nothing (maybe I looked badly). At first I was upset, and even thought of quitting this venture ... But at the last moment I got ready and decided what would have happened to finish the job.

So, do the missing plugin!

The easiest way to add your own build tools to Gradle is to create the buildSrc module in the project root directory. The code from this module will be automatically available in all other build scripts (all options are described here ).

Next we create build.gradle inside the buildSrc directory:
 apply plugin: 'groovy' repositories { mavenCentral() } dependencies { //      compile 'org.avaje.ebeanorm:avaje-ebeanorm-agent:4.1.5' } 

Although the buildSrc approach does not require the creation of a plugin (you can simply write and call code from a Groovy script), we will go the right way, extending the Gradle API. After all, for sure, later, you will want to arrange it all as a finished product and put it somewhere for general use.

The main idea of ​​the plugin is that after each compilation step of Java, Groovy or Scala, find and process the compiler-created .class files that will be used by Ebean. This task is solved like this:

 class EbeanPlugin implements Plugin<Project> { //   - ! private static def supportedCompilerTasks = ['compileJava', 'compileGroovy', 'compileScala'] //     ,   void apply(Project project) { //        def params = project.extensions.create('ebean', EbeanPluginParams) //  ,    ... def tasks = project.tasks //...         supportedCompilerTasks.each { compileTask -> tryHookCompilerTask(tasks, compileTask, params) } } private static void tryHookCompilerTask(TaskContainer tasks, String taskName, EbeanPluginParams params) { try { def task = tasks.getByName(taskName) //      , ..  task.doLast({ completedTask -> //        enhanceTaskOutput(completedTask.outputs, params) }) } catch (UnknownTaskException _) { ; //    } } private static void enhanceTaskOutput(TaskOutputs taskOutputs, EbeanPluginParams params) { //      ,      taskOutputs.files.each { outputDir -> if (outputDir.isDirectory()) { def classPath = outputDir.toPath() // ,        def fileFilter = new EbeanFileFilter(classPath, params.include, params.exclude) //     new EBeanEnhancer(classPath, fileFilter).enhance() } } } } //   ,      build.gradle class EbeanPluginParams { String[] include = [] String[] exclude = [] } 


Next, it's up to the expander itself. The algorithm is very simple: first, we recursively collect all .class files inside the base directory suitable for the filter, and then pass them through the “agent”. The processing itself is simple: there is an entity — a transformer, as well as a wrapper assistant for processing from the input stream. Having created and connected both of us, all that remains is to open the file and call transform (...), simultaneously placing a bunch of catch for possible errors. Everything in the collection looks like this:

 class EBeanEnhancer { private final Path classPath private final FileFilter fileFilter EBeanEnhancer(Path classPath) { this(classPath, { file -> true }) } EBeanEnhancer(Path classPath, FileFilter fileFilter) { this.classPath = classPath this.fileFilter = fileFilter } void enhance() { collectClassFiles(classPath.toFile()).each { classFile -> if (fileFilter.accept(classFile)) { enhanceClassFile(classFile); } } } private void enhanceClassFile(File classFile) { def transformer = new Transformer(new FileSystemClassBytesReader(classPath), "debug=" + 1);//0-9 -> none - all def streamTransform = new InputStreamTransform(transformer, getClass().getClassLoader()) def className = ClassUtils.makeClassName(classPath, classFile); try { classFile.withInputStream { classInputStream -> def enhancedClassData = streamTransform.transform(className, classInputStream) if (null != enhancedClassData) { //transformer returns null when nothing was transformed try { classFile.withOutputStream { classOutputStream -> classOutputStream << enhancedClassData } } catch (IOException e) { throw new EbeanEnhancementException("Unable to store an enhanced class data back to file $classFile.name", e); } } } } catch (IOException e) { throw new EbeanEnhancementException("Unable to read a class file $classFile.name for enhancement", e); } catch (IllegalClassFormatException e) { throw new EbeanEnhancementException("Unable to parse a class file $classFile.name while enhance", e); } } private static List<File> collectClassFiles(File dir) { List<File> classFiles = new ArrayList<>(); dir.listFiles().each { file -> if (file.directory) { classFiles.addAll(collectClassFiles(file)); } else { if (file.name.endsWith(".class")) { classFiles.add(file); } } } classFiles } } 

How filters are made to show no sense (or shame). This can be any implementation of the java.io.FileFilter interface. And in fact, this functionality is not required.

FileSystemClassBytesReader is another matter. This is a very important element of the process. It reads the associated .class files if the transformer needed them. For example, when a subclass is being analyzed, the ebean-agent requests a superclass through the ClassBytesReader in order to check it for the @MappedSuperclass annotation. Without this java.lang.IllegalStateException: Bean class xxxSubClassEntity is not enhanced? flies without hesitation.

 class FileSystemClassBytesReader implements ClassBytesReader { private final Path basePath; FileSystemClassBytesReader(Path basePath) { this.basePath = basePath; } @Override byte[] getClassBytes(String className, ClassLoader classLoader) { def classFilePath = basePath.resolve(className.replace(".", "/") + ".class"); def file = classFilePath.toFile() def buffer = new byte[file.length()] try { file.withInputStream { classFileStream -> classFileStream.read(buffer) } } catch (IOException e) { throw new EbeanEnhancementException("Failed to load class '$className' at base path '$basePath'", e); } buffer } } 

In order to call the plugin by the beautiful 'ebean' identifier, you need to add the ebean.properties file to the buildSrc / resources / META-INF folder:
 implementation-class=com.avaje.ebean.gradle.EbeanPlugin 

Everything. Plugin ready.

And finally, we add some wonderful lines to the build.gradle of the main project:

 apply plugin: 'ebean' // property-     ebean { //    include = ["com.vendor.product"] exclude = ["SomeWeirdClass"] } 

This is the story of Ebean’s successful encounter with Gradle. Everything is going and working as needed.

You can download the plugin on the GitHub gradle-ebean-enhancer . Unfortunately, for now everything is damp and the code needs to be copied to buildSrc. In the near future we will finish and send it to Maven Central and the Gradle repository.

IntelliJ Idea


Good news: for Idea 13 there is a plugin from Yevgeny Krasik, for which many thanks to him! The plugin “listens” to the build process and, hot on the heels of the compiler, extends .class files. I need this to launch and debug Spring Boot application from Idea itself, because it is so much more convenient and familiar.

The bad news: the plugin works with the old version of the agent library 3.2.2. The product of its activity is incompatible with Ebean 4.x and results in strange start actions.

Solution: make a fork on github and rebuild the plugin for the correct version.

Everything went like clockwork. The application has started. The test entity was able to save and load.

In fact, it was possible not to write about it ... but there is one “but”. As soon as I started building the hierarchy of entities and BaseEntity appeared with @MappedSuperclass, java.lang.IllegalStateException: Bean class xxxSubClassEntity is not enhanced? here again.

javap showed that for some reason all subclasses are not extended. Why-yyyy? Why?

It turned out that the IDE-plugin crept into an annoying error. When expanding the current class, the transformer, as always, tries to analyze the subclass as well. To do this, I remind you that he needs to provide an implementation of the ClassBytesReader. Only for some reason, the implementation of the IDE plug-in, instead of binary data, “fed” the source code to Groovy to the transformer, with which he “choked”.

So that the fork was very helpful. It was:

 //virtualFile       Groovy if (virtualFile == null) { return null; } try { return virtualFile.contentsToByteArray(); // o_O ? } catch (IOException e) { throw new RuntimeException(e); } 

It became:
 if (virtualFile == null) { compileContext.addMessage(CompilerMessageCategory.ERROR, "Unable to detect source file '" + className + "'. Not found in output directory", null, -1, -1); return null; } final Module fileModule = compileContext.getModuleByFile(virtualFile); final VirtualFile outputDirectory = compileContext.getModuleOutputDirectory(fileModule); //      final VirtualFile compiledRequestedFile = outputDirectory.findFileByRelativePath(classNamePath + ".class"); if (null == compiledRequestedFile) { compileContext.addMessage(CompilerMessageCategory.ERROR, "Class file for '" + className + "' is not found in output directory", null, -1, -1); return null; } try { return compiledRequestedFile.contentsToByteArray(); } catch (IOException e) { throw new RuntimeException(e); } 

Profit! I admit that the author of the plugin did not use the ORM framework very much. Although, the only thing I can complain about is the absence of imputed error messages. After all, it is somehow sad to observe a quietly not working product.

The complete, corrected plug-in code can be found in the only fork at the time of this writing on the GitHub idea-ebean-enhancer . There is also a link to a zip ready for installation.

Results


IntelliJ Idea users finally got a fully working plugin, with support for the latest version of Ebean.

Along with Maven, Gradle also got an extension to support this ORM framework.

I hope that the way I have made will help readers to dare to try, what kind of beast is this Ebean . After all, it seems that all significant obstacles on this path have been overcome. Well, or at least inspire to go the same way and delight in the insides of some unfamiliar library.

It also seemed pretty funny to me that writing the code took significantly less time than preparing this publication.

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


All Articles