Bootstrap

apache的BeanUtils的Converter被相互污染覆盖问题

问题描述

apache的BeanUtils工具集中用来把map对象转换为java对象的BeanUtils#populate方法会因为单例的原因其转换器Converter被相互污染覆盖问题

maven依赖

<dependency>
    <groupId>commons-beanutils</groupId>
    <artifactId>commons-beanutils</artifactId>
    <version>1.9.4</version>
</dependency>

方法源码


public static void populate(final Object bean, final Map<String, ? extends Object> properties)
    throws IllegalAccessException, InvocationTargetException {
 
    // 源码此处获取的是单例对象
    BeanUtilsBean.getInstance().populate(bean, properties);
}

测试场景

场景一

map不存在属性时

测试代码

/**
 * map没有age属性
 */
public static void test1() throws Exception {
    DemoObj fromObj = new DemoObj();
    Map<String, Object> map = Maps.newHashMap();
    map.put("name", "张三");
 
    BeanUtils.populate(fromObj, map);
    System.out.println(Thread.currentThread().getName() + ":没有age属性,转换器不生效:"+ JsonUtil.toStr(fromObj));
}

输出结果

main:没有age属性,转换器不生效:{"age":null,"name":"张三"}

场景二

map存在age属性,自带的转换器默认值为0

测试代码


/**
 * map存在age属性,自带的转换器默认值为0
 */
public static void test2() throws Exception {
    DemoObj fromObj = new DemoObj();
    Map<String, Object> map = Maps.newHashMap();
    map.put("name", "李四");
    map.put("age", null);
 
    BeanUtils.populate(fromObj, map);
    System.out.println(Thread.currentThread().getName() +":自带的转换器默认值为0:"+JsonUtil.toStr(fromObj));
}

输出结果

main:自带的转换器默认值为0:{"age":0,"name":"李四"}

场景三

静态代码块注入自定义转换器,默认值设置为null

测试代码


static {
    ConvertUtils.register(new IntegerConverter(null), Integer.class);
}
 
 
public static void test2() throws Exception {
    DemoObj fromObj = new DemoObj();
    Map<String, Object> map = Maps.newHashMap();
    map.put("name", "李四");
    map.put("age", null);
 
    BeanUtils.populate(fromObj, map);
    System.out.println(Thread.currentThread().getName() +":转换器默认值为:"+JsonUtil.toStr(fromObj));
}

输出结果

main:转换器默认值为:{"age":null,"name":"李四"}

场景四

在场景三的基础上,我们增加了一个转换器重新注册的函数调用,该函数会重置转换器对象

转换器重置的源代码

    public static void deregister() {
        ConvertUtilsBean.getInstance().deregister();
    }
 
// 获取的依旧是单例对象
    protected static ConvertUtilsBean getInstance() {
        return BeanUtilsBean.getInstance().getConvertUtils();
    }
 
    public void deregister() {
 
        converters.clear();
 
        registerPrimitives(false);
        registerStandard(false, false);
        registerOther(true);
        registerArrays(false, 0);
        register(BigDecimal.class, new BigDecimalConverter());
        register(BigInteger.class, new BigIntegerConverter());
    }

测试代码

static {
    ConvertUtils.register(new IntegerConverter(null), Integer.class);
}
 
public static void test2() throws Exception {
    DemoObj fromObj = new DemoObj();
    Map<String, Object> map = Maps.newHashMap();
    map.put("name", "李四");
    map.put("age", null);
 
    BeanUtils.populate(fromObj, map);
    System.out.println(Thread.currentThread().getName() +":转换器默认值为:"+JsonUtil.toStr(fromObj));
}
 
 
public static void test3() throws Exception {
    DemoObj fromObj = new DemoObj();
    Map<String, Object> map = Maps.newHashMap();
    map.put("name", "王五");
    map.put("age", null);
 
    BeanUtils.populate(fromObj, map);
    System.out.println(Thread.currentThread().getName() + ":age被赋值默认值:"+ JsonUtil.toStr(fromObj));
    // 重置转换器
    ConvertUtils.deregister();
}
 
 
public static void main(String[] args) throws Exception {
    new Thread(()-> {
        try {
            // 睡眠,等test2先跑完
            Thread.sleep(1000);
            test2();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }).start();
    new Thread(()-> {
        try {
            test3();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }).start();
 
}

输出结果

当前线程执行时,转换器还是静态代码块所注册的默认值为null的转换器,但是在test3执行完后会重置单例的转换器对象

Thread-2:age被赋值默认值:{"age":null,"name":"王五"}

所以线程睡眠结束后再执行转换时,发现转换器已被污染,转换的属性默认值非预期值

Thread-1:转换器默认值为:{"age":0,"name":"李四"}


场景五

在场景四的基础上重新创建一个单例对象,并且该单例提前注册一个自定义默认值为null的的转换器

新增类


public class CustBeanUtils {
    private static final BeanUtilsBean beanUtilsBean;
 
    public CustBeanUtils() {
    }
 
    public static void populate(Object bean, Map<String, ? extends Object> properties) throws Exception {
        beanUtilsBean.populate(bean, properties);
    }
 
    static {
        ConvertUtilsBean convertUtilsBean = new ConvertUtilsBean();
        convertUtilsBean.register(new IntegerConverter(null), Integer.class);
        beanUtilsBean = new BeanUtilsBean(convertUtilsBean, new PropertyUtilsBean());
    }
}

测试代码

static {
    ConvertUtils.register(new IntegerConverter(null), Integer.class);
}
 
public static void test2() throws Exception {
    DemoObj fromObj = new DemoObj();
    Map<String, Object> map = Maps.newHashMap();
    map.put("name", "李四");
    map.put("age", null);
 
    BeanUtils.populate(fromObj, map);
    System.out.println(Thread.currentThread().getName() +":转换器默认值为:"+JsonUtil.toStr(fromObj));
}
 
 
public static void test3() throws Exception {
    DemoObj fromObj = new DemoObj();
    Map<String, Object> map = Maps.newHashMap();
    map.put("name", "王五");
    map.put("age", null);
 
    BeanUtils.populate(fromObj, map);
    System.out.println(Thread.currentThread().getName() + ":age被赋值默认值:"+ JsonUtil.toStr(fromObj));
    // 重置转换器
    ConvertUtils.deregister();
}
 
public static void test4() throws Exception {
    DemoObj fromObj = new DemoObj();
    Map<String, Object> map = Maps.newHashMap();
    map.put("name", "赵六");
    map.put("age", null);
 
    CustBeanUtils.populate(fromObj, map);
    System.out.println(Thread.currentThread().getName() + ":CustBeanUtils 新建了一个BeanUtilsBean,不受其影响,age也不会被赋值默认值0:"+ JsonUtil.toStr(fromObj));
}
 
 
public static void main(String[] args) throws Exception {
    new Thread(()-> {
        try {
            Thread.sleep(1000);
            test2();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }).start();
    new Thread(()-> {
        try {
            test3();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }).start();
    new Thread(()-> {
        try {
            Thread.sleep(2000);
            test4();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }).start();
 
}

输出结果

线程2执行时,转换器还是静态代码块所注册的默认值为null的转换器,但是在test3执行完后会重置单例的转换器对象

Thread-2:age被赋值默认值:{"age":null,"name":"王五"}

所以线程1睡眠结束后再执行转换时,发现转换器已被污染,转换的属性默认值非预期值

Thread-1:转换器默认值为:{"age":0,"name":"李四"}

由于线程3用的是一个新的单例对象,所以其转换结果并不受原生的工具集中转换器重置的函数所影响

Thread-3:CustBeanUtils 新建了一个BeanUtilsBean,不受其影响,age也不会被赋值默认值0:{"age":null,"name":"赵六"}
;