前情提要:源码下载&编译
速览
- 设计一个最简单的spring项目
- spring的入口在哪?怎么启动的?
- 搭建源码阅读环境
- 新建module并添加依赖
- 编写一个微型spring项目(配置bean->获取bean->使用bean)
- 不打无准备之仗:源码一周目简单规划
设计一个最简单的spring项目
这个问题貌似简单,但实际上绝对不是一个菜鸟能随便搞定的。首先我需要知道spring需要哪些最基本的配置(要对源码学习最有利),其次最好尽量不引入其它更多的框架(例如查询db时用到的持久化框架)。所以我想了下,这个简单的spring项目最好就是配置、创建、使用一个bean。在我肤浅的认知里,这正是spring的基础功能:管理bean。
spring的入口在哪?怎么启动的?
有个关键问题需要想明白:spring是怎么“启动”的。回想一下,我平时工作时对spring的使用都是“无痕”的:在idea里写完业务代码,然后直接在idea内部启动tomcat后就可以本地调试了——这说明spring是被tomcat启动的。在此前的实况记录中也提到过,说到底spring本身也是一个java程序,所以入口是也就是个main方法。tomcat能通过某种方式执行main方法,那我通过某段代码也执行这个main方法不就“启动”了spring嘛。
解下来的问题反而简单了,我只需写一个main方法“启动”spring即可。当然了,所谓的spring“启动”是做了哪些事,目前肯定是不知道的,这不是还没开始看源码么。那怎么写main方法呢?网上搜现成的呗,例如这篇,或者这篇,或者直接百度“搭建spring源码阅读环境”。里面都有类似“编写一个简单的applicationContext用于获取bean”的内容,其中的main方法的内容,其实就是在启动spring了。解下来就是本老鸟的操作。
搭建源码阅读环境
新建module并添加依赖
此时,映入我眼帘的是编译后的spring源码,并且已经导入到了idea中:
这时我有了一个不知当讲不当讲的问题,我的main方法写在哪呢?网上搜搜吧:
在spring-framework项目上右键:new>module,然后左边选Gradle,右边选Java,再补全命名信息后finish即可:
这时我新建的module是作为一个子module出现在spring-framework路径下的,编辑子模块的build.gradle文件,添加spring核心模块的依赖:
但此时我这里出现了报错:Could not find method compile() for arguments [project ‘:spring-context’] on object of type org.gradle.api.internal.artifacts.dsl.dependencies.DefaultDependencyHandler.
这个问题是由于gradle版本问题导致的,不再支持"compile"语法,改为"api"问题消失。此时又出现了新问题:Could not find method testCompile() for arguments [{group=junit, name=junit, version=4.12}] on object of type org.gradle.api.internal.artifacts.dsl.dependencies.DefaultDependencyHandler. 看来"testCompile"语法也不支持了,改成“testImplementation”之后成功build。
编写一个微型spring项目(配置bean->获取bean->使用bean)
发现刚才新建的module下并没有src路径,那就自己建:直接右键然后New》directory,最终建立一个简单的项目,目录结构就按照旧例:
其中Department和User分别是两个bean,将分别用两种方式进行配置和注入。前者通过xml方式配置,而后者通过注解方式配置:
package com.dustin.dto;
/**
* @author qimy
* @version 1.0
* @Description
* @date 2024/7/18
*/
public class User {
private Long id;
private String name;
private int age;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public User(){}
public User(Long id, String name, int age){
this.id = id;
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "User{" +
"id=" + this.id +
", name='" + this.name + '\'' +
", age=" + this.age +
'}';
}
}
package com.dustin.dto;
/**
* @author dustin
* @version 1.0
* @Description
* @date 2024/7/18
*/
public class Department {
private Long id = 456L;
private String name = "CC";
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Department{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
ContextConfig是配置类,通过扫描注解的方式进行bean的注入,本例中User这个bean是通过此配置类注入的:
package com.dustin.config;
import com.dustin.dto.User;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
/**
* @author dustin
* @version 1.0
* @Description
* @date 2024/7/18
*/
@Configuration
@ComponentScan
public class ContextConfig {
@Bean
public User user(){
return new User(123L, "dq", 18);
}
}
config.xml是配置文件,放在resources目录下,本例中Department这个bean是通过这个配置文件注入的:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="department" class="com.dustin.dto.Department"></bean>
</beans>
最后这个ContextApplication类就是spring启动的入口了,在main方法中分别通过扫描注解和读取xml的方式,获取到User和Department两个bean,最后使用这两个bean(打印bean信息):
package com.dustin;
import com.dustin.config.ContextConfig;
import com.dustin.dto.Department;
import com.dustin.dto.User;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
* @author dustin
* @version 1.0
* @Description
* @date 2024/7/18
*/
public class ContextApplication {
public static void main(String[] args) {
// 通过读注解的方式获取bean
AnnotationConfigApplicationContext annotationContext = new AnnotationConfigApplicationContext(ContextConfig.class);
User user = annotationContext.getBean(User.class);
System.out.println("User:" + user.toString());
// 通过读xml的方式获取bean
ClassPathXmlApplicationContext xmlApplicationContext = new ClassPathXmlApplicationContext("config.xml");
Department department = xmlApplicationContext.getBean(Department.class);
System.out.println("Department:" + department.toString());
}
}
运行main方法,看看这个配置bean-》获取bean-》使用bean的微型spring项目是否运行正常:
可以看到,两个bean的信息都成功打印出来了,一切正常。接下来就可以真正开始与源码0距离接触了!可以预料到的是,这必将是一场神秘坎坷但又充满宝藏的旅途!好比金庸的悬崖谷底,虽然遍体鳞伤但总能收获神功秘籍~~
不打无准备之仗:源码一周目简单规划
激动之余也要清醒,面对我们完全未知的领域,盲干是不行的,何不问问已经归来的水手?以下内容来自知乎,主要是关于spring源码学习方法论的一些观点:
- 从上至下全部通读的方式,个人不太推荐,这是建立在很熟悉的基础上的,当我们对某个框架已经比较熟悉了,再从上至下进行通读,彻底了解,这是我认为正确的方式;但是从不熟悉到熟悉这个过程,个人不推荐全部通读,而是推荐使用idea进行断点局部追踪。
- 不要为了看源码而看源码,一定要带着问题去看源码,这样比较容易带入进去。Spring整个框架非常复杂,不建议直接去读,从Spring的上层框架入手会更好。比如你可以读Spring MVC的源码,这部分的源码就简单了很多,读源码的时候都给自己提一些问题,这样能集中精力去读源码,比如:一个http请求怎么路由到具体controller?spring mvc怎么处理的参数映射,前端提交的Json数据,如何映射到我们的VO里面的?为什么spring mvc能返回rest接口,也能返回视图?Spring Mvc如何根据自己的需求进行扩展?
其实这里就是一个简单的spring学习路径了(始终牢记着我的源码学习终极目标:画一张spring的唬人大图)。结合我一个老菜鸡的工作经验、spring源码同好者、诸多“专升本成人自考级”spring付费课程的总结,我简单规划了下学习计划,其中主要参考了这位好兄弟的文章:spring源码深度解析。谢谢你,因为有你,温暖了司机~~
第一步:spring启动时是如何解析配置文件并注册bean的,可以聚焦于这个过程来阅读ApplicationContext容器的源码内容,重要的是找到读源码的感觉;
第二步:在上一步的学习中,应该能自然而然的引出BeanFactory。a.k.a. bean容器,也是spring基础中的基础;
第三步:spring提供了很多后门对bean进行操作,而AOP、事务等其实就是利用后门对相关逻辑进行了封装,所以这一步要去探索spring怎样实现AOP和管理事务的;
第四步:在上面三步之后,spring的基础已经已经真正意义上有所掌握了,那么此时就可以回归应用层,看看spring(MVC)是如何配合tomcat,一起帮助我们实现业务逻辑的!
第五步:神功初成,可考虑二周目了!