面向侧面的程序设计(aspect-oriented programming,AOP),通过将解决特定领域问题的代码从业务逻辑中独立出来,从而提高代码的可维护性。
从主关注点中分离出横切关注点是面向侧面的程序设计的 核心概念 。分离关注点使得解决特定领域问题的代码从业务逻辑中独立出来,业务逻辑的代码中不再含有针对特定领域问题代码的调用,业务逻辑同特定领域问题的关系通过侧面来封装、维护,这样原本分散在在整个应用程序中的变动就可以很好的管理起来。 - 维基百科
示例是根据最近正在负责的 APP 后端项目简化版,需求简单说如下:
思路:首先整个目前项目的主关注点 (core concern) 是 REST 风格的资源服务器 —— 即通过约定俗成的风格来对应具体的数据/资源操作。在这个功能外,需要完成的其他关注点包括:
于是,在 Sinatra 中,可以通过 extensions 的方式将请求加密校验完成,配合 before 来进行统一处理:
require 'sinatra/base' module Sinatra module RequestHeadersVerify module Helpers def headers_valid? # 此处省略真实业务代码 false end end def self.registered(app) app.helpers RequestHeadersVerify::Helpers app.before do unless headers_valid? halt 400, json(ResponseErrror::InvalidHeadersError.new) end end end end register RequestHeadersVerify end
最终采用"中间件"的方式,在请求的最前面一层(横切关注点 crosscutting concerns)将非法请求进行拦截。
于是紧接着第二个流程,验证用户是否登录,与获取当前联系人所在的公司、小组、以及其管理的小组信息一样,这里最快速/方便的做法就是通过 helpers 来实现:
require 'sinatra/base' module Sinatra module UserSessionHelpers HTTP_USER_TOKEN_KEY = 'HTTP_AUTH_TOKEN' def current_user @current_user ||= ( user = User.first token: env[HTTP_USER_TOKEN_KEY] halt 400, json(ResponseErrror::InvalidTokenError.new) unless user user ) end end module OrganizationHelpers # 这里省略掉相关 helpers 代码 end helpers UserSessionHelpers helpers OrganizationHelpers end
最终在 REST 相关的构建代码中,就不需要去考虑用户请求加密的内容,也不需要去考虑用户是否登录(因为如果需要使用到用户信息但是用户没有登录,会直接抛出错误返回)。只需要按照约定的设计风格,把请求的内容在校验了内容和权限后,转成对应的数据库操作,最终再按照约定的内容返回。
第一版已经尽可能的考虑到 解决特定领域问题的代码从业务逻辑中独立出来 。但是现实开发里面经常会涉及到多人开发、跨语言合作、更快速的迭代等等的问题,最终需要把他们拆成独立的低耦合度的 Server。于是随之而来的是如何在服务间进行通讯/共享数据。
这里的方案选择通常会根据实际业务以及难易程度来权衡,例如最快捷的 webServer 的方式内部通信,稍微复杂点的基于 TCP 的 RPC 通信方案(例如 thrift),或者某些特殊的情景,例如是生产者/消费者关系的话,则可能通过 MQ 来进行通信。 最终我们采用的是通过Nginx 的 lua 模块来将 server 以面向侧面的思路耦合 。
首先,Nginx 的 Lua 模块可以做什么?如果可以,单纯 nginx 和 lua 就可以完成完整的 web 服务。可以连接 redis、memcache、postgresql 等等,同时可以取得请求的所有内容,可以设置返回的头部、正文。配合 lua 的对数据处理能力,基本功能都可以实现。同时 nginx 的 lua 模块整体都是异步,所以性能也相对较好。当然也可以通过 lua 脚本来控制权限,如果验证通过则继续下面的操作,例如是 proxy_pass 代理,简单的示例如下文:
location = /foo { access_by_lua_block { -- check the client IP address is in our black list if ngx.var.remote_addr == "132.5.72.3" then ngx.exit(ngx.HTTP_FORBIDDEN) end -- check if the URI contains bad words if ngx.var.uri and string.match(ngx.var.request_body, "evil") then return ngx.redirect("/terms_of_use.html") end -- tests passed } proxy_pass http://blah.blah.com; }
不过,我们这里将用到的主要还是 proxy_pass
, 和 ngx.location.capture
,基本代码如下:
-- 禁止任何以下划线开始的请求地址 if string.sub(ngx.var.uri, 2, 2) == "_" then ngx.exit(404) end local cjson = require "cjson" local custom_header_prefix = "V-" local request_args = ngx.req.get_uri_args(64) local request_body = ngx.req.read_body() local request_path = ngx.var.uri local request_method = ngx["HTTP_"..ngx.req.get_method()] for header, _ in pairs(ngx.req.get_headers()) do if string.upper(string.sub(header, 1, 2)) == crm_header_prefix then ngx.req.clear_header(header); end end function res_with_json(body, status) ngx.header["Content-Type"] = "application/json" ngx.print(body) return ngx.exit(status) end function request_to_server(uri) -- 发起请求至其他地址并取得结果 res = ngx.location.capture(uri..request_path, { body = request_body, args = request_args, method = request_method, }) local json_response = cjson.decode(res.body) -- 解析返回内容 if not json_response.next == true then res_with_json(res.body, res.status) end for key, value in json_response.params do ngx.req.set_header(custom_header_prefix..string.upper(key), value) end return false end
上面的代码主完成了清理用户恶意提交的请求头,以及 request_to_server
的代码,实现了将原请求内容转发给另一个接口并获得请求后的内容。得到请求结果后,验证请求的参数。
同时在 nginx 里面通过 stream 和 proxy_pass 的方式来配置多个内部地址:
upstream authentication-server { server 192.168.21.1:6011; server 192.168.21.2:6011; } server { location /_authentication { proxy_redirect off; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; rewrite ^/_authentication/(.*) /$1 break; proxy_pass http://authentication-server; } }
于是,将两个结合起来,就可以实现 通过 lua 脚本把原请求的所有参数,包括头部、正文、请求地址、请求方法都带过去,请求另一个地址(和 proxy_pass 类似),并且可以得到最终返回的结果处理 。
拥有这个能力后,便是本文的重点了:在 Sinatra
的第一版本中,最终都是 ruby 代码不断调用方法,来完成整个请求的流程。那如果我们把整个流程的打通交给 Nginx 的话该如何实现呢?
request_to_server
的能力,把请求依次转发给负责 横切关注点 的服务,例如用户请求校验以及登录校验服务、用户的组织架构服务,最终再去调用主关注点,即本文中的资源服务器; next: true
这个关键字,则将返回值中的其他内容以请求头的形式传递给下一个服务,且每个服务都会完全信赖这些请求头(所以请求刚进来的时候需要做一些请求头和请求地址处理)。 如果到这里都没有太大问题,你应该可以理解我的意图了。即 Nginx 通过 Lua 脚本来依次请求 横切关注点服务器,如果一路顺畅(每次都有 next: true
),最终会把携带有 横切关注点服务返回的内容的 headers 带给主关注点服务。
于是,在本需求里面,为了保证可拓展性和低耦合性,最终分为了三个服务:
负责具体的 REST 请求处理的资源服务
next: true
字段,所以直接返回结果; next: true
字段的返回值,于是 Nginx 直接返回结果; proxy_pass
给了资源服务。 最终,这样做的优势: