目录
一、手写Tomcat分析
1、tomcat提供的服务:
第一:提供Socket服务
第二:进行请求的分发
第三:需要把请求和响应封装成request/response
2、tomcat的结构:
annotations(注解包)
- Controller(注解:用于描述controller类)
- RequestMapping(注解:用于描述controller类下面的业务逻辑方法,并需要向该注解传递路
core(核心包)
- Application.java(class:传入主类,用于连接客户端,并调用ClientHandler类,处理客户端的请求和响应)
- ClientHandler.java(class:传入socket,协调调用其他核心类和Http类)
- DispatcherServlet.java(class:根据请求,调用对应被@RequestMapping注解标注的处理方法)
- HandMapping.java(class:维护所有请求对对应的Controller的处理业务方法)
http(HTTP包)
HttpServletRequest.java(class:读取socket的客户端请求,并将请求进行封装)
HttpServletResponse.java(class:将响应数据进行封装,并写入socket,传递到客户端)
EmptyRequestException.java(class:异常处理类)
3、手写tomcat的流程:
创建主类 WebServerApplication类,主类使用 serverSocket 提供Socket服务,等待客户端连接,创建线程池处理与客户端的交互,有一个客户端连接就测评线程池中拿出一个线程去处理客户端的请求。
创建客户端处理类 ClientHandler类,向里面传入一个 socket 进行接收和返回客户端的数据,在该类中调用 HttpServletRequest类 和 HttpServletResponse类,解析请求,并封装请求体和响应体。调用 DispatcherServlet类 的 service方法,进行处理和分发请求。最后调用 HttpServletResponse类 的response方法,返回响应。
创建 HttpServletRequest类,向里面传入一个 socket ,通过 socket 将请求数据传输进 HttpServletRequest类,读取 socket 里的请求数据,将数据按回车符+换行符拆分成请求行、消息头、消息正文:
解析请求行,将其拆分成请求方法、抽象路径、协议版本,再将抽象路径进行进一步拆分,拆分成url和请求参数,拆分完后进行封装。
解析消息头,将其按 ": " 拆分,并按照键值对形式进行封装。
解析消息正文,拿到请求长度和请求类型,读取正文内容并封装。
创建 HttpServletResponse 类,向里面传递 socket,通过 socket 将响应数据传输给客户端。封装响应体,每个响应体封装状态行、响应头、响应正文。先判断是否有动态数据,有动态数据就获取动态数据的长度,并封装进响应头:
发送状态行,状态行默认"HTTP/1.1 200 ok",有别的请求就在设置。
发送响应头,读取请求头内容并以键值对形式封装进响应头。
发送响应正文,判断是否有动态数据,有动态数据直接发送,在判断正文是否为空,不为空,则发送响应正文。
创建 HandMapping类,通过反射去查找每个被 controller注解 标注的 controller类 的业务方法,并获取方法上的 RequestMapping注解的值,根据值将方法按照键值对形式存储。
创建 DispatcherServlet类,接收 HttpServletRequest对象和 HttpServletResponse对象,从HttpServletRequest对象获取请求路由,先通过路由去查找controller类的业务方法,进行业务逻辑处理,如果没有对应的方法,就去static目录下面找对应的资源,并返回。如果都没有找到就返回404页面。
最后通过 HttpServletResponse对象的response方法将响应数据返回给客户端。
二、手写Tomcat源码
1、创建Maven项目
2、设置pom.xml文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.zzn</groupId>
<artifactId>BirdBoot</artifactId>
<version>1.0</version>
<packaging>jar</packaging>
<properties>
<!-- 设置 JDK 版本为 1.8 -->
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.source>1.8</maven.compiler.source>
<!-- 设置编码为 UTF-8 -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.encoding>UTF-8</maven.compiler.encoding>
</properties>
</project>
annotations(注解包)
3、Controller.java
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 该注解只描述类,且只在程序运行时保留
* 描述controller类
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Controller {}
4、RequestMapping.java
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 该注解只描述方法,且只在程序运行时保留
* 描述controller类下面的业务逻辑方法,并需要向注解传递路由
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestMapping {
String value();
}
core(核心包)
5、Application.java
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 启动类:提供Socket服务
*/
public class WebServerApplication {
private ServerSocket serverSocket; //服务套接字
private ExecutorService threadPool; //线程池
protected static Class BootClass; //启动类的类对象
/**
* 构造方法
*/
public WebServerApplication(){
try{
System.out.println("正在启动服务器...");
serverSocket = new ServerSocket(8088);
threadPool = Executors.newFixedThreadPool(50);//创建线程池,设置50个线程
System.out.println("服务器启动完毕");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
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) {
throw new RuntimeException(e);
}
}
/**
* 主程序
* @param args
*/
public static void run(Class cls, String[] args) {
BootClass = cls;
WebServerApplication application = new WebServerApplication();
application.start();
}
}
6、ClientHandler.java
import com.webserver.http.EmptyRequestException;
import com.webserver.http.HttpServletRequest;
import com.webserver.http.HttpServletResponse;
import java.io.IOException;
import java.net.Socket;
/** 客户端处理程序
* 处理一次与客户端的HTTP交互操作
* 1:解析请求
* 2:处理请求
* 3:发送响应
*/
public class ClientHandler implements Runnable {
private Socket socket;
public ClientHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try{
//1.解析请求
HttpServletRequest request = new HttpServletRequest(socket);
HttpServletResponse response = new HttpServletResponse(socket);
//2.处理请求
DispatcherServlet.getDispatcherServlet().service(request, response);
//3.发送响应
response.response();
} catch (IOException e){
e.printStackTrace();
} catch (EmptyRequestException e) {
//空请求什么都不做,直接断开连接
} finally {
//关闭socket
try {
socket.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
7、DispatcherServlet.java
import com.webserver.http.HttpServletRequest;
import com.webserver.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.URISyntaxException;
/**
* 处理请求:
* DispatcherServlet是Spring MVC框架提供的一个核心类,用于和底层容器TOMCAT整合使用。
* 通过它使得程序员在写业务类时,无需再关注请求是如何调用到对应的业务处理类(某Controller)中
* 对应的处理方法(被@RequestMapping注解标注的方法),其会根据请求自动调用。
* 这里忽略了Tomcat和SpringMVC框架整合的细节,直接使用该类完成核心业务逻辑的剖析。
*/
public class DispatcherServlet {
private static DispatcherServlet dispatcherServlet;
private static File rootDir;
private static File staticDir;
static {
dispatcherServlet = new DispatcherServlet();
try {
//定位到:target/classes
rootDir = new File(DispatcherServlet.class.getClassLoader().getResource(".").toURI());
//定位static目录
staticDir = new File(rootDir, "static");
} catch (URISyntaxException e) {
e.printStackTrace();
}
}
//私有化构造方法
private DispatcherServlet() {}
public void service(HttpServletRequest request, HttpServletResponse response) throws IOException {
// 获取请求路由
String path = request.getRequestURI();
//判断本次请求是否为请求某个业务
try {
Method method = HandMapping.getMethod(path);
if (method != null) {
method.invoke(method.getDeclaringClass().newInstance(), request, response);
return;
}
} catch (Exception e) {
throw new RuntimeException(e);
}
File file = new File(staticDir, path);
System.out.println("该页面是否存在:" + file.exists());
if (file.isFile()) {
if (file.isFile()) {//用户请求的资源在static目录下 存在 且是一个文件
response.setContentFile(file);
} else {
response.setStatusCode(404);
response.setStatusReason("NotFound");
response.setContentFile(new File(staticDir, "/root/404.html"));
}
}
}
//向外界提供dispatcherServlet实例
public static DispatcherServlet getDispatcherServlet() {
return dispatcherServlet;
}
}
8、HandMapping.java
import com.webserver.annotations.Controller;
import com.webserver.annotations.RequestMapping;
import java.io.File;
import java.lang.reflect.Method;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Map;
/**
* 使用当前类维护所有请求对对应的Controller的处理业务方法
*/
public class HandMapping {
private static Map<String, Method> mapping = new HashMap<>();
private static File rootDir; //定位应用项目中启动类所在的包
static {
try {
rootDir = new File(WebServerApplication.BootClass.getResource(".").toURI());
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
init();
}
/* 根据请求路径判断是否为处理某个业务
当我们得到本次请求路径path的值后,我们首先要查看是否为请求业务:
1:扫描controller包下的所有类
2:查看哪些被注解@Controller标注的过的类(只有被该注解标注的类才认可为业务处理类)
3:遍历这些类,并获取他们的所有方法,并查看哪些时业务方法
只有被注解@RequestMapping标注的方法才是业务方法
4:遍历业务方法时比对该方法上@RequestMapping中传递的参数值是否与本次请求
路径path值一致,如果一致则说明本次请求就应当由该方法进行处理
因此利用反射机制调用该方法进行处理。
5:如果扫描了所有的Controller中所有的业务方法,均未找到与本次请求匹配的路径
则说明本次请求并非处理业务,那么执行下面请求静态资源的操作
*/
private static void init() {
try {
//从根目录定位到controller目录
//E:\code\JavaCode\JavaBigData\SpringBootWebServer\v22\target\classes\com\webserver\controller
File dir = new File(rootDir, "controller");
//获取该目录下的所有类
File[] files = dir.listFiles(f -> f.getName().endsWith(".class"));
//如果这个目录不存在,直接就不干活了,所以必须创建controller包
if (!dir.exists()) {
return;
}
//
for (File sub : files) {
/**
我们约定Boot所有的业务必须放在controller包里,且这个包必须和启动类放在一个包中。
这样我们定位包名时,可以通过启动类去找controller包,然后创建controller类的类对象
*/
String fileName = sub.getName();
String className = fileName.substring(0, fileName.indexOf("."));
String packageName = WebServerApplication.BootClass.getPackage().getName();
className = packageName + ".controller." + className;
Class cls = Class.forName(className);
if (cls.isAnnotationPresent(Controller.class)) {
Object obj = cls.newInstance();//实例化这个Controller
Method[] methods = cls.getDeclaredMethods();
for (Method method : methods) {
if (method.isAnnotationPresent(RequestMapping.class)) {
RequestMapping rm = method.getAnnotation(RequestMapping.class);
String value = rm.value();
mapping.put(value, method);
}
}
}
}
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
public static Method getMethod(String path) {
return mapping.get(path);
}
}
http(HTTP包)
9、HttpServletRequest.java
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.Locale;
import java.util.Map;
/**
* 解析请求
* 请求对象
* 该类的每一个实例用于表示浏览器发送过来的一个HTTP的请求内容
* 每个请求HTTP协议要求由三部分构成:请求行,消息头,消息正文
*/
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.1解析请求行
parseRequestLine();
//1.2:解析消息头
parseHeaders();
//1.3:解析消息正文
parseContent();
}
/**
* 1.1解析请求行
* 请求方法 抽象路径 协议版本
*/
private void parseRequestLine() throws IOException, EmptyRequestException {
String line = readLine();
// 如果请求行为空字符串,则本次请求为空请求
if (line.isEmpty()) {
throw new EmptyRequestException();
}
System.out.println("请求行:" + line);
String[] data = line.split("\\s");
method = data[0];
uri = data[1];
protocol = data[2];
//进一步解析uri
parseURI();
}
//解析uri
private void parseURI() {
/**uri有两种情况,带参数和不带参数
1: 不含参数
例如:/index.html
直接将uri的值赋给requestURI
2.含参数
例如:http://localhost:8088/regUser.html?username=shusheng&password=123456&nickname=jack&age=18
将uri中"?"左侧的请求部分赋值给requestURI
将uri中"?"右侧的参数部分赋值给queryString
将参数部分首先按照&拆成一对对的参数,在按照=拆成参数名和参数值,参数名作为键,参数值作为值,存入parameters。
*/
String[] data = uri.split("\\?");
requestURI = data[0];
if (data.length > 1) {
queryString = data[1];
parseParameters(queryString); //&&
}
System.out.println("requestURI:" + requestURI);
System.out.println("queryString:" + queryString);
System.out.println("parameters:" + parameters);
}
//解析参数,因为GET请求来自抽象路径的"?"右侧,而POST来自消息正文,所有格式一样,可以重用解析操作
private void parseParameters(String line){
//username=%E5%B0%8F%E7%89%9B%E9%A9%AC&password=root&nickname=%E5%B0%8F%E7%89%9B%E9%A9%AC&age=15
//先将参数转成中文
try {
line = URLDecoder.decode(line, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
//将参数按&分隔成一对对的
String[] data = line.split("&");
for (String e : data) {
//将参数按=分隔成键值对
String[] paras = e.split("=");
parameters.put(paras[0], paras.length > 1 ? paras[1] : "");
}
}
//1.2解析消息头
private void parseHeaders() throws IOException {
//1.2解析消息头
while (true) {
String line = readLine();
if (line.isEmpty()) { //如果readLine返回空字符串,则是读取到了换行符和回车符
break;
}
System.out.println("消息头:" + line);
//将消息头分为消息头的名字和消息头的值,以键值对的方式存储在Map中
String[] data = line.split(":\\s");
headers.put(data[0].toLowerCase(Locale.ROOT), data[1]);
}
//while循环结束,消息头解析完毕
System.out.println("headers:" + headers);
}
//解析消息正文
private void parseContent() throws IOException {
//判断本次请求方式是否为post请求
if ("POST".equalsIgnoreCase(method)) { //忽略大小写
//根据消息头Content-Length来确定正文的字节数量以便进行读取
String contentLength = getHeader("Content-Length");
if(contentLength!=null){//判断不为null的目的是确保有消息头Content-Length
int length = Integer.parseInt(contentLength);
System.out.println("正文长度:"+length);
byte[] data = new byte[length];
InputStream in = socket.getInputStream();
in.read(data);
//根据Content-Type来判断正文类型,并进行对应的处理
String contentType = getHeader("Content-Type");
//分支判断不同的类型进行不同的处理
if("application/x-www-form-urlencoded".equals(contentType)){//判断类型是否为form表单不带附件的数据
//该类型的正文就是一行字符串,就是原GET请求提交表单是抽象路径中"?"右侧的参数
String line = new String(data, StandardCharsets.ISO_8859_1);
System.out.println("正文内容:"+line);
parseParameters(line);
}
//扩展其他类型并进行对应的处理
}
}
}
/**
* 按照回车符和换行符,获取请求信息
*
* @return
* @throws IOException
*/
private String readLine() throws IOException {
//同一个Socket对象无论调用多少次getInputStream获取的都是同一个输入流
//测试读取一行字符串(CRLF结尾)
InputStream is = socket.getInputStream();
int d;
//cur表示本次读取的字符,pre表示上次读取的字符
char cur = 'a', pre = 'a';
StringBuilder builder = new StringBuilder();
while ((d = is.read()) != -1) {
cur = (char) d;
//是否已经连续读取到了回车符+换行符
if (pre == 13 && cur == 10) {
break;
}
builder.append(cur);
pre = cur;
}
return builder.toString().trim();
}
public String getHeader(String name) {
return headers.get(name.toLowerCase(Locale.ROOT));
}
public String getParameter(String name) {
return parameters.get(name);
}
//Getter和Setter方法
public String getMethod() {
return method;
}
public void setMethod(String method) {
this.method = method;
}
public String getUri() {
return uri;
}
public void setUri(String url) {
this.uri = url;
}
public String getProtocol() {
return protocol;
}
public void setProtocol(String protocol) {
this.protocol = protocol;
}
public String getRequestURI() {
return requestURI;
}
public String getQueryString() {
return queryString;
}
}
10、HttpServletResponse.java
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 baos;
//构造方法 初始化socket
public HttpServletResponse(Socket socket) {
this.socket = socket;
}
/**
* 将当前响应对象内容以标准的HTTP响应格式,发送给客户端(浏览器)
*/
public void response() throws IOException {
//发送前的准备工作:向响应头中添加响应正文长度
sendBefore();
//3.1发送状态行
sendStatusLine();
//3.2发送响应头
sendHeaders();
//3.3发送响应正文
sendContent();
}
/**
* 发送前的准备工作
*/
private void sendBefore(){
//判断是否有动态数据存在
if (baos!=null){
addHeader("Content-Length", baos.size()+"");
}
}
/**
* 发送状态行
*/
private void sendStatusLine() throws IOException {
println("HTTP/1.1" + " " + statusCode + " " + statusReason);
}
/**
* 发送响应头
*/
private void sendHeaders() throws IOException {
//当发送响应头时,所有待发送的都应当都作为键值对存入了headers中
Set<Map.Entry<String, String>> entrySet = headers.entrySet();
for (Map.Entry<String, String> e : entrySet) {
String key = e.getKey();
String value = e.getValue();
println(key + ": " + value);
}
//单独发送一组回车+换行表示响应头部分发送完了!
println("");
}
/**
* 发送正文
*/
private void sendContent() throws IOException {
if (baos!=null){ //存在动态数据
byte[] data = baos.toByteArray();
OutputStream out = socket.getOutputStream();
out.write(data);
}else if (contentFile != null) { //当正文不为空,我们才进行发送正文
try (FileInputStream fis = new FileInputStream(contentFile);) {
OutputStream outputStream = socket.getOutputStream();
int len;
byte[] data = new byte[1024 * 10];
while ((len = fis.read(data)) != -1) {
outputStream.write(data, 0, len);
}
}
}
}
/**
* 路由重定向
*/
public void sendRedirect(String path){
statusCode = 302;
statusReason = "Moved Temporarily";
addHeader("Location", path);
}
/**
* 向浏览器发送一行字符串
*/
private void println(String line) throws IOException {
OutputStream outputStream = socket.getOutputStream();
outputStream.write(line.getBytes(StandardCharsets.ISO_8859_1));
outputStream.write(13);//发送回车符
outputStream.write(10);//发送换行符
}
/**
* 添加响应头的参数
*/
public void addHeader(String key, String value) {
headers.put(key, value);
}
public void setContentFile(File contentFile) throws IOException {
this.contentFile = contentFile;
//分析tomcat文件类型
String contentType = Files.probeContentType(contentFile.toPath());
//如果没有根据文件分析出文件类型,就不添加这个响应头了,由浏览器自己判定这个文件类型
if (contentType != null) {
addHeader("Content-Type", contentType);
}
addHeader("Content-Length", contentFile.length() + "");
}
public OutputStream getOutputStream(){
if (baos == null){
baos = new ByteArrayOutputStream();
}
return baos;
}
public PrintWriter getWriter(){
PrintWriter pw = new PrintWriter(new BufferedWriter(new OutputStreamWriter(getOutputStream(), StandardCharsets.UTF_8)), true);
return pw;
}
/**
* 通过返回的字节输出流写出的字节最终会作为响应正文内容发送给客户端
*/
public void setContentType(String mime){
addHeader("Context-Type", mime);
}
//Getter和Setter方法
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;
}
}
11、EmptyRequestException.java
/**
* 处理空请求异常
*/
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);
}
}
三、使用手写Tomcat启动项目
1、BirdBoot的约定
我们约定Boot所有的业务必须放在controller包里,且这个包必须和启动类放在一个包中。
2、将手写Tomcat的Maven项目打成jar包,并安装到本地Maven仓库。
3、创建Maven项目,导入手写tomcat坐标,创建主类。
1.创建Maven项目
2.导入手写Tomcat坐标
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.dute</groupId>
<artifactId>MyBirdBoot01</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<!-- 设置 JDK 版本为 1.8 -->
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.source>1.8</maven.compiler.source>
<!-- 设置编码为 UTF-8 -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.encoding>UTF-8</maven.compiler.encoding>
</properties>
<dependencies>
<!-- 手写Tomcat的坐标 -->
<dependency>
<groupId>org.zzn</groupId>
<artifactId>BirdBoot</artifactId>
<version>1.0</version>
</dependency>
</dependencies>
</project>
3.创建主类,创建页面
4.Application.java
import com.webserver.core.WebServerApplication;
/**
* 所有创建的包必须在启动类的包里,且必须将controller的类放在controller包下面
*/
public class Application {
public static void main(String[] args) {
WebServerApplication.run(Application.class,args);
}
}
5.index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>Hello World!</h1>
</body>
</html>
4、测试
启动Application主类,并在浏览器访问index.html,访问成功。