-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Optimize dynamic config: integrate Zookeeper & Nacos, support interface-level dynamic config #1430
Changes from 2 commits
528d0fb
853ed3f
92351f2
d841de9
7a354b3
b8c60ae
08a0a85
0f613cf
117b33c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
|
@@ -28,6 +28,8 @@ | |||||||
import com.alipay.sofa.rpc.config.RegistryConfig; | ||||||||
import com.alipay.sofa.rpc.context.RpcRuntimeContext; | ||||||||
import com.alipay.sofa.rpc.core.exception.SofaRpcRuntimeException; | ||||||||
import com.alipay.sofa.rpc.dynamic.ConfigChangedEvent; | ||||||||
import com.alipay.sofa.rpc.dynamic.ConfigChangeType; | ||||||||
import com.alipay.sofa.rpc.dynamic.DynamicConfigKeys; | ||||||||
import com.alipay.sofa.rpc.dynamic.DynamicConfigManager; | ||||||||
import com.alipay.sofa.rpc.dynamic.DynamicConfigManagerFactory; | ||||||||
|
@@ -42,10 +44,7 @@ | |||||||
import com.alipay.sofa.rpc.registry.Registry; | ||||||||
import com.alipay.sofa.rpc.registry.RegistryFactory; | ||||||||
|
||||||||
import java.util.ArrayList; | ||||||||
import java.util.HashMap; | ||||||||
import java.util.List; | ||||||||
import java.util.Map; | ||||||||
import java.util.*; | ||||||||
import java.util.concurrent.ConcurrentHashMap; | ||||||||
import java.util.concurrent.ConcurrentMap; | ||||||||
import java.util.concurrent.CountDownLatch; | ||||||||
|
@@ -156,13 +155,26 @@ public T refer() { | |||||||
proxyIns = (T) ProxyFactory.buildProxy(consumerConfig.getProxy(), consumerConfig.getProxyClass(), | ||||||||
proxyInvoker); | ||||||||
|
||||||||
//动态配置 | ||||||||
//启用请求级别动态配置 | ||||||||
final String dynamicAlias = consumerConfig.getParameter(DynamicConfigKeys.DYNAMIC_ALIAS); | ||||||||
if (StringUtils.isNotBlank(dynamicAlias)) { | ||||||||
final DynamicConfigManager dynamicManager = DynamicConfigManagerFactory.getDynamicManager( | ||||||||
consumerConfig.getAppName(), dynamicAlias); | ||||||||
dynamicManager.initServiceConfiguration(consumerConfig.getInterfaceId()); | ||||||||
} | ||||||||
|
||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing In the request-level dynamic configuration setup, you initialize the Consider adding a |
||||||||
//启用接口级别动态配置 | ||||||||
final String dynamicUrl = consumerConfig.getParameter(DynamicConfigKeys.DYNAMIC_URL); | ||||||||
if (StringUtils.isNotBlank(dynamicUrl)) { | ||||||||
final DynamicConfigManager dynamicManager = DynamicConfigManagerFactory.getDynamicManagerWithUrl( | ||||||||
consumerConfig.getAppName(), dynamicUrl); | ||||||||
|
||||||||
//build listeners for dynamic config | ||||||||
consumerConfig.setConfigListener(buildConfigListener(this,dynamicManager)); | ||||||||
} | ||||||||
|
||||||||
|
||||||||
|
||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Overwriting existing In the interface-level dynamic configuration, you set a new consumerConfig.setConfigListener(buildConfigListener(this, dynamicManager)); Since Consider modifying the implementation to support multiple |
||||||||
} catch (Exception e) { | ||||||||
if (cluster != null) { | ||||||||
cluster.destroy(); | ||||||||
|
@@ -202,6 +214,17 @@ protected ConfigListener buildConfigListener(ConsumerBootstrap bootstrap) { | |||||||
return new ConsumerAttributeListener(); | ||||||||
} | ||||||||
|
||||||||
/** | ||||||||
* Build ConfigListener for consumer bootstrap with dynamic config. | ||||||||
* | ||||||||
* @param bootstrap ConsumerBootstrap | ||||||||
* @param dynamicManager DynamicConfigManager | ||||||||
* @return ConfigListener | ||||||||
*/ | ||||||||
protected ConfigListener buildConfigListener(ConsumerBootstrap bootstrap, DynamicConfigManager dynamicManager) { | ||||||||
return new ConsumerAttributeListener(dynamicManager); | ||||||||
} | ||||||||
|
||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Clarify the usage of overloaded The introduction of an overloaded
To improve code readability and maintainability, consider renaming one of the methods to more clearly reflect its purpose, such as |
||||||||
/** | ||||||||
* Build ProviderInfoListener for consumer bootstrap. | ||||||||
* | ||||||||
|
@@ -438,8 +461,46 @@ public void updateAllProviders(List<ProviderGroup> groups) { | |||||||
*/ | ||||||||
private class ConsumerAttributeListener implements ConfigListener { | ||||||||
|
||||||||
private Map<String, String> newValueMap = new HashMap<>(); | ||||||||
|
||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use ConcurrentHashMap for thread safety.
Apply this diff to use ConcurrentHashMap: -private Map<String, String> newValueMap = new HashMap<>();
+private Map<String, String> newValueMap = new ConcurrentHashMap<>(); 📝 Committable suggestion
Suggested change
|
||||||||
ConsumerAttributeListener() { | ||||||||
|
||||||||
} | ||||||||
|
||||||||
ConsumerAttributeListener(DynamicConfigManager dynamicManager) { | ||||||||
this.initWith(consumerConfig.getInterfaceId(),dynamicManager); | ||||||||
} | ||||||||
|
||||||||
public void initWith(String key, DynamicConfigManager dynamicManager) { | ||||||||
dynamicManager.addListener(key, this); | ||||||||
// 初始化配置值 | ||||||||
String rawConfig = dynamicManager.getConfig(key); | ||||||||
if (!StringUtils.isEmpty(rawConfig)) { | ||||||||
process(new ConfigChangedEvent(key, "sofa-rpc",rawConfig)); | ||||||||
} | ||||||||
} | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ensure listeners are unregistered to prevent memory leaks In the dynamicManager.addListener(key, this); However, there is no corresponding logic to remove this listener when it is no longer needed. This might lead to memory leaks if the listener remains registered after the consumer is unreferenced. Consider implementing a mechanism to unregister the listener when the consumer is unreferenced or destroyed. This can be done in the dynamicManager.removeListener(key, this); |
||||||||
|
||||||||
@Override | ||||||||
public void process(ConfigChangedEvent event){ | ||||||||
if (event.getChangeType().equals(ConfigChangeType.DELETED)) { | ||||||||
newValueMap = null; | ||||||||
} else { | ||||||||
// ADDED or MODIFIED | ||||||||
String[] lines = event.getContent().split("\n"); | ||||||||
for (String line : lines) { | ||||||||
String[] keyValue = line.split("=", 2); | ||||||||
if (keyValue.length == 2) { | ||||||||
String key = keyValue[0].trim(); | ||||||||
String value = keyValue[1].trim(); | ||||||||
newValueMap.put(key, value); | ||||||||
} | ||||||||
} | ||||||||
} | ||||||||
attrUpdated(newValueMap); | ||||||||
} | ||||||||
|
||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add error handling for configuration parsing in In the String[] lines = event.getContent().split("\n");
for (String line : lines) {
String[] keyValue = line.split("=", 2);
if (keyValue.length == 2) {
String key = keyValue[0].trim();
String value = keyValue[1].trim();
newValueMap.put(key, value);
}
} If the configuration content is malformed, this could lead to incorrect behavior or missed configurations. Consider adding error handling to manage malformed lines gracefully. For example, log a warning when a line doesn't conform to the expected for (String line : lines) {
String[] keyValue = line.split("=", 2);
if (keyValue.length == 2) {
String key = keyValue[0].trim();
String value = keyValue[1].trim();
newValueMap.put(key, value);
} else {
LOGGER.warn("Malformed configuration line: {}", line);
}
} |
||||||||
@Override | ||||||||
public void configChanged(Map newValue) { | ||||||||
public void configChanged(Map newValueMap) { | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unused The method public void configChanged(Map newValueMap) {
} Since it doesn't perform any actions and isn't called within this class, it may be unnecessary. If this method is not required, consider removing it to clean up the code. If it's intended for future use or to fulfill an interface contract, consider adding a comment explaining its purpose. |
||||||||
|
||||||||
} | ||||||||
|
||||||||
|
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -16,13 +16,24 @@ | |||||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||||||
package com.alipay.sofa.rpc.dynamic.apollo; | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
import com.alipay.sofa.common.config.SofaConfigs; | ||||||||||||||||||||||||||||||
import com.alipay.sofa.rpc.auth.AuthRuleGroup; | ||||||||||||||||||||||||||||||
import com.alipay.sofa.rpc.dynamic.DynamicConfigKeyHelper; | ||||||||||||||||||||||||||||||
import com.alipay.sofa.rpc.dynamic.DynamicConfigManager; | ||||||||||||||||||||||||||||||
import com.alipay.sofa.rpc.dynamic.DynamicHelper; | ||||||||||||||||||||||||||||||
import com.alipay.sofa.rpc.dynamic.*; | ||||||||||||||||||||||||||||||
import com.alipay.sofa.rpc.ext.Extension; | ||||||||||||||||||||||||||||||
import com.alipay.sofa.rpc.listener.ConfigListener; | ||||||||||||||||||||||||||||||
import com.alipay.sofa.rpc.log.Logger; | ||||||||||||||||||||||||||||||
import com.alipay.sofa.rpc.log.LoggerFactory; | ||||||||||||||||||||||||||||||
import com.ctrip.framework.apollo.Config; | ||||||||||||||||||||||||||||||
import com.ctrip.framework.apollo.ConfigChangeListener; | ||||||||||||||||||||||||||||||
import com.ctrip.framework.apollo.ConfigService; | ||||||||||||||||||||||||||||||
import com.ctrip.framework.apollo.enums.PropertyChangeType; | ||||||||||||||||||||||||||||||
import com.ctrip.framework.apollo.model.ConfigChange; | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
import java.util.Collections; | ||||||||||||||||||||||||||||||
import java.util.Set; | ||||||||||||||||||||||||||||||
import java.util.concurrent.ConcurrentHashMap; | ||||||||||||||||||||||||||||||
import java.util.concurrent.ConcurrentMap; | ||||||||||||||||||||||||||||||
import java.util.concurrent.CopyOnWriteArraySet; | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||||
* @author bystander | ||||||||||||||||||||||||||||||
|
@@ -34,11 +45,30 @@ | |||||||||||||||||||||||||||||
@Extension(value = "apollo", override = true) | ||||||||||||||||||||||||||||||
public class ApolloDynamicConfigManager extends DynamicConfigManager { | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
Logger LOGGER = LoggerFactory.getLogger(ApolloDynamicConfigManager.class); | ||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Specify Access Modifier and Make LOGGER Static Final The Apply this diff to correct it: - Logger LOGGER = LoggerFactory.getLogger(ApolloDynamicConfigManager.class);
+ private static final Logger LOGGER = LoggerFactory.getLogger(ApolloDynamicConfigManager.class); Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
private static final String APOLLO_APPID_KEY = "app.id"; | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
private static final String APOLLO_ADDR_KEY = "apollo.meta"; | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
private static final String APOLLO_PROTOCOL_PREFIX = "http://"; | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
private Config config; | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
private final ConcurrentMap<String, ApolloListener> watchListenerMap = new ConcurrentHashMap<>(); | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
protected ApolloDynamicConfigManager(String appName) { | ||||||||||||||||||||||||||||||
super(appName); | ||||||||||||||||||||||||||||||
config = ConfigService.getAppConfig(); | ||||||||||||||||||||||||||||||
System.setProperty(APOLLO_APPID_KEY, appName); | ||||||||||||||||||||||||||||||
System.setProperty(APOLLO_ADDR_KEY, APOLLO_PROTOCOL_PREFIX + SofaConfigs.getOrDefault(DynamicConfigKeys.APOLLO_ADDRESS)); | ||||||||||||||||||||||||||||||
config = ConfigService.getConfig(DynamicConfigKeys.DEFAULT_GROUP); | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
protected ApolloDynamicConfigManager(String appName,String address) { | ||||||||||||||||||||||||||||||
super(appName); | ||||||||||||||||||||||||||||||
System.setProperty(APOLLO_APPID_KEY, appName); | ||||||||||||||||||||||||||||||
System.setProperty(APOLLO_ADDR_KEY, APOLLO_PROTOCOL_PREFIX + address); | ||||||||||||||||||||||||||||||
config = ConfigService.getConfig(DynamicConfigKeys.DEFAULT_GROUP); | ||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoid Using Setting system properties within a constructor can have unintended side effects, as system properties are global to the JVM and affect all parts of the application. This may lead to unpredictable behavior, especially in multi-threaded environments or when multiple instances are created. Consider refactoring the code to avoid setting system properties in the constructor. Instead, pass the necessary configuration directly to the components that require it. |
||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
@Override | ||||||||||||||||||||||||||||||
|
@@ -77,4 +107,49 @@ public AuthRuleGroup getServiceAuthRule(String service) { | |||||||||||||||||||||||||||||
//TODO 暂不支持 | ||||||||||||||||||||||||||||||
return null; | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
@Override | ||||||||||||||||||||||||||||||
public String getConfig(String key){ | ||||||||||||||||||||||||||||||
return config.getProperty(key, DynamicHelper.DEFAULT_DYNAMIC_VALUE); | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
@Override | ||||||||||||||||||||||||||||||
public void addListener(String key, ConfigListener listener){ | ||||||||||||||||||||||||||||||
ApolloListener apolloListener = watchListenerMap.computeIfAbsent(key, k -> new ApolloListener()); | ||||||||||||||||||||||||||||||
apolloListener.addListener(listener); | ||||||||||||||||||||||||||||||
config.addChangeListener(apolloListener, Collections.singleton(key)); | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
public class ApolloListener implements ConfigChangeListener { | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
private Set<ConfigListener> listeners = new CopyOnWriteArraySet<>(); | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
ApolloListener() {} | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
@Override | ||||||||||||||||||||||||||||||
public void onChange(com.ctrip.framework.apollo.model.ConfigChangeEvent changeEvent) { | ||||||||||||||||||||||||||||||
for (String key : changeEvent.changedKeys()) { | ||||||||||||||||||||||||||||||
ConfigChange change = changeEvent.getChange(key); | ||||||||||||||||||||||||||||||
if ("".equals(change.getNewValue())) { | ||||||||||||||||||||||||||||||
LOGGER.info("an empty rule is received for key: " + key); | ||||||||||||||||||||||||||||||
return; | ||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Incorrect Use of Using Apply this diff to fix the issue: - if ("".equals(change.getNewValue())) {
LOGGER.info("an empty rule is received for key: " + key);
- return;
+ continue;
} Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
ConfigChangedEvent event = | ||||||||||||||||||||||||||||||
new ConfigChangedEvent(key, change.getNamespace(), change.getNewValue(), getChangeType(change)); | ||||||||||||||||||||||||||||||
listeners.forEach(listener -> listener.process(event)); | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
private ConfigChangeType getChangeType(ConfigChange change) { | ||||||||||||||||||||||||||||||
if (change.getChangeType() == PropertyChangeType.DELETED) { | ||||||||||||||||||||||||||||||
return ConfigChangeType.DELETED; | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
return ConfigChangeType.MODIFIED; | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Handle All Possible Property Change Types The Update the method to handle additional change types: private ConfigChangeType getChangeType(ConfigChange change) {
if (change.getChangeType() == PropertyChangeType.DELETED) {
return ConfigChangeType.DELETED;
+ } else if (change.getChangeType() == PropertyChangeType.ADDED) {
+ return ConfigChangeType.ADDED;
}
return ConfigChangeType.MODIFIED;
} 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
void addListener(ConfigListener configListener) { | ||||||||||||||||||||||||||||||
this.listeners.add(configListener); | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
Comment on lines
+176
to
+178
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Provide a method to remove listeners The Implement the void addListener(ConfigListener configListener) {
this.listeners.add(configListener);
}
+void removeListener(ConfigListener configListener) {
+ this.listeners.remove(configListener);
+} 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ensure Thread Safety When Managing Listeners The Evaluate whether |
||||||||||||||||||||||||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,6 +16,8 @@ | |
*/ | ||
package com.alipay.sofa.rpc.dynamic.apollo; | ||
|
||
import com.alipay.sofa.rpc.dynamic.DynamicConfigManager; | ||
import com.alipay.sofa.rpc.dynamic.DynamicConfigManagerFactory; | ||
import com.alipay.sofa.rpc.dynamic.DynamicHelper; | ||
import com.alipay.sofa.rpc.log.Logger; | ||
import com.alipay.sofa.rpc.log.LoggerFactory; | ||
|
@@ -24,10 +26,11 @@ | |
|
||
public class ApolloDynamicConfigManagerTest { | ||
|
||
private final static Logger logger = LoggerFactory | ||
.getLogger(ApolloDynamicConfigManagerTest.class); | ||
private final static Logger logger = LoggerFactory | ||
.getLogger(ApolloDynamicConfigManagerTest.class); | ||
|
||
private ApolloDynamicConfigManager apolloDynamicConfigManager = new ApolloDynamicConfigManager("test"); | ||
private DynamicConfigManager apolloDynamicConfigManager = DynamicConfigManagerFactory.getDynamicManager("test", | ||
"apollo"); | ||
|
||
@Test | ||
public void getProviderServiceProperty() { | ||
|
@@ -37,17 +40,19 @@ public void getProviderServiceProperty() { | |
|
||
@Test | ||
public void getConsumerServiceProperty() { | ||
String result = apolloDynamicConfigManager.getConsumerServiceProperty("serviceName", "timeout"); | ||
Assert.assertEquals(DynamicHelper.DEFAULT_DYNAMIC_VALUE, result); | ||
} | ||
|
||
@Test | ||
public void getProviderMethodProperty() { | ||
String result = apolloDynamicConfigManager.getProviderMethodProperty("serviceName", "methodName", "timeout"); | ||
Assert.assertEquals(DynamicHelper.DEFAULT_DYNAMIC_VALUE, result); | ||
} | ||
|
||
@Test | ||
public void getConsumerMethodProperty() { | ||
} | ||
|
||
@Test | ||
public void getServiceAuthRule() { | ||
String result = apolloDynamicConfigManager.getConsumerMethodProperty("serviceName", "methodName", "timeout"); | ||
Assert.assertEquals(DynamicHelper.DEFAULT_DYNAMIC_VALUE, result); | ||
Comment on lines
+43
to
+56
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Enhance test coverage with additional test scenarios. The current tests only verify default values. Consider adding:
Example test to add: @Test
public void testDynamicConfigUpdate() {
// Setup initial value
String serviceName = "testService";
String property = "timeout";
String initialValue = apolloDynamicConfigManager.getConsumerServiceProperty(serviceName, property);
// Simulate config change
// TODO: Add mechanism to trigger config change
// Verify updated value
String updatedValue = apolloDynamicConfigManager.getConsumerServiceProperty(serviceName, property);
Assert.assertNotEquals("Config should be updated", initialValue, updatedValue);
}
@Test(expected = IllegalArgumentException.class)
public void testInvalidServiceName() {
apolloDynamicConfigManager.getConsumerServiceProperty(null, "timeout");
} |
||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Avoid using wildcard imports; import specific classes instead
Using wildcard imports like
import java.util.*;
can lead to namespace pollution and may introduce ambiguity between class names. It's best practice to import only the specific classes you need.Apply this diff to import specific classes: