HHH-9809 - Improve Hibernate Gradle plugin

This commit is contained in:
Steve Ebersole 2015-05-21 00:24:56 -05:00
parent acea523607
commit 1812bb2ef3
11 changed files with 463 additions and 67 deletions

View File

@ -0,0 +1,93 @@
= Bytecode Enhancement
:toc:
This guide covers Hibernate's ability to enhance an applications domain model, the ways to perform that
enhancement and the capabilities introduced into the domain model by the enhancement.
== The capabilities
Hibernate will enhance the classes in an application's domain model in order to add one or more of the
following capabilities:
. Lazy state initialization
. Dirtiness tracking
. Automatic bi-directional association management
. Performance optimizations
todo : explain each in detail
== Performing enhancement
Ultimately all enhancement is handled by the `org.hibernate.bytecode.enhance.spi.Enhancer` class. Custom means to
enhancement can certainly be crafted on top of Enhancer, but that is beyond the scope of this guide. Here we
will focus on the means Hibernate already exposes for performing these enhancements.
=== Run-time enhancement
Currently run-time enhancement of the domain model is only supported in managed JPA environments following the
JPA defined SPI for performing class transformations. Even then, this support is disabled by default. In this
scenario, run-time enhancement can be enabled by specifying `hibernate.ejb.use_class_enhancer=true` as a
persistent unit property.
=== Build-time enhancement
Hibernate also offers the ability to integrate the enhancement of the domain model as part of the
normal build cycle of that domain model. Gradle, Ant and Maven are all supported. One possible benefit
of this approach is that the enhanced classes are what gets added to the jar and can then be used on both
sides of serialization.
=== Gradle Plugin
Hibernate provides a Gradle plugin that is capable of providing build-time enhancement of the domain model as they are
compiled as part of a Gradle build. To use the plugin a project would first need to apply it:
[[gradle-plugin-apply-example]]
.Apply the plugin
====
[source, GROOVY]
----
ext {
hibernateVersion = 'hibernate-version-you-want'
}
buildscript {
dependencies {
classpath "org.hibernate:hibernate-gradle-plugin:$hibernateVersion"
}
}
----
====
At the moment there is not much to configure with regard to the Enhancer, but what is configurable is exposed
through a DSL extension registered. By default enhancement will not be done (in preparation for when this
Gradle plugin offers additional capabilities). To enable it you must configure the following DSL extension:
[[gradle-plugin-apply-example]]
.Apply the plugin
====
[source, GROOVY]
----
hibernate {
enhance {
// any configuration goes here
}
}
----
====
Currently the "enhance" extension supports 3 properties:
* `enableLazyInitialization`
* `enableDirtyTracking`
* `enableAssociationManagement`
Once enhancement overall is enabled, the default for these 3 properties is `true`.
=== Ant Task
=== Maven Plugin

View File

@ -19,6 +19,7 @@ NOTE: This is still very much a work in progress. <<helping,Help>> is definitely
== Tooling
* See the <<metamodelgen/MetamodelGenerator.adoc#,Metamodel Generator Guide>> for details on generating a JPA "Static Metamodel"
* see the <<bytecode/BytecodeEnhancement.adoc#,Bytecode Enhancement Guide>> for information on bytecode enhancement
* Guide on the Gradle plugin coming soon
* Guide on the Ant tasks coming soon
* Guide on the Maven plugin coming soon

View File

@ -0,0 +1,6 @@
Defines a Gradle plugin for introducing Hibernate specific tasks and capabilities into and end-user build.
Currently the only capability added is for bytecode enhancement of the user domain model, although other capabilities are
planned.
todo : usage

View File

@ -4,12 +4,14 @@
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
apply plugin: 'java-gradle-plugin'
apply plugin: 'groovy'
apply plugin: 'maven'
apply plugin: 'java'
dependencies {
compile( project(':hibernate-core') )
compile( project( ':hibernate-core' ) )
compile( libraries.jpa )
compile( libraries.javassist )
compile gradleApi()

View File

@ -0,0 +1,23 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.orm.tooling.gradle
/**
* Gradle DSL extension for configuring various Hibernate bytecode enhancement. Registered
* under "hibernate.enhance".
*
* @author Steve Ebersole
*/
class EnhanceExtension {
def boolean enableLazyInitialization = true
def boolean enableDirtyTracking = true
def boolean enableAssociationManagement = true
boolean shouldApply() {
return enableLazyInitialization || enableDirtyTracking || enableAssociationManagement;
}
}

View File

@ -4,7 +4,7 @@
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.tooling.gradle
package org.hibernate.orm.tooling.gradle
import javax.persistence.ElementCollection
import javax.persistence.Entity

View File

@ -0,0 +1,53 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.orm.tooling.gradle
import org.gradle.api.Project
import org.gradle.api.plugins.JavaPluginConvention
import org.gradle.api.tasks.SourceSet
import org.gradle.util.ConfigureUtil
/**
* Gradle DSL extension for configuring various Hibernate built-time tasks. Registered
* under "hibernate".
*
* @author Steve Ebersole
*/
class HibernateExtension {
private final Project project
/**
* The source sets that hold persistent model. Default is project.sourceSets.main
*/
def SourceSet[] sourceSets
/**
* Configuration for bytecode enhancement. Private; see instead {@link #enhance(groovy.lang.Closure)}
*/
protected EnhanceExtension enhance
HibernateExtension(Project project) {
this.project = project
this.sourceSet( project.getConvention().getPlugin( JavaPluginConvention ).sourceSets.main )
}
/**
* Add a single SourceSet.
*
* @param sourceSet The SourceSet to add
*/
void sourceSet(SourceSet sourceSet) {
if ( sourceSets == null ) {
sourceSets = []
}
sourceSets += sourceSets
}
void enhance(Closure closure) {
enhance = new EnhanceExtension()
ConfigureUtil.configure( closure, enhance )
}
}

View File

@ -0,0 +1,235 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.orm.tooling.gradle;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;
import javax.persistence.Embedded;
import javax.persistence.Entity;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtField;
import org.gradle.api.Action;
import org.gradle.api.GradleException;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.Task;
import org.gradle.api.file.FileCollection;
import org.gradle.api.file.FileTree;
import org.gradle.api.logging.Logger;
import org.gradle.api.logging.Logging;
import org.gradle.api.tasks.SourceSet;
import org.hibernate.bytecode.enhance.spi.DefaultEnhancementContext;
import org.hibernate.bytecode.enhance.spi.EnhancementContext;
import org.hibernate.bytecode.enhance.spi.Enhancer;
/**
* The Hibernate Gradle plugin. Adds Hibernate build-time capabilities into your Gradle-based build.
*
* @author Jeremy Whiting
* @author Steve Ebersole
*/
@SuppressWarnings("serial")
public class HibernatePlugin implements Plugin<Project> {
private final Logger logger = Logging.getLogger( HibernatePlugin.class );
public void apply(Project project) {
project.getPlugins().apply( "java" );
final HibernateExtension hibernateExtension = new HibernateExtension( project );
project.getLogger().debug( "Adding Hibernate extensions to the build [{}]", project.getName() );
project.getExtensions().add( "hibernate", hibernateExtension );
project.afterEvaluate(
new Action<Project>() {
@Override
public void execute(Project project) {
if ( hibernateExtension.enhance != null ) {
applyEnhancement( project, hibernateExtension );
}
}
}
);
}
private void applyEnhancement(final Project project, final HibernateExtension hibernateExtension) {
if ( !hibernateExtension.enhance.shouldApply() ) {
return;
}
for ( final SourceSet sourceSet : hibernateExtension.getSourceSets() ) {
project.getLogger().debug( "Applying Hibernate enhancement action to SourceSet.{}", sourceSet.getName() );
final Task compileTask = project.getTasks().findByName( sourceSet.getCompileJavaTaskName() );
final ClassLoader classLoader = toClassLoader( sourceSet.getRuntimeClasspath() );
compileTask.doLast(
new Action<Task>() {
@Override
public void execute(Task task) {
project.getLogger().debug( "Starting Hibernate enhancement on SourceSet.{}", sourceSet.getName() );
EnhancementContext enhancementContext = new DefaultEnhancementContext() {
@Override
public ClassLoader getLoadingClassLoader() {
return classLoader;
}
@Override
public boolean doBiDirectionalAssociationManagement(CtField field) {
return hibernateExtension.enhance.getEnableAssociationManagement();
}
@Override
public boolean doDirtyCheckingInline(CtClass classDescriptor) {
return hibernateExtension.enhance.getEnableDirtyTracking();
}
@Override
public boolean hasLazyLoadableAttributes(CtClass classDescriptor) {
return hibernateExtension.enhance.getEnableLazyInitialization();
}
@Override
public boolean isLazyLoadable(CtField field) {
return hibernateExtension.enhance.getEnableLazyInitialization();
}
};
final Enhancer enhancer = new Enhancer( enhancementContext );
final ClassPool classPool = new ClassPool( false );
final FileTree fileTree = project.fileTree( sourceSet.getOutput().getClassesDir() );
for ( File file : fileTree ) {
if ( !file.getName().endsWith( ".class" ) ) {
continue;
}
final CtClass ctClass = toCtClass( file, classPool );
if ( !ctClass.hasAnnotation( Entity.class )
&& !ctClass.hasAnnotation( Embedded.class ) ) {
logger.debug( "Skipping class file [" + file.getAbsolutePath() + "], not an entity nor embedded" );
continue;
}
final byte[] enhancedBytecode = doEnhancement( ctClass, enhancer );
writeOutEnhancedClass( enhancedBytecode, ctClass, file );
}
}
}
);
}
}
private ClassLoader toClassLoader(FileCollection runtimeClasspath) {
List<URL> urls = new ArrayList<URL>();
for ( File file : runtimeClasspath ) {
try {
urls.add( file.toURI().toURL() );
}
catch (MalformedURLException e) {
throw new GradleException( "Unable to resolve classpath entry to URL : " + file.getAbsolutePath(), e );
}
}
return new URLClassLoader(
urls.toArray( new URL[urls.size()] ),
ClassLoader.getSystemClassLoader().getParent()
);
}
private CtClass toCtClass(File file, ClassPool classPool) {
try {
final InputStream is = new FileInputStream( file.getAbsolutePath() );
try {
return classPool.makeClass( is );
}
catch (IOException e) {
throw new GradleException( "Javassist unable to load class in preparation for enhancing : " + file.getAbsolutePath(), e );
}
finally {
try {
is.close();
}
catch (IOException e) {
logger.info( "Was unable to close InputStream : " + file.getAbsolutePath(), e );
}
}
}
catch (FileNotFoundException e) {
// should never happen, but...
throw new GradleException( "Unable to locate class file for InputStream: " + file.getAbsolutePath(), e );
}
}
private byte[] doEnhancement(CtClass ctClass, Enhancer enhancer) {
try {
return enhancer.enhance( ctClass.getName(), ctClass.toBytecode() );
}
catch (Exception e) {
throw new GradleException( "Unable to enhance class : " + ctClass.getName(), e );
}
}
private void writeOutEnhancedClass(byte[] enhancedBytecode, CtClass ctClass, File file) {
try {
if ( file.delete() ) {
if ( !file.createNewFile() ) {
logger.error( "Unable to recreate class file [" + ctClass.getName() + "]" );
}
}
else {
logger.error( "Unable to delete class file [" + ctClass.getName() + "]" );
}
}
catch (IOException e) {
logger.warn( "Problem preparing class file for writing out enhancements [" + ctClass.getName() + "]" );
}
try {
FileOutputStream outputStream = new FileOutputStream( file, false );
try {
outputStream.write( enhancedBytecode );
outputStream.flush();
}
catch (IOException e) {
throw new GradleException( "Error writing to enhanced class [" + ctClass.getName() + "] to file [" + file.getAbsolutePath() + "]", e );
}
finally {
try {
outputStream.close();
ctClass.detach();
}
catch (IOException ignore) {
}
}
}
catch (FileNotFoundException e) {
throw new GradleException( "Error opening class file for writing : " + file.getAbsolutePath(), e );
}
}
}

View File

@ -1,58 +0,0 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.tooling.gradle;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.Task;
import org.gradle.api.plugins.BasePlugin;
import org.gradle.api.plugins.JavaPlugin;
/**
* The Hibernate Gradle plugin. Adds Hibernate build-time capabilities into your Gradle-based build.
*
* @author Jeremy Whiting
* @author Steve Ebersole
*/
@SuppressWarnings("serial")
public class HibernatePlugin implements Plugin<Project> {
public static final String ENHANCE_TASK_NAME = "enhance";
public void apply(Project project) {
applyEnhancement( project );
}
private void applyEnhancement(Project project) {
project.getLogger().debug( "Applying Hibernate enhancement to project." );
// few things...
// 1) would probably be best as a doLast Action attached to the compile task rather than
// a task. Really ideally would be a task association for "always run after", but Gradle
// does not yet have that (mustRunAfter is very different semantic, finalizedBy is closer but
// will run even if the first task fails). The initial attempt here fell into the "maven" trap
// of trying to run a dependent task by attaching it to a task know to run after the we want to run after;
// which is a situation tailored made for Task.doLast
// 2) would be better to allow specifying which SourceSet to apply this to. For example, in the Hibernate
// build itself, this would be best applied to the 'test' sourceSet; though generally speaking the
// 'main' sourceSet is more appropriate
// for now, we'll just:
// 1) use a EnhancerTask + finalizedBy
// 2) apply to main sourceSet
EnhancerTask enhancerTask = project.getTasks().create( ENHANCE_TASK_NAME, EnhancerTask.class );
enhancerTask.setGroup( BasePlugin.BUILD_GROUP );
// connect up the task in the task dependency graph
Task classesTask = project.getTasks().getByName( JavaPlugin.CLASSES_TASK_NAME );
enhancerTask.dependsOn( classesTask );
classesTask.finalizedBy( enhancerTask );
}
}

View File

@ -4,4 +4,4 @@
# License: GNU Lesser General Public License (LGPL), version 2.1 or later.
# See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
#
implementation-class=org.hibernate.tooling.gradle.HibernatePlugin
implementation-class=org.hibernate.orm.tooling.gradle.HibernatePlugin

View File

@ -0,0 +1,41 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.orm.tooling.gradle
import org.gradle.api.Project
import org.gradle.testfixtures.ProjectBuilder
import org.junit.Test
import static org.junit.Assert.assertNotNull
import static org.junit.Assert.assertTrue
/**
* Test what we can. ProjectBuilder is better than nothing, but still quited limited in what
* you can test (e.g. you cannot test task execution).
*
* @author Steve Ebersole
*/
class HibernatePluginTest {
@Test
public void testHibernatePluginAddsExtension() {
Project project = ProjectBuilder.builder().build()
project.plugins.apply 'org.hibernate.orm'
assertNotNull( project.extensions.findByName( "hibernate" ) )
}
@Test
public void testHibernateExtensionConfig() {
Project project = ProjectBuilder.builder().build()
project.plugins.apply 'org.hibernate.orm'
project.extensions.findByType( HibernateExtension.class ).enhance {
enableAssociationManagement = false
}
}
}