案例
下面的代码来自我们某一平台产品源码(Java语言)中(代码一
):
public class ServiceFactory {
private static ServiceFactory instance = new ServiceFactory();
public static ServiceFactory getInstance() {
return instance;
}
@Getter
@Setter
@Autowired
private AppTemplateDesignServie appTemplateDesignServie;
@Getter
@Setter
@Autowired
private AppTemplateExportServie appTemplateExportServie;
// 下面还有十多个Service对象注入,提供Getter与Setter,不再一一列出
}
再来看另一平台服务的代码(Java语言)(代码二
):
public WebConfig implements WebMvcConfigurer {
@Autowired
private AucInterceptor aucInterceptor;
@Autowired
private AuthInterceptor authInterceptor;
@override
public void addInterceptor(InterceptorRegistry registry) {
int order = 1;
InterceptorRegistration metricInterceptorRegistration = registry.addInterceptor(new MetricInterceptor());
metricInterceptorRegistration.addPathPatterns("/**");
metricInterceptorRegistration.order(order++);
InterceptorRegistration aucInterceptorRegistration = registry.addInterceptor(aucInterceptor);
aucInterceptorRegistration.addPathPatterns("/**");
aucInterceptorRegistration.order(order++);
InterceptorRegistration authInterceptorRegistration = registry.addInterceptor(authInterceptor);
authInterceptorRegistration.addPathPatterns("/**");
authInterceptorRegistration.order(order++);
}
}
代码一
的问题:
- ServiceFactory并不一个Factory,Factory的设计应该只生产对象,实际上它不产生对象,对象是注入的。
- ServiceFactory也不是一个合格的单例,缺少private构造方法, instance未声明为final。
- 出现了十多个类似的对象注入代码,那后面我加一个XXXService,是不是要再修改代码,若有几十个Service要使用,是不是这个类就膨胀了。
写出ServiceFactory目的是想解决由于@Autowired
只能用于Spring管理的Bean对象中,并不能用于其它对象中。当代码要在其它非Bean对象中随时调用XXXService时,ServiceFactory则提供了获取Spring管理的XXXService一种变通途径,只是这种途径没有使用到Spring提供的扩展机制。
代码二
的问题:
- 比较直观地发现,代码出现了重复(相似),但似乎也没有重复太多,一般人也能接受。
- 如果后面再增加一个Interceptor,那同样也要先采
@Autowired
注入,再在addInterceptor方法再增加三行相似代码。
上面两处代码都是基于Spring框架开发,不约而同的出现了重复(相似,冗余)。重复只是表象,背后则违背了开闭原则
:
- 没有做到:对扩展开放,对修改关闭。
- 扩展新的类似功能,需要修改此类的代码。
为什么上面集中且容易察觉到的重复(相似),我们不去消除它。因为写重复代码太容易了,传统消除重复的套路(提取函数)在上面的代码已不再能有效解决,更别说满足开闭原则
。若消除这类重复,则需要我们对Spring的IoC运作机制较深入了解。
代码一
的优化建议:
@Component
public class SpringContextUtil implements ApplicationContextAware {
private static ApplicationContext springContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
SpringContextUtil.springContext = applicationContext;
}
private static void checkContextNotNull() {
// ...
}
public static <T> T getBean(Class<T> interClazz) {
checkContextNotNull();
return springContext.getBean(interClazz);
}
public static <T> T getBean(Class<T> clazz, String name) {
checkContextNotNull();
return clazz.cast(springContext.getBean(name));
}
}
拿到ApplicationContext之后,我们就可以获取任一Bean对象了,消除了相似代码,也解决了违背开闭原则
的问题。
代码二
的优化建议:
@Component
public ServiceConfigurer implements WebMvcConfigurer {
@Autowired(required=false)
private List<Interceptor> interceptors;
@override
public void addInterceptor(InterceptorRegistry registry) {
if (CollectionUtils.isEmpty(interceptors)){
return;
}
int order = 1;
for(Interceptor interceptor: interceptors) {
InterceptorRegistration registration = registry.addInterceptor(interceptor);
registration.addPathPatterns("/**");
registration.order(order++);
}
}
}
通过采用List注入方式解决了扩展性问题,那又怎么解决不同Interceptor注册先后问题?Spring还提供一个@Order注解,只需要在各个类声明时,给它排个序,那List中注入的对象就具有先后顺序:
@Order(1)
@Component
public class AucInterceptor ....
背后的知识
Spring提供的IoC容器负责管理Bean对象的生命周期,其底层核心对象:
- BeanDefinition:每个bean对应一个BeanDefinition实例。BeanDefinition负责保存bean对象的所有必要信息,包括bean对象的class类型、构造方法和参数、属性等等。
- BeanDefinitionRegistry:抽象bean描述信息的注册逻辑。
- BeanFactory:抽象出了bean的管理逻辑,各个BeanFactory的实现类就具体承担了bean的创建以及管理工作。
BeanFactory只是IoC容器的一种基础实现,它默认采用延迟初始化策略:只有当访问容器中的某个对象时,才对该对象进行初始化和依赖注入操作。而在实际场景下,我们更多的使用另外一种类型的容器(ApplicationContext),它构建在BeanFactory之上,除了具有BeanFactory的所有能力之外,还提供对事件监听机制以及国际化的支持等。它管理的bean,在容器启动时全部完成初始化和依赖注入操作。
在bean生命周期的不同阶段,Spring提供了不同的扩展点来观察与改变bean的命运。bean的整个生命周期简述如下:
在实例化和初始化Bean对象过程中,提供了一些生命周期回调方法:
- 容器的启动阶段:提供BeanFactoryPostProcessor接口,允许我们在容器实例化相应Bean对象之前,对Bean进行预处理。比如我们经常使用到
@Value("${jdbc.url}")
来获取配置属性,则是PropertyPlaceholderConfigurer作为BeanFactoryPostProcessor来对属性占位替换。 - 对象实例化阶段:提供BeanPostProcessor接口,允许我们在Bean对象初始化之前/之后进行观察与处理。
除了这些,还有如下常用生命周期回调,本文就不再一一展开了,按他们的名称我们也就大概知道做什么(取好名字多么重要):
- ApplicationContextAwareProcessor
- InitDestroyAnnotationBeanPostProcessor
- InstantiationAwareBeanPostProcessor
- CommonAnnotationBeanPostProcessor
- AutowiredAnnotationBeanPostProcessor
- RequiredAnnotationBeanPostProcessor
- BeanValidationPostProcessor
除了生命周期回调方法,还提供XXXAware
系列接口,Aware字面义即可感知的,意味着实现了这个接口能够感知并获取到对应的对象组件。常见有:
- BeanFactoryAware :感知BeanFactory
- ApplicationContextAware : 感知ApplicationContext,再次感慨Spring的命名规范性
- EnvironmentAware
- EmbeddedValueResolverAware
- ResourceLoaderAware
- ApplicationEventPublisherAware
- MessageSourceAware
Spring还提供Bean与Bean间的消息通信机制。当一个Bean处理完了一个任务以后,可以通知另一个Bean做出相应的处理,这是我们就需要让另一个Bean监听当前Bean所发送的事件。Spring加载过程中也大量采用此事件机制。事件处理核心概念主要涉及如下:
- 定义事件:事件继承ApplicationEvent
- 发布事件:调用ApplicationContext.pushEvent方法发布事件
- 监控事件:实现ApplicationListener或采用@EventListener接收事件
@Order
注解,底层对应提供了Ordered
这个接口,是使用了策略模式,用来处理相同接口实现类的优先级问题。默认实现是在DefaultListableBeanFactory.resolveMultipleBeans
方法会对依赖注入的对象进行排序处理。
Spring的IoC知识内容非常多,不可能全都深入了解,但我们可以简单记注几个接口名称后辍:XXXRegistry,XXXFactory,XXXProcessor,XXXAware, XXXListener。不一定要记得细节,当你需要使用到某个能力时,就按后辍名去搜索找到我们需要的扩展机制。
DI与IoC
一提到Spring的Bean管理,我们也会提到两个词:DI与IoC。Spring是Bean的IoC容器,我们使用Bean时需要DI,初学者容易混淆他们。
- DI:依赖注入,将类的依赖通过外部注入进来。
- IoC:控制反转,将类的对象创建交给框架来配置。
- 关系:不同角度描述,依赖注入则站在使用者角度,来说明被注入的对象依赖于IoC容器给配置依赖对象;控制反转是站在管理者角度,来说明你需要的依赖由我来配置。
IoC是一种设计思想,意味着将你设计好的对象交给框架容器控制,而不是传统的在你的对象内部直接控制。为何是反转?因为由容器帮我们查找及注入依赖对象,对象只是被动的接受依赖对象,所以是反转。
IoC对编程带来的最大改变不是从代码上,而是从思想上,发生了“主从换位”的变化。 IoC很好的体现了面向对象设计法则之—-好莱坞法则:“别找我们,我们找你”,即由IoC容器帮对象找相应的依赖对象并注入,而不是由对象主动去找。
IoC的意义是为了解除耦合,IoC创建对象的控制权进行转移,以前创建对象的主动权和创建时机是由自己把控的,而现在这种权力转移到IoC容器。你要什么对象,它就给你什么对象。有了IoC容器,依赖关系就变了,原先的依赖关系就没了,它们都依赖IoC容器了,通过IoC容器来建立它们之间的关系。
回到案例代码,消除重复需要理解Spring的运行机制,其进一步所思考的是如何学会IoC的本质。控制反转,让框架来帮我们完成依赖注入,而不是我们主动显示依赖其它对象,破坏代码的稳定性。
如何学习
本文提到的Spring IoC知识只是点到为止,市面上编程、框架介绍这类书籍都通常非常的厚,我们哪些时间去学习啊;甚至通篇大段的代码讲解,让我对学习失去了新鲜感。我们每个人都想掌握更多的知识,把代码写得更简练。面对编程中的各种体系,我们怎么去学习?学习有个金字塔理论:
- 听讲:两周以后学习的内容只能留下5%
- 阅读:可以保留10%
- 声音、图片:可以记住20%
- 示范:可以记住30%
- 小组讨论:可以记住50%
- 做中学:可以记住75%
- 教别人:可以记住90%
后面几种效果高的学习方式,都是团队学习、主动学习与参与式学习。结合编程应用上述理论:
- 我们应该积极参与代码Review,这不仅仅是Committer的工作。因为Review不仅可以学习其它同学是怎么写代码,当遇到不同或更好的写法,我们可以及时交流;还可以搜索相应的知识,举一反三地深入了解其背后的原理。
- 个人不建议平时花时间无目的地去看什么编程、框架之类的书籍,而是当工作中遇到问题,针对问题去主动寻求答案。正如案例代码一的写法,当你写一到三个对象注入时,可能不会想到优化。当写到七到八个时,那就应该想想有没有更好的办法。采取顺藤摸瓜式的思考,面向搜索学习:Spring怎么管理Bean的->是否有Bean查询接口->怎么拿到查询接口->是否有扩展点……刚开始可能完全没有背景知识毫无思路,但如果不走出第一步,也将永远停留在原地。
- 平时注意总结与分享,分享不一定要搞个正式会议,形式可以多样。当你去考虑如何教别人时,就会先去了解更多的相关知识,也会在总结中有更多的思考,在讲解的过程中有更深的体会。这样日积月累下来,将会有越来越多的收获。
在工作中 不断学习,将是程序员不断提升的不二法门。
结语
写出干净的代码,需要我们对所使用的框架机制有较深入的了解,一是可以避免出现框架已有机制没有使用出现重复与复杂的代码; 二是可以避免当出现问题时两手抓狂不知如何定位。在工作中带着问题,带着思考有目的地去学习。学习框架的运行机制,以及其应用的原理、设计原则,将会使自己从编码的纯体力劳动解放出来,发现更多有趣的新技术与技巧。把这些再应用到工作中,解决现实的问题,也将收获编程带来的满足感,快乐感。