/*
* Copyright 2014 - 2020 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 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 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 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, 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. /**
* 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.
/**
* 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.
/**
* The infix symbol for advice classes.
*/
private static final String ADVICE_INFIX = "$";
A random string to use for avoid field name collisions.
/**
* 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.
/**
* 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.
/**
* 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<TypeDescription, TypeDescription> adviceByType;
Creates a plugin for caching method return values.
/**
* Creates a plugin for caching method return values.
*/
public CachedReturnPlugin() {
super(declaresMethod(isAnnotatedWith(Enhance.class)));
randomString = new RandomString();
classFileLocator = ClassFileLocator.ForClassLoader.of(CachedReturnPlugin.class.getClassLoader());
TypePool typePool = TypePool.Default.of(classFileLocator);
adviceByType = new HashMap<TypeDescription, TypeDescription>();
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}
/**
* {@inheritDoc}
*/
public Plugin make() {
return this;
}
{@inheritDoc}
/**
* {@inheritDoc}
*/
public DynamicType.Builder<?> apply(DynamicType.Builder<?> builder, TypeDescription typeDescription, ClassFileLocator classFileLocator) {
for (MethodDescription.InDefinedShape methodDescription : typeDescription.getDeclaredMethods()
.filter(not(isBridge()).<MethodDescription>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).load().value();
if (name.length() == 0) {
name = methodDescription.getName() + NAME_INFIX + randomString.nextString();
}
builder = builder
.defineField(name, methodDescription.getReturnType().asErasure(), methodDescription.isStatic()
? Ownership.STATIC
: Ownership.MEMBER, Visibility.PRIVATE, SyntheticState.SYNTHETIC, FieldPersistence.TRANSIENT)
.visit(Advice.withCustomMapping()
.bind(CacheField.class, new CacheFieldOffsetMapping(name))
.to(adviceByType.get(methodDescription.getReturnType().isPrimitive()
? methodDescription.getReturnType().asErasure()
: TypeDescription.OBJECT), this.classFileLocator)
.on(is(methodDescription)));
}
return builder;
}
{@inheritDoc}
/**
* {@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.
/**
* 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.
Returns: The fields name or an empty string if the name should be generated randomly.
/**
* 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.
/**
* 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.
/**
* An offset mapping for the cached field.
*/
@HashCodeAndEqualsPlugin.Enhance
protected static class CacheFieldOffsetMapping implements Advice.OffsetMapping {
The field's name.
/**
* The field's name.
*/
private final String name;
Creates an offset mapping for the cached field.
Params: - name – The field's name.
/**
* Creates an offset mapping for the cached field.
*
* @param name The field's name.
*/
protected CacheFieldOffsetMapping(String name) {
this.name = name;
}
{@inheritDoc}
/**
* {@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());
}
}
}