Java Web-servlet、HTTP in servlet和捎带的Java绘图学习
Java Web-servlet、HTTP in servlet和捎带的Java绘图学习
server applet:运行在服务器端的小程序
动态项目的动态内容的java类依赖于服务器才能运行,由tomcat执行,所以需要遵守一定的规则(接口)才能被Tomcat所识别,这个接口就是servlet。
快速入门
-
创建一个JavaEE项目
-
定义一个类,实现Servlet接口
-
实现接口中的方法
package com.jiading.web.servlet; import javax.servlet.*; import java.io.IOException; /* servlet快速入门 */ public class ServletDemo1 implements Servlet { @Override public void init(ServletConfig servletConfig) throws ServletException { } @Override public ServletConfig getServletConfig() { return null; } @Override public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException { /* 提供服务的方法 */ System.out.println("hello,servlet"); } @Override public String getServletInfo() { return null; } @Override public void destroy() { } }
-
配置Servlet:将java类映射为URL中的资源,在WEB-INF目录的web.xml文件中进行配置
WEB-INF下的lib目录用来存放jar包
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" version="3.1"> <servlet> <servlet-name>demo1</servlet-name> <servlet-class>com.jiading.web.servlet.ServletDemo1</servlet-class> </servlet> <!-- 配置映射--> <servlet-mapping> <!-- 要配置的servlet的名称,与上面servelt标签下的name一致--> <servlet-name>demo1</servlet-name> <!-- 对应的URL中的路径名称--> <url-pattern>/demo1</url-pattern> </servlet-mapping> </web-app>
可以在run/configurations/Deployment下的Application context修改部署的路径,注意不是修改Server/URL,那个只控制打开浏览器时默认打开的页面。
在Deployment下还可以修改输出的项目名称
执行原理
客户端输入URL后,先通过localhost:8080找到tomcat,再通过项目名找到对应的项目,然后从web.xml中遍历查找资源名,然后通过servlet-map的映射查找到对应的servlet,再找到其下的servlet-class.
Tomcat将全类名对应的字节码文件加载进内存:Class.forName()
创建对应的对象:class.newInstance()
调用对象的service方法
所以,我们在写servlet对象时,我们不需要创建Main方法、实例化对象、自己调用service方法,这些都是服务器在收到访问请求后来完成的。借助这样的机制,我们就可以把前端和后端联系起来
Servlet的生命周期
servlet的生命周期对应着它的几个方法:
- 被创建
- 提供服务
- 被销毁
方法如下:
package com.jiading.web.servlet;
import javax.servlet.*;
import java.io.IOException;
public class ServletDemo2 implements Servlet {
@Override
public void init(ServletConfig servletConfig) throws ServletException {
/*
初始化方法,在servlet被创建时(也就是Servlet第一次被请求时)执行,只执行一次
*/
}
@Override
public ServletConfig getServletConfig() {
/*
获取ServletConfig对象,是Servlet的配置对象
*/
return null;
}
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
/*
提供服务的方法,是主要的方法。每一次servlet被访问都会执行
*/
}
@Override
public String getServletInfo() {
/*
获取Servlet的一些信息,版本、作者等,不常用
*/
return null;
}
@Override
public void destroy() {
/*
在Servlet被杀死时执行,也就是服务器正常关闭时执行。
和析构函数一样,是在Servlet被销毁之前执行,而不是之后
一般用来释放资源
*/
}
}
除了默认的被第一次调用时创建之外,我们也可以在web.xml中指定创建的事件
<servlet>
<servlet-name>demo2</servlet-name>
<servlet-class>com.jiading.web.servlet.ServletDemo2</servlet-class>
<!-- 指定被创建的事件
1. 第一次被访问时创建,是默认,配置load-on-startup配置为负数时生效(默认是-1)
2. 在服务器启动时创建,要配置为0或者正数
-->
<load-on-startup>1</load-on-startup>
</servlet>
一个Servlet在内存中只存在一个对象,说明Servlet是单例的。但是这种单例在多用户同时访问时可能存在线程安全问题,也就是共享资源的问题。但是也不能加锁,否则同时只能有一个用户访问。解决方法就是,尽量不要在Servlet中定义成员变量,从源头解决问题。即使定义了,也不要对其进行做修改值的操作
Servlet3.0的注解配置
为了简化每次对新的Servlet类都要进行配置这一操作,Servlet3.0支持了注解配置,不需要使用web.xml进行配置了。从JavaEE6开始支持Servlet3.0
步骤:
-
创建JavaEE项目,注意选择3.0以上的Servlet版本。此时再创建就不需要创建web.xml了
-
定义一个类,实现Servlet接口
-
复写方法
-
在类上使用@WebServlet注解。在注解上配置URL pattern,例如:
@WebServlet(urlPatterns="/demo")
其可以简写为:
@WebServlet("/demo")
这是因为@WebServlet类中有个value属性,可以输入一些很重要的配置,当然最重要的就是urlPattern,所以如果只输入一个值给value的话默认就是给urlPattern赋值。而value的输入又可以不写value=,所以可以简化到上面那一步
注解配置的最大优点就是方便
Servlet体系结构
除了直接继承Servlet之外,我们其实还有其他的选择。Servlet已经提供了两个子类GenericServlet和HttpServlet,他们都是抽象类,并且HttpServlet是GenericServlet的子类。
- 对于GenericServlet,继承时只需要复写Service方法,因为该类对于其他方法都已经做了默认的空实现,只有service方法还是抽象的:
这样做的好处就是不需要每次都对所有方法进行复写了,而只需要复写我们最经常使用的service方法即可。当然,其他的方法也不是不允许复写,想复写还是可以随意复写的
-
对于HttpServlet,它实际上就是对HTTP协议的封装和描述。这个类出现的原因是:HTTP访问有get和post两种方式,两种方式的参数传递方法是不一样的(get直接接在URL后面,而post是放到数据包其他标签下的),所以获取数据的方式也不一样。如果我们直接使用service方法,就需要每次都去判断目前的访问是get还是post以获取传进来的参数,这显然是很麻烦的。而HttpServlet已经将判断的这个过程写入了service方法,并提供了doGet和doPost两个方法,我们继承这个类后直接在这两个方法下写业务代码就好了,不需要显式地复写service方法。
其实提供的不只是doGet和doPost,只是这两个比较常用而已
注意,通过浏览器直接访问,默认是get方式
Servlet的相关配置
-
urlPattern
它是url的访问路径,但注意它不是唯一的,可以为一个servlet定义多个访问路径
路径配置规则:
- /xxx,至少要有一个/,当然也可以写多层
- *.<后缀>:只通过后缀名进行区分,这种前面不能加/
- /*,这种就是所有的都匹配。这种方式在匹配时优先级是最低的,只要有冲突都以其他的为先
HTTP in Servlet
连接复用(Http1.1)
和1.0不一样,1.1中有连接复用的设置:建立连接之后,当数据传输完成后连接不会马上断开,而是先等待一段时间。如果在这段时间内有其他数据再发送,就使用这个连接,并且传输完之后再等待
HTTP数据格式
请求消息:servletRequest
-
请求行
请求方式 请求url 请求的协议/版本
eg.GET /login.html HHTP/1.1
请求方式有七种,常用的是get和post,他们的区别是:
- get的参数在请求行(就是请求行的url部分,用?将url和要传递的参数隔开)中,而post在请求体中
- get方式请求的url长度是有限制的,而post方式对于url长度是没有限制的,所以例如文件的上传,就只能用post方式
- get请求不安全,post相对安全点:说的是参数是否显式传输(其实一抓包都不安全)
-
请求头
键值对格式,用冒号分割,值有多个的话用逗号分割
常见的请求头:
- host:表示请求的主机
- User-Agent:浏览器版本信息:用来解决浏览器兼容性问题(对资源的解析不一样,使得页面显示不一样)
- Accept:告诉服务器本机可以解析的文件类型
- Accept-Language:支持的语言环境,例如zh-CN、zh-TW、zh-HK、en-US等等
- Accept-Encoding:支持的压缩格式
- Refer:告诉服务器当前请求从哪里来。作用是:
- 防盗链:不是从我自己的网站来访问该资源的的就不返回该资源,例如图片、视频网站
- 统计:统计访问量,例如网站主页用来统计从各个搜索引擎引入的流量数量
- Connection:keep-alive表示连接在数据传输完成后不立即端口(连接复用)
- upgrade-Insecure-Request:关于这个参数,可以看这篇文章: https://blog.csdn.net/qq_33019839/article/details/101513886
-
请求空行
就是一个空行,用来分割请求头和请求体
-
请求体
GET方式没有请求体。
POST方式的请求体中其实就是要传递的参数
ServletRequest
Request对象和Ressponse对象的原理
- tomcat服务器根据请求url中的资源路径创建对应的Servlet对象
- tomcat创建两个request和response对象,request对象封装请求消息数据(相当于数据预处理,这样当我们需要请求数据时直接从对象中调用就好了)
- tomcat将request和response对象作为参数传递给service方法,并且调用service方法
- 程序员对service进行编写,并且通过response对象设置响应消息数据
- tomcat将response取出响应消息数据,然后给浏览器做出响应
Request对象的继承体系结构
ServletRequest和HttpServletRequest是两个接口,HttpServletRequest是对ServletRequest的继承。而org.apache.catalina.connetcor.RequestFacade是对HttpServletRequest的实现类,由tomcat编写,并进行实例化、传递给service方法
Request对象的功能
-
获取请求消息数据
-
获取请求行
-
获取请求方式:getMethod()
-
获取虚拟目录(项目所在路径):getContextPath()
-
获取Servlet路径:getServletPath()
-
获取虚拟目录+Servlet路径(也就是URI):getRequestURI()\getRequestURL:
URL和URI的区别:
来源: https://www.php.cn/div-tutorial-413616.html
URI(统一资源标识符)是标识逻辑或物理资源的字符序列,与URL类似,也是一串字符。通过使用位置,名称或两者来标识Internet上的资源;它允许统一识别资源。
有两种类型的URI,统一资源标识符(URL)和统一资源名称(URN), 可以说URL是URI的子集
URL指定要使用的协议类型,而URI不涉及协议规范。
例如对一个项目下的资源,用getRequestURI()得到的是"/test/demo1",而用getRequestURL()得到的就是"http://localhost/test/demo1"
-
获取请求参数:getQueryString()
-
获取协议和版本:getProtocol()
-
获取客户机的ip地址:getRemoteAddr()
-
-
请求头
- getHeader(String name):通过请求头的名称获取对应的值
- Enumeration
getHeaderNames():获取所有的请求头名称.注意这个返回值类型:一种迭代器类型,只有两个方法:hasMoreElements()判断此枚举是否包括更多元素、nextElement()获取下一个元素
-
请求体
请求体重封装了Post请求的请求参数
请求体对象被封装为流对象。
-
获取流对象:
-
字符流(字符对象,例如表单上传):getReader()
-
字节流(例如文件、图片、视频上传):getInputStream().注意字节流也可以用来处理字符流对象,只是一般没有人这么做
-
-
读取流对象
- 字符流,readLine()
- 字节流,例如read()
-
-
-
其他功能
-
获取请求参数的通用方式(指对于get方法和post方法都适用,可想而知比上面的方法要流行):
- getParameter(String name),根据参数名称获取参数值
- getParameterValues(String name):根据参数名称获取参数值的数组,多用于一些复选框
- getParameterNames():获取所有请求的参数名称
- Map<String,String[]>getParameterMap():获取所有参数名称和参数值的键值对
-
请求转发(forward):
请求转发可以理解为在服务器内部的资源跳转方式,就是在servlet的内部再调用另外一个servlet,这样做的目的是降低程序的耦合度,不要把所有的功能都写在一个类里面,从而保持类的功能的单一、便于分工协作和降低出错
步骤:
- 通过request对象获取请求转发器对象:RequestDispatcher getRequestDispatcher(String path)
- 使用得到的转发器对象进行转发:forward(ServletRequest request,ServletResponse response),将request和response要传递进去。这样转发器对象就会创建目标Servlet类的对象并调用其service方法
特点:和网页跳转相区分
- 资源路径(也就是浏览器地址栏中的路径)不发生变化
- 只能转发访问服务器内部的HTML、servlet等资源
- 客户机只对服务器发送一次请求
-
共享数据:
在servlet之间进行数据通信,例如请求转发的servlet之间
概念:域对象:一个有作用范围的对象,可以在范围内共享数据:
- request域:代表一次请求能达到的范围,一般用于请求转发的多个资源中共享数据
方法:
- setAttribute(String name,Object obj):存储要共享的数据
- Object getAttribute(String name):通过键获取值
- removeAttribute(String name):通过键来移除键值对
-
获取ServletContext对象:
ServletContext getServletContext()
ServletContext对象
它代表整个web应用,可以和程序的容器来通信
获取:
除了上面提到的、使用request对象的getServletContext()方法之外来获取ServletContext对象,我们还可以通过HttpServlet的this.getServletContext()方法来获取.
功能:
-
获取MIME类型
MIME类型是互联网通信过程中的定义的一种文件数据类型。例如HTTP就遵循了MIME类型的要求
其格式为:大类型/小类型,例如我们之前用过的text/html,还有image/jpeg等
我们可以通过获取MIME类型来设置response的context-Type
获取方法:
- getMimeType(String file)
该方法实际上是通过获取文件的拓展名来返回MIME类型的:
例如:服务器的web.xml中的配置文件中就设置了MIME的映射关系。(每个项目都有一个web.xml,但是服务器的web.xml是所有项目的web.xml的源文件
-
域对象:共享数据
方法:
setAttrbute(String name,Object value)
getAttribute(String name)
removeAttribute(String name)
范围:
范围是整个web应用,是最大的域对象,可以共享同一个项目下所有用户、所有请求的数据。共享范围最大!共享数据的对象之间只需要在同一个web项目下即可、不需要任何关系
所以,该方法使用起来要很谨慎:容易信息冲突、信息泄露、内存溢出(因为它的生命周期非常长,只有服务器关机才会结束,所以如果一直往里面写参数的话会一直驻留在内存中)
-
获取文件的真实路径(服务器路径)
web项目会在本地工作空间和tomcat服务器下都存在一份。使用该对象,我们可以获取到项目文件在tomcat服务器下的真实路径:
String getRealPath(String path)
输入的path是相对路径,默认当前目录是web目录,也就是说如果是直接放在web目录下的文件,直接用/<文件名>就可以了,输出的RealPath就是在服务器上的绝对路径
-
-
-
常见问题
-
中文乱码问题;指在表单等输入中文后传递给服务器的是乱码。
解决:对流对象设置编码:
request.setCharacterEncoding("utf-8")
,注意具体的编码格式要和HTML页面上说设置的保持一致
-
-
BeanUtils工具类
这个工具类的作用是什么?
例如对一个注册页面,可能需要传递十几个参数。我们可以将这些参数对象化,从而在之后只传递对象即可,不需要直接传递参数。但是这就要求我们在每一个相关页面都要手动地创建这样的一个类、将其实例化并传入参数,这是很重复的步骤。
一个更好的做法是使用getParameterMap()直接以map的形式获取所有参数,而BeanUtils的作用就是将map转换为我们所要的对象,也就是实现自动的数据封装.但是注意类的定义还是需要我们去做的,这个类需要满足javaBean的规范
BeanUtils包可以直接下载或者使用maven等框架
Map<String,String[]>map=req.getParameterMap(); User loginUser=new User();//我们先自定义User类 BeanUtils.populate(loginUser,map);//完事了
相关内容:
-
什么是JavaBean?
标准的、遵守规范的Java类:
- 类必须被public修饰
- 必须提供空参的构造器
- 成员变量必须使用private修饰
- 提供public修饰的setter和getter
JavaBean一般放在domain、entity包下
-
JavaBean的功能
一般用来封装数据
-
属性和成员变量的区别
属性,严格上说,指的是setter和getter方法截取后的产物。属性和成员变量的名称可以是不一样的,例如setName对应的属性是name,但是这个setter可以操作叫任意名字的成员变量,不一定叫name。
-
BeanUtils相关的方法:
- setProperty(Object bean,String name,Object value):给java bean对象的特定属性赋值
- getProperty():相应的,就是获取java bean对象的特定属性值
- populate()
-
响应消息:servletResponse
响应消息的请求格式
-
响应行
-
组成:协议/版本 响应的状态码 状态码的描述
例如: HTTP/1.1 200 OK
-
响应的状态码:服务器去告诉客户端浏览器本次请求和响应的一个状态。状态码都是三位数字,分为5类。
分类:
-
1xx:服务器接收客户端消息,但是没有接收完成,等待一段时间后发送1xx状态码,表示处于等待状态
-
2xx:表示成功:200
-
3xx:表示重定向:302(重定向)、304(访问缓存)
访问缓存:例如传输图片类的二进制文件,占用时间比较长。如果同一用户连续请求一个图片,并且在请求间隔中该图片没有发生变化,那么服务器就给浏览器发生304,让浏览器从自己的缓存中去找该图片而不是自己重新发送一遍
-
4xx:客户端错误(请求错误,比如请求不存在的资源:404;405:该请求方式不支持,例如没有写doGet或者doPost方法)
-
5xx:服务器端错误:例如500
-
-
-
响应头
- 格式: 头名称:值
- 常见的响应头
- Content-Type:服务器告诉浏览器本次的响应消息体的数据格式和编码格式,例如
contentType="text/html;charset=UTF-8"
- Content-Length:响应体的长度(字节个数)
- Date
- Content-disposition:服务器告诉客户端以什么格式打开响应体数据,默认值是in-line(在当前页面内打开),可以修改为attachment;filename=xxx(以附件形式打开响应体,例如文件下载页面)
- Content-Type:服务器告诉浏览器本次的响应消息体的数据格式和编码格式,例如
-
响应空行
-
响应体
响应体就是我们要发送的数据,例如HTML的文本和图片、视频的二进制文件
Response对象
功能:设置响应消息
-
设置响应行
- 设置状态码:setStatus(int sc)
-
设置响应头
setHeader(String name,String value)
-
设置响应体
同样是通过流的方式进行传输
-
获取输出流对象
-
字符输出流PrintWriter getWriter()
-
字节输出流ServletOutputStream getOutputStream()
-
-
使用输出流输出数据
-
案例
-
完成重定向(redirect)
-
完成重定向的步骤
设置状态码返回302
设置响应头location:要转发的资源的路径
-
重定向和请求转发(forward)的区别:
- 浏览器访问路径,即地址栏路径发生变化
- 不仅可以访问当前服务器资源,也可以访问外网资源
- 重定向需要由浏览器进行两次访问,不能使用request对象共享数据:因为是两次访问,是两个不一样的request对象
-
代码
Servlet.java
@WebServlet("/Servlet") public class Servlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { System.out.println("Servlet1被访问了"); //通过重定向,访问Servlet2 //1.设置状态码为302 response.setStatus(302); //2.设置响应头location为Servlet2 response.setHeader("location","/ServletTest/Servlet2"); } protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { this.doPost(request,response); } }
Servlet2.java
@WebServlet("/Servlet2") public class Servlet2 extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { System.out.println("Servlet2被访问了"); } protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { this.doPost(request,response); } }
上面是一个手写的方式,但是由于重定向的步骤也是很固定的,所以更加简单的方法已经被创建出来了
Servlet.java:
@WebServlet("/Servlet") public class Servlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { System.out.println("Servlet1被访问了"); //简化操作的重定向 response.sendRedirect("/ServletTest/Servlet2"); } protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { this.doPost(request,response); } }
-
-
服务器输出字符数据到浏览器
-
步骤
-
获取字符输出流
注意,我们不需要在传输完成后清空流,因为这个输出流是由response获取的,它在response传输完成后就会被销毁,不需要手动清空
-
输出数据
-
-
代码
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { System.out.println("Servlet2被访问了"); //如果不指定使用的字符集,输出中文会造成乱码,因为输出流的默认编码是ISO编码 //【注意,设置流对象的编码要在获取之前!】 //response.setCharacterEncoding("GBK"); //但是我们不能提前知道客户浏览器默认使用的是什么字符集,所以我们需要: // 【通过设置响应消息头,告诉浏览器服务器发送的消息体数据的编码,建议浏览器使用该编码进行解码】 //text/html表示文本类型是text下的html类型 response.setHeader("content-Type","text/html;charset=UTF-8"); //注意,设置了content-Type之后,服务器传输的字符串类型也随之确定下来了,所以我们就不需要再设置response.setCharacterEncoding了 //1.获取字符输出流 PrintWriter pw=response.getWriter(); //2.输出数据,其实它就是响应的消息体 //pw.write("Hello,world!"); //写HTML标签也可以 pw.write("<h1>Test head</h1>"); //如果不指定使用的字符集,输出中文会造成乱码,因为输出流的默认编码是ISO编码 pw.write("你好"); }
同样的,response对象中也有方法可以简单地进行字符集设置:
response.setContentType("text/html;charset=utf-8");
为了避免可能的乱码,建议在获取所有的字符输出流之前都进行编码的设置
-
-
服务器输出字节数据到浏览器
和输出字符数据差别不大
代码:
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { System.out.println("Servlet2被访问了"); response.setContentType("text/html;charset=utf-8"); ServletOutputStream outputStream = response.getOutputStream(); //要将字符串转换为字节数组的形式 outputStream.write("hello".getBytes()); //输出中文同样有字符集的问题,这里我们要设置一下转换为字符数组时候的编码 outputStream.write("您好".getBytes("utf-8")); }
-
验证码案例
在验证码案例中,我们也会学到java绘图的方法
验证码是为了防止恶意表单注册
-
验证码是如何生成的?
- 保存海量验证码
- 动态生成验证码
显然第二种方法更好
-
我们这里要生成的验证码效果:
代码:
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { int width=100; int height=50; ServletOutputStream outputStream = response.getOutputStream(); /* 生成验证码 */ //1.创建在内存中验证码图片对象 BufferedImage img=new BufferedImage(width,height,BufferedImage.TYPE_INT_RGB); //2.绘制验证码 //2.1获取画笔对象 Graphics g = img.getGraphics(); //2.2使用画笔绘制验证码 g.setColor(Color.PINK);//设置画笔颜色 g.fillRect(0,0,width,height);//填充背景色 g.setColor(Color.BLUE); g.drawRect(0,0,width-1,height-1);//绘制边框,注意边框的开始位置是width-1(如果边框宽度是1的话) String str="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567989";//验证码的字符 for(int i=0;i<4;i++){ //创建随机数 Random ran=new Random(); int index = ran.nextInt(str.length()); g.drawString(String.valueOf(str.charAt(index)),20+20*i,25);//绘制验证码的字符 } //绘制干扰线 g.setColor(Color.green); //随机生成坐标点 Random ran=new Random(); int times=ran.nextInt(3)+3; for(int i=0;i<times;i++){ int xBegin=ran.nextInt(width); int xEnd=ran.nextInt(width); int yBegin=ran.nextInt(height); int yEnd=ran.nextInt(height); g.drawLine(xBegin,xEnd,yBegin,yEnd); } //3.输出验证码 ImageIO.setUseCache(false); //为什么要写这句话呢?看这篇文章:https://blog.csdn.net/cwfreebird/article/details/51820993 //究其原因应该是没有temp文件夹所致,所以这里换成使用内存缓存 ImageIO.write(img,"jpg",outputStream); }
-