Spi extension loading mechanism

 

 I. Overview

       We all know Java's SPI mechanism: (service provider interface) For a detailed overview of the mechanism, please Baidu. In fact, Spi is simply provided for use and extension by developers of service providers (in fact, it is a way of interface programming + strategy mode + configuration file).

       Scenario: If an interface A in a jar package has three implementations of interface A: B, C, D, when we use the implementation of interface A elsewhere, then we have to hardcode to specify the corresponding implementation Class, in order to decouple we can use the Spi mechanism to solve this problem.

 

    Conventions for this Spi extension loading mechanism:

           1. Only scan the configured spi service interface files in the META-INF/services directory.

           2. The name of the spi service interface file in the META-INF/services directory must be the same as the specified spi interface name.

           3. The content package of the spi service interface file in the META-INF/services directory contains the class name of the implementation of the corresponding interface.

           4. The spi service interface must contain the @Spi annotation.

           5. The @SpiMeta annotation is optional on the spi service interface implementation class.

          6. The @Spi annotation can specify whether the implementation class is singleton mode or prototype mode (default is prototype mode).

          7. The getExtension(String name) method is used to obtain the corresponding service instance (if the service implementation class contains the @SpiMeta annotation and the name is not empty, it is obtained with this name value, otherwise it is obtained according to the class name of the service implementation).

        8. The prototype mode or the singleton mode can be specified through the @Spi annotation.

        

 

2. Schematic diagram of directory structure

     src

      --- com.shotdog.panda.core.registry.RegistryFactory

     ----com.shotdog.panda.core.registry.support.ZookeeperRegistryFactory

    META-INF

    ------------- services

   -------------------------com.shotdog.panda.core.registry.RegistryFactory(内容:com.shotdog.panda.core.registry.support.ZookeeperRegistryFactory) 

                              

 

package com.shotdog.panda.core.extension;

import com.shotdog.panda.core.common.annotation.Spi;
import com.shotdog.panda.core.common.annotation.SpiMeta;
import com.shotdog.panda.core.common.enums.Scope;
import com.shotdog.panda.core.exception.PandaFrameworkException;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;

/***
 * Spi extension loader
 * jdk spi enhancement
 * @date 2017-10-31 21:42:04
 */
public class ExtensionLoader<T> {

    private final static Logger log = LoggerFactory.getLogger(ExtensionLoader.class);

    private final static String PREFIX = "META-INF/services/";

    private Class<T> type;

    private ClassLoader classLoader;

    private volatile transient boolean init;

    private ConcurrentHashMap<String, Class<T>> extensions = new ConcurrentHashMap<String, Class<T>>();
    private ConcurrentHashMap<String, T> singletons;


    private static ConcurrentHashMap<Class<?>, ExtensionLoader<?>> extensionLoaders = new ConcurrentHashMap<Class<?>, ExtensionLoader<?>>();


    public ExtensionLoader(Class<T> type) {
        this(type, Thread.currentThread().getContextClassLoader());
    }

    public ExtensionLoader(Class<T> type, ClassLoader classLoader) {
        this.type = type;
        this.classLoader = classLoader;
    }


    /**
     * Get the extension loader
     *
     * @param clz
     * @param <T>
     * @return
     */
    public synchronized static <T> ExtensionLoader<T> getExtensionLoader(Class<T> clz) {

        checkInterface(clz);
        ExtensionLoader<?> extensionLoader = extensionLoaders.get(clz);
        if (extensionLoader != null) return (ExtensionLoader<T>) extensionLoader;

        return newExtensionLoader(clz);

    }


    /**
     * Create a new extension loader
     *
     * @param clz
     * @param <T>
     * @return
     */
    private static <T> ExtensionLoader<T> newExtensionLoader(Class<T> clz) {

        ExtensionLoader<?> extensionLoader = extensionLoaders.get(clz);
        if (extensionLoader != null) return (ExtensionLoader<T>) extensionLoader;

        ExtensionLoader<T> loader = new ExtensionLoader<T>(clz);
        extensionLoaders.putIfAbsent(clz, loader);
        return loader;

    }

    /**
     * Check if the interface is the interface loaded by the extension
     *
     * @param clz
     * @param <T>
     */
    private static <T> void checkInterface(Class<T> clz) {

        if (clz == null) {

            throw new PandaFrameworkException("extension loader service interface is not allow null");
        }

        if (!clz.isAnnotationPresent(Spi.class)) {

            throw new PandaFrameworkException(String.format("extension loader service interface has no (%s) annotation", Spi.class.getName()));
        }

    }


    /***
     * Get the extension loading service object
     * @param name
     * @return
     */
    public T getExtension(String name) {

        if (!init) {
            this.initExtensionLoader();
        }

        Class<T> clz = this.extensions.get(name);
        if (clz == null) return null;

        try {


            Spi spi = type.getAnnotation(Spi.class);
            if (spi.scope() == Scope.SINGLETON) {

                return this.newInstanceToSingleton(clz, name);

            } else {

                return clz.newInstance();

            }
        } catch (Exception e) {
            throw new PandaFrameworkException(String.format("class:(%) newInstance fail", clz.getName()));

        }


    }

    /**
     * Create a new singleton object
     *
     * @param clz
     * @param name
     * @return
     * @throws IllegalAccessException
     * @throws InstantiationException
     */
    private T newInstanceToSingleton(Class<T> clz, String name) throws IllegalAccessException, InstantiationException {

        synchronized (singletons) {

            T t = this.singletons.get(name);
            if (t != null) return t;

            t = clz.newInstance();

            this.singletons.putIfAbsent(name, t);
            return t;
        }
    }

    private void initExtensionLoader() {


        this.extensions = this.loadExtensions();
        this.singletons = new ConcurrentHashMap<String, T>();
        this.init = true;


    }

    private ConcurrentHashMap<String, Class<T>> loadExtensions() {

        String configFiles = PREFIX + this.type.getName();

        List<String> classNames = new ArrayList<String>();

        try {

            Enumeration<URL> url = null;
            if (this.classLoader == null) {
                url = ClassLoader.getSystemResources(configFiles);
            } else {
                url = this.classLoader.getResources(configFiles);
            }


            while (url.hasMoreElements()) {

                URL u = url.nextElement();

                this.parseUrl(u, classNames);

            }


        } catch (Exception e) {

            throw new PandaFrameworkException(String.format("extension loader loadExtensions :(%s) fail", configFiles), e);
        }


        return this.loadAllClasses(classNames);
    }

    /***
     * load all classes
     * @param classNames
     * @return
     */
    private ConcurrentHashMap<String, Class<T>> loadAllClasses(List<String> classNames) {
        ConcurrentHashMap<String, Class<T>> classes = new ConcurrentHashMap<String, Class<T>>();
        if (classNames == null || classNames.isEmpty()) return classes;

        for (String className : classNames) {


            try {

                Class<T> clz = null;

                if (this.classLoader == null) {
                    clz = (Class<T>) Class.forName(className);
                } else {
                    clz = (Class<T>) Class.forName(className, false, this.classLoader);
                }

                this.checkExtensionType(clz);

                String name = this.getSpiName(clz);

                if (!classes.containsKey(name)) {

                    classes.putIfAbsent(name, clz);
                }
            } catch (Exception e) {

                throw new PandaFrameworkException(String.format("extension loader load class:(%s)  fail", className));
            }

        }


        return classes;
    }

    /**
     * Get the spi service name
     *
     * @param clz
     * @return
     */
    private String getSpiName(Class<T> clz) {

        SpiMeta spiMeta = clz.getAnnotation(SpiMeta.class);
        if (spiMeta != null && StringUtils.isNotBlank(spiMeta.name())) {
            return spiMeta.name();
        }

        return clz.getSimpleName();
    }

    /***
     * Check the extension service class type
     * @param clz
     */
    private void checkExtensionType(Class<T> clz) {


        // Check if it is a public type
        if (!Modifier.isPublic(clz.getModifiers())) {
            throw new PandaFrameworkException(String.format("class :(%s) is not public type class", clz.getName()));
        }

        // Check if it is the spi service interface implementation class

        if (!type.isAssignableFrom(clz)) {
            throw new PandaFrameworkException(String.format("class :(%s) is not assignable from interface ", clz.getName(), type.getName()));
        }
        // Check if there is a parameterless default constructor

        Constructor<?>[] constructors = clz.getConstructors();

        for (Constructor constructor : constructors) {

            if (constructor.getParameterTypes().length == 0) {
                return;
            }
        }


        throw new PandaFrameworkException(String.format("class :(%s) is has not default constructor method ", clz.getName()));


    }

    /**
     * parse each url
     *
     * @param url
     * @param classNames
     */
    private void parseUrl(URL url, List<String> classNames) throws IOException {

        InputStream inputStream = null;
        BufferedReader reader = null;
        try {

            inputStream = url.openStream();
            reader = new BufferedReader(new InputStreamReader(inputStream, "utf-8"));

            while (this.parseReadLine(reader, classNames) > 0) ;

        } catch (Exception e) {

            throw new PandaFrameworkException("extension loader parseUrl fail");
        } finally {


            try {

                if (reader != null) reader.close();
                if (inputStream != null) inputStream.close();

            } catch (Exception e) {
                // ignore
            }
        }

    }

    /**
     * read each line
     *
     * @param reader
     * @param classNames
     * @return
     * @throws IOException
     */
    private int parseReadLine(BufferedReader reader, List<String> classNames) throws IOException {

        String line = reader.readLine();
        if (line == null) return -1;

        int index = line.indexOf("#");
        if (index >= 0) line = line.substring(0, index);

        line = line.trim();

        int length = line.length();
        if (length > 0) {

            if (line.indexOf(' ') >= 0 || line.indexOf('\t') > 0) {

                throw new PandaFrameworkException(String.format("Syntax error:(%s)", line));
            }

            // check initials
            int codePoint = line.codePointAt(0);
            if (!Character.isJavaIdentifierStart(codePoint)) {

                throw new PandaFrameworkException(String.format("Syntax error:(%s)", line));
            }

            // Loop to check if the content is valid

            for (int i = Character.charCount(codePoint); i < length; i += Character.charCount(codePoint)) {

                codePoint = line.codePointAt(i);
                if (!Character.isJavaIdentifierPart(codePoint) && codePoint != '.') {

                    throw new PandaFrameworkException(String.format("Syntax error:(%s)", line));
                }

                if (!classNames.contains(line)) {
                    classNames.add(line);
                }


            }


        }


        return length + 1;
    }


}

 Second, the test code

 

 spi interface

   

@Spi
public interface EchoService {

    String echo(String name);
}
 

 

SPI service interface implementation class

@SpiMeta(name = "hello")
public class HelloEchoService implements EchoService {
    public String echo(String name) {
        return "hello " + name;
    }
}

   

   Create a file named: com.shotdog.panda.test.extension.EchoService in the META-INF/services/ directory 

The content is: com.shotdog.panda.test.extension.HelloEchoService

 

The test code is as follows:

 

EchoService hello = ExtensionLoader.getExtensionLoader(EchoService.class).getExtension("hello");
        String shotdog = hello.echo("shotdog");

        log.info(shotdog);

 

 

Guess you like

Origin http://10.200.1.11:23101/article/api/json?id=326607438&siteId=291194637