Servlet介绍
什么是servlet
- Servlet(Servlet Applet的缩写,全称 Java Servlet):适用于Java编写的服务器程序,其主要功能是在于交互式的浏览和修改数据,生成动态Web内容。狭义的Servlet是指Java语言实现的一个接口,广义的Servlet 是指任何实现了这个Servlet接口的类,一般情况下,人们将Servlet,理解为后者。
- Servlet运行于支持Java的应用服务器中,从原理上将讲,Servlet可以响应任何类型的请求,但是绝大多数情况下Servlet只用于扩展基于HTTP协议的Web服务器。
- Servlet是一种实现动态页面的技术,是一组由Tomcat提供给程序员简单高效的开发一个web app
Servlet的主要工作
- 允许程序员注册一个类,当Tomcat收到的某个特定的请求,执行这个类中的一些代码
- 帮助程序员解析HTTP请求,把HTTP请求从一个字符串解析成一个HttpRequest对象
- 帮助程序员构造HTTP响应,程序员只要给指定的HttpResponse对象填写一些属性字段,Servlet就会自动按照HTTP协议的方式构造出一个HTTP响应字符串,并通过Socket编写返回客户端
Servlet程序创建步骤
创建项目
使用IDEA创建一个Maven项目
创建好的Maven如下
通过上图我们可以了解到一些目录结构,这是Maven项目的标准结构,其中
- src:用于存放源代码和测试代码的根目录
- main:用于存放源代码的目录
- test:用于存放测试代码的目录
- java:用于存放Java代码的目录
- resoures:用于存放依赖的资源文件
- pom.xml:是Maven项目的核心配置文件,关于这个Maven项目的相关属性,都是在这个xml中进行配置的
引入依赖
Maven项目创建完成之后,就i会自动生成一个pom.xml文件,我们需要在这个文件中引入Servlet API依赖的jar包
- 打开中央仓库,搜索Servlet,点击 Java Servlet API
- 选择对应的Tomcat版本的Servlet
- 将中央仓库提供的该版本的xml复制到项目的pom.xml中
- 修改后的pom.xml如下
一个项目中可以有多个依赖,每个以来都是一个<dependency>标签,引入的以来都要放在一个<dependencies>标签中,该标签用于放置项目以来的jar包,Maven会自动下载到该依赖带本地
创建目录
Web项目对于目录结构还有自己的要求只有Maven的标准目录还是不够的,需要再创建以下目录进行配置
- 在main目录下,创建一个webapp目录:webapp目录就是用于部署到Tomcat的一个重要的目录,里面可以存放一些静态资源
- 在webapp目录下,创建一个WEB-INF目录
- 在WEB-INF目录下,创建一个web.xml文件:Tomcat通过找到这个web.xml文件才能正确的处理webapp的动态资源
- 编写 web.xml:Selvlet中的web.xml中的内容是不能为空的,里面的写法是固定的(这里的写法专属于serlet),用到的时候可以直接拷贝下面的代码
<!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>
编写代码
以下编写一个让相应返回一个自定义的字符串代码:
- 创建一个TestServlet类,并且让他继承于HttpServlet
HttpServlet 这个类来自于pom.xml中引入的Servlet API依赖的jar包
- 在TestServlet类中重写doGet方法
doGet是HttpServlet类中的方法,此处是在子类中重写了父类的doGet
- 为了了解doGet方法的作用,我们可以看看它的源码
- HttpServletRequest:表示HTTP请求,Tomcat按照HTTP请求的格式把字符串格式的的请求转换为一个HttpServletRequest对象,通过这个对象就可以获取请求中的信息
- HttpServletResponse:表示HTTP响应,通过代码可以把响应的对象构造好,然后Tomcat将响应返回给浏览器
通过doGet的源码我们可以大致的了解,它的作用就是根据收到的请求通过响应返回一个405或者400,那么我们可以重写这个方法,根据收到的请求执行自己的业务逻辑,把结果构造成响应对象
- 在doGet方法中,通过HttpServletResponse类的getWriter()方法往响应的body中写入文本格式的数据
resp.getWriter()会获取到一个流对象,通过这个流对象就可以写入一些数据,写入的数据会被构造成一个HTTP相应的body部分,Tomcat会把整个响应转换成字符串,通过Socket写回给浏览器
- 需要给TesttServlet加上一个特定的注解@Webservlet("/test")
上述注解表示Tomcat收到请求中,URL的Servlet Path路径为/test的请求才会调用TestServlet这个类的代码,注解中的字符串表示着URL的Servlet Path
到这里程序的编写已经完成!但是你可能会议或上述代码不是通过main方法作为入口,这是因为main方法包含在Tomcat中,我们写的程序并不能单独执行,而是需要Tomcat才能执行起来(Tomcat的伪代码我们会具体的分析这个问题)
打包程序
在程序编写好之后,就可以使用Maven进行打包
- 首先修改pom.xml,加入一些必要的配置(打包类型和打包后的包名)
- packaging标签中用于设置打包类型(如果不进行修改,默认的类型是jar包,jar包是普通Java程序打包的结果,里面包含一i写.class文件;而部署在Tomcat中的压缩包一般为war包,war包里面是Java Web程序,里面除了.class文件以外还包含HTML、CSS、JavaScript、图片等...)
- finaName标签中用于设置打包好后的名字(包名很重要,它对应着请求中的URL的Context Path)
- 执行打包操作(打开Maven窗口,展开Liftcycle,双击package进行打包)
- 打包成功之后,可以发现多了一个target目录,该目录下有一个testSevlet.war的压缩包
部署程序
接下来我们就可以进行程序部署
- 首先将大号的war包拷贝到Tomcat的webapps目录下
- 启动Tomcat(bin目录下的startup.bat)
验证程序
此时通过浏览器访问 http://127.0.0.1:8080/testServlet/test 就可以看到程序实现的结果了
注意:URL的路径分为两部分 Context Path 和 Servlet Path
- Context Path:这个路径表示一个webapp,来与那与打包的包名
- Servlet Path: 这个路径表示一个webapp中的一个页面,来源于对应的Servlet类@webServlet注解中的内容
安装Smart Tomcat进行部署
为了简化上述操作流程,其实是有一些更加简单的方式:
- 对于创建项目、引入依赖、创建目录这三个步骤,其实可以使用项目模板来快速生成的,但是由于项目模板加载速度比较慢,因此这里不是很推荐
- 对于打包和部署程序这两个步骤,其实可以使用Smart Tomcat插件来快速实现,以下将来介绍使用方式
安装Smart Tomcat
- 在插件商店中搜索 Smart Tomcat进行安装
配置Smart Tomcat
- 点击Edit Configurations(新版本),老版本可能是Add Configuration
- 点击左上角的+号,并选择Smart Tomcat
- 主要修改这三个参数
- name:这一栏可以随意填写
- Tomcat Servlet:表示Tomcat所在的目录
- Deployment Directory:表示项目发布的目录
- Context Path:表示项目路径,默认值就是项目名称
- Servlet Port:表示服务端口
- Admin Port:表示管理端口
- VM options:表示JVM参数
- 配置好的Smart Tomcat之后,Edit Configurations就会显示成name的名字,并且右面多一个三角形运行符号
使用Smart Tomcat
- 点击三角号就可以进行正常的运行
- 点击蓝色的链接就会跳转到项目路径,再增加Servlet Path就可以显示出该程序的结果
Servlet运行原理
在servlet代码中,我们并没有写main方法,那么对应的doGet方法是如何被调用的呢?响应又是如何返回给服务器的呢?
Servlet的架构
我们自己实现的Servlet实在Tomcat基础上运行的,下图显示了Web应用程序中的位置
当浏览器给服务器发送请求的时候,Tomcat作为HTTP服务器,就可以接收到这个请求,Tomcat的工作就是解析HTTP请求,并把请求交给Servlet的代码来进行进一步的处理,Servlet的代码根据请求计算生成响应对象,Tomcat再把这个响应对象构造成HTTP响应,返回给浏览器,并且Servlet的代码也经常会和数据库进行数据的传递。
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 服务器,并用线程池的方式分别处理每一个 RequestServerSocket 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方法进行初始化
- 这些实例被销毁之前,就会调用其中的destory方法进行收尾工作
- Tomcat内部也是通过Socket API进行网络通信
- Tomcat为了能够同时处理多个HTTP请求,采取了多线程的方式实现,因此Servlet是运行在多线程环境下的
- 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;}// 走到这里的逻辑都是动态内容了// 找到要处理本次请求的 Servlet 对象Servlet ins = findInstance(req.getURL());// 调用 Servlet 对象的 service 方法// 这里就会最终调用到我们自己写的 HttpServlet 的子类里的方法了try {ins.service(req, resp);} catch (Exception e) {// 返回 500 页面,表示服务器内部错误}}
}
- Tomcat从socket中读到的HTTP请求是一个字符串,然后Tomcat会按照HTTP协议的格式解析成一个HttpServletRequest对象
- Tomcat会根据URL中的Path判定这个请求是请求一个静态资源还是动态资源,如果是静态资源,直接找到对应的文件,把文件的容通过Socket返回;如果是动态资源,才会执行Servlet相关逻辑
- Tomcat会根据URL中的Context Path和Servlet Path确定要调用哪个Servlet实例的service方法
- 通过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的service方法内部会根据当前的请求方式,决定调用其中的某个do...方法
- 在调用do...方法的时候,就会触发多态机制,从而执行到我们自己写好的子类do...方法
Servlet API 详解
对于Servlet主要介绍三个类,分别是HttpServlet、HttpServletRequest和HttpServletResponse
其中HttpSerletRequest和HttpServletResponse是Servlet规范中规定的两个接口,HttpServlet中并灭有实现这两个接口的成员变量,它们知识HttpServlet的service和do...等方法的参数,这两个接口类的实例化是在Servlet容器中实现的。
HttpServlet
核心方法
方法名称 | 调用时机 |
init | 在HttpServlet实例化之后被调用一次 |
destory | 在HttpServlet实例不再使用的时候调用一次 |
service | 收到HTTP请求的时候调用 |
doGet | 收到GET请求的时候调用(由service方法调用) |
doPost | 收到POST请求的时候调用(由Service方法调用) |
doPut/doDelete/doOptions... | 收到其他请求的时候调用(由Service方法调用) |
Servlet的生命周期:Servlet的生命周期就是Servlet对象从创建到销毁的过程,下面就来介绍其生命周期的过程
- Servlet对象由Tomcat来进行实例化的,并且在实例化完毕之后调用init方法(只调用一次)
- Tomcat对于收到的请求,都会通过对应的Servlet的service方法来进行处理(可以调用多次)
- Tomcat结束之前,会调用Servlet的destory方法来进行回收资源(最多调用一次)
注意:init和service能够保证在各自的合适时机被Tomcat调用,但是destory不一定,它是否能够被调用取决于Tomcat是如何结束的
- 如果是直接杀死进程,那么就来不及调用destory
- 如果是通过Tomcat的管理端口(默认是8005)进行关闭,就能够调用destory
处理GET请求示例:
- 直接通过URL发送一个GET方法的请求,来对这个请求进行处理
@WebServlet("/get")
public class TestServlet extends HttpServlet{@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {resp.setContentType("text/plain");resp.setCharacterEncoding("UTF-8");resp.getWriter().write("响应了一个Get请求");}
}
处理POST请求示例:
由于通过浏览器URL发送的请求是GET方法的请求,因此我们需要通过其他方式来发送一个POST请求用于处理。发送POST请求的方式有Ajax、form表单或者Socket API 构造,如果单纯用于测试就比较麻烦,所以我们可以用一个软件postman,是一个强大的API调试、Http请求的工具
@WebServlet("/post")
public class TestServlet extends HttpServlet {@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {resp.getWriter().write("post");}
}
HttpServletRequest
核心方法
方法 | 作用 |
String getProtocol() | 返回协议的名称和版本号 |
String getMethod() | 返回请求的HTTP方法名称 |
String getRequesURL() | 返回请求的URL,不带查询字符串 |
String getRequestURI() | 返回URL的一部分,不带协名,端口号,查询字符串 |
String getContextPath() | 返回指示请求URL中的Context Path 部分 |
String getServletPath() | 返回首行中的Servlet Path 部分 |
String getQueryString() | 返回首行后面的查询字符串 |
Enumeration getParameterNames() | 返回一个String 对象的枚举,包括在该请求中的参数名称 |
String getParameter(String name) | 以字符串形式返回请求参数的值,如果参数不存在则返回null |
String[] getParameterValues(String name) | 返回一个字符串对象的数组,包括所有给定的请求参数,如果参数不存在返回null |
Enumeration getHeaderNames() | 返回一个枚举,包括该请求中所有的头名 |
String getHeader(String name) | 以字符串形式返回指定的请求头的值 |
String getCharacterEncoding() | 返回请求正文中使用的字符编码的名称 |
String getContentType() | 返回请求正文的 MIME 类型,如果不知道类型则返回 null |
int getContentLength() | 以字节位单位返回请求正文的长度,并提供输入流,如果长度未知则返回-1 |
InputStream getInputStream() | 用于读取请求的正文内容,返回一个 InputStream 对象 |
- 示例一:通过上诉方法返回一个页面是该请求的具体HTTP请求格式
@WebServlet("/showRequest")
public class TestServlet extends HttpServlet{@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {//此处返回一个HTMl,在HTML中显示 HttpRequestServlet 类中的核心方法//把这些API 的返回结果 通过 StringBuilder进行拼接resp.setContentType("text/html;charset=utf-8");StringBuilder html = new StringBuilder();html.append(req.getMethod());html.append(" ");html.append(req.getRequestURL());html.append("?");html.append(req.getQueryString());html.append(" ");html.append(req.getProtocol());html.append("</br>");Enumeration<String> headerNames = req.getHeaderNames();while(headerNames.hasMoreElements()){String headName = headerNames.nextElement();String header = req.getHeader(headName);html.append(headName);html.append(": ");html.append(header);html.append("</br>");}html.append("</br>");resp.getWriter().write(html.toString());}
}
- 实例二:处理HTTP请求的body中的数据格式
如果body的内容格式是x-www-form-urlencoded(这是form表单提交的数据格式,此时body的格式就类似于 query string (是键值对的结构,键值对之间使用&分割,键与值之间使用=进行分割),使用getParameter(获取键值对)进行处理
- 此处是要获取body的数据,由于GET方法一般没有body,这里使用POST方法演示
- 约定body的数据格式是:x-www-form-urlencoded
- 约定body的数据内容是:username=123&password=111
@WebServlet("/postParameter")
public class TestServlet extends HttpServlet{@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {resp.setContentType("text/html;charset=utf-8");String username = req.getParameter("username");String password = req.getParameter("password");resp.getWriter().write("username=" + username + "</br>" +"passwd=" + password);}
}
如果body的内容格式是json,首先将整个body都读取出来,再借助第三方库方法按照json的格式来进行解析,Java标准库没有内置对于json解析的方法
- 此处是要获取body的数据,由于GET方法一般没有body,这里使用POST方法进行演示
- 约定body的数据格式为:json
- 约定body的数据内容为:
{
username:123,
password:111
}
- 此处使用jackson第三方库,使用之前需要去Maven的中央仓库将jackson的依赖引入pom.xml中
jacjkson中的核心类是ObjectMapper,通过这个类的readValue(Stringcontent,Class<T> valueType)方法,就可以将json字符串转化为一个类的对象(第一个参数是json字符串,第二个参数是类对象),ObjectMapper会遍历定义的类中的每一个成员的名称,去json字符串中的key中查找,如果找到了就将对应的值返回给该成员
/自定义的将json字符串转化的类
class UserInfo{public String username;public String password;
}
@WebServlet("/jsonParameter")
public class TestServlet extends HttpServlet{@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {resp.setContentType("text/html;charset=utf-8");//将整个body中的数据读取出来String body = readBody(req);//按照 json、格式进行解析ObjectMapper objectMapper = new ObjectMapper();UserInfo userInfo = objectMapper.readValue(body,UserInfo.class);resp.getWriter().write("username=" + userInfo.username + "</br>" + "passwd=" + userInfo.password);}private String readBody(HttpServletRequest req) throws IOException {int contextLength = req.getContentLength();//准备一个字节数组,来存放body内容byte[] buffer = new byte[contextLength];//获取·到InputStream对象InputStream inputStream = req.getInputStream();inputStream.read(buffer);//将存放在body内容的字节数组转换成字符串return new String(buffer,"utf-8");}
}
HttpServletResponse
核心方法
方法 | 作用 |
void setStatus(int sc) | 为该响应设置状态码 |
void setHeader(String name,String value ) | 设置一个带有给定的名称和值的header,如果name已经存在,则会覆盖该值 |
void addHeader(String name,String value) | 添加一个带有给定的名称和值的header,如果name已经存在,不覆盖旧值,而是添加一个新的键值对 |
void setContentType(String type) | 设置被发送到客户端的响应的内容类型 |
void setCharacterEncoding(String charset) | 设置被发送到客户端的响应的字符编码,例如utf-8 |
void sendRedirect(String location) | 设置Location字段,实现重定向 |
PrintWriter getWriter() | 用于往body中写入文本格式的数据 |
OutputStream getOutputStream() | 用于往body中写入二进制格式数据 |
示例一:通过代码,构造出不同的响应状态码
@WebServlet("/status")
public class TestServlet extends HttpServlet{@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {int status = 404;resp.setStatus(status);resp.getWriter().write("status"+status);}
}
Fiddler抓包结果:
- 实例二:在响应报头设置一个Refresh字段,实现字段刷新程序
@WebServlet("/autoRefresh")
public class TestServlet extends HttpServlet{@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {//给响应一个Refresh的header,每隔一秒刷新一次resp.setHeader("Refresh","1");//返回一个当前的时间,用来显示刷新效果resp.getWriter().write("timestamp: "+System.currentTimeMillis());}
}
- 实例三:实现重定向操作
方法一:在响应报头设置状态码和Location来实现重定向
@WebServlet("/redirect")
public class TestServlet extends HttpServlet{@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {//将状态码设置为3XXresp.setStatus(302);//设置一个Locationresp.setHeader("Location","https://blog.csdn.net/loss_rose777?type=blog");}
}
方法二:直接使用sendRedirect()方法来实现重定向
@WebServlet("/redirect")
public class RedirectServlet extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {resp.sendRedirect("https://blog.csdn.net/weixin_51367845?spm=1000.2115.3001.5343");}
}
实现服务器版表白墙程序
基本介绍
下面是表白墙的内容展示
下面是程序代码:
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title><style>*{margin: 0px;padding: 0px;}.container{width: 400px;margin: 0 auto;}h1{text-align: center;padding-bottom: 20px;}p{text-align: center;color: gray;line-height:40px;}.button{text-align: center;}.edit{margin-bottom: 20px;width: 200px;height: 30px;}.n1{margin-right: 60px;}.n2{margin-right: 41px;}.n3{margin-right: 60px;}.button{height: 40px;width: 300px;background-color: orange;color: white;border: none;}span{font-size: 20px;}</style>
</head><body><div class="container"><h1>表白墙</h1><p>输入后点击提交,会将信息显示在表格中</p><div><span class="n1">谁:</span><input type="text" class="edit"></div><div><span class="n2">对谁:</span><input type="text" class="edit"></div><div><span class="n3">说:</span><input type="text" class="edit"></div><div><input type="button" value="提交" class="button" onclick="submit()"></div></div>
</body><script>let edits = document.querySelectorAll('.edit')function submit(){let n1 = edits[0].valuelet n2 = edits[1].valuelet n3 = edits[2].valuelet div = document.createElement('div')div.className = "div1"div.innerHTML = n1+"对"+n2+"说"+n3let container = document.querySelector('.container')container.appendChild(div)alert(n1+"对"+n2+"说"+n3)}</script>
</html>
准备操作
1、创建一个Servlet项目
2、将之前写好的纯前端的表白前代码拷贝到webapp目录下
3、约定好前后端交互接口,该程序只需要约定两个接口
- 从服务器获取全部留言
- 约定请求:方法时GET,请求路径是/message
- 约定响应:版本号为HTTP/1.1,状态码是200 OK,采用的是JSON格式
- JSON具体请求格式:
[{from:"",to:"",message:""} ]
- 通过客户端给服务器新增一个留言
- 约定请求:方法为POST,请求路径为/message
- 约定响应:版本号为HTTP/1.1,状态码为 200 OK,提交成功响应页面显示“提交成功”
4、创建一个MessageServlet类。@WebServlet注解为 /message,对应着约定的请求路径,通过上方的约定完成服务器代码
5、更改前端代码
代码实现
后端代码实现:
import com.fasterxml.jackson.databind.ObjectMapper;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;
import java.util.ArrayList;
import java.util.List;//这个类表示一条消息的详细信息
class Message{public String from;public String to;public String message;
}
@WebServlet("/message")
public class messageServlet extends HttpServlet {//通过这个数组来表示所有消息private List<Message> messages = new ArrayList<>();//获取服务器所有消息操作@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {resp.setContentType("application/json;charset=utf-8");//获取到消息列表//此处需要将message数组中的数据转换成json格式返回给浏览器ObjectMapper objectMapper = new ObjectMapper();//通过ObjectMapper 的 writeValueAsString() 方法就可以将一个对象转换成一个json字符串String jsonString = objectMapper.writeValueAsString(messages);resp.getWriter().write(jsonString);}@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {resp.setContentType("text/html;charset=utf-8");ObjectMapper objectMapper = new ObjectMapper();Message message = objectMapper.readValue(req.getInputStream(),Message.class);messages.add(message);resp.getWriter().write("提交成功!");}
}
前端代码实现:
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>表白墙</title><!-- 引入Jquery --><script src="https://cdn.staticfile.org/jquery/1.10.2/jquery.min.js"></script><style>* {margin: 0px;padding: 0px;}.container {width: 400px;margin: 0 auto;}h1 {text-align: center;padding-bottom: 20px;}p {text-align: center;color: gray;line-height: 40px;}.button {text-align: center;}.edit {margin-bottom: 20px;width: 200px;height: 30px;}.n1 {margin-right: 60px;}.n2 {margin-right: 41px;}.n3 {margin-right: 60px;}.button {height: 40px;width: 300px;background-color: orange;color: white;border: none;}span {font-size: 20px;}</style>
</head><body><div class="container"><h1>表白墙</h1><p>输入后点击提交,会将信息显示在表格中</p><div><span class="n1">谁:</span><input type="text" class="edit"></div><div><span class="n2">对谁:</span><input type="text" class="edit"></div><div><span class="n3">说:</span><input type="text" class="edit"></div><div><input type="button" value="提交" class="button"></div></div>
</body>
<script>let edits = document.querySelectorAll('.edit')let Button = document.querySelector('.button');Button.onclick = function () {//1、获取到输入框中的三个请求let n1 = edits[0].valuelet n2 = edits[1].valuelet n3 = edits[2].valueif (n1 == '' || n2 == '' || n3 == '') {return;}//2、构造新的divlet div = document.createElement('div')div.className = "div1"div.innerHTML = n1 + "对" + n2 + "说" + n3let container = document.querySelector('.container')container.appendChild(div)//3、清空之前输入框的内容for (let input of edits) {input.value = '';}//4、通过ajax构造post请求,把这个请求提交给服务器//构造一个js对象let body = {from: n1,to: n2,message: n3};$.ajax({type: 'post',contentType: "application/json;charset=utf-8",//设置服务器地址的所在位置 使用相对路径表示url: 'message',//完成json对象和json格式字符串转换data: JSON.stringify(body),success: function (body) {//这是响应成功之后,要调用的回调函数console.log("消息发送给服务器成功!")}});}//在页面加载的时候,希望能够从服务器获取到所有的消息,并显示到网页中$.ajax({type: 'get',url: 'message',success: function (body) {//body是收到响应的正文 由于在响应中我们设置的是 application/json 所以此时收到的body会被jquery自动把它//从字符串转换为js对象数组,此处就不需要手动进行JSON.parsefor (let message of body) {//构造新的divlet div = document.createElement('div')div.className = "div1"div.innerHTML = message.from + "对" + message.to + "说" + message.messagelet container = document.querySelector('.container')container.appendChild(div)}}});
</script></html>
持久化存储
上述代码,我们实现了,前端与后端的交互,以及服务器的部署,但是我么可以保证在页面刷新的时候不会将数据清楚,但是当我们服务器关闭的时候,数据还是会消失,为了解决这个问题,就需要让数据能够持久化存储。
持久化存储:是把数据保存到可以永久保存的存储设备中(如硬盘),是一种将程序数据在持久状态和瞬时状态间转换的机制
持久化存储机制包括:JDBC和文件IO
1、建立数据库
drop database if exits messagewall;
create database messagewall;use messagewall;drop table if exits message;
create table message (`from` varchar(50),`to` varchar(50),`message` varchar(1024)
);
2、在pom.xml中引入mysql的jar包
3、将代码与数据库进行连接(主要是将list数组删除,添加一个save和load方法)
import com.fasterxml.jackson.databind.ObjectMapper;
import com.mysql.cj.jdbc.MysqlDataSource;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 javax.sql.DataSource;
import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;//这个类表示一条消息的详细信息
class Message{public String from;public String to;public String message;@Overridepublic String toString() {return "Message{" +"from='" + from + '\'' +", to='" + to + '\'' +", message='" + message + '\'' +'}';}
}
@WebServlet("/message")
public class messageServlet extends HttpServlet {//通过这个数组来表示所有消息//private List<Message> messages = new ArrayList<>();//获取服务器所有消息操作@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {resp.setContentType("application/json;charset=utf-8");//获取到消息列表//此处需要将message数组中的数据转换成json格式返回给浏览器ObjectMapper objectMapper = new ObjectMapper();List<Message> messages = null;try {messages = load();} catch (SQLException e) {throw new RuntimeException(e);}//通过ObjectMapper 的 writeValueAsString() 方法就可以将一个对象转换成一个json字符串String jsonString = objectMapper.writeValueAsString(messages);resp.getWriter().write(jsonString);}//这个方法用来往数据库中存一条数据private List<Message> load() throws SQLException {List<Message> messages = new ArrayList<>();DataSource dataSource = new MysqlDataSource();((MysqlDataSource)dataSource).setUrl("jdbc:mysql://127.0.0.1:3306/message?characterEncoding=utf-8&useSSL=false");((MysqlDataSource)dataSource).setUser("root");((MysqlDataSource)dataSource).setPassword("");Connection connection = dataSource.getConnection();String sql = "select* from message";PreparedStatement statement = connection.prepareStatement(sql);ResultSet resultSet = statement.executeQuery();while(resultSet.next()){Message message = new Message();message.from = resultSet.getString("from");message.to = resultSet.getString("to");message.message = resultSet.getString("message");messages.add(message);}resultSet.close();statement.close();connection.close();return messages;}@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {resp.setContentType("text/html;charset=utf-8");ObjectMapper objectMapper = new ObjectMapper();Message message = objectMapper.readValue(req.getInputStream(),Message.class);try {save(message);} catch (SQLException e) {throw new RuntimeException(e);}System.out.println("消息提交成功! message=" + message);resp.getWriter().write("提交成功!");}private void save(Message message) throws SQLException {DataSource dataSource = new MysqlDataSource();((MysqlDataSource)dataSource).setUrl("jdbc:mysql://127.0.0.1:3306/message?characterEncoding=utf-8&useSSL=false");((MysqlDataSource)dataSource).setUser("root");((MysqlDataSource)dataSource).setPassword("");Connection connection = dataSource.getConnection();String sql = "insert into message value(?,?,?)";PreparedStatement statement = connection.prepareStatement(sql);statement.setString(1,message.from);statement.setString(2, message.to);statement.setString(3, message.message);statement.executeUpdate();statement.close();connection.close();}
}
Cookie和Session
Cookie
Cookie介绍我们已经在上一篇Http中说的很详细了Cookie介绍
在了解Cookie之后,我们发现Cookie是不能够用于存储和用户直接相关的信息的,一是Cookie的存储空间有限,二是发送请求时占用的带宽很多,三是不安全。即这些数据不适合保存到客户端,保险粗在服务器是最合适的,通过会话(Session)的方式就能够保存这些数据。
Session会话机制介绍
基本介绍:
在计算机中,尤其是网络应用中,Session称为“会话控制”。Session对象存储特定用户会话所需的属性及配置信息,当用户在应用程序的Web页面跳转时,存储在Session对象中变量将不会丢失,而是在整个用户会话中一直存在下去。当用户请求来程序的Web页时,如果该用户还没有会话,则Web服务器将自动会创建一个Session对象,当会话过期或放弃后,服务器会终止该会话。Session对象最常见的一个用法就是存储用户首选项,例如,如果用户知名不喜欢查看图形的时候,就可以将该信息储存在Session对象中,注意会话状态仅在支持Cookie的浏览器中保存。
会话本质:
会话本质就是一个哈希表,其中存储一些键值对结构,key叫做sessionnId,是一个不随机的,不重复的,唯一的字符串,value就是要保存的身份信息,通过HttpSession对象来保存,key和value都是Servlet自动创建的。
每个用户登录都会产生一个会话,服务器会以哈希表的方式将这些会话管理起来
一个会话的详细数据通过一个HttpSession对象来存储,并且HttpSession对象中存储的数据也是键值对结构,key和value都是程序员自定义的
接着Cookie不适合用于存储用户相关的直接信息来讲,由于客户端不适合存储这些数据,服务器这边可以通过Session会话方式来进行保存。下面将会以用户的登录流程来介绍Session会话机制
- 当用户登录成功之后,服务器会在Session中生成一个新的记录,并把sessionId返回给客户端(例如Http响应中可以通过Set-Cookie字段返回,其中Cookie的key为"JSESSION",value为服务器生成的sessionId的具体的值)
- 然后客户端只需要保存这个sessionId,当后续再给服务器发送请求的时,请求中会带上sessionIdd(例如HTTP请求中会带上Cookie字段用于传递Session)
- 服务器收到请求之后,就会根据请求中的sessionId在Session中查询对应用户的身份信息,在后续操作
Session会话机制的好处
- 是客户端很轻量,不在保存太多的数据
- 客户端和服务器之间传输的数据量少,节省带宽
- 数据都在服务器存储,即使客户端出现问题,数据也不会丢失
注意:Servlet的session默认是保存在服务器内存中的,如果重启服务器Session数据会消失
Cookie和Session的区别
- Cookie是客户端存储数据的一种机制,可以存储身份信息,也可以存储其他信息,是键值对结构的
- Session是服务器存储数据的一种机制,主要适用于存储身份相关的信息,是键值对结构的
- Cookie和Session经常配合使用,但是不是必须的
- Cookie也完全可以保存一些数据在客户端,这些数据不一定是用户的身份信息,不一定是sessionId
- Session中的sessionId也不需要非得通过Cookie和Set-Cookie来传递
Servlet中Cookie和Session的核心方法
HttpServletRequest类中相关的方法
方法 | 作用 |
HttpSession getSession(参数) | 在服务器中获取会话,参数如果是true,当不存在会话时,会创建一个会话(包括生成一个新的sessionId和HttpSession对象),并通过Set-Cookie将sessionId返回给客户端;参数如果是false,当不存在会话时会返回null。如果存在sessionIOd且合法,就根据这个sessionId找到对应的HttpSession对象并返回 |
Cookie[] getCookies() | 返回一个数组,包含客户端发送请求的所有Cookie对象,会自动把Cookie中的格式解析成键值对 |
HttpServletresponse类中的方法
方法 | 作用 |
void addCookie(Cookie cookie) | 把指定的cookie添加到响应中 |
HttpSession类中相关方法
- HttpSession是Java平台对session机制的实现规范,因为它仅仅是个接口,具体实现为每个Web应用服务器的提供商
- 服务器会为每一个用户创建一个和独立的HttpSession,表示为一个会话,并且为一个会话,并且一个HttpSession对象中包含多个键值对,可以往HttpSession中存储需要的数据
方法 | 作用 |
Object getAttribute(String name) | 该方法返回在Session会话中会有指定名称的对象,如果没有指定名称的对象,返回null |
void setAttribute(String name,Object value) | 该方法使用指定名称绑定一个对象到该Session会话中 |
boolean isNew() | 判断当前会话是否是先创建的 |
Cookie类中的相关方法
- 这个类描述了一个Cookie,通过Cookie类创建的对象,每一个对象就是一个键值对
- HTTP的Cookie字段中实际上存储的是多个键值对,每一个键值对在Servlet中都对应一个Cookie对象
方法 | 作用 |
String getName() | 该方法返回的Cookie名称(这个值是Set-Cookie字段设置给浏览器的,创建之后不会改变) |
String getValue() | 该方法获取与Cookie关联的值 |
void setValue(String newValue) | 该方法设置与Cookie关联的值 |
实现用户登录功能
接下来将使用上述的Session和Cookie相关的方法来实现一个用户登录功能,并且可以记录访问页面次数
登录功能实现思路:
- 读取用户提交的用户和代码
- 对用密码进行校验
- 判断是否登录成功
- 创建会话,保存自定义信息
- 重定向到指定页面
登录功能实现流程:
- 实现一个登录页面
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head>
<body><form action="login" method="post"><input type="text" name="username"><br><input type="password" name="password"><br><input type="submit" value="登录"></form>
</body>
</html>
- 实现一个Servlet来处理上面的登录请求
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 javax.servlet.http.HttpSession;
import java.io.IOException;@WebServlet("/login")
public class loginServlet extends HttpServlet {@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {resp.setContentType("text/html;charset=utf-8");//1、从请求中获取到用户名和密码String username = req.getParameter("username");String password = req.getParameter("password");//2、对账号密码进行校验if(username==null||username.isEmpty()||password==null||password.isEmpty()){resp.getWriter().write("账号或密码不可以为空!");return;}//3、判断是否登录成功(假设用户名为 admin,密码为 1234。不过账号密码应该用数据库存储,这里只是用来测试)if(!username.equals("admin") || !password.equals("1234")){resp.getWriter().write("<h3>账号或密码错误!</h3>");return;}//4、登录成功,创建一个会话,用来记录当前用户信息HttpSession session = req.getSession(true);//通过这个操作,程序员就可以给session增加自定义信息,如访问次数session.setAttribute("visitCount",0);//5、把登录成功的结果返回给客户端(这里的反馈不是简单的提示登陆成功,而是直接跳转到指定页面)resp.sendRedirect("index");}
}
- 通过实现一个Servlet来表示登录成功后重定向页面
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 javax.servlet.http.HttpSession;
import java.io.IOException;
@WebServlet("/index")
public class IndexServlet extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {resp.setContentType("text/html;charset=utf8");//只有登陆成功参数才是true,这里是拿参数,所以填falseHttpSession session = req.getSession(false);//判断当前用户是否登录if(session == null){//返回登录界面resp.sendRedirect("login.html");return;}//表示用户登录过,获取会话中的访问次数Integer visitCount = (Integer) session.getAttribute("visitCount");visitCount+=1;session.setAttribute("visitCount",visitCount);resp.getWriter().write("<h3>visitCount = " + visitCount + "</h3>");}
}
实现效果如下:
上传文件操作
上传文件是日常开发中的一类常见的需求,在Servlet中也进行支持
Servlet中上传文件的核心操作
HttpServletrequest类中的核心方法
方法 | 作用 |
Part getPart(String name) | 获取请求中给定name的文件 |
Collection<Part> getParts() | 获取所有的文件 |
Part类中的相关方法
方法 | 作用 |
String getSubmittedFileName() | 获取提交的文件名 |
String getContentType() | 获取提交文件类型 |
long getSize() | 获取文件大小,单位为字节 |
void write(String path) | 把提交的文件数据写入磁盘文件 |
上传文件操作实现
1、写一个前端页面,用于上传文件
- 上传一个文件一啊不能使用post请求的表单实现
- 通过from表单构造上传文件,要加上以恶搞enctype字段,它表示的是body中的数据格式,它的默认值为:x-www-form-urlencoded,这里要修改成:multipart/form-data,他是上传文件或者图片的数据格式
- 第一个input中的type的值为file,它表示第一个输入框为文件选择框,naem的值与后端中通过getPart获取指定文件的操作有关
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head>
<body><form action="upload" method="post" enctype="multipart/form-data"><input type="file" name="Myfile" id=""><input type="submit" value="上传"></form>
</body>
</html>
2、写一个Servlet用于处理上传的文件
- 上传文件操作还需要给Servlet嘉善一个@MultipartConfig注解,否则服务器代码无法受用getPart()方法
- getPart()方法中的参数和form表单input="file"标签的name属性对应
- 客户端一次可以提交多个文件,getPart()方法根据name属性来获取不同的文件
- 写入磁盘文件操作的类路径之间可以使用两个反斜杠\\,也可以使用一个正斜杠/
- 写入磁盘文件操作的路径最后为保存后的文件名,包括文件后缀
import javax.servlet.ServletException;
import javax.servlet.annotation.MultipartConfig;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.Part;
import java.io.IOException;@MultipartConfig
@WebServlet("/upload")
public class fileServlet extends HttpServlet {@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {resp.setContentType("text/html;charset=utf-8");//通过getPart方法获取到前端传来的文件Part part = req.getPart("MyFile");//获取文件名String fileName = part.getSubmittedFileName();System.out.println("文件名:"+fileName);//获取提交的文件类型String fileType = part.getContentType();System.out.println("文件类型:"+fileType);//获取文件的大小long fileSize = part.getSize();System.out.println("文件大小为:"+fileSize);part.write("D:\\test\\text.mp4");resp.getWriter().write("上传成功!");}
}