声明:本文属原创文章,首发于公号 程序员自学之道 ,转载请注明出处
开发实践中,对于开发一个 jar 包,很多人都只是完成功能,只要功能使用没问题,就算是完事了,但其实远远不够。当用户使用 jar 包的时候,可能会遇到以下这些问题:
因为经常会遇到这样的槽点,我在写公共组件包的时候会特别留心。
在这里我总结出了以下七点改进建议,如果你也要提供 jar 包给其他人使用,可以参考。 提升自己,方便他人。
作为一个公共的 jar 包,很多项目可能会使用到,如果你没有文档,那么每次有人要用的时候就会找你各种询问, 这样即浪费自己的时间也会浪费大家的时间 。而且用的人越多,你会发现,他们问的永远都是那几个问题: 这个怎么用?你支持多种实现方式,我要选择哪一种?如何申请使用?
如果你有一份简单文档就可以解决绝大多数的问题。
一份合格的文档应该包含如下内容:
一定要及时更新文档,如果有文档中没有说明的问题,用户找我们解决, 记得要将这个解决方法记录在常见问题中 ,为以后使用的人做参考。
其实一份文档,说到底是为 自己减轻工作量 。试想,如果天天有人因为一些“鸡毛蒜皮”的小事来各种问你,你又不得不花很多时间去沟通,有时沟通不好还会伤和气。提供一份文档,大家就都省事了。
如无必要,勿引依赖。若有必要引入,但是并非必须,记得使用 provided 。
例如,我们的 jar 包提供了快速整合 Spring 的功能,为此,我们需要添加 Spring 相关依赖,但是这个依赖是可选的,那么可以这样设置:
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.1.8.RELEASE</version> <scope>provided</scope> </dependency>
加上 provided 意味着 打包的时候不会将这个依赖加入到 jar 包中,而是需要使用者自己引入 。
一个小小的设置,带来的好处就是,如果这使用者不打算与 Spring 整合,那么他就不会间接地引入 Spring 的依赖了。这在一个大工程中相当重要,当一个项目中的外部依赖多了之后,外部依赖之间如果存在冲突,解决起来将会相当棘手。
不知道你有没有过这样的经历:引用了一个 jar 包,准备开始使用的时候,代码提示全是 var1, var2, var3 这种的,点进去一看,傻眼了:
这时 IDEA 还亲切地问你,要不要下载源码(Download Sources)看一下?你满心期待了点了 Download!结果:
下载不了来问我要不要下载?玩我?
试想一下,这时你的用户在用你的 jar 包的时候会不会也是这样吐槽。那么怎么解决呢?
其实很简单, 只要在 pom 文件中添加 maven-source-plugin 插件即可 。
<!--配置生成源码包--> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-source-plugin</artifactId> <version>3.0.1</version> <executions> <execution> <id>attach-sources</id> <goals> <goal>jar</goal> </goals> </execution> </executions> </plugin>
这样就可以在编译时添加源码包,当发布到maven仓库时,也会自动带上源码。用户在使用 IDEA 的时候也就可以直接下载并关联源码了。因为关联上源码,你写在上面的注释也可以被使用者看见, 这可比文档好用得多哦 !
Java8 的反射中添加了 Parameter 类,让我们能在程序运行期间通过反射获取到方法参数信息,包括参数名。但是需要 在程序编译的时候添加 -parameters 参数 。做为一个 jar 包,如果我们在编译的时候没有加这个参数,那么用户将 永远无法通过反射获取到参数名称 !这在某些场合下,可能会造成很大的不便。
其实,添加 -paramters 参数非常简单,我们只需要 在 pom 文件中添加 maven-compiler-plugin 插件,并且将 parameters 设置为 true 即可:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.0</version> <configuration> <source>${java.version}</source> <target>${java.version}</target> <parameters>true</parameters> <encoding>UTF-8</encoding> </configuration> </plugin>
做为一个公共 jar 包,我们是要对各个工程提供一个通用功能的,而这些 功能一旦提供出去,需要保证兼容性,否则每次升级都将困难重重 。
因此,我们应该与使用者订立“协议”,即通过接口订立协议,宣告“ 我给大家提供这些能力,并且为之负责,你们无需关注我的底层实现,只需要按照协议使用即可 ”。在接口注释中注明使用的场景和注意事项,因为我们前面添加了源码包,因此使用者可以直接关联并查看到我们写下的注释,例如:
更极致的做法是 我们只对接口负责 。 我们可以隐藏实现类(将实现类设置为包级私有的),然后通过工厂方法提供接口的实现 ,而不是让用户自己 new。
这样做之后,将来如果我们需要扩展,或者随着技术的升级,我们需要更换底层实现时,无需担心实现类中的兼容问题,只需要提供一个新的实现相同接口的实现类,让工厂方法返回新的实现即可。 而且旧的实现类,我们可以随时删除,减少历史包袱 !
包级私有的实现类:
每个 jar 包基本都会有自己的一些配置,这些配置如果初始化,也是有很多讲究。 我遇到最不靠谱的做法就是要求必须提供文件的绝对路径,甚至有些是只支持默认绝对路径不支持自定义!
因为遇到很多这样奇葩的包,因此在写 jar 包的时候都会特别留意。
总结起来,我们应该提供如下三种配置的初始化方式:
以上面的客户端为例,我们可以提供这样三个构造器:
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); } }
随着 SpringBoot 越来越流行,starter 这种配置方式让我们感受到原来整合第三方依赖可以这么方便。如果我们的 jar 包也支持 starter 肯定很酷。但是我一般会考虑到很多项目不是使用 SpringBoot 构建,而是传统的 Spring 项目,为了兼顾这些项目,其实我们可以采用 @EnableXxx 的模式,它与 starter 之间只是多了一个注解。我们只需要这么做:
以前面的事件客户端为例,可以这样做:
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.1.8.RELEASE</version> <scope>provided</scope> </dependency>
@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; }
/** * 事件客户端自动装配配置类 * * @author dadiyang * @since 2019-06-29 */ @Configuration public class EventClientConfiguration { @Bean public EventClient eventClient(Config config) { return EventClientFactory.createClient(config); } }
/** * 启用事件客户端模块 * * @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 包的时候,应该考虑以下七个点: