Latke 快速上手指南

该文档适合 Java Web 应用框架初学者以及 Latke 应用开发者,大家在看文档的同时,欢迎提出问题,我们一起讨论如何解决,帮助框架演进。

概述

Latke('l ɑ:tk ə,土豆饼)是一个简单易用的 Java Web 应用开发框架,包含 MVC、IoC/AOP、事件通知、ORM、插件等组件。

在实体模型上使用 JSON 贯穿前后端,使应用开发更加快捷。这是 Latke 不同于其他框架的地方,非常适合小型应用的快速开发, 为什么又要造一个叫 Latke 的轮子

核心组件

Hello World!

Demo 项目完整代码: https://github.com/88250/latke-demo

项目结构

└─src
    └─main
        ├─java
        │  └─latke
        │      └─demo
        │          │  Server.java
        │          │
        │          ├─processor
        │          │      HelloProcessor.java
        │          │      RegisterProcessor.java
        │          │      SimpleFMRenderer.java
        │          │
        │          ├─repository
        │          │      UserRepository.java
        │          │
        │          └─service
        │                  UserService.java
        │
        └─resources
            │  latke.properties
            │  local.properties
            │  log4j.properties
            │  repository.json
            │  static-resources.xml
            │
            └─template
                │  hello.ftl
                │  index.ftl
                │  register.ftl
                │
                └─images
                        logo.png

启动服务器

public class Server extends BaseServer {

    public static void main(String[] args) {
        Latkes.setScanPath(Server.class.getPackage().getName());
        Latkes.init();
        // 初始化数据库表
        JdbcRepositories.initAllTables();

        final Server server = new Server();
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            server.shutdown();
            Latkes.shutdown();
        }));

        final BeanManager beanManager = BeanManager.getInstance();
        final HelloProcessor helloProcessor = beanManager.getReference(HelloProcessor.class);
        final RegisterProcessor registerProcessor = beanManager.getReference(RegisterProcessor.class);

        // 配置路由
        final Dispatcher.RouterGroup routeGroup = Dispatcher.group();
        routeGroup.get("/", helloProcessor::index).
                get("/register", registerProcessor::showRegister).
                post("/register", registerProcessor::register).
                get("/var/{pathVar}", registerProcessor::paraPathVar).
                router().get().post().uri("/greeting").handler(helloProcessor::greeting);
        Dispatcher.mapping();

        server.start(8080);
    }
}

请求处理

@Singleton
public class HelloProcessor {

    private static final Logger LOGGER = LogManager.getLogger(HelloProcessor.class);

    public void index(final RequestContext context) {
        context.setRenderer(new SimpleFMRenderer("index.ftl"));

        final Map<String, Object> dataModel = context.getRenderer().getRenderDataModel();
        dataModel.put("greeting", "Hello, Latke!");

        Requests.log(context.getRequest(), Level.DEBUG, LOGGER);
    }

    public void greeting(final RequestContext context) {
        context.setRenderer(new SimpleFMRenderer("hello.ftl"));

        final Map<String, Object> dataModel = context.getRenderer().getRenderDataModel();
        dataModel.put("time", new Date());
        final String name = context.param("name");
        if (StringUtils.isNotBlank(name)) {
            dataModel.put("name", name);
        }
    }
}

另外,通过使用不同的响应渲染器可以生成不同类型的响应,例如 HTML、RSS、PNG 等。

服务调用

@Singleton
public class RegisterProcessor {

    @Inject
    private UserService userService;

    public void showRegister(final RequestContext context) {
        context.setRenderer(new SimpleFMRenderer("register.ftl"));
    }

    public void register(final RequestContext context) { // 函数式路由,在 Server 中配置
        context.setRenderer(new SimpleFMRenderer("register.ftl"));
        final Map<String, Object> dataModel = context.getRenderer().getRenderDataModel();

        final Request request = context.getRequest();
        final String name = request.getParameter("name");
        if (StringUtils.isNotBlank(name)) {
            dataModel.put("name", name);

            userService.saveUser(name, 3);
        }
    }

    public void paraPathVar(final RequestContext context) {
        final String paraVar = context.param("paraVar");
        final String pathVar = context.pathVar("pathVar");
        context.renderJSON(new JSONObject().put("paraVar", paraVar).put("pathVar", pathVar));
    }
}

服务实现

@Service
public class UserService {

    private static final Logger LOGGER = Logger.getLogger(UserService.class);

    @Inject
    private UserRepository userRepository;

    @Transactional
    public void saveUser(final String name, final int age) {
        final JSONObject user = new JSONObject();
        user.put("name", name);
        user.put("age", age);

        String userId;

        try {
            userId = userRepository.add(user);
        } catch (final RepositoryException e) {
            LOGGER.log(Level.ERROR, "Saves user failed", e);

            // 抛出异常后框架将回滚事务
            throw new IllegalStateException("Saves user failed");
        }

        LOGGER.log(Level.INFO, "Saves a user successfully [userId={0}]", userId);
    }
}

DAO

@Repository
public class UserRepository extends AbstractRepository {

    public UserRepository() {
        super("user");
    }

    public JSONObject getByName(final String name) throws RepositoryException {
        return getFirst(new Query().setFilter(new PropertyFilter("name", FilterOperator.EQUAL, name)));
    }
}

最佳实践

表名前缀

在 local.properties 中有一项配置 jdbc.tablePrefix,如果配置了该项,则初始化表(JdbcRepositories.initAllTables())时生成的表名就会带有前缀。

建议应用配置该项,以屏蔽不同数据库迁移数据时关键字对表名的影响。

实体模型

Lakte 使用 JSON 作为实体载体,管理 JSON 的键就是对实体的建模。实体的键对应了数据库表列名,实体内嵌的关联对象是服务中组装的。例如对于 User 实体,键包含了简单类型属性:nameage,关联类型属性:books,构造的对象如:

{
    "name": "Daniel",
    "age": 23,
    "books": [{
        "name": "TAO of Life"
    }]
}

则键管理可以通过 User 类:

public class User {
    public static final String USER_NAME = "name";
    public static final String USER_AGE = "age";
    public static final String USER_T_BOOKS = "books";
}

T(Transient) 表示这个属性是非持久化的(User 表中无此列),是通过在服务中组装而来的。

repository.json

这个文件可以手工编写,然后使用 JdbcRepositories#initAllTables 方法自动创建数据库;也可以使用 JdbcRepositories#initRepositoryJSON 方法从已有数据库表生成这个文件。

repository.json -> tables:

tables -> repository.json:

这两种方式没有什么本质上的区别,可由开发自由决定。

关联查询

实体 JSON 对象中的关联属性是通过组装而来,需要先把这个属性查询出来,再编程组装到这个实体 JSON 对象中。这一点相对于一些 ORM 框架(例如 Hibernate)来说是比较繁琐,但这样做的优势之一就是能够使实体变得更灵活、更容易加入缓存优化性能。

也支持自定义 SELECT SQL,请参考接口 repository#select。

  • Latke

    Latke 是一款以 JSON 为主的 Java Web 框架。

    66 引用 • 504 回帖 • 528 关注
  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    2675 引用 • 7947 回帖 • 778 关注
  • 框架
    38 引用 • 321 回帖 • 1 关注
  • MVC
    7 引用 • 113 回帖
2 操作
88250 在 2020-04-01 16:55:59 更新了该帖
88250 在 2020-02-10 10:43:57 更新了该帖
优质回帖
  • ydcun 1 1 赞同

    希望能为这个轮子做点贡献

  • umeone 1 赞同

    不错,以前还真没这么去想过,准备研究一下。。

  • 88250 1 赞同

    可行的,等后面有空再加吧。

78 回帖
请输入回帖内容...
  • lucky

    我尝试创建了一张表写了一个添加的功能,结果报了这个错误。

    [ERROR]-[2018-09-14 21:51:19]-[com.vseu.blog.processor.console.SentenceConsole:62]: org.b3log.latke.repository.RepositoryException: com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'from,oId,type,gmt_modified) values ('2018-09-14 21:51:19.952','天空本是一' at line 1
    [ERROR]-[2018-09-14 21:51:29]-[org.b3log.latke.repository.jdbc.JdbcRepository:151]: Add failed
    com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'from,oId,type,gmt_modified) values ('2018-09-14 21:51:29.937','你永远也看' at line 1
    	at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    	at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
    	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
    	at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
    
    

    这个字段的顺序和我在数据库或 model 中的顺序不同,repository.json 已按照上面的 demo 写了,无法打印 SQL,搞不清是哪里错了 。 求助大佬

    from,oId,type,gmt\_modified) values ('2018-09-14 21:51:29.937','你永远也看'
    
    1 回复
  • 88250 1 赞同

    改下 log4j 的配置就可以打印 SQL 了。

    log4j.logger.org.b3log.latke.repository.jdbc.util=TRACE
    
  • lucky

    请问,访问 http://127.0.0.1:8080/articles/1537552669251.html 的时候会映射到 @RequestProcessing(value \= "/article", method \= HTTPRequestMethod.GET) 而不会映射到 @RequestProcessing(value \= "/articles/*", method \= HTTPRequestMethod.GET)
    这是在哪里匹配的呢?

    1 回复
  • 88250

    从 PermalinkFilter.java 看起。

    1 回复
  • lucky

    我想再问一下 tags.html 这个映射到哪里了,直接放回页面的话 tags 的值是哪来的 = =

    1 回复
  • 88250

    UserTemplateProcessor.java

  • Ahian 1

    真的有必要研究研究这个框架👍

  • a7223923 1

    666666,现在正在研究这个源码哈哈。

  • MonkeyL

    你好,请问如何才能成为 latke 开源项目的 contributor 呢?想贡献一些代码😄

    1 回复
  • 88250

    请参考 Latke 贡献指南

  • yangyu1

    新手上路 有没有大佬可以发一下详细的文档 表示很多方法根本看不懂😭

  • chenzexin

    如果是使用其他数据库,怎么办?需要重写 latke-core 吗?

    1 回复
  • 88250

    引用不同的 repository 支持即可,目前支持 H2、MySQL、Oracle、SQLServer,具体可以看下 Latke 的项目代码。

  • chenzexin

    AGPL 的相关条款是什么?这个免费给个人使用吗?

    1 回复
  • 88250

    可以免费甚至商用,但必须开源所有代码。

  • skqing

    既然使用 JSON,那为什么不使用 MongoDB 呢?

    1 回复
  • 88250

    很早之前考虑过,不过当时 MongoDB 还不那么流行。现在等有时间会实现一下,很容易扩展的。

  • brandim

    事件通知 有使用介绍没???

    1 回复
  • 88250

    那个是个简单的观察者模式实现,基本是参考 JDK 代码实现的。具体使用示例可以参考 Solo 或者 Sym 里的代码。

  • jnuff-u

    执行 add()方法时需要传表的主键么?
    我看不传的话在 buildAddSql()方法里直接会添加默认的键(MySQL - JdbcRepository), 而默认键是'oId',表里面没有此字段的时候就会报错。另外,为什么默认键是‘oId’呢?
    还有一个问题,表的键是如何确认的?——执行的时候看到 JdbcRepositories 和 Keys 类,而 Keys 里面的 OBJECT_ID = JdbcRepositories.DefaultKeyName(),这个 DefaultKeyName 属性也没有看到哪里有调用 set 方法的,有个 fillFieldDefinitionData 方法有确认 iskey,是需要在 JSON 文件里面设置一个 isKey 属性么?
    JSON 文件是使用 initRepositoryJSON 生成的,我看里面对于表的主键也没有明确的标明,所以这些疑问,希望大大不吝赐教~

    2 操作
    jnuff-u 在 2019-09-26 17:59:53 更新了该回帖
    jnuff-u 在 2019-09-26 17:58:09 更新了该回帖
  • jnuff-u

    我是看 JSONObject 类里有 optBoolean()方法获取 JSON 文件里表的属性信息的。手动设置了 isKey 为 true 的属性:

     "keys": [
        {
          "name": "id",
          "length": 10,
          "type": "int",
          "description": "主键",
          "iskey": true
        },
    

    但是仍然会构建到:
    java.sql.SQLSyntaxErrorException: Unknown column 'oId' in 'field list'
    所以想问一下这个过程~🙏

    1 回复
    2 操作
    jnuff-u 在 2019-09-26 18:10:41 更新了该回帖
    jnuff-u 在 2019-09-26 18:10:24 更新了该回帖
  • 88250

    就用 oId 吧 🤣

  • Wordsdonotread

    我想问一下,我有个 Jar 包文件,想通过 IoC 来时先某个类的管理,通过 get,set 方式或者构造函数进行参数设置,改怎么是配置?

    1 回复
  • 88250

    不好意思,我没看懂你说的。

  • adlered 1

    没看明白,马一下

  • adlered 1

    终于看懂了,点个赞,设计思想很强~

  • JoeysCat

    @88250 大佬,注解式路由不能用了吗?找不到 RequestProcessing

    1 回复
  • 88250 1

    是的,现在仅支持编程式路由了。

请输入回帖内容 ...