Java Bean 详解

在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.xml中,获取作者是因为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(依赖关系不完整异常),在创建customerControllerBean时发生了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之间的依赖关系如何装配之后,这些错误出现的原因也就非常清晰明了了。
 ———————————————— 
版权声明:本文为CSDN博主「AryaLee」的原创文章,遵循CC 4.0 by-sa版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/lbqssss/article/details/79306142

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值