上篇博客学习了控制反转IoC的细节,接下来就开始学习依赖注入DI的细节。
依赖注入是一个过程,是指IoC容器在创建Bean时,去提供运行时所依赖的资源,而资源指的就是对象。我们使用 @Autowired 注解,完成依赖注入的操作。
简单来说,就是把对象取出来,放到某个类的属性中。在一些文章中,依赖注入也被称之为 “对象注入”、“属性装配”,具体含义需要结合文章的上下文来理解。
关于依赖注入,Spring也给我们提供了三种方式:
1、属性注入(Field Injection)
2、构造方法注入(Construct Injection)
3、Setter 注入(Setter Injection)
一、属性注入
属性注入是使用 @Autowired 实现的,将UserService 类注入到UserController类中,UserService 类代码如下:
@Service public class UserService { public void doService() { System.out.println("do Service..."); } }
UserController类的实现代码如下:
@Controller public class UserController { //注入方法1:属性注入 @Autowired private UserService userService; public void sayHi() { userService.doService(); System.out.println("hi, UserController"); } }
启动类获取到UserController的doService方法,代码如下:
@SpringBootApplication public class SpringIoC2Application { public static void main(String[] args) { //获取Spring上下文对象 ApplicationContext context = SpringApplication.run(SpringIoC2Application.class, args); //从Spring上下文中获取对象 UserController userController = (UserController) context.getBean("userController"); //使用对象 userController.doService(); } }
执行结果:
如果去掉 注解@Autowired,再运行一下程序,结果如下:
报错了,提示空指针异常,没办法调用doService() 方法,因为userServer为空。原因就是没有加@Autowired注解,没有注入依赖,Spring拿不到这个属性,也自然不会给它初始化了,那么它肯定就是一个空指针了。
二、构造方法注入
构造方法注入就是在类的构造方法中实现注入,代码如下:
@Controller public class UserController { //注入方法2:构造方法注入 private UserService userService; @Autowired public UserController(UserService userService) { this.userService = userService; } public void doController() { userService.doService(); System.out.println("do UserController"); } }
注意事项:如果类只有一个构造方法,那么@Autowired注解可以省略;但如果类有多个构造方法,那么就需要添加上@Autowired来明确指定要使用哪个构造方法了。下面的代码练习:
@Controller public class UserController { //注入方法2:构造方法注入 private UserService userService; public UserController(){ } public UserController(UserService userService) { this.userService = userService; } public void doController() { userService.doService(); System.out.println("do UserController"); } }
加了个无参的构造函数,运行程序会报错,如图:
报错解释:空指针异常,虽然这个类交给Spring管理了,Spring内部也会给我们自动创建对象,但也是通过构造函数创建的,这里我们写了两个构造函数,如果不加@Autowired注解,Spring默认使用无参构造函数,自然也就没有成功创建对象了,那么这个userService也自然是空指针了。
现在代码改一下,把无参构造函数注释掉,再加一个构造函数,代码如下:
@Controller public class UserController { //注入方法2:构造方法注入 private UserService userService; private UserComponent userComponent; // public UserController(){ // // } public UserController(UserService userService) { this.userService = userService; } public UserController(UserService userService, UserComponent userComponent) { this.userService = userService; this.userComponent = userComponent; } public void doController() { userComponent.doComponent(); userService.doService(); System.out.println("do UserController"); } }
运行代码报错了,报错信息如下:
报错解释:没有找到默认的构造方法,因为这里出现了两个构造方法,Spring不知道该用哪个,现在给第二个构造方法加上@Autowired注解,表示让Spring使用被注解的构造方法,代码如下:
@Controller public class UserController { //注入方法2:构造方法注入 private UserService userService; private UserComponent userComponent; // public UserController(){ // // } public UserController(UserService userService) { this.userService = userService; } @Autowired public UserController(UserService userService, UserComponent userComponent) { this.userService = userService; this.userComponent = userComponent; } public void doController() { userComponent.doComponent(); userService.doService(); System.out.println("do UserController"); } }
运行结果:
程序跑起来了。
小结:构造函数的注入
1、只有一个构造函数的情况,可以不加@Autowired
2、如果有多个构造函数,需要指定默认的构造函数(通过@Autowired指定,如果未指定,默认使用无参的构造函数)。
构造规范:如果添加构造函数,把无参构造函数显示添加(也就是把构造函数写下来,再给注释掉)
三、Setter注入
Setter注入和属性的Setter方法实现类似,只不过在set方法的时候需要加上@Autowired注解,代码如下:
@Controller public class UserController { private UserService userService; @Autowired public void setUserService(UserService userService) { this.userService = userService; } public void doController() { userService.doService(); System.out.println("do UserController"); } }
启动类代码:
@SpringBootApplication public class SpringIoC3Application { public static void main(String[] args) { //拿到Spring上下文 ApplicationContext context = SpringApplication.run(SpringIoC3Application.class, args); //拿到Spring上下文对象 UserController userController = context.getBean(UserController.class); //使用对象 userController.doController(); } }
执行结果:
如果我们把@Autowired注解去掉呢?结果如下:
又是空指针异常,原因和属性注入把@Autowired的情况一样。
四、三种注入的优缺点分析(面试题)
1、属性注入
优点:
1、简洁、使用方便。
缺点:
1、只能用于IoC容器,如果非IoC容器则不可用,并且只有在使用的时候才会回出现NPE(空指针异常)。
2、不能注入一个final修饰的属性。
1 的解释:
可以看到,它的来源是Spring的,那么就只能用于IoC容器了。
2 的解释:
final修饰的属性有个特点,必须初始化,如果我们给属性加上final,会有两种解决方案:1、提供构造函数,如图:
如果加上构造方法,那不就是构造方法注入了吗,也就不是属性注入了,违背了初心。
2、给它初始化,new 一个对象,如图:
但是这样不就是我们自己管理我们的代码了吗,并不是Spring帮我们管理了,也违背了初心。
2、构造方法注入(Spring4.X推荐)
SpringBoot和Spring的版本号对应:
Spring Boot3.X -> Spring 6
Spring Boot2.X -> Spring 4
优点:
1、可以注入final修饰的属性。
2、注入的对象不会被修改。
3、依赖对象在使用前一定会被完全初始化,因为依赖是在类的构造方法中执行的,而构造方法是在类加载阶段就会执行的方法。
4、通用性好,构造方法是JDK支持的,所以更换任何框架,它都是适用的。
1 的解释:final修饰的属性要初始化,构造方法会进行初始化,复合它的要求。
2 的解释:因为构造方法在这个类创建的时候就会给它进行赋值了,不会再对它进行修改。
3 的解释:在类加载的这个阶段,就会执行构造方法了,把依赖对象给初始化完成。
4 的解释:因构造方法是JDK支持的,不再Spring框架也能正常的使用。
缺点:
1、注入多个对象时,代码会比较繁琐。
3、Setter注入(Spring3.X推荐)
优点:
1、方便在类实例之后,重新对该对象进行配置或注入。
1 的解释:因为Setter方法是可以被其他地方调用的,所以即使类已经被实例化了,也可以重新对该对象进行配置或注入。
缺点:
1、不能注入一个final修饰的属性。
2、注入对象可能会被改变,因为Setter方法可能会被多次调用,就有被修改的风险。
1 的解释:因为可能会被多次修改,所以不能使用final修饰。
2 的解释:Setter方法当然能被修改了,这即使它的优点,也是它的缺点。
4、Autowired查找依赖顺序:
1、根据名称和类型去查。
2、如果根据名称查不到,就会根据类型去查。
根据名称查不到了,去查找类型,这时如果类型匹配多个,就会报错。
五、@Autowired存在问题
当同一类型存在多个bean时,使用@Autowired会存在问题。代码如下:
@Component public class BeanConfig { @Bean public UserInfo userInfo1() { UserInfo user = new UserInfo(); user.setId(6); user.setName("zhangsan"); user.setAge(18); return user; } @Bean public UserInfo userInfo2() { UserInfo user = new UserInfo(); user.setId(7); user.setName("lisi"); user.setAge(19); return user; } }
此时我们在UserController类里面注入UserInfo类,代码如下:(下面这几个的注入方式都是错误的)
1、引入Autowired的错误代码:
(1)属性注入:
@Controller public class UserController { @Autowired private UserInfo userInfo; public void sayHi() { System.out.println("hello, controller"); } }
可以看到,还没运行代码,就报红线了,如图:
(2)构造方法注入:
@Controller public class UserController { private UserInfo userInfo; @Autowired public UserController(UserInfo userInfo) { this.userInfo = userInfo; } }
这个也是,还没运行代码,就报红线了,如图:
(3)Setter方法注入:
@Controller public class UserController { private UserInfo userInfo; @Autowired public void setUserInfo(UserInfo userInfo) { this.userInfo = userInfo; } public void sayHi() { System.out.println("hello, controller"); } }
可以看到,这个也报红线了,如图:
(4)启动类代码:
@SpringBootApplication public class SpringIoC3Application { public static void main(String[] args) { //拿到Spring上下文 ApplicationContext context = SpringApplication.run(SpringIoC3Application.class, args); //拿到Spring上下文对象 UserController bean = context.getBean(UserController.class); //使用对象 bean.sayHi(); } }
上面这三种方式注入都会出问题,启动程序后,而报错信息也都是一样的(有两个细微的差别,Field和Parameter,下面会介绍),如下图:
可以看到,程序还没启动就失败了,可以看出来,Spring的依赖注入是在项目启动前就开始注入的了。
上面报错提示:UserController类需要一个bean,但是有两个,下面也打印出是哪两个bean了,Action介绍的就是解决方案。报错日志非常详细。
报错信息说UserController类找不到依赖注入的是哪一个,因为有多个方法被@bean注释,而依赖注入时,三个方法的属性名都是userInfo,先根据userInfo名称查找,但BeanConfig类没有这个方法名,那就只能去根据类名找了,但有2个bean,Spring就不知道要找那个了,并不是@bean注释下的方法名(userInfo1 / userInfo2),Spring并不知道该注入哪个依赖,所以报错了。
在项目启动失败,可以把Filed看都属性,如图:这个是使用属性注入的,会有Field
构造方法和Setter注入都是第一张报错图,但就不是Field了,而是Parameter,但其实三种注入的报错原因都是一样的。
解决方案:使用属性注入时,就把属性名改成bean修饰的其中之一的方法名就好了;使用@Parimary注解;使用@Qualifier注解;使用@Resource注解。
3、解决方案
(1)解决方案一:根据名称去查
属性名改成bean修饰的其中之一的方法名
属性注入:
@Controller public class UserController { @Autowired private UserInfo userInfo2; public void sayHi() { System.out.println("hello, controller"); } }
构造方法注入:
@Controller public class UserController { private UserInfo userInfo; @Autowired public UserController(UserInfo userInfo2) { this.userInfo = userInfo2; } }
Setter方法注入:
@Controller public class UserController { private UserInfo userInfo; @Autowired public void setUserInfo(UserInfo userInfo2) { this.userInfo = userInfo2; } public void sayHi() { System.out.println("hello, controller"); } }
执行结果都一样,这里虽然注入了依赖,但没有用这些依赖,所以打印的都是:hello controller,如图:
(2)解决方案二:给想要拿到的对象加上@Primary注解
这个可以理解为依赖注入后,有多个依赖的方法,但默认使用加了@Primary的方法,代码如下:
UserController类:
@Controller public class UserController { @Autowired private UserInfo userInfo; public void sayHi() { System.out.println(userInfo.toString()); System.out.println("hello, controller"); } }
BeanConfig类:
@Component public class BeanConfig { @Primary @Bean public UserInfo userInfo1() { UserInfo user = new UserInfo(); user.setId(6); user.setName("zhangsan"); user.setAge(18); return user; } @Bean public UserInfo userInfo2() { UserInfo user = new UserInfo(); user.setId(7); user.setName("lisi"); user.setAge(19); return user; } }
启动类:
@SpringBootApplication public class SpringIoC3Application { public static void main(String[] args) { //拿到Spring上下文 ApplicationContext context = SpringApplication.run(SpringIoC3Application.class, args); //拿到Spring上下文对象 UserController bean = context.getBean(UserController.class); //使用对象 bean.sayHi(); } }
代码运行结果:
其他注入的方式,使用@Primary注解用法也一样,把想要拿到的对象加上@Primary注解就好了。
(3)解决方案三:加@Qualifier注解,指定引入的依赖对象
UserController类:
@Controller public class UserController { @Qualifier("userInfo2") @Autowired private UserInfo userInfo; public void sayHi() { System.out.println(userInfo.toString()); System.out.println("hello, controller"); } }
BeanConfig类:
@Component public class BeanConfig { @Bean public UserInfo userInfo1() { UserInfo user = new UserInfo(); user.setId(6); user.setName("zhangsan"); user.setAge(18); return user; } @Bean public UserInfo userInfo2() { UserInfo user = new UserInfo(); user.setId(7); user.setName("lisi"); user.setAge(19); return user; } }
启动类:
@SpringBootApplication public class SpringIoC3Application { public static void main(String[] args) { //拿到Spring上下文 ApplicationContext context = SpringApplication.run(SpringIoC3Application.class, args); //拿到Spring上下文对象 UserController bean = context.getBean(UserController.class); //使用对象 bean.sayHi(); } }
执行结果:
(4)解决方案四:加@Resource注解
这里和上面的三方案代码改变的只有UserController类,其他都不变,UserController类的代码如下:
@Controller public class UserController { @Resource(name = "userInfo2") @Autowired private UserInfo userInfo; public void sayHi() { System.out.println(userInfo.toString()); System.out.println("hello, controller"); } }
运行结果如下:
但是这个注解不是Spring的,而是JDK本身自带的,JDK早期也有这样的注解,所以Spring不推荐使用这个注解,因为不是自己开发的,如果出现啥问题,不好处理,是有着不可控因素的原因;还有使用自家的东西也肯定会更放心一些。
4、@Autowired 与 @Resource 的区别(常见面试题)
1、@Autowired 是 Spring框架提供的注解,而@Resource是JDK提供的注解。
2、@Autowired默认是按照类型注入,而@Resource是按照名称注入。相比于 @Autowired 来说,@Resource 支持更多的参数设置;例如 name 设置,根据名称获取 Bean。
六、Spring IoC&DI 的总结
1、Spring,Spring Boot 和 Spring MVC的关系以及区别
(1)Spring
简单来说,Spring是一个开发应用框架;那是什么样的框架呢?有这么几个标签:轻量级、一站式、模块化,其目的是用于简化企业级应用程序开发。
Spring的主要功能:管理对象,以及对象之间的依赖关系,面向切面编程、数据库事务管理、数据访问、web框架支撑等等。
但是Spring具备高度可开发性,并不强制依赖Spring,开发者可以自由选择Spring的部分或者全部,Spring可以无缝继承第三方框架,比如数据访问框架(Hibernate、JPA等等),web框架(如:Struts、JSF等等)。
(2)Spring MVC
Spring MVC是Spring的一个子框架,Spring诞生之后,大家觉得很好用,于是按照MVC模式设计了一个MVC框架(一些用Spring解耦的组件),主要用于开发WEB应用和网络接口,所以Spring MVC是一个Web框架。
Spring MVC基于Spring进行开发的,天生的与Spring框架集成。可以让我们更简洁的进行Web层开发,支持灵活的URL到页面控制器的映射,提供了强大的约定大于配置的契约式编程支持,非常容易与其他视图框架集成,如 Velocity、FreeMarker等等。
(3)Spring Boot
Spring Boot 是对Spring的一个封装,为了简化Spring一样的开发而出现的,中小型企业,没有成本研究自己的框架,使用Spring Boot 可以更加快速的搭建框架,降低开发成本,让开发人员更加专注于Spring应用的开发,而无需过多关注XML的配置和一些底层的实现。
Spring Boot是脚手架,插拔式搭建项目,可以快速的集成其他框架进来。
比如想使用Spring Boot开发Web项目,只需要引入Spring MVC框架即可,Web开发的工作是Spring MVC完成的,而不是Spring Boot,想完成数据访问,只需要引入Mybatis框架即可。
Spring Boot只是辅助简化项目开发的,让开发变得更加简单,甚至不需要额外的web服务器,直接生成jar包执行即可。
(4)总结
Spring MVC和Spring Boot都属于Spring,Spring MVC是基于Spring的一个MVC框架,而Spring Boot是基于Spring的一套快速开发整合包。
例如之前写的图书管理系统代码中,整体框架是通过SpringBoot搭建的IoC,DI功能是Spring提供的,而Web相关功能是Spring MVC提供的。
因为这三个专注的领域不同,所以解决的问题也不一样,总的来说,Spring就像一个大家族,有众多衍生产品,但它们的基础都是Spring,上面三者的关系如下图:
2、bean的命名
(1)五大注解存储的bean
1、类名前面两位字母均为大写,则bean名称为该类名本身(特殊情况)。
2、不是上面的特殊情况,其他的类名,首字母小写,驼峰形式命名。
3、通过 value 属性设置bean名 ,例如:@Controller (value = "user")。
(2)@Bean注解存储的bean
1、bean名称为方法名。
2、通过name属性设置bean名,例如:@Bean (name = {"u1", "user1"})。
3、常见面试题
(1)三种注入方式的优缺点(参考上面)
(2)常见注解有哪些,分别是什么作用?
web url映射:@RequestMapping
参数接收和接口响应:@RequestParam、@RequestBody、@ResponseBody
bean的存储:@Controller、@Service、@Repository、@Component、@Configuration、@Bean
bean的获取:@Autowired、@Qualifier、@Resource
(3)@Autowired 和 @Resource 的区别(参考上面)
(4)说下你对Spring、Spring MVC、Spring Boot的理解(参考上面)