1 background
In componentized pattern design, modules are programmed based on interfaces, and implementation classes are not hardcoded within modules. Because any code that involves a specific implementation class violates the principle of pluggability, when an implementation needs to be replaced, the code needs to be changed. A service discovery mechanism is needed in order to realize the dynamic specification during module assembly. SPI is one such mechanism: a mechanism for finding service implementations for an interface. Similar to the IOC idea of moving control of assembly out of the program, this mechanism is especially important in modular design.
2 Technical solutions of the industry
2.1 General module dependency mode
During component development, if you want to instantiate classes in module A in module B or use methods in module A, the conventional way is to make module B depend on module A, as shown below:
However, in componentized development, it is often not desirable to have direct dependencies between modules, because if dependencies are established, the coupling between modules is high.
2.2 Java SPI mechanism
Is there any way to solve this problem? You can use the SPI mechanism provided by Java!
Service Provider Interface (SPI) is a built-in Service discovery mechanism in JDK. It is very widely used, especially in the server-side development stack,
-
Different driver implementations are loaded in JDBC via SPI
-
SLF4J loads logging implementation classes from different vendors via SPI
-
Gradle source code has a large number of services based on the SPI mechanism to do service implementation extensions
-
SpringFactoriesLoader is one of the most important extension mechanisms in Spring. It is a variant of SPI and works in the same way
2.3 How does SPI solve the above problems?
Java’s SPI mechanism usage flow is shown in the figure above.
- First, A Base module is required to provide the IA interface. Both modules A and B rely on the Base module
- Class A in module A implements the IA interface
- Create resources/ meta-INF /services parent directory in SourceSet
- In the parent directory, create a text file with the fully qualified filename of the IA interface. The file content is the fully qualified list of the IA implementation classes, split by carriage return
- The serviceloader.load (IA. Class) method is used by module B to create class A objects in module A
2.4 Shortcomings of the Java native SPI mechanism
If you are familiar with the SPI mechanism you will find that, whether in JDBC, SLF4J,Gradle, or Spring, it is often sufficient for a module to simply provide individual key interfaces as a service entry point for external modules to discover.
However, if there are a large number of services that need to be discovered, write many interface files in the Resources/meta-INF /services directory and load them using ServiceLoader.
- Problem 1: The writing method is too tedious, the interface is not easy to maintain, can simplify?
- Resources /META-INF/services interface file is a configuration file. The ServiceLoader reads the fully qualified name of the interface implementation class through the file stream, and then instantiates the specific implementation class object through reflection.
- Problem three: It only provides service discovery capabilities. The ServiceLoader is only responsible for instantiating the service. There is no management of the instantiated object.
3. Spa, an easy-to-use SPI mechanism
3.1 SPA service discovery mechanism
Github link: github.com/luqinx/sp
Spa (Service Pool for Android) regards the classes to be instantiated as services one by one, which is a new SPI mechanism created based on the Java SPI idea. However, it not only has Service discovery ability, but also has Service life cycle management and Service priority management. Capabilities other than Java SPI, such as service interception management, are native to Android, but are not only available on the Android side, but also theoretically applicable to Jvm environments.
The basic idea of SPA is shown in the figure above:
- Spa uses annotations instead of the cumbersome configuration of services files, which greatly simplifies usage
- Spa implements class creation factory method through bytecode generation at compilation stage as an interface marked by @service annotation. Implementation class objects are created through factory method. There is no file stream operation and no need to reflect instantiated objects, improving performance.
- There is no need to read the configuration, no need to cache the mapping table, so SPA does not even need manual initialization.
Other similar frameworks commonly need to read configuration files (IO) at runtime, cache configuration maps, and create objects by reflection, all of which have some impact on performance. Spa does not. Spa generates factory classes at compile time instead of configuration files and cache mapping, which is easy to understand. How does a SPA create a service object without reflection?
In fact, module isolation is simply a pattern design designed to prevent unrelated modules from having reference relationships with each other during the development process, thus reducing the coupling between modules. Module compiled code eventually becomes bytecode/JAR/AAR /dex, and bytecode/JAR/AAR /dex has no concept of a module. In layman’s terms, when class A creates A class B object, class A doesn’t care and doesn’t know which module B is written to.
This is why SPA uses bytecode generation instead of APT code generation.
Up to this point, SPA has solved the problem of instantiating objects without direct dependencies across modules.
As you can see from this, the mainstream routing component, ARouter, has similar capabilities. Why not just use ARouter? Just because spa performance is a little better?
Instantiating objects across modules is a core capability of SPA, but it is far from the whole story. For example, when does spa create a service object, and when is a service created and recycled by gc?
3.2 Life cycle of SPA service
For those of you who have used ARouter, ARouter implements the IProvider interface to define a service that, when created, has a global lifecycle and is globally unique as a singleton.
This is very useful in some scenarios. For example, I need a global StorageService StorageService, which provides storage-related capabilities such as save, get, delete and so on. I can obtain this singleton service through the StorageService interface at any time and in any module and use it, which is very convenient.
But ARouter can only create services with a global lifecycle, which is not enough. For example, if multiple business modules need a CategoryFragment named CategoryFragment, I need to treat CategoryFragment as a service for module decoupling. Multiple categoryFragments need to be created based on different category ids. ARouter’s service cannot create more than one Fragment, and since it is a singleton Fragment that cannot be collected, it will cause memory leaks.
Spas can define the life cycle of a Service through the scope field of the @Service annotation. Common life cycles are the two described above.
The StorageService pseudocode is as follows:
@service (scope = spa. Global) public class StorageServiceImpl implements StorageService {... }Copy the code
CategoryFragment pseudocode is as follows:
// By default, a new CategoryFragment is created each time, @service public Class CategoryFragment implements CategoryService {... }Copy the code
The pseudocode to get the service is as follows
. StorageService storageService = Spa.getService(StorageService.class); CategoryService categoryFragment = Spa.getService(CategoryService.class); .Copy the code
In addition to the above two common life cycles, there are also life cycles held by weak references, soft references, and custom life cycle management.
3.3 SPA Service Priority Management
When a service has multiple implementation classes, which one does spa.getService (xx.class) get? Therefore, Spa introduces service priority management.
3.3.1 Why is Priority management needed?
When you want to write a basic service that needs to perform different behaviors in different environments, what’s a good idea? (Different environments can mean different build environments, different runtime environments (Java or Android, Windows or Mac), different business scenarios, even different projects, etc.)
The code in my project is strictly separate from the code in the production environment, and the code related to the internal environment such as logging, debugging tools, analysis tools, data mocks, etc., is never included in the build environment installation package. This ensures the security and performance of the generated environment.
Take log output as an example.
public interface LogService implements IService { void e(String tag, String message); . }Copy the code
In an internal environment, logs need to be exported to the console for problem detection because the internal environment code is isolated from the production environment and can be set to a higher priority. It will be instantiated preferentially when it exists
@Service(scope = Spa.global, priority = 100) public class AlphaLogService implements LogService { void e(String tag, String message) { ... Log.e(tag, message); }... }Copy the code
In the online production environment, AlphaLogService is not exported to the console, but is uploaded to the logging platform according to certain policies. Because the code environment is isolated from the production environment, AlphaLogService does not exist, so ProductLogService is instantiated even though it is of low priority
@Service(scope = Spa.global, priority = 10) public class ProductLogService implements LogService { void e(String tag, String message) { ... RemoteLog.e(tag, message); }... }Copy the code
One may think: why are you making things so complicated when a simple if-else solution can be solved??
Yes, if-else can handle different log output for different environments, but if-else can hardly get the LogService implementation class for different environments without isolating the environment
3.3.2 SPA can also obtain multiple service implementations at the same time
The Load method of the ServiceLoader returns the ServiceLoader object as an Iterator. The Iterator is a list of implementations of the service interface. Does Spa implement this function? Go straight to code
Suppose the Interceptor service interface is Interceptor, which has three interceptors A, B, and C
Public interface Interceptor extends IService{void intercept(); String interceptorName(); } @service (priority = 10) public class AInterceptor implements Interceptor {void intercept() { System.out.println("interceptor A is running..." ); } public String interceptorName() { return "A"; }} @service (priority = 30) public class BInterceptor implements Interceptor {void intercept() { System.out.println("interceptor B is running..." ) } public String interceptorName() { return "B"; } // c@service (priority = 20) public class CInterceptor implements Interceptor {void intercept() { System.out.println("interceptor C is running..." ) } public String interceptorName() { return "C"; }}Copy the code
Spa uses CombineService to combine multiple service interface implementations. CombineService is also an Iterator, and the iterators are returned in order of the server priority value
CombineService<Interceptor> as = Spa.getCombineService(Interceptor.class); for (Interceptor interceptor: interceptors) { System.out.print(interceptor.interceptorName()); } // output BCACopy the code
The object returned by spa.getCombineservice (interceptor.class) is also an Interceptor proxy, and when the Intercept () method of the proxy object is executed, the Intercept () method of each service implementation is executed in order of precedence
Interceptor interceptor = Spa.getCombineService(Interceptor.class); interceptor.intercept(); // interceptor B is running... // interceptor C is running... // interceptor A is running...Copy the code
The default CombineService strategy is to determine the order of execution of multiple services according to the priority. In the above example, the general process of calling the Intercept () method of multiple Interceptor service objects is as follows:
ComineService can also implement the CombineStrategy interface to support custom execution policies.
1. Define a user-defined multi-service execution policy
public class InterceptorStrategy implements CombineStrategy { @Override public boolean filter(Class serviceClass, Method method, Object[] args) { return Interceptor.class.isAssignableFrom(serviceClass); } @Override Public Object Invoke (Final List<ServiceProxy> Proxies, Class serviceClass, Final Method Method, Final Object[] args) {// Custom call procedure}}Copy the code
2. Customize policies for services
Interceptor interceptor = Spa.getCombineService(Interceptor.class, InterceptorStrategy);
interceptor.intercept()
Copy the code
Custom multi-service execution policies are very useful and have multiple applications within a SPA
- Scenario 1: Routing interception policy in SpRouter. Services can implement the RouteInterceptor interface and call onContinue or onInterrapt() to decide whether to continue a route or intercept a route. The implementation class is RouteCombineStrategy. Each routing framework generally has the route interception capability, the way is similar, here will not repeat, interested can see the implementation of their own.
- Scenario 2: User-defined life-cycle type checking strategy. The implementation class is CustomCombineStrategy. You can view it for yourself.
- Scenario 3: An interception strategy for SPA service interception. The business can implement the IServiceInterceptor interface and call onContinue or onInterrapt() to decide whether to continue executing the method, intercept the method and not execute it, or change the method to execute it, etc. Service interception is an integral part of SPA.
3.4 SPA Service Interception
By default, spA-defined services support Service interception capability, which can be used to implement AOP operations on services. It is also easy to intercept spa services by implementing the IServiceInterceptor interface, and the Service interceptor is also treated as a Service, so you need to use the @service annotation
@Service public class MinPriorityServiceInterceptor implements IServiceInterceptor { @Override public void intercept(Class<? extends IService> originClass, IService source, Method method, Object[] args, IServiceInterceptorCallback callback) { logger.log(source.toString() + ": " + method.getName()); if (method.getReturnType() == int.class) { callback.onInterrupt(100); // Intercept the method and return 100} else {callback.onContinue(method, args); }}}Copy the code
Service interception can be disabled by setting the @service annotation parameter disableIntercept to true.
3.5 Service Alias
In the previous section, services are found by class. Spa also supports aliasing classes and then finding services by alias. Aliases with @service annotated path(why not alias? Historical cause) parameter id.
@Service(path = "firstAlias", scope = Spa.Scope.Global) public class MyAliasService implements IService { .... } // use MyAliasService byPath = spa.getService ("firstAlias"); // SPA uses the interface/abstract class to find the service corresponding to its implementation class/subclass using getService(ixxx.class) // SPA creates the service object by specifying the specific service implementation class using getFixedService(xxx.class) MyAliasService byClass = Spa.getFixedService(MyAliasService.class); assert byPath == byClass;Copy the code
The core of Spa is to find services by class. Alias lookup also makes a layer mapping on the basis of class lookup: Spa generates the PathServicesInstance class during compilation, which maintains a mapping table of alias (Path) to service classes.
4. To summarize
Spa is the most complete SPI open source solution at present. Although major manufacturers’ open source routing solutions such as ARouter, DRouter and WMRouter all have similar SPI capabilities, their foothold is to solve the routing problem under the Android framework, rather than a pure SPI solution. The goal of Spa is to create and manage services across modules. It does not care about the specific services of the upper layer, so the service management ability of Spa is more powerful and easier to expand.
If you want the same routing capability as ARouter, you can use SpRouter under SPA. SpRouter is a set of routing solutions based on SPA.
5. Think about
Imagine abstracting the pages, pop-ups, and functions of a project into services, and then exposing these services in the form of resources (urls) for internal and external access (H5, Flutter, interface access, push access). Access via ADB, etc.), when these services reach a certain scale, does the whole App become more flexible and dynamic? This is the servitization framework of my current application.
Reference Documents:
RGB – 24 bit. Making. IO/blog / 2019 / j…
www.cnblogs.com/jalja365/p/…