ConcurrentHashMap的keySet()的兼容问题

本文分析了使用Java8编译的程序在JRE6上因ConcurrentHashMap$KeySetView类缺失导致的ClassNotFoundException问题,并提供了解决方案。建议使用Map接口而非具体实现类定义变量,以提高代码的兼容性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

(本文最早由我于2018-06-28 11:50在cnblogs上发布:https://www.cnblogs.com/firas/p/9237986.html

这两天遇到一个问题,我用JDK 8编译出来的程序在JRE 6上运行报ClassNotFoundException,说找不到java.util.concurrent.ConcurrentHashMap$KeySetView这个类。

原来Java 7或以下版本中是没有java.util.concurrent.ConcurrentHashMap$KeySetView这个类的。在Java 8中,ConcurrentHashMap中才新增了KeySetView这个public的内部类,而ConcurrentHashMap的keySet方法的返回值也改成了KeySetView。这样,对于以下代码:

ConcurrentHashMap<String, Object> map = new ConcurrentHashMap<String, Object>();
for (String key : map.keySet()) {
    // do something
}

解开for-in的语法糖,JDK 8实际上会把代码看成以下的样子:

ConcurrentHashMap<String, Object> map = new ConcurrentHashMap<String, Object>();
ConcurrentHashMap.KeySetView<String, Object> temp1 = map.keySet();
for (Iterator<String> iterator = temp1.iterator(); iterator.hasNext(); ) {
    String key = iterator.next();
    // do something
}

因此,在JRE 6上执行的时候,就会找不到java.util.concurrent.ConcurrentHashMap$KeySetView这个类。

解决方法是在定义map变量的时候,用Map接口来定义,即

Map<String, Object> map = new ConcurrentHashMap<String, Object>();
for (String key : map.keySet()) {
    // do something
}

通过这个问题,我们可以看到,在定义变量的时候用接口来定义而不是用实现类来定义,在实现类的具体实现变化的时候能避免产生一些兼容性的问题。

<think>我们使用Spring的DataBinder进行属性绑定时,默认支持两种属性命名风格: 1. 驼峰命名:如 `appender[redis].maxHistory` 2. 烤肉串命名(kebab-case):如 `appender[redis].max-history` 但是,默认情况下,Spring的DataBinder使用的是`DefaultConversionService`,它并不自动支持将烤肉串命名转换为驼峰命名。因此,如果我们希望同时支持两种命名方式,我们需要自定义一个`ConversionService`,并注册一个`PropertyEditor`或`Converter`来处理这种转换。 然而,实际上Spring Boot在绑定`@ConfigurationProperties`时,会自动支持多种命名风格(包括驼峰、烤肉串、下划线等)的转换。但如果我们直接使用`DataBinder`,则需要手动配置。 解决方案: 1. 使用`DefaultFormattingConversionService`并注册一个自定义的`PropertyEditor`,或者 2. 在绑定之前,将属性名称统一转换为驼峰式(因为我们的JavaBean属性是驼峰式命名的)。 这里我们选择第二种方法,因为更直接:遍历`MutablePropertyValues`,将每个属性的名称从烤肉串式转换为驼峰式。 步骤: - 将属性名中的中划线(-)移除,并将后续字母大写(驼峰式)。 - 注意:我们只转换属性名中非索引部分。例如,对于`appender[redis].max-history`,我们只需要将`max-history`转换为`maxHistory`,而`appender[redis]`保持不变。 具体实现: 我们可以编写一个工具方法,将属性名中的每个部分(以点分隔)进行转换(最后一个部分进行驼峰转换)。 但是注意:在属性名中,可能存在数组索引(如`appender[redis].logger[0]`),我们不需要转换索引部分,只需要转换属性标识符部分。 因此,我们可以将属性名按点分割,然后对每个点分割的部分,如果包含`[`,则只对`[`之前的部分进行转换(因为`[`之后是索引或key),然后对最后一部分进行驼峰转换(如果没有`[`,则整个部分转换)。 然而,为了简单起见,我们只对属性名中最后一个点之后的部分进行转换(因为前面的部分都是嵌套属性,而我们的目标bean中嵌套属性也是驼峰命名的,但注意Map的key(如[redis])不需要转换)。 所以,转换规则: - 将整个属性名按点分割成多个部分。 - 将最后一部分中的烤肉串命名转换为驼峰命名。 - 重新组合。 例如:`appender[redis].max-history` -> 分割为`appender[redis]`和`max-history`,然后将`max-history`转换为`maxHistory`,得到`appender[redis].maxHistory`。 代码实现: 我们创建一个方法,用于转换属性名: ```java import org.springframework.beans.MutablePropertyValues; import org.springframework.beans.PropertyValue; import java.util.ArrayList; import java.util.List; public class PropertyNameUtils { // 将属性名中的烤肉串风格转换为驼峰风格 public static String toCamelCase(String propertyName) { if (propertyName == null || propertyName.isEmpty()) { return propertyName; } // 按点分割 String[] parts = propertyName.split("\\."); List<String> convertedParts = new ArrayList<>(); for (int i = 0; i < parts.length; i++) { String part = parts[i]; // 如果是最后一部分,则进行转换 if (i == parts.length - 1) { convertedParts.add(kebabToCamel(part)); } else { convertedParts.add(part); } } return String.join(".", convertedParts); } private static String kebabToCamel(String str) { if (str == null || str.isEmpty()) { return str; } // 如果字符串中包含[,则只处理[前面的部分,因为后面是索引 if (str.contains("[")) { int bracketIndex = str.indexOf('['); String prefix = str.substring(0, bracketIndex); String suffix = str.substring(bracketIndex); return kebabToCamel(prefix) + suffix; } // 将烤肉串命名转换为驼峰 StringBuilder result = new StringBuilder(); boolean nextUpper = false; for (char c : str.toCharArray()) { if (c == '-') { nextUpper = true; } else { if (nextUpper) { result.append(Character.toUpperCase(c)); nextUpper = false; } else { result.append(c); } } } return result.toString(); } // 转换MutablePropertyValues中的所有属性名 public static void convertToCamelCase(MutablePropertyValues propertyValues) { List<PropertyValue> propertyValueList = new ArrayList<>(); propertyValues.getPropertyValueList().forEach(pv -> propertyValueList.add(pv)); for (PropertyValue pv : propertyValueList) { String camelName = toCamelCase(pv.getName()); if (!camelName.equals(pv.getName())) { // 移除原属性,添加新属性 propertyValues.removePropertyValue(pv.getName()); propertyValues.add(camelName, pv.getValue()); } } } } ``` 然后,在绑定之前,调用这个工具方法转换属性名: ```java MutablePropertyValues propertyValues = buildMutablePropertyValues(); // 原始属性值 PropertyNameUtils.convertToCamelCase(propertyValues); // 转换为驼峰命名 DataBinder dataBinder = new DataBinder(target); dataBinder.bind(propertyValues); ``` 这样,无论是`appender[redis].maxHistory`还是`appender[redis].max-history`,都会被转换为`appender[redis].maxHistory`,从而与JavaBean中的属性名`maxHistory`匹配。 注意:这种方法要求我们的JavaBean属性使用驼峰命名法。 另外,如果属性名中还有下划线,我们可以用同样的方式处理,只需在转换方法中增加对下划线的处理即可。 但是,如果我们的配置中同时存在多种命名风格(如下划线),我们可以扩展`kebabToCamel`方法,使其同时转换下划线。 修改`kebabToCamel`方法,使其同时转换下划线和短横线: ```java private static String kebabToCamel(String str) { if (str == null || str.isEmpty()) { return str; } if (str.contains("[")) { int bracketIndex = str.indexOf('['); String prefix = str.substring(0, bracketIndex); String suffix = str.substring(bracketIndex); return kebabToCamel(prefix) + suffix; } StringBuilder result = new StringBuilder(); boolean nextUpper = false; for (char c : str.toCharArray()) { if (c == '-' || c == '_') { nextUpper = true; } else { if (nextUpper) { result.append(Character.toUpperCase(c)); nextUpper = false; } else { result.append(c); } } } return result.toString(); } ``` 这样,`max_history`也会被转换为`maxHistory`。 总结:通过预处理属性名,将烤肉串命名(和下划线命名)转换为驼峰命名,可以兼容多种配置属性名的写法。 注意:这种方法可能会改变原始属性名,如果原始属性名中同时包含短横线和下划线,也会被转换。确保这是你想要的行为。 另外,如果属性名中已经包含大写字母,转换过程不会改变它们(除了将分隔符后的字母大写)。例如,`my-Property-Name`会被转换为`myPropertyName`。 最后,如果配置中同时存在两种写法的同一个属性(例如,同时有`max-history`和`maxHistory`),那么转换后会出现两个相同的属性名,后绑定的会覆盖先绑定的。因此,需要确保配置中不要出现重复的属性。 另一种方案:使用Spring Boot的`RelaxedDataBinder`(在Spring Boot 1.x中)或者`Binder`(在Spring Boot 2.x中),它们本身就支持多种属性命名风格。但是,由于你直接使用的是`DataBinder`,我们采用上述转换方法。 如果你的项目是Spring Boot,建议使用`@ConfigurationProperties`和`EnableConfigurationProperties`进行绑定,这样Spring Boot会自动处理多种命名风格。 但是,如果你必须使用`DataBinder`,那么上述转换是一个可行的方案。 相关代码已经给出,请根据实际需求调整。 §§相关问题§§: 1. 在Spring Boot中,如何使用`@ConfigurationProperties`绑定嵌套Map结构的配置? 2. 除了手动转换属性名,还有哪些方法可以让DataBinder支持多种命名风格? 3. 如何处理属性绑定时的类型转换问题,例如将字符串转换为枚举? 4. 在Spring Boot 2.x中,如何使用新的`Binder` API来绑定配置?</think>### 兼容 `appender[redis].maxHistory` 和 `appender[redis].max-history` 格式 在 Spring 的 `DataBinder` 中,要同时兼容驼峰式(`maxHistory`)和烤肉串式(`max-history`)属性命名,需要自定义属性访问器。以下是完整解决方案: ```java import org.springframework.beans.*; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.format.support.DefaultFormattingConversionService; import java.beans.PropertyDescriptor; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; public class CustomDataBinder extends DataBinder { // 缓存属性名映射关系 private final Map<Class<?>, Map<String, String>> propertyAliasCache = new ConcurrentHashMap<>(); public CustomDataBinder(Object target) { super(target); // 配置自定义属性访问器 this.setAutoGrowNestedPaths(true); this.setConversionService(createConversionService()); this.initBeanWrapper(); } private void initBeanWrapper() { BeanWrapper beanWrapper = new CustomBeanWrapper(getTarget()); beanWrapper.setAutoGrowNestedPaths(true); beanWrapper.setConversionService(getConversionService()); setInternalBeanWrapper(beanWrapper); } private ConversionService createConversionService() { return new DefaultFormattingConversionService(); } // 自定义 BeanWrapper 处理属性名兼容 private static class CustomBeanWrapper extends BeanWrapperImpl { public CustomBeanWrapper(Object object) { super(object); } @Override protected PropertyDescriptor getCachedIntrospectionResults() { return super.getCachedIntrospectionResults(); } @Override public Object getPropertyValue(String propertyName) throws BeansException { return super.getPropertyValue(normalizePropertyName(propertyName)); } @Override public void setPropertyValue(String propertyName, Object value) throws BeansException { super.setPropertyValue(normalizePropertyName(propertyName), value); } // 统一属性名格式为驼峰式 private String normalizePropertyName(String originalName) { // 处理数组索引 [0] 部分 String arrayPart = ""; if (originalName.contains("[")) { int bracketIndex = originalName.indexOf('['); arrayPart = originalName.substring(bracketIndex); originalName = originalName.substring(0, bracketIndex); } // 转换烤肉串式为驼峰式 if (originalName.contains("-")) { StringBuilder camelCase = new StringBuilder(); boolean nextUpper = false; for (char c : originalName.toCharArray()) { if (c == '-') { nextUpper = true; } else { camelCase.append(nextUpper ? Character.toUpperCase(c) : c); nextUpper = false; } } return camelCase.toString() + arrayPart; } return originalName + arrayPart; } } } ``` ### 使用示例 ```java public class LogConfigBinder { public static void bind(LogProperties target, Map<String, Object> configs) { // 1. 初始化 appender Map target.setAppender(new LinkedHashMap<>()); // 2. 提取所有唯一前缀 (redis, rocket-mq, web) Set<String> prefixes = configs.keySet().stream() .map(key -> key.split("\\.")[0]) .collect(Collectors.toSet()); // 3. 为每个前缀创建 CustomAppenderConfig 实例 prefixes.forEach(prefix -> { CustomAppenderConfig config = new CustomAppenderConfig(); target.getAppender().put(prefix, config); }); // 4. 使用自定义 DataBinder CustomDataBinder dataBinder = new CustomDataBinder(target); // 5. 转换并绑定属性 MutablePropertyValues propertyValues = new MutablePropertyValues(); configs.forEach((key, value) -> { // 两种格式都支持 String formattedKey1 = "appender[" + key.replaceFirst("\\.", "]."); // appender[redis].maxHistory String formattedKey2 = "appender." + key; // appender.redis.maxHistory propertyValues.add(formattedKey1, value); propertyValues.add(formattedKey2, value); }); // 6. 绑定属性 dataBinder.bind(propertyValues); // 7. 检查绑定错误 if (dataBinder.getBindingResult().hasErrors()) { dataBinder.getBindingResult().getAllErrors().forEach(error -> System.err.println("绑定错误: " + error)); } } } ``` ### 关键实现说明 1. **自定义 DataBinder**: - 继承 Spring 的 `DataBinder` 并覆盖内部 `BeanWrapper` - `CustomBeanWrapper` 处理属性名转换逻辑 2. **属性名规范化**: ```java private String normalizePropertyName(String originalName) { // 处理数组索引部分 String arrayPart = ""; if (originalName.contains("[")) { int bracketIndex = originalName.indexOf('['); arrayPart = originalName.substring(bracketIndex); originalName = originalName.substring(0, bracketIndex); } // 转换烤肉串式为驼峰式 if (originalName.contains("-")) { StringBuilder camelCase = new StringBuilder(); boolean nextUpper = false; for (char c : originalName.toCharArray()) { if (c == '-') { nextUpper = true; } else { camelCase.append(nextUpper ? Character.toUpperCase(c) : c); nextUpper = false; } } return camelCase.toString() + arrayPart; } return originalName + arrayPart; } ``` - 将 `max-history` 转换为 `maxHistory` - 保留数组索引 `[0]` 不变 - 支持混合格式如 `appender[redis].max-history` 3. **多种路径格式支持**: ```java // 支持两种路径格式 String formattedKey1 = "appender[" + key.replaceFirst("\\.", "]."); String formattedKey2 = "appender." + key; propertyValues.add(formattedKey1, value); propertyValues.add(formattedKey2, value); ``` - `appender[redis].maxHistory` (Spring 标准格式) - `appender.redis.maxHistory` (点分隔格式) 4. **枚举类型处理**: - 确保枚举类有 `fromValue` 方法: ```java public enum CompressionType { NONE, GZ, GZIP, ZIP; public static CompressionType fromValue(String value) { if (value == null) return NONE; String normalized = value.trim().toUpperCase(); if ("GZ".equals(normalized)) return GZ; if ("GZIP".equals(normalized)) return GZIP; if ("ZIP".equals(normalized)) return ZIP; return NONE; } } ``` ### 替代方案:使用 Spring Boot 的 @ConfigurationProperties 如果使用 Spring Boot,更简单的解决方案是: ```java @Configuration @ConfigurationProperties(prefix = "appender") public class LogProperties { private Map<String, CustomAppenderConfig> appender = new LinkedHashMap<>(); // getters/setters } // 在配置文件中 appender.redis.max-history=20 appender.redis.level=info appender.redis.logger[0]=tech.honghu.framework.sample.core ``` Spring Boot 会自动处理: 1. 驼峰式和烤肉串式命名兼容 2. 嵌套属性绑定 3. 数组/列表转换 4. 枚举类型转换 ### 常见问题排查 1. **属性未初始化**: ```java // LogProperties 中需要初始化 Map @Getter @Setter private Map<String, CustomAppenderConfig> appender = new LinkedHashMap<>(); ``` 2. **嵌套对象未创建**: ```java // 绑定前需要初始化嵌套对象 prefixes.forEach(prefix -> { target.getAppender().put(prefix, new CustomAppenderConfig()); }); ``` 3. **类型转换错误**: - 确保数值属性有正确的 setter 方法 - 枚举类型实现 `fromValue` 方法 - 列表属性已初始化
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值