1000行代码手写服务器
1000行代码手写服务器
开发技术&开发环境和工具
使用技术
基于Java IO,多线程,Socket网络编程,XML解析,只引入Junit,dom4j(解析xml),熟悉javaweb基本应用。
环境参数
- Java环境 JDK1.8 maven maven3.6
- 开发工具 IDEA
环境搭建
pom.xml文件配置
只需在xml导入dom4j jar包
<dependencies>
<dependency>
<groupId>org.dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>2.1.1</version>
</dependency>
</dependencies>
手写服务器 整体架构,编写XML文件
配置解析XML文件
<?xml version="1.0" encoding="UTF-8"?>
<web-app >
<servlet>
<servlet-name>login</servlet-name>
<servlet-class>com.feng.servlet.LoginServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>login</servlet-name>
<url-pattern>/login</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>login</servlet-name>
<url-pattern>/log</url-pattern>
</servlet-mapping>
</web-app>
POJO类
package com.feng.server;
public class Entity { //servlet-name或每一个servlet-name所对应得实体类
private String name;
private String clazz;
public Entity() {
}
public Entity(String name, String clazz) {
this.name = name;
this.clazz = clazz;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getClazz() {
return clazz;
}
public void setClazz(String clazz) {
this.clazz = clazz;
}
}
Mapping类
package com.feng.server;
import java.util.ArrayList;
import java.util.List;
public class Mapping { //映射关系,多个路径可以访问资源
private String name; //servlet-name
private List<String> urlPattern;//url-pattern
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<String> getUrlPattern() {
return urlPattern;
}
public void setUrlPattern(List<String> urlPattern) {
this.urlPattern = urlPattern;
}
public Mapping() {
this.urlPattern = new ArrayList<String>();
}
public Mapping(String name, List<String> urlPattern) {
this.name = name;
this.urlPattern = urlPattern;
}
}
配置dom4j
package com.feng.server;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import java.io.File;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class WebDom4j { //用于解析xml
private List<Entity> entityList; //用于储存实体类,每一个实体类一个servlet-name对应一个servlet-class
private List<Mapping> mappingList;//用于储存映射类,每一个servlet-name对应一个url-pattern
public List<Entity> getEntityList() {
return entityList;
}
public void setEntityList(List<Entity> entityList) {
this.entityList = entityList;
}
public List<Mapping> getMappingList() {
return mappingList;
}
public void setMappingList(List<Mapping> mappingList) {
this.mappingList = mappingList;
}
//构造方法
public WebDom4j() {
entityList = new ArrayList<Entity>();
mappingList = new ArrayList<Mapping>();
}
//获取Document对象的方法
public Document getDocument(){
try {
//创建SAXReader对象
SAXReader reader = new SAXReader();
//调用read方法
return reader.read(new File("web/WEB-INF/web.xml"));
} catch (DocumentException e) {
e.printStackTrace();
}
return null;
}
//获取元素
public void parse(Document doc) {
//1获取根元素
Element root = doc.getRootElement(); //web-app
//2获取servlet子元素
for (Iterator<Element> ite = root.elementIterator("servlet"); ite.hasNext();) {
Element subElement = ite.next(); //得到每一个servlet
//创建一个实体类
Entity ent = new Entity(); //用于储存servlet-name与servlet-class
for (Iterator<Element> subIte = subElement.elementIterator(); subIte.hasNext();) {
Element ele = subIte.next(); //servlet-name与servlet-class都有可能
if ("servlet-name".equals(ele.getName())) {
ent.setName(ele.getText());
} else if ("servlet-class".equals(ele.getName())) {
ent.setClazz(ele.getText());
}
}
entityList.add(ent);//放到集合中
}
//测试
/* for (Entity entity : entityList) {
System.out.println(entity.getName() + "\t" + entity.getClazz());
}*/
//解析servlet-mapping
for (Iterator<Element> ite = root.elementIterator("servlet-mapping"); ite.hasNext();){
Element subEle = ite.next();//得到每一个servlet-mapping
//创建一个mapping对象
Mapping map = new Mapping();
//解析mapping下面每一个子元素
for (Iterator<Element> subIte = subEle.elementIterator(); subIte.hasNext();){
Element ele = subIte.next();//可能是name,也可能是url
if ("servlet-name".equals(ele.getName())) {
map.setName(ele.getText());
} else if ("url-pattern".equals(ele.getName())) {
//获取集合对象,调用集合对象的添加方法,添加元素
map.getUrlPattern().add(ele.getText());
}
}
mappingList.add(map);
}
//测试
/*for (Mapping m : mappingList) {
System.out.println(m.getName());
for (String s : m.getUrlPattern()) {
System.out.println(s);
}
}*/
}
//用于测试
public static void main(String[] args) {
WebDom4j web = new WebDom4j();
web.parse(web.getDocument());
}
}
servlet和servlet-mapping解析结果
更新编写XML
先更新编写才得出上面的结果
<?xml version="1.0" encoding="UTF-8"?>
<web-app>
<servlet>
<servlet-name>login</servlet-name>
<servlet-class>com.feng.servlet.LoginServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>login</servlet-name>
<url-pattern>/log</url-pattern>
<url-pattern>/login</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>register</servlet-name>
<servlet-class>com.feng.servlet.RegisterServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>register</servlet-name>
<url-pattern>/reg</url-pattern>
<url-pattern>/regis</url-pattern>
<url-pattern>/register</url-pattern>
</servlet-mapping>
</web-app>
编写servletContext Servlet 上下文,容器用于存储映射关系
package com.feng.server;
import java.util.HashMap;
import java.util.Map;
/**
*ServletContext 上下文 就是一个容器
*/
public class ServletContext { //Entity与Mapping之间的映射关系
private Map<String, String> servlet;//key是servlet-name(实体类中的name),值servlet-class,实体类中的值
private Map<String, String> mapping;//key是url-pattern(Map中的每一个元素),值servlet-name,实体类中的name
public Map<String, String> getServlet() {
return servlet;
}
public void setServlet(Map<String, String> servlet) {
this.servlet = servlet;
}
public Map<String, String> getMapping() {
return mapping;
}
public void setMapping(Map<String, String> mapping) {
this.mapping = mapping;
}
public ServletContext(){
servlet = new HashMap<String, String>();
mapping = new HashMap<String, String>();
}
}
编写WebAPP 初始化程序数据
package com.feng.server;
import com.feng.servlet.Servlet;
import java.util.List;
import java.util.Map;
public class WebApp { //应用程序
private static ServletContext context;
static {
context = new ServletContext();
//分别获取对应关系的Map集合
Map<String, String> servlet = context.getServlet();
Map<String, String> mapping = context.getMapping();
//创建解析XML文件对象
WebDom4j web = new WebDom4j();
//解析xml
web.parse(web.getDocument());
//获取解析xml之后的List集合
List<Entity> entityList = web.getEntityList();
List<Mapping> mappingList = web.getMappingList();
//将List集合储存到map集合中
for (Entity entity : entityList) {
servlet.put(entity.getName(),entity.getClazz());
}
System.out.println(servlet);
for (Mapping map : mappingList) {
//遍历url-pattern的集合
List<String> urlPattern = map.getUrlPattern();
for (String s : urlPattern) {
mapping.put(s,map.getName());
}
}
System.out.println(mapping);
}
public static void main(String[] args) {
WebDom4j web = new WebDom4j();
web.parse(web.getDocument());
}
}
获取解析xml集合的结果
创建不同的Servlet对象
/*
* 根据url创建不同的Servlet对象
* */
public static Servlet getServlet(String url){
if (url ==null||url.trim().equals("")){
return null;
}
try {
//如果url正确
//根据url的key获取servlet-name的值
String servletName = context.getMapping().get(url);
//根据servlet-name找到对应得servlet-class
String servletClass = context.getServlet().get(servletName);
//得到的是一个完整的包+类名的字符串
//使用反射创建servlet对象
Class<?> clazz = Class.forName(servletClass);
Servlet servlet = (Servlet) clazz.newInstance();
return servlet;
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return null;
}
在WebApp类调用getServlet方法得到如下结果:
编写服务(启动或关闭服务)
public class Server { //服务器,用于启动或停止服务
private ServerSocket server;
public static void main(String[] args) {
Server server = new Server();//创建服务器
server.start();
}
private void start() {
this.start(8888);
}
public void start(int port){
try {
server = new ServerSocket(port);
this.receive();//调用接受请求的方式
} catch (IOException e) {
e.printStackTrace();
}
}
private void receive() {
try {
//1监听
Socket client = server.accept();
//获取用户的请求
InputStream is = client.getInputStream();
byte[] buf = new byte[20480];
int len = is.read(buf);
System.out.println(new String(buf, 0, len));
} catch (IOException e) {
e.printStackTrace();
}
}
public void stop(){
}
}
在浏览器访问localhost:8888,控制台会出现如下结果:
编写login.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登入</title>
</head>
<body>
<form action="http://localhost:8888/log" method="post">
<p>用户名:<input type="text" name="username" id="username"></p>
<p>密码:<input type="password" name="pwd" id="password"></p>
<p>
爱好:<input type="checkbox" name="hobby" value="ball"/>足球
<input type="checkbox" name="hobby" value="read"/>读书
<input type="checkbox" name="hobby" value="paint"/>画画
</p>
<p><input type="submit" value="登入"></p>
</form>
</body>
</html>
封装Request
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.security.PrivateKey;
import java.util.*;
public class Request { //请求
//输入流
private InputStream is;
private String requestInfo; //请求字符串,请求方式,请求路径,参数,协议,协议版本,请求正文。。
private String method;//请求的方式
private String url;//请求的路径
public String getUrl() {
return url;
}
/*
* 输入框的name为key,值为value
* */
private Map<String, List<String>> parameterMapValues;//参数
private static final String CRLF="\r\n";//换行
private static final String BLANK =" ";//空格
//构造方法,初始化属性
public Request(){
parameterMapValues=new HashMap<String, List<String>>();
method="";
url="";
requestInfo="";
}
public Request(InputStream is){
this();//调用本类无参构造方法
this.is=is;
try {
byte[] buf = new byte[20480];
int len = this.is.read(buf);
requestInfo = new String(buf, 0, len);
} catch (IOException e) {
return;
}
//调用本类中的分解信息的方法
this.parseRequestInfo();
}
//分解请求信息的方法
/**
* 请求方式
* 请求路径
* 请求的参数
* */
public void parseRequestInfo() {
String paraString="";//用于存储请求参数
//获取参数的第一行
String firstLine = requestInfo.substring(0, requestInfo.indexOf(CRLF)).trim();//从0开始到第一个换行的位置
//分解出请求方式
int index = firstLine.indexOf("/");
this.method = firstLine.substring(0, index).trim();
//分解url ,get可能包含的参数,也可能不包含的参数post
String urlString = firstLine.substring(index, firstLine.indexOf("HTTP/")).trim();
//判断请求方式是GET或POST
if ("get".equalsIgnoreCase(this.method)) { //包含请求参数
if (urlString.contains("?")){
String[] urlArray = urlString.split("\\?");
this.url=urlArray[0];
paraString=urlArray[1];
}else {
this.url=urlString;
}
}else {//post不包含请求参数
this.url=urlString;
paraString=requestInfo.substring(requestInfo.lastIndexOf(CRLF)).trim();
}
if (paraString.equals("")){
return;
}
//请求参数
System.out.println(paraString);
}
//用于测试
public void show(){
System.out.println(this.url);
System.out.println(this.method);
}
}
成功封装后启动服务,控制台输出如下:
储存参数,处理中文
/**
*username = admin
* pwd = 123
* hobby =ball
* hobby = paint
* */
public void parseParam(String prarString){
String[] token = prarString.split("&");
for (int i=0;i<token.length;i++){
String keyValues = token[i];
String[] keyValue = keyValues.split("=");//把=分割掉,得到K和V
if (keyValue.length==1){ //username=
keyValue = Arrays.copyOf(keyValue, 2);
keyValue[1] = null;
}
//将表单中的元素的name与name对应的值储存到Map集合
String key = keyValue[0].trim();
String value = keyValue[1]==null?null:decode(keyValue[1].trim(),"utf-8");
//放到集合中存储
if (!parameterMapValues.containsKey(key)) { //键不存在就创建
parameterMapValues.put(key,new ArrayList<String>());
}
List<String> values = parameterMapValues.get(key);
values.add(value); //创建一个集合添加值
}
}
//根剧表单元素的name获取多个值
public String [] getParamterValues(String name) {
//根据key获取value
List<String> values = parameterMapValues.get(name);
if (values == null ){
return null;
} else {
return values.toArray(new String[0] );
}
}
public String getParamter(String name){
//调用本类中根据name获取单个值的方法
String[] values = this.getParamterValues(name);
if (values ==null){
return null;
} else {
return values[0];
}
}
//处理中文,因浏览器对中文进行了编码,进行解码
private String decode(String value,String code){
try {
return URLDecoder.decode(value,code);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return null;
}
public static void main(String[] args) {
Request req = new Request();
//调用分解参数的方法
req.parseParam("username=中文加密&pwd=123&hobby=ball&hobby=read");
System.out.println(req.parameterMapValues);
//调用获取多个值的方法
String[] str = req.getParamterValues("hobby");
for (String string : str) {
System.out.println(string);
}
//调用单个获取值的方法
System.out.println(req.getParamter("pwd"));
}
备注:中文参数加密(在百度上随便搜索,截取wd后面的参数)例如:苹果举例
https://www.baidu.com/s?ie=utf-8&f=8&rsv_bp=1&rsv_idx=1&tn=baidu&wd=%E8%8B%B9%E6%9E%9C&fenlei=256&rsv_pq=b162262500064c7f&rsv_t=3f02zT3Tvav%2FKayD%2BwKuGWxcDuG3ZnH0aQoblznxlDr2LxisTdsiuUCkLbE&rqlang=cn&rsv_enter=1&rsv_dl=tb&rsv_sug3=13&rsv_sug1=17&rsv_sug7=101&rsv_sug2=0&rsv_btype=i&inputT=13852&rsv_sug4=43101
%E8%8B%B9%E6%9E%9C(这个就是加密参数)
封装之后结果如下:
加到Server类中,测试响应
//封装请求信息
Request req = new Request(client.getInputStream());
/**
* 做出响应
* */
StringBuilder sb = new StringBuilder();
sb.append("HTTP/1.1").append(" ").append(200).append(" ").append("OK").append("\r\n");
sb.append("Content-Type:text/html;charset=utf-8").append("\r\n");
//内容
String str = "<html><head><title>响应结果</title></head><body>成功</body></html>";
sb.append("Content-Length:"+str.getBytes("utf-8").length).append("\r\n");
sb.append("\r\n");
sb.append(str);
//通过输出流发送出去
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(client.getOutputStream(),"utf-8"));
bw.write(sb.toString());
bw.flush();
bw.close();
模拟响应结果,测试浏览器是否在登入之后出现成功状态
编写Response
package com.feng.server;
import com.feng.util.IOCloseUtil;
import java.io.*;
public class Response { //响应
private StringBuilder headInfo; //响应头
private StringBuilder content; //响应内容
private int length; //响应内容的长度
//流
private BufferedWriter bw;
//两个常量,换行和空格
private static final String CRLF="\r\n"; //换行
private static final String BLANK=" "; //空格
//构造方法
public Response() {
headInfo= new StringBuilder();
content = new StringBuilder();
}
//带构造方法
public Response(OutputStream os) {
this();//调用本类的无参构造方法
try {
bw = new BufferedWriter(new OutputStreamWriter(os,"utf-8"));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
//构造正文部分
public Response print(String info){
content.append(info);
try {
length+= info.getBytes("utf-8").length;
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return this;
}
public Response printLn(String info){
content.append(info).append(CRLF);
try {
length+=(info+CRLF).getBytes("utf-8").length;
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return this;
}
//构造响应头
private void createHeadInfo(int code){
headInfo.append("HTTP/1.1").append(BLANK).append(code).append(BLANK);
switch (code){
case 200:
headInfo.append("OK");
break;
case 500:
headInfo.append("SERVER ERROR");
default:
headInfo.append("NOT FOUND");
break;
}
headInfo.append(CRLF);
headInfo.append("Content-Type:text/html;charset=utf-8").append(CRLF);
headInfo.append("Content-Length:"+length).append(CRLF);
headInfo.append(CRLF);
}
/**
* 推送到客户机的浏览器
* */
public void pushToClient(int code){
if (headInfo==null){
code=500;
}
//调用本类中的构造响应头
this.createHeadInfo(code);
try {
bw.write(headInfo.toString());
bw.write(content.toString());
bw.flush();
this.close();
} catch (IOException e) {
e.printStackTrace();
}
}
public void close() {
IOCloseUtil.closeAll(bw);
}
}
然后再把Server里面做出响应那段代码注释掉
package com.feng.server;
import com.feng.servlet.Servlet;
import com.feng.util.IOCloseUtil;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.net.ServerSocket;
import java.net.Socket;
public class Server { //服务器,用于启动或停止服务
private ServerSocket server;
public static void main(String[] args) {
Server server = new Server();//创建服务器
server.start();
}
private void start() {
this.start(8888);
}
public void start(int port){
try {
server = new ServerSocket(port);
this.receive();//调用接受请求的方式
} catch (IOException e) {
e.printStackTrace();
}
}
private void receive() {
try {
//1监听
Socket client = server.accept();
//创建线程类
Dispatcher dis = new Dispatcher(client);
//创建代理线程
new Thread(dis).start();
//封装请求信息
Request req = new Request(client.getInputStream());
//req.show();
/**
* 做出响应
* */
/*StringBuilder sb = new StringBuilder();
sb.append("HTTP/1.1").append(" ").append(200).append(" ").append("OK").append("\r\n");
sb.append("Content-Type:text/html;charset=utf-8").append("\r\n");
//内容
String str = "<html><head><title>响应结果</title></head><body>成功</body></html>";
sb.append("Content-Length:"+str.getBytes("utf-8").length).append("\r\n");
sb.append("\r\n");
sb.append(str);
//通过输出流发送出去
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(client.getOutputStream(),"utf-8"));
bw.write(sb.toString());
bw.flush();
bw.close();*/
Response rep = new Response(client.getOutputStream());
Servlet servlet = WebApp.getServlet(req.getUrl());
int code=200;
if (servlet ==null){
code=404;
}
//调用Servlet中的服务方法
try {
servlet.service(req,rep);
} catch (Exception e) {
e.printStackTrace();
}
rep.pushToClient(code);
IOCloseUtil.closeAll(client); //单线程已被删除
} catch (IOException e) {
e.printStackTrace();
}
}
public void stop(){
}
}
封装Response
编写servlet
public abstract class Servlet { //是所有请求servlet的父类
public void service(Request req, Response rep) throws Exception {
this.deGet(req,rep);
this.doPost(req,rep);
}
public abstract void deGet(Request req, Response rep) throws Exception;
public abstract void doPost(Request req, Response rep) throws Exception;
}
编写LoginServlet
public class LoginServlet extends Servlet {
@Override
public void deGet(Request req, Response rep) throws Exception {
//获取请求参数
String name = req.getParamter("username");
String pwd = req.getParamter("pwd");
if (this.login(name,pwd)) {
//调用响应中的构建内容的方法
rep.printLn(name+"登入成功");
}else {
rep.printLn(name+"登录失败,对不起,账号和密码不准确");
}
}
private boolean login(String name,String pwd){
if ("admin".equals(name)&&"123".equals(pwd)) {
return true;
}
return false;
}
@Override
public void doPost(Request req, Response rep) throws Exception {
}
}
在Server创建Response对象,用Servlet调用service方法,请求和响应之后并推送到客服端,用户名和密码正确的话如图所示:
如果用户名和密码不匹配的话:
封装分发器,实现多线程(Dispatcher)
package com.feng.server;
import com.feng.servlet.Servlet;
import com.feng.util.IOCloseUtil;
import java.io.IOException;
import java.net.Socket;
/**
* 一个请求与响应就是一个Dispatcher
*
* @author asus
* */
public class Dispatcher implements Runnable {
private Request req;
private Response rep;
private Socket client;
private int code =200;//状态码
//构造方法初始化属性
public Dispatcher(Socket client) {
this.client = client;
try {
req=new Request(this.client.getInputStream());
rep=new Response(this.client.getOutputStream());
} catch (IOException e) {
code=500;
return;
}
}
@Override
public void run() {
//根据不同的url创建指定的servlet对象
Servlet servlet = WebApp.getServlet(req.getUrl());
if (servlet==null){
this.code=404;
}else {
//调用响应的servlet中service方法
try {
servlet.service(req,rep);
} catch (Exception e) {
this.code=500;
}
}
//将有响应的结果推送到客户端的浏览器
rep.pushToClient(code);
IOCloseUtil.closeAll(client);
}
}
实现多线程
这是修改后的服务器主线程类
public class Server { //服务器,用于启动或停止服务
private ServerSocket server;
private boolean isShutDown=false; //默认没有出错
public static void main(String[] args) {
Server server = new Server();//创建服务器
server.start();
}
private void start() {
this.start(8888);
}
public void start(int port){
try {
server = new ServerSocket(port);
this.receive();//调用接受请求的方式
} catch (IOException e) {
isShutDown=true;
}
}
private void receive() {
try {
while (!isShutDown){
//1监听
Socket client = server.accept();
//创建线程类
Dispatcher dis = new Dispatcher(client);
//创建代理线程
new Thread(dis).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
public void stop(){
isShutDown=true;
IOCloseUtil.closeAll(server);
}
}
但是这样还没有成功,控制台还会出现bug,虽然服务器没有停止,可能会出现潜在的风险,控制台出现空指针异常:
修改空指针异常
为了更好查看bug,我在Dispatcher类中的run方法里面添加了一个测试路径
System.out.println(req.getUrl()); //测试
控制台会出现一个新增的类:
缺少favicon映射类,在xml里面配置
<?xml version="1.0" encoding="UTF-8"?>
<web-app>
<servlet>
<servlet-name>login</servlet-name>
<servlet-class>com.feng.servlet.LoginServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>login</servlet-name>
<url-pattern>/log</url-pattern>
<url-pattern>/login</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>register</servlet-name>
<servlet-class>com.feng.servlet.RegisterServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>register</servlet-name>
<url-pattern>/reg</url-pattern>
<url-pattern>/regis</url-pattern>
<url-pattern>/register</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>favicon</servlet-name>
<servlet-class>com.feng.servlet.FaviconServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>favicon</servlet-name>
<url-pattern>/favicon.ico</url-pattern>
</servlet-mapping>
</web-app>
添加FaviconServlet
package com.feng.servlet;
import com.feng.server.Request;
import com.feng.server.Response;
public class FaviconServlet extends Servlet {
@Override
public void deGet(Request req, Response rep) throws Exception {
}
@Override
public void doPost(Request req, Response rep) throws Exception {
}
}
最后运行成功,控制台没有出现bug
避坑bug经验
return reader.read(new File("web/WEB-INF/web.xml"));
假如要配置正确的路径格式,那么先从根目录下开始写相对路径,当路径格式配置有问题时,例如WEB-INF看错了可能写成WEB_INF,控制台马上会出现如下错误: