Spring @Autowired注入为null的解决方法

作者 | 2020年4月7日

1. 问题描述

我有一个加了@Service注解的类MileageFeeCalculator,我在其中一个字段rateService上使用了@Autowired注解,但是,该字段的值却一直是null,日志显示MileageFeeCalculatorMileageRateService都已经在Spring中创建了。但是在调用mileageCharge方法时却抛出NullPointerException异常,为什么Spring不能自动注入该字段的值?

Controller类

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

Service类

@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
    }
}

按理来说下面这个Service会自动注入到rateService字段中,但是却失败了:

@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)
    ...

2. 高赞答案

@Autowired注解的字段为null,是因为Spring不知道MileageFeeCalculator是你手动使用new方法创建出来的,所致导致@Autowired注入失败。

Spring IOC容器逻辑上可分为三个组件:Bean的注册中心(称之为ApplicationContext)中存放了应用可以使用的所有Bean,一个配置管理组件(原文是configurer system)将ApplicationContext中的Bean按照对应的依赖匹配关系注入到相应的对象中,一个依赖解决组件用于根据不同的依赖顺序实例化不同的Bean。

Ioc容器不是魔法,除非你主动告诉它那些对象应该注册到ApplicationContext中,否则它没法知道那些Java对象应该被放入ApplicationContext中,当你调用new时,JVM直接实例化一个新的对象给你使用,Spring永远不知道该对象的存在。这里有三种方法让你把自定义的Bean放入Spring中。

注入你的Bean

最可取的方法是使用Spring的@Autowire注解让Spring进行自动注入,该方法需要的代码最少,也易于维护。
看起来使用@Autowire注解是你所希望的方式,因此你可以像这样自动注入MileageFeeCalculator

 @Controller
public class MileageFeeController {

    @Autowired
    private MileageFeeCalculator calc;

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

如果你需要在每一个请求中都创建一个新的实例,你可以通过Bean的作用域来实现.

利用@Configurable

如果你真的希望Spring可以自动发现你自己new出来的对象,那么你可以使用Spring提供的注解@Configurable,该注解通过AspectJ编译时编织,可以在对象的构造方法中插入额外的代码来告诉Spring某个对象已经被创建了,该方法不仅在编译时需要进行一些配置(例如在编译时使用ajs)而且还需要使用注解@EnableSpringConfigured开启Spring的运行时配置功能。该方法被Roo Active Record系统用于记录你new出来的实例对象,并往其中注入一些必要的持久化信息。

@Service
@Configurable
public class MileageFeeCalculator {

    @Autowired
    private MileageRateService rateService;

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

非常不推荐手动地注入Bean

该方法只适合于遗留的系统进行对接时使用。在大多情况下,最好的方法是创建一个单例的适配器用于处理Spring自动注入与遗留代码间的调用,然后它可以直接向Spring的ApplicationContext请求一个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);
    }
}

发表评论

电子邮件地址不会被公开。 必填项已用*标注