DATACMNS-578 - Speed up instance creation by generating factory classes with ASM.
Introduced BytecodeGeneratingEntityInstantiator which dynamically generates classes to speed up the dynamic instantiation of objects. Since we cannot support every use case with byte code generation we gracefully fallback to the ReflectiveEntityInstantiator. BytecodeGeneratingEntityInstantiator has support for unboxing of constructor parameters and more robust handling for cases where we cannot generate a custom ObjectInstantiator. Original pull request: #98.
This commit is contained in:
committed by
Oliver Gierke
parent
48966b50d6
commit
1b9cdc941f
@@ -0,0 +1,489 @@
|
||||
/*
|
||||
* Copyright 2014 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.convert;
|
||||
|
||||
import static org.springframework.asm.Opcodes.*;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.security.AccessController;
|
||||
import java.security.PrivilegedAction;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.asm.ClassWriter;
|
||||
import org.springframework.asm.MethodVisitor;
|
||||
import org.springframework.asm.Opcodes;
|
||||
import org.springframework.asm.Type;
|
||||
import org.springframework.data.mapping.PersistentEntity;
|
||||
import org.springframework.data.mapping.PersistentProperty;
|
||||
import org.springframework.data.mapping.PreferredConstructor;
|
||||
import org.springframework.data.mapping.PreferredConstructor.Parameter;
|
||||
import org.springframework.data.mapping.model.MappingInstantiationException;
|
||||
import org.springframework.data.mapping.model.ParameterValueProvider;
|
||||
import org.springframework.data.util.TypeInformation;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* An {@link EntityInstantiator} that can generate byte code to speed-up dynamic object instantiation.
|
||||
* <p>
|
||||
* Uses the {@link PersistentEntity}'s {@link PreferredConstructor} to instantiate an instance of the entity by
|
||||
* dynamically generating factory methods with appropriate constructor invocations via ASM.
|
||||
* <p>
|
||||
* If we cannot generate byte code for a type, we gracefully fall-back to the {@link ReflectionEntityInstantiator}.
|
||||
*
|
||||
* @author Thomas Darimont
|
||||
* @since 1.10
|
||||
*/
|
||||
public enum BytecodeGeneratingEntityInstantiator implements EntityInstantiator {
|
||||
|
||||
INSTANCE;
|
||||
|
||||
private final ObjectInstantiatorClassGenerator classGenerator = ObjectInstantiatorClassGenerator.INSTANCE;
|
||||
|
||||
private volatile Map<TypeInformation<?>, EntityInstantiator> entityInstantiators = new HashMap<TypeInformation<?>, EntityInstantiator>(
|
||||
32);
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.data.convert.EntityInstantiator#createInstance(org.springframework.data.mapping.PersistentEntity, org.springframework.data.mapping.model.ParameterValueProvider)
|
||||
*/
|
||||
@Override
|
||||
public <T, E extends PersistentEntity<? extends T, P>, P extends PersistentProperty<P>> T createInstance(E entity,
|
||||
ParameterValueProvider<P> provider) {
|
||||
|
||||
EntityInstantiator ei = this.entityInstantiators.get(entity.getTypeInformation());
|
||||
|
||||
if (ei != null) {
|
||||
return ei.createInstance(entity, provider);
|
||||
}
|
||||
|
||||
ei = potentiallyCreateAndRegisterEntityInstantiator(entity);
|
||||
|
||||
return ei.createInstance(entity, provider);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param entity
|
||||
* @return
|
||||
*/
|
||||
private synchronized EntityInstantiator potentiallyCreateAndRegisterEntityInstantiator(PersistentEntity<?, ?> entity) {
|
||||
|
||||
Map<TypeInformation<?>, EntityInstantiator> map = this.entityInstantiators;
|
||||
|
||||
EntityInstantiator ei = map.get(entity.getTypeInformation());
|
||||
if (ei != null) {
|
||||
return ei;
|
||||
}
|
||||
|
||||
ei = createEntityInstantiator(entity);
|
||||
|
||||
map = new HashMap<TypeInformation<?>, EntityInstantiator>(map);
|
||||
map.put(entity.getTypeInformation(), ei);
|
||||
|
||||
this.entityInstantiators = map;
|
||||
|
||||
return ei;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param entity
|
||||
* @return
|
||||
*/
|
||||
private EntityInstantiator createEntityInstantiator(PersistentEntity<?, ?> entity) {
|
||||
|
||||
if (shoudUseReflectionEntityInstantiator(entity)) {
|
||||
return ReflectionEntityInstantiator.INSTANCE;
|
||||
}
|
||||
|
||||
try {
|
||||
return new EntityInstantiatorAdapter(createObjectInstantiator(entity));
|
||||
} catch (Throwable ex) {
|
||||
return ReflectionEntityInstantiator.INSTANCE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param entity
|
||||
* @return
|
||||
*/
|
||||
private boolean shoudUseReflectionEntityInstantiator(PersistentEntity<?, ?> entity) {
|
||||
|
||||
Class<?> type = entity.getType();
|
||||
|
||||
if (type.isInterface() //
|
||||
|| type.isArray() //
|
||||
|| !Modifier.isPublic(type.getModifiers()) //
|
||||
|| (type.isMemberClass() && !Modifier.isStatic(type.getModifiers())) //
|
||||
|| ClassUtils.isCglibProxyClass(type)) { //
|
||||
return true;
|
||||
}
|
||||
|
||||
PreferredConstructor<?, ?> persistenceConstructor = entity.getPersistenceConstructor();
|
||||
if (persistenceConstructor == null || !Modifier.isPublic(persistenceConstructor.getConstructor().getModifiers())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a dynamically generated {@link ObjectInstantiator} for the given {@link InstantiatorKey}. There will always
|
||||
* be exactly one {@link ObjectInstantiator} instance per {@link PersistentEntity}.
|
||||
* <p>
|
||||
*
|
||||
* @param entity
|
||||
* @return
|
||||
*/
|
||||
private ObjectInstantiator createObjectInstantiator(PersistentEntity<?, ?> entity) {
|
||||
try {
|
||||
return (ObjectInstantiator) classGenerator.generateCustomInstantiatorClass(entity).newInstance();
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link ClassLoader} that can load classes from {@code byte[]} representations.
|
||||
*
|
||||
* @author Thomas Darimont
|
||||
*/
|
||||
private static class ByteArrayClassLoader extends ClassLoader {
|
||||
|
||||
public ByteArrayClassLoader(ClassLoader parent) {
|
||||
super(parent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to load a class given {@code byte[]}.
|
||||
*
|
||||
* @param name must not be {@literal null}
|
||||
* @param bytes must not be {@literal null}
|
||||
* @return
|
||||
*/
|
||||
public Class<?> loadClass(String name, byte[] bytes) {
|
||||
|
||||
Assert.notNull(name, "name must not be null");
|
||||
Assert.notNull(bytes, "bytes must not be null");
|
||||
|
||||
try {
|
||||
Class<?> clazz = findClass(name);
|
||||
if (clazz != null) {
|
||||
return clazz;
|
||||
}
|
||||
} catch (ClassNotFoundException ignore) {}
|
||||
|
||||
return defineClass(name, bytes, 0, bytes.length);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @author Thomas Darimont
|
||||
*/
|
||||
private static class EntityInstantiatorAdapter implements EntityInstantiator {
|
||||
|
||||
private static final Object[] EMPTY_ARRAY = new Object[0];
|
||||
|
||||
private final ObjectInstantiator instantiator;
|
||||
|
||||
/**
|
||||
* @param instantiator
|
||||
*/
|
||||
public EntityInstantiatorAdapter(ObjectInstantiator instantiator) {
|
||||
this.instantiator = instantiator;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T, E extends PersistentEntity<? extends T, P>, P extends PersistentProperty<P>> T createInstance(E entity,
|
||||
ParameterValueProvider<P> provider) {
|
||||
|
||||
Object[] params = extractInvocationArguments(provider, entity.getPersistenceConstructor());
|
||||
try {
|
||||
return (T) instantiator.newInstance(params);
|
||||
} catch (Exception e) {
|
||||
throw new MappingInstantiationException(entity, Arrays.asList(params), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param provider
|
||||
* @param prefCtor
|
||||
* @return
|
||||
*/
|
||||
private <P extends PersistentProperty<P>, T> Object[] extractInvocationArguments(
|
||||
ParameterValueProvider<P> provider, PreferredConstructor<? extends T, P> prefCtor) {
|
||||
|
||||
if (provider == null || prefCtor == null || !prefCtor.hasParameters()) {
|
||||
return EMPTY_ARRAY;
|
||||
}
|
||||
|
||||
List<Object> params = new ArrayList<Object>();
|
||||
for (Parameter<?, P> parameter : prefCtor.getParameters()) {
|
||||
params.add(provider.getParameterValue(parameter));
|
||||
}
|
||||
|
||||
return params.toArray();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @author Thomas Darimont
|
||||
*/
|
||||
public static interface ObjectInstantiator {
|
||||
|
||||
Object newInstance(Object... args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a new {@link ObjectInstantiator} class for the given custom class.
|
||||
* <p>
|
||||
* This code generator will generate a custom factory class implementing the {@link ObjectInstantiator} interface for
|
||||
* every publicly accessed constructor variant.
|
||||
* <p>
|
||||
* Given a class {@code ObjCtor1ParamString} like:
|
||||
*
|
||||
* <pre>
|
||||
* {
|
||||
* @code
|
||||
* public class ObjCtor1ParamString extends ObjCtorNoArgs {
|
||||
*
|
||||
* public final String param1;
|
||||
*
|
||||
* public ObjCtor1ParamString(String param1) {
|
||||
* this.param1 = param1;
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* The following factory class {@code ObjCtor1ParamString_Instantiator_asdf} is generated:
|
||||
*
|
||||
* <pre>
|
||||
* {
|
||||
* @code
|
||||
* public class ObjCtor1ParamString_Instantiator_asdf implements ObjectInstantiator {
|
||||
*
|
||||
* public Object newInstance(Object... args) {
|
||||
* return new ObjCtor1ParamString((String) args[0]);
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* @author Thomas Darimont
|
||||
*/
|
||||
enum ObjectInstantiatorClassGenerator {
|
||||
|
||||
INSTANCE;
|
||||
|
||||
private static final String INIT = "<init>";
|
||||
private static final String TAG = "_Instantiator_";
|
||||
private static final String JAVA_LANG_OBJECT = "java/lang/Object";
|
||||
private static final String CREATE_METHOD_NAME = "newInstance";
|
||||
|
||||
private static final String[] IMPLEMENTED_INTERFACES = new String[] { Type
|
||||
.getInternalName(ObjectInstantiator.class) };
|
||||
|
||||
private final ByteArrayClassLoader classLoader;
|
||||
|
||||
private ObjectInstantiatorClassGenerator() {
|
||||
|
||||
this.classLoader = AccessController.doPrivileged(new PrivilegedAction<ByteArrayClassLoader>() {
|
||||
public ByteArrayClassLoader run() {
|
||||
return new ByteArrayClassLoader(ObjectInstantiatorClassGenerator.class.getClassLoader());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a new class for the given {@link InstantiatorKey}.
|
||||
*
|
||||
* @param key
|
||||
* @return
|
||||
*/
|
||||
public Class<?> generateCustomInstantiatorClass(PersistentEntity<?, ?> entity) {
|
||||
|
||||
String className = generateClassName(entity);
|
||||
byte[] bytecode = generateBytecode(className, entity);
|
||||
|
||||
return classLoader.loadClass(className, bytecode);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param key
|
||||
* @return
|
||||
*/
|
||||
private String generateClassName(PersistentEntity<?, ?> entity) {
|
||||
return entity.getType().getName() + TAG + Integer.toString(entity.hashCode(), 36);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a new class for the given {@link InstantiatorKey}.
|
||||
*
|
||||
* @param key
|
||||
* @return
|
||||
*/
|
||||
public byte[] generateBytecode(String internalClassName, PersistentEntity<?, ?> entity) {
|
||||
|
||||
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
|
||||
|
||||
cw.visit(Opcodes.V1_6, ACC_PUBLIC + ACC_SUPER, internalClassName.replace('.', '/'), null, JAVA_LANG_OBJECT,
|
||||
IMPLEMENTED_INTERFACES);
|
||||
|
||||
visitDefaultConstructor(cw);
|
||||
|
||||
visitCreateMethod(cw, entity);
|
||||
|
||||
cw.visitEnd();
|
||||
|
||||
return cw.toByteArray();
|
||||
}
|
||||
|
||||
private void visitDefaultConstructor(ClassWriter cw) {
|
||||
|
||||
MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, INIT, "()V", null, null);
|
||||
mv.visitCode();
|
||||
mv.visitVarInsn(ALOAD, 0);
|
||||
mv.visitMethodInsn(INVOKESPECIAL, JAVA_LANG_OBJECT, INIT, "()V", false);
|
||||
mv.visitInsn(RETURN);
|
||||
mv.visitMaxs(0, 0); // (0, 0) = computed via ClassWriter.COMPUTE_MAXS
|
||||
mv.visitEnd();
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts the bytecode definition for the create method for the given {@link PersistentEntity}.
|
||||
*
|
||||
* @param cw
|
||||
* @param entity
|
||||
*/
|
||||
private void visitCreateMethod(ClassWriter cw, PersistentEntity<?, ?> entity) {
|
||||
|
||||
String entityTypeResourcePath = Type.getInternalName(entity.getType());
|
||||
|
||||
MethodVisitor mv = cw.visitMethod(ACC_PUBLIC + ACC_VARARGS, CREATE_METHOD_NAME,
|
||||
"([Ljava/lang/Object;)Ljava/lang/Object;", null, null);
|
||||
mv.visitCode();
|
||||
mv.visitTypeInsn(NEW, entityTypeResourcePath);
|
||||
mv.visitInsn(DUP);
|
||||
|
||||
Constructor<?> ctor = entity.getPersistenceConstructor().getConstructor();
|
||||
Class<?>[] parameterTypes = ctor.getParameterTypes();
|
||||
for (int i = 0; i < parameterTypes.length; i++) {
|
||||
mv.visitVarInsn(ALOAD, 1);
|
||||
|
||||
visitArrayIndex(mv, i);
|
||||
|
||||
mv.visitInsn(AALOAD);
|
||||
|
||||
if (parameterTypes[i].isPrimitive()) {
|
||||
insertUnboxInsns(mv, Type.getType(parameterTypes[i]).toString().charAt(0), "");
|
||||
} else {
|
||||
mv.visitTypeInsn(CHECKCAST, Type.getInternalName(parameterTypes[i]));
|
||||
}
|
||||
}
|
||||
mv.visitMethodInsn(INVOKESPECIAL, entityTypeResourcePath, INIT, Type.getConstructorDescriptor(ctor), false);
|
||||
|
||||
mv.visitInsn(ARETURN);
|
||||
mv.visitMaxs(0, 0); // (0, 0) = computed via ClassWriter.COMPUTE_MAXS
|
||||
mv.visitEnd();
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert an appropriate value on the stack for the given index value {@code idx}.
|
||||
*
|
||||
* @param mv
|
||||
* @param idx
|
||||
*/
|
||||
private static void visitArrayIndex(MethodVisitor mv, int idx) {
|
||||
|
||||
if (idx >= 0 && idx < 6) {
|
||||
mv.visitInsn(ICONST_0 + idx);
|
||||
return;
|
||||
}
|
||||
|
||||
mv.visitLdcInsn(idx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert any necessary cast and value call to convert from a boxed type to a primitive value.
|
||||
* <p>
|
||||
* Taken from Spring Expression 4.1.2 {@code org.springframework.expression.spel.CodeFlow#insertUnboxInsns}.
|
||||
*
|
||||
* @param mv the method visitor into which instructions should be inserted
|
||||
* @param ch the primitive type desired as output
|
||||
* @param stackDescriptor the descriptor of the type on top of the stack
|
||||
*/
|
||||
private static void insertUnboxInsns(MethodVisitor mv, char ch, String stackDescriptor) {
|
||||
|
||||
switch (ch) {
|
||||
case 'Z':
|
||||
if (!stackDescriptor.equals("Ljava/lang/Boolean")) {
|
||||
mv.visitTypeInsn(CHECKCAST, "java/lang/Boolean");
|
||||
}
|
||||
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Boolean", "booleanValue", "()Z", false);
|
||||
break;
|
||||
case 'B':
|
||||
if (!stackDescriptor.equals("Ljava/lang/Byte")) {
|
||||
mv.visitTypeInsn(CHECKCAST, "java/lang/Byte");
|
||||
}
|
||||
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Byte", "byteValue", "()B", false);
|
||||
break;
|
||||
case 'C':
|
||||
if (!stackDescriptor.equals("Ljava/lang/Character")) {
|
||||
mv.visitTypeInsn(CHECKCAST, "java/lang/Character");
|
||||
}
|
||||
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Character", "charValue", "()C", false);
|
||||
break;
|
||||
case 'D':
|
||||
if (!stackDescriptor.equals("Ljava/lang/Double")) {
|
||||
mv.visitTypeInsn(CHECKCAST, "java/lang/Double");
|
||||
}
|
||||
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Double", "doubleValue", "()D", false);
|
||||
break;
|
||||
case 'F':
|
||||
if (!stackDescriptor.equals("Ljava/lang/Float")) {
|
||||
mv.visitTypeInsn(CHECKCAST, "java/lang/Float");
|
||||
}
|
||||
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Float", "floatValue", "()F", false);
|
||||
break;
|
||||
case 'I':
|
||||
if (!stackDescriptor.equals("Ljava/lang/Integer")) {
|
||||
mv.visitTypeInsn(CHECKCAST, "java/lang/Integer");
|
||||
}
|
||||
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Integer", "intValue", "()I", false);
|
||||
break;
|
||||
case 'J':
|
||||
if (!stackDescriptor.equals("Ljava/lang/Long")) {
|
||||
mv.visitTypeInsn(CHECKCAST, "java/lang/Long");
|
||||
}
|
||||
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Long", "longValue", "()J", false);
|
||||
break;
|
||||
case 'S':
|
||||
if (!stackDescriptor.equals("Ljava/lang/Short")) {
|
||||
mv.visitTypeInsn(CHECKCAST, "java/lang/Short");
|
||||
}
|
||||
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Short", "shortValue", "()S", false);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unboxing should not be attempted for descriptor '" + ch + "'");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012 the original author or authors.
|
||||
* Copyright 2012-2014 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -26,6 +26,7 @@ import org.springframework.util.Assert;
|
||||
* default one.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
* @author Thomas Darimont
|
||||
*/
|
||||
public class EntityInstantiators {
|
||||
|
||||
@@ -54,7 +55,7 @@ public class EntityInstantiators {
|
||||
* @param customInstantiators must not be {@literal null}.
|
||||
*/
|
||||
public EntityInstantiators(Map<Class<?>, EntityInstantiator> customInstantiators) {
|
||||
this(ReflectionEntityInstantiator.INSTANCE, customInstantiators);
|
||||
this(BytecodeGeneratingEntityInstantiator.INSTANCE, customInstantiators);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -41,7 +41,7 @@ public interface PersistentEntity<T, P extends PersistentProperty<P>> {
|
||||
* Returns the {@link PreferredConstructor} to be used to instantiate objects of this {@link PersistentEntity}.
|
||||
*
|
||||
* @return {@literal null} in case no suitable constructor for automatic construction can be found. This usually
|
||||
* indicates that the instantiation of the object of tthat persistent entity is done through either a customer
|
||||
* indicates that the instantiation of the object of that persistent entity is done through either a customer
|
||||
* {@link EntityInstantiator} or handled by custom conversion mechanisms entirely.
|
||||
*/
|
||||
PreferredConstructor<T, P> getPersistenceConstructor();
|
||||
|
||||
Reference in New Issue
Block a user