什么是Spring?
Spring 是一个包含众多工具方法的框架,它大大开发了项目开发的效率。从本质上来讲,Spring 本质上是一个 IoC 容器,它负责创建、管理和销毁对象。
传统的应用程序中,对象的创建和生命周期管理由应用程序自身负责,这使得代码耦合度高,难以维护。Spring 通过 IOC 将这些职责从应用程序中分离出来,由容器统一管理。
我们可以联想一下生活中的容器,都是用来存放东西的,比如水杯,是用来装水的,Spring容器亦是如此,它里面存放了众多对象,当程序员需要使用时,只需要从里面去取出来就可以。这样就无需手动去创建对象,大大提高了开发效率。
一、什么是IoC?
IoC是 Inversion of Control 的单词缩写,中文意思是:控制反转。什么意思呢?在传统的程序开发中,对象的创建和销毁由应用程序负责,而将对象存放在Sping IoC容器之后,对象的创建和销毁的控制权就从应用程序交到了Spring IoC容器手中,这个就是控制反转的意思:控制权得到了反转。
我们前面的文章中,其实就已经涉及到了IoC的使用,只是我们还不知道,比如,我们在类上添加了@RestController或者@Controller注解,意思就是将该类交给IoC容器管理,此时这个类的实例化对象就存放在了容器之中。
不知你是否会有这样一个疑问,在下面这个代码中,我们明明没有实例化一个Controller对象,为什么却可以在使用URL路径访问:user/v1 时调用到这个对象的v1方法?在一般的情况下,我们要想调用一个类的成员方法,前提都是建立在创建出这个类的实例化对象,然后调用,为什么这里却没有实例化Controller对象,却可以调用里面的成员方法?
其实是因为,当你使用@RestController 注解Controller类时,此时Spring就会自动创建该类的一个实例对象,存放在IoC容器中。
在通过URL路径访问这个路径时,服务器会从IoC容器中寻找Controller类的实例对象,然后调用里面的v1方法。
二、什么的DI?
DI 是英文 Dependency Injection 的缩写,中文意思是:依赖注入。DI 是实现 IoC 的一种具体方式 ,是指在运行时,将对象所依赖的其他对象通过某种方式(如构造函数、方法参数、属性等)注入到该对象中,从而使对象能够正常工作。
比如,我们现在需要造一辆车,一辆车依赖一个合适的车身,因此我们需要注入一个车身;而一个车身又依赖一个合适的底盘,因此我们需要注入一个底盘;而一个底盘又依赖四个合适尺寸的轮胎,因此我们需要注入四个轮胎,这个注入的过程,就是DI。
class Tire{//轮胎
int size;
public Tire (int size){
this.size=size;
System.out.println("Tire init……");
}
}
class Bottom{//底盘
Tire tire;
public Bottom(Tire tire){
this.tire=tire;
System.out.println("Bottom init……");
}
}
class Framework{//车身
Bottom bottom;
public Framework(Bottom bottom){
this.bottom=bottom;
System.out.println("Framework init……");
}
}
public class Car {//汽车
Framework framework;
public Car (Framework framework){
this.framework=framework;
System.out.println("Car init……");
}
}
class Main{
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);//创建汽车时,注入车身
}
}
三、IoC详解
(一) 什么是Bean?
Spring IoC既然是一个容器,那么它就包含容器的两大基本功能:存和取
Spring IoC 容器管理的主要是对象,这些对象,我们统称为" Bean " 。我们把这些对象存放在容器中,由Spring 负责对象的创建和销毁,应用程序只需要负责告诉Spring,这些对象哪些需要存,哪里需要使用容器中的对象即可。下面我举一个Spring IoC 存取对象的一个例子。
代码解析:
(1)当启动类启动时,Spring会自动扫描启动类所在目录及其子目录下面的所有注解,当它扫描到UseInfo类被@Component 注解时,会将这个类创建一个实例化对象,存放在IoC容器中,等待被使用
(2)我们将启动了中的 SpringApplication.run 的返回值用 ApplicationContext 类型的变量contex来接收,我们可以把context想象成管理容器的管家,通过它我们可以获取到存放在容器中的对象。
(3)最后,我们通过 context.getBean(UserInfo.class) 获取到UserInfo类型的对象,调用UserInfo对象的say()方法,这就是IoC容器的的存取的整个过程。
通过getBean()获取对象的方式有点过于繁琐,那么这里我们介绍另外一种获取对象的方式,使用@Autowired注解
@Component//实例化一个UserInfo对象存放中容器中
public class UserInfo {
private String name;
private Integer age;
public void say(){
System.out.println("UserInfo say……");
}
}
@RestController
@RequestMapping("/user")
class User{
@Autowired//从容器中取出UserInfo对象,赋值给userInfo
private UserInfo userInfo;
@RequestMapping("/say")
public void say(){
System.out.println("调用UserInfo的say()方法");
userInfo.say();
}
}
接下来我们通过Postman访问, 然后看看IDE控制台的效果:
可以看到,当我们使用 Postman 访问 http:127.0.0.1:8080/user/say 时,会调用User类中的say方法。然后say方法再去调用UserInfo对象的say方法。
可以看到,整个过程,我们都没有给UserInfo实例化对象,但是却可以调用其方法,这是因为,@Autowired注解 private UserInfo userInfo时,也就是告诉容器:帮我在容器中找出一个UserInfo对象,赋值给userInfo。
(二) Bean的命名规则
我们都知道,在我们实例化对象的时候,都会给这个对象取一个名称,那么IoC容器里面既然存放的是对象,那么每个对象理所当然也都有自己的名称,那么它究竟是如何来命名的呢?
默认情况下,如果在使用@Component注解时,创建对象的默认的名称是:将类名的名称首字符改为小写,比如UserInfo默认的名称是userInfo。
UserInfo --> userInfo
StudentMarjar --> studentMarjar
MyName --> myName
特殊情况:如果类名的前两个字符都是大写,那么对象名和类名保持一致,假如存放USerInfo类,它默认的对象名称是USerInfo,和类名保持一致。
MClass --> MClass
YName --> YName
MFather --> MFather
我们可以通过类型或者名称获取Bean,因此,获取Bean的方式分为了三种:
(三)Bean的存储
在之前的入门案例中,要把某个对象交给IOC容器管理,需要在类上添加⼀个注解: 而Spring框架为了更好的服务web应用程序,提供了更丰富的注解。
共有两类注解类型可以实现:
类注解:@Controller、@Service、@Repository、@Component、@Configuration。
方法注解:@Bean。
接下来让我们分别来看看它们是如何存储对象的。
1、类注解:
@Controller(控制器存储)
存放对象代码:
@Controller//将对象交给Spring IoC容器管理
public class UserController {
public void say(){
System.out.println("UserController say……");
}
}
@Service(服务存储)
存放对象代码:
@Service//将对象交给Spring IoC容器管理
public class UserService {
public void say(){
System.out.println("UserService say……");
}
}
@Repository(仓库存储)
存放对象代码:
@Repository//将对象交给Spring IoC容器管理
public class UserRepository {
public void say(){
System.out.println("UserRepository say……");
}
}
@Component(组件存储)
存放对象代码:
@Component//将对象交给Spring IoC容器管理
public class UserComponent {
public void say(){
System.out.println("UserComponent say……");
}
}
@Configuration(配置存储)
存放对象代码:
@Configuration//将对象交给Spring IoC容器管理
public class UserConfiguration {
public void say(){
System.out.println("UserConfiguration say……");
}
}
接下来,我们在启动类中统一获取刚才存放在Spring IoC容器中的对象:
取出对象代码:
程序运行结果:
为什么要有这门多类注解?
通过上面注解的学习,我们发现,上述注解的功能好像都是一致的,即将类的实例对象交给IoC容器管理。那么还有必要将它们划分成为不同的名称吗?这背后其实和应用分层有关!
什么是应用分层呢?一个公司,需要有不同的部门,分别去负责抓专门的工作;一个学校,需要有不同的老师,去负责不同的学科,一个项目也是如此,它也需要根据某种规范,将里面的文件划分为不同的层次,比如控制层、业务逻辑层等……
@Controller:控制层,接收请求,对请求进行处理,并进行响应.
@Service:业务逻辑层,处理具体的业务逻辑.
@Repository:数据访问层,也称为持久层。负责数据访问操作
@Configuration:配置层。处理项目中的一些配置信息.
通过分层,不同的层次使用不同的注解,这样程序员在看到注解之后,也就明白了该类的用途。比如,当看到一个类被@Controller注解时,我们就知道该类是用来接收请求,并对请求进行处理做出响应的。
类注解之间的关系:
当我们按下ctrl然后点击上述注解时,我们会发现,@Controller、@Service、@Repository和@Configuration注解里面,都包含有@Component注解,因此,@Component是其他四个注解的元注解,其他四个注解是@Component的衍生注解,它们都共同用于@Component注解的功能。
2、方法注解:@Bean
@Bean注解的使用
虽然类注解可以帮助我们将实例对象存放在IoC容器中管理,但是如果我们要存放多个实例对象在容器中,使用类注解好像就无法实现了,因为类注解只能定义在类上,但是一个类只能定义一次,不能重复定义。
因此,我们使用方法注解,帮助我们在容器中存放多个相同类型的对象!接下来,让我们看看@Bean注解的简单使用。
首先,我们先定义一个Student类:
public class Student {
private String name;
private Integer age;
//生成Getter和Setter方法和toString方法
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
使用@Bean注解代码:
注意:当使用@Bean注解方法时,创建的对象名称默认是方法名,意思就是,此时@Bean注解的作用是创建一个Student类的对象交给容器管理,对象名称是student1。
获取Student对象代码:
程序运行结果:
@Bean定义多个对象
当使用@Bean注解多个方法,存放多个相同类型的Student对象,此时我们在获取对象的时候,该如何获取呢?
(1)存放两个Student对象在Spring容器中,根据@Bean注解的对象命名规则:两个对象的名称应该是方法名:student1和student2
(2)从容器中获取Student对象
可以看到,程序报错,它的意思是:
没有符合条件的类型为 “com.example.test2.student.Student” 的 Bean:期望找到单个匹配的 Bean,但找到了 2 个:student1,student2 。
(3) 因为我们前面在容器中存放了两个Student对象,此时如果按照类型取出Student对象,Spring不知道该取出来哪一个,这种情况,我们可以根据对象名称获取对象。
3、@Bean重命名
如果我们想自己指定对象的名称,就可以使用@Bean重命名。
四、DI详解
上面我们讲解了控制反转IoC的细节,接下来呢,我们学习依赖注入 DI 的细节。依赖注入是⼀个过程,是指 IoC 容器在创建 Bean 时,去提供运行时所依赖的资源,而资源指的就是对象。 在上面程序案例中,我们使用了 @Autowired 这个注解,完成了依赖注入的操作。简单来说,就是把对象取出来放到某个类的属性中。
关于依赖注入,Spring 也给我们提供了三种方式:
1、属性注入 (Field Injection)
2、构造方法注入 (Constructor Injection)
3、Setter 注入 (Setter Injection)
(一)属性注入
属性注入就是通过@Autowired注解实现的,可以从IoC容器中获取对象,注入到属性中。
属性注入代码: (注入UserService对象,调用其say()方法)
启动类代码:
程序运行结果:
(二)构造方法注入
构造方法注入,顾名思义,就是在类的构造方法中实现依赖注入。
构造方法注入代码:
启动类代码:
程序运行结果:
在使用构造方法注入时,Spring在创建UserComponent对象时,也要通过调用其构造方法创建对象,那么它在调用带有参数的构造方法时,Spring是如何给构造方法传递参数的呢?
其实,Spirng在调用带有参数的构造方法时,会从IoC容器中查询相同类型的对象,查询到之后,会将该对象作为构造方法的参数。
比如,在这个例子中,Spring在创建UserComponent对象时,会调用其构造方法(需要UserService类型的参数),此时,Spring就从容器中去查询UserService类型的对象,查到之后,作为构造方法的参数。
接下来让我们通过打印的方式验证一下:
已知Spring创建UserComponent对象时,需要调用其带有UserService类型参数的构造方法,以实现依赖注入,接下来我们分别从IoC容器中获取UserComponent和UserService对象,先打印UserService对象的值,再打印UserComponent对象中的UserService属性。
程序运行:
发现打印出来的userService内容一致,说明它们是同一个对象,因此证明:Spring在调用有参构造方法时,会从容器中获取合适的对象作为参数!
我们还有一个疑问:当这个类中包含多个构造方法时,Spring会调用哪个构造方法呢?
让我们在这个类中,创建两个构造方法:一个带有参数,一个不带参数,看看Spring会默认调用哪个构造方法!为方便观察,我们在构造方法中,写入了打印的语句!
启动类代码:
程序运行结果:
我们发现,Spring默认调用了无参构造方法。那么,有没有什么方法,可以自定义Spring默认调用的构造方法呢?我们可以使用@Autowired注解!
(三)Setter注入
Setter 注⼊和属性的Setter⽅法实现类似,只不过在设置set⽅法的时候需要加上@Autowired 注解。
Setter方法注入代码:
启动类代码:
程序运行:
(四)@Autowired注解存在的问题
当同⼀类型存在多个bean时,使用@Autowired会存在问题
可以看到,当IoC容器中存放两个相同类型(Student)的对象时,此时用@Autowired注解实现属性注入会报错,报错的原因是:Bean类型不是唯一的。
如何解决上述问题呢?Spring 提供了以下几种解决方案:
@Primary
@Qualifier
@Resource
1、使用 @Primary 注解:当存在多个相同类型的 Bean 注入时,加上 @Primary 注解,来确定默认的实现。
启动类代码:为方便观察,我们打印一下Student对象,看看属性注入的是哪个对象(STUDENT1还是STUDENT2)
让我们使用@Primary注解之后,再运行一下程序,此时程序报错了,显示出现了依赖循环,这是为什么呢?
这可能是因为,我们在启动这个类的时候,Spring开始将对象存放在容器中,或者将对象从容器中取出放在指定的位置中。
如果我们将存放Student对象和取出Student对象写在同一个类代码中,很可能出现这样一种情况,在Student对象还没有存放进容器时,就有指令要取出Student对象了,此时就出现了循环依赖。因为,为避免这种情况,我们将它们分开写在不同的类代码中。
代码修改:将@Bean注解的方法挪到Student类中。
Student类:
@Component//@Bean注解要和类注解搭配使用!!!
public class Student {
private String name;
private Integer age;
@Primary//指定默认选择的对象为STUDENT1
@Bean(name="STUDENT1")//给存放在容器的对象取名称为STUDENT1
public Student student1(){
Student student=new Student();
student.setName("zhangSan");
student.setAge(18);
return student;
}
@Bean(name="STUDENT2")//给存放在容器的对象取名称为STUDENT2
public Student student2(){
Student student=new Student();
student.setName("wangWu");
student.setAge(19);
return student;
}
//生成Getter和Setter方法和toString方法
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
StudentController类:
@Component//@Bean注解要和类注解配合使用,否则会报错
public class StudentController {
@Autowired
public Student student;//使用public修饰
}
启动程序,看看程序运行结果:
此时,程序运行成功,可以看到,属性注入,默认注入的是STUDENT1对象
2、使用 @Qualifier 注解:指定当前要注入的 bean 对象。在 @Qualifier 的 value 属性中,指定注入的 bean 的名称。
注意:在使用之前,记得把前面的@Primary注解给注释掉!!!
可以看到,当使用@Qualifier注解默认取出STUDENT2对象后,程序获取到的就是STUDENT2对象!
3、使用@Resource注解:是按照bean的名称进行注⼊。通过name属性指定要注⼊的bean的名称
可以看到,当使用@Qualifier注解默认取出STUDENT1对象后,程序获取到的就是STUDENT1对象!