Netty:我的小故事

我的韩国欧巴,让我从 JDK 的混杂的环境中诞生。workaround 过不予修复“陷阱(BUG,BUG..)”默认失败地,跳过不可使用的 IP_TOS 选项,放弃 AIO 这个鸡肋。这一切让我变得如此与众不同,要说特别之处,我行异步,事件驱动之道;内有性能加持,外有呼风唤雨之术法;以三大形态互换,三大法相容相生,三几数笔达万丈之渊。现在的互联网,非复吴下阿蒙,一变不知样。以前尚且可以 CRUDER,现在只能去学习我,然后用上 Spring WebFlux 再与微服务搞上。此外,还得我来搞个前所未有的话术,提提性能冲点业绩,换点银子讨好老婆,养养儿子,爽歪歪(欧巴,我**你) 。在爽之余,要知道有我曾经有孪生兄弟他叫 5阿发,不过早早出家不问世事了(我会继承你的遗志的)。。。
那,我们就从 Echo 与 Http 开始,入门 Netty 吧! 首先,我的韩国欧巴把我放在 https://netty.io 上,如果要见我那你得去下载页面找到我,然后把我配置到 项目 中。不然你用 Maven 帮助你把我从另外一个地方出来帮你也行,别忘了我的版本号是3个点。 二话不说,先来个我需要的几个小家伙的介绍: 如果要搞个自定义协议的服务器端,要知道这几个 Bigman ,大人物 skr,skr,skr。
  • EventLoopGroup:任务分组,最多可以使用两组(是的,你没看错)
  • ServerBootstrap:Server 运作组织类,指挥部
  • SocketChannel:工作频道
  • ChannelHandler/ChannelInboundHandler:提供各种事件处理
ServerBootstrap 需要指定什么呢?
  • group():指定 事件循环 组
  • channel():指定 SocketChannel 类
  • handler:也是指定事件(对于 第一个 EventLoopGroup)
  • childHandler:指定事件处理
  • option():链接选项
  • childOption():链接选项
来看一段代码就明白了:
package com.codimiracle.practice;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;

public class EchoServer {
private int port;

private EchoServer(int port) {
this.port = port;
}

public static void main(String[] args) {
EchoServer server = new EchoServer(3400);
server.start();
}

private void start() {
NioEventLoopGroup majorEventLoopGroup = new NioEventLoopGroup();
NioEventLoopGroup seniorEventLoopGroup = new NioEventLoopGroup();
ServerBootstrap serverBootstrap = new ServerBootstrap();
try {
serverBootstrap
.group(majorEventLoopGroup, seniorEventLoopGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline()
.addLast(new LoggingHandler(LogLevel.INFO))
.addLast(new EchoService());
}
})
.option(ChannelOption.SO_BACKLOG, 128)
.childOption(ChannelOption.SO_KEEPALIVE, true);
ChannelFuture channelFuture = serverBootstrap.bind(this.port).sync();
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
majorEventLoopGroup.shutdownGracefully();
seniorEventLoopGroup.shutdownGracefully();
}
}
}
Handler 代码:
package com.codimiracle.practice;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

public class EchoService extends ChannelInboundHandlerAdapter {

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// 内部缓存写入
ctx.write(msg);
// 把缓存发出
ctx.flush();
}

@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
  • EventLoopGroup:任务分组,最多一个
  • Bootstrap:连长
  • SocketChannel:是的还是它
  • ChannelHandler/ChannelInboundHandler:没错,也是它
如果要搞个自定义的客户端,需要知道这几个 小东西。 就换了指挥的人。
package com.codimiracle.practice;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;

public class EchoClient {
private String host;
private int port;

public EchoClient(String host, int port) {
this.host = host;
this.port = port;
}
public void connect() {
NioEventLoopGroup clientEventLoopGroup = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap
.group(clientEventLoopGroup)
.channel(NioSocketChannel.class)
.option(ChannelOption.SO_KEEPALIVE, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline()
.addLast(new LoggingHandler(LogLevel.INFO))
.addLast(new EchoServiceProxy());
}
});
ChannelFuture channelFuture = bootstrap.connect(host, port).sync();
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
clientEventLoopGroup.shutdownGracefully();
}
}
public static void main(String[] args) {
EchoClient client = new EchoClient("localhost", 3400);
client.connect();
}
}
Client Handler 代码:
package com.codimiracle.practice;

import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

public class EchoServiceProxy extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(Unpooled.wrappedBuffer("Hello world\n".getBytes()));
}
}

我的三个模式

  • 排队打饭(排队模式 OIO/BIO,后来放弃了)
  • 点单自领(NIO模式)
  • 包厢等吃(AIO模式,后来移除了)
要更换模式,只需要 把 EventLoopGroup,ServerSocketChannel 的对象换成对应前缀的对象即可(如 OioEventLoopGroup,OioSocketChannel/OioServerSocketChannel) NIO 实现: EventLoopGroup:如果通过源码追溯进去,实际上是一个特殊化的 ExecutorService(线程池),特殊在那里呢? EventLoop:看一看代码,可以发现 EventLoop 处理了选择器当前(selectNow() 方法)的选择策略 SocketChannel:对应的事件频道

我的三种 Reactor

Reactor更像是一种事件的派发机制,但这些派发机制与响应的 IO 模式相关联。
  • ThreadPreConnection:使用 new NioEventLoopGroup(1) 创建,即可使用单 Reactor 单线程模式
  • Reactor:使用 new NioEventLoopGroup() 创建,即可使用单 Reactor 多线程模式
  • Proactor:使用两个 NioEventLoopGroup() 对象,即可创建主从 Reactor 多线程模式

Spring MVC:DispatcherServlet

在 Spring MVC 中,有一个类是必定要去了解和熟悉的,这个类就是 DispatcherServlet,该类负责了 MVC 中最主要的内容——请求转发处理。 在本文中,我们会在源码级别讲述 DispatcherServlet 的运行机制和高度灵活的扩展性。

DispatcherServlet 的处理过程

DispatcherServlet 的处理过程更像是一个循环执行接口程序的过程,只是有几个相对独立的部分。如果使用 IDE 进行断点你会发现它的流程如下:
  1. doService
    1. 解析请求属性
  2. doDispatch
    1. getHandler( 请求到 Handler 对象映射 )
    2. getHanderAdapter
    3. preHandle
    4. handle
    5. postHandle
    6. processDispatchResult
      1. 完成异常解析
      2. render
        1. 区域处理
        2. 视图名解析
        3. 视图呈现
    7. afterCompletion

DispatcherServlet 的功能

DispatcherServlet 运行过程有以下几个大的功能板块:
  • 基于 JavaBeans 的方式进行配置
  • 控制 Handler 对象路由:HandlerMapping
  • Handler 适配处理:HandlerAdapter
  • 异常处理策略
  • 视图解析策略
  • 请求视图名转译
  • 复合表单内容处理
  • 区域解析
  • 主题解析
Java Beans 方式配置是 Java EE 中的一个规范,其定义 Java Beans 支持属性,事件和方法。如果你对 Java Beans 还不够熟悉可以查看 Oracle 对 Java Beans 是怎样编写的来了解具体的信息
HandlerMapping 负责了请求的路由,也就是说在接收到请求后,请求会经过它的处理最后返回 Handler 对象(可以是任意的类型)。其运行机制由两个组件组成:HandlerInterceptor 和 HandlerExecutionChain。HandlerInterceptor 有三个主要方法:preHandle(),postHandle(),afterCompletion()。
  • preHandle() 方法在 HandlerAdapter 发起调用 Handler 时触发。如果返回 true 表示会处理下一个 Interceptor,否则会认为以及处理完请求。
  • postHandle() 方法在 HandlerAdapter 调用 Handler 并且在 呈现视图 之前调用
  • afterCompletion() 仅在 preHandle() 方法返回 true 并且呈现视图之后执行
HandlerExcutionChain 类真正地组织 HandlerInterceptor 和 Handler 对象。并且真正在调用preHandle(),postHandle(),afterCompletion()。最后这个对象在 HandlerMapping 中 getHandler() 进行返回。
HandlerAdapter (适配器模式)负责完成 Handler 的实际处理。在 HandlerMapping 的处理结果的基础上,HandlerAdapter 表达了如何运作 Handler 对象,如 SimpleControllerHandlerAdapter 是针对 Controller 接口调用 Handler 对象的 handleRequest() 方法完成处理的。
异常处理由 HandlerExceptionResolver 负责,在流程 3-7 步中,如果抛出了异常会经由 processDispatchResult() 进行判断最后交由 processHandlerException() 进行检查异常解析结果,循环判断(最后一个
HandlerExceptionResolver 为第一个判断)有一个可以处理(判断 ModelAndView 返回值不为空)那么就会认为处理完成并把 ModelAndVIew 进行返回,间接使用 视图解析 的执行路径 。
视图解析,由 ViewResolver 负责,ViewResolver 会通过 viewName 获取到相应的 View 对象最后完成 ModelAndView 的呈现工作。与上面的做法先类似,都是使用循环的方式进行解析,一旦解析成功直接返回。 processDispatchResult() 负责调用 render() 方法呈现相应的页面结果。在 render() 方法中,地区解析是在这里完成的,它可以使用 Accept-Language 请求头来给 response 对象设置区域(Content-Language)响应头。最后通过 resolveViewName 获取到交由 View 对象进行实际的呈现工作。
请求视图名转译,通过请求获取相应的视图名,最后经过 ViewResolver 解析出相应的 View 对象,默认的转译类(DefaultRequestToViewNameTranslator)是把请求路径和资源名返回,如 http://localhost/a/b/c.html 将会返回 a/b/c

DispatchServlet 应用

  1. 自定义 HandlerIntercepter ,可以完成与 Handler 进行绑定的拦截处理,而不需要 AOP 编程
  2. 自定义 HandlerAdapter,可以实现自定义的 Handler 如实现一套不同的控制器处理逻辑
  3. 自定义 ViewResovler 视图解析,实现定制的 视图 呈现方式
  4. 自定义 HandlerExceptionResolver 异常处理,

毕业回顾

回想已经经过的时间,总是让人着迷和反思。每个选择的节点依然清晰无比,却又那么的扎心。每个人或许都觉得之前能够选择得更好,不过我至少现在的我看来这一切都是固定的。现在的你觉得之前可以做得更好是因为你得到了成长,那么接下来会是什么等待自己,重蹈覆辙还是踏出与之前完全不同的一步呢?
自出生尔来,每个人的际遇不同从而塑造了不一样的人格和性格。对于事物和道理的意识到和行动到,两者之间永远有差距。我于之前环境和周遭所赋予我的,暂且只是记录和要求自己改变,受现实的曲折,行赤子之心,结果云云。改变和成长是多么有趣的词语,让人生的体验更为完好,想改变吗?想,如同孤独形影相随。《那年那兔那些事儿》告诉我,理想从现在开始就会早一步实现。那么如何改变?以同理之心,上善若水,心诚而明理,不妄自菲薄,亦不增长自负之心。《认知的尺度》引用“最难的事情是认清自己”,告诉我了解了自己是最有意思的事情啊。 一时迷失,一时失去,一时怨恨,患得患失。这样的人又如何担起大任来呢?却也承受精神之涣散,肉体之酸痛,明力之未逮,失道以无能。矛盾啊,我本是平凡之人,却强迫自己如神仙下凡一般,动指而移山,挥手而洒雨,飞天而畅游,遁地如游戏。其种种,不如相信积累的力量,要说结果,总得畜够结果的养分啊。

计划篇

回顾所学,自我 Debug 之余,我想一个败笔就是没有好好地把写过的东西进行汇总和整理。现我有一点空余,不如整理、反思和改进。maven的 jira 账号已经注册了太久了是时候发起 issue 了。

Java Web 整理

概念: contract:指以特定的对象来实现特定的重复,这个重复具备稳定的约束。 包: basic-contract:基础约束,这个约束处理的是 controller 中一些重复使用的部分。(即将发布 Jar 包到中央仓库)
mybatis-contract:mybatis 约束,定制了自己常用的 关于 mybatis 的约束,如分页器的复用,Mapper 一些常常出现的方法。
content-middleware:企图把 内容作为一个单独的模块抽象出来。

Spring Boot:AbstractMethodError

场景

这个异常由虚拟机在运行时抛出。其原因是程序调用了抽象方法,这说明有一些方法没有被实现。在 Java 编译器编译时,会告知类的抽象方法没有实现。一些可能的情况如下:
  • 使用了版本不正确的包(新版本提供了额外的抽象方法)

实际例子

在使用 Jadira Usertype Core 时,在 Spring Boot 2.1.x 与 JPA、Jadira 3.2.0.GA、Joda Money 0.10.0 配合下,出现了 AbstractMethodError 。

解决过程

开始发现这个问题后,我呆滞了半响。花费很多的时间寻求问题无果后,实施排除法。 在排除之前,我先使用官方的 JPA 例子运行一个 JPA 程序,结果发现这个例子能够正确地运行。这说明Spring Boot JPA 本身没有什么问题。加入 自行编写部分 如实体类与 Repository 以及 POM 依赖,执行出现 AbstractMethodError 异常。 逐步减少自行编写部分,在减少 POM 中依赖时发现这个问题发生的原因:Jadira Usertype 使用了不正确的版本。更换更新的的版本后解决了问题。

参考

Oracle Java SE Docs:
https://docs.oracle.com/javase/8/docs/api/java/lang/AbstractMethodError.html

给予帮助何须问这里问那里

纠结之人莫纠结。无意义的纠结不会给你带来任何有意义的结果。

《我给自己写的名言》——codimiralce

为啥要问来问去?

问来问去,对于一些重要的决策而言,的确是需要的。但对于举手之劳而言,又那里需要问来问去,就像慈善机构募捐赈灾过程中,不断问捐款的人会不会捐款一样,多此一举,毫无必要,人家都来捐款了还要你问捐不捐款。。。。这样问来问去不过是对能够判断的事情极度不自信,极度消极,不清楚自己的位置,既毫无作为,又毫无担当的做法而已,总是在心里问我这么做真的好吗?说得有多么善解人意,多么征求他人的见解。实际上,这不过是另外一种披上了“我征求过别人同意才做,大家都好。”的华丽的个人正确的外衣的逃避罢了。

我该这么做吗?

在你这么想的时候,可能你已经想着要做了。如果公正客观来说的确是好事不妨去做啊?想什么乱七八糟的事情,八竿子打不着。这时候就应该问自己这么做会给自己和他人带来痛苦吗?想做的事情多数并不会给自己和他人带来任何痛苦,不过是有抵触心里而已。这比起精神上的痛苦有什么可比的呢?倘若你没做这件事情,你会感到无力的痛苦,感到无动于衷的失败感,落魄感。那你还不如做了这件事情。至少这让你减少消极感。

这么做好吗?

好个**,先回到开头,问这个不过是重蹈覆辙。事实上,自己无法确定,但可以确定的是——每个人做一件事是通过别人评价来确定做的好不好的。但这样是否是好呢?不该是听从内心吗?自己的内心告诉你,你可以这么做,为何要呆呆傻傻的坐着站着不动,仅仅是怕引发后续的事情,达不到别人的期盼吗?到这里还是傻傻的,不愿意做,不会做,拒绝不就好了吗?搞什么,拒绝多不好意思,毕竟是认识的,哪里帮助过自己。这还是傻,你都没办法做,不拒绝干啥子飞机呀,这只会给别人添麻烦而已嘛。反观过来,如果这件事情对于自己,对于他人没有什么坏处,为什么不做呢?不知道有什么值得纠结来纠结去的,为了征求他人同意你这么做吗?这么做除了不愿承担结果还有什么。不要给自己找“我只是想要确定好,再做而已,这样不会有问题。”的借口,明明错误只能避免,不管这么个修补法都会有不可磨灭的印记。

不要问那么多

不要问那么多,问自己会不会因此痛苦。许多的事情根本不需要问这问那,往往迈开步伐,行动就好。2019/9/17 这天的珠海,傍晚的时候下了一场雨,我的两位朋友在食堂等着雨停。在我回到宿舍后,我问她们需不需送伞,在日常生活中的这明明不过是常见的举手之劳而已。可能是觉得我刚刚回到宿舍不好让我送伞,也许是当时雨水也蛮大的所以也就说先等雨变小。 结果雨水变小,我却没有去送伞,让一位好友淋雨去拿伞,自己心情也不好。何必呢?今日看来,回想错过友人的生日,我却也这么问需不需补偿,作为朋友当然会不好意思呀。今日之言,莫忘,谨记。

第一期 编程我建议你定好概念

为什么要注意概念

编程本身是一个思维活动过程,思维的清晰程度体现在每个编程中创建或主观确定的概念上,思路清晰要求我们确立的概念是清楚明了的。每一个概念都是必须清楚而明确的,如果没有明确的概念,就无法让代码产生真正的力量。知识的作用在于其清楚明白,而不在于其“熵增”。在不断的概念定义后,软件便慢慢的集中了庞大的知识体系,这个知识体系慢慢地成为领域语言。这个领域语言的完善程度越高,意味着软件实现越稳定。邻域语言的合理程度越高,意味着软件实现越正确。 这些概念的作用体现在哪里呢?首先,顶层概念决定了编码的方式。例如,在使用 MVC 模式时,编码的方式就变成了先写一下 V,再写一下 C,最后再写一下M。实际上MVC实质上与分层区别不了多少,分层会引入相应的层次概念。例如,View 层的概念:视图,负责页面展示与数据输入。Controller 层:控制器层,负责数据处理与数据流转。Model:模型层,保存数据,提供数据。其次,中层概念决定了业务编码。例如,对于消息业务,我们大可能使用发布订阅模式,这样我们会引入主题,订阅者,消息这三个概念。再次,底层概念决定了数据流转。例如,如果你有使用消息队列,你可能会把消息队列作为一个桥梁,用以与其它应用程序进行通信、协作,这样相比与其它进程通信如管道,共享内存而言更加灵活。在软件世界,概念无处不在。

如何确立一个概念

通常我们会在编程中直接引入概念,例如:我们定义这个类完成文件操作。那么,什么是“文件操作”?可能的一种情况是我们在进行 Java IO 流的操作,从而定义了文件操作是指Java 中 IO流的操作。由此我们得到一个定义概念的一种形式:<什么概念>是指<什么地方>的<什么处理>。这个形式界定了概念的名称,概念有效的范围,概念的作用,这样可以让这个概念明确起来。那这时概念确立了吗?是的,这样一个简单的概念就确立起来了。另外一个可能的情况就是文件系统的操作,文件系统操作更多地是新建文件,编辑文件,删除文件,移动文件,复制文件等一系列操作。在这样看来,我们所定义的是一个二义性概念,二义性概念以及泛义性概念都是具有一个以上的定义,如果没有找好定义就会产生不一样的理解,让人对代码晦涩难懂。那我们如何确立一个概念呢?单义概念?还是多义概念?答案是:视情况而定。对于需要高稳定性的应用,它需要概念都清楚明确,没有二义性。这样该软件缺陷数量便会相应地少,每个处理都有着自身实际定义。对于高扩展的应用,它需要包容度高的概念作为基本概念作为入口。例如,在 Tomcat 的架构中,Tomcat 由Server到Acceptor层层递进的定义使得 Tomcat具有稳定的结构和易于配置。在Jetty 的架构中Handler 的核心概念让 Jetty 便于扩展形成链式扩展。

应用与遵循概念

       应用概念通常在确立时并已经正式要用上了。在编程中,概念的应用通常是一个类的定义或者方法的定义。例如对于一个文件IO操作的定义中,我们可能定义一个 FileIOOperation 类,这个类中我们定义一个 byte[] readFileTypeBytes() 方法用于读取文件前三、四个字节(通常文件会使用头四个字节作为文件类型的标识位)。接着可能是 String readContent() 方法,用于读取文件内容。这样我们就得到了一个自然而然的概念及相应类实现。遵循概念要求你时刻保证概念定义一致,如果我们在上述的文件IO操作类 FileIOOperation中定义一个 boolean move(File dir) 方法时,我们对文件IO操作的定义变得宽泛。那如果我们确实要对概念继续宽泛处理呢?通常是把概念变得更加包容。在上述的文件IO操作概念中,如果把IO去掉那么就成为文件操作这样一个包容性较好的概念,这样就能够和原有的概念相容,但这样会引入二义性概念,也不利于符合编程中“高内聚,低耦合”。现在,我想你已经知道泛化是什么意思了,泛化其实是概念逐渐变得多义的过程。

客观概念与主观概念

       在编写程序时,我们可以有自己的对程序的定义。这些定义在某种程度上说可以分为两种,一种是客观概念,另一种是主观概念。对于主观概念,我们可以进一步分为主体主观,和客体主观。主观概念在程序中会导致程序表现的语义并不能贴切现实的业务。通常在沟通时能够体会到这一点,往往就是你说A,用户却会说成B。那这时候我们会说,不如我们使用客体主观概念,这时对于用户来说是贴切的,却导致程序缺乏一定的抽象定义。这对程序编写会出现冗余的情况。例如,没有必要的数据保存,一些需求方会要求你对每一个过程的数据进行独立分开进行运算。对于客观概念,只要符合业务环境,往往能够开箱即用,例如在前面介绍到的MVC 模式作为一种一种客观概念,已经普遍地使用。客观概念往往简单,并且能够相容比较多的概念,这也突出了客观概念在编程中的重要作用。

实际示例

       在我一次外包中,急于需求方的修改意见,我原先考虑增加一个概念,在那时,我加入周期组概念来把几个不同周期的工作合在一起以方便汇总。在编码中,我逐渐把周期组的概念抽象成了工作会话概念,用于记录他们的业务处理过程。做了一天半发现我可以直接引入一个更加简单的统计范围,这样我只需要做简单的界面更新和后端接口更改便可完成需求方功能。结果,只花了一个晚上便完成了原定的功能。可见概念在泛化时带来的概念包容的力量是多么强大。

总结

       总地来说,本篇文章强调了以下几点:
  1. 概念在编程中大致分为顶层概念、中层概念和底层概念,可以说软件设计,概念无处不在。设计概念对于程序设计及其重要,没有概念将没有正确的程序。
  2. 如果确立单义性概念,那么这个概念将会稳定,在编码中也将始终不变。如果确立泛义性概念,那个这个概念将会变得灵活,但也会给编码带来风险。
  3. 视情况决定需要确立单义性的概念还是泛义性概念,这样会给程序带来合理的扩展,也带来了一定不稳定,平衡这两者的之间的矛盾,以指导自己进行开发。
  4. 概念从客观上分为主观概念,客观概念。客观概念构成应用程序是我们追求的目标。
概念是我们用以编写程序中必不可少的自然语言词汇。这些词汇表达了我们对实际业务的理解。一个良好的概念会给程序带来确定性和准确性。

理想和现实

无论我有多么愚蠢,我都知道我需要有一个目标来让我达到我想达到样子。那个我需要达到的地方一定不会让我轻易地到达,到达的那个地方并不美好,或许没有自己真真切切能够触及到的东西,又或许只是平凡无奇的一片草地。但是,我想达到我理想的样子。
理想
C/C++ 指针指向着一个雪花,我想表达什么?雪花在自然界中是不会相同的,然而在画图中我们总能得到相同的雪花。C/C++ “->”(指针成员运算符)总是得到一个类型的内容(或许这个类型是你定义的)。背景百搭黑(我朋友说黑色是百搭的)可以无限的延伸,或许它代表世界?或许它代表我自己?不管这样, 
我想说明的是我要表达的东西是:编码出独一无二(哪来的独一无二?黑盒技术给我们带来的任意代码实现重复的可能性,这大概是无法完成的)。我想我会继续做下去吧?
现实
然后呀,愚蠢的我用 CSS 写出了这样的东西(好吧,做成这样让我萌生放弃的念头)。我想我没有做到现实中我要编码的东西呀,而且在现实中我也没有我想成为的那样子啊。那个理想的自己总是在旁边提醒着,引导着。而我却无耻地担心着,害怕着选择了最为容易的方式。正确的方式在理想的自己一声又一声的叹息中埋没,那个懈怠的自己不断地鼓励着自己走向不好的地方。一次接着一次,愚蠢地重蹈覆辙。不断重复地说出没经过脑子的的话语,不断地想回到起初的时间点。但是,我依然想达到自己想要达到的样子。
实现理想只需完成
我可以达到自己想要的样子吗?我用 CSS 做出了与刚开始不一样的东西,这个和刚开始完完全全不一样,风格也许大概是在哪里看过的吧,有我自己的东西吗?有我自己的印记吗?加以自己的 CSS 能力,发挥自己的创造力了吗?理想的那个自己会不会满意地给我一个答复了呢?那个懈怠的自己会不会嘲笑我:这个傻子做出了一个奇怪的,又不是自己的东西呢?那我自己会想什么?会是——我可以做的更好,达到我想要达到的样子吗?

实现理想是一个及其艰难的过程,也许你会为自己寻找着借口,让自己不去达到旁边那个理想的自己。但是你只需要付出一点点的努力便会让自己有改变的地方。改变自己总是对自己有着强大的帮助。如果你不知道理想的自己,我想你会在某本书、某句话语、某个人或者自己的内心深处得到正确的答案吧。
最后,我想达到我想达到的样子。

爬虫服务化

Web 应用中,一个重要的话题就是服务化,服务化意味着 Web 服务的体积得到减小,并提高了 Web 服务的内聚和解藕程度,并给用户提供了更加可扩展的服务。本文将会带大家了解如何服务化爬虫。

原因

在基于爬虫的 Web 服务中,爬虫关乎整个系统的正常运作。在以爬虫为核心的Web 服务中,我们面临着爬虫进程的管理、爬取数据的管理和爬取数据的分析等问题。在处理爬虫运行状态管理时,爬虫的运行状态需要记录起来以方便找出爬虫中止的原因,排除爬虫的故障。在处理爬虫数据时,根据爬虫的数据进而结构化存储的对应操作需要具备一定的灵活性和纠错能力。分析爬虫爬取的数据时,数据的分析和处理需要额处的处理,如使用其它的服务等等。同时,为了给用户或者开发者带来开放能力,又必须给用户提供爬虫服务的接口,方便进行二次开发,大大提高服务的可重用性。

做法

通常一个应用的服务化,总是离不开操作系统,在处理服务化时离不开以下几种形式:

命令行模式

命令行模式在处理爬虫服务故障时,是最简单,把日志重定向到文件就可以方便日后的故障排查。但数据的处理和分析需要单独对输出的数据文件进行处理,中间有一定的烦琐,如果数据量多的话,很考验服务器的 IO 处理能力和Web 服务读取数据的内存优化能力。通常命令行模式的做法与以下步骤相似:
  1. 执行爬虫命令,爬虫日志与数据单独保存
  2. 记录爬虫运行状态和退出值,如创建,运行,终止。保存爬虫的日志文件和数据文件。
  3. Web 服务在爬虫进程结束后,读取爬虫数据,和日志数据(如果需要)。

RESTful API/RPC

RESTful API 或者远程过程调用的形式在第一种形式的基础上,减少了服务器的 IO 处理能力。但无法优化内存,一但传输结构确定下来便难以提高结构与算法相适应的改进。

第三方托管(PaaS)

以 Scrapy 为例,Scrapy Hub 提供了 Scrapy Cloud ,可以替我们管理和运行爬虫。优点与 RESTful/RPC 形式相似。极大地减轻服务器负担(别人帮我们完成了!)并以 RESTful API 形式开放接口。
可以前往 Scrapy Cloud 了解更多信息。

示例

本示例以命令行模式进行构建爬虫服务,在开始之前,需要读者具备一定的 Python 编程能力,大致上能够想起怎样写类就差不多了。另外,本示例使用 Scrapy 作为爬虫框架。
我们首先需要一个 Pyhton 编译器,我使用的是 Python 3.x 可以在这里下载:https://python.org/ 。 安装好后,接着安装 Scrapy 爬虫框架:
pip install scrapy
接下来,讲述如何简单地创建一个爬虫项目,让大家熟悉一下。
可以直接把 loveletter-spider (https://github.com/codimiracle/loveletter-spider) 克隆下来这样可以节约不少的时间。 安装完 Scrapy 后,执行scrapy会得到以下结果:
$ scrapy
Scrapy 1.5.2 - no active project
Usage:
scrapy [options] [args]
Available commands:
bench Run quick benchmark test
fetch Fetch a URL using the Scrapy downloader
genspider Generate new spider using pre-defined templates
runspider Run a self-contained spider (without creating a project)
settings Get settings values
shell Interactive scraping console
startproject Create new project
version Print Scrapy version
view Open URL in browser, as seen by Scrapy
[ more ] More commands available when run from project directory
Use "scrapy -h" to see more info about a command
接着,运行 scrapy startproject [project_name] 创建一个爬虫项目。 进入 loveletter-spider 目录,再次执行 scrapy 你应该可以看到以下内容
$ scrapy
Scrapy 1.5.2 - project: loveletter
Usage:
scrapy [options] [args]
Available commands:
bench Run quick benchmark test
check Check spider contracts
crawl Run a spider
edit Edit spider
fetch Fetch a URL using the Scrapy downloader
genspider Generate new spider using pre-defined templates
list List available spiders
parse Parse URL (using its spider) and print the results
runspider Run a self-contained spider (without creating a project)
settings Get settings values
shell Interactive scraping console
startproject Create new project
version Print Scrapy version
view Open URL in browser, as seen by Scrapy
Use "scrapy -h" to see more info about a command
接着,我们可以使用 scrapy list 显示当前可以运行的 spider。使用 scrapy crawl [spider_name] 可以运行一个爬虫。不管怎样,您可以在上面给出的爬虫链接中得到提示。 我们需要的只是爬虫给我们产生的日志和数据。在上边给出的 loveletter-spider 中是 *.json 和 *.log 在这个爬虫运行结束后会产生这两个文件,我们的服务化过程就有了输入点。然后,我们需要做的就是运行 spider 和处理 json 和 log 文件。在 Java 中运行一条命令使用的是 Runtime#exec() 方法。具体要如做呢?
@Async
public ListenableFuture<CrawlingResult> crawlFully() {
String runId = getRunId();
String logfile = "crawled/" + runId + ".log";
String outfile = "crawled/" + runId + ".json";
String command = getCommand(outfile, logfile);
//插入爬取结果
CrawlingResult result = getCrawlingResult(runId, outfile, logfile);
crawlingResultRepository.insert(result);
try {
//执行爬虫
Process process = Runtime.getRuntime().exec(command);
result.setStatus(CrawlingResult.RUNNING);
crawlingResultRepository.updateIdempotently(result, CrawlingResult.CREATED);
//等待爬虫执行完成
while (process.isAlive()) {
Thread.sleep(3000);
}
//处理爬虫进程结束退出值
result.setExitValue(process.exitValue());
if (result.getExitValue() == 0) {
result.setStatus(CrawlingResult.FINISHED);
//载入爬取的数据
loadCrawledData(runId);
//分析爬取的数据
analysisCrawledData(runId);
} else {
result.setStatus(CrawlingResult.SPIDER_ERROR);
}
crawlingResultRepository.updateIdempotently(result, CrawlingResult.RUNNING);
return AsyncResult.forValue(result);
} catch (Exception e) {
log.error("spider service terminated:", e);
int previousStatus = result.getStatus();
result.setStatus(CrawlingResult.SYSTEM_ERROR);
crawlingResultRepository.updateIdempotently(result, previousStatus);
return AsyncResult.forExecutionException(e);
}
}
这样可以记录好爬虫执行状态。处理好爬虫的运行接着我们就需要处理爬取到的数据了,这时候我们可以这样处理例如:
try {
//读取爬取的数据
InputStream inputStream = Files.newInputStream(Paths.get(result.getOutfile()));
String rawData = StreamUtils.copyToString(inputStream, Charset.defaultCharset());
List<CrawledData> crawledDataList = JSON.parseArray(rawData, CrawledData.class);
//主题摘要
String[] rawThemeData = null;
Map<String, Theme> map = new HashMap<>();
for (CrawledData crawlData : crawledDataList) {
//处理爬虫数据,结构化地存入数据库
Theme theme = map.get(crawlData.title);
if (Objects.isNull(theme)) {
theme = new Theme();
BeanUtils.copyProperties(crawlData, theme);
rawThemeData = crawlData.title.split("\\|");
theme.setEpisode(rawThemeData[0]);
theme.setId(getThemeId(crawlData.title, crawlData.getPublishTime()));
theme.setRunId(runId);
map.put(crawlData.title, theme);
log.debug("read title data of crawled data: [{}]", theme);
themeRepository.insert(theme);
}
Letter letter = new Letter();
BeanUtils.copyProperties(crawlData, letter);
letter.setTheme(theme);
letter.setRunId(runId);
letter.setId(getLetterId(theme.getId(), crawlData.letterIndex));
log.debug("read letter data of crawled data: [{}]", letter);
letterRepository.insert(letter);
}
} catch (IOException e) {
log.error("can not load the data of crawled data by runId [{}], it throws [{}]", runId, e);
throw e;
}
这样如果中途出错应该怎么办呢?我们可以使用事务来帮助处理。在 Spring Boot 中我们使用 @Transaction(rollbackFor = IOException.class) 注解处理,若发生问题回滚操作。
@Async
@Transactional(rollbackFor = IOException.class)
public void loadCrawledData(String runId) throws IOException {
CrawlingResult result = crawlingResultRepository.findByRunId(runId);
if (Objects.nonNull(result)) {
// 与上一致
}
}
如果需要控制爬虫本身呢?我们需要把处理这个爬虫的线程Thread.currentThread()保存下来,并对其进行相应的结束,暂停等操作,不过需要注意的是一旦服务重启这个就会失去效用。

具体的爬虫和该示例的源代码:
爬虫:https://github.com/codimiracle/loveletter-spider
服务:https://github.com/codimiracle/loveletter-service

注意:Java 程序在运行时,PATH 路径要包含 Python 解析器。

总结

我们通过命令行模式进行爬虫的服务化,了解了 Web 应用中如何爬虫的服务化设计。在本次学习中,我们可以得到爬虫服务化的过程中需要注意以下几点:
  • 爬虫运行的方式,其运行方式决定了爬虫自身服务化的方式。
  • 爬虫状态管理,通常服务需要管理爬虫的状态,以方便进行爬虫的管理。这里笔者只是视作一项成功或失败的任务。
  • 爬取数据的处理与分析,爬取数据是需要处理和分析的。没有处理爬虫的数据,没有分析爬虫的数据,就得不到这次爬虫的成果。

Java 虚拟机 – 结构

数据类型

Java 虚拟机中有两种数据类型:原始类型和引用类型。这两种类型的值可以存进变量,作为参数传递,被方法返回,并通过原始值或者引用值进行操作。虚拟机不进行类型检查,所有的检查都在运行时之前处理完成。虚拟机通过虚拟机指令来确定参数类型如 iadd,ladd,fadd,dadd 都是两个数值操作类型返回一个数值结果的指令,分别对 integer, long, float, double 进行操作。
原始类型byteshortintlongchar
字节数816326416
默认值0000‘\u000000’
数值范围[-128,
127]
[-32768,
32767]

[-2147483648,
-2147483647]

[-9223372036854775808
, 9223372036854775807]
[0,
65535]
原始类型 float IEEE 754 double IEEE 754
字节数 32 64
默认值
+0+0
returnAddress 类型被虚拟机 jsr,ret,jsr_w 指令进行操作,returnAddress 的值指向一条操作码(opcode), returnAddress 不会作为 Java 语言中特定类型并且不能通过编程语言进行修改。 boolean 类型虽然在虚拟机中进行了定义,但实际中是使用 int 来表示的,虚拟机也是使用 byte 相关的指令对其进行操作,不存在 boolean 相关的操作指令, boolean 数组使用 newarray 指令创建,并使用 byte 数组指令 baload,bastore 进行访问。 引用类型有三种,分别是 class 类型,array 类型,interface 类型,null 没有具体的运行时类型,但可以强制转换为任意的引用类型。虚拟机规范没有规定 null 的具体值。

运行时数据区域

Swagger

入门

Swagger 给我们提供了两个有用的工具: Swagger Editor 和 Swagger Inspector 和一个 API 托管平台 SwaggerHub。其中 Swagger Hub 是一个可以让个人拥有 3个公共 API 文档仓库和一个私有仓库的在线 API 文档生成以及Mock API Server的一整套服务,使用 YAML 以及 Open API Specification 编写出能够使 Swagger 自动生成文档的 API。下面,笔者将会带大家学习 Swagger 这个极好的 API 编写平台。

我们打开 Swagger 后,我们就会来到 Swagger 的首页,点击 “Try Swagger Hub” 就可以来到 “Swagger Hub”的首页,再接着 “Sign Up Free” 便可以进入注册界面,当然可以直接进入注册界面如下所示:
SwaggerHub 注册界面
接下来我们分别输入“User Name”, “Email”, 以及 “Password” 点下 “SIGN UP” 静候佳音即可,完成后可以看到下面的界面,输入组织名称 “Name your Organization”点下按钮即可,当然你可以不管它直接进入 Swagger Hub
创建组织
接着来到 Swagger Hub 界面,新建一个 Swagger API 。点击 “Create New API”
Swagger Hub 界面
创建 API 对话框
OpenAPI version 默认为 2.0 如果有兴趣也可以使用 3.0,Template 为一些基础的 API 文档,这里选择的是宠物商店(Petstore)模板,在 Name 中输入一个“test”,可见性(Visibility)选择“Public”,Public 可以直接让别人看到你的API,通常不告诉别人几乎不大可能看到,看到也没多大关系。而 Private 就相反了,只有你和协作者才可以看到。现在可以点击“Create API”继续下去了。 成功后会来到 Design View 如下图所示:
Design View
绿色区域是导航视图、编辑器视图、文档视图三个视图的显示,隐藏,编辑器与导航可以单独隐藏,但至少显示一个。蓝色区域便是导航视图,可以看到由一个可以展开和折叠的“Tags”做页标并以HTTP请求方法和请求路径的项目构成。点击导航的项目可以很方便地导航到该项目在编辑器中定义的位置。紫色区域为文档区域,在文档区域可以查看 Open API 给我们解释并生成的 API 文档。深蓝色区域与一般高亮的编辑器没什么特别之处。编辑器编写的 Open API 代码会实时更新到那里文档区域和导航区域。橘色区域可以调整编辑器的文字大小,白天、夜晚背景色和评论。评论可以帮助组织或者团队之间的协作和沟通。粉红色区域则是编辑器所编写的 Open API 代码能否顺利通过验证,可以点击它展开来看到不正确的地方。
Open API 验证
API 文档
文档区域中比较简明易懂,参数列表,响应码以及相应描述和响应结果一应俱全。左上方的“Try it out”还可以直接使用 Mock API Server 进行返回结果。
Model 文档
Model 文档直接显示了这份 Open API 所用到的实体,对数据库设计很方便啊!显示的方式与 JSON 格式相差不大。 接下来,笔者将会讲述一些必要的 YAML 知识和 Open API 知识。

YAML 结构

YAML 使用缩进(这里使用两个空格)作为嵌套层,类似与 Python 使用缩进界定代码块。与 JSON 类似可以有 number, string, object, array 类似的概念,故这里使用来简单地介绍 YAML。这里给出 YAML 的官网以及会用到的一些写法帮助大家简单地了解一下 YAML 。 YAML 的基础写法
key: value
key:
  key1: value
  key2: value
YAML number 的写法
grade: 100
YAML string 的写法,使用第二种允许预先换行。
title: Hello world
title: |
  Hello world
  this is a good example
YAML object 的写法
person:
  name: hello
  age: 18
  gender: male
YAML array 的写法
persons:
- name: hello
  age: 18
  gender: male
- name: mike
  age: 20
  gender: male

Open API 结构

Open API 2.0 组成

简述

Open API 组成很直观,顶头注明Open API 的版本,使用一个 info object,介绍这份 Open API, 并使用 tags array,描述这份 Open API 中用到的标签,使用 paths object,描述各部分 REST API 中的 HTTP 链接 。securityDefinitions object 定义了有关登录和权限的限定。definitions object 定义了各个 Model,externalDocs object 则定义这份 Open API 的额外信息,后面的是 Swagger Hub 给我们提供的 Mock Server。

info object

info object 的内容
info object 的内容很直观,这份API 相关的描述,版本,标题, 联系方式,开发协议等等。

tags array

如同前面所言,数组元素使用“- ”开头然后写key: value 对构成,每一个“- ”及其缩进的内容为一个 YAML object。

paths object

paths object
paths object的写法很明显,先是一个API 请求路径 “/pet” object 接着是一个 “请求方法” object,然后写明这个请求 object 的 tags array,写上“summary”说明这个 API 是拿来干什么的,operationId 用来标识整个 API ,consumes (消耗),意味着该 API 可以接受怎样的数据(accept header)。produces (产出) 意味着API会产生什么类型的响应(content-type)。parameters,很显然是该 API 接受的参数。responses 则是定义响应。security 则是相应的权限。Open API 允许我们在一个请求路径 object 下接着写其它 请求方法 object 的 API。如上面没有显示完全的“put”。
安全认证定义
在 securityDefinitions object 中,petstore_auth 用于 oauth2 的认证,api_key 用于 API 调用的认证(类似与 Web 中经常放在地址后面的 Token )。如果是 cookie 的认证则简单的由 api_key auth 的 i n 改为 “basic” (2.0) 或者 “cookie ” (3.0)。
definitions
在 definitions object 中,每个像 Order, Category 一样层级的都是Model, 这些 Model 在编写 API 的参数,输出结果中在 schema 属性中进行引用。引用方式一般是以下的方式:
schema:
  $ref: '#/definitions/ModelName'
写法如如英文所言,先指定好 type, 接着写属性 properties,properties 中为一个Model 的属性名 object 指定该属性的类型,在这里依然可以使用 schema 进行应用。更多 Open API 的说明可以查看 Open API 规范