简介

学习的是廖雪峰老师的Summer Framework项目,在这个项目中我们手写实现了一个简单的spring框架。在这个框架中,我们会实现

  • context模块:实现ApplicationContext容器与Bean的管理
  • aop模块:实现AOP功能;
  • jdbc模块:实现JdbcTemplate,以及声明式事务管理;
  • web模块:实现Web MVC和REST API;
  • boot模块:实现一个简化版的“Spring Boot”,用于打包运行;

实现ResourceResolver

spring使用容器来管理bean,但是显而易见,容器是不可能自己感知到bean是在哪里存在的,所以我们需要对整个项目的Class进行一个扫描。java的ClassLoader机制可以获取到指定的Class,但是,给出一个包名,它并不能获取到该包下的所有Class,也不能获取子包。

所以我们需要自己动手实现一个文件扫描的功能

首先创建一个文件类型

public class Resource {
String path;
String name;
public Resource(String path,String name){
this.path = path;
this.name = name;
}

public String getPath() {
return path;
}

public void setPath(String path) {
this.path = path;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

@Override
public String toString() {
final StringBuffer sb = new StringBuffer("Resource{");
sb.append("path='").append(path).append('\'');
sb.append(", name='").append(name).append('\'');
sb.append('}');
return sb.toString();
}
}

在spring中提供了一个ResourceResolver类,提供一个scan方法来扫描。

思路

首先要认清java中的各个路径(即资源定位与解析)

  • URI形式的绝对路径资源
  • 本地系统的绝对路径
  • 相对于classpath的相对路径
  • 相对于当前用户目录的相对路径·

我们选择通过classpath相对路径来解析,所以要拿到ClassLoader。

当前线程的类加载会从classes下开启

主类的类加载带/从当前类所在包路径去获取资源

主类的类加载不带/从classPath的根路径开启

getResource

ClassLoader首先从Thread.getContextClassLoader()获取,如果获取不到,再从当前Class获取,因为Web应用的ClassLoader不是JVM提供的基于Classpath的ClassLoader,而是Servlet容器提供的ClassLoader,它不在默认的Classpath搜索,而是在/WEB-INF/classes目录和/WEB-INF/lib的所有jar包搜索,从Thread.getContextClassLoader()可以获取到Servlet容器专属的ClassLoader。

使用getResource获取文件路径URI之后,我们对URI进行编码的处理后拿到路径返回值。根据路径的头的协议来选择如何创建Resource

  • 要注意/的处理,头或者尾的处理,以及不同系统的处理
public class ResourceResolver {
//基础包名
String basePackage;

public ResourceResolver(String basePackage) {
this.basePackage = basePackage;
}

//这是对外暴露的扫描方法,接收一个 Function<Resource, R> 类型的映射函数 mapper,用于将扫描到的 Resource 对象转换为指定类型 R 的对象
public <R> List<R> scan(Function<Resource, R> mapper) {
//将基础包名的路径替换得到路径进行扫描
String basePackagePath = this.basePackage.replace(".", "/");
String path = basePackagePath;
try {
List<R> collector = new ArrayList<>();
scan0(basePackagePath, path, collector, mapper);
return collector;
} catch (IOException e) {
throw new UncheckedIOException(e);
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
}

<R> void scan0(String basePackagePath, String path, List<R> collector, Function<Resource, R> mapper) throws IOException, URISyntaxException {
logger.atDebug().log("scan path: {}", path);

//比较老的迭代器接口 但是属于是懒加载机制,遍历过程中可以更快地开始处理元素,不需要等待所有元素加载完成。
Enumeration<URL> en = getContextClassLoader().getResources(path);
while (en.hasMoreElements()) {
URL url = en.nextElement();
URI uri = url.toURI();
String uriStr = removeTrailingSlash(uriToString(uri));
String uriBaseStr = uriStr.substring(0, uriStr.length() - basePackagePath.length());
System.out.println("URL: " + url + " HashCode: " + url.hashCode());
if (uriBaseStr.startsWith("file:")) {
uriBaseStr = uriBaseStr.substring(5);
}
if (uriStr.startsWith("jar:")) {
scanFile(true, uriBaseStr, jarUriToPath(basePackagePath, uri), collector, mapper);
} else {
scanFile(false, uriBaseStr, Paths.get(uri), collector, mapper);
}
}
}

//获取类加载器
ClassLoader getContextClassLoader() {
ClassLoader cl = null;
cl = Thread.currentThread().getContextClassLoader();
if (cl == null) {
cl = getClass().getClassLoader();
}
return cl;
}

//转换jar包的路径
Path jarUriToPath(String basePackagePath, URI jarUri) throws IOException {
return FileSystems.newFileSystem(jarUri, new HashMap<>()).getPath(basePackagePath);
}

//对文件的扫描方式
<R> void scanFile(boolean isJar, String base, Path root, List<R> collector, Function<Resource, R> mapper) throws IOException {
String baseDir = removeTrailingSlash(base);
Files.walk(root).filter(Files::isRegularFile).forEach(file -> {
Resource res = null;
if (isJar) {
res = new Resource(baseDir, removeLeadingSlash(file.toString()));
} else {
String path = file.toString();
String name = removeLeadingSlash(path.substring(baseDir.length()));
res = new Resource("file:" + path, name);
}
logger.atDebug().log("found resource: {}", res);
R r = mapper.apply(res);
if (r != null) {
collector.add(r);
}
});
}

//转换url的路径
String uriToString(URI uri) throws UnsupportedEncodingException {
return URLDecoder.decode(uri.toString(), String.valueOf(StandardCharsets.UTF_8));
}

//移除开始的/
String removeLeadingSlash(String s) {
if (s.startsWith("/") || s.startsWith("\\")) {
s = s.substring(1);
}
return s;
}

//移除最后的/
String removeTrailingSlash(String s) {
if (s.endsWith("/") || s.endsWith("\\")) {
s = s.substring(0, s.length() - 1);
}
return s;
}
}

实现PropertyResolver

即实现@Autowired,@Value
我们在这里实现的@Autowired,涉及到Bean的依赖,而对于@Value,则仅仅是将对应的配置注入
支持,我们首先实现@Value。
支持3种注入方式:

  • 按配置的key查询,例如:getProperty(“app.title”)
  • 以${abc.xyz}形式的查询
  • 带默认值的,以${abc.xyz:defaultValue}形式的查询

思路

@Value要支持配置文件的注入,所以要配置上扫描配置文件。将文件内容存入方便查找。
同时也要有一个字符的解析器,方便解析abc.xyz:defaultValue的内容来注入

解析的时候也要注意不带${}的注入即普通key的处理
同时也要注意对${key:${value}}这种的解析所以当我们解析拿到值之后还要调用一次处理值的方法

值得注意的是,真正的spring中涉及到了很复杂的语言解析,这部分的内容要靠专业知识去补充了,目前解析就支持到这里吧

部分实现

java自带着对key-value查询的Properties

public class PropertyResolver {

Map<String, String> properties = new HashMap<>();

public PropertyResolver(Properties props) {
// 存入环境变量:即计算机系统的环境变量
this.properties.putAll(System.getenv());
// 存入Properties:
Set<String> names = props.stringPropertyNames();
for (String name : names) {
this.properties.put(name, props.getProperty(name));
}
}

//加入根据键获取值的方法
//....
}

我们先解析${abc.xyz:defaultValue}这种类型的key

//先定义一个解析表达式的存储类
record PropertyExpr(String key, String defaultValue) {
}

//解析字符
PropertyExpr parsePropertyExpr(String key) {
if (key.startsWith("${") && key.endsWith("}")) {
// 是否存在defaultValue?
int n = key.indexOf(':');
if (n == (-1)) {
// 没有defaultValue: ${key}
String k = key.substring(2, key.length() - 1);
return new PropertyExpr(k, null);
} else {
// 有defaultValue: ${key:default}
String k = key.substring(2, n);
return new PropertyExpr(k, key.substring(n + 1, key.length() - 1));
}
}
return null;
}

// 获取值的代码

~~~java
public String getProperty(String key,String defaultValue) {}
public String getProperty(String key){}

值转换

除了String类型外,@Value注入时,还允许boolean、int、Long等基本类型和包装类型。此外,Spring还支持Date、Duration等类型的注入。我们既要实现类型转换,又不能写死,否则,将来支持新的类型时就要改代码

@Nullable
public <T> T getProperty(String key, Class<T> targetType) {
String value = getProperty(key);
if (value == null) {
return null;
}
// 转换为指定类型:
return convert(targetType, value);
}

对于类型转换,实际上是从String转换为指定类型,因此,用函数式接口Function<String, Object>就很合适:

// 转换到指定Class类型:
<T> T convert(Class<?> clazz, String value) {
Function<String, Object> fn = this.converters.get(clazz);
if (fn == null) {
throw new IllegalArgumentException("Unsupported value type: " + clazz.getName());
}
return (T) fn.apply(value);
}

各种要转换的类型要存放

public PropertyResolver(Properties props) {
// String类型:
converters.put(String.class, s -> s);
// boolean类型:
converters.put(boolean.class, s -> Boolean.parseBoolean(s));
converters.put(Boolean.class, s -> Boolean.valueOf(s));
// int类型:
converters.put(int.class, s -> Integer.parseInt(s));
converters.put(Integer.class, s -> Integer.valueOf(s));
// 其他基本类型...
// Date/Time类型:
converters.put(LocalDate.class, s -> LocalDate.parse(s));
converters.put(LocalTime.class, s -> LocalTime.parse(s));
converters.put(LocalDateTime.class, s -> LocalDateTime.parse(s));
converters.put(ZonedDateTime.class, s -> ZonedDateTime.parse(s));
converters.put(Duration.class, s -> Duration.parse(s));
converters.put(ZoneId.class, s -> ZoneId.of(s));
}

集成YAML配置

首先引入依赖org.yaml:snakeyaml:2.0,通过YamlUtils返回Map并且和properties的集合起来

public class YamlUtils {
public static Map<String, Object> loadYamlAsPlainMap(String path) {
return ...
}
}

创建BeanDefinition

在此之前,我们实现了ResourceResolver扫描Class,并且通过PropertyResolver获取配置,接下来就可以开始着手实现IOC了

思路

BeanDefinition是用来记录bean信息的类,作为IOC,有着管理所有bean,即实例的生命周期。

public class BeanDefinition {
// 全局唯一的Bean Name:
String name;

// Bean的声明类型:
Class<?> beanClass;

// Bean的实例:
Object instance = null;

// 构造方法/null:
Constructor<?> constructor;

// 工厂方法名称/null:
String factoryName;

// 工厂方法/null:
Method factoryMethod;

// Bean的顺序:
int order;

// 是否标识@Primary:
boolean primary;

// init/destroy方法名称:
String initMethodName;
String destroyMethodName;

// init/destroy方法:
Method initMethod;
Method destroyMethod;
}

对于自己定义的带@Component注解的Bean,我们需要获取Class类型,获取构造方法来创建Bean,然后收集@PostConstruct和@PreDestroy标注的初始化与销毁的方法,以及其他信息,如@Order定义Bean的内部排序顺序,@Primary定义存在多个相同类型时返回哪个“主要”Bean。一个典型的定义如下:

@Component
public class Hello {
@PostConstruct
void init() {}

@PreDestroy
void destroy() {}
}

对于@Configuration定义的@Bean方法,我们把它看作Bean的工厂方法,我们需要获取方法返回值作为Class类型,方法本身作为创建Bean的factoryMethod,然后收集@Bean定义的initMethod和destroyMethod标识的初始化于销毁的方法名,以及其他@Order、@Primary等信息。典型:

@Configuration
public class AppConfig {
@Bean(initMethod="init", destroyMethod="close")
DataSource createDataSource() {
return new HikariDataSource(...);
}
}
一定要区分@Component和@Bean,对于用@Bean工厂方法创建的Bean,它的声明类型与实际类型不一定是同一类型。上述createDataSource()定义的Bean,声明类型是DataSource,实际类型却是某个子类,例如HikariDataSource,因此要特别注意,我们在BeanDefinition中,存储的beanClass是声明类型,实际类型不必存储,因为可以通过instance.getClass()获得

在spring中分为按类型注入和按名称注入,按名称注入很简单,因为一个名字不会对应多个bean,而对于按类型来说,会有同父类的,这个时候就不知道该注入到什么了,所以需要@Primary来确定一个主要的bean,在多个同类型的时候选择这个主类注入

所以我们首先写出一个找出所有类型的方法

// 根据Type查找若干个BeanDefinition,返回0个或多个:
List<BeanDefinition> findBeanDefinitions(Class<?> type) {
return this.beans.values().stream()
// 按类型过滤:
.filter(def -> type.isAssignableFrom(def.getBeanClass()))
// 排序:
.sorted().collect(Collectors.toList());
}
}

上述只是查找了所有的BeanDefinition,我们还要对多个bean@Primary的进行过滤

@Nullable
public BeanDefinition findBeanDefinition(Class<?> type) {
List<BeanDefinition> defs = findBeanDefinitions(type);
if (defs.isEmpty()) { // 没有找到任何BeanDefinition
return null;
}
if (defs.size() == 1) { // 找到唯一一个
return defs.get(0);
}
// 多于一个时,查找@Primary:
List<BeanDefinition> primaryDefs = defs.stream().filter(def -> def.isPrimary()).collect(Collectors.toList());
if (primaryDefs.size() == 1) { // @Primary唯一
return primaryDefs.get(0);
}
if (primaryDefs.isEmpty()) { // 不存在@Primary
throw new NoUniqueBeanDefinitionException(String.format("Multiple bean with type '%s' found, but no @Primary specified.", type.getName()));
} else { // @Primary不唯一
throw new NoUniqueBeanDefinitionException(String.format("Multiple bean with type '%s' found, and multiple @Primary specified.", type.getName()));
}
}

上面定义好了简单的方法,下面要开始注入所有的BeanDefinition的信息。

首先,我们要使用以前定义的scan扫描指定包下的所有Class,然后返回Class的名字

//首先,我们要获取到@ComponentScan注解
public static <A extends Annotation> A findAnnotation(Class<?> target, Class<A> annoClass) {
A a = target.getAnnotation(annoClass);
for (Annotation anno : target.getAnnotations()) {
Class<? extends Annotation> annoType = anno.annotationType();
if (!annoType.getPackage().getName().equals("java.lang.annotation")) {
A found = findAnnotation(annoType, annoClass);//在类的注解中找到了Component 又在注解的注解中找到了Component
if (found != null) {
if (a != null) {
throw new BeanDefinitionException("Duplicate @" + annoClass.getSimpleName() + " found on class " + target.getSimpleName());
}
a = found;
}
}
}
return a;
}
findAnnotation(主类,ComponentScan.class)


Set<String> scanForClassNames(Class<?> configClass) {
// 获取@ComponentScan注解:
ComponentScan scan = ClassUtils.findAnnotation(configClass, ComponentScan.class);
// 获取注解配置的package名字,未配置则默认当前类所在包:
String[] scanPackages = scan == null || scan.value().length == 0 ? new String[] { configClass.getPackage().getName() } : scan.value();

Set<String> classNameSet = new HashSet<>();
// 依次扫描所有包:
for (String pkg : scanPackages) {
logger.atDebug().log("scan package: {}", pkg);
// 扫描一个包:
var rr = new ResourceResolver(pkg);
List<String> classList = rr.scan(res -> {
// 遇到以.class结尾的文件,就将其转换为Class全名:
String name = res.name();
if (name.endsWith(".class")) {
return name.substring(0, name.length() - 6).replace("/", ".").replace("\\", ".");
}
return null;
});
// 扫描结果添加到Set:
classNameSet.addAll(classList);
}

// 继续查找@Import(Xyz.class)导入的Class配置:
Import importConfig = configClass.getAnnotation(Import.class);
if (importConfig != null) {
for (Class<?> importConfigClass : importConfig.value()) {
String importClassName = importConfigClass.getName();
classNameSet.add(importClassName);
}
}
return classNameSet;
}

在查找到所有的类名称之后,开始对这些类进行处理,将这些注解的各种信息注入到BeanDefinition中去,
在扫描@Component的时候要注意他的拓展,因为@Controller这些都是@Component的拓展。

Map<String, BeanDefinition> createBeanDefinitions(Set<String> classNameSet) {
Map<String, BeanDefinition> defs = new HashMap<>();
for (String className : classNameSet) {
// 获取Class:
Class<?> clazz = null;
try {
clazz = Class.forName(className);
} catch (ClassNotFoundException e) {
throw new BeanCreationException(e);
}
// 是否标注@Component?
Component component = ClassUtils.findAnnotation(clazz, Component.class);
if (component != null) {
// 获取Bean的名称:
String beanName = ClassUtils.getBeanName(clazz);
var def = new BeanDefinition(
beanName, clazz, getSuitableConstructor(clazz),
getOrder(clazz), clazz.isAnnotationPresent(Primary.class),
// init/destroy方法名称:
null, null,
// 查找@PostConstruct方法:
ClassUtils.findAnnotationMethod(clazz, PostConstruct.class),
// 查找@PreDestroy方法:
ClassUtils.findAnnotationMethod(clazz, PreDestroy.class));
addBeanDefinitions(defs, def);
// 查找是否有@Configuration:
Configuration configuration = ClassUtils.findAnnotation(clazz, Configuration.class);
if (configuration != null) {
// 查找@Bean方法:
scanFactoryMethods(beanName, clazz, defs);
}
}
}
return defs;
}

而对于@Configuration的Class,我们将他们视为是一个工厂类,用于创建普通类的,我们实现一个scanFactoryMethods方法来处理这些类,将带@Bean的方法提取出来

关于getBeanName方法要注意,1.@Component是可以指定bean的名称的,2.没有@Component的要注意是否有@Component的拓展注解。3.都没有就返回类名

同时,在注入信息的时候,我们注意到工厂方法中的@Bean是没有办法获取到init和destroy方法的,只能通过@Bean(initMethod = "init", destroyMethod = "destroy")指定的名称以及getMethod方法去调用。所以bean的方法名称和方法相关信息至少有一个是空的。
void scanFactoryMethods(String factoryBeanName, Class<?> clazz, Map<String, BeanDefinition> defs) {
for (Method method : clazz.getDeclaredMethods()) {
// 是否带有@Bean标注:
Bean bean = method.getAnnotation(Bean.class);
if (bean != null) {
// Bean的声明类型是方法返回类型:
Class<?> beanClass = method.getReturnType();
var def = new BeanDefinition(
ClassUtils.getBeanName(method), beanClass,
factoryBeanName,
// 创建Bean的工厂方法:
method,
// @Order
getOrder(method),
// 是否存在@Primary标注?
method.isAnnotationPresent(Primary.class),
// init方法名称:
bean.initMethod().isEmpty() ? null : bean.initMethod(),
// destroy方法名称:
bean.destroyMethod().isEmpty() ? null : bean.destroyMethod(),
// @PostConstruct / @PreDestroy方法:
null, null);
addBeanDefinitions(defs, def);
}
}
}

完成bean的实例化

在拿到所有的beanDefinition之后,我们便可以开始完成bean的实例化了,即在ioc容器中完成new objct这一步。

根据分析bean的依赖注入有四种模式

对于构造方法和工厂方法这种类型的,因为在bena的实例化过程中,一定需要用到依赖注入,所以这种我们称之为强依赖。
而对于3,4这两种方法的,我们称之为弱依赖,因为可以先new再将属性注入进去,但是在1,2中new的过程就需要注入了
  1. 构造方法注入,注入到参数中国
@Component
public class Hello {
JdbcTemplate jdbcTemplate;
public Hello(@Autowired JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
}
  1. 工厂方法注入注入到bean的参数中,一般不会写@Autowire
@Configuration
public class AppConfig {
@Bean
Hello hello(@Autowired JdbcTemplate jdbcTemplate) {
return new Hello(jdbcTemplate);
}
}
  1. Setter方法注入
@Component
public class Hello {
JdbcTemplate jdbcTemplate;

@Autowired
void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
}
  1. 字段注入(最常用)
@Component
public class Hello {
@Autowired
JdbcTemplate jdbcTemplate;
}

思路

我们先解决强依赖的问题,在上一节中,我们获得了所有bean的资料,这时我们需要先检测循环依赖的问题。对于检测是否重复的问题,我们一般选择的是使用数据结构set集合。

我们创建一个set集合用来记住bean的名字,因为名字是多一对应的。

我们创建一个方法createBeanAsEarlySingleton()来检测bean循环以及做早期的实例创建

public Object createBeanAsEarlySingleton(BeanDefinition def) {
if (!this.creatingBeanNames.add(def.getName())) {
// 检测到重复创建Bean导致的循环依赖:
throw new UnsatisfiedDependencyException();
}
...
}

对于接下来的实例化,我们会选择先实例化带@Configuration的工厂类,因为只有实例化工厂类之后,接下来bean的内容才有机会完全实例化。

使用流式来过滤list< BeanDifination >中的集合,找到带@Configuration的工厂,并且实例化它,实例化完工厂之后,才有机会实例化其余的内容

this.beans.values().stream()
// 过滤出@Configuration:
.filter(this::isConfigurationDefinition).sorted().map(def -> {
// 创建Bean实例:
createBeanAsEarlySingleton(def);
return def.getName();
}).collect(Collectors.toList());

// 创建其他普通Bean:
List<BeanDefinition> defs = this.beans.values().stream()
// 过滤出instance==null的BeanDefinition:
.filter(def -> def.getInstance() == null)
.sorted().collect(Collectors.toList());
// 依次创建Bean实例:
defs.forEach(def -> {
// 如果Bean未被创建(可能在其他Bean的构造方法注入前被创建):
if (def.getInstance() == null) {
// 创建Bean:
createBeanAsEarlySingleton(def);
}
});

接下来完善createBeanAsEarlySingleton的内容,我们从上可以知道,无论是普通bean还是工厂bean的创建,都是调用createBeanAsEarlySingleton完成的,所以我们的createBeanAsEarlySingleton方法两种情况都要支持

  1. 工厂bean,工厂bean的构造方法参数不能使用@Autowired,否则会导致代理的失效以及其他问题,且工厂bean的参数要带有@value或者@Autowired之间的一个
  2. 注意优先使用工厂bean创建bean,这也符合我们在日常使用spring时的方式,因为在工厂中我们会对bean进行配置。
  3. 同时在bean未初始化时候要注意递归调用

然后使用反射完成方法的创建方法的调用

//建造方法调用
instance = def.getConstructor().newInstance(args);
//或者是
//先拿到工厂方法的实例
Object configInstance = getBean(def.getFactoryName());
instance = def.getFactoryMethod().invoke(configInstance, args);

自此我们完成了创建Bean的实例,并且实现了强依赖的注入,接下来便是对Bean进行初始化的内容,也就是弱依赖的注入