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>
Code language: HTML, XML (xml)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";
}
}
Code language: JavaScript (javascript)现在,启动程序,在浏览器中输入地址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;
}
Code language: PHP (php)可以看出,框架将请求中的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");
}
Code language: PHP (php)
<title>Jerry MVC</title>
<span th:text="${key}"></span>
Code language: HTML, XML (xml)返回JSON对象
默认情况下,你的方法可以返回任何对象实例或基本数据类型,如果你返回的是对象实例,框架就会自动地序列化实例并返回JSON响应。
我们添加一个Student类,getter和setter方法已省略,它拥有id和name两个字段。
class Student {
private Integer id;
private String name;
}
Code language: PHP (php)我们向Api控制器中添加一个getStudent方法。
public Student getStudent() {
return new Student(12345, "小明");
}
Code language: PHP (php)访问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, "小明");
}
Code language: PHP (php)格式:json(使用|隔开的所有字段名, 字段1的值, 字段2的值, 字段3的值)
嵌套JSON对象也是可以的,使用jsono,如下。
public IResult getStudent3() {
return json("id|name|info", 12345, "小明", jsono("grade|age", "二年级", "八岁"));
}
Code language: PHP (php)访问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/v1,B的路由为/api/v1请求Body反序列化
使用POST方法发送一个对象,Web层接收并反序列化,这是非常常见的场景。Jerry MVC提供了类似Spring MVC的处理形式,只需要添加一个@RequestBody注解,框架会自动完成Body的映射与反序列化。
public Student requestBody(@RequestBody Student student) {
student.name = "Azure99";
return student;
}
Code language: PHP (php)请求方法限定
如果你想限制一个方法只响应GET或POST亦或是其他方法,那么可以使用@HttpGet/@HttpPost/@HttpDelete/@HttpPatch/@HttpPut等注解,非限定方法将返回404。
@HttpGet
public String getHello() {
return "getHello";
}
@HttpPost
public String postHello() {
return "postHello";
}
Code language: JavaScript (javascript)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"};
}
}
Code language: JavaScript (javascript)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();
}
Code language: JavaScript (javascript)综合演示
/**
* 自动映射: /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"};
}
}
Code language: JavaScript (javascript)小结
本文简单介绍了JerryFramework的功能与设计思想,并给出了最基本功能的样例。但这只是冰山一角,框架的更多细节以及实例将在未来分享,例如:
- 静态Web服务
- 全局错误处理
- 中间件扩展(实现授权认证等功能)
- 请求/响应过滤
- 控制器继承
- 整合Spring
咕咕咕~

看起来很像jfinal和palyframework的结合体,吸收了二者的优点,爱了爱了!不知道博主还有开发计划吗
大二就这么牛了
太强大了,学习榜样
%%%
%%%
为了课设单独写一个框架
这就是人和人的差距吗