开发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 {

    @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

你可能感兴趣的:(开发SPI时不要犯这个错误)