完全支持 POJO 中的 JavaFX 属性

Full support for JavaFX properties in POJOs

关于将 JavaFX 属性 支持添加到现有 POJO classes 的 SO 上有很多问题。那些 classes 的属性可以通过在 javafx.beans.property.adapter package. However, properties created in such way will not reflect changes made using setter methods of POJO classes, unless PropertyChangeSupport 中使用适配器创建被添加到 POJO class。

更改现有的 classes 有时是不可能的,即使可以,如果您有很多 classes,添加 PropertyChangeSupport 也会非常乏味。所以我想分享一种不需要更改现有 classes.

的方法

该解决方案的灵感来自 article by Ben Galbraith, and uses AspectJ。它绝对不需要更改现有模型 classes。安装 AspectJ 超出了本教程的范围,只要说所有主要的 IDE 都支持它就够了(在 Eclipse 中安装它很简单)。

此示例假设您的所有模型 class 都扩展了一个基础 class,在本例中称为 BaseEntity。如果您的实施与此不同,您当然需要调整方面。

首先,我们将创建一个定义 PropertyChangeSupport 所需方法的接口。

package com.mycompany.myapp;

import java.beans.PropertyChangeListener;

public interface ChangeSupport {
    // Add listener for all properties
    public void addPropertyChangeListener(PropertyChangeListener listener);
    // Remove listener for all properties
    public void removePropertyChangeListener(PropertyChangeListener listener);
    // Add listener for specific property
    public void addPropertyChangeListener(String propertyName, PropertyChangeListener listener);
    // Remove listener for specific property
    public void removePropertyChangeListener(String propertyName, PropertyChangeListener listener);
    // Fire change event for specific property
    public void firePropertyChange(String propertyName, Object oldValue, Object newValue);
    // Check if property has any listeners attached
    public boolean hasListeners(String propertyName);
}

接下来,我们将创建该接口的实现。

package com.mycompany.myapp;

import com.mycompany.myapp.model.BaseEntity;

import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;

public class ChangeSupportImpl implements ChangeSupport {
    // Declared transient as there is no need to serialize these fields
    private transient PropertyChangeSupport propertyChangeSupport;
    private final transient Object source;

    public ChangeSupportImpl() {
        super();
        this.source = this;
    }

    // Needed for annotation-style aspect
    public ChangeSupportImpl(final BaseEntity baseEntity) {
        super();
        this.source = baseEntity;
    }

    @Override
    public void addPropertyChangeListener(final PropertyChangeListener listener) {
        // PropertyChangeSupport is loaded lazily
        if (this.propertyChangeSupport == null)
            this.propertyChangeSupport = new PropertyChangeSupport(this.source);
        this.propertyChangeSupport.addPropertyChangeListener(listener);
    }

    @Override
    public void removePropertyChangeListener(final PropertyChangeListener listener) {
        if (this.propertyChangeSupport != null)
            this.propertyChangeSupport.removePropertyChangeListener(listener);
    }

    @Override
    public void addPropertyChangeListener(final String propertyName, final PropertyChangeListener listener) {
        // PropertyChangeSupport is loaded lazily
        if (this.propertyChangeSupport == null)
            this.propertyChangeSupport = new PropertyChangeSupport(this.source);
        this.propertyChangeSupport.addPropertyChangeListener(propertyName, listener);
    }

    @Override
    public void removePropertyChangeListener(final String propertyName, final PropertyChangeListener listener) {
        if (this.propertyChangeSupport != null)
            this.propertyChangeSupport.removePropertyChangeListener(propertyName, listener);
    }

    @Override
    public void firePropertyChange(final String propertyName, final Object oldValue, final Object newValue) {
        if (this.propertyChangeSupport != null)
            this.propertyChangeSupport.firePropertyChange(propertyName, oldValue, newValue);
    }

    @Override
    public boolean hasListeners(final String propertyName) {
        return this.propertyChangeSupport != null && (this.propertyChangeSupport.hasListeners(propertyName)
                || this.propertyChangeSupport.hasListeners(null));
    }
}

最后,我们将创建一个将 PropertyChangeSupport 添加到 BaseEntity class 的方面。该方面使用自定义 class ReflectUtils 来获取 属性 的旧值。您可以使用任何您喜欢的实用程序,或普通的旧 Java 反射(但这可能会影响性能)。

package com.mycompany.myapp;

import com.mycompany.myapp.model.BaseEntity;
import com.mycompany.myapp.util.ReflectUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.DeclareMixin;

import java.util.Objects;

@Aspect
public class BaseEntityObservabilityAspect {
    @DeclareMixin("com.mycompany.myapp.model.BaseEntity")
    public static ChangeSupport createChangeSupportImplementation(final BaseEntity baseEntity) {
        return new ChangeSupportImpl(baseEntity);
    }

    // Intercept setters in all BaseEntity objects in order to notify about property change
    @Around("this(baseEntity) && execution(public void set*(*))")
    public void firePropertyChange(final BaseEntity baseEntity,
            final ProceedingJoinPoint joinPoint) throws Throwable {
        // Get property name from method name
        final String setterName = joinPoint.getSignature().getName();
        final String property = setterName.substring(3, 4).toLowerCase() + setterName.substring(4);
        final ChangeSupport support = (ChangeSupport)baseEntity;
        if (support.hasListeners(property)) {
            // Get old value via reflection
            final Object oldValue = ReflectUtils.invokeGetter(baseEntity, property);

            // Proceed with the invocation of the method
            joinPoint.proceed();

            // New value is the first (and only) argument of this method
            final Object newValue = joinPoint.getArgs()[0];
            // Fire only if value actually changed
            if (!Objects.equals(oldValue, newValue))
                support.firePropertyChange(property, oldValue, newValue);
        } else {
            // No listeners have been registered with BaseEntity, so there is no need to fire property change event
            joinPoint.proceed();
        }
    }
}

如果由于某种原因不能使用注解风格,这里同样使用AspectJ代码风格。

package com.mycompany.myapp;

import java.util.Objects;

import com.mycompany.myapp.model.BaseEntity;
import com.mycompany.myapp.util.ReflectUtils;

public aspect BaseEntityObservabilityAspect {
    declare parents: BaseEntity extends ChangeSupportImpl;

    // Intercept setters in all BaseEntity objects in order to notify about property change
    void around(final BaseEntity entity, final ChangeSupport support):
            this(entity) && this(support) && execution(public void BaseEntity+.set*(*)) {
        // Get property name from method name
        final String setterName = thisJoinPoint.getSignature().getName();
        final String property = setterName.substring(3, 4).toLowerCase() + setterName.substring(4);
        if (support.hasListeners(property)) {
            final Object oldValue;
            try {
                // Get old value via reflection
                oldValue = ReflectUtils.invokeGetter(entity, property);
            } catch (final Throwable e) {
                // Should not happen
                proceed(entity, support);
                return;
            }

            // Proceed with the invocation of the method
            proceed(entity, support);

            // New value is the first (and only) argument of this method
            final Object newValue = thisJoinPoint.getArgs()[0];
            // Fire only if value actually changed
            if (!Objects.equals(oldValue, newValue))
                support.firePropertyChange(property, oldValue, newValue);
        } else {
            // No listeners have been registered with BaseEntity, so there is no need to fire property change event
            proceed(entity, support);
        }
    }
}