Iris V1.12.0-alpha2 is currently used, which automatically parses the RequestBody into an object to inject into the parameters
Iris injection is divided into two parts:
- Dependent source list ready
Register(dependencies… Interface {}). Global dependencies are only stored at registration time and injected after the request is received
Registration process:
- Inject dependencies on request
Mvcap.register (), if passed in to a function, will perform the injection after receiving the request to the request
Dependency injection starts when a request is received, and tracing dependency injection starts at the request
Request process
-
The request is received and processed
core.router.router.go.Router.buildMainHandlerWithFilters()#router.mainHandler
router.mainHandler = func(w http.ResponseWriter, r *http.Request) {
ctx := cPool.Acquire(w, r)
filterExecuted := false
for _, f := range sortedFilters { // from subdomain, largest path to shortest.
// fmt.Printf("Sorted filter execution: [%s] [%s]\n", f.Subdomain, f.Path)
// Match the route
if f.Matcher.Match(ctx) {
// fmt.Printf("Matched [%s] and execute [%d] handlers [%s]\n\n", ctx.Path(), len(f.Handlers), context.HandlersNames(f.Handlers))
filterExecuted = true
// execute the final handlers chain.
ctx.Do(f.Handlers)
break // and break on first found.}}// There is no matching route
if! filterExecuted {// If not at least one match filter found and executed,
// then just run the router.
router.requestHandler.HandleRequest(ctx)
}
// Release iris context
cPool.Release(ctx)
}
Copy the code
Route matching calls the default
router.api_builder.go.defaultPartyMatcher
func defaultPartyMatcher(ctx *context.Context, p Party) bool {
subdomain, path := splitSubdomainAndPath(p.GetRelPath())
// The path before the variable, eg: /users/{uid} gets /usersstaticPath := staticPath(path) hosts := subdomain ! =""
if p.IsRoot() {
// ALWAYS executed first when registered
// through an `Application.UseRouter` call.
return true
}
if hosts {
// Note(@kataras): do NOT try to implement something like party matcher for each party
// separately. We will introduce a new problem with subdomain inside a subdomain:
// they are not by prefix, so parenting calls will not help
// e.g. admin. and control.admin, control.admin is a sub of the admin.
if! canHandleSubdomain(ctx, subdomain) {return false}}// this is the longest static path.
return strings.HasPrefix(ctx.Path(), staticPath)
}
Copy the code
As you can see, only matching paths does not match methods, which will be determined later when operating on the request
After the route is matched, the Iris context begins to call the interware
context.context.go.Context.Do
// Do sets the "handlers" as the chain
// and executes the first handler,
// handlers should not be empty.
//
// It's used by the router, developers may use that
// to replace and execute handlers immediately.
func (ctx *Context) Do(handlers Handlers) {
if len(handlers) == 0 {
return
}
ctx.handlers = handlers
handlers[0](ctx)
}
Copy the code
This method is relatively simple, starting with the first call to the middleware, and then the middleware itself traverses the call
context.context.go.Context.Next
// Next calls the next handler from the handlers chain,
// it should be used inside a middleware.
func (ctx *Context) Next(a) {
if ctx.IsStopped() {
return
}
nextIndex := ctx.currentHandlerIndex + 1
handlers := ctx.handlers
if n := len(handlers); nextIndex == n {
atomic.StoreUint32(&ctx.proceeded, 1) // last handler but Next is called.
} else if nextIndex < n {
ctx.currentHandlerIndex = nextIndex
handlers[nextIndex](ctx)
}
}
Copy the code
If there’s a next one, there’s no next step, the job is done, and both the global middleware and the single request middleware are in the form of these two methods that trigger the chain call, and from the single request there’s actually a handler at the bottom of the chain, To the Router. BuildMainHandlerWithFilters. Handlers
Request context invocation
core.router.router.go.Router.buildMainHandlerWithFilters#f.handlers
f.Handlers = append(f.Handlers, func(ctx *context.Context) {
// The context is reusable and the data needs to be reset
// set the handler index back to 0 so the route's handlers can be executed as expected.
ctx.HandlerIndex(0)
// execute the main request handler, this will fire the found route's handlers
// or if error the error code's associated handler.
router.requestHandler.HandleRequest(ctx)
})
Copy the code
The router. The requestHandler. HandleRequest is the starting point of a single request, iris judge whether routing fully meet here, method is correct
core.router.handler.go.routerHandler.HandlerRequest
func (h *routerHandler) HandleRequest(ctx *context.Context) {
method := ctx.Method()
path := ctx.Path()
config := h.config // ctx.Application().GetConfigurationReadOnly()
if! config.GetDisablePathCorrection() { ... Disable path correction, redirection, and other code elisions}// The route is matched again, starting with the nearest party and judging the parameters
for i := range h.trees {
t := h.trees[i]
ifmethod ! = t.method {continue
}
ifh.hosts && ! canHandleSubdomain(ctx, t.subdomain) {continue
}
// Route tree traversal with parameters
n := t.search(path, ctx.Params())
ifn ! =nil {
ctx.SetCurrentRoute(n.Route)
ctx.Do(n.Handlers)
// found
return
}
// not found or method not allowed.
break}...404Or other redirects to handle ctx.statusCode (http.statusNotFound)}Copy the code
A single request still runs its own middleware, usually JWT, casbin, etc
The first middleware chain of a single request is special to the last one, where the dependency injection and the result of the request will be distributed
hero.handler.go.makeHandler#Func2
return func(ctx *context.Context) {
inputs := make([]reflect.Value, numIn)
Bindings are ready when iris.application is initialized by the handler via getBindingsForFunc when the handler registers with iris.Application
for _, binding := range bindings {
input, err := binding.Dependency.Handle(ctx, binding.Input)
iferr ! =nil {
if err == ErrSeeOther {
continue
}
c.GetErrorHandler(ctx).HandleError(ctx, err)
// return [13 Sep 2020, commented that in order to be able to
// give end-developer the option not only to handle the error
// but to skip it if necessary, example:
// read form, unknown field, continue without StopWith,
// the binder should bind the method's input argument and continue
// without errors. See `mvc.TestErrorHandlerContinue` test.]
}
// If ~an error status code is set or~ execution has stopped
// from within the dependency (something went wrong while validating the request),
// then stop everything and let handler fire that status code.
if ctx.IsStopped() {
return
}
inputs[binding.Input.Index] = input
}
// Call the controller's business logic
outputs := v.Call(inputs)
// Request result distribution processing
iferr := dispatchFuncResult(ctx, outputs, resultHandler); err ! =nil {
c.GetErrorHandler(ctx).HandleError(ctx, err)
}
}
Copy the code
As mentioned earlier, the injection module has two parts, one is initial initialization, the other is injection at request processing, here is injection. Registers are wrapped as Dependency, while injection is an implementation of Dependency.Handle. There are two final injections: instance object and function injection
By function injection
hero.dependency.go.fromDependencyFunc
handler := func(ctx *context.Context, _ *Input) (reflect.Value, error) {
inputs := make([]reflect.Value, numIn)
// Processing of function entry parameters
for _, binding := range bindings {
// The parameters needed for each injection are still injected objects, until there is no need to pass parameters from outside or static injection
input, err := binding.Dependency.Handle(ctx, binding.Input)
iferr ! =nil {
if err == ErrSeeOther {
continue
}
return emptyValue, err
}
inputs[binding.Input.Index] = input
}
// Call the function
outputs := v.Call(inputs)
if firstOutIsError {
return emptyValue, toError(outputs[0])}else if secondOutIsError {
return outputs[0], toError(outputs[1])}// Register allows two parameters, one is the return value and the other is error. The return value is the object we want to inject.
return outputs[0].nil
}
Copy the code
The Dependency.Handle is recursed so that each function parameter is properly entered, Mvcapp.register (func (val B) A) mvcapp.register (func (val A) B) this is not allowed, but what happens if this operation is actually done? The first injection function needs to be retrieved from the user’s RequestBody since B is not in its injection list and has no place to be generated
Statically injected fetching is simpler because it is wrapped, so there is only one line of code:
hero.dependency.go.fromStructValue
handler := func(*context.Context, *Input) (reflect.Value, error) {
return v, nil
}
Copy the code
RequestBody injection
hero.binding.go.payloadBinding
Handle: func(ctx *context.Context, input *Input) (newValue reflect.Value, err error) {
wasPtr := input.Type.Kind() == reflect.Ptr
ifserveDepsV := ctx.Values().Get(context.DependenciesContextKey); serveDepsV ! =nil {
if serveDeps, ok := serveDepsV.(context.DependenciesMap); ok {
if newValue, ok = serveDeps[typ]; ok {
return}}}if input.Type.Kind() == reflect.Slice {
newValue = reflect.New(reflect.SliceOf(indirectType(input.Type)))
} else {
newValue = reflect.New(indirectType(input.Type))
}
ptr := newValue.Interface()
err = ctx.ReadBody(ptr)
if! wasPtr { newValue = newValue.Elem() }return
}
Copy the code
After the handler loops and recurses, the injection of the function is complete. But the question in mind is when is the Controller property assigned?
Redebugging shows that Controller bindings are injected into the list, which is a member of bindings (first) and handled in struct
Handling of controller
hero.struct.go.Struct.Acquire
// Acquire returns a struct value based on the request.
// If the dependencies are all static then these are already set-ed at the initialization of this Struct
// and the same struct value instance will be returned, ignoring the Context. Otherwise
// a new struct value with filled fields by its pre-calculated bindings will be returned instead.
func (s *Struct) Acquire(ctx *context.Context) (reflect.Value, error) {
if s.Singleton {
ctx.Values().Set(context.ControllerContextKey, s.ptrValue)
return s.ptrValue, nil
}
ctrl := ctx.Controller()
ifctrl.Kind() == reflect.Invalid || ctrl.Type() ! = s.ptrType/* in case of changing controller in the same request (see RouteOverlap feature) */ {
ctrl = reflect.New(s.elementType)
ctx.Values().Set(context.ControllerContextKey, ctrl)
elem := ctrl.Elem()
for _, b := range s.bindings {
input, err := b.Dependency.Handle(ctx, b.Input)
iferr ! =nil {
if err == ErrSeeOther {
continue
}
s.Container.GetErrorHandler(ctx).HandleError(ctx, err)
if ctx.IsStopped() {
// return emptyValue, err
return ctrl, err
} / / # 1629
}
// Inject data into struct
elem.FieldByIndex(b.Input.StructFieldIndex).Set(input)
}
}
return ctrl, nil
}
Copy the code
Now that the injection is complete, how are bindings, which are important for injection, initialized?
Dependent source list ready
NewDependency in the mvcapp.register method
hero.dependency.NewDependency
// NewDependency converts a function or a function which accepts other dependencies or static struct value to a *Dependency.
//
// See `Container.Handler` for more.
func NewDependency(dependency interface{}, funcDependencies ... *Dependency) *Dependency {
if dependency == nil {
panic(fmt.Sprintf("bad value: nil: %T", dependency))
}
if d, ok := dependency.(*Dependency); ok {
// already a *Dependency.
return d
}
v := valueOf(dependency)
if! goodVal(v) {panic(fmt.Sprintf("bad value: %#+v", dependency))
}
dest := &Dependency{
Source: newSource(v),
OriginalValue: dependency,
}
if! resolveDependency(v, dest, funcDependencies...) {panic(fmt.Sprintf("bad value: could not resolve a dependency from: %#+v", dependency))
}
return dest
}
func resolveDependency(v reflect.Value, dest *Dependency, funcDependencies ... *Dependency) bool {
return fromDependencyHandler(v, dest) || // This is a copy of DependencyHandler
fromStructValue(v, dest) || // Static injection
fromFunc(v, dest) || // Function injection. The first input parameter is context and has 1 or 2 inputs
len(funcDependencies) > 0 && fromDependentFunc(v, dest, funcDependencies) // Function injection
}
Copy the code
The binding of obtaining
Either func or struct, this function is called to get bindings that it needs in addition to its own logic
hero.binding.go.getBindingsFor
func getBindingsFor(inputs []reflect.Type, deps []*Dependency, paramsCount int) (bindings []*binding) {
// Path parameter start index is the result of [total path parameters] - [total func path parameters inputs],
// moving from last to first path parameters and first to last (probably) available input args.
//
// That way the above will work as expected:
// 1. mvc.New(app.Party("/path/{firstparam}")).Handle(.... Controller.GetBy(secondparam string))
// 2. mvc.New(app.Party("/path/{firstparam}/{secondparam}")).Handle(... Controller.GetBy(firstparam, secondparam string))
// 3. usersRouter := app.Party("/users/{id:uint64}"); usersRouter.ConfigureContainer().Handle(method, "/", handler(id uint64))
// 4. usersRouter.Party("/friends").ConfigureContainer().Handle(method, "/{friendID:uint64}", handler(friendID uint64))
//
// Therefore, count the inputs that can be path parameters first.
shouldBindParams := make(map[int]struct{})
totalParamsExpected := 0
ifparamsCount ! =- 1 {
for i, in := range inputs {
if_, canBePathParameter := context.ParamResolvers[in]; ! canBePathParameter {continue
}
shouldBindParams[i] = struct{}{}
totalParamsExpected++
}
}
startParamIndex := paramsCount - totalParamsExpected
if startParamIndex < 0 {
startParamIndex = 0
}
lastParamIndex := startParamIndex
getParamIndex := func(a) int {
paramIndex := lastParamIndex
lastParamIndex++
return paramIndex
}
bindedInput := make(map[int]struct{})
for i, in := range inputs { //order matters.
_, canBePathParameter := shouldBindParams[i]
prevN := len(bindings) // to check if a new binding is attached; a dependency was matched (see below).
for j := len(deps) - 1; j >= 0; j-- {
d := deps[j]
// Note: we could use the same slice to return.
//
// Add all dynamic dependencies (caller-selecting) and the exact typed dependencies.
//
// A dependency can only be matched to 1 value, and 1 value has a single dependency
// (e.g. to avoid conflicting path parameters of the same type).
if _, alreadyBinded := bindedInput[j]; alreadyBinded {
continue
}
match := matchDependency(d, in)
if! match {continue
}
if canBePathParameter {
// wrap the existing dependency handler.
paramHandler := paramDependencyHandler(getParamIndex())
prevHandler := d.Handle
d.Handle = func(ctx *context.Context, input *Input) (reflect.Value, error) {
v, err := paramHandler(ctx, input)
iferr ! =nil {
v, err = prevHandler(ctx, input)
}
return v, err
}
d.Static = false
d.OriginalValue = nil
}
bindings = append(bindings, &binding{
Dependency: d,
Input: newInput(in, i, nil})),if! d.Explicit {// if explicit then it can be binded to more than one input
bindedInput[j] = struct{}{}
}
break
}
// The dependency source cannot be found after traversing all the dependencies passed in
if prevN == len(bindings) {
// Basic types such as int string can be path parameters
if canBePathParameter {
// no new dependency added for this input,
// let's check for path parameters.
bindings = append(bindings, paramBinding(i, getParamIndex(), in))
continue
}
// Do not get them from the RequestBody
// else add builtin bindings that may be registered by user too, but they didn't.
if isPayloadType(in) {
bindings = append(bindings, payloadBinding(i, in))
continue}}}return
}
Copy the code
Route parsing in Controller
application.Handler -> application.handler -> c := newControllerActivator -> c.activite() -> c.parseMethods(); c.parseHTTPErrorHandler() -> parseMethod -> ControllerActivitor.Handle
mvc.mvc.go.Application.handle
func (app *Application) handle(controller interface{}, options ... Option) *ControllerActivator {
// initialize the controller's activator, nothing too magical so far.
// Some information about the controller is stored here, which can be used to generate the Controller object when the request comes in
c := newControllerActivator(app, controller)
// check the controller's "BeforeActivation" or/and "AfterActivation" method(s) between the `activate`
// call, which is simply parses the controller's methods, end-dev can register custom controller's methods
// by using the BeforeActivation's (a ControllerActivation) `.Handle` method.
if before, ok := controller.(interface {
BeforeActivation(BeforeActivation)
}); ok {
before.BeforeActivation(c)
}
for _, opt := range options {
ifopt ! =nil {
opt.Apply(c)
}
}
// The route is parsed and registered in iris.app, which generates a new router
c.activate()
if after, okAfter := controller.(interface {
AfterActivation(AfterActivation)
}); okAfter {
after.AfterActivation(c)
}
// Add mvcApplication to the Controllers list
app.Controllers = append(app.Controllers, c)
return c
}
Copy the code
Iris initialization and request:
summary
- Iris MVC for
Party
To encapsulate, toParty
callHandler()
registeredController
Will be convertedAPIBuilder
, generate a list of routers, each Router will contain its path information, controller, superior information, middleware, etc - call
app.Listen
When will iris be rightAPIBuilder
Some information about the router is added (e.gmainHandler
),iris.New()
Is generated whenRouter
As the vertex of the tree, it will be set to handle requests when they arrive - When the request arrives, the Router will be matched from the root Router to find the Router that best matches the path. The Router will then process the request. The path and request mode will be matched again before the business logic to find the final business logic processor
- Dependency injection’s dependency sources are an ordered list. Later registered dependencies can reference previously registered dependencies, but cannot reference later registered dependencies. Referencing unregistered dependencies may result in null Pointers or request resolution exceptions
- The core of IRIS should be middleware. Request processing of IRIS is accomplished through different middleware, including dependency injection, parameter parsing, request processing, etc