package ru.vyarus.guice.ext.core.generator;
import com.google.common.base.Preconditions;
import com.google.inject.ImplementedBy;
import com.google.inject.Inject;
import com.google.inject.ProvidedBy;
import com.google.inject.internal.Annotations;
import javassist.*;
import javassist.bytecode.*;
import javassist.bytecode.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;
/**
* Dynamically generates new class from abstract class or interface.
* Resulted class may be used as implementation in guice.
* Generated class will be tied to class loader of original type.
* It is safe to to use with dynamic class loaders (like play dev mode).
* <p>Guice will be able to apply aop to generated class methods. Annotations are copied from original class,
* to let aop mechanisms work even for interfaces. As a result you may forget about class generation and think
* of abstract class or interface as usual guice bean.</p>
* <p>Abstract methods will not be implemented: abstract method call error will be thrown if you try to call it.
* All abstract methods must be covered by guice aop.</p>
* <p>It may be used directly in guice module to register interface or abstract class:
* {@code bind(MyType.class).to(DynamicClassGenerator.generate(MyType.class))}.
* Another option is to use {@link com.google.inject.internal.DynamicClassProvider}: annotate type with
* {@code @ProvidedBy(DynamicClassProvider.class)} and either rely on JIT (don't register type at all) or
* simply register type: @{code bind(MyType.class)}.
* If used with injectors hierarchy or within private modules, use "anchor" dependency to prevent bubbling up
* for resulted binding (see for example anchor implementation for use with {@code @ProvidedBy} annotation
* {@link ru.vyarus.guice.ext.core.generator.anchor.GeneratorAnchorModule}).</p>
* <p>Don't use scope annotations directly - instead wrap them into
* {@link ru.vyarus.guice.ext.core.generator.ScopeAnnotation}, because guice doesn't allow scope definition on
* abstract types.</p>
*
* @author Vyacheslav Rusakov
* @see com.google.inject.internal.DynamicClassProvider
* @since 10.12.2014
*/
@SuppressWarnings("checkstyle:ClassDataAbstractionCoupling")
public final class DynamicClassGenerator {
/**
* Postfix applied to interface or abstract class name to get generated class name.
*/
public static final String DYNAMIC_CLASS_POSTFIX = "$GuiceDynamicClass";
private DynamicClassGenerator() {
}
/**
* Shortcut for {@link #generate(Class, Class, Class)} method to create default scoped classes.
* <p>
* Method is thread safe.
*
* @param type interface or abstract class
* @param <T> type
* @return implementation class for provided type (will not generate if class already exist)
*/
public static <T> Class<T> generate(final Class<T> type) {
return generate(type, null);
}
/**
* Shortcut for {@link #generate(Class, Class, Class)} method to create classes with provided scope
* (and without extra anchor).
* <p>
* Method is thread safe.
*
* @param type interface or abstract class
* @param scope scope annotation to apply on generated class (may be null for default prototype scope)
* @param <T> type
* @return implementation class for provided type (will not generate if class already exist)
*/
public static <T> Class<T> generate(final Class<T> type,
final Class<? extends java.lang.annotation.Annotation> scope) {
return generate(type, scope, null);
}
/**
* Generates dynamic class, which guice may use as implementation and generate proxy above it,
* correctly applying aop features.
* <p>
* New class will inherit type annotations and constructor with annotations
* (if base class use constructor injection). Also constructor inherits all annotations, including
* parameters annotations. If anchor is provided then it will be added as last constructor parameter
* or (when abstract type has no constructor) new constructor added with one parameter (anchor).
* <p>
* Method is thread safe.
*
* @param type interface or abstract class
* @param scope scope annotation to apply on generated class (may be null for default prototype scope)
* @param anchor existing binding to depend generated class on (to prevent binding bubbling up to root injector)
* @param <T> type
* @return implementation class for provided type (will not generate if class already exist)
* @see ru.vyarus.guice.ext.core.generator.anchor.GeneratorAnchorModule for more details about anchor usage
*/
@SuppressWarnings("unchecked")
public static <T> Class<T> generate(final Class<T> type,
final Class<? extends java.lang.annotation.Annotation> scope,
final Class<?> anchor) {
Preconditions.checkNotNull(type, "Original type required");
Preconditions.checkArgument(type.isInterface() || Modifier.isAbstract(type.getModifiers()),
"Type must be interface or abstract class, but provided type is not: %s", type.getName());
final String targetClassName = type.getName() + DYNAMIC_CLASS_POSTFIX;
final ClassLoader classLoader = type.getClassLoader();
/*
* Synchronization is required to avoid double generation and consequent problems.
* Very unlikely that this method would be called too often and synchronization become bottleneck.
* Using original class as monitor to allow concurrent generation for different classes.
*/
synchronized (type) {
Class<?> targetClass;
try {
// will work if class was already generated
targetClass = classLoader.loadClass(targetClassName);
} catch (ClassNotFoundException ex) {
targetClass = generateClass(type, targetClassName, classLoader, scope, anchor);
}
return (Class<T>) targetClass;
}
}
@SuppressWarnings("unchecked")
private static Class<?> generateClass(final Class<?> type, final String targetClassName,
final ClassLoader classLoader,
final Class<? extends java.lang.annotation.Annotation> scope,
final Class<?> anchor) {
try {
// have to use custom pool because original type classloader could be thrown away
// and all cached CtClass objects would be stale
final ClassPool classPool = new ClassPool();
classPool.appendClassPath(new LoaderClassPath(classLoader));
final CtClass impl = generateCtClass(classPool, targetClassName, type, scope, anchor);
return impl.toClass(classLoader, null);
} catch (Exception ex) {
throw new DynamicClassException("Failed to generate class for " + type.getName(), ex);
}
}
private static CtClass generateCtClass(final ClassPool classPool, final String targetClassName, final Class type,
final Class<? extends java.lang.annotation.Annotation> scope,
final Class<?> anchor)
throws Exception {
final CtClass ctType = classPool.get(type.getName());
final CtClass ctAnchor = anchor == null ? null : classPool.getCtClass(anchor.getName());
final CtClass impl;
if (type.isInterface()) {
impl = classPool.makeClass(targetClassName);
impl.addInterface(ctType);
} else {
impl = classPool.makeClass(targetClassName, ctType);
final Constructor diConstructor = findDIConstructor(type);
if (diConstructor != null) {
copyConstructor(impl, ctType, diConstructor, ctAnchor);
}
}
if (anchor != null && impl.getConstructors().length == 0) {
// create new constructor with anchor dependency
createAnchorConstructor(impl, ctAnchor);
}
final ConstPool constPool = impl.getClassFile().getConstPool();
final AnnotationsAttribute annotations = copyAnnotations(classPool, constPool, type);
impl.getClassFile().addAttribute(annotations);
applyScopeAnnotation(classPool, annotations, type, scope);
return impl;
}
private static void copyConstructor(final CtClass impl, final CtClass ctType, final Constructor ctor,
final CtClass anchor) throws Exception {
final ClassPool classPool = impl.getClassPool();
final CtClass[] parameters = JavassistUtils.convertTypes(classPool, ctor.getParameterTypes());
final CtConstructor ctConstructor = CtNewConstructor.make(
parameters,
JavassistUtils.convertTypes(classPool, ctor.getExceptionTypes()),
CtNewConstructor.PASS_PARAMS, null, null, impl);
if (anchor != null) {
ctConstructor.addParameter(anchor);
}
final ConstPool constPool = impl.getClassFile().getConstPool();
final MethodInfo methodInfo = ctConstructor.getMethodInfo();
methodInfo.addAttribute(copyAnnotations(classPool, constPool, ctor));
methodInfo.addAttribute(copyConstructorParametersAnnotations(classPool, constPool, ctor, anchor != null));
final SignatureAttribute info = copyConstructorGenericsSignature(constPool, parameters, ctType, anchor);
if (info != null) {
methodInfo.addAttribute(info);
}
impl.addConstructor(ctConstructor);
}
private static void createAnchorConstructor(final CtClass impl, final CtClass anchor)
throws Exception {
final ClassPool classPool = impl.getClassPool();
final CtConstructor ctConstructor = CtNewConstructor.make(
new CtClass[]{anchor},
null,
CtNewConstructor.PASS_NONE, null, null, impl);
final ConstPool constPool = impl.getClassFile().getConstPool();
final MethodInfo methodInfo = ctConstructor.getMethodInfo();
// add injection annotation
final AnnotationsAttribute attr = new AnnotationsAttribute(constPool, AnnotationsAttribute.visibleTag);
final Annotation annotation = new Annotation(constPool,
classPool.get(javax.inject.Inject.class.getName()));
attr.addAnnotation(annotation);
methodInfo.addAttribute(attr);
impl.addConstructor(ctConstructor);
}
private static Constructor findDIConstructor(final Class<?> type) {
Constructor target = null;
for (Constructor ctor : type.getConstructors()) {
if (ctor.isAnnotationPresent(Inject.class) || ctor.isAnnotationPresent(javax.inject.Inject.class)) {
target = ctor;
break;
}
}
return target;
}
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
private static ParameterAnnotationsAttribute copyConstructorParametersAnnotations(
final ClassPool classPool, final ConstPool constPool, final Constructor ctor,
final boolean anchorAdded) throws Exception {
final int count = ctor.getParameterTypes().length;
final Annotation[][] paramAnnotations = new Annotation[count + (anchorAdded ? 1 : 0)][];
for (int i = 0; i < count; i++) {
final java.lang.annotation.Annotation[] anns = ctor.getParameterAnnotations()[i];
paramAnnotations[i] = new Annotation[anns.length];
for (int j = 0; j < anns.length; j++) {
paramAnnotations[i][j] = JavassistUtils.copyAnnotation(classPool, constPool, anns[j]);
}
}
if (anchorAdded) {
paramAnnotations[count] = new Annotation[0];
}
final ParameterAnnotationsAttribute paramAnns = new ParameterAnnotationsAttribute(
constPool, ParameterAnnotationsAttribute.visibleTag);
paramAnns.setAnnotations(paramAnnotations);
return paramAnns;
}
@SuppressWarnings("PMD.UnusedPrivateMethod")
private static void applyScopeAnnotation(final ClassPool classPool, final AnnotationsAttribute annotations,
final AnnotatedElement source,
final Class<? extends java.lang.annotation.Annotation> scope)
throws Exception {
if (scope != null) {
Preconditions.checkState(Annotations.isScopeAnnotation(scope),
"Provided annotation %s is not scope annotation", scope.getSimpleName());
for (java.lang.annotation.Annotation ann : source.getAnnotations()) {
Preconditions.checkArgument(!(ann instanceof ScopeAnnotation),
"Duplicate scope definition: scope is specified as %s and also defined "
+ "in @ScopeAnnotation.", scope.getSimpleName());
}
annotations.addAnnotation(new Annotation(annotations.getConstPool(), classPool.get(scope.getName())));
}
}
private static AnnotationsAttribute copyAnnotations(final ClassPool classPool, final ConstPool constPool,
final AnnotatedElement source) throws Exception {
final AnnotationsAttribute attr = new AnnotationsAttribute(constPool, AnnotationsAttribute.visibleTag);
if (source.getAnnotations().length > 0) {
for (java.lang.annotation.Annotation ann : source.getAnnotations()) {
final Annotation annotation = processAnnotation(classPool, constPool, ann);
if (annotation != null) {
attr.addAnnotation(annotation);
}
}
}
return attr;
}
private static Annotation processAnnotation(final ClassPool classPool, final ConstPool constPool,
final java.lang.annotation.Annotation ann) throws Exception {
Annotation res = null;
// if we copy these annotation guice will go to infinite loop
if (!(ann instanceof ProvidedBy || ann instanceof ImplementedBy)) {
Preconditions.checkState(!Annotations.isScopeAnnotation(ann.annotationType()),
"Don't use scope annotations directly - use @ScopeAnnotation(TargetScope) wrapper, "
+ "because guice doesn't allow scope annotations on abstract types");
if (ann instanceof ScopeAnnotation) {
res = new Annotation(constPool,
classPool.get(((ScopeAnnotation) ann).value().getName()));
} else {
res = JavassistUtils.copyAnnotation(classPool, constPool, ann);
}
}
return res;
}
private static SignatureAttribute copyConstructorGenericsSignature(
final ConstPool constPool, final CtClass[] params, final CtClass source, final CtClass anchor)
throws Exception {
final CtConstructor ctConstructor = source.getConstructor(Descriptor.ofConstructor(params));
String signature = null;
for (Object attr : ctConstructor.getMethodInfo().getAttributes()) {
if (attr instanceof SignatureAttribute) {
signature = ((SignatureAttribute) attr).getSignature();
break;
}
}
if (signature != null && anchor != null) {
// add anchor to generics signature
final String type = "L" + (anchor.getName().replaceAll("\\.", "/")) + ";";
final int idx = signature.lastIndexOf(')');
signature = signature.substring(0, idx) + type + signature.substring(idx);
}
return signature == null ? null : new SignatureAttribute(constPool, signature);
}
}