当前位置: 首页 > 科技观察

Yoyo,这些代码有点臭,重构大法带你看看(SPI接口)

时间:2023-03-12 09:13:04 科技观察

本文转载自微信公众号《狼王编程》,作者狼王。转载本文请联系狼王编程公众号。如果说普通重构是为了消除代码的臭味,那么高级重构就是为了消除架构的臭味。组件需要重构。当你拿到公司老项目的旧代码,需要打开或者重构的时候,你就会头大,无从下手。之前一直都是这种状态,但是慢慢的熟悉了一些重构的思路和方法之后,就可以稍微舒服一些了。接下来开始讲重构,然后在重构中重点讲SPI接口。先给大家看一个最近重构的一个组件——分布式存储,使用SPI接口。好家伙,重构前的代码结构,所有的第三方存储都写在一个模块里,各种阿里云,腾讯云,华为云等等,这样的代码结构前期可能不需要经常扩容。开启时仍然可用。但是当一个新的需求来的时候,比如我遇到的:需要支持多云多账号的上传下载功能。这是因为不同账号的权限,安全认证等都是一样的,所以在某个时刻,提出了这个需求,就是你可以上传到你想上传到哪个云的任何一个账号。然后拿到代码,看架构。可以在此基础上完成需求,但是扩展起来很麻烦,而且代码会越来越繁琐,架构也会越来越复杂,不清晰。于是干脆借此机会重构,和其他同事一起讨论。我决定把模块分成SPI。好处是根据自己要用的东西引入相应的依赖,这样代码结构更清晰,后续更容易。展开!以下是重构后的总体架构:是不是更清晰了?哪怕某个云存储需要增加新的功能,或者需要兼容更多的云,也会变得更加容易。好吧,让我们开始谈论重构。什么是重构?重构就是通过调整程序代码来提高软件的质量和性能,使程序的设计模式和结构更加合理。提高软件的可扩展性和可维护性。重构最重要的思想是让普通的程序员也能写出优秀的程序。将优化代码质量的过程分解成一个个小步骤,让重构项目的巨大工作量变成修改变量名、提取函数、提取接口等简单的工作目标。作为一名普通的程序员,你可以通过实现这些容易实现的工作目标来提高自己的编码能力,加深对项目的理解,从而为最高层次的重构打下基础。而且,高层次的重构仍然是由无数个小目标组成的,而不是长期的、大规模的实施。重构的本质是极限编程的一部分,极限编程的完整实现可以最大化重构的价值。极限编程本身就提倡拥抱变化,增强适应性,所以将极限编程中的功能进行分解,以适应项目的需要和团队的现状,才是最好的操作方式。重构的重点是重复代码、函数太长、类太大、参数列表太长、发散变化、鸟枪修改、附件复杂、数据泥球、基本类型偏执、并行继承系统、冗余类等。一些常见的用过的或者比较基础的例子:我觉得一些基本的原则还是需要了解尽量避免创建Java对象过多过长尽量使用局部变量尽量使用StringBuilder和StringBuffer进行字符串拼接尽量减少变量的重复计算尝试在finally块中释放资源尝试缓存经常使用的对象,及时将不用的对象设置为null尝试考虑使用静态方法尝试在适当的地方使用单例尝试使用final修饰符以下是关于类和方法的优化:提取的重复代码冗长方法的分割,嵌套的条件分支,或循环递归的优化,常量的提取在类或继承系统中,提取继承系统中重复的属性和方法,传递给父类。今天的主角是SPI,请往下看SPI!什么是SPI?SPI的全称是ServiceProviderInterface。它是Java提供的一组API,由第三方实现或扩展。它可用于启用框架扩展和替换组件。它是一种服务发现机制,在ClassPath路径下的META-INF/services文件夹中搜索文件,并自动加载文件中定义的类。这种机制为许多框架扩展提供了可能性。比如Dubbo、JDBC中都使用了SPI机制。以下是SPI的机制流程。SPI实际上是一种基于接口编程+策略模式+配置文件组合实现的动态加载机制。系统设计的每一个抽象往往有许多不同的实现方案。在面向对象设计中,一般建议模块间基于接口进行编程,模块间不要硬编码实现类。一旦代码涉及到具体的实现类,就违反了可插拔性原则。如果需要更换一个实现,就需要修改代码。为了实现模块组装时不能在程序中动态指定,需要一种服务发现机制。SPI就是提供这样一种机制:一种为接口寻找服务实现的机制。有点类似于IOC的思想,就是把汇编的控制权移出程序。这种机制在模块化设计中尤为重要。所以SPI的核心思想就是解耦。SPI使用介绍要使用JavaSPI,一般需要遵循以下约定:当服务提供者提供接口的具体实现时,在META-INF/services目录下创建一个以接口全限定名命名的服务的jar包。文件,内容是实现类的完全限定名;接口实现类所在的jar包放在主程序的类路径下;主程序通过java.util.ServiceLoder动态加载实现模块,它会扫描META-INF/services目录下的配置文件找到实现类的完全限定名,并将该类加载到JVM中;SPI实现类必须携带无参构造函数;SPI使用场景一般适用于:调用方根据实际使用需要启用和扩展类,或者替换框架的实现策略。下面是比较常见的例子:数据库驱动加载接口实现类加载JDBC加载不同类型的数据库驱动日志门面接口实现类加载SLF4J加载不同提供者日志实现类SpringSpring被广泛使用的SPI,例如:Servlet3的ServletContainerInitializer实现.0规范,自动类型转换TypeConversionSPI(ConverterSPI,FormatterSPI)等DubboDubbo也使用SPI实现框架的扩展,但不支持Java提供的原生SPI。做封装让用户扩展实现Filter接口SPI简单例子先定义接口类packagecom.test.spi.learn;importjava.util.List;publicinterfaceSearch{publicListsearchDoc(Stringkeyword);}文档搜索实现packagecom.test.spi.learn;importjava.util.List;publicclassFileSearchimplementsSearch{@OverridepublicListsearchDoc(Stringkeyword){System.out.println("文件搜索"+关键字);returnnull;}}数据库搜索实现packagecom.test.spi.learn;importjava.util.List;publicclassDBSearchimplementsSearch{@OverridepublicListsearchDoc(Stringkeyword){System.out.println("数据库搜索"+关键字);returnnull;}}接下来可以在resources下新建一个META-INF/services/目录,然后新建一个接口完全限定名的文件:com.test.spi.learn.Search,添加我们需要的实现类使用com.test.spi.learn.FileSearchcom.test.spi.learn.DBSearch并编写测试方法packagecom.test.spi.learn;importjava.util.Iterator;importjava.util.ServiceLoader;publicclassTestCase{publicstaticvoidmain(String[]args){ServiceLoaders=ServiceLoader.load(Search.class);Iteratoriterator=s.iterator();while(iterator.hasNext()){Searchsearch=iterator.next();search.searchDoc("helloworld");}}}可以看到输出结果:文件搜索helloworld数据库搜索helloworldSPI原理分析通过查看源码ServiceLoader,整理一下,实现过程如下:应用程序调用ServiceLoader.load方法ServiceLoader.load方法首先新建一个ServiceLoader,并实例化该类中的成员变量,包括如下:loader(ClassLoader类型,classloader)acc(AccessControlContexttype,accesscontroller)providers(LinkedHashMap应用通过iterator接口获取对象实例ServiceLoader首先判断成员变量providers对象(LinkedHashMap有缓存,没有缓存直接返回,执行类的加载,实现如下:(1)读取META-INF/services/下的配置文件,获取names所有可以实例化的类,值得注意的是ServiceLoader可以跨jar包获取META-INF下的配置文件(2)通过反射方法Class.forName()加载类对象,并用实例实例化类()方法。(3)实例化后类缓存在providers对象中,(LinkedHashMap总结了使用SPI机制实现解耦的优点,让接口的定义和具体的业务实现分离,而不是耦合应用进程可以根据实际业务情况启用或替换特定组件缺点是不能按需加载,ServiceLoader虽然做懒加载,但是can只能通过遍历得到,即接口的所有实现类都必须加载并实例化。如果不想用一些实现类,或者一些类的实例化很耗时,还要加载实例化,造成浪费。获取某个实现类的方式不够灵活,只能以Iterator的形式获取,不能根据某个参数获取。将ServiceLoader类的实例用于多个并发多线程处理是不安全的。无法加载实现类时抛出非真正原因的异常,错误很难定位。看到上面这么多缺点,你肯定会想,为什么你还要用这些缺点呢?是的,在重构的过程中,SPI接口是一个非常有用的方式。当你需要扩展和适配的时候,越早使用,你就会越早受益,在合适的时间和合适的机会,你要有重构的勇气!好的。今天就这些,我会继续分享我的所学所想。希望我们一起走在成功的路上!