1.servlet线程特性
单实例 多线程
不要使用成员变量 成员变量会在多线程间共享 写操作不安全
之前讲servlet生命周期,servlet实例由web服务器自己创建,且只创建一次,故servlet实例只有一个。
为了满足多用户的同时访问,Tomcat服务器必须是多线程的。 多线程用同一个servlet对象(和类变量很类似,共享),是否有线程安全问题?若使用成员变量,会出现线程安全问题。
面试问题:servlet本身是不是线程安全的?
若加了成员变量,且对成员变量有写操作,绝对不安全。
不建立成员变量,也就不共享数据,也不会有线程安全问题。
故使用servlet时,一般不建立成员变量,基于方法做一些操作即可。若非要建成员变量,则要开启多实例模式。
2.servlet域对象
不是在代码层面数据共享,而是在服务器运行过程中,依据请求,响应和用户访问的过程,服务器自己创建几个对象,允许你在对象里传入或取出数据,从而达到数据共享的目的。
1.Servlet三大域对象介绍
1.request域
一个用户可有多个 每次请求产生一个 响应结束后对象失效 常用在页面间数据传递2.session域
一个用户一个,每一个浏览器都会产生一个,会话过期或者关闭浏览器对象消失,建议存放一些用户信息,不要把过多的信息存放在session里 同一个浏览器 多次访问间 使用同一个session对象(同一个浏览器多次访问间 可以共享数据) session失效条件 1.浏览器关闭 2.session超过有效期 有效非活动时间 (两次访问的间隔) 3.session.invalidate() 服务器设置session失效3.servletContext域 (服务器对象)
全局只有一个,是一个全局的储存信息的空间,服务器开始就存在,服务器关闭才释放。为了节省空间,提高效率,只存放少量的重要信息 服务器启动产生 服务器关闭销毁 全局共享 尽量只读
有效非活动时间指两次访问的间隔。
域对象统一方法:
void | setAttribute(String key,Object value) | 以key/value的形式保存对象值 |
---|---|---|
Object | getAttribute(String key) | 通过key获取对象值 |
2.Servlet三大域对象使用
2.1request域对象的使用
1.新建项目(Project Structrue->Modules->+->new Module.)(弹出的New Module界面,java Enterprise,勾选Web Application,下一步,起个新名字day2_servlet)
2.src目录下新建com.javasm.controller包,该包下新建ServletDemo1类
@WebServlet("/demo1") public class ServletDemo1 extends HttpServlet { @Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { //request的作用 //1.请求报文相关信息封装在request里,且2.request还可做域对象 req.setAttribute("mykey","myval"); //放置,这些对象只在服务器生效,浏览器请求过来,自动生效 //Object mykey = req.getAttribute("mykey"); //根据key取值,默认返object,故根据自己要取东西的类型强转 String mykey = (String)req.getAttribute("mykey"); System.out.println(mykey); } }
(其它两个域也是get和set方法)
3.配置服务器
(配置过一次,再新建项目时,Tomcat会自动将新建项目部署出来,同时创建处Web服务器)
右上角文本框,Deployment中已出现该项目,不用再添加。在其右边文本框添加/day2,作为模拟的根路径。
4.运行
弹出的页面输入http://localhost:8080/day2/demo1
控制台会出现myval
(这里最初报错:Address localhost:1099 is already in use
解决方案:(1)win+R,打开命令提示符框,输入cmd,进入命令提示符 (2)输入netstat -aon | findstr 1099,找到占用1099端口的进程ID:PID (3)输入taskkill -f -pid PID (4)重启Tomcat )
5.新建ServletDemo2类
@WebServlet("/demo2") public class ServletDemo2 extends HttpServlet { @Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String mykey = (String)req.getAttribute("mykey"); System.out.println(mykey); } }
在ServletDemo2里仅仅取。重新部署启动后,在弹出的页面输入http://localhost:8080/day2/demo2,控制台输出null。再次输入http://localhost:8080/day2/demo1,还是myval。因为ServletDemo2的request域里没放东西。
(request域对象,每次请求都会产生一个)
2.2session域对象的使用
同2.1
(session域对象需要创建)
1.在ServletDemo1类:
@WebServlet("/demo1") public class ServletDemo1 extends HttpServlet { @Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { HttpSession session = req.getSession(); //获取session域对象,类型是HttpSession session.setAttribute("mykey","myval"); String mykey = (String)session.getAttribute("mykey"); System.out.println(mykey); } }
2.在ServletDemo2类,仍然是只取:
@WebServlet("/demo2") public class ServletDemo2 extends HttpServlet { @Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { HttpSession session = req.getSession(); //获取session域对象,类型是HttpSession String mykey = (String)session.getAttribute("mykey"); System.out.println(mykey); } }
3.启动服务器
先访问demo2,http://localhost:8080/day2/demo2,结果是null。(没地方放值,自然也就没值)
再访问demo1,http://localhost:8080/day2/demo1,结果为myval。
再次访问demo2,http://localhost:8080/day2/demo2,结果为myval。
(session域对象可在多次访问间共享数据,一旦有一个类将数据放入,其他类就可读取到)
4.关闭浏览器,再重新打开浏览器,服务器会重新创建session域对象,之前放入的东西没有了
访问demo2,http://localhost:8080/day2/demo2,结果是null。
(每一个浏览器的请求都可以使服务器创建一个session域对象,只要session域对象生效,同一个浏览器多次访问间,都可使用同一个session域对象,数据可共享。)
2.3servletContext域对象的使用
同2.1
1.在ServletDemo1类:
@WebServlet("/demo1") public class ServletDemo1 extends HttpServlet { @Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { //获取servletContext对象,可用当前实例获取,也可用请求获取 ServletContext servletContext = req.getServletContext(); //servletContext就是服务器对象 servletContext.setAttribute("mykey","myval"); String mykey = (String)servletContext.getAttribute("mykey"); System.out.println(mykey); } }
2.在ServletDemo2类,仍然是只取:
@WebServlet("/demo2") public class ServletDemo2 extends HttpServlet { @Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { ServletContext servletContext = req.getServletContext(); String mykey = (String)servletContext.getAttribute("mykey"); System.out.println(mykey); } }
3.启动服务器
先访问demo2,http://localhost:8080/day2/demo2,结果是null。(没人放过东西)
再访问demo1,http://localhost:8080/day2/demo1,结果为myval。
再次访问demo2,http://localhost:8080/day2/demo2,结果为myval。
4.关闭浏览器,再重新打开浏览器
访问demo2,http://localhost:8080/day2/demo2,结果为myval。
5.换个浏览器
访问demo2,http://localhost:8080/day2/demo2,结果为myval。
(一旦生成servletContext域对象,所有请求过来都可以访问到。是一个全局共享的数据)
3.Servlet中常用类和方法
3.1 ServeltContext接口
(常表示服务器对象。最主要的功能是当域对象使用,有setAttribute和getAttribute方法。getInitParameter方法拿初始化参数,昨天讲过)
方法 | 说明 |
---|---|
public void setAttribute(String name,Object object) | 绑定一个java对象和一个属性名,并存放到ServletContext中,参数name指定属性名,参数Object表示共享数据 |
pulbic Object getAttribute(String name) | 根据参数给定的属性名,返回一个Object类型的对象 |
public String getContextpath() | 返回当前web应用的URL入口 |
public String getInitParameter(String name) | 返回web应用范围内匹配的初始化参数值。在web.xml中,<web-app>元素中的<context-param>元素表示应用范围内的初始化参数 |
看一下getContextpath()方法:
1.新建ServletDemo3类:
@WebServlet("/demo3") public class ServletDemo3 extends HttpServlet { @Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { ServletContext servletContext = req.getServletContext(); System.out.println(servletContext.getContextPath()); } }
2.重新部署,运行
访问demo3,http://localhost:8080/day2/demo3,结果是/day2。
(通过该方法可拼接一些项目要访问的地址)
3.2ServletConfig接口
方法 | 说明 |
---|---|
public String getInitParameter(String path) | 获取web.xml中指定Servlet的初始化参数值 |
public ServletContext getServletContext() | 获取ServletContext实例 |
public String getServletName() | 获取当前Servlet的名称 |
getInitParameter()方法是直接在servlet里调用,对应servlet里配置的初始化参数。
@WebServlet("/demo3") public class ServletDemo3 extends HttpServlet { @Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { this.getInitParameter("xxx"); //获取servlet上配置的初始化参数 this.getServletContext(); //获取ServletContext对象的第二种方式 this.getServletName(); //获取servlet配置的name } }
对于String getServletName(),获取的是web.xml里的name
<servlet> <servlet-name>servlet3</servlet-name> <!--获取的是servlet3--> <servlet-class>com.javasm.controller.ServletDemo3</servlet-class> <init-param> <param-name>mykey</param-name> <param-value>abc123</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet>
该接口方法不重要,了解即可。
3.3HttpRequest(很常用)
方法 | 说明 |
---|---|
public String getParameter(String name) | 获取页面提交指定名称的参数值 |
public String[] getParameterValues(String name) | 获取页面提交相同名称参数的数组值 |
public void setCharacterEncoding("UTF-8") | 设置字符编码格式 |
public Map getParameterMap() | 返回一个保存了请求的所有参数和值的Map对象 |
public void setAttribute(String name,Object obj) | 向request范围内设置属性,参数name为属性名,obj为属性值 |
public Object getAttribute(String name) | 返回一个请求中属性名为name的属性值 |
public String getContextPath() | 返回当前Web项目的根路径 |
(HttpRequest就是使用的request对象,每个请求里都会用到它,大部分方法都是针对请求报文数据的封装)
1.getParameter(),通过key取值。
@WebServlet("/demo3") public class ServletDemo3 extends HttpServlet { @Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String mykey = req.getParameter("mykey"); System.out.println(mykey); } }
若浏览器请求时不传参数,即http://localhost:8080/day2/demo3,读到的结果是null。
若浏览器请求时传参数,http://localhost:8080/day2/demo3?mykey=abc123,读到的结果是abc123。(传数据实际是客户端和服务端通过流做数据传输,故服务端拿到数据是没有对象的。拿到数据封装成对象是另一码事。数据本身只有字符串,故req.getParameter返回的数据类型都是字符串)
还有一种情况,参数有key,但没给value,如:http://localhost:8080/day2/demo3?mykey=&mykey2=xxx,
结果是空字符串。
故,传值时空值有两种情况:一种是key不存在,后台拿到的是null;另一种是key存在,但没有值,后台拿到的是空字符串。
2.getParameterValues()
也是获取页面传过来的参数,获取同名的参数。
多选框就是用的相同的name属性,传值时就可能是http://localhost:8080/day2/demo3?hobby=1&hobby=3
@Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String[] hobbies = req.getParameterValues("hobby"); System.out.println(hobbies[0]); System.out.println(hobbies[1]); }
结果是1 3
(该方法一般对应checkbox和多选select)
3.getParameterMap()
参数以键值对存在,键值对间用&隔开,服务器其实就是用&分割,将键值对存在Map集合里。
注:Map<String, String[]>,key是字符串,value是字符串数组
@Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { Map<String, String[]> parameterMap = req.getParameterMap(); System.out.println(parameterMap.get("mykey")[0]); System.out.println(parameterMap.get("hobby")[0]); }
运行后,浏览器输入http://localhost:8080/day2/demo3?mykey=abc123&hobby=1&hobby=3
结果是abc123 1
(1的getParameter()和2的getParameterValues()都是基于getParameterMap()又做了一次封装)
4.getInputStream()
获得的不是完整的请求报文,而是请求报文正文部分
ServletInputStream inputStream = req.getInputStream();
拿到一个输入流。request已经将请求报文解析过,按理说流只能读一次,这是干什么?
若使用post方式传数据,参数在请求正文部分,它可以将请求正文部分的内容不经过封装,原原本本地拿出来。
eg:
1.在web目录中新建myPage.xml,还是做一个注册页面,引入bootstrap,过程和昨天一样。这里将form里的action和method修改:
<form action="/day2/demo3" method="post" class="form-horizontal">
2.在后端,也就是ServletDemo3类中,想拿到浏览器传过来的原始数据处理:
@WebServlet("/demo3") public class ServletDemo3 extends HttpServlet { @Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { ServletInputStream inputStream = req.getInputStream(); BufferedReader br = new BufferedReader(new InputStreamReader(inputStream)); String s = br.readLine(); System.out.println(s); } }
3.重新部署,运行
弹出的浏览器输入http://localhost:8080/day2/myPage.html
输入用户名jack和密码123456,点击提交
结果:后台输出username=jack&password=123456
(注意:流只能读一次,用其他方法读了,这里就读不了了)
5.setCharacterEncoding()
昨天已讲,自动封装成Map的过程中,设置字符编码
处理请求乱码,需要在获取参数前执行:
req.setCharacterEncoding("utf-8");
6.当成域对象时使用的两个方法
setAttribute()
getAttribute()
7.getContextPath()
讲到ServeltContext接口时有一个getContextpath()
他们获取的东西一样
8.Request对象中封装了很多获取跟http相关的路径的方法
request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+request.getContextPath()+"/";
(request.getScheme()获得协议
request.getServerName()获得服务器名称
request.getServerPort()获得服务器端口
request.getContextPath()获得请求入口的路径)
结果:http://localhost:8080/day2 当前项目的根的绝对路径
request.getRemoteAddr() 客户端ip地址
(看一下谁来访问你)
request.getRealPath("/") 获取代码在Tomact里运行的真实目录
(该方法最初是放在request里,现在已过时。最终放在了ServletContext里)
@Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { ServletContext servletContext = this.getServletContext(); System.out.println( servletContext.getRealPath("/")); }
弹出的浏览器输入http://localhost:8080/day2/demo3
运行后的结果:E:\workspace\two\javaEE\out\artifacts\day2_servlet_war_exploded\
(在服务器里做本地IO时会用到该路径)
request.getServletPath() 获得配置的当前servlet的访问路径
request.getRequestURI(); 获得请求路径
@Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { System.out.println(req.getServletPath()); System.out.println(req.getRequestURI()); System.out.println(req.getRequestURL()); }
弹出的浏览器输入http://localhost:8080/day2/demo3
运行后的结果:/demo3 /day2/demo3 http://localhost:8080/day2/demo3
(URI统一资源标识符,概念上的地址
URL统一资源定位符。实际地址
URL地址一定能访问到,URI一般是地址的一部分,用来指代当前要访问的资源)
3.4HttpResponse
方法 | 说明 |
---|---|
Public PrintWriter getWriter() | 获取响应的打印输出流对象 |
Public void setContentType("text/html;charset=UTF-8") | 设置响应内容的类型和字符编码格式 |
主要和输出相关。
setContentType()
使用之后,响应头会多加一句内容。
text/html表示返回的数据是什么格式。通过URL访问的资源除了html页面,还有图片,js,css等,不同资源,这个部分不同。虽然浏览器会自动解析,做适配,但如果把所有返回的数据都写成text/html是不行的。这里是返回文本格式的html页面格式 charset=utf-8告诉浏览器返回的数据用什么格式去解析。可用它处理响应的中文乱码。注意:若设置的是GBK,返回的数据也是GBK,这里也要写成GBK。(右下角可看到项目默认的编码格式)
@Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html;charset=utf-8"); //PrintWriter是封装后的输出流,之后用起来会很方便. PrintWriter writer = resp.getWriter(); //writer.print()会包含各种类型,主要用它输出页面信息 //也保留有原始的write()方法 writer.write(); writer.print("xxxx"); //流用完要刷新和关闭 writer.flush(); //把流数据清空,看还有没有数据没有发出,没发的都推过去 writer.close(); //close时还会自动做一次推流 }
3.5HttpSession
1.session常用API
方法 | 说明 |
---|---|
setAttribute(String key,Object value) | 以key/value的形式保存对象值 |
getAttribute(String key) | 通过key获取对象值 |
getMaxInactiveInterval() | 获取session的有效非活动时间,以秒为单位 |
getId() | 获取session对象的编号 |
invalidate() | 设置session对象失效 |
session作为域对象使用的两个方法:setAttribute getAttribute
@WebServlet("/demo4") public class ServletDemo4 extends HttpServlet { @Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { HttpSession session = req.getSession(); String id = session.getId(); System.out.println(id); } }
重新部署,运行
弹出的浏览器输入 http://localhost:8080/day2/demo4
结果:00A5551ECBD2E66972F3430C363FAB0D
(当前使用的浏览器使用的session编号)
换个浏览器,会创建另一个单独的session对象,产生另一个session编号。
getMaxInactiveInterval()
@Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { HttpSession session = req.getSession(); //设置最大有效非活动时间 session.setMaxInactiveInterval(5); //单位是秒 //若两次访问时间超过5秒,服务器会重新创建session对象 //读取最大有效非活动时间 System.out.println(session.getMaxInactiveInterval()); }
Tomcat也可通过配置文件修改最大有效非活动时间,默认值是30分钟
invalidate()
服务器可主动设置session失效
session.invalidate();
会使当前session对象直接失效,现在再次访问,结果:
每次访问的都会创建新的session对象,session编号每次都不同。
总结session失效的三种办法:
1.关闭浏览器
2.超过有效时间(再次访问的间隔)
3.服务器主动设置session失效
在session生效的过程中,同一个用户(浏览器)多次访问间,可共享数据。
2.session常用场景:
1.数据共享
一个servlet里放入信息,同一个用户多次访问可访问到,其他用户不能访问到。不同用户间有隔离性。
前面的例子有。
2.同一个请求地址展示不同的界面
有如下四个servlet,分别是订单/用户/商品/登录服务。前两个必须登录后才能访问,可加逻辑判断。通过session里是否放入指定的值判断是否登陆过。
代码如下:
1.新建controller2包
2.在该包下新建LoginServlet类,读取用户名密码,判断用户名密码是否正确(这里先不连接数据库)。都正确则登陆成功,放session标记。
@WebServlet("/login") public class LoginServlet extends HttpServlet { @Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String username = req.getParameter("username"); String password = req.getParameter("password"); //假设正确的用户名是admin,正确的密码是abc123 if ("admin".equals(username)&&"abc123".equals(password)){ HttpSession session = req.getSession(); //放session前要先创建session对象 session.setAttribute("loginUser",username); //放session标记,这里值不重要,主要看是否有key System.out.println("登录成功"); } } }
3.新建UserServlet类,象征性写一下用户服务的代码
@WebServlet("/user") public class UserServlet extends HttpServlet { @Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { System.out.println("执行用户服务的代码"); } }
4.新建OrderServlet类
@WebServlet("/order") public class OrderServlet extends HttpServlet { @Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { System.out.println("执行订单服务代码"); } }
5.为了使订单服务在登录成功后才能访问,增加逻辑判断
@WebServlet("/order") public class OrderServlet extends HttpServlet { @Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html;charset=utf-8"); PrintWriter writer = resp.getWriter(); if ("登陆过"){ System.out.println("执行订单服务代码"); writer.print("显示订单列表"); }else{ System.out.println("没有登陆过"); writer.print("对不起 请先登录"); } writer.flush(); writer.close(); } }
这里直接将响应展示到页面上。
如何表示是否登录成功:
@WebServlet("/order") public class OrderServlet extends HttpServlet { @Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html;charset=utf-8"); PrintWriter writer = resp.getWriter(); HttpSession session = req.getSession(); //Object loginUser = session.getAttribute("loginUser"); String loginUser = (String)session.getAttribute("loginUser"); if (loginUser!=null){ System.out.println("执行订单服务代码"); writer.print("显示订单列表"); }else{ System.out.println("没有登陆过"); writer.print("对不起 请先登录"); } writer.flush(); writer.close(); } }
6.重新部署,运行
弹出的页面输入 http://localhost:8080/day2/order
页面直接显示 对不起 请先登录
输入http://localhost:8080/day2/login?username=admin&password=abc123
控制台显示 登录成功
再次访问 http://localhost:8080/day2/ord
页面显示 显示订单列表
换一个浏览器,输入 http://localhost:8080/day2/order
页面直接显示 对不起 请先登录
(不同用户拥有不同的session)
7.完善登录界面
还是用bootstrap(复制三个文件夹到web包,新建loginPage.html在web包下)
代码和昨天的差不多。
loginPage.html想做起始页面有两个方法:
1.在idea里配置
点击右上角文本框,选择Edit .....
在弹出的Run/Debug Configurations界面,在Tomcat 8.5.34选项,将URL对应的文本框中http://localhost:8080/day2/改为http://localhost:8080/day2/loginPage.html
2.在项目里设置
在web项目核心配置文件web.xml中,增加欢迎页面
<welcome-file-list> <welcome-file>loginPage.html</welcome-file> </welcome-file-list>
此代码放在web-app标签里。
<?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_4_0.xsd" version="4.0"> <welcome-file-list> <welcome-file>loginPage.html</welcome-file> </welcome-file-list> </web-app>
loginPage.html最终的代码为:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> <link rel="stylesheet" type="text/css" href="css/bootstrap.css"> <script src="js/jquery-3.6.0.js" type="text/javascript" charset="utf-8"></script> <script src="./js/bootstrap.js" type="text/javascript" charset="ytf-8"></script> <style> .mycls{ width:180px; height:285px; border:1px solid gray; border-radius:10px; margin:200px auto; padding:50px; } </style> </head> <body> <div class="mycls"> <h1>登录</h1> <hr/> <form action="/day2/login" method="post" class="form-horizontal"> <div class="form-group"> <label for="username" class="col-sm-2 control-label">用户名</label> <div class="col-sm-10"> <input type="text" class="form-control" id="username" name="username" placeholder="username"> </div> </div> <div class="form-group"> <label for="password" class="col-sm-2 control-label">密码</label> <div class="col-sm-10"> <input type="password" class="form-control" id="password" name="password" placeholder="Password"> </div> </div> <hr/> <div class="form-group"> <div class="col-sm-offset-2 col-sm-10"> <button type="submit" class="btn btn-default">登录</button> </div> </div> </form> </div> </body> </html>
8.检查
将LoginServlet类修改
@Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String username = req.getParameter("username"); String password = req.getParameter("password"); System.out.println(username); System.out.println(password); /*//假设正确的用户名是admin,正确的密码是abc123 if ("admin".equals(username)&&"abc123".equals(password)){ HttpSession session = req.getSession(); //放session前要先创建session对象 session.setAttribute("loginUser",username); //放session标记,这里值不重要,主要看是否有key System.out.println("登录成功"); }*/ }
重新部署,运行
1.F12打开开发者工具。在页面输入用户名jack,密码abc123,看一下开发者工具中传输的参数是否为name=jack&&password=abc123。没问题则说明可以正常发出
2.在idea控制台看是否有jack和abc123。能接收到,则说明连通成功。
9.数据库效验
登录服务拿到用户名和密码,需要和数据库信息效验
①.打开Navicat,新建test库,在该库新建tb_users表
②.与数据库建立连接
先找jar包:mysql-connector-java-8.0.27.jar
Tomcat指定:在使用web项目时,jar包必须放在WEB-INF的lib目录里,它就从这里找。故先在WEB-INF新建lib目录,再将jar包粘贴进来
Tomcat也会在自己的运行环境找(就是安装Tomcat的地方。apache-tomcat-8.5.34-->lib)。两个地方都找不到,Tomcat会告知这个包不存在。
加载该jar包(目的是让idea找到该jar包),点击右上角Project Structure中,弹出的界面点击右边的加号。将放在web目录下的jar包加进来。
以上,不管idea还是Tomcat都可以找到该jar包了。
同理,将lombok.jar以同样方式加进来。
第一阶段将数据库连接池和DBUtil都用起来了,第三阶段会学数据库方面的框架,后面不用DBUtil,这里复习一下jdbc,也不用DBUtil。
第二阶段有三层结构:
控制层:放servlet代码,负责请求的接收,代码的调用,响应的反馈;
业务逻辑层:service层,写业务场景(业务逻辑)。场景主要有:
1.数据库出来的数据和要显示的格式不匹配,需要转换
2.数据库出来的数据,在数值或数据上,需要计算(比如加前后缀,加减操作等)
数据持久层:dao层(数据持久指内存的数据存到硬盘,数据库就是干这个事的),基本的增删改查操作。(与数据库交互,写入是添加,修改,删除;读取是查询)
③.在javasm包下新建dao包
该包下创建LoginDao接口。需要做的是:传入用户名,密码,返回用户对象。
public interface LoginDao { //通过用户名和密码寻找用户 Users getUser(String username, String userpwd); }
④.在javasm包下新建entity包,用来封装用户对象。
在该包下新建Users类。
@NoArgsConstructor @AllArgsConstructor @Getter @Setter @ToString public class Users { private Integer userId; private String userName; private String userPwd; private String userPhone; }
⑤.为了使用方便,通常会把数据封装成对象去传。对于③的username和password属于Users对象。故控制层通常接收到请求后,会将请求的参数(都是字符串)封装成对象。
在LoginServlet类中:
@Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String username = req.getParameter("username"); String password = req.getParameter("password"); Users inserUser = new Users(username,password); }
相应的在Users类中增加有参构造:
@NoArgsConstructor @AllArgsConstructor @Getter @Setter @ToString public class Users { private Integer userId; private String userName; private String userPwd; private String userPhone; public Users(String userName, String userPwd) { this.userName = userName; this.userPwd = userPwd; } }
将LoginDao接口相应也修改:
public interface LoginDao { //通过用户名和密码寻找用户 Users getUser(Users insertUser); }
⑥.在dao包下新建impl包,在imp包下新建LoginDaoImpl类:
接下来与jdbc会有关,获得驱动,获得连接对象.....这些都是模式性的代码,故在Javasm包下新建util包,包下新建DBHelper类。同时在src包下新建配置文件jdbc.properties。
## \u914D\u7F6E\u4FE1\u606F jdbc.user=root jdbc.pass=root jdbc.url=jdbc:mysql://localhost:3306/test1?useSSL=true&characterEncoding=utf-8 jdbc.driver=com.mysql.jdbc.Driver
DBHelper类:
public class DBHelper { static String username; static String pwd; static String url; static String drivername; static { Properties p = new Properties(); try { //程序运行时 不一定在你的工程目录的编译目录下 p.load(DBHelper.class.getResourceAsStream("/jdbc.properties")); username = p.getProperty("jdbc.user"); pwd = p.getProperty("jdbc.pass"); url = p.getProperty("jdbc.url"); drivername = p.getProperty("jdbc.driver"); Class.forName(drivername); } catch (Exception e) { e.printStackTrace(); } } public static Connection getConn() { Connection con = null; try { con = DriverManager.getConnection(url, username, pwd); } catch (SQLException e) { e.printStackTrace(); } return con; } public static void CloseConn(Connection conn,Statement stat,PreparedStatement psta,ResultSet rs){ try { if(stat!=null)stat.close(); if(psta!=null)psta.close(); if(rs!=null)rs.close(); if(conn!=null)conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }
回到LoginDaoImpl类:
public class LoginDaoImpl implements LoginDao { @Override public Users getUser(Users insertUser) { Connection conn = DBHelper.getConn(); String sql = ""; PreparedStatement preparedStatement = null; try { preparedStatement = conn.prepareStatement(sql); } catch (SQLException throwables) { throwables.printStackTrace(); } return null; } }
发现关键还是写对sql,故为了写对sql,无论哪个阶段,都要在Navicat的查询中写需要的sql语句,从而保证sql语句的正确性。
这里是带条件的查询,查用户名和密码,在Navicat的查询中新建查询,写出相应的sql:
结果是能查出来,说明该sql语句没问题。
(一般公司不建议用*,而是把需要用到的字段清楚地写出来。一般公司的表的字段会很多,而且表与表之间的关系也很复杂)
最终的sql语句:
select tu.user_id,tu.user_name,tu.user_pwd,tu.user_phone from tb_users tu where tu.user_name = 'jack' and tu.user_pwd = 'abc123'
回到LoginDaoImpl类,将sql填入,用户名和密码处用占位符:
String sql = "select tu.user_id,tu.user_name,tu.user_pwd,tu.user_phone from tb_users tu where tu.user_name = ? and tu.user_pwd = ?";
使用数据库时有prepareStatement预编译对象和Statement非预编译对象。区别就是:预编译对象在参数上可以用?去占位。用?占位的主要好处是防止注入攻击。(sql注入可百度)
完整的LoginDaoImpl类代码:
public class LoginDaoImpl implements LoginDao { @Override public Users getUser(Users insertUser) { Connection conn = DBHelper.getConn(); String sql = "select tu.user_id,tu.user_name,tu.user_pwd,tu.user_phone from tb_users tu where tu.user_name = ? and tu.user_pwd = ?"; PreparedStatement preparedStatement = null; ResultSet resultSet = null; Users loginUser = null; try { preparedStatement = conn.prepareStatement(sql); preparedStatement.setString(1,insertUser.getUserName()); preparedStatement.setString(2,insertUser.getUserPwd()); resultSet = preparedStatement.executeQuery(); if(resultSet.next()){ int userId = resultSet.getInt("user_id"); String userName = resultSet.getString("user_name"); String userPsw = resultSet.getString("user_pwd"); String userPhone = resultSet.getString("user_phone"); loginUser = new Users(userId,userName,userPsw,userPhone); } } catch (SQLException throwables) { throwables.printStackTrace(); }finally { DBHelper.CloseConn(conn,null,preparedStatement,resultSet); } return loginUser; } }
⑦.建一个测试类
开始项目前做的本地测试,将有问题的地方修改。
在javasm包下新建test包,该包下新建Mytest类:
public class Mytest { public static void main(String[] args) { LoginDao ld = new LoginDaoImpl(); Users user = ld.getUser(new Users("jack", "abc123")); System.out.println(user); } }
(运行出现问题,可能没勾选:File-->Settings-->Bulid,Execution,Deployment-->Complier-->Annotation Procession。选择day2_servlet,右上角勾选)
运行结果:控制台打印输出:Users(userId=1, userName=jack, userPwd=abc123, userPhone=14434343434)
⑧.dao层与service层连接
在javasm包下新建service包,新建LoginService接口:
public interface LoginService { Users getUsers(Users insertUser); }
在service包下新建impl包,新建LoginServiceImpl类:
public class LoginServiceImpl implements LoginService { @Override public Users getUsers(Users insertUser) { LoginDao ld = new LoginDaoImpl(); Users user = ld.getUser(insertUser); return user; } }
这里发现service没什么业务逻辑,此时service层充当了通道。
也可强行添加业务逻辑:
public class LoginServiceImpl implements LoginService { @Override public Users getUsers(Users insertUser) { LoginDao ld = new LoginDaoImpl(); Users user = ld.getUser(insertUser); if (user!=null){ //名字前加了个亲爱的 user.setUserName("my Dear!!!"+user.getUserName()); } return user; } }
⑨.与控制层的连接
控制层要做的事:1.接收参数,封装对象。前面已完成
2.调用业务逻辑
3.3.根据业务逻辑 层返回的Users对象决定返回什么页面
在LoginServlet类中:
@WebServlet("/login") public class LoginServlet extends HttpServlet { @Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { //1.接收参数,封装对象。 String username = req.getParameter("username"); String password = req.getParameter("password"); Users inserUser = new Users(username,password); //2.调用业务逻辑 LoginService ls = new LoginServiceImpl(); Users user = ls.getUser(inserUser); //调用service层,传入参数,并返回用户对象 //3.根据Users对象决定返回什么页面 resp.setContentType("text/html;charset=utf-8"); PrintWriter writer = resp.getWriter(); //这里查用户名和密码,若返回的Users对象为空,说明没查到 if (user!=null){ //登录成功 writer.print("恭喜你 登录成功"); }else { //用户名或密码错误 writer.print("对不起 用户名或者密码错误"); } //注:文案上不要用太生硬的词,比如:操作非法,输入非法等。公司里有专门做文案的东西,若没有参考京东淘宝的文案 writer.flush(); writer.close(); } }
⑩.测试
现在用服务器去运行,右上角文本框选择Tomcat 8.5.34,重新部署,运行
弹出登录页面,输入jack abc123
页面会显示 恭喜你 登录成功
这里输入中文有问题,在LoginServlet类重写方法的最上面加:
req.setCharacterEncoding("utf-8");
(有啥问题,自己一定要测出来。被专门的测试人员测出来就是她涨钱,你扣钱)
10.登录成功执行用户服务和订单服务
在LoginServlet类中登录成功,改成如下代码:
if (user!=null){ //登录成功 HttpSession session = req.getSession(); session.setAttribute("loginUser",inserUser); writer.print("恭喜你 登录成功 请使用服务<br/><a href='user'>用户服务</a><a href='order'>订单服务</a>"); }else { //用户名或密码错误 writer.print("用户名或者密码错误 请重新<a href='/day2/loginPage.html'> 登录</a>"); }
相应的OrderServlet类中:
Users loginUser = (Users)session.getAttribute("loginUser");
可用学过的知识把相对路径拼出来:
if (user!=null){ //登录成功 HttpSession session = req.getSession(); session.setAttribute("loginUser",user); writer.print("恭喜你 登录成功 请使用服务<br/><a href='"+req.getContextPath()+"/user'>用户服务</a><a href='"+req.getContextPath()+"/order'>订单服务</a>"); }else { //用户名或密码错误 writer.print("用户名或者密码错误 请重新<a href='"+req.getContextPath()+"/loginPage.html'> 登录</a>"); }
11.对用户服务做一些功能,重新来一次以上流程
①.完善UserServlet
@WebServlet("/user") public class UserServlet extends HttpServlet { @Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { HttpSession session = req.getSession(); Users loginUser = (Users) session.getAttribute("loginUser"); resp.setContentType("text/html;charset=utf-8"); PrintWriter writer = resp.getWriter(); if(loginUser!=null){ writer.print("显示用户列表"); }else{ writer.print("对不起 请先<a href='"+req.getContextPath()+"/loginPage.html'> 登录</a>"); } writer.flush(); writer.close(); } }
(下面做显示用户列表的操作)
②.dao包新建UserDao接口
public interface UserDao { //查询用户列表,返回的数据需要List集合 List<Users> getAllUser(); }
③.dao.impl包下新建UserDaoImpl类:
该方法类似于登录
public class UserDaoImpl implements UserDao { @Override public List<Users> getAllUser() { Connection conn = DBHelper.getConn(); String sql = "select tu.user_id,tu.user_name,tu.user_pwd,tu.user_phone from tb_users tu "; PreparedStatement preparedStatement = null; ResultSet resultSet = null; List<Users> listuser = new ArrayList<Users>(); try { preparedStatement = conn.prepareStatement(sql); resultSet = preparedStatement.executeQuery(); while(resultSet.next()){ Integer userId = resultSet.getInt("user_id"); String userName = resultSet.getString("user_name"); String userPsw = resultSet.getString("user_pwd"); String userPhone = resultSet.getString("user_phone"); Users loginUser = new Users(userId,userName,userPsw,userPhone); listuser.add(loginUser); } } catch (SQLException throwables) { throwables.printStackTrace(); }finally { DBHelper.CloseConn(conn,null,preparedStatement,resultSet); } return listuser; } }
④.service包新建UserService接口:
public interface UserService { List<Users> getAllUser(); }
⑤.service.impl包下新建UserServiceImpl类:
public class UserServiceImpl implements UserService { @Override public List<Users> getAllUser() { UserDao ud = new UserDaoImpl(); List<Users> allUser = ud.getAllUser(); return allUser; } }
⑥.测试,在Mytest类中:
public class Mytest { public static void main(String[] args) { UserService us = new UserServiceImpl(); System.out.println(us.getAllUser()); } }
结果:[Users(userId=1, userName=jack, userPwd=abc123, userPhone=14434343434), Users(userId=2, userName=rose, userPwd=abc123, userPhone=13343454544), Users(userId=3, userName=小明, userPwd=abc123, userPhone=15545454444), Users(userId=4, userName=小白, userPwd=abc123, userPhone=14567654454), Users(userId=5, userName=小亮, userPwd=abc123, userPhone=16656565656)]
没有问题
⑦.回到UserServlet类中:
调用service层:
@WebServlet("/user") public class UserServlet extends HttpServlet { @Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { HttpSession session = req.getSession(); Users loginUser = (Users) session.getAttribute("loginUser"); resp.setContentType("text/html;charset=utf-8"); PrintWriter writer = resp.getWriter(); if(loginUser!=null){ writer.print("显示用户列表"); //调用service层 UserService us = new UserServiceImpl(); List<Users> allUser = us.getAllUser(); //查到用户列表,在页面应该用table展示 writer.print("<link rel=\"stylesheet\" type=\"text/css\" href=\"css/bootstrap.css\">"); writer.print("<table class='table'>"); writer.print("<tr>"); writer.print("<th>用户编号</th><th>用户名</th><th>密码</th><th>电话</th>"); writer.print("</tr>"); for (Users user:allUser){ writer.print("<tr>"); writer.print("<td>"+user.getUserId()+"</td>"); writer.print("<td>"+user.getUserName()+"</td>"); writer.print("<td>"+user.getUserPwd()+"</td>"); writer.print("<td>"+user.getUserPhone()+"</td>"); writer.print("</tr>"); } writer.print("</table>"); }else{ writer.print("对不起 请先<a href='"+req.getContextPath()+"/loginPage.html'> 登录</a>"); } writer.flush(); writer.close(); } }
运行,输入jack abc123,结果页面输出:
显示用户列表
用户编号 | 用户名 | 密码 | 电话 |
---|---|---|---|
1 | jack | abc123 | 14434343434 |
2 | rose | abc123 | 13343454544 |
3 | 小明 | abc123 | 15545454444 |
4 | 小白 | abc123 | 14567654454 |
5 | 小亮 | abc123 | 16656565656 |
⑧.美化table
bootstrap已经引入,将table标签引入
在⑦的基础上加一句,并在table标签将class属性设置为table
//查到用户列表,在页面应该用table展示 writer.print("<link rel=\"stylesheet\" type=\"text/css\" href=\"css/bootstrap.css\">"); writer.print("<table class='table'>");
⑨.总结
和后台没什么关系的页面,可使用静态页面,即在web包下新建类似myPage.html的文件。
和后台有关系的页面,即有数据关联的页面,可用后台servlet去画页面。
请求到达servlet,servlet做一些数据处理,剩下就是显示的逻辑,控制给用户展示什么样的页面。