目录

Servlet 是什么

第一个Servlet程序

创建项目

引入依赖

创建目录结构

编写代码

打包程序

部署程序

验证程序

更方便的部署方式

配置 Smart Tomcat 插件

Smart Tomcat的运行原理与限制

访问出错怎么办?

出现404

出现405

出现500

出现"空白页面"

出现"无法访问此网站"

Servlet API详解

HttpServlet

核心方法

代码案例

HttpServletRequest

核心方法

前端给后端传参

HttpServletResponse

核心方法

代码示例: 服务器版表白墙

明确需求

约定前后端交互接口

编写后端代码

编写前端代码

数据存入数据库

执行结果

Servlet 是什么

Servlet 是一种实现动态页面的技术. 是一组 Tomcat 提供给程序猿的 API, 帮助程序猿简单高效的开发一个 web app.

回顾 动态页面 vs 静态页面

静态页面也就是内容始终固定的页面. 即使

用户不同/时间不同/输入的参数不同 , 页面内容也不会发生变化. (除非网站的开发人员修改源代码, 否则页面内容始终不变).

对应的, 动态页面指的就是

用户不同/时间不同/输入的参数不同, 页面内容会发生变化.

注: 静态页面只是单纯的 html; 动态页面则是 html + 数据.

构建动态页面的技术有很多, 每种语言都有一些相关的库/框架来做这件事.

Servlet 就是 Tomcat 这个 HTTP 服务器提供给 Java 的一组 API, 来完成构建动态页面这个任务.

Servlet 主要做的工作

允许程序猿注册一个类, 在 Tomcat 收到某个特定的 HTTP 请求的时候, 执行这个类中的一些代码.帮助程序猿解析 HTTP 请求, 把 HTTP 请求从一个字符串解析成一个 HttpRequest 对象.帮助程序猿构造 HTTP 响应. 程序猿只要给指定的 HttpResponse 对象填写一些属性字段, Servlet 就会自动的安装 HTTP 协议的方式构造出一个 HTTP 响应字符串, 并通过 Socket 写回给客户端.

简而言之, Servlet 是一组 Tomcat 提供的 API, 让程序猿自己写的代码能很好的和 Tomcat 配合起来, 从而更简单的实现一个 web app.

而不必关注 Socket, HTTP协议格式, 多线程并发等技术细节, 降低了 web app 的开发门槛, 提高了开发效率.

第一个Servlet程序

创建项目

使用IDEA创建一个Maven项目

Maven是一个"工程管理"工具

1. 规范目录结构

2. 管理依赖(使用了哪个第三方库, 都给处理好)

3. 构建

4. 打包

5. 测试

......

我们主要使用的功能是 管理依赖 和打包.

File->New->Project...

如果首次使用Maven, 项目创建好之后, 会在IDEA窗口的下面读条, 从中央仓库加载一些Maven的依赖.

1. 需要联网

2. Maven仓库在国外, 网络不一定稳定, 这里的读条可能会比较久.

注: Maven是一个独立的程序, 但是不需要单独下载安装. 因为IDEA已经自带了(不需要安装任何额外的插件).

创建好Maven项目后,其目录结构如下:

首先src目录表示里面是源代码, 这里面再分成main和test, main用于放业务代码, test用于放测试代码. main下有java目录, 用于放java代码, resources则用于放程序依赖的文件(配置文件, 数据文件, 图片, 图标, 声音...). 而pom.xml则是Maven总的配置文件, 这个文件通过xml的格式组织.

引入依赖

引入依赖就是引入Servlet对应的jar包.

注意我们所写的Servlet代码是基于Tomcat API来进行的, 那么这个API就得要通过第三方库的方式引入进来. 于是我们就需要打开Maven的中央仓库. 并搜索servlet, 如图, 点击 Java Servlet API之后就会看到很多版本.

(注意要选择3.1.0版本, 和Tomcat相匹配)

点击版本号进去之后, 我们直接将下面Maven标签页的内容拷贝到pom.xml中.

注意此处粘贴要先创建dependencies标签. 这个标签是我们自己写的, 它是project顶级标签的字标签, 位置不能放错. 如果有多个依赖, 都往标签里一次粘贴就行.

首次使用 artifactId 标签中的 javax.servlet-api 可能是红色的, 说明此时还没下载完.

一般只要粘贴进来之后, IDEA的Maven就会自动触发依赖的下载, 下载完毕就不红了.(下载只需一次). 如果粘贴进来半天还没动, 就手动在Maven菜单中刷新.

注: 也把这段引入依赖的代码称为该依赖在Maven仓库的坐标.

创建目录结构

当前Maven已经帮我们自动的创建了一些目录, 但是还不够, 此处是需要使用Maven开发一个web程序, 还需要别的目录.

在main目录下, (和java, resource并列),创建一个webapp目录.在webapp下创建WEB-INF目录.再在WEB-INF目录下创建一个web.xml文件

web.xml文件的作用: 让Tomcat能够识别当前代码为webapp, 并进行加载.

注意: 此处的目录名字和结构, 都不能写错(包括大小写)

给web.xml写入以下内容

"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"

"http://java.sun.com/dtd/web-app_2_3.dtd" >

Archetype Created Web Application

当前写的servlet程序和以往写的别的代码相比, 有一个非常大的区别——没有main方法.

main方法可以视为是汽车的发动机, 有发动机才能跑.

如果现在有辆车, 没有发动机, 那么就要挂个车头拽着它才能跑起来.

那么此处写的servlet程序就相当于是车厢, 即没有发动机的车. Tomcat就是车头.

我们把写好的servlet程序拷贝到webapp目录下, 就相当于把车厢给挂到车头后面了.

而Tomcat就是靠WEB-INF/web.xml识别webapps目录下需要拉着跑的车厢.

编写代码

在 java 目录中创建一个类 HelloServlet, 代码如下:

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("/hello")

public class HelloServlet extends HttpServlet {

@Override

protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

// 这个代码一定要注释掉, 父类里的这个方法只是返回了一个错误页面~

// super.doGet(req, resp);

// 这个是在服务器的控制台打印

System.out.println("hello world");

// 要想把 hello world 返回到客户端, 需要使用下面的代码.

// getWriter会得到一个Writer对象.

resp.getWriter().write("hello java");

}

}

HttpServlet是Servlet API里提供的现成的类, 写servlet代码一般都是继承这个HttpServlet, 针对HttpServlet进行功能的扩展.继承完毕之后要重写doGet方法. 注意删掉super.doGet做的工作就是根据请求计算响应.HttpServletRequest表示一个HTTP请求; HttpServletResponse表示一个HTTP响应.我们写的doGet方法, 不需要我们自己手动调用, 而是交给Tomcat来调用. Tomcat收到get请求, 就会自动触发doGet方法, 构造好两个参数 req 和 resp, 一个是请求, 一个是响应.

请求req是Tomcat针对请求已经解析构造好的对象. req是从TCP Socket中读出来的字符串, 按照HTTP协议解析得到的字符串. 这个对象里的属性就是和HTTP请求报文格式相对应的. 也就是说Tomcat会把接收到的请求封装成对象, 方便我们程序员后续直接通过对象.方法使用.响应对象resp是一个空的对象(不是null, 只是new了一个对象, 里面的各种属性没设置), 这个对象就是程序员根据请求req结合业务逻辑来构造出具体的对象. resp相当于一个"输出型参数"super.doGet()是父类的doGet, 务必要注释掉这行代码, 它没有做任何实际的工作, 返回的是一个错误的页面.

resp.getWriter().write("hello world");

查询源码可知, getWriter会得到一个Writer对象, 这个对象存储于resp中, 也就是说这个Writer对象是从属于resp对象的, 此处的write操作其实是往resp的body部分进行写入. 等resp对象整个构造好了, Tomcat会统一的转成HTTP响应的格式, 再写socket.

注: 流对象不一定非得是写入网卡, 也不一定是写入硬盘, 也可以写到内存缓冲区里(关键是看程序员代码实现的细节). 当然这里也不排除write是直接通过resp对象往网卡里写的(具体是哪种方式实现, 取决于写Tomcat的大佬怎么决策了)

注解@WebServlet("/hello")

@WebServlet注解起到的作用:能够把当前的类和具体的HTTP请求的路径关联起来, 也就是URL中带层次结构的资源路径.

注解是Java中的特殊的类. Java专门定义了一种"语法糖"来实现注解.

注解的作用: 针对一个类/方法, 进行额外的"解释说明", 赋予这个类/方法额外的功能/含义.

@Override是jdk里面自带的一个注解; 而@WebServlet则是Tomcat给我们实现的一个注解, 后期的Spring也会有很多种注解.

"doGet是Tomcat收到GET请求的时候就会调用" 这个说法其实并不准确.

具体要不要调用doGet, 还得看当前GET请求的路径是什么, 不同的路径可以触发不同的代码(关联到不同的类上)

一个Servlet程序中, 可以有很多的Servlet类.

每个Servlet类都可以关联到不同的路径(对应到不同的资源), 因此此处的多个Servlet就实现了不同的功能.

注意: 路径和Servlet类之间是一对一关系.

打包程序

把程序编译好得到一些.class文件. 再把这些.class打成 压缩包.

注意: 前面介绍的jar包就是一种 .class构成的压缩包, 但是此处要打的是war包.

jar包只是一个普通的Java程序, war则是Tomcat专属的用来描述webapp的程序. 一个war就是一个webapp.

借助Maven点击即可.

展开Maven面板, 在Lifecycle目录中选中package, 双击或者右键运行即可.

点击之后Maven就会开始下面的构造工作, 如果构建成功就会显示BUILD SUCCESS.

构建出来的包在Project的target下, 这个target目录中就包含了我们刚才打好的包.

注意: 可以看到, 打好的包是.jar而并不是.war, 因为默认情况下Maven打的是jar包, 此处需要打war包,我们还需要修改一下pom.xml

在project顶级标签下加上packaging标签, 这个标签描述了打的包是哪种.

war

再然后添加build标签, 指定finalName, finalName起到的作用是描述了打的war包的名字.(这里可以不写)

hello_servlet

我们再次打包, 稍等片刻, 就可以看到生成的war包了.

为什么这里要如此繁琐的打包?

因为在实际开发中, 手动打包, 手动部署这样的操作很常用.

开发环境(自己写代码的电脑) 和 运行环境(另外一个服务器) 很可能不是一个环境.

而使用IDEA的直接点击Run运行是本地运行, 而不是在另一个服务器上运行.

所以要想能够程序能够在指定的环境中运行, 就需要进行这样的打包部署操作. 主要的目的就是为了能够完成不同环境的切换.

部署程序

把刚才打包好的war拷贝到Tomcat的webapps目录中即可.

在IDEA中, 右键war包->Open in->Explorer, 打开war包所在目录.

进行拷贝.

然后启动Tomcat(如果Tomcat正在运行, 直接拷贝, Tomcat也能识别.

这个识别操作在Windows上可能存在bug, 实际工作中, Tomcat基本都是在Linux上运行的

验证程序

打开浏览器, 输入URL, 访问写好的这个代码, 最终我们的hello world就显示出来了.

注意URL路径的写法, 这里有两级路径.

第一级路径, 也叫做context path / application path, 这个目录就代表一个webapp, 网站第二级路径也叫做servlet path, 对应到代码中的注解.

务必保证这两级路径都是正确的, 上述规则, 都是Tomcat给出的要求, 程序员必须遵守.

小结

在浏览器地址栏中输入URL后, 浏览器就构造了一个对应的HTTP GET请求, 发给了Tomcat.

Tomcat就根据第一级路径, 确定了具体的webapp. 根据第二级路径, 确定了是调用哪个类. 然后在通过GET / POST方法确定调用HelloServlet的哪个方法(doGet, doPost......)

此处Tomcat就执行对应的代码, 完成对应工作.

HTTP服务器也是基于TCP的服务器.

创建TCP server socket绑定端口->accept接收连接->收到连接之后, 从socket读取请求->把读到的请求构造成HttpServletRequest对象.

再根据请求里的URL两级路径, 确定一个Servlet类.(HTTP服务器里提前构造好一个hash, 把 路径 和 类 保存好)

->创建对应Servlet实例, 根据HTTP方法决定Servlet类的方法, 执行方法就行了.

->执行完了之后得到HttpServletResponse对象

->按照HTTP协议构造HTTP响应(得到字符串), 写回socket.

上述步骤是使用Servlet最朴素的步骤, 当然也可以通过一些操作来简化上述过程.

更方便的部署方式

手动拷贝 war 包到 Tomcat 的过程比较麻烦. 我们还有更方便的办法.

此处我们使用 IDEA 中的 Smart Tomcat 插件完成这个工作.

配置 Smart Tomcat 插件

如果是首次使用Smart Tomcat, 就需要进行配置(配置一次之后后续就不必)

新增运行配置

点击+新增配置

设置一下Tomcat所在路径.

运行Tomcat

此时可以看到, Tomcat启动成功了, 提示了Server startup in 1775 ms

在启动之后, 我们还是在浏览器进行同样的验证操作, 可以看到成功运行了.(把显示到客户端的字符改为了hello java以区分.)

注意: Tomcat运行之后, 运行日志中的链接不用点, 会直接404, 因为不包含servlet path.

Smart Tomcat的运行原理与限制

当我们在这里面进行一系列的操作之后, 其实我们在Tomcat的webapp中并没有出现新的war包, 所以Smart Tomcat的工作原理并不是说自动把war包拷贝了(webapp里是不变), 是

通过另一种方式启动Tomcat的.

Tomcat支持启动的时候显式指定特定的webapp目录. 相当于是让Tomcat加载单个webapp运行. 这个操作的意思就是让IDEA直接调用Tomcat, 让Tomcat加载当前项目中的目录. 换句话说, 这个过程其实没有打war包, 也没有拷贝, 也没有解压缩的过程...

此时虽然程序是可以运行, 但是像之前webapps下的一些已有的内容(比如欢迎页面)就没有了.

比如此时再访问127.0.0.1:8080, 页面就不存在了.

访问出错怎么办?

出现404

主要有两个方面

路径写错了

少写了 Context Path

通过 /hello 访问服务器

少写了 Servlet Path

通过 /ServletHelloWorld 访问服务器

Servlet Path 写的和 URL 不匹配

修改 @WebServlet 注解的路径

URL 中的路径写作 "/helloServlet" , 而代码中写作的 Servlet Path 为 "/hello" , 两者不匹配.

webapp没有正确被部署

web.xml 内容错了文件名错了文件路径错了文件位置错了......

出现405

比如浏览器里发了个GET请求, 但是代码里没写doGet, 此时就会405.

将上面代码的doGet方法注释掉, 重启Tomcat, 打开浏览器刷新, 此时看到了405.

super.doGet没有删掉.

因为super.doGet里面的代码就是返回405, 所以会出现405以及一堆?

以下为doGet源码.

出现500

本质是是代码抛出异常了, 500的时候日志中会明确写出异常调用栈, 直接写出是哪一行代码出的异常.

将以下代码写入HelloServlet.java中, 重启Tomcat运行并打开浏览器刷新, 就会看到显示出来的异常.

出现"空白页面"

出现这个错误的最可能原因就是代码里没写resp.getWriter().write

出现"无法访问此网站"

这种情况一般就是Tomcat没启动.

出现这种情况的时候要去检查Tomcat是否启动, 看看有没有Server startup in xxx ms这句话. 如果没启动好, 往上翻日志, 很可能是端口冲突了.

Servlet API详解

HttpServlet

我们写 Servlet 代码的时候, 首先第一步就是先创建类, 继承自 HttpServlet, 并重写其中的某些方法.

核心方法

方法名称 调用时机 init 在HttpServlet实例化之后被调用一次 destroy 在HttpServlet实例不再使用的时候调用一次 service 收到HTTP请求的时候调用 doGet 收到GET请求的时候调用(由service方法调用) doPost 收到POST请求的时候调用(由service方法调用) doPut/doDelete/doOptions/... 收到其他请求的时候调用(由service方法调用)

init, destroy, service是HttpServlet比较关键的三个方法init在HttpServlet实例化之后被调用一次. 注意不是手动调用, 而是由Tomcat自动调用. 但是也不是Tomcat启动之后立即调用(立即实例化), 而是Tomcat首次收到该类相关联的请求的时候调用. (类似于之前所讲的懒汉模式).

"相关联的请求": Tomcat收到了 /hello 这样的路径请求(后文的"请求"若无说明也代表此处), 就会调用到HelloServlet. 于是就需要先对HelloServlet进行实例化(实例化只进行一次).

后续再收到 /hello, 此时不必再重复实例化, 直接复用之前的HelloServlet即可.

我们这里面的init方法, 就是在首次收到这样一个请求的时候, Tomcat自动创建这个类的实例(注意这个类继承自HttpServlet, 意思是HttpServlet里面的各种方法属性HelloServlet也都有, 也就是init会首先在父类中存在并调用), 所以我们在代码中可以重写init方法, 从而去插入一些我们自己的"初始化"相关逻辑.

比如说在这里我们在HelloServlet中重写init方法并写一个打印"init".

@Override

public void init() throws ServletException {

// 可以在这里重写 init 方法, 插入一些我们自己的"初始化"相关的逻辑

System.out.println("init");

}

@Override

protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

System.out.println("hello world");

}

此时我们运行程序并通过浏览器访问代码, 如下图, 服务器已启动.

可以看到当服务器启动完成之后, "init"并没有直接打印出来. 注意, 只有服务器收到这样一个请求(http://localhost:8080/hello_servlet), 关联到hello路径的时候才会触发.

在浏览器中输入地址: 127.0.0.1:8080/hello_servlet/hello, 点击回车, 此时就触发了请求.

然后在服务器日志上就可以看到如下所示, 此时也说明初始化操作已经执行了:

如果后续再多刷新几次, 每刷新一次就是多发送一个请求, 可以看到运行日志中出现了多个hello world, 但是init只出现了一次, 说明init()只执行一次, 而后面的doGet()可以每一次请求都去触发.

destroy在HttpServlet实例不再使用(即服务器终止时.)的时候调用一次. 使用该方法可以进行一些清理类的工作.

@Override

public void destroy() {

System.out.println("destroy");

}

重写以上代码并重启服务器. 然后我们发一个请求, 并在浏览器刷新.可以看到运行日志.

当我们终止这个程序时, 可以看到"destroy"打印了, 注意这里的destroy能不能被执行到, 是有待商榷的.

(1) 如果是通过Smart Tomcat的停止按钮, 这个操作本质上是通过Tomcat的8005端口, 主动停止, 能够触发destroy.

(2) 如果是直接杀进程, 此时可能就来不及执行destroy就没了.

service收到HTTP请求(路径匹配的请求(doGet就是在service中调用的))就会触发.

Servlet的生命周期

Servlet生命周期描述的是Servlet创建到销毁的过程:

当一个请求从HTTP服务器转发给Servlet容器时,容器检查对应的Servlet是否创建,没有创建就实例化该Servlet,并调用

init()方法,

init()方法只调用一次,之后的请求都从第二步开始执行;

请求进入

service()方法,根据请求类型转发给对应的方法处理,如doGet, doPost, 等等

容器停止前,调用

destory()方法,进行清理操作,

该方法只调用一次,随后JVM回收资源。

简述:

开始的时候, 执行init每次收到请求, 执行service销毁之前, 执行destroy.

一个Servlet程序里面可以包含很多个Servlet.

某个Servlet的生死, 不影响整个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 java.io.IOException;

@WebServlet("/method")

public class MethodServlet extends HttpServlet {

@Override

protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

System.out.println("doGet");

resp.getWriter().write("doGet");

}

@Override

protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

System.out.println("doPost");

resp.getWriter().write("doPost");

}

@Override

protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

System.out.println("doPut");

resp.getWriter().write("doPut");

}

@Override

protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

System.out.println("doDelete");

resp.getWriter().write("doDelete");

}

}

使用浏览器来访问这些代码, 注意请求路径是method.

同时日志中也打印了doGet, 可见从浏览器直接输入URL发送的是一个GET请求.

使用Postman构造不同种类的请求

使用Postman演示其他代码的效果.

打开Postman, 创建新的标签页输入URL选择请求进行构造.

我们可以通过Postman非常方便的构造出不同的请求.

运行日志:

使用ajax构造不同种类的请求

在webapp目录下创建test.html

content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">

Document

然后我们打开浏览器将URL改为127.0.0.1:8080/hello_servlet/test.html .注意当前页面什么都没有布置, 所以没有任何内容, 但是可以打开控制台, 就可以看到这里面的doGet日志了.

注意当前页面所放位置与访问这个html所使用的方式.

URL与文件位置要相匹配:

访问这个html时, 依然要加上context path. 然后后面的test.html要和文件名相匹配, 于是这个页面就找到了.

构造请求的代码的URL:

以下直接写的是相对路径, 相对路径有个基准目录(基准目录就是该html所在的路径.)

此处写的method就相当于在 http://127.0.0.1:8080/hello_servlet基础上再拼上一个method, 最终就是http://127.0.0.1:8080/hello_servlet了

那么此处也可以写作绝对路径.

url: '/hello_servlet/method',

这里这个绝对路径访问的是method这个servlet构成的动态页面, 对应的是@WebServlet("/method")

绝对路径是一个广义的概念

对于文件系统来说, 有绝对路径, 这个是带盘符的

对于HTTP的URL来说, 也有绝对路径, 这个和盘符无关, 是个网络路径.

@WebServlet("/method")

@WebServlet注解里的这个路径必须以 / 开头, 但是表示的含义并非绝对路径. 这是Servlet里的要求.

我们把ajax当中的type改成post, put, 或者delete. 改成不同的方法, 最终发送过来的请求就都是对应的方法(下图只实例post).

type: 'post',

HttpServletRequest

HttpServletRequest表示的是HTTP请求.这个HTTP请求就和之前的HTTP协议里面是一模一样的. 一个HTTP协议中有各种各样的部分: 首行(方法, URL, 版本号), 若干行header, body. 那么这个一个HTTP协议中包含的各个部分都会在HttpServletRequest对象中体现出来. 当然HttpServletRequest这个对象是Tomcat自动构造的, 换句话说, Tomcat会实现监听端口, 接受连接, 读取请求, 解析请求, 构造请求对象等一系列工作.

那么既然我们要了解这个类的使用, 其实要结合HTTP请求的协议格式来理解, 进一步讲, HTTP请求里面有哪些东西, 就在这个类中有对应的体现.

核心方法

方法 描述 String getProtocol() 返回请求协议的名称和版本 String getMethod() 返回请求的HTTP方法的名称, 例如, GET、POST或PUT String getRequestURI() 从协议名称直到HTTP请求的第一行的查询字符串中, 返回该请求的URL的一部分. String getContextPath() 返回指示请求上下文的请求 URI 部分。 String getQueryString() 返回包含在路径后的请求 URL 中的查询字符串。 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() 用于读取请求的 body 内容. 返回一个 InputStream 对象.

方法演示

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.Enumeration;

@WebServlet("/showRequest")

public class ShowRequestServlet extends HttpServlet {

@Override

protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

// 设置相应的 content-Type. 告诉浏览器, 响应 body 里的数据格式是什么样的.

resp.setContentType("text/html");

// 搞个 StringBuilder, 把这些 API 的结果拼起来, 统一写回到响应中

StringBuilder stringBuilder = new StringBuilder();

stringBuilder.append(req.getProtocol());

stringBuilder.append("
");

stringBuilder.append(req.getMethod());

stringBuilder.append("
");

stringBuilder.append(req.getRequestURI());

stringBuilder.append("
");

stringBuilder.append(req.getContextPath());

stringBuilder.append("
");

stringBuilder.append(req.getQueryString());

stringBuilder.append("
");

stringBuilder.append("
");

// 获取到 header 中所有的键值对

Enumeration headerNames = req.getHeaderNames();

while (headerNames.hasMoreElements()) {

String headerName = headerNames.nextElement();

stringBuilder.append(headerName + ": " + req.getHeader(headerName));

stringBuilder.append("
");

}

resp.getWriter().write(stringBuilder.toString());

}

}

运行程序打开浏览器输入URL刷新, 可以看到我们的结果, 这里和代码是相匹配的.

注: getQueryString()由于没有query string, 所以得到的是null.

如果在URL中加上query string, 刷新页面立马就会显示出来.

同时编写程序, 重启服务器并刷新获取header中所有的键值对.

可以看到, 下面这一段就是请求报头中所有的内容, 每一个键值对都打印出来了.

这里面的结果如果对应的抓包结果会发现都是一样的.

通过这一组API可以很方便的获取到请求报头的每一个键值对.

前端给后端传参

GET的query string方式

例: 在前端要给后端传两个数字, 一个是同学的studentId, 一个是classId.

如果是通过URL的方式, 一般是: ?studentId=10&classId

那么接下来我们通过代码来处理这样的请求.

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("/getParameter")

public class GetParameterServlet extends HttpServlet {

@Override

protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

// 预期浏览器会发一个形如 /getParameter?studentId=10&classId=20 请求

// 借助 req 里的 getParameter 方法就能拿到 query string 中的键值对内容了

// getParameter 得到的是 String 类型的结果

String studentId = req.getParameter("studentId");

String classId = req.getParameter("classId");

resp.setContentType("text.html");

resp.getWriter().write("studentId = " + studentId + "classId = " + classId);

}

}

这里的query string键值对会自动被Tomcat处理成形如Map这样的结构, 后续就可以随时通过key来获取value(对应方法getParameter)了. 如果key在query string中不存在, 此时返回值就是null.

POST的form表单方式

对于前端是form表单这样格式(和query string的格式一样, 只是这部分内容在body中.)的数据, 后端还是使用getParameter来获取.

所以我们的请求就形如:

POST postParameter HTTP/1.1

[若干header]

studentId=20&classId=20

像这样的一段内容就可以通过html的form标签来构造上述请求.

接下来如果填写内容, 点击提交, 就能够构造出一个POST请求, 并且body就是form表单格式的数据. 可以通过fidder抓包看一下是什么样子的.

那么这样一个请求就是通过form表单构造出来的. 这个请求和上面的前端代码密切相关, 要注意着两者之间的一个对应关系.

同时还要关注请求中的Content-Type: application/x-www-form-urlencoded

那么这一种Content-Type就意味着是form构造的请求, 对应的body就是浏览器中所显示的这种格式.

服务器代码

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("/postParameter")

public class PostParameterServlet extends HttpServlet {

@Override

protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

String studentId = req.getParameter("studentId");

String classId = req.getParameter("classId");

resp.setContentType("text.html");

resp.getWriter().write("studentId = " + studentId + "classId = " + classId);

}

}

运行结果

使用getParameter, 既可以获取到query string中的键值对, 也可以获取到form表单构造的body中的键值对.

POST的JSON格式

JSON是一种非常主流的数据格式, 也是键值对结构, 例如:

{

classId: 20,

studentId: 10

}

那么我们也可以吧body按照这个格式来组织. 具体在前端可以通过ajax的方式构造成这个内容, 更简单的办法是使用Postman直接构造.

示例

使用路径postParameter2, 方法设为POST, 然后在body里进行设置, 选择raw并在JSON下编写, 那么此时就构造了一个JSON格式的body.

有了这个数据之后就可以把它发送给服务器这边, 服务器就可以进行处理.

postParameter2代码

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.io.InputStream;

@WebServlet("/postParameter2")

public class PostParameter2Servlet extends HttpServlet {

@Override

protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

// 通过这个方法来处理 body 为 JSON 格式的数据

// 直接把 req 对象里的 body 完整的读取出来

// 有 getInputStream 这个对象可以完成对数据的读取.

// 在流对象中, 读多少个字节取决于 Content-Length

int length = req.getContentLength();

byte[] buffer = new byte[length];

InputStream inputStream = req.getInputStream();

inputStream.read(buffer);

// 把这个字节数组构造成 String, 打印出来

String body = new String(buffer, 0, length, "utf-8");

System.out.println("body = " + body);

resp.getWriter().write(body);

}

}

写好以上代码之后重启服务器, 然后切到Postman, 点击发送请求, 可以看到响应结果就是返回的数据.

服务器日志也能看到打印出了从body中读到的数据.

那么从fiddler抓包也能更清楚的看到, 此时我们构造的请求的body就是JSON格式.

代码基本执行过程分析

当前通过JSON传递数据, 但是服务器这边只是把整个body读出来, 没有按照键值对的方式来处理. (还不能根据key获取value)

而form表单是可以根据key获取value的(getParameter就支持了).

所以这件事情就比较麻烦. 那么我们可以通过第三方库把JSON里面的键值对也解析出来.

通过Maven引入第三方库

选择一个版本, 将其下的Maven代码粘贴到pom.xml的dependencies标签下.(此处选择2.14.1版本)

com.fasterxml.jackson.core

jackson-databind

2.14.1

引入库之后, 对代码进行调整.

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;

class Student {

public int studentId;

public int classId;

}

@WebServlet("/postParameter2")

public class PostParameter2Servlet extends HttpServlet {

@Override

protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

// 使用 JSON 涉及到的核心对象

ObjectMapper objectMapper = new ObjectMapper();

// readValue 就是把一个 JSON 格式的字符串转成 Java 对象.

Student student = objectMapper.readValue(req.getInputStream(), Student.class);

System.out.println(student.studentId + ", " + student.classId);

}

}

关于objectMapper.readValue(req.getInputStream(), Student.class);

1. 会从body中读出JSON格式的字符串.

{

"studentId": 20,

"classId": 10

}

2. 根据第二个参数类对象, 创建 Student 实例

3. 解析上述JSON格式的字符串, 处理成 map 键值对结构

4. 遍历所有键值对, 看键的名字和 Student 实例的哪个属性名字匹配, 就把对应的 value 设置到该属性中

5. 返回该 Student 实例.

运行程序, 在Postman中发送POST请求, 可以看到, 当前运行日志中打印出来20, 10.

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) 设置被发送到客户端的响应的字符编码(MIME 字符集)例如,UTF-8。 void sendRedirect(String location) 使用指定的重定向位置 URL 发送临时重定向响应到客户端。 PrintWriter getWriter() 用于往 body 中写入文本格式数据. OutputStream getOutputStream() 用于往 body 中写入二进制格式数据.

响应的字符编码

将上文"GET的query string方式中"的代码的响应改为中文, 查看会发生什么效果.

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("/getParameter")

public class GetParameterServlet extends HttpServlet {

@Override

protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

String studentId = req.getParameter("studentId");

String classId = req.getParameter("classId");

resp.setContentType("text/html");

resp.getWriter().write("学生id = " + studentId + " 班级id = " + classId);

}

}

可以看到, 同样的代码, 但是由于代码里面返回的是中文, 浏览器中就显示出乱码的情况.

本来写的英文的班级id, 学生id是没有问题的, 但是改成中文之后就乱码了. 原因就是浏览器对于中文不知道使用哪种编码方式进行理解, 只能胡乱猜一个. 那么此时就需要告诉浏览器: 响应的编码格式是哪种.

在代码中加上setCharacterEncoding方法设置字符集.

resp.setCharacterEncoding("utf8");

重启服务器再次刷新, 可以看到当我们加上响应字符集之后, 此时浏览器呈现的就是正常的了.

注意:

我们在设置ContentType和字符集的时候, 必须要写在write上面.

那么我们也可以把字符集和ContentType一起设置, 如下:

resp.setContentType("text/html; charset=utf8");

sendRedirect构造重定向响应.

前面提到的3xx状态码都是表示重定向的, 意思就是浏览器会自动的跳转到指定的新地址.

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("/redirect")

public class RedirectServlet extends HttpServlet {

@Override

protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

resp.sendRedirect("https://www.baidu.com");

}

}

当通过URL: 127.0.0.1:8080/hello_servlet2/redirect访问时, 就会自动跳转到百度首页.

我们也可以通过抓包来查看这里的请求与响应, 如下: 这里就是一个普通的GET请求.

那么这个GET请求就会触发到服务器的Servlet代码, 这段代码就会返回响应.

可以看到, 当前响应中的状态码为302.

注: 使用如下代码也能起到重定向的效果.

resp.setStatus(302);

resp.setHeader("Location", "https://www.baidu.com");

代码示例: 服务器版表白墙

明确需求

之前在前端入门基础中有写了表白墙的前端页面, 但是这个页面有两个非常严重的问题.

如果刷新页面 / 关闭页面重开, 之前输入的消息就都不见了;如果一个机器上输入了数据, 第二个机器上是看不到的(这些数据都是在本地浏览器中).

那么针对上述的问题, 有如下解决思路:

让服务器来存储用户提交的数据, 由服务器保存. 当有新的浏览器打开页面的时候, 从服务器获取数据.

所以此处服务器就可以用来进行"存档"和"读档".

"存档": 每次点击提交按钮, 触发一次 存档 的操作. 把当前用户输入的内容, 保存到服务器;

"读档": 每次页面加载(打开 / 刷新)触发一次 读档 的操作. 把当前服务器上存储的所有记录获取到, 展示到页面中.

后续所有写的代码, 都是围绕这个需求展开的.

按照这样的思路, 我们就可以进行程序的设计.

注意写web程序务必要重点考虑前后端如何交互, 也就是说要约定好前后端交互的数据格式, 即设计前后端交互接口.

这件事情本质上来说就是设计好:

请求是什么样的;

响应是什么样的;

浏览器什么时候发这个请求;

浏览器按照什么格式进行解析......

约定前后端交互接口

点击提交: 浏览器把表白信息发到服务器这里.(存档)

// 请求:

POST /message

// 提交一般使用POST(使用GET也行)

// 约定body中的数据按照JSON格式解析

{

from: "鸡你",

to: "实在太美",

message: "OhBaby"

}

// 响应:

HTTP/1.1 200 OK

// 告诉浏览器当前已经把数据传输成功.

页面加载: 浏览器从服务器获取到表白信息.(读档)

// 请求:

GET /message

// 从服务器获取数据一般更多的使用GET

// 响应:

// 需要把服务器存储的消息都返回回去

HTTP/1.1 200 OK

// body部分使用JSON数组方式组织

[

{

from: "鸡你",

to: "实在太美",

message: "OhBaby"

},

{

from: "鸡你",

to: "实在太美",

message: "OhBaby"

}

]

此时我们就把两次交互的请求和响应约定好了.

注意这里的措词: 约定前后端交互数据的格式. 这里的约定方式是存在很多种, 具体的设定都不尽相同, 十分灵活, 没有固定的强制要求. 此处的目的就是为了前端代码和后端代码能够对上号.

编写后端代码

新建Maven项目, 引入必要的依赖, 并创建必要的目录.

调整pom.xml

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

4.0.0

org.example

MessageWall

1.0-SNAPSHOT

8

8

UTF-8

javax.servlet

javax.servlet-api

3.1.0

provided

com.fasterxml.jackson.core

jackson-databind

2.14.2

mysql

mysql-connector-java

5.1.47

再把之前实现的表白墙前端页面拷贝到webapp目录中.

编写代码

创建MessageServlet类.

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.sql.Connection;

import java.util.ArrayList;

import java.util.List;

// 定义一个类, 描述请求body内容, 方柏霓Jackson进行JSON解析.

// 注意要保证这个类中属性的名字, 要和 约定的 请求的名字是匹配的.

class Message {

// 设为private也可以, 但是要提供对应的getter和setter方法

public String from;

public String to;

public String message;

}

@WebServlet("/message")

// 这里的路径要和所约定的前后端交互接口的路径一致

public class MessageServlet extends HttpServlet {

// 使用这个List变量保存所有消息

private List messageList = new ArrayList<>();

private ObjectMapper objectMapper = new ObjectMapper();

// 向服务器提交数据

// 把解析的message往List里填.

@Override

protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

// 把body中的内容读取出来, 解析成一个Message对象

Message message = objectMapper.readValue(req.getInputStream(), Message.class);

// 此处通过简单粗暴的方式来完成保存

messageList.add(message);

// 此处的设定状态码可以省略, 不设置默认也是200.

resp.setStatus(200);

}

// 从服务器获取数据

// 把List的结果返回给前端

@Override

protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

resp.setContentType("application/json; charset=utf8");

// objectMapper.writeValue(resp.getWriter(), messageList); // 这个方法同时完成了: 把Java对象转成JSON字符串和把这个字符串写到响应对象中.

// 把Java对象转成json字符串

String jsonResp = objectMapper.writeValueAsString(messageList);

System.out.println("jsonResp: " + jsonResp);

// 把这个字符串写回到响应body中

resp.getWriter().write(jsonResp);

}

}

启动程序之后, 我们通过Postman发送请求查看效果.

首先我们发送GET请求, 点击发送之后可以看到, 得到的是空的, 还没有任何message数据, 所以我们直接获取是空的.

接下来我们就要POST数据让它不空, 构造好JSON数据并Send发送. 此时响应是没有的, 因为并没有在响应中构造结果, 只返回了200.

我们发送数据之后再次进行GET, 这时GET的结果就有内容了.

我们多POST几次, 再进行GET, 可以看到此时结果中包含了多个JSON数据.

注:

{ } 是JSON的对象;

[ ] 是JSON的数组.

我们通过代码简单说明JSON的使用.

import com.fasterxml.jackson.core.JsonProcessingException;

import com.fasterxml.jackson.databind.ObjectMapper;

class Student{

public int classId;

public int studentId;

}

public class TestJackson {

public static void main(String[] args) throws JsonProcessingException {

ObjectMapper objectMapper = new ObjectMapper();

// readValue 是把json格式的字符串转成Java对象

String s = "{ \"classId\": 10, \"studentId\": 20}";

// readValue 第一个参数可以直接写个String, 也可以写个InputStream

Student student = objectMapper.readValue(s, Student.class);

System.out.println(student.classId);

System.out.println(student.studentId);

}

}

// writeValue / writeValueAsString是把Java对象转成json字符串

Student student = new Student();

student.classId = 10;

student.studentId = 20;

String s = objectMapper.writeValueAsString(student);

System.out.println(s);

编写前端代码

编写前端代码, 让页面能够发起上述请求, 并解析响应. 同时也让浏览器ajax发送GET.

请求即上述的GET与POST. 注意POST是点击提交按钮的时候发起, GET是页面加载的时候发起.

将之前的前端页面拷贝到webapp目录下.

然后使用vscode进行编写代码, 要想完成前后端交互就得使用ajax, 这里仅展示JS部分的代码.

// 实现提交操作, 点击提交按钮, 就能够把用户输入的内容提交到页面上显示

// 点击的时候, 获取到三个输入框中的文本内容

// 创建一个新的 div.row 把内容构造到这个 div 中即可

let containerDiv = document.querySelector('.container');

let inputs = document.querySelectorAll('input');

let button = document.querySelector('#submit');

button.onclick = function() {

// 1. 获取到三个输入框的内容

let from = inputs[0].value;

let to = inputs[1].value;

let msg = inputs[2].value;

if (from == '' || to == '' || msg == ''){

return;

}

// 2. 构造新 div

let rowDiv = document.createElement('div');

rowDiv.className = 'row message';

rowDiv.innerHTML = from + ' 对' + to + ' 说:' + msg;

containerDiv.appendChild(rowDiv);

// 3. 清空之前的输入框内容

for (let input of inputs) {

input.value = '';

}

// 4. 给服务器发起POST请求, 把上述数据提交到服务器这边

let body = {

"from": from,

"to": to,

"message": msg

};

let strBody = JSON.stringify(body);

console.log("strBody: " + strBody);

$.ajax({

type: 'post',

url: 'message',

data: strBody,

contentType: "application/json; charset=utf8",

success: function(body) {

console.log("数据发布成功");

}

});

}

let revertButton = document.querySelector("#revert");

revertButton.onclick = function() {

// 删除最后一条消息

// 选中所有的row, 找到最后一个row, 然后进行删除

let rows = document.querySelectorAll('.message');

if (rows == null || rows.length == 0){

return;

}

containerDiv.removeChild(rows[rows.length - 1]);

}

// 在页面加载的时候, 发送GET请求, 从服务器获取到数据并添加到页面中

$.ajax({

type: 'get',

url: 'message',

success: function(body) {

// 此处拿到的body就是一个JS对象的数组了

// 本来服务器返回的是一个JSON格式的字符串, 但是jQuery的ajax能够自动识别

// 自动帮我们把JSON字符串转成JS对象数据

// 接下来遍历这个数组, 把元素取出来, 构造到页面中即可

for (let message of body) {

// 针对每个元素构造一个div

let rowDiv = document.createElement('div');

rowDiv.className = 'row message';

rowDiv.innerHTML = message.from + ' 对' + message.to + ' 说:' + message.message;

containerDiv.appendChild(rowDiv);

}

}

});

let body = {

"from": from,

"to": to,

"message": msg

};

// 这个代码是 定义一个JS对象(非常类似于JSON的键值对, key是字符串, value则是JS中的变量/常量.(注意加引号与不加引号效果相同))

// 注意后面的三个value变量是从前面 获取输入框内容 的三个变量(如下)来的, 需要注意对应关系

let from = inputs[0].value;

let to = inputs[1].value;

let msg = inputs[2].value;

此时构造的body是一个JS对象, 不是字符串. 而网络传输, 只能传字符串, 不能传对象. 要想传对象也要转成字符串, 再进行传输. 另一方面, 前面约定, 希望传输的body是一个JSON格式的字符串, 所以需要把body对象构造成JSON格式.

即:

把这个对象

let body = {

"from": from,

"to": to,

"message": msg

};

转成 这个JSON格式的字符串.

{

from: "鸡你",

to: "实在太美",

message: "OhBaby"

}

那么如何进行转换? JS内置了JSON转换的库.(Java还得有第三方库JSON, JS的标准库则内置了)

JSON.stringify(body);

这个操作就相当于把JS对象转成JSON格式的字符串.

我们运行一下程序, 看一下代码的效果是什么样子的.

通过浏览器访问页面, 输入上文内容后点击提交, 可以看到有了一个HTTP请求, 打开fidder.

同时可以查看请求的详细情况, 首先是一个POST请求, Content-Type是我们设置好的application/json; charset=UTF-8. body部分的格式就是{"from":"鸡你","to":"实在太美","message":"OhBaby"}, 是一个典型的JSON字符串.

所以通过上述代码就把请求构造出来了, 这个请求就是上面的ajax代码构造的.

这个数据被发送到服务器, 然后服务器代码就会就会触发doPost()进一步执行里面的代码, 读取请求返回相应.

rowDiv.innerHTML = message.from + ' 对' + message.to + ' 说:' + message.message;

其中message.from, message.to, message.message三个参数都是根据后端返回响应的JSON格式来的, 格式就是 约定 中的:

[

{

"from": "鸡你",

"to": "实在太美",

"message": "OhBaby"

},

{

"from": "鸡你",

"to": "实在太美",

"message": "OhBaby"

},

{

"from": "鸡你",

"to": "实在太美",

"message": "OhBaby"

}

]

这一套格式里面就会带有这样一个数组, 数组中的每个元素都是一个JS对象, 取到的每个message就是这个JS对象, 整个数组为body. (message就是 for-of中定义的)

数据存入数据库

当前我们已经完成了前后端的交互代码, 前端通过ajax的方式把请求发送给后端, 后端处理提交消息与获取消息的请求, 那么这一块的整体逻辑已经跑通了, 但是这里最大的问题是服务器此时是使用普通的一个变量来保存的, 这也就意味着一旦服务器重启, 变量(也就是内存中的值)就没了, 所以为了做到更加持久的数据保存, 就使用到数据库.

当前数据是在内存(变量)中保存的. 重启服务器就没了. 要想持久化保存, 就需要写入文件中(硬盘).

直接使用"流对象"写入文本文件借助数据库

接下来通过数据库完成这样的一个操作.

创建数据表

create table message(`from` varchar(20), `to` varchar(20), message varchar(1024));

实现数据库操作

单独创建一个类DBUtil封装数据库连接过程. 此处把DBUtil作为一个工具类, 提供static方法供其他代码来调用.

静态成员特点: 跟随类对象. 类对象在整个进程中只有唯一一份

静态成员相当于也是唯一的实例. (单例模式, 饿汉模式)

import com.mysql.jdbc.jdbc2.optional.MysqlDataSource;

import javax.sql.DataSource;

import java.sql.Connection;

import java.sql.PreparedStatement;

import java.sql.ResultSet;

import java.sql.SQLException;

public class DBUtil {

private static DataSource dataSource = new MysqlDataSource();

static {

// 使用静态代码块, 针对DataSource进行初始化操作

((MysqlDataSource)dataSource).setUrl("jdbc:mysql://127.0.0.1:3306/java106?characterEncoding=utf8&useSSL=false");

((MysqlDataSource)dataSource).setUser("root");

((MysqlDataSource)dataSource).setPassword("111111");

}

// 通过这个方法来建立连接

public static Connection getConnection() throws SQLException {

return dataSource.getConnection();

}

// 通过这个方法来断开连接, 释放资源

public static void close(Connection connection, PreparedStatement statement, ResultSet resultSet){

// 此处将三个 try catch 分开写, 避免前面的异常导致后面的代码不能执行.

if (resultSet != null){

try {

resultSet.close();

} catch (SQLException e) {

e.printStackTrace();

}

}

if (statement != null){

try {

statement.close();

} catch (SQLException e) {

e.printStackTrace();

}

}

if (connection != null){

try {

connection.close();

} catch (SQLException e) {

e.printStackTrace();

}

}

}

}

DBUtil创建完成之后就可以在MessageServlet中进行使用了.

修改前文 MessageServlet.java 代码

1. 那么private List messageList = new ArrayList<>();就不需要这个变量再使用了, 取而代之的是 提供一对方法:

save(): 往数据库中村一条消息

load(): 从数据库取所有消息

2. 而messageList.add(message);也不再使用, 改为save(message);

3. 同理objectMapper.writeValueAsString(messageList); 也不再去messageList, 而是使用

List messageList = load();

MessageServlet代码

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.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;

}

@WebServlet("/message")

public class MessageServlet extends HttpServlet {

private ObjectMapper objectMapper = new ObjectMapper();

@Override

protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

// 把body中的内容读取出来, 解析成一个Message对象

Message message = objectMapper.readValue(req.getInputStream(), Message.class);

save(message);

// 此处的设定状态码可以省略, 不设置默认也是200.

resp.setStatus(200);

}

// 从服务器获取数据

@Override

protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

resp.setContentType("application/json; charset=utf8");

// 把Java对象转成json字符串

List messageList = load();

String jsonResp = objectMapper.writeValueAsString(messageList);

System.out.println("jsonResp: " + jsonResp);

// 把这个字符串写回到响应body中

resp.getWriter().write(jsonResp);

}

private void save(Message message) {

// JDBC 操作

Connection connection = null;

PreparedStatement statement = null;

try {

// 1. 建立连接

connection = DBUtil.getConnection();

// 2. 构造SQL语句

String sql = "insert into message values(?, ?, ?)";

statement = connection.prepareStatement(sql);

statement.setString(1, message.from);

statement.setString(2, message.to);

statement.setString(3, message.message);

// 3. 执行SQL

statement.executeUpdate();

} catch (SQLException e) {

e.printStackTrace();

} finally {

//4. 关闭连接

DBUtil.close(connection, statement, null);

}

}

// 从数据库取所有消息

private List load() {

List messageList = new ArrayList<>();

Connection connection = null;

PreparedStatement statement = null;

ResultSet resultSet = null;

try {

// 1. 和数据库建立连接

connection = DBUtil.getConnection();

// 2. 构造sql

String sql = "select * from message";

statement = connection.prepareStatement(sql);

// 3. 执行sql

resultSet = statement.executeQuery();

// 4. 遍历结果集合

while (resultSet.next()) {

Message message = new Message();

message.from = resultSet.getString("from");

message.to = resultSet.getString("to");

message.message = resultSet.getString("message");

messageList.add(message);

}

} catch (SQLException e) {

e.printStackTrace();

} finally {

// 5. 需要释放资源, 断开连接

DBUtil.close(connection, statement, resultSet);

}

return messageList;

}

}

执行结果

启动服务器, 访问"127.0.0.1:8080/MessageWall/messageWall.html"页面.

当前是没有数据的, 所以我们写入一些数据,

点击提交,可以看到, 给服务器发了一个请求,

同时, 服务器的处理逻辑(save)中就会执行数据库的保存.

此时我们查询一下数据库, 可以看到这个消息赫然在列了.

如果再多提交几条, 再查询数据库, 能看到两条数据, 说明我们已经把数据存入数据库了.

把这些数据存入数据库之后我们就可以在前端进行查询了.

刷新页面, 可以看到这里的结果仍然存在, 这些结果就是从数据库中查询了, 可以在服务器的运行日志中查询到.

我们接下来可以再次刷新页面, 此时执行的就是"读档"操作, 可见我们的数据被持久化存储了, 即使重启服务器, 数据依旧存在.

文章来源

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: