继承的@Autowired字段在构造函数执行时未初始化?原因与解决方法
父类@Autowired字段在子类构造函数中为空的原因及解决办法
问题场景
子类UserQuestionCommentRestControllerTest继承自IntegrationTestContext,在构造函数中调用父类的testUtil获取JWT token时触发空指针异常(NPE)。父类的testUtil字段用@Autowired修饰,但子类构造执行时该字段并未完成初始化。
异常原因
Spring Bean的初始化顺序是先执行构造函数,再完成依赖注入:
- 创建子类对象时,会先调用父类的构造函数(子类构造默认隐含
super()调用); - 整个对象构造完成后,Spring才会通过反射为
@Autowired标注的字段赋值; - 因此子类构造函数执行时,父类的
testUtil还是null,调用其方法自然会抛出NPE。你误以为的“反射初始化父类字段”是在构造函数执行完毕后才会发生的操作。
解决方法
方法1:使用@PostConstruct初始化token
将token的初始化逻辑放到被@PostConstruct标注的方法中,该方法会在Spring完成所有依赖注入、Bean初始化完成后自动执行。注意需要去掉token的final修饰符:
public class UserQuestionCommentRestControllerTest extends IntegrationTestContext { private static final String testUsername = "mickey_m"; private static final String testPassword = "mickey"; private String token; // 移除final修饰符 private static final String BASE_URI = "/api/v1/user/question-comments/"; private static final String BASE_SCRIPT_PATH = "/sql/UserQuestionCommentRestControllerTest/"; private ExpectationTester expectationTester; @PostConstruct public void initToken() throws Exception { token = testUtil.getToken(testUsername, testPassword); } }
方法2:改用构造函数注入(推荐)
让父类通过构造函数接收依赖,子类继承后也通过构造函数获取所需依赖。这种方式下,依赖会在对象构造阶段就被注入,从根源避免空指针问题:
父类调整:
@SpringBootTest @AutoConfigureMockMvc @Testcontainers public class IntegrationTestContext { protected final MockMvc mockMvc; protected final ObjectMapper objectMapper; protected final TestUtil testUtil; @PersistenceContext protected EntityManager entityManager; // 构造函数注入依赖 public IntegrationTestContext(MockMvc mockMvc, ObjectMapper objectMapper, TestUtil testUtil) { this.mockMvc = mockMvc; this.objectMapper = objectMapper; this.testUtil = testUtil; } }
子类调整:
public class UserQuestionCommentRestControllerTest extends IntegrationTestContext { private static final String testUsername = "mickey_m"; private static final String testPassword = "mickey"; private final String token; private static final String BASE_URI = "/api/v1/user/question-comments/"; private static final String BASE_SCRIPT_PATH = "/sql/UserQuestionCommentRestControllerTest/"; private ExpectationTester expectationTester; // 调用父类构造注入依赖,直接使用testUtil初始化token public UserQuestionCommentRestControllerTest(MockMvc mockMvc, ObjectMapper objectMapper, TestUtil testUtil) throws Exception { super(mockMvc, objectMapper, testUtil); token = testUtil.getToken(testUsername, testPassword); } }
Spring会自动将对应的Bean注入到构造函数中,此时testUtil在构造函数执行时已经是实例化完成的对象。
方法3:使用JUnit的@BeforeEach(适配测试场景)
由于这是测试类,可借助JUnit的@BeforeEach注解标注初始化方法,该方法会在每个测试方法执行前运行,此时Spring已完成所有依赖注入:
public class UserQuestionCommentRestControllerTest extends IntegrationTestContext { private static final String testUsername = "mickey_m"; private static final String testPassword = "mickey"; private String token; private static final String BASE_URI = "/api/v1/user/question-comments/"; private static final String BASE_SCRIPT_PATH = "/sql/UserQuestionCommentRestControllerTest/"; private ExpectationTester expectationTester; @BeforeEach public void setup() throws Exception { token = testUtil.getToken(testUsername, testPassword); } }
这种方式适合测试场景,能确保每次测试前token都是有效的最新值。
内容的提问来源于stack exchange,提问作者Sergey Zolotarev
相关产品推荐
相关产品推荐

