JerryFramework简介及入门教程

简介

JerryFramework是我在大二下学期独立开发的一个侵入式Web框架,倡导约定优先,用于JavaWeb这门课程的期末课程设计,它包含内嵌Web容器JerryMouse(名字灵感来源于Tomcat)与一整套组件(如错误处理、Session、静态Web、MVC等)。因为我初中与高中都以.NET技术栈为主,大二开始学习Java技术栈后,并不是很喜欢Spring MVC的设计哲学,所以你在这个框架中可以看到一些ASP.NET的影子。

更新说明:2019.5.24,在我完成了这个框架的第一版后,写下了这篇简单的使用教程。时隔一年,我又对它进行了些许打磨,增加了许多新特性,是时候更新一下这篇入门教程了。

设计哲学

设计思想分两部分介绍:Web容器与MVC框架。当然,具体的实现细节并不是一篇文章就能够讲完的,这里只介绍思想与API的使用。

Web容器

JerryFramework不基于任何现有技术(如Servlet),它本身就含有一个自托管的Web容器(JerryMouse),Web容器只负责接收请求与生成响应,构造出HTTP上下文(可以类比ServletRequest/Response)并送入由多个中间件组成的请求处理管道,每个中间件实现具体功能(如错误拦截、静态资源、授权认证、MVC等)。

请求处理模型与ASP.NET Core类似,中间件依次按序排列,Web容器调用第一个中间件,随后由中间件决定何时调用下一个中间件,相对于下一个中间件,可以前置/后置/环绕执行,也可以不执行,进而打破请求处理管道(如静态Web中间件已经找到了请求的资源,就无需将上下文传递给MVC中间件)。用户可以编写自己的中间件以扩展框架功能。当然,这种设计决定了处理管道里的中间件必须按一定顺序排列。因此,框架使用构造者模式实现了JerryBuilder,用来快速构建服务。

MVC框架

Jerry MVC是一个侵入式Web框架,倡导约定优于配置

为何要侵入式?

侵入式与非侵入式是一个可以长久讨论的话题。在服务层,服务间调用、依赖关系复杂,并涉及许多业务,这时采用侵入式设计是非常糟糕的,会大大加重耦合,导致维护、测试困难。而在Web层,情况有一些不同,首先,Web层作为应用的边界,往往不会和同级组件发生相互依赖(例如在一个设计良好的订单系统中,OrderController并不会依赖UserController,这些应当在服务层处理);其次,Web层通常需要进行上下文的交互(如Request/Response/Session等等),非侵入式框架只能通过注入的形式实现,而侵入式框架可以在基类中提供操作方法;另外,Web不可避免的会引入框架相关的代码,导致项目与框架绑定,Spring非侵入的思想在Web层更像是一个”美丽的谎言”(例如在SpringMVC中需要向框架传递Model,则引入了框架相关的类)。

非侵入式框架的典型代表是Spring MVC,侵入式框架的典型代表则是ASP.NET MVC,Jerry MVC类似后者

为何要约定优于配置?

约定优先只是一种设计哲学,有优点,也有缺点,是否接受这种思想则取决于开发人员

好处:遵守约定,可以避免不必要的描述,进而大幅减少开发人员的工作量;组织的约定是神圣不容侵犯的,开发人员遵守统一的约定,更容易开发出风格统一的项目。坏处:开发人员需要记忆约定,这无疑是一种负担;另外,约定就意味着限制,牺牲了一定灵活性。

特性

  • 约定优先的请求映射、查询参数映射表单参数映射
  • HTTP动词映射:GET/POST/DELETE/PATCH/PUT等方法名前缀映射
  • 使用但不滥用注解:只在必要的情况下提供注解(如方法限定、自定义路由、Body映射等)
  • RequestBody反序列化:只需使用@RequestBody注解
  • MVC/WebAPI支持:模板引擎支持Thymeleaf,也可返回JSON
  • 具体的返回值类型:语义精确,避免手动指定ResponseBody或ContentType
  • 自动返回值包装:可直接返回Java对象,由框架自动序列化为JSON响应
  • 丰富的响应类型:由基控制器提供响应方法(view、html、json、redirect等)
  • 全局错误拦截:统一方便的自定义错误处理
  • 静态Web服务:用于托管图片、JS、CSS、HTML页面等静态资源,支持数百种类型的MIME
  • Session、Cookie
  • 自动的URL、Content编解码
  • Quick Json:提供快速构建JSON的API
  • 请求前置/后置处理:可在方法调用前后执行特定代码,以实现过滤、统一处理等功能
  • 中间件功能扩展:支持用户自定义中间件以扩展框架功能

入门教程

引入JerryFramework

使用Maven引入。为了快速实现参数映射功能,框架使用了JDK8中通过反射获取方法实际参数名的特性,因此,您必须使用JDK8(或更高版本)并在编译时添加-parameters参数。

<dependency>
  <groupId>com.rainng</groupId>
  <artifactId>jerryframework</artifactId>
  <version>1.0-SNAPSHOT</version>
</dependency>

Hello World

引入框架后,我们来创建第一个Web服务,新建一个App类,包含标准的main入口,再新建一个ApiController控制器类。为了演示方便,两个类放在同一个文件。

JerryBuilder构造了一个包含MVC的Web服务。它使用了默认的9615端口,并启用了错误处理、Session、静态Web(wwwroot目录)、MVC中间件,使用start方法启动Web服务。

ApiController控制器继承自Controller(所有控制器都必须继承自它),包含了一个hello方法,返回String值。

public class App {
    public static void main(String[] args) {
        JerryBuilder.createMvc(App.class).start();
    }
}

class ApiController extends Controller {
    public String hello() {
        return "Hello JerryFramework";
    }
}

现在,启动程序,在浏览器中输入地址http://localhost:9615/api/hello,浏览器会显示如下内容

“Hello JerryFramework”

这就是我们在Api控制器的hello方法中返回的值,神奇的是,不同于Spring RestController,无需指定@RequestMapping("/api/hello"),框架会自动扫描控制器中的方法,并映射请求,这就是约定优先的请求映射,可以避免大量且没有必要的映射注解。当然您也可以指定路由,这会在下面讲到。

自动参数映射

我们来向Api控制器中添加一个add方法,随后访问http://localhost:9615/api/add?a=1&b=2.1,这次,浏览器会显示

3.1

public Double add(Integer a, Double b) {
    return a + b;
}

可以看出,框架将请求中的a、b参数自动映射到了add方法的a、b参数,并且正确地识别了对应类型。此外,所有方法支持重载,框架会自动根据请求参数映射对应的方法。

渲染视图

Jerry MVC是一个侵入式的Web框架,因此你可以直接利用基类提供的putModel方法向框架传递模型,使用view方法向框架传递要渲染的视图。而无需像Spring MVC那样手动注入Model或者ModelAndView。编写视图并没有什么两样,按照Thymeleaf模板引擎的语法即可。访问http://localhost:9615/api/mvc,这次,浏览器会显示

Jerry MVC with Thymeleaf

public Result mvc() {
    putModel("key", "Jerry MVC with Thymeleaf");
    return view("index.html");
}
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Jerry MVC</title>
</head>

<body>
<span th:text="${key}"></span>
</body>
</html>

返回JSON对象

默认情况下,你的方法可以返回任何对象实例或基本数据类型,如果你返回的是对象实例,框架就会自动地序列化实例并返回JSON响应。

我们添加一个Student类,getter和setter方法已省略,它拥有idname两个字段。

class Student {
    private Integer id;
    private String name;
}

我们向Api控制器中添加一个getStudent方法

public Student getStudent() {
    return new Student(12345, "小明");
}

访问http://localhost:9615/api/getstudent,浏览器会显示序列化后的Student。

{“id”:12345,”name”:”小明”}

使用Quick Json

Quick Json是框架提供的用于快速构建JSON实例的API。如下,不再需要编写Student类,只需要提供字段名和字段值即可生成JSON响应。

public IResult getStudent2() {
    return json("id|name", 12345, "小明");
}
格式:json(使用|隔开的所有字段名, 字段1的值, 字段2的值, 字段3的值)

嵌套JSON对象也是可以的,使用jsono,如下

public IResult getStudent3() {
    return json("id|name|info", 12345, "小明", jsono("grade|age", "二年级", "八岁"));
}

访问http://localhost:9615/api/getstudent3,浏览器会显示如下JSON

{“name”:”小明”,”id”:12345,”info”:{“grade”:”二年级”,”age”:”八岁”}}

路由

框架提供了简单的路由机制,支持约定路由或使用@Route注解指定路由,默认支持全部HTTP方法,可以使用@HttpGet/@HttpPost等注解限制请求方法

1) 约定路由

如果不指定任何@Route注解,框架将按照如下规则映射请求。注意:路由是可以由类继承关系继承的,也就是说,子控制器类会继承父控制器类的路由路径。

默认映射规则:基控制器的路由路径/控制器名(去除末尾的Controller)/方法名

2) 指定路由

使用@Route注解指定路由,@Route注解可以修饰类和方法,支持相对路径绝对路径两种模式

相对路径:Route("api/v1")
绝对路径:Route("/api/v1")
它们的区别,相对路径会继承父类的路由,而绝对路径会从/截断继承关系。
例如BaseController的路由为Route("/base")
AController的路由为Route("api/v1")
BController的路由为Route("/api/v1")
那么A的路由为/base/api/v1B的路由为/api/v1

请求Body反序列化

使用POST方法发送一个对象,Web层接收并反序列化,这是非常常见的场景。Jerry MVC提供了类似Spring MVC的处理形式,只需要添加一个@RequestBody注解,框架会自动完成Body的映射与反序列化

public Student requestBody(@RequestBody Student student) { 
    student.name = "Azure99";
    return student;
}

请求方法限定

如果你想限制一个方法只响应GET或POST亦或是其他方法,那么可以使用@HttpGet/@HttpPost/@HttpDelete/@HttpPatch/@HttpPut等注解,非限定方法将返回404

@HttpGet
public String getHello() {
    return "getHello";
}

@HttpPost
public String postHello() {
    return "postHello";
}

HTTP动词映射

许多时候,我们的一个Web层组件可能会接受增、删、查、改、列表等请求,例如

  • GET /user 获取用户实体
  • POST /user 创建用户实体
  • DELETE /user 删除用户实体
  • PUT/PATCH /user 更新用户实体
  • GET /user/list 获取用户列表

JerryMVC提供了HTTP方法名前缀映射特性,只需要在控制器上添加@HttpMethodMapping,即可对控制器开启此特性。开启后,以get/post/delete/put/patch开头的方法路径会自动去掉HTTP方法前缀,同时限定HTTP方法

例如:User控制器的get()方法会被映射到/user,只能使用GET方法访问;post()方法依旧会被映射到/user,只能使用POST方法访问;而getList会被映射到/user/list(即去掉get),只能使用GET方法访问

@HttpMethodMapping
class UserController extends Controller {
    // GET    /user
    public String get() {
        return "get";
    }

    // POST   /user
    public String post(String data) {
        return "post: " + data;
    }

    // DELETE /user
    public String delete() {
        return "delete";
    }

    // PATCH  /user
    public String patch(String data) {
        return "patch: " + data;
    }

    // GET /user/list
    public String[] getList() {
        return new String[]{"1", "2", "3"};
    }
}

Session/Cookie

在Web开发中,Session用于服务端保存信息,Cookie用于客户端保存信息。框架提供了多种方法操作Session和cookie,这里介绍两个最简单的例子:使用一组get/set/contains方法。它们记录客户访问服务器的时间

public Object session() {
    if(!containsSession("time")) {
        setSession("time", new Date().toString());
    }
    return getSession("time");
}

public Object cookie() {
    if(!containsCookie("time")) {
        setCookie("time", new Cookie("time", new Date().toString()));
    }
    return getCookie("time").getValue();
}

综合演示

/**
 * 自动映射: /demo/hello、/demo/add ...
 */
class DemoController extends Controller {
    // 返回一个视图, 可通过putModel来传递数据
    public Result hello() {
        putModel("key", "Jerry MVC with thymeleaf");
        return view("index.html");
    }

    // 请求参数映射
    public Double add(Integer a, Double b) {
        return a + b;
    }

    // 自动将返回的Student实例序列化为JSON
    public Student json() {
        return new Student();
    }

    // 快速构建JSON的API, 字段使用|分隔, 后面接n个参数为n字段赋值
    public Result quickJson() {
        return json("id|name", 1, "Azure99");
    }

    // 快速构建复杂JSON的API, 使用jsono方法来嵌套一个Json对象
    public Result nestJson() {
        return json("id|name|info", 1, "Azure99", jsono(
                "birthday|friends",
                new Date(), new String[]{"A", "B", "C", "D"}));
    }

    // 根据类型自动反序列化Body并映射到student参数
    public Result requestBody(String message, @RequestBody Student student) {
        return json("message|student", message, student);
    }

    // 相对路径的路由指定, 会继承父亲的路径
    @Route("route")
    public String relativeRoute() {
        return "My path is /demo/route";
    }

    // 绝对路径的路由指定, 不会继承父亲的路径
    @Route("/route2")
    public String absoluteRoute() {
        return "My path is /route2";
    }

    // 限定请求方法为GET
    @HttpGet
    public String get() {
        return "Http GET only";
    }

    // 返回302重定向
    public Result redirect() {
        return redirect("https://www.baidu.com");
    }

    // 返回一段Html
    public Result html() {
        return html("<h1>Html</h1>");
    }

    // 使用Cookie, 可通过setCookie设置
    public Object cookie() {
        return getCookie("foo").getValue();
    }

    // 使用Session, 可通过setSession设置
    public Object session() {
        return getSession("datetime");
    }
}

/**
 * 自动映射:
 * GET/POST/DELETE/PATCH /user
 * GET /user/list
 */
@HttpMethodMapping
class UserController extends Controller {
    public String get() {
        return "get";
    }

    public String post(String data) {
        return "post: " + data;
    }

    public String delete() {
        return "delete";
    }

    public String patch(String data) {
        return "patch: " + data;
    }

    public String[] getList() {
        return new String[]{"1", "2", "3"};
    }
}

小结

本文简单介绍了JerryFramework的功能与设计思想,并给出了最基本功能的样例。但这只是冰山一角,框架的更多细节以及实例将在未来分享,例如:

  • 静态Web服务
  • 全局错误处理
  • 中间件扩展(实现授权认证等功能)
  • 请求/响应过滤
  • 控制器继承
  • 整合Spring

咕咕咕~

Azure99

底层码农,休闲音游玩家,偶尔写写代码

看看这些?

6 条评论

  1. 布丁布丁布说道:

    看起来很像jfinal和palyframework的结合体,吸收了二者的优点,爱了爱了!不知道博主还有开发计划吗

  2. uuz说道:

    大二就这么牛了

  3. qiaoguangtong说道:

    太强大了,学习榜样

  4. 小伍说道:

    %%%

  5. coder说道:

    %%%

  6. xlc说道:

    为了课设单独写一个框架
    这就是人和人的差距吗

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注