零基础学Servlet
一。介绍:
servlet是一种比较古老的编写网站的方式,在2010年之前比较流行,在此之后,有一堆大佬创造了Spring(一种框架),Spring是针对Servlet进行进一步封装,从而让我们编写程序更加简单,现在使用java进行商业级别的开发几乎都是使用Spring为主
Servlet是Tomcat提供的一组api,封装了HTTP协议相关操作,让我们更加方便的搭建一个网站的后端
Servlet的主要工作就是让程序员自己实现一些类,然后把这些类加载到Tomcat之中,后续Tomcat收到HTTP请求,就会执行咱们自己写的类,通过这些代码完成一些业务逻辑
二。使用Srevlet的基本步骤
下面进行手动操作,写一个简单的servlet的hello world程序
Step1:创建项目
此处创建的是Maven项目,Maven是java中一个知名的构建工具(帮助我们编译/打包代码的工具),只要在电脑上下载好IDEA,就自然有了Maven
点击创建项目,然后选中Build system中的Maven即可创建Maven项目,Maven项目首次创建的时候,会从Maven网站上下载一些依赖的组建(这个过程要保证网络的稳定)
当创建完成之后,就会进入一下和原来写java代码相似的页面,然后关注其左半边部分(如下图所示)
这里的main目录下放的是业务代码(解决实际问题的具体过程就叫业务)
main目录之中的java目录是用来存放java代码的,resourses是用来存放java代码中依赖的其他资源(例如:图片,配置文件…)
test目录下主要存放的是测试代码
pom.xml是maven项目的入口配置文件,点开这个文件就可以发现里面已经有一些内容,这些是默认生成的情况,后续还需要修改这个文件添加更多的内容完成一些效果
Step2:引入依赖
接下来写的程序就要用到Servlet,这个api是Tomcat提供的,不是jdk提供的
可以进行手动下载导入,这样很麻烦(工作中涉及的项目会很多,可能依赖的内容也很多),手动管理就很麻烦
因此maven就可以很方便的解决这些问题
(1)打开maven中央仓库(大佬们把市面上的第三方库都收集起来,搞了一个网站,方便我们下载使用),找到Servlet的jar包
网址: https://mvnrepository.com/search?q=Servlet
找到这个后,点进去找3.1.0的版本然后拷贝下面图片画圈部分
复制之后,回到pom.xml之中,然后加一个标签,将复制的东西粘贴到这个标签之中(如下图所示)
这个dependencies标签可以加入多个依赖,后续加入到依赖都可放到这个标签之中
一般来说,只要复制进来就会自动进行下载,如果依然爆红,那么就要进行手动点击刷新键下载
Step3:创建目录
此处的Maven项目是普适的,不仅仅局限于Tomcat;而对与Tomcat来说,项目结构上有一些额外的要求
如上图所示,要在main这个文件夹中创建一个新的文件夹(名叫webapp),然后在webapp中创建一个文件夹(叫WEB-INF),最后在这个文件夹中创建一个文件,名叫web.xml
放入内容如下:
<!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>
此处无需记忆,只需复制粘贴即可
这里是Tomcat要求的结构,咱们写的代码以后要放到Tomcat之中,就必须按照Tomcat的要求进行编写代码
前面三步都不复杂,是一些固定的操作
Step4:编写代码
在java文件夹中创建一个class
import javax.servlet.http.HttpServlet;public class HelloServlet extends HttpServlet {
}
如果第一句import爆红了,那么就表示引入的依赖有问题
这里的继承是为了重写父类中的方法
HttpServletRequest 是Http请求
HttpSerletResponse是Http响应
doGet是处理Http GET请求,当Tomcat收到一个GET请求时,有可能执行到这个指令上来
写一个服务器主要步骤就三步:
1.读取请求并解析
2.根据请求计算响应
3.把响应返回给客户端
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {System.out.println("hello world");resp.getWriter().write("hello world");}
}
第一行就是让这个类和HTTP请求路径关联起来(配置路由)
第一个打印(System.out.println(“hello world”);)是把这个打印显示在控制台上
第二个打印(resp.getWriter().write(“hello world”);)是把打印显示在客户端上
Tomcat不是收到所有的get请求都调用这个doGet方法,而是同时判断请求是否为get,和请求的路径是否为/hello
Step5:打包程序
把写好的程序打包成war包(Tomcat要有一个war包在上面)
首先修改pom.xml,设置打包类型和包的名字
<packaging>war</packaging>
<build><finalName>java666</finalName>
</build>
注:默认打包的是jar包(java中通用的打包方式),但是Tomcat中要求是war包
然后右侧maven面板中找到backage按钮,并双击
看到这个就表明打包成功
Step6:部署程序
把war包拷贝到webapps中(目录中有想要的war包)
然后启动Tomcat(点bin文件夹中有一个startup.bat)
Strp7:验证程序
通过浏览器,访问Tomcat,获取到代码返回Hello World
127.0.0.1:8080/java666/hello
三。优化Servlet打包部署过程
如果此时想改代码怎么改:1.修改代码 2.重新打包 3.重新部署
这个过程会很麻烦,尤其到后期经常进行增删,这时我们就可以使用IDEA给的插件,把tomcat集成到IDEA中,此时就可以一键打包部署
Step1:安装
File ->settings ->plugins
搜索Smart Tomcat即可找到此插件
Step2:运行三角框旁边的位置,点Exit configuration
然后点击左上角的加号,找到tomcat
Steo3:配置
这里的context path决定了浏览器中访问这个Servlet的时候,第一级路径怎么写
不使用smart tomcat是直接写作war包的名字(目录第名字)
使用smart tomcat的话,context path就要手动进行配置,如果不配置则默认是项目名字
Step4 :运行
补充:如果输出是乱码的情况:
乱码意味着有多个环节对于编码方式的理解不一致,比如构造数据使用utf-8的形式,解析数据使用gbk的形式,那么就很容易出现乱码的状况了
构造数据:此处“你好”是在IDEA编译器上构造的(utf-8)
解析数据:浏览器上进行解析(默认跟随系统的,win10使用的字符集是jbk)
因此要把gbk改为utf-8,在http响应报文中,告诉浏览器返回的body字符集是啥
因此,加上下面这串代码在doGet之中:
resp.setContentType("text/html; charset=utf8");
在http响应报文的header中,就会有content-type,这里描述了body的数据格式,以及编码方式
四。常见的运行问题
1.出现404:
表示用户访问的资源不存在
原因:(1)url路径写错了(在浏览器中把地址栏的内容写错了)
(2)webapp没有正确加载,比如web.xml的内容不正确
2.出现405:
表示方法没有被实现(就比如发一个Get请求,但是Servlet中doGet没有被实现)
还一种可能就是没有删除super.doget(…);
3.出现500:
表示服务器内部出现问题,写的代码抛出异常了
4.出现空白页面:
这个时候就要检查服务器是否返回了带有正文的响应报文
5.出现“无法访问此网站”:
Tomcat没有正确运行
五。Servlet API:
Servlet API有三大类:1.HttpServlet 2.HttpServletRequest。3.HttpServletResponse
1.HttpServlet
方法名称 | 调用时机 |
---|---|
init | 通过这个方法来完成初始化 |
destroy | 用来释放资源 |
service | 收到Http请求的时候调用 |
doGet | 收到Get请求时调用 |
doPost | 收到Post请求的时候调用 |
doPut/doDelete… | 收到其他请求的时候进行调用 |
前三个方法都不用进行手动调用,会被Tomcat在合适的时机自动调用
前三个方法在实际开发中很少用到,但是面试题中常出现
init还算比较有用;service一般会被doPost和doGet替代;但是destory就非常尴尬了,说了不算,算了不说,这个方法大概率时执行不到的,一个Servlet不用了,说明Tomcat关闭了,而关闭Tomcat一般就两种办法:1.直接干掉Tomcat进程(在任务管理器上结束进程,或者是直接在IDEA上按X)这样就完全来不及调用destroy了。2.通过8005管理端口,给Tomcat发送一个“停机”指令,这个时候就能执行destory了 但是结合实际场景发现使用第一种方式关闭Tomcat的人更多一些
以后经常会涉及到咱们自己写好的代码让别人来调用,这就是所谓的“框架”(一个程序已经被大佬写好了,有些细节的内容,咱们可以插入咱们自己写的自定义逻辑
当我们开发人员想测试一下自己写的服务器代码是否能得到自己的预期时,我们可以使用Postman这个软件进行测试构造请求
还是以上面的服务器代码为例,运行服务器,然后打开Postman,构造GET请求
然后我们可以从图片上看到,Postman的下面就是服务器返回响应的结果,这样就可以查看自己的预期和实际运行结果是否相同了
2.HttpServletRequest
方法 | 描述 |
---|---|
String getProtocol() | 返回请求协议的名称和版本 |
String getMethod() | 返回请求的HTTP方法的名称,例如POST,GET,PUT等等 |
String getRequestURI() | 从协议名称知道HTTP请求第一行查询字符串中,返回该URL的一部分 |
String getContextPath() | 返回指令请求上下文的请求URI的部分(就是?之前的部分) |
String getQueryString() | 返回URL中的查询字符串(就是?之后的部分) |
Enumeration getParameterNames() | 获取quary String中所有的key |
String getParameter(String name) | 这里的name就是key,这个方法就是根据key获取value |
String[ ] getParameterValues(String name) | 当key存在重复的情况下,一个key可以有多个值(?a=10&a=20),这种情况就是把key对应的所有value都放到数组之中 |
String getCharacterEncoding() | 获取请求主体获取的字符编码的名称 |
String getContentType() | 获取主体的类型 |
int getContentLength() | 获取以字节为单位返回请求主体的长度 |
inputStream getinputStream() | 通过这个方法可以得一个流对象,读取这个流对象可以获得整个请求的body |
String getHeader(String name) | 这里的name时header中的key,根据这个key在header中获取到对应的value |
通过上述方法,可以看到上面的都是get系列(读方法)没有set系列(写方法)
3.HttpServletResponse
方法 | 描述 |
---|---|
void setStatus(int sc) | 为响应设置状态码(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) | 设置发送到客户端响应的字符编码(比如utf8) |
void sendRedirect(String location) | 指定重定向位置URL |
PrintWriter getWriter() | 用于往body中写入文本格式的数据 |
OutputStream getOutputStream() | 用于往body中写入二进制格式的数据 |
例子:
1.设置状态码和报错信息
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {//resp.setStatus(200);设置状态码resp.sendError(404,"这是一个错误的页面");//设置报错信息和报错状态码}
}
2.通过setHeader给响应中设置一些特殊的header
比如可以设置refreash:1,让浏览器每秒自动刷新一次
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {resp.setHeader("refresh","1");resp.getWriter().write((int) System.currentTimeMillis());//获取时间戳}
}
3.构造一个重定向响应
重定向的状态码是(3xx),一般使用302
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {resp.setStatus(302);resp.setHeader("Location","https://mvnrepository.com/");}
}
另一种写法是直接写resp.sendRedirect(“https://mvnrepository.com/”);
六。前后端交互:服务器获取请求中的参数
1.使用Query String:
query String时程序员自定义的,开发的时候经常使用
这里的query string是在URL中“?”后的内容
例如:127.0.0.1:8080/hello_servlet/hello?username=zhangsan&password=lisi
上面加上下滑线的部分就是query string
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {String username=req.getParameter("username");String password=req.getParameter("password");System.out.println("username="+username);System.out.println("password="+password);resp.getWriter().write("ok");}
}
这里前后端约定请求中给定的query string是形如:username=xxxx&password=xxx
上述query string会被tomcat自动解析成一个Map这样的结构
getParameter就是在查询Map<String,String>里面的内容
注:一般使用Query String都是在GET请求中使用,而POST请求一般都是使用form表单
2.使用form表单:
form表单是通过http请求的body来获取表单,body的格式就是query string
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {String username=req.getParameter("username");String password=req.getParameter("password");System.out.println("username="+username);System.out.println("password="+password);resp.getWriter().write("ok");}
}
这里可以发现代码的写法和上面的query string代码是完全相同的,只是把GET改成了POST,但是,实际上的差别还是很大的;query string是把键值对写在url中,而form表单是把键值对写在body之中
通过postman来构造一个post请求,去验证服务器代码
从postman进行构造就可以很容易的发现form表单发送post请求的方式是通过body进行的
3.使用json:
前面两种方式都是servlet天然支持的,但是json是需要引入第三方
此处为了针对json格式的数据进行解析和构造,就要引入json的库
Step1:下载json库:
在浏览器找到maven中央仓库,然后搜索Jackson Dateblind,然后找到任意一个版本进行复制,最后粘贴到pom.xml之中
Step2:使用Jackson,一个类,两个方法
类:objectMapper
两个方法:read方法(把json字符串映射成一个java对象) write方法(把java对象,映射成json字符串)
站在服务器的角度上,收到的请求就是json字符串,就需要把json字符串先映射成java对象,再进行一系列业务逻辑处理,处理完之后,把java对象映射回json字符串,并且通过响应来返回
class Request{public String username;public String password;
}class Response{public boolean ok;
}
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {ObjectMapper objectMapper=new ObjectMapper();Request request=objectMapper.readValue(req.getInputStream(),Request.class);System.out.println("username="+request.username);System.out.println("password="+request.password);Response response=new Response();response.ok=true;String respJson=objectMapper.writeValueAsString(response);resp.setContentType("application/json; charset=utf8");resp.getWriter().write(respJson);}
}
下面解释一下上面的代码:
首先doGet方法里的第一条就是刚才说的那个类(ObjectMapper),在写json的时候一定要先把这个类进行实例话;
然后来到第二行,我们要想在下面对来的数据进行更改和使用,就要先将数据转变成我们能处理的形式,所有就需要把json字符串映射成一个java对象,那么这个java对象需要一个类来进行接收,这个类中要包含json字符串中的所有的key,这样就可以把json字符串转换成我们想要的样子了,readvalue括号中第一个是表示通过获取流对象,来得到到body中的内容,第二个表示把拿到的东西转换成什么样的形式
然后下面的打印就是根据获取到的username和password进行处理
接着到了把java对象转回json字符串了,因此先在最上方写一个类,类中把要转换的对象写出,然后下面代码中进行实例话,接着对变量进行赋值
然后使用一个字符串来接收转换的结果,使用writeValueAsString这个方法,对实例话的类进行转换
最后指定返回的类型和编码方式,然后进行打印
七。Cookie和Session
1.概述:
Cookie是Http请求中header中的一个机制,也是浏览器持久话存储数据的一种机制
页面无法访问主机的文件系统,要想存储数据,就要通过其他方式
cookie中保存数据也是以键值对的形式,最终还是要把这个键值对返回给服务器的,服务器要使用cookie来完成一系列的业务逻辑
其中就有一种情况,是会使用cookie存储当前用户的身份信息
假设去医院挂号,医生给了一个就诊卡,这个就诊卡就相当于cookie,就诊卡中存储来用户的身份信息(身份标识),拿着这个就诊卡去各个科室,见到医生之后,医生会让你先刷就诊卡,刷卡这个过程,就会通过医院的系统,查询你的身份标识,进一步得到完成的身份信息(当前诊断信息,过往开药信息,以往病例,账户余额…)
每个患者都有这样的一份数据,那么这些数据在服务器中如何组织的呢?那必然是存储在数据库之中的
在服务器代码逻辑展开执行的过程中,这些数据就会被从数据库中查询出来,把他们先临时保存到某个内存结构中,后续有修改之类的,直接修改内存,然后重新写入数据库
这个内存结构就叫会话(session)
cookie是客户端存储数据的机制:在cookie中存储的用户身份标识,也经常被理解成sessionid
session是服务器存储数据的机制(不算持久化存储):服务器中有很多session,也有不同的sessionid
服务器通常会使用hash这样的键值对形式来存储session的,sessionid就是key,session本身就是value,在session里面存储用户的自身信息(是程序员自定的)
2.通过servlet api来操作上述结构:
cookie是浏览器的机制,servlet提供了api获取到cookie
session是服务器机制,servlet也提供了api来让我们使用
(1).HttpServletRequest类中的方法
(1)Cookie[ ] getCookies( ) :这个方法拿到请求中所有的cookie内容,每个cookie都是一个键值对
(2)HttpSession getSession( ) :这个方法就能完成,从cookie中获取到sessionid,并且查询到对应的session的过程;如果当前sessionid没查到,那么也能自动化的创建出新的键值对(分配新的sessionid,以及创建一个新的HttpSession对象),这个方法常用在登陆场景中
这里的HttpSession在servlet中表示一个会话,服务器上同时含有多个会话,服务器上会存在一个类似于这样的HashMap<String,HttpSession>,这里的String就是sessionid,HttpSession就是session对象
(2).HttpServletResponse类中的方法:
void addCookie(Cookie cookie):把指定的cookie添加到响应之中
(3).HttpSession类中的方法:
一个HttpSession对象里面包含多个键值对,我们往HttpSession中存任何我们需要的信息
(1)Object getAttribute(String name):该方法返回在该session对象中指定名称的对象,如果没有这个指定名称,那么就返回null
(2)void setAttribute(String name, object value):该方法用指定的名称绑定一个对象到该session对象(就是往session中添加元素内容)
(3)boolean isNew( ):判断当前是否创建出新对话
(4).cookie类中的方法:
每个cookie对象就是一个键值对
(1)String getName( ):该方法返回的是cookie的名称,名称在创建之后不能改变
(2)String getValue( ):获取于cookie相关联的值
(3)String setValue( ):设置于cookie相关的值
3.实战:用户登陆:
实际上cookie和session机制重要的作用就是辅助完成登陆功能的实现
Step1:写登陆的前端页面(html)
<form action="login" method="post"><input type="text" name="username"><input type="text" name="password"><input type="submit" name="登陆">
</form>
假设使用form表单,要求为Post请求,login路径
当然此处也可使用json,但是使用json就不能使用form表单了,使用json就要通过ajax的方式构造请求
注:浏览器构造http请求,有这么几种方式
1.url输入地址(GET)
2.特殊的html标签(a,img,script)(GET)
3.form表单(GET,POST)
4.ajax(GET,POST,PUT,DELETE…)
Step2:写一个Servlet处理请求
@WebServlet("/login")
public class HelloServlet extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {//1.先读取传来的数据req.setCharacterEncoding("utf8");String username=req.getParameter("username");String password=req.getParameter("password");//2.验证账户和密码是否正确//此处为了简单一点,就不使用数据库了,先把用户名和密码写死,假设用户名是zhangsan,密码是123if(!"zhangsan".equals(username)||!"123".equals(password)){//登陆失败resp.setContentType("text/html; charset=utf8");resp.getWriter().write("当前用户名或者密码错误");return;}//登陆成功HttpSession session=req.getSession(true);session.setAttribute("username",username);//登陆成功之后跳转到的页面resp.sendRedirect("index");}
}
前面的内容都容易理解,从登陆成功之后开始解析:
HttpSession session=req.getSession(true);这个方法就是根据请求中的sessionid查询服务器的hash表,从而找到session对象;如果此时cookie中没有sessionid(首次登陆就是没有的)或者没有查询到session对象,那么就看自动创建一个sessionid和一个session对象,把这个键值对保存到hash表之中,并且会把sessionid设置到响应之中,传回给浏览器,让浏览器使用cookie来进行保存
session.setAttribute(“username”,username);这里使用attribute的作用就是为了让一个数据在多个servlet之间共享,同时attribute是会话级的,每个用户都有自己的数据,相互之间不会干扰
这就能很好的说明,同样的页面,不同的用户看到的数据是不同的,例如支付宝查看余额
Step3:跳转的网页的后端代码
@WebServlet("/index")
public class jump extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {//先获取到用户对应的会话对象,生成的页面要根据当前用户的信息来进行构造HttpSession session=req.getSession(false);//sessionid不存在if(session==null){resp.setContentType("text/html;charset=utf8");resp.getWriter().write("不可登陆");return;}//从会话中拿到之前存储的用户信息String username= (String) session.getAttribute("username");//生成一个页面,把上述数据显示到页面上resp.setContentType("test/html;charset=utf8");String respBody="欢迎你"+username;resp.getWriter().write(respBody);}
}
HttpSession session=req.getSession(false);这句话表示根据cookie中的sessionid来查询Servlet这里的hash表,参数设置成false,如果查到了,就直接返回,没查到,就返回null
因为是跳转到页面,所以要使用GET请求
String username= (String) session.getAttribute(“username”);由于get Attribute是object类型,所以要进行强转
当验证通过就会保存会话信息(hash),客户端也会保存身份标识(sessionid);后续浏览器再访问这个网站都会带上cookie,服务器不要让浏览器重新登录,也能识别出浏览器的用户身份信息