系统接口设计
最近的工作redmine的二次开发,需要频繁的与别的系统打交道,在写接口这方面的代码 有了点心得写下来 分享一下。
首先 redmine作为一个前端展示给客户的系统,需要和 翻译系统,代码管理系统,SDK管理系统交互。
这三个系统 通过普通的Net::HTTP请求,返回内容。
返回内容格式不尽相同,
1.翻译系统直接返回 xml字符串,
2.sdk系统返回 {“code”: “200″, “data”: “xxxx”} 或 {“code”: “400″, “data”: ” xxxx”, “message”: “xx wrong”}
3.代码管理系统返回 {“status”: 0, “val”: “xxxxx”, } 或 {“status” : -1, “err”: {“message”: “xxxx”, “event”: “xxx”}}
我希望无论请求哪个接口都能在日志种记录下来,发生错误时能有合适的客户提示,代码不要重复同时也要便于修改。
定义加强版Net::HTTP
require "net/http"
require "tempfile"
require "base64"
require "digest"
class EnhancedNetHttp
class Error < StandardError; end
@@default_logger = Logger.new(Rails.root.join("log/net_http.log"))
def self.default_logger
@@default_logger
end
USER_AGENT = "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/523.10.6 (KHTML, like Gecko) Version/3.0.4 Safari/523.10.6"
BOUNDARY = "---xxxxxxxxxxxxxxxxxx"
CONTENT_TYPE = "multipart/form-data; boundary=#{BOUNDARY};"
attr_accessor :error_klass
def initialize(*args)
options = args.extract_options!
self.logger = options[:logger]
self.error_klass = options[:error_klass]
end
def logger
@logger || EnhancedNetHttp.default_logger
end
def logger=(new_logger)
@logger = new_logger
end
def error_klass
@error_klass || EnhancedNetHttp::Error
end
def error_klass=(new_error_klass)
@error_klass = new_error_klass
end
def post_multipart_form(url, params = {})
uri = URI.parse(url)
http = Net::HTTP.new(uri.host, uri.port)
http.request(multipart_request(uri, params))
end
def post_form(url, params = {})
uri = URI.parse(url)
response = Net::HTTP.post_form(uri, params)
if response.is_a?(Net::HTTPSuccess)
logger.info format_message(uri, "POST", response, params)
else
raise error_klass.new(format_message(uri, "POST", response, params))
end
response
end
def get_response(url)
uri = URI.parse(url)
response = Net::HTTP.get_response(URI.parse(url))
if response.is_a?(Net::HTTPSuccess)
logger.info format_message(uri, "GET", response)
else
raise error_klass.new(format_message(uri, "GET", response))
end
response
end
def oauth_post(url, params = {})
uri = URI.parse(url)
auth_header = oauth_header(url, params)
data = ActiveSupport::JSON.encode(params)
response = Net::HTTP.start(uri.host, uri.port) {|http|
http.request_post(uri.path, data, auth_header)
}
if response.is_a?(Net::HTTPSuccess)
logger.info format_message(uri, "OAUTH POST", response, params)
else
raise error_klass.new(format_message(uri, "OAUTH POST", response, params))
end
response
end
private
def oauth_header(url, params= {})
oauth = {
"oauth_consumer_key" => "xxxxxxxxxxxx",
"oauth_signature_method" => "HMAC-SHA1",
"oauth_timestamp" => Time.now.to_i,
"oauth_nonce" => Time.now.to_i,
"oauth_version" => "1.0"
}
data = ActiveSupport::JSON.encode(params)
param_string = oauth.keys.sort.collect{ |item| "#{item}=#{oauth[item]}"}.join('&')
base = 'POST&' << CGI.escape(url) << '&' << CGI.escape(param_string)
base << "&#{CGI.escape(data)}" unless params.blank?
signature = Base64.encode64(OpenSSL::HMAC.digest(OpenSSL::Digest::Digest.new('sha1'), "xxxxxxxxxxxxxxxxxxxxxx", base))
oauth["oauth_signature"] = signature.strip
header_info = oauth.keys.sort.collect{ |item| "#{item}=\"#{oauth[item]}\""}.join(',')
{"Authorization" => "OAuth:" + header_info}
end
def multipart_request(uri, params = {})
request = Net::HTTP::Post.new(uri.request_uri)
post_body = []
params.stringify_keys.each do |key, value|
post_body << "--#{BOUNDARY}\r\n"
post_body << to_multipart(key, value)
end
post_body << "--#{BOUNDARY}--\r\n" unless post_body.empty?
request.body = post_body.join
request["Content-Type"] = CONTENT_TYPE
request["User-Agent"] = USER_AGENT
request
end
def to_multipart(key, value)
if value.class == Tempfile
"Content-Disposition: form-data; name=\"#{CGI::escape(key)}\"; filename=\"#{value.original_filename}\"\r\n" <<
"Content-Type: \"application/octet-stream\"\r\n\r\n#{value.read}\r\n"
elsif value.respond_to?(:path) and value.respond_to?(:read)
"Content-Disposition: form-data; name=\"#{CGI::escape(key)}\"; filename=\"#{value.path}\"\r\n" <<
"Content-Type: \"application/octet-stream\"\r\n\r\n#{value.read}\r\n"
else
"Content-Disposition: form-data; name=\"#{CGI::escape(key)}\"\r\n\r\n#{value.to_s}\r\n"
end
end
def format_message(uri, method, response, params = {})
"#{Time.now} [#{response.class.name}] #{method} #{uri} \n #{params.inspect if params.present?}"
end
end
这里的EnhancedNetHttp就是相当于一个NetHttp请求的代理,这个是读Gary的blog想到的。
以前在javascript中 经常写回调函数,用于在多个方法中交互,在这里我就想 为何不传入错误类型 来细化请求错误的原因呢?
于是在EnhancedNetHttp里就有了 logger 与 error_klass
post_multipart_form 和 oauth_post 则是在Net::HTTP的基础上增加的2个方法。
调用接口时则使用EnhancedNetHttp类来负责发送请求。
接下来就非常简单了 对三个不同系统的接口写三个Service,配置不通的错误类型和日志。这样方便反映错误。
实际上调用接口出错 我认为发让系统邮件给开发人员会比较好,第一时间发现错误总比过了很久去日志里查要好。
class Sdk::Service
class Error < StandardError; end
@@logger = Logger.new(Sdk.config[:log_path])
cattr_reader :logger
def self.net_http
@net_http ||= EnhancedNetHttp.new(:logger => logger, :error_klass => Sdk::Service::Error)
end
attr_accessor :host, :platform
def initialize(*args)
options = args.extract_options!
options.assert_valid_keys(:host, :platform)
self.host = options[:host] =~ /\/$/ ? options[:host].chop : options[:host]
self.platform = options[:platform]
end
def admin_user_query(user_id)
url = "/xxxxxxx/xxxxx"
params = {:abc => user_id, :efg => 456}
get_json(url, params)
end
def admin_user_xml(version_id)
url = "/bbbbb/cccccl"
params = {:version => version_id}
get_xml(url, params)
end
def admin_owned_item_xml(version_id)
url = "/ttttttttt/uuuuu"
params = {:version => version_id}
get_xml(url, params)
end
private
def get_response(full_url, params)
url = "#{full_url}?#{params.to_query}"
Sdk::Service.net_http.get_response(url)
end
def oauth_post(url, params = {})
Sdk::Service.net_http.oauth_post(url, params)
end
def get_xml(url_path, params = {})
response = oauth_post("#{host}#{url_path}", params)
Zlib::GzipReader.new(StringIO.new(response.body)).read
end
def get_json(url_path, params = {})
response = oauth_post("#{host}#{url_path}", params)
json = ActiveSupport::JSON.decode response.body
if json["code"] == 200
HashWithIndifferentAccess.new(json["data"])
else
logger.error("Response [#{json['code']}]: #{url_path} - #{json.inspect}")
raise Sdk::Service::Error.new(json["message"])
end
end
end
在重构代码的时候还用到一个有趣又简单的代理
class Translate::Proxy
attr_accessor :service
def initialize(service)
self.service = service
end
def cache_exception(&block)
yield
rescue Exception => e
raise Translate::Service::Error.new(e.message)
end
def method_missing(method_sym, *arguments, &block)
if self.service.respond_to? method_sym
cache_exception { self.service.send(method_sym, *arguments, &block) }
else
super
end
end
end
这个Proxy负责统一抛出的错误类型,如果是Net::HTTP请求发生错误 在这里则也会进行一次转换类型。
总结:优秀的程序员,总是能把代码组织的非常漂亮。多学习 多思考 多运用 会让生活变的更美好。