ginkgo 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
ginkgo spi 错误