- request/response封装
- 缓存管理
- 配置文件管理
- 路由/访问频次/拦截器配置
- 类似 spring bean 服务管理
- mysql和redis访问封装
- httpclient等常用工具封装
- web系统支持
请参考官网介绍,https://openresty.org/cn/installation.html
建议安装目录:/usr/local/openresty
从github下载luastar到本地目录,例如:/data/apps/luastar下。
替换配置文件『/yourpath/luastar/conf/luastar*.conf』中的openresty安装路径和luastar存放路径,如下:
## 该配置文件最好放到openresty/nginx/conf/**/下统一进行管理
## 设置lua包路径(';;'是默认路径,?.dylib是macos上的库,?.so是centos上的库)
lua_package_path '/Users/zhuminghua/Documents/work-private/luastar/luastar/libs/?.lua;/Users/zhuminghua/Documents/work-private/luastar/luastar/src/?.lua;;';
lua_package_cpath '/Users/zhuminghua/Documents/work-private/luastar/luastar/libs/?.dylib;/Users/zhuminghua/Documents/work-private/luastar/luastar/libs/?.so;;';
## luastar初始化
init_by_lua_file '/Users/zhuminghua/Documents/work-private/luastar/luastar/src/luastar_init.lua';
## 设置成一样避免获取request_body时可能会缓存到临时文件
#client_max_body_size 50m;
#client_body_buffer_size 50m;
## 请求频次限制字典
lua_shared_dict dict_limit_req 100m;
lua_shared_dict dict_limit_count 100m;
server {
listen 8001;
## 关闭后不用重启nginx即可访问最新代码,生产环境一定要置为on(默认值)
#lua_code_cache off;
server_name localhost;
## luastar路径
set $LUASTAR_PATH '/Users/zhuminghua/Documents/work-private/luastar/luastar';
## 应用名称
set $APP_NAME 'demo';
## 应用路径
set $APP_PATH '/Users/zhuminghua/Documents/work-private/luastar/demo';
## 应用使用的配置,可区分开发/生产环境,默认使用app.lua
set $APP_CONFIG '/config/app_dev.lua';
## 访问日志
access_log '/Users/zhuminghua/logs/nginx/demo/access.log' main;
## 错误/输出日志
error_log '/Users/zhuminghua/logs/nginx/demo/error.log' info;
location / {
default_type text/html;
content_by_lua_file '${LUASTAR_PATH}/src/luastar_content.lua';
}
}
server {
listen 8002;
## web项目关闭lua_code_cache后session会失效
#lua_code_cache off;
server_name localhost;
## luastar路径
set $LUASTAR_PATH '/Users/zhuminghua/Documents/work-private/luastar/luastar';
## 应用名称
set $APP_NAME 'demo2';
## 应用路径
set $APP_PATH '/Users/zhuminghua/Documents/work-private/luastar/demo2';
## 应用使用的配置,可区分开发/生产环境,默认使用app.lua
set $APP_CONFIG '/config/app_dev.lua';
## template模板跟路径,web项目需要配置
set $template_root '/Users/zhuminghua/Documents/work-private/luastar/demo2/views';
## 访问日志
access_log '/Users/zhuminghua/logs/nginx/demo2/access.log' main;
## 错误/输出日志
error_log '/Users/zhuminghua/logs/nginx/demo2/error.log' info;
location / {
default_type text/html;
content_by_lua_file '${LUASTAR_PATH}/src/luastar_content.lua';
}
## 静态文件目录(*.js,*.css...)
location /assets {
root '/Users/zhuminghua/Documents/work-private/luastar/demo2';
index index.html index.htm;
}
}
luastar/conf/目录下多个文件分别对应不同环境,例如luastar_dev.conf是开发环境的配置,luastar.conf是生产环境的配置
修改openresty/nginx/conf/nginx.conf,引入luastar项目配置文件:
include /Users/zhuminghua/Documents/work-private/luastar/luastar/conf/luastar_dev.conf;
/usr/local/openresty/nginx/sbin/nginx -c openresty/nginx/conf/nginx.conf
http://localhost:8001/api/test/hello http://localhost:8001/api/test/hello?name=haha
luastar //luastar项目
conf //可移到openresty/nginx/conf/下
libs //第三方库
src //luastar源码
demo //api项目
config //配置目录
app*.lua //配置文件
bean.lua //bean配置
msg.lua //文案配置
route.lua //路由/频次控制/拦截器配置
src // 源码目录
com
luastar
demo //包
ctrl //控制类目录
interceptor //拦截器
service //服务类
util //辅助类
demo2 //web项目
config //配置目录
src //源码目录
views //template视图目录
assets //静态文件目录
luastar 在初始化时,定义了几个常用的全局变量,在项目中可以直接使用,不用require引入,详情请参看:luastar/src/luastar_init.lua
全局变量 | 说明 |
---|---|
Class | luastar中的类定义 |
cjson | json工具类 |
_ | moses工具类(luastar修改过) |
template | html模板类 |
luastar_cache | luastar缓存 |
luastar_config | luastar配置 |
luastar_context | luastar上下文 |
logger | 日志辅助 |
session | web session |
luastar提供了lua内存缓存,根据openresty机制,每个worker存有一份,所以在使用缓存前,需要先判断是否存在(即使初始化存储过),luastar中使用缓存存储了配置文件信息、bean信息、路由和拦截器信息等等。 例如:luastar/src/core/config.lua
local _M = {}
local util_file = require("luastar.util.file")
function _M.getConfig(k, default_v)
-- 从缓存中获取配置信息
local app_config = luastar_cache.get("app_config")
if app_config then
-- 如果配置信息存在,返回
return app_config[k] or default_v
end
-- 如果配置信息不存在,初始化
ngx.log(ngx.INFO, "init app config.")
-- 加载配置文件
local app_config_file = ngx.var.APP_CONFIG or "/config/app.lua"
local config_file = ngx.var.APP_PATH .. app_config_file
app_config = util_file.loadlua_nested(config_file) or {}
-- 缓存配置信息
luastar_cache.set("app_config", app_config)
-- 返回结果
return app_config[k] or default_v
end
return _M
说明:内存缓存的好处在于支持所有的lua结构,没有限制。
如果需要缓存的内容比较简单或者可以序列化成json,可以考虑使用ngx.shared.DICT,实现全局共享。
有些内容在init_by_lua阶段无法初始化,需要延后在content_by_lua阶段执行,不能放到初始化阶段里作为全局变量直接使用,所以放到了上下文中,详见:luastar/src/luastar/core/context.lua
初始化项目包路径和获取路由已经在请问入口类中调用了,实际项目中应该不会调用,详见:luastar/src/luastar/luastar_content.lua
beanFactory是参考spring中的bean管理实现的一套lua bean,用于服务层,和require进来的对象相比,最大的区别是lua bean是用类实例化出来的对象,可以是单例的,也可以是多实例的,有自己的属性和方法。
-- 获取mysql服务
local beanFactory = luastar_context.getBeanFactory()
local mysql_util = beanFactory:getBean("mysql")
-- 创建链接
local mysql = mysql_util:getConnect()
-- 执行sql
local res, err, errno, sqlstate = mysql:query("select * from user")
ngx.log(logger.i(cjson.encode({
sql = sql,
res = res,
err = err,
errno = errno,
sqlstate = sqlstate
})))
-- 关闭链接
mysql_util:close(mysql)
项目中可能需要将输出的文案配置到配置文件中,便于做国际化或替换。 例如:demo/conf/msg.lua
--[[
提示消息配置
普通消息
local message = luastar_context.getMsg("msg_live", "100001")
占位直接使用string的格式化方法,例如%s, %d等
local message = luastar_context.getMsg("msg_live", "100002"):format(100.00)
多级配置消息获取方法
local message = luastar_context.getMsg("msg_live", "100003", "001")
--]]
msg_pub = {
["100001"] = "错误1!", --
["100002"] = "金额不能超过%d元!", --
["100003"] = {
["001"] = "错误3-1!", --
["002"] = "错误3-2"
}, --
["199999"] = nil
}
msg_uc = {
["200001"] = "错误1!", --
["200002"] = "错误2!", --
["200003"] = "错误3!", --
["299999"] = nil
}
luastar/openresty可以利用ZeroBraneStudio工具调试。
openresty使用ZeroBraneStudio调试步骤可参考链接:http://notebook.kulchenko.com/zerobrane/debugging-openresty-nginx-lua-scripts-with-zerobrane-studio
luastar使用ZeroBranStudio调试步骤如下:
1、在包路径中增加ZeroBranStudio相关库文件,注意macos使用.dylib,centos上使用.so库
lua_package_path 'luastar其他库;/Applications/ZeroBraneStudio.app/Contents/ZeroBraneStudio/lualibs/?/?.lua;/Applications/ZeroBraneStudio.app/Contents/ZeroBraneStudio/lualibs/?.lua;;';
lua_package_cpath 'luastar其他库;/Applications/ZeroBraneStudio.app/Contents/ZeroBraneStudio/bin/clibs/?.dylib;;';
2、在需要调试的代码前后加上
require('mobdebug').start("127.0.0.1")
-- 调试代码
require('mobdebug').done()
3、断点,按ZeroBranStudio方法启动调试
如果觉得调试起来麻烦,日志就是最好的调试办法,简单高效(熟练后完全可以不需要调试)。
luastar直接使用ngx.log输出,之前也有用过第三方库 https://github.com/Neopallium/lualogging 在多worker模式中容易造成日志丢失。ngx.log的缺点是不能个性化按天输出(可以用脚本定时分割),输出大小有限制,不过一般也够用了。
luastar只是简单封装了固定输出request_id和简化的方法,不包装起来是为了直观的输出日志的位置
ngx.log(logger.info("name=", name))
-- 或者
ngx.log(logger.i("name=", name))
输出结果:
2016/12/19 17:01:50 [info] 14545#0: *553 [lua] hello.lua:12: --[2y6hNDFGd4Nxi7FE9UAP]--name=world, try to give a param with name., client: 127.0.0.1, server: localhost, request: "GET /api/test/hello HTTP/1.1", host: "localhost:8001"
[2y6hNDFGd4Nxi7FE9UAP]是本次请求的request_id,便于在日志量大的情况下定位一次请求的所有日志。
一般项目都会有配置文件,在luastar项目中,配置文件放在demo/config/目录下,可以通过在luastar.conf文件中指定不同环境的配置,默认使用app.lua文件
server {
listen 8001;
...
set $APP_CONFIG '/config/app_dev.lua';
...
}
配置文件的内容直接使用lua语法
--[[
应用配置文件
--]]
mysql = {
host = "10.1.1.2",
port = "3306",
user = "root",
password = "lajin2015",
database = "cms_admin",
timeout = 30000,
pool_size = 1000
}
redis = {
host = "10.1.1.4",
port = "6382",
auth = "lajin@2015",
timeout = 30000,
pool_size = 1000
}
_include_ = {
"/config/app_dev_a.lua",
"/config/app_dev_b.lua"
}
include 是一个特殊的用法,支持配置文件嵌套引入。
配置文件的内容在代码中,可以通过luastar_config.getConfig来获取:
local mysqlDataSource = luastar_config.getConfig("mysql")
local mysqlDataSourceHost = luastar_config.getConfig("mysql")["host"]
配置文件的内容也可以直接在bean.lua中使用,
mysql = {
class = "luastar.db.mysql",
arg = {
{ value = "${mysql}" }
}
}
详情请参考bean的配置用法。
频次控制/路由/拦截器在demo/config/route.lua文件中配置
-- 频次限制
limit = { class = "com.luastar.demo.ctrl.test.limit", method = "limit" }
-- 全匹配路由,优先级高
route = {
{ "*", "/api/test/hello", "com.luastar.demo.ctrl.test.hello", "hello", { p1="v1", p2="v2" } },
{ "POST", "/api/test/pic", "com.luastar.demo.ctrl.test.hello", "pic" },
{ "*", "/api/test/mysql", "com.luastar.demo.ctrl.test.mysql", "mysql" },
{ "*", "/api/test/mysql/transaction", "com.luastar.demo.ctrl.test.mysql", "transaction" },
{ "GET,POST", "/api/test/redis", "com.luastar.demo.ctrl.test.redis", "redis" }
}
-- 模式匹配路由
route_pattern = {
{ "*", "/aaa/.*", "com.luastar.demo.ctrl.test.dispatcher", "aaa", { p1="v1", p2="v2" } }, -- aaa
{ "*", "/bbb/.*", "com.luastar.demo.ctrl.test.dispatcher", "bbb" }, -- bbb
{ "*", "/ccc/.*", "com.luastar.demo.ctrl.test.dispatcher", "ccc" }, -- ccc
{ "*", "/.*", "com.luastar.demo.ctrl.test.dispatcher", "other" } -- 默认
}
-- 拦截器配置,注:拦截器必须实现beforeHandle和afterHandle方法
interceptor = {
{
url = {
{ "*", "/api/.*", true }
},
class = "com.luastar.demo.interceptor.common"
}
}
频次限制需要配置实现的类和方法,方法需要返回布尔类型表示是否受限制,目前实现了ip限制,频次限制,单位时间内次数限制,基于 resty.limit 或 redis实现,resty.limit中的count实现需要openresty更高版本(支持dict:expire方法)。
参考: luastar/src/luastar/util/limit.lua demo/src/com/luastar/demo/ctrl/test/limit.lua
路由分为全匹配路由和模式匹配路由,全匹配优先级高,不支持路径取值(不建议),模式使用lua自带的模式。
路由是一个二维数组,每一行表示一个接口地址,第一列表示请求方式(*表示不限制,多个请求方式用逗号分隔,v1.4版本新增),第二列表示请求地址,第三列表示对应的处理类,第四列表示处理类中的方法,第五列表示自定义参数(以第三个参数传到处理类方法中)
luastar默认给ctrl类请求处理方法传入了request/response对象(其他地方可通过ngx.ctx.request和ngx.ctx.response获取)和路由中第五列的自定义参数,用于处理输入和输出和路由扩展。
参考: demo/src/com/luastar/demo/ctrl/test/hello.lua
拦截器每一行表示一个拦截器(优先级取决于数组顺序),url为数组,支持同时拦截多个url,每个url是一个数组(第一列表示拦截的请求方法,*代表所有,第二列可以是模式的,取决于第三列),class代表拦截器实现,excludes表示该拦截器不处理的请求数组。
注:1.2 版本前后结构不同
拦截器要实现两个方法beforeHandle和afterHandle,beforeHandle必须返回布尔类型的结果,只要有一个拦截器返回false,则ctrl不会执行,beforeHandle可以返回第二个参数(字符串类型),用于返回false后的输出结果(返回true时忽略)
参考:demo/src/com/luastar/demo/interceptor/common.lua
luastar实现了简化版的spring bean factory,默认将bean实例化后以单例模式(每个worker一份)存在缓存中,和require进来的对象相比,最大的区别是lua bean是用类实例化出来的对象,可以是单例的,也可以是多实例的,有自己的属性和方法。
bean在配置文件demo/config/bean.lua文件中配置,注意保证id的唯一性
--[[
id = { -- bean id
class = "", -- 类地址
arg = { -- 构造参数注入
{value/ref = ""} -- value赋值,ref引用其他bean
},
property = { -- set方法注入,实现set_${name}方法
{name = "",value/ref = ""}
},
init_method = "", -- 初始化方法,默认使用init()
single = 0 -- 是否单例,默认是1
}
--]]
-- mysql服务
mysql = {
class = "luastar.db.mysql",
arg = {
{ value = "${mysql}" }
}
}
-- redis服务
redis = {
class = "luastar.db.redis",
arg = {
{ value = "${redis}" }
}
}
-- 系统用户服务
userService = {
class = "com.luastar.demo2.service.system.userService"
}
-- 功能服务
funcService = {
class = "com.luastar.demo2.service.system.funcService"
}
-- 角色服务
roleService = {
class = "com.luastar.demo2.service.system.roleService"
}
-- 角色关系服务
userRoleRelationService = {
class = "com.luastar.demo2.service.system.userRoleRelationService"
}
-- 引入其他模块
_include_ = {
"/config/bean_uc.lua"
}
bean配置文件也支持_include_引入其他配置的语法。 在类中定义的方法最好使用类的模式,存储私有变量,可以使用luastar框架中的class类定义。 参考: demo2/src/com/luastar/demo2/service/system/userService.lua
在代码中先获取bean工厂,再获取bean
function _M.list(request, response)
local param = {
draw = request:get_arg("draw"),
start = tonumber(request:get_arg("start")) or 0,
limit = tonumber(request:get_arg("length")) or 10,
keyword = request:get_arg("query_username")
}
-- 查询结果
local beanFactory = luastar_context.getBeanFactory()
local userService = beanFactory:getBean("userService")
local num = userService:countUser(param);
local data = {}
if num > 0 then
data = userService:getUserList(param);
end
-- 返回结果
local result = {
draw = param["draw"],
recordsTotal = num,
recordsFiltered = num,
data = data
}
response:writeln(json_util.toJson(result, true))
end
luastar中对mysql和redis的操作基于openresty官方提供的组件: LuaRestyMySQLLibrary LuaRestyRedisLibrary
luastar中对mysql和redis提供了以下功能:
- 数据源配置
- 获取连接
- 关闭连接(使用连接池)
- mysql事务
- sql语句动态拼装
- 未关闭连接监控
demo2/conf/app.lua中配置相关数据源,例如:
mysql = {
host = "127.0.0.1",
port = "3306",
user = "root",
password = "xxx",
database = "xxx",
timeout = 30000,
pool_size = 1000
}
redis = {
host = "127.0.0.1",
port = "6379",
auth = "xxx",
timeout = 30000,
pool_size = 1000
}
demo2/conf/bean.lua中配置mysql/redis bean,多数据源可以配置多个,id不一样即可。
mysql = {
class = "luastar.db.mysql",
arg = {
{ value = "${mysql}" }
}
}
redis = {
class = "luastar.db.redis",
arg = {
{ value = "${redis}" }
}
}
-- 获取封装类
local beanFactory = luastar_context.getBeanFactory()
local mysql_util = beanFactory:getBean("mysql")
local redis_util = beanFactory:getBean("redis")
-- 对于单次请求操作,可直接使用下列语句,不用获取和关闭连接
mysql_util.query("sql")
redis_util.hgetall("key")
-- 对于多次请求操作,需要先获取到连接,依次执行,最后关闭连接
local mysql = mysql_util:getConnect()
local res1, err1, errno1, sqlstate1 = mysql:query(sql1)
local res2, err2, errno2, sqlstate2 = mysql:query(sql2)
mysql_util:close(mysql)
local redis = redis_util:getConnect()
local userinfo = table_util.array_to_hash(redis:hgetall("user:info:" .. uid))
redis_util:close(redis)
local _M = {}
local sql_util = require("luastar.util.sql")
function _M.mysql(request, response)
local name = request:get_arg("name") or ""
local sql_table = {
sql = [[
select * from SYS_USER
@{where}
order by ID desc
limit #{start},#{limit}
]],
where = {
"LOGIN_NAME = #{loginName}",
[[
and USER_NAME like concat('%',#{userName},'%')
]]
}
}
local data = { userName = name, start = 0, limit = 10 }
local sql = sql_util.getsql(sql_table, data)
local beanFactory = luastar_context.getBeanFactory()
local mysql_util = beanFactory:getBean("mysql")
local mysql = mysql_util:getConnect()
local res, err, errno, sqlstate = mysql:query(sql)
mysql_util:close(mysql)
response:writeln(cjson.encode({
sql = sql,
res = res,
err = err,
errno = errno,
sqlstate = sqlstate
}))
end
function _M.transaction(request, response)
local beanFactory = luastar_context.getBeanFactory()
local mysql_util = beanFactory:getBean("mysql")
local sqlArray = {
"update SYS_USER set USER_NAME='管理员1' where ID=1",
"update SYS_USER set USER_NAME_A='管理员2' where ID=1" -- USER_NAME_A not exists
}
local result_table = mysql_util:queryTransaction(sqlArray)
response:writeln(cjson.encode(result_table))
end
return _M
完整配置如下:
-- #{},如果值为字符串,则增加单引号防sql注入,如果为空,处理为null
-- ${},直接替换,如果为空,处理为null
-- @{},引用其他语句
sql_table = {
sql = [[
update SYS_USER @{set} @{where} @{limit}
]],
set = {
"USER_NAME = #{userName}", -- userName为nil时忽略
"UPDATED_TIME = #{updatedTime}"
},
where = {
"LOGIN_NAME = #{loginName}", -- loginName为nil时该语句忽略
[[
and USER_NAME like concat('%',#{userName},'%') -- userName为nil时该语句忽略
]]
},
limit = {
start = "${start}", -- start和limit为nil时忽略
limit = "${limit}"
}
}
luastar默认开启了mysql和redis的未关闭连接监控,如果有没有关闭的连接,会输出错误日志:
2016/12/20 16:34:23 [error] 40144#0: *45 [lua] monitor.lua:42: check(): check info +...luastar/db/mysql.lua:73, client: 127.0.0.1, server: localhost, request: "GET /api/test/mysql/transaction HTTP/1.1", host: "localhost:8001"
加号代表开启了连接的位置,减号代表关闭了连接的位置,如果有不匹配的+和-,则能定位到未关闭的位置,如果一次请求中开启和关闭的次数太多,日志可能输出不全(ngx.log的限制)。
luastar中session的管理使用的是第三方库: lua-resty-session
session已放入到全局变量中,可以在代码中直接使用,支持cookie、shm、memcache和redis持久化方式。
session保存
-- session保存
ngx.log(logger.i("保存session: ", cjson.encode(userInfo)))
session.save("user", userInfo)
session校验
function _M.beforeHandle()
-- session校验
if session.check() then
local data = session.getData("user")
ngx.log(logger.i("用户session验证通过", cjson.encode(data)))
return true
end
ngx.log(logger.i("用户session验证不通过"))
local xRequestedWith = ngx.ctx.request:get_header("x-requested-with")
if _.isEmpty(xRequestedWith) then
template.render("login.html", { message = "登录超时!" })
else
ngx.ctx.response:set_header("session-status", "timeout");
ngx.ctx.response:writeln(json_util.timeout())
end
return false
end
session数据获取
-- 登录用户信息
local userInfo = session.getData("user")
session销毁
-- 销毁session
session.destroy()
其他用法可参考官方文档
luastar中页面布局和渲染使用第三方库 lua-resty-template
配置web相关目录,相比api项目,需要额外配置template模板根路径和静态文件访问, 详见:luastar/conf/luastar.conf 中 demo2 的配置
template页面渲染语法在这里不多介绍了,请参考 lua-resty-template 和 demo2 中的实现。
sql语句在 demo2/src/resources/luastar-cms.sql 中,配置好数据源后可使用� admin / admin 登录系统,目前只实现了登录和用户管理的简单功能,1.3版本中前端框架改用 layui,整体简洁了不少。
luastar 完全开源,不限制,欢迎使用和交流。
QQ交流群:545501138
Email:[email protected]