「Go框架」深入理解iris中的mvc之实现原理

843 阅读8分钟

一、mvc的基本使用

在iris中,还封装了mvc包,该包可以让开发者快速的搭建出基于mvc(model-view-controller)分层的业务系统。其基本使用如下:

package main

import (
	"github.com/kataras/iris/v12"
	"github.com/kataras/iris/v12/context"
	"github.com/kataras/iris/v12/mvc"
)

func main() {
	app := iris.New()

    // 基于app.Party("/")分组 初始化mvcApplication
	mvcApplication := mvc.New(app.Party("/"))

    // 注册controller
	mvcApplication.Handle(new(testController))

	app.Listen(":8080")
}

//  定义controller处理器
type testController struct {
	Ctx *context.Context
}

func (c *testController) GetHome() {
	c.Ctx.Writef(ctx.Method())
}

func (c *testController) Post() {
	c.Ctx.Writef(ctx.Method())
}

这样,就能搭建一个最简单的controller处理器了。然后运行该服务,在浏览器中输入 http://localhost:8080/home 就能访问到testControllerGetHome方法。

我们知道,在iris框架中,是需要注册路由的,即将路径对应到特定的context.Handler类型(函数类型)的处理器,当用户访问对应的路径时,才能执行对应处理器上的函数体的逻辑的。那么,mvc包是如何做到路径到controller中函数的映射的呢?

二、mvc的实现原理

通过上面使用基于mvc包的示例,我们可以了解到,mvc包的操作实际上是基于mvc.Application类型的实例进行的。在初始化mvc.Application实例的时候,传入参数是一个app.Party的对象,即一个路由组。因此,一个mvc.Application实例本质上就是一个路由分组。

然后,通过mvc.Application.Handle方法,将testController类型的对象进行注册。实际上是controller依赖的对象(model、service等)注入到controllerController中的方法转换成路由controller中执行的结果渲染到view的过程。

image.png

2.1 从controller到ControllerActivator的转换

在第一部分的代码示例中,当初始化了mvc.Application对象后,就是通过如下这行代码对controller中的方法进行转换的:

mvcApplication.Handle(new(testController))

代码就一行,很简单。但就是这个Handle函数将testController中的方法转换成了路由,使用户可以通过对应的路径访问到controller中的方法。接下来我们看看该Handle方法中都做了哪些事情。

点击进入源代码,直到iris/mvc/mvc.go文件中的handle方法,如下图示所示。 image.png 该方法显示,首先将controller包装成了一个ControllerActivator对象c。其结构体如下: image.png 各字段含义如下:

  • injector:该controller中依赖的字段类型。即该controller结构体中有哪些字段。这个我们后面详细解释。
  • Value:该字段即在一开始new出来的controller的对象值。比如new(testController)的值。
  • Typ:该字段是controller的具体类型。比如上面示例中的testController类型。
  • routes:controller中每个函数名对应的具体的路由。

然后该对象c做了3件事情:执行对象c的BeforeActivation方法(如果controller中定义了该方法)、activate方法和AfterActivation方法(如果定义了该方法)。我们先跳过BeforeActivation和AfterActivation方法,重点看下activate方法。

2.2 ControllerActivator的activate方法

c.activate方法的源码如下图所示,主要是parseMethods方法,根据名字也能猜到是要解析方法了。 image.png

我们再进入c.parseMethods方法的源代码,如下: image.png

这里的逻辑也很简单,先是获取该类型(即controller的结构体类型,例如示例中的testController类型)的方法个数,然后依次遍历所有的方法,并对每个方法进行解析,也就是代码中的c.parseMethod(m)方法。

接下来进入到c.parseMethod(m)的代码逻辑中,看看该方法做了些什么。 image.png

这里看到,通过parseMethod解析出来了请求的方法名称httpMethod(例如GET、POST等)、请求的路径httpPath。然后再通过c.Handle函数将对应的请求方法和请求路径以及方法名注册成iris的常规路由。

到这里我们先总结一下controller转换的过程:

  • 首先,先将controller对象封装成ControllerActivator对象。该对象包含了controller类型的值以及具体的controller数据类型。
  • 其次,通过reflect包获取该controller类型的有哪些方法,并依次遍历解析这些方法。
  • 然后,根据方法名称解析出标准的HTTP请求的方法以及请求路径,即代码中的parseMethord函数。
  • 最后,将该请求方法以及请求路径转换成iris常规的路由,即代码中的c.Handle函数。

因为请求方法和请求路径是根据controller中的方法名称解析出来的,所以开发人员在给controller的方法的命名时,需要遵循以下规则

  • controller中的方法必须采用驼峰式命名,且首字母必须大小。
  • parseMethod会大写字母为分割符,将方法名分割成多个单词。例如。GetHome会分割成Get、Home两个单词。方法名HOME,则会被分割成H、O、M、E四个单词。具体实现算法可查看源代码methodLexer.reset函数
  • 方法名的第一个单词必须是http请求的方法名。有效的请求方法为GET、HEAD、POST、PUT、PATCH、DELETE、CONNECT、OPTIONS、TRACE。具体的实现可查看源码请求方法校验逻辑

image.png

  • 从方法名的第二个单词开始,使用 "/" 符号将各个单词连接成对应的请求路径。所以方法名实际上是由请求方法+请求路径模式组成的。

例如在testController中有如下GetMyHome方法,那么,对应的请求路径是/my/home。请求http://localhost:8080/my/home就会执行testController的GetMyHome方法的逻辑。

  • 在方法名中可以通过 By可以在路由中增加动态参数

例如在testController中有如下方法GetByHome(username string),则对应的请求路径会被转换成 /{param1:string}/home。请求http://localhost:8080/yufuzi/home 就能访问到testController中的GetByHome函数的逻辑,并且在该方法中username的变量值就是路径中的yufizi。如下:

  • 若By在方法名的中间位置,一个By只对应一个动态参数;若By在方法名的最后,则方法中的所有输入参数都被解析成路径中的动态参数。

示例一:By在方法名中间位置 GetByHome方法名中有两个参数,那么,访问该路径http://localhost:8080/yufuzi/home时就会报panic。如下:

func (c *testController) GetByHome(username string, param2 int) {
	c.Ctx.Writef(c.Ctx.Method() + " " + username + " Home")
    c.Ctx.Writef("param2:" + param2) //这里param2是对应类型的默认值:0
}

**示例二:**By在方法名最后位置 GetHomeBy方法名中有两个参数,则会转换成对应的路径 /home/{param1:string}/{param2:int}。如下:

func (c *testController) GetHomeBy(username string, param2 int) {
	c.Ctx.Writef(c.Ctx.Method() + " " + username + " Home")
    c.Ctx.Writef("param2:" + param2)
}

以上是对controller中方法的解析以及命名时需要遵守的规则。接下来,我们继续看如何将controller的方法转换成具体的路由请求处理器。

2.3 ControllerActivator的Handle方法 -- 将controller中的方法转换成请求处理器

我们知道iris的标准路由处理器是type Handler func(*Context)类型的这一个函数。那在mvc中,是如何将controller中的方法转换成这样标准的请求处理器类型的呢?

这个转换主要在ControllerActivator.parseMethod方法的第二部分的功能:ControllerActivator.Handle。进入该函数的源代码,直到ControllerActivator.handleMany函数,如下: image.png

在handleMany函数中主要做了两件事:

  • 将函数转换成请求处理器。即c.handlerOf函数做的事情
  • 将请求处理注册成标准的路由。即c.app.Router.HandleMany函数做的事情。

这里我们主要看c.handlerOf是如何将函数转换请求处理器的。至于注册成标准路由,大家可以参考这篇iris路由相关的文章:深入理解iris路由的底层实现原理

2.4 将controller的函数转换成标准的请求处理器类型

处理该功能的是逻辑在handlerOf函数中。下面是handlerOf的源代码,我们看到主要做了两件事:一是attachInjector函数;一个是injector中的MethodHandler函数。 image.png

实际将controller的函数转换成请求处理器的是在injector的MethodHandler中进行的,即返回的handler对象。而injector就是在第一步的c.attachInjector函数中构造出来的。

那么,c.injector是什么呢?下面我们看下其对应的结构体,如下图: image.png

可以看出来,injector实际上是对controller具体类型及其对象的描述。还是以testController为例,在Struct.ptrType就是代表testController这种数据类型;Struct.ptrValue代表的是testController对象值;bindings代表的是在testController中有哪些字段值。比如testController中要是定义如下:

type testController struct {
	model *userModel
}

那么,testController对象就需要绑定userModel类型的值。在实例化testController时,需要将一个具体的userModel类型的值赋值给model变量,这样在testController的方法中就能引用该变量从数据源中读取数据。而这种依赖就是保存在bingdings中的。

我们主要看第二部分,c.injector.MethodHandler将函数转换成路由处理器类型。点击进入源码,直到/iris/hero/handler.go文件的makeHandler函数,如下所示(注:这里为了突出最终的返回值,省略了一些代码)。 image.png

首先,makeHandler返回值就是一个context.Handler类型的函数。在函数体内有3部分:获取controller函数的输入参数值、通过v.Call函数调用controller实际的函数体、通过dispatchFuncResult分发函数的返回值。

到此,就把controller中的函数转换成了标准的路由处理器类型context.Handler。并且,只有controller中的方法名的第一个单词是HTTP标准的请求方法(GET、HEAD、POST、PUT、PATCH、DELETE、CONNECT、OPTIONS、TRACE)时,才会将该方法自动注册成对应的路由。

在了解了mvc的实现原理后,下一篇我们讲解mvc的高级使用,敬请期待。

---特别推荐---

特别推荐:一个专注go项目实战、项目中踩坑经验及避坑指南、各种好玩的go工具的公众号,「Go学堂」,专注实用性,非常值得大家关注。点击下方公众号卡片,直接关注。关注送《100个go常见的错误》pdf文档。