在Spring应用中,我们经常会遇到如下的报错字样,本文将介绍传统Spring项目以及SpringBoot项目如何定义Bean以及配置Bean的依赖关系,并结合这些错误出现的场景加以说明其原理。
- java.lang.NullPointerException
- BeanCreationException
- Error creating bean
- Unsatisfied dependency
- UnsatisfiedDependencyException
- NoSuchBeanDefinitionException
- No qualifying bean of type
- NoUniqueBeanDefinitionException
- Injection of autowired dependencies failed
- Could not autowire field
一切都要从Bean开始。
1996年Java刚出现的时候是为了创建拥有丰富的客户端体验的动态Web应用,即Applet。
之后开发者发现Java可以以模块化的方式构建复杂的应用系统,同年12月Sun公司就发布了JavaBean1.00-A规范——一套用于轻松构建复杂应用的编码策略。
但是这个规范太简易,没有事务支持,安全,分布式等服务,无法胜任“实际”的工作。1998年3月Sun就发布了EJB1.0,即EJBBean。
又因为EJB在部署描述符合配套代码实现等方面太冗余复杂,所以出现了Spring。
我们现在所说的Bean,JavaBean都是指Spring容器管理的Bean,除了名字,他们之间没有什么关系。
不是所有Java对象都称之为Bean
,只有被Spring容器管理的Java对象称之为Bean。
Spring应用通过容器(Spring上下文Application Context)来管理Bean
,负责Bean
的创建和装配(常说的依赖注入DI)。
Spring常见的容器有两种类型,一种是Bean工厂,另一种是应用上下文,后者是在前者的基础上构建,比前者多了更多企业级的应用服务。所以后者也是用得最多的一种容器,我们在Spring应用中也经常能看到*application*.xml
的配置文件,在SpringBoot应用中常看到@Bean
注解。
Spring容器负责管理Bean
的创建和装配,开发人员可以定义需要创建的Bean
,配置Bean
之间的依赖关系。
Bean的定义
在传统的Spring应用中(SSM,SSH),注解以及xml是常见的创建Bean
的方式。
最近正在做XXL-JOB相关的工作,就以XXL-JOB-ADMIN的源码为例子,源码资源很容易获取到,是一个非常典型的SSM工程。
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close">
<property name="driverClass" value="${xxl.job.db.driverClass}" />
<property name="jdbcUrl" value="${xxl.job.db.url}" />
<property name="user" value="${xxl.job.db.user}" />
<property name="password" ref="${xxl.job.db.password}"/>
<property name="initialPoolSize" value="3" />
<property name="minPoolSize" value="2" />
<property name="maxPoolSize" value="10" />
<property name="maxIdleTime" value="60" />
<property name="acquireRetryDelay" value="1000" />
<property name="acquireRetryAttempts" value="10" />
<property name="preferredTestQuery" value="SELECT 1" />
</bean>
这是我们平时项目中最常见的一个Bean
,创建了一个数据源,属性通过properties文件配置。
还有的就是基于注解的方式,常用的注解有@Controller
,@Service
,@Component
等。
SpringMVC
为了说明上文中的注解的作用以及容器如何创建和装配Bean
,此处引入SpringMVC的工作机制。
如图所示,在SpringMVC的应用中,当一个请求到达服务器时,首先会被DispatchServlet获取,当然在此之前会被一些安全控制相关的拦截器处理,到了DispatchServlet之后,DispatchServlet会根据HandlerMapping的机制(比如常用的@RequestMapping
),将请求分配给处理该请求的Controller,Controller会调用一个或多个Service完成具体的业务逻辑处理,而Service则会调用一个或多个Repository(即我们常说的Dao层)与数据库交互。然后Controller完成业务逻辑处理之后,将数据返回客户端,或者调用视图解析器将视图返回客户端。
所以我们可以看到,在SpringMVC应用中,Controller,Service,以及Dao都是必须的,并且他们之间是相互依赖的。如果我们把这些类都以上文数据源的方式声明到xml中,
那么xml中的配置将长到让人瞠目结舌,无法阅读。所以Spring引入了注解的方式,Spring容器通过扫描注解,将带注解的类创建为Bean,并装配他们的依赖关系。
传统Spring应用
我们只需要配置具体的扫描路径即可。有直接配置一个大的路径的:
<context:component-scan base-package="com.xxl.job.admin" />
也有分开配置的,目的都一样,都是为了告诉Spring容器扫描的路径:
<context:component-scan base-package="com.xxl.job.admin.controller" />
<context:component-scan base-package="com.xxl.job.admin.service, com.xxl.job.admin.dao, com.xxl.job.admin.core.cyberark" />
分开的配置是开源项目xxl-job的配置,这两段配置分别配在SpringMVC-Context.xml
和applicationcontext-admin.xm
l中,获取作者是因为SpringMVC与Spring并不是一个东西,如此分开配置更规范吧,总而言之就是注解+扫描路径的配置就能定义需要创建的Bean。
也就是说,我们提到的那些注解(@Controller
、@Service
、@Component
等)其实是与xml文件中所写的<bean id="" class="">
的作用是一样的,都是为了定义Bean,也就是配置哪些对象需要Spring容器去创建以及装配它们之间的依赖关系。
SpringBoot应用
在SpringBoot
中,我们可以不用配置扫描的路径,因为通常在Application
启动类中,会有@SpringBootApplication
注解,该注解包含了@ComponentScan
注解,如下所示:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {
@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {...}
下图是@ComponentScan
注解的注释,我们可以看到,它其实与Spring XML配置中的<context:component-scan>
元素的作用是一样的,并且它默认扫描的是当前类所在包下的所有文件夹,这就是为什么我们的SpringBoot应用中,Application.java通常是放在最外面的包中,这样的它的@ComponentScan
注解才能起到扫描其它子包中的Bean
的效果。
在SpringBoot应用中,常以如下方式定义Bean
:
所以创建datasource
的代码在SpingBoot项目中大体如下所示:
@Configuration
public static class SingleDataSourceAutoConfiguration extends DataSourceAutoConfiguration {
@Autowired
private DataSourceProperties dataSourceProperties;
@Bean("dataSource")
@ConditionalOnMissingClass("com.codingapi.tx.datasource.relational
.LCNTransactionDataSource")
public DataSource dataSource() {
DruidDataSource dateSource = createDataSource(dataSourceProperties, DruidDataSource.class);
// druid features
datesource.setInitialSize(druidDataSourceProperties.getInitialSize());
datesource.setMinIdle(druidDataSourceProperties.getMinIdle());
datesource.setMaxActive(druidDataSourceProperties.getMaxActive());
datesource.setMaxWait(druidDataSourceProperties.getMaxWait());
...
return dataSource;
}
}
其内容与XML文件的形式相差无几,只是两种应用的写法不一样罢了。
下图是@Component
注解的描述,我们可以看到,当一个类引用了这个注解,Spring容器将根据前文提及的配置扫描响应路径,并将带有该注解的类当做Bean处理,同样的注解还有@Repository
、@Service
、@Controller
等。
所以,这就是为什么我们的Controller层,Service层,Dao层的类上,经常会看到这些注解,因为他们之间有很多的依赖关系,这些依赖关系需要通过Spring容器来装配(依赖注入),那么首先它们必须是Spring容器管理和维护的Bean,这样,它们的依赖关系才能被Spring容器自动注入。
区别
在Spring应用中,初始化自定义bean
可以使用如下方式:
public class TestFactoryBean implements FactoryBean<String> {
private String value1;
public TestFactoryBean(String value1) {
this.value1 = value1;
}
@Override
public String getObject() throws Exception {
//do something with value1
this.value1 = "";
return "";
}
@Override
public Class<?> getObjectType() {
return String.class;
}
@Override
public boolean isSingleton() {
return false;
}
}
这样,我们自定义了一个TestFactoryBean
,通过配置文件properties
读取到某属性value1
,并通过构造器注入将该值注入到这个bean
中,然后在bean
中完成了一定的业务逻辑(do something with value1),最后在将这个bean
的值注入到的aBean
中。
<bean id="testFactoryBean" class="xxx.TestFactoryBean">
<constructor-arg name="value1" value="${properties.value1}"/>
</bean>
<bean id = "aBean" class="xxxx.ABean">
<property name="value1" ref ="testFactoryBean"></property>
</bean>
这种方法可以用于读取配置文件的属性,进行处理,并注入到其他bean
中的情景。
在SpringBoot应用中,可以先写properties.java
类,使用@ConfigurationProperties(prefix = "swagger2")
,然后就可以在yml文件中提示出properties
类中的属性,供开发者配置,最后在写一个autoconfiguration.java
类使用@bean
注解创建Bean
即可。
@ConfigurationProperties(prefix = "swagger2")
@Validated
public class Swagger2Properties {
private String description;
@NotNull
private String version;
.
.
.
}
这样,在yml中就能生成配置提示:
同理的,如上文提到的dataSourceProperties
一样,Swagger2Properties
也成为一个bean
,可以注入到需要它的任何Bean中。
依赖注入
就像上文中SpringMVC模块所描述的一样,我们的应用都是由很多组件构成的,组件之间相互依赖,共同协作支撑应用运作。Spring容器会负责管理和维护这些依赖关系,我们需要做的就是声明这些依赖关系。传统的做法还是两种,一种是通过XML配置实现,另一种是通过@Autowired
注解实现。
xml配置:
其他Bean注入:
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
如上所示,我们在定义一个数据源事务管理器Bean的时候,使用property
和ref
属性配置了它与dataSource
的依赖关系。
构造器注入:
<bean id="" class="">
<constructor-arg name="" value=""/>
<constructor-arg name="" value=""/>
</bean>
传统的Spring应用中常用以上两种注入方式初始化一些完成特定功能的Bean
,比如先声明一个工厂类的Bean
,其属性通过读取配置文件,或者构造器注入,然后将该工厂类注入其他Bean
的属性中,完成一些初始化的配置工作。
而在SpringBoot应用中,则是使用@Configuration
,@Bean
注解完成同样的工作。
@Autowired:
@RestController
@RequestMapping(value = "/web/customer/", produces = { MediaType.APPLICATION_JSON_VALUE })
public class CustomerController {
@Autowired
private CustomerService customerService;
@GetMapping(value = "/{customerNo}")
public ServiceResponse<CustomerVO> getCustomer(@PathVariable("customerNo") Integer customerNo) {
CustomerVO customer = this.customerService.getCustomer(customerNo);
return ServiceResponse.success(customer);
}
}
如上所示,我们用@RestController
注解(不再描述与@Controller
的差异)声明了这是一个需要自动扫描创建的Bean,并且用@Autowired
注解将customerService
自动注入到了该customerController
中,注意这边的两个Bean
都是首字母小写,因为使用注解声明的Bean
,容器创建的时候,默认是使用该类名首字母小写作为BeanName。
综上,我们介绍了Spring如何定义Bean以及配置Bean之间的依赖关系,接下来介绍一些在此过程中常见的错误。
错误描述
当我们没有配置依赖关系,如CustomerController中,没有使用@Autowired注解,那么customerService将自动注入失败,调用getCustomer()接口时将会报出customerService为null的空指针异常。
java.lang.NullPointerException: null
at com.XXXX.XXX.sys.module.controller.web.CustomerController.getCustomer(CustomerController.java:44)
当我们配置了依赖关系,如CustomerController
中,我们使用了@Autowired
注解配置了该Controller
对customerService
的依赖关系,却没有在CustomerService
的实现类中添加@Service
注解,即我们没有让该Service
被Spring容器自动扫描创建为Bean
,就会出现下面代码中的错误。也就是UnsatisfiedDependencyException(依赖关系不完整异常),在创建customerController
Bean时发生了NoSuchBeanDefinitionException(Bean定义不存在的异常,Service
没定义):
org.springframework.beans.factory.UnsatisfiedDependencyException:
Error creating bean with name 'customerController': Unsatisfied dependency expressed through field 'customerService';
nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.XXXX.XXX.sys.module.service.CustomerService' available:
expected at least 1 bean which qualifies as autowire candidate.
Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}
如下的错误出现在一个接口有多种实现的情况下,如CustomerService
接口有CustomerServiceImplA
、CustomerServiceImplB
两种实现,并且都使用的@Service
注解为Bean,在Spring中使用@Autowired自动注入,有byName、byType、constructor、autodetect4种模式,默认是byType的,即根据类型自动装配,那么还是之前CustomerController中的代码,CustomerService类型的Bean会有两个,没有指明在CustomerController
中需要注入的是哪一个Bean
,就会出现NoUniqueBeanDefinitionException(不唯一的Bean
定义异常),SpringBoot应用的日志还是非常清晰明了的:
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'customerController': Unsatisfied dependency expressed through field 'customerService';
nested exception is org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'com.XXXX.XXX.sys.module.service.CustomerService' available: expected single matching bean but found 2: customerServiceImplA,customerServiceImplB
这时候我们就需要使用@Qualifier
注解指明我们需要注入的Bean
,如前文提到的,如果我们没有显示的定义Bean
的名称,那么Spring容器默认使用类名且首字母小写作为Bean
的名称,所以我们使用类名首字母小写即可注入该Bean
,也就是说@Qualifier
是byName的注入模式。
@Autowired
@Qualifier("customerServiceImplB")
private CustomerService customerService;
当我们了解的什么是Bean,Bean是如何创建以及Bean之间的依赖关系如何装配之后,这些错误出现的原因也就非常清晰明了了。