bug描述:
项目开发过程中, 遇到了一个奇怪的现象, 在测试环境去利用反射拿一个类的字段时, 发现拿到的field数组中多了一个奇怪的变量: $jacocoData, 它是一个boolean数组.
在开发环境中没有这个问题, 但是部署到测试环境之后有这个问题.
发现这个问题的缘由: 对接了一个第三方系统, 可能他们系统对于数据类型比较严格, 要求传的字段是规定的类型且不能为null. 然后部署到测试环境发现报错了,
报错内容: com.sensorsdata.analytics.javasdk.exceptions.InvalidArgumentException: The property 'groupBuy_e_$jacocoData' should be a basic type: Number, String, Date, Boolean, List<String>.
因为我们自己业务系统中要求字段前缀是以"groupBuy_e_"开头, 所以我利用反射写了个方法, 对字段属性名称进行拼接.
public class BeanToMapUtils {
private static Logger LOGGER = LoggerFactory.getLogger(BeanToMapUtils.class);
/**
* 属性名前缀
*/
private static final String PREFIX = "groupBuy_e_";
/**
* 将一个 JavaBean 对象转化为一个 Map
*
* @param bean 要转化的JavaBean 对象
* @return 转化出来的 Map 对象
* @throws IntrospectionException 如果分析类属性失败
* @throws IllegalAccessException 如果实例化 JavaBean 失败
* @throws InvocationTargetException 如果调用属性的 setter 方法失败
*/
public static Map<String, Object> beanToMap(Object bean) {
Class type = bean.getClass();
Map<String, Object> returnMap = new HashMap<>(16);
Field[] fieldArray = type.getDeclaredFields();
for (Field field : fieldArray) {
field.setAccessible(true);
try {
Object value = field.get(bean);
if (ObjectUtils.isEmpty(value)) {
// 为空是设置默认值
if (field.getType().isInstance(new BigDecimal(0))) {
value = BigDecimal.ZERO;
}
if (field.getType().isInstance(new String())) {
value = "";
}
if (field.getType().isInstance(new Integer(0))) {
value = new Integer(0);
}
if (field.getType().isInstance(new Boolean(false))) {
value = new Boolean(false);
}
}
returnMap.put(PREFIX + field.getName(), value);
} catch (Exception e) {
LOGGER.error("BeanToMapUtils beanToMap fail: {}", e);
}
}
return returnMap;
}
}
莫名其妙多出这个$jacocoData属性, 技术菜鸟表示很懵逼.
于是开始了寻找解决方案之路.
首先我在网上看到一篇关于类似问题的博客: https://zhanglijun1217.github.io/blog/2019/03/10/%E4%B8%80%E6%AC%A1%E6%8E%92%E6%9F%A5-jacocoData%E7%9A%84%E8%BF%87%E7%A8%8B/
原来和JaCoco有关系, 对JaCoco了解不甚多, 只知道它是代码覆盖率工具. 了解了一下它的基本原理, 它使用的ASM技术修改字节码方法, 可以修改Jar文件、class文件字节码文件(详细可以参考博客https://cloud.tencent.com/developer/article/1038055)
JaCoco官网FAQ中有一条是关于$jacocoData的, 如下:
https://www.eclemma.org/jacoco/trunk/doc/faq.html
官方建议是修改代码, 忽略synthetic成员.
至于什么是synthetic, 你可以参考一下:https://www.cnblogs.com/bethunebtj/p/7761596.html 和 https://www.baeldung.com/java-synthetic
"Any constructs introduced by a Java compiler that do not have a corresponding construct in the source code must be marked as synthetic, except for default constructors, the class initialization method, and the values and valueOf methods of the Enum class."
解决方案自然是忽略synthetic成员了, 可以用下面这个方法判断是否是synthetic field:
所以BeanToMapUtils类加几行代码:
public class BeanToMapUtils {
private static Logger LOGGER = LoggerFactory.getLogger(BeanToMapUtils.class);
/**
* 属性名前缀
*/
private static final String PREFIX = "groupBuy_e_";
/**
* 将一个 JavaBean 对象转化为一个 Map
*
* @param bean 要转化的JavaBean 对象
* @return 转化出来的 Map 对象
* @throws IntrospectionException 如果分析类属性失败
* @throws IllegalAccessException 如果实例化 JavaBean 失败
* @throws InvocationTargetException 如果调用属性的 setter 方法失败
*/
public static Map<String, Object> beanToMap(Object bean) {
Class type = bean.getClass();
Map<String, Object> returnMap = new HashMap<>(16);
Field[] fieldArray = type.getDeclaredFields();
for (Field field : fieldArray) {
// 忽略synthetic成员
if (field.isSynthetic()) {
continue;
}
field.setAccessible(true);
try {
Object value = field.get(bean);
if (ObjectUtils.isEmpty(value)) {
// 为空是设置默认值
if (field.getType().isInstance(new BigDecimal(0))) {
value = BigDecimal.ZERO;
}
if (field.getType().isInstance(new String())) {
value = "";
}
if (field.getType().isInstance(new Integer(0))) {
value = new Integer(0);
}
if (field.getType().isInstance(new Boolean(false))) {
value = new Boolean(false);
}
}
returnMap.put(PREFIX + field.getName(), value);
} catch (Exception e) {
LOGGER.error("BeanToMapUtils beanToMap fail: {}", e);
}
}
return returnMap;
}
}
总结与反思:
When your code uses reflection, please ignore synthetic members. This is a good practice anyways as also the Java compiler creates synthetic members in certain situation.
...... 以上仅供参考, 欢迎批评与指正!
参考:
https://cloud.tencent.com/developer/article/1038055
https://github.com/jacoco/jacoco/issues/168