Spring IOC容器详解 Spring IOC容器详解引入我们在Spring基础 - Spring简单例子引入Spring的核心中向你展示了IoC的基础含义同时以此发散了一些IoC相关知识点。Spring框架管理这些Bean的创建工作即由用户管理Bean转变为框架管理Bean这个就叫控制反转 - Inversion of Control (IoC)Spring 框架托管创建的Bean放在哪里呢 这便是IoC Container;Spring 框架为了更好让用户配置Bean必然会引入不同方式来配置Bean 这便是xml配置Java配置注解配置等支持Spring 框架既然接管了Bean的生成必然需要管理整个Bean的生命周期等应用程序代码从Ioc Container中获取依赖的Bean注入到应用程序中这个过程叫依赖注入(Dependency InjectionDI) 所以说控制反转是通过依赖注入实现的其实它们是同一个概念的不同角度描述。通俗来说就是IoC是设计思想DI是实现方式在依赖注入时有哪些方式呢这就是构造器方式Autowired, Resource, Qualifier... 同时Bean之间存在依赖可能存在先后顺序问题以及循环依赖问题等本节将在此基础上进一步解读IOC的含义以及IOC的使用方式后续的文章还将深入IOC的实现原理Spring进阶- Spring IOC实现原理详解之IOC体系结构设计Spring进阶- Spring IOC实现原理详解之IOC初始化流程Spring进阶- Spring IOC实现原理详解之Bean的注入和生命周期如何理解IoC如果你有精力看英文首推 Martin Fowler大师的 Inversion of Control Containers and the Dependency Injection pattern其次IoC作为一种设计思想不要过度解读而是应该简化理解所以我这里也整合了张开涛早前的博客IoC基础并加入了自己的理解。Spring Bean是什么IoC Container管理的是Spring Bean 那么Spring Bean是什么呢Spring里面的bean就类似是定义的一个组件而这个组件的作用就是实现某个功能的这里所定义的bean就相当于给了你一个更为简便的方法来调用这个组件去实现你要完成的功能。IoC是什么Ioc—Inversion of Control即控制反转不是什么技术而是一种设计思想。在Java开发中Ioc意味着将你设计好的对象交给容器控制而不是传统的在你的对象内部直接控制。我们来深入分析一下谁控制谁控制什么传统Java SE程序设计我们直接在对象内部通过new进行创建对象是程序主动去创建依赖对象而IoC是有专门一个容器来创建这些对象即由Ioc容器来控制对 象的创建谁控制谁当然是IoC 容器控制了对象控制什么那就是主要控制了外部资源获取不只是对象包括比如文件等。为何是反转哪些方面反转了?有反转就有正转传统应用程序是由我们自己在对象中主动控制去直接获取依赖对象也就是正转而反转则是由容器来帮忙创建及注入依赖对象为何是反转因为由容器帮我们查找及注入依赖对象对象只是被动的接受依赖对象所以是反转哪些方面反转了依赖对象的获取被反转了。用图例说明一下?传统程序设计下都是主动去创建相关对象然后再组合起来。当有了IoC/DI的容器后在客户端类中不再主动去创建这些对象了。IoC能做什么IoC不是一种技术只是一种思想一个重要的面向对象编程的法则它能指导我们如何设计出松耦合、更优良的程序。传统应用程序都是由我们在类内部主动创建依赖对象从而导致类与类之间高耦合难于测试有了IoC容器后把创建和查找依赖对象的控制权交给了容器由容器进行注入组合对象所以对象与对象之间是 松散耦合这样也方便测试利于功能复用更重要的是使得程序的整个体系结构变得非常灵活。其实IoC对编程带来的最大改变不是从代码上而是从思想上发生了主从换位的变化。应用程序原本是老大要获取什么资源都是主动出击但是在IoC/DI思想中应用程序就变成被动的了被动的等待IoC容器来创建并注入它所需要的资源了。IoC很好的体现了面向对象设计法则之一——好莱坞法则别找我们我们找你即由IoC容器帮对象找相应的依赖对象并注入而不是由对象主动去找。IoC和DI是什么关系控制反转是通过依赖注入实现的其实它们是同一个概念的不同角度描述。通俗来说就是IoC是设计思想DI是实现方式。DI—Dependency Injection即依赖注入组件之间依赖关系由容器在运行期决定形象的说即由容器动态的将某个依赖关系注入到组件之中。依赖注入的目的并非为软件系统带来更多功能而是为了提升组件重用的频率并为系统搭建一个灵活、可扩展的平台。通过依赖注入机制我们只需要通过简单的配置而无需任何代码就可指定目标需要的资源完成自身的业务逻辑而不需要关心具体的资源来自何处由谁实现。我们来深入分析一下谁依赖于谁当然是应用程序依赖于IoC容器为什么需要依赖应用程序需要IoC容器来提供对象需要的外部资源谁注入谁很明显是IoC容器注入应用程序某个对象应用程序依赖的对象注入了什么就是注入某个对象所需要的外部资源包括对象、资源、常量数据。IoC和DI有什么关系呢其实它们是同一个概念的不同角度描述由于控制反转概念比较含糊可能只是理解为容器控制对象这一个层面很难让人想到谁来维护对象关系所以2004年大师级人物Martin Fowler又给出了一个新的名字依赖注入相对IoC 而言依赖注入明确描述了被注入对象依赖IoC容器配置依赖对象。通俗来说就是IoC是设计思想DI是实现方式。Ioc 配置的三种方式在Spring基础 - Spring简单例子引入Spring的核心已经给出了三种配置方式这里再总结下总体上目前的主流方式是注解 Java 配置。xml 配置顾名思义就是将bean的信息配置.xml文件里通过Spring加载文件为我们创建bean。这种方式出现很多早前的SSM项目中将第三方类库或者一些配置工具类都以这种方式进行配置主要原因是由于第三方类不支持Spring注解。优点 可以使用于任何场景结构清晰通俗易懂缺点 配置繁琐不易维护枯燥无味扩展性差举例配置xx.xml文件声明命名空间和配置bean?xml version1.0 encodingUTF-8? beans xmlnshttp://www.springframework.org/schema/beans xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd !-- services -- bean iduserService classtech.pdai.springframework.service.UserServiceImpl property nameuserDao refuserDao/ !-- additional collaborators and configuration for this bean go here -- /bean !-- more bean definitions for services go here -- /beansJava 配置将类的创建交给我们配置的JavcConfig类来完成Spring只负责维护和管理采用纯Java创建方式。其本质上就是把在XML上的配置声明转移到Java配置类中优点适用于任何场景配置方便因为是纯Java代码扩展性高十分灵活缺点由于是采用Java类的方式声明不明显如果大量配置可读性比较差举例创建一个配置类 添加Configuration注解声明为配置类创建方法方法上加上bean该方法用于创建实例并返回该实例创建后会交给spring管理方法名建议与实例名相同首字母小写。注实例类不需要加任何注解/** * author pdai */ Configuration public class BeansConfig { /** * return user dao */ Bean(userDao) public UserDaoImpl userDao() { return new UserDaoImpl(); } /** * return user service */ Bean(userService) public UserServiceImpl userService() { UserServiceImpl userService new UserServiceImpl(); userService.setUserDao(userDao()); return userService; } }注解配置通过在类上加注解的方式来声明一个类交给Spring管理Spring会自动扫描带有ComponentControllerServiceRepository这四个注解的类然后帮我们创建并管理前提是需要先配置Spring的注解扫描器。优点开发便捷通俗易懂方便维护。缺点具有局限性对于一些第三方资源无法添加注解。只能采用XML或JavaConfig的方式配置举例对类添加Component相关的注解比如ControllerServiceRepository设置ComponentScan的basePackage, 比如context:component-scan base-packagetech.pdai.springframework, 或者ComponentScan(tech.pdai.springframework)注解或者new AnnotationConfigApplicationContext(tech.pdai.springframework)指定扫描的basePackage./** * author pdai */ Service public class UserServiceImpl { /** * user dao impl. */ Autowired private UserDaoImpl userDao; /** * find user list. * * return user list */ public ListUser findUserList() { return userDao.findUserList(); } }依赖注入的三种方式常用的注入方式主要有三种构造方法注入Construct注入setter注入基于注解的注入接口注入setter方式在XML配置方式中property都是setter方式注入比如下面的xml:?xml version1.0 encodingUTF-8? beans xmlnshttp://www.springframework.org/schema/beans xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd !-- services -- bean iduserService classtech.pdai.springframework.service.UserServiceImpl property nameuserDao refuserDao/ !-- additional collaborators and configuration for this bean go here -- /bean !-- more bean definitions for services go here -- /beans本质上包含两步第一步需要new UserServiceImpl()创建对象, 所以需要默认构造函数第二步调用setUserDao()函数注入userDao的值, 所以需要setUserDao()函数所以对应的service类是这样的/** * author pdai */ public class UserServiceImpl { /** * user dao impl. */ private UserDaoImpl userDao; /** * init. */ public UserServiceImpl() { } /** * find user list. * * return user list */ public ListUser findUserList() { return this.userDao.findUserList(); } /** * set dao. * * param userDao user dao */ public void setUserDao(UserDaoImpl userDao) { this.userDao userDao; } }在注解和Java配置方式下/** * author pdai */ public class UserServiceImpl { /** * user dao impl. */ private UserDaoImpl userDao; /** * find user list. * * return user list */ public ListUser findUserList() { return this.userDao.findUserList(); } /** * set dao. * * param userDao user dao */ Autowired public void setUserDao(UserDaoImpl userDao) { this.userDao userDao; } }在Spring3.x刚推出的时候推荐使用注入的就是这种, 但是这种方式比较麻烦所以在Spring4.x版本中推荐构造函数注入。构造函数在XML配置方式中constructor-arg是通过构造函数参数注入比如下面的xml:?xml version1.0 encodingUTF-8? beans xmlnshttp://www.springframework.org/schema/beans xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd !-- services -- bean iduserService classtech.pdai.springframework.service.UserServiceImpl constructor-arg nameuserDao refuserDao/ !-- additional collaborators and configuration for this bean go here -- /bean !-- more bean definitions for services go here -- /beans本质上是new UserServiceImpl(userDao)创建对象, 所以对应的service类是这样的/** * author pdai */ public class UserServiceImpl { /** * user dao impl. */ private final UserDaoImpl userDao; /** * init. * param userDaoImpl user dao impl */ public UserServiceImpl(UserDaoImpl userDaoImpl) { this.userDao userDaoImpl; } /** * find user list. * * return user list */ public ListUser findUserList() { return this.userDao.findUserList(); } }在注解和Java配置方式下/** * author pdai */ Service public class UserServiceImpl { /** * user dao impl. */ private final UserDaoImpl userDao; /** * init. * param userDaoImpl user dao impl */ Autowired // 这里Autowired也可以省略 public UserServiceImpl(final UserDaoImpl userDaoImpl) { this.userDao userDaoImpl; } /** * find user list. * * return user list */ public ListUser findUserList() { return this.userDao.findUserList(); } }在Spring4.x版本中推荐的注入方式就是这种 具体原因看后续章节。注解注入以Autowired自动注入注解注入为例修饰符有三个属性ConstructorbyTypebyName。默认按照byType注入。constructor通过构造方法进行自动注入spring会匹配与构造方法参数类型一致的bean进行注入如果有一个多参数的构造方法一个只有一个参数的构造方法在容器中查找到多个匹配多参数构造方法的bean那么spring会优先将bean注入到多参数的构造方法中。byName被注入bean的id名必须与set方法后半截匹配并且id名称的第一个单词首字母必须小写这一点与手动set注入有点不同。byType查找所有的set方法将符合符合参数类型的bean注入。比如/** * author pdai */ Service public class UserServiceImpl { /** * user dao impl. */ Autowired private UserDaoImpl userDao; /** * find user list. * * return user list */ public ListUser findUserList() { return userDao.findUserList(); } }IoC和DI使用问题小结这里总结下实际开发中会遇到的一些问题为什么推荐构造器注入方式先来看看Spring在文档里怎么说The Spring team generally advocates constructor injection as it enables one to implement application components as immutable objects and to ensure that required dependencies are not null. Furthermore constructor-injected components are always returned to client (calling) code in a fully initialized state.简单的翻译一下这个构造器注入的方式能够保证注入的组件不可变并且确保需要的依赖不为空。此外构造器注入的依赖总是能够在返回客户端组件代码的时候保证完全初始化的状态。下面来简单的解释一下依赖不可变其实说的就是final关键字。依赖不为空省去了我们对其检查当要实例化UserServiceImpl的时候由于自己实现了有参数的构造函数所以不会调用默认构造函数那么就需要Spring容器传入所需要的参数所以就两种情况1、有该类型的参数-传入OK 。2无该类型的参数-报错。完全初始化的状态这个可以跟上面的依赖不为空结合起来向构造器传参之前要确保注入的内容不为空那么肯定要调用依赖组件的构造方法完成实例化。而在Java类加载实例化的过程中构造方法是最后一步之前如果有父类先初始化父类然后自己的成员变量最后才是构造方法所以返回来的都是初始化之后的状态。所以通常是这样的/** * author pdai */ Service public class UserServiceImpl { /** * user dao impl. */ private final UserDaoImpl userDao; /** * init. * param userDaoImpl user dao impl */ public UserServiceImpl(final UserDaoImpl userDaoImpl) { this.userDao userDaoImpl; } }如果使用setter注入缺点显而易见对于IOC容器以外的环境除了使用反射来提供它需要的依赖之外无法复用该实现类。而且将一直是个潜在的隐患因为你不调用将一直无法发现NPE的存在。// 这里只是模拟一下正常来说我们只会暴露接口给客户端不会暴露实现。 UserServiceImpl userService new UserServiceImpl(); userService.findUserList(); // - NullPointerException, 潜在的隐患循环依赖的问题使用field注入可能会导致循环依赖即A里面注入BB里面又注入Apublic class A { Autowired private B b; } public class B { Autowired private A a; }如果使用构造器注入在spring项目启动的时候就会抛出BeanCurrentlyInCreationExceptionRequested bean is currently in creation: Is there an unresolvable circular reference从而提醒你避免循环依赖如果是field注入的话启动的时候不会报错在使用那个bean的时候才会报错。我在使用构造器注入方式时注入了太多的类导致Bad Smell怎么办比如当你一个Controller中注入了太多的Service类Sonar会给你提示相关告警对于这个问题说明你的类当中有太多的责任那么你要好好想一想是不是自己违反了类的单一性职责原则从而导致有这么多的依赖要注入。pdai 想起来一句话所有困难问题的解决方式都在另外一个层次Autowired和Resource以及Inject等注解注入有何区别Autowired和Resource以及Inject等注解注入有何区别 这时平时在开发中或者常见的面试题。AutowiredAutowired注解源码在Spring 2.5 引入了 Autowired 注解Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE}) Retention(RetentionPolicy.RUNTIME) Documented public interface Autowired { boolean required() default true; }从Autowired注解源码上看可以使用在下面这些地方Target(ElementType.CONSTRUCTOR) #构造函数 Target(ElementType.METHOD) #方法 Target(ElementType.PARAMETER) #方法参数 Target(ElementType.FIELD) #字段、枚举的常量 Target(ElementType.ANNOTATION_TYPE) #注解还有一个value属性默认是true。简单总结1、Autowired是Spring自带的注解通过AutowiredAnnotationBeanPostProcessor 类实现的依赖注入2、Autowired可以作用在CONSTRUCTOR、METHOD、PARAMETER、FIELD、ANNOTATION_TYPE3、Autowired默认是根据类型byType 进行自动装配的4、如果有多个类型一样的Bean候选者需要指定按照名称byName 进行装配则需要配合Qualifier。指定名称后如果Spring IOC容器中没有对应的组件bean抛出NoSuchBeanDefinitionException。也可以将Autowired中required配置为false如果配置为false之后当没有找到相应bean的时候系统不会抛异常简单使用代码在字段属性上。Autowired private HelloDao helloDao;或者private HelloDao helloDao; public HelloDao getHelloDao() { return helloDao; } Autowired public void setHelloDao(HelloDao helloDao) { this.helloDao helloDao; }或者private HelloDao helloDao; //Autowired public HelloServiceImpl(Autowired HelloDao helloDao) { this.helloDao helloDao; } // 构造器注入也可不写Autowired也可以注入成功。将Autowired写在被注入的成员变量上setter或者构造器上就不用再xml文件中配置了。如果有多个类型一样的Bean候选者则默认根据设定的属性名称进行获取。如 HelloDao 在Spring中有 helloWorldDao 和 helloDao 两个Bean候选者。Autowired private HelloDao helloDao;首先根据类型获取发现多个HelloDao然后根据helloDao进行获取如果要获取限定的其中一个候选者结合Qualifier进行注入。Autowired Qualifier(helloWorldDao) private HelloDao helloDao;注入名称为helloWorldDao 的Bean组件。Qualifier(XXX) 中的 XX是 Bean 的名称所以 Autowired 和 Qualifier 结合使用时自动注入的策略就从 byType 转变成 byName 了。多个类型一样的Bean候选者也可以Primary进行使用设置首选的组件也就是默认优先使用哪一个。注意使用Qualifier 时候如何设置的指定名称的Bean不存在则会抛出异常如果防止抛出异常可以使用Qualifier(xxxxyyyy) Autowired(required false) private HelloDao helloDao;在SpringBoot中也可以使用BeanAutowired进行组件注入将Autowired加到参数上其实也可以省略。Bean public Person getPerson(Autowired Car car){ return new Person(); } // Autowired 其实也可以省略ResourceResource注解源码Target({TYPE, FIELD, METHOD}) Retention(RUNTIME) public interface Resource { String name() default ; // 其他省略 }从Resource注解源码上看可以使用在下面这些地方Target(ElementType.TYPE) #接口、类、枚举、注解 Target(ElementType.FIELD) #字段、枚举的常量 Target(ElementType.METHOD) #方法name 指定注入指定名称的组件。简单总结1、Resource是JSR250规范的实现在javax.annotation包下2、Resource可以作用TYPE、FIELD、METHOD上3、Resource是默认根据属性名称进行自动装配的如果有多个类型一样的Bean候选者则可以通过name进行指定进行注入简单使用代码Component public class SuperMan { Resource private Car car; }按照属性名称 car 注入容器中的组件。如果容器中BMW还有BYD两种类型组件。指定加入BMW。如下代码Component public class SuperMan { Resource(name BMW) private Car car; }name 的作用类似 QualifierInjectInject注解源码Target({ METHOD, CONSTRUCTOR, FIELD }) Retention(RUNTIME) Documented public interface Inject {}从Inject注解源码上看可以使用在下面这些地方Target(ElementType.CONSTRUCTOR) #构造函数 Target(ElementType.METHOD) #方法 Target(ElementType.FIELD) #字段、枚举的常量简单总结1、Inject是JSR330 (Dependency Injection for Java)中的规范需要导入javax.inject.Inject jar包 才能实现注入2、Inject可以作用CONSTRUCTOR、METHOD、FIELD上3、Inject是根据类型进行自动装配的如果需要按名称进行装配则需要配合Named简单使用代码Inject private Car car;指定加入BMW组件。Inject Named(BMW) private Car car;Named 的作用类似 Qualifier总结1、Autowired是Spring自带的Resource是JSR250规范实现的Inject是JSR330规范实现的2、Autowired、Inject用法基本一样不同的是Inject没有required属性3、Autowired、Inject是默认按照类型匹配的Resource是按照名称匹配的4、Autowired如果需要按照名称匹配需要和Qualifier一起使用Inject和Named一起使用Resource则通过name进行指定