Spring IOC

烟雨 5年前 (2021-06-02) 阅读数 393 #Spring
文章标签 Spring

一,什么是Spring IOC

         控制反转(Inversion of Control,缩写为IoC),是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。其中最常见的实现方式(实现技术手段)叫做依赖注入(Dependency Injection,简称DI),还有一种方式叫“依赖查找”(Dependency Lookup)(JNDI数据源配置实现方式就是依赖查找)。
        IOC与DI关系:目标在于IOC,其实现方式(实现技术手段)为DI。
        面试容易问这个概念
什么是依赖
    比如TestServiceImpl.class中有一个TestDao.class的属性,那么我们可以理解为TestServiceImpl依赖了TestDao
public class TestServiceImpl implements TestService{
    @Autowired
    private TestDao testDao;
}

二、为什么要使用spring IOC

        在日常程序开发过程当中,我们推荐面向抽象编程,面向抽象编程会产生类的依赖,当然如果你够强大可以自己写一个管理的容器,但是既然spring以及实现了,并且spring如此优秀,我们仅仅需要学习spring框架便可。
        当我们有了一个管理对象的容器之后,类的产生过程也交给了容器,至于我们自己的app则可以不需要去关系这些对象的产生了。

三、spring实现IOC的思路和方法

        spring实现IOC的思路是提供一些配置信息用来描述类之间的依赖关系,然后由容器去解析这些配置信息,继而维护好对象之间的依赖关系,前提是对象之间的依赖关系必须在类中定义好,比如A.class中有一个B.class的属性,那么我们可以理解为A依赖了B。
        spring实现IOC的思路大致可以拆分成3点:
    1. 应用程序中提供类,提供依赖关系(属性或者构造方法)。

    2. 把需要交给容器管理的对象通过配置信息告诉容器(xml、annotation,javaConfiguration)。

    3. 把各个类之间的依赖关系通过配置信息告诉容器,让容器去管理和产生对象。

四、SpringIOC 3种编程的风格

提前修改pom.xml,引入Spring-context
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>Spring Test</artifactId>
    <version>1.0-SNAPSHOT</version>
	<!-- 管理Spring版本号 -->
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <spring.version>5.2.1.RELEASE</spring.version>
    </properties>

    <dependencies>
        <!-- Spring Context -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>${spring.version}</version>
        </dependency>
    </dependencies>
</project>
4.1、xml风格(set方法注入)
TestDaoImpl
public class TestDaoImpl implements TestDao {
    @Override
    public String test() {
        return "TestDaoImpl";
    }
}

TestServiceImpl类依赖了TestDaoImpl类,通过set方法注入TestDao

public class TestServiceImpl implements TestService {

    private TestDao testDao;

    public void setTestDao(TestDao testDao) {
        this.testDao = testDao;
    }

    @Override
    public void printTest() {
        System.out.println(testDao.test());
    }
}

xml配置文件,描述各个类的依赖关系

<?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">

    <bean id="testDaoImpl" class="com.zender.springioc.dao.impl.TestDaoImpl"/>
    <!-- 通过set方法注入 -->
    <bean id="testService" class="com.zender.springioc.service.impl.TestServiceImpl">
    	<!-- name:set方法名为:testDao(setTestDao去掉前缀set,并小写首字母,得到set方法名)-->
        <!-- testDaoImpl:需要注入的bean的id -->
        <property name="testDao" ref="testDaoImpl"></property>
    </bean>
</beans>

测试类

public class Test {
    public static void main(String[] args) {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:Test.xml");
        TestServiceImpl testService = (TestServiceImpl)applicationContext.getBean("testService");
        testService.printTest();
    }
}

image.png

4.2、annotation(注解)风格

TestDaoImpl
@Component("testDaoImpl")
public class TestDaoImpl implements TestDao {
    @Override
    public String test() {
        return "TestDaoImpl";
    }
}

TestServiceImpl类依赖了TestDaoImpl类,通过注解@Autowired注入TestDao。

@Component("testServiceImpl")
public class TestServiceImpl implements TestService {
    @Autowired
    private TestDao testDao;

    @Override
    public void printTest() {
        System.out.println(testDao.test());
    }
}

@Autowired默认是按照类型装配注入的,如果bean中存在多个以上同类型会如下报错:

org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'com.zender.springioc.dao.TestDao' available: expected single matching bean but found 2: testDaoImpl,testDaoImpl2
xml配置文件,开启注解扫描,并指定扫描包路径。

<context>子标记:


1,resource-pattern:仅希望扫描特定的类而非基包下的所有类。

2,include-filter:指定需要包含的

3,exclude-filter:指定需要排除的

4,type表示采用的过滤类型,共有如下5种类型:

过滤类型        

例如        

解释

annotation        

org.example.SomeAnnotation        

注解了SomeAnnotation类型的类

assignable        

org.example.SomeClass        

所有扩展或者实现SomeClass的类

aspectj        

org.example..*Service+        

AspectJ语法表示org.example包下所有包含Service的类及其子类

regex        

org\.example\.Default.*        

Regelar Expression,正则表达式

custom        

org.example.MyTypeFilter        

通过代码过滤,实现org.springframework.core.type.TypeFilter接口

例如:
<?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">
    <context:component-scan base-package="com.zender"> 
        <!-- 扫描注解了org.springframework.stereotype.Repository的类 -->
        <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Repository" />
        <!-- aspectj类型,扫描com.zender.dao下所有的类,排除entity下所有的类 -->
        <context:include-filter type="aspectj" expression="com.zender.dao. *" />
        <context:exclude-filter type="aspectj" expression="com.zender.entity.*.*" />
    </context:component-scan>
</beans>

测试类

public class Test {
    public static void main(String[] args) {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:Test.xml");
        TestServiceImpl testService = (TestServiceImpl)applicationContext.getBean("testServiceImpl");
        testService.printTest();
    }
}

image.png

在类的开头使用了@Component注解,它可以被Spring容器识别,启动Spring后,会自动把它转成Bean, 交给容容器管理。使用@Component注解时不指定名称时,Spring会使用默认的命名机制,即简单类名且第一个字母小写。

除了@Component外,Spring提供了3个基本注解和@Component等效,分别对应于用于对DAO,Service,和Controller进行注解(官方文档中说,在未来的Spring版本中这三个注解可能有更多的语义):

  1. @Repository 用于对DAO实现类进行注解。

  2. @Service 用于对业务层注解,但是目前该功能与 @Component 相同。

  3. @Constroller用于对控制层注解,但是目前该功能与 @Component 相同。

@Component是这3个注解的父类
4.3、java Configuration风格
TestDaoImpl
@Component("testDaoImpl")
public class TestDaoImpl implements TestDao {
    @Override
    public String test() {
        return "TestDaoImpl";
    }
}

TestServiceImpl类依赖了TestDaoImpl类,通过注解@Autowired注入TestDao。

@Component("testServiceImpl")
public class TestServiceImpl implements TestService {
    @Autowired
    private TestDao testDao;

    @Override
    public void printTest() {
        System.out.println(testDao.test());
    }
}

创建一个SpringIocConfig用于描述Spring的配置

//表示这个类是Spring的配置类
@Configuration
//开启注解扫描,等同于xml中的context:component-scan
@ComponentScan("com.zender")
public class SpringIocConfig {
}

测试类,通过AnnotationConfigApplicationContext来加载我们的SpringIocConfig配置类。

public class Test {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(SpringIocConfig.class);
        TestServiceImpl testService = (TestServiceImpl)annotationConfigApplicationContext.getBean("testServiceImpl");
        testService.printTest();
    }
}

image.png

五、Spring注入的2种方式

5.1、set方法注入

参考:4.1、xml风格(set方法注入)
关于Spring注入详细配置(字符串、数组等)请参考Spring官方文档(实际开发中基本不会使用):
https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans-factory-properties-detailed

5.2、构造方法注入

TestDaoImpl同4.1
TestServiceImpl显示的增加一个有参构造方法对test进行初始化
public class TestServiceImpl implements TestService {
    private TestDao testDao;

    public TestServiceImpl(TestDao testDao) {
        this.testDao = testDao;
    }

    @Override
    public void printTest() {
        System.out.println(testDao.test());
    }
}

xml配置文件

<?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">

    <bean id="testDaoImpl" class="com.zender.springioc.dao.impl.TestDaoImpl"/>
     <!-- 通过constructor-arg指定构造方法注入的bean -->
    <bean id="testService" class="com.zender.springioc.service.impl.TestServiceImpl">
        <!-- name指的是构造方法注入的参数名称 -->
        <constructor-arg name="testDao" ref="testDaoImpl"></constructor-arg>
        <!-- 
			也可以通过下标来进行注入 
			<constructor-arg index="0" ref="testDaoImpl"></constructor-arg>
		-->
    </bean>
</beans>
结果同4.1

六、Spring xml风格配置扩展,P命名空间的使用(C命名空间也同样如此)

通过p命名空间实现set方法注入
TestDaoImpl和TestServiceImpl同4.1
修改xml配置文件,引入p命名空间:xmlns:p="http://www.springframework.org/schema/p"
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:p="http://www.springframework.org/schema/p"
       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">

    <bean id="testDaoImpl" class="com.zender.springioc.dao.impl.TestDaoImpl"/>
    <!-- 通过set注入 -->
    <bean id="testService" class="com.zender.springioc.service.impl.TestServiceImpl" p:testDao-ref="testDaoImpl">
    </bean>
</beans>
结果同4.1

七,自动装配

        这里有个疑问,既然我们在类中已经定义了他们之间的依赖关系那么为什么还需要在xml配置文件中去描述和定义呢?

        上面说过,IOC的注入有两个地方需要提供依赖关系,一是类的定义中,二是在Spring的配置中需要去描述。自动装配则把第二个取消了,即我们仅仅需要在类中提供依赖,继而把对象交给容器管理即可完成注入。

        在实际开发中,描述类之间的依赖关系通常是大篇幅的,如果使用自动装配则省去了很多配置,并且如果对象的依赖发生更新我们可以不需要去更新配置。

7.1、Spring注入方式ByType和ByName

可以通过beans标签添加(default-autowire="")指定全局的注入方式,也可以通过bean标签(autowire="")指定单个bean的注入方式。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:p="http://www.springframework.org/schema/p"
       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="byType">
       <!-- 其实就是set方式注入,在没有指定name,取得是id的值 -->
   	   <bean id="testDaoImpl" class="com.zender.springioc.dao.impl.TestDaoImpl" autowire="byName"/>
</beans>
自动装配模式:
模式说明
no(默认)无自动装配。Bean引用必须由ref元素定义。对于较大的部署,建议不要更改默认设置,因为明确指定协作者可以提供更好的控制和清晰度。在某种程度上,它记录了系统的结构。
byName按属性名称自动注入。Spring寻找与需要自动装配的属性同名的bean。例如,如果一个bean定义被设置为按名称自动装配并且包含一个master属性(即它具有一个 setMaster(..)方法),那么Spring将查找一个名为的bean定义,master并使用它来设置该属性。
byType如果容器中恰好存在一个该属性类型的bean,则使该属性自动装配。如果存在多个错误,则会引发致命异常,这表明您不能byType对该bean 使用自动装配。如果没有匹配的bean,则什么都不会发生(未设置该属性)。
constructor
类似于byType但适用于构造函数参数。如果容器中不存在构造函数参数类型的一个bean,则将引发致命错误。

7.2、自动装配注解

自动装配注解主要有:@Autowired、@Qualifier、@Resource
它们的特点是:
  1. @Resource默认是按byName称来装配注入的(根据属性名字来的,不是根据set方法的名字),只有当找不到与名称匹配的bean才会按照byType来装配注入,@Resource注解是由J2EE提供,而@Autowired是由spring提供,故减少系统对spring的依赖建议使用@Resource的方式。例如:

    1. @Resource(name="testDao") 
      private TestDao testDao
  2. @Autowired默认是按照byType装配注入的,如果想按照byName来装配注入,则需要结合@Qualifier一起使用。

  3. @Resource@Autowired都可以书写注解在字段或者该字段的setter方法之上。

  4. @Autowired 可以对成员变量、方法以及构造函数进行注释,而 @Qualifier 的注解对象是成员变量、方法入参、构造函数入参。

  5. @Qualifier("XXX") 中的XXX是 Bean 的名称,所以 @Autowired 和 @Qualifier 结合使用时,自动注入的策略就从 byType 转变成 byName 了。例如:

    1. @Autowired 
      @Qualifier("testDao") 
      private TestDao testDao
  6. @Autowired 注释进行自动注入时,Spring 容器中匹配的候选 Bean 数目必须有且仅有一个,通过属性required可以设置非必要。

    1. //默认required = true
      @Autowired(required = false) 
      @Qualifier("testDao") 
      private TestDao testDao
  7. @Resource装配顺序

    1. 如果同时指定了name和type,则从Spring上下文中找到唯一匹配的bean进行装配,找不到则抛出异常。

    2. 如果指定了name,则从上下文中查找名称(id)匹配的bean进行装配,找不到则抛出异常。

    3. 如果指定了type,则从上下文中找到类型匹配的唯一bean进行装配,找不到或者找到多个,都会抛出异常。

    4. 如果既没有指定name,又没有指定type,则自动按照byName方式进行装配;如果没有匹配,则回退为一个原始类型进行匹配,如果匹配则自动装配;

八、懒加载

可以通过beans标签添加default-lazy-init=""指定全局是否启用懒加载,也可以通过bean标签lazy-init=""指定单个bean是否启用懒加载。

懒加载是程序使用到这个类时才加载,初始化。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:p="http://www.springframework.org/schema/p"
       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-lazy-init="true">

    <bean id="testDaoImpl" class="com.zender.springioc.dao.impl.TestDaoImpl" lazy-init="true"/>
    <!-- 通过set注入 -->
    <bean id="testService" class="com.zender.springioc.service.impl.TestServiceImpl" p:testDao-ref="testDaoImpl">
    </bean>
</beans>

九、Spring Bean的作用域

我们可以使用scope属性指定作用域,作用域有以下7个:

image.png

例如:

<!-- 修改对象的作用域 -->
<bean id="person" class="com.zender.springioc.dto.Person" scope="prototype"></bean>
public class Test {
    public static void main(String[] args) {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:Test.xml");
        Person person = (Person)applicationContext.getBean("person");
        Person person2 = (Person)applicationContext.getBean("person");
        System.out.println(person.hashCode());
        System.out.println(person2.hashCode());
    }
}

image.png

9.1、关于Bean的作用域的一个问题

如果一个单例类A里面注入了一个非单例类B,多次生成单例类A,那么非单例类B生成了几次?
通过代码来验证,以4.2,和4.3中的代码为例:
在TestDaoImpl类中添加@Scope("prototype")注解来改变TestDaoImpl的作用域
@Component("testDaoImpl")
//更改TestDaoImpl的作用域prototype(非单例)
@Scope("prototype")
public class TestDaoImpl implements TestDao {
    @Override
    public String test() {
        return "TestDaoImpl";
    }
}

测试类

public class Test {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(SpringIocConfig.class);;
        TestService testService = (TestService)annotationConfigApplicationContext.getBean("testServiceImpl");
        TestDao testDao = testService.getTestDao();
        TestService testService2 = (TestService)annotationConfigApplicationContext.getBean("testServiceImpl");
        TestDao testDao2 = testService2.getTestDao();
        System.out.println("testService:"+testService.hashCode()+"----testDao:"+testDao.hashCode());
        System.out.println("testService:"+testService2.hashCode()+"----testDao:"+testDao2.hashCode());
    }
}

image.png

其实,在创建TestService时,由于TestService是单例类,Spring只初始化了一次,所依赖的TestDao也只是初始化了一次,所以打印出的TestDao的hashCode是相同的。
如果想让每次生成的TestDao不同,需要如下更改,方式有2种:

1、修改TestServiceImpl类,实现ApplicationContextAware接口(该方式过于依赖Spring代码,不建议使用)

@Component("testServiceImpl")
public class TestServiceImpl implements TestService, ApplicationContextAware {
    private ApplicationContext applicationContext;

    @Override
    public TestDao getTestDao() {
        return (TestDao)applicationContext.getBean("testDaoImpl");
    }

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

image.png

2、通过注解@Lookup()

@Component("testServiceImpl")
public class TestServiceImpl implements TestService {

    @Autowired
    private TestDao testDao;

    @Lookup
    public TestDao getTestDao() {
        return testDao;
    }

    @Override
    public TestDao getDao() {
        return this.getTestDao();
    }
}
结果同上
非注解模式时通过lookup-method来实现
<bean id="myCommand" class="fiona.apple.AsyncCommand" scope="prototype"></bean>

<bean id="commandManager" class="fiona.apple.CommandManager">
    <lookup-method name="createCommand" bean="myCommand"/>
</bean>


版权声明

非特殊说明,本文由Zender原创或收集发布,欢迎转载。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

作者文章
热门