1.第一个Servlet程序
我们首先通过idea来完成一个最简单的Servlet程序,共需要以下七步来完成,在该部分最后也会介绍较为简单的插件配置方法。
1.1 操作流程
第一步:
创建一个maven项目。
1)新建项目
2)选择maven项目
3)编辑信息
第二步:
引入依赖
1)在maven中央仓库找到Servlet的依赖
2)找到3.1版本,复制maven依赖
3)将依赖引入pom.xml文件中
第三步:
创建目录结构
1)在main目录创建webapp目录
2)在webapp目录创建WEB-INF目录
3)在WEB-INF目录新建web.xml文件
4)在web.xml文件中写入下面的代码段:
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd" > <web-app> <display-name>Archetype Created Web Application</display-name> </web-app>
第四步:
编写Servlet代码
在main-java目录下新建类,开始编写servlet代码
继承HttpServlet类
重写doGet方法
在方法里写执行的代码
加上WebServlet注解+路径,将该类与http请求中的URL关联起来
第五步:
打包
当前的代码是不能单独运行的(没有main方法),需要把当前的代码打包,然后部署到Tomcat上,由Tomcat来进行调用。
1)先进行准备工作,修改pom.xml
2)双击package,进行打包
打包完成
第六步:
部署
1)打开war包所在路径
2)把war包拷贝到tomcat的webapps目录里
3)启动tomcat
第七步:
验证程序
在网页访问路径127.0.0.1:8080/hello/test
此处的路径共有三部分组成
1.2 插件简化
在我们每次修改Servlet类代码后,都需要重复进行1.1中5-6步操作,较为繁琐,可以利用idea中内嵌的smart tomcat插件来简化开发流程。
完成上图插件的安装后,按下图流程操作。
注意,利用插件时,五六步操作完全不再需要手动进行了,不需要再进行第五步的准备工作——在pom.xml文件中加代码,直接在下图中定义Context Path即可。
点击小三角一次性完成打包、部署工作:
2.常见的访问出错
初学Servlet,肯定会遇到很多的问题,我们不仅要学习Servlet代码的基本写法,还需要认识常见的错误,在出现错误时能够解决。
2.1 出现404
404表示用户访问的资源不存在,大概率是URL路径写的不正确。
还有可能是因为web.xml文件写错了,需要写正确web.xml中内容,并重新启动tomact
web.xml文件代码片段:
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd" > <web-app> <display-name>Archetype Created Web Application</display-name> </web-app>
2.2 出现405
405表示对应的HTTP请求方法没有实现
在地址栏输入URL路径进行访问,浏览器默认构造的是GET请求,如果在服务器中的代码没有实现doGet方法,就会报这个错误。
在什么时候浏览器发的是GET请求呢?
1.直接在地址栏里输入URL
2.通过a标签跳转
3.通过img/link/script...
4.通过form表单,method指定为GET
5.通过ajax,type指定为GET
那在什么时候浏览器发的是POST请求呢?
1.通过form表单,method指定为POST
2.通过ajax,type指定为POST
2.3 出现500
500意味着服务器代码抛出异常了,并且代码没有处理异常,异常向上传递到了Tomcat这里了。
这种错误是最好解决的,因为异常的调用栈,会直接打印在页面上,可以直接找到问题的所在。
2.4 出现空白页面
没有resp.getWritter().write() 操作。
2.5 出现“无法访问此网站”
最大可能性是因为Tomcat没有正确启动(比如Tomcat的8080端口被占用,启动失败)
3.Servlet运行原理
3.1 Servlet运行原理
Servlet是位于应用层的上层建筑,下面的传输层、网络层、和数据链路层属于经济基础,经济基础决定上层建筑。
Tomcat其实就是一个运行在用户态的应用程序(普通的Java进程)。
当用户操作浏览器,发送请求给服务器的时候,Tomcat作为HTTP Servlet会调用Servlet API,然后执行我们所写的Servlet程序来处理请求。
更详细的交互过程见下图:
用户浏览器发出请求后,客户端主机会将请求数据包一层层封装,最后到物理层转换成光电信号传输给服务器;到达服务器后再将数据包进行一层层分用,最后到服务器的应用层处理请求并计算响应。
3.2 Tomcat的执行逻辑
我们通过用伪代码的方式来演示Tomcat在Servlet程序运行中起到的作用。
1)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的代码中内置了main方法。当我们启动Tomcat的时候,就是从Tomcat的main方法开始执行的。
被@WebServlet注解修饰的类会在Tomcat启动的时候被获取到,并集中管理。
Tomcat通过反射机制来创建被@WebServlet注解修饰的类的实例。
这些实例被创建完了之后,会调用其中的init方法进行初始化(该方法是HttpServlet自带的,我们自己写的类可以重写init)
这些实例被销毁之前,会调用其中的destory方法进行收尾工作(只有正常退出Tomcat才会触发该方法,而通过直接杀死进程的方法是来不及调用该方法的)
Tomcat内部也是通过Socket API进行网络通信
Tomcat为了能同时响应多个HTTP请求,采取了多线程的方式实现。因此Servlet是运行在多线程环境下的
2)Tomcat处理请求流程
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从Socket中读取到HTTP请求,然后按照HTTP协议的格式解析成一个HttpServletRequest对象。
Tomcat会根据URL中的path判定这个请求是请求一个静态资源还是动态资源;如果是静态资源,直接找到对应的文件把文件的内容通过Socket返回;如果是动态资源,才会执行到Servlet的相关逻辑。
Tomcat会根据URL中的Context Path和Servlet Path确定要调用哪个Servlet实例的service方法(如果没有找到匹配的Servlet类,就会返回404)
通过service方法,就会进一步调用到我们之前写的doGet或者doPost
3)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的service方法内部会根据当前请求的方法,决定调用其中的某个doXXX方法。
在整个流程中,有三个关键的方法:
init方法:在初始化阶段执行,用来初始化每一个Servlet对象,对象创建好之后就会执行到。用户可以重写这个方法,来执行一些初始化逻辑。
service方法:在处理请求阶段来调用,每个请求都要调用一次service
destory方法:退出主循环,tomcat结束之前会调用,用来释放资源。