Skip to content

Instantly share code, notes, and snippets.

@kalgon
Last active July 13, 2022 07:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kalgon/7bc9e9b1171b060ecca46335555b72fe to your computer and use it in GitHub Desktop.
Save kalgon/7bc9e9b1171b060ecca46335555b72fe to your computer and use it in GitHub Desktop.
Generate observable JavaBean types from interface with ByteBuddy
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.modifier.FieldManifestation;
import net.bytebuddy.description.modifier.Visibility;
import net.bytebuddy.dynamic.DynamicType.Builder;
import net.bytebuddy.dynamic.loading.ClassLoadingStrategy;
import net.bytebuddy.dynamic.scaffold.subclass.ConstructorStrategy;
import net.bytebuddy.implementation.FieldAccessor;
import net.bytebuddy.implementation.Implementation;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.implementation.SuperMethodCall;
import net.bytebuddy.matcher.ElementMatchers;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Stream;
public final class JavaBeanTypeFactory {
public interface Customizer {
enum Empty implements Customizer {
INSTANCE
}
final class Composite implements Customizer {
private final Customizer before, after;
Composite(Customizer before, Customizer after) {
this.before = before;
this.after = after;
}
@Override
public String customizeName(String name) {
return after.customizeName(before.customizeName(name));
}
@Override
public Implementation customizeConstructor(Implementation implementation) {
return after.customizeConstructor(before.customizeConstructor(implementation));
}
@Override
public <T> Builder<T> customizeClass(Builder<T> builder) {
return after.customizeClass(before.customizeClass(builder));
}
@Override
public Implementation customizeGetter(Implementation implementation) {
return after.customizeGetter(before.customizeGetter(implementation));
}
@Override
public Implementation customizeSetter(Implementation implementation) {
return after.customizeSetter(before.customizeSetter(implementation));
}
}
default String customizeName(String name) {
return name;
}
default <T> Builder<T> customizeClass(Builder<T> builder) {
return builder;
}
default Implementation customizeConstructor(Implementation implementation) {
return implementation;
}
default Implementation customizeGetter(Implementation implementation) {
return implementation;
}
default Implementation customizeSetter(Implementation implementation) {
return implementation;
}
default Customizer andThen(Customizer after) {
return new Composite(this, after);
}
}
public static final class SerializableCustomizer implements Customizer {
public static final SerializableCustomizer DEFAULT = new SerializableCustomizer(1L);
private final long serial;
public SerializableCustomizer(long serial) {
this.serial = serial;
}
@Override
public <T> Builder<T> customizeClass(Builder<T> builder) {
return builder
.implement(Serializable.class)
.serialVersionUid(this.serial);
}
}
public enum ObservableCustomizer implements Customizer {
INSTANCE;
public interface Observable {
void addPropertyChangeListener(PropertyChangeListener listener);
void removePropertyChangeListener(PropertyChangeListener listener);
void addPropertyChangeListener(String propertyName, PropertyChangeListener listener);
void removePropertyChangeListener(String propertyName, PropertyChangeListener listener);
}
public static final class Initializer {
@Advice.OnMethodExit
public static void initialize(@Advice.FieldValue(value = SUPPORT, readOnly = false) PropertyChangeSupport support, @Advice.This Object target) {
support = new PropertyChangeSupport(target);
}
}
public static final class Setter {
@Advice.OnMethodEnter
public static void set(@Advice.FieldValue(SUPPORT) PropertyChangeSupport support, @Advice.Origin("#p") String property, @Advice.Argument(0) Object newValue, @Advice.FieldValue Object oldValue) {
support.firePropertyChange(property, oldValue, newValue);
}
}
private static final String SUPPORT = "$support";
@Override
public String customizeName(String name) {
return "Observable".concat(name);
}
@Override
public <T> Builder<T> customizeClass(Builder<T> builder) {
return builder
.implement(Observable.class)
.defineField(SUPPORT, PropertyChangeSupport.class, Visibility.PRIVATE, FieldManifestation.FINAL)
.method(ElementMatchers.isDeclaredBy(Observable.class)).intercept(MethodDelegation.toField(SUPPORT));
}
@Override
public Implementation customizeConstructor(Implementation implementation) {
return Advice.to(Initializer.class).wrap(implementation);
}
@Override
public Implementation customizeSetter(Implementation implementation) {
return Advice.to(Setter.class).wrap(implementation);
}
}
private final Map<Class<?>, Class<?>> cache = new ConcurrentHashMap<>();
private final Customizer customizer;
public JavaBeanTypeFactory(Customizer... customizer) {
this.customizer = Stream.of(customizer).reduce(Customizer::andThen).orElse(Customizer.Empty.INSTANCE);
}
public <T> T newInstance(Class<T> type) {
try {
return type.cast(getBeanClass(type).getDeclaredConstructors()[0].newInstance());
} catch (InstantiationException | IllegalAccessException | InvocationTargetException exception) {
throw new RuntimeException("Cannot instantiate bean", exception);
}
}
public <T> Class<? extends T> getBeanClass(Class<T> type) {
return cache.computeIfAbsent(type, this::createBeanClass).asSubclass(type);
}
private <T> Class<? extends T> createBeanClass(Class<T> type) {
Builder<T> builder = new ByteBuddy()
.subclass(type, ConstructorStrategy.Default.NO_CONSTRUCTORS)
.name(type.getName().concat(customizer.customizeName("Bean")))
.defineConstructor().intercept(customizer.customizeConstructor(SuperMethodCall.INSTANCE))
.method(ElementMatchers.isSetter()).intercept(customizer.customizeSetter(FieldAccessor.ofBeanProperty()))
.method(ElementMatchers.isGetter()).intercept(customizer.customizeGetter(FieldAccessor.ofBeanProperty()));
return Stream.of(type.getDeclaredMethods())
.filter(m -> m.getName().startsWith("set") && m.getParameterCount() == 1)
.reduce(customizer.customizeClass(builder), (b, m) -> b.defineField(Character.toLowerCase(m.getName().charAt(3)) + m.getName().substring(4), m.getParameterTypes()[0], Visibility.PRIVATE), (x, y) -> null)
.make()
.load(type.getClassLoader(), ClassLoadingStrategy.Default.INJECTION)
.getLoaded();
}
}
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
public class Main {
interface PersonState {
String getFirstName();
void setFirstName(String firstName);
String getLastName();
void setLastName(String lastName);
}
private static final JavaBeanTypeFactory FACTORY = new JavaBeanTypeFactory(
JavaBeanTypeFactory.SerializableCustomizer.DEFAULT,
JavaBeanTypeFactory.ObservableCustomizer.INSTANCE
);
public static void main(String... args) throws Exception {
PersonState personState = FACTORY.newInstance(PersonState.class);
((JavaBeanTypeFactory.ObservableCustomizer.Observable) personState).addPropertyChangeListener(System.out::println);
personState.setFirstName("John");
personState.setLastName("Doe");
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (ObjectOutput output = new ObjectOutputStream(bytes)) {
output.writeObject(personState);
}
try (ObjectInput input = new ObjectInputStream(new ByteArrayInputStream(bytes.toByteArray()))) {
personState = (PersonState) input.readObject();
}
personState.setFirstName("Jack");
((JavaBeanTypeFactory.ObservableCustomizer.Observable) personState).addPropertyChangeListener(System.out::println);
personState.setFirstName("Jude");
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment