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)。
生命周期
- 被创建:执行init方法,只执行一次。默认情况下是第一次访问的时候被创建,当然你可以配置
<load-on-startup>标签的值为正整数让其在服务器启动的时候就创建。值得注意的是servlet使用的是单例模式,所以多线程访问会出现线程安全问题,所以尽量不要再servlet中定义成员变量。PS:servlet中的每一个req和resp,都是随着每次的请求而变化的,这两个不是单例。 - 提供服务:每次访问对应的url的时候,
service方法都会执行一次。 - 被销毁:服务器正常关闭前,会执行这个方法,也是只执行一次。如果异常关闭就不会执行这个方法。
体系结构
servlet本身是一个接口,有一个抽象类叫GenericServlet继承了它,同时又有一个HttpServlet抽象类继承了generic。即这三者是爷爷-爸爸-儿子关系。
- 由于servlet有5个方法要实现,而实际中我们一般会写service方法,所以
GenericServlet这个抽象类继承了接口,并在其中实现了另外四个方法(空实现)。 - 实际中我们一般使用的是
HttpServlet,因为它能够判断用户的请求,并且根据不同的请求使用不同的方法。我们不在需要重写service()方法,而是需要去处理doGet等方法。
Request
这里将request请求分成了四部分:
- 请求行:
GET /index.html HTTP/1.1 - 请求头:一些键值对组成的值
- 空行
- 请求体:
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
同样也是分成了四个部分:
- 响应行:
HTTP/1.1 200 OK,所以我们只需要设置一下状态码即可。setStatus(int sc)即可。 - 响应头:
setHeader(String name,String value) - 响应空行
- 响应体(大部分都是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 | 302 |
也就是本质上其实是发送了一个302,Moved Temporarily,资源存在,只是临时修改了位置。很久以前做项目的时候,遇到过一个坑,就是当时的Okhttp是不会自动去追踪302的,需要手动继续操作;当然现在默认已经继续追踪了。接下来浏览器就会解析接受到的302的包,显然它能够知道应该去/hello去获取对应的资源了。
那如果希望是301呢?servlet并没有直接封装对应的301方法,不过知道了原理,自己来也是很简单,不过就是返回一个301,然后在Location里面写上对应的url地址就是了:
1 | resp.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY); |
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技术。比较推荐的一种方法。
cookie
这个技术相当于已经非常熟悉了,具体步骤比较简单:
- 创建cookie:
new Cookie(String name,String value) - 将cookie加入到response中:
response.addCookie(Cookie c) - 接下来的只需要每次获取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
- 创建session:
request.getSession - 使用:
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 | public class GetDispatcher { |
当获取了上面的四个参数的时候,就可以利用反射来执行对应的方法了。post同理,只不过post的参数多种多样,可以是json,也可以是各种形式的数据。
这里为了便于理解,把Spring MVC的逻辑搬过来:
- 用户发送出请求到前端控制器DispatcherServlet,这个Servlet其实本质上就是
urlPattern='/'的这个Servlet,它会收集所有的请求。 - DispatcherServlet收到请求调用HandlerMapping(处理器映射器),也就是通过这个映射器知道,应该下一步把请求给谁。
- HandlerMapping找到具体的处理器(可查找xml配置或注解配置),生成处理器对象及处理器拦截器(如果有),再一起返回给DispatcherServlet。
- DispatcherServlet调用HandlerAdapter(处理器适配器)。
- HandlerAdapter经过适配调用具体的处理器(Handler/Controller)。
- Controller执行完成返回ModelAndView对象。ModelAndView真的就是Model和View这两个对象封装在一起的对象。
- HandlerAdapter将Controller执行结果ModelAndView返回给DispatcherServlet。
- DispatcherServlet将ModelAndView传给ViewReslover(视图解析器)。
- ViewReslover解析后返回具体View(视图)。
- DispatcherServlet根据View进行渲染视图(即将模型数据填充至视图中)。
- 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中,看谁先定义就先拦截。
监听器
作为三大组件之一,但是使用起来还是比较容易的,就是用来监听某些具体的事件发生,当事件发生的时候,就可以执行对应的操作。