0%

Tomcat和Servlet简单入门

Tomcat

tomcat是一个web服务器软件,说到web服务器软件,肯定免不了要提到Apache/Nginx,那tomcat和它们之间的区别是什么呢?

这里有必要先科普一个概念:

  • 静态资源:白话点就是用html、css、javascript写的那些东西,所有用户看到的代码都是一样的。
  • 动态资源:用servlet/jsp、php写的,动态的内容;动态内容需要先转换成静态才可以使用。

既然都是服务器软件,那它们必然都是能够绑定ip地址、监听端口并处理来自浏览器的HTTP请求。tomcat不仅能够做到这些,它还能够支持servlet和jsp,即所谓的动态内容。它能够根据你所做的配置,当用户访问指定url的时候,它会去加载特定的java类,在特定的类中有代码来处理页面内容。简单点就是如果你希望能够运行用servlet编写的java程序,那么必然需要使用tomcat;而如果只是一些静态页面的话,直接使用Nginx效率会更好。

Apache Tomcat and Nginx server, were created for two different tasks. NGINX is a free, open-source, high-performance HTTP server and reverse proxy, as well as an IMAP/POP3 proxy server and Apache Tomcat is an open source implementation of the Java Servlet, JavaServer Pages, Java Expression Language and Java WebSocket technologies. The Java Servlet, JavaServer Pages.

安装使用

  • 安装:直接去官网下载核心包,解压即可使用。卸载直接删除这个安装包即可。
  • 部署:直接将项目放到webapps目录下即可,如果是war还会自动解压。如果是idea的话,推荐使用tomcat模板,这样比较方便。

路径问题

因为这个问题困扰我很久,所以这里先提出来。我一般放置配置文件会放到三个地方:

  • 如果一个文件夹被标记为源根(IDEA的叫法),那么它的整体会在编译之后放到WEB-INF/classes目录下。当然也可以通过classLoader来获取。
  • 如果是直接放到web(webapp)目录下,那可以直接通过/filename来找到。比如我们一般会把jsp文件直接放到这里,然后直接访问即可。
  • 如果是放到web(webapp)目录下的WEB-INF下(比如标准的web.xml),那么可以通过/WEB-INF/filename来找到

Servlet

简单来说它就是一个运行在服务器上的Java程序。

执行原理

当服务器接收到请求之后,它会解析url请求(就是把TCP/IP的包,解析它的头部,然后封装成对应的req和resp对象),然后通过web.xml文件,查询里面是否有<url-pattern>标签,如果有就会通过<servlet-class>这个标签找到全限定类名(到这里可以发现,其实我们只需要通过url能够让其找到对应的类即可,所以可以直接使用注解来简化操作),并且让tomcat将其加载进内存并创建对象。同时Tomcat也会根据获得的请求来创建request对象,封装好并将其传递给servlet的service方法(doGet和doPost)。

生命周期

  1. 被创建:执行init方法,只执行一次。默认情况下是第一次访问的时候被创建,当然你可以配置<load-on-startup>标签的值为正整数让其在服务器启动的时候就创建。值得注意的是servlet使用的是单例模式,所以多线程访问会出现线程安全问题,所以尽量不要再servlet中定义成员变量。PS:servlet中的每一个req和resp,都是随着每次的请求而变化的,这两个不是单例。
  2. 提供服务:每次访问对应的url的时候,service方法都会执行一次。
  3. 被销毁:服务器正常关闭前,会执行这个方法,也是只执行一次。如果异常关闭就不会执行这个方法。

体系结构

servlet本身是一个接口,有一个抽象类叫GenericServlet继承了它,同时又有一个HttpServlet抽象类继承了generic。即这三者是爷爷-爸爸-儿子关系。

  • 由于servlet有5个方法要实现,而实际中我们一般会写service方法,所以GenericServlet这个抽象类继承了接口,并在其中实现了另外四个方法(空实现)。
  • 实际中我们一般使用的是HttpServlet,因为它能够判断用户的请求,并且根据不同的请求使用不同的方法。我们不在需要重写service()方法,而是需要去处理doGet等方法。

Request

这里将request请求分成了四部分:

  1. 请求行:GET /index.html HTTP/1.1
  2. 请求头:一些键值对组成的值
  3. 空行
  4. 请求体:username=aaa&password=bbb

Java中当然对这个提供了封装,具体是Tomcat会对收到的请求信息进行封装,然后传递给servlet中的service方法。下面是一些具体的方法:

  • String getMethod()
  • String getContextPath()
  • String getServletPath()
  • String getQueryString()
  • String getRequestURI()
  • StringBuffer getRequestURL()
  • String getProtocol()
  • String getRemoteIP()
  • String getHeader(String name)

这些方法一目了然,直接就可以获取到相应的信息。请求体中可以通过流来获取,分别用getReader和getInputStream来获取字符流和字节流。

为了获取参数值的方便,还提供了一些通用的方法来获取数据:

  • getParameter(String name):根据参数名字来获取对应的值。
  • getParameterMap():获取所有参数的map集合。

请求转发

假设你请求的url所对应的servlet无法完成任务,需要请求别的servlet来帮助一起完成,可以直接使用getRequestDispatcher.forward()方法来完成,如果需要携带数据,可以使用setAttribute(String name,Object obj)方法来传入,接收方可以用对应方法取出来。这里注意的是,由于是服务器内部处理的,所以客户端是完全感受不到的。

Response

同样也是分成了四个部分:

  1. 响应行:HTTP/1.1 200 OK,所以我们只需要设置一下状态码即可。setStatus(int sc)即可。
  2. 响应头:setHeader(String name,String value)
  3. 响应空行
  4. 响应体(大部分都是html的代码):也是获取流并进行操作。getWrite或者getOutputStream就能获取输出流,然后使用对应的方法即可。

重定向还有一个更简单的方法,setRedirect(String path)直接可以完成重定向任务。

还有值得注意的是,为了以后防止乱码,可以在获取流之前,加上一句response.setContentType("text/html,charset=utf-8");

一些疑问

Q:如果我没有重写post方法,只重写了get方法,那么当我使用post方法获取对应的url的时候,会发生什么?

A:HTTP协议设计了一个错误号码:405,Method Not Allowed

Q:如果urlPattern写了一个/,这意味着什么?

A:任何的url都能够匹配到它,当然是优先匹配其它的servlet,其它servlet失败的话才会匹配它。

重定向和转发

重定向

重定向就是浏览器希望请求一个页面,但是服务器告知它这个url已经没用了,需要到另外一个新的url地址去。

在代码里很方便,直接一句话就过去了:

1
resp.sendRedirect("/hello");

当然如果用telnet来进行模拟的话,可以发现本质上其实回应的是:

1
2
3
HTTP/1.1 302
Location: /hello
Content-Length: 0

也就是本质上其实是发送了一个302,Moved Temporarily,资源存在,只是临时修改了位置。很久以前做项目的时候,遇到过一个坑,就是当时的Okhttp是不会自动去追踪302的,需要手动继续操作;当然现在默认已经继续追踪了。接下来浏览器就会解析接受到的302的包,显然它能够知道应该去/hello去获取对应的资源了。

那如果希望是301呢?servlet并没有直接封装对应的301方法,不过知道了原理,自己来也是很简单,不过就是返回一个301,然后在Location里面写上对应的url地址就是了:

1
2
resp.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY);
resp.setHeader("Location", "/hello");

301和302的区别是,浏览器会记住那些301的请求,下次它会直接去对应的新的url去请求,而不是去原来的。

转发

转发就是当一个servlet自己处理不了或者需要多个servlet共同合作的情况,这种就需要从一个servlet转发到另外一个servlet。

在servlet代码层面实现是很简单的,只需要从req中获取到要转发的对应的servlet的路径,然后继续把req和resp传递下去就可以了,当然了,可以在传递之前对req和resp进行处理。

1
req.getRequestDispatcher("/hello").forward(req,resp);

在浏览器(客户端)看来,这就是一个请求,因为这是服务器内部进行转发的,客户端当然不会知道的。

ServletContext

这个是代表了整个web应用,可以用来和容器(即tomcat服务器)进行通信。比如能够获取MIME类型、共享数据、获取文件的真实路径。

  • 获取:this.getServletContext或者request.getServletContext都可以。因为这个对象代表的是整个web应用,所以仅有一个,而且获取的都是相同的。
  • 获取MIME类型:首先解释下,文件的对应类型恰好和服务器里的一个配置文件所对应,所以可以用方法调用直接获取。其实就是一个文件后缀对应一个MIME类型。
  • 数据共享:由于代表的是整个web应用,所以所有用户之间共享。
  • 获取文件真实路径:getRealPath()

会话

为了能够在一次会话范围内,即多次的请求之间来共享数据。

  • 客户端会话技术——cookie
  • 服务器端会话技术——session

两者的区别:

  • session没有大小限制,由于存在服务器端,比较安全。

session

大部分情况下,session是基于cookie的;更为准确的说,session用的是一种利用唯一ID识别用户身份的机制,而这种机制大部分情况下是cookie,所以才会说session是基于cookie的。当禁用了cookie的时候,只要还能找到别的途径(比如我直接把id放到url栏里),就仍然可以使用session。

在servlet中,HttpSession这个类就是靠发送JSESSIONID来实现的。

session还面临的一个问题是,由于session是单个服务器中存储的,那怎么保证集群中共享呢?

  • 直接把session序列化后(通常要加密)变成cookie,并且放入到根域中发回给用户。用户访问该根域下其他的网站自然会带上这个cookie,服务器端进行反序列化即可。相当于把session当成cookie用了。
  • 让Nginx这类的反向代理转发的时候必定转发到同一个服务器;显然负载均衡就不能用了,这个不太推荐。
  • 利用mysql数据库。但是很考验数据库的能力。
  • 和第一种思想接近,就是不用session,而是把session变成token,不论去哪里都带着这个token即可。
  • Tomcat内置的session共享(强烈不推荐,浪费带宽)
  • Spring-session + Redis技术。比较推荐的一种方法。

这个技术相当于已经非常熟悉了,具体步骤比较简单:

  1. 创建cookie:new Cookie(String name,String value)
  2. 将cookie加入到response中:response.addCookie(Cookie c)
  3. 接下来的只需要每次获取request的时候获取下cookie即可。request.getCookies()

一些小tips:

  • 可以使用多个cookie吗?当然可以,创建多个,然后都加入进去即可。
  • cookie能够保存多长时间呢?默认的情况下浏览器关闭,cookie就没了;当然可以由服务器传递一个字段给客户端,让它保存得久一点:setMaxAge(int seconds);正数就是保存的秒数;负数就是默认值;如果是0的话就是删除cookie。
  • cookie在默认情况下,同一服务器下的项目之间是隔离的。因为默认情况下,cookie设置的是当前的虚拟目录,所以只需要cookie.setPath("/");,这样同一个tomcat服务器中不同的项目就可以共享了。
  • cookie可以通过设置域名,在多个服务器之间共享。例如:cookie.setDomain(".baidu.com");

session

和cookie对应,只不过它的数据是存放在服务器端,对应的对象是HttpSession

  1. 创建session:request.getSession
  2. 使用:session.setAttribute(String name, Object obj);

我们都知道,HTTP是无状态的,那么服务器是怎么知道来的请求是哪一个的呢?只有知道了来自同一个用户,才可以为其分配相同的session。其实session是依赖于cookie的。在第一次获取session的时候,那么在内存中就会新建一个session对象,然后有一个对应的sessionID对应它,然后就会在响应的时候把这个数据发还给客户端,形如这样:set-cookie:JSESSIONID=sessionID,之后客户端就能凭借这个cookie来找到这个session了。

小tips:

  • 客户端关闭,服务器不关闭,那么获取的session是否是同一个呢?默认情况下不是。但是我们可以通过设置cookie的存活时间,来让session也能活很久。
  • 客户端不关闭,服务器关闭,获取的session还是同一个吗?当然不是!!服务器都关了,内存里的对象当然也全部都没了。但是这样会引发一个问题,假设我的session时间是一个月,那我过几天再访问同一个服务器会发现自己的购物车里的内容没了。当然我们也有办法解决这个办法:session对象序列化到硬盘上就行了。之后反序列化就行了,分别叫session的钝化和活化。而且这是tomcat是自动完成的。
  • Session默认的生效时间是30分钟(在web.xml中可以修改),当然也可以调用对象的销毁方法
  • 和cookie不同,因为放在服务器上,所以理论上可以任意大。

jsp

太累了,不学这东西。老老实实用vue吧。只需要知道jsp本质上其实是一个servlet即可。

MVC

Servlet用来处理复杂的逻辑,JSP用来进行页面的展示。当然其实它们俩任何一个都可以完成工作,不能分层而已。

所以一般我们的逻辑是这样的,首先基于java一切都是对象的思想,我们创建好对应的对象。然后创建好一个servlet用来处理对应的逻辑,处理完成之后,把对应的java对象放入到req中,并通过forward发送到另外一个jsp页面中;jsp把对应的数据取到,然后渲染出来就可以了。这就是最初的MVC的样子了。M对应的java对象,C对应的Servlet来处理复杂的逻辑,V对应的JSP来渲染页面。

自己来做一个MVC框架

虽然其实本质上我们使用servlet+jsp本质上就是在用mvc,但是缺点太多了:比如servlet必须要继承Httpservlet,jsp的一些语法太复杂了,我们还需要自己处理url带来的参数,而不是java面向对象的思想。

自定义的MVC的只需要能够处理GET请求和POST请求就够了。

我们首先定义一个DispatcherServlet(Dispatcher是调度员的意思,用在这里还是很贴切的),它能够接受所有的请求,接到请求之后,再分发到特殊的Servlet去处理。

然后对应的Get方法需要能够找到对应的servlet,POST同理,都需要两个对象来指代它们。

首先是get的处理对象。必然是需要有某个具体的对象的具体的方法来进行处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class GetDispatcher {
// 是哪个controller来处理
Object controller;

// 是controller的哪个方法
Method method;

// 参数的名字
String[] parameterNames;

// 参数的类型
Class<?> parameterClasses;
}

当获取了上面的四个参数的时候,就可以利用反射来执行对应的方法了。post同理,只不过post的参数多种多样,可以是json,也可以是各种形式的数据。

这里为了便于理解,把Spring MVC的逻辑搬过来:

  1. 用户发送出请求到前端控制器DispatcherServlet,这个Servlet其实本质上就是urlPattern='/'的这个Servlet,它会收集所有的请求。
  2. DispatcherServlet收到请求调用HandlerMapping(处理器映射器),也就是通过这个映射器知道,应该下一步把请求给谁。
  3. HandlerMapping找到具体的处理器(可查找xml配置或注解配置),生成处理器对象及处理器拦截器(如果有),再一起返回给DispatcherServlet。
  4. DispatcherServlet调用HandlerAdapter(处理器适配器)。
  5. HandlerAdapter经过适配调用具体的处理器(Handler/Controller)。
  6. Controller执行完成返回ModelAndView对象。ModelAndView真的就是Model和View这两个对象封装在一起的对象。
  7. HandlerAdapter将Controller执行结果ModelAndView返回给DispatcherServlet。
  8. DispatcherServlet将ModelAndView传给ViewReslover(视图解析器)。
  9. ViewReslover解析后返回具体View(视图)。
  10. DispatcherServlet根据View进行渲染视图(即将模型数据填充至视图中)。
  11. DispatcherServlet响应用户。

而我们上面的GetDispatcher其实就是HandlerAdapter。

过滤器

每次浏览器去访问服务器的时候,都会通过一个过滤器,一般用来做通用的处理,比如做编码处理等。

和servlet一样,也是一个接口,所以定义一个类来实现即可;同样的,也可以通过注解或者在web.xml中书写代码来完成配置。

执行原理

具体的流程是这样的:浏览器发送一个请求,如果这个请求匹配到了过滤器,那么就会先来到过滤器里,然后过滤器中会有放行的代码,之后就会到servlet去处理,处理完了之后还会再次通过过滤器,接着执行当时放行后的代码。所以前半部分关注的是request,后半部分关注的是response。

生命周期

一共有三个方法,分别是

  • init,服务器启动的时候就会执行,所以一般在这里加载资源。
  • destroy,服务器正常关闭的时候会执行,在这里释放资源。
  • doFilter,这里注意的是,需要在doFilter里面放行,否则就后面收不到了。

详细配置

路径拦截:

  • 直接配置特定资源,如/index.jsp
  • 配置指定目录,如/dir,这样这个目录下所有的servlet都会被拦截
  • 后缀名拦截:*.jsp

资源访问方式拦截,通过设置dispatcherType=资源类型,可以有下面几个:

  • REQUEST:默认值,即浏览器访问服务器资源
  • FORWARD:转发访问资源。
  • INCLUDE:包含访问资源
  • ERROR:错误跳转
  • ASYNC:异步访问资源

过滤器链

执行顺序:先过过滤器1,然后过过滤器2,回来的时候先过过滤器2,在过过滤器1。那么问题来了,怎么配置呢?

还是可以注解配置和web.xml配置,注解配置的直接对比过滤器的类名的字符串比较…(⊙﹏⊙),也就是aFilter会比bFilter先执行。而在web.xml中,看谁先定义就先拦截。

监听器

作为三大组件之一,但是使用起来还是比较容易的,就是用来监听某些具体的事件发生,当事件发生的时候,就可以执行对应的操作。