一,Spring IoC&DI 的定义
我们知道Spring是一个开源框架,简单来说就是一个包含众多工具的IoC容器。容器都知道是用来装东西的,那么什么是IoC?IoC是Spring的核心思想之一,在上一篇SpringMVC的博客中也使用到了,即类的注解,比如@RestController,@Controller注解,它们就表示将这个对象交给Spring管理,Spring框架启动时就会加载该类,把对象交给Spring管理,就是IoC思想。
1.1 IoC——控制反转
IoC,全称 Inversion of Control(控制反转),就是将控制权反转,再进一步来说就是将获得依赖对象的过程反转。IoC思想的主要目的就是降低代码的耦合度。比如说,做一件事,要先做...,再做...,最后做,,,,每一个操作要依赖于前一个操作,而Spring IoC容器就是用来管理这些操作,使我们不必关心前一个操作是否完成,而是只关注于本身的实现。
举个例子,如果不使用IoC思想:
public class demo1 {
public static void main(String[] args) {
Car car = new Car();
}
static class Car{
private Framework framework;
public Car(){
framework = new Framework();
System.out.println("初始化car");
}
}
static class Framework{
private Bottom bottom;
public Framework(){
bottom = new Bottom();
System.out.println("初始化framework");
}
}
static class Bottom{
public Bottom(){
System.out.println("初始化bottom");
}
}
}
如果我们要改变bottom的大小,就需要把所有的代码都修改,即代码耦合度太高:
public class demo1 {
public static void main(String[] args) {
int size = 100;
Car car = new Car(size);
}
static class Car{
private Framework framework;
public Car(int size){
framework = new Framework(size);
System.out.println("初始化car");
}
}
static class Framework{
private Bottom bottom;
public Framework(int size){
bottom = new Bottom(size);
System.out.println("初始化framework");
}
}
static class Bottom{
public Bottom(int size){
System.out.println("初始化bottom");
}
}
}
1.2 DI——依赖注入
依赖注入就是实现控制反转的一种方式,我们不在每一个类中自己创建需要依赖的类,而是通过传递参数的形式来获得依赖的类(这里指自己实现,Spring中有专门的注解来帮我们实现,下面会讲),这种获得依赖的方式就称为DI(依赖注入),这样就实现了程序的解耦。
举个例子,将上述程序使用DI的方式实现:
public class demo1 {
public static void main(String[] args) {
int size = 100;
Bottom bottom = new Bottom(size);
Framework framework = new Framework(bottom);
Car car = new Car(framework);
}
static class Car{
private Framework framework;
public Car(Framework framework){
this.framework = framework;
System.out.println("初始化car");
}
}
static class Framework{
private Bottom bottom;
public Framework(Bottom bottom){
this.bottom = bottom;
System.out.println("初始化framework");
}
}
static class Bottom{
private int size;
public Bottom(int size){
System.out.println("初始化bottom");
}
}
}
这样写的话,之后底层类无论如何变化,整个调用链都不用做出任何改变,这样就实现了代码之间的解耦。
1.3 IoC容器的优势
- 原本代码中类的创建顺序是:Car -> Framework -> Bottom
- 使用DI后代码中类的创建顺序是:Bottom -> Framework -> Car
可以发现改进后的代码,它创建类的顺序是反的,也是我们之前说的控制权发生了反转,不再是使用法对象创建并控制依赖对象了,而是把依赖对象注入到当前对象中,依赖对象的控制权不再由当前类控制了,这样的话无论依赖类发生任何改变,当前类都是不受影响的,这就是为什么我们将其称之为"控制反转",这也是IoC的实现思想。
这里我们就彻底了解了上面所说的IoC思想,那么这里的IoC容器究竟有什么作用呢?
Bottom bottom = new Bottom(size);
Framework framework = new Framework(bottom);
Car car = new Car(framework);
上述代码就是IoC容器所做的工作,也就是说我们将需要使用的资源交给不使用资源的第三方管理,这可以带来很多好处:
- 实现了资源的集中管理:IoC容器会帮我们管理一些对象,当我们需要使用时,直接去IoC容器中取就行了
- 创建实例时不需要了解其中的细节,降低了耦合度
二,IoC 详解
既然Spring是一个IoC(控制反转)容器,作为容器,那么它就具备两个基本功能:存储,获取。Spring容器管理的主要是对象,这些对象,我们称之为"Bean",我们将这些对象交给Spring管理,我们程序只要告诉Spring,哪些存,以及如何从Spring中取出对象。
2.1 Bean的存储
要把某个对象交给IoC容器管理,需要在类或方法上添加一个注解:
- 类注解:@Controller、@Service、@Repository、@Component、@Configuration
- 方法注解:@Bean
接下来分别来看
2.1.1 @Controller(控制器存储)
@Controller//将对象存储到Spring容器中
public class UserController {
public void run(){
System.out.println("UserController running...");
}
}
为了观察这个对象是否存储到Spring中,我们需要从Spring中获取对象,代码如下:
@SpringBootApplication
public class JavaEeSpringIoCApplication {
public static void main(String[] args) {
//获取Spring上下文
ApplicationContext context = SpringApplication.run(JavaEeSpringIoCApplication.class, args);
//获取对象
UserController userController = context.getBean(UserController.class);
//使用对象
userController.run();
}
}
这里出现了一个Spring上下文,上下文就是指当前的运行环境,我们可以把它当作一个容器,容器中存储的就是当前的运行环境。
观察运行结果,发现成功从Spring中获取到UserController对象,并执行了它的run方法:
2.1.2 获取Bean的方式
上述代码是根据类型来查找对象,如果Spring容器中,同一个类存在多个Bean(即对象)的话,我们就需要通过其他方式获取,这里介绍三种常用的方法:
public interface BeanFactory {
//以上省略...
//根据bean的名称获取bean
Object getBean(String var1) throws BeansException;
//根据bean的名称和类型获取bean
<T> T getBean(String var1, Class<T> var2) throws BeansException;
//根据类型获取bean
<T> T getBean(Class<T> var1) throws BeansException;
//以下省略...
}
前两种方法都涉及到根据名称来获取对象,那么bean的名称是什么呢?Spring bean是Spring框架再运行时管理的对象,Spring会给管理的对象取一个名字,根据Bean的名称就可以获取到对应的对象。它的底层代码是这样的:
//如果name前两个字符是大写,那么直接返回name
//否则将name的第一个字符小写后,返回name
public static String decapitalize(String name) {
if (name == null || name.length() == 0) {
return name;
}
if (name.length() > 1 && Character.isUpperCase(name.charAt(1)) &&
Character.isUpperCase(name.charAt(0))){
return name;
}
char chars[] = name.toCharArray();
chars[0] = Character.toLowerCase(chars[0]);
return new String(chars);
}
演示一下上述三种方法:
@SpringBootApplication
public class JavaEeSpringIoCApplication {
public static void main(String[] args) {
//获取Spring上下文
ApplicationContext context = SpringApplication.run(JavaEeSpringIoCApplication.class, args);
//获取对象
UserController userController1 = (UserController) context.getBean("userController");
UserController userController2 = context.getBean("userController", UserController.class);
UserController userController3 = context.getBean(UserController.class);
//使用对象
System.out.println(userController1);
System.out.println(userController2);
System.out.println(userController3);
}
}
2.1.3 @Service(服务存储)
@Service
public class UserService {
public void run(){
System.out.println("userService run...");
}
}
获取该对象:
@SpringBootApplication
public class JavaEeSpringIoCApplication {
public static void main(String[] args) {
//获取Spring上下文
ApplicationContext context = SpringApplication.run(JavaEeSpringIoCApplication.class, args);
//获取对象
UserService userService = context.getBean(UserService.class);
//使用对象
userService.run();
}
}
2.1.4 @Repository(仓库存储)
@Repository
public class UserRepository {
public void run(){
System.out.println("UserRepository run...");
}
}
获取该对象:
@SpringBootApplication
public class JavaEeSpringIoCApplication {
public static void main(String[] args) {
//获取Spring上下文
ApplicationContext context = SpringApplication.run(JavaEeSpringIoCApplication.class, args);
//获取对象
UserRepository userRepository = context.getBean(UserRepository.class);
//使用对象
userRepository.run();
}
}
2.1.5 @Component(组件存储)
@Component
public class UserComponent {
public void run(){
System.out.println("UserComponent run...");
}
}
2.1.6 @Configuration(配置存储)
@Configuration
public class UserConfiguration {
public void run(){
System.out.println("UserConfiguration run...");
}
}
获取与上面的做法一样,这里就不做过多的赘述。
2.2 五大注释的区别与作用
之所以有这么多注释就是为了让程序员区分被该注释的类的用途:
- @Controller:控制层,接收请求,对请求进行处理,并进行响应
- @Service:业务逻辑层,处理具体的业务逻辑
- @Repository:数据访问层,也称为持久层,负责访问数据库的操作
- @Configuration:配置层,处理项目中的一些配置
通过源码来看一下类注解之间的关系:
//@Component源码
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Indexed
public @interface Component {
String value() default "";
}
//@Repository源码
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Repository {
@AliasFor(
annotation = Component.class
)
String value() default "";
}
//@Configuration源码
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Configuration {
@AliasFor(
annotation = Component.class
)
String value() default "";
boolean proxyBeanMethods() default true;
}
//@Controller源码
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Controller {
@AliasFor(
annotation = Component.class
)
String value() default "";
}
//@Service源码
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Service {
@AliasFor(
annotation = Component.class
)
String value() default "";
}
可以发现@Controller / @Service / @Repository / @Configuration这些注解中都有一个@Component注解,说明它们本身就属于@Component的子类,@Component 是⼀个元注解,也就是说可以注解其他类注解,如 @Controller , @Service , @Repository 等. 这些注解被称为 @Component 的衍⽣注解,@Component也是Spring能够自动识别并管理类的实例的原因。
下图可以看出它们之间的关系
2.3 方法注释@Bean
为什么还需要有一个方法注释?
- 如果要使用第三方库中的类,就没法自主添加类注释
- 一个类可能会创建多个对象,使用类注释无法实现
2.3.1 定义一个对象
@Component
public class UserBean {
@Bean
public User user(){
User user = new User(18,"zhangsan");
return user;
}
}
注意:@Bean注解必须搭配类注解一起使用!!!,不然不会存储到Spring容器中
2.3.2 定义多个对象
@Component
public class UserBean {
@Bean
public User user1(){
User user = new User(18,"zhangsan");
return user;
}
@Bean
public User user2(){
User user = new User(10,"lisi");
return user;
}
}
当一个类出现多个对象时,我们就不能使用类来获取对象,不然会出现下面的报错:
这时就必须要通过它们的名字来获取对象,注意@Bean注释定义的对象,默认名称就是方法名,不需要额外去判断修改,我们可以通过名称获取:
当然我们也可以通过name属性来重命名:
@Component
public class UserBean {
@Bean({"UserInfo"})//可以取多个名称
public User user(){
User user = new User(18,"zhangsan");
return user;
}
}
2.4 扫描路径
想要将对象交给Spring管理,不仅需要添加类注解,方法注解,还需要让这些类在Spring的扫描路径下。
这时我们可以通过@ComponentScan()注解来配置Spring的扫描路径:
注:{}中可以配置多个路径
三,DI详解
在Spring中,我们可以使用@Autowired注解来完成依赖注入的操作,Spring提供了三种方式:
- 属性注入
- 构造方法注入
- Setter注入
3.1 属性注入
注入多个对象时,每个对象都要添加@Autowired注解
@Controller
public class UserController {
@Autowired
private UserService userService;
@Autowired
private UserConfiguration userConfiguration
public void run(){
userService.run();
System.out.println("UserController running...");
}
}
3.2 构造方法注入
当只有一个构造方法,@Autowired注解可以不加
@Controller
public class UserController {
private UserService userService;
@Autowired
public UserController(UserService userService){
this.userService = userService;
}
public void run(){
userService.run();
System.out.println("UserController running...");
}
}
当有多个构造方法,如果有@Autowired注解,那么就会调用有注解的那个;如果没有@Autowired注解,那么就会默认调用无参的构造方法。
@Controller
public class UserController {
private UserService userService;
private UserRepository userRepository;
public UserController(){}
public UserController(UserService userService){
this.userService = userService;
}
@Autowired
public UserController(UserService userService, UserRepository userRepository){
this.userService = userService;
this.userRepository = userRepository;
}
public void run(){
userService.run();
userRepository.run();
System.out.println("UserController running...");
}
}
3.3 Setter注入
@Controller(value = "u1")//可以通过value属性重命名
public class UserController {
private UserService userService;
private UserRepository userRepository;
@Autowired
public void setUserService(UserService userService){
this.userService = userService;
}
@Autowired
public void setUserRepository(UserRepository userRepository){
this.userRepository = userRepository;
}
public void run(){
userService.run();
userRepository.run();
System.out.println("UserController running...");
}
}
3.4 @Autowired存在的问题
@Autowired注解会先根据类来查找需要注入的对象,如果该类有多个对象,就会根据名称来选择注入的对象,但是不推荐这种用法(因为在公司中一个项目通常是由多个人完成,默认变量名的修改不会影响业务的实现):
为了避免上述做法,Spring提供了以下几种解决方法:@Primarily,@Qualifier,@Resource
- @Primarily:可以在其中一个bean上添加
@Primary
注解,以指示Spring容器在存在多个候选者时应该优先考虑哪一个bean。如果有且仅有一个bean被标记为@Primary
,则Spring将使用该bean进行注入。 @Component public class UserBean { @Bean @Primary public User user1(){ User user = new User(18,"zhangsan"); return user; } @Bean public User user2(){ User user = new User(10,"lisi"); return user; } }
- @Qualifier:如果
@Primary
不足以解决问题(例如,如果有多个@Primary
标记的bean,或者想基于不同的条件注入不同的bean),可以使用@Qualifier
注解来明确指定要注入的bean的名称。@Qualifier
可以与@Autowired
一起使用,或者与@Bean
方法一起使用在配置类中 @Repository public class UserRepository { @Autowired @Qualifier("user1") private User user; public void run(){ System.out.println("UserRepository run..."); } }
@Resource:是按照bean的名称进⾏注⼊,通过name属性指定要注⼊的bean的名称。
@Repository public class UserRepository { @Resource(name = "user1") private User user; public void run(){ System.out.println(user); System.out.println("UserRepository run..."); } }
四,常见面试题
4.1 三种注入方式的优缺点
1.属性注入
- 优点:简洁方便
- 缺点:只能在IoC容器中使用;不能注入一个final修饰的属性
2.构造方法注入:
- 优点:可以注入final修饰的属性;通用性好,是JDK支持的,可以任用于其他框架;注入对象不会被修改;依赖对象在使⽤前⼀定会被完全初始化,因为构造方法在类加载阶段就会执行
- 缺点:代码比较繁琐
3.Setter注入:
- 优点:方便在类实例之后, 重新对该对象进行配置或者注入
- 缺点:不能注入一个Final修饰的属性;注入对象可能会被改变, 因为setter方法可能会被多次调用, 就有被修改的风险
4.2 @Autowired与@Resource的区别
- @Autowired 是spring框架提供的注解,⽽@Resource是JDK提供的注解
- @Autowired 默认是按照类型注入,⽽@Resource是按照名称注入. 相比于 @Autowired 来说,@Resource 支持更多的参数设置,例如 name 设置,根据名称获取 Bean。
4.3 ApplicationContext与BeanFactory
BeanFactory 提供了基础的访问容器的能力,ApplicationContext是BeanFactory的子类,继承了BeanFactory的所有功能之外,还有独特的功能,比如:支持国际化,支持资源访问。 ApplicationContext是一次性加载并初始化所有Bean对象,而BeanFactory是用到哪个才去加载哪个,所以BeanFactory比ApplicationContext更加轻量。(空间换时间)