preface

In the last article we talked about how to implement an SPI that supports key-value pairs. How do you implement an SPI with an interceptor

What is an interceptor

Intercepting a method or field before it is accessed and then adding some action before or after it

What is an interceptor chain

Refers to connecting interceptors in a chain in a certain order. When accessing the intercepted method or field, interceptors in the interceptor chain are called in the order they were previously defined

Implement interceptor logic

The core idea of this paper: the use of responsibility chain + dynamic proxy

Define the interceptor interface

public interface Interceptor {

  int HIGHEST_PRECEDENCE = Integer.MIN_VALUE;

  int LOWEST_PRECEDENCE = Integer.MAX_VALUE;

  Object intercept(Invocation invocation) throws Throwable;

  default Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }


  int getOrder(a);

}
Copy the code

2. Customize class interface annotations that need to be intercepted by interceptors

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Intercepts {
  Signature[] value();
}

Copy the code
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({})
public @interfaceSignature { Class<? > type();String method(a); Class<? >[] args(); }Copy the code

3. Implement interceptor invocation logic through JDK dynamic proxy

public class Plugin implements InvocationHandler {

  private final Object target;
  private final Interceptor interceptor;
  private finalMap<Class<? >, Set<Method>> signatureMap;private Plugin(Object target, Interceptor interceptor, Map
       
        , Set
        
         > signatureMap)
        
       > {
    this.target = target;
    this.interceptor = interceptor;
    this.signatureMap = signatureMap;
  }

  public static Object wrap(Object target, Interceptor interceptor) { Map<Class<? >, Set<Method>> signatureMap = getSignatureMap(interceptor); Class<? > type = target.getClass(); Class<? >[] interfaces = getAllInterfaces(type, signatureMap);if (interfaces.length > 0) {
      return Proxy.newProxyInstance(
          type.getClassLoader(),
          interfaces,
          new Plugin(target, interceptor, signatureMap));
    }
    return target;
  }

  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      Set<Method> methods = signatureMap.get(method.getDeclaringClass());
      if(methods ! =null && methods.contains(method)) {
        Invocation invocation = new Invocation(target, method, args);
        return interceptor.intercept(invocation);

      }
      return method.invoke(target, args);
    } catch (Exception e) {
      throwExceptionUtils.unwrapThrowable(e); }}private staticMap<Class<? >, Set<Method>> getSignatureMap(Interceptor interceptor) { Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);if (interceptsAnnotation == null) {
      throw new PluginException("No @Intercepts annotation was found in interceptor "+ interceptor.getClass().getName()); } Signature[] sigs = interceptsAnnotation.value(); Map<Class<? >, Set<Method>> signatureMap =new HashMap<>();
    for (Signature sig : sigs) {
      Set<Method> methods = signatureMap.computeIfAbsent(sig.type(), k -> new HashSet<>());
      try {
        Method method = sig.type().getMethod(sig.method(), sig.args());
        methods.add(method);
      } catch (NoSuchMethodException e) {
        throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: "+ e, e); }}return signatureMap;
  }

  private staticClass<? >[] getAllInterfaces(Class<? > type, Map<Class<? >, Set<Method>> signatureMap) { Set<Class<? >> interfaces =new HashSet<>();
    while(type ! =null) {
      for(Class<? > c : type.getInterfaces()) {if (signatureMap.containsKey(c)) {
          interfaces.add(c);
        }
      }
      type = type.getSuperclass();
    }
    return interfaces.toArray(new Class<?>[interfaces.size()]);
  }

}

Copy the code

4. Construct the interceptor chain

public class InterceptorChain {

  private final List<Interceptor> interceptors = new ArrayList<>();


  public Object pluginAll(Object target) {
    if(CollectionUtil.isNotEmpty(interceptors)){
      for(Interceptor interceptor : getInterceptors()) { target = interceptor.plugin(target); }}return target;
  }

  public InterceptorChain addInterceptor(Interceptor interceptor) {
    interceptors.add(interceptor);
    return this;
  }

  public List<Interceptor> getInterceptors(a) {
    List<Interceptor> interceptorsByOrder = interceptors.stream().sorted(Comparator.comparing(Interceptor::getOrder).reversed()).collect(Collectors.toList());
    returnCollections.unmodifiableList(interceptorsByOrder); }}Copy the code

5. Bind to the SPI previously implemented through the interceptor chain

@Activate
@Slf4j
public class InterceptorExtensionFactory implements ExtensionFactory {

    private InterceptorChain chain;


    @Override
    public <T> T getExtension(final String key, final Class<T> clazz) {
        if(Objects.isNull(chain)){
            log.warn("No InterceptorChain Is Config" );
            return null;
        }
        if (clazz.isInterface() && clazz.isAnnotationPresent(SPI.class)) {
            ExtensionLoader<T> extensionLoader = ExtensionLoader.getExtensionLoader(clazz);
            if(! extensionLoader.getSupportedExtensions().isEmpty()) {if(StringUtils.isBlank(key)){
                   return (T) chain.pluginAll(extensionLoader.getDefaultActivate());
               }
               return(T) chain.pluginAll(extensionLoader.getActivate(key)); }}return null;
    }

    public InterceptorChain getChain(a) {
        return chain;
    }

    public InterceptorExtensionFactory setChain(InterceptorChain chain) {
        this.chain = chain;
        return this;
    }

Copy the code

Example demonstrates

Define an interceptor and specify the class interface method to intercept

@Intercepts(@Signature(type = SqlDialect.class,method = "dialect",args = {}))
public class MysqlDialectInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        System.out.println("MysqlDialectInterceptor");
        return invocation.proceed();
    }

    @Override
    public int getOrder(a) {
        return HIGHEST_PRECEDENCE;
    }


    @Override
    public Object plugin(Object target) {
        if(target.toString().startsWith(MysqlDialect.class.getName())){
            return Plugin.wrap(target,this);
        }
        returntarget; }}Copy the code

2. Construct the interceptor chain and set it up in the SPI factory

  @Before
    public void before(a){
        InterceptorChain chain = new InterceptorChain();
        chain.addInterceptor(new DialectInterceptor()).addInterceptor(new MysqlDialectInterceptor()).addInterceptor(new OracleDialectInterceptor());
        factory = (InterceptorExtensionFactory) (ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getActivate("interceptor"));
        factory.setChain(chain);
    }
Copy the code

3, test,

    @Test
    public void testMysqlDialectInterceptor(a){
        SqlDialect dialect = factory.getExtension("mysql",SqlDialect.class);
        Assert.assertEquals("mysql",dialect.dialect());
    }
Copy the code

You can see from the console output that the interceptor call logic has been implemented

conclusion

See this interceptor implementation, sharp-eyed friends will find that you are not copy Mybatis interceptor implementation. That’s true, but I prefer to shamefully call it applying what you learn. The implementation of mybatis interceptor is quite clever, because we normally implement interceptor chain calls in a recursive way, but MyBatis uses dynamic proxy.

Of course, the interceptor of this article also added some Easter eggs, such as adding the custom execution order function that the original Mybatis interceptor does not provide. The native MyBatis interceptor only intercepts Executor, ParameterHandler, StatementHandler, ResultSetHandler. There is no such limitation in this article, but one caveat is that because the interceptor implementation is based on JDK dynamic proxies, custom annotation classes can only be specified as interfaces, not implementations

The demo link

Github.com/lyb-geek/sp…