1. Relationship between AOP and IOC

AOP (Aspect Oriented Programming) is a programming design idea that aims to reduce the coupling between business logics by intercepting business process tangents and implementing specific modularization capabilities. This idea has been practiced in many well-known projects. For example, Spring’s PointCut, gRPC’s Interceptor, and Dubbo’s Filter. aOP is just a concept that has been applied in different scenarios, resulting in different implementations.

Let’s start by discussing more specific RPC scenarios, using gRPC as an example.

gRPC

For a single RPC process, gRPC provides a user-extendable Interceptor interface that allows developers to write business-related interceptor logic. For example, introducing authentication, service discovery, observability, and other capabilities, there are many extensions based on Interceptor implementations in the gRPC ecosystem, see go-grpc-middleware. These extended implementations belong to the gRPC ecosystem and are limited to the concepts of Client and Server sides, and to RPC scenarios.

We abstract the concrete scenario by referring to Spring.

Spring has a powerful dependency injection capability, on top of which it provides AOP capabilities for adapting and business object methods, allowing interceptors to be encapsulated outside of business functions by defining cutpoints. These concepts of “facets” and “cutpoints” are limited to the Spring framework and managed by its dependency injection (also known as IOC) capabilities.

The point I want to make is that the concept of AOP needs to be grounded in a specific scenario and must be constrained by the ecological constraints from which it is integrated. For example, I can write a series of function calls along the lines of procedural oriented programming, and I can say that this is an implementation of AOP, but it is not scalable, migratable, or generic. This constraint is necessary, can be strong or weak, such as Spring ecological AOP, weaker constraints have greater scalability, but the implementation is relatively complex, the developer needs to learn its ecological concepts and APIs, and if Dubbo, gRPC ecological AOP adapted to RPC scenarios, the developer only needs to implement the interface and a single API injection can be, its ability is relatively The ability is relatively limited.

The above “constraints” can be visualized in the actual development scenario as dependency injection, or IOC, where the objects that developers need to use are managed and encapsulated by the ecology, whether it is Dubbo’s Invoker or Spring’s Bean, the IOC process provides a constraint for the practice of AOP, provides a model The IOC process provides a constraint for the practice of AOP, a model, and a value for implementation.

go ioc

2. Go Ecology and AOP

The AOP concept has nothing to do with the language, and while I agree that best practice solutions using AOP require the Java language, I don’t think AOP is exclusive to Java. In the Go ecosystem that I am familiar with, there are still many good projects based on the AOP idea, and the commonality of these projects, as I explained in the previous section, is that they all combine a specific ecosystem to solve a specific business scenario, where the breadth of the solution depends on the binding power of the IOC ecosystem. An IOC ecosystem that does not provide AOP can be very clean and refreshing, while an IOC ecosystem that provides AOP capabilities can be very inclusive and powerful.

Last month I open sourced the IOC-golang service framework, which focuses on solving the dependency injection problem in Go application development. Many developers compare this framework with Google’s open source wire framework and think it is not as clean and easy to use as wire, but the essence of this problem is that the two ecologies are designed for different purposes. wire focuses on IOC rather than AOP, so developers can learn some simple concepts and APIs, use scaffolding and code generation capabilities to quickly implement dependency injection, and have a good development experience. IOC-golang focuses on IOC-based AOP capabilities and embraces this layer of extensibility, viewing AOP capabilities as a point of difference and value between this framework and other IOC frameworks.

Compared to SDKs that solve specific problems, we can consider the IOC capabilities of the dependency injection framework as a “weakly constrained IOC scenario” and compare the differences between the two frameworks to throw light on two core issues.

  • Does the Go ecosystem need AOP in “weakly constrained IOC scenarios”?
  • What can GO ecosystem AOP be used for in “weakly constrained IOC scenarios”?

My view is that the Go ecosystem must need AOP, even in the “weakly constrained IOC scenario”, but still can use AOP to do some business-independent things, such as enhance the application’s operation and maintenance observability. Go does not support annotations, which limits the convenience of developers to use the AOP layer for writing business semantics. I would prefer to give the Go ecosystem an AOP layer with observable capabilities for operations and maintenance, while developers would be impervious to AOP.

For example, the IOC-golang framework can be used to encapsulate the operations AOP layer for any interface implementation structure, thus making all objects of an application observable. In addition, we can also combine RPC scenarios, service governance scenarios, and fault injection scenarios to generate more “operations” domain extension ideas.

3. AOP principles for IOC-golang

There are two ways to implement method proxies in Go: interface proxies via reflection, and function pointer swapping based on Monkey patches. The latter does not rely on interfaces and can encapsulate function proxies for methods of any structure, requires intrusion into the underlying assembly code, turns off compilation optimizations, has CPU architecture requirements, and can significantly degrade performance when handling concurrent requests.

The former makes more sense for production, relies on interfaces, and is the focus of this section.

3.1 IOC-golang’s interface injection

As mentioned in the first open source article of this framework, IOC-golang has two perspectives on the dependency injection process, the structure provider and the structure user. The framework accepts the structure defined by the structure provider and provides the structure as requested by the structure user. The structure provider only needs to care about the structure ontology, not about which interfaces the structure implements. The structure user needs to care about how the structure is injected and used: is it injected into an interface? To a pointer? Is it obtained through the API? Or through tag injection?

  • Injecting dependencies via tags

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    // +ioc:autowire=true
    // +ioc:autowire:type=singleton
    
    type App struct {
        // Injecting the implementation into the structure pointer
        ServiceStruct *ServiceStruct `singleton:""`
    
        // Injecting the implementation into the interface
        ServiceImpl Service `singleton:"main.ServiceImpl1"`
    

    The ServiceStruct field of an App is a pointer to a concrete structure, and the field itself can already locate the structure expected to be injected, so there is no need to give the name of the structure expected to be injected in the tag. For such fields injected into structure pointers, AOP capabilities cannot be provided by injecting interface proxies, only by the monkey patching scheme mentioned above, which is not recommended.

    The ServiceImpl field of the App is an interface named Service and the expected injected structure pointer is main.ServiceImpl. It is essentially an assertion logic from the structure to the interface, and although the framework can perform a verification of the interface implementation, it still requires the structure user to ensure that the injected interface implements the method. For this injection into the interface, the IOC-golang framework automatically creates a proxy for the main.ServiceImpl structure and injects the proxy structure into the ServiceImpl field, so this interface field has AOP capabilities.

    Therefore, ioc recommends developers to program interface-oriented instead of relying directly on concrete structures. In addition to AOP capabilities, interface-oriented programming will also improve the readability of go code, unit testing capabilities, module decoupling level, etc.

  • Obtaining objects by means of API

    Developers of the IOC-golang framework can get structure pointers by way of an API, by calling the GetImpl method of an autoloading model (such as a singleton).

    1
    2
    3
    4
    5
    6
    7
    8
    
    func GetServiceStructSingleton() (*ServiceStruct, error) {
    i, err := singleton.GetImpl("main.ServiceStruct", nil)
    if err != nil {
        return nil, err
    }
    impl := i.(*ServiceStruct)
    return impl, nil
    }
    

    Developers using the IOC-golang framework prefer to get the interface object by means of an API. By calling the GetImplWithProxy method of an autoloading model (e.g. singleton), you can get a proxy structure that can be asserted as an interface for use. This interface is not created manually by the structure provider, but is a “structure-specific interface” automatically generated by iocli, as explained below.

    1
    2
    3
    4
    5
    6
    7
    8
    
    func GetServiceStructIOCInterfaceSingleton() (ServiceStructIOCInterface, error) {
    i, err := singleton.GetImplWithProxy("main.ServiceStruct", nil)
    if err != nil {
        return nil, err
    }
    impl := i.(ServiceStructIOCInterface)
    return impl, nil
    }
    

    These two ways to get objects through the API can be automatically generated by the iocli tool. Note that the role of these codes are to facilitate developers to call the API and reduce the amount of code, while the logic kernel of ioc autoloading is not generated by the tool, which is one of the different points from the idea of dependency injection provided by wire, and is a point that many developers misunderstand.

  • IOC-golang’s architecture exclusive interface

    From the above, we know that the IOC-golang framework’s recommended approach to AOP injection is to strongly rely on interfaces. But requiring developers to write a matching interface by hand for all their structures can be time-consuming. So the iocli tool can automatically generate structure-specific interfaces to reduce the amount of code developers have to write.

    For example, a structure called ServiceImpl, which contains the GetHelloString method

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    // +ioc:autowire=true
    // +ioc:autowire:type=singleton
    
    type ServiceImpl struct {
    }
    
    func (s *ServiceImpl) GetHelloString(name string) string {
        return fmt.Sprintf("This is ServiceImpl1, hello %s", name)
    }
    

    When the iocli gen command is executed, a copy of the code zz_generated.ioc.go is generated in the current directory, which contains the “proprietary interface” for the structure.

    1
    2
    3
    
    type ServiceImplIOCInterface interface {
        GetHelloString(name string) string
    }
    

    The exclusive interface is named $(structure name)IOCInterface, and the exclusive interface contains all the methods of the structure. The role of the exclusive interface is twofold.

    1. To reduce the workload of developers, it is convenient to get to the proxy structure directly by means of API, which is convenient to inject directly as a field.
    2. Structure exclusive interface can directly locate the structure ID, so when injecting the exclusive interface, the tag does not need to explicitly specify the structure type.
    1
    2
    3
    4
    5
    6
    7
    
    // +ioc:autowire=true
    // +ioc:autowire:type=singleton
    
    type App struct {
        // Inject the ServiceImpl structure-specific interface without specifying the structure ID in the tag
        ServiceOwnInterface ServiceImplIOCInterface `singleton:""`
    }
    

    Therefore, a random existing go project, which uses the structure of the location of the pointer, we recommend replacing it with a structure-exclusive interface, the framework default injection agent; for the fields in which the interface has been used, we recommend injecting the structure directly through the label, also by the framework default injection agent. The project developed according to this model, all its objects will have the ability to operate and maintain.

3.2 Proxy generation and injection

The objects mentioned in the previous section as “injected into an interface” are encapsulated by the framework as proxies by default, with the ability to run and maintain them, and mention that iocli generates “proprietary interfaces” for all structures. In this section, we explain how the framework encapsulates the proxy layer and how it is injected into the interface.

  • Code generation and registration of proxy structures

    The code generated in zz.generated.ioc.go, mentioned above, contains the structure-specific interfaces and, similarly, the definition of the structure proxy. Taking the ServiceImpl structure mentioned above as an example, it generates a proxy structure as follows.

    1
    2
    3
    4
    5
    6
    7
    
    type serviceImpl1_ struct {
        GetHelloString_ func(name string) string
    }
    
    func (s *serviceImpl1_) GetHelloString(name string) string {
        return s.GetHelloString_(name)
    }
    

    The proxy structure is named $(structure name)_, which implements all the methods of the “structure-specific interface” and proxies all method calls to the method field of $(method name)_, which will be implemented by the framework in a reflective manner.

    Like the structure code, the proxy structure is also registered to the framework in this generated file.

    1
    2
    3
    4
    5
    6
    7
    
    func init(){
    normal.RegisterStructDescriptor(&autowire.StructDescriptor{
            Factory: func() interface{} {
                return &serviceImpl1_{} // Registered Agent Structure
            },
        })
    }
    
  • Injection of proxy objects

    The above describes the definition of the proxy structure and the registration process. When the user expects to get the proxy object that encapsulates the AOP layer, it will first load the real object, then try to load the proxy object, and finally instantiate the proxy object through reflection to inject the interface, thus giving the interface operation and maintenance capabilities. The process can be demonstrated by the following diagram.

    Injection of proxy objects

4. IOC-golang AOP-based applications

Understanding the implementation ideas mentioned above, we can assume that all interface objects injected and obtained from the framework in applications developed using the IOC-golang framework are operationally capable. We can extend the capabilities we expect based on the idea of AOP. We provide a simple e-commerce system demo shopping-system, which demonstrates the AOP-based visualization capabilities of IOC-golang in a distributed scenario. Interested developers can refer to the README and run the system in their own clusters to get a feel of its operation and maintenance capabilities base.

4.1 Methods, parameters observable

  • View application interfaces and methods

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    % iocli list
    github.com/alibaba/ioc-golang/extension/autowire/rpc/protocol/protocol_impl.IOCProtocol
    [Invoke Export]
    
    github.com/ioc-golang/shopping-system/internal/auth.Authenticator
    [Check]
    
    github.com/ioc-golang/shopping-system/pkg/service/festival/api.serviceIOCRPCClient
    [ListCards ListCachedCards]
    
  • Listening for call parameters

    With the iocli watch command, we can listen for calls to the Check method of the forensic interface.

    1
    
    iocli watch github.com/ioc-golang/shopping-system/internal/auth.Authenticator Check
    

    Initiate a call against the portal.

    1
    
    curl -i -X GET 'localhost:8080/festival/listCards?user_id=1&num=10'
    

    You can see the call parameters and return value of the method being listened to, with user id 1.

    1
    2
    3
    4
    5
    6
    7
    8
    
    % iocli watch github.com/ioc-golang/shopping-system/internal/auth.Authenticator Check
    ========== On Call ==========
    github.com/ioc-golang/shopping-system/internal/auth.Authenticator.Check()
    Param 1: (int64) 1
    
    ========== On Response ==========
    github.com/ioc-golang/shopping-system/internal/auth.Authenticator.Check()
    Response 1: (bool) true
    

AOP layer based on IOC-golang, can provide user-aware, business non-intrusive distributed scenarios full-link tracking capabilities. That is, a system developed by this framework can take any interface method as an entry point to capture the full link of cross-process calls at method granularity.

Full-link tracking

Based on the shopping-system’s full-link time consumption information, the gorm.First() method of the festival process can be identified as the bottleneck of the system.

The implementation of this capability consists of two parts: method granularity link tracing within processes, and RPC call link tracing between processes. IOC aims to create out-of-the-box application development eco-components for developers, and these built-in components and the RPC capabilities provided by the framework are equipped with O&M capabilities.

  • AOP-based in-process link tracing

    The in-process implementation of link tracing provided by IOC-golang is based on the AOP layer. In order to be business-aware, we do not identify the call link by contextual context, but by go routine id. The go runtime call stack is used to record the depth of the current call relative to the entry function.

  • IOC native RPC-based inter-process link tracing

    IOC-golang provides native RPC capabilities without defining an IDL file, just mark // +ioc:autowire:type=rpc for the service provider, generate the relevant registration code and client call stubs, and expose the interface at startup. The client only needs to introduce the client stub for this interface to initiate the call. This native RPC capability is based on json serialization and http transfer protocol, which facilitates the carrier link tracking id.

5. Outlook

IOC-golang open source has broken 700 star so far, its hot growth is beyond my imagination, also hope this project can bring more open source value and production value, welcome more and more developers to participate in the discussion and construction of this project.