先放一下Github地址:
Github
这篇文章是继上篇1000行代码手写Web服务器(一)的后续,在写完上一个版本的代码后,经过今年4月份和5月份的两次重构补充,现在大概有2500行代码,主要变更为:
- 网络模块的增强,从最初的BIO版本,变为现在支持BIO、NIO、AIO三个版本,并且支持根据配置文件进行IO模型的切换
- Filter
- Listener
- TemplateEngine的表达式解析功能增强
- 添加了很多方法注释和行注释
本来是希望实现一下Servlet3.1标准的,但是很多API/继承体系过于繁琐,并且不是所有功能都有挑战性,所以还是选择尽量模仿Servlet API,但只选择比较感兴趣的部分去实现一下。
先复习一下几种Unix IO模型:
- 异步I/O 是指用户程序发起IO请求后,不等待数据,同时操作系统内核负责I/O操作把数据从内核拷贝到用户程序的缓冲区后通知应用程序。数据拷贝是由操作系统内核完成,用户程序从一开始就没有等待数据,发起请求后不参与任何IO操作,等内核通知完成。
- 同步I/O 就是非异步IO的情况,也就是用户程序要参与把数据拷贝到程序缓冲区(例如java的InputStream读字节流过程)。
- 同步IO里的非阻塞 是指用户程序发起IO操作请求后不等待数据,而是调用会立即返回一个标志信息告知条件不满足,数据未准备好,从而用户请求程序继续执行其它任务。执行完其它任务,用户程序会主动轮询查看IO操作条件是否满足,如果满足,则用户程序亲自参与拷贝数据动作。
对应到Java中的API:
- BIO:同步阻塞,每个客户端的连接会对应服务器的一个线程
- NIO:同步非阻塞,多路复用器轮询客户端的请求,每个客户端的IO请求会对应服务器的一个线程
- AIO: 异步非阻塞,客户端的IO请求由OS完成后再通知服务器启动线程处理(需要OS支持)
1、进程向操作系统请求数据
2、操作系统把外部数据加载到内核的缓冲区中,
3、操作系统把内核的缓冲区拷贝到进程的缓冲区
4、进程获得数据完成自己的功能
Java NIO属于同步非阻塞IO,即IO多路复用,单个线程可以支持多个IO
即询问时从IO没有完毕时直接阻塞,变成了立即返回一个是否完成IO的信号。
而异步IO就是指AIO,AIO需要操作系统支持。
为了实现类似于Netty的以很小的改动就可以切换IO模型的概念,设置了一个配置文件server.properties(类似于Tomcat的配置文件)。
server.port=8080
server.connector=bio
connector可以取值为bio,nio,aio。那么如何根据不同的connector来选择不同的IO模型呢?
我们重点关注network包,只有这个包与下面的IO模型有关,其他的包都是与IO模型实现无关的。
首先IO模型的入口类在Bootstrap类中被初始化:
/**
* 服务器启动入口
* 用户程序与服务器的接口
*/
public static void run() {
String port = PropertyUtil.getProperty("server.port");
if(port == null) {
throw new IllegalArgumentException("server.port 不存在");
}
String connector = PropertyUtil.getProperty("server.connector");
if(connector == null || (!connector.equalsIgnoreCase("bio") && !connector.equalsIgnoreCase("nio") && !connector.equalsIgnoreCase("aio"))) {
throw new IllegalArgumentException("server.network 不存在或不符合规范");
}
Endpoint server = Endpoint.getInstance(connector);
server.start(Integer.parseInt(port));
Scanner scanner = new Scanner(System.in);
String order;
while (scanner.hasNext()) {
order = scanner.next();
if (order.equals("EXIT")) {
server.close();
System.exit(0);
}
}
}
重点是Endpoint的getInstance方法:
Endpoint是每种Endpoint的基类
public abstract class Endpoint {
/**
* 启动服务器
* @param port
*/
public abstract void start(int port);
/**
* 关闭服务器
*/
public abstract void close();
/**
* 根据传入的bio、nio、aio获取相应的Endpoint实例
* @param connector
* @return
*/
public static Endpoint getInstance(String connector) {
StringBuilder sb = new StringBuilder();
sb.append("com.sinjinsong.webserver.core.network.endpoint")
.append(".")
.append(connector)
.append(".")
.append(StringUtils.capitalize(connector))
.append("Endpoint");
try {
return (Endpoint) Class.forName(sb.toString()).newInstance();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
throw new IllegalArgumentException(connector);
}
}
其实就是使用反射来实现不同的字符串映射到不同的类。
一个具体的IO模型的实现一般是由Endpint(入口,对应ServerSocket),Acceptor(请求接收线程),Dispatcher(请求分发线程,往往对应一个线程池,负责读取Request以及将RequestHandler放入线程池中执行),SocketWrapper(Socket的包装类),RequestHandler(往往是一个Runnable,负责执行servlet等)组成;NIO有些特别,因为它不仅是简单的NIO,而是使用了借鉴了Tomcat的Reactor NIO模型。
@Slf4j
public class BioEndpoint extends Endpoint {
private ServerSocket server;
private BioAcceptor acceptor;
private BioDispatcher dispatcher;
private volatile boolean isRunning = true;
@Override
public void start(int port) {
try {
dispatcher = new BioDispatcher();
server = new ServerSocket(port);
initAcceptor();
log.info("服务器启动");
} catch (Exception e) {
e.printStackTrace();
log.info("初始化服务器失败");
close();
}
}
private void initAcceptor() {
acceptor = new BioAcceptor(this, dispatcher);
Thread t = new Thread(acceptor, "bio-acceptor");
t.setDaemon(true);
t.start();
}
@Override
public void close() {
isRunning = false;
dispatcher.shutdown();
try {
server.close();
} catch (IOException e) {
e.printStackTrace();
}
}
public Socket accept() throws IOException {
return server.accept();
}
public boolean isRunning() {
return isRunning;
}
}
主要是启动服务器以及关闭服务器,都比较简单,BIO的ServerSocket初始化是new出来就可以工作了。注意BIO的Acceptor是一个daemon(守护) 线程,也就是在所有非daemon线程都退出后,该线程会自动退出。
接收到客户端的连接后,将socket进行封装,然后交给Dispatcher。
public class BioAcceptor implements Runnable {
private BioEndpoint server;
private BioDispatcher dispatcher;
public BioAcceptor(BioEndpoint server,BioDispatcher dispatcher) {
this.server = server;
this.dispatcher = dispatcher;
}
@Override
public void run() {
log.info("开始监听");
while (server.isRunning()) {
Socket client;
try {
//TCP的短连接,请求处理完即关闭
client = server.accept();
log.info("client:{}", client);
dispatcher.doDispatch(new BioSocketWrapper(client));
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
负责先读取请求,然后构造RequestHandler然后放到线程池中执行。
这里遇到了一个坑,本来是打算把读取也放到RequestHandler中并发执行的,但是在测试的时候发现线程池中有多个线程都在执行读取,就是一个客户端请求被分到了多个线程中执行,并且Request无法读取完整。所以只能把客户端的读取放到单线程执行的Dispatcher中。
还有一个坑是在读取Request完之后不能把inputStream关掉,否则会把socket(客户端连接)也关掉,导致后面的响应写回时抛出Socket已经关闭的异常。
public class BioDispatcher extends AbstractDispatcher {
@Override
public void doDispatch(SocketWrapper socketWrapper) {
BioSocketWrapper bioSocketWrapper = (BioSocketWrapper) socketWrapper;
Socket socket = bioSocketWrapper.getSocket();
Request request = null;
Response response = null;
try {
BufferedInputStream bin = new BufferedInputStream(socket.getInputStream());
byte[] buf = null;
try {
buf = new byte[bin.available()];
int len = bin.read(buf);
if (len <= 0) {
throw new RequestInvalidException();
}
} catch (IOException e) {
e.printStackTrace();
}
// 这里这里不要把in关掉,把in关掉就等同于把socket关掉
//解析请求
response = new Response();
request = new Request(buf);
pool.execute(new BioRequestHandler(socketWrapper, servletContext, exceptionHandler, resourceHandler, request, response));
} catch (ServletException e) {
exceptionHandler.handle(e, response, socketWrapper);
} catch (IOException e) {
e.printStackTrace();
}
}
}
不同IO模型的请求处理器是大同小异的,前面的业务逻辑执行都是一样的,都在父类中,只有响应写回是不同的,所以这里使用了继承来复用代码。
父类:
@Slf4j
@Getter
public abstract class AbstractRequestHandler implements FilterChain, Runnable {
protected Request request;
protected Response response;
protected SocketWrapper socketWrapper;
protected ServletContext servletContext;
protected ExceptionHandler exceptionHandler;
protected ResourceHandler resourceHandler;
protected boolean isFinished;
protected Servlet servlet;
protected List filters;
private int filterIndex = 0;
public AbstractRequestHandler(SocketWrapper socketWrapper, ServletContext servletContext, ExceptionHandler exceptionHandler, ResourceHandler resourceHandler, Request request, Response response) throws ServletNotFoundException, FilterNotFoundException {
this.socketWrapper = socketWrapper;
this.servletContext = servletContext;
this.exceptionHandler = exceptionHandler;
this.resourceHandler = resourceHandler;
this.isFinished = false;
this.request = request;
this.response = response;
request.setServletContext(servletContext);
request.setRequestHandler(this);
response.setRequestHandler(this);
// 根据url查询匹配的servlet,结果是0个或1个
servlet = servletContext.mapServlet(request.getUrl());
// 根据url查询匹配的filter,结果是0个或多个
filters = servletContext.mapFilter(request.getUrl());
}
/**
* 入口
*/
@Override
public void run() {
// 如果没有filter,则直接执行servlet
if (filters.isEmpty()) {
service();
} else {
// 先执行filter
doFilter(request, response);
}
}
/**
* 递归执行,自定义filter中如果同意放行,那么会调用filterChain(也就是requestHandler)的doiFilter方法,
* 此时会执行下一个filter的doFilter方法;
* 如果不放行,那么会在sendRedirect之后将响应数据写回客户端,结束;
* 如果所有Filter都执行完毕,那么会调用service方法,执行servlet逻辑
* @param request
* @param response
*/
@Override
public void doFilter(Request request, Response response) {
if (filterIndex < filters.size()) {
filters.get(filterIndex++).doFilter(request, response, this);
} else {
service();
}
}
/**
* 调用servlet
*/
private void service() {
log.info("socket isClosed: {}",this.socketWrapper.isClosed());
try {
//处理动态资源,交由某个Servlet执行
//Servlet是单例多线程
//Servlet在RequestHandler中执行
servlet.service(request, response);
} catch (ServletException e) {
exceptionHandler.handle(e, response, socketWrapper);
} catch (Exception e) {
//其他未知异常
e.printStackTrace();
exceptionHandler.handle(new ServerErrorException(), response, socketWrapper);
} finally {
if (!isFinished) {
flushResponse();
}
}
log.info("请求处理完毕");
}
/**
* 响应数据写回到客户端
*/
public abstract void flushResponse();
}
构造方法中使用url找到了对应的servlet和filter,然后在run方法中先考虑执行filter,最后执行servlet。
注意在两种情况下会将响应数据立即写回客户端,一种是servlet执行完毕的时候,另一种是调用了response的重定向的时候。因为filter存在一种不放行的情况,就无法调用servlet执行完毕后的写回代码,并且此时往往是会使用重定向来结束请求。
BIO子类:
public class BioRequestHandler extends AbstractRequestHandler {
public BioRequestHandler(SocketWrapper socketWrapper, ServletContext servletContext, ExceptionHandler exceptionHandler, ResourceHandler resourceHandler, Request request, Response response) throws ServletNotFoundException, FilterNotFoundException {
super(socketWrapper, servletContext, exceptionHandler, resourceHandler, request, response);
}
/**
* 写回后立即关闭socket
*/
@Override
public void finishRequest() {
isFinished = true;
BioSocketWrapper bioSocketWrapper = (BioSocketWrapper) socketWrapper;
byte[] bytes = response.getResponseBytes();
OutputStream os = null;
try {
os = bioSocketWrapper.getSocket().getOutputStream();
os.write(bytes);
os.flush();
} catch (IOException e) {
e.printStackTrace();
log.error("socket closed");
} finally {
try {
os.close();
bioSocketWrapper.close();
} catch (IOException e) {
e.printStackTrace();
}
}
WebApplication.getServletContext().afterRequestDestroyed(request);
}
}
这里就是把response转为byte数组,然后写回即可。
注意这里在写回之后会将连接立刻关闭,对应着HTTP的connection:close,也就是HTTP短连接。在NIO中实现了HTTP持久连接,也就是connection:keep-alive。在后面的基于JMeter压测中,在测试BIO时注意要设置请求头Connection为close,否则会出现大量的异常(connection refused)。
@Slf4j
@Getter
public class BioSocketWrapper implements SocketWrapper {
private Socket socket;
public BioSocketWrapper(Socket socket) {
this.socket = socket;
}
@Override
public void close() throws IOException {
socket.close();
}
}
Tomcat中的NIO Reactor模型用最简单的话来介绍就是:
多个(1个或2个)Acceptor阻塞式获取socket连接,然后多个Poller(处理器个数个)非阻塞式轮询socket读事件,检测到读事件时将socket交给线程池处理业务逻辑。
下面是Tomcat中的NIO Reactor实现,我的实现与其大同小异:
包含了三个组件:
Acceptor:后台线程,负责监听请求,将接收到的Socket请求放到Poller队列中
Poller:后台线程,当Socket就绪时,将Poller队列中的Socket交给Worker线程池处理
SocketProcessor(Worker):处理socket,本质上委托ConnectionHandler处理
Connector 启动以后会启动一组线程用于不同阶段的请求处理过程。
Acceptor 线程组。用于接受新连接,并将新连接封装一下,选择一个 Poller 将新连接添加到 Poller 的事件队列中。
Poller 线程组。用于监听 Socket 事件,当 Socket 可读或可写等等时,将 Socket 封装一下添加到 worker 线程池的任务队列中。
worker 线程组。用于对请求进行处理,包括分析请求报文并创建 Request 对象,调用容器的 pipeline 进行处理。
Netty是一个Java编写的网络编程框架,比如Dubbo,RocketMQ等都是基于它编写的,它的NIO版本也是基于Reactor模式实现的。
Netty中使用的Reactor模式,引入了多Reactor,也即一个主Reactor负责监控所有的连接请求,多个子Reactor负责监控并处理读/写请求,减轻了主Reactor的压力,降低了主Reactor压力太大而造成的延迟。并且每个子Reactor分别属于一个独立的线程,每个成功连接后的Channel的所有操作由同一个线程处理。这样保证了同一请求的所有状态和上下文在同一个线程中,避免了不必要的上下文切换,同时也方便了监控请求响应状态。
多Reactor下,mainReactor是一个,subReactor是多个。mainReqactor对应着一个Selector,也是注册了ServerSocket的Accept事件,当接收到连接事件时,接收到Socket,把它交给subReactor,每个subReactor对应着自己的Selector,把Socket的读事件注册到自己的Selector中。
mainReactor上有一个Selector,注册了ServerSocketChannel的Accept事件;各个subReactor各自对应着自己的Selector,注册了自己对应的SocketChannel的Read事件。
我的实现思路是:NIOAcceptor以阻塞方式来接收客户端的连接,接收到后将其注册到某一个Poller的Queue中,每个Poller对应一个独立的Selector以及一个Queue中,每个Poller也是一个线程,会以无限循环的方式去将Queue中的客户端连接注册到自己所持有的Selector中,然后以非阻塞方式去检测Selector读就绪事件,检测到后将客户端连接交给Dispatcher,NIO Dispatcher类似于BIO的Dispatcher,读取Request数据后放入线程池中执行业务逻辑。
专为NIO部分画了一张时序图:
@Slf4j
public class NioEndpoint extends Endpoint {
private int pollerCount = Math.min(2, Runtime.getRuntime().availableProcessors());
private ServerSocketChannel server;
private NioDispatcher nioDispatcher;
private volatile boolean isRunning = true;
private NioAcceptor nioAcceptor;
private List nioPollers;
/**
* poller轮询器
*/
private AtomicInteger pollerRotater = new AtomicInteger(0);
/**
* 1min
*/
private int keepAliveTimeout = 60 * 1000 ;
/**
* 针对keep-alive连接,如果长期没有数据交换则将其关闭
*/
private IdleConnectionCleaner cleaner;
//********************************初始化*************************************************************
private void initDispatcherServlet() {
nioDispatcher = new NioDispatcher();
}
private void initServerSocket(int port) throws IOException {
server = ServerSocketChannel.open();
server.bind(new InetSocketAddress(port));
server.configureBlocking(true);
}
private void initPoller() throws IOException {
nioPollers = new ArrayList<>(pollerCount);
for (int i = 0; i < pollerCount; i++) {
String pollName = "NioPoller-" + i;
NioPoller nioPoller = new NioPoller(this, pollName);
Thread pollerThread = new Thread(nioPoller, pollName);
pollerThread.setDaemon(true);
pollerThread.start();
nioPollers.add(nioPoller);
}
}
/**
* 初始化Acceptor
*/
private void initAcceptor() {
this.nioAcceptor = new NioAcceptor(this);
Thread t = new Thread(nioAcceptor, "NioAcceptor");
t.setDaemon(true);
t.start();
}
/**
* 初始化IdleSocketCleaner
*/
private void initIdleSocketCleaner() {
cleaner = new IdleConnectionCleaner(nioPollers);
cleaner.start();
}
//************************初始化结束***************************************************************
@Override
public void start(int port) {
try {
initDispatcherServlet();
initServerSocket(port);
initPoller();
initAcceptor();
initIdleSocketCleaner();
log.info("服务器启动");
} catch (Exception e) {
e.printStackTrace();
log.info("初始化服务器失败");
close();
}
}
@Override
public void close() {
isRunning = false;
cleaner.shutdown();
for (NioPoller nioPoller : nioPollers) {
try {
nioPoller.close();
} catch (IOException e) {
e.printStackTrace();
}
}
nioDispatcher.shutdown();
try {
server.close();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 调用dispatcher,处理这个读已就绪的客户端连接
* @param socketWrapper
*/
public void execute(NioSocketWrapper socketWrapper) {
nioDispatcher.doDispatch(socketWrapper);
}
/**
* 轮询Poller,实现负载均衡
* @return
*/
private NioPoller getPoller() {
int idx = Math.abs(pollerRotater.incrementAndGet()) % nioPollers.size();
return nioPollers.get(idx);
}
public boolean isRunning() {
return isRunning;
}
/**
* 以阻塞方式来接收一个客户端的链接
* @return
* @throws IOException
*/
public SocketChannel accept() throws IOException {
return server.accept();
}
/**
* 将Acceptor接收到的socket放到轮询到的一个Poller的Queue中
*
* @param socket
* @return
*/
public void registerToPoller(SocketChannel socket) throws IOException {
server.configureBlocking(false);
getPoller().register(socket, true);
server.configureBlocking(true);
}
public int getKeepAliveTimeout() {
return this.keepAliveTimeout;
}
}
@Slf4j
public class NioAcceptor implements Runnable {
private NioEndpoint nioEndpoint;
public NioAcceptor(NioEndpoint nioEndpoint) {
this.nioEndpoint = nioEndpoint;
}
@Override
public void run() {
log.info("{} 开始监听",Thread.currentThread().getName());
while (nioEndpoint.isRunning()) {
SocketChannel client;
try {
client = nioEndpoint.accept();
if(client == null){
continue;
}
client.configureBlocking(false);
log.info("Acceptor接收到连接请求 {}",client);
nioEndpoint.registerToPoller(client);
log.info("socketWrapper:{}", client);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
注意Poller中保存了所有的活跃Socket(成员变量sockets),其中有些socket是初次连接的,有些是keep-alive的,我还另外设置了一个IdleConnectionCleaner,用于清除一段时间内没有任何数据交换的socket,实现就是在SocketWrapper中添加一个waitBegin成员变量,在建立连接/keep-alive时设置waitBegin,并设置一个Scheduler定期扫描sockets,将当前时间距waitBegin超过阈值的连接关闭。
@Slf4j
public class NioPoller implements Runnable {
private NioEndpoint nioEndpoint;
private Selector selector;
private Queue events;
private String pollerName;
private Map sockets;
public NioPoller(NioEndpoint nioEndpoint, String pollerName) throws IOException {
this.sockets = new ConcurrentHashMap<>();
this.nioEndpoint = nioEndpoint;
this.selector = Selector.open();
this.events = new ConcurrentLinkedQueue<>();
this.pollerName = pollerName;
}
public void register(SocketChannel socketChannel, boolean isNewSocket) {
log.info("Acceptor将连接到的socket放入 {} 的Queue中", pollerName);
NioSocketWrapper wrapper;
if (isNewSocket) {
// 设置waitBegin
wrapper = new NioSocketWrapper(nioEndpoint, socketChannel, this, isNewSocket);
// 用于cleaner检测超时的socket和关闭socket
sockets.put(socketChannel, wrapper);
} else {
wrapper = sockets.get(socketChannel);
wrapper.setWorking(false);
}
wrapper.setWaitBegin(System.currentTimeMillis());
events.offer(new PollerEvent(wrapper));
// 某个线程调用select()方法后阻塞了,即使没有通道已经就绪,也有办法让其从select()方法返回。只要让其它线程在第一个线程调用select()方法的那个对象上调用Selector.wakeup()方法即可。阻塞在select()方法上的线程会立马返回。
selector.wakeup();
}
public void close() throws IOException {
for (NioSocketWrapper wrapper : sockets.values()) {
wrapper.close();
}
events.clear();
selector.close();
}
@Override
public void run() {
log.info("{} 开始监听", Thread.currentThread().getName());
while (nioEndpoint.isRunning()) {
try {
events();
if (selector.select() <= 0) {
continue;
}
log.info("select()返回,开始获取当前选择器中所有注册的监听事件");
//获取当前选择器中所有注册的监听事件
for (Iterator it = selector.selectedKeys().iterator(); it.hasNext(); ) {
SelectionKey key = it.next();
//如果"接收"事件已就绪
if (key.isReadable()) {
//如果"读取"事件已就绪
//交由读取事件的处理器处理
log.info("serverSocket读已就绪,准备读");
NioSocketWrapper attachment = (NioSocketWrapper) key.attachment();
if (attachment != null) {
processSocket(attachment);
}
}
//处理完毕后,需要取消当前的选择键
it.remove();
}
} catch (IOException e) {
e.printStackTrace();
} catch (ClosedSelectorException e) {
log.info("{} 对应的selector 已关闭", this.pollerName);
}
}
}
private void processSocket(NioSocketWrapper attachment) {
attachment.setWorking(true);
nioEndpoint.execute(attachment);
}
private boolean events() {
log.info("Queue大小为{},清空Queue,将连接到的Socket注册到selector中", events.size());
boolean result = false;
PollerEvent pollerEvent;
for (int i = 0, size = events.size(); i < size && (pollerEvent = events.poll()) != null; i++) {
result = true;
pollerEvent.run();
}
return result;
}
public Selector getSelector() {
return selector;
}
public String getPollerName() {
return pollerName;
}
public void cleanTimeoutSockets() {
for (Iterator> it = sockets.entrySet().iterator(); it.hasNext(); ) {
NioSocketWrapper wrapper = it.next().getValue();
log.info("缓存中的socket:{}", wrapper);
if (!wrapper.getSocketChannel().isConnected()) {
log.info("该socket已被关闭");
it.remove();
continue;
}
if (wrapper.isWorking()) {
log.info("该socket正在工作中,不予关闭");
continue;
}
if (System.currentTimeMillis() - wrapper.getWaitBegin() > nioEndpoint.getKeepAliveTimeout()) {
// 反注册
log.info("{} keepAlive已过期", wrapper.getSocketChannel());
try {
wrapper.close();
} catch (IOException e) {
e.printStackTrace();
}
it.remove();
}
}
}
@Data
@AllArgsConstructor
private static class PollerEvent implements Runnable {
private NioSocketWrapper wrapper;
@Override
public void run() {
log.info("将SocketChannel的读事件注册到Poller的selector中");
try {
if (wrapper.getSocketChannel().isOpen()) {
wrapper.getSocketChannel().register(wrapper.getNioPoller().getSelector(), SelectionKey.OP_READ, wrapper);
wrapper.setWaitBegin(System.currentTimeMillis());
} else {
log.error("socket已经被关闭,无法注册到Poller", wrapper.getSocketChannel());
}
} catch (ClosedChannelException e) {
e.printStackTrace();
}
}
}
}
@Data
@Slf4j
public class NioDispatcher extends AbstractDispatcher {
/**
* 分发请求,注意IO读取必须放在IO线程中进行,不能放到线程池中,否则会出现多个线程同时读同一个socket数据的情况
* 1、读取数据
* 2、构造request,response
* 3、将业务放入到线程池中处理
* @param socketWrapper
*/
@Override
public void doDispatch(SocketWrapper socketWrapper) {
NioSocketWrapper nioSocketWrapper = (NioSocketWrapper) socketWrapper;
log.info("已经将请求放入worker线程池中");
ByteBuffer buffer = ByteBuffer.allocate(1024);
log.info("开始读取Request");
Request request = null;
Response response = null;
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
while (nioSocketWrapper.getSocketChannel().read(buffer) > 0) {
buffer.flip();
baos.write(buffer.array());
}
baos.close();
request = new Request(baos.toByteArray());
response = new Response();
pool.execute(new NioRequestHandler(nioSocketWrapper, servletContext, exceptionHandler, resourceHandler, request, response));
} catch (IOException e) {
e.printStackTrace();
exceptionHandler.handle(new ServerErrorException(), response, nioSocketWrapper);
} catch (ServletException e) {
exceptionHandler.handle(e, response, nioSocketWrapper);
}
}
}
这里涉及了keep-alive的实现,如果请求头中有connection:keep-alive,就将其重新注册到Poller的Queue,等待下一次读就绪事件。
@Setter
@Getter
@Slf4j
public class NioRequestHandler extends AbstractRequestHandler {
public NioRequestHandler(SocketWrapper socketWrapper, ServletContext servletContext, ExceptionHandler exceptionHandler, ResourceHandler resourceHandler, Request request, Response response) throws ServletNotFoundException, FilterNotFoundException {
super(socketWrapper, servletContext, exceptionHandler, resourceHandler, request, response);
}
/**
* 写入后会根据请求头Connection来判断是关闭连接还是重新将连接放回Poller,实现保活
*/
@Override
public void flushResponse() {
isFinished = true;
NioSocketWrapper nioSocketWrapper = (NioSocketWrapper) socketWrapper;
ByteBuffer[] responseData = response.getResponseByteBuffer();
try {
nioSocketWrapper.getSocketChannel().write(responseData);
List connection = request.getHeaders().get("Connection");
if (connection != null && connection.get(0).equals("close")) {
log.info("CLOSE: 客户端连接{} 已关闭", nioSocketWrapper.getSocketChannel());
nioSocketWrapper.close();
} else {
// keep-alive 重新注册到Poller中
log.info("KEEP-ALIVE: 客户端连接{} 重新注册到Poller中", nioSocketWrapper.getSocketChannel());
nioSocketWrapper.getNioPoller().register(nioSocketWrapper.getSocketChannel(), false);
}
} catch (IOException e) {
e.printStackTrace();
}
WebApplication.getServletContext().afterRequestDestroyed(request);
}
}
空闲连接的清除,避免keep-alive连接长期不使用,占用服务器资源。
@Slf4j
public class IdleConnectionCleaner implements Runnable {
private ScheduledExecutorService executor;
private List nioPollers;
public IdleConnectionCleaner(List nioPollers) {
this.nioPollers = nioPollers;
}
public void start() {
ThreadFactory threadFactory = new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "IdleConnectionCleaner");
}
};
executor = Executors.newSingleThreadScheduledExecutor(threadFactory);
executor.scheduleWithFixedDelay(this, 0, 5, TimeUnit.SECONDS);
}
public void shutdown() {
executor.shutdown();
}
@Override
public void run() {
for (NioPoller nioPoller : nioPollers) {
log.info("Cleaner 检测{} 所持有的Socket中...", nioPoller.getPollerName());
nioPoller.cleanTimeoutSockets();
}
log.info("检测结束...");
}
}
@Slf4j
@Data
public class NioSocketWrapper implements SocketWrapper {
private final NioEndpoint server;
private final SocketChannel socketChannel;
private final NioPoller nioPoller;
private final boolean isNewSocket;
private volatile long waitBegin;
private volatile boolean isWorking;
public NioSocketWrapper(NioEndpoint server, SocketChannel socketChannel, NioPoller nioPoller, boolean isNewSocket) {
this.server = server;
this.socketChannel = socketChannel;
this.nioPoller = nioPoller;
this.isNewSocket = isNewSocket;
this.isWorking = false;
}
public void close() throws IOException {
socketChannel.keyFor(nioPoller.getSelector()).cancel();
socketChannel.close();
}
@Override
public String toString() {
return socketChannel.toString();
}
}
Java的AIO的编程风格与BIO、NIO很不相似,accept、read、write都是异步方法,后续代码执行必须放在CompletionHandler中进行回调。
@Slf4j
public class AioEndpoint extends Endpoint {
private AsynchronousServerSocketChannel server;
private AioDispatcher aioDispatcher;
private AioAcceptor aioAcceptor;
private ExecutorService pool;
private void initDispatcherServlet() {
aioDispatcher = new AioDispatcher();
}
private void initServerSocket(int port) throws IOException {
ThreadFactory threadFactory = new ThreadFactory() {
private int count;
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "Endpoint Pool-" + count++);
}
};
int processors = Runtime.getRuntime().availableProcessors();
pool = new ThreadPoolExecutor(processors, processors, 1, TimeUnit.SECONDS, new ArrayBlockingQueue<>(200), threadFactory, new ThreadPoolExecutor.CallerRunsPolicy());
// 以指定线程池来创建一个AsynchronousChannelGroup
AsynchronousChannelGroup channelGroup = AsynchronousChannelGroup
.withThreadPool(pool);
// 以指定线程池来创建一个AsynchronousServerSocketChannel
server = AsynchronousServerSocketChannel.open(channelGroup)
// 指定监听本机的PORT端口
.bind(new InetSocketAddress(port));
// 使用CompletionHandler接受来自客户端的连接请求
aioAcceptor = new AioAcceptor(this);
// 开始接收客户端连接
accept();
}
/**
* 接收一个客户端连接
*/
public void accept() {
server.accept(null, aioAcceptor);
}
@Override
public void start(int port) {
try {
initDispatcherServlet();
initServerSocket(port);
log.info("服务器启动");
} catch (Exception e) {
e.printStackTrace();
log.info("初始化服务器失败");
close();
}
}
@Override
public void close() {
aioDispatcher.shutdown();
try {
server.close();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 执行读已就绪的客户端连接的请求
* @param socketWrapper
*/
public void execute(AioSocketWrapper socketWrapper) {
aioDispatcher.doDispatch(socketWrapper);
}
}
@Slf4j
public class AioDispatcher extends AbstractDispatcher {
@Override
public void doDispatch(SocketWrapper socketWrapper) {
AioSocketWrapper aioSocketWrapper = (AioSocketWrapper) socketWrapper;
ByteBuffer buffer = ByteBuffer.allocate(1024);
aioSocketWrapper.getSocketChannel().read(buffer, buffer, new CompletionHandler() {
@Override
public void completed(Integer result, ByteBuffer attachment) {
Request request = null;
Response response = null;
try {
//解析请求
request = new Request(attachment.array());
response = new Response();
pool.execute(new AioRequestHandler(aioSocketWrapper, servletContext, exceptionHandler, resourceHandler, this, request, response));
} catch (ServletException e) {
exceptionHandler.handle(e, response, aioSocketWrapper);
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void failed(Throwable e, ByteBuffer attachment) {
log.error("read failed");
e.printStackTrace();
}
});
}
}
@Setter
@Getter
@Slf4j
public class AioRequestHandler extends AbstractRequestHandler {
private CompletionHandler readHandler;
public AioRequestHandler(SocketWrapper socketWrapper, ServletContext servletContext, ExceptionHandler exceptionHandler, ResourceHandler resourceHandler, CompletionHandler readHandler, Request request, Response response) throws ServletNotFoundException, FilterNotFoundException {
super(socketWrapper, servletContext, exceptionHandler, resourceHandler,request,response);
this.readHandler = readHandler;
}
/**
* 写回后重新调用readHandler,进行读取(猜测AIO也是保活的)
*/
@Override
public void flushResponse() {
isFinished = true;
ByteBuffer[] responseData = response.getResponseByteBuffer();
AioSocketWrapper aioSocketWrapper = (AioSocketWrapper) socketWrapper;
AsynchronousSocketChannel socketChannel = aioSocketWrapper.getSocketChannel();
socketChannel.write(responseData, 0, 2, 0L, TimeUnit.MILLISECONDS, null, new CompletionHandler() {
@Override
public void completed(Long result, Object attachment) {
log.info("写入完毕...");
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
socketChannel.read(byteBuffer, byteBuffer, readHandler);
}
@Override
public void failed(Throwable e, Object attachment) {
log.info("写入失败...");
e.printStackTrace();
}
});
WebApplication.getServletContext().afterRequestDestroyed(request);
}
}
@Slf4j
@Data
public class AioSocketWrapper implements SocketWrapper {
private AioEndpoint server;
private AsynchronousSocketChannel socketChannel;
private volatile long waitBegin;
private volatile boolean isWorking;
public AioSocketWrapper(AioEndpoint server, AsynchronousSocketChannel socketChannel) {
this.server = server;
this.socketChannel = socketChannel;
this.isWorking = false;
}
public void close() throws IOException {
socketChannel.close();
}
@Override
public String toString() {
return socketChannel.toString();
}
}
使用JMeter对三个版本都进行了压力测试,仅仅是访问一个静态页面,目的还是测试网络部分的性能有什么区别。
使用JMeter进行压力测试:connection:close
- 2个线程,每个线程循环访问10000次,吞吐量为556个请求/sec,平均响应时间为3ms
- 20个线程,每个线程循环访问1000次,吞吐量为650个请求/sec,平均响应时间为22ms
- 200个线程,每个线程循环访问100次,吞吐量为644个请求/sec,平均响应时间为209ms
- 1000个线程,每个线程循环访问20次,吞吐量为755个请求/sec,平均响应时间为774ms
使用JMeter进行压力测试:connection:keep-alive
使用JMeter进行压力测试:connection:keep-alive
总体上感觉性能差不多,可能是没有测试出瓶颈,我对压力测试还是不够了解,另外其实最好是压测机器和测试机器分开来,现在还没有这个条件,准备过一段时间再重新设计这个压力测试。
主要是在RequestHandler中的实现,先调用filter,再调用servlet。
public interface Filter {
/**
* 过滤器初始化
*/
void init();
/**
* 过滤
* @param request
* @param response
* @param filterChain
*/
void doFilter(Request request, Response response,FilterChain filterChain) ;
/**
* 过滤器销毁
*/
void destroy();
}
public interface FilterChain {
/**
* 当前filter放行,由后续的filter继续进行过滤
* @param request
* @param response
*/
void doFilter(Request request,Response response) ;
}
这里有一个递归的过程。
run方法开始执行会先调用第一个filter,第一个filter如果放行,那么会调用filterChain(也就是requestHandler,this)的doFilter,此时会将filterIndex++,然后调用下一个filter的doFilter方法,如此反复,直至所有filter都被调用,此时将调用service方法,执行servlet业务逻辑。如果不放行,那么会在某个filter的doFilter执行完毕后结束整个逻辑(一般需要进行重定向)。
@Slf4j
@Getter
public abstract class AbstractRequestHandler implements FilterChain, Runnable {
protected Request request;
protected Response response;
protected SocketWrapper socketWrapper;
protected ServletContext servletContext;
protected ExceptionHandler exceptionHandler;
protected ResourceHandler resourceHandler;
protected boolean isFinished;
protected Servlet servlet;
protected List filters;
private int filterIndex = 0;
public AbstractRequestHandler(SocketWrapper socketWrapper, ServletContext servletContext, ExceptionHandler exceptionHandler, ResourceHandler resourceHandler, Request request, Response response) throws ServletNotFoundException, FilterNotFoundException {
this.socketWrapper = socketWrapper;
this.servletContext = servletContext;
this.exceptionHandler = exceptionHandler;
this.resourceHandler = resourceHandler;
this.isFinished = false;
this.request = request;
this.response = response;
request.setServletContext(servletContext);
request.setRequestHandler(this);
response.setRequestHandler(this);
// 根据url查询匹配的servlet,结果是0个或1个
servlet = servletContext.mapServlet(request.getUrl());
// 根据url查询匹配的filter,结果是0个或多个
filters = servletContext.mapFilter(request.getUrl());
}
/**
* 入口
*/
@Override
public void run() {
// 如果没有filter,则直接执行servlet
if (filters.isEmpty()) {
service();
} else {
// 先执行filter
doFilter(request, response);
}
}
/**
* 递归执行,自定义filter中如果同意放行,那么会调用filterChain(也就是requestHandler)的doiFilter方法,
* 此时会执行下一个filter的doFilter方法;
* 如果不放行,那么会在sendRedirect之后将响应数据写回客户端,结束;
* 如果所有Filter都执行完毕,那么会调用service方法,执行servlet逻辑
* @param request
* @param response
*/
@Override
public void doFilter(Request request, Response response) {
if (filterIndex < filters.size()) {
filters.get(filterIndex++).doFilter(request, response, this);
} else {
service();
}
}
/**
* 调用servlet
*/
private void service() {
try {
//处理动态资源,交由某个Servlet执行
//Servlet是单例多线程
//Servlet在RequestHandler中执行
servlet.service(request, response);
} catch (ServletException e) {
exceptionHandler.handle(e, response, socketWrapper);
} catch (Exception e) {
//其他未知异常
e.printStackTrace();
exceptionHandler.handle(new ServerErrorException(), response, socketWrapper);
} finally {
if (!isFinished) {
flushResponse();
}
}
log.info("请求处理完毕");
}
/**
* 响应数据写回到客户端
*/
public abstract void flushResponse();
}
Filter映射是实现了如何通过url查找到与之对应的filter,servlet查找同理。
有几种匹配方式,比如精确匹配, 它的优先级是最高的,其次是路径匹配,比如/*,注意这里可能会有多个匹配,就servlet而言,会选择最合适的那个(最具体的,路径最长的),就filter而言,会将所有匹配的都找出来。
其实Tomcat的web.xml中匹配全部路径是/*
,它的Matcher是自己实现的,这部分代码比较复杂, 我不打算重写一遍了,于是就用了Spring提供的AntPathMatcher,它实现了Ant风格的路径匹配器,它的匹配全部路径是/**
,读者不要感觉奇怪。
注意servlet、filter都是延迟加载的,只有在第一次访问时才会被初始化。
/**
* 由URL得到对应的一个Servlet实例
*
* @param url
* @return
* @throws ServletNotFoundException
*/
public Servlet mapServlet(String url) throws ServletNotFoundException {
// 1、精确匹配
String servletAlias = servletMapping.get(url);
if (servletAlias != null) {
return initAndGetServlet(servletAlias);
}
// 2、路径匹配
List matchingPatterns = new ArrayList<>();
Set patterns = servletMapping.keySet();
for (String pattern : patterns) {
if (matcher.match(pattern, url)) {
matchingPatterns.add(pattern);
}
}
if (!matchingPatterns.isEmpty()) {
Comparator patternComparator = matcher.getPatternComparator(url);
Collections.sort(matchingPatterns, patternComparator);
String bestMatch = matchingPatterns.get(0);
return initAndGetServlet(bestMatch);
}
return initAndGetServlet(DEFAULT_SERVLET_ALIAS);
}
/**
* 初始化并获取Servlet实例,如果已经初始化过则直接返回
*
* @param servletAlias
* @return
* @throws ServletNotFoundException
*/
private Servlet initAndGetServlet(String servletAlias) throws ServletNotFoundException {
ServletHolder servletHolder = servlets.get(servletAlias);
if (servletHolder == null) {
throw new ServletNotFoundException();
}
if (servletHolder.getServlet() == null) {
try {
Servlet servlet = (Servlet) Class.forName(servletHolder.getServletClass()).newInstance();
servlet.init();
servletHolder.setServlet(servlet);
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
return servletHolder.getServlet();
}
/**
* 由URL得到一系列匹配的Filter实例
*
* @param url
* @return
*/
public List mapFilter(String url) throws FilterNotFoundException {
List matchingPatterns = new ArrayList<>();
Set patterns = filterMapping.keySet();
for (String pattern : patterns) {
if (matcher.match(pattern, url)) {
matchingPatterns.add(pattern);
}
}
Set filterAliases = matchingPatterns.stream().flatMap(pattern -> this.filterMapping.get(pattern).stream()).collect(Collectors.toSet());
List result = new ArrayList<>();
for (String alias : filterAliases) {
result.add(initAndGetFilter(alias));
}
return result;
}
/**
* 初始化并返回Filter实例,如果已经初始化过则直接返回
*
* @param filterAlias
* @return
* @throws FilterNotFoundException
*/
private Filter initAndGetFilter(String filterAlias) throws FilterNotFoundException {
FilterHolder filterHolder = filters.get(filterAlias);
if (filterHolder == null) {
throw new FilterNotFoundException();
}
if (filterHolder.getFilter() == null) {
try {
Filter filter = (Filter) Class.forName(filterHolder.getFilterClass()).newInstance();
filter.init();
filterHolder.setFilter(filter);
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
return filterHolder.getFilter();
}
public interface ServletContextListener extends EventListener {
/**
* 应用启动
* @param sce
*/
void contextInitialized(ServletContextEvent sce);
/**
* 应用关闭
* @param sce
*/
void contextDestroyed(ServletContextEvent sce);
}
public interface HttpSessionListener extends EventListener {
/**
* session创建
* @param se
*/
void sessionCreated(HttpSessionEvent se);
/**
* session销毁
* @param se
*/
void sessionDestroyed(HttpSessionEvent se);
}
public interface ServletRequestListener extends EventListener {
/**
* 请求初始化
* @param sre
*/
void requestInitialized(ServletRequestEvent sre);
/**
* 请求销毁
* @param sre
*/
void requestDestroyed(ServletRequestEvent sre);
}
listener的加载过程是在servletContext,其实servlet和filter也是,只是不必把代码贴出来了,注意listener是单例的,同一个listener实例实现了多种listener接口,不要多次创建。
先看示例项目的web.xml:
<web-app>
<servlet>
<servlet-name>LoginServletservlet-name>
<servlet-class>com.sinjinsong.webserver.sample.web.servlet.LoginServletservlet-class>
servlet>
<servlet>
<servlet-name>LogoutServletservlet-name>
<servlet-class>com.sinjinsong.webserver.sample.web.servlet.LogoutServletservlet-class>
servlet>
<servlet>
<servlet-name>UserServletservlet-name>
<servlet-class>com.sinjinsong.webserver.sample.web.servlet.UserServletservlet-class>
servlet>
<servlet>
<servlet-name>UserEditServletservlet-name>
<servlet-class>com.sinjinsong.webserver.sample.web.servlet.UserEditServletservlet-class>
servlet>
<servlet>
<servlet-name>DefaultServletservlet-name>
<servlet-class>com.sinjinsong.webserver.core.servlet.impl.DefaultServletservlet-class>
servlet>
<servlet-mapping>
<servlet-name>LoginServletservlet-name>
<url-pattern>/loginurl-pattern>
servlet-mapping>
<servlet-mapping>
<servlet-name>LogoutServletservlet-name>
<url-pattern>/logouturl-pattern>
servlet-mapping>
<servlet-mapping>
<servlet-name>UserServletservlet-name>
<url-pattern>/userurl-pattern>
servlet-mapping>
<servlet-mapping>
<servlet-name>UserEditServletservlet-name>
<url-pattern>/user/editurl-pattern>
servlet-mapping>
<servlet-mapping>
<servlet-name>DefaultServletservlet-name>
<url-pattern>/url-pattern>
servlet-mapping>
<filter>
<filter-name>LoginFilterfilter-name>
<filter-class>com.sinjinsong.webserver.sample.web.filter.LoginFilterfilter-class>
filter>
<filter>
<filter-name>LogFilterfilter-name>
<filter-class>com.sinjinsong.webserver.sample.web.filter.LogFilterfilter-class>
filter>
<filter-mapping>
<filter-name>LoginFilterfilter-name>
<url-pattern>/**url-pattern>
filter-mapping>
<filter-mapping>
<filter-name>LogFilterfilter-name>
<url-pattern>/**url-pattern>
filter-mapping>
<listener>
<listener-class>com.sinjinsong.webserver.sample.web.listener.ServletContextAndSessionListenerlistener-class>
<listener-class>com.sinjinsong.webserver.sample.web.listener.MyServletRequestListenerlistener-class>
listener>
web-app>
这里有几个listener,下面看一下它们是怎么加载的。
这部分涉及XML的解析,我使用了dom4j来解析。就listener而言,需要动态判断实现了哪些listener接口,然后保存起来,待到合适时机时被调用。
/**
* web.xml文件解析,比如servlet,filter,listener等
*
* @throws ClassNotFoundException
* @throws IllegalAccessException
* @throws InstantiationException
*/
private void parseConfig() throws ClassNotFoundException, IllegalAccessException, InstantiationException {
Document doc = XMLUtil.getDocument(ServletContext.class.getResourceAsStream("/web.xml"));
Element root = doc.getRootElement();
// 解析servlet
List servlets = root.elements("servlet");
for (Element servletEle : servlets) {
String key = servletEle.element("servlet-name").getText();
String value = servletEle.element("servlet-class").getText();
this.servlets.put(key, new ServletHolder(value));
}
List servletMapping = root.elements("servlet-mapping");
for (Element mapping : servletMapping) {
List urlPatterns = mapping.elements("url-pattern");
String value = mapping.element("servlet-name").getText();
for (Element urlPattern : urlPatterns) {
this.servletMapping.put(urlPattern.getText(), value);
}
}
// 解析 filter
List filters = root.elements("filter");
for (Element filterEle : filters) {
String key = filterEle.element("filter-name").getText();
String value = filterEle.element("filter-class").getText();
this.filters.put(key, new FilterHolder(value));
}
List filterMapping = root.elements("filter-mapping");
for (Element mapping : filterMapping) {
List urlPatterns = mapping.elements("url-pattern");
String value = mapping.element("filter-name").getText();
for (Element urlPattern : urlPatterns) {
List values = this.filterMapping.get(urlPattern.getText());
if (values == null) {
values = new ArrayList<>();
this.filterMapping.put(urlPattern.getText(), values);
}
values.add(value);
}
}
// 解析listener
Element listener = root.element("listener");
List listenerEles = listener.elements("listener-class");
for (Element listenerEle : listenerEles) {
EventListener eventListener = (EventListener) Class.forName(listenerEle.getText()).newInstance();
if (eventListener instanceof ServletContextListener) {
servletContextListeners.add((ServletContextListener) eventListener);
}
if (eventListener instanceof HttpSessionListener) {
httpSessionListeners.add((HttpSessionListener) eventListener);
}
if (eventListener instanceof ServletRequestListener) {
servletRequestListeners.add((ServletRequestListener) eventListener);
}
}
}
以HttpSessionListener为例:
在ServletContext中createSession时会回调listener的create方法。
/**
* 创建session
* @param response
* @return
*/
public HttpSession createSession(Response response) {
HttpSession session = new HttpSession(UUIDUtil.uuid());
sessions.put(session.getId(), session);
response.addCookie(new Cookie("JSESSIONID", session.getId()));
HttpSessionEvent httpSessionEvent = new HttpSessionEvent(session);
for (HttpSessionListener listener : httpSessionListeners) {
listener.sessionCreated(httpSessionEvent);
}
return session;
}
销毁session时会回调listener的destroy方法。
/**
* 销毁session
* @param session
*/
public void invalidateSession(HttpSession session) {
sessions.remove(session.getId());
afterSessionDestroyed(session);
}
/**
* 清除空闲的session
* 由于ConcurrentHashMap是线程安全的,所以remove不需要进行加锁
*/
public void cleanIdleSessions() {
for (Iterator> it = sessions.entrySet().iterator(); it.hasNext(); ) {
Map.Entry entry = it.next();
if (Duration.between(entry.getValue().getLastAccessed(), Instant.now()).getSeconds() >= DEFAULT_SESSION_EXPIRE_TIME) {
// log.info("该session {} 已过期", entry.getKey());
afterSessionDestroyed(entry.getValue());
it.remove();
}
}
}
private void afterSessionDestroyed(HttpSession session) {
HttpSessionEvent httpSessionEvent = new HttpSessionEvent(session);
for (HttpSessionListener listener : httpSessionListeners) {
listener.sessionDestroyed(httpSessionEvent);
}
}
这里的增强是指原来是仅支持如requestScope.username的表达式,现在支持requestScope.a.b.c….这样较为负责的表达式,实现原理就是基于反射获取属性值。
@Slf4j
public class TemplateResolver {
public static final Pattern regex = Pattern.compile("\\$\\{(.*?)}");
public static String resolve(String content, Request request) throws TemplateResolveException {
Matcher matcher = regex.matcher(content);
StringBuffer sb = new StringBuffer();
while (matcher.find()) {
log.info("{}", matcher.group(1));
// placeHolder 格式为scope.x.y.z
// scope值为requestScope,sessionScope,applicationScope
String placeHolder = matcher.group(1);
if (placeHolder.indexOf('.') == -1) {
throw new TemplateResolveException();
}
ModelScope scope = ModelScope
.valueOf(
placeHolder.substring(0, placeHolder.indexOf('.'))
.replace("Scope", "")
.toUpperCase());
// key 格式为x.y.z
String key = placeHolder.substring(placeHolder.indexOf('.') + 1);
if (scope == null) {
throw new TemplateResolveException();
}
Object value = null;
// 按照.分隔为数组,格式为[x,y,z]
String[] segments = key.split("\\.");
log.info("key: {} , segments:{}", key,Arrays.toString(segments));
switch (scope) {
case REQUEST:
value = request.getAttribute(segments[0]);
break;
case SESSION:
value = request.getSession().getAttribute(segments[0]);
break;
case APPLICATION:
value = request.getServletContext().getAttribute(segments[0]);
break;
default:
break;
}
// 此时value为x,如果没有y、z,那么会直接返回;如果有,就会递归地进行属性读取(基于反射)
if (segments.length > 1) {
try {
value = parse(value, segments, 1);
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
throw new TemplateResolveException();
}
}
log.info("value:{}", value);
// 如果解析得到的值为null,则将占位符去掉;否则将占位符替换为值
if (value == null) {
matcher.appendReplacement(sb, "");
} else {
//把group(1)得到的数据,替换为value
matcher.appendReplacement(sb, value.toString());
}
}
// 将源文件后续部分添加至尾部
matcher.appendTail(sb);
String result = sb.toString();
return result.length() == 0 ? content : result;
}
/**
* 基于反射实现多级查询,比如user.dept.name
*
* @param segments
* @return
*/
private static Object parse(Object value, String[] segments, int index) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
if (index == segments.length) {
return value;
}
Method method = value.getClass().getMethod("get" + StringUtils.capitalize(segments[index]), new Class[0]);
return parse(method.invoke(value, new Object[0]), segments, index + 1);
}
}
这个项目在春招实习中经常被问到,而一个普通的网站项目却无人问津,足以看出有一些想法和设计的轮子项目是面试官比较喜欢的,重复造轮子是有价值的;另外在写这种轮子的时候感觉也很好,很多地方都需要仔细设计,没有在开发普通web项目时的那种“全是套路”的感觉。希望大家在寻求技术提升的时候也可以多多尝试造轮子的方法。
WebServer项目目前来看还是有一些提升空间的,比如对WebSocket的支持、异步Servlet的支持,以及尝试一些支持更高并发量的网络模型(比如Tomcat的APR);其压力测试也有很多的优化空间。就目前而言,该项目的维护将暂告一段落,可能下一次的更新要等到2019年的春夏学期,如果有任何疑问和建议,欢迎评论或者在Github上提issue。