SpringBoot之仿写Tomcat实现方式的程序
前提
在使用SpringBoot 的时候,也就一直再想,怎么去思考下他的部分底层,于是,开始对Tomcat实现方式的程序进行编写.
该程序通过使用,Java提供的socket,线程池,io流,String,map等API,模拟服务端对客户端的请求解析与结果响应的过程,然后我自身在创建了annotation注解,并使用dispatchServelet,handlerMapping对controller类和方法进行关系的映射与调用,从而实现了spring mvc对于controller中不同类型的传参方式.最后通过对项目中的容器和默认配置进行打包,最终程序可以按照SpringBoot开箱即用的方式来进行运行.
接下来是代码部分
一 WebServer 启动类
其中使用时socket,还有线程池的知识
package com.webserver.core;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* WebServer web容器
* 用于实现Tomcat基本功能。
*/
public class WebServerApplication {
private ServerSocket serverSocket;
private ExecutorService threadPool;
public WebServerApplication(){
try {
System.out.println("正在启动服务端...");
serverSocket = new ServerSocket(8088);
threadPool = Executors.newFixedThreadPool(50)//设置线程池大小;
System.out.println("服务端启动完毕!");
} catch (IOException e) {
e.printStackTrace();
}
}
public void start(){
try {
while(true) {
System.out.println("等待客户端连接...");
Socket socket = serverSocket.accept();
System.out.println("一个客户端连接了!");
//启动一个线程处理该客户端的交互
ClientHandler handler = new ClientHandler(socket);
threadPool.execute(handler);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
WebServerApplication server = new WebServerApplication();
server.start();
}
}
二 客户端
用于连接操作
package com.webserver.core;
import com.webserver.http.EmptyRequestException;
import com.webserver.http.HttpServletRequest;
import com.webserver.http.HttpServletResponse;
import java.io.IOException;
import java.net.Socket;
/**
* 与客户端完成一次HTTP的交互
* 按照HTTP协议要求,与客户端完成一次交互流程为一问一答
* 因此,这里分为三步完成该工作:
* 1:解析请求 目的:将浏览器发送的请求内容读取并整理
* 2:处理请求 目的:根据浏览器的请求进行对应的处理工作
* 3:发送响应 目的:将服务端的处理结果回馈给浏览器
*
*
*/
public class ClientHandler implements Runnable{
private Socket socket;
public ClientHandler(Socket socket){
this.socket = socket;
}
public void run(){
try {
//1解析请求,实例化请求对象的过程就是解析的过程
HttpServletRequest request = new HttpServletRequest(socket);
HttpServletResponse response = new HttpServletResponse(socket);
//2处理请求
DispatcherServlet.getInstance().service(request,response);
//3发送响应
response.response();
} catch (IOException e) {
e.printStackTrace();
} catch (EmptyRequestException e) {
} finally {
//按照HTTP协议要求,一问一答后断开连接
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
三 DispatcherServlet
用于接收客户端发过来的请求并处理
package com.webserver.core;
import com.webserver.annotations.Controller;
import com.webserver.annotations.RequestMapping;
import com.webserver.controller.UserController;
import com.webserver.http.HttpServletRequest;
import com.webserver.http.HttpServletResponse;
import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URISyntaxException;
/**
* 用于完成一个http交互流程中处理请求的环节工作.
* 实际上这个类是Spring MVC框架提供的一个核心的类,用于和Web容器(Tomcat)整合,
* 使得处理请求的环节可以由Spring MVC框架完成.
*/
public class DispatcherServlet {
private static DispatcherServlet instance = new DispatcherServlet();
private static File dir;
private static File staticDir;
static {
//定位环境变量ClassPath(类加载路径)中"."的位置
//在IDEA中执行项目时,类加载路径是从target/classes开始的
try {
dir = new File(
DispatcherServlet.class.getClassLoader()
.getResource(".").toURI()
);
//定位target/classes/static目录
staticDir = new File(dir, "static");
} catch (URISyntaxException e) {
e.printStackTrace();
}
}
private DispatcherServlet() {
}
public static DispatcherServlet getInstance() {
return instance;
}
/**
* 处理请求的方法
*
* @param request 请求对象,通过这个对象可以获取来自浏览器提交的内容
* @param response 响应对象,通过设置响应对象将处理结果最终发送给浏览器
*/
public void service(HttpServletRequest request, HttpServletResponse response) {
String path = request.getRequestURI();
System.out.println("请求的抽象路径:" + path);
//path====>/regUser
//首先判断该请求是否为请求一个业务
try {
Method method = HandlerMapping.getMethod(path);
if(method!=null){
method.invoke(method.getDeclaringClass().newInstance(),
request,response);
return;
}
} catch (Exception e) {
e.printStackTrace();
}
File file = new File(staticDir, path);
if (file.isFile()) {//浏览器请求的资源是否存在且是一个文件
//正确响应其请求的文件
response.setContentFile(file);
} else {
//响应404
response.setStatusCode(404);
response.setStatusReason("NotFound");
file = new File(staticDir, "/root/404.html");
response.setContentFile(file);
}
}
}
四 HandlerMapping
使用反射,用来调用DispatcherServlet的各类请求
package com.webserver.core;
import com.webserver.annotations.Controller;
import com.webserver.annotations.RequestMapping;
import java.io.File;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
/**
* 维护请求路径对对应的业务处理方法(某个Controller的某个方法)
*/
public class HandlerMapping {
/*
key:请求路径 例如:/regUser
value:方法对象(Method实例) 例如:表示Controller的reg方法的Method对象
*/
private static Map<String, Method> mapping = new HashMap<>();
static {
initMapping();
}
private static void initMapping(){
try {
File dir = new File(
HandlerMapping.class.getClassLoader()
.getResource(".").toURI()
);
File controllerDir = new File(dir,"/com/webserver/controller");
File[] subs = controllerDir.listFiles(f->f.getName().endsWith(".class"));
for(File sub : subs){//遍历目录中所有的.class文件
String fileName = sub.getName();//获取文件名
String className = fileName.substring(0,fileName.indexOf("."));//根据文件名截取出类名
Class cls = Class.forName("com.webserver.controller."+className);//加载类对象
if(cls.isAnnotationPresent(Controller.class)){//判断这个类是否被@Controller注解标注
Method[] methods = cls.getDeclaredMethods();//获取这个类定义的所有方法
for(Method method : methods){//遍历每一个方法
if(method.isAnnotationPresent(RequestMapping.class)){//判断该方法是否被@RequestMapping注解标注
RequestMapping rm = method.getAnnotation(RequestMapping.class);//获取该方法上的注解@RequestMapping
String value = rm.value();//获取该注解上定义的参数
mapping.put(value,method);
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 根据请求路径返回对应的处理方法
* @param path
* @return
*/
public static Method getMethod(String path){
return mapping.get(path);
}
public static void main(String[] args) {
Method method = mapping.get("/regUser");
//通过方法对象可以获取其所属的类的类对象
Class cls = method.getDeclaringClass();
System.out.println(cls);
System.out.println(method);
}
}
五 HttpServletRequest
解析消息头,解析请求行,解析请求正文
package com.webserver.http;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.Socket;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
public class HttpServletRequest {
private Socket socket;
//请求行相关信息
private String method;//请求方式
private String uri;//抽象路径
private String protocol;//协议版本
private String requestURI;//抽象路径中请求部分,即:uri中"?"左侧的内容
private String queryString;//抽象路径中参数部分,即:uri中"?"右侧的内容
private Map<String,String> parameters = new HashMap<>();//保存每一组参数
//消息头相关信息
private Map<String,String> headers = new HashMap<>();
public HttpServletRequest(Socket socket) throws IOException, EmptyRequestException {
this.socket = socket;
//1解析请求行
parseRequestLine();
//2解析消息头
parseHeaders();
//3解析消息正文
parseContent();
}
//解析请求行
private void parseRequestLine() throws IOException, EmptyRequestException {
String line = readLine();
if(line.isEmpty()){//若请求行是空字符串,则说明本次为空请求,并抛出异常
throw new EmptyRequestException("request is empty");
}
System.out.println("请求行:"+line);
String[] data = line.split("\\s");
method = data[0];
uri = data[1];
protocol = data[2];
//进一步解析uri
parseURI();
System.out.println("method:"+method);
System.out.println("uri:"+uri);
System.out.println("protocol:"+protocol);
}
//进一步解析uri
private void parseURI(){
/*
uri有两种情况:
1:不含有参数的
例如: /index.html
直接将uri的值赋值给requestURI即可.
2:含有参数的
例如:/regUser?username=fancq&password=&nickname=chuanqi&age=22
将uri中"?"左侧的请求部分赋值给requestURI
将uri中"?"右侧的参数部分赋值给queryString
将参数部分首先按照"&"拆分出每一组参数,再将每一组参数按照"="拆分为参数名与参数值
并将参数名作为key,参数值作为value存入到parameters中。
*/
String[] data = uri.split("\\?");
requestURI = data[0];
if(data.length>1){//有参数
//queryString:username=fancq&password=&nickname=chuanqi&age=22
queryString = data[1];
parseParameter(queryString);
}
System.out.println("requestURI:"+requestURI);
System.out.println("queryString:"+queryString);
System.out.println("parameters:"+parameters);
}
/**
* 解析参数
* 参数的格式应当为:name1=value1&name2=value2&...
* @param line
*/
private void parseParameter(String line){
try {
//中文格式的参数
line = URLDecoder.decode(line,"UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
//paras:[username=fancq, password=, nickname=chuanqi, age=22]
String[] paras = line.split("&");
//para:username=
for(String para : paras){
//array:[username,fancq] 若没参数值array:[password]
String[] array = para.split("=",2);
parameters.put(array[0],array[1]);
}
}
//解析消息头
private void parseHeaders() throws IOException {
//读取消息头
while(true) {
String line = readLine();
if(line.isEmpty()){
break;
}
System.out.println("消息头:" + line);
String[] data = line.split(":\\s");
//key:Connection value:keep-alive
headers.put(data[0],data[1]);//key:消息头的名字 value:消息头的值
}
System.out.println("headers:"+headers);
}
//解析消息正文
private void parseContent() throws IOException {
//请求方式是否为POST请求
if("post".equalsIgnoreCase(method)){
if(headers.containsKey("Content-Length")) {
//根据消息头Content-Length确定正文长度
int contentLength = Integer.parseInt(
headers.get("Content-Length")
);
System.out.println("正文长度:"+contentLength);
//读取正文数据
InputStream in = socket.getInputStream();
byte[] data = new byte[contentLength];
in.read(data);
/*
根据Content-Type来分析正文是什么以便进行对应的处理
*/
String contentType = headers.get("Content-Type");
if("application/x-www-form-urlencoded".equals(contentType)){//是否为form表单提交数据
String line = new String(data, StandardCharsets.ISO_8859_1);
parseParameter(line);
}
//TODO
// else if(){//比如判断表单提交时附带附件的.
//
// }
}
}
}
private String readLine() throws IOException {
//当对同一个socket调用多次getInputStream方法时,获取回来的输入流始终是同一条流
InputStream in = socket.getInputStream();
int d;
StringBuilder builder = new StringBuilder();
char pre='a',cur='a';
while((d = in.read())!=-1){
cur = (char)d;
if(pre==13&&cur==10){
break;
}
builder.append(cur);
pre = cur;
}
return builder.toString().trim();
}
public String getMethod() {
return method;
}
public String getUri() {
return uri;
}
public String getProtocol() {
return protocol;
}
public String getHeader(String name) {
return headers.get(name);
}
public String getRequestURI() {
return requestURI;
}
public String getQueryString() {
return queryString;
}
public String getParameter(String name) {
return parameters.get(name);
}
}
六 HttpServletResponse,响应,发送状态行,发送响应头,发送响应正文
package com.webserver.http;
import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* 响应对象
* 该类的每一个实例用于表示一个HTTP协议要求的响应内容
* 每个响应由三部分构成:
* 状态行,响应头,响应正文
*/
public class HttpServletResponse {
private Socket socket;
//状态行相关信息
private int statusCode = 200;
private String statusReason = "OK";
//响应头相关信息
private Map<String,String> headers = new HashMap<>();
//响应正文相关信息
private File contentFile;//正文对应的实体文件
//动态数据可以先通过该流写出到其内部维护的字节数组中,发送响应时将该数组内容作为正文
private ByteArrayOutputStream out;
public HttpServletResponse(Socket socket){
this.socket = socket;
}
/**
* 将当前响应对象内容按照标准的响应格式发送给客户端
*/
public void response() throws IOException {
//发送前的准备工作
sendBefore();
//发送状态行
sendStatusLine();
//发送响应头
sendHeaders();
//发送响应正文
sendContent();
}
//发送响应前的准备工作
private void sendBefore(){
if(out!=null){//说明有动态数据
//根据动态数据长度添加响应头Content-Length
addHeader("Content-Length",out.size()+"");
}
}
//发送状态行
private void sendStatusLine() throws IOException {
// HTTP/1.1 200 OK
println("HTTP/1.1" + " " + statusCode + " " + statusReason);
}
//发送响应头
private void sendHeaders() throws IOException {
/*
headers:
key value
Content-Type text/html
Content-Length 245
... ...
*/
Set<Map.Entry<String,String>> entrySet = headers.entrySet();
for(Map.Entry<String,String> e: entrySet){
String name = e.getKey();
String value = e.getValue();
println(name + ": " + value);
}
//单独发送个回车+换行表示响应头发送完毕
println("");
}
//发送响应正文
private void sendContent() throws IOException {
OutputStream out = socket.getOutputStream();
if(this.out!=null){
byte[] data = this.out.toByteArray();
out.write(data);//将动态数据作为正文发送给浏览器
}else if(contentFile!=null) {
FileInputStream fis = new FileInputStream(contentFile);
byte[] buf = new byte[1024 * 10];//10kb
int len = 0;//记录每次实际读取的字节数
while ((len = fis.read(buf)) != -1) {
out.write(buf, 0, len);
}
}
}
private void println(String line) throws IOException {
OutputStream out = socket.getOutputStream();
byte[] data = line.getBytes(StandardCharsets.ISO_8859_1);
out.write(data);
out.write(13);
out.write(10);
}
public int getStatusCode() {
return statusCode;
}
public void setStatusCode(int statusCode) {
this.statusCode = statusCode;
}
public String getStatusReason() {
return statusReason;
}
public void setStatusReason(String statusReason) {
this.statusReason = statusReason;
}
public File getContentFile() {
return contentFile;
}
public void setContentFile(File contentFile) {
this.contentFile = contentFile;
//添加用于说明正文的响应头Content-Type和Content-Length
try {
String contentType = Files.probeContentType(contentFile.toPath());
/*
如果根据正文文件分析出了Content-Type的值则设置该响应头.
HTTP协议规定如果服务端发送的响应中没有包含这个头,就表明让浏览器自行判断
响应正文的内容类型.
*/
if(contentType!=null){
addHeader("Content-Type",contentType);
}
} catch (IOException e) {
e.printStackTrace();
}
addHeader("Content-Length",contentFile.length()+"");
}
/**
* 添加一个响应头
* @param name 响应头的名字
* @param value 响应头的值
*/
public void addHeader(String name,String value){
headers.put(name,value);
}
/**
* 发送重定向响应,要求浏览器重新请求path指定的位置.
* @param path
*/
public void sendRedirect(String path){
//重定向的状态代码为302
statusCode = 302;
statusReason = "MovedTemporarily";
//响应头Location
addHeader("Location",path);
}
/**
* 获取字节输出流,通过这个流写出的所有字节最终都会作为响应正文发送给客户端
* @return
*/
public OutputStream getOutputStream(){
if(out==null){
out = new ByteArrayOutputStream();
}
return out;
}
public PrintWriter getWriter(){
return new PrintWriter(
new BufferedWriter(
new OutputStreamWriter(
getOutputStream(),
StandardCharsets.UTF_8
)
),true
);
}
/**
* 添加响应头Content-Type
* @param mime
*/
public void setContentType(String mime){
addHeader("Content-Type",mime);
}
}
七 空请求异常类
package com.webserver.http;
/**
* 空请求异常
* 当HttpServletRequest在解析请求时发现本次为空请求就会抛出这个异常
*/
public class EmptyRequestException extends Exception{
public EmptyRequestException() {
}
public EmptyRequestException(String message) {
super(message);
}
public EmptyRequestException(String message, Throwable cause) {
super(message, cause);
}
public EmptyRequestException(Throwable cause) {
super(cause);
}
public EmptyRequestException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
八 Controller 和 RequestMapping 的注解类
package com.webserver.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Controller {
}
package com.webserver.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestMapping {
String value();
}
以上就是其中所用要用到的类,基本可以实现SpringBoot开箱即用的功能.
下一篇文章会介绍如何将这个项目打包,让自己可以使用…