spring拾遗(一)——IOC的应用

陌路散爱 提交于 2020-02-28 03:45:05

前言

spring这个玩意,其实要单纯谈使用的话,其实内容也不少,工作中有些东西用的挺多,但是很不系统。去年也尝试过看spring的源码,但是通常陷入到"我是谁,我在哪儿的,我干嘛要看这玩意"的思考(因为太晕了),想想这玩意还是得学啊,不能因为不懂,不能因为它让我怀疑人生就放弃,还是得从头撸。后来思考发现自己太过依赖百度,忽略了官网这玩意,这次就从官网出发。总结一些更多的操作,让spring的学习更加系统。这篇博客并不是一个官网的翻译,整理的内容会非常零散,毕竟只是补充我们平常使用spring框架忽略的东西。

什么是IOC,IOC和DI有啥区别

这又是一个灵魂拷问,在面试中遇到过很多次,不同的人回答方式不同。个人觉得这个东西一句话就能解决,DI其实就是IOC的一种实现,IOC只是一种设计理念。除了DI的方式来实现IOC,DL的方式也是IOC的一种实现。(之前有一种说法,DI是IOC的另一种表达方式,感觉不太全面)

控制反转(Inversion of Control,缩写为IoC),是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。其中最常见的方式叫做依赖注入(Dependency Injection,简称DI),还有一种方式叫“依赖查找”(Dependency Lookup

为什么要有IOC

自己new它不香吗?不好意思,自己new,真的香不起来。我们在刚接触spring的时候,可能很多人会告诉你spring就是避免了你new一个引用对象的过程,比如类A中维护了一个B的引用,我们在A中使用B的时候就不需要自己去new一个B对象了,IOC直接给我们注入了。但是我们自己new也能达到目标,为啥new就香不起来呢?

这样想想,A依赖B,正常使用没有问题,但是如果某一天,需求说B中需要加入事务控制,这时候我们需要对B做一个代理的处理,生成一个新的代理对象,这就尴尬了,所有引用了B的对象都得重新new,这就不那么香了。

所以我们需要有个东西能帮我们管理各个对象之间的依赖,并帮我们完成这种依赖注入的工作,这就是IOC为啥重要的原因。因此从一定层度上来讲IOC就是完成我们注入的工作(这也就是为啥有些人理解为IOC就是DI)

给出spring文档的地址吧:https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html

顺带提一句官网中其实提供了三种实例,基于xml的(schema-based),基于注解的(annotation-based)和基于java config的。

Dependency Injection(DI)

关于依赖注入,官网中有这样一句话

DI exists in two major variants: Constructor-based dependency injection and Setter-based dependency injection.

DI 主要有两种方式:一种是构造器注入,另一种就是getter和setter注入。

构造器注入

有一个这个类,这个类的构造方法有两个参数,这两个参数

package x.y;

public class ThingOne {

    public ThingOne(ThingTwo thingTwo, ThingThree thingThree) {
        // ...
    }
}

将bean的管理告知spring容器

<beans>
    <bean id="beanOne" class="x.y.ThingOne">
        <constructor-arg ref="beanTwo"/>
        <constructor-arg ref="beanThree"/>
    </bean>

    <bean id="beanTwo" class="x.y.ThingTwo"/>

    <bean id="beanThree" class="x.y.ThingThree"/>
</beans>

通过标签完成构造参数的注入

使用xml配置的时候,可以直接注入属性的value值,但是这种用法并不常见,官网的1.4.2节中有相关介绍,其中还提到了c-namespace和p-namespace来完成注册,这个并不常用,这里只是顺带提一下而已。

基于setter方法的注入

官网中的例子是这样的

public class ExampleBean {

    private AnotherBean beanOne;

    private YetAnotherBean beanTwo;

    private int i;

    public void setBeanOne(AnotherBean beanOne) {
        this.beanOne = beanOne;
    }

    public void setBeanTwo(YetAnotherBean beanTwo) {
        this.beanTwo = beanTwo;
    }

    public void setIntegerProperty(int i) {
        this.i = i;
    }
}

基于这个实例的配置是这样的

<bean id="exampleBean" class="examples.ExampleBean">
    <!-- setter injection using the nested ref element -->
    <property name="beanOne">
        <ref bean="anotherExampleBean"/>
    </property>

    <!-- setter injection using the neater ref attribute -->
    <property name="beanTwo" ref="yetAnotherBean"/>
    <property name="integerProperty" value="1"/>
</bean>

<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>

从配置上看似乎就是标签完成了注入。

这里抛出一个问题:例如上述的ExampleBean类,已经有了AnotherBean和YetAnotherBean的引用,按照道理说我们只要告知spring这三个bean就可以了不需要再设置property了,为啥还需要在xml配置文件中动态配置property呢?这不是有点多余么?能不能让spring自动给我们完成设置property的操作?

这就要提到自动注入了。

自动装配

在spring的文档中的1.4.5节中介绍了自动装配的相关内容,关于自动装配的优缺点文档中有以下表述:

优点

1.Autowiring can significantly reduce the need to specify properties or constructor arguments. 
	自动装配可以显著减少属性的指定和构造方法中的参数。
2.Autowiring can update a configuration as your objects evolve. For example, if you need to add a dependency to a class, that dependency can be satisfied automatically without you needing to modify the configuration. 
	如果对象的配置更改了,自动装配可以自动更新这个配置。例如:如果你在一个类中增加了一个引用,如果这个引用的对象发生了改变,你不用在配置中重新指定了,自动装配在开发中非常常用。

缺点:

Explicit dependencies in property and constructor-arg settings always override autowiring. You cannot autowire simple properties such as primitives, Strings, and Classes (and arrays of such simple properties). This limitation is by-design.
简单翻译一下:自动装配机制从设计层面就不支持简单类型的注入。

Autowiring is less exact than explicit wiring. Although, as noted in the earlier table, Spring is careful to avoid guessing in case of ambiguity that might have unexpected results. The relationships between your Spring-managed objects are no longer documented explicitly.
自动装配并不如显示指定精确,尽管如前面章节介绍的,spring在模棱两可的状态下尽量避免猜测的操作,但是使用了自动装配之后,spring管理的对象之间的关系就不如通过文档来管理清晰了。

Wiring information may not be available to tools that may generate documentation from a Spring container.
不需要XML等文档来维护类与类之间的依赖了,注入信息就无法从文档中获取了。

Multiple bean definitions within the container may match the type specified by the setter method or constructor argument to be autowired. For arrays, collections, or Map instances, this is not necessarily a problem. However, for dependencies that expect a single value, this ambiguity is not arbitrarily resolved. If no unique bean definition is available, an exception is thrown.
容器中定义的多个bean,可能需要与setter和构造方法中的参数匹配,这对数组,集合和map实例并不是什么大问题。但是如果一个类需要一个明确的bean,而这个bean类型对应的实现类并不唯一则会抛出异常。

总体来说如果编码中采用相关的规范,则这些缺点完全是可以规避的,同时针对简单类型的注入在实际中用的并不多。

自动装配的模式

官网中关于自动装配的方式有如下四种,这里简单说一下,no就是不开启自动装配,byName就是根据名称进行自动装配,byType就是根据类型完成自动装配,constructor就是根据构造函数完成自动装配
在这里插入图片描述

引入自动装配

基于xml

官网并没有给出详细的例子,但是我们在使用spring的时候就会知道,在xml配置文件中beans标签中,开启default-autowire属性,就可以设置一个默认的注入方式。但是在每一个bean标签,我们也可指定autowire,么一个bean标签指定的autowired属性会覆盖全局的default-autowire属性。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation=" http://www.springframework.org/schema/beans
                            http://www.springframework.org/schema/beans/spring-beans.xsd"
        default-autowire="byName">

    <bean id="indexDao" class="com.learn.ioc.dao.IndexDao"></bean>

    <bean id="indexService" class="com.learn.ioc.service.IndexService"></bean>

</beans>

在已经定义了两个bean的情况下分别设置一个全局的autowire属性。

本身bean的定义如下

@Repository
public class IndexDao {
    public void beanTest(){
        System.out.println("bean test dao");
    }
}

@Service
public class IndexService {
    private IndexDao indexDao;
    public void setDao(IndexDao indexDao) {
        this.indexDao = indexDao;
    }
    public void testIndexService(){
        System.out.println("this in service");
        indexDao.beanTest();
    }
}

测试的类

/**
 * autor:liman
 * createtime:2020/2/25
 * comment:
 */
public class XmlApplication {

    public static void main(String[] args) {
        ClassPathXmlApplicationContext classPathXmlApplicationContext =
                new ClassPathXmlApplicationContext("classpath:applicationContext.xml");

        IndexService indexService = (IndexService) classPathXmlApplicationContext.getBean("indexService");
        indexService.testIndexService();
    }
}

正常的调用而已,没啥可说的,但是运行结果会出现这样的问题

在这里插入图片描述

sorry还是会出现空指针异常的情况,因为设置的是注入方式是byName,这个就是根据set方法的名称来的,我们在service中定义的是set方法的方法名称是setDao,则对应的是给我们注入name属性(默认情况如果一个bean没有配置name则id=name)值为dao的bean,我们将setDao改成setIndexDao或者将bean中的indexDao改成dao,则能顺利运行。

还是上面的实例,只是加入byType的注入方式,则依旧能顺利运行。

<bean id="indexService" class="com.learn.ioc.service.IndexService" autowire="byType"></bean>

一个接口多个实现类

如下所示,多个实现类交给spring托管,配置service注入方式为byType,但是,运行后会出现经典的一个异常

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation=" http://www.springframework.org/schema/beans
                            http://www.springframework.org/schema/beans/spring-beans.xsd
                            http://www.springframework.org/schema/context
                            http://www.springframework.org/schema/context/spring-context.xsd">


    <bean id="indexDaoOne" class="com.learn.ioc.dao.IndexDaoOne"></bean>
    <bean id="indexDaoTwo" class="com.learn.ioc.dao.IndexDaoTwo"></bean>

    <bean id="indexService" class="com.learn.ioc.service.IndexService" autowire="byType"></bean>
</beans>

运行之前的代码,会出现如下异常。

在这里插入图片描述

毕竟根据byType注入,两个实现类,就变得模糊了。如果变成byName的方式注入,则能正常完成操作。

注解的方式

其实谈到注解的注入方式,我们用过spring的都应该知道,@Autowired、@Component和@Service等等一些注解(关于@Component和@Service,@Repository等的区别,spring官网中有这样一句话:@Repository, @Service, and @Controller can also carry additional semantics in future releases of the Spring Framework. Thus, if you are choosing between using @Component or @Service for your service layer, @Service is clearly the better choice. 这句话的意思就是@Repository等注解后续会有其他含义,如果考虑到服程序的扩展性,建议用@Service这一类更加清晰的注解)

加入一个config类

/**
 * autor:liman
 * createtime:2020/2/25
 * comment:
 */
@Configuration
@ComponentScan("com.learn.ioc")//扫描指定的包
public class AppConfig {
}

原来的各个类中注解修改为如下:

@Service
public class IndexService {
    @Autowired
    private IIndexDao indexDao;
    public void setDao(IIndexDao indexDao) {
        this.indexDao = indexDao;
    }
    public void testIndexService(){
        System.out.println("this in service");
        indexDao.beanTest();
    }
}

@Repository
public class IndexDaoOne implements IIndexDao{
    public void beanTest(){
        System.out.println("bean test dao one");
    }
}

@Repository
public class IndexDaoTwo implements IIndexDao{
    public void beanTest(){
        System.out.println("bean test dao two");
    }
}

测试类:这里采用获取注解配置的applicationContext

/**
 * autor:liman
 * createtime:2020/2/25
 * comment:
 */
public class AnnotationApplication {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext annotationConfigApplicationContext
                = new AnnotationConfigApplicationContext(AppConfig.class);
        IndexService indexService = annotationConfigApplicationContext.getBean(IndexService.class);
        indexService.testIndexService();

    }
}

运行之后会发现异常,因为这里用的是@Autowired注解自动注入的,但是这个接口有两个实现类,依旧会出现上述运行类型指向模糊的问题,如果将@Autowired改成@Resource并指定name属性,则正常运行

如下所示:

@Service
public class IndexService {

    @Resource(name="indexDaoTwo")	//这里通过resource的name属性指定
    private IIndexDao indexDao;

    public void setDao(IIndexDao indexDao) {
        this.indexDao = indexDao;
    }

    public void testIndexService(){
        System.out.println("this in service");
        indexDao.beanTest();
    }
}

由于这个问题比较难以通过文本来表述,因此这里直接抛出官网中的一个结论:@Autowired与@Resource最大的区别就在于@Autowired默认采用byType的方式注入,如果byType注入失败则采用byName注入。@Resource则相反,默认采用byName的方式注入,如果默认的方式失败会采用byType。两种方式都找不到指定需要注入的bean则会抛出异常。

关于懒加载

在官网的1.4.4节

By default, ApplicationContext implementations eagerly create and configure all singleton beans as part of the initialization process. Generally, this pre-instantiation is desirable, because errors in the configuration or surrounding environment are discovered immediately, as opposed to hours or even days later. When this behavior is not desirable, you can prevent pre-instantiation of a singleton bean by marking the bean definition as being lazy-initialized. A lazy-initialized bean tells the IoC container to create a bean instance when it is first requested, rather than at startup.
翻译:默认情况下,容器的初始化过程中是创建并配置所有的Singleton的bean实例的。通常来说,这种预先初始化的方式是值得推荐的,因为配置或者环境变量方面的错误可以在这一阶段直接暴露并被发现。如果不想这样,我们通过将这个bean标记为懒加载的方式来阻止容器提前初始化这个bean。一个被标记为懒加载的bean就是告知IOC,让IOC在第一次需要的时候进行加载而不是容器启动的时候。

这句话似乎没啥重要的,只是告知了我们可以通过配置一些属性来实现懒加载,但是接下来官网中的话就有点意思了。

In XML, this behavior is controlled by the lazy-init attribute on the <bean> element, as the following example shows:

<bean id="lazy" class="com.something.ExpensiveToCreateBean" lazy-init="true"/>
<bean name="not.lazy" class="com.something.AnotherBean"/>

When the preceding configuration is consumed by an ApplicationContext, the lazy bean is not eagerly pre-instantiated when the ApplicationContext starts, whereas the not.lazy bean is eagerly pre-instantiated.

依旧来个翻译:

在XML配置的方式下,可通过配置bean中的lazy-init属性来告知容器这个bean是懒加载的。当进一步解析上述配置的时候,id为lazy的bean就会被懒加载,而name为“not.lazy”的bean则不会。

However, when a lazy-initialized bean is a dependency of a singleton bean that is not lazy-initialized, the ApplicationContext creates the lazy-initialized bean at startup, because it must satisfy the singleton’s dependencies. The lazy-initialized bean is injected into a singleton bean elsewhere that is not lazy-initialized.

个人觉得上述一段话比较重要,当一个被标记为懒加载的bean为一个Singleton(非懒加载)bean的依赖的时候,这个bean就不是懒加载的了,容器依旧会在启动的时候就创建这个被标记为懒加载的bean,因为必须要满足Singleton的bean的依赖,懒加载的bean依旧会在启动的时候注入到非懒加载的bean中

总体来说懒加载用的比较少,但是面试到了上面一段话还是加分项。

作用域

这个在官网的1.5节中介绍,六种作用域如下表所示,这里简单总结一下Singleton和prototype两种类型的作用域,其他作用域等在学习了spring mvc之后再继续讨论。

在这里插入图片描述

singleton——其实就是单例的意思,prototype是原型的意思,初步理解其实Singleton和prototype就是对应容器中实例的个数,前者初始化的实例在容器中只有一个,而标记为后者的则在容器中会有多个。

先来一个基本的实例

prototype的实例

/**
 * autor:liman
 * createtime:2020/2/27
 * comment:
 */
@Service("prototypeBean")
@Scope("prototype")
public class PrototypeBean {

    public void testPrototype(){
        System.out.println("i am prototype bean");
    }
}

singleton的实例

/**
 * autor:liman
 * createtime:2020/2/27
 * comment:
 */
@Service("singletonBean")
@Scope("singleton")
public class SingletonBean {

    public void testSingleton(){
        System.out.println("i am singleton");
    }
}

然后通过getBean查看两者的区别

/**
 * autor:liman
 * createtime:2020/2/27
 * comment:
 */
public class SingleAndPrototypeApplication {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext applicationContext = new
                AnnotationConfigApplicationContext(AppConfig.class);

        System.out.println("================prototype hashcode===============");
        PrototypeBean proOne = applicationContext.getBean(PrototypeBean.class);
        PrototypeBean proTwo = applicationContext.getBean(PrototypeBean.class);
        System.out.println(proOne.hashCode());
        System.out.println(proTwo.hashCode());

        System.out.println("=================singleton hashcode==============");
        SingletonBean singletonBeanOne = applicationContext.getBean(SingletonBean.class);
        SingletonBean singletonBeanTwo = applicationContext.getBean(SingletonBean.class);
        System.out.println(singletonBeanOne.hashCode());
        System.out.println(singletonBeanTwo.hashCode());

    }

}

运行结果如下所示:

在这里插入图片描述

prototype获得的两个实例并不一样。singleton获得的实例是一样的。

如果singleton的依赖prototype的,会有啥情况呢?这个问题其实在spring文档中的1.5.3节中也有解释

When you use singleton-scoped beans with dependencies on prototype beans, be aware that dependencies are resolved at instantiation time. Thus, if you dependency-inject a prototype-scoped bean into a singleton-scoped bean, a new prototype bean is instantiated and then dependency-injected into the singleton bean. The prototype instance is the sole instance that is ever supplied to the singleton-scoped bean.

翻译:当你想使用一个single作用域的bean且这个bean依赖prototype的bean,需要知道这个依赖是在初始化的时候解析的。因此,如果你往一个single的bean中注入prototype的bean,确实会注入一个prototype的bean。但是这个prototype的bean是唯一的。

However, suppose you want the singleton-scoped bean to acquire a new instance of the prototype-scoped bean repeatedly at runtime. You cannot dependency-inject a prototype-scoped bean into your singleton bean, because that injection occurs only once, when the Spring container instantiates the singleton bean and resolves and injects its dependencies. If you need a new instance of a prototype bean at runtime more than once, see Method Injection
翻译:虽然你想通过singleton的bean获取一个prototype的bean。但是你并不能往singleton的bean中注入一个prototype的bean,因为这个注入只会发生一次,这个注入就发生在spring容器初始化的时候。如果你真的想在singleton的bean中获取一个真正多例的bean的话,需要参考Method Injection这一节。

测试一下:

@Service("singletonBean")
@Scope("singleton")
public class SingletonBean {

    @Autowired
    private PrototypeBean prototypeBean; //这里注入一个多例的bean

    public void testSingleton(){
        System.out.println("i am singleton");
    }

    public PrototypeBean getPrototypeBean() {
        return prototypeBean;
    }
}

运行结果如下:

在这里插入图片描述

并没有给我们想要的多例,因为单例的bean只能初始化一次。但是官网中给了一种方式,通过单例的bean获取多例的bean的方式——实现ApplicationContextWare接口

@Service("singletonBean")
@Scope("singleton")
public class SingletonBean implements ApplicationContextAware {

    private ApplicationContext applicationContext;

    @Autowired
    private PrototypeBean prototypeBean; //这里注入一个多例的bean

    public void testSingleton(){
        System.out.println("i am singleton");
    }

    public PrototypeBean getPrototypeBean() {
        //在getPrototype的时候,从容器中去获取
        return applicationContext.getBean(PrototypeBean.class);
    }

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

运行结果:

在这里插入图片描述

hashCode不同,则说明不是同一个实例。这样处理与spring框架本身比较耦合。后面还有一种lookup的方式,这里就不介绍了,官网中有相关实例。

bean的一些其他处理

可能初步看的时候这个标题有点让人迷惑,好比我们在实际编码过程中,我们写了正常的核心逻辑之后,还需要做一个try/catch/finally的处理,这里的其他处理就相当于bean初始化之后的操作。这一节其实是想总结spring官网中的1.6.1这一节

简单来说,就是在bean创建完成正式使用之前,或者bean销毁的时候,spring提供了一些我们自处理的回调方法。

Initialization Callbacks

初始化的回调

The org.springframework.beans.factory.InitializingBean interface lets a bean perform initialization work after the container has set all necessary properties on the bean.

翻译:InitializingBean这个接口提供了一个容器设置完bean的必要属性之后的一些处理逻辑

We recommend that you do not use the InitializingBean interface, because it unnecessarily couples the code to Spring. Alternatively, we suggest using the @PostConstruct annotation or specifying a POJO initialization method.In the case of XML-based configuration metadata, you can use the init-method attribute to specify the name of the method that has a void no-argument signature. With Java configuration, you can use the initMethod attribute of @Bean.

翻译:但是我们并不推荐使用这个接口,因为这样与spring框架本身就耦合了。我们推荐使用@PostConstruct注解或者显示指定POJO的初始化方法。如果使用的是XML的配置,则指定init-method的属性,如果是java config则指定@Bean中的initMethod属性。

<bean id="exampleInitBean" class="examples.ExampleBean" init-method="init"/>

后两者就好的多并没有与spring框架耦合

Destruction Callbacks

Implementing the org.springframework.beans.factory.DisposableBean interface lets a bean get a callback when the container that contains it is destroyed.

同样的,针对销毁的操作,spring提供了DisposableBean这个接口,但是spring依旧不推荐这种方式,而是推荐采用@PreDestroy,或者在XML中配置destroy-method或者java config中配置destroyMethod的方式来完成。

实例

因为比较简单,这里就直接利用spring不推荐的方式来实现了

定义的一个bean

@Component("beanCallback")
public class BeanCallback implements InitializingBean,DisposableBean {

    public BeanCallback() {
        System.out.println("this is construct method");
    }


    @Override
    public void destroy() throws Exception {
        System.out.println("bean destory");
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("bean life cycle after properties set ");
    }
}

具体操作的实例

/**
 * autor:liman
 * createtime:2020/2/27
 * comment:
 */
public class BeanlifecycleApplication {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext applicationContext =
                new AnnotationConfigApplicationContext(AppConfig.class);

        BeanCallback bean = applicationContext.getBean(BeanCallback.class);
        applicationContext.close();
    }

}

运行结果

在这里插入图片描述

总结

本篇博客按照官网中总结了一些自己平时使用spring框架过程中容易忽略的东西,大部分都根据官网来梳理。基本梳理出了spring文档中关于ioc的关键点。针对component-scan这个并没有总结,这个可以直接参考官网。

针对spring的扫描可以增加扫描的效率,这个在1.10.9节中有简单的介绍,需要引入这依赖包

 <dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context-indexer</artifactId>
        <version>5.2.3.RELEASE</version>
        <optional>true</optional>
    </dependency>
</dependencies>
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!