简介
学习的是廖雪峰老师的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的根路径开启

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; }
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; }
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); } }); }
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()); 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("}")) { int n = key.indexOf(':'); if (n == (-1)) { String k = key.substring(2, key.length() - 1); return new PropertyExpr(k, null); } else { 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>就很合适:
<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) { converters.put(String.class, s -> s); converters.put(boolean.class, s -> Boolean.parseBoolean(s)); converters.put(Boolean.class, s -> Boolean.valueOf(s)); converters.put(int.class, s -> Integer.parseInt(s)); converters.put(Integer.class, s -> Integer.valueOf(s)); 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 { String name;
Class<?> beanClass;
Object instance = null;
Constructor<?> constructor;
String factoryName;
Method factoryMethod;
int order;
boolean primary;
String initMethodName; String destroyMethodName;
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,在多个同类型的时候选择这个主类注入
所以我们首先写出一个找出所有类型的方法
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()) { return null; } if (defs.size() == 1) { return defs.get(0); } List<BeanDefinition> primaryDefs = defs.stream().filter(def -> def.isPrimary()).collect(Collectors.toList()); if (primaryDefs.size() == 1) { return primaryDefs.get(0); } if (primaryDefs.isEmpty()) { throw new NoUniqueBeanDefinitionException(String.format("Multiple bean with type '%s' found, but no @Primary specified.", type.getName())); } else { throw new NoUniqueBeanDefinitionException(String.format("Multiple bean with type '%s' found, and multiple @Primary specified.", type.getName())); } }
|
上面定义好了简单的方法,下面要开始注入所有的BeanDefinition的信息。
首先,我们要使用以前定义的scan扫描指定包下的所有Class,然后返回Class的名字
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); 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 scan = ClassUtils.findAnnotation(configClass, ComponentScan.class); 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 -> { String name = res.name(); if (name.endsWith(".class")) { return name.substring(0, name.length() - 6).replace("/", ".").replace("\\", "."); } return null; }); classNameSet.addAll(classList); }
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<?> clazz = null; try { clazz = Class.forName(className); } catch (ClassNotFoundException e) { throw new BeanCreationException(e); } Component component = ClassUtils.findAnnotation(clazz, Component.class); if (component != null) { String beanName = ClassUtils.getBeanName(clazz); var def = new BeanDefinition( beanName, clazz, getSuitableConstructor(clazz), getOrder(clazz), clazz.isAnnotationPresent(Primary.class), null, null, ClassUtils.findAnnotationMethod(clazz, PostConstruct.class), ClassUtils.findAnnotationMethod(clazz, PreDestroy.class)); addBeanDefinitions(defs, def); Configuration configuration = ClassUtils.findAnnotation(clazz, Configuration.class); if (configuration != null) { 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 = method.getAnnotation(Bean.class); if (bean != null) { Class<?> beanClass = method.getReturnType(); var def = new BeanDefinition( ClassUtils.getBeanName(method), beanClass, factoryBeanName, method, getOrder(method), method.isAnnotationPresent(Primary.class), bean.initMethod().isEmpty() ? null : bean.initMethod(), bean.destroyMethod().isEmpty() ? null : bean.destroyMethod(), null, null); addBeanDefinitions(defs, def); } } }
|
完成bean的实例化
在拿到所有的beanDefinition之后,我们便可以开始完成bean的实例化了,即在ioc容器中完成new objct这一步。
根据分析bean的依赖注入有四种模式
对于构造方法和工厂方法这种类型的,因为在bena的实例化过程中,一定需要用到依赖注入,所以这种我们称之为强依赖。
而对于3,4这两种方法的,我们称之为弱依赖,因为可以先new再将属性注入进去,但是在1,2中new的过程就需要注入了
- 构造方法注入,注入到参数中国
@Component public class Hello { JdbcTemplate jdbcTemplate; public Hello(@Autowired JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } }
|
- 工厂方法注入注入到bean的参数中,一般不会写@Autowire
@Configuration public class AppConfig { @Bean Hello hello(@Autowired JdbcTemplate jdbcTemplate) { return new Hello(jdbcTemplate); } }
|
- Setter方法注入
@Component public class Hello { JdbcTemplate jdbcTemplate;
@Autowired void setJdbcTemplate(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } }
|
- 字段注入(最常用)
@Component public class Hello { @Autowired JdbcTemplate jdbcTemplate; }
|
思路
我们先解决强依赖的问题,在上一节中,我们获得了所有bean的资料,这时我们需要先检测循环依赖的问题。对于检测是否重复的问题,我们一般选择的是使用数据结构set集合。
我们创建一个set集合用来记住bean的名字,因为名字是多一对应的。
我们创建一个方法createBeanAsEarlySingleton()来检测bean循环以及做早期的实例创建
public Object createBeanAsEarlySingleton(BeanDefinition def) { if (!this.creatingBeanNames.add(def.getName())) { throw new UnsatisfiedDependencyException(); } ... }
|
对于接下来的实例化,我们会选择先实例化带@Configuration的工厂类,因为只有实例化工厂类之后,接下来bean的内容才有机会完全实例化。
使用流式来过滤list< BeanDifination >中的集合,找到带@Configuration的工厂,并且实例化它,实例化完工厂之后,才有机会实例化其余的内容
this.beans.values().stream() .filter(this::isConfigurationDefinition).sorted().map(def -> { createBeanAsEarlySingleton(def); return def.getName(); }).collect(Collectors.toList());
List<BeanDefinition> defs = this.beans.values().stream() .filter(def -> def.getInstance() == null) .sorted().collect(Collectors.toList());
defs.forEach(def -> { if (def.getInstance() == null) { createBeanAsEarlySingleton(def); } });
|
接下来完善createBeanAsEarlySingleton的内容,我们从上可以知道,无论是普通bean还是工厂bean的创建,都是调用createBeanAsEarlySingleton完成的,所以我们的createBeanAsEarlySingleton方法两种情况都要支持
- 工厂bean,工厂bean的构造方法参数不能使用@Autowired,否则会导致代理的失效以及其他问题,且工厂bean的参数要带有@value或者@Autowired之间的一个
- 注意优先使用工厂bean创建bean,这也符合我们在日常使用spring时的方式,因为在工厂中我们会对bean进行配置。
- 同时在bean未初始化时候要注意递归调用
然后使用反射完成方法的创建方法的调用
instance = def.getConstructor().newInstance(args);
Object configInstance = getBean(def.getFactoryName()); instance = def.getFactoryMethod().invoke(configInstance, args);
|
自此我们完成了创建Bean的实例,并且实现了强依赖的注入,接下来便是对Bean进行初始化的内容,也就是弱依赖的注入