记录一次排查问题遇到apache.commons.beanutils包的坑

本文介绍了在使用Apache Commons Beanutils时遇到的一个bug,即自定义的日期转换器未生效,导致String转Date时出错。通过分析源码,发现由于线程上下文类加载器和弱引用HashMap的使用,导致多线程环境下转换器被错误清理。解决方案是在每次转换前重新注册个性化日期转换器。

引言 

        在开发过程中,难免需要用到对象转换器,比如apache的BeanUtils、ConvertUtils还有spring的BeanUtils。我们在公司的项目中就使用了apache的BeanUtils和ConvertUtils作为公共的对象转换工具。但是在没有充分理解源代码的情况下,添加个性化Converter就会出现意想不到的bug。下面我来介绍一下我在排查问题中遇到的这个bug。

问题

        本文采用的是jdk1.8 commons-beanutils 1.9.3的代码。先来描述一下我遇到的问题,我的使用场景是将String类型的日期转换成Date类型的日期,当然我事先已经配置好了String类型转换Date类型的转换器。但是还是出现了如下报错。而且这个报错非常有特点:

  1. 出现一次以后就会一直出现,重启后又恢复
  2. 同样的代码在某一个线程会报错,在其他线程又不报错非常奇怪。
2019-12-03 09:02:27.119 [TID:136.209.15753349468560005] [http-nio-9092-exec-59] WARN  org.apache.commons.beanutils.converters.DateConverter -    DateConverter does not support default String to 'Date' conversion.
2019-12-03 09:02:27.119 [TID:136.209.15753349468560005] [http-nio-9092-exec-59] WARN  org.apache.commons.beanutils.converters.DateConverter -    (N.B. Re-configure Converter or use alternative implementation)
org.apache.commons.beanutils.ConversionException: DateConverter does not support default String to 'Date' conversion.
	at org.apache.commons.beanutils.converters.DateTimeConverter.toDate(DateTimeConverter.java:474)
	at org.apache.commons.beanutils.converters.DateTimeConverter.convertToType(DateTimeConverter.java:347)
	at org.apache.commons.beanutils.converters.AbstractConverter.convert(AbstractConverter.java:169)
	at org.apache.commons.beanutils.converters.ConverterFacade.convert(ConverterFacade.java:61)
	at org.apache.commons.beanutils.ConvertUtilsBean.convert(ConvertUtilsBean.java:566)
	at org.apache.commons.beanutils.ConvertUtils.convert(ConvertUtils.java:282)

       其次,我们在代码中是自定义了BeanUtils,只是使用了ConvertUtils,当然大部分的BeanUtils代码还是和apache的BeanUtils代码一致的。以下是这个问题代码。

public class RabbitBeanUtils {
    static {
        ConvertUtils.register(new ConverterWrapper(ConvertUtils.lookup(Date.class)) {
            @Override
            public Object convert(Class type, Object arg) {
                if (arg instanceof Date) {
                    return arg;
                }
                if (arg instanceof Number) {
                    Number value = (Number) arg;
                    return new Date(value.longValue());
                } else if (arg instanceof String) {
                    return TypeConverter.parse((String) arg);
                }
                return super.convert(type, arg);
            }
        }, Date.class);

        ConvertUtils.register(new ConverterWrapper(ConvertUtils.lookup(List.class)) {
            @Override
            public Object convert(Class type, Object arg) {
                if (arg instanceof String) {
                    return Arrays.asList(((String) arg).split(","));
                }
                return super.convert(type, arg);
            }
        }, List.class);
    }
    /**
     * 一个值赋值到另外一个值,空的省略,但是不包含强制赋值里面
     *
     * @param to             目标对象
     * @param from           原始对象
     * @param ignore         省略的属性名称
     * @param ifNullContinue 为空时继续赋值的属性
     */
    public static void copyWithoutNull(Object to, Object from, String[] ignore, String ifNullContinue[]) {
        List<String> list = new ArrayList<String>();
        if (ignore != null) {
            list = Arrays.asList(ignore);
        }
        List<String> continueList = new ArrayList<String>();
        if (null != ifNullContinue) {
            continueList = Arrays.asList(ifNullContinue);
        }
        PropertyDescriptor[] descr = PropertyUtils.getPropertyDescriptors(to);
        for (int i = 0; i < descr.length; ++i) {
            PropertyDescriptor d = descr[i];
            if (d.getWriteMethod() != null && !list.contains(d.getName())) {
                try {
                    Object e = PropertyUtils.getProperty(from, d.getName());
                    if (e != null || continueList.contains(d.getName())) {
                        Object sameType = null;
                        if (e == null || d.getPropertyType().equals(e.getClass())) {
                            sameType = e;
                        } else {
                            sameType = ConvertUtils.convert(e, d.getPropertyType());
                        }
                        PropertyUtils.setProperty(to, d.getName(), sameType);
                    }
                } catch (Exception var8) {
                    var8.printStackTrace();
                }
            }
        }
    }
}

排查

        排查的第一步就是看报错信息,从上面的异常信息可以看到,确实是String类型转Date类型失败了。然而奇怪的是我已经在上述代码中重新注册了日期转换器,为什么还是进入了ConvertUtils默认的DateConverter呢?如果进入自定义的日期转换器是能够转换成对应日期类型的,但是就是没进来。带着这个疑问我去查看了ConvertUtils的源代码。

       一开始进入的是convert方法,convert方法里面又根据lookup方法去寻找相应的转换器,最终是从converters这个自定义的WeakFastHashMap缓存中得到了相应目标类型的转换器(注意这里的WeakFastHashMap并不是虚对象,并不会在垃圾回收器清理,一开始我以为这里是虚对象觉得日期转换器在这里被清理了,觉得找到了原因欣喜若狂,然而并不是)。这整个过程行云流水并且全部是ConvertUtils的内部代码出错的可能性都比较低。一时间找不出问题。

     


//部分关键代码


public class ConvertUtilsBean {
......
    private final WeakFastHashMap<Class<?>, Converter> converters =
            new WeakFastHashMap<Class<?>, Converter>();

    public static Object convert(final Object value, final Class<?> targetType) {
        return ConvertUtilsBean.getInstance().convert(value, targetType);
    }
    public Object convert(final Object value, final Class<?> targetType) {
    ......
        Object converted = value;
        Converter converter = lookup(sourceType, targetType);
    ......
    }

    public Converter lookup(final Class<?> sourceType, final Class<?> targetType) {
    ......
        if (targetType == String.class) {.....}
        if (targetType == String[].class) {......}
        return lookup(targetType);
    }
    public Converter lookup(final Class<?> clazz) {
        return (converters.get(clazz));
    }
......
}


public class BeanUtilsBean {
......
    public BeanUtilsBean() {
        this(new ConvertUtilsBean(), new PropertyUtilsBean());
    }
    private static final ContextClassLoaderLocal<BeanUtilsBean>
            BEANS_BY_CLASSLOADER = new ContextClassLoaderLocal<BeanUtilsBean>() {
                        // Creates the default instance used when the context classloader                                                         
                       is unavailable
                        @Override
                        protected BeanUtilsBean initialValue() {
                            return new BeanUtilsBean();
                        }
                    };

    public static BeanUtilsBean getInstance() {
        return BEANS_BY_CLASSLOADER.get();
    }
......    
}

         后来注意到这个ConvertUtilsBean的初始化方式是用的单例模式就想进去看看这一部分的代码。从ConvertUtilsBean.getInstance()方法进入一直到BEANS_BY_CLASSLOADER.get()方法才算真正进入获取逻辑,这个BEANS_BY_CLASSLOADER是ContextClassLoaderLocal对象初始化的静态对象。

       然后我们来认真看一下这个get()方法。获取当前线程的classLoader类,然后从valueByClassLoader这个weakHashMap中获取对应的BeanUtilsBean,看到这里就感觉有点意思了,这里的valueByClassLoader是一个weakHashMap类型说明可以被清理,而且这个key是当前线程的ClassLoader。这两个地方就非常符合上面出现问题的现象,所以我觉得再深入看看源码。如果根据当前线程找不到对应的对象这会进入初始化方法initialValue().可以从代码中看到这个initialValue()方法在创建BEANS_BY_CLASSLOADER的时候已经重写了。进入new BeanUtilsBean的构造器发现有一个new ConvertUtilsBean()的过程,再进去发现ConvertUtilsBean的构造器里面做了converters的清空和重新注册。 到这里才明白,为什么明明已经注册过的转换器还是走了原来默认的。


//部分关键代码
public class ConvertUtils {
......
    public static Object convert(final Object value, final Class<?> targetType) {
        return ConvertUtilsBean.getInstance().convert(value, targetType);
    }
......
}


public class ConvertUtilsBean {
......
    protected static ConvertUtilsBean getInstance() {
        return BeanUtilsBean.getInstance().getConvertUtils();
    }
    public ConvertUtilsBean() {
        converters.setFast(false);
        deregister();
        converters.setFast(true);
    }
......
}


public class BeanUtilsBean {
......
    public BeanUtilsBean() {
        this(new ConvertUtilsBean(), new PropertyUtilsBean());
    }
    private static final ContextClassLoaderLocal<BeanUtilsBean>
            BEANS_BY_CLASSLOADER = new ContextClassLoaderLocal<BeanUtilsBean>() {
                        // Creates the default instance used when the context classloader                                                         
                       is unavailable
                        @Override
                        protected BeanUtilsBean initialValue() {
                            return new BeanUtilsBean();
                        }
                    };

    public static BeanUtilsBean getInstance() {
        return BEANS_BY_CLASSLOADER.get();
    }
......    
}


public class ContextClassLoaderLocal<T> { 
......
    private final Map<ClassLoader, T> valueByClassLoader = new WeakHashMap<ClassLoader,T>();
    protected T initialValue() {
        return null;
    }
    public synchronized T get() {
        // synchronizing the whole method is a bit slower
        // but guarantees no subtle threading problems, and there's no
        // need to synchronize valueByClassLoader

        // make sure that the map is given a change to purge itself
        valueByClassLoader.isEmpty();
        try {

            final ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
            if (contextClassLoader != null) {

                T value = valueByClassLoader.get(contextClassLoader);
                if ((value == null)
                && !valueByClassLoader.containsKey(contextClassLoader)) {
                    value = initialValue();
                    valueByClassLoader.put(contextClassLoader, value);
                }
                return value;

            }

        } catch (final SecurityException e) { /* SWALLOW - should we log this? */ }

        // if none or exception, return the globalValue
        if (!globalValueInitialized) {
            globalValue = initialValue();
            globalValueInitialized = true;
        }//else already set
        return globalValue;
    }
......

}

原因

       我们来理一下上面分析的思路和最终造成bug的原因。BeanUtilsBean是以当前线程的classloader作为key获取的单例,在多线程的情况下,不同线程可能获取到的classloader不同。然而一开始注册的日期转换器由于是写在static语句块里面初始的,所以当获取到不同classloader去重新注册ConvertUtilsBean的时候,原缓存被清空,转换器被重新注册并且没有注册个性化的日期转换器。

解决

       只初始化转换器对象,在调用 ConvertUtils.convert(e, d.getPropertyType())之前,每次都注册日期转换器确保每次能将个性化日期转换器注册到ConvertUtilsBean中。最终解决问题。

public class RabbitBeanUtils {

    public static void copyWithoutNull(Object to, Object from, String[] ignore, String ifNullContinue[]) {
        List<String> list = new ArrayList<String>();
        if (ignore != null) {
            list = Arrays.asList(ignore);
        }
        List<String> continueList = new ArrayList<String>();
        if (null != ifNullContinue) {
            continueList = Arrays.asList(ifNullContinue);
        }
        PropertyDescriptor[] descr = PropertyUtils.getPropertyDescriptors(to);
        for (int i = 0; i < descr.length; ++i) {
            PropertyDescriptor d = descr[i];
            if (d.getWriteMethod() != null && !list.contains(d.getName())) {
                try {
                    Object e = PropertyUtils.getProperty(from, d.getName());
                    if (e != null || continueList.contains(d.getName())) {
                        Object sameType;
                        if (e == null || d.getPropertyType().equals(e.getClass())) {
                            sameType = e;
                        } else {
                            registerConverter();
                            sameType = ConvertUtils.convert(e, d.getPropertyType());
                        }
                        PropertyUtils.setProperty(to, d.getName(), sameType);
                    }
                } catch (Exception var8) {
                    var8.printStackTrace();
                }
            }
        }
    }
    private static final Converter DATE_CONVERTER = new ConverterWrapper(ConvertUtils.lookup(Date.class)) {
        @Override
        public Object convert(Class type, Object arg) {
            if (arg instanceof Date) {
                return arg;
            }
            if (arg instanceof Number) {
                Number value = (Number) arg;
                return new Date(value.longValue());
            } else if (arg instanceof String) {
                return TypeConverter.parse((String) arg);
            }
            return super.convert(type, arg);
        }
    };
    private static void registerConverter(){
        ConvertUtils.register(DATE_CONVERTER, Date.class);
    }

}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值