您的大多数代码都是私有的,内部的,专有的,并且永远不会公开。 在这种情况下,您可以放轻松–您可以重构所有错误,包括那些可能导致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 {
@Override
public void init(
Connection conn,
String schemaName,
String triggerName,
String tableName,
boolean before,
int type
)
throws SQLException {}
@Override
public void fire(
Connection conn,
Object[] oldRow,
Object[] newRow
)
throws SQLException {
// Using jOOQ inside of the trigger, of course
DSL.using(conn)
.insertInto(LOG, LOG.FIELD1, LOG.FIELD2, ..)
.values(newRow[0], newRow[1], ..)
.execute();
}
@Override
public void close() throws SQLException {}
@Override
public 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 default
if (timing == INSTEAD_OF)
throw new SQLFeatureNotSupportedException();
// Call through to old feature by default
init(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 */
@Deprecated
public 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 properties
final 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