Home > rails, ruby > 系统接口设计

系统接口设计

最近的工作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请求发生错误 在这里则也会进行一次转换类型。

总结:优秀的程序员,总是能把代码组织的非常漂亮。多学习 多思考 多运用 会让生活变的更美好。

Categories: rails, ruby Tags:
  1. No comments yet.
  1. No trackbacks yet.