Skip to content

Commit 8c6560c

Browse files
authored
bugfix:修复 client 中 ServiceInfoHolder 中数据可能被修改的风险 (alibaba#14062)
* feat: 为 ServiceInfo 添加深度复制 clone 方法并更新 ServiceInfoHolder 返回 clone 对象 - 实现 Cloneable 接口,添加 @OverRide 注解的 clone() 方法 - 深度复制所有字段,包括 hosts 列表和 Instance 的 metadata Map - 修改 ServiceInfoHolder.getServiceInfo() 返回 clone 对象以防止外部修改 - 完善单元测试,覆盖各种场景 * fix: 修复 testCloneWithHostsNullMetadata 测试断言 - Instance 的 metadata 字段默认初始化为空 HashMap - 当原始 metadata 为 null 时,clone 后应为空 map 而非 null * fix: 恢复 getHosts() 方法为原始实现 - 将 getHosts() 方法恢复为 return new ArrayList<>(hosts) - 更新相关测试用例
1 parent 0e30f41 commit 8c6560c

4 files changed

Lines changed: 257 additions & 2 deletions

File tree

api/src/main/java/com/alibaba/nacos/api/naming/pojo/ServiceInfo.java

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@
2424
import java.io.UnsupportedEncodingException;
2525
import java.net.URLEncoder;
2626
import java.util.ArrayList;
27+
import java.util.HashMap;
2728
import java.util.List;
29+
import java.util.Map;
2830

2931
/**
3032
* Service Information with instances and without cluster information, used in data pushing and cached for nacos-client.
@@ -33,7 +35,7 @@
3335
* @author shizhengxing
3436
*/
3537
@JsonInclude(Include.NON_NULL)
36-
public class ServiceInfo {
38+
public class ServiceInfo implements Cloneable {
3739

3840
/**
3941
* file name pattern: groupName@@name@@clusters.
@@ -278,4 +280,43 @@ public boolean isReachProtectionThreshold() {
278280
public void setReachProtectionThreshold(boolean reachProtectionThreshold) {
279281
this.reachProtectionThreshold = reachProtectionThreshold;
280282
}
283+
284+
@Override
285+
public ServiceInfo clone() {
286+
ServiceInfo cloned = new ServiceInfo();
287+
cloned.jsonFromServer = this.jsonFromServer;
288+
cloned.name = this.name;
289+
cloned.groupName = this.groupName;
290+
cloned.clusters = this.clusters;
291+
cloned.cacheMillis = this.cacheMillis;
292+
cloned.lastRefTime = this.lastRefTime;
293+
cloned.checksum = this.checksum;
294+
cloned.allIps = this.allIps;
295+
cloned.reachProtectionThreshold = this.reachProtectionThreshold;
296+
cloned.hosts = new ArrayList<>();
297+
298+
if (this.hosts != null) {
299+
for (Instance host : this.hosts) {
300+
Instance clonedHost = new Instance();
301+
clonedHost.setInstanceId(host.getInstanceId());
302+
clonedHost.setIp(host.getIp());
303+
clonedHost.setPort(host.getPort());
304+
clonedHost.setWeight(host.getWeight());
305+
clonedHost.setHealthy(host.isHealthy());
306+
clonedHost.setEnabled(host.isEnabled());
307+
clonedHost.setEphemeral(host.isEphemeral());
308+
clonedHost.setClusterName(host.getClusterName());
309+
clonedHost.setServiceName(host.getServiceName());
310+
311+
if (host.getMetadata() != null) {
312+
Map<String, String> clonedMetadata = new HashMap<>(host.getMetadata());
313+
clonedHost.setMetadata(clonedMetadata);
314+
}
315+
316+
cloned.hosts.add(clonedHost);
317+
}
318+
}
319+
320+
return cloned;
321+
}
281322
}

api/src/test/java/com/alibaba/nacos/api/naming/pojo/ServiceInfoTest.java

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,14 @@
2929
import java.io.UnsupportedEncodingException;
3030
import java.net.URLEncoder;
3131
import java.util.Collections;
32+
import java.util.HashMap;
3233
import java.util.LinkedList;
3334
import java.util.List;
35+
import java.util.Map;
3436

3537
import static org.junit.jupiter.api.Assertions.assertEquals;
3638
import static org.junit.jupiter.api.Assertions.assertFalse;
39+
import static org.junit.jupiter.api.Assertions.assertNotSame;
3740
import static org.junit.jupiter.api.Assertions.assertThrows;
3841
import static org.junit.jupiter.api.Assertions.assertTrue;
3942

@@ -202,4 +205,197 @@ void testGetKeyWithoutClusters() {
202205
ServiceInfo serviceInfo3 = new ServiceInfo("group@@name@@cluster", "");
203206
assertEquals("group@@name@@cluster", serviceInfo3.getKeyWithoutClusters());
204207
}
208+
209+
@Test
210+
void testCloneBasicFields() {
211+
// Setup original ServiceInfo with all fields
212+
ServiceInfo original = new ServiceInfo("testGroup@@testName", "testClusters");
213+
original.setCacheMillis(2000L);
214+
original.setLastRefTime(1234567890L);
215+
original.setChecksum("testChecksum");
216+
original.setAllIps(true);
217+
original.setReachProtectionThreshold(true);
218+
original.setJsonFromServer("testJson");
219+
220+
// Clone
221+
ServiceInfo cloned = original.clone();
222+
223+
// Verify it's a different object
224+
assertNotSame(original, cloned);
225+
226+
// Verify all basic fields are copied
227+
assertEquals(original.getName(), cloned.getName());
228+
assertEquals(original.getGroupName(), cloned.getGroupName());
229+
assertEquals(original.getClusters(), cloned.getClusters());
230+
assertEquals(original.getCacheMillis(), cloned.getCacheMillis());
231+
assertEquals(original.getLastRefTime(), cloned.getLastRefTime());
232+
assertEquals(original.getChecksum(), cloned.getChecksum());
233+
assertEquals(original.isAllIps(), cloned.isAllIps());
234+
assertEquals(original.isReachProtectionThreshold(), cloned.isReachProtectionThreshold());
235+
assertEquals(original.getJsonFromServer(), cloned.getJsonFromServer());
236+
}
237+
238+
@Test
239+
void testCloneWithNullHosts() {
240+
final ServiceInfo original = new ServiceInfo("testGroup@@testName", "testClusters");
241+
original.setHosts(null);
242+
243+
ServiceInfo cloned = original.clone();
244+
245+
assertNotSame(original, cloned);
246+
// Clone method initializes hosts to empty list even if original is null
247+
assertTrue(cloned.getHosts().isEmpty());
248+
}
249+
250+
@Test
251+
void testCloneWithEmptyHosts() {
252+
ServiceInfo original = new ServiceInfo("testGroup@@testName", "testClusters");
253+
original.setHosts(new LinkedList<>());
254+
255+
ServiceInfo cloned = original.clone();
256+
257+
assertNotSame(original, cloned);
258+
assertNotSame(original.getHosts(), cloned.getHosts());
259+
assertTrue(cloned.getHosts().isEmpty());
260+
}
261+
262+
@Test
263+
void testCloneWithHosts() {
264+
// Setup original ServiceInfo with hosts
265+
final ServiceInfo original = new ServiceInfo("testGroup@@testName", "testClusters");
266+
267+
Instance instance1 = new Instance();
268+
instance1.setInstanceId("instance1");
269+
instance1.setIp("192.168.1.1");
270+
instance1.setPort(8080);
271+
instance1.setWeight(1.0);
272+
instance1.setHealthy(true);
273+
instance1.setEnabled(true);
274+
instance1.setEphemeral(true);
275+
instance1.setClusterName("cluster1");
276+
instance1.setServiceName("service1");
277+
278+
Instance instance2 = new Instance();
279+
instance2.setInstanceId("instance2");
280+
instance2.setIp("192.168.1.2");
281+
instance2.setPort(8081);
282+
instance2.setWeight(2.0);
283+
instance2.setHealthy(false);
284+
instance2.setEnabled(false);
285+
instance2.setEphemeral(false);
286+
instance2.setClusterName("cluster2");
287+
instance2.setServiceName("service2");
288+
289+
original.addHost(instance1);
290+
original.addHost(instance2);
291+
292+
// Clone
293+
ServiceInfo cloned = original.clone();
294+
295+
// Verify it's a different object
296+
assertNotSame(original, cloned);
297+
298+
// Verify hosts list is different
299+
assertNotSame(original.getHosts(), cloned.getHosts());
300+
assertEquals(original.getHosts().size(), cloned.getHosts().size());
301+
302+
// Verify each host is a different object but with same values
303+
for (int i = 0; i < original.getHosts().size(); i++) {
304+
Instance originalHost = original.getHosts().get(i);
305+
Instance clonedHost = cloned.getHosts().get(i);
306+
307+
assertNotSame(originalHost, clonedHost);
308+
assertEquals(originalHost.getInstanceId(), clonedHost.getInstanceId());
309+
assertEquals(originalHost.getIp(), clonedHost.getIp());
310+
assertEquals(originalHost.getPort(), clonedHost.getPort());
311+
assertEquals(originalHost.getWeight(), clonedHost.getWeight());
312+
assertEquals(originalHost.isHealthy(), clonedHost.isHealthy());
313+
assertEquals(originalHost.isEnabled(), clonedHost.isEnabled());
314+
assertEquals(originalHost.isEphemeral(), clonedHost.isEphemeral());
315+
assertEquals(originalHost.getClusterName(), clonedHost.getClusterName());
316+
assertEquals(originalHost.getServiceName(), clonedHost.getServiceName());
317+
}
318+
}
319+
320+
@Test
321+
void testCloneWithHostsMetadata() {
322+
final ServiceInfo original = new ServiceInfo("testGroup@@testName", "testClusters");
323+
324+
Instance instance = new Instance();
325+
instance.setIp("192.168.1.1");
326+
instance.setPort(8080);
327+
328+
Map<String, String> metadata = new HashMap<>();
329+
metadata.put("key1", "value1");
330+
metadata.put("key2", "value2");
331+
instance.setMetadata(metadata);
332+
333+
original.addHost(instance);
334+
335+
// Clone
336+
ServiceInfo cloned = original.clone();
337+
338+
// Verify metadata is deep copied
339+
Instance originalHost = original.getHosts().get(0);
340+
Instance clonedHost = cloned.getHosts().get(0);
341+
342+
assertNotSame(originalHost.getMetadata(), clonedHost.getMetadata());
343+
assertEquals(originalHost.getMetadata(), clonedHost.getMetadata());
344+
assertEquals(originalHost.getMetadata().size(), clonedHost.getMetadata().size());
345+
assertEquals("value1", clonedHost.getMetadata().get("key1"));
346+
assertEquals("value2", clonedHost.getMetadata().get("key2"));
347+
}
348+
349+
@Test
350+
void testCloneWithHostsNullMetadata() {
351+
final ServiceInfo original = new ServiceInfo("testGroup@@testName", "testClusters");
352+
353+
Instance instance = new Instance();
354+
instance.setIp("192.168.1.1");
355+
instance.setPort(8080);
356+
instance.setMetadata(null);
357+
358+
original.addHost(instance);
359+
360+
ServiceInfo cloned = original.clone();
361+
362+
Instance clonedHost = cloned.getHosts().get(0);
363+
// Instance metadata is initialized to empty HashMap by default
364+
// When clone method doesn't set metadata (because original is null),
365+
// the cloned Instance keeps its default empty HashMap
366+
assertTrue(clonedHost.getMetadata() != null && clonedHost.getMetadata().isEmpty());
367+
}
368+
369+
@Test
370+
void testCloneModificationDoesNotAffectOriginal() {
371+
ServiceInfo original = new ServiceInfo("testGroup@@testName", "testClusters");
372+
original.setCacheMillis(1000L);
373+
original.setAllIps(false);
374+
375+
Instance instance = new Instance();
376+
instance.setIp("192.168.1.1");
377+
instance.setPort(8080);
378+
Map<String, String> metadata = new HashMap<>();
379+
metadata.put("key1", "value1");
380+
instance.setMetadata(metadata);
381+
original.addHost(instance);
382+
383+
// Clone
384+
ServiceInfo cloned = original.clone();
385+
386+
// Modify cloned object
387+
cloned.setCacheMillis(2000L);
388+
cloned.setAllIps(true);
389+
cloned.setName("modifiedName");
390+
cloned.getHosts().get(0).setIp("10.0.0.1");
391+
cloned.getHosts().get(0).getMetadata().put("key2", "value2");
392+
393+
// Verify original is not affected
394+
assertEquals(1000L, original.getCacheMillis());
395+
assertFalse(original.isAllIps());
396+
assertEquals("testGroup@@testName", original.getName());
397+
assertEquals("192.168.1.1", original.getHosts().get(0).getIp());
398+
assertEquals(1, original.getHosts().get(0).getMetadata().size());
399+
assertFalse(original.getHosts().get(0).getMetadata().containsKey("key2"));
400+
}
205401
}

client/src/main/java/com/alibaba/nacos/client/naming/cache/ServiceInfoHolder.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,8 @@ public Map<String, ServiceInfo> getServiceInfoMap() {
100100

101101
public ServiceInfo getServiceInfo(final String serviceName, final String groupName) {
102102
String key = NamingUtils.getGroupedName(serviceName, groupName);
103-
return serviceInfoMap.get(key);
103+
ServiceInfo serviceInfo = serviceInfoMap.get(key);
104+
return serviceInfo == null ? null : serviceInfo.clone();
104105
}
105106

106107
/**

client/src/test/java/com/alibaba/nacos/client/naming/cache/ServiceInfoHolderTest.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040

4141
import static org.junit.jupiter.api.Assertions.assertEquals;
4242
import static org.junit.jupiter.api.Assertions.assertFalse;
43+
import static org.junit.jupiter.api.Assertions.assertNotSame;
4344
import static org.junit.jupiter.api.Assertions.assertNull;
4445
import static org.junit.jupiter.api.Assertions.assertTrue;
4546
import static org.mockito.ArgumentMatchers.anyInt;
@@ -271,9 +272,25 @@ void testGetServiceInfo() {
271272
String serviceName = "b";
272273
String groupName = "a";
273274
ServiceInfo actual = holder.getServiceInfo(serviceName, groupName);
275+
276+
// Verify it's a clone (different object)
277+
assertNotSame(expect, actual);
278+
279+
// Verify content is the same
274280
assertEquals(expect.getKey(), actual.getKey());
275281
assertEquals(expect.getHosts().size(), actual.getHosts().size());
276282
assertEquals(expect.getHosts().get(0), actual.getHosts().get(0));
283+
284+
// Verify hosts list is different
285+
assertNotSame(expect.getHosts(), actual.getHosts());
286+
}
287+
288+
@Test
289+
void testGetServiceInfoReturnsNull() {
290+
String serviceName = "nonExistent";
291+
String groupName = "group";
292+
ServiceInfo actual = holder.getServiceInfo(serviceName, groupName);
293+
assertNull(actual);
277294
}
278295

279296
@Test

0 commit comments

Comments
 (0)