Last active
July 13, 2022 07:38
-
-
Save kalgon/7bc9e9b1171b060ecca46335555b72fe to your computer and use it in GitHub Desktop.
Generate observable JavaBean types from interface with ByteBuddy
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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