在游戏开发中,lua脚本被大量运用。其思路是,将脚本作为黏合组件的胶水,利用基础组件提供的功能,灵活地组合出各种不同的功能,就好像创造出一种新的DSL(domain specific language)一样。在后端开发中,显然也可以借鉴这里面的宝贵经验。在java生态下,使用groovy就显得自然而然。
作为厚重java组件的胶水语言,他具有以下优势:
new String[]{"a"}
、(a,b) -> a+b
等java语法也得到了支持,def m = [:] as Map>
在groovy2中因为末尾的>导致编译器解析报错的bug也得到了解决——这门语言还是在蓬勃发展呀!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要复杂地多,至少培训机构三个月量产不出来。