一、IoC 控制反转
IoC: Inversion of Control (控制反转), 也就是说 Spring 是⼀个"控制反转"的容器.什么是控制反转呢? 也就是控制权反转. 什么的控制权发⽣了反转? 获得依赖对象的过程被反转了也就是说, 当需要某个对象时, 传统开发模式中需要⾃⼰通过 new 创建对象, 现在不需要再进⾏创 建, 把创建对象的任务交给容器, 程序中只需要依赖注⼊ (Dependency Injection,DI)就可以了.这个容器称为:IoC容器. Spring是⼀个IoC容器, 所以有时Spring 也称为Spring 容器.
二、案例 - 造⼀辆⻋
造⼀辆⻋我们的实现思路是这样的:先设计轮⼦(Tire),然后根据轮⼦的⼤⼩设计底盘(Bottom),接着根据底盘设计⻋⾝(Framework),最 后根据⻋⾝设计好整个汽⻋(Car)。这⾥就出现了⼀个"依赖"关系:汽⻋依赖⻋⾝,⻋⾝依赖底盘,底 盘依赖轮⼦.![]()
2.1 传统方式 new 创建对象
package com.ameris.iocdemo;
public class Main {
public static void main(String[] args) {
//创建车
Car car = new Car();
//让车跑起来
car.run();
}
}
package com.ameris.iocdemo;
public class Car {
//车要有车身,依赖于车身
private Framework framework;
//车初始化
public Car() {
//车子初始化的时候,就要有车身
this.framework = new Framework();
System.out.println("car init..");
}
public void run() {
System.out.println("car run..");
}
}
package com.ameris.iocdemo;
public class Framework {
//车身依赖于 底盘
private Bottom bottom;
//那么在初始化车身的时候 底盘就要创建好
public Framework() {
this.bottom = new Bottom();
System.out.println("framework init..");
}
}
package com.ameris.iocdemo;
public class Bottom {
//底盘依赖于 轮胎
private Tire tire;
public Bottom() {
this.tire = new Tire();
System.out.println("bottom init..");
}
}
package com.ameris.iocdemo;
public class Tire {
private int size = 17;
public Tire() {
System.out.println("tire init..(size:"+size+")");
}
}
运行结果:
2.2 用代码模拟 IoC 思想
假设,轮胎的尺寸,不满足我的需求,需要修改,就会产生一系列的问题
一个代码改动,接下来其他的代码也要改,耦合性太高了
2.2.1 IoC 控制反转 的思想
相当于,一个制车厂造车,需要车身,底盘、车轮等。厂商不自己造零件,而是外包给别人,他不管你是怎么制造这些零件的,只要提供给厂商就行, 厂商来组装成他要的东西。
而Spring完成的就是这里
package com.ameris.iocdemo.v2;
public class Main {
public static void main(String[] args) {
Tire tire = new Tire(17,"blue");
Bottom bottom = new Bottom(tire);
Framework framework = new Framework(bottom);
Car car = new Car(framework);
car.run();
}
}
package com.ameris.iocdemo.v2;
public class Car {
//依赖于车身
private Framework framework;
//传统方法是 new
//而 IoC 的方法是,直接拿 对象
public Car(Framework framework) {
this.framework = framework;
System.out.println("car init...");
}
public void run() {
System.out.println("car run...");
}
}
package com.ameris.iocdemo.v2;
public class Framework {
private Bottom bottom;
public Framework(Bottom bottom) {
this.bottom = bottom;
System.out.println("framework init...");
}
}
package com.ameris.iocdemo.v2;
public class Bottom {
private Tire tire;
public Bottom(Tire tire) {
this.tire = tire;
System.out.println("bottom init...");
}
}
package com.ameris.iocdemo.v2;
public class Tire {
private int size;
private String color;
public Tire(int size,String color) {
this.size = size;
System.out.print("tire init...");
System.out.print(" size:" + size);
System.out.print(" color:" + color);
System.out.println();
}
}
在在tire这个类里加上了color属性,但是并不用去修改其他依赖于 tire 的类,只需要在main方法里加一个参数就好了。
相比传统的方式,在方法内部不停的new对象。
IoC思想,直接去要对象,在方法传参的时候就拿到这个对象,直接使用。
耦合性低了,即使做各种修改,也不会特别影响到其他的代码。
三、DI 依赖注入
3.1 属性注入
spring IoC/DI
对象的管理:
1.存对象 (把对象交给spring管理) @Component 等 五大注解和一个方法注解
2.取对象(依赖注入) @Autowired
拿之前写的 书架系统做案例
修改一下
修改完发现,可以正常获取到书架的书籍
从这个例子中能看出来,IoC的好处
第⼀,资源集 中管理,实现资源的可配置和易管理。第⼆,降低了使⽤资源双⽅的依赖程度,也就是我们说的耦合 度。
3.2 构造方法注入
3.2.1 没有写构造函数,spring会自动使用Java提供的无参的构造函数
package com.ameris.iocdemo.controller; import com.ameris.iocdemo.config.UserConfig; import com.ameris.iocdemo.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; @Controller public class UserController { //无属性,没有写构造方法 //自动生成无参构造方法 public void sayHi(){ System.out.println("hello,UserController..."); } }
所以运行spring时没有出错
3.2.2 写了一个构造函数 ,spring 就会使用当前这个写好的构造函数
当写了一个构造函数时,spring 就会使用当前这个写好的构造函数,
package com.ameris.iocdemo.controller; import com.ameris.iocdemo.config.UserConfig; import com.ameris.iocdemo.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; @Controller public class UserController { private UserService userService; /** * 构造函数注入 */ public UserController(UserService userService) { this.userService = userService; } public void sayHi(){ System.out.println("hello,UserController..."); userService.sayHi(); } }
所以spring没报错
3.2.3 写了多个构造函数写,默认会使用无参的构造函数,需要使用@Autowired指定构造方法,否则会报错
//当写了多个构造函数时,写了多个构造函数,默认会使用无参的构造函数,如果没有这个无参构造函数,spring会报错 // spring 不知道使用哪一个构造函数,就会报错 package com.ameris.iocdemo.controller; import com.ameris.iocdemo.config.UserConfig; import com.ameris.iocdemo.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; @Controller public class UserController { private UserService userService; private UserConfig userConfig; /** * 构造函数注入 */ // //当写了多个构造函数时,写了多个构造函数,默认会使用无参的构造函数,如果没有这个无参构造函数,spring会报错 // // spring 不知道使用哪一个构造函数,就会报错 // // 手动写了其他构造函数之后,养成好习惯 把默认无参的构造函数也写上 ,避免其他组件报错 public UserController() { } public UserController(UserService userService) { this.userService = userService; } public UserController(UserService userService, UserConfig userConfig) { this.userService = userService; this.userConfig = userConfig; } public void sayHi(){ System.out.println("hello,UserController..."); userService.sayHi(); userConfig.sayHi(); } }
如果没有这个无参构造函数,spring会报错
加上 @Autowired 就好了
3.3 set方法注入
package com.ameris.iocdemo.controller; import com.ameris.iocdemo.config.UserConfig; import com.ameris.iocdemo.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; @Controller public class UserController { /** * set 方法注入 */ private UserService userService; @Autowired public void setUserService(UserService userService) { this.userService = userService; } public void sayHi(){ System.out.println("hello,UserController..."); userService.sayHi(); } }
3.4 三个注入的区别
可能上面的几个注入,给人感觉最简单的是 属性注入,但在spring中 属性注入不被推荐。特别是专业版的idea ,属性注入时,注解下面会有黄色的小波浪线(就是不推荐使用的意思)
但是其他注入方式就不会这样
实际上各个注入各有特点
属性注⼊◦ 优点: 简洁,使⽤⽅便;◦ 缺点:▪ 只能⽤于 IoC 容器,如果是⾮ IoC 容器不可⽤,并且只有在使⽤的时候才会出现 NPE(空指 针异常)▪ 不能注⼊⼀个Final修饰的属性构造函数注⼊(Spring 4.X推荐)◦ 优点:▪ 可以注⼊final修饰的属性▪ 注⼊的对象不会被修改▪ 依赖对象在使⽤前⼀定会被完全初始化,因为依赖是在类的构造⽅法中执⾏的,⽽构造⽅法 是在类加载阶段就会执⾏的⽅法.▪ 通⽤性好, 构造⽅法是JDK⽀持的, 所以更换任何框架,他都是适⽤的◦ 缺点:▪ 注⼊多个对象时, 代码会⽐较繁琐Setter注⼊(Spring 3.X推荐)◦ 优点: ⽅便在类实例之后, 重新对该对象进⾏配置或者注⼊◦ 缺点:▪ 不能注⼊⼀个Final修饰的属性▪ 注⼊对象可能会被改变, 因为setter⽅法可能会被多次调⽤, 就有被修改的⻛险.
3.4.1 属性注入 不能使用 final,但是构造函数可以
加上 final 之后,代码立马爆红
分析原因:
final 的特点:
1)被 final 关键字修饰,需要手动初始化。
手动创建这个对象的方法是 new 实例化一个对象,这就与 @Autowired 的本意 冲突了。
@Autowired 就是为了把交给spring管理的对象,取出来用(依赖注入),就是为了简便 new这个过程。不初始化,违背 关键字 final 的用法,初始化了和@Autowired 直接矛盾了。既然都初始化了,还 @Autowired 干嘛,多此一举。
那直接初始化成null 可以吗?语法上没问题,没有报错。但是我既然赋值给 null ,就等于没有创建 这个对象,逻辑上 和 @Autowired 还是冲突了,给这个注解要用这个对象,赋值null,等于没有这个对象了。
2)如果不初始化,在构造方法中进行赋值
3.4.2 构造方法注入的对象不会被修改,而setter方法注入的对象可能会被修改
原因很简单,构造方法是对象的初始化,从实例化对象之后就固定了。最重要的一点是,构造方法不可能被调用。
而setter方法,本身就是一个提供给别人调用的方法,对 对象的属性值 进行修改,所以 setter方法注入的对象 有被修改的风险
四、IoC的详细用法
五大注解: @Controller , @Service ,@Component ,@Repository ,@Configuration
方法注解: @Bean
从功能上说,除了 @Controller ,其他的效果都是一样的。因为@Controller除了具备让Spring 管理的功能外,还是接口的入口,必须为@Controller
而之所以还需要用不同的注解,是为了通过注解来区分代码,方便管理
程序的应用分层
4.1 @Controller (控制器存储)
首先写了一个UserController类,并加上了类注解 @Controller
把这个类 交给 spring 管理
再来看启动类 加上了@SpringBootApplication 这个注解
对启动类代码进行修改,观察spring 管理的类
package com.ameris.iocdemo; import com.ameris.iocdemo.controller.UserController; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.ApplicationContext; @SpringBootApplication //这个是spring的启动类注解 public class IocDemoApplication { public static void main(String[] args) { //.run方法就是执行spring,让它运行起来 //并且这个方法是由返回值的,这个返回值是 spring上下文 ApplicationContext context = SpringApplication.run(IocDemoApplication.class, args); //可以通过上下文拿到 spring 管理的对象 UserController bean = context.getBean(UserController.class);//拿到 UserController bean.sayHi();//使用UserController的方法 } }
使用getBean从spring容器中获取对象
这里是根据 类型 获取bean
打断点调试,发现执行到这里了
说明@Controller 真的有管理这个对象
4.2 @Service (服务存储)
这里通过 Bean 的名称去获取 Bean
成功执行代码
4.3 @Component (组件存储)
这里通过 Bean 的名字和对象的类型 来获取 Bean
4.4 @Repository(仓库存储)
4.5 @Configuration (配置存储)
4.6 @Bean 将方法中的对象交给spring进行管理
发现有问题:没有找到UserInfo这个类型的bean
奇怪,明明在方法前面加上了 @Bean
原因是:虽然方法加上了 @Bean ,但是这个方法存在在 BeanConfig 这个类中,而这个类没有加注释,也就是没有交给 spring 管理,所以spring 运行的时候,就没有管理整个代码。
也就不会注意到里面的方法。
于是修改代码,加上注解 @Configuration
重启spring项目,运行正常了
4.6.1 @Bean 注解是方法注解,并且 @Bean 需要与五大注解搭配使用才能生效。
另外:
注意spring 的扫描路径,是 启动类 所在的路径,注解 必须在扫描路径下 才能生效
如果spring启动类的位置变化了,到了其他目录下,那么即时加了五大注解,其他的类也可能扫描不到。
例如:当前的启动类 在 iocdemo整个目录下,默认这个扫描这个目录下所有文件
所以,启动类写的代码,能正常执行
如果把启动类移动了,如下:
移动到controller 这个包下
发现 spring只扫描了 controller这个目录的类,其他的都没扫描到
但是如果偏要放在这里,还想要能扫描到其他的类
可以在启动类上面加上@ComponentScan(""),指定spring的扫描路径
4.6.2 @Bean 注解定义的对象,默认名称为 方法名(无论方法名大小写)
一个类下,可能有多个 @Bean,就需要用 Bean名称+类型 去获取了,否则就会异常
报错
修改获取 Bean 的方式
获取成功
4.6.3 @Bean("")自定义 Bean的名称
还可以命名多个bean名称
都能正确获取到
五、@Autowired 存在的问题
1)@Autowired 先通过类型查找bean,没有获取到,抛异常。获取到,如果只有一个bean,正常响应。如果匹配到多个对象时,会查看是否配置了@Qualifier,再通过名称获取 Bean,获取到了就响应,否则报错。
userInfo里使用了两个 @Bean
在 usercontroller2 注入UserInfo,然后调用 UserInfo 的sayHi方法
可以发现,userInfo 代码下面标红了(说明存在问题)
其实在这里就已经提示了问题所在(有多个bean)
启动spring,发现启动就失败了
查看下面的描述:
这个UserInfo类型只需要一个 @Bean ,但是发现了两个 (验证了上面爆红的代码)
spring不知道要使用的是哪个对象
现在 通过修改变量名 为 bean的名字 来取对象
的确是正常取出来了
但是存在很大的隐患
通常:不使用变量名称 来指定 获取某个 bean,而是通过其他的手段来指定bean的名称
通常情况下,变量 的名称是随便取的,而在程序员看来变量名的修改不影响业务的逻辑处理。
在团队协作中,变量名很容易被认为只是一个普通的变量名。而实际上,如果这个变量名,是指的 bean。此时有人将变量名修改,整个项目都有可能无法正常启动。
解决方法:
1)@Primary
这个注解使用不多
在@Bean 注解注释的方法的位置,写上@Primary,代表 默认@Autowired 获取到的是 该方法下 产生这个对象
加上这个注解后,也没有报红线了
获取到了 userinfo2这个对象
2)@Qulifier ("Bean的名称")
常用
在@Autowired 取对象的(注入依赖)位置,注释 @Qulifier ,并在括号里写上要获取的Bean的名称
即使,被注入依赖的对象的类的中已经有其他方法 被注解了@Primary,但此时 依然以 @Qulifier 为主
3)@Resource (jdk提供)
常用
@Resource (name ="bean的名称")等于
@Qualifier("bean的名称") + @Autowired
2)@Autowired 和 @Resource 的区别
1)@Autowired 由spring框架提供
@Resource 由JDK框架提供
2)@Autowired 先通过名称获取 Bean
获取到了,正确响应;
没有获取到,根据类型匹配,如果匹配到多个对象时,@Autowired 取对象时,报错
@Resource 直接通过 名称获取。
3)@Autowired 没有参数设置
@Resource 有一些参数设置
(例如:常见参数name ,使用比较多的 @Resource (name ="bean的名称"))