开发SPI时不要犯这个错误
您的大多數代碼都是私有的,內部的,專有的,并且永遠不會公開。 在這種情況下,您可以放輕松–您可以重構所有錯誤,包括那些可能導致API更改中斷的錯誤。
但是,如果要維護公共API,則不是這種情況。 如果您要維護公共SPI( 服務提供商接口 ),那么情況就更糟了。
H2觸發SPI
在最近的有關如何使用jOOQ實現H2數據庫觸發器的 Stack Overflow問題中,我再次遇到了org.h2.api.Trigger SPI –一種實現觸發器語義的簡單且易于實現的SPI。 觸發器在H2數據庫中的工作方式如下:
使用扳機
CREATE TRIGGER my_trigger BEFORE UPDATE ON my_table FOR EACH ROW CALL "com.example.MyTrigger"實施觸發器
public class MyTrigger implements Trigger {@Overridepublic void init(Connection conn, String schemaName,String triggerName, String tableName, boolean before, int type)throws SQLException {}@Overridepublic void fire(Connection conn, Object[] oldRow, Object[] newRow)throws SQLException {// Using jOOQ inside of the trigger, of courseDSL.using(conn).insertInto(LOG, LOG.FIELD1, LOG.FIELD2, ..).values(newRow[0], newRow[1], ..).execute();}@Overridepublic void close() throws SQLException {}@Overridepublic void remove() throws SQLException {} }整個H2觸發器SPI實際上相當好用,通常您只需要實現fire()方法。
那么,這個SPI有什么問題呢?
這是非常微妙的錯誤。 考慮init()方法。 它具有一個boolean標志,指示觸發器是在觸發事件之前還是之后觸發,即UPDATE 。 如果突然之間,H2還支持INSTEAD OF觸發器怎么辦? 理想情況下,此標志將被enum代替:
public enum TriggerTiming {BEFORE,AFTER,INSTEAD_OF }但是我們不能簡單地引入這種新的enum類型,因為init()方法不應不兼容地更改,從而破壞所有實現代碼! 使用Java 8,我們至少可以這樣聲明一個重載:
default void init(Connection conn, String schemaName,String triggerName, String tableName, TriggerTiming timing, int type)throws SQLException {// New feature isn't supported by defaultif (timing == INSTEAD_OF)throw new SQLFeatureNotSupportedException();// Call through to old feature by defaultinit(conn, schemaName, triggerName,tableName, timing == BEFORE, type);}這將允許新的實現處理INSTEAD_OF觸發器,而舊的實現仍將起作用。 但這感覺很毛,不是嗎?
現在,想象一下,我們還將支持ENABLE / DISABLE子句,并且希望將這些值傳遞給init()方法。 或者,也許我們想處理FOR EACH ROW 。 目前尚無法使用此SPI進行此操作。 因此,我們將越來越多地實現這些重載,這些重載很難實現。 實際上,這已經發生了,因為還有org.h2.tools.TriggerAdapter ,它與Trigger冗余(但與Trigger略有不同)。
那么,哪種方法更好呢?
SPI提供者的理想方法是提供“參數對象”,如下所示:
public interface Trigger {default void init(InitArguments args)throws SQLException {}default void fire(FireArguments args)throws SQLException {}default void close(CloseArguments args)throws SQLException {}default void remove(RemoveArguments args)throws SQLException {}final class InitArguments {public Connection connection() { ... }public String schemaName() { ... }public String triggerName() { ... }public String tableName() { ... }/** use #timing() instead */@Deprecatedpublic boolean before() { ... }public TriggerTiming timing() { ... }public int type() { ... }}final class FireArguments {public Connection connection() { ... }public Object[] oldRow() { ... }public Object[] newRow() { ... }}// These currently don't have any propertiesfinal class CloseArguments {}final class RemoveArguments {} }如上例所示,使用適當的棄用警告已成功開發了Trigger.InitArguments 。 沒有客戶端代碼被破壞,并且如果需要,可以使用新功能。 另外,即使我們不需要任何參數, close()和remove()也為將來的發展做好了準備。
該解決方案的開銷是每個方法調用最多分配一個對象,這不會造成太大的損失。
另一個示例:Hibernate的UserType
不幸的是,這個錯誤經常發生。 另一個著名的例子是Hibernate難以實現的org.hibernate.usertype.UserType SPI:
public interface UserType {int[] sqlTypes();Class returnedClass();boolean equals(Object x, Object y);int hashCode(Object x);Object nullSafeGet(ResultSet rs, String[] names, SessionImplementor session, Object owner) throws SQLException;void nullSafeSet(PreparedStatement st, Object value, int index, SessionImplementor session) throws SQLException;Object deepCopy(Object value);boolean isMutable();Serializable disassemble(Object value);Object assemble(Serializable cached, Object owner);Object replace(Object original, Object target, Object owner); }SPI看起來很難實現。 也許您可以使某些工作很快完成,但是您會感到放心嗎? 你會認為你做對了嗎? 一些例子:
- 從來沒有在nullSafeSet()也需要owner引用的情況嗎?
- 如果您的JDBC驅動程序不支持按名稱從ResultSet獲取值怎么辦?
- 如果需要在存儲過程的CallableStatement使用用戶類型怎么辦?
此類SPI的另一個重要方面是實現者可以向框架提供價值的方式。 在SPI中使用非void方法通常是一個壞主意,因為您將永遠無法再更改方法的返回類型。 理想情況下,您應該具有接受“結果”的參數類型。 上面的許多方法都可以用單個configuration()方法代替,例如:
public interface UserType {default void configure(ConfigureArgs args) {}final class ConfigureArgs {public void sqlTypes(int[] types) { ... }public void returnedClass(Class<?> clazz) { ... }public void mutable(boolean mutable) { ... }}// ... }另一個示例,SAX ContentHandler
在這里看看這個例子:
public interface ContentHandler {void setDocumentLocator (Locator locator);void startDocument ();void endDocument();void startPrefixMapping (String prefix, String uri);void endPrefixMapping (String prefix);void startElement (String uri, String localName,String qName, Attributes atts);void endElement (String uri, String localName,String qName);void characters (char ch[], int start, int length);void ignorableWhitespace (char ch[], int start, int length);void processingInstruction (String target, String data);void skippedEntity (String name); }此SPI缺點的一些示例:
- 如果在endElement()事件中需要元素的屬性怎么辦? 您必須自己記住它們。
- 如果您想在endPrefixMapping()事件中知道前綴映射uri怎么辦? 還是其他任何事件?
顯然,SAX針對速度進行了優化,并且在JIT和GC仍然較弱的時候針對速度進行了優化。 盡管如此,實現SAX處理程序并非易事。 部分原因是由于SPI難以實現。
我們不知道未來
作為API或SPI提供程序,我們根本不知道未來。 現在,我們可能認為給定的SPI就足夠了,但是我們將在下一個次要版本中將其破壞。 否則我們不會破壞它,并告訴我們的用戶我們無法實現這些新功能。
通過以上技巧,我們可以繼續發展我們的SPI,而不會引起任何重大變化:
- 始終將唯一一個參數對象傳遞給方法。
- 總是返回void 。 讓實現者通過參數對象與SPI狀態進行交互。
- 使用Java 8的default方法,或提供“空”默認實現。
翻譯自: https://www.javacodegeeks.com/2015/05/do-not-make-this-mistake-when-developing-an-spi.html
總結
以上是生活随笔為你收集整理的开发SPI时不要犯这个错误的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 粘贴选项的快捷键(粘贴键的快捷方式)
- 下一篇: _stat64获取错误_Log4j,St