学习的是廖雪峰老师的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,也不能获取子包。
所以我们需要自己动手实现一个文件扫描的功能
首先创建一个文件类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 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
要注意/的处理,头或者尾的处理,以及不同系统的处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 public class ResourceResolver { ; String basePackage; public ResourceResolver (String basePackage) { ; this .basePackage = basePackage; } ; public List scan (Function mapper) { ; String basePackagePath = this .basePackage.replace("." , "/" ); String path = basePackagePath; try { ; List collector = new ArrayList <>(); scan0(basePackagePath, path, collector, mapper); return collector; } ; catch (IOException e) { ; throw new UncheckedIOException (e); } ; catch (URISyntaxException e) { ; throw new RuntimeException (e); } ; } ; void scan0 (String basePackagePath, String path, List collector, Function mapper) throws IOException, URISyntaxException { ; logger.atDebug().log("scan path: {}" , path); Enumeration 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); } ; void scanFile (boolean isJar, String base, Path root, List collector, Function 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种注入方式:
思路 @Value要支持配置文件的注入,所以要配置上扫描配置文件。将文件内容存入方便查找。 同时也要有一个字符的解析器,方便解析abc.xyz:defaultValue的内容来注入
解析的时候也要注意不带${}的注入即普通key的处理 同时也要注意对${key:${value}}这种的解析所以当我们解析拿到值之后还要调用一次处理值的方法
值得注意的是,真正的spring中涉及到了很复杂的语言解析,这部分的内容要靠专业知识去补充了,目前解析就支持到这里吧
部分实现 java自带着对key-value查询的Properties
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class PropertyResolver { ; Map properties = new HashMap <>(); public PropertyResolver (Properties props) { ; this .properties.putAll(System.getenv()); Set names = props.stringPropertyNames(); for (String name : names) { ; this .properties.put(name, props.getProperty(name)); } ; } ; } ;
我们先解析${abc.xyz:defaultValue}这种类型的key
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 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等类型的注入。我们既要实现类型转换,又不能写死,否则,将来支持新的类型时就要改代码
1 2 3 4 5 6 7 8 9 @Nullable public T getProperty (String key, Class targetType) { ; String value = getProperty(key); if (value == null ) { ; return null ; } ; return convert(targetType, value); } ;
对于类型转换,实际上是从String转换为指定类型,因此,用函数式接口Function<String, Object>就很合适:
1 2 3 4 5 6 7 8 T convert (Class clazz, String value) { ; Function fn = this .converters.get(clazz); if (fn == null ) { ; throw new IllegalArgumentException ("Unsupported value type: " + clazz.getName()); } ; return (T) fn.apply(value); } ;
各种要转换的类型要存放
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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的集合起来
1 2 3 4 5 public class YamlUtils { ; public static Map loadYamlAsPlainMap (String path) { ; return ... } ; } ;
创建BeanDefinition 在此之前,我们实现了ResourceResolver扫描Class,并且通过PropertyResolver获取配置,接下来就可以开始着手实现IOC了
思路 BeanDefinition是用来记录bean信息的类,作为IOC,有着管理所有bean,即实例的生命周期。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 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。一个典型的定义如下:
1 2 3 4 5 6 7 8 @Component public class Hello { ; @PostConstruct void init () { ;} ; @PreDestroy void destroy () { ;} ; } ;
对于@Configuration定义的@Bean方法,我们把它看作Bean的工厂方法,我们需要获取方法返回值作为Class类型,方法本身作为创建Bean的factoryMethod,然后收集@Bean定义的initMethod和destroyMethod标识的初始化于销毁的方法名,以及其他@Order、@Primary等信息。典型:
1 2 3 4 5 6 7 @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,在多个同类型的时候选择这个主类注入
所以我们首先写出一个找出所有类型的方法
1 2 3 4 5 6 7 8 9 List findBeanDefinitions (Class type) { ; return this .beans.values().stream() .filter(def -> type.isAssignableFrom(def.getBeanClass())) .sorted().collect(Collectors.toList()); } ; } ;
上述只是查找了所有的BeanDefinition,我们还要对多个bean@Primary的进行过滤
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Nullable public BeanDefinition findBeanDefinition (Class type) { ; List defs = findBeanDefinitions(type); if (defs.isEmpty()) { ; return null ; } ; if (defs.size() == 1 ) { ; return defs.get(0 ); } ; List 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的名字
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 public static A findAnnotation (Class target, Class annoClass) { ; A a = target.getAnnotation(annoClass); for (Annotation anno : target.getAnnotations()) { ; Class 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 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 classNameSet = new HashSet <>(); for (String pkg : scanPackages) { ; logger.atDebug().log("scan package: {}" , pkg); var rr = new ResourceResolver (pkg); List 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的拓展。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 Map createBeanDefinitions (Set classNameSet) { ; Map 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的方法名称和方法相关信息至少有一个是空的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 void scanFactoryMethods (String factoryBeanName, Class clazz, Map 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的过程就需要注入了
1 2 3 4 5 6 7 @Component public class Hello { ; JdbcTemplate jdbcTemplate; public Hello (@Autowired JdbcTemplate jdbcTemplate) { ; this .jdbcTemplate = jdbcTemplate; } ; } ;
工厂方法注入注入到bean的参数中,一般不会写@Autowire
1 2 3 4 5 6 7 @Configuration public class AppConfig { ; @Bean Hello hello (@Autowired JdbcTemplate jdbcTemplate) { ; return new Hello (jdbcTemplate); } ; } ;
1 2 3 4 5 6 7 8 9 @Component public class Hello { ; JdbcTemplate jdbcTemplate; @Autowired void setJdbcTemplate (JdbcTemplate jdbcTemplate) { ; this .jdbcTemplate = jdbcTemplate; } ; } ;
1 2 3 4 5 @Component public class Hello { ; @Autowired JdbcTemplate jdbcTemplate; } ;
思路 我们先解决强依赖的问题,在上一节中,我们获得了所有bean的资料,这时我们需要先检测循环依赖的问题。对于检测是否重复的问题,我们一般选择的是使用数据结构set集合。
我们创建一个set集合用来记住bean的名字,因为名字是多一对应的。
我们创建一个方法createBeanAsEarlySingleton()来检测bean循环以及做早期的实例创建
1 2 3 4 5 6 7 public Object createBeanAsEarlySingleton (BeanDefinition def) { ; if (!this .creatingBeanNames.add(def.getName())) { ; throw new UnsatisfiedDependencyException (); } ; ... } ;
对于接下来的实例化,我们会选择先实例化带@Configuration的工厂类,因为只有实例化工厂类之后,接下来bean的内容才有机会完全实例化。
使用流式来过滤list< BeanDifination >中的集合,找到带@Configuration的工厂,并且实例化它,实例化完工厂之后,才有机会实例化其余的内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 this .beans.values().stream() .filter(this ::isConfigurationDefinition).sorted().map(def -> { ; createBeanAsEarlySingleton(def); return def.getName(); } ;).collect(Collectors.toList()); List 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方法两种情况都要支持
然后使用反射完成方法的创建方法的调用
1 2 3 4 5 6 instance = def.getConstructor().newInstance(args); Object configInstance = getBean(def.getFactoryName());instance = def.getFactoryMethod().invoke(configInstance, args);
自此我们完成了创建Bean的实例,并且实现了强依赖的注入,接下来便是对Bean进行初始化的内容,也就是弱依赖的注入