品Spring:bean定义上梁山

认真阅读,收获满满,向智慧又迈进一步。。。



技术不枯燥,先来点闲聊


先说点好事高兴一下。前段时间看新闻说,我国正式的空间站建设已在进行当中。下半年,长征五号B运载火箭将在海南文昌航天发射场择机将空间站核心舱发射升空。预计用2到3年将空间站建好。

虽然到时你们不让我上去,不过我也为这件事出不了什么力,算扯平了。哈哈,但是我还是会衷心的祝福你。

长征五号火箭首次采用5米大直径的箭体结构,总加注量达到780吨,起飞时共有10台发动机产生1078吨的推力,具备近地轨道25吨、地球同步转移轨道14吨的运载能力。

不要误会,本文不是讲火箭的,关键我也讲不了呀。但是我们可以分析下火箭的特点。自身体积和重量非常大,内部结构与实现极其复杂。能够运送的物品与自身比起来微乎其微,关键这玩意老贵老贵了。

因此SpaceX推出的猎鹰可回收火箭,从一开始就受到了全世界的关注。回收之后可以被重新利用,不但缩短了火箭发射的周期,平均下来每次发射的成本也少了很多。享受同样的服务,花更少的钱,不是所有人的最爱嘛。

回到程序,其实现在大部分程序员,包括我自己在内都应该属于2.0或3.0版本的程序员,因为我们的职业生涯都是从Spring开始的。在Spring之前Java企业级应用开发标准其实是EJB(企业级JavaBean)。

EJB只是一个标准或规范,由各大服务提供商来实现。EJB非常笨重而且用起来也痛苦,关键还很贵。其实和火箭的性质一样,自身的负担太重。这个问题必须要解决。

因此,Elon Musk认识到了这点,就开始着手研制可回收火箭。在当时,Rod Johnson也认识到了EJB特点并深受其害,于是发明了Spring,轻量级、免费开源,慢慢就流行起来了。

看到了吧,在工作或生活中,凡是遇到苦逼的地方,就说明这也是一个极有可能产生大成功的地方。这个秘密我可是告诉诸位了,能不能成功就看你们的了。哈哈。

其实Spring现在也已经变得足够复杂了。


要想渡人,必先渡己


易中天老师曾说过,能够制造工具标志着人类的诞生。目前人类建造的文明,哪一个是通过双手刨出来的,不都是通过工具建造出来的嘛。

比如我们盖高楼用的建材,不都是通过塔吊一点点吊上去的,随着建材的堆砌,楼越来越高。那么我们来思考一个问题,塔吊本身是如何上去的?

塔吊本身就是一个工具了,所以(一般情况下)不可能再有其它工具来帮它了。所以塔吊只能实现自我爬升,实际也是这样的,相信大多人都见过,速度很慢的。

我们都在用Windows操作系统,点点鼠标,敲敲键盘,用着很爽。但是在开发Windows系统本身时却是很苦逼的一件事。

我主要想表达两方面的意思:

如果有一天,我们能从工具的使用者变成设计者或制造者,那就厉害多了。

还有就是,一个工具在对外提供服务前,一定要解决好自身的搭建或构建问题。

Spring其实就是个工具,开发人员把业务代码写好后,把类作为bean注册到工具上,后续的运行时全部交由Spring这个工具接管,简直不要太爽。

当然,这也是Spring存在的价值和意义。但是不要忘了,建造Spring这个工具的那批人可就不爽了,为了“取悦”这个工具的使用者天天挖空心思。

所以“品Spring”这个系列文章,是想要逐步解密Spring这个工具的建造细节,而不是教怎么使用这个工具的。目前还不能熟练使用这个工具的小伙伴可就要加油了。

所以Spring这个工具应该优先考虑好自我的搭建,才能更好的为开发员人服务。

就像开发人员,必须有一个良好的家庭,吃饱喝足睡够之后,才能写出水平最高的代码。如果刚在家和老婆吵完架,那写出的代码应该都是bug的样子,哈哈。


相同的套路,相似的处理


编译器是编译代码的,但它本身也是代码。饭店的工作人员是服务客人吃饭的,但他们本身也要吃饭。指针是指向地址的,但它们本身也有地址。

描述业务的数据通常称为业务数据,描述业务数据的数据通常称为元数据。业务数据和元数据都是数据,只不过是取了两个更加细化的名字罢了。

上面描述的这些情形里面都包含了两方面的事物,可以理解为是“一前一后”的感觉。它们既不是相同也不是不同,而是相似。即处理方式相同,侧重点不同。

就以饭店为例吧,这个所有人都熟悉。给客人做的饭,要求色香味俱全,即当作艺术品来对待。员工自己吃的饭,要求味美可口简单快捷,即当作食物来对待。

因为客人和员工的要求不同,所以处理的侧重点、工序和用料不同,但是处理方式大致都一样,无非就是煎炒炸蒸煮等等。

相信都已经明白了,如果还不清楚,就想想盖商品房、盖集资房和盖职工宿舍有什么区别?本质上没有区别,而且还可能是同一个施工队干的。

回到程序,经常听到说做中间件开发的人都很厉害,写纯业务代码的好像都是菜鸟。怎么说呢,做中间件和做业务确实目的不同、侧重点不同、用到的技术不同,所以要求也不同。

写出代码的好坏只取决于个人水平,因为中间件代码和业务代码没有本质的区别,都是对数据的操作和传递,都是顺序分支循环的程序结构。

而且都是用同一个编译器编译,关键编译器才不管你是什么代码呢,反正都是Java代码,对吧,哈哈。

回到Spring,既然Spring能把业务开发人员写的业务类作为bean来对待(即注册bean定义),难道就不能把用来处理这些业务类的“系统类”(即Spring自身的类)也当作bean来对待吗?

答案是完全可以,而且实际也是这样做的。本来业务类和系统类就是人为划分的,本质上都是程序,没啥区别。

都作为bean来对待后,带来了一个非常好非常好的好处,就是统一了编程模型,这非常重要。

就是业务开发人员可以使用@Component/@Bean注册bean定义,使用@Autowired装配bean依赖。框架开发人员在开发Spring框架时也可以这样使用。

这样就模式统一、写法统一、思维统一,当然很爽了。

可以这样理解,在建造这个工具时,就已经可以享受这个工具带来的便利了。

如果无法体会到这种好处的,照例通过一个小事情说明下。

比如一群中国人,彼此协作的很好,突然来了一个老外,互相又听不懂对方,但是还要交流,只要让翻译在中间翻来覆去,是不是净瞎耽误工夫,肯定是不爽的。

当然,这个事情有时也是不成立的,比如老外是一个年轻漂亮的小姐姐,是吧,所以说任何事情都不是绝对的。

Spring统一了编程模型后,Spring自身的类和业务类都注册了bean定义,那这些bean定义间真的就完全一样吗?

答案当然是否定的,毕竟作用不同,肯定是有差别的,只是大小罢了。


身先士卒,冲锋陷阵


业务类注册bean定义的目的是为了将来运行这个bean实现业务逻辑的。那么它的bean定义是怎么注册的呢?显然是Spring为它注册的。

因此在Spring内部,有一个专门为其它类注册bean定义的接口,即BeanDefinitionRegistryPostProcessor接口:

public interface BeanDefinitionRegistryPostProcessor extends BeanFactoryPostProcessor {

    void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException;

}


接口只有一个方法,就是注册bean定义。

这个接口有一个非常重要的实现类,即ConfigurationClassPostProcessor类,它里面包含了所有Spring支持的注册bean定义方式的实现。

因Spring统一了编程模型,所以这个类也被注册了bean定义。又因为这个bean只有运行起来后才可以为其它类注册bean定义,所有这个类无论是注册bean定义还是运行,在时间轴上都是比较靠前的。

事实上,它是Spring注册的第一个bean定义,bean的类就是刚才提到的那个实现类,bean的名称是:

org.springframework.context.annotation.internalConfigurationAnnotationProcessor


说白了这个类就是用来处理那些使用注解标注的类,为它们注册bean定义的。所以这个类它自己的bean定义注册绝对不能再使用注解的方式,而是使用代码直接注册的,可以认为是写死的。

通过高铁乘务员和旅客的这种情况,可能会理解的更明白些。

1、乘务员是人,旅客也是人,他们都是人。框架里的类注册bean定义,业务类注册bean定义,它们都注册bean定义。

2、乘务员必须先到达,然后才能迎接旅客,在时间轴上要靠前些。框架里的类必须先注册bean定义,然后才能为其它类注册bean定义,在时间轴上同样也要靠前些。

3、乘务员是走工作人员通道进入,旅客是正常买票/检票进入,进入方式是不同的。框架里的类是直接通过写代码的方式注册的bean定义,业务类是通过标注解的方式注册的bean定义,注册bean定义的方式也是不同的。

所以慢慢就会发现,任何上层封装的很完美的代码,当一步步到达底层时,都会进行一些“特殊”的处理。这种现象并不只是程序代码里才有,在历史上一直都存在。

易中天品三国中讲过一个小故事,曹操领兵打仗时定了一条军规,凡是谁的马践踏了农田,是要杀头的。有一天不凑巧,曹操自己的马惊了,跳进了农田。

曹操喊来相关负责人,问他,“按规定应该怎么处置啊”?答曰:“应该杀头”。但这可是曹操啊,怎么能杀头呢。于是那个人又说:“身体发肤受之于父母,我们自己没有权力损坏”。

于是曹操就拔出剑,割下了自己的一缕头发,扔到了地上。这就相当于受到了非常严重的处罚,等于死过了。这不就是特殊处理嘛。

这也说明了,跟领导混的,必须得有几把刷子,毕竟古语道,伴君如伴虎啊。

扯得有点远了,再回到正题。

Spring为了使bean定义的处理更加灵活,还提供了一个特殊的接口,即BeanFactoryPostProcessor接口,也就是上面那个接口的父接口:

public interface BeanFactoryPostProcessor {

    void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException;

}


当执行到这个接口的时候,所有的bean定义其实都已经注册好了,所以这个接口的目的就是再给一次修改(或完善)bean定义的机会。

当这个接口执行完后,所有的bean定义就真的确定下来了,不会再变了。

如果对Spring不是很熟悉的,只要记住下面这个总结就行了:

这两个接口是特殊的,它们就是用来为其它类注册bean定义或修改已注册好的bean定义的。

这两个接口的实现类也会被注册bean定义,只不过在时间轴上会非常的靠前。

这两个接口可以有多个实现类,可以通过实现排序接口进行排序,以保证逻辑上先后顺序的正确性。

其实不懂这些也无所谓,不影响成为一个合格的程序员。


bean定义上梁山


108位梁山好汉上梁山的原因和过程各不相同,但最后都到了梁山。

bean定义的注册方式和方法也有好几种,但最后都能注册成功。

因为历史已经走到了注解和Java配置的时代,所以它们就是主角了。

使用注解注册bean定义

标有@Component注解的类

标有@Configuration注解的类

标有@Configuration注解的类里标有@Bean注解的方法返回的类

这种方式既可以用于业务类的注册,也可以用于Spring框架内部的类注册。

优点是简单方便,缺点是不够灵活。尤其很难根据一个条件判断来注册不同的bean定义。

使用XML配置注册bean定义

在标有@Configuration注解的类上,使用@ImportResource注解引入XML配置文件。

这种方式使在注解里也能使用XML。个人觉得除非是遗留系统,否则全用注解。毕竟时代潮流不会逆转。

使用@Import注解引入

在标有@Configuration注解的类上,使用@Import注解引入其它标有@Configuration注解的类。

这仅用于没有类路径扫描功能的项目代码,现在大都基于SpringBoot,这种方式已经用的很少了。

使用ImportSelector接口注册bean定义

public interface ImportSelector {

    String[] selectImports(AnnotationMetadata importingClassMetadata);

}


接口方法返回的是类名称的数组,这些类将会被注册bean定义。

在实现这个接口时,可以根据不同的情况返回不同的类名称,这就非常的灵活,实现了动态注册bean定义的需求。

这个接口用于Spring框架内部开发,或其它框架与Spring的整合对接。

典型用法是在一个特定功能注解上使用@Import注解引入这个接口的实现类。

这个接口方法的参数importingClassMetadata是最终标有这个特定功能注解且同时标有@Configuration注解的那个类。

请参考文章末尾示例代码。

使用ImportBeanDefinitionRegistrar接口注册bean定义

public interface ImportBeanDefinitionRegistrar {

    void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry);

}


这个接口方法没有返回值,所以需要自己写代码直接注册bean定义,当然也是非常灵活的。

在用法上和上面那个接口一模一样,包括接口方法参数的含义也一样。

除了这种用法,还可以把这个接口的实现类的类名称放入上面那个接口方法的返回数组中,照样是管用的。

这个接口的目的和上面那个接口也是一样的。

请参考文章末尾示例代码。

使用BeanDefinitionRegistryPostProcessor接口注册bean定义

这在上一小节已经介绍了,属于最基础的方式。使用它可以做到尽早的注册bean定义。

上面这几种就是Spring支持的bean定义的注册方式,有适合业务开发的,有适合Spring内置功能或其它框架与Spring整合的。

PS:随着本系列文章走下去,慢慢就会明白其它框架是如何与Spring整合的,其实不难。

示例代码:
https://github.com/coding-new-talking/taste-spring.git


(END)

 

作者是工作超过10年的码农,现在任架构师。喜欢研究技术,崇尚简单快乐。追求以通俗易懂的语言解说技术,希望所有的读者都能看懂并记住。下面是公众号和知识星球的二维码,欢迎关注!

 

       

posted on 2019-09-26 22:40  编程新说(李新杰)  阅读(1003)  评论(4编辑  收藏  举报