JavaEE & Servlet的API详解
对于Tomcat服务器的代码实现,就不做讲解了,本质上就是一个TCP服务器,感兴趣的可以去了解!
接下来就是Servlet的API(有很多很多)
主要就是三个重点类:
HttpServlet
HttpServletRequest
HttpServletResponse
而后面两个类,只要你熟悉HTTP协议,我想你也会知道怎么使用(猜)~
1. HttpServlet抽象类
每一个Servlet程序都要继承这个HttpServlet类,重写其中的方法
既然如此,我们就需要熟悉我们能重写什么方法,以及这些方法有什么用~
按住ctrl点进HttpServlet:
方法名称 调用时机
init 在HttpServlet实例化之后呗调用一次
destory 在HttpServlet实力不再使用的时候调用一次
service 收到HTTP请求的时候调用
doGet 顾名思义
doPost 顾名思义
doPut/doDelete/doOptions/… 顾名思义
1.1 init方法
初始化相关的工作
HttpServlet被实例化之后会被调用一次
即,首次收到匹配的请求(建立连接请求)的时候,实例化
记得打开服务器
记得发送请求(建立连接,后得到request)
所以init的打印在helloworld之前(doGet之前),且只调用了一次:
1.2 destroy方法
这个方法是该webapp被卸载,被销毁之前执行一次
做一些收尾工作
示例:
编写代码,启动服务器再关闭:
然而好像并没有执行这个语句
实际上,destroy不太靠谱
通过8005管理端口,来停止服务区,此时destroy都执行
直接通过杀死进程(ctrl + f2),此时destroy执行不了
常见的简单粗暴的方法
所以不建议使用destroy方法,因为可能也不作数呀~
8005管理端口
前面提到,Tomcat用到两个端口
8080业务端口
8005管理端口
这就类似于,一个人的两个微信
工作微信,(同事客户领导…)
生活微信,(家人朋友…)
而不同的端口,就会有不同的请求响应
8080就是我们的业务,我们需要通过这个端口定位到服务器的位置,以及其部署的资源
8005则是做一些加载配置,重新启动,调整设置项…
其中就包括,关闭服务器
1.3 service方法
每次收到路径匹配的请求,都会执行
这是一个写好了的子框架!
doGet和doPost…都会在service中被调用,所以我们一般是不会重写这个方法,而是重写doGet和doPost就行了
init在连接时刻下调用一次
destroy在结束之前调用一次
service每次收到路径匹配的请求都会调用一次
Servlet的生命周期
一个事物的“一生”,各个阶段干什么,就是生命周期
doGet等doXXX方法,已经写过很多次了,不做讲解~
举一反三~
响应乱码问题:
原因就是,数据返回的编码方式和浏览器展示的编码方式,两者对应不上
统一编码方式即可
IDEA默认返回的编码方式为utf8
java中,char类型是unicode(两个字节)
但是String类型的字符是utf8的(三个字节)
只不过java中不太留意字节大小,所以无感
这也解决粘包问题(unicode的硬伤)
而我们只需要通过一些类进行套壳(Scanner),就会自动解析字节流了
至于char数组到String的转化,java帮你做了,不必留意这些细节~
而java中sout的时候,都是转化为字符串输出的~
浏览器是根据系统默认的编码方式走的,(Windows11默认gbk)
gbk:两个字节去表示,能表示的汉字有限~
utf8:三个字节,可以表示丰富的文字
关闭再启动:
2. HttpRequest接口
一个HTTP请求里有啥,这个对象中就有啥
方法
URL(host,queryString)
版本号
header
body
Cookie
…
而这些内容,在这个对象中,都可以用对应的方法进行获取和设置~-
方法总览:
不需要我们重写,因为这些是因为doXXX方法的传参向上转型,已经是框架重写好的了~
Protocol:
协议名称和版本号
URL 与 URI:
前者是网络资源定位符,后者是网络资源标识符
特别相似,相似到我们很多时候**直接混着用~**
Parameter:参数
其实就请求中的键值对
Enumeration:列举
是请求中所有键值对的所有key的名称
querystring
form(body)
前者就是获取参数名称(所有key),后者通过key获取value(首个)
key是可以对应多个值的(在querystring允许key重复出现)
所以此方法能够获取key对应的value的字符串数组
很罕见,这种方法,一般有这种特殊要求,服务器也是知道的,所以也会调用这个方法
(不会出现使用了只能获取一个value的那个方法,因为都是约定好的)
处理header的方法:
类似:
前者是获取header里的所有key,后者是获取key对应的value
一般header的key是不重复的(一般不会用getHeaders)
获取特定的header的key对应的值:
不需要输入名字字符串,而是通过编译器提词
获取字节输入流对象:
用Scanner套壳可以处理字节输入流
进一步去读取body的内容!(压缩的body会被自动转换,不必担心)
除了文本/二进制模式外的格式,随后讲解
而这些格式的读写是得通过第三方库(依赖)的
自己构造太麻烦了
表中未出现的其他方法,暂时不讲,随用随查随学!
2.1 在浏览器上显示请求首行
@WebServlet("/show")//没有分号~ public class ShowRequest extends HttpServlet { StringBuilder result = new StringBuilder(); result.append(req.getProtocol());//版本号 result.append('\n'); result.append(req.getMethod()); result.append('\n'); result.append(req.getRequestURI()); result.append('\n'); result.append(req.getQueryString());//无字符串 result.append('\n'); result.append(req.getContextPath()); result.append('\n'); resp.getWriter().write(result.toString()); } }
启动服务器,通过浏览器发送请求~
自己搞个queryString:
如果响应的body是html格式的,则回车符就不能生效了~
设置响应的body格式(提前一提),用的是setContentType方法
如果不改回车符:
重新启动服务器哦!
没有起到换行作用,而是相当于在html代码中按了一下回车,展示效果其实就是一个空格~
用<br>代替:
2.2 在浏览器上显示请求header
result.append("<hr>"); Enumeration<String> headerNames = req.getHeaderNames(); //用不了for each语法,因为这个集合类没继承那个集合接口,也不是数组~ //而其本身,也可以看做是自身的迭代器 while(headerNames.hasMoreElements()) { String name = headerNames.nextElement(); String value = req.getHeader(name); result.append(name); result.append(": "); result.append(value); result.append("<br>"); } resp.setContentType("text/html; charset=utf8"); resp.getWriter().write(result.toString());
这个headerNames对象,本身是一个集合,也是一个自身的一个迭代器~
效果:
2.3 getParameter方法 - 最常用的API之一
前端给后端传递数据,是很常见的:
querystring传递
body(form)
body(json)
result.append("<hr>"); Enumeration<String> keys = req.getParameterNames(); //同样可以迭代~ while(keys.hasMoreElements()) { String key = keys.nextElement(); String value = req.getParameter(key); result.append(key); result.append("="); result.append(value); result.append("<br>"); } result.append("<hr>"); String value1 = req.getParameter("a"); String value2 = req.getParameter("b"); String value3 = req.getParameter("c"); result.append(value1 == null ? "noFound" : value1); result.append("<br>"); result.append(value2 == null ? "noFound" : value2); result.append("<br>"); result.append(value3 == null ? "noFound" : value3); result.append("<br>");
效果:
querystring传递
body(form)
body(json)
@Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { //获取请求的body StringBuilder result = new StringBuilder(); Enumeration<String> keys = req.getParameterNames(); while(keys.hasMoreElements()) { String key = keys.nextElement(); String value = req.getParameter(key); result.append(key); result.append("="); result.append(value); result.append("<br>"); } resp.setContentType("text/html; charset=utf8"); resp.getWriter().write(result.toString()); }
通过Postman构造post请求发送个服务器
可见,获取到了body里的form相关的键值对
中文乱码现象1:
在querystring中写中文也是有风险的(成功也只不过凑巧可以打印)
querystring的键值对包含中文/特殊字符,就需要用urlencode的方式进行转码,否则存在风险(querystring的机制似乎优化过了,软件/浏览器支持的很好)
进行转码是有必要的,减少变数
如果出现乱码,服务器将无法正确识别,从而无法返回正确的响应!!!
UrlEncode编码和UrlDecode解码-在线URL编码解码工具
通过转码工具转码:
作为后端程序员,一般也感知不到这个问题,因为我们获得乱码,那肯定是前端程序员的锅~
自己的前后端分离的项目也是要注意一下的
中文乱码现象2:
在form(body)的键值对中,前端传过来的utf8,而后端并不清楚它就是utf8,所以后端代码不知道/用其他编码方式读取,就会乱码
显示告知编码方式:
请求本来就有一套编码方式,这个操作设置的是后端代码怎么识别这串请求的
效果:
2.4 json(body)的解析与构造
querystring传递
body(form)
body(json)
肥肠重要!
json一般有对象构造而来,以对象来理解,所以通过String识别字符串(key与value),即utf8,所以不告知编码方式也行
用getParameter方法的话,似乎是摄取不到json里面的键值对的
因为Servlet是没有内置解析json的功能的~
没有关系!我们只需要引入一个第三方库(一个依赖)
json的依赖,有很多
用法差不多,功能相似,例如fastjson、gson、jackson…
jackson是spring官方的指定产品(提前认识点,后续spring有关操作也恰好要用到jackson)
Maven Repository: Central (mvnrepository.com)
中央仓库里去下载/复制代码
搜jackson,选择这个:
我选择的是这个版本:
复制代码:
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.12.3</version> </dependency>
在pom.xml中引入依赖:
刷新触发加载~
2.4.1 ObjectMapper类
private ObjectMapper objectMapper = new ObjectMapper(); //解析json的核心类就是,ObjectMapper
这个类我的理解就是:对象映射制造器
object即对象,map即映射
通过对象内部的映射关系,制作json格式的字符串
通过json格式的字符串,构造对象
class User { public String username; private String password; public double score; public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } }
2.4.1 通过json构造对象
用readValue方法(readValues的话,就是传过来的body是json数组)
传入req的输入流(输入,即喂给后端代码)
传入类对象(类名.class)
这个方法通过这个类名(普通类,不能是抽象类/接口),构造对类名对应的对象!
没错,底层它会通过反射(只有这个反射才能通过类名通过成员名方法名去设置和获取)去构造一个对象,
json的key对应成员名,value对应成员的值
不存在的还是默认值~
private或者其他此时权限不够的修饰符修饰成员,必须有对应的getter和setter,才能被反射获取到!才能正确制造对象
注意:setter和getter用编译器快速构造即可(alt + insert)
boolean外的其他类型,都是getXxx和setXxx
boolean类型,则是isXxx和setXxx
这个方法的重载方法很多,你甚至可以传一个字符串和类对象就够了
@WebServlet("/show_json") public class showJSON extends HttpServlet { private ObjectMapper objectMapper = new ObjectMapper(); //解析json的核心类就是,ObjectMapper @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("utf8"); User user = objectMapper.readValue(req.getInputStream(), User.class); resp.setContentType("application/json; charset=utf8"); } }
启动服务器,通过Postman发送请求:
这里是500,就是认定请求的body为标准,只是后端代码(服务器那边)没有做好匹配
错误1 :
成员的类型与值不匹配
错误2:
不存在此成员:
请求的key值较少,无所谓~
竟然是映射关系,那么也可以用Map储存:
这样用的话,我们还要手写成员名才能获取value(这两个编译器都不会提词)
麻烦!
@Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("utf8"); //User user = objectMapper.readValue(req.getInputStream(), User.class); //System.out.println(user.username + " " + user.getPassword() + " " + user.score); resp.setContentType("application/json; charset=utf8"); Map<String, String> map = objectMapper.readValue(req.getInputStream(), HashMap.class); System.out.println(map.get("username") + " " + map.get("password") + " " + map.get("score")); }
重新启动服务器,发送post请求:
2.4.3 通过对象构造json:
一般构造json,是为了写入响应返回给客户端,所以从习惯上是客户端发送GET请求,所以重写doGet方法
用writeValueAsString方法
传入任何对象,都会帮你转换为json
基本数据类型/String/包装类/非复杂(自定义)类型的数组和集合类:
"value.toString()" //基本数据类型就是:value //数组就是Arrays.toString(value)
自定义类
{ "成员1": "value1", "成员2": "value2", //...... "成员n": "valuen"//最后没有逗号 }
自定义类的数组/集合
[ { //json1 } { //json2 } // ... { //json_n } ]
注意:
如果对象没有new出来,还是null,那么转换的也就是null(没有指向)
自带映射的Map
{ "key1": "value1", //... "keyn": "value2" }
3. HttpResponse接口
表示一个HTTP响应,响应有什么,里面就有什么~
只讲几个常用的~
处理header的方法
set => key存在,覆盖;key不存在,创建一个key: value键值对
add => key存在/不存在:创建一个key: value键值对
即,header可能出现重复
设置body的格式,设置字符集…
设置body格式后面加个“; charset=XXX”,也能起到设置字符集的作用
设置重定向
获得输出字符流,输出字节流(输出,即后端代码投喂给响应)
3.1 设置首行(版本号固定)
@Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setStatus(200); resp.getWriter().write("200响应"); }
抓包:
Content-Length等非自定义的header属性自动补上了~
3.2 设置格式和字符集
3.3 自动刷新页面
通过setHeader方法,对Refresh属性进行设置value
含义就是,在value秒后,页面自动刷新(浏览器)
但是如果浏览器有互动性的东西,就不会触发刷新(因为强制刷新,就会清空用户输入了的东西,影响体验)
@Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html; charset=utf8"); //不设置状态码,默认为200 ok,状态码描述跟状态码对应的,并不需要设置 resp.setHeader("Refresh", "1"); resp.getWriter().write(String.valueOf(System.currentTimeMillis())); }
虽然不能精确到1000ms整,但是很接近1s了(机器也要工作时间嘛~)
3.4 设置重定向
3.4.1 设置状态码 + header属性Location
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html; charset=utf8"); resp.setStatus(302); resp.setHeader("Location", "https://www.bilibili.com/"); }
效果:
3.4.2 调用sendRedirect方法
是等价于上述两步的
@Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html; charset=utf8"); resp.sendRedirect("https://www.bilibili.com/"); }
效果一致~