Решение основано на article от Ben Galbraith и использует AspectJ. Это не требует абсолютно никаких изменений в существующих классах моделей. Установка AspectJ выходит за рамки этого руководства, достаточно сказать, что все основные IDE поддерживают его (установка в Eclipse тривиальна).
В этом примере предполагается, что все ваши классы моделей расширяют базовый класс, называемый 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);
}
Далее мы создадим реализацию этого интерфейса.
Наконец, мы создадим аспект, который добавит свойство PropertyChangeSupport в класс BaseEntity. Аспект использует настраиваемый класс 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);
}
}
}