springboot集成groovy脚本环境

背景

在游戏开发中,lua脚本被大量运用。其思路是,将脚本作为黏合组件的胶水,利用基础组件提供的功能,灵活地组合出各种不同的功能,就好像创造出一种新的DSL(domain specific language)一样。在后端开发中,显然也可以借鉴这里面的宝贵经验。在java生态下,使用groovy就显得自然而然。

why groovy

作为厚重java组件的胶水语言,他具有以下优势:

  • 完全兼容java语法
    到了groovy3,new String[]{"a"}(a,b) -> a+b等java语法也得到了支持,def m = [:] as Map>在groovy2中因为末尾的>导致编译器解析报错的bug也得到了解决——这门语言还是在蓬勃发展呀!
  • 集成方式简单
    使用GroovyShell类即可。网上有很多集成的方法,这里就略去不表了。
  • 老生常谈
    “java程序员更容易接受groovy,学习曲线平滑”。这在实际中真没发现=.=。即使《clean code》早在上个世纪就已经完成了,今天大多数coder还是会使用cv大法。人们显得功利和浮躁,往往不会去了解技术的本质。
    有本书提过的“救火”类程序员竟然是真实存在的。根据我多年的java经验,这个虚拟机很温柔,说java是现存最简单的语言也不为过,因为市面上大把的是培训三个月出来混的,其创造力十分惊人,写出下面的代码就不足为奇了。
Map map = new HashMap() {{
  put("a", 1);
  put("")
}};

第一眼看去,很多人注意到这代码没好好写泛型。然而,应该还有更多人不知道上面代码里所谓的“动态初始化块”、以及其创造出一个匿名内部类的事实(有个5年java工作经验的同事竟也不知道)。
题外话扯多了,干脆再说一句吧。写出好的代码和语言无关,详见redis源码,因为好的代码用所有语言写出来都是一个样的,不好的代码才是千奇百怪。尤其对jvm而言,当你写的代码足够好的时候,性能自然而然就好了。根本没所谓的调优一说。至少在我的实践里这是真的。

回归主题

springboot如何从groovy脚本环境中受益呢?比如你开发了个接口,为了稳健,一般即使没有绩效上的要求,你也会加个查询接口,暴露一些内部的东西,或者去跟踪程序的运行。这样做,每次会产生很多接口,让人头晕@@。
理论来讲,没用且重复的代码还是少点为好,不然太多的垃圾信息会掩埋真正需要关心内容。
我们可以用脚本来轻松达到目的。空说无益,用几个例子来展示下脚本的价值吧!假设我们已经初始化好GroovyShell:

def binding = new Binding()
...
def groovyShell = new GroovyShell(binding, this.class.classLoader)

我们可以这样给它准备一个redis template。

@Service
class ShellRedisOptions {
  @Autowired
  StringRedisTemplate stringRedisTemplate

  // 封装标准的redis方法
  def get(str) {
    return stringRedisTemplate.opsForValue().get(str)
  }

  def hget(key, subKey) {
    return stringRedisTemplate.opsForHash().get(key, subKey)
  }
  // ... other stuff
}

然后,把这个基础设施暴露给shell环境:

@Autowired
ShellRedisOptions shellRedisOptions

binding.set('redis', shellRedisOptions)

更传统地,我们可以使用groovy sql,暴露数据库基础设施给shell:

@Service
class ShellDbOptions {
  @Autowired
  DataSource dataSource

  Sql sql
  
  @PostConstruct
  void init() {
    sql = new Sql(dataSource)
  }

  // 再封装groovy sql里最牛逼的两个方法firstRow, rows
}

// 暴露给shell
binding.set('sql', shellDbOptions)

接着,我们在统一暴露一个shell接口:

@Controller
class GroovyShellController {
  @Autowired
  GroovyShellService groovyShellService

  @GetMapping('/shell/exec')
  String exec(@RequestParam String script) {
    return groovyShellService.exec(script)
  }
}

最后,我们可以用python的tkinter,或者javafx做个简单的界面,这个界面类似于groovyconsole^^,你可以给它一个ctrl + R的快捷键,用于调用上面的接口。
我们可以在springboot环境中愉快地享用了:

def keys = redis.keys('some_key:*')
println keys
println redis.type(keys.first())

def rows = sql.rows('select name from persons')
rows.each { println it.name }

是不是很酷。当然,上面省略了一些细节。比如,拦截并重定向shell的输出流(不然println会打印到日志里去。),这个可以参考GroovyConsole源码里的OutputIntercepter。

几个应用

调试
除去上面的招式外,假如你把spring context的Object getBean(String beanName)Object getBean(Class clazz)适配到groovy的下标访问契约:getAt,并暴露给shell,存入到名为spring的变量里,你的武器库就更强大了!

// 使用你开发的服务
def s = spring['youServiceBean']
s.doWork()

用这招去测试环境调试也是可以的,当然,如果暴露太多的东西,把测试同事的数据环境给整挺好了,那就不妙了==。
压测
能用程序解决的事情,还是尽量用程序解决。比如我们需要压测一个接口,用jmeter是很nice的,但是过于笨重,还不够灵活。测试程序的并发性能,用脚本我们可以这么干:

def s = spring['yourServiceBean']

(1..10).collect {
  Thread.start {
    100.times {
      def start = System.currentTimeMillis()
      s.doWork()
      println """${Thread.currentThread().name} cost: \
         ${System.currentTimeMillis() - start}ms"""
    }
  }
}.each { it.join() }

随写随用,用完就丢。
ScriptHub
其实脚本大多还是有保留价值的。譬如,我用javafx开发了上述后端接口的前端界面,并用redis做持久化,企图在同事间共享脚本,为了方便,我还实现了所有主流的idea快捷键。无奈这个项目写到内网去了。所以这里是看不到了。
这个思路是极好的,开发写脚本,给做数据、测试的同事用,快速响应,挺敏捷的,毕竟在门外汉眼里,做工程的现在不吃香了,搞点这种东西装下13,挺直下腰杆子,还是可以的。
然而正如前面提到的jemeter,一般人还是更习惯GUI,眼睛看不到的东西让他们很慌,脚本什么的,就更是让他们崩溃了。咋推广还真是个问题,难道要把脚本封装到可拖拽的pipeline里去么?_?。不得不说这是个方向。

写在最后

最好使用@Conditional注解,不要让上面的东西泄露到生产环境中去,这是为了你的安全着想。
前置知识:groovy、groovy、groovy。
其实变通一下,集成jython也是可以(亲测可行)的和必要的,很多人还是习惯看似简单无脑的python,殊不知这个语言比java要复杂地多,至少培训机构三个月量产不出来。

你可能感兴趣的:(groovy应用)