Skip to content

Instantly share code, notes, and snippets.

@mickroll
Last active December 1, 2020 08:07
Show Gist options
  • Save mickroll/1310eeed7f34a514461b25a470d434db to your computer and use it in GitHub Desktop.
Save mickroll/1310eeed7f34a514461b25a470d434db to your computer and use it in GitHub Desktop.
build plugin for byte-buddy-maven-plugin, enriches bean classfiles with a default constructor. annotations for identifying bean classes are configurable.
package com.github.mickroll.bytebuddy.plugin;
import static net.bytebuddy.matcher.ElementMatchers.is;
import static net.bytebuddy.matcher.ElementMatchers.isAnnotatedWith;
import static net.bytebuddy.matcher.ElementMatchers.isConstructor;
import static net.bytebuddy.matcher.ElementMatchers.isFinal;
import static net.bytebuddy.matcher.ElementMatchers.isPackagePrivate;
import static net.bytebuddy.matcher.ElementMatchers.isProtected;
import static net.bytebuddy.matcher.ElementMatchers.isPublic;
import static net.bytebuddy.matcher.ElementMatchers.isStatic;
import static net.bytebuddy.matcher.ElementMatchers.not;
import static net.bytebuddy.matcher.ElementMatchers.takesNoArguments;
import java.lang.annotation.Annotation;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.inject.Inject;
import net.bytebuddy.asm.MemberAttributeExtension;
import net.bytebuddy.build.BuildLogger;
import net.bytebuddy.build.Plugin;
import net.bytebuddy.description.NamedElement.WithRuntimeName;
import net.bytebuddy.description.annotation.AnnotationDescription;
import net.bytebuddy.description.annotation.AnnotationList;
import net.bytebuddy.description.field.FieldDescription;
import net.bytebuddy.description.field.FieldList;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.method.MethodDescription.InDefinedShape;
import net.bytebuddy.description.method.MethodList;
import net.bytebuddy.description.modifier.Visibility;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.ClassFileLocator;
import net.bytebuddy.dynamic.DynamicType.Builder;
import net.bytebuddy.implementation.FieldAccessor;
import net.bytebuddy.implementation.Implementation.Composable;
import net.bytebuddy.implementation.MethodCall;
/**
* Enriches bean classes for usage in both jacoco and a cdi container.
* <ol>
* <li>an existing unique constructor will be annotated with {@code @Inject}, if this annotation is missing
* <li>a default constructor will be created, if missing (this is needed by jacoco)
* <li>the new constructor initializes all non-static final fields with default values (null, 0, false, ...)
* </ol>
* This is done to all beans, whose annotation hierarchy contains one of the annotations that were configured for the transformation, for example:
*
* <pre>
&lt;transformation>
&lt;plugin>com.github.mickroll.bytebuddy.plugin.BeanDefaultConstructorPlugin&lt;/plugin>
&lt;arguments>
&lt;argument>
&lt;index>0&lt;/index>
&lt;value>javax.enterprise.context.NormalScope, javax.inject.Scope, javax.interceptor.Interceptor&lt;/value>
&lt;/argument>
&lt;/arguments>
&lt;/transformation>
* </pre>
*
* @author mickroll
*/
public class BeanDefaultConstructorPlugin implements Plugin {
/** Filter for existing constructors that are annotated with {@code @Inject}. */
private static final Junction<MethodDescription.InDefinedShape> EXISTING_ANNOTATED_CONSTRUCTOR = isConstructor().and(isAnnotatedWith(Inject.class));
/** Filter for existing constructors that are candidates for automatic annotation with {@code @Inject}. */
private static final Junction<MethodDescription.InDefinedShape> CONSTRUCTOR_ANNOTATE_CANDIDATE_FILTER =
isConstructor().and(not(isAnnotatedWith(Inject.class)).and(isPublic().or(isProtected()).or(isPackagePrivate())));
/** Filter for existing default constructor. */
private static final Junction<MethodDescription.InDefinedShape> EXISTING_DEFAULT_CONSTRUCTOR =
isConstructor().and(takesNoArguments()).and(isPublic().or(isProtected()).or(isPackagePrivate()));
/**
* Filter for existing default constructor in a super class, that may be called by a new constructor.
* <p>
* A package private constructor may of course only be used by a subclass within the same package!
*/
private static final Junction<MethodDescription.InDefinedShape> EXISTING_SUPERCLASS_DEFAULT_CONSTRUCTOR =
isConstructor().and(takesNoArguments()).and(isPublic().or(isProtected()).or(isPackagePrivate()));
/** Filter for fields that have to be initialized by a new constructor. */
private static final Junction<FieldDescription.InDefinedShape> FIELDS_TO_INITIALIZE = isFinal().and(not(isStatic()));
/** {@code @Inject}-Annotation that should be added to existing constructor. */
private static final Annotation INJECT_CONSTRUCTOR_ANNOTATION = new Inject() {
@Override
public Class<? extends Annotation> annotationType() {
return Inject.class;
}
};
/** Annotation to be added to generated default constructor. */
private static final Annotation GENERATED_CONSTRUCTOR_ANNOTATION = new Deprecated() {
@Override
public Class<? extends Annotation> annotationType() {
return Deprecated.class;
}
@Override
public String since() {
return "";
}
@Override
public boolean forRemoval() {
return false;
}
};
private final BuildLogger logger;
private final List<String> beanAnnotationClassNames;
/**
* Initialize BeanConstructorPlugin.
*
* @param classnames class names of annotations that mark a class as a bean
* @param logger for logging to maven console
*/
public BeanDefaultConstructorPlugin(final String classnames, final BuildLogger logger) {
this.logger = logger;
this.beanAnnotationClassNames = classnames == null ? Collections.emptyList()
: Stream.of(classnames.split(",")).map(String::trim).collect(Collectors.toList());
logger.debug("configured bean annotation classes: " + beanAnnotationClassNames);
}
@Override
public boolean matches(final TypeDescription target) {
return !target.isAnnotation()
&& !target.isEnum()
&& !target.isInterface()
&& isAnnotationPresentInAnnotationHierarchy(target, new HashSet<>());
}
@Override
public void close() {
}
@Override
public Builder<?> apply(final Builder<?> origBuilder, final TypeDescription target, final ClassFileLocator classFileLocator) {
Builder<?> builder = origBuilder;
if (target.getDeclaredMethods().filter(EXISTING_ANNOTATED_CONSTRUCTOR).isEmpty()) {
builder = annotateExistingConstructorWithInject(builder, target);
} else {
logger.debug("found @Inject on bean constructor in: " + target.getName());
}
if (!target.getDeclaredMethods().filter(EXISTING_DEFAULT_CONSTRUCTOR).isEmpty()) {
logger.debug("bean already has a default constructor: " + target.getName());
return builder;
}
return createDefaultConstructor(builder, target);
}
/**
* Create default public constructor, if possible.
*
* @param builder Builder
* @param target target class for new constructor
* @return builder with new constructor
*/
private Builder<?> createDefaultConstructor(final Builder<?> builder, final TypeDescription target) {
// find public or protected default super constructor (0..1)
final Optional<InDefinedShape> superClassDefaultConstructor =
target.getSuperClass().asErasure().getDeclaredMethods().filter(EXISTING_SUPERCLASS_DEFAULT_CONSTRUCTOR).stream().findFirst();
if (superClassDefaultConstructor.isEmpty()) {
logger.error("Bean superclass lacks default constructor. Unable to create super() call. Skipping constructor creation on " + target.getName());
return builder;
}
if (superClassDefaultConstructor.get().isPackagePrivate()) {
final String packageName = target.getPackage().getName();
final String superClassPackageName = target.getSuperClass().asErasure().getPackage().getName();
if (!Objects.equals(packageName, superClassPackageName)) {
logger.error(String.format("Cannot call package private constructor of super class in other package.%nthis: %s%nsuper: %s",
target.getName(), target.getSuperClass().asErasure().getName()));
return builder;
}
}
final FieldList<FieldDescription.InDefinedShape> fieldsToInitialize = target.getDeclaredFields().filter(FIELDS_TO_INITIALIZE);
final String fieldNames = fieldsToInitialize.stream().map(WithRuntimeName::getName).collect(Collectors.joining(", "));
logger.debug(String.format("creating default constructor with final fields [%s] for %s", fieldNames, target.getName()));
Composable code = MethodCall.invoke(superClassDefaultConstructor.get());
for (final FieldDescription.InDefinedShape field : fieldsToInitialize) {
code = code.andThen(FieldAccessor.of(field).setsDefaultValue());
}
return builder.defineConstructor(Visibility.PUBLIC).intercept(code).annotateMethod(GENERATED_CONSTRUCTOR_ANNOTATION);
}
/**
* Add {@code @Inject} - annotation to one existing constructor, if one candidate is found.
*
* @param builder Builder
* @param target target class that contains the constructors
* @return builder with annotated constructor
*/
private Builder<?> annotateExistingConstructorWithInject(final Builder<?> builder, final TypeDescription target) {
final MethodList<MethodDescription.InDefinedShape> annotateCandidates = target.getDeclaredMethods().filter(CONSTRUCTOR_ANNOTATE_CANDIDATE_FILTER);
if (annotateCandidates.isEmpty()) {
return builder;
} else if (annotateCandidates.size() > 1) {
final String candidateNames = annotateCandidates.stream().map(ctor -> ctor.asSignatureToken().toString()).collect(Collectors.joining(",\n"));
logger.error("Skipping automatic @Inject annotation, found multiple matching constructors. Please manually annotate one of:\n" + candidateNames);
return builder;
}
final MethodDescription.InDefinedShape injectConstructorCandidate = annotateCandidates.getOnly();
logger.debug("adding @Inject to bean constructor: " + injectConstructorCandidate);
return builder.visit(new MemberAttributeExtension.ForMethod().annotateMethod(INJECT_CONSTRUCTOR_ANNOTATION).on(is(injectConstructorCandidate)));
}
/**
* Looks for the presence of at least one of the given annotations on a type. Also looks at the annotations on those annotations and so on.
*
* @param type the type
* @param alreadyCheckedTypes types that were checked, for skipping multiple occurrences of the same annotation
* @return {@code true}, if at least one of the given annotations is found on the type or on one of its annotations (recursive on all found annotations)
*/
private boolean isAnnotationPresentInAnnotationHierarchy(final TypeDescription type, final Collection<TypeDescription> alreadyCheckedTypes) {
if (alreadyCheckedTypes.contains(type)) {
return false;
}
alreadyCheckedTypes.add(type);
final AnnotationList annotations = type.getDeclaredAnnotations();
return isBeanAnnotationPresent(annotations)
|| annotations.stream().anyMatch(annotation -> isAnnotationPresentInAnnotationHierarchy(annotation.getAnnotationType(), alreadyCheckedTypes));
}
/**
* Determines, if the given AnnotationList contains any of the bean-identifying annotations.
*
* @param annotations AnnotationList
* @return {@code true}, if bean-identifying annotation was found
*/
private boolean isBeanAnnotationPresent(final AnnotationList annotations) {
return annotations.stream()
.map(AnnotationDescription::getAnnotationType)
.map(TypeDescription::getName)
.anyMatch(beanAnnotationClassNames::contains);
}
}
@famod
Copy link

famod commented Nov 30, 2020

@mickroll Any chance to update this to the latest state of our internal plugin? Thanks!

@mickroll
Copy link
Author

mickroll commented Dec 1, 2020

@famod Just updated to the latest state. Only a minor change, now plugin skips enums and interfaces.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment