Java SPI mechanism introduction and ServiceLoader application

1. What is SPI

The full name of SPI is Service Provider Interface. It is an API that is specifically implemented or extended by a third party. It can be used to enable framework extensions and replacement components, and provides a mechanism for finding the corresponding service class for an interface.

The overall mechanism is as follows:
Insert picture description here
(Image source: https://www.jianshu.com/p/46b42f7f593c)

Java SPI is actually a dynamic loading mechanism implemented by the combination of "interface-based programming + strategy mode + configuration file".

Implementation elements of Java SPI:

  • First define the service and interface name;
  • After different service providers have implemented the implementation class of the interface, they need to create a file named "interface fully qualified name" in the META-INF/services directory of the jar package, and the corresponding content is the fully qualified name of the implementation class;
  • The jar package where the interface implementation class is located is placed under the main program classpath;
  • When the service caller passes this interface, it imports the provider jar package, java.util.ServiceLoderdynamically loads the provider’s implementation module, scans the provider’s configuration file in the META-INF/services directory to find the fully qualified name of the implementation class, and then the class Load into the JVM;
  • The SPI implementation class must carry a construction method without parameters;

2 、 Java SPI demo

Take an example of the application of SPI.

Before applying Java SPI, suppose there is an mq message processing service:

public interface KafkaProcessService {
    
    
    void processOrderMsg(Object msg);

}

After we provide the service implementation, package it, assuming the version number is version=1.0; Team 1 uses the imported jar and directly calls the service method processOrderMsg().

After team 2 wants to call a payment message processing method, then we need to provide a corresponding implementation and increase the version number to version=2.0; team 2 uses the imported jar and calls the service method directly processPayMsg().

public interface KafkaProcessService {
    
    
    void processPayMsg(Object msg);
}

Imagine that if we add a new implementation, we have to upgrade a version, which will cause a lot of trouble.

How to use Java SPI to deal with this kind of trouble?
(1) First define the interface:

public interface KafkaProcessService {
    
    
    void processMsg(Object msg);
}

具体实现类
public class KafkaOrderProcessServiceImpl implements KafkaProcessService {
    
    
    @Override
    public void processMsg(Object msg) {
    
    
        System.out.println("Order kafka msg process");
    }
}

public class KafkaPayProcessServiceImpl implements KafkaProcessService {
    
    
    @Override
    public void processMsg(Object msg) {
    
    
        System.out.println("Pay kafka msg process");
    }
}


(2) Create a new /META_INF/services directory under the resources directory, and create a file with the same fully qualified name as the interface, and enter the fully qualified name of the interface corresponding to the two implementation classes in the file:

(3) Execution procedure:

public class Invoker {
    
    

    public static void main(String[] args) {
    
    
            ServiceLoader<KafkaProcessService> processServices = ServiceLoader.load(KafkaProcessService.class);
            for (KafkaProcessService s : processServices) {
    
    
                s.processMsg(new Object());
            }

        }
}

After running the project, you can see that both implementation classes are executed.

3. Application of Java SPI in the framework

Some people will say that the above example looks no different from the implementation of the local application strategy pattern. In fact, SPI has more application scenarios: Team 1 defines interfaces and can provide default implementations, Teams 2, 3...refer to interfaces, and can expand the implementation of interfaces by themselves.

Let's experience the application of SPI through specific examples.

3.1 JDBC loads different types of database drivers

The design of the JDBC database driver is to apply the Java SPI mechanism. First define a unified specification: java.sql.DriverMajor manufacturers (MySQL, Oracle) will implement their own drive logic according to this specification. When we use the JDBC client, we don't need to change the code, we directly import the corresponding jar package, and the actual call is implemented using the corresponding driver.

Analyze the source code:

After we introduce the MySQL driver, the java.sql.DriverManagerSPI mechanism will be used to load the specific driver implementation in the JDBC connection database :

public class DriverManager {
    
    
    // 省略部分代码
    static {
    
    
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }
   
   // 使用SPI机制初始化驱动
   private static void loadInitialDrivers() {
    
    
        // 省略部分代码
		    
        // 使用ServiceLoader找到实现Driver的接口,进行实例化
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
    
    
            public Void run() {
    
    

                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();

                try{
    
    
                    while(driversIterator.hasNext()) {
    
    
                        driversIterator.next();
                    }
                } catch(Throwable t) {
    
    
                    // Do nothing
                }
                return null;
            }
        });

        println("DriverManager.initialize: jdbc.drivers = " + drivers);

        if (drivers == null || drivers.equals("")) {
    
    
            return;
        }
        String[] driversList = drivers.split(":");
        println("number of Drivers:" + driversList.length);
     
        // 遍历引用的驱动,通过Class.forName连接数据库
        for (String aDriver : driversList) {
    
    
            try {
    
    
                println("DriverManager.Initialize: loading " + aDriver);
                Class.forName(aDriver, true,
                        ClassLoader.getSystemClassLoader());
            } catch (Exception ex) {
    
    
                println("DriverManager.Initialize: load failed: " + ex);
            }
        }
    }
  
  
}

Check the specific implementation of the driver in the MySQL jar package: declare your own implementation class to implement the Driver interface under META_INF/services, so that the JDBCDriverManager can find the MySQL implementation through SPI when it is loaded.

3.2 The Log Facade interface implements class loading

In the same way, SLF4J loads log implementation classes from different providers. The interface we use when printing logs is provided by Slf4j, but the specific implementation can be implemented by log4j or logback. If you want to use log4j or logback services, you just need to introduce the corresponding The jar package is fine.

3.3 Analysis of SPI mechanism in Dubbo framework

The Java SPI mechanism will load all interface implementation classes and instantiate them all, some may not be used, so this causes a waste of resources.

When there are many extensions, the SPI will take some time to load, and the failure of one extension to load will cause the rest to fail to load.

Therefore, Dubbo optimized the Java SPI mechanism and implemented its own SPI mechanism.

Dubbo does not use the native ServiceLoader, but implements ExtensionLoader by itself to load the extension implementation.

And Dubbo's configuration directory is **/META-INF/dubbo/internal**, not META-INF/services .

For example, serialization interface: org.apache.dubbo.remoting.Codec2Dubbo provides a default implementation class:, org.apache.dubbo.rpc.protocol.dubbo.DubboCountCodecand declare in /META-INF/dubbo/internal:
Insert picture description here

When using, load all implementation classes through ExtensionLoader:

Codec2 codec = ExtensionLoader.getExtensionLoader(Codec2.class).getExtension("dubbo");

3.4 Analysis of SPI mechanism in Sentinel framework

Sentinel uses the Java SPI mechanism when building the slot chain during application initialization.

Sentinel defines the interface slot chain constructs: com.alibaba.csp.sentinel.slotchain.SlotChainBuilderdeclare the resources at the interface / META-INF / services directory, and provides a default implementation: com.alibaba.csp.sentinel.slots.DefaultSlotChainBuilder.

After the client imports the Sentinel jar package, it can implement the SlotChainBuilder interface by itself.

You can look at the source code:

public final class SlotChainProvider {
    
    
      // 通过Java SPI 中的ServiceLoader加载实现SlotChainBuilder接口的实现类
      private static final ServiceLoader<SlotChainBuilder> LOADER =   ServiceLoader.load(SlotChainBuilder.class);
      
     private static void resolveSlotChainBuilder() {
    
    
        List<SlotChainBuilder> list = new ArrayList<SlotChainBuilder>();
        boolean hasOther = false;
        // 遍历SlotChainBuilder接口的实现类(包括Sentinel提供的默认实现类和客户端自己扩展的实现类)
        for (SlotChainBuilder builder : LOADER) {
    
    
            if (builder.getClass() != DefaultSlotChainBuilder.class) {
    
    
                hasOther = true;
                list.add(builder);
            }
        }
        if (hasOther) {
    
    
            builder = list.get(0);
        } else {
    
    
            // No custom builder, using default.
            builder = new DefaultSlotChainBuilder();
        }

    }  
}

(4) Apply ServiceLoader to load the implementation class

Through the above analysis, we can feel the application of ServiceLoader in Java SPI. In fact, ServiceLoader also has many applications in daily development, such as the application of ServiceLoader to load implementation classes.

// (1)定义接口
public interface OrderRefundService {
    
    

    /**
     * 退款
     *
     * @param orderId
     */
    void refund(long orderId);
}

// (2)实现类:商家退款、客服退款、用户退款
@Service
@RefundTypeAnno(refundType = "customer")
public class CustomerOrderRefundService implements OrderRefundService {
    
    

    @Override
    public void refund(long orderId) {
    
    
        System.out.println("客服退款操作, orderId: " + orderId);
    }
}

@Service
@RefundTypeAnno(refundType = "merchant")
public class MerchantOrderRefundService implements OrderRefundService {
    
    


    @Override
    public void refund(long orderId) {
    
    
        System.out.println("商家退款操作, orderId: " + orderId);
    }
}

@Service
@RefundTypeAnno(refundType = "user")
public class UserOrderRefundService implements OrderRefundService {
    
    

    @Override
    public void refund(long orderId) {
    
    
        System.out.println("用户退款操作, orderId: " + orderId);
    }
}

//(3)注解代表退款类型
@Retention(RetentionPolicy.RUNTIME)
@Target({
    
    ElementType.TYPE})
@Documented
public @interface RefundTypeAnno {
    
    

    String refundType();
}

//(4)退款manager类
@Component
public class OrderRefundServiceManager {
    
    

    private static AtomicBoolean initialized = new AtomicBoolean(false);

    private static Map<String, OrderRefundService> orderRefundMap = new HashMap<>();


    static {
    
    
        doInit();
    }

    private static void doInit() {
    
    
        if (!initialized.compareAndSet(false, true)) {
    
    
            return;
        }
        try {
    
    
            // 加载OrderRefundHandler接口的实现类
            ServiceLoader<OrderRefundService> orderRefundServiceLoader = ServiceLoader.load(OrderRefundService.class);
            if (orderRefundServiceLoader != null) {
    
    
                for (OrderRefundService orderRefundService : orderRefundServiceLoader) {
    
    
                    String refundType = parseRefundType(orderRefundService);
                    if (!StringUtils.isEmpty(refundType)) {
    
    
                        orderRefundMap.put(refundType, orderRefundService);
                    }
                }
            }
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
    }

    private static String parseRefundType(OrderRefundService orderRefundService) {
    
    
        // 获取注解中的退款类型
        RefundTypeAnno refundTypeAnno = orderRefundService.getClass().getAnnotation(RefundTypeAnno.class);
        if (refundTypeAnno != null) {
    
    
            return refundTypeAnno.refundType();
        } else {
    
    
            return null;
        }
    }


    public OrderRefundService getRefundProcessor(String refundType) {
    
    
        if (orderRefundMap != null && orderRefundMap.size() > 0) {
    
    
            return orderRefundMap.get(refundType);
        }
        return null;
    }


}


(5)测试类
@Controller
public class ServiceLoaderTest {
    
    

    @Autowired
    private OrderRefundServiceManager orderRefundServiceManager;

    @RequestMapping("/orderRefund")
    @ResponseBody
    public void refund(@RequestParam("refundType") String refundType,
                       @RequestParam("orderId") long orderId) {
    
    
        OrderRefundService orderRefundService = orderRefundServiceManager.getRefundProcessor(refundType);
        orderRefundService.refund(orderId);
    }
}

Finally, you need to declare the class under META-INF/services:

4. Analysis of the principle of Java SPI

Then look at the ServiceLoader source code:

ServiceLoader properties

public final class ServiceLoader<S> implements Iterable<S> {
    
    
    // 扫描的目录
    private static final String PREFIX = "META-INF/services/";
    // 需要加载的类或接口
    private final Class<S> service;
    // 用于定位、加载、实例化实现的类的类加载器
    private final ClassLoader loader;
    // 创建ServiceLoader时采用的访问控制上下文
    private final AccessControlContext acc;
    // 缓存提供的实现类,按实例化的顺序
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
    // 懒查找迭代器
    private LazyIterator lookupIterator;

}

Look at the specific implementation of ServiceLoader from the entry ServiceLoader.load() method:

1、入口
public static <S> ServiceLoader<S> load(Class<S> service) {
    
    
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
}

private ServiceLoader(Class<S> svc, ClassLoader cl) {
    
    
        service = Objects.requireNonNull(svc, "Service interface cannot be null");
        loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
        acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
        // 实现在这里
        reload();
}

public void reload() {
    
    
        providers.clear();
        lookupIterator = new LazyIterator(service, loader);
 }

2、看下LazyIterator中的实现:
// 获取所有jar包中META-INF/services下的声明的实现类路径
String fullName = PREFIX + service.getName();
if (loader == null)
    configs = ClassLoader.getSystemResources(fullName);
else
    // 获取所有实现类
    configs = loader.getResources(fullName);

3、加载类、实例化类、缓存类
// 通过Class.forName加载类
c = Class.forName(cn, false, loader);
// 实例化类
S p = service.cast(c.newInstance());
// 缓存类 (LinkedHashMap)
providers.put(cn, p);

To summarize:

  • Get the path of the implementation class declared under META-INF/services;
  • Get all implementation classes through the path;
  • Load the class through Class.forName;
  • Instantiated class;
  • Cache the instantiated class (LinkedHashMap);

Java SPI summary

Advantages: decoupling. Separate the service module from the caller's business code instead of coupling it together. The application can enable the framework or replace the components in the framework according to actual business conditions.

Disadvantages:

  • ServiceLoader uses lazy loading, which traverses the implementation classes of all interfaces and loads them all for instantiation. If certain classes do not want to be used, they will be loaded and instantiated, causing waste. You can refer to the Dubbo SPI mechanism for optimization.
  • It is not safe for multiple concurrent and multithreaded instances to use the ServiceLoader class;

Reference: https://www.jianshu.com/p/46b42f7f593c

https://www.itcodemonkey.com/article/14716.html

Guess you like

Origin blog.csdn.net/noaman_wgs/article/details/102529022