公司的ORM框架在Model类属性变化时候会记录变化的属性,然后在持久化时候只更新有变化的字段。这个功能之前是通过CGLib实现的,但是cglib在2019年之后就不再维护了,之前的代码也已经无法兼容JDK17和JDK21,所以我决定使用Byte-Buddy来重新实现这个功能。

代码如下:

import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.AbstractList;
import java.util.AbstractMap;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.Callable;
import lombok.Getter;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import lombok.var;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.NamingStrategy;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.implementation.bind.annotation.AllArguments;
import net.bytebuddy.implementation.bind.annotation.Origin;
import net.bytebuddy.implementation.bind.annotation.RuntimeType;
import net.bytebuddy.implementation.bind.annotation.SuperCall;
import net.bytebuddy.implementation.bind.annotation.This;
import net.bytebuddy.matcher.ElementMatchers;
import org.jetbrains.annotations.NotNull;

/**
 * 监听类的属性变化
 *
 * @author p_x_c
 */
@Slf4j
public abstract class ObservableEntity implements Serializable {
    private static final long serialVersionUID = -705252046123130932L;
    private final PropertyChangeSupport pcs = new PropertyChangeSupport(this);

    /**
     * 是否有属性变化
     */
    @Getter
    protected Boolean hasChanged = false;

    /**
     * 有变化的属性名称
     */
    @Getter
    protected Set<String> changedProperties = Sets.newHashSet();

    /**
     * 创建类
     *
     * @param clazz         类Class
     * @param defaultValues 默认属性
     * @param listeners     属性变化时的监听器
     * @return 类实例
     */
    @SneakyThrows
    public static <T> T create(Class<T> clazz, Map<String, Object> defaultValues, PropertyChangeListener... listeners) {
        T instance = new ByteBuddy()
                .with(new NamingStrategy.AbstractBase() {
                    @Override
                    protected @NotNull String name(@NotNull TypeDescription superClass) {
                        return Objects.requireNonNull(superClass.getPackage())
                                      .getName() + ".bytebuddy." + superClass.getSimpleName();
                    }
                })
                .subclass(clazz)
                .method(ElementMatchers.nameStartsWith("set"))
                // .method(ElementMatchers.isSetter())
                .intercept(MethodDelegation.to(SetterInterceptor.class))
                .make()
                .load(clazz.getClassLoader())
                .getLoaded()
                .getConstructor()
                .newInstance();

        if (instance instanceof ObservableEntity) {
            var observableEntity = (ObservableEntity) instance;
            for (PropertyChangeListener listener : listeners) {
                observableEntity.addPropertyChangeListener(listener);
            }

            // 设置默认属性值
            for (Map.Entry<String, Object> entry : defaultValues.entrySet()) {
                val fieldName = entry.getKey();
                var fieldValue = entry.getValue();
                var field = clazz.getDeclaredField(fieldName);
                field.setAccessible(true);

                // Wrap collections
                fieldValue = observableEntity.wrapCollection(fieldValue, fieldName);

                field.set(instance, fieldValue);

                // 手动触发属性变化事件
                observableEntity.firePropertyChange(fieldName, null, fieldValue);
            }
        }

        return instance;
    }

    /**
     * 创建类,默认打印属性变化
     *
     * @param clazz 类Class
     * @return 类实例
     */
    public static <T> T create(Class<T> clazz) {
        return create(clazz, Maps.newHashMap());
    }

    /**
     * 创建类,默认打印属性变化
     *
     * @param clazz         类Class
     * @param defaultValues 类的默认值
     * @return
     */
    public static <T> T create(Class<T> clazz, Map<String, Object> defaultValues) {
        return create(clazz, defaultValues, evt -> log.debug("Property changed: " + evt.getPropertyName() + " Old value: " + evt.getOldValue() + " New value: " + evt.getNewValue()));
    }

    /**
     * 添加属性变化监听器
     *
     * @param listener
     */
    private void addPropertyChangeListener(PropertyChangeListener listener) {
        pcs.addPropertyChangeListener(listener);
    }

    /**
     * 包装集合类型的属性,防止List和Map属性变动监听不到
     *
     * @param value        现有集合对象
     * @param propertyName 属性名
     * @param <T>
     * @return
     */
    @SuppressWarnings("unchecked")
    private <T> T wrapCollection(T value, String propertyName) {
        if (value instanceof List) {
            return (T) new ObservableList<>((List<?>) value, pcs, propertyName);
        } else if (value instanceof Map) {
            return (T) new ObservableMap<>((Map<?, ?>) value, pcs, propertyName);
        }
        return value;
    }

    /**
     * 触发属性变化事件
     *
     * @param propertyName 属性名
     * @param oldValue     旧值
     * @param newValue     新值
     */
    private void firePropertyChange(String propertyName, Object oldValue, Object newValue) {
        pcs.firePropertyChange(propertyName, oldValue, newValue);
        changedProperties.add(propertyName);
        hasChanged = true;
    }

    /**
     * set方法拦截器
     */
    public static class SetterInterceptor {
        @RuntimeType
        public static Object intercept(
                @This Object obj, // 注入被拦截的目标对象。
                @Origin Method method, // 目标方法
                @SuperCall Callable<?> zuper,  // 调用目标方法,必不可少哦
                @AllArguments Object[] args // 注入目标方法的全部参数
        ) throws Exception {
            val sourceName = method.getName()
                                   .substring(3);
            val propertyName = Character.toLowerCase(sourceName.charAt(0)) + sourceName.substring(1);

            var getter = obj.getClass()
                            .getMethod("get" + sourceName);
            val oldValue = getter.invoke(obj);
            val returnValue = zuper.call(); // 调用目标方法
            var newValue = getter.invoke(obj);

            if (!Objects.equals(oldValue, newValue)) {
                // Wrap collections
                if (newValue instanceof List || newValue instanceof Map) {
                    newValue = ((ObservableEntity) obj).wrapCollection(newValue, propertyName);
                    val optional = Arrays.stream(obj.getClass()
                                                    .getMethods())
                                         .filter(x -> x.getName()
                                                       .equals("set" + sourceName))
                                         .findAny();
                    if (optional.isPresent()) {
                        optional.get()
                                .invoke(obj, newValue);
                    }
                }
                ((ObservableEntity) obj).firePropertyChange(propertyName, oldValue, newValue);
            }

            return returnValue;
        }
    }

    /**
     * 包装List
     *
     * @param <E>
     */
    static class ObservableList<E> extends AbstractList<E> {
        private final List<E> list;
        private final PropertyChangeSupport pcs;
        private final String propertyName;


        public ObservableList(List<E> list, PropertyChangeSupport pcs, String propertyName) {
            this.list = list;
            this.pcs = pcs;
            this.propertyName = propertyName;
        }

        @Override
        public E get(int index) {
            return list.get(index);
        }

        @Override
        public E set(int index, E element) {
            E oldValue = list.set(index, element);
            pcs.firePropertyChange(propertyName, oldValue, element);
            return oldValue;
        }

        @Override
        public void add(int index, E element) {
            list.add(index, element);
            pcs.firePropertyChange(propertyName, null, element);
        }

        @Override
        public E remove(int index) {
            E oldValue = list.remove(index);
            pcs.firePropertyChange(propertyName, oldValue, null);
            return oldValue;
        }

        @Override
        public int size() {
            return list.size();
        }
    }

    /**
     * 包装Map
     *
     * @param <K>
     * @param <V>
     */
    static class ObservableMap<K, V> extends AbstractMap<K, V> {
        private final Map<K, V> map;
        private final PropertyChangeSupport pcs;
        private final String propertyName;

        public ObservableMap(Map<K, V> map, PropertyChangeSupport pcs, String propertyName) {
            this.map = map;
            this.pcs = pcs;
            this.propertyName = propertyName;
        }

        @Override
        public V put(K key, V value) {
            V oldValue = map.put(key, value);
            pcs.firePropertyChange(propertyName, oldValue, value);
            return oldValue;
        }

        @Override
        public V remove(Object key) {
            V oldValue = map.remove(key);
            pcs.firePropertyChange(propertyName, oldValue, null);
            return oldValue;
        }

        @Override
        public @NotNull Set<Entry<K, V>> entrySet() {
            return map.entrySet();
        }
    }
}

使用方式:

@Data
@Accessors(chain = true)
public class Person extends ObservableEntity {

    private String name;
    private int age;
    private List<String> hobbies;
    private Map<String, String> attributes;

    public static void main(String[] args) {
        Map<String, Object> defaultValues = new HashMap<>();
        defaultValues.put(
                "name", "Default Name");
        defaultValues.put("age", 25);
        defaultValues.put("hobbies", Lists.newArrayList("a"));
        defaultValues.put("attributes", Maps.newHashMap());

        Person person = Person.create(Person.class, defaultValues);
        System.out.println(person.getClass()
                                 .getName());
        Person person2 = Person.create(Person.class);
        System.out.println(person.getChangedProperties());
        System.out.println(person.getHasChanged());
        System.out.println(person2.getHasChanged());
        System.out.println(person2.getChangedProperties());
        person2.setName("ccc");
        System.out.println(person2.getChangedProperties());
        System.out.println("Name: " + person.getName());
        System.out.println("Age: " + person.getAge());
        System.out.println("Hobbies: " + person.getHobbies());
        System.out.println("Attributes: " + person.getAttributes());

        person.setName("Alice");
        person.setAge(30);
        person.setHobbies(Lists.newArrayList("Swimming"));
        System.out.println("Hobbies: " + person.getHobbies());
        person.getHobbies()
              .add("Pingpong");  // This will trigger a property change event
        System.out.println("Hobbies: " + person.getHobbies());
        person.getHobbies()
              .set(0, "x");
        person.setAttributes(Maps.newHashMap());
        System.out.println("Attributes: " + person.getAttributes());
        person.getAttributes()
              .put("c", "c");
        System.out.println("Hobbies: " + person.getHobbies());
        person.getHobbies()
              .addAll(Lists.newArrayList("xx"));
        System.out.println("Hobbies: " + person.getHobbies());
        person.getHobbies()
              .clear();
        System.out.println("Hobbies: " + person.getHobbies());
        System.out.println(person.getChangedProperties());
        System.out.println(person.getHasChanged());
        System.out.println(person2.getHasChanged());
        System.out.println(person2.getChangedProperties());
    }
}