目录
使用jdk的PropertyChangeSupport实现属性监听
背景
java在使用JavaBean的时候,有时我们需要监听属性的变更。例如在访问bean的getter方法,或者调用bean的setter方法时,进行拦截。在不对现有的所有代码进行入侵修改的前提下,有什么方法优雅解决这个问题呢?
JS数据劫持
JS的“数据劫持”提供了一种机制,允许程序对对象数据的访问与修改之前进行拦截。Vue能够在修改模型属性的时候,自动更新视图,使用的是便于JS提供的API,Object.defineProperty()。
例如下面的代码:
var player = {
level : 1,
name : "Tom"
}
Object.keys(player).forEach(function(key){
Object.defineProperty(player,key,{
get:function(){
console.log('访问变量:'+key);
},
set:function(){
console.log('修改变量:'+key);
}
})
});
player.name = "Lucy"
player.level
// 修改变量:name
// 访问变量:level
那么,Java是否也能实现这种机制呢?答案是肯定的。
Java数据挟持
对于一个简单的JavaBeran,如下
public class Player {
private String name;
private int level;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getLevel() {
return level;
}
public void setLevel(int level) {
this.level = level;
}
}
我们怎么监听该对象的所有属性变更呢?jdk提供了一个工具,PropertyChangeSupport类。可以实现这个效果。其实其原理也很简单,就是事件驱动。
使用jdk的PropertyChangeSupport实现属性监听
import java.beans.PropertyChangeSupport;
public class PropertyListener {
public static void main(String[] args) {
Player player = new Player();
PropertyChangeSupport support = new PropertyChangeSupport(player);
// 为player添加属性变更监听器
support.addPropertyChangeListener(listener ->
System.out.println(String.format("对象 [%s]属性发生变更,从%s变为%s",
listener.getPropertyName(), listener.getOldValue(), listener.getNewValue()))
);
player.setName("Tom");
// 手动抛出事件
support.firePropertyChange("name", null, player.getName());
// 程序输出
// 对象 [name]属性发生变更,从null变为Tom
}
}
虽然以上的代码确实实现了属性监听,但不难发现,以上的代码非常麻烦,特别是需要手动调用 support.firePropertyChange()这个方法。想象一下,这意味着我们需要对所有的setter()方法进行修改,这是灾难性的。有没有优雅的方式呢?
熟悉SpringAop原理的同学,很快就想到了面向切面的方法拦截。没错,这就是我们的解决方案。我们可以通过代码织入,在不修改现有javabean的同时,达到目的。
说到AOP,不得不说下java的动态代理。动态代理说到底也就是设计模式中的“代理模式”。该模式要求被代理的对象实现了某一个接口,然后我们生成的动态代理也实现了该接口。这也意味着,我们只能拦截接口的方法,对于非接口方法则黔驴技穷。
使用Cglib实现属性监听
Cglib是一个强大的代码生成类库,允许在运行起拓展java类与实现接口。不同于jdk的动态代理,Cglib动态代理是利用Asm动态生成被代理类的一个子类,然后对父类的所有非final方法进行重写,从而达到方法拦截。
基于此,我们可以把support.firePropertyChange()这个动作写在方法拦截的统一入口,代码如下:
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import org.springframework.util.ReflectionUtils;
import java.beans.PropertyChangeSupport;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class PropertyListenerInterceptor implements MethodInterceptor {
private PropertyChangeSupport support;
public void binding(Object target) {
support = new PropertyChangeSupport(target);
support.addPropertyChangeListener(evt ->
System.out.println(String.format("对象 [%s]属性发生变更,从%s变为%s",
evt.getPropertyName(), evt.getOldValue(), evt.getNewValue()))
);
}
@Override
public Object intercept(Object target, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
if (method.getName().startsWith("set")) {
String fieldName = method.getName().replace("set", "");
fieldName = fieldName.substring(0,1).toLowerCase() + fieldName.substring(1);
Field field = ReflectionUtils.findField(target.getClass(), fieldName);
field.setAccessible(true);
Object oldValue = field.get(target);
Object ret = methodProxy.invokeSuper(target, args);
Object newValue = field.get(target);
support.firePropertyChange(fieldName, oldValue, newValue);
return ret;
} else if (method.getName().startsWith("get")) {
String fieldName = method.getName().replace("get", "");
fieldName = fieldName.substring(0,1).toLowerCase() + fieldName.substring(1);
System.out.println(String.format("访问对象 [%s]属性", fieldName));
}
return methodProxy.invokeSuper(target, args);
}
public static void main(String[] args) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Player.class);
PropertyListenerInterceptor interceptor = new PropertyListenerInterceptor();
enhancer.setCallback(interceptor);
Player player = (Player) enhancer.create();
interceptor.binding(player);
player.setLevel(10);
player.setName("Kitty");
player.getName();
// 程序输出:
// 对象 [level]属性发生变更,从0变为10
// 对象 [name]属性发生变更,从null变为Kitty
// 访问对象 [name]属性
}
}