面向接口编程的三个例子

admin 2022年5月3日01:05:14安全开发评论6 views5667字阅读18分53秒阅读模式

背景

在用go操作mysql数据库时会经常见到类似下面的代码,空导入"go-sql-driver/mysql"

import (
 "database/sql"
 "time"

 _ "github.com/go-sql-driver/mysql"   // 空导入
)

// ...

db, err := sql.Open("mysql""user:[email protected]/dbname")

我之前对上面代码有点疑问:空导入有什么意义吗?

后面知道go在导入包时,会执行包中的init函数,所以上面的空导入会执行 github.com/go-sql-driver/mysql/driver.go[1] 中的init函数来注册驱动

func init() {
 sql.Register("mysql", &MySQLDriver{})
}

再后来,见到好多次类似"注册模式"的写法,逐渐能从中体会到"面向接口编程"的思想。然后就多了一个好处:在做代码审计时,因为了解这种"业务套路",所以更容易理解代码逻辑;

下面分享三个用到这种模式的例子,分别是go sql库、go swagger库、python flask库

分析

这种"注册模式"是什么?

包含有三个角色:

  • 接口层:定义接口、提供"注册实例接口"、提供"获取服务"或者"功能接口"
  • 服务提供者:实现接口、注册"接口实现"
  • 服务使用者:调用"获取服务接口"

这个结论是我根据三个例子总结出来的,下面来具体看看三个角色的功能

go sql

是什么?

https://github.com/go-sql-driver/mysql[2] 文档中的例子,用户可以使用驱动名获取到"包含驱动实例的对象"

db, err := sql.Open("mysql""user:[email protected]/dbname") // db中包含驱动实现

这里"用户"就是"服务使用者"。

"接口层"是"database/sql"库,它在 database/sql/driver/driver.go[3] 文件中定义了接口,"驱动"需要实现下面的Open方法

type Driver interface {
 ...
 Open(name string) (Conn, error)
}

database/sql/sql.go[4]中提供"注册实例接口","驱动"可以调用Register函数注册。

func Register(name string, driver driver.Driver) {
 driversMu.Lock()
 defer driversMu.Unlock()
 if driver == nil {
  panic("sql: Register driver is nil")
 }
 if _, dup := drivers[name]; dup {
  panic("sql: Register called twice for driver " + name)
 }
 drivers[name] = driver
}

database/sql/sql.go[5]中提供"获取服务接口","用户"可以调用Open函数获取DB实例

func Open(driverName, dataSourceName string) (*DB, error) {
 driversMu.RLock()
 driveri, ok := drivers[driverName]
 driversMu.RUnlock()
 ...

 return OpenDB(dsnConnector{dsn: dataSourceName, driver: driveri}), nil
}

"服务提供者"就是"驱动",这里就是 github.com/go-sql-driver/mysql[6]

它实现了Driver接口

type MySQLDriver struct{}
...
func (d MySQLDriver) Open(dsn string) (driver.Conn, error) {
 cfg, err := ParseDSN(dsn)
 if err != nil {
  return nil, err
 }
 c := &connector{
  cfg: cfg,
 }
 return c.Connect(context.Background())
}

并且调用Register方法注册"接口实现",代码见github.com/go-sql-driver/mysql/driver.go[7]

func init() {
 sql.Register("mysql", &MySQLDriver{})
}

swagger

是什么?

在审计 tidb-dashboard[8] 项目时,关注到swagger[9]

tidb-dashboard用swagger来提供在线的api文档服务。

服务使用者

"服务使用者",这里是tidb-dashboard[10]

func Handler() http.Handler {
 return httpSwagger.Handler()
}

如果跟进方法,就会看到调用swag.ReadDoc()方法,这个方法就是"接口层"提供的"获取服务接口"。

接口层

"接口层"就是swagger库,在 swagger.go[11] 文件中

定义接口

// Swagger is an interface to read swagger document.
type Swagger interface {
 ReadDoc() string
}

提供"注册实例接口"

// Register registers swagger for given name.
func Register(name string, swagger Swagger) {
  ...
 swags[name] = swagger
}

提供"获取服务接口"

func ReadDoc(optionalName ...string) (string, error) {
 ...
 swag, ok := swags[name]
 ...
 return swag.ReadDoc(), nil
}

服务提供者

"服务提供者"这里是用户自己。这里用法有点特殊,tidb-dashboard仓库中没有"服务实现"相关代码,在编译tidb-dashboard项目时会生成代码。

生成后的代码我放在了gist[12]

可以看到它实现了Swagger接口

type s struct{}

func (s *s) ReadDoc() string {
 ...

 return tpl.String()
}

注册接口

func init() {
 swag.Register(swag.Name, &s{})
}

和第一个例子的区别

区别在于,这个例子中,"服务提供者"和"服务使用者"都是用户自己。

那么为什么不直接自己调自己,还经过"接口层"呢?

python flask扩展

是什么?

用户可以用flask框架的cors扩展来做跨域请求时的限制。

插件文档[13]中的例子如下

from flask import Flask
from flask_cors import CORS

app = Flask(__name__)
CORS(app)

@app.route("/")
def helloWorld():
  return "Hello, cross-origin-world!"

这里"服务使用者"不需要找"接口层"要"服务实例"

接口层

flask框架[14]中定义了一个函数AfterRequestCallable类型

提供"注册实例接口",如下,只是把函数放进了列表中

@setupmethod
def after_request(self, f: AfterRequestCallable) -> AfterRequestCallable:
 ..
  self.after_request_funcs.setdefault(None, []).append(f)
  return f

服务提供者

cors插件在flask框架基础上,提供了cors相关的安全能力。

flask_cors/extension.py[15]

实现接口:cors_after_request函数是AfterRequestCallable类型具体的实现

def make_after_request_function(resources):
  def cors_after_request(resp):
      ...
      normalized_path = unquote_plus(request.path)
      for res_regex, res_options in resources:
          if try_match(normalized_path, res_regex):
              ...
      ...
      return resp
  return cors_after_request

注册接口:在CORS实例化时,会注册实例提供安全能力

class CORS(object):
    ...

    def __init__(self, app=None, **kwargs):
        ...
        self.init_app(app, **kwargs)

    def init_app(self, app, **kwargs):
        ...
        cors_after_request = make_after_request_function(resources)
        app.after_request(cors_after_request)   # 有点注册一个中间件的感觉

和前面两个例子的区别

细品的话,可以看到这个例子和前两个例子又有很多不同:

  • 没有显式的"接口定义",毕竟python没有接口关键字
  • 注册的是"函数",而不是"对象"
  • "接口层"不提供"获取服务","服务使用者"也不需要"获取服务"
  • 由"服务使用者"注册"接口实例",而不是"服务提供者"注册接口实例

可以想一想为什么会有这些区别,能把这些区别"修改"回去吗?比如如果我是cors扩展库作者,我就不能在cors库里自动注册服务,让库的使用者少写几行代码吗?

总结

通过分析这三个"注册模式"的例子,我自己对"面向接口编程"有点感觉。后面感觉这种思想很基础、很常见、很实用,比如rpc、spring ioc容器、微服务的服务注册等都和这个"注册模式"很像。

如果你觉得疑惑,或者觉得我写得比较怪,推荐你找一个你熟悉的库自己分析一下。

参考资料

[1]

github.com/go-sql-driver/mysql/driver.go: https://github.com/go-sql-driver/mysql/blob/v1.6.0/driver.go

[2]

https://github.com/go-sql-driver/mysql: https://github.com/go-sql-driver/mysql

[3]

database/sql/driver/driver.go: https://github.com/golang/go/blob/go1.17.9/src/database/sql/driver/driver.go

[4]

database/sql/sql.go: https://github.com/golang/go/blob/go1.17.9/src/database/sql/sql.go

[5]

database/sql/sql.go: https://github.com/golang/go/blob/go1.17.9/src/database/sql/sql.go

[6]

github.com/go-sql-driver/mysql: https://github.com/go-sql-driver/mysql/blob/v1.6.0/driver.go

[7]

github.com/go-sql-driver/mysql/driver.go: https://github.com/go-sql-driver/mysql/blob/v1.6.0/driver.go

[8]

tidb-dashboard: https://github.com/pingcap/tidb-dashboard

[9]

swagger: https://github.com/swaggo/swag

[10]

tidb-dashboard: https://github.com/pingcap/tidb-dashboard/blob/v2022.03.31.1/pkg/swaggerserver/handler.go

[11]

swagger.go: https://github.com/swaggo/swag/blob/v1.8.1/swagger.go

[12]

gist: https://gist.github.com/leveryd/5ae3ce4940831464d43945f42c68b4c0

[13]

插件文档: https://github.com/corydolphin/flask-cors

[14]

flask框架: https://github.com/pallets/flask/blob/main/src/flask/scaffold.py

[15]

flask_cors/extension.py: https://github.com/corydolphin/flask-cors/blob/master/flask_cors/extension.py


原文始发于微信公众号(leveryd):面向接口编程的三个例子

特别标注: 本站(CN-SEC.COM)所有文章仅供技术研究,若将其信息做其他用途,由用户承担全部法律及连带责任,本站不承担任何法律及连带责任,请遵守中华人民共和国安全法.
  • 我的微信
  • 微信扫一扫
  • weinxin
  • 我的微信公众号
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年5月3日01:05:14
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                  面向接口编程的三个例子 http://cn-sec.com/archives/960503.html

发表评论

匿名网友 填写信息

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: