net.bytebuddy.build.CachedReturnPlugin Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of byte-buddy-dep Show documentation
Show all versions of byte-buddy-dep Show documentation
Byte Buddy is a Java library for creating Java classes at run time.
This artifact is a build of Byte Buddy with a remaining dependency onto ASM.
You should never depend on this module without repackaging Byte Buddy and ASM into your own namespace.
/*
* Copyright 2014 - Present Rafael Winterhalter
*
* 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 net.bytebuddy.build;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.modifier.FieldPersistence;
import net.bytebuddy.description.modifier.Ownership;
import net.bytebuddy.description.modifier.SyntheticState;
import net.bytebuddy.description.modifier.Visibility;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.ClassFileLocator;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.implementation.bytecode.assign.Assigner;
import net.bytebuddy.pool.TypePool;
import net.bytebuddy.utility.RandomString;
import java.lang.annotation.*;
import java.util.HashMap;
import java.util.Map;
import static net.bytebuddy.matcher.ElementMatchers.*;
/**
* A plugin that caches the return value of a method in a synthetic field. The caching mechanism is not thread-safe but can be used in a
* concurrent setup if the cached value is frozen, i.e. only defines {@code final} fields. In this context, it is possible that
* the method is executed multiple times by different threads but at the same time, this approach avoids a {@code volatile} field
* declaration. For methods with a primitive return type, the type's default value is used to indicate that a method was not yet invoked.
* For methods that return a reference type, {@code null} is used as an indicator. If a method returns such a value, this mechanism will
* not work. This plugin does not need to be closed.
*/
@HashCodeAndEqualsPlugin.Enhance
public class CachedReturnPlugin extends Plugin.ForElementMatcher implements Plugin.Factory {
/**
* An infix between a field and the random suffix if no field name is chosen.
*/
private static final String NAME_INFIX = "_";
/**
* The infix symbol for advice classes.
*/
private static final String ADVICE_INFIX = "$Advice$";
/**
* A description of the {@link Enhance#value()} method.
*/
private static final MethodDescription.InDefinedShape ENHANCE_VALUE = TypeDescription.ForLoadedType.of(Enhance.class)
.getDeclaredMethods()
.filter(named("value"))
.getOnly();
/**
* {@code true} if existing fields should be ignored if the field name was explicitly given.
*/
private final boolean ignoreExistingFields;
/**
* A random string to use for avoid field name collisions.
*/
@HashCodeAndEqualsPlugin.ValueHandling(HashCodeAndEqualsPlugin.ValueHandling.Sort.IGNORE)
private final RandomString randomString;
/**
* The class file locator to use.
*/
private final ClassFileLocator classFileLocator;
/**
* A map of advice types mapped by their argument type. All advice types are precompiled using Java 6 to allow
* for releasing Byte Buddy with a Java 5 byte code level where compiled classes do not contain stack map frames.
* Byte Buddy filters stack map frames when applying advice in newer version but it cannot add stack map frames
* without explicit frame computation which is expensive which is why precompilation was used. To avoid loading
* Java classes in incompatible versions, all advice types are resolved using a type pool.
*/
@HashCodeAndEqualsPlugin.ValueHandling(HashCodeAndEqualsPlugin.ValueHandling.Sort.IGNORE)
private final Map adviceByType;
/**
* Creates a plugin for caching method return values. If a field name exists before applying this plugin, an exception is raised.
*/
public CachedReturnPlugin() {
this(false);
}
/**
* Creates a plugin for caching method return values.
*
* @param ignoreExistingFields {@code true} if existing fields should be ignored if the field name was explicitly given.
*/
public CachedReturnPlugin(boolean ignoreExistingFields) {
super(declaresMethod(isAnnotatedWith(Enhance.class)));
this.ignoreExistingFields = ignoreExistingFields;
randomString = new RandomString();
classFileLocator = ClassFileLocator.ForClassLoader.of(CachedReturnPlugin.class.getClassLoader());
TypePool typePool = TypePool.Default.of(classFileLocator);
adviceByType = new HashMap();
for (Class> type : new Class>[]{
boolean.class,
byte.class,
short.class,
char.class,
int.class,
long.class,
float.class,
double.class,
Object.class
}) {
adviceByType.put(TypeDescription.ForLoadedType.ForLoadedType.of(type), typePool.describe(CachedReturnPlugin.class.getName()
+ ADVICE_INFIX
+ type.getSimpleName()).resolve());
}
}
/**
* {@inheritDoc}
*/
public Plugin make() {
return this;
}
/**
* {@inheritDoc}
*/
@SuppressFBWarnings(value = "NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE", justification = "Annotation presence is required by matcher.")
public DynamicType.Builder> apply(DynamicType.Builder> builder, TypeDescription typeDescription, ClassFileLocator classFileLocator) {
for (MethodDescription.InDefinedShape methodDescription : typeDescription.getDeclaredMethods()
.filter(not(isBridge()).and(isAnnotatedWith(Enhance.class)))) {
if (methodDescription.isAbstract()) {
throw new IllegalStateException("Cannot cache the value of an abstract method: " + methodDescription);
} else if (!methodDescription.getParameters().isEmpty()) {
throw new IllegalStateException("Cannot cache the value of a method with parameters: " + methodDescription);
} else if (methodDescription.getReturnType().represents(void.class)) {
throw new IllegalStateException("Cannot cache void result for " + methodDescription);
}
String name = methodDescription.getDeclaredAnnotations().ofType(Enhance.class)
.getValue(ENHANCE_VALUE)
.resolve(String.class);
if (name.length() == 0) {
name = methodDescription.getName() + NAME_INFIX + randomString.nextString();
} else if (ignoreExistingFields && !typeDescription.getDeclaredFields().filter(named(name)).isEmpty()) {
return builder;
}
builder = builder
.defineField(name, methodDescription.getReturnType().asErasure(), methodDescription.isStatic()
? Ownership.STATIC
: Ownership.MEMBER, methodDescription.isStatic()
? FieldPersistence.PLAIN
: FieldPersistence.TRANSIENT, Visibility.PRIVATE, SyntheticState.SYNTHETIC)
.visit(Advice.withCustomMapping()
.bind(CacheField.class, new CacheFieldOffsetMapping(name))
.to(adviceByType.get(methodDescription.getReturnType().isPrimitive()
? methodDescription.getReturnType().asErasure()
: TypeDescription.ForLoadedType.of(Object.class)), this.classFileLocator)
.on(is(methodDescription)));
}
return builder;
}
/**
* {@inheritDoc}
*/
public void close() {
/* do nothing */
}
/**
* Indicates methods that should be cached, i.e. where the return value is stored in a synthetic field. For this to be
* possible, the returned value should not be altered and the instance must be thread-safe if the value might be used from
* multiple threads.
*/
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Enhance {
/**
* The fields name or an empty string if the name should be generated randomly.
*
* @return The fields name or an empty string if the name should be generated randomly.
*/
String value() default "";
}
/**
* Indicates the field that stores the cached value.
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
protected @interface CacheField {
/* empty */
}
/**
* An offset mapping for the cached field.
*/
@HashCodeAndEqualsPlugin.Enhance
protected static class CacheFieldOffsetMapping implements Advice.OffsetMapping {
/**
* The field's name.
*/
private final String name;
/**
* Creates an offset mapping for the cached field.
*
* @param name The field's name.
*/
protected CacheFieldOffsetMapping(String name) {
this.name = name;
}
/**
* {@inheritDoc}
*/
public Target resolve(TypeDescription instrumentedType,
MethodDescription instrumentedMethod,
Assigner assigner,
Advice.ArgumentHandler argumentHandler,
Sort sort) {
return new Target.ForField.ReadWrite(instrumentedType.getDeclaredFields().filter(named(name)).getOnly());
}
}
}