Odoo Routing

前置知识:

Odoo中所有的web请求都是由库 werkzeug 驱动的,Odoowerkzeug的基础上进一步封装,隐藏了werkzeug的复杂性,让我们更加方便地定义路由。探索隐藏在Odoo带来的便利背后的工作原理将是一件有趣的体验,现在,就让我们来体验这种乐趣吧。

首先,我们先看看Odoo是如何定义一个路由的。

# controllers/main.py

from odoo import http
from odoo.http import request


class Main(http.Controller):
    @http.route('/hello', type='http', auth='none')
    def hello(self):
        return "<h1>hello world!</h1>"

可以看到,Odoo的路由实现方式和Flask的实现方式相似,相对于werkzeug简单便利多了。

重启服务器后,访问/hello我们就能看到熟悉的hello world!了。

odoo url

Odoo的路由定义有两个重要的部分:

我们先来看看odoo.http.route,为了方便讨论,我们暂时忽略route里边的typeauth参数。

# part of odoo/http.py
def route(route=None, **kw):
    routing = kw.copy()
    assert 'type' not in routing or routing['type'] in ("http", "json")
    def decorator(f):
        if route:
            if isinstance(route, list):
                routes = route
            else:
                routes = [route]
            routing['routes'] = routes
        @functools.wraps(f)
        def response_wrap(*args, **kw):
            response = f(*args, **kw)
            if isinstance(response, Response) or f.routing_type == 'json':
                return response

            if isinstance(response, basestring):
                return Response(response)

            if isinstance(response, werkzeug.exceptions.HTTPException):
                response = response.get_response(request.httprequest.environ)
            if isinstance(response, werkzeug.wrappers.BaseResponse):
                response = Response.force_type(response)
                response.set_default()
                return response

            _logger.warn("<function %s.%s> returns an invalid response type for an http request" % (f.__module__, f.__name__))
            return response
        response_wrap.routing = routing
        response_wrap.original_func = f
        return response_wrap
    return decorator

可以看出,route是个装饰器,这个装饰器主要做了两件事

由于装饰器这个语法糖,新的视图函数和原视图函数的名字和作用都是一样的,不过新视图函数多了一个返回值自动处理过程和两个函数属性。(这里是一个很好的运用函数属性的例子,把函数当作一个名字空间。)

现在,我们来看看odoo.http.Controller

# part of odoo/http.py
class Controller(object):
    __metaclass__ = ControllerType

这个类啥都没实现,就指定了一个元类,重要的东西还是在元类里实现的。

# part of odoo/http.py

controllers_per_module = collections.defaultdict(list)

class ControllerType(type):
    def __init__(cls, name, bases, attrs):
        super(ControllerType, cls).__init__(name, bases, attrs)

        # 将请求类型设置在原视图函数上(原视图函数也是个命名空间)
        for k, v in attrs.items():
            if inspect.isfunction(v) and hasattr(v, 'original_func'):
                routing_type = v.routing.get('type')
                v.original_func.routing_type = routing_type

        # 将控制器存储在controllers_per_module中
        name_class = ("%s.%s" % (cls.__module__, cls.__name__), cls)
        class_path = name_class[0].split(".")
        if not class_path[:2] == ["odoo", "addons"]:
            module = ""
        else:
            module = class_path[2]
        controllers_per_module[module].append(name_class)

 # 注:这里省略了关于类继承的处理

我们自定义的controller继承odoo.http.Controllerodoo.http.Controller的元类为ControllerType,也就是说,自定义controller是由odoo.http.Controller生成的。odoo.http.Controller在生成自定义controller后会进行一些操作

这里有个疑问,为什么不打这一步放到装饰器route中呢,毕竟route中有response_wrap.routing = routing,即把url以及各种参数保存到新视图函数的属性routing上。我觉得把请求类型绑定到原视图函数这一步放到route中,更能体现代码的一致性。

controllers_per_module

至此,关于odoo.http.routeodor.http.Controller的研读已完,但我们的脚步不应停止,因为我们还没见到一点werkzeug的影子。目前,我们只是发现ODOO将控制器保存到controllers_per_module而已。

http.py中,我们发现 Odoo web application 分发请求时会调用routing_map函数。

# part of odoo/http.py

def routing_map(modules, nodb_only, converters=None):
    # 生成 Map 实例
    routing_map = werkzeug.routing.Map(strict_slashes=False, converters=converters)
    for module in modules:
        if module not in controllers_per_module:
            continue
        for _, cls in controllers_per_module[module]:
            o = cls()
            members = inspect.getmembers(o, inspect.ismethod)
            for _, mv in members:
                if hasattr(mv, 'routing'):
                    # 请求类型默认为http,权限默认为user
                    routing = dict(type='http', auth='user', methods=None, routes=None)
                    routing.update(mv.routing)
                    # 这里是权限判断,我们暂时认为其值为`True`即可
                    if not nodb_only or routing['auth'] == "none":
                        assert routing['routes'], "Method %r has not route defined" % mv
                        # 构建endpoint
                        endpoint = EndPoint(mv, routing)
                        for url in routing['routes']:
                            xtra_keys = 'defaults subdomain build_only strict_slashes redirect_to alias host'.split()
                            kw = {k: routing[k] for k in xtra_keys if k in routing}
                            # 添加rule
                            routing_map.add(werkzeug.routing.Rule(url, endpoint=endpoint, methods=routing['methods'], **kw))
    return routing_map

# 注:为了便于分析,这里同样删除了跟控制器继承相关的代码

routing_map这个函数是比较简单的,(说明: 第一个参数表示目标模块列表。odoo是按模块分割业务功能的,我们需要什么功能就加载什么模块,而controllers_per_module中存有所有模块的控制器信息,所以这里需要modules这个参数),就是生成一个werkzeug.routing.Map对象,然后遍历modules,从controllers_per_module找到响应路由信息,生成werkzeug.routing.Rule对象,添加到Map对象中。

The end.

标签: Odoo