蘭陵N梓記

一指流沙,程序年华


  • 首页

  • 归档

  • 关于

  • 搜索
close

飞哥讲代码4:消除重复,需要了解框架机制

时间: 2020-06-05   |   分类: 技术     |   阅读: 4224 字 ~9分钟

案例

下面的代码来自我们某一平台产品源码(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的整个生命周期简述如下:

spring_bean_lifecycle

在实例化和初始化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查询接口->怎么拿到查询接口->是否有扩展点……刚开始可能完全没有背景知识毫无思路,但如果不走出第一步,也将永远停留在原地。
  • 平时注意总结与分享,分享不一定要搞个正式会议,形式可以多样。当你去考虑如何教别人时,就会先去了解更多的相关知识,也会在总结中有更多的思考,在讲解的过程中有更深的体会。这样日积月累下来,将会有越来越多的收获。

在工作中 不断学习,将是程序员不断提升的不二法门。

结语

写出干净的代码,需要我们对所使用的框架机制有较深入的了解,一是可以避免出现框架已有机制没有使用出现重复与复杂的代码; 二是可以避免当出现问题时两手抓狂不知如何定位。在工作中带着问题,带着思考有目的地去学习。学习框架的运行机制,以及其应用的原理、设计原则,将会使自己从编码的纯体力劳动解放出来,发现更多有趣的新技术与技巧。把这些再应用到工作中,解决现实的问题,也将收获编程带来的满足感,快乐感。

#软件开发# #java#
飞哥讲代码5:消除重复,需要搞点设计模式
飞哥讲代码3:简洁高效的线程安全
微信扫一扫交流

标题:飞哥讲代码4:消除重复,需要了解框架机制
作者:兰陵子
关注:lanlingthink(览聆时刻)
声明:自由转载-非商用-非衍生-保持署名(创作共享3.0许可证)

  • 文章目录
  • 站点概览
兰陵子

兰陵子

Programmer & Architect

164 日志
4 分类
57 标签
GitHub 知乎
  • 案例
    • 背后的知识
  • DI与IoC
  • 如何学习
  • 结语
© 2009 - 2022 蘭陵N梓記
Powered by - Hugo v0.101.0
Theme by - NexT
0%