暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

使用BeanUtils进行属性复制的坑

shysheng 2019-01-13
1367

问题起因

上篇文章我们说到了一个MySQL的死锁问题(传送门:一次线上Mysql死锁引发的血案),当时案例1已经找到原因,但是案例2依然无解(是的,我现在依然无解o(╯□╰)o)。但是按照我们的线上问题首要原则,我们必须第一时间解决问题。

第一时间解决问题,保留现场事后分析。

没找到原因,可以先解决问题吗?当然可以。

虽然对于死锁是怎么发生的,我们暂时还不清楚。但是从当时的死锁日志来看,我们知道,发生死锁是因为有两个事务以不同的加锁顺序分别对主键和idxpaynokdt进行加锁,导致两个事务分别持有对方所需要的锁,并且等待对方释放锁引起的。

因此DBA给了一个建议,就是在事务1中,先查出需要更新的记录的主键,然后根据主键进行更新。在这种更新策略下,事务1就不会先持有idxpaynokdt上的锁,而是会直接对主键进行加锁,如此一来就破坏了造成死锁的条件。

第一次尝试

看起来似乎很简单,我三下五除二就改好了代码。然而,下单支付之后发现,我的钱明明白白的扣成功了,怎么订单状态显示还是待支付?

当时我还是很淡定的,并且很快就找到了真凶。

我们知道在软件设计中非常讲究分层的概念,从数据库中取到的数据一般放到DO对象中,这个对象的活动范围也非常受限,并不会直接与上层进行交互。在我们的设计中,一般会把DO对象复制到其对应的实体类中,也就是Entity对象,然后由这个实体类负责对外的交互。

好,问题就出在这个复制的过程中。在将对象属性从DO类复制到Entity类的过程中,我们使用的是Spring提供的BeanUtils类,然而复制过程中,DO中存在主键在Entity中神奇地丢失了!

在说明问题之前,我们先简单看下DO类和Entity类的定义:

  1. public class OrderDO extends BaseDO<Long> {


  2.    private String orderId;

  3. }

复制
  1. public abstract class BaseDO<T extends Serializable> {


  2.    @Id

  3.    @GeneratedValue(strategy = GenerationType.AUTO)

  4.    protected T id;

  5. }

复制
  1. public class OrderEntity implements Serializable {


  2.    private Long id;


  3.    private String orderId;

  4. }

复制

属性复制的代码也非常简单:

  1. OrderEntity orderEntity = new OrderEntity();

  2. BeanUtils.copyProperties(orderDO, orderEntity);

复制

那么主键id到底是怎么丢失的呢?代码跟进去看一看,很快定位到了问题所在。

  1. for (int i = 0; i < length; ++i) {

  2.    PropertyDescriptor targetPd = targetPds[i];

  3.    Method writeMethod = targetPd.getWriteMethod();

  4.    if (writeMethod != null && (ignoreList == null || !ignoreList.contains(targetPd.getName()))) {

  5.        PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());

  6.        if (sourcePd != null) {

  7.            Method readMethod = sourcePd.getReadMethod();

  8.            if (readMethod != null && ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())) {

  9.                try {

  10.                    if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {

  11.                        readMethod.setAccessible(true);

  12.                    }

  13.                    Object value = readMethod.invoke(source);

  14.                    if (value != null) {

  15.                        if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {

  16.                            writeMethod.setAccessible(true);

  17.                        }

  18.                        writeMethod.invoke(target, value);

  19.                    }

  20.                } catch (Throwable t) {

  21.                    throw new FatalBeanException("Could not copy property " + targetPd.getName() + " from source to target", t);

  22.                }

  23.            }

  24.        }

  25.    }

  26. }

复制

重点看这一行代码,原来在进行属性复制时,Spring的BeanUtils会比较目标属性类型是否是源属性类型的父类或者父接口。

  1. ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())

复制

在我们的例子中,writeMethod就是目标属性OrderEntity.id的setter方法,readMethod就是源目标属性OrderDO.id的getter方法。从下图可以看到,OrderEntity.id是Long类型的没有问题,但是OrderDO.id却不是我们想象中的Long类型,而是其对应的泛型Serializable,显然Long并不是Serializable的父类,因此id并不满足进行属性复制的条件,导致复制完成后,OrderEntity.id为null.


在我们的例子中,总结来说就是Spring的BeanUtils无法很好地支持泛型。

第二次尝试

既然Spring的BeanUtils不行,那就试试Apache的吧😆

可是换了Apache的BeanUtils之后,进行属性复制时直接报错了:

  1. org.apache.commons.beanutils.ConversionException: No value specified for 'Date'

  2.    at org.apache.commons.beanutils.converters.AbstractConverter.handleMissing(AbstractConverter.java:310)

  3.    at org.apache.commons.beanutils.converters.AbstractConverter.convert(AbstractConverter.java:136)

  4.    at org.apache.commons.beanutils.converters.ConverterFacade.convert(ConverterFacade.java:60)

  5.    at org.apache.commons.beanutils.BeanUtilsBean.convert(BeanUtilsBean.java:1078)

  6.    at org.apache.commons.beanutils.BeanUtilsBean.copyProperty(BeanUtilsBean.java:437)

  7.    at org.apache.commons.beanutils.BeanUtilsBean.copyProperties(BeanUtilsBean.java:286)

  8.    at org.apache.commons.beanutils.BeanUtils.copyProperties(BeanUtils.java:137)

复制

此刻,我的心情是这样的:

第三次尝试

原来,Apache的BeanUtils对日期的支持不是很好,当源对象的日期属性为空时,进行属性复制就会报错。当然要解决这个问题也很简单,Apache提供了扩展接口,只需要注册自己的日期转换器,当日期对象为空时,对目标对象给默认值null就好了,如下:

  1. @Slf4j

  2. public class BeanUtils {


  3.    static {

  4.        // 注册Date的转换器,当日期对象为空时,对目标对象给默认值null

  5.        ConvertUtils.register(new SqlDateConverter(null), Date.class);

  6.    }



  7.    public static void copyProperties(Object dest, Object orig) {

  8.        try {

  9.            org.apache.commons.beanutils.BeanUtils.copyProperties(dest, orig);

  10.        } catch (Exception e) {

  11.            log.error("copy properties error", e);

  12.        }

  13.    }

  14. }

复制

这么一改之后,所有流程走下来都没有问题了,当然死锁是否还会继续发生还有待进一步的观察。

总结

你看,从第一次出现死锁到现在,没有一步是走得比较顺利的。整个过程总结下来就是:你永远不知道在你前面等待你的是一个什么样的坑。其实这就是程序员的日常,也许下面这幅图最能表达我们的这种感受。

最后,用一句鸡汤文来结尾吧:

你能走多远,取决于你填的坑有多大!


文章转载自shysheng,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论