Author: epbernard Date: 2006-05-04 18:36:54 -0400 (Thu, 04 May 2006) New Revision: 9883 Added: trunk/Hibernate3/src/org/hibernate/bytecode/AbstractClassTransformerImpl.java trunk/Hibernate3/src/org/hibernate/bytecode/ClassTransformer.java trunk/Hibernate3/src/org/hibernate/bytecode/cglib/CglibClassTransformer.java trunk/Hibernate3/src/org/hibernate/bytecode/javassist/JavassistClassTransformer.java trunk/Hibernate3/test/org/hibernate/test/instrument/runtime/InstrumentedClassLoader.java trunk/Hibernate3/test/org/hibernate/test/instrument/runtime/JavassistInstrumentationTest.java Modified: trunk/Hibernate3/src/org/hibernate/bytecode/BytecodeProvider.java trunk/Hibernate3/src/org/hibernate/bytecode/cglib/BytecodeProviderImpl.java trunk/Hibernate3/src/org/hibernate/bytecode/javassist/BytecodeProviderImpl.java trunk/Hibernate3/test/org/hibernate/test/AllTests.java trunk/Hibernate3/test/org/hibernate/test/instrument/runtime/AbstractTransformingClassLoaderInstrumentTestCase.java Log: HHH-1719 add a bytecode entry for class transformation, also provide a ClassTransformer friendly ClassLoader (in the test suite), also add a JavassistInstrumentationTest Added: trunk/Hibernate3/src/org/hibernate/bytecode/AbstractClassTransformerImpl.java =================================================================== --- trunk/Hibernate3/src/org/hibernate/bytecode/AbstractClassTransformerImpl.java 2006-05-04 20:46:47 UTC (rev 9882) +++ trunk/Hibernate3/src/org/hibernate/bytecode/AbstractClassTransformerImpl.java 2006-05-04 22:36:54 UTC (rev 9883) @@ -0,0 +1,60 @@ +//$Id: $ +package org.hibernate.bytecode; + +import java.security.ProtectionDomain; +import java.util.HashSet; +import java.util.Set; + +/** + * @author Emmanuel Bernard + */ +public abstract class AbstractClassTransformerImpl implements ClassTransformer { + + final private Set<String> entities; + final private String[] packages; + + + public AbstractClassTransformerImpl(String[] packages, String[] classes) { + this.packages = packages; + if (classes == null) { + this.entities = null; + } + else { + this.entities = new HashSet<String>(); + for ( String clazz : classes ) { + entities.add( clazz ); + } + } + } + + public byte[] + transform( + ClassLoader loader, String className, Class<?> classBeingRedefined, + ProtectionDomain protectionDomain, byte[] classfileBuffer + ) { + boolean enhance = false; + String safeClassName = className.replace( "/", "." ); + if ( entities == null && packages == null ) { + enhance = true; + } + if ( ! enhance && entities != null && entities.contains( safeClassName ) ) { + enhance = true; + } + if ( ! enhance && packages != null ) { + for ( String packageName : packages ) { + if ( safeClassName.startsWith( packageName ) ) { + enhance = true; + break; + } + } + } + if ( ! enhance ) return classfileBuffer; + + return doTransform( loader, className, classBeingRedefined, protectionDomain, classfileBuffer ); + } + + protected abstract byte[] doTransform( + ClassLoader loader, String className, Class<?> classBeingRedefined, + ProtectionDomain protectionDomain, byte[] classfileBuffer + ); +} Modified: trunk/Hibernate3/src/org/hibernate/bytecode/BytecodeProvider.java =================================================================== --- trunk/Hibernate3/src/org/hibernate/bytecode/BytecodeProvider.java 2006-05-04 20:46:47 UTC (rev 9882) +++ trunk/Hibernate3/src/org/hibernate/bytecode/BytecodeProvider.java 2006-05-04 22:36:54 UTC (rev 9883) @@ -47,11 +47,30 @@ * * @param parent The parent classloader * @param classpath The classpath to be searched - * @param packages can be null; use to limnit the packages to be loaded + * @param packages can be null; use to limit the packages to be loaded * via this classloader (and transformed). * @return The appropriate ClassLoader. */ public ClassLoader generateDynamicFieldInterceptionClassLoader(ClassLoader parent, String[] classpath, String[] packages); + /** + * Generate a ClassTransformer capable of performing dynamic bytecode manipulation + * on classes as they are loaded for the purpose of field-level interception. + * The returned ClassTransformer can be combined to an appropriate ClassLoader + * is used for run-time bytecode manipulation as + * opposed to the more common build-time manipulation, since here we get + * into SecurityManager issues and such. + * <p/> + * + * @param packages can be null; use to limit the packages to be transformed + * via this classtransformer. + * @param classes can be null; use to limit the classes to be transformed + * via this class transformer. + * @return The appropriate ClassTransformer. + */ + public ClassTransformer getEntityClassTransformer( + String[] packages, String[] classes + ); + public void releaseDynamicFieldInterceptionClassLoader(ClassLoader classLoader); } Added: trunk/Hibernate3/src/org/hibernate/bytecode/ClassTransformer.java =================================================================== --- trunk/Hibernate3/src/org/hibernate/bytecode/ClassTransformer.java 2006-05-04 20:46:47 UTC (rev 9882) +++ trunk/Hibernate3/src/org/hibernate/bytecode/ClassTransformer.java 2006-05-04 22:36:54 UTC (rev 9883) @@ -0,0 +1,34 @@ +//$Id: $ +package org.hibernate.bytecode; + +import java.security.ProtectionDomain; + +/** + * A persistence provider provides an instance of this interface + * to the PersistenceUnitInfo.addTransformer method. + * The supplied transformer instance will get called to transform + * entity class files when they are loaded and redefined. The transformation + * occurs before the class is defined by the JVM + * + * + * @author <a href="mailto:bi...@jb...">Bill Burke</a> + * @author Emmanuel Bernard + */ +public interface ClassTransformer +{ + /** + * Invoked when a class is being loaded or redefined to add hooks for persistence bytecode manipulation + * + * @param loader the defining class loaderof the class being transformed. It may be null if using bootstrap loader + * @param classname The name of the class being transformed + * @param classBeingRedefined If an already loaded class is being redefined, then pass this as a parameter + * @param protectionDomain ProtectionDomain of the class being (re)-defined + * @param classfileBuffer The input byte buffer in class file format + * @return A well-formed class file that can be loaded + */ + byte[] transform(ClassLoader loader, + String classname, + Class<?> classBeingRedefined, + ProtectionDomain protectionDomain, + byte[] classfileBuffer); +} Modified: trunk/Hibernate3/src/org/hibernate/bytecode/cglib/BytecodeProviderImpl.java =================================================================== --- trunk/Hibernate3/src/org/hibernate/bytecode/cglib/BytecodeProviderImpl.java 2006-05-04 20:46:47 UTC (rev 9882) +++ trunk/Hibernate3/src/org/hibernate/bytecode/cglib/BytecodeProviderImpl.java 2006-05-04 22:36:54 UTC (rev 9883) @@ -1,24 +1,24 @@ package org.hibernate.bytecode.cglib; +import java.lang.reflect.Modifier; + +import net.sf.cglib.beans.BulkBean; +import net.sf.cglib.beans.BulkBeanException; +import net.sf.cglib.reflect.FastClass; +import net.sf.cglib.transform.ClassFilter; +import net.sf.cglib.transform.ClassTransformer; +import net.sf.cglib.transform.ClassTransformerFactory; +import net.sf.cglib.transform.TransformingClassLoader; +import net.sf.cglib.transform.impl.InterceptFieldFilter; +import net.sf.cglib.transform.impl.InterceptFieldTransformer; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.hibernate.bytecode.BytecodeProvider; import org.hibernate.bytecode.ProxyFactoryFactory; import org.hibernate.bytecode.ReflectionOptimizer; import org.hibernate.util.StringHelper; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; import org.objectweb.asm.Type; -import net.sf.cglib.reflect.FastClass; -import net.sf.cglib.beans.BulkBean; -import net.sf.cglib.beans.BulkBeanException; -import net.sf.cglib.transform.TransformingClassLoader; -import net.sf.cglib.transform.ClassFilter; -import net.sf.cglib.transform.ClassTransformerFactory; -import net.sf.cglib.transform.ClassTransformer; -import net.sf.cglib.transform.impl.InterceptFieldTransformer; -import net.sf.cglib.transform.impl.InterceptFieldFilter; -import java.lang.reflect.Modifier; - /** * Bytecode provider implementation for CGLIB. * @@ -108,6 +108,12 @@ ); } + public org.hibernate.bytecode.ClassTransformer getEntityClassTransformer( + String[] packages, String[] classes + ) { + return new CglibClassTransformer( packages, classes ); + } + public void releaseDynamicFieldInterceptionClassLoader(ClassLoader classLoader) { } Added: trunk/Hibernate3/src/org/hibernate/bytecode/cglib/CglibClassTransformer.java =================================================================== --- trunk/Hibernate3/src/org/hibernate/bytecode/cglib/CglibClassTransformer.java 2006-05-04 20:46:47 UTC (rev 9882) +++ trunk/Hibernate3/src/org/hibernate/bytecode/cglib/CglibClassTransformer.java 2006-05-04 22:36:54 UTC (rev 9883) @@ -0,0 +1,115 @@ +//$Id: $ +package org.hibernate.bytecode.cglib; + +import java.security.ProtectionDomain; +import java.util.Arrays; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.ByteArrayOutputStream; + +import net.sf.cglib.transform.ClassTransformer; +import net.sf.cglib.transform.TransformingClassGenerator; +import net.sf.cglib.transform.ClassReaderGenerator; +import net.sf.cglib.transform.impl.InterceptFieldEnabled; +import net.sf.cglib.transform.impl.InterceptFieldFilter; +import net.sf.cglib.transform.impl.InterceptFieldTransformer; +import net.sf.cglib.core.ClassNameReader; +import net.sf.cglib.core.DebuggingClassWriter; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.hibernate.bytecode.AbstractClassTransformerImpl; +import org.hibernate.HibernateException; +import org.objectweb.asm.Attribute; +import org.objectweb.asm.Type; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.attrs.Attributes; + +/** + * Enhance the classes allowing them to implements InterceptFieldEnabled + * This interface is then used by Hibernate for some optimizations. + * + * @author Emmanuel Bernard + */ +public class CglibClassTransformer extends AbstractClassTransformerImpl { + + private static Log log = LogFactory.getLog( CglibClassTransformer.class.getName() ); + + public CglibClassTransformer(String[] packages, String[] classes) { + super(packages, classes); + } + + protected byte[] doTransform( + ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, + byte[] classfileBuffer + ) { + ClassReader reader; + try { + reader = new ClassReader( new ByteArrayInputStream( classfileBuffer ) ); + } + catch (IOException e) { + log.error( "Unable to read class", e ); + throw new HibernateException( "Unable to read class: " + e.getMessage() ); + } + + String name[] = ClassNameReader.getClassInfo( reader ); + ClassWriter w = new DebuggingClassWriter( true ); + ClassTransformer t = getClassTransformer( name ); + if ( t != null ) { + if ( log.isDebugEnabled() ) { + log.debug( "Enhancing " + className ); + } + ByteArrayOutputStream out; + byte[] result; + try { + reader = new ClassReader( new ByteArrayInputStream( classfileBuffer ) ); + new TransformingClassGenerator( + new ClassReaderGenerator( + reader, + attributes(), skipDebug() + ), t + ).generateClass( w ); + out = new ByteArrayOutputStream(); + out.write( w.toByteArray() ); + result = out.toByteArray(); + out.close(); + } + catch (Exception e) { + log.error( "Unable to transform class", e ); + throw new HibernateException( "Unable to transform class: " + e.getMessage() ); + } + return result; + } + return classfileBuffer; + } + + + private Attribute[] attributes() { + return Attributes.getDefaultAttributes(); + } + + private boolean skipDebug() { + return false; + } + + private ClassTransformer getClassTransformer(String[] classInfo) { + + if ( Arrays.asList( classInfo ).contains( InterceptFieldEnabled.class.getName() ) ) { + return null; + } + else { + return new InterceptFieldTransformer( + new InterceptFieldFilter() { + public boolean acceptRead(Type owner, String name) { + return true; + } + + public boolean acceptWrite(Type owner, String name) { + return true; + } + } + ); + } + + } +} Modified: trunk/Hibernate3/src/org/hibernate/bytecode/javassist/BytecodeProviderImpl.java =================================================================== --- trunk/Hibernate3/src/org/hibernate/bytecode/javassist/BytecodeProviderImpl.java 2006-05-04 20:46:47 UTC (rev 9882) +++ trunk/Hibernate3/src/org/hibernate/bytecode/javassist/BytecodeProviderImpl.java 2006-05-04 22:36:54 UTC (rev 9883) @@ -1,14 +1,15 @@ package org.hibernate.bytecode.javassist; +import java.lang.reflect.Modifier; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.hibernate.bytecode.BytecodeProvider; +import org.hibernate.bytecode.ClassTransformer; import org.hibernate.bytecode.ProxyFactoryFactory; import org.hibernate.bytecode.ReflectionOptimizer; import org.hibernate.util.StringHelper; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import java.lang.reflect.Modifier; - /** * Bytecode provider implementation for Javassist. * @@ -78,6 +79,12 @@ return new TransformingClassLoader( parent, classpath ); } + public ClassTransformer getEntityClassTransformer( + String[] packages, String[] classes + ) { + return new JavassistClassTransformer( packages, classes ); + } + public void releaseDynamicFieldInterceptionClassLoader(ClassLoader classLoader) { if ( ! TransformingClassLoader.class.isAssignableFrom( classLoader.getClass() ) ) { return; Added: trunk/Hibernate3/src/org/hibernate/bytecode/javassist/JavassistClassTransformer.java =================================================================== --- trunk/Hibernate3/src/org/hibernate/bytecode/javassist/JavassistClassTransformer.java 2006-05-04 20:46:47 UTC (rev 9882) +++ trunk/Hibernate3/src/org/hibernate/bytecode/javassist/JavassistClassTransformer.java 2006-05-04 22:36:54 UTC (rev 9883) @@ -0,0 +1,104 @@ +//$Id: $ +package org.hibernate.bytecode.javassist; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.security.ProtectionDomain; + +import javassist.bytecode.ClassFile; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.hibernate.HibernateException; +import org.hibernate.bytecode.AbstractClassTransformerImpl; +import org.hibernate.tool.instrument.javassist.FieldFilter; +import org.hibernate.tool.instrument.javassist.FieldHandled; +import org.hibernate.tool.instrument.javassist.FieldTransformer; + +/** + * Enhance the classes allowing them to implements InterceptFieldEnabled + * This interface is then used by Hibernate for some optimizations. + * + * @author Emmanuel Bernard + */ +public class JavassistClassTransformer extends AbstractClassTransformerImpl { + + private static Log log = LogFactory.getLog( JavassistClassTransformer.class.getName() ); + + public JavassistClassTransformer(String[] packages, String[] classes) { + super(packages, classes); + } + + protected byte[] doTransform( + ClassLoader loader, String className, Class<?> classBeingRedefined, + ProtectionDomain protectionDomain, byte[] classfileBuffer + ) { + ClassFile classfile = null; + try { + // WARNING: classfile only + classfile = new ClassFile( new DataInputStream( new ByteArrayInputStream( classfileBuffer ) ) ); + } + catch (IOException e) { + log.error( "Unable to build enhancement metamodel for " + className ); + return classfileBuffer; + } + FieldTransformer transformer = getFieldTransformer( classfile ); + if ( transformer != null ) { + if ( log.isDebugEnabled() ) { + log.debug( "Enhancing " + className ); + } + DataOutputStream out = null; + try { + transformer.transform( classfile ); + ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); + out = new DataOutputStream( byteStream ); + classfile.write( out ); + return byteStream.toByteArray(); + } + catch (Exception e) { + log.error( "Unable to transform class", e ); + throw new HibernateException( "Unable to transform class: " + e.getMessage() ); + } + finally { + try { + if ( out != null ) out.close(); + } + catch (IOException e) { + //swallow + } + } + } + return classfileBuffer; + } + + protected FieldTransformer getFieldTransformer(ClassFile classfile) { + if ( alreadyInstrumented( classfile ) ) { + return null; + } + else { + return new FieldTransformer( + new FieldFilter() { + public boolean handleRead(String desc, String name) { + return true; + } + + public boolean handleWrite(String desc, String name) { + return true; + } + } + ); + } + } + + private boolean alreadyInstrumented(ClassFile classfile) { + String[] intfs = classfile.getInterfaces(); + for ( int i = 0; i < intfs.length; i++ ) { + if ( FieldHandled.class.getName().equals( intfs[i] ) ) { + return true; + } + } + return false; + } +} Modified: trunk/Hibernate3/test/org/hibernate/test/AllTests.java =================================================================== --- trunk/Hibernate3/test/org/hibernate/test/AllTests.java 2006-05-04 20:46:47 UTC (rev 9882) +++ trunk/Hibernate3/test/org/hibernate/test/AllTests.java 2006-05-04 22:36:54 UTC (rev 9883) @@ -47,6 +47,7 @@ import org.hibernate.test.immutable.ImmutableTest; import org.hibernate.test.instrument.buildtime.InstrumentTest; import org.hibernate.test.instrument.runtime.CGLIBInstrumentationTest; +import org.hibernate.test.instrument.runtime.JavassistInstrumentationTest; import org.hibernate.test.interceptor.InterceptorTest; import org.hibernate.test.interfaceproxy.InterfaceProxyTest; import org.hibernate.test.iterate.IterateTest; @@ -282,6 +283,7 @@ suite.addTest( InstrumentCacheTest2.suite() ); } suite.addTest( CGLIBInstrumentationTest.suite() ); + suite.addTest( JavassistInstrumentationTest.suite() ); suite.addTest( SybaseTimestampVersioningTest.suite() ); suite.addTest( DbVersionTest.suite() ); suite.addTest( TimestampGeneratedValuesWithCachingTest.suite() ); Modified: trunk/Hibernate3/test/org/hibernate/test/instrument/runtime/AbstractTransformingClassLoaderInstrumentTestCase.java =================================================================== --- trunk/Hibernate3/test/org/hibernate/test/instrument/runtime/AbstractTransformingClassLoaderInstrumentTestCase.java 2006-05-04 20:46:47 UTC (rev 9882) +++ trunk/Hibernate3/test/org/hibernate/test/instrument/runtime/AbstractTransformingClassLoaderInstrumentTestCase.java 2006-05-04 22:36:54 UTC (rev 9883) @@ -1,7 +1,6 @@ package org.hibernate.test.instrument.runtime; import java.lang.reflect.InvocationTargetException; -import java.net.URL; import org.hibernate.HibernateException; import org.hibernate.bytecode.BytecodeProvider; @@ -15,16 +14,17 @@ private BytecodeProvider provider; protected ClassLoader buildIsolatedClassLoader(ClassLoader parent) { - String myFileName = AbstractTransformingClassLoaderInstrumentTestCase.class.getName().replace( '.', '/' ) + ".class"; - URL fileURL = this.getClass().getClassLoader().getResource( myFileName ); - String filePath = fileURL.getPath(); - String classPath = filePath.substring( 0, filePath.length() - myFileName.length() ); +// String myFileName = AbstractTransformingClassLoaderInstrumentTestCase.class.getName().replace( '.', '/' ) + ".class"; +// URL fileURL = this.getClass().getClassLoader().getResource( myFileName ); +// String filePath = fileURL.getPath(); +// String classPath = filePath.substring( 0, filePath.length() - myFileName.length() ); provider = buildBytecodeProvider(); - return provider.generateDynamicFieldInterceptionClassLoader( + return new InstrumentedClassLoader( parent, - new String[] { classPath }, - new String[] { "org.hibernate.test.instrument" } - ); + provider.getEntityClassTransformer( + new String[] { "org.hibernate.test.instrument" }, + null) ); + } protected void releaseIsolatedClassLoader(ClassLoader isolatedLoader) { Added: trunk/Hibernate3/test/org/hibernate/test/instrument/runtime/InstrumentedClassLoader.java =================================================================== --- trunk/Hibernate3/test/org/hibernate/test/instrument/runtime/InstrumentedClassLoader.java 2006-05-04 20:46:47 UTC (rev 9882) +++ trunk/Hibernate3/test/org/hibernate/test/instrument/runtime/InstrumentedClassLoader.java 2006-05-04 22:36:54 UTC (rev 9883) @@ -0,0 +1,81 @@ +//$Id: $ +package org.hibernate.test.instrument.runtime; + +import java.io.IOException; +import java.io.InputStream; + +import org.hibernate.HibernateException; +import org.hibernate.bytecode.ClassTransformer; + +/** + * @author Emmanuel Bernard + */ +public class InstrumentedClassLoader extends ClassLoader { + + private ClassTransformer classTransformer; + + public InstrumentedClassLoader(ClassLoader parent, ClassTransformer classTransformer) { + super( parent ); + this.classTransformer = classTransformer; + } + + @Override + public Class<?> loadClass(String name) throws ClassNotFoundException { + if ( name.startsWith( "java" ) ) return getParent().loadClass( name ); + Class c = findLoadedClass( name ); + if ( c != null ) return c; + InputStream is = this.getResourceAsStream( name.replace( ".", "/" ) + ".class" ); + if ( is == null ) throw new ClassNotFoundException( name ); + byte[] buffer = new byte[409600]; + byte[] originalClass = new byte[0]; + int r = 0; + try { + r = is.read( buffer ); + } + catch (IOException e) { + throw new ClassNotFoundException( name + " not found", e ); + } + while ( r >= buffer.length ) { + byte[] temp = new byte[ originalClass.length + buffer.length ]; + System.arraycopy( originalClass, 0, temp, 0, originalClass.length ); + System.arraycopy( buffer, 0, temp, originalClass.length, buffer.length ); + originalClass = temp; + } + if ( r != -1 ) { + byte[] temp = new byte[ originalClass.length + r ]; + System.arraycopy( originalClass, 0, temp, 0, originalClass.length ); + System.arraycopy( buffer, 0, temp, originalClass.length, r ); + originalClass = temp; + } + try { + is.close(); + } + catch (IOException e) { + throw new ClassNotFoundException( name + " not found", e ); + } + if (classTransformer != null) { + byte[] transformed = new byte[0]; + try { + transformed = classTransformer.transform( + getParent(), + name, + null, + null, + originalClass + ); + if ( originalClass == transformed) { + return getParent().loadClass(name); + } + else { + return defineClass( name, transformed, 0, transformed.length ); + } + } + catch (HibernateException e) { + throw new ClassNotFoundException( name + " not found", e ); + } + } + else { + return getParent().loadClass(name); + } + } +} Added: trunk/Hibernate3/test/org/hibernate/test/instrument/runtime/JavassistInstrumentationTest.java =================================================================== --- trunk/Hibernate3/test/org/hibernate/test/instrument/runtime/JavassistInstrumentationTest.java 2006-05-04 20:46:47 UTC (rev 9882) +++ trunk/Hibernate3/test/org/hibernate/test/instrument/runtime/JavassistInstrumentationTest.java 2006-05-04 22:36:54 UTC (rev 9883) @@ -0,0 +1,20 @@ +//$Id: $ +package org.hibernate.test.instrument.runtime; + +import org.hibernate.bytecode.BytecodeProvider; +import org.hibernate.bytecode.javassist.BytecodeProviderImpl; +import junit.framework.Test; +import junit.framework.TestSuite; + +/** + * @author Steve Ebersole + */ +public class JavassistInstrumentationTest extends AbstractTransformingClassLoaderInstrumentTestCase { + protected BytecodeProvider buildBytecodeProvider() { + return new BytecodeProviderImpl(); + } + + public static Test suite() { + return new TestSuite( JavassistInstrumentationTest.class ); + } +} |