
Spring框架是最复杂的理解和学习框架之一。 大多数开发人员通过实际任务和Google慢慢学习它。 这种方法是无效的,因为它不能提供完整的图像,同时又很昂贵。
我想为您提供一种全新的春季学习方法。 它包含以下事实:一个人要经过一系列专门准备的教程并独立实现spring的功能。 这种方法的独特之处在于,除了对Spring的已研究方面有100%的了解外,它还极大地提高了Java Core(注释,反射,文件,泛型)。
这篇文章将为您带来难忘的体验,并使您感觉像是Pivotal开发人员。 逐步,您将使您的类bean并组织它们的生命周期(与真正的春季相同)。 您将实现的类是
BeanFactory , 
Component , 
Service , 
BeanPostProcessor , 
BeanNameAware , 
BeanFactoryAware , 
InitializingBean , 
PostConstruct , 
PreDestroy , 
DisposableBean , 
ApplicationContext , 
ApplicationListener , 
ContextClosedEvent 。
关于你自己的一点
我的名字叫Yaroslav,我是一位有4年经验的Java开发人员。 目前,我在EPAM Systems(SPB)工作,并深入研究了我们使用的技术。 我经常不得不应对春季问题,而且我在春季中看到可以成长的中间地带(每个人都非常了解Java,并且特定的工具和技术可以来去去)。
几个月前,我通过了Spring Professional v5.0认证(不参加课程)。 之后,我想到了如何教别人弹跳。 不幸的是,目前没有有效的教学方法。 大多数开发人员对框架及其功能都有很肤浅的想法。 从训练的角度来看,调试弹簧源太困难了,而且绝对没有效果(我对此有点喜欢)。 做10个项目? 是的,在某个地方您可以加深知识并获得许多实践经验,但是“幕后”的许多内容永远都不会在您面前出现。 阅读Spring in Action? 很酷,但是工作成本很高。 我已经完成了40%的工作(在准备认证过程中),但这并不容易。
最终理解某件事的唯一方法是自己开发它。 最近,我想到您可以带领一个人完成一个有趣的教程,该教程将监督其DI框架的开发。 其主要特征是该API将与正在研究的API一致。 这种方法的出色之处在于,除了对弹簧有深刻的理解(没有空格)之外,一个人还将在Java Core中获得大量的经验。 坦白说,我自己在撰写本文时在Spring和Java Core上学到了很多新东西。 让我们开始开发!
从头开始
因此,要做的第一件事是打开您喜欢的IDE并从头开始创建一个项目。 我们不会连接任何Maven或任何第三方库。 我们甚至都不会连接Spring依赖项。 我们的目标是开发与Spring API最相似的API,并自己实现。
在一个干净的项目中,创建2个主要包。 第一个包是您的应用程序( 
com.kciray ),并且在其中是
Main.java类。 第二个软件包是org.springframework。 是的,我们将复制原始spring的包结构,其类的名称及其方法。 有一个如此有趣的效果-当您创建自己的东西时,您自己的东西似乎开始变得简单易懂。 然后,当您在大型项目中工作时,您似乎会根据工件在此处创建所有内容。 这种方法对于整体理解系统,改进系统,修复错误,解决问题等具有非常积极的作用。
如果您有任何问题,可以
在这里进行工作。
创建一个容器
首先,设置任务。 假设我们有两个类
ProductFacade和
PromotionService 。 现在,假设您想将这些类彼此连接,但是这些类本身彼此之间并不了解(模式DI)。 我们需要一个单独的类来管理所有这些类并确定它们之间的依赖关系。 我们称它为容器。 让我们创建
Container类...尽管不,请耐心等待! Spring没有单个容器类。 我们有很多容器实现,所有这些实现都可以分为两种类型-bin工厂和上下文。 bin工厂创建bean并将它们链接在一起(依赖注入,DI),并且上下文执行的操作大致相同,此外还添加了一些其他功能(例如,对消息进行国际化)。 但是我们现在不需要这些附加功能,因此我们将与垃圾箱工厂一起工作。
创建一个新的
BeanFactory类,并将其放入
org.springframework.beans.factory包中。 让
Map<String, Object> singletons存储在此类内,其中bin的
id映射到bin本身。 向其添加
Object getBean(String beanName)方法,该方法按标识符提取bean。
 public class BeanFactory { private Map<String, Object> singletons = new HashMap(); public Object getBean(String beanName){ return singletons.get(beanName); } } 
请注意, 
BeanFactory和
FactoryBean是两个不同的东西。 第一个是垃圾箱工厂(容器),第二个是垃圾箱工厂,它位于容器内部,还生产垃圾箱。 工厂内工厂。 如果您对这些定义感到困惑,您可能还记得在英语中第二个名词是开头的名词,第一个名词是形容词。 在Bean 
Factory中,主要词是factory;在Factory 
Bean中 ,主要词是bean。
现在,创建
ProductService和
PromotionsService类。 
ProductService将从数据库中返回产品,但是在此之前,您需要检查是否有任何折扣(促销)适用于此产品。 在电子商务中,打折工作通常分配给单独的服务类别(有时分配给第三方Web服务)。
 public class PromotionsService { } public class ProductService { private PromotionsService promotionsService; public PromotionsService getPromotionsService() { return promotionsService; } public void setPromotionsService(PromotionsService promotionsService) { this.promotionsService = promotionsService; } } 
现在,我们需要使容器( 
BeanFactory )检测到我们的类,为我们创建它们,然后将其中一个注入另一个。 诸如
new ProductService()应位于容器内部,并由开发人员完成。 让我们使用最现代的方法(类扫描和注释)。 为此,我们需要使用
@Component创建一个
@Component批注( 
org.springframework.beans.factory.stereotype )。
 @Retention(RetentionPolicy.RUNTIME) public @interface Component { } 
默认情况下,程序运行时( 
RetentionPolicy.CLASS )不会将注释加载到内存中。 我们通过新的保留策略( 
RetentionPolicy.RUNTIME )更改了此行为。
现在,在
ProductService类之前和
PromotionService之前添加
@Component 。
 @Component public class ProductService {  
我们需要
BeanFactory扫描包( 
com.kciray )并在其中查找
@Component注释的
@Component 。 这个任务绝非易事。 Java Core中
没有现成的解决方案 ,我们将不得不自己做一个拐杖。 成千上万的弹簧应用程序通过此拐杖进行组件扫描。 您已经学到了可怕的真理。 您将必须从
ClassLoader提取
ClassLoader名,并检查它们
ClassLoader以“ .class”结尾,然后构建其全名并从中拉出类对象!
我想立即警告您,会有许多检查过的异常,因此请准备好包装它们。 但是首先,让我们决定我们想要什么。 我们要向
BeanFactory添加一个特殊方法,并在
Main调用它:
 
接下来,我们需要获取
ClassLoader 。 它负责加载类,并且提取非常简单:
 ClassLoader classLoader = ClassLoader.getSystemClassLoader(); 
您可能已经注意到,程序包之间用点分隔,文件用正斜杠分隔。 我们需要将批处理路径转换为文件夹路径,并获得类似
List<URL> (文件系统中可在其中搜索类文件的路径)。
 String path = basePackage.replace('.', '/');  
所以等一下! 
Enumeration<URL>不是
List<URL> 。 这是怎么回事? 哦,太恐怖了,这是
Iterator的旧版本,从Java 1.0开始可用。 这是我们必须处理的遗产。 如果可以使用for遍历
Iterable (所有集合都实现它),那么在
Enumeration的情况下,您必须通过
while(resources.hasMoreElements())和
nextElement()进行句柄绕过。 但是,还没有办法从集合中删除项目。 只有1996年,只有铁杆。 哦,是的,在Java 9中,他们添加了
Enumeration.asIterator()方法,因此您可以完成它。
让我们走得更远。 我们需要提取文件夹并逐一处理它们的内容。 将URL转换为文件,然后获取其名称。 这里应该注意,我们不会扫描嵌套的程序包,以免使代码复杂化。 您可以根据需要使任务复杂化并进行递归。
 while (resources.hasMoreElements()) { URL resource = resources.nextElement(); File file = new File(resource.toURI()); for(File classFile : file.listFiles()){ String fileName = classFile.getName(); 
接下来,我们需要获取不带扩展名的文件名。 在2018年的院子里,Java已经开发了文件I / O(NIO 2)多年了,但仍无法将扩展名与文件名分开。 我必须创建自己的自行车,因为 我们决定不使用诸如Apache Commons之类的第三方库。 让我们使用旧的祖父方式
lastIndexOf(".") :
 if(fileName.endsWith(".class")){ String className = fileName.substring(0, fileName.lastIndexOf(".")); } 
接下来,我们可以使用类的全名来获取类对象(为此,我们将类称为
Class ):
 Class classObject = Class.forName(basePackage + "." + className); 
好吧,现在我们的课程就在我们手中。 此外,仅突出显示其中带有
@Component批注的组件:
 if(classObject.isAnnotationPresent(Component.class)){ System.out.println("Component: " + classObject); } 
运行并检查。 控制台应该是这样的:
 Component: class com.kciray.ProductService Component: class com.kciray.PromotionsService 
现在我们需要创建我们的bean。 您需要执行类似
new ProductService() ,但是对于每个bean,我们都有自己的类。 Java中的反射为我们提供了一个通用解决方案(称为默认构造函数):
 Object instance = classObject.newInstance(); 
接下来,我们需要将此bean放入
Map<String, Object> singletons 。 为此,请选择Bean名称(其ID)。 在Java中,我们将变量称为类(仅首字母小写)。 这种方法也可以应用于bean,因为Spring是Java框架! 转换bin名称,使第一个字母变小,然后将其添加到地图中:
 String beanName = className.substring(0, 1).toLowerCase() + className.substring(1); singletons.put(beanName, instance); 
现在确保一切正常。 容器必须创建bean,并且必须按名称检索它们。 请注意,您的
instantiate()方法的名称和该
classObject.newInstance();方法的名称
classObject.newInstance(); 有共同的根源。 而且, 
instantiate()是bean生命周期的一部分。 在Java中,一切都是相互联系的!
 
还尝试实现
org.springframework.beans.factory.stereotype.Service批注。 它执行与
@Component完全相同的功能,但调用方式有所不同。 重点在于名称-您证明了类是服务,而不仅仅是组件。 这有点像概念上的打字。 在春季认证中,有一个问题“刻板印象是什么?” (列出的那些)。” 因此,刻板注解是
stereotype包中的那些。
填写属性
看下面的图,它显示了bean生命周期的开始。 在此之前,我们要做的是实例化(通过
newInstance()创建bean)。 下一步是豆的交叉注入(依赖注入,它也是控制反转(IoC))。 您需要遍历bean的属性,并了解需要注入哪些属性。 如果现在调用
productService.getPromotionsService() ,则会得到
null ,因为 依赖关系尚未添加。

首先,创建
org.springframework.beans.factory.annotation包,并向其中添加
@Autowired批注。 想法是标记与此批注相关的字段。
 @Retention(RetentionPolicy.RUNTIME) public @interface Autowired { } 
接下来,将其添加到属性中:
 @Component public class ProductService { @Autowired PromotionsService promotionsService;  
现在我们需要教我们的
BeanFactory查找这些注释并注入对它们的依赖关系。 为此添加一个单独的方法,并从
Main调用它:
 public class BeanFactory {  
接下来,我们只需要遍历
singletons映射中的所有bean,并为每个bean遍历其所有字段( 
object.getClass().getDeclaredFields()方法将返回所有字段,包括私有字段)。 并检查该字段是否具有
@Autowired批注:
 for (Object object : singletons.values()) { for (Field field : object.getClass().getDeclaredFields()) { if (field.isAnnotationPresent(Autowired.class)) { } } } 
接下来,我们需要再遍历所有垃圾箱,然后查看它们的类型-突然,这就是我们垃圾箱想要自己处理的类型。 是的,我们得到了三维循环!
 for (Object dependency : singletons.values()) { if (dependency.getClass().equals(field.getType())) { } } 
此外,当我们发现上瘾时,我们需要注入它。 您可能想到的第一件事就是直接使用反射来编写
promotionsService字段。 但是春天不是那样。 毕竟,如果该字段具有
private修饰符,则我们首先必须将其设置为
public ,然后写入我们的值,然后再次将其设置为
private (以保持完整性)。 听起来像个大拐杖。 代替大拐杖,让我们做一个小拐杖(我们将设置塞特的名称并称之为):
 String setterName = "set" + field.getName().substring(0, 1).toUpperCase() + field.getName().substring(1); 
现在运行您的项目,并确保在调用
productService.getPromotionsService()而不是
null ,返回了我们的bean。
我们实现的是按类型进行注入。 还有一个按名称的注入( 
javax.annotation.Resource注释)。 它的不同之处在于,将提取其名称,而不是字段的类型,并根据该名称-来自映射的依赖关系。 这里的一切都相似,甚至更简单。 我建议您进行实验并创建自己的bean,然后使用
@Resource注入它并扩展
populateProperties()方法。
我们支持知道其名称的咖啡豆

有时候,您需要在垃圾箱中获取他的名字。 这种需求并不经常出现,因为 从本质上讲,垃圾箱不应该彼此了解,它们是垃圾箱。 在Spring的第一个版本中,假定Bean是POJO(普通的旧Java对象,普通的旧Java对象),并且整个配置都呈现在XML文件中并与实现分开。 但是我们实现了此功能,因为名称注入是bin生命周期的一部分。
我们如何知道哪个豆想要知道他的名字是什么以及他不想什么? 首先想到的是制作一个
@InjectName类型的新注释,并将其雕刻到String类型的字段中。 但是这种解决方案太笼统了,它会让您无所事事(将此注释放置在不适当类型的字段(不是String)上,或尝试将名称注入同一类的多个字段中)。 还有另一种更精确的解决方案-用一个设置方法创建一个特殊的界面。 实现它的所有垃圾桶都具有其名称。 在
org.springframework.beans.factory包中创建
BeanNameAware类:
 public interface BeanNameAware { void setBeanName(String name); } 
接下来,让我们的
PromotionsService实施它:
 @Component public class PromotionsService implements BeanNameAware { private String beanName; @Override public void setBeanName(String name) { beanName = name; } public String getBeanName() { return beanName; } } 
最后,向bean工厂添加新方法。 这里的一切都很简单-我们遍历bin-singleton,检查bin是否实现了我们的接口,然后调用setter:
 public void injectBeanNames(){ for (String name : singletons.keySet()) { Object bean = singletons.get(name); if(bean instanceof BeanNameAware){ ((BeanNameAware) bean).setBeanName(name); } } } 
运行并确保一切正常:
 BeanFactory beanFactory = new BeanFactory(); beanFactory.instantiate("com.kciray"); beanFactory.populateProperties(); beanFactory.injectBeanNames();  
应该注意的是,在春天还有其他类似的接口。 我建议您
自己实现
BeanFactoryAware接口,该接口允许Bean接收到Bean工厂的链接。 它以类似的方式实现。
初始化Bean

假设您遇到一种情况,在注入依赖项(设置bin属性)后,需要执行一些代码。 简单来说,我们需要使垃圾箱具有初始化自身的能力。 另外,我们可以创建一个
InitializingBean接口,并在其中放置
void afterPropertiesSet()方法的签名。 该机制的实现与为
BeanNameAware接口提供的实现完全相同,因此解决方案在破坏者的控制之下。 马上练习并自己做:
添加后处理器
想象一下,您将取代第一批Spring开发人员。 您的框架正在不断发展,并且在开发人员中非常受欢迎,每天都会将信件发送给邮件,并要求添加一个或另一个有用的功能。 如果为每个这样的功能添加自己的接口并在bean的生命周期中对其进行检查,则该接口(生命周期)将被不必要的信息所阻塞。 取而代之的是,我们可以创建一个通用接口,该接口允许我们添加一些逻辑(绝对是任何逻辑,无论是检查注解,将bin替换为另一个bin,设置某些特殊属性,等等)。
让我们考虑一下此接口的作用。 它需要对bean进行一些后期处理,因此可以称为BeanPostProcessor。 但是,我们面临一个难题:何时应该遵循逻辑? 毕竟,我们可以在初始化之前执行它,但是我们可以在初始化之后执行它。 对于某些任务,第一种选择更好,对于其他任务则更好-第二种...怎么做?
我们可以同时启用这两个选项。 让一个后处理器带有两种逻辑,两种方法。 一个在初始化之前(在
afterPropertiesSet()方法之前
afterPropertiesSet()执行,另一个在初始化之后执行。 现在让我们考虑一下方法本身-它们应该具有哪些参数? 显然, 
Object bean本身( 
Object bean )必须在那里。 为了方便起见,除了bean之外,还可以传递此bean的名称。 您还记得垃圾箱本身不知道其名称。 而且,我们不想强制所有bean实现BeanNameAware接口。 但是,在后处理器级别,bean名称可能非常有用。 因此,我们将其添加为第二个参数。
后处理bean时,该方法应该返回什么? 让我们让它返回垃圾箱本身。 这为我们提供了超强的灵活性,因为您可以代替包裹对象的代理对象(并增加安全性)来代替bin。 或者您可以通过重新创建垃圾箱来完全返回另一个对象。 给开发人员很大的行动自由。 以下是设计界面的最终版本:
 package org.springframework.beans.factory.config; public interface BeanPostProcessor { Object postProcessBeforeInitialization(Object bean, String beanName); Object postProcessAfterInitialization(Object bean, String beanName); } 
接下来,我们需要向bean工厂添加一个简单处理器列表,并具有添加新处理器的功能。 是的,这是一个常规的ArrayList。
 
现在更改
initializeBeans方法,以便将后处理器考虑在内:
 public void initializeBeans() { for (String name : singletons.keySet()) { Object bean = singletons.get(name); for (BeanPostProcessor postProcessor : postProcessors) { postProcessor.postProcessBeforeInitialization(bean, name); } if (bean instanceof InitializingBean) { ((InitializingBean) bean).afterPropertiesSet(); } for (BeanPostProcessor postProcessor : postProcessors) { postProcessor.postProcessAfterInitialization(bean, name); } } } 
让我们创建一个小型的后处理器,该处理器简单地跟踪对控制台的调用并将其添加到我们的bean工厂中:
 public class CustomPostProcessor implements BeanPostProcessor { @Override public Object postProcessBeforeInitialization(Object bean, String beanName) { System.out.println("---CustomPostProcessor Before " + beanName); return bean; } @Override public Object postProcessAfterInitialization(Object bean, String beanName) { System.out.println("---CustomPostProcessor After " + beanName); return bean; } } 
 
现在运行并确保一切正常。 作为培训任务,创建一个后处理器,该处理器将提供
@PostConstruct (javax.annotation.PostConstruct)批注
@PostConstruct (javax.annotation.PostConstruct) 。 它提供了另一种初始化方式(植根于Java,而不是Spring)。 其本质是将注释放置在某个方法上,并且该方法将被称为标准弹簧初始化(InitializingBean)之前。
确保手动创建所有注释和包(甚至javax.annotation),请勿连接依赖项! 这将帮助您了解spring核心及其扩展(javax支持)之间的区别,并记住它。 这将在将来保留一种样式。
, 
@PostConstruct , - CommonAnnotationBeanPostProcessor. , .
, 
void close() BeanFactory . — 
@PreDestroy (javax.annotation.PreDestroy) , , . — 
org.springframework.beans.factory.DisposableBean , 
void destroy() . , , ( , ).
@PreDestroy + DisposableBean 
, . , .
程序员经常使用上下文一词,但并不是每个人都理解它的真正含义。现在,我们将一切整理妥当。正如我在文章开头所指出的,上下文是容器的实现,以及BeanFactory。但是,除了基本功能(DI)之外,它还增加了一些很酷的功能。这些功能之一是在容器之间发送和处理事件。文章太大了,内容开始被删节,因此我将上下文信息放在破坏者的下面。我们意识到背景. 
org.springframework.context , 
ApplicationContext . 
BeanFactory . , 
close() .
 public class ApplicationContext { private BeanFactory beanFactory = new BeanFactory(); public ApplicationContext(String basePackage) throws ReflectiveOperationException{ System.out.println("******Context is under construction******"); beanFactory.instantiate(basePackage); beanFactory.populateProperties(); beanFactory.injectBeanNames(); beanFactory.initializeBeans(); } public void close(){ beanFactory.close(); } } 
Main , , :
 ApplicationContext applicationContext = new ApplicationContext("com.kciray"); applicationContext.close(); 
, . 
close() , « » - . , :
 package org.springframework.context.event; public class ContextClosedEvent { } 
ApplicationListener , . , ( 
ApplicationListener<E> ). , Java-, . , , :
 package org.springframework.context; public interface ApplicationListener<E>{ void onApplicationEvent(E event); } 
ApplicationContext . 
close() , , . 
ApplicationListener<ContextClosedEvent> , 
onApplicationEvent(ContextClosedEvent) . , ?
 public void close(){ beanFactory.close(); for(Object bean : beanFactory.getSingletons().values()) { if (bean instanceof ApplicationListener) { } } } 
但是没有 . 
bean instanceof ApplicationListener<ContextClosedEvent> . Java. 
(type erasure) , <T> <Object>. , ? , 
ApplicationListener<ContextClosedEvent> , ?
, , . , , , , :
 for (Type type: bean.getClass().getGenericInterfaces()){ if(type instanceof ParameterizedType){ ParameterizedType parameterizedType = (ParameterizedType) type; } } 
, , , — . , :
 Type firstParameter = parameterizedType.getActualTypeArguments()[0]; if(firstParameter.equals(ContextClosedEvent.class)){ Method method = bean.getClass().getMethod("onApplicationEvent", ContextClosedEvent.class); method.invoke(bean, new ContextClosedEvent()); } 
ApplicationListener:
 @Service public class PromotionsService implements BeanNameAware, ApplicationListener<ContextClosedEvent> {  
, Main , , :
 
 结论
Baeldung , , . , . 30, . , Spring Core, , 
Core Spring 5.0 Certification Study Guide . , Java-.
Update 10/05/2018
« , ». , . , - , -. , .
:
Spring Container — [ ]
Spring AOP — [ ]
Spring Web — [ ]
Spring Cloud — [ ]