资讯专栏INFORMATION COLUMN

【极致】一个 jar 包的自我修养

pumpkin9 / 1360人阅读

摘要:提升自己,方便他人。其实一份文档,说到底是为自己减轻工作量。总结总结起来,我们在提供一个通用包的时候,应该考虑以下七个点文档。支持的类反射获取参数名面向接口编程。一个注解,快速整合

声明:本文属原创文章,首发于公号程序员自学之道,转载请注明出处
遇到槽点

开发实践中,对于开发一个 jar 包,很多人都只是完成功能,只要功能使用没问题,就算是完事了,但其实远远不够。当用户使用 jar 包的时候,可能会遇到以下这些问题:

文档缺失,一个功能怎么用,往往需要花半天到一天的时候到处找负责人,一步步沟通,很浪费时间;

依赖冲突,我只是引用了一个用户认证包,结果把它支持的 SpringMVC、Jersey 和 Struts2 全引进来了;

方法完全不知道参数名,一个有三个参数的接口,我得对着文档才能知道它们分别是什么意思,没有文档就得找负责人沟通或者自己一个个猜了;

跟Spring整合很不友好,例如初始化配置强制要求文件全路径;

因为经常会遇到这样的槽点,我在写公共组件包的时候会特别留心。

在这里我总结出了以下七点改进建议,如果你也要提供 jar 包给其他人使用,可以参考。提升自己,方便他人。

文档

作为一个公共的 jar 包,很多项目可能会使用到,如果你没有文档,那么每次有人要用的时候就会找你各种询问,这样即浪费自己的时间也会浪费大家的时间。而且用的人越多,你会发现,他们问的永远都是那几个问题:这个怎么用?你支持多种实现方式,我要选择哪一种?如何申请使用?

如果你有一份简单文档就可以解决绝大多数的问题。

一份合格的文档应该包含如下内容:

一句话描述本模块的功能

快速开始,展示如何最简单地开始使用

注意事项及常见问题

负责人联系方式

一定要及时更新文档,如果有文档中没有说明的问题,用户找我们解决,记得要将这个解决方法记录在常见问题中,为以后使用的人做参考。

其实一份文档,说到底是为自己减轻工作量。试想,如果天天有人因为一些“鸡毛蒜皮”的小事来各种问你,你又不得不花很多时间去沟通,有时沟通不好还会伤和气。提供一份文档,大家就都省事了。

最小依赖

如无必要,勿引依赖。若有必要引入,但是并非必须,记得使用 provided

例如,我们的 jar 包提供了快速整合 Spring 的功能,为此,我们需要添加 Spring 相关依赖,但是这个依赖是可选的,那么可以这样设置:


    org.springframework
    spring-context
    5.1.8.RELEASE
    provided

加上 provided 意味着打包的时候不会将这个依赖加入到 jar 包中,而是需要使用者自己引入

一个小小的设置,带来的好处就是,如果这使用者不打算与 Spring 整合,那么他就不会间接地引入 Spring 的依赖了。这在一个大工程中相当重要,当一个项目中的外部依赖多了之后,外部依赖之间如果存在冲突,解决起来将会相当棘手。

附上源码

不知道你有没有过这样的经历:引用了一个 jar 包,准备开始使用的时候,代码提示全是 var1, var2, var3 这种的,点进去一看,傻眼了:

这时 IDEA 还亲切地问你,要不要下载源码(Download Sources)看一下?你满心期待了点了 Download!结果:

下载不了来问我要不要下载?玩我?

试想一下,这时你的用户在用你的 jar 包的时候会不会也是这样吐槽。那么怎么解决呢?

其实很简单,只要在 pom 文件中添加 maven-source-plugin 插件即可



    org.apache.maven.plugins
    maven-source-plugin
    3.0.1
    
        
            attach-sources
            
                jar
            
        
    

这样就可以在编译时添加源码包,当发布到maven仓库时,也会自动带上源码。用户在使用 IDEA 的时候也就可以直接下载并关联源码了。因为关联上源码,你写在上面的注释也可以被使用者看见,这可比文档好用得多哦

-parameters 参数

Java8 的反射中添加了 Parameter 类,让我们能在程序运行期间通过反射获取到方法参数信息,包括参数名。但是需要在程序编译的时候添加 -parameters 参数。做为一个 jar 包,如果我们在编译的时候没有加这个参数,那么用户将永远无法通过反射获取到参数名称!这在某些场合下,可能会造成很大的不便。

其实,添加 -paramters 参数非常简单,我们只需要在 pom 文件中添加 maven-compiler-plugin 插件,并且将 parameters 设置为 true 即可:


    org.apache.maven.plugins
    maven-compiler-plugin
    3.8.0
    
        ${java.version}
        ${java.version}
        true
        UTF-8
    
面向接口编程

做为一个公共 jar 包,我们是要对各个工程提供一个通用功能的,而这些功能一旦提供出去,需要保证兼容性,否则每次升级都将困难重重

因此,我们应该与使用者订立“协议”,即通过接口订立协议,宣告“我给大家提供这些能力,并且为之负责,你们无需关注我的底层实现,只需要按照协议使用即可”。在接口注释中注明使用的场景和注意事项,因为我们前面添加了源码包,因此使用者可以直接关联并查看到我们写下的注释,例如:

更极致的做法是我们只对接口负责我们可以隐藏实现类(将实现类设置为包级私有的),然后通过工厂方法提供接口的实现,而不是让用户自己 new。

这样做之后,将来如果我们需要扩展,或者随着技术的升级,我们需要更换底层实现时,无需担心实现类中的兼容问题,只需要提供一个新的实现相同接口的实现类,让工厂方法返回新的实现即可。而且旧的实现类,我们可以随时删除,减少历史包袱

包级私有的实现类:

多种配置传入方式

每个 jar 包基本都会有自己的一些配置,这些配置如果初始化,也是有很多讲究。我遇到最不靠谱的做法就是要求必须提供文件的绝对路径,甚至有些是只支持默认绝对路径不支持自定义!

因为遇到很多这样奇葩的包,因此在写 jar 包的时候都会特别留意。

总结起来,我们应该提供如下三种配置的初始化方式:

文件路径,必须支持 classpath: 前缀,代表从类路径中加载

InputStream,支持从流中读取

自定义的 Config 类,包含所有需要用到的配置项,并设置默认值

其中第三种,自定义的 Config 类,是最推荐的做法。

以上面的客户端为例,我们可以提供这样三个构造器:

RocketMqEventClient(Config config) {
    this.config = config;
    client = new RocketMqClient();
}

RocketMqEventClient(InputStream in) {
    init(in);
}

RocketMqEventClient(String filePath) {
    if (filePath == null || filePath.trim().isEmpty()) {
        throw new IllegalArgumentException("文件路径不能为空");
    }
    if (filePath.startsWith(CLASSPATH)) {
        // 从类路径中加载
        String path = filePath.replaceFirst(CLASSPATH, "");
        try (InputStream in = RocketMqEventClient.class.getClassLoader().getResourceAsStream(path)) {
            init(in);
        } catch (IOException e) {
            throw new IllegalArgumentException("配置文件读取失败: " + filePath, e);
        }
    } else {
        // 直接读取文件路径
        try (InputStream in = new FileInputStream(filePath)) {
            init(in);
        } catch (IOException e) {
            throw new IllegalArgumentException("配置文件读取失败: " + filePath, e);
        }
    }
}

private void init(InputStream in) {
    config = new Config(in);
    client = new RocketMqClient();
}

然后在工厂类中支持这几种参数类型:

/**
 * 事件客户端工厂
 *
 * @author huangxuyang
 * @since 2019-06-29
 */
public class EventClientFactory {
    /**
     * 创建默认的事件客户端
     *
     * @param config 各个配置项
     * @return 默认的事件客户端
     */
    public static EventClient createClient(Config config) {
        return new RocketMqEventClient(config);
    }

    /**
     * 创建默认的事件客户端
     *
     * @param in 配置文件输入流
     * @return 默认的事件客户端
     */
    public static EventClient createClient(InputStream in) {
        return new RocketMqEventClient(in);
    }

    /**
     * 创建默认的事件客户端
     *
     * @param filePath 配置文件路径,支持 classpath: 前缀
     * @return 默认的事件客户端
     */
    public static EventClient createClient(String filePath) {
        return new RocketMqEventClient(filePath);
    }
}
支持 Spring @Enable 模式

随着 SpringBoot 越来越流行,starter 这种配置方式让我们感受到原来整合第三方依赖可以这么方便。如果我们的 jar 包也支持 starter 肯定很酷。但是我一般会考虑到很多项目不是使用 SpringBoot 构建,而是传统的 Spring 项目,为了兼顾这些项目,其实我们可以采用 @EnableXxx 的模式,它与 starter 之间只是多了一个注解。我们只需要这么做:

引入 spring-context 依赖,注意加上 provided

在我们自定义的 Config 类的字段上使用 @Value 注解,自动从 Spring 上下文注入配置项

增加 XxxConfiguration 类,注册 Bean

增加 @EnableXxx 注解,并导入刚刚定义的配置类 @Import({Config.class, XxxConfiguration.class})

以前面的事件客户端为例,可以这样做:

引入 spring-context


    org.springframework
    spring-context
    5.1.8.RELEASE
    provided

为 Config 配置类的字段添加 @Value 注解

@lombok.Data
public class Config {
    @Value("${event.mq.namesrvaddr}")
    private String rocketMqNameSrvAddr;
    @Value("${event.mq.clientName}")
    private String rocketMqClientName;
    @Value("${event.mq.subject}")
    private String subject;
    @Value("${event.mq.pool.maxSize}")
    private int maxPoolSize;
}

添加 EventClientConfiguration 类

/**
 * 事件客户端自动装配配置类
 *
 * @author dadiyang
 * @since 2019-06-29
 */
@Configuration
public class EventClientConfiguration {
    @Bean
    public EventClient eventClient(Config config) {
        return EventClientFactory.createClient(config);
    }
}

添加 @EnableEventClient 注解

 /**
  * 启用事件客户端模块
  *
  * @author dadiyang
  * @since 2019-06-29
  */
 @Documented
 @Target(ElementType.TYPE)
 @Retention(RetentionPolicy.RUNTIME)
 @Import({Config.class, EventClientConfiguration.class})
 public @interface EnableEventClient {
 }

有了这个注解之后,使用者如果与 Spring 整合的话,只需要在带有 @Configuration 注解的类上标注 @EnableEventClient,然后就可以 @Autowired 自动注入我们的 EventClient 类了!

如果团队全部都使用 SpringBoot 进行开发,也可以提供一个 starter。

总结

总结起来,我们在提供一个通用 jar 包的时候,应该考虑以下七个点:

文档。节省沟通成本,你好我也好;

最小依赖。如无必要勿加依赖,可选依赖添加 provided;

附上源码。代码本身就是最好的文档,加上 maven-source-plugin 就搞定;

编译时加上 -parameters 参数。支持 Java8 的Parameter类反射获取参数名;

面向接口编程。对且只对契约负责,隐藏实现,方便将来更换实现又保持兼容性;

多种配置传入方式。让使用者以最方便的方式提供配置;

支持 Spring @Enable 模式。一个注解,快速整合 Spring;

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/75079.html

相关文章

  • 道器相融,由Angel论一个优秀机器学习平台的自我修养

    摘要:而道器相融,在我看来,那炼丹就需要一个好的丹炉了,也就是一个优秀的机器学习平台。因此,一个机器学习平台要取得成功,最好具备如下五个特点精辟的核心抽象一个机器学习平台,必须有其灵魂,也就是它的核心抽象。 *本文首发于 AI前线 ,欢迎转载,并请注明出处。 摘要 2017年6月,腾讯正式开源面向机器学习的第三代高性能计算平台 Angel,在GitHub上备受关注;2017年10月19日,腾...

    leo108 评论0 收藏0
  • 自由职业者的自我修养

    摘要:自由职业是一个令人向往的的职业。为了努力成为一个成功的自由职业者,牺牲了无数的休息时间,导致了身体过早的出现问题。 自由职业是一个令人向往的的职业。 如何成为自由职业者?自由职业的种类有很多, 自由撰稿人,网络作家, 自媒体,微商, 淘宝店, 个人站长, 懂技术的还可以自己接活,猪八戒上面有很多这种任务, 开源中国还开发了一个众包平台, 如果英语过硬的话还可以上国外的网站接活,有名的f...

    binaryTree 评论0 收藏0
  • 切图崽的自我修养-[ES6] 迭代器Iterator浅析

    摘要:任何数据结构只要部署接口,就可以完成遍历操作即依次处理该数据结构的成员。的遍历某个数据结构过程是这样的比如对进行遍历创建一个指针对象,指向当前数组的起始位置。 Iterator 这真是毅种循环 Iterator不是array,也不是set,不是map, 它不是一个实体,而是一种访问机制,是一个用来访问某个对象的接口规范,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署Ite...

    neu 评论0 收藏0
  • 切图崽的自我修养-[ES6] 迭代器Iterator浅析

    摘要:任何数据结构只要部署接口,就可以完成遍历操作即依次处理该数据结构的成员。的遍历某个数据结构过程是这样的比如对进行遍历创建一个指针对象,指向当前数组的起始位置。 Iterator 这真是毅种循环 Iterator不是array,也不是set,不是map, 它不是一个实体,而是一种访问机制,是一个用来访问某个对象的接口规范,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署Ite...

    springDevBird 评论0 收藏0

发表评论

0条评论

pumpkin9

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<