聲明:本文屬原創文章,首發於公號程序員自學之道,轉載請註明出處java
開發實踐中,對於開發一個 jar 包,不少人都只是完成功能,只要功能使用沒問題,就算是完事了,但其實遠遠不夠。當用戶使用 jar 包的時候,可能會遇到如下這些問題:程序員
文檔缺失,一個功能怎麼用,每每須要花半天到一天的時候處處找負責人,一步步溝通,很浪費時間;spring
依賴衝突,我只是引用了一個用戶認證包,結果把它支持的 SpringMVC、Jersey 和 Struts2 全引進來了;apache
方法徹底不知道參數名,一個有三個參數的接口,我得對着文檔才能知道它們分別是什麼意思,沒有文檔就得找負責人溝通或者本身一個個猜了;編程
跟Spring整合很不友好,例如初始化配置強制要求文件全路徑;maven
由於常常會遇到這樣的槽點,我在寫公共組件包的時候會特別留心。ide
在這裏我總結出瞭如下七點改進建議,若是你也要提供 jar 包給其餘人使用,能夠參考。提高本身,方便他人。this
做爲一個公共的 jar 包,不少項目可能會使用到,若是你沒有文檔,那麼每次有人要用的時候就會找你各類詢問,這樣即浪費本身的時間也會浪費你們的時間。並且用的人越多,你會發現,他們問的永遠都是那幾個問題:這個怎麼用?你支持多種實現方式,我要選擇哪種?如何申請使用?spa
若是你有一份簡單文檔就能夠解決絕大多數的問題。插件
一份合格的文檔應該包含以下內容:
必定要及時更新文檔,若是有文檔中沒有說明的問題,用戶找咱們解決,記得要將這個解決方法記錄在常見問題中,爲之後使用的人作參考。
其實一份文檔,說究竟是爲本身減輕工做量。試想,若是每天有人由於一些「雞毛蒜皮」的小事來各類問你,你又不得不花不少時間去溝通,有時溝通很差還會傷和睦。提供一份文檔,你們就都省事了。
如無必要,勿引依賴。如有必要引入,可是並不是必須,記得使用 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 包的時候都會特別留意。
總結起來,咱們應該提供以下三種配置的初始化方式:
其中第三種,自定義的 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);
}
}
複製代碼
隨着 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 包的時候,應該考慮如下七個點: