Service Provider Interface (SPI) is a dynamic Service discovery mechanism provided by Java. Through SPI mechanism, we can directly find the desired interface implementation class across modules, thus avoiding unnecessary inter-module dependencies and reducing the coupling between modules. This has important implications for communication between Android components. Imagine if there was a component that could provide such a capability, whether our component communication would be easy: through an interface, I can easily find the object of any one or more implementation classes of this interface under any module; Furthermore, you can cache objects any way you want (simple objects, singletons, strong and weak references, custom, etc.); Each method of an interface implementation class object can be intercepted by a custom interception policy and interceptor. If the interface has multiple implementation classes, you can choose which one or more implementation classes to use. This component is the Service Pool for Android (SPA) introduced in this article. (A little abstract, look back)

Before introducing SPA, what is the SPI mechanism provided by Java

Java’s built-in SPI mechanism finds and instantiates all the implementation classes for an interface through ServiceLoader. For each module that needs to be instantiated, put the fully qualified name of the interface (package name + class name) in the resources/ meta-INF /services/ directory. Then write the fully qualified name of its implementation class to the file one by one, assuming the interface com.sample.IPrinter.

  • Module A has its implementation class com.sample. APrinter, which will be like the directory structure as shown below

  • B module has its implementation class com.sample. BPrinter, which will be like the directory structure below

  • Resources/meta-INF /services/ resources/ meta-INF /services/

  • Now load the IPrinter using ServiceLoader(java.util.serviceloader) in any module:
/ / to find out all IPrinter implementation class / / there is no CPrinter, because there is no defined C/meta-inf/services/com. Sample. Interface. IPrinter ServiceLoader < IPrinter > printers = ServiceLoader.load(IPrinter.class); for (IPrinter printer: printers) { printer.print(); }Copy the code
  • Output:
this is A
this is B
Copy the code

This is the SPI mechanism provided by Java, which does instantiate objects across modules, and Android does support this SPI mechanism. But this pattern applies to so few scenarios that it doesn’t have much use in Android development scenarios. Here are a few reasons:

  1. It would be too tedious to configure/meta-INF /services/ XXX files for each implementation class of each interface. If my business needed many interfaces, the configuration would be extremely complex and confusing (SPA only needs one annotation).
  2. Implementation-class objects are directly instantiated by ServiceLoader and have an uncontrolled lifecycle that is not conducive to multi-module collaboration (instances created by spAs can be singletons, soft, weak references, plain objects, or custom lifecycle)
  3. ServiceLoader can only look up implementation classes by Class and does not support path/ ID lookup (this is important for both unified solutions and configuration capabilities)
  4. When there are multiple implementation objects, ServiceLoader simply provides an Iterator and does not get the exact implementation object (SPA can get the implementation object through priority, manual specification, and interception policy).
  5. ServiceLoader does not have any management policy when there are multiple implementation classes (SPA can customize the order of execution of multiple implementation classes)
  6. The ServiceLoader creates objects that execute methods without a good interception policy (SPA can implement route interception, process RPC communication mechanism, etc.)
  7. The ServiceLoader loads and reads files in the Resources directory using the Classloader, which involves reading and writing file streams and consumes high performance

Let me write it up front. What does SPA do?

  • Activity routing (standalone component in SPA, spRouter)
  • RPC communication (SPRPC, a standalone component in SPA)
  • Component routing
  • Service distribution
  • Component Mock

SPA has what advantages

  1. concise
  2. High degree of freedom, strong expansion ability
  3. High efficiency, no performance loss

So much nonsense, what kind of SPI framework is SPA

Please refer to the SPA access modeREADME.md, here is only the use of SPA, first a simple pictureAs shown in the figure above, SPA can be simply understood as a tool that can create objects across modules. Let’s start with the model in the figure above and see how the code should be written.

** interfaces module — iprInterservice.java **

// Note: The interface must inherit from IService, which is an empty interface
public interface IPrinterService extends IService {
    void print(a);
}
Copy the code

** MODULE A — APrinterService **

@Service(priority = 1)
public class APrinterService implements IPrinterService {
    @Override
    public void print(a) {
        System.out.println("this is a printer service."); }}Copy the code

B module — BPrinterService **

@Service(path="b_printer", priority = 2)
public class BPrinterService implements IPrinterService {
    @Override
    public void print(a) {
        System.out.println("this is b printer service."); }}Copy the code

C module — CPrinterService **

@Service(priority = 3)
public class CPrinterService implements IPrinterService {
    @Override
    public void print(a) {
        System.out.println("this is c printer service."); }}Copy the code

** The following logic can also exist in modules A,B, and C **

IPrinterService printer = Spa.getService(IPrinterService.class); // Take the highest priority
printer.print(); // This is c printer service.

APrinterService aprinter = Spa.getFixedService(APrinterService.class);
aprinter.print();// This is a printer service.

BPrinterService bPrinter = Spa.getFixedService(BPrinterService.class);
bPrinter.print();// This is b printer service.

// Equivalent to spa.getFixedService (bprInterservice.class) above
IPrinterService pathPrinter = Spa.getService("b_printer"); // Is there a sense of routing
pathPrinter.print(); // This is b printer service.

Copy the code

This is the basic usage of SPA, so far it has the ability of SPI mechanism, isn’t it very simple!! Is that all there is in SPA? Of course not!

What is the life cycle of an object created by a SPA?

BPrinter == pathPrinter == BPrinterService == BPrinterService

** The scope attribute of the @service annotation ** is described below

Scope defines the life cycle of an object. The scope built into SPA has

  • Normal, a normal object, each time returns a newly created object, the default scope
  • Global, a global object, can be thought of as a singleton that returns the same object each time, and the object is created the first time it is used
  • Weak, objects use weak reference caching and are not recreated if they are not collected by the GC
  • Soft, objects use soft reference caching and are not recreated if they are not collected by gc
  • Custom cache policy. When scope is not a value listed above, it is considered a custom cache policy. Custom cache policy will be described in the Spa advanced section

So is bPrinter equal to pathPrinter? The default life cycle of a SPA object is Nornal, that is, a new object is created every time, so bPrinter! = pathPrinter. If you want bPrinter == pathPrinter, just define the scope of BPrinterService as global!

@Service(path="b_printer", priority = 2, scope=Spa.Scope.GLOBAL) //scope set to global
public class BPrinterService implements IPrinterService {
    @Override
    public void print(a) {
        System.out.println("this is b printer service."); }}Copy the code

SPA method interception capability

SPA does not simply create and return an object. SPA actually returns a proxy of the target object. Through the proxy, we can intercept the object when it executes a method.

** SPA has flexible interception capabilities, not only to set up interceptors, but also to set interception policies **

  • Custom interceptors. Interceptors execute interceptions in order of priority by default
  • You can customize an interception policy. If there are multiple interceptors, the execution sequence and execution mode of interceptors are determined by the interception policy

The usage of the interceptor will be described in the next step. Let’s first look at the flow chart of the SPA execution method. The flow chart illustrates the print method call process of the CPrinterService example in the previous section

How is the interceptor used in the code? 六四屠杀

A class that implements the IServiceInterceptor interface and is marked by @Service is considered a method invocation interceptor by SPA

  • Start by defining a high-priority interceptor
@Service(priority = Spa.Priority.MAX)
public class MaxPriorityServiceInterceptor implements IServiceInterceptor {
    @Override
    public void intercept(Class<? extends IService> originClass, IService source, Method method, Object[] args, IServiceInterceptorCallback callback) {
    	System.out.println("this is a max priority interceptor.") callback.onContinue(method, args); }}Copy the code
  • Define an interceptor of normal priority
@Service
public class NormalServiceInterceptor implements IServiceInterceptor {

    @Override
    public void intercept(Class<? extends IService> originClass, IService source, Method method, Object[] args, IServiceInterceptorCallback callback) {
        System.out.println("this is a normal priority interceptor.") callback.onContinue(method, args); }}Copy the code
  • Define a lower priority interceptor
@Service(priority = Spa.Priority.MIN)
public class MinPriorityServiceInterceptor implements IServiceInterceptor {
    @Override
    public void intercept(Class<? extends IService> originClass, IService source, Method method, Object[] args, IServiceInterceptorCallback callback) {
    	System.out.println("this is a min priority interceptor.")
        if ("chao.sample.c.CPrinterService".equalse(originClass.getName()) && "print".equals(method.getName())) { CPrinterService print ();
            callback.onInterrupt(null); // If the method returns a value, null can be replaced with the intercepted value
        } else{ callback.onContinue(method, args); }}}Copy the code
  • Execute the print method
IPrinterService printService = Spa.getService(IPrinterService.class); //cPrinter
printService.print(); 
Copy the code
  • Finally, look at the output
this is a max priority interceptor. this is a normal priority interceptor. this is a min priority interceptor. # this is C printer service. cPrinter's print is blocked and not executed, so it will not have this outputCopy the code
  • Take a look at the sequence diagram of the entire process

SPA Application Practice 1 — How do submodules get BuildConfig information for the main module

Multi-module development/componentalization development process, the main module (plugin is com.Android.application module, generally refers to app module) can rely on any module, but the sub-module cannot rely on the main module, if the sub-module wants to take the main module content how to do? Here’s how to use Spa to get the Context and BuildConfig contents of the main module. ** First define a BuildService** at the interface layer

BuildService.java

public interface BuildService extends IService {
    String buglyId(a); // Build. Gradle uses the buglyId defined by buildConfigField

    boolean debuggable(a);

    String versionName(a);

    int versionCode(a);

    String applicationId(a);

    String buildType(a);
}
Copy the code

** In the APP module, implement the service interface and use the @service flag **

  1. BuildServiceImpl.java
@Service(scope = Spa.Scope.GLOBAL) //Global can be regarded as a singleton
public class BuildServiceImpl implements BuildService {
    @Override
    public String buglyId(a) {
        return BuildConfig.BUGLY_ID;
    }

    @Override
    public boolean debuggable(a) {
        return BuildConfig.DEBUG;
    }

    @Override
    public String versionName(a) {
        return BuildConfig.VERSION_NAME;
    }

    @Override
    public int versionCode(a) {
        return BuildConfig.VERSION_CODE;
    }

    @Override
    public String applicationId(a) {
        return BuildConfig.APPLICATION_ID;
    }

    @Override
    public String buildType(a) {
        returnBuildConfig.BUILD_TYPE; }}Copy the code

The preparation is complete, and now we apply it in the BuildInfoActivity of the Pages module

BuildInfoActivity.java

public class BuildInfoActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        BuildInfoPageBinding viewBinding = BuildInfoPageBinding.inflate(LayoutInflater.from(this));
        setContentView(viewBinding.getRoot());

        BuildService buildService = Spa.getService(BuildService.class);
        viewBinding.applicationId.setText("applicationId: " + buildService.applicationId());
        viewBinding.versionName.setText("versionName: " + buildService.versionName());
        viewBinding.versionCode.setText("versionCode: " + buildService.versionCode() + "");
        viewBinding.buildType.setText("buildType: " + buildService.buildType());
        viewBinding.debuggable.setText("debuggable: " + buildService.debuggable());
        viewBinding.buglyId.setText("buglyId:"+ buildService.buglyId()); }}}Copy the code

** See the final result **This is the simplest application scenario of SPA. More practical applications will be introduced in the advanced chapter of SPA

All of the sample code covered above is here

Advanced links:

  • SPA Advanced 1 — Service distribution
  • SPA Advanced 2 — Routing SPRouter
  • SPA Step 3 — Component Mocks
  • SPA Advanced 4 — RPC Communication SPRpc

conclusion

This article mainly introduces the ability and usage of SPA(Service Pool for Android), an easy-to-use SPI framework on Android, and compares it with Java’S SPI mechanism. I believe you can see that SPA is more powerful, simpler and less expensive. Later, I will introduce the spRouter, SPRPC, service distribution, component Mock and a series of extension capabilities I developed based on SPI step by step. If you are interested in my project, please scan the qr code below and add my personal work wechat, so that we can study and discuss together. Here is my Github address, your star/fork is the biggest motivation for me to move forward, thank you!