在Spring/Spring-Boot的入门上我绕了很多弯路,我绕过的一个典型的弯路是:
找一个Spring-Boot
的入门案例,下载代码跟着跑了一遍,跑通之后确不能理解代码,尤其是各种配置文件以及注解让我难以理解这个程序究竟是怎么跑起来的。@Autowired
, @Controller
这些注解到底是什么意思,它的工作流程是怎样的? 我怀疑自己是注解相关的知识没有学好,于是往下挖注解的工作原理-->动态代理机制-->反射-->类加载过程-->类加载器的工作原理。这样一套走下来之后,即使我懵懵懂懂明白了类加载器、反射、动态代理、注解, 我还是不能看懂spring的这些注解是怎么工作的,也不知道怎么去实际使用他们。
很多的入门教程,给出了很多具体的操作,但是并没有告诉我们为什么spring要用这个注解,这个注解用了之后他会有什么作用,以及这些注解的工作流程(我指的是使用流程不是底层的实现原理)。
下面是我自己的一些学习反思。
首先我们从最基本的需求讲起。
java是面向对象的语言,每个对象都可以绑定一定的操作,通过将多个对象组合,我们可以完成一个任务所需要的所有操作。
举个例子,假设我们的任务是接收用户对 localhost:8080/user
这个链接的get请求,返回给用户浏览器一个含有用户数据的json数据串 “{"name":"bob"}”。我们把这个任务分成到多个对象去完成:数据访问对象DAO用来查询数据库,得到用户名称 bob;控制对象controller接收外界请求; model对象用来装配数据; 这些一个个分立的对象如果要一起完成这个任务,我们需要将组合起来完成数据的传递。我们需要在controller
对象中包含model
对象,因为model对象包含了要返回给前台的数据;在model
对象中我们需要包含DAO
对象,因为生成model
所需要的数据需要去数据库中查。通过对象的组合,实际上是各种操作的组合,我们完成了数据的传递,完成整个任务。
在上面的过程中,我们发现,在某个对象中我们需要包含一些提供本类对象操作的下游对象。如DAO对象就是model
对象的下游对象,model
对象的操作需要DAO对象提供的数据支持,因此我们需要在model
对象包含一个DAO
对象,将DAO
对象作为model
的成员。问题是这个DAO
对象是从哪儿来的?
一个直接的思路是在构造model
对象的时候,直接在构造函数中提供DAO
对象,这样model
对象就能得到这个DAO
对象的引用。DAO
对象怎么来的呢?当然也是构造出来的了(new),那如果DAO对象中也有需要包含的下游对象怎么办呢?如DAO对象必然需要一个成员是数据库连接对象connector, 直接的想法是像 model 对象一样,在构造的时候直接提供connector。但是connector又可能需要其他的成员....这样就形成了一层层的递归调用。在构造顶层对象的时候,由于这一层层的包含关系,我们需要一层层的递归构造下游的所有对象。递归在生产中的坏处不言而喻,就是容易OOM。那怎么解决这个问题呢?一个很自然的想法是,如果我们提前知道有哪些下游对象,我们就在环境启动之前将这些下游对象都提前构造好,然后上层对象要使用的时候就直接拿过来使用就好,不必再去自己递归构造。如果说之前的做法是“懒人式”的(事到临头才去准备需要的对象), 那么现在的做法就是“勤奋式”的,不管你后面要什么,凡是下游的对象我都提前给你准备好。好,既然思路有了,那么我们要怎么做呢?首先,你怎么知道哪些对象是下游的?其次,你把这些下游对象构造,暂时不用,放到哪儿呢?最后,你在上游使用这些对象的时候,怎么去取用呢?
这三个问题引出了spring/spring-boot的解决方案。依赖注入。
第一个问题:你如何知道哪些对象是下游的?
Spring/Spring-Boot
的解决方式有两种:xml配置或者注解配置。
假设我们有一个下游类叫Student, 定义如下:
public class Student{
private String name;
//getter and setter...
}
xml方法
在Spring中我们可以用Spring的配置文件来告诉Spring容器,我有一个下游类Student, 我需要你帮我提前实例化好:
<bean id="student" class="com.autowiredtest.entity.Student">
<property name="name" value="小红"/>
</bean>
通过上面的语法, 我们可以告诉Spring, 我有一个Student类(要在class属性中指定这个类定义的路径,这样spring容器才能找得到它), 我需要你帮我实例化这个类,实例化后的对象的名字(id属性)叫 student(这样上游类在使用的时候可以凭这个实例的名字问spring容器要到这个实例), 用property属性 和value属性 指定这个对象的内部成员name的初始值。
值得注意的是,我们完全可以让spring对同一个类实例化多个对象:
<bean id="student" class="com.autowiredtest.entity.Student">
<property name="name" value="小红"/>
</bean>
<bean id="student02" class="com.autowiredtest.entity.Student">
<property name="name" value="小明"/>
</bean>
上面就是让spring容器实例化两个student对象,一个叫 student, 它的name设置为 小红, 一个叫 student02, 它的name设置为 小明。这样上游对象要使用一个Student实例的时候,就可以根据 id 来拿不同的实例。
注解方式
当下游类太多的时候,除了要写这些下游类的定义,我们还要在xml文件中为每个下游类写配置,一则很麻烦,二则不容易管理,试想如果你有几百个下游类要实例化,如果你想要改动其中某个类的属性,那配置文件必然要跟着改动,从几百个配置行中找到你要的那个类的配置就很麻烦了。于是spring的进化为我们提供了新能力:当我们需要告诉spring哪些下游类需要spring容器提前为我们初始化好的时候,不用再去专门写到xml文件中告诉它了,我们可以专门写一个配置类来告诉spring容器我们要你帮我实例化哪些类,但spring怎么知道哪些是你的配置类呢? 这就需要为这些配置类打上专门的标记 -- 注解。这种告诉spring我们有哪些下游类需要它提前实例化好的方式就是 配置类+注解 的方式。还以Student为例:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class StudentConfiguration{
@Bean
Student student03(){
Student student = new Student();
student.setName("小华");
return student;
}
@Bean
Student student04(){
Student student = new Student();
student.setName("小玲");
return student;
}
}
我们新定义了一个类叫StudentConfiguration, 但并不是它的名字带configuration,spring就能认出来它是一个配置类了。He needs more! 注意到这个类的上面有一个@Configuration注解了没?这个才是关键,对一个类打上了这个注解,Spring就会知道这个类是专门用来配置的类。进一步的,Spring知道这个类会有一些方法,这些方法会返回一些实例,这些实例就是要让spring容器提前实例化的。继续看代码,代码中有两个方法,这两个方法上面都打了@Bean注解,spring 看到这个注解就知道,好的,我要用这个方法来提前构造好下游类,至于下游类构造的细节,都写在@Bean标注的方法里面了。
以代码的第一个方法student03为例,spring容器首先通过@Bean注解知道可以用这个方法提前实例化好一个下游类,这个下游类的定义在哪呢?看返回值就知道了我们是要实例化一个Student类型的对象, 然后就像普通的构造方式一样,我们在这个方法里面为我们要实例的对象赋值就好,比如这个对象的name我们设置成"小华"。那么这个对象spring生成好了叫什么呢, 如果不知道叫什么的话上游类要使用的时候怎么找呢? 秘诀在方法的名称上, 以上面的方法为例,第一个方法叫student03, 那么spring容器生成好这个Student对象后,就把它叫做student03, 上游类使用的时候就用student03去找到这个对象。下面的方法叫student04, 那么它实例化后的对象就叫student04。类似与写xml配置文件,我们也可以为同一个下游类写多个实例化方法。
另外需要注意的是如果要使用这种方法注入,一定要在Student类中提供getter和setter方法,注意看student03方法和student04方法,都是通过setName方法为name属性赋值的,这就意味着Student一定要提供setter和getter方法。
对比两种方法
xml | 配置类+注解 | |
---|---|---|
要实例化哪个类 | 由class属性给出的全类名给定 | 由配置类中的@bean方法的返回值类型给定 |
实例化后的对象叫啥 | 由id属性指定 | 由配置类中的@bean方法的方法名给定 |
对象的属性怎么给 | 由property+value属性指定 | 由配置类中的@bean方法内部定 |
第二题问题: 这些下游对象创建好了,放哪里?
可以想象 Spring容器是一个公共仓库, spring 根据xml配置文件或者配置类的方式知道了要提前实例化好的下游类并进行实例化得到了下游类之后,放到这个公共仓库中保存着,这些对象有类型和名称两个属性,上游类要使用某个下游类的时候根据这两个属性可以唯一定位到某个对象。以上面的Student类为例,我们一共实例化好了四个不同的对象,分别叫student, student2, student3, student4, 它们都是Student类型的对象。凭借这些信息上游类可以取用到想要用的对象。
第三个问题: 上游对象怎么去取用下游对象?#
自然是去“公共仓库”里取,这里面放了所有提前实例化好的对象。上游对象凭借 类型+对象名称就可以取用到仓库中的对象,这个过程就叫“注入”。比如说我的上游类需要用到一个Student对象:
public class AutowiredTestController{
private Student student02;
}
光这个样子是不行的,spring哪里知道你这个student对象是要自己去new一个出来,还是要从我的仓库中取出来呢?区别的方法还是注解,只要给上游类中声明的成员对象打上@Autowired注解, spring容器就知道了,哦,你这个对象要去我的仓库里去取实例出来。
@Controller
public class AutowiredTestController{
@Autowired
private Student student02;
@RequestMapping("/AutowiredTest")
@ResponseBody
public String loanSign(){
String docSignUrl = "ok";
System.out.println("--------------要打印了------------");
System.out.println(student.getName());
System.out.println("--------------打印结束------------");
return docSignUrl;
}
}
我仓库那么多对象你要取哪个实例呢?声明这个成员的时候,给出的类型是Student, 并且对象的名称是student02,好的,那么去仓库取一个类型为Student名称为student02的对象出来,打印一下看看:
这正是我们在xml里为student02对象的name成员赋的名字。
总结
通过上面的过程,我们大概明白spring帮我们做的事情了。在没有spring的时代,我们的对象需要我们自己去构造出来,也就是手动用new方法去构造出来。问题是当我们要做的事情比较复杂时,可能需要在一个对象中引用其他对象去分工完成一个大任务。引用的其他对象也是需要实例化的,就这样,一个对象引用另外的对象,另外的对象又引用其他的对象,就像个多叉树似的。我们需要自己去手动构造每一个节点,并且这种方式是递归式的,很容易OOM。于是我们就想到,可以在实例化顶层对象之前把它需要引用的对象都提前实例化好,不用遇到的时候再去准备。Spring就是做了这样一件事,我们定义好一些类的时候,只要给他们打上特定的注解,或者写好配置文件,spring就知道提前为我们实例化这些类,并放到spring仓库中,在有需要的地方,我们再打上特定的注解,spring容器就会自动的把仓库中的对象“注入”到要使用的位置。
其实上面提到的注解方式(即 配置类+注解 @Configuration+@Bean), 是非常原始的方式,因为这种方法还需要写额外的配置类。除了这种方式以外,spring对不同的场景,还提供了更高级的注解,使得我们不必去写配置类,直接用注解就搞定。我们下一节见。