前面我们大概知道了什么是 Spring,以及 Spring 家族中 Spring Boot 和 Spring MVC的开发,但是 Spring 到底是什么呢?
前面我为大家简单介绍了什么是 Spring 【Spring】什么是Spring,不过前面的介绍较为简单,要想知道Spring 的原理,这些知识不不足以帮助我们了解 Spring 的,所以这篇文章我将详细为大家介绍什么是 Spring。
通过前面的学习,我们知道了 Spring 是一个开源的框架,它让我们的开发变得更加简单,它支持广泛的应用场景,有着活跃而庞大的社区,这也是 Spring 能够经久不衰的原因。
但是这个概念对于我们来说,还是太抽象了,用一句话概括:Spring 是包含了众多工具的 IoC 容器。那么什么是 IoC 容器呢?
容器是指能够容纳某种物品的装置。在生活中,储物箱、垃圾桶、冰箱等这些都属于容器,而在计算机中,我们前面学习的List/map就是数据存储的容器,Tomcat就是Web容器。
IoC 是 Spring 的核心思想。
IoC是Inversion of Control的缩写,多数书籍翻译成“控制反转”。1996年,Michael Mattson在一篇有关探讨面向对象框架的文章中,首先提出了IoC这个概念。对于面向对象设计及编程的基本思想,简单来说就是把复杂系统分解成相互合作的对象,这些对象类通过封装以后,内部实现对外部是透明的,从而降低了解决问题的复杂度,而且可以灵活地被重用和扩展。
在传统的程序设计中,对象的创建和管理都是由代码直接完成的。而在IoC中,对象的创建和管理权交给了IoC Service Provider(IoC思想的具体实现),我们只需要告诉它需要什么对象,它就会为我们准备好。这种机制的引入,使得应用程序的各个部分之间的依赖关系变得非常清晰,并且可以将各个部分解耦,提高代码的可重用性和可维护性。
给大家举个例子,传统的汽车开发过程是这样的:
用代码体现就是这样的:
public class NewCarExample { public static void main(String[] args) { Car car = new Car(); car.run(); } /** * 汽车对象 */ static class Car { private Framework framework; public Car() { framework = new Framework(); System.out.println("Car init..."); } public void run() { System.out.println("Car run..."); } } /** * 车身类 */ static class Framework { private Bottom bottom; public Framework() { bottom = new Bottom(); System.out.println("Framework init..."); } } /** * 底盘类 */ static class Bottom { private Tire tire; public Bottom() { tire = new Tire(); System.out.println("Bottom init..."); } } /** * 轮胎类 */ static class Tire { private int size; public Tire() { this.size = 17; System.out.println("轮胎尺寸:" + size); } } }
如果我们在造车的时候,需要造车的一方指定轮胎大小的话,那么这个生产车的代码进行较大的改动。
可以看到,当需要造车方指定轮胎的大小的时候,基本上所有的零件的代码都需要做出更改,这就叫做 高耦合
什么叫做高内聚、低耦合呢?
相比大家经常会听到高内聚、低耦合这句话吧,那么它们到底代表的什么意思呢?
“高内聚、低耦合”是软件工程中的概念,是判断软件设计好坏的标准,主要用于程序的面向对象的设计,主要看类的内聚性是否高,耦合度是否低。
上面我们设计的代码的耦合程度就比较高,那么应该如何降低耦合度呢?
我们可以将各个零件之间的依赖关系给改变一下。
我们先根据需要,创造出指定大小的轮胎,然后将造好的轮胎给底盘创造厂,然后再造好底盘,将造好的底盘交给车身制造厂,制造出车身,最后将造好的车身交给汽车制造厂,最终制造出来一个汽车。
public class NewCarExample { public static void main(String[] args) { Tire tire = new Tire(20); Bottom bottom = new Bottom(tire); Framework framework = new Framework(bottom); Car car = new Car(framework); car.run(); } /** * 汽车对象 */ static class Car { private Framework framework; public Car(Framework framework) { this.framework = framework; System.out.println("Car init..."); } public void run() { System.out.println("Car run..."); } } /** * 车身类 */ static class Framework { private Bottom bottom; public Framework(Bottom bottom) { this.bottom = bottom; System.out.println("Framework init..."); } } /** * 底盘类 */ static class Bottom { private Tire tire; public Bottom(Tire tire) { this.tire = tire; System.out.println("Bottom init..."); } } /** * 轮胎类 */ static class Tire { private int size; public Tire(int size) { this.size = size; System.out.println("轮胎尺寸:" + size); } } }
通过更改各个类之间的依赖关系,那么就算底层轮胎如何变化,也不会影响整个产业链,这样就实现了代码之间的解耦,从而实现了更加灵活、通用的程序设计了。
通过上面的优化,我们发现:类的创建顺序是相反的,之前是 Car 控制并创建了 Framework,Framework 创建并控制创建了 Bottom,Bottom 创建并控制创建了 Tire,改进之后的控制权发生了反转,不再是使用方对象创建并控制依赖对象了,而是把依赖对象注入到当前对象中,依赖对象的控制权不再由当前类控制了。
这样,即使依赖对象发生任何变化,当前类都是不受影响的,这就是典型的控制反转,也就是是 IoC 的实现思想。
知道了什么是容器以及什么是 IoC 之后我们就知道了什么叫做 IoC 容器了。
IoC 容器的优点:
通过上面的案例我们可以看出来,使用 IoC 容器,资源不再由使用资源的双方管理,而是由不使用资源的第三方进行管理,这样可以带来以下好处:1. 实现资源的集中统一管理;2. 降低了使用资源的双方的依赖程度,也就是耦合程度。
DI(Dependency Injection)即依赖注入,是面向对象编程中的一种设计模式,用来减少代码之间的耦合度。
具体来说,依赖注入将对象的创建和管理权从代码中转移到了外部容器,通过外部容器来创建对象并注入需要的依赖。这种方式可以降低代码的耦合度,提高代码的可重用性和可维护性。
在Java中,Spring框架是使用依赖注入最广泛的开源框架之一。通过使用依赖注入,Spring可以将应用程序中的各个组件解耦,使得它们之间的依赖关系变得更加清晰和易于管理。
容器在运行期间,动态的为应用程序提供运行时所依赖的资源,称之为依赖注入。
从这点来看,依赖注⼊(DI)和控制反转(IoC)是从不同的⻆度的描述的同⼀件事情,就是指通过引⼊ IoC 容器,利⽤依赖关系注⼊的⽅式,实现对象之间的解耦。
在造汽车的过程中,将 Tire 这个依赖注入到 Bottom 中造出 Bottom,然后将造好的 Bottom 依赖注入到 Framework 中造出 Framework,最后将造好的 Framework 依赖注入到 Car 中,最终创建出 Car。
IoC 是⼀种思想,也是"⽬标",⽽思想只是⼀种指导原则,最终还是要有可⾏的落地⽅案,⽽ DI 就属于具体的实现。所以也可以说, DI 是 IoC 的⼀种实现。
Spring 既然是一个 IoC 容器,那么他肯定具有两个基本的功能:存和取。
Spring 容器管理的主要是对象,这些对象我们称之为“Bean”,这个跟我们前面学习的 Bean 不一样。我们把这些 Bean 交给 Spring 进行管理,由 Spring 来负责对象的创建和销毁,我们在写 Spring 代码的时候只需要告诉 Spring,哪些对象是我们要交给 Spring 管理,我们又要取出哪些对象进行使用。
那么在 Spring 中,如何存储和取出 Bean 呢?
package com.example.springiocdi20231209; import org.springframework.stereotype.Component; @Component public class UserComponent { public void sayHi() { System.out.println("hello spring"); } }
package com.example.springiocdi20231209; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/component") public class GetMessage { @Autowired private UserComponent userComponent; @RequestMapping("/get") public void get() { userComponent.sayHi(); } }
这里显示出了我们想要的结果,就说明我们使用 @Componnet 注解和 @Autowired 注解对 Bean 实现了存储和取出。
需要注意的是:当我们在使用 @Autowired 注解的时候,需要保证这个类有 Controller 或者 RestController 注解,因为我们既然要想使用 Spring 的 IoC 容器肯定要保证这个类是被 Spring 管理的。
上面为大家展示了 IoC 和 DI 的基本使用,接下来将为大家详细的讲解一下 IoC。
上面我们存储 Bean 使用的是 @Component 注解,而 Spring 框架为了更好的服务 Web 应用程序,提供了更丰富的注解。
package com.example.springiocdi20231209.Controller; import org.springframework.stereotype.Controller; @Controller public class UserController { public void sayHi() { System.out.println("hi, spring"); } }
使用 @Controller 就将这个 Bean 给存储到 IoC 容器中了,那么我们如何获取这个 Bean 呢?
获取 Bean 的方法有很多种,我们只要介绍下面的第1、2、4种。
我们可以根据 Bean 的名字来获取到指定的 Bean,但是某个 Bean 的名称是什么,我们该怎么知道呢?我们来看看官方的解释。
简单来讲就是当类名中前两个字母中大写字母小于2的时候,那么该类交给 IoC 后就会以第一个字母小写的小驼峰形式命名,当类名的前两个字母都为大写字母的时候,那么该 Bean 名就是原类名。
所以要以 Bean 名获取到 UserController 这个 Bean 的话,就需要将 userController 作为参数。
package com.example.springiocdi20231209; import com.example.springiocdi20231209.Controller.UserController; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.ApplicationContext; @SpringBootApplication public class SpringIoCDi20231209Application { public static void main(String[] args) { ApplicationContext context = SpringApplication.run(SpringIoCDi20231209Application.class, args); UserController userController = (UserController) context.getBean("userController"); } }
我们这个代码是在 项目名称+Application 这个类中写的,准确来说是在有 @SpringBootApplication 这个注解的类中写的,并且 SpringApplication.run() 方法是可以有返回值也可以没有返回值的,我们可以根据需要使用变量来接收这个方法的返回值。要想获取到 IoC 容器中的 Bean,需要依靠 ApplicationContext 这个类,所以我们就用这个类的变量来接收 run 方法的返回值。
启动项目的时候,就会发现我们预想中的结果出现在了控制台中,并且这个不需要我们发送什么 Http 请求,而是启动项目就会自动执行这个类当中的代码。并且通过 Bean 名获取到的 Bean 名返回的是一个 Object 类型,所以在拿变量进行接收的时候就需要进行类型的转换。
可以通过 Bean 类型来获取到 Bean。
package com.example.springiocdi20231209; import com.example.springiocdi20231209.Controller.UserController; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.ApplicationContext; @SpringBootApplication public class SpringIoCDi20231209Application { public static void main(String[] args) { ApplicationContext context = SpringApplication.run(SpringIoCDi20231209Application.class, args); //1. 根据Bean名字来获取Bean //UserController userController = (UserController) context.getBean("userController"); //2. 根据Bean类型来获取Bean UserController userController = context.getBean(UserController.class); userController.sayHi(); } }
通过 Bean 名获取 Bean 需要进行类型的转换,可以在传递参数的时候就指定返回值的类型。
package com.example.springiocdi20231209; import com.example.springiocdi20231209.Controller.UserController; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.ApplicationContext; @SpringBootApplication public class SpringIoCDi20231209Application { public static void main(String[] args) { ApplicationContext context = SpringApplication.run(SpringIoCDi20231209Application.class, args); //1. 根据Bean名字来获取Bean //UserController userController = (UserController) context.getBean("userController"); //2. 根据Bean类型来获取Bean //UserController userController = context.getBean(UserController.class); //3. 根据Bean名和Bean类型获取Bean UserController userController = context.getBean("userController", UserController.class); userController.sayHi(); } }
通过这个注解,也可以将类交给 Spring 进行管理。
package com.example.springiocdi20231209.Controller; import org.springframework.stereotype.Controller; import org.springframework.stereotype.Service; @Service public class UserController { public void sayHi() { System.out.println("hi, spring"); } }
package com.example.springiocdi20231209.Controller; import org.springframework.stereotype.Controller; import org.springframework.stereotype.Repository; import org.springframework.stereotype.Service; @Repository public class UserController { public void sayHi() { System.out.println("hi, spring"); } }
package com.example.springiocdi20231209.Controller; import org.springframework.stereotype.Component; import org.springframework.stereotype.Controller; import org.springframework.stereotype.Repository; import org.springframework.stereotype.Service; @Component public class UserController { public void sayHi() { System.out.println("hi, spring"); } }
package com.example.springiocdi20231209.Controller; import org.springframework.context.annotation.Configuration; import org.springframework.stereotype.Component; import org.springframework.stereotype.Controller; import org.springframework.stereotype.Repository; import org.springframework.stereotype.Service; @Configuration public class UserController { public void sayHi() { System.out.println("hi, spring"); } }
这个也是和咱们前⾯讲的应⽤分层是呼应的.让程序员看到类注解之后,就能直接了解当前类的⽤途。
并且通过观察这五个注解的源码我们可以发现一些问题。
查看 @Controller / @Service / @Repository / @Configuration 等注解的源码发现:
其实这些注解⾥⾯都有⼀个注解 @Component ,说明它们本⾝就是属于@Component 的"⼦类".@Component 是⼀个元注解,也就是说可以注解其他类注解,如 @Controller , @Service ,@Repository 等.这些注解被称为 @Component 的衍⽣注解.
@Controller @Service 和 @Repository ⽤于更具体的⽤例(分别在控制层,业务逻辑层,持久化层),在开发过程中,如果你要在业务逻辑层使⽤ @Component 或@Service,显然@Service是更好的选择。
类注解是写在我们项目代码中的类上的,但是存在两个问题:
上面两个问题是无法使用类注解来解决的。所以也就出现了方法注解 @Bean
假设我们这里的 User 类是一个外部包里的类,那么我们就无法在这个类中添加类注解,这是就需要使用到方法注解。
package com.example.springiocdi20231209; import org.springframework.context.annotation.Bean; public class BeanConfig { @Bean public User user() { User user = new User(); user.setName("zhangsan"); user.setAge(18); return user; } }
运行 @SpringApplication 注解的代码,看看什么效果。
这里报错说这个 Bean 没有被定义。其实使用方法注解 @Bean 的时候,需要保证该方法所在的类也有被类注解注释。
package com.example.springiocdi20231209; import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component; @Component public class BeanConfig { @Bean public User user() { User user = new User(); user.setName("zhangsan"); user.setAge(18); return user; } }
package com.example.springiocdi20231209; import com.example.springiocdi20231209.Controller.UserController; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.ApplicationContext; @SpringBootApplication public class SpringIoCDi20231209Application { public static void main(String[] args) { ApplicationContext context = SpringApplication.run(SpringIoCDi20231209Application.class, args); User user = context.getBean(User.class); System.out.println(user); } }
同一个类定义多个对象。
package com.example.springiocdi20231209; import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component; @Component public class BeanConfig { @Bean public User user() { User user = new User(); user.setName("zhangsan"); user.setAge(18); return user; } @Bean public User user2() { User user = new User(); user.setName("liis"); user.setAge(20); return user; } }
当我们使用方法注解,并且一个类有多个相同类型的 Bean 类型的时候,并且我们通过 Bean 类型获取 Bean 的话就会出错。
所以这里获取 Bean 的话就需要指定 Bean 名称。
User user = context.getBean("user2", User.class);
@Bean 注解的 Bean,Bean 的,名称就是方法名。
前面我们呢说了 Bean 的默认名称,但其实我们可以指定 Bean 的名称。那么如何重命名 Bean 呢?
@Bean("beanName")
package com.example.springiocdi20231209; import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component; @Component public class BeanConfig { @Bean("u1") public User user() { User user = new User(); user.setName("zhangsan"); user.setAge(18); return user; } @Bean("u2") public User user2() { User user = new User(); user.setName("liis"); user.setAge(20); return user; } }
ApplicationContext context = SpringApplication.run(SpringIoCDi20231209Application.class, args); User user = context.getBean("u2", User.class); System.out.println(user);
可以看到我们通过重命名的名字u2获取到了Bean。
不仅如此,通过观察 @Bean 的源码我们可以发现,这里的name参数是一个字符串数组,也就是说一个 Bean 可以有多个名字。
@Bean({"u2", "s2"})
同样的类注解也可以重命名,但是类注解只支持一个名字。
@Configuration("c1") public class UserController { public void sayHi() { System.out.println("hi, spring"); } }
其实并不是项目下的所有文件中的加了注解的类都会被 Spring 进行管理,而是需要看扫描路径在哪。假设我们将 @SpringBootApplication 注解所在的类给换个路径。
@SpringBootApplication public class SpringIoCDi20231209Application { public static void main(String[] args) { ApplicationContext context = SpringApplication.run(SpringIoCDi20231209Application.class, args); User user = context.getBean("u1", User.class); System.out.println(user); } }
这里就报错说找不到 u1 这个 Bean,说明这个注解没有被扫描到,那么为什么呢?
这其实跟 @ComponentScan 注解配置的扫描路径有关,但是我们 SpringBootApplication 注解的类当中不是没有这个注解吗?其实这个注解继承了 @ComponentScan 注解。
而如果 @ComponnetScan 没有配置的话,就默认的是当前 @ComponentScan 注解的文件所在的路径。
这里 @SpringBootApplication 注解的类所在的路径是这个 package com.example.springiocdi20231209.springiocdi20231209.Controller;
而我们的 u1 Bean 所在的路径是 package com.example.springiocdi20231209.springiocdi20231209;,所以这个 Bean 是 Component 无法扫描到的。
要想扫描到这个路径,我们可以对 @ComponentScan 注解进行配置。
@ComponentScan({"com.example.springiocdi20231209.springiocdi20231209"}) @SpringBootApplication public class SpringIoCDi20231209Application { public static void main(String[] args) { ApplicationContext context = SpringApplication.run(SpringIoCDi20231209Application.class, args); User user = context.getBean("u1", User.class); System.out.println(user); } }
@ComponentScan 也是可以配置多个扫描路径的。
依赖注⼊是⼀个过程,是指IoC容器在创建Bean时,去提供运⾏时所依赖的资源,⽽资源指的就是对象。在上⾯程序案例中,我们使⽤了 @Autowired 这个注解,完成了依赖注⼊的操作。简单来说,就是把对象取出来放到某个类的属性中。
在⼀些⽂章中,依赖注⼊也被称之为"对象注⼊",“属性装配”,具体含义需要结合⽂章的上下⽂来理解。
关于依赖注入,Spring 为我们提供了三种方法:
属性注⼊是使⽤ @Autowired 实现的。
package com.example.springiocdi2; import org.springframework.stereotype.Service; @Service public class UserService { public void sayHi() { System.out.println("Hi UserService"); } }
package com.example.springiocdi2; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; @Controller public class UserController { //属性注入 @Autowired private UserService userService; public void sayHi() { System.out.println("Hi UserController..."); userService.sayHi(); } }
package com.example.springiocdi2; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.ApplicationContext; @SpringBootApplication public class SpringIoCDi2Application { public static void main(String[] args) { ApplicationContext context = SpringApplication.run(SpringIoCDi2Application.class, args); UserController userController = context.getBean("userController", UserController.class); userController.sayHi(); } }
运行结果:
构造⽅法注⼊是在类的构造⽅法中实现注⼊。
package com.example.springiocdi2; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; @Controller public class UserController2 { //构造方法注入 private UserService userService; @Autowired public UserController2(UserService userService) { this.userService = userService; } public void sayHi() { System.out.println("Hi UserController2"); userService.sayHi(); } }
注意事项:如果类只有⼀个构造⽅法,那么 @Autowired 注解可以省略;如果类中有多个构造⽅法,
那么需要添加上 @Autowired 来明确指定到底使⽤哪个构造⽅法。
Setter 注⼊和属性的 Setter ⽅法实现类似,只不过在设置 set ⽅法的时候需要加上 @Autowired 注
解。
package com.example.springiocdi2; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; @Controller public class UserController3 { //setter方法注入 private UserService userService; @Autowired public void setUserService(UserService userService) { this.userService = userService; } public void sayHi() { System.out.println("Hi UserController3"); userService.sayHi(); } }
如果没加@Autowired 注解,就会报错。
属性注入,注入一个final修饰的属性:
构造方法注入,注入一个final修饰的属性:
Setter注入,注入一个final修饰的属性:
为什么有些注入不能注入 final 修饰的属性?
如果一个属性被final关键字修饰,那么这个属性就成为了一个常量,它的值就不能被改变。而依赖注入的属性注入需要动态地修改属性的值,所以不能对被final关键字修饰的属性进行依赖注入。但是,在构造方法中,final属性可以被赋值。这是因为构造方法是在对象创建时执行的,此时final属性还没有被赋值。因此,在构造方法中可以对final属性进行赋值操作。
当同一个类类型存在多个 Bean 时,就会出现问题。
package com.example.springiocdi2; import lombok.Data; @Data public class User { private String name; private Integer age; }
package com.example.springiocdi2; import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component; @Component public class BeanConfig { @Bean("u1") public User user1() { User user = new User(); user.setName("zhangsan"); user.setAge(17); return user; } @Bean("u2") public User user2() { User user = new User(); user.setName("lisi"); user.setAge(18); return user; } }
package com.example.springiocdi2; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component public class UserController4 { @Autowired private User user; public void sayHi() { System.out.println("Hi UserController4"); System.out.println(user); } }
如何解决这个一个类型有多个 bean 的问题呢?Spring 提供了以下的几种方案:
使⽤@Primary注解:当存在多个相同类型的Bean注⼊时,加上@Primary注解,来确定默认的实现。
@Bean("u1") @Primary //指定该bean为默认实现 public User user1() { User user = new User(); user.setName("zhangsan"); user.setAge(17); return user; }
使⽤@Qualifier注解:指定当前要注⼊的bean对象。在@Qualifier的value属性中,指定注⼊的bean
的名称。
@Qualifier("u2") @Autowired private User user;
使⽤@Resource注解:是按照bean的名称进⾏注⼊。通过name属性指定要注⼊的bean的名称。
@Resource(name = "u2") private User user;
常见面试题:
@Autowird 与 @Resource的区别: