Java实现数据劫持——监听属性变更

目录

 

背景

JS数据劫持

Java数据挟持

使用jdk的PropertyChangeSupport实现属性监听

使用Cglib实现属性监听


背景

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]属性
    }
}

 

上一篇:将对象转换成map打印到控制台方法


下一篇:C#反射获取属性值和设置属性值