Servlet创建项目流程
创建Servlet项目七步骤:
创建Maven项目
引入依赖(Servletjar包导入到pom.xml)
创建目录src/main/webapp/WEB-INF/web.xml
编写servlet代码
打包
部署(这里打包和部署可以通过引入smart tomcat插件完成)
验证
Servlet常见出错响应状态码
405 Method Not Allowed
方法不匹配
我们构造的doGet就只能用get请求处理!
doPost需要通过post请求处理!
如果不匹配响应就会访问这个405状态码!
可以看到我们需要用Post请求处理该servlet代码,而我们却直接输入url这样的方式就是直接通过get请求访问服务器!
当我们没有把调用父类下的doGet方法注释掉时,也会返回405状态码!
我们看一下源码就知道了!
这里父类的doGet方法直接返回405!
我们怎样区分get和post请求呢?
get请求
直接在浏览器搜索框中输入url
我们html下的<a>标签,img/linkscript标签等等!
form表单指定method属性为get
ajax构造get请求在type设置为get
post请求
通过form表单,method指定为post
ajax构造post请求,type指定为post
500 Internal Server Error
服务器出错
这里的500状态码对我们初学者来说是很常见的,就是我们的服务器出错,也就是我们的Servlet代码发生异常并没有处理掉!这回将异常抛到tomcat而tomcat直接将异常返回给客户端!
这里出bug了,但是浏览器还是将响应信息返回到浏览器上了!
如果我们将上面的响应信息给去掉,就可以看到返回的错误信息在页面上,通过这个错误可以精准找到我们的bug!
服务器未启动或者端口号被占用
出现这个错误,说明是TCP连接出现了问题!
这个错误说明我们该Servlet类的路径没有按指定规则编写!
我们需要加上/
Servlet运行原理
我们的Servlet代码连一个main方法都没有是怎么运行呢?
这里我们需要了解一下tomcat帮我们做的工作和处理机制!
tomcat定位
我们知道tomcat就是一个http服务器,而http是用户层协议!
所以我们的Servlet代码是基于tomcat 运行的!
当用户在浏览器中发送请求后,tomcat作为应用层服务器就可以接这个请求,而http只是一个应用层协议,需要通过其他层协议协助传输!
这里的传输过程也是要经过5层协议进行封装分用,这里和之前的一样!
我们分析一下上述流程
接收请求
我们浏览器客户端发送一个请求,然后用户的请求参数随着查询字符串或者body构造了一个http请求然后到达了用户层,用户层协议就是http,然后调用操作系统内核下的,socket api发送到网络层,网络层加上TCP报头,到达传输层加上IP协议报头,然后就传输到了数据链路层,加上帧头帧尾,最后到达物理层调用网卡设备将这些信息转换成光信号或者高低电平,通过网络设备传输到达服务器主机,服务器主机通过网卡接收到这一组信号解析成以太网数据帧,进行分用!层层解析最后解析成一个http请求并交给tomcat进程进行处理!
tomcat拿到http协议报(字符串)按照协议报格式进行解析,根据ContentPath路径确定webapp,在通过ServletPath确定具体的类,根据请求的方法,决定调用doGET/POST方法,此时我们的HttpServletResquest对象就包含了这个请求的详细信息!
根据请求处理响应
我们通过HttpServletRequest中的请求信息,计算相应的响应信息,通过HttpServletResponse这个对象,存放响应信息!比如我们可以设置一些响应的状态码,body字段等!
返回响应
我们的doGet/doPost 执行结束后,就会自动把HttpServletResponse以及我们已经设置的一些属性转换成相应的http响应,通过socket发送!后面的过程就是网络传输层层分用分装的过程,最后将响应中的body信息展现在浏览器上给用户!
tomcat伪代码
通过下面tomcat伪代码,了解tomcat初始化/接收请求两部分核心内容!
tomcat初始化流程
class Tomcat { // 用来存储所有的 Servlet 对象 private List<Servlet> instanceList = new ArrayList<>(); public void start() { // 根据约定,读取 WEB-INF/web.xml 配置文件; // 并解析被 @WebServlet 注解修饰的类 // 假定这个数组里就包含了我们解析到的所有被 @WebServlet 注解修饰的类. Class<Servlet>[] allServletClasses = ...; // 这里要做的的是实例化出所有的 Servlet 对象出来; for (Class<Servlet> cls : allServletClasses) { // 这里是利用 java 中的反射特性做的 // 实际上还得涉及一个类的加载问题,因为我们的类字节码文件,是按照约定的 // 方式(全部在 WEB-INF/classes 文件夹下)存放的,所以 tomcat 内部是 // 实现了一个自定义的类加载器(ClassLoader)用来负责这部分工作。 Servlet ins = cls.newInstance(); instanceList.add(ins); } // 调用每个 Servlet 对象的 init() 方法,这个方法在对象的生命中只会被调用这一次; for (Servlet ins : instanceList) { ins.init(); } // 利用我们之前学过的知识,启动一个 HTTP 服务器 // 并用线程池的方式分别处理每一个 Request ServerSocket serverSocket = new ServerSocket(8080); // 实际上 tomcat 不是用的固定线程池,这里只是为了说明情况 ExecuteService pool = Executors.newFixedThreadPool(100); while (true) { Socket socket = ServerSocket.accept(); // 每个请求都是用一个线程独立支持,这里体现了我们 Servlet 是运行在多线程环境下的 pool.execute(new Runnable() { doHttpRequest(socket); }); } // 调用每个 Servlet 对象的 destroy() 方法,这个方法在对象的生命中只会被调用这一次; for (Servlet ins : instanceList) { ins.destroy(); } } public static void main(String[] args) { new Tomcat().start(); } }
这里就是tomcat初始化
我们看到这里tomcat其实是有main方法的,tomcat启动就从main方法开始!
启动后就会将@webServlet标记的类获取到,这些类已经是.class文件,需要通过反射机制创建好对应的实例,这些实例创建好就会调用init方法进行初始化,这个方法在HttpServlet类中,我们也可以重写这个方法!
这些请求处理业务完成后,就会将这些实例销毁调用其destroy方法,这里的方法也是在HttpServlet类中,我们也可以进行重写!
我们可以看到tomcat的内部也是调用操作系统中的socket进行网络通信的!
还有这里tomcat需要处理多个htttp请求,这里采取了多线程的方式,Servlet运行在多线程状态下的!
处理请求流程
class Tomcat { void doHttpRequest(Socket socket) { // 参照我们之前学习的 HTTP 服务器类似的原理,进行 HTTP 协议的请求解析,和响应构建 HttpServletRequest req = HttpServletRequest.parse(socket); HttpServletRequest resp = HttpServletRequest.build(socket); // 判断 URL 对应的文件是否可以直接在我们的根路径上找到对应的文件,如果找到,就是静态 内容 // 直接使用我们学习过的 IO 进行内容输出 if (file.exists()) { // 返回静态内容 return; } // 走到这里的逻辑都是动态内容了 // 根据我们在配置中说的,按照 URL -> servlet-name -> Servlet 对象的链条 // 最终找到要处理本次请求的 Servlet 对象 Servlet ins = findInstance(req.getURL()); // 调用 Servlet 对象的 service 方法 // 这里就会最终调用到我们自己写的 HttpServlet 的子类里的方法了 try { ins.service(req, resp); } catch (Exception e) { // 返回 500 页面,表示服务器内部错误 } } }
我们这里的tomcat通过调用socketapi然后获取到http请求,然后将请求按照http协议报的格式解析成HttpServlet对象,然后通过url中的资源目录,获取到对应的ContentPath和Servlet路径获取到对应的文件,如果是静态资源就直接通过socket返回给客户端,如果是动态资源就会调用HttpServlet下的service方法,通过这个方法就可以调用对应的doGET/doPOST处理请求,然后再将计算对应的响应,最后返回!
Servlet 的service的实现
class Servlet { public void service(HttpServletRequest req, HttpServletResponse resp) { String method = req.getMethod(); if (method.equals("GET")) { doGet(req, resp); } else if (method.equals("POST")) { doPost(req, resp); } else if (method.equals("PUT")) { doPut(req, resp); } else if (method.equals("DELETE")) { doDelete(req, resp); } ...... } }
这里就是根据对应的请求调用对应的请求处理方法,这里是通过多态的机制处理!
Servlet API详解
Servlet API有很多,我们只需要掌握HttpServlet/HttpServletRequset/HttpServletResponse这时个关键类中的核心方法即可!
HttpServlet
我们编写Servlet代码第一步就是继承HttpServlet类,并重写该类中的某些方法,处理请求!
核心方法:
而我们实际开发很少重写init/destory这些tomcat会帮我们调用!
我们主要任务还是处理对应请求,对不同方法请求,重写匹配的doXxx方法,处理不同的请求,返回对应的响应即可!
这些上述方法的调用时机,又称Servlert的生命周期!
这里的init方法,当HttpServlet实例化后就会通过该方法进行初始化,然后生命周期结束就是在destroy方法调用后将HttpServlet实例化对象销毁!期间可能要处理不同方法请求,所以可能会多次调用service方法!
注意:
HttpServlet实例只是在程序启动后创建一次就好了,并不是每次收到http请求都创建实例!
上述这些方法我们都可以进行重写,从而设置某些特有的属性,当时我们很少这样做,我们最常用的就是重写处理请求的方法doXXX!
代码示例
处理一个Get请求
我们分别通过ajax和form表单进行构造!
基于ajax
<!--引入jQuery--> <script src="http://code.jquery.com/jquery-2.1.1.min.js"></script> <script> //构造请求 $.ajax({ type:'get', url:'test', //不要加/表示绝对路径! success: function(body){ console.log(body); //在浏览器控制台打印body信息! }, error:function(){ console.log("请求失败"); } }); </script>
注意:
这里的url不用加/和servlet不同,而且这里的url就和我们的@WebServlet注释对应!
我们通过ajax的方式用前端构造请求,记得将这个html文件放入到webapp目录下,通过访问这个网页就向服务器发送了一个get方式的请求!
然后这里ajax通过回调的方式,如果请求成功,就会在控制台打印服务器给我们放回的body内容!
基于form表单
我们通过form表单构造一个post请求!
<form action="test" method="post"> <input type="text" name="name"> <input type="password" name="password"> <input type="submit" name="post请求" value="post请求"> </form>
客户端:服务器
import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @WebServlet("/test") public class Test extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.getWriter().write("hello world!"); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String nameValue = req.getParameter("name"); String passwordValue = req.getParameter("password"); resp.getWriter().write("name:"+nameValue+" password:"+passwordValue); } }
将响应输入到了浏览器上!
HttpServletResquest
我们知道HttpServletResquest类就是我们收到的请求,通过这个类将接收到了http的请求信息然后转换了对应http协议格式的字符串!我们通过该类提供的一些方法就可以获取到请求的报头信息还有内容了!
核心方法
上述方法,我们根据其英文意思和之前对http协议报头的学习就可以大概得出什么功能!
我们们通过上述方法就是为了得到一个http请求的报头和内容!
所以我们多使用上述方法就知道使用场景如何了!
代码示例
import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Enumeration; @WebServlet("/request") public class HttpServletRequest extends HttpServlet { @Override protected void doGet(javax.servlet.http.HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String Protocolname = req.getProtocol();//返回协议名称和版本! String method = req.getMethod();//返回请求方法 String url = req.getRequestURI(); String QueuryString = req.getQueryString();//获取到查询字符串 Enumeration<String> headernames = req.getHeaderNames();//请求header部分! resp.setContentType("text/html;charset=utf8");//响应的格式以及编码方式! resp.getWriter().write(Protocolname+"<br>"+method+"<br>"+url+"<br>"+QueuryString); while(headernames.hasMoreElements()){ String headerKey = headernames.nextElement();//获取到header中的key值 String headerVal = req.getHeader(headerKey);//通过key值找到val值! resp.getWriter().write(headerKey+":"+headerVal+"<br>"); } } }
浏览器返回的结果!
这就是HttpServletRequest类中核心方法的使用!
HttpServletRespondse
这个类就是我们服务器用来返回响应的类!
我们Servlet处理请求的doxx方法,我们根据请求计算出响应,我们可以根据请求将响应信息通过该类中的方法构造好,然后该类方法的对象通过http协议格式,转化成一个字符串,并通过socket写会给浏览器!
核心方法:
代码示例
import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * Created with IntelliJ IDEA. * Description: * User: hold on * Date: 2022-06-30 * Time: 13:03 */ @WebServlet("/response") public class HttpResponse extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html;charset=utf8");//设置响应内容类型,和编码方式! resp.setHeader("name","bug郭");//设置响应头部分,传入键值对信息! resp.setHeader("password","666666"); resp.setStatus(404);//设置响应状态码! resp.getWriter().write("收到响应!"); } }
浏览器获取到的结果:
fiddler抓包获取到的响应
可以看到我们自己构造的响应头部分,出现了中文乱码!!!
但是我们刚刚不是已经设置过了编码方式utf8嘛?
为啥还这样呢?
x
我们看到这里的setContentType只是设置响应内容,就是body部分的格式和字符编码!我们的响应头部分中的属性一般都是已有的属性,一般没有中文,所以并不能设置!
resp.setStatus(304);//设置响应状态码重定向!(也可以省略) resp.sendRedirect("https://www.bilibili.com/");//重定向后跳转的网页
请求
重定向
fiddler抓取到的响应!