引言
在开发过程中,难免需要用到对象转换器,比如apache的BeanUtils、ConvertUtils还有spring的BeanUtils。我们在公司的项目中就使用了apache的BeanUtils和ConvertUtils作为公共的对象转换工具。但是在没有充分理解源代码的情况下,添加个性化Converter就会出现意想不到的bug。下面我来介绍一下我在排查问题中遇到的这个bug。
问题
本文采用的是jdk1.8 commons-beanutils 1.9.3的代码。先来描述一下我遇到的问题,我的使用场景是将String类型的日期转换成Date类型的日期,当然我事先已经配置好了String类型转换Date类型的转换器。但是还是出现了如下报错。而且这个报错非常有特点:
- 出现一次以后就会一直出现,重启后又恢复
- 同样的代码在某一个线程会报错,在其他线程又不报错非常奇怪。
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);
}
}
本文介绍了在使用Apache Commons Beanutils时遇到的一个bug,即自定义的日期转换器未生效,导致String转Date时出错。通过分析源码,发现由于线程上下文类加载器和弱引用HashMap的使用,导致多线程环境下转换器被错误清理。解决方案是在每次转换前重新注册个性化日期转换器。

8465

被折叠的 条评论
为什么被折叠?



