sinatra分析(旧)

这是10年初写的了:)

sinatra简介

  • Sinatra is a DSL for quickly creating web applications in Ruby with minimal。
  • Fewer classes, less inheritance
  • controller object mapping & routes vs. URLs—Dont’s fear the URLs
  • Exposed Simplicity instead of hidden complexity
  • Small things, loosely joined, written fast

sinatra分析

Rack机制

sinatra 作为一个web框架,是基于rack规范的。rack规范和Java的servlet规范有点类似,Rack中间件和filter机制有些类似, 都是能够拦截request/response做一些事情。所谓的rack兼容的中间件无非是一个可以执行 call(env) 的对象,详细关于rack的内容可以参考rack官网,还有这个rack入门文档也很好。 在源码中可以看到,sinatra的Request和Response都是基于rack扩展的,并对Rack::Request和Rack::Response分别做了一些调整。 sinatra是通过Application.run!来启动服务器的

         def run!(options={})
            set options
            handler      = detect_rack_handler
            handler_name = handler.name.gsub(/.*::/, '')
            puts "== Sinatra/#{Sinatra::VERSION} has taken the stage " +
              "on #{port} for #{environment} with backup from #{handler_name}" unless handler_name =~/cgi/i
            handler.run self, :Host => bind, :Port => port do |server|
              trap(:INT) do
                ## Use thins' hard #stop! if available, otherwise just #stop
                server.respond_to?(:stop!) ? server.stop! : server.stop
                puts "\n== Sinatra has ended his set (crowd applauds)" unless handler_name =~/cgi/i
              end
              set :running, true
            end
          rescue Errno::EADDRINUSE => e
            puts "== Someone is already performing on port #{port}!"
          end

其中detect_rack_handler是通过 Rack::Handler.get来检测rack处理器的,默认的server有thin/mongrel/webrick,绑定的地址是 0.0.0.0,端口是4567

    module Sinatra
        class Base
            set :server, %w[thin mongrel webrick]
            set :bind, '0.0.0.0'
            set :port, 4567
        end
    end
注意到handler.run self, :Host => bind, :Port => port do server ,这个self指的是Sinatra::Base,根据rack规范,最终的请求的入口就是 Sinatra::Base.call(env)方法
          def prototype
            @prototype ||= new
          end

          # Create a new instance of the class fronted by its middleware
          # pipeline. The object is guaranteed to respond to #call but may not be
          # an instance of the class new was called on.
          def new(*args, &bk)
            builder = Rack::Builder.new
            builder.use Rack::Session::Cookie if sessions?
            builder.use Rack::CommonLogger    if logging?
            builder.use Rack::MethodOverride  if method_override?
            builder.use ShowExceptions        if show_exceptions?
            middleware.each { |c,a,b| builder.use(c, *a, &b) }

            builder.run super
            builder.to_app
          end

          def call(env)
            synchronize { prototype.call(env) }
          end

从call方法可以看到,是通过生成一个Sinatra::Base实例对象来运行的,最终会调用的是call(env) -> call!(env), 接下去的工作就是等客户端发送请求过来就可以了。在生成这个实例对象@prototype的时候,直接引入rack中间件机制, 同样,sinatra允许你使用use方法来增加新的中间件(use只是把中间件加入@middleware变量中去而已)。这样sinatra就已经启动起来了。

路由机制

sinatra 的路由机制和rails不大一样,sinatra是在controller里边用get/post path这样来指定的。 而rails是把controller和map分开处理,通过map来找到对应的controller和action。 rails当初这么搞主要是为了兼容controller和路由不匹配的情况,个人觉得sinatra的写法是非常直观的,也非常的灵活。

    delegate :get, :put, :post, :delete, :head, :template, :layout,
                 :before, :after, :error, :not_found, :configure, :set, :mime_type,
                 :enable, :disable, :use, :development?, :test?, :production?,
                 :helpers, :settings

看 main.rb可以看到include Sinatra::Delegator,可以把get/post等众多方法代理给Sinatra::Application去执行, 在后面使用get ‘/’ do xxx end的时候其实会调用Sinatra::Application(即Sinatra::Base)的get方法。

      require 'rubygems'
      require 'sinatra'
      get '/' do
        'Hello world!'
      end

例如这样一个简单的web应用就可以响应’/’的请求路径,那么Sinatra::Base是怎么识别到这个路由的呢?我们继续来看看上面的get方法做了什么事情, 可以看到最终是调用route方法的(同时,从代码可以看到sinatra支持get/post/put/post/delete/head几种method的请求)。 按照我们的大概思路,在看到某个请求方法的时候,sinatra会把{请求类型_路径 => 代码块}放到一个专门放路由的地方上去,然后在每一次请求调用call(env)的时候, 根据“请求类型_路径”来获得需要执行的代码块。好,继续看看 route的代码是怎么实现的?

          def route(verb, path, options={}, &block)
            # Because of self.options.host
            host_name(options.delete(:bind)) if options.key?(:host)
            options.each {|option, args| send(option, *args)}

            pattern, keys = compile(path)
            conditions, @conditions = @conditions, []

           define_method "#{verb} #{path}", &block
            unbound_method = instance_method("#{verb} #{path}")
            block =
              if block.arity != 0
                proc { unbound_method.bind(self).call(*@block_params) }
              else
                proc { unbound_method.bind(self).call }
              end

            invoke_hook(:route_added, verb, path, block)

            (@routes[verb] ||= []).
              push([pattern, keys, conditions, block]).last
          end

这个代码处理的事情比较多,我们来仔细分析分析,前面两句代码是用来记录能够处理的请求的约束(例如特定的host_name,user_agent), 然后compile(path)的工作是把path换成一个正则表达式(这样通过match就可以获得匹配的组),还有提取keys(例如*的就变成 splat,:name就变成name)。 重要的是把get ‘/’ do xxx end动态生成一个”#{verb} #{path}”的方法并最终封装成一个带有上下文状态的proc对象, 最终是把[pattern, keys, conditions, block]加入@routes[verb]里边去。而call(env)能够处理请求就得靠这个@routes来实现。

先来看看call(env) -> call!(env),最重要的部分是invoke { dispatch! },可以看到dispatch!的整个流程是 判断并处理static文件 -> before_filter! -> route! -> after_filter!,主要的处理过程是route!方法

       def route!(base=self.class, pass_block=nil)
          if routes = base.routes[@request.request_method]
            original_params = @params
            path            = unescape(@request.path_info)

            routes.each do |pattern, keys, conditions, block|
              if match = pattern.match(path)
                values = match.captures.to_a
                params =
                  if keys.any?
                    keys.zip(values).inject({}) do |hash,(k,v)|
                      if k == 'splat'
                        (hash[k] ||= []) < values}
                  else
                    {}
                  end
                @params = original_params.merge(params)
                @block_params = values

                pass_block = catch(:pass) do
                  conditions.each { |cond|
                    throw :pass if instance_eval(&cond) == false }
                  route_eval(&block)
                end
              end
            end

            @params = original_params
          end

首先sinatra先从@routes里边取得符合请求类型的[pattern, keys, conditions, block]列表,然后逐个扫描, 通过pattern来match路径,如果符合的话,取得通配符,命名参数的值并封装到params去(得益于 compile(path)的工作)。 接下去判断conditions是否符合,如果都符合,则执行业务,即block。整个流程处理完之后,把 params恢复为原本的状态。

拦截器

在上面已经提到,sinatra的拦截器是通过before_filter!和after_filter!来执行的,如下所示:

        def before_filter!(base=self.class)
          before_filter!(base.superclass) if base.superclass.respond_to?(:before_filters)
          base.before_filters.each { |block| instance_eval(&block) }
        end

配置过滤器也非常简单,定义一个前置过滤器,例如

      before do
        @note = 'Hi!'
        request.path_info = '/foo/bar/baz'
      end

sinatra通过Sinatra::Base的before把block加入到@before_filters中去,这个应该很容易明白的。 不过,这个拦截器功能比起rails那个显得简陋了,毕竟不能直接针对某些路径进行拦截处理。

模板渲染

sinatra通过Tilt实现多模板的渲染机制,生成页面的过程是在业务代码块那里注明的,例如

      require 'erb'
      get '/' do
        erb :index
      end

sinatra的模板方法是在Sinatra::Templates模块里边定义的,能够支持erb,erubis,haml,sass,less,builder,具体的实现如下:

        def render(engine, data, options={}, locals={}, &block)
          # merge app-level options
          options = settings.send(engine).merge(options) if settings.respond_to?(engine)

          # extract generic options
          locals = options.delete(:locals) || locals || {}
          views = options.delete(:views) || settings.views || "./views"
          layout = options.delete(:layout)
          layout = :layout if layout.nil? || layout == true

          # compile and render template
          template = compile_template(engine, data, options, views)
          output = template.render(self, locals, &block)

          # render layout
          if layout
            begin
              options = options.merge(:views => views, :layout => false)
              output = render(engine, layout, options, locals) { output }
            rescue Errno::ENOENT
            end
          end

          output
        end

具体的流程是先找到template engine,通过template的render方法渲染子页面,然后在把子页面的内容作为一个block参数放到渲染layout的render方法上去, 这样在父页面里边的yield就会被子页面的内容所取代,从而实现整体页面的渲染。

错误及状态处理

sinatra在这方面的处理,我觉得非常巧妙,还认识了一些从来没用过的api。几个重要的特性:

halt:

      halt 410
      halt 'this will be the body'
      halt 401, 'go away!'
error:

      error do
        'Sorry there was a nasty error - ' + env['sinatra.error'].name
      end
      error MyCustomError do
        'So what happened was...' + request.env['sinatra.error'].message
      end
      error 400..510 do
        'Boom'
      end

error的实现很简单,只是把error code和block记录到@errors上去,而not_found其实就是404的error了。halt从代码实现上看,它是throw一个halt的异常。 这些处理方式在sinatra最终是怎么处理的呢?我们先回到dispatch!这个主方法,从源码中可以看到如果是静态页面,会抛出halt(line 173),到了route!方法的时候,如下

                pass_block = catch(:pass) do
                  conditions.each { |cond|
                    throw :pass if instance_eval(&cond) == false }
                    route_eval(&block)
                end

catch(args,&block) 这个方法是会忽视在遇到pass异常的时候忽略异常并跳出block的运行,所以conditions验证不通过的时候, 就会转入下一个pattern验证,而在验证通过后到了route_eval(&block) 就会抛出halt从而跳出循环,表示已经匹配成功。 抛出异常之后会在dispatch!通过rescue来处理。error_block!(*keys) 就是用来处理error的,@errors根据error code来获取block,这样就可以输出自定义的错误页面了。

Linux经验随手记(旧)

这是10年的老文了。:)

其实谈不上什么经验,自己也是在使用Linux的过程中学习到不少东西。在使用Linux的过程中, 常常会冒出“自己好弱”的感觉,与人交流的过程中学到新东西,总会感觉很兴奋 ,而跟人讲解的时候,也总是希望可以得到一点反馈从而加深自己的理解~~(感觉)

工作会促使你不断去学习,而学习的效果会反应在你的工作上。 如果你对学习,工作的内容感兴趣的话,那肯定会让学习工作的效果更明显。应了那句话,兴趣是最好的老师~~(学习与工作)

学习linux,需要抛弃windows那套惯性思路。每个系统都有它的长处和短处,重要的是扬长避短,而不是取长补短。 相对windows来说,一开始会觉得Linux麻烦,使用不方便,很多工作要绕一个大弯才能解决问题。只有坚持学习,坚持使用linux,才能逐渐体会linux的好处。 就现在而言,linux主要的领域还是服务器方面,而windows的桌面一直都是非常棒的~~(linux与windows)

和linux打交道,基本是有两条路的,要么就是linux开发,要么是linux管理;linux开发嘛,几乎都是c的天下了。 咱们不是专门搞这个的,是其他语言+linux管理的搭配方式,主要用的也是shell和python,ruby,perl等等(我说的也是这个方向的)。 两者的区别还是比较大的,所以学习linux,最好还是好好考虑选择哪个方向把~~(关于linux学习的方向)

我第一次接触linux是在大学的时候,看到隔壁宿舍一大牛同学在玩linux,一个redhat8的发行版。 又上网去查查看,觉得蛮有趣的样子,借了光盘自己也装了双系统。不过好景不长呀,玩桌面的劲头很快就过去了,很快我就遗忘了linux, 重新回到windows。到了后来硬盘空间不够了(60g的盘),删除linux的时候,还把整个硬盘都给格式化了, 后来给网易的人取笑了一番。现在都觉得自己当时好糗呀~~(ps:我想很多人和我一样,linux的门找错了)

到后来要走上工作岗位了,才第一次听到李老大提到centos,fedora这些发行版(我真是孤陋寡闻呀,汗~~)。不过这个时候学习的方向总算不会偏离太多了, 而且这个时候空闲的时间也比较多(我是被遗忘的一族)。为什么说方向没有偏离太多,这是因为我根据网上一份优秀的教程来学习的,每天有空就在机器上捣鼓捣鼓。 虽然我不是很清楚学这些东西以后可不可以用上,但直觉告诉我,总不会没有价值的。这个时候我使用的是fedora8,教程就是鸟哥的私房菜了。 虽然说鸟哥的私房菜是很有用,但是熟能生巧,很多东西没用到,忘记也得特别快,体会也不会非常深刻。即使如此,我还是蛮有热情继续学下去,至少找到一点点门道了,在有一年春节回家前, 去书店买了本Linux技术管理手册(第二版),那个时候还没有中文版出来,咬咬牙在春节的时候把主要部分都读了一遍。 这个时候基本上是个人兴趣啦,毕竟工作上基本没什么机会使用到(ps:好的教材有必要,学习需要坚持)

后来也不知什么时候了,在工作中接触到越来越多的linux相关的东西,例如环境搭建,应用部署,安全与监控,分析与调优等。 这个时候也是个不断探索,加深印象的过程。这段时间看到自己学习的东西能够派得上用场,的确是很幸福的事情。 现在把工作环境都搬到centos,也没觉得有什么不习惯的。从我看来,熟悉linux的命令和目录结构是有效使用Linux的重要标志了。 呵,装个系统,然后开始linux之旅吧。下面的推荐书籍应该可以让你搭上linux 的“贼船”了。进阶的话,还是要不断通过充实理论和实践经验来获得的,linux博大精深,各自修炼,加强交流吧~~

推荐书籍:

  • Linux新手管理员指南
  • Linux系统管理员指南
  • Linux网络管理员指南
  • Linux操作系统之奥秘
  • Linux系统架构与目录解析(这本书我没看过,看邱老师的书另外一本感觉不错)
  • 高级shell 脚本编程指南
  • 鸟哥的Linux私房菜
  • Linux技术管理手册
  • Linux 目录结构简介(鄙人的,见笑见笑~~)

从某维护系统的架构改造谈起–分层的理解(旧)

这是刚到公司时写的~

公司的项目是一个维护有好几年的大规模电信项目,并且经过无数人的摧残,代码极其混乱。我们想改变这个层次不清代码混成一团的现状,引入业界广为使用的三层架构开发模式。如下图所示: 三层结构

最近小组一直在探讨如何从现有架构上逐渐迁移为分层清晰的状况,虽然我们主要的精力还是放在新增的功能上,但是在这个整个过程还是令我对分层有了更深的理解。这里主要说说对于通用分层的理解:

  1. 虽然大家都或多或少知道Action,Service,Dao干的是什么,不过实践起来的时候,有很多细节问题需要考虑(我们的系统有些比较蹩脚的调用接口,又不能抛弃)
  2. Action主要关注大的流程处理,其中可能会包括多个Service的处理(像我们需要与大量外围系统交互更是如此),一个Service方法代表一个具有完整事务边界的流程。
  3. Action和Service的边界主要由参数来决定,Service不需要知道调用方是一个GUI还是web请求,所以调用参数不应该出现request/response/session之类的对象, 应该限制为java基本类型,基本集合类型,简单的值对象(用于避免长参数调用),或者是具有特殊业务意义的变量(例如用户等信息)。
  4. Service和Dao的区分主要是业务相关性,Service不需要关心Dao究竟调用DB还是其他的数据源获得的(多数据源在大型系统中非常常见), 所以从调用参数和返回值上看,参数和返回值都避免具体的Dao实现有关。针对某些Dao调用返回值需要进一步处理,这部分的工作放在Dao还是Service取决于处理工作更靠近业务还是靠近具体Dao实现协议。
  5. 从关注对象上看,Action关注request、response、session等; Service关注java基本类型,java集合类型,值对象等;Dao关注Sql,ResultSet等底层数据结构。
  6. 从关注的异常处理看,Action关注特定的业务异常(BusinessException),并统一业务异常处理流程, Service只关心Dao是否发生异常(例如统一的Dao异常接口DataAccessException),记录并转化为相应的业务异常交个业务处理;Dao关注底层的异常,通常也无法恢复,只好转化成统一接口交给上层处理。

晚了,下一次就谈谈关于这个项目的异常处理的改造或者Dao的改造情况吧~~

从某维护系统的架构改造谈起–异常处理(旧)

这是刚到公司时写的~

Java 提供了两类主要的异常:runtime exception和checked exception。 所有的checked exception是从java.lang.Exception类衍生出来的, 而runtime exception则是从java.lang.RuntimeException或java.lang.Error类衍生出来的。

如果你希望强制你的类调用者来处理异常,那么就用Checked Exception; 如果你不希望强制你的类调用者来处理异常,就用UnChecked。 那么究竟强制还是不强制,权衡的依据在于从业务系统的逻辑规则来考虑, 如果业务规则定义了调用者应该处理,那么就必须Checked,如果业务规则没有定义,就应该用UnChecked。

至于类调用者catch到NoSuchUserException和PasswordNotMatchException怎么处理,也要根据他自己具体的业务逻辑了。 或者他有能力也应该处理,就自己处理掉了;或者他不关心这个异常,也不希望上面的类调用者关心,就转化为RuntimeException; 或者他希望上面的类调用者处理,而不是自己处理,就转化为本层的异常继续往上抛出来。 根据上面的一些观点在现有的系统上建立基于三层架构的异常处理模型,主要有以下做法:

  • Dao层次关注底层异常,当很多SQLException无法恢复,转换成统一的RuntimeException交给Service处理
  • Service层关心某些有业务含义的业务处理,并转化成相应的业务异常(同样也是RuntimeException)交给Action处理
  • Action层可能对某些特定的业务异常感兴趣,感兴趣就处理(或许尝试修复),不感兴趣就交给统一业务异常处理器处理
  • 统一业务异常处理器会记录exception log,并负责相应的错误展现页面

实现思路(待续):

  • exception util:提供一些异常处理的工具,例如转换异常
  • exception wrapper:作为统一的异常类,可以用来保存原始的exception信息
  • exception collector:可以保存多个exception信息的异常类,用于某些特殊场合
  • exception handler:用于处理exception的统一处理,主要是日志与错误页面

从某维护系统的架构改造谈起–公用代码库(旧)

这是刚到公司时写的~

有时候真是不看不知道,这样一个每月数亿业务量,数十亿营收的大型系统,原来也就是这样的质量。 我们可以想象这种系统一开始都是美好的,只是渐渐的变味了。就拿我们这边java的来说,原来也应该是有公用代码库的,只是那么久每人去维护,项目赶工, 自立山寨的情况多了,这些东西的反向价值就越来越明显了。典型的表现有:

  • 自立门户的现象很突出,经常可以找到n个方法是做同样事情的;
  • 神级的类很多,例如某个类可以提供字符串,时间,web,业务相关的操作;
  • 公用类代码质量良莠不齐,有很多明显写得不好的地方(曾经有个数千行的类就给我砍倒1/3代码);
  • 缺乏公用库的document和quickstart

这样造成浪费,花了很多冤枉时间去实现已有功能,缺乏统一规划。大家都有体会,维护一个杂乱无章的系统是多么的困难, 在这种系统想写出漂亮的代码是很考水平的。现在代码库又特别庞大,怪不得很多同事慢慢地就给系统同化了, 缺乏持续改进的动力。在上次retro会议的时候,针对缺乏统一的公用代码库的问题,针对质量和维护的问题,我提了一些做法:

公共代码库迁移

  1. 方向是在保留现有逻辑的基础上,针对新添加的功能和修改的功能,逐渐迁移到新的公用库。
  2. 至于公用库的选材,主要是从各大现有公用库进行提炼,对神级类进行拆分,并结合开发人员提需求的做法,形成我们的公用库。
  3. 为了避免山寨的出现,培训和文档是必须有的。
  4. 针对如何让程序员接受并找到新公用库的问题,建立新的source目录,并在文档上通过目录-包结构-类的层次做好文档(javadoc)。
  5. 除了必要的培训之外,主要的审查方式还是通过每天的code diff和定期的code review来做到大家对公用库的认同和理解。
  6. 维护是要靠大家去推动的,就像上面说的,由程序员反馈需求或提供补丁的方式,保证公用库的持续改进。

现在工作进展还算顺利,过段时间再看看效果怎样,或许咨询一下顾问看看有没有改进的地方。