A Kotlin/Java mvc framework inspired by Zend Framework 1
For demo, please check release-manager
In short, Kotlin is better Java.
- No checked exception(May I say stupid feature in java?)
- String("abc") == String("abc") is true
- Auto type inference
val longValue = 5L
- String expression
logger.warn("script running error, file: ${script.file}, line: ${script.getCurrentLine()} at $script")
- Multiline String
val text = """first line
second line
third line
""".trimMargin()
- Operator override
val cat = Cargo["cat"]
Cargo["cat"] = Cat()
Instead of
Cat cat = Cargo.get("cat")
Cargo.set("cat", new Cat())
- Default parameter
fun setCache(key: String, value: String, expireSeconds: Long = 3600){...}
- Call method via named parameter
setCookie("userid", userId, domain = "example.com", path = "/", maxAge = 3600)
- Data class
data class HelperPair(val name: String, val clazz: Class<*>)
val loggerHelper = HelperPair("logger helper", com.example.LoggerHelper)
println(loggerHelper.name)
val logger= loggerHelper.clazz.newInstance()
- singleton class
object ASingletonClass(){val name = "singleton"}
- import alias
import System.out.print as p
import System.out.println as pn
- and many others, such as auto close resource with use{...} etc
For now, Kotlin compiler is pretty bad at syntax error tips, especially if compiler can't find your annotation, the only hint you got is somthing like "@error.NonExistentClass()"
@TinyApplication
class MyApp : TinyBootstrap {
val name = "myapp"
val env = System.getProperty("tiny.app.env") ?: "production"
override fun bootstrap() {
/*the application config file is classpath:"config/${env}/${name}.ini" */
TinyApp.init(env, name)
}
}
A @WebServlet annotated servlet is auto generated and @TinyApplication annotated class.bootstrap() is called in servlet.init() method.
Run application in embedded jetty server
fun main(args: Array<String>){
TinyApp.runJetty()
}
java -jar myapp.jar -Dtiny.app.env=development
Test something in shell
fun main(args: Array<String>){
val app = MyApp()
app.bootstrap()
val jdbc: TinyJdbc = TinyRegistry["db.account"]
try{
val users = jdbc.queryForList("select id, name from users where 1 order by id desc limit 5")
users.ex?.printStackTrace()
DebugUtil.print(users.data)
DebugUtil.print(TinyRegistry.getStorage())
}finally{
TinyApp.shutdown()
}
}
java -jar myapp.jar -Dtiny.app.env=development
Config files are located in classpath: "config/${evn}" directory and end with ".ini".
#Application config: ${appName}.ini
#a must-have
timezone = Asia/Shanghai
#a must-have
log.path = /data/logs
#extra static file path for development hot reload
static.extra.dir = src/main/resources/static
#extra template file path for development hot reload
template.extra.dir = src/main/resources/templates
#cookie domain
cookie.domain =
session.enable = true
session.name = MYSESSIONID
#for now the only storage supported is "redis"
session.storage = redis
#redis provider in "redis.ini"
session.storage.provider = redis.default
session.expire.seconds = 3600
cache.enable = true
cache.prefix = myapp_
cache.expire.seconds = 3600
#redis provider in "redis.ini"
cache.storage.provider = redis.default
#apache commons fileupload config
upload.fileInMemory.maxSize.megabyte = 5
upload.tempfile.dir = /tmp
upload.post.maxSize.megabyte = 50
#datasource config: db.ini
db.account.autoload = true #autoload on application start or not
db.account.url = jdbc:mysql://127.0.0.1:3306/account?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT
db.account.username = username
db.account.password = password
db.account.hikari.minimumIdle = 1
db.account.hikari.maximumPoolSize = 30
#redis config: redis.ini
redis.default.autoload = true
redis.default.host = 127.0.0.1
redis.default.port = 6379
redis.default.database = 1
redis.default.pool.maxTotal = 10
redis.default.pool.maxIdle = 5
redis.default.pool.minIdle = 1
/*Controllers need in a package name ending with ".controller", e.g "myapp.controller",
by doing this hotswap plugin can identify it as a controller*/
package myapp.controller
import tiny.annotation.Controller
import tiny.TinyController
@Controller
/* URL http://yourhost/hello/world */
class HelloController : TinyController(){
fun worldAction(): String {
return "hello world"
}
fun greetingAction(): String{
val username = ctx.params["username"]
return "How are you $username"
}
}
The URI and controller/action name is case insensitive, so /HeLLo/WorlD and HeLLoController.WOrlDAction() work, but you have to end with "Controller" and "Action"
/*
* /greeting/lina rewrite to IndexController.greetingAction()
* with ctx.params["username"] == "lina"
*/
TinyRouter.addRoute("/greeting/([a-zA-Z]+)", "index/greeting", arrayOf(Pair(1, "username")))
The only template engine supported is Groovy GString
<%=view.render("header")%>
This is a groovy gstring template<br />
Greeting, $username <br />
25*25 = <%=helper.Square.getSquare(25)%> <br />
<%=view.render("footer")%>
class HelloController : TinyController(){
fun tplAction(): Any{
this.view["username"] = "Lina"
return render("body") /* groovy.lang.Writable */
}
}
template file is in classpath: "templates" and ending with ".tpl", template cache is disabled in "development" environment.
/*Helpers need in a package name ending with ".helper", e.g "myapp.helper",
by doing this hotswap plugin can identify it as a helper*/
package example.helper
import tiny.annotation.Helper
@Helper
/*helper class need end with "Helper"*/
class SquareHelper {
fun getSquare(value: Long): Long { /* in template: helper.Square.getSquare(25) */
return value * value
}
}
class TestController : TinyController(){
fun testAction(): String {
val request = ctx.request /*javax.servlet.http.HttpServletRequest*/
val response = ctx.response /*javax.servlet.http.HttpServletResponse*/
/* params */
val userAge = ctx.params.getLong("age", 18) //defalut age = 18
val userName = ctx.params["name"]
val action = ctx.params.getString("action")
/* session */
val userId: Long? = ctx.session["userid"] ?: 0
if(userId < 1) {
val loggedinId = doUserlogin()
/* loggedin with a new session for better security */
ctx.newSession()
ctx.session["userid"] = loggedinId
}
if(action == "logout") {
ctx.session.destroy()
ctx.session["flashMessage"] = "You have been logged out successfully"
}
/* cookie */
val currentArticleId = ctx.cookies.getInt("current_article_id")
val currentEditor = ctx.cookies["current_editor"]
ctx.setCookie("currentEditor", "Zorro",
maxAge = 3600,
domain = "example.com",
path = "/",
secure = false,
httponly = false)
/* fileupload */
val avatar = ctx.files["avatar"] /* org.apache.commons.fileupload.FileItem */
if(avatar == null || avatar.size == 0L){
return "no avatar uploaded."
}
val saveTo = File("/tmp/avatar_${UniqueIdUtil.getUniqueId()}")
try{
avatar.write(saveTo)
}catch(e: Throwable){
logger.warn("Save avator error: " + e)
throw e
}
return "done."
}
}
fun testing() {
/* TinyConfig */
val redisConfig = TinyConfig("config/${TinyApp.getEnvString()}/redis.ini")
val loader = TinyResourceLoader()
val redisLocal = loader.loadRedis(redisConfig, "redis.local")
val redis: TinyRedis = TinyRegistry["redis.default"]
val jdbcAccount: TinyJdbc = TinyRegistry["db.account"]
/* TinyCache */
/* real key == "demo_user_id_123" since app.ini, cache.prefix = demo_ */
TinyCache.set("user_id_123", HashMap<String, Any>(
"id" to 123,
"name" to "nana"
), expireSeconds = 3600)
val userNana = TinyCache.get("user_id_123", HashMap::class.java) /*HashMap*/
val userNanaString = TinyCache.get("user_id_123") /*String*/
TinyCache.delete("user_id_1")
/* TinyRedis */
redis.set("demo_user_id_123", HashMap<String, Any>(
"id" to 123,
"name" to "nana"
), expireSeconds = 3600)
redis.exec({ connection ->
val commands = connection.sync()
commands.expire("demo_user_id_123", 600)
})
/* TinyJdbc */
val p = HashMap<String, Any>(
"id" to 1001,
"name" to "grrr"
)
jdbcAccount.insert("insert into users(id, name) values(:id, :name)", p)
val userGrrr = jdbcAccount.selectForMap("select id, name from user where id = :id", p)
userGrrr.ex?.printStackTrace()
DebugUtil.print(userGrrr.data)
}
fun main(args: Array<String>){
TinyApp.init("development", "myapp")
try{
testing()
}finally{
TinyApp.shutdown()
}
}
The dependency injection framework using is Dagger2
Tiny framework automatically create the dagger component(tiny.weaver.MagicBox) and component holder(tiny.weaver.TinyBird)
@WeaverBird links a module to current component:
package myapp.weaver
import tiny.annotation.WeaverBird
import javax.inject.Inject
import dagger.Provides
import dagger.Module
import javax.inject.Named
private val dbAccount: TinyJdbc = TinyRegistry["db.account"]
@WeaverBird
@Module
class JdbcWeaver {
@Provides @Named("db.account") fun provideAccount() : TinyJdbc {
return dbAccount
}
}
@AutoWeave create "weave" method in dagger commponent, you can call "TinyBird.get().weave(this)" in constructor to inject Dagger to target class, or do it automatically via aop AutoWeaveHandle
package myapp.dao
import javax.inject.Inject
import javax.inject.Named
import tiny.weaver.TinyBird
import tiny.annotation.AutoWeave
import tiny.lib.db.SqlResult
import tiny.lib.TinyJdbc
class AccountDao {
@Inject @Named("db.account")
private lateinit var _dbAccount: TinyJdbc
@AutoWeave fun constructor(){
/* do not need this if you have a aop */
TinyBird.get().weave(this)
}
fun getUserInfo (userId: Long): SqlResult<Map<String, Any>> {
val p = HashMap<String, Any>(
"id" to userId,
)
val info = _dbAccount.queryForMap("select id, name from user where id = :id", p)
return info
}
}
/*
* create log file in ${log.path}/yyyy/mm/myevent_dd.log with content:
* 1998-01-01T16:25:21+0800 my message
*/
TinyLog.log("my message", "myevent")
To run a batch command, you need to avoid those things:
- Don't use logback log to the same file, since logback lock the opened file.
- Don't autoload redis and database, since big resource pool can be used in production environment, for example, you may have hikari.minimumIdle == hikari.maximumPoolSize, once you start a batch command, it creates maximumPoolSize connections immediately.
fun main(args: Array<String>) {
val name = "myapp"
val env = System.getProperty("tiny.app.env") ?: "production"
val script = System.getProperty("tiny.app.script") ?: ""
if(!script.isEmpty()){
TinyScript.run(env, name, script)
}
}
java -jar myapp.jar -Dtiny.app.env=development -Dtiny.app.script=myapp.script.Hello
package myapp.script
import tiny.*
import tiny.lib.*
class Hello{
fun run() {
val redisConfig = TinyConfig("config/${TinyApp.getEnvString()}/redis.ini")
val dbConfig = TinyConfig("config/${TinyApp.getEnvString()}/db.ini")
val loader = TinyResourceLoader()
loader.loadRedis(redisConfig, "redis.default", fixedPoolSize = 1)
loader.loadJdbc(dbConfig, "db.account", fixedPoolSize = 1)
printRegister()
printHello()
throw HelloScriptException("something is wrong")
}
fun printRegister() {
DebugUtil.print(TinyRegistry.getStorage())
}
fun printHello() {
println("this is hello script")
}
}
private class HelloScriptException(message: String?) : Throwable(message)
- For static and template files
static.extra.dir = src/main/resources/static
template.extra.dir = src/main/resources/templates
Now, refresh browser and check result after you modify the source code, please make sure the path is correct, otherwise it fallback to classpath files which can't auto reload after modifying the source code
- For class files
There is a hotswap agent plugin tiny.hotswap.TinyHotSwap, once you have hotswap agent installed
pluginPackages=tiny.hotswap
autoHotswap=false
disabledPlugins=Hibernate, Hibernate3JPA, Hibernate3, Spring, Jersey1, Jersey2, Jetty, Tomcat, ZK, Logback, Log4j2, MyFaces, Mojarra, Omnifaces, Seam, ELResolver, WildFlyELResolver, OsgiEquinox, Owb, WebObjects, Weld, JBossModules, ResteasyRegistry, Deltaspike, GlassFish, Vaadin, Wicket
Add this to hotswap-agent.properties, compile classes and check your work.
That's all. Hope you enjoy it:)