By Li Zhixin (Jifeng)
Aspect Oriented Programming (AOP) is an idea based on programming design that aims to achieve specific modularity and reduce the coupling between business logic by intercepting the aspect of business processes. This idea has been practiced in many well-known projects, such as Spring PointCut, gRPC Interceptor, and Dubbo Filter. AOP is just a concept that is applied in different scenarios and produced with different implementations. Let's first discuss specific RPC scenarios, taking gRPC as an example.
From grpc.io
For a one-time RPC process, gRPC provides an Interceptor interface that can be extended to facilitate developers to write business-related interception logic. For example, it introduces capabilities (such as authentication, service discovery, and observability). There are many extended implementations based on Interceptor in the gRPC ecosystem. Please see go-grpc-middleware [1] for more information. These extended implementations belong to the gRPC ecosystem and are limited to the concepts on both sides of the Client and the Server. All are limited to RPC scenarios.
We will abstract the concrete scene with Spring's method as the reference.
Spring has a powerful dependency injection capability. On this basis, it provides the AOP capability that adapts to business object methods. In addition, it can encapsulate interceptors outside business functions by defining pointcut. These concepts of aspect and pointcut are limited to the Spring framework and managed by its dependency injection capability, namely inversion of control (IOC).
The concept of AOP needs to be combined with specific scenarios and must be constrained by the integrated ecology. I think the concept of AOP alone is not development and production-friendly. For example, I can write a series of function calls according to the idea of process-oriented programming. It can be said that AOP is realized, but it is not scalable, transferable, or universal. This constraint is necessary and can be strong or weak. For example, the AOP of Spring ecology has greater scalability with lower restriction. However, it is complicated to implement. Developers need to learn many concepts and APIs of its ecology. If the AOP of Dubbo and gRPC ecology is suitable for RPC scenarios, developers only need to implement interfaces and inject a single API, and their capabilities are constrained.
The constraints can be embodied as dependency injection in development scenarios, which is IOC. The objects that developers need are managed and encapsulated by the ecosystem, whether it is Invoker (of Dubbo) or Bean (of Spring). The IOC process provides constraint excuses, models, and landing values for AOP practices.
The AOP concept has nothing to do with the language. While I am in favor of the point that Java language is required for the best practice scenario when using AOP, I don't think AOP is exclusive to the Java language. There are many excellent projects based on AOP ideas in the Go ecosystem. All these projects share something in common. As I explained in the previous section, they are combined with specific ecosystems to solve specific business scenario problems. The breadth of problem-solving depends on the constraints of its IOC ecosystem. IOC is the cornerstone and AOP is a derivative of IOC ecology. An IOC ecology that does not provide AOP can be clear, while those that provide can be inclusive and powerful.
Last month, I opened the source of the IOC-golang [2] service framework to solve the dependency injection problem in the Go application development process. Many developers compare this framework with Google's open-source wire framework [3] and came to the conclusion that it is not as clear as wire. The essence of this problem is that the design intentions of the two ecosystems are different. Wire focuses on IOC rather than AOP, so developers can quickly implement dependency injection by learning simple concepts and APIs and using scaffolding and code generation capabilities. The development experience is good. IOC-golang focuses on IOC-based AOP capabilities and embraces the scalability of this layer. It regards AOP capabilities as the difference and value point between this framework and other IOC frameworks.
Compared with the SDK for solving specific problems, we can regard the IOC capability of the dependency injection framework as a weakly constrained IOC scenario. After comparing the differences between the two frameworks, two core problems are posed here:
My opinion is that Go ecology needs AOP. Even in the weak constrained IOC scenario, AOP can still be used to do business-independent things, such as enhancing the O&M observability of the application. Due to language features, the AOP of the Go ecosystem is inequivalent to Java. Go does not support annotations, which limits the convenience for developers to use the AOP layer for writing business semantics. Therefore, the AOP of Go is unsuitable for processing business logic. Even if it is forcibly implemented, it is counterintuitive. I prefer to assign O&M observability to the AOP layer of the Go ecosystem. Developers are unaware of AOP.
For example, for the implementation structure of any interface, the IOC-golang framework can be used to encapsulate the O&M AOP layer, thus making all the objects of an application equipped with observability. In addition, we can combine RPC scenarios, service governance scenarios, and fault injection scenarios to generate more O&M expansion ideas.
There are two ideas when using the Go language to implement method proxy: interface proxy through reflection and function pointer exchange based on the monkey patch. The latter does not rely on interfaces and can encapsulate function proxies against methods of any structure. It needs to invade the underlying assembly code and disable compilation optimization, which has requirements for the CPU architecture and impairs performance when processing concurrent requests.
The production of the former is meaningful and depends on interfaces, which is the focus of this section.
As mentioned in the first article about this framework, IOC-golang has two perspectives in the process of dependency injection: structure provider and structure user. The framework accepts the structure defined by the structure provider and provides the structure according to the requirements of the structure user. The structure provider only needs to pay attention to the structure itself and does not need to focus on which interfaces the structure implements. However, structure users need to be concerned about the injection and use of the structure. Is it injected into the interface? Is it injected into pointer? Is it obtained through API? Is it obtained through tag injection?
// +ioc:autowire=true
// +ioc:autowire:type=singleton
type App struct {
// Inject the implementation into the structure pointer
ServiceStruct *ServiceStruct `singleton:""`
// Inject the implementation into the interface
ServiceImpl Service `singleton:"main.ServiceImpl1"`
}
The App's ServiceStruct field is a pointer to a specific structure. The field can locate the structure that is expected to be injected, so there is no need to give the name of the structure that is expected to be injected in the tag. For this kind of field injected into the structure pointer, the AOP capability cannot be provided by injecting the interface proxy. It can only realize through the monkey patch scheme mentioned before, which is not recommended.
The ServiceImpl field of an app is an interface named Service, and the expected injected structure pointer is main.ServiceImpl. It is essentially an assertion logic from structure to interface. Although the framework can verify the interface implementation, the structure user is still required to ensure that the injected interface implements the method. For this method of injecting 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 capability.
Therefore, IOC recommends that developers implement interface-oriented programming instead of directly relying on specific structures. In addition to AOP capability, interface-oriented programming will improve the readability, unit testing capability, and module decoupling degree of the Go code.
Developers of the IOC-golang framework can obtain the structure pointer by API. The structure pointer can be obtained by calling the GetImpl method of the automatic loading model (such as singleton).
func GetServiceStructSingleton() (*ServiceStruct, error) {
i, err := singleton.GetImpl("main.ServiceStruct", nil)
if err != nil {
return nil, err
}
impl := i.(*ServiceStruct)
return impl, nil
}
We recommend developers using the IOC-golang framework to obtain the interface object through API. We can obtain the proxy structure by calling the GetImplWithProxy method of the automatic loading model (such as singleton), which can be asserted as an interface for use. This interface is not manually created by the structure provider but is a structure-specific interface automatically generated by iocli, which will be explained below.
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 of obtaining objects through API can be automatically generated by iocli tools. Note: The function of these codes is to facilitate developers to call API and reduce the amount of code, while the core logic automatically loaded by IOC is not generated by tools, which is different from the implementation idea of dependency injection provided by wire and is also a misunderstanding of many developers.
We know the AOP injection method recommended by the IOC-golang framework is strongly dependent on the interface. However, it takes a lot of time for developers to write a matching interface for all of their structures manually. Therefore, the iocli tool can automatically generate structure-specific interfaces to reduce the amount of code written by developers.
For example, a structure named ServiceImpl contains the GetHelloString method.
// +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)
}
After the iocli gen command is executed, a copy of code zz_generated.ioc.go is generated in the current directory, and the structure-specific interface is contained.
type ServiceImplIOCInterface interface {
GetHelloString(name string) string
}
The name of the specific interface is $(structure name) IOCInterface. The specific interface contains all methods of the structure. The specific interface has two functions:
// +ioc:autowire=true
// +ioc:autowire:type=singleton
type App struct {
// Inject the specific interface of the ServiceImpl structure. There is no need to specify the structure ID in the tag.
ServiceOwnInterface ServiceImplIOCInterface `singleton:""`
}
Therefore, if we look for an existing Go project where the structure pointer is used, it is recommended to replace the Go project with a structure-specific interface and set the framework defaults to the injection agent. For fields with interfaces that have been used, we recommend injecting the structure directly through tags, which is injected agent by the framework by default. For projects developed in this mode, all objects will have O&M capabilities.
The objects injected into interfaces mentioned in the previous section are all encapsulated by the framework by default, with operation and maintenance capabilities. We mentioned that iocli would generate specific interfaces for all structures. In this section, I will explain how the framework encapsulates the proxy layer and how it is injected into the interface.
The structure-specific interface is included in the generated zz.generated.ioc.go code mentioned earlier. Similarly, the definition of the structure agent is included. Let's take the ServiceImpl structure as an example. The following is the proxy structure it generates.
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) with the beginning of a lowercase letter, which implements all methods of the *structure-specific interface* and proxies all method calls to the method field of $(method name). The field is implemented by the framework in a reflective manner.
Like the structure code, the proxy structure is registered to the framework in this generated file.
func init(){
normal.RegisterStructDescriptor(&autowire.StructDescriptor{
Factory: func() interface{} {
return &serviceImpl1_{} // Register Proxy structure
},
})
}
This describes the definition and registration process of the agent structure. When the user expects to obtain a proxy object that encapsulates the AOP layer, he loads the real object, tries to load the proxy object, and instantiates the proxy object through reflection and injects the interface, thus giving the interface operation and maintenance capabilities. This process can be shown in the following figure:
After understanding the implementation ideas, we can think that in the application developed using the IOC-golang framework, all interface objects injected and obtained from the framework are capable of operation and maintenance. We can expand our expected capabilities based on AOP. We provide a simple e-commerce system demo shopping-system [4] to demonstrate the AOP-based visualization capabilities of IOC-golang in distributed scenarios. Developers interested in this can refer to README to run this system in clusters and experience its O&M capability base.
github.com/alibaba/ioc-golang/extension/autowire/rpc/protocol/protocol_impl.IOCProtocol
% 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]
We can monitor the calling of the Check method of the authentication interface using the iocli watch command:
iocli watch github.com/ioc-golang/shopping-system/internal/auth.Authenticator Check
Initiate a call against the entry:
curl -i -X GET 'localhost:8080/festival/listCards?user_id=1&num=10'
You can view the call parameters and return values of the monitored method. The user id is 1.
% 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
Based on the IOC-golang AOP layer, it can provide full Tracing Analysis capabilities in distributed scenarios with no user awareness or business intrusion. That means a system developed by this framework can use any interface method as the entry to collect the full trace of cross-process calls with method granularity.
The implementation of this capability consists of method granularity tracing analysis within a process and RPC call Tracing Analysis between processes. IOC is designed to create out-of-the-box application development ecosystem components. All of these built-in components and RPC capabilities provided by frameworks have O&M capabilities. AOP-based in-process tracing analysis:
The in-process implementation of the tracing analysis capability provided by IOC-golang is based on the AOP layer. We do not identify the call trace through context but through Go routine id to achieve business unawareness. Use the Go runtime call stack to record the depth of the current call relative to the entry function.
The native RPC capability provided by the IOC-golang only needs to tag // + ioc:autowire:type=rpc
for the service provider to generate the relevant registration code and the client call stub without the need to define an IDL file. The interface is exposed during startup. The client only needs to introduce the client stub of this interface to initiate the call. This native RPC capability is based on JSON serialization and the HTTP transport protocol to facilitate the hosting of Tracing Analysis IDs.
The open-source of IOC-golang has broken through 700 stars so far. I hope this project can bring greater open-source and production value. You are welcome to participate in the construction of this project.
[1] https://github.com/grpc-ecosystem/go-grpc-middleware
[2] https://github.com/alibaba/ioc-golang
KubeVela: The Golden Path towards Application Delivery Standards
From VLAN to IPVLAN: Virtual Network Device and Its Application in Cloud-Native
506 posts | 48 followers
FollowAlibaba Cloud Community - July 14, 2023
Alibaba Cloud Community - November 4, 2024
XianYu Tech - September 8, 2020
Alibaba Tech - July 11, 2019
Alibaba Cloud Community - May 7, 2024
Alibaba Cloud Community - February 28, 2024
506 posts | 48 followers
FollowMulti-source metrics are aggregated to monitor the status of your business and services in real time.
Learn MoreMake identity management a painless experience and eliminate Identity Silos
Learn MoreA convenient and secure cloud-based Desktop-as-a-Service (DaaS) solution
Learn MoreBaaS provides an enterprise-level platform service based on leading blockchain technologies, which helps you build a trusted cloud infrastructure.
Learn MoreMore Posts by Alibaba Cloud Native Community