开发SPI时不要犯这个错误
您的大多數(shù)代碼都是私有的,內(nèi)部的,專(zhuān)有的,并且永遠(yuǎn)不會(huì)公開(kāi)。 在這種情況下,您可以放輕松–您可以重構(gòu)所有錯(cuò)誤,包括那些可能導(dǎo)致API更改中斷的錯(cuò)誤。
但是,如果要維護(hù)公共API,則不是這種情況。 如果您要維護(hù)公共SPI( 服務(wù)提供商接口 ),那么情況就更糟了。
H2觸發(fā)SPI
在最近的有關(guān)如何使用jOOQ實(shí)現(xiàn)H2數(shù)據(jù)庫(kù)觸發(fā)器的 Stack Overflow問(wèn)題中,我再次遇到了org.h2.api.Trigger SPI –一種實(shí)現(xiàn)觸發(fā)器語(yǔ)義的簡(jiǎn)單且易于實(shí)現(xiàn)的SPI。 觸發(fā)器在H2數(shù)據(jù)庫(kù)中的工作方式如下:
使用扳機(jī)
CREATE TRIGGER my_trigger BEFORE UPDATE ON my_table FOR EACH ROW CALL "com.example.MyTrigger"實(shí)施觸發(fā)器
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 {} }整個(gè)H2觸發(fā)器SPI實(shí)際上相當(dāng)好用,通常您只需要實(shí)現(xiàn)fire()方法。
那么,這個(gè)SPI有什么問(wèn)題呢?
這是非常微妙的錯(cuò)誤。 考慮init()方法。 它具有一個(gè)boolean標(biāo)志,指示觸發(fā)器是在觸發(fā)事件之前還是之后觸發(fā),即UPDATE 。 如果突然之間,H2還支持INSTEAD OF觸發(fā)器怎么辦? 理想情況下,此標(biāo)志將被enum代替:
public enum TriggerTiming {BEFORE,AFTER,INSTEAD_OF }但是我們不能簡(jiǎn)單地引入這種新的enum類(lèi)型,因?yàn)閕nit()方法不應(yīng)不兼容地更改,從而破壞所有實(shí)現(xiàn)代碼! 使用Java 8,我們至少可以這樣聲明一個(gè)重載:
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);}這將允許新的實(shí)現(xiàn)處理INSTEAD_OF觸發(fā)器,而舊的實(shí)現(xiàn)仍將起作用。 但這感覺(jué)很毛,不是嗎?
現(xiàn)在,想象一下,我們還將支持ENABLE / DISABLE子句,并且希望將這些值傳遞給init()方法。 或者,也許我們想處理FOR EACH ROW 。 目前尚無(wú)法使用此SPI進(jìn)行此操作。 因此,我們將越來(lái)越多地實(shí)現(xiàn)這些重載,這些重載很難實(shí)現(xiàn)。 實(shí)際上,這已經(jīng)發(fā)生了,因?yàn)檫€有org.h2.tools.TriggerAdapter ,它與Trigger冗余(但與Trigger略有不同)。
那么,哪種方法更好呢?
SPI提供者的理想方法是提供“參數(shù)對(duì)象”,如下所示:
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 {} }如上例所示,使用適當(dāng)?shù)臈売镁嬉殉晒﹂_(kāi)發(fā)了Trigger.InitArguments 。 沒(méi)有客戶(hù)端代碼被破壞,并且如果需要,可以使用新功能。 另外,即使我們不需要任何參數(shù), close()和remove()也為將來(lái)的發(fā)展做好了準(zhǔn)備。
該解決方案的開(kāi)銷(xiāo)是每個(gè)方法調(diào)用最多分配一個(gè)對(duì)象,這不會(huì)造成太大的損失。
另一個(gè)示例:Hibernate的UserType
不幸的是,這個(gè)錯(cuò)誤經(jīng)常發(fā)生。 另一個(gè)著名的例子是Hibernate難以實(shí)現(xiàn)的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看起來(lái)很難實(shí)現(xiàn)。 也許您可以使某些工作很快完成,但是您會(huì)感到放心嗎? 你會(huì)認(rèn)為你做對(duì)了嗎? 一些例子:
- 從來(lái)沒(méi)有在nullSafeSet()也需要owner引用的情況嗎?
- 如果您的JDBC驅(qū)動(dòng)程序不支持按名稱(chēng)從ResultSet獲取值怎么辦?
- 如果需要在存儲(chǔ)過(guò)程的CallableStatement使用用戶(hù)類(lèi)型怎么辦?
此類(lèi)SPI的另一個(gè)重要方面是實(shí)現(xiàn)者可以向框架提供價(jià)值的方式。 在SPI中使用非void方法通常是一個(gè)壞主意,因?yàn)槟鷮⒂肋h(yuǎn)無(wú)法再更改方法的返回類(lèi)型。 理想情況下,您應(yīng)該具有接受“結(jié)果”的參數(shù)類(lèi)型。 上面的許多方法都可以用單個(gè)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) { ... }}// ... }另一個(gè)示例,SAX ContentHandler
在這里看看這個(gè)例子:
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缺點(diǎn)的一些示例:
- 如果在endElement()事件中需要元素的屬性怎么辦? 您必須自己記住它們。
- 如果您想在endPrefixMapping()事件中知道前綴映射uri怎么辦? 還是其他任何事件?
顯然,SAX針對(duì)速度進(jìn)行了優(yōu)化,并且在JIT和GC仍然較弱的時(shí)候針對(duì)速度進(jìn)行了優(yōu)化。 盡管如此,實(shí)現(xiàn)SAX處理程序并非易事。 部分原因是由于SPI難以實(shí)現(xiàn)。
我們不知道未來(lái)
作為API或SPI提供程序,我們根本不知道未來(lái)。 現(xiàn)在,我們可能認(rèn)為給定的SPI就足夠了,但是我們將在下一個(gè)次要版本中將其破壞。 否則我們不會(huì)破壞它,并告訴我們的用戶(hù)我們無(wú)法實(shí)現(xiàn)這些新功能。
通過(guò)以上技巧,我們可以繼續(xù)發(fā)展我們的SPI,而不會(huì)引起任何重大變化:
- 始終將唯一一個(gè)參數(shù)對(duì)象傳遞給方法。
- 總是返回void 。 讓實(shí)現(xiàn)者通過(guò)參數(shù)對(duì)象與SPI狀態(tài)進(jìn)行交互。
- 使用Java 8的default方法,或提供“空”默認(rèn)實(shí)現(xiàn)。
翻譯自: https://www.javacodegeeks.com/2015/05/do-not-make-this-mistake-when-developing-an-spi.html
總結(jié)
以上是生活随笔為你收集整理的开发SPI时不要犯这个错误的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 粘贴选项的快捷键(粘贴键的快捷方式)
- 下一篇: 小米笔记本电脑输入法怎么切换输入法设置在