问题的产生

本节通过一个简单的消息发送 demo 来讲解。首先介绍消息发送的场景,比如每个安装有手淘 App 的移动设备有一个设备 ID,每个 App(比如手淘 App)有一个 appkey 用来标识这个应用。可以根据不同的 appkey 选择不同的发送策略,对注册到自己的设备进行消息发送,每个消息有一个消息 ID 和消息体字段。下面首先贴出实例代码,如下所示。

  1. // (1)不同 appkey 注册不同的服务
  2. static Map<Integer StrategyService> serviceMap = new HashMap<Integer
  3. StrategyService>();
  4. static {
  5. serviceMap.put111, new StrategyOneService());
  6. serviceMap.put222, new StrategyTwoService());
  7. }
  8. public static void mainString[] args {
  9. // (2)key 为 appkey, value 为设备 ID 列表
  10. Map<Integer List<String>> appKeyMap = new HashMap<Integer List<String>>();
  11. // (3)创建 appkey=111 的设备列表
  12. List<String> oneList = new ArrayList<>();
  13. oneList.add(「device_id1」);
  14. appKeyMap.put111 oneList);
  15. // 创建 appkey=222 的设备列表
  16. List<String> twoList = new ArrayList<>();
  17. twoList.add(「device_id2」);
  18. appKeyMap.put222 twoList);
  19. // (4)创建消息
  20. List<Msg> msgList = new ArrayList<>();
  21. Msg msg = new Msg();
  22. msg.setDataId(「abc」);
  23. msg.setBody(「hello」);
  24. msgList.addmsg);
  25. // (5)根据不同的 appKey 使用不同的策略进行处理
  26. appKeyItr = appKeyMap.keySet().iterator();
  27. while appKeyItr.hasNext()) {
  28. int appKey = appKeyItr.next();
  29. // 这里根据 appkey 获取自己的消息列表
  30. StrategyService strategyService = serviceMap.getappKey);
  31. ifnull ! = strategyService){
  32. strategyService.sendMsgmsgList, appKeyMap.get(appKey));
  33. }else{
  34. System.out.println(String.format(「appkey:%s is not registerd
  35. service」, appKey));
  36. }
  37. }
  38. }
  39. }

代码(1)给不同的 appkey 注册对应的处理策略,appkey=111 和 appkey=222 时分别注册了 StrategyOneService 和 StrategyTwoService 服务,它们都实现了 StrategyService 接口,具体代码如下。

  1. public interface StrategyService {
  2. public void sendMsg(List<Msg> msgList, List<String> deviceIdList);
  3. }
  4. public class StrategyOneService implements StrategyService {
  5. @Override
  6. public void sendMsg(List<Msg> msgList, List<String> deviceIdList) {
  7. for (Msg msg : msgList) {
  8. msg.setDataId("oneService_" + msg.getDataId());
  9. System.out.println(msg.getDataId() + " " + JSON.
  10. toJSONString(deviceIdList));
  11. }
  12. }
  13. }
  14. public class StrategyTwoService implements StrategyService {
  15. @Override
  16. public void sendMsg(List<Msg> msgList, List<String> deviceIdList) {
  17. for (Msg msg : msgList) {
  18. msg.setDataId("TwoService_" + msg.getDataId());
  19. System.out.println(msg.getDataId() + " " + JSON.
  20. toJSONString(deviceIdList));
  21. }
  22. }
  23. }

每个消息对应一个 DataId,其用来唯一标识一个消息。在每个发送消息的实现里面都会添加一个前缀以用于分类统计。

代码(2)和代码(3)则是给对应的 appkey 新增设备列表。

代码(4)创建消息体。

代码(5)实现根据不同的 appkey 使用不同的发送策略进行消息发送。

运行上面代码,我们期望的输出结果为

  1. TwoService_abc ["device_id2"]
  2. oneService_abc ["device_id1"]

但是实际结果却是

  1. TwoService_abc ["device_id2"]
  2. oneService_TwoService_abc ["device_id1"]

问题产生了。这个例子运行的结果是固定的,但是如果在每个发送消息的 sendMsg 方法里面异步修改消息的 DataId,那么运行的结果就不是固定的了。

问题分析

分析输出结果可以知道,代码(5)先执行了 appkey=222 的发送消息服务,然后再执行 appkey=111 的服务,之所以后者打印出来的 DataId 是 oneService_TwoService 而不是 oneService,是因为在 appkey=222 的消息服务里面修改了消息体 msg 的 DataId 为 TwoService_abc,而方法 sendMsg 里面的消息是引用传递的,所以导致 appkey=111 的服务在调用 sendMsg 方法时 msg 里面的 DataId 已经变成了 TwoService_abc,然后在 sendMsg 方法内部又会在它的前面添加 oneService 前缀,最后 DataId 就变成了 oneService_TwoService_abc。

那么该问题如何解决呢?首先应该想到的是不同的 appkey 应该有自己的一份 List,这样不同的服务只会修改自己的消息的 DataId 而不会相互影响。那么下面修改代码(5)中的部分代码如下。

  1. serviceMap.get(appKey).sendMsg(new ArrayList<Msg>(msgList), appKeyMap. get(appKey));

也就是在具体发送消息前重新 new 一个消息列表传递过去,这样应该可以了吧?其实这还是不行的,因为如果 appkey 的个数大于 1,那么在第二个 appkey 服务发送时 ArrayList 构造函数里面的 msgList 已经是第一个 appkey 的服务修改后的了。那么自然会想到应该在代码(5)前面给每个 appkey 事先准备好自己的消息列表,那么新增和修改代码(5)如下。

  1. //这里给每个 appkey 准备自己的消息列表
  2. Iterator<Integer> appKeyItr = appKeyMap.keySet().iterator();
  3. Map<Integer List<Msg>> appKeyMsgMap = new HashMap<Integer List<Msg>>();
  4. whileappKeyItr.hasNext()){
  5. appKeyMsgMap.putappKeyItr.next(), new ArrayList<>(msgList));
  6. }
  7. // (5)根据不同的 appKey 使用不同的策略进行处理
  8. appKeyItr = appKeyMap.keySet().iterator();
  9. while appKeyItr.hasNext()) {
  10. int appKey = appKeyItr.next();
  11. // 这里根据 appkey 获取自己的消息列表
  12. StrategyService strategyService = serviceMap.getappKey);
  13. ifnull ! = strategyService){
  14. strategyService.sendMsgappKeyMsgMap.get(appKey), appKeyMap.
  15. get(appKey));
  16. }else{
  17. System.out.println(String.format(「appkey:%s is not registerd
  18. service」, appKey));
  19. }
  20. }

如上代码首先给每个 appkey 创建消息列表,然后放入 appKeyMsgMap。之后在代码(5)具体发送消息时根据 appkey 去获取相应的消息列表,这样应该没问题了吧?但是当你信心满满地执行并查看结果时就傻眼了,结果竟然和之前的一样。

那么问题出在哪里呢?给每个 appkey 搞一份消息列表,然后发送时使用自己的消息列表进行发送,这个策略是没问题的,那么只有一个情况,就是给每个 appkey 创建一份消息列表时出错了,所有 appkey 用的还是同一份列表。难道 new ArrayList<>(msgList)里面还是引用?其实确实是,因为 Msg 本身是引用类型,而 new ArrayList<>(msgList)这种方式是浅复制,每个 appkey 消息列表都是对同一个 Msg 的引用,修改代码如下。

  1. // 这里给每个 appkey 准备一个消息列表
  2. Iterator<Integer> appKeyItr = appKeyMap.keySet().iterator();
  3. Map<Integer List<Msg>> appKeyMsgMap = new HashMap<Integer List<Msg>>();
  4. while appKeyItr.hasNext()) {
  5. //复制每个消息到临时消息列表
  6. List<Msg> tempList = new ArrayList<Msg>();
  7. Iterator<Msg> itrMsg = msgList.iterator();
  8. while itrMsg.hasNext()) {
  9. Msg tmpMsg = null
  10. try {
  11. //使用 BeanUtils.cloneBean 对 Msg 对象进行属性复制
  12. tmpMsg = Msg BeanUtils.cloneBeanitrMsg.next());
  13. } catch Exception e {
  14. e.printStackTrace();
  15. }
  16. if null ! = tmpMsg {
  17. tempList.addtmpMsg);
  18. }
  19. }
  20. //存放当前 appkey 对应的经过深复制的消息列表
  21. appKeyMsgMap.putappKeyItr.next(), tempList);
  22. }

如上代码使用工具类 BeanUtils.cloneBean 而不是 new ArrayList<>(msgList)来构造每个 appkey 对应的消息列表,修改后运行结果如下。

  1. TwoService_abc ["device_id2"]
  2. oneService_abc ["device_id1"]

至此问题得到解决。

小结

本节通过一个简单的消息发送例子说明了需要复用但是会被下游修改的参数要进行深复制,否则会导致出现错误的结果;另外引用类型作为集合元素时,如果使用这个集合作为另外一个集合的构造函数参数,会导致两个集合里面的同一个位置的元素指向的是同一个引用,这会导致对引用的修改在两个集合中都可见,所以这时候需要对引用元素进行深复制。