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方法已省略,它拥有id
和name
两个字段。
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/v1,B的路由为/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
咕咕咕~
看起来很像jfinal和palyframework的结合体,吸收了二者的优点,爱了爱了!不知道博主还有开发计划吗
大二就这么牛了
太强大了,学习榜样
%%%
%%%
为了课设单独写一个框架
这就是人和人的差距吗