为什么我的Spring @Autowired字段为空?

769

注意:这是针对常见问题的标准答案。

我有一个Spring的@Service类(MileageFeeCalculator),其中有一个@Autowired字段(rateService),但当我尝试使用它时,该字段为null。日志显示,MileageFeeCalculator bean和MileageRateService bean都被创建了,但是每当我尝试调用我的服务bean上的mileageCharge方法时,就会出现NullPointerException。为什么Spring没有自动装配该字段?

控制器类:

@Controller
public class MileageFeeController {    
    @RequestMapping("/mileage/{miles}")
    @ResponseBody
    public float mileageFee(@PathVariable int miles) {
        MileageFeeCalculator calc = new MileageFeeCalculator();
        return calc.mileageCharge(miles);
    }
}

服务类:

@Service
public class MileageFeeCalculator {

    @Autowired
    private MileageRateService rateService; // <--- should be autowired, is null

    public float mileageCharge(final int miles) {
        return (miles * rateService.ratePerMile()); // <--- throws NPE
    }
}

应该自动装配到 MileageFeeCalculator 中的服务 bean,但它没有被自动装配:

@Service
public class MileageRateService {
    public float ratePerMile() {
        return 0.565f;
    }
}

当我尝试执行GET /mileage/3时,我会收到以下异常:
java.lang.NullPointerException: null
    at com.chrylis.example.spring_autowired_npe.MileageFeeCalculator.mileageCharge(MileageFeeCalculator.java:13)
    at com.chrylis.example.spring_autowired_npe.MileageFeeController.mileageFee(MileageFeeController.java:14)
    ...

6
另一种情况是当一个名为F的Bean在另一个名为S的Bean的构造函数中被调用时。在这种情况下,将所需的F Bean作为参数传递给其他Beans S构造函数,并在S的构造函数上注释@Autowire。记得要在第一个BeanF的类上注释@Component - aliopi
2
我使用Gradle编码了几个非常类似于这个的示例,链接在这里:https://github.com/swimorsink/spring-aspectj-examples。希望对某些人有用。 - Ross117
21个回答

782

@Autowired注释的字段为null,因为Spring不知道你用new创建了MileageFeeCalculator的副本并且没有将其自动装配。

Spring控制反转(IoC)容器有三个主要逻辑组件:一个组件(bean)注册表(称为ApplicationContext),其中存放着应用程序可用的所有组件,一个配置器系统,它通过将依赖项与上下文中的bean匹配来向对象注入其依赖项,以及一个依赖项解析器,可以查看多个不同bean的配置,并确定如何按必要顺序实例化和配置它们。

IoC容器并非魔法,除非你以某种方式告知它,否则它无法了解Java对象。当你调用new时,JVM会实例化新对象的副本并直接交给你,而它永远不会经过配置过程。有三种方法可以使你的bean被配置。

我在这个GitHub项目上发布了所有代码,使用Spring Boot启动。你可以查看每种方法的完整运行项目,以查看使其正常工作所需的所有内容。 NullPointerException的标签:nonworking

注入你的bean

最可取的选择是让Spring自动装配所有的bean,这需要最少的代码并且是最可维护的。为了使自动装配像你想要的那样工作,还要像这样自动装配MileageFeeCalculator

@Controller
public class MileageFeeController {

    @Autowired
    private MileageFeeCalculator calc;

    @RequestMapping("/mileage/{miles}")
    @ResponseBody
    public float mileageFee(@PathVariable int miles) {
        return calc.mileageCharge(miles);
    }
}

如果您需要为不同的请求创建服务对象的新实例,仍然可以使用注入,方法是利用Spring bean作用域

通过注入@MileageFeeCalculator服务对象工作的标签: working-inject-bean

使用@Configurable

如果您确实需要使用new创建的对象进行自动装配,您可以使用Spring @Configurable注释和AspectJ编译时织入来注入您的对象。这种方法会在对象的构造函数中插入代码,以提示Spring正在创建它,以便Spring可以配置新实例。这需要在您的构建中进行一些配置(例如使用ajc编译)并打开Spring的运行时配置处理程序(使用JavaConfig语法的@EnableSpringConfigured)。Roo Active Record系统使用此方法允许将实体的new实例注入所需的持久性信息。

@Service
@Configurable
public class MileageFeeCalculator {

    @Autowired
    private MileageRateService rateService;

    public float mileageCharge(final int miles) {
        return (miles * rateService.ratePerMile());
    }
}

使用服务对象上的@Configurable标签的方法: working-configurable

手动查找Bean:不建议使用

这种方法仅适用于特殊情况下与遗留代码进行接口交互。通常最好创建一个Spring可以自动连接并且遗留代码可以调用的单例适配器类,而不是直接向Spring应用程序上下文请求Bean。

要做到这一点,您需要一个类,Spring可以将ApplicationContext对象的引用传递给它:

@Component
public class ApplicationContextHolder implements ApplicationContextAware {
    private static ApplicationContext context;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        context = applicationContext;   
    }

    public static ApplicationContext getContext() {
        return context;
    }
}

那么您的旧代码可以调用 getContext() 并检索所需的bean:

@Controller
public class MileageFeeController {    
    @RequestMapping("/mileage/{miles}")
    @ResponseBody
    public float mileageFee(@PathVariable int miles) {
        MileageFeeCalculator calc = ApplicationContextHolder.getContext().getBean(MileageFeeCalculator.class);
        return calc.mileageCharge(miles);
    }
}

在Spring上下文中手动查找服务对象的标签:working-manual-lookup


2
另一个需要注意的是,在@Configuration bean中为bean创建对象,其中用于创建特定bean类实例的方法带有@Bean注释。 - Donal Fellows
1
@DonalFellows,我不太确定你在说什么(“making” 是含糊的)。您是在谈论使用 Spring Proxy AOP 时多次调用 @Bean 方法的问题吗? - chrylis -cautiouslyoptimistic-
2
嗨,我遇到了类似的问题,但是当我使用你的第一个建议时,我的应用程序在调用“mileageFee”方法时认为“calc”为空。就好像它从未初始化@Autowired MileageFeeCalculator calc一样。有什么想法吗? - Theo
1
请纠正我,如果我错了,那么在 MilegageFeeCalculator 上指定 @Service@Configurable 注释可能是不正确的,根据 Spring AOP 文档:* …确保您不要将 @Configurable 用于已向容器注册为常规 Spring Bean 的 Bean 类:否则,您将获得双重初始化,一次通过容器,一次通过方面。* 因此,实质上,您只应该选择其中一个。 - Priidu Neemre
@PriiduNeemre 在这种通常情况下出现的问题是程序员没有将Java对象注册为Spring bean。 是的,这可能效率较低,但它不应该产生错误行为,只是次优的。 无论如何,我打算更新这个建议使用构造函数注入。 - chrylis -cautiouslyoptimistic-
显示剩余8条评论

72

如果你没有编写Web应用程序,请确保使用@Autowiring进行注入的类是Spring bean。通常情况下,Spring容器不会知道我们将其视为Spring Bean的类。我们需要告诉Spring容器我们的Spring类。

可以通过在appln-contxt中进行配置或者更好的方法是将该类注释为@Component,并且请勿使用new运算符创建已注释的类。请确保按照以下方式从Appln-context获取。

@Component
public class MyDemo {


    @Autowired
    private MyService  myService; 

    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub
            System.out.println("test");
            ApplicationContext ctx=new ClassPathXmlApplicationContext("spring.xml");
            System.out.println("ctx>>"+ctx);

            Customer c1=null;
            MyDemo myDemo=ctx.getBean(MyDemo.class);
            System.out.println(myDemo);
            myDemo.callService(ctx);


    }

    public void callService(ApplicationContext ctx) {
        // TODO Auto-generated method stub
        System.out.println("---callService---");
        System.out.println(myService);
        myService.callMydao();

    }

}

你好,我已经查看了你的解决方案,它是正确的。在这里,我想知道为什么我们不能使用new运算符创建带注释类的实例,可以告诉我背后的原因吗? - Ashish
4
如果您使用 new 关键字创建对象,那么您将需要自己管理 Bean 的生命周期,这与 IOC 的概念相矛盾。我们需要请求容器来完成这个过程,容器会以更好的方式来处理。 - Shirish Coolkarni

54

实际上,您应该使用JVM管理的对象或Spring管理的对象来调用方法。 在您的控制器类中,从上面的代码中可以看出,您正在创建一个新对象来调用已经自动装配了对象的服务类。

MileageFeeCalculator calc = new MileageFeeCalculator();

那样是行不通的。

解决方案是将MileageFeeCalculator作为自动装配对象放在控制器(Controller)本身中。

将你的Controller类更改如下。

@Controller
public class MileageFeeController {

    @Autowired
    MileageFeeCalculator calc;  

    @RequestMapping("/mileage/{miles}")
    @ResponseBody
    public float mileageFee(@PathVariable int miles) {
        return calc.mileageCharge(miles);
    }
}

4
这是答案。因为你自己实例化了一个新的 MileageFeeCalculator,所以 Spring 没有参与这个实例化过程,也就不知道这个对象的存在。因此,它无法对其进行任何操作,例如注入依赖项。 - cyotee doge

35

在我刚接触IoC(控制反转)世界时,我遇到了同样的问题。我的一个bean的@Autowired字段在运行时为null。

根本原因是,我没有使用Spring IoC容器自动创建的bean(其@Autowired字段确实被正确注入),而是使用new关键字创建了自己的该类型的实例并使用它。当然这个实例的@Autowired字段为null,因为Spring没有机会将其注入。


1
这个帮了我很久! - Abdulbasith
1
只要“new”对象本身不依赖于IoC,这就可以工作。 - PowerAktar

26

你的问题是新的(Java风格中的对象创建)

MileageFeeCalculator calc = new MileageFeeCalculator();

使用注解 @Service@Component@Configuration,在 Spring 应用程序上下文启动服务器时创建 bean。但是,当我们使用 new 操作符创建对象时,该对象不会在已创建的应用程序上下文中注册。例如,我使用了 Employee.java 类。
看一下这个:
public class ConfiguredTenantScopedBeanProcessor implements BeanFactoryPostProcessor {
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        String name = "tenant";
        System.out.println("Bean factory post processor is initialized"); 
        beanFactory.registerScope("employee", new Employee());

        Assert.state(beanFactory instanceof BeanDefinitionRegistry,
                "BeanFactory was not a BeanDefinitionRegistry, so CustomScope cannot be used.");
        BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory;

        for (String beanName : beanFactory.getBeanDefinitionNames()) {
            BeanDefinition definition = beanFactory.getBeanDefinition(beanName);
            if (name.equals(definition.getScope())) {
                BeanDefinitionHolder proxyHolder = ScopedProxyUtils.createScopedProxy(new BeanDefinitionHolder(definition, beanName), registry, true);
                registry.registerBeanDefinition(beanName, proxyHolder.getBeanDefinition());
            }
        }
    }
}

15

这似乎是个罕见的情况,但这就是我的经历:

我们使用了 @Inject 而不是 Spring 支持的 JavaEE 标准 @Autowired。在大多数地方它都能正常工作并正确注入 bean,只有一个地方出现问题。看起来 bean 注入的方式是相同的。

@Inject
Calculator myCalculator

最终我们发现错误是因为我们(实际上是 Eclipse 自动完成功能)导入了 com.opensymphony.xwork2.Inject 而不是 javax.inject.Inject

因此,总结一下,确保您的注释(@Autowired@Inject@Service 等)具有正确的包!


15

我对Spring还不熟悉,但是我发现这个解决方案有效。请告诉我这种方法是否已经过时。

我在这个bean中使用了Spring注入applicationContext

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;

@Component
public class SpringUtils {

    public static ApplicationContext ctx;

    /**
     * Make Spring inject the application context
     * and save it on a static variable,
     * so that it can be accessed from any point in the application. 
     */
    @Autowired
    private void setApplicationContext(ApplicationContext applicationContext) {
        ctx = applicationContext;       
    }
}
你可以将这段代码放在主应用程序类中,如果你愿意的话。
其他类可以这样使用它:
MyBean myBean = (MyBean)SpringUtils.ctx.getBean(MyBean.class);

通过这种方式,任何应用程序中使用new实例化的对象都可以以静态方式获得任何bean。


2
这种模式是为了使Spring bean可以被遗留代码访问而必要的,但在新代码中应该避免使用。 - chrylis -cautiouslyoptimistic-
在我的情况下,我需要这样做是因为有一些第三方类。Spring(IOC)无法对它们进行控制。这些类从来没有从我的Spring Boot应用程序中调用过。我采用了这种方法,并且它对我很有效。 - Joginder Malik

14

这里未提及的内容在“执行顺序”段落中这篇文章中有描述。

在“学习”后,我发现必须使用@Component或其派生类@Service或@Repository(可能还有更多)对类进行注释,以便在其中自动装配其他组件,但是我想知道这些其他组件在父组件的构造函数中仍为空。

使用@PostConstruct解决了这个问题:

@SpringBootApplication
public class Application {
    @Autowired MyComponent comp;
}

并且:

@Component
public class MyComponent {
    @Autowired ComponentDAO dao;

    public MyComponent() {
        // dao is null here
    }

    @PostConstruct
    public void init() {
        // dao is initialized here
    }
}

11

以下其中一个方法会起作用:

  1. 您使用@Autowired的类不是Bean(我确定您在某处使用了new())。

  2. 在SpringConfig类中,您没有指定Spring应该查找@Component的包(我在谈论@ComponentScan(basePackages"这里"))

如果上述两种方法都不起作用...请开始使用System.out.println(),找出问题所在。


1
我的代码中使用了new(),这引起了问题。我需要使用那个new()。但是在新的类中我也需要使用@Autowire。如何实现呢? - Soumik Das
2
你不需要创建新的对象!你只需声明类变量并在其上方使用@Autowire。你还必须确保已在要自动装配的类(例如,在ABC类上方)上方包含了@Component。这样就可以工作了 :) - Ashutosh Tiwari

9

如果这是在测试类中出现的问题,请确保您没有忘记给类加注释。

例如,在Spring Boot中:

@RunWith(SpringRunner.class)
@SpringBootTest
public class MyTests {
    ....

一段时间过去了...

Spring Boot 不断发展进化。如果您使用正确版本的JUnit,就不再需要使用@RunWith

为了使@SpringBootTest能够独立工作,您需要使用JUnit5而非JUnit4中的@Test

//import org.junit.Test; // JUnit4
import org.junit.jupiter.api.Test; // JUnit5

@SpringBootTest
public class MyTests {
    ....

如果您配置错误,测试可能会编译通过,但是@Autowired@Value字段(例如)将会是null。由于Spring Boot运行时使用魔法操作,因此您可能很难直接调试此故障。

参见:https://dev59.com/gm855IYBdhLWcg3wz33o - Brent Bradburn
注意:当与static字段一起使用时,@Value将为空。 - Brent Bradburn
Spring提供了许多失败的方式(没有编译器的帮助)。当出现问题时,最好的方法是回到原点——仅使用您知道可以一起工作的注释组合。 - Brent Bradburn

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接