@月黑风高食肉虎 噗噗虎的技术博客

Groovy学习笔记之Jetty+Docker实现快速原型


实现一个快速原型是一个苦逼的全栈程序员经常碰到的事情,对于使用快速原型进行迭代开发,最可怕的情况就是80%的时间花在了原型的框架选型开发环境部署上,而20%的时间才用在解决和实现业务上。这是有多低的效率,可能是仅次于去机关办证的低效率了吧!而浪费的时间远远大于坐马桶上刷手机微博所消耗的时间总和。

今天,我们要在使用Jetty + Docker快速实现和部署一个能显示随机正态分布的页面,非常简单,最终效果如下:

最终效果图

图中的钟型曲线是使用Java的Random.nextGaussian产生的。用户可以在页面上输入限制最大值、最小值、数学期望值、标准差和样本总量并由这些参数生成正态分布图。

Jetty服务器

要实现这个快速原型,首先假设你已经在自己的机器上安装了Java,Groovy和Docker。

上述各软件安装好了之后,我们需要写一个Groovy脚本来跑Jetty,首先新建一个文件夹,把这个文件夹作为这个原型的根目录。

然后在原型根目录中新建app.groovy作为程序入口。这个脚本主要的任务就是为我们启动Jetty服务器,内容如下:

@Grab('org.eclipse.jetty.aggregate:jetty-server')
@Grab('org.eclipse.jetty.aggregate:jetty-servlet')
@Grab('javax.servlet:javax.servlet-api')

import groovy.servlet.GroovyServlet
import org.eclipse.jetty.server.Server
import org.eclipse.jetty.servlet.DefaultServlet
import org.eclipse.jetty.servlet.ServletContextHandler

def server = new Server(8080)
def context = new ServletContextHandler(server, '/', ServletContextHandler.SESSIONS)

context.with {
	resourceBase = 'webroot'               // 使用webroot文件夹作为根目录
	addServlet(DefaultServlet, '/')        // 挂入DefaultServlet为*.groovy脚本以外的文件提供访问
	addServlet(GroovyServlet, '*.groovy')  // Groovy脚本用的Servlet
	welcomeFiles = ['index.groovy']        // welcome文件设置为index.groovy,此处Optional
}

server.start()

简单说明一下这个脚本的作用。

首先我们用@Grab抓取需要的jar包,完了之后导入一些Servlet和Server,然后把这些Servlet挂到端口为8080的Jetty Server上,最后启动Jetty。

服务器有了,接下来我们需要写正态分布的页面了。

正态分布

在原型根目录下新建webroot,在其中新建一个脚本,名字随意取,在这里我们取名叫test1.groovy。我不准备在这里贴这个脚本的全部代码了,你可以在这个项目里面看到源代码。我准备首先说一下算法的基本思路,然后贴一下怎么使用markupBuilder生产html页面配合chart.js生成钟图。

随机正态分布数据的收集

Java的java.util.Random中自带一个能生成数学期望(u)是0、标准差(a)是1的近似随机标准正态分布的随机方法,叫做nextGaussian,我们用它来生成随机数。

由标准正态分布的特性可知,99%以上的数值落在(u-2.58a, u+2.58a)区间中,由于标准正态分布u = 0, a = 1所以我们可以假定nextGaussian方法生成的双精度值范围在-2.58 ~ +2.58之间,我们要做的只是对这个结果值做一下线性平移,然后收集结果即可。

譬如,我们可以把nextGaussian产生的结果a(99%以上落于(-2.58 ~ +2.58)区间)乘以100,然后截取整数位,放入一个map做key,value设置成1。下次再随机到这个值时,map中的value + 1。然后用样本总量值作为循环次数收集产生的随机值。

收集完成后,将map.key进行升序排序,然后map.key的第一个值便是随机生成的最小值min,map.key的最后一个值是最大值max。

之后用Groovy的Range生成从min到max的范围,针对范围中每一个值i,map[i]便可以取得该值出现过的次数。我们可以用'='*map[i]打印到Console中,就能很形象地看到一个正态分布图像了。

废话不多说,看代码吧

def random = new Random()
def map = [:] // 收集结果的map
10000.times { // 样本总量10000,循环10000次
  int gaussian = (int) (random.nextGaussian() * 100) // 放大100倍,大部分随机数应该落在(-258 ~ +258)范围内,小数点截断。
  map[gaussian] = map[gaussian] ? map[gaussian] + 1 : 1 // 计数
}

def sorted = map.keySet().sort()  // 排序
(sorted[0]..sorted[-1]).each {
  println it.toString().padLeft(3, ' ') + '=' * (map[it]?:0) // 打印结果
}

运行一下,你应该能看到console上打印出下面这坨东西(太长,截取部分)

...
-20======================================
-19============================================
-18===================================
-17====================================
-16=================================================
-15==========================================
-14======================================
-13=======================================
-12===============================================
-11=============================================
-10===================================
 -9========================================
 -8=========================================================
 -7===============================================
 -6====================================
 -5===============================
 -4=============================================
 -3================================================
 -2====================================
 -1=====================================================
  0========================================================================
  1=======================================
  2========================================
  3==============================================
  4==============================================
  5===========================================
  6========================================
  7=======================================
  8=============================================
  9============================
 10========================================
 11==============================
 12===================================
 13============================
 14==========================================
 15=================================
 16========================================
 17=========================================
 18========================================
 19==============================
 20=================================
 ...

如果你把它打印成txt文件(用groovy test.groovy > out.txt),缩小后能看到

console中的高斯分布

看到没,近似正态分布了吧。就是console中比较丑,没事儿,接下来我们把上面的概念想办法搞到html页面上去。

test1.groovy脚本中代码虽然不是完全按照上面的来,但是思路是一样的。具体代码可以看这里

GroovyServlet

好了,正态分布的数据有了,接下来要可视化。我们准备使用chart.js的曲线图来显示。chart.js的曲线图实现起来比较简单,只要提供data和options(可忽略),把canvas包到Chart类中去即可,具体可以参考这里的中文文档

好了,有了chart.js的加持,我们只要生成html和json的数据即可了。首先我们来看看怎么生成html。

你可以把test1.groovy看做一个servlet,如果你查看groovy.servlet.ServletBinding,可以看到在GroovyServlet中,已经默认绑定了以下几个变量供我们调遣

Eager variables
  • “request” : the HttpServletRequest object
  • “response” : the HttpServletRequest object
  • “context” : the ServletContext object
  • “application” : same as context
  • “session” : shorthand for request.getSession(false) - can be null!
  • “params” : map of all form parameters - can be empty
  • “headers” : map of all request header fields
Lazy variables
  • “out” : response.getWriter()
  • “sout” : response.getOutputStream()
  • “html” : new MarkupBuilder(response.getWriter()) - expandEmptyElements flag is set to true
  • “json” : new JsonBuilder()
Methods
  • “forward(String path)” : request.getRequestDispatcher(path).forward(request, response)
  • “include(String path)” : request.getRequestDispatcher(path).include(request, response)
  • “redirect(String location)” : response.sendRedirect(location)

有没有jsp中的隐含变量的感觉?OK,里面有个叫html的MarkupBuilder,好了我们用它来生成html。咱先把chart.js的javascript和form表单部分省略了吧。

html.html {
    head {
        title 'Gaussian Distribution Test'
        script src:'//cdn.bootcss.com/Chart.js/1.0.2/Chart.min.js'
    }
    body {
      h1 '(伪)正态分布研究'
      canvas id:'myChart', width: 800, height: 500
      script {
        // TODO 在这里生成chart.js的javascript代码
      }
    }
}

会生成以下html页面源码

<html>
  <head>
    <title>Gaussian Distribution Test</title>
    <script src='//cdn.bootcss.com/Chart.js/1.0.2/Chart.min.js'></script>
  </head>
  <body>
    <h1>(伪)正态分布研究</h1>
    <canvas id='myChart' width='800' height='500'></canvas>
    <script>
      // 等下chart.js的javascript代码会在这里生成。这段注释不会由markupBuilder生成。
    </script>
  </body>
</html>

好了,html的框架有了,接下来我们要把之前算出来的随机数结果改造成chart.js能用的数据格式然后显示,我们来看看怎么做:

def map = [:]

// ...省略正态分布的随机算法...
// 假设你到这里已经把所有结果都放在了map里

// 对结果key进行排序
def keys = map.keySet().sort()
// 生成最小到最大值的list提供给chart.js的labels用
def dataLabels = (keys[0]..keys[-1]).collect { it.toString() }
// 生成labels对应的数据list
def dataList = (keys[0]..keys[-1]).collect { map[it]?:0 }
html.html {
  // ...省略head部分..
  body {
    h1 '(伪)正态分布研究'
    canvas id:'myChart', width: 800, height: 500
    script {
      mkp.yield 'var data =' // mkp.yield会吧参数直接打印到out中去
      json { // 此处开始使用JsonBuilder生成json对象
        labels dataLabels // 之前生成的dataLabels对象
        datasets ([[
              fillColor : "rgba(151,187,205,0.5)",
              strokeColor : "rgba(151,187,205,1)",
              pointColor : "rgba(151,187,205,1)",
              pointStrokeColor : "#fff",
              data : dataList // 之前生成的dataList对象
        ]])
      }
      // mkp.yieldUnescaped和yield一样,只是不escape了(这不是废话嘛)
      mkp.yieldUnescaped '''
        var ctx = document.getElementById("myChart").getContext("2d");
        new Chart(ctx).Line(data);
      '''
    }
  }
}

完成!浏览器访问http://localhost:8080/test1.groovy,塔拉~~~

最终效果图

哦,form提交什么的很简单的就不再赘述啦请戳这里看源代码

部署

部署Groovy其实是个很简单的事情,只要你有Java环境然后安装上Groovy,然后用groovy运行即可。

什么?你说这没有一点部署服务器的庄重感?感觉太简单不能完成你的需求?怕运维人员失业?好吧,那我们就用Docker部署吧,因为很简单,写个Dockerfile就行了。

# 使用openjdk-8
FROM java:openjdk-8-jdk

# 安装wget和unzip
RUN apt-get update && \
    apt-get -y install wget unzip && \
    apt-get clean

# 设定环境变量
ENV GROOVY_VERSION=2.4.5
ENV JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64 \
    GROOVY_HOME=/opt/groovy-${GROOVY_VERSION}

ENV PATH=$GROOVY_HOME/bin/:$JAVA_HOME/bin:$PATH

# Install groovy
ADD http://dl.bintray.com/groovy/maven/apache-groovy-binary-${GROOVY_VERSION}.zip /tmp/

RUN unzip -d /opt/ /tmp/apache-groovy-binary-${GROOVY_VERSION}.zip \
  && rm /tmp/apache-groovy-binary-${GROOVY_VERSION}.zip

# 复制代码
ADD ./src/ /groovyApp

EXPOSE 8080

WORKDIR /groovyApp

# 运行groovy
ENTRYPOINT ["groovy", "app.groovy"]

Docker源文件可以在这里看到。

OK,然后在docker环境中运行:(如果你是docker-machine的话,就是mac或者windows的docker,就是在Docker Quickstart Terminal中,对就是头上有个金鱼鲸鱼的那个终端)

docker build -t GaussianRandDemo .

等待Docker下载并构建成功后运行

docker run -d -p 8080:8080 GaussianRandDemo

浏览器访问http://localhost:8080, 如果是docker-machine环境请访问http://192.168.99.100:8080

塔拉~~~完成。

哦对了,首次运行Groovy需要下载依赖包, 可能需要点时间, 请耐心等候.

好了,现在,你有了一个app,有可以通过命令行直接运行的源代码(命令行运行groovy app.groovy),还有了可以通过registry push到生产环境的docker image,怎么?还不满意?没事儿,你还可以用maven、gradle这种构建工具来编译打包groovy项目,当然,还可以用spring CLI直接运行groovy脚本,或者一键打包成可执行jar或可部署war,可选项太多了。

对了,还有数据库相关的都还没说呢。

且听下回分解。