Ch2 中间件 or Controller

(题前话:我本来一直以为这东西都叫controller的,结果被徐同学打脸表示在Node 的Express框架里统称中间件,所以这里就叫Controller好了)

什么是Controller

在上一话中,我们使用了简单的“hello world”作为响应,主要是测试我们的基础框架是否可用。但是,作为一个功能齐全的框架,我们怎么可能对所有的请求都返回”Hello world”呢。

所以,这里我们需要Controller出场了。

从基础概念上来说,Controller就是对 request 与response进行处理的模块。

在MVC中,M为data modeling,即与数据层通讯/抽象/封装,以及算法实现等的模块。V是View,就是设计师设计出的页面模板。C就是Controller,负责转发,处理请求。

Glance at Controller

首先,为了方便Lambda的拓展,以及未来框架的拓展,我们定义一个接口。

[code lang=”java”]
@FunctionalInterface
public interface IController {
// main logic

void execute(HttpServletRequest request,
HttpServletResponse response)
throws Exception;
}

[/code]

这个接口被FunctionalInterface标记修饰,而且只有一个方法,这样我们就可以用lambda表达式快速实现一个Controller对象。

With Route

有了Controller,下一个需要的就是Route。Route的作用,就是将用户请求的URL,如实的映射到预设好的Controller上。

所以,我们需要定义一个 Route接口,用来将URL与Controller结合在一起。

[code lang=”java”]

public interface Router {

void addController(String url,
IController controller,
String method)
throws Exception ;

IController route(HttpServletRequest request)
throws Exception;

List<String> routeRules();

}

[/code]

一般来说,URL 的格式类似于: [protocal name]://[hostname]/[path]/[to]/[resource]/{parameter}?{other_parameter}

在我的实现里,我利用了正则来做匹配。并保留{}圈住的内容作为传入参数。

[code lang=”java”]
private static Pattern variablesPattern =
Pattern.compile("\\$\\{(\\w+)\\}");
[/code]

这个语法就是用Java的正则库将大括号包住的变量“抽”出来处理。

匹配的代码如下:

[code lang=”java”]
@Override
public boolean match(HttpServletRequest request) {
Matcher matcher = pattern.matcher(request.getRequestURI());
if (matcher.matches()){
if (pathVariables != null && pathVariables.size() > 0){
for (int i = 0; i < matcher.groupCount(); ++i){
request.setAttribute(pathVariables.get(i), matcher.group(i + 1));
}
}
return true;
}
return false;
}
[/code]

(全部代码请翻阅 https://github.com/andysim3d/JxpressTutorial/tree/ch2 ,限于篇幅这里只有部分代码)

Bind

创建一个类,存放router 与 controller 实例各一。

[code lang=”java”]
public class MatchAndController {

private final UrlMatcher matcher;
private final IController controller;

public MatchAndController(UrlMatcher matcher, IController controller)
{ this.controller = controller
; this.matcher = matcher;
}

public UrlMatcher getMatcher(){
return matcher;
}

public IController getController(){
return controller;
}

}
[/code]

同时修改的还有JettyServer里面的list,之前我们存放的是静态url,现在,需要存放的是MatchAndController。

[code lang=”java”]
public class JettyServer implements WebServer {
//…
//change here
protected UrlRouter urlRouter = new UrlRout

//change here
public WebServer get(String url, IController ctl) {
try {
urlRouter.addController(url,ctl,"get");
return this;
}
catch (Exception exp){
// do nothing
}finally {
return this;
}
}

public WebServer start() {
//…
//change here
Handler hdlr = new WebServerHandler(urlRouter);
//…
}

private static class WebServerHandler extends AbstractHandler {
// change here
private UrlRouter urlRouter;
public WebServerHandler(UrlRouter urlRouter){
this.urlRouter = urlRouter;
}
public void handle(String s, Request request,
HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse)
throws IOException, ServletException {
try{
process(httpServletRequest, httpServletResponse);
}
catch (Exception e){
e.printStackTrace();
}
}

private void process(HttpServletRequest request,
HttpServletResponse response)
throws IOException {
try {
urlRouter.route(request).
execute(request, response);;
}
catch(Exception e){
}
}
}
}
[/code]

Test

将入口方法写成如下形式:

[code lang=”java”]
public class Main {

public static void main(String[] args) {

WebServer server = new JettyServer();
server.get("/", (req, res)->{
OutputStreamWriter optwriter = new OutputStreamWriter(res.getOutputStream(), "utf-8");
optwriter.write("Hello You!");
res.setStatus(200);
optwriter.flush();
}).get("/test", (req, res)->{
OutputStreamWriter optwriter = new OutputStreamWriter(res.getOutputStream(), "utf-8");
optwriter.write("Hello There!");
res.setStatus(200);
optwriter.flush();
}).
listen(8080);
server.start();
}
}
[/code]

然后用浏览器分别访问 localhost:8080/ 与 localhost:8080/test

大功告成!

What did we do

简单来说,我们定义了一个叫做IController的接口,这个接口负责接收请求,并返回响应。

为了保证对应的请求能被正确的IController实例处理,我们又定义了一个Route的接口,它负责匹配定义好的路由与请求。

接下来,我们将IController与Route对象一对一绑定在一起,放在容器中(因为我们会有多个Route+IController)。在有请求进来的时候,我们遍历所有Route+IController对象,并试图匹配。(所以如果有多个可匹配项,那优先匹配第一个定义的)。若找到匹配项,则利用对应的 IController来处理请求。

And … then?

下一次教程,我们将要开始搞定中间件与异常处理这两个模块。

[教程] ch1 Web界的Hello World

Ch0中,我们大概讲述了需要做一个怎样的东西。那么,从这篇开始,我们就着手搭建这样一个框架。

创建工程与解决依赖

所谓万事开头难。我们先定一个小目标:跑起来一个空的框架,这样再往里面添加东西就方便很多,也更直观一些。

首先,创建一个空的Java应用(这里我用了maven管理,习惯性上我是先建立项目再添加maven管理,你也可以直接创建maven项目)。我叫它Jxpress。这里我用的IDE是IntellJ。当然eclipse或者其他的IDE也可以。

有了一个新的工程之后,我们需要一个底层连接的库。这里我选用Jetty。

在pom.xml里面添加jetty的依赖。

<dependencies>
    <dependency>
        <groupId>org.eclipse.jetty</groupId>
        <artifactId>jetty-server</artifactId>
        <version>9.3.11.v20160721</version>
    </dependency>

    <dependency>
        <groupId>org.eclipse.jetty</groupId>
        <artifactId>jetty-util-ajax</artifactId>
        <version>9.3.11.v20160721</version>
    </dependency>
</dependencies>

然后选择maven -> download source,jetty就被包含在项目里啦。

第一行代码

处理好了整个工程依赖后,我们可以开始写代码了。

首先,我们的整个项目是要做一个webserver。那么,我们先来定义一下我们需要哪些方法。

listen:监听方法。这个方法告诉我们该监听哪个端口。

start/stop:开始,结束。这个方法用于启动/结束我们的server。

get:注册一个get方法,来处理get请求。

于是我们就有了如下的代码:

[code lang=”java”]
public interface WebServer {
WebServer listen(int port);
WebServer get(String url);
WebServer stop();
WebServer start();
}
[/code]

如果你好奇为什么我的方法返回类型都是WebServer,答案是:这样我可以通过多个get方式来注册不同的处理方法。现在我们还不处理controller(在不久的将来我们会处理的),而是用一个固定的方法来处理它。

有了这个接口后,我们就可以实现它了!下面我们用JettyServer来实现这个接口。

[code lang=”java”]
public class JettyServer implements WebServer {
private Server server;

protected int port = 80;
//用一个list记录所有注册的get方法的URL
List<String> getURL = new ArrayList<String>();

public WebServer listen(int port) {
this.port = port;
return null;
}

//每调用一次get,就在list里添加一个url
public WebServer get(String url) {
getURL.add(url);
return this;
}

public WebServer stop() {
// 如果server对象已经存在
if ( server != null){
try{
server.stop();
}
catch (Exception e){
// do nothing
}
}
return this;
}

public WebServer start() {
server = new Server(this.port);
try{
server.start();
}
catch (Exception e){
e.printStackTrace();
}
return this;
}
}
[/code]

 

敲完以上代码后,我们建立一个Main.java文件。

[code lang=”java”]
public class Main {

public static void main(String[] args) {
WebServer server = new JettyServer();
server.get("/").get("/introduce").listen(8081);
server.start();
}
}
[/code]

这样运行后,我们会看到如下图:

添加处理器

小伙伴们好奇了:我们不是添加了 “/”应该显示hello world么?

不错,逻辑上来说是这样没错,但是事实上,我们需要将我们的逻辑放在处理器里,并将JettyServer的处理器设为我们的处理器才行。

创建一个class 叫WebServerHandler,让它实现AbstractHandler.

[code lang=”java”]
private static class WebServerHandler extends AbstractHandler {

private List&lt;String&gt; getURLs;
public WebServerHandler(List getURLs){
this.getURLs = getURLs;
}
public void handle(String s, Request request,
HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse)
throws IOException, ServletException {
try{
process(s, httpServletRequest, httpServletResponse);
}
catch (Exception e){
e.printStackTrace();
}
}

private void process(String s,
HttpServletRequest request,
HttpServletResponse response)
throws IOException {
for (String each : getURLs){
// 如果请求的url 与任意一个get注册的url 相等
if (each.equalsIgnoreCase(s)){
// 在响应里输出hello world,并把状态字设为200(OK)
OutputStreamWriter optwriter = new OutputStreamWriter(
response.getOutputStream(),
"utf-8");
optwriter.write("Hello world!");
response.setStatus(200);
optwriter.flush();
}
}
}
}
[/code]

然后将start方法改成下面的样子:

[code lang=”java”]
public WebServer start() {
server = new Server(this.port);
// 生成处理器
Handler hdlr = new WebServerHandler(getURL);
// 将我们的处理器设为Jetty的处理器
server.setHandler(hdlr);
try{
server.start();
}
catch (Exception e){
e.printStackTrace();
}
return this;
}
[/code]

Hello World!

再次启动我们的服务器,然后访问一下:

Bravo!


本文完整代码在Github上有收录,链接为:https://github.com/andysim3d/JxpressTutorial 

 

[教程] ch0 一个简单的web框架

Web是何如工作的

在讨论框架之前,我们需要先了解一下web是如何工作的。在浏览器里输入”https://andysim.us”并回车后,你会看到我的个人博客主页。在这一个简单的 “回车——页面”的背后,就是一个Web工作循环(这里不考虑DNS等)

Request

在你敲下回车的一瞬间,你通过浏览器,向我的服务器(andysim.us)发送了一个请求。

Web App

在我的博客里,我部署了Apache作为服务器,其上运行着Wordpress。Wordpress会接受到你的请求,处理请求后返回响应。

Response

响应会由我的服务器发回到你的电脑,里面包含整个页面的信息。你的浏览器拿到Response信息后,就可以解析,渲染成整个页面了。

Web框架浅谈

通过上面的例子,我想你已经对Web工作有一些了解了。

那么什么是Web框架呢?

我们先来简化一下模型。

根据刚才的例子,整个web应用就是一个接受Request信息,并返回Response的程序。

如下图,蓝色为request,红色为response。

Web 应用模型

如果 Web 应用什么都不干,只返回一个200作为status code(意为正常),这就是一个简单的Web 应用模型。

(这里涉及到一些网络的概念,比如request一般是由Http协议传输,而http大多又基于TCP协议通信等,本文暂不涉及到过于底层的内容)

说了这么多,我们还没有说到什么是web框架:简单来说,如果有一个程序,它负责处理接收Request,返回Response,而你只需要添加处理resquest与生成response的逻辑,这就可以当作一个web框架。

Web框架解决两大问题

围绕web应用的所有问题中,两个问题尤其突出:

  1. 我们如何将动态的URL映射到对应的逻辑?(路由)
  2. 我们如何将静态HTML与动态消息结合?(模板)

而这些就是主流的Web框架需要解决的。

本Web框架简介

本次系列教程将会带领读者从0开始做一个web框架,基于主流的MVC设计模式。

微服务应该有多“微”

https://www.innoq.com/blog/st/2014/11/how-small-should-your-microservice-be/

随着微服务概念的普及,关于“微” 的大小有了很多不同的定义。一个广受开发者支持的定义是,一个微服务应该小到只做一件事。对我个人来说,我并不觉得这是一个有意义的解释—— “一件事”这个概念本身就有不同的理解,它并不能起到有效约束微服务大小的作用。因此,我同样反对那些认为每个独立的服务应该能且仅能实现单一功能的观点。假设,一个方法根据三个输入值来计算输出——你真的认为我们有必要将这个功能抽离成一个微服务并独立部署么?

相反,我认为从另一个角度看待这个问题比较容易获得我们想要的答案。我们举一个例子:一个电子邮件系统。为了尽量简单化这个模型,我们假设它是传统的邮件系统,只有基本特性,如登录、登出,保持用户设置,创建、删除邮件,查看收件箱,创建/修改文件夹,记录通讯录,搜索邮件等。从单体模式的设计角度出发,我们可以用一个应用来实现所有功能。接下来,我们可以用模块化设计的方式将它的功能拆分成模块,比如我们可以用DDD的方式来拆分。当然,我们需要一些其他的依赖来实现某些功能——UI,数据存储,外部搜索系统等。最后,我们可能得到一个六边形或多层结构的单体应用。

所有与这个邮件系统相关的团队都必须紧密联系在一起,一旦应用有任何改变,(几乎)所有人的代码都会受到影响。这种应用被我们成为 All-or-Nothing。并且,我们只能选择运行/不运行整个系统,没法灵活的开启/关闭某些功能。对于某些团队,这样的结构就很好。但,我们假设,你的团队并不满足于这种架构,你们想要将整个应用切分成相对独立,有着自己生命周期的子应用/服务/库。

如何切分?首先,登陆/登出(或者我们成为授权系统)和用户资料这两块可以拆分成单独的服务。并且因为它们在安全上的特殊性,它们也应该被单独考量。邮件和文件夹两者联系很紧密,所以我将它们划分到一个服务(你也可以尝试拆分它们,尽管我个人并不建议)。接下来,如果我们有不同的通讯协议,如web interface, POP3, IMAP, SMAP等, 我会将每个协议的实现部分拆成对应的服务。同样,信息存储,可以被抽离成独立的服务。我们可以将附件与邮件同样作为信息文件存储,这样信息存储就变成了存储服务。带有UI和API的通讯录可以被抽离成一个独立的服务。

最后,我可能将整个邮件系统拆离成15~20个独立服务。对于任意的请求/操作,我们都可以将它映射到若干微服务之上——比如,用户在表格里输入数据之后,点击了一个按钮,数据应该被保存在这个表格里。我们会用3-5个服务组合来处理整个逻辑。

这就是我拆分微服务的思路 —— 按照业务逻辑与迭代频率区分,而非单纯的追求KISS(Keep It Small and Simple).

换言之,我认为将“服务拆分到尽可能小”并不是你的目的。如果你这样做,只能说明你将拆分服务当作了你的首要业务,而忽视了业务之间的交互性。选择更适合自己的模块方式,才是微服务的最佳实践。