关于@Autowired注入依赖失败的问题及解决

@Autowired注入依赖失败的问题

1、现象描述

在Spring Boot项目中使用@Autowired注解,程序启动时发现服务启动失败,提示:

Description:
 
Field metrics in com.be.fallback.servlet.FallbackServlet required a bean of type 'com.be.fallback.metrics.FallbackMetrics' that could not be found.
 
The injection point has the following annotations:
    - @org.springframework.beans.factory.annotation.Autowired(required=true)
 
 
Action:
 
Consider defining a bean of type 'com.be.fallback.metrics.FallbackMetrics' in your configuration.

2、问题分析

这里的错误原因很好分析。结合报错信息及代码,报错处的代码为FallbackMetrics注解了@Autowired进行依赖注入,但是没有找到可以被用来注入的实例。即Spring Boot获取FallbackMetrics的实例失败。

3、解决方案

根据分析,需要检查可能导致Spring Boot依赖注入失败的因素。

1.检查扫描路径。

扫描路径是由@ComponentScan来指定的,默认为标注类当前包及当前包的子包。

也就是说,标注了@ComponentScan的启动类放在com.be.fallback包下面,只会扫描com.be.fallback包中的类,以及com.be.fallback.servlet、com.be.fallback.util等子包中的类,对于com.be.service等包中的类是不会扫描的。

  • 注意事项一:很多人没有使用@ComponentScan,但是使用了@SpringBootApplication。@SpringBootApplication是通过内部封装@ComponentScan注解来实现实例扫描的,所以使用@SpringBootApplication也是一样的。
  • 注意事项二:也可以通过为@ComponentScan或@SpringBootApplication注解指定参数来修改扫描路劲,示例:
// 如果使用@ComponentScan注解:
@ComponentScan(basePackages = "com.be.fallback")
 
// 如果使用@SpringBootApplication注解:
@SpringBootApplication(scanBasePackages = "com.be.fallback")

2.检查实例注册。

检查想要使用@Autowired注解自动注入依赖的类,是否标注了用来注册给Spring Boot的注解。这些注解包括@Component,@Service,@Repository,@Controller等。

3.其他问题。

如果上述步骤检查完成,服务启动又没有产生其他异常,这时候基本上已经排查代码的问题。这时候需要检查依赖、开发环境等是否有问题。检查依赖需要了解自己需要哪些依赖,看是否配置齐全;检查开发环境,可以通过将代码拷贝到其他机器上执行来判断。

@Autowired依赖注入为啥不推荐了

这几天更新升级了一下java编码神器IDEA,升级完进行日常开发,可能是以前用的IDEA版本比较老旧,升级之后发现之前的日常写法有了个warning提醒。来看图:

关于@Autowired注入依赖失败的问题及解决_第1张图片

如上图,这就奇怪了,我们经常写的业务层就是service接口层和对应的实现类层进行属性注入的时候都是采用注解进行注入的。这也是springIOC给提供的比较方便的地方。我使用IDEA提供的自动修复提示修复了之后变成采用构造函数的形式进行注入了。

关于@Autowired注入依赖失败的问题及解决_第2张图片

但是多年面向Spring开发的经验告诉我,使用@Autowired注解进行依赖注入,肯定是没有问题的。但是我的代码洁癖不允许我这么不明不白的留一个警告在这里。所以,带着我的洁癖,和我的好奇心,我开始研究起了这个警告。

警告内容

warning3

我们简单翻译一下自动提示的是啥意思:

不建议直接在字段上进行依赖注入。

Spring 开发团队建议:在Java Bean中永远使用构造方法进行依赖注入。对于必须的依赖,永远使用断言来确认。

我们说明上面的问题之前先回顾几个spring相关的问题:

依赖注入的方式

Spring 有三种依赖注入的方式

1.基于属性的注入

这种注入方式就是在bean的变量上使用注解进行依赖注入。本质上是通过反射的方式直接注入到field。这是我平常开发中看的最多也是最熟悉的一种方式。

@Autowired
PushTaskService pushTaskService;

2.基于setter方法的注入

通过对应变量的setXXX()方法以及在方法上面使用注解,来完成依赖注入。比如:

private static TaskGroupTemplateRepository taskGroupTemplateRepository;
private static TaskGroupService taskGroupService;

@Autowired
public void setTaskGroupTemplateRepository(TaskGroupTemplateRepository taskGroupTemplateRepository,TaskGroupService taskGroupService){
    ExcelListener2.taskGroupTemplateRepository = taskGroupTemplateRepository;
    ExcelListener2.taskGroupService = taskGroupService;
}

说明:在 Spring 4.5 及更高的版本中,setXXX 上面的 @Autowired 注解是可以不写的。

3.基于构造方法的注入

将各个必需的依赖全部放在带有注解构造方法的参数中,并在构造方法中完成对应变量的初始化,这种方式,就是基于构造方法的注入。比如:

@Autowired
public ExcelListener(@Qualifier("taskGroupService") TaskGroupService taskGroupService) {
    this.taskGroupService = taskGroupService;
}

@Autowired是干啥的

我们一般开发需要注入属性的时候都会使用的这个注解@Autowired,跟这个注解类似的还有2个,@Resource, @Inject。Spring 支持使用@Autowired, @Resource, @Inject 三个注解进行依赖注入。我们先看一下有啥区别:

@Autowired为Spring框架提供的注解,可以理解是Spring的亲儿子。这里先给出一个示例代码

public interface IndexService {

    void sayHello();
}

@Service
public class IndexServiceImpl implements IndexService {

    @Override
    public void sayHello() {
        System.out.println("hello, this is IndexServiceImpl");
    }

}

@Service
public class IndexServiceImpl2 implements IndexService {

    @Override
    public void sayHello() {
        System.out.println("hello, this is IndexServiceImpl2");
    }

}

测试方法

@SpringBootTest
public class Stest {

    @Autowired
    // @Qualifier("indexServiceImpl2")
    IndexService indexService;

    @Test
    void gooo() {
        Assertions.assertNotNull(indexService);
        indexService.sayHello();
    }
}

按照type在上下文中查找匹配,查找type为IndexService的bean

  • 如果有多个bean,则按照name进行匹配

如果有@Qualifier注解,则按照@Qualifier指定的name进行匹配,查找name为indexServiceImpl2的bean

如果没有,则按照变量名进行匹配。查找name为indexService的bean

  • 匹配不到,则报错。(@Autowired(required=false),如果设置required为false(默认为true),则注入失败时不会抛出异常)

@Inject是干啥的

在Spring 的环境下,@Inject和@Autowired 是相同的,因为它们的依赖注入都是使用AutowiredAnnotationBeanPostProcessor这个后置处理器来处理的。

关于@Autowired注入依赖失败的问题及解决_第3张图片

这两个的区别,首先@Inject是Java EE包里的,在SE环境需要单独引入。另一个区别在于@Autowired可以设置required=false而@Inject并没有这个属性。也有的说@Inject是spring的干儿子。

@Resource是干啥的

@Resource是JSR-250定义的注解。Spring 在 CommonAnnotationBeanPostProcessor实现了对JSR-250的注解的处理,其中就包括@Resource。

这个@Resource有2个属性name和type。在spring中name属性定义为bean的名字,type这是bean的类型。

如果属性上加@Resource注解那么他的注入流程是:

  • 如果同时指定了name和type,则从Spring上下文中找到唯一匹配的bean进行装配,找不到则抛出异常。
  • 如果指定了name,则从上下文中查找名称匹配的bean进行装配,找不到则抛出异常。
  • 如果指定了type,则从上下文中找到类型匹配的唯一bean进行装配,找不到或是找到多个,都会抛出异常。
  • 如果既没有指定name,又没有指定type,则默认按照byName方式进行装配;如果没有匹配,按照byType进行装配。

上面的内容都了解了之后我们接下来看为啥IDEA会有个warning提醒。IDEA 提示 Field injection is not recommended。warning提醒的注入方式就是第一种使用属性注解的方式进行注入。

属性注入优点

代码看起来很简单,通俗易懂。你的类可以专注于业务而不被依赖注入所污染。你只需要把@Autowired扔到变量之上就好了方便开发人员进行代码的编写。

属性注入可能出现的问题

  • 问题1

基于 field 的注入,虽然不是绝对禁止使用,但是它可能会带来一些隐含的问题。来我们举个例子:

@Autowired
private Person person;

private String company;

public UserServiceImpl(){
    this.company = person.getCompany();
}

初看起来好像没有什么问题,Person 类会被作为一个依赖被注入到当前类中,同时这个类的 company 属性将在初始化时通过person.getCompany() 方法来获得值。我们尝试运行一下就会发现报错了。其实类似这个问题有人在stackoverflow上提问过,点我跳转。

Instantiation of bean failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [...]: Constructor threw exception; nested exception is java.lang.NullPointerException

在执行this.company = person.getCompany();这段代码的时候报了空指针。

出现这个问题的原因是,Java 在初始化一个类时,是按照静态变量或静态语句块 –> 实例变量或初始化语句块 –> 构造方法 -> @Autowired 的顺序。所以在执行这个类的构造方法时,person 对象尚未被注入,它的值还是 null。

  • 问题2

使用这种基于 field 注入的方式,添加依赖是很简单的,就算你的类中有十几个依赖你可能都觉得没有什么问题,如果你一个类注入非常多的其它的对象,拥有太多的依赖通常意味着你的类要承担更多的责任,明显违背了单一职责原则。顺便我看了一下我们现在的业务代码这个问题在我们的项目代码中真的很常见。

  • 问题3

这种注入形式就会造成你的类不能绕过反射(例如单元测试的时候)进行实例化,必须通过依赖容器才能实例化。也就是类和依赖容器强耦合,不能在容器外使用。

spring建议

Since you can mix constructor-based and setter-based DI, it is a good rule of thumb to use constructors for mandatory dependencies and setter methods or configuration methods for optional dependencies.

其实大概的意思就是2句话

  • 强制依赖就用构造器方式
  • 可选、可变的依赖就用setter注入

spring对采用构造方法注入的说明

Spring 团队提倡使用基于构造方法的注入,因为这样一方面可以将依赖注入到一个不可变的变量中 (注:final 修饰的变量),另一方面也可以保证这些变量的值不会是 null。此外,经过构造方法完成依赖注入的组件 (注:比如各个 service),在被调用时可以保证它们都完全准备好了。与此同时,从代码质量的角度来看,一个巨大的构造方法通常代表着出现了代码结构问题,这个类可能承担了过多的责任。

spring对采用setter方法注入的说明

基于 setter 的注入,则只应该被用于注入非必需的依赖,同时在类中应该对这个依赖提供一个合理的默认值。如果使用 setter 注入必需的依赖,那么将会有过多的 null 检查充斥在代码中。使用 setter 注入的一个优点是,这个依赖可以很方便的被改变或者重新注入。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

你可能感兴趣的:(关于@Autowired注入依赖失败的问题及解决)