昨天客户反馈,从购物车页面点击结算按钮跳转到下单页面,又想修改商品,返回到购物车页面时,商品的顺序和上次查看的顺序不一致。
查看线上日志,发现是CartController类的方法问题。大致流程是:
1、根据用户cookie属性从数据库查到用户的购物车cart;
2、将cart(数据库数据类型的DO)转换cartvo(页面显示的数据类型VO)返回给前端。
其中第二步数据转换有问题,这部分的源码如下
CartVo cartVo = BeanMapper.map(cart, BeanMapper.getType(Cart.class), BeanMapper.getType(CartVo.class));
CartVo类有如下属性(简化其他属性)
@Data
public class CartVo implements Serializable {
/** 子购物车 */
private List<CartVo> children;
/** 购物车项 */
private Set<CartItemVo> cartItems = new TreeSet<>();
Cart类属性和CartVo类属性类似。通过阅读BeanMapper的map转换源码(第三方jar包“orika”),发现set集合使用的hashset进行集合包装,而hashset具有有序性(sort),但不具有稳定性(order),结果导致每次数据转换的顺序不稳定。BeanMapper源码如下
public final <S, D> Set<D> mapAsSet(final Iterable<S> source, final Type<S> sourceType, final Type<D> destinationType,
final MappingContext context) {
return (Set<D>) mapAsCollection(source, sourceType, destinationType, new HashSet<D>(), context);
}
知道问题后就好处理,改为TreeSet集合来包装数据。本来想使用门面设计模式继承BeanMapper,发现它的源码很复杂,不是两小时内能搞定的,就采用再封装一层转换的思路委婉的解决这个问题。BeanMapper类中源码如下
public static <S, D> Set<D> mapAsSet(Iterable<S> sourceList, Type<S> sourceType, Type<D> destinationType) {
return new TreeSet<>(mapper.mapAsSet(sourceList, sourceType, destinationType));
}
上层Controller调用稍微复杂一些,本来1行代码现在改为8行代码,源码如下
CartVo cartVo = BeanMapper.map(cart, BeanMapper.getType(Cart.class), BeanMapper.getType(CartVo.class));
cartVo.setChildren(new ArrayList<CartVo>());
for (Cart child : cart.getChildren()) {
CartVo childVO = BeanMapper.map(child, BeanMapper.getType(Cart.class), BeanMapper.getType(CartVo.class));
childVO.setCartItems(BeanMapper.mapAsSet(child.getCartItems(), BeanMapper.getType(CartItem.class),
BeanMapper.getType(CartItemVo.class)));
cartVo.getChildren().add(childVO);
}
知识点:
集合的有序性是指遍历的结果按照指定的比较规则依次排列;集合的稳定性是指每次遍历,元素次序是不会改变。
java的ArrayList是无序但稳定;HashMap是无序也不稳定;HashSet是有序但不稳定;TreeSet是有序且稳定。