Spring Boot2.7.16+JDK8升21后反射报错NoSuchFieldException: modifiers
问题
在将旧Spring Boot项目升级到2.7.16、JDK从8升级到21时,遇到如下错误:
出错代码
import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.List; import org.apache.log4j.Logger; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.SqlParameter; import org.springframework.jdbc.core.simple.AbstractJdbcCall; import org.springframework.jdbc.core.simple.SimpleJdbcCall; public class CustomJDBCCall extends SimpleJdbcCall { private final CustomCallMetaDataContext callMetaDataContext; private static final String CALL_META_DATA_CONTEXT = "callMetaDataContext"; private static final String MODIFIERS = "modifiers"; private static final Logger LOGGER =Logger.getLogger(CustomJDBCCall.class); public CustomJDBCCall(JdbcTemplate jdbcTemplate) throws Exception { super(jdbcTemplate); try { callMetaDataContext = new CustomCallMetaDataContext(); // Access private field Field callMetaDataContextField = AbstractJdbcCall.class.getDeclaredField(CALL_META_DATA_CONTEXT); callMetaDataContextField.setAccessible(true); Field modifiersField = Field.class.getDeclaredField(MODIFIERS); modifiersField.setAccessible(true); modifiersField.setInt(callMetaDataContextField, callMetaDataContextField.getModifiers() & ~Modifier.FINAL); callMetaDataContextField.set(this, this.callMetaDataContext); } catch (NoSuchFieldException | IllegalAccessException ex) { LOGGER.error("Error while using custom JDBC", ex); throw new RuntimeException("Exception thrown overriding AbstractJdbcCall.callMetaDataContext field", ex); } catch (Exception ex) { LOGGER.error("Error while using custom JDBC", ex); throw ex; } } public List<SqlParameter> getParamerters() throws Exception { return this.callMetaDataContext.getCallParameters(); } }
错误位置
Field modifiersField = Field.class.getDeclaredField(MODIFIERS);
异常信息
Caused by: java.lang.NoSuchFieldException: modifiers at java.base/java.lang.Class.getDeclaredField(Class.java:2782)
疑问:
- 这段代码是不是通过反射访问并修改私有final字段?理解是否正确?
- JDK12及以上版本是否禁用了修改final私有变量的操作?
- 有没有可行的替代方案?
回答
1. 你的理解完全正确
这段代码的核心逻辑就是通过反射hack父类的私有final字段:
- 先获取
AbstractJdbcCall的私有字段callMetaDataContext并开启访问权限 - 再反射修改
Field类的modifiers属性,移除目标字段的final修饰符 - 最后把自定义的
CustomCallMetaDataContext实例赋值给父类的这个私有字段
2. JDK12+的变化
JDK12开始,Field类内部的modifiers字段被直接移除了,所以反射获取该字段会抛出NoSuchFieldException。而且从JDK9开始,Java模块化机制和反射访问限制逐步收紧,即使之前能修改final字段的hack方式,在高版本JDK中也会因为安全规则或模块权限被限制,属于随时可能失效的未定义行为。
3. 可行替代方案
方案一:利用Spring原生扩展点(推荐)
SimpleJdbcCall本身提供了自定义CallMetaDataContext的扩展方法,完全不需要反射hack:
重写createCallMetaDataContext()方法,直接返回你的自定义上下文实例即可,符合Spring的设计规范。
修改后的代码示例:
public class CustomJDBCCall extends SimpleJdbcCall { private final CustomCallMetaDataContext callMetaDataContext; private static final Logger LOGGER = Logger.getLogger(CustomJDBCCall.class); public CustomJDBCCall(JdbcTemplate jdbcTemplate) { super(jdbcTemplate); this.callMetaDataContext = createCallMetaDataContext(); // 可在此添加自定义上下文的初始化逻辑 } @Override protected CallMetaDataContext createCallMetaDataContext() { return new CustomCallMetaDataContext(); } public List<SqlParameter> getParameters() throws Exception { return this.callMetaDataContext.getCallParameters(); } }
方案二:直接继承AbstractJdbcCall
如果SimpleJdbcCall的扩展灵活性不够,可以直接继承AbstractJdbcCall,自行实现相关逻辑,彻底避免依赖父类的私有字段。
方案三:调整CustomCallMetaDataContext的设计
如果自定义上下文只是需要扩展原有逻辑,可以继承Spring原生的CallMetaDataContext,而非完全重写,复用原有功能的同时减少自定义代码量。
内容的提问来源于stack exchange,提问作者Sreejesh
相关产品推荐
相关产品推荐

