迁移至Java21+SpringBoot3.2.0时Cassandra配置的私有字段访问问题
问题场景
从Java8+SpringBoot v1.x迁移至Java21+SpringBoot v3.2.0,配置Cassandra时遇到反射访问java.util.GregorianCalendar私有字段的权限错误。临时通过添加JVM参数--add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED可解决,但该方案存在安全风险,需寻求合规的解决方式。
报错信息
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'cassandraMappingContext' defined in class path resource [com/blabla/main/CassandraConfig.class]: Unable to make field private long java.util.GregorianCalendar.gregorianCutover accessible: module java.base does not "opens java.util" to unnamed module @61862a7f at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1775) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:601) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:523) at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:325) at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:323) at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:224) at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveNamedBean(DefaultListableBeanFactory.java:1321) at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveNamedBean(DefaultListableBeanFactory.java:1282) at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveBean(DefaultListableBeanFactory.java:484) at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:339) at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:332) at org.springframework.data.cassandra.config.AbstractSessionConfiguration.requireBeanOfType(AbstractSessionConfiguration.java:103) at org.springframework.data.cassandra.config.AbstractCassandraConfiguration.cassandraConverter(AbstractCassandraConfiguration.java:76) at com.blabla.main.CassandraConfig$$SpringCGLIB$$0.CGLIB$cassandraConverter$20(<generated>) at com.blabla.main.CassandraConfig$$SpringCGLIB$$FastClass$$0.invoke(<generated>) at org.springframework.cglib.proxy.MethodProxy.invokeSuper(MethodProxy.java:258) at org.springframework.context.annotation.ConfigurationClassEnhancer$BeanMethodInterceptor.intercept(ConfigurationClassEnhancer.java:331) at com.blabla.main.CassandraConfig$$SpringCGLIB$$0.cassandraConverter(<generated>) at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) at java.base/java.lang.reflect.Method.invoke(Method.java:580) at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:140) 142 common frames omitted Caused by: java.lang.reflect.InaccessibleObjectException: Unable to make field private long java.util.GregorianCalendar.gregorianCutover accessible: module java.base does not "opens java.util" to unnamed module @61862a7f at java.base/java.lang.reflect.AccessibleObject.throwInaccessibleObjectException(AccessibleObject.java:391) at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:367) at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:315) at java.base/java.lang.reflect.Field.checkCanSetAccessible(Field.java:183) at java.base/java.lang.reflect.Field.setAccessible(Field.java:177) at org.springframework.util.ReflectionUtils.makeAccessible(ReflectionUtils.java:804) at org.springframework.data.mapping.context.AbstractMappingContext$PersistentPropertyCreator.doWith(AbstractMappingContext.java:616) at org.springframework.util.ReflectionUtils.doWithFields(ReflectionUtils.java:728) at org.springframework.data.mapping.context.AbstractMappingContext.doAddPersistentEntity(AbstractMappingContext.java:471) at org.springframework.data.mapping.context.AbstractMappingContext.addPersistentEntity(AbstractMappingContext.java:424) at org.springframework.data.cassandra.core.mapping.CassandraMappingContext.addPersistentEntity(CassandraMappingContext.java:326) at org.springframework.data.mapping.context.AbstractMappingContext$PersistentPropertyCreator.lambda$createAndRegisterProperty$3(AbstractMappingContext.java:671) at java.base/java.lang.Iterable.forEach(Iterable.java:75) at org.springframework.data.mapping.context.AbstractMappingContext$PersistentPropertyCreator.createAndRegisterProperty(AbstractMappingContext.java:668) at org.springframework.data.mapping.context.AbstractMappingContext$PersistentPropertyCreator.doWith(AbstractMappingContext.java:622) at org.springframework.util.ReflectionUtils.doWithFields(ReflectionUtils.java:728) at org.springframework.data.mapping.context.AbstractMappingContext.doAddPersistentEntity(AbstractMappingContext.java:471) at org.springframework.data.mapping.context.AbstractMappingContext.addPersistentEntity(AbstractMappingContext.java:424) at org.springframework.data.cassandra.core.mapping.CassandraMappingContext.addPersistentEntity(CassandraMappingContext.java:326) at org.springframework.data.mapping.context.AbstractMappingContext$PersistentPropertyCreator.lambda$createAndRegisterProperty$3(AbstractMappingContext.java:671) at java.base/java.lang.Iterable.forEach(Iterable.java:75) at org.springframework.data.mapping.context.AbstractMappingContext$PersistentPropertyCreator.createAndRegisterProperty(AbstractMappingContext.java:668) at org.springframework.data.mapping.context.AbstractMappingContext$PersistentPropertyCreator.doWith(AbstractMappingContext.java:622) at org.springframework.util.ReflectionUtils.doWithFields(ReflectionUtils.java:728) at org.springframework.data.mapping.context.AbstractMappingContext.doAddPersistentEntity(AbstractMappingContext.java:471) at org.springframework.data.mapping.context.AbstractMappingContext.addPersistentEntity(AbstractMappingContext.java:424) at org.springframework.data.cassandra.core.mapping.CassandraMappingContext.addPersistentEntity(CassandraMappingContext.java:326) at org.springframework.data.mapping.context.AbstractMappingContext.addPersistentEntity(AbstractMappingContext.java:384) at java.base/java.lang.Iterable.forEach(Iterable.java:75) at org.springframework.data.cassandra.CassandraManagedTypes.forEach(CassandraManagedTypes.java:81) at org.springframework.data.mapping.context.AbstractMappingContext.initialize(AbstractMappingContext.java:563) at org.springframework.data.cassandra.core.mapping.CassandraMappingContext.initialize(CassandraMappingContext.java:114) at org.springframework.data.mapping.context.AbstractMappingContext.afterPropertiesSet(AbstractMappingContext.java:555) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1822) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1771) 163 common frames omitted
当前Cassandra配置代码
@Configuration @EnableCassandraRepositories(basePackages = "com.blabla") public class CassandraConfig extends AbstractCassandraConfiguration { @Value("${cassandra.contactpoints}") private String contactPoints; @Value("${cassandra.port}") private int port; @Value("${cassandra.keyspace}") private String keySpace; @Value("${cassandra.basePackages}") private String basePackages; @Value("${cassandra.cluster}") private String clusterName; @Value("${cassandra.username}") private String username; @Value("${cassandra.consistency}") private String consistency; private String password; private static final Logger log = LoggerFactory.getLogger(CassandraConfig.class); @PostConstruct public void init(){ // logic to get my password to connect with cassandra from another repo. } protected AuthProvider getAuthProvider() { return new PlainTextAuthProvider(username, password); } @Override protected String getKeyspaceName() { return keySpace; } @Override protected String getContactPoints() { return contactPoints; } @Override protected int getPort() { return port; } @Override public SchemaAction getSchemaAction() { return SchemaAction.CREATE_IF_NOT_EXISTS; } @Override public String[] getEntityBasePackages() { return new String[] {basePackages}; } @Override protected String getClusterName() { return clusterName; } @Bean public QueryLogger queryLogger(Cluster cluster) { QueryLogger queryLogger = QueryLogger.builder().build(); cluster.register(queryLogger); return queryLogger; } protected AddressTranslator getAddressTranslator() { return new CassandraAddressTranslator(); } protected QueryOptions getQueryOptions() { QueryOptions queryOptions = new QueryOptions(); queryOptions.setConsistencyLevel(ConsistencyLevel.valueOf(consistency)); return queryOptions; } }
使用的依赖
<dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-cassandra</artifactId> <version>4.3.3</version> </dependency>
安全合规解决方案
1. 升级Spring Data Cassandra版本
Spring Data Cassandra 4.3.x对Java模块系统的兼容性存在缺陷,建议升级到与SpringBoot3.2.0匹配的最新稳定版(SpringBoot3.2.0对应Spring Data Cassandra 4.4.x系列)。升级后框架会修复反射访问JDK内部类的问题:
<dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-cassandra</artifactId> <version>4.4.1</version> <!-- 或更高兼容版本 --> </dependency>
2. 替换实体类中的GregorianCalendar类型
将实体类中使用的GregorianCalendar替换为Java8+标准日期时间API类型(如LocalDateTime、ZonedDateTime或Instant),Spring Data Cassandra对这些类型有原生支持,无需反射访问JDK内部私有字段:
// 原字段 // private GregorianCalendar createTime; // 替换后 private LocalDateTime createTime;
确保Cassandra表中对应字段类型为timestamp或datetime,保持类型匹配。
3. 自定义类型转换器(保留GregorianCalendar时)
若因业务需求必须保留GregorianCalendar,可自定义类型转换器,让框架通过转换逻辑处理类型映射,避免直接反射:
import org.springframework.core.convert.converter.Converter; import org.springframework.data.convert.ReadingConverter; import org.springframework.data.convert.WritingConverter; import java.time.Instant; import java.util.GregorianCalendar; import java.util.TimeZone; @WritingConverter public enum GregorianCalendarToInstantConverter implements Converter<GregorianCalendar, Instant> { INSTANCE; @Override public Instant convert(GregorianCalendar source) { return source.toInstant(); } } @ReadingConverter public enum InstantToGregorianCalendarConverter implements Converter<Instant, GregorianCalendar> { INSTANCE; @Override public GregorianCalendar convert(Instant source) { GregorianCalendar calendar = new GregorianCalendar(); calendar.setTimeZone(TimeZone.getTimeZone("UTC")); calendar.setTimeInMillis(source.toEpochMilli()); return calendar; } }
在CassandraConfig中注册转换器:
import org.springframework.data.cassandra.core.convert.CassandraSimpleTypeHolder; import org.springframework.data.cassandra.core.convert.CustomConversions; import java.util.Arrays; @Override public CustomConversions customConversions() { return CustomConversions.of(CassandraSimpleTypeHolder.DEFAULT, Arrays.asList(GregorianCalendarToInstantConverter.INSTANCE, InstantToGregorianCalendarConverter.INSTANCE)); }
4. 缩小实体扫描范围
确认getEntityBasePackages()返回的包路径仅包含自身业务实体类,避免框架扫描到包含GregorianCalendar的第三方类或JDK内部类,减少不必要的反射操作。
内容的提问来源于stack exchange,提问作者Prakhar

