JSP技术
一、为什么需要JSP技术
【存在问题】
原先仅使用Servlet开发的项目,需要利用Servlet的request对象的out.print方法将html标签输出到浏览器上,导致Servlet类中存在众多html代码。
简而言之,java程序中编写html代码存在以下问题:
- 编写难度大,麻烦
- 无报错提示
- 每次调试都需要编译java,生成class文件
- 代码耦合度过高
- 代码不美观
- 维护成本高
【解决方案】
-
思考:
能否让程序员只需编写Servlet程序中的“前端代码“,然后让机器将我们写的“前端代码”自动翻译生成“Servlet”这种“java”程序。再自动将“java程序”编译成“class”程序,再使用JVM调用这个class中的方法。
---jsp技术!
二、底层原理
(1)浏览器中访问index.jsp文件
实际上Tomcat会先将index.jsp文件翻译生成index_jsp.java文件,然后进行编译生成index_jsp.class文件,最后执行class文件,显示浏览器页面
(2)JSP实际上是一个Servlet
当index.jsp访问时,会自动翻译生成index_jsp.java,会自动编译生成index_jsp.class,那么index.jsp就是一个类。
inde_jsp继承HttpJspBase,而HttpJspBase类继承HttpSerlvet,所以inde_jsp就是一个Servlet类
(3)jsp生命周期与Servlet生命周期完全相同
-
两者是一个东西,无区别
-
两者都是单例的 (假单例)
-
【jsp生命周期】
- Servlet创建调用无参构造方法
- 调用init方法初始化
- 用户请求一次访问一次service方法
- 最后销毁时调用destory方法
(3)jsp第一次访问效率较低
- 为什么运维人员给客户演示时,都提前访问一遍jsp文件?
- 第一次:
- 要把jsp文件翻译成java源文件
- java源文件要编译生成class字节码文件
- 然后通过class创建servlet对象
- 调用servlet对象的init方法
- 调用servlet对象的service方法
- 第二次:
- 直接调用单例Servlet对象的service方法
三、JSP概述
3.1 什么是jsp
jsp是一种动态网页开发技术
①JSP是一套规范,所有的web容器/web服务器都遵循该规范进行的翻译。
-
Servlet是javaEE的13个规范之一,JSP也是规范之一
-
遵循不同规范时,jsp会按照不同的符号输出在jsp文件的不同位置
-
每个Web容器,所有的服务器都内置一个JSP翻译引擎
②同时JSP是java程序,本质仍是一个Servlet
Tomcat会将jsp文件变为对应的xx_jsp.java文件,编译为xx_jsp.class文件
- JSP是JavaServer Pages的缩写。
- 【基于java语言实现的服务器端的页面】
- 所以对jsp进行错误调试时,仍要打开jsp对应的java文件。检查java代码
③由jsp生成的java文件在哪?
可在Tomcat启动时,打开Tomcat控制台找到CATALINA_BASE,复制其后路径,在本机中访问找到
(开发jsp,最高境界:眼前是jsp,脑里中java代码)
3.2jsp基础语法
3.2.1 jsp输出原理
jsp文件中编写的内容,都会被自动翻译到对应的Java文件,从而编译生成.class文件
-
jsp文件中编写的文字
-
---翻译到servlet类的service方法的out.write("");中
-
类似于Servlet中的out.print("")
- 同时会被当做普通的字符串处理
- 同时jsp将html/css/js以字符串的形式输出到浏览器后,浏览器会对其进行解释执行,展现页面效果
-
不同的标签会生成在jsp(生成的java程序)的不同位置
3.2.2 jsp中文乱码
【存在问题】
当在jsp页面中输出中文,会发现页面出现中文乱码问题
【解决方案】
可利用jsp的page指令解决,响应的乱码问题
- 步骤:在jsp文件上方,添加以下代码设置响应内容类型为UTF-8
<%@page contentType="text/html;charset=UTF-8" %> <%--表示响应的内容是text/html,采用的字符集为UTF-8--%>
-
原理:
添加该指令后,jsp对应的java文件中代码就会相应更改
3.2.3编写java代码
(1)<%%>--翻译到service方法里
①<%xxx%>翻译到哪?
- xxx会被翻译到其jsp对应的java类的service方法中
②<%xxx%>翻译成了什么?
- 翻译成: service方法中的一条条语句
③<%%>什么时候用?
用于在jsp页面中编写java语句
-
当jsp中不添加特殊标记的内容,默认输出为字符串到service方法的out.write("")中
-
当想在jsp页面中编写java语句,应采用<%%>
<%java语句;%>
- 该符号中编写的被视作java程序,会被翻译到Servlet类的service内部,而非out.print("")当中
④注意:
- ❗<%%>写代码,就类似于在service方法体写代码!!
- service方法不能写静态代码块、不能写方法、不能定义成员变量
- 同时编写的代码要符合编译顺序,因为就相当于将jsp<%%>中的语句,写入其对应java的service方法里
- eg: private int i=0;因为方法中只允许有私有化变量/可以写int i =0
(2)<%!%>--翻译到service方法外
①<%!%>翻译到哪里?
- 翻译到jsp文件生成的java文件的service方法外面
②<%!%>翻译成什么?
- 翻译成service方法外的一条条语句
③<%!%>什么时候用?
- 当想将java代码输出jsp对应java程序的service方法外,可以使用<%!%>
- 使用较少
- 因为在service方法中写静态变量和实例变量,都会存在线程安全问题,因为jsp就是servlet,而servlet是单例的,多线程并发的环境下,该静态变量和实例变量一旦有修改操作,必然会存在线程安全问题
- 使用较少
<%!java代码; %>
【注意】同时使用<%①%>和<%!②%>时,java程序中②在上service方法外,①在下service方法里
(3)<%=%>--翻译到service方法out.print()里
①<%=xxxx%>会被翻译到哪?
- 其中的xxx会被翻译到jsp其对应java代码的service方法的out.print()中
②<%=xxx%>会被翻译成什么?
- 翻译成: out.print()
<%=100+200%> <%--被翻译为out.print(100+200) --%>
③<%=xx%>什么时候用?
- 当输出的是一个动态变化的内容/含有java变量,使用<%=%>输出
<% int a = 5; int b = 4; int i =a+b; %> <%=i%>
3.2.5 jsp的注释
-
jsp中的专业注释为:
<%-- jsp专业注释,不会被翻译到java源代码中 --%>
-
html注释为
<!--html注释不专业,仍会被翻译到java源代码中-->
3.2.6 jsp输出语句
要向浏览器输出一个java变量,可使用内置对象out的write("")方法
- jsp代码如下:
- 此处的out为JSP的九大内置对象之一,可直接在service内部使用。
- 内置对象,只能在Servlet生成的java程序的service方法中使用哦!
- 九大内置对象:pageContext、session、application、config、out、page、request、reponse、Exception
- 此处的out为JSP的九大内置对象之一,可直接在service内部使用。
<% String name = "jack";out.write("name="+name);%>
- 注意:
①固定的内容输出,直接写在jsp文件中即可,不必写到<%%>中,因为jsp文件的内容,本身就会变为ou.write,然后输出到浏览中
②输出的内容若有变量,则利用<%java代码%>的形式展现
③输出内容有java代码,使用以下语法格式:
- <%=%> 在=号后编写要输出的内容
始终记得:jsp就是servlet,只是职责不同。
3.3jsp和Servlet的区别
jsp的本质就是一个Servlet,但两者职责不同
- Servlet职责是:收集数据
- 其强项在于逻辑处理,业务处理,连接数据库,获取/收集数据
- jsp职责是:展示数据
- 其强项在于做数据的展示
3.4总结
jsp语法总结:
(1)jsp中直接写代码
- 翻译到哪?
翻译到jsp生成的java文件serice()方法的out.write("")中
- 翻译成什么?
翻译成:out.write("");
- 何时使用?
当输出静态的字符串到浏览器时
如:html标签,不变的标题文字等等
(2)<%%>
- 翻译到哪?
翻译到jsp生成的java文件service()方法体中
- 翻译成什么?
一条条语句,写什么翻译为什么,只是放在service()方法体中
- 何时使用?
要在service()方法体中定义java代码时
(3)<%!%>
- 翻译到哪?
翻译到jsp生成的java文件service方法体外
- 翻译成什么?
一条条代码语句,写什么翻译为什么,只是放在service()方法体外
- 何时使用?
要在service()方法体外定义java代码时
(4)<%=%>
- 翻译到哪?
翻译到jsp生成的java文件的service方法中的out.print()中
- 翻译成什么?
翻译成: out.print();
- 何时使用?
要输出动态的内容
(5)<%@ %>
<%@page contentType="text/html;charset=UTF-8"%>
- 翻译成什么?
为page指令,通过contentType属性用来设置响应的内容类型
主要用于解决防止乱码
四、利用Servlet+JSP改造纯Servlet的oa项目
4.1未改造前代码
原先未改造的代码,主要利用纯servlet编写,弊端是:
- 在java后端代码中需要使用很多out.print语句去输出html标签
- 前后端代码耦合性差
- 代码繁琐
- 等....
4.2改造后代码
采用jsp技术,去代替servlet展示html代码,好处是:
- 前后端代码编写分离
- jsp会帮助我们将jsp页面中的html代码响应给浏览器
- 简化代码
- 省去了许多out.print语句
①创建一个新java工程,右键工程->Add Framework support添加web框架支持
②File->Project Structure->Modules ->选中项目->Dependencies添加servlet,jsp的jar
③Add configurations->添加Tomcat,并进行配置->Deployment 设置项目访问路径
④在工程的web->WEB-INF目录下,新建lib包用于存放数据库连接的jar包
⑤在src中创建utils包,放置DBUtils工具类,用于连接数据库
⑥正式开始编码
-
首先将所有html文件,变为.jsp文件
-
同时为防止中文乱码,在文件首行添加<%@page contentType="text/html;charset=UTF-8"%>语句
-
更改原先所有的html静态路径,变为jsp静态路径
-
注意页面的路径,需要携带/项目名
-
因为,当前端发送请求路径是绝对路径时,要以"/"开始,加项目名
-
同时项目名最好不要写死,在此jsp提供了<%=requet.getContextPat()%>解决这个问题
-
<a href="<%=request.getContextPath()%>/list.jsp">查看</a>
-
-
-
实现页面的正常流转
⑦编码思想,由servlet获取处理数据,jsp去展示
(1)action包下: 编写servlet代码,用于获取数据
- 1️⃣为简化servlet请求路径配置,采用@WebServlet的servlet注解式开发
@WebServlet({"请求路径,不带项目名"}) -写在类名上,表示该类中所有方法的请求路径前缀为/xx/xx
- 2️⃣为防止一个表增删改查要写多个servlet类,采用将每个Servlet类写成方法的模板方法设计模式开发
servlet中直接重写service方法 -request.getSerlvetPath();//得到servlet请求路径 -if结合"".equals(path);//判断请求路径为xx -调用对应的doXxx方法
- 3️⃣利用实体类的思想,存储查询的各字段数据
⑧编写serlvet代码
- 获取数据,进行处理,跳转至页面
DeptServlet.java代码
- 将数据库获取的数据存储在javabean中,然后形成数据链表,放入请求域
- 最后跳转至jsp页面
@WebServlet({"/dept/list","/dept/detail","/dept/delete","/dept/add","/dept/update"}) public class DeptServlet extends HttpServlet { @Override protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String servletPath = request.getServletPath(); if("/dept/list".equals(servletPath)){ doList(request,response); }else if("/dept/detail".equals(servletPath)){ doDetail(request,response); }else if("/dept/delete".equals(servletPath)){ doDel(request,response); }else if("/dept/add".equals(servletPath)){ doAdd(request,response); }else if("/dept/update".equals(servletPath)){ doUpdate(request,response); } } /** * 连接数据库,查询所有部门信息,再跳转至jsp做页面展示 * @param request * @param response * @throws ServletException * @throws IOException */ private void doList(HttpServletRequest request,HttpServletResponse response) throws ServletException,IOException{ //1.连接数据库 Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; List<Dept> deptList = new ArrayList<>(); try { //2.获取连接 conn = DBUtils.getConnection(); //3.执行查询语句 String sql = "select deptno,dname,loc from dept"; ps = conn.prepareStatement(sql); rs = ps.executeQuery(); while(rs.next()){ String deptno = rs.getString("deptno"); String dname = rs.getString("dname"); String loc = rs.getString("loc"); //创建一个表实体类对象,将数据放入 Dept dept = new Dept(); dept.setDeptno(deptno); dept.setDname(dname); dept.setLoc(loc); //将存放数据的实体类,存入链表 deptList.add(dept); } } catch (SQLException e) { e.printStackTrace(); }finally { //4.释放资源 DBUtils.close(conn,ps,rs); } //将最后的链表数据存入请求域中 request.setAttribute("deptList",deptList); //转发,注意不可重定向,否则无法共享请求域数据 request.getRequestDispatcher("/list.jsp").forward(request,response); } private void doDetail(HttpServletRequest request,HttpServletResponse response)throws ServletException,IOException{ //从请求域中获取deptno String deptno = request.getParameter("deptno"); //获取数据库连接 Connection conn = null; ResultSet rs = null; PreparedStatement ps= null; Dept dept = new Dept(); try { conn = DBUtils.getConnection(); String sql = "select dname,loc,principal from dept where deptno = ?"; ps = conn.prepareStatement(sql); ps.setString(1,deptno); rs = ps.executeQuery(); //结果集只有一条数据,不必循环 if(rs.next()){ String dname = rs.getString("dname"); String loc = rs.getString("loc"); String principal = rs.getString("principal"); dept.setDeptno(deptno); dept.setDname(dname); dept.setLoc(loc); dept.setPrincipal(principal); } } catch (SQLException e) { e.printStackTrace(); }finally { DBUtils.close(conn,ps,rs); } request.setAttribute("dept",dept); String f = request.getParameter("f"); if(f.equals("m")){//转发到修改页面 request.getRequestDispatcher("/edit.jsp").forward(request,response); }else if(f.equals("d")){//转发到详情页面 request.getRequestDispatcher("/detail.jsp").forward(request,response); } } private void doDel(HttpServletRequest request,HttpServletResponse response)throws ServletException,IOException { String deptno = request.getParameter("deptno"); Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; int count = 0; try { conn = DBUtils.getConnection(); String sql = "delete from dept where deptno = ?"; ps = conn.prepareStatement(sql); ps.setString(1, deptno); count = ps.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); } finally { DBUtils.close(conn, ps, rs); } if (count == 1) {//删除成功,重定向到列表页面 //要加项目名,因为此时是重新发起一次请求 response.sendRedirect(request.getContextPath() + "/dept/list"); } else {//删除失败 response.sendRedirect(request.getContextPath() + "/error.jsp"); } } private void doAdd(HttpServletRequest request,HttpServletResponse response)throws ServletException,IOException{ request.setCharacterEncoding("UTF-8"); String deptno = request.getParameter("deptno"); String dname = request.getParameter("dname"); String loc = request.getParameter("loc"); String principal = request.getParameter("principal"); Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; int i = 0; try { conn = DBUtils.getConnection(); String sql = "insert into dept(deptno,dname,loc,principal) values (?,?,?,?)"; ps = conn.prepareStatement(sql); ps.setString(1,deptno); ps.setString(2,dname); ps.setString(3,loc); ps.setString(4,principal); i = ps.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); }finally { DBUtils.close(conn,ps,rs); } if(i == 1){//添加成功 response.sendRedirect(request.getContextPath() +"/dept/list"); }else { response.sendRedirect(request.getContextPath() +"/error.jsp"); } } private void doUpdate(HttpServletRequest request,HttpServletResponse response)throws IOException,ServletException{ request.setCharacterEncoding("UTF-8"); String deptno = request.getParameter("deptno"); String dname = request.getParameter("dname"); String loc = request.getParameter("loc"); String principal = request.getParameter("principal"); Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; int i = 0; try { conn = DBUtils.getConnection(); String sql = "update dept set dname=?,loc=?,principal=? where deptno = ?"; ps = conn.prepareStatement(sql); ps.setString(4,deptno); ps.setString(1,dname); ps.setString(2,loc); ps.setString(3,principal); i = ps.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); }finally { DBUtils.close(conn,ps,rs); } if(i == 1){//添加成功 response.sendRedirect(request.getContextPath() +"/dept/list"); }else { response.sendRedirect(request.getContextPath() +"/error.jsp"); } } }
⑨在页面中通过请求域,获取动态展示的数据
- 利用<%%>即编写java语句至service方法,通过srequest请求域,获取存储的List数据
- 利用<%=%>即out.write,循环将数据输出到浏览器中
- 获取servlet发给页面的数据,request.getAttribute、request.setAttribute
- 获取页面发给servlet的数据,request.getParameter
<table border="1px" align="center" width="50%"> <tr> <th>序号</th> <th>部门编号</th> <th>部门名字</th> <th>部门地址</th> <th>操作</th> </tr> <% int index = 1; List<Dept> deptList = (List<Dept>) request.getAttribute("deptList"); for(Dept dept :deptList){ %> <tr> <td><%=index++%></td> <td><%=dept.getDeptno()%></td> <td><%=dept.getDname()%></td> <td><%=dept.getLoc()%>></td> <td> <a href="javascript:void(0)" οnclick="window.confirm('确认删除该部门信息吗?')">删除部门</a> <a href="<%=request.getContextPath()%>/edit.jsp">修改部门</a> <a href="<%=request.getContextPath()%>/detail.jsp">查看详情</a> </td> </tr> <%}%>
【拓展】:
-
如何清除服务器缓存
ctrl+shift+delete
-
可否只用jsp独立开发web应用?
仅用jsp也可以独立开发web应用,因其本质就是servlet,但是不推荐,会使代码解耦性变差
因为servlet用于数据获取及处理,jsp用于数据展示,jsp中的java代码越少越好
- jsp文件的扩展名必须是.jsp么?
并非一定是jsp,可通过在Tomcat目录/conf/web.xml文件中进行配置即可
因为对于Tomcat而言,jsp就是一个普通的文本文件,web容器会将生成jsp_serlvet.java文件,再编译成class,最后运行调用java对象相关方法,真正执行与jsp文件无关。
- 之后还可以学到什么?
- Serlvet+jsp
- session
- cookie
- EL表达式
- jstl标签
- Filter
- Listener
- Ajax
- jQuery
- Vue
- mvc架构模式
- 连接池
- SSM
- maven
- git
- Serlvet+jsp
页面代码如下:
- 在此以list.jsp为例,代码如下:
- 注意使用的Dept是自定义的实体类,使用时要导包
<%@ page import="java.util.List" %> <%@ page import="com.bean.Dept" %> <%@page contentType="text/html;charset=UTF-8"%> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>列表页面</title> </head> <body> <h1 align="center">部门列表</h1> <hr> <table border="1px" align="center" width="50%"> <tr> <th>序号</th> <th>部门编号</th> <th>部门名字</th> <th>部门地址</th> <th>操作</th> </tr> <% int index = 1; List<Dept> deptList = (List<Dept>) request.getAttribute("deptList"); for(Dept dept :deptList){ %> <tr> <td><%=index++%></td> <td><%=dept.getDeptno()%></td> <td><%=dept.getDname()%></td> <td><%=dept.getLoc()%></td> <td> <a href="javascript:void(0)" οnclick="del(<%=dept.getDeptno()%>)">删除部门</a> <a href="<%=request.getContextPath()%>/dept/detail?f=m&deptno=<%=dept.getDeptno()%>">修改部门</a> <a href="<%=request.getContextPath()%>/dept/detail?f=d&deptno=<%=dept.getDeptno()%>">查看详情</a> </td> </tr> <%}%> </table> <hr> <a href="<%=request.getContextPath()%>/add.jsp">新增部门</a> <a href="javascript:void(0)" οnclick="del(no)" >删除</a> <script type="text/javascript"> function del(no){ if(window.confirm("亲,删了不可恢复哦!")){ document.location.href = "<%=request.getContextPath()%>/dept/delete?deptno=" + no; } } </script> </body> </html>
五、实现登录功能
5.1不带Session
①步骤一:创建user表
- user表包括登录用户名+登录密码
- 同时密码采用密文存储(MD5加密)
②步骤二:编写登录页面
- 页面含有用户名和密码的输入框
- 点击登录,提交表单,提交用户名和密码,利用post提交方式
③步骤三:编写对应的Servlet处理登录请求
- 登录成功:跳转部门列表
- 登录失败:跳转失败页面
④步骤四:编写提供一个失败页面
【存在问题】:即便用户不登录,但若用户知晓请求路径,即可通过路径跨过登录页面,直接访问部门列表页
【解决方法】:利用session判断其是否登录
5.2带session
要解决用户不登录就可通过部门列表请求路径访问这个问题,可以将用户登录时的信息存储到session中,即当session中存在用户信息,则表示已登录。未存在用户信息则表示未登录
①登录成功时,获取Session对象,同时将登录信息存储在其中。
- 此时必须要获取到session用于存储登录信息
if(flag){ //获取session对象,没有则创建 HttpSession session = request.getSession(); session.setAttribute("username",username); //跳转到部门列表页面 response.sendRedirect(request.getContextPath()+"/dept/list"); }else { //跳转失败页面 response.sendRedirect(request.getContextPath()+"/error.jsp"); }
②在servlet中(跳转部门列表的请求中),判断是否取得当前session,获取到表示已登录,获取不到表示未登录
@Override protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { //此处不一定要获取到Session对象,有表示曾登录过,无表示未登录过 HttpSession session = request.getSession(false); //获取session对象中的username属性 String username = (String)session.getAttribute("username"); if(session!=null && username!=null){ String servletPath = request.getServletPath(); if("/dept/list".equals(servletPath)){ doList(request,response); }else if("/dept/detail".equals(servletPath)){ doDetail(request,response); }else if("/dept/delete".equals(servletPath)){ doDel(request,response); }else if("/dept/add".equals(servletPath)){ doAdd(request,response); }else if("/dept/update".equals(servletPath)){ doUpdate(request,response); } }else { //跳转到登录页面,request.getContextPath()为项目路径 response.sendRedirect(request.getContextPath()); } }
❗访问jsp也会生成session对象
在访问了jsp页面,就会创建session对象,因为jsp的九大内置对象之一,就有session。如果访问jsp时,不想升生成session,只需在jsp页面中添加以下配置指令:
<%@page session="false"%>
六、登录后显示用户名
登录后,如果要在jsp页面中显示用户名,只需要利用jsp内置对象的session对象,取出username属性即可
- 注意使用jsp的session对象时,要保证其存在
<h1>欢迎<%=session.getAttribute("username")%></h1>
七、安全退出功能
安全退出指将当前的Sesiion对象,进行手动销毁
核心servlet代码如下:
//获取session对象,不需要创建新的,因而为false HttpSession session = request.getSession(false); //如果存在session if(session != null){ //将其手动销毁 session.invalidate(); //跳转至登录页面 response.sendRedirect(request.getContextPath()+'/login.jsp'); }
八、jsp的指令
8.1 指令的作用
指令的作用:指导当前jsp的翻译引擎如何进行工作
- 即如何翻译
8.2 常用指令
①include指令
- 包含指令,用于在jsp中完成静态包含,目前很少被使用
②taglib指令
- 引入标签库指令,多在JSTL标签库中使用
③page指令
- 页面指令,用于定义JSP页面的全局属性
8.3指令的使用语法
<%@指令名 属性名=属性值 属性名=属性值....%>
8.4常用的page指令
①session属性
- 是否启用session对象
<%@page session="true|false"%> true表示启用jsp的内置对象Session,若无session对象,则创建 false表示不启用jsp的内置对象Session,则当前jsp页面中无法使用内置对象session 若未设置,默认true
②contentType属性
- 用于设置响应的内容类型
<%@page contentType="text/json"%> <!--contentType用于设置内容的响应类型,相当于response.setContentType("text/json");--> <%@page contentType="text/json;charset=utf-8"%> <!--在设置响应内容类型的同时,也设置响应时采用的字符集-->
③pageEncoding属性
- 设置响应时采用的字符集
- 常和pageEncoding搭配使用,也可直接在contentType中直接写 contentType="text/json;charset=UTF-8"
<%@page contentType="text/json" pageEncoding="UTF-8"%> <!--相当于response.setConteneType("text/json;charset=UTF-8")-->
④import属性
- 用于导包,导入后该jsp页面可使用包的方法
- 导多个包,用逗号( , )分隔
<%@page import="java.util.list, java.util.Date"%> <!-- import属性用于导包-->
⑤errorPage属性
- 用于配置出错页面
- 当前jsp页面出错后,就会跳转到配置的出错页面上
- 路径地址从web根目录开始
<%@page errorPage="/路径地址" %>
【存在问题】:
当设置出错页面时,页面出错会跳转至错误页面,但此刻浏览器和控制台并不会输出错误信息,不利于程序员维护
【解决方法】:
可以启用页面的exception内置对象,其表示刚刚发生的异常对象
⑥isErrorPage属性
- 设置该页面exception对象
- exception为jsp的九大内置对象之一
- true表示启用
- false表示不启用
- 默认为false
- 常与errorPage属性一起搭配,用于设置的出错页面
<%@page isErrorPage="true"%> <!--启用exception对象--> <% //利用以下语句,打印异常堆栈信息,输出到后台控制台 exception.printStackTrace(); %>
九、九大内置对象
[注:我使用的是jdk17,因而javax.servlet更名为jakar.servlet]
9.1 pageContext
-
【页面作用域】
-
包名 jakarta.servlet.jsp.PageContext pageContext
9.2 request
- 【请求作用域】
- 包名 jakarta.servlet.http.HttpServletRequest request
9.3 session
- 【会话作用域】
- 包名 jakarta.servlet.http.HttpSession session
9.4 application
- 【应用作用域】
- 包名 jakarta.servlet.ServletContext application
-----------------------------以上是四个作用域对象-----------------------------
1️⃣作用域的大小: pageContext<request<session<application
2️⃣四个作用域都有 :setAttribute、getAttribute、removeAttribute方法
3️⃣使用原则:尽可能使用小的域
----------------------------------------结束-----------------------------------------------
9.5 exception
- 【异常对象】
- 包名 java.lang.Throwable exception
9.6 config
- 【配置对象】
- 即ServletConfig ,表示web.xml文件的配置
- 包名 jakarta.servlet.ServletConfig config
9.7 page
-
【当前页面对象=this】
- 其实是this,表示当前的servlet对象
-
包名 java.lang.Object page
9.8 response
-
【响应对象】
- 负责响应
-
包名 jakarta.servlet.http.HttpServletResponse response
9.9 out
-
【输出对象】
- 负责输出
-
包名 jakarta.servlet.jsp.JspWriter out
Session机制和Cookie
关于B/S结构的会话机制称为session机制
一、什么是会话session?
-
会话对应的英文是:session
- 而session机制其实是一种规范,而不同的语言会对其进行不同的实现
-
一次会话:用户打开浏览器,进行一系列操作,最终关闭浏览器,整个过程称为“一次会话”。
- 请求的服务器端的java对象是:session
- 一个会话会包含多次请求(一次会话对应N次请求)
- 一个会话对应一个Session对象
-
【回顾】**一次请求:**用户在浏览器进行一次点击(超链接/按钮),然后跳转,可以粗略认为是“一次请求”。
- 请求的服务器端的java对象是:request
- 一次请求对应一个request对象
java和session都是服务器端的java对象,都在JVM中
二、Session作用
①session主要作用:保存会话信息
- 如:用户登录成功,该如何将登陆成功的状态一直保存下来,就应该使用session对象保存
②为什么需要session对象保存会话状态?
-
因为Http是一种无状态协议
- 无状态:请求时,B和S是连接的,但请求结束后,连接就会中断
- 即请求瞬间是连接,请求结束后连接断开,大大减轻服务器压力
- 因而就需要一个对象去存储状态,即session会话对象
- 无状态:请求时,B和S是连接的,但请求结束后,连接就会中断
-
java的servlet规范中,session对应的类名是:
- jarkata.servlet.http.HttpSession【jdk17】
- javax.servlet.http.HttpSession【jdk17以下】
三、为什么用Session保存会话状态?
request请求域(HttpServletRequest)< session会话域(HttpSession) < application应用域(ServletContext)
以上三者都具有getAttribute()【取】和setAttribute()【存】方法
但request作用域和ServletContext应用域,
- reuquest是一次请求一个对象
- ServletContext是服务器启动时创建对象,服务器关闭时销毁对象,该ServletContext对象只有一个
因而request域太小,ServletContext域太大,最适合的还是session
四、session对象的使用
①获取session对象代码如下:
//从WEB服务器中获取当前session对象,如果未获取session对象,则新建 HttpSession session = request.getSession(); //从Web服务中获取当前session对象,如果未获取到session对象,不新建则返回null HttpSession sesssion = request.getSession(false);
②存储数据,获取数据
session.setAttribute("存储名",存储值名); session.getAttribute("设置取到值的名",取值名);
五、session对象的实现原理
5.1实现原理
1️⃣在web服务器中有一个session列表。
类似于map集合:
该map集合的key存储的是sessionid
该map集合的value存储的是session对象
**2️⃣当用户打开浏览器,第一次发起请求时:**服务器会创建一个新的session对象,同时为session对象生成一个session的id,然后web服务器会将session的id发送到浏览器,浏览器再将session的id保存到浏览器缓存中。
**3️⃣用户第二次发起请求时:**会自动将浏览器缓存中的sessionid自动发送给web服务器,服务器获取sessionid,根据sessionid查找其对应的session对象。
用户第N次发起请求,同上.....
4️⃣当用户关闭浏览器后,浏览器缓存中保存的sessionid会消失,下次重新打开浏览器后,缓存中找不到sessionid,就自然找不到web服务器中sessionid对应的session对象,而找不到session对象等同于会话结束,web服务器就会重新再创建一个session对象,生成其对应的sessionid,然后存到浏览器缓存中....
6️⃣同时当会话结束,但其对应的session对象并非立刻销毁,因为Http是无状态协议,所以Tomcat会根据会话超时机制,在某个时间后,若用户仍未请求Tomcat中的该session会话对象时,就将为该用户创建的该session对象给销毁。
而其中的sessionid是以cookie的形式存储
其存储在浏览器的内存中
7️⃣浏览器关闭会话一定结束么?
不一定,通常情况下,关闭浏览器后,缓存中的sessionid会消失,即访问web服务器就找不到原先对应的session对象。但有时当浏览器打开一定时间,超过一定时间没有进行操作,虽然并没有关闭浏览器,但根据session的超时机制,它也会销毁其对应的session对象。同时还有手动配置超时时长的情况。
8️⃣手动设置超时时长
如果想要手动设置session超时机制的时长,可在web.xml添加以下代码:
<!--在web.xml添加以下配置,表示设置session的超时机制中,超时时长为30分钟--> <session-config> <session-timeout>30</session-timeout> </session-config>
5.2注意
①session对象存储在服务器端,由服务器创建,并销毁
②一个session对象一个会话,浏览器打开->关闭
③一次会话内,包含多个请求
如:南京网友多次请求服务器,获取的都是同一个session1对象,前提是在一次会话内
若:第一次请求服务器,不存在session对象,服务器会为其创建一个新的session对象
而:原先的旧session对象并不会马上销毁,而是根据超时机制:当一定时间内,用户仍未访问该session对象,则销毁
④获取当前session对象时,有两种情况
-
未获取到,则新建
HttpSession session = request.getSession();
-
未获取到,则返回null
HttpSession session = request.getSession(false);
5.3流程图解【一个会话中】
①当用户第一次发起请求时,不存在sessionid,因而服务器为其创建一个session对象,将sessionid响应给浏览器,放入缓存
- 其中的Jsessionid是以cookie的形式存储
②当用户第N次发起请求时,浏览器缓存以存在sessionid,会请求服务器,获取sessionid对应的session对象
六、Cookie
6.1 Cookie概念
在session的实现原理中,每个session对象都会关联一个sessionid
不同的路径所提交的cookie不同哦!
- 如:JSESSION=41C481F208EF1234578
- 而以上的键值对数据,就是cookie
- 对于session关联的cookie而言,该cookie保存在浏览器的“运行内存”中
- 只要浏览器不关闭,用户再次发起请求时,就会自动将运行内存的cookie发送给服务器
- 而服务器再根据cookie去找到对应的session对象
6.2 cookie组成
http协议中规定,任何一个cookie都是由name和value组成的,且都是字符串类型。
- java的servlet中,对cookie提供了哪些支持?
- 一个Cookie类专门表示cookie数据
- jarkata.servlet.Cookie;【jdk17】
- javax.servlet.Cookie;【jdk17以下】
- 一个Cookie类专门表示cookie数据
- java怎么把cookie发送给浏览器呢?
- 利用response.addCookie(cookie)方法
6.3Cookie保存在何处
Cookie最终保存在浏览器客户端上
- 也可保存在运行内存中(浏览器关闭则消失)
- 也可保存在硬盘文件中(永久保存)
6.4Cookie的作用
cookie和session机制都是为了保存会话状态
- cookie是将会话的状态保存在浏览器客户端上 (cookie数据存储在浏览器客户端上)
- session是将会话的状态保存在服务器端上 (session数据存储在服务器上)
cookie和session存在的意义?因为http协议是无状态 无连接协议。
【经典案例】:京东商城,在未登录情况下,仍可以添加商品至购物车,而下次访问购物车商品仍存在
1️⃣做法:
- 将购物车中的商品编号放到cookie中,cookie保存在硬盘文件中,即使关闭浏览器。硬盘上cookie还在,再次打开京东商城时,查看购物车,会自动读取本地硬盘中存储的cookie,拿到商品编号,并动态显示。
- 京东存储购物车的cookie可能如下:productIds = xxx.yyy.zzz 其中为一个个商品编号
- 注意:如果在浏览器cookie清除掉,是清除对应硬盘的cookie数据,购物车的商品就会消失
【经典案例】:126邮箱的,十天内免登陆
1️⃣做法:
- 当勾选[十天内免登陆] 登陆成功后,浏览器客户端会保存一个cookie,该cookie会保存将登陆时的用户名和密码信息,保存到本地的硬盘文件中,十天有效,当第二次访问该地址,浏览器自动直接从硬盘文件中取得对应cookie发送给浏览器,浏览器接收到cookie后,获取用户名和密码,自动进行登录
6.5cookie属于谁
cookie机制和session机制,属于http协议的一部分,php开发中也有cookie和session机制,只要做web开发,都会使用到。
6.5浏览器何时发送Cookie
http协议中规定:当发送请求时,会自动携带该path下的cookie数据给服务器
6.6发送Cookie给服务器
利用response对象的addCookie()方法,设置不同的关联路径(URL),选择在哪些路径中携带cookie数据,最后发送给服务器
①关于cookie的关联路径:
-
若此时发送的请求路径为:http://localhost:8080/servlet/cookie/generate,由此生成了cookie
-
若cookie没有手动设置path,默认path是什么?
- 为:http://localhost:8080/servlet 及其子路径,满足其条件cookie就会自动发送到服务器中
-
**若cookie手动设置了path,**则为设置的路径及其子路径,都会自动发送cookie到服务器
-
cookie.setPath("/xxx");
-
-
【案例】将cookie从服务器端发送到客户端上
- 主要实现服务器生成cookie,然后将cookie响应给浏览器,浏览器接收cookie,将cookie展示在客户端上
①编写jsp页面
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>$Title$</title> </head> <body> <a href="<%=request.getContextPath()%>/cookie/generate">服务器生成cookie,然后cookie响应给浏览器,浏览器接受cookie,再将cookie放到客户端中</a> </body> </html>
②编写对应的servlet代码
@WebServlet({"/cookie/generate"}) public class GenerateCookie extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { //创建Cookie对象 Cookie cookie = new Cookie("productid","1234567891245"); //设置cookie的失效时间 cookie.setMaxAge(30); //设置cookie的关联路径 cookie.setPat(request.getContextPath()); //将Cookie响应到浏览器上 response.addCookie(cookie); } }
③访问编写好的servlet请求路径即可
6.7服务器接收Cookie
**接收时,利用request对象的getCookie方法接收**该路径携带的cookie数据,返回类型为String[]
①接收cookie
- 返回值为string[],若浏览器未提交cookie,返回null
//通过request对象,接收浏览器发来的cookie Cookie[] cookies = request.getCookies();
【案例】通过java程序接收浏览器发送过来的cookie
①编写页面,跳转至servlet
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>$Title$</title> </head> <body> <a href="<%=request.getContextPath()%>/cookie/send">接收浏览器发来的cookie,在java中接收,同时遍历cookie数据</a> </body> </html>
②编写servlet,通过request对象的方法接收发来的cookie,同时进行遍历
@WebServlet({"/cookie/send"}) public class SendCookie extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { //获取浏览器发送到服务器的cookie数据 //注意:如果浏览器没有提交cookie,则返回null Cookie[] cookies = request.getCookies(); //如果不是null,则遍历取出cookie if(cookies != null){ for (Cookie cookie : cookies){ String name = cookie.getName(); String value = cookie.getValue(); System.out.println(name+"+"+value ); } } } }
③访问路径,再查看控制台信息
6.8Cookie有效时间
①设置cookie的有效时间
cookie.setMaxAge()
-
未设置cookie的有效时间:默认保存在浏览器的运行内存中(浏览器关闭则消失)
-
若设置cookie的有效时间>0
- 表示一定会保存在硬盘文件中
-
若设置cookie的有效时间=0
-
表示该cookie被删除
-
使用场景:多用于删除同名cookie
-
-
若设置cookie的有效时间<0
- 表示该cookie不会被存储
- 仅不会被存储到硬盘文件中,会放在浏览器运行内存中
- 和不调研serMaxAge()一个效果
- 表示该cookie不会被存储
②创建cookie
Cookie cookie = new Cookie("存储在cookie的数据名",存储的数据);
6.9实现十天内免登陆功能
6.9.1效果图
类似于126邮箱的30天免登陆
6.9.2思路
①首先实现基本的登录功能
- 登录成功
- 跳转部门列表页面
- 登录失败
- 跳转登录失败页面
②修改登录页面,添加勾选复选框[十天内免登陆]
<form action="<%=request.getContextPath()%>/user/login" method="post"> 用户名<input type="text" name="username" value="" > <br> 密码<input type="text" name="password" value=""> <br> <input type="checkbox" name="remember" value="1">十天内免登陆<br> <button type="submit" >登录</button> </form>
③修改Servlet中的login方法
- 若勾选
- 则在登录时,创建cookie
- 同时将用户名和密码进行存储,同时设置过期时间为10天
- 同时设置cookie的关联路径,使其一访问该路径,就自动携带设置的cookie数据
- 最后将cookie响应给浏览器
- 浏览器就会将其自动保存到硬盘文件当中10天
- 若未勾选,则正常登录跳转
if(flag){ //获取Session HttpSession session = request.getSession(); session.setAttribute("username",username); String remember = request.getParameter("remember"); if("1".equals(remember) ){//如果用户点击十天免登陆 //创建Cookie对象存储用户名,密码 Cookie cookie1 = new Cookie("username",username); Cookie cookie2 = new Cookie("password",password);//真实情况需要加密 // 设置失效时间 十天 cookie1.setMaxAge(60*60*24*10); cookie2.setMaxAge(60*60*24*10); //设置关联路径 cookie1.setPath(request.getContextPath()); cookie2.setPath(request.getContextPath()); //响应cookie给浏览器 response.addCookie(cookie1); response.addCookie(cookie2); } //跳转到部门列表页面 response.sendRedirect(request.getContextPath()+"/dept/list"); }else { //跳转失败页面 response.sendRedirect(request.getContextPath()+"/error.jsp"); }
④在web.xml配置文件中,设置欢迎页,跳转到servlet请求中
<!-- 会访问welcome,不用添加/,直接从根路径开始--> <welcome-file-list> <welcome-file>welcome</welcome-file> </welcome-file-list>
⑤编写WelcomeServlet
-
用户再次访问时:
- 获取cookie,遍历得到其中的用户名username和密码passwrod两个cookie值
- 若都取到了,进行登录的操作,若成功->跳转部门列表页面
- 若未取到,说明之前并未勾选[十天免登陆]/cookie有效期到,跳转登录页面
-
要么跳转部门列表页面
-
要么跳转登录页面
@WebServlet({"/welcome"}) public class WelcomeServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { //获取Cookie,不可能size=0,只可能size>0,或为null Cookie[] cookies = request.getCookies(); String username = null; String password = null; //如果获取的cookie不为空 if(cookies != null){ //循环遍历cookie,得到username和password for(Cookie cookie : cookies){ String name = cookie.getName(); if("username".equals(name)){ username = cookie.getValue(); }else if("password".equals(name)){ password = cookie.getValue(); } } //如果获取的username和password都不为空,跳转至部门列表页面 if(username !=null && password != null){ //进行登录的操作 Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; Boolean flag = false; try { conn = DBUtils.getConnection(); String sql = "select * from user where username = ? and password = ?"; ps = conn.prepareStatement(sql); ps.setString(1, username); ps.setString(2, password); System.out.println(sql); System.out.println(ps); rs = ps.executeQuery(); System.out.println(rs); if(rs.next()){ flag = true; } } catch (SQLException e) { e.printStackTrace(); }finally { DBUtils.close(conn,ps,rs); } if(flag) { //获取Session HttpSession session = request.getSession(); session.setAttribute("username", username); response.sendRedirect(request.getContextPath()+"/dept/list"); } }else { //否则跳转登录页面 response.sendRedirect(request.getContextPath()+"/index.jsp"); } } }
⑥到此,十天免登录功能完成
- 同时:只有修改密码,或删除cookie的硬盘文件,十天免登陆才会失效
可访问浏览器,测试点击十天免登陆后成功登录时,再次访问该路径,是否直接跳过登录页面,进入到部门列表
七、cookie禁用
cookie禁用是指,服务器正常发送cookie给浏览器,但浏览器拒收。
- cookie禁用了,sessionid就会一直找不到,因此每次请求都获取到新的session对象
- 同时每次请求生成的session,只能利用超时机制去销毁
当cookie禁用,该怎样实现session机制?
-
利用URL重写机制
-
在路径后添加分号 (;),然后将sessionid的值拼接过去
-
但使用该机制,会大大提高开发者成本,开发者在编写所有请求路径时,都要添加一个Sessionid
因而正常若禁用cookie,就干脆不让用户使用
-
localhost:8080/项目名/访问资源;jsessionid=xxxxxxxxxxxxxxx
八、Javaweb域对象
①一共有以下三个域对象:
- request(对应类名: HttpServletRequest)
- 请求域 [ 请求级别 ]
- session(对应类名: HttpSession)
- 会话域 [ 会话级别 ]
- applicaion(对应类名: ServletContext)
- 应用域 [ 项目级别 ]
②三个域的大小关系:
- request<session<application
③三者都有以下的公共方法:
- setAttribute(向域当中绑定数据)
- getAttribute(向域当中获取数据)
- removeAttribute(移除域中的数据)
④使用原则:尽量使用小的域
EL表达式
一、EL表达式的作用
EL(Expression Language)表达式语言,可以算是jsp语法的一部分,它有以下功能:
- EL可以代替jsp中的java代码,使jsp文件的程序更加整洁、美观
- 原先的jsp夹杂了许多<% java代码 %>、<%= java代码%>,导致jsp中既有html又有java,十分混乱
主要作用:进行页面展示
(从域中取+输出到浏览器)从某个域中取数据,然后转换为字符串,输出到浏览器
因而要利用EL表达式取数据,首先必须得存储在域中
①作用一:从某个域中取数据
- 四个域
- pageContext
- request
- session
- application
②作用二:将取出的数据转为字符串
- 如果是一个java对象,也会自动调用java对象的toString()方法将其转换为字符串
③作用三:将字符串输出到浏览器
- 同jsp的:<%=%>一致,将其输出到浏览器
二、EL表达式的基本语法
-
注意:不能加双引号 “ ”,会被看做字符串
-
面试题: abc和���和{"abc"}有什么区别?
${abc}表示从某个域中取数据,并且被取的数据name为“abc”,之前一定有该语句:域.setAttribute("abc",对象)
${“abc”}表示将“abc”作为字符串输出到浏览器,而并非从某个域中取到
①第一步:声明当前页面的字符集编码形式
<%page contentType="text/html;charset=UTF-8"%>
②第二步:通过作用域.setAttribute存数据
request.setAttribute("存的值",存储的名)
③第三步:通过EL表达式取数据,并输出浏览器
${xx.xx}
2.1从域中取数据并展示
直接在${}中写“存储的数据名“,servlet中利用request.setAttribute("存储的数据名",存储的数据)
【示例】
2.1.1取出单个的值
1️⃣语法
${取值的名}
2️⃣示例
<% request.setAttribute("username","zhangsan") %> <!--取出userename的值--> ${username} 等同于<%=request.getAttribute("username")%>
2.1.2取出实体类对象的属性值
1️⃣语法
-
不带引号,会将其看做变量。
-
带双引号,则会去找 对象的对应属性
-
常用于要取的属性name含有特殊字符:xxx.xx的情况
eg:${requestScope["adb.de"]}可取出属性名为 adb.de的值
-
${ 对象名.属性名 } ${ 对象名[" 属性名 "] }
2️⃣底层原理
-
先创建一个符合javabean规范的User类
-
底层上:是调用了其属性名对应的getter方法
- 注意:若没有get方法,则报异常
- 没有按驼峰命名法编写的get方法,不报异常,但编码规范不建议这样写
- 注意:若没有get方法,则报异常
-
但是EL省略get同时小写 ≈ 属性名
- 相当于一个username---》EL会将其➕get,再将u->U-》getUsername()
3️⃣示例
<% User user = new User(); user.setUsername("jackjson"); user.setPassword("1234"); user.setAge(50); //数据必须存储在某个域的范围内,因为EL表达式只能从某个域中取数据 request.setAttribute("userObj",user); %> <!--利用EL表达式取--> <!--输出内容:EL表达式会调用对应的toString()方法--> ${userObj} <!-- ${userObj}底层是从域中取出userObj对象,然后调用其toString()方法--> <!--输出内容:实体类对应的属性名username的值--> ${userObj.username} ${userObj["username"]} 使用这些语法的前提是:userObj对象有getUsername()方法
2.1.3取出两个关联实体类对象的属性
1️⃣语法
${实体类.对象名.属性值}
2️⃣示例
- 如:User类中含有Address类型属性,怎样取出user其中所在的城市呢?
- ${user.addr.city}
<% class User{ //创建User对象 private String username; User user = new User(); private String password; user.setUsername("jackjson"); private int age; user.setPassword("1234"); private Address addr; user.setAge(50); .......对应的get、set方法 } //创建Address对象 Address a = new Address(); class Address{ a.setCity("北京"); private String city; a.setStreet("大兴区"); private String street; a.setZipcode("1111"); private String zipcode; ....对应的get、set方法 //设置User对象的address } user.setAddr(a); //将user对象存入域中 request.setAttribute("userObj",user); %> <!-- 利用EL表达式 --> <!--取出user对象是哪个城市的--> ${userObj.addr.city} 相当于user对象.getAddr().getCity() <!--取出user对象是哪个街道的--> ${userObj,addr.street} 相当于user对象.getAddr().getStreet()
2.1.4从Map集合中取数据
1️⃣语法
- 通过key来获取
- 底层仍然调用其getXxx方法
${集合名.对象名.属性名}
2️⃣示例
①先向域中存储一个Map类型的数据 <% Map<String,User> userMap = new HashMap<>(); User user = new User(); user.setUsername("lisi"); userMap2.put("user",user); request.setAttribute("hhh",userMap2); %> ②将域中的数据取出 <!--取出map的数据--> ${hhh.user.username}
2.1.5从数组中取数据
1️⃣语法
- 直接输出数组对象
- 底层调用其toString()方法
${nameArray}
- 输出数组对象中的某个值
- 通过下标来获取
- ❗:即便下标越界,仅取不出数,输出内容为空
- 因为EL表达式会对空进行处理
${nameArray[下标值]}
2️⃣示例
<% //①字符串数组对象 String[] usernames = {"zhangsan","lisi","wangwu"}; //向域中存储数组 request.setAttribute("nameArray",usernams); //②实体类数组对象 User u1 = new User(); u1.setUsername("zhangsan"); User u2 = new User(); u2.setUsername("lisi"); User[] user = {u1,u2}; request.setAttribute("userArray",users); %> ${nameArray[0]} <!--取出nameArray数组的第一个--> ${userArray[1].username} <!--取出userArray数组的第二个,其中的username值-->
2.1.6从List集合中取数据
1️⃣语法
- 下标值从0开始
${list列表名[下标值]}
2️⃣示例
<% List<String> list = new ArrayList<>(); list.add("abc"); list.add("def"); request.setAttribute("myList",list) %> ${myList} ${myList[0]} ${myList[1]}
2.1.7从Set集合中取数据
1️⃣语法
2️⃣示例
<% Set<String> set = new HashSet<>(); set.add("a"); set.add("b"); request.setAttribute("set",set); %>
三、向域中取值的优先级
四个域存储数据的属性名一致的情况,取值的优先级是?
优先从小范围内读取数据
3.1 未指定范围时
- 默认的取值优先级是:pageContext->reuqest->session->application (从小范围域-->大范围域)
3.2 指定范围时
-
EL表达式有四个隐含的隐式的范围对象:
- pageScope
- requestScope
- sessionScope
- applicationScope
-
可利用隐含的范围对象,进行指定
${pageScope.data} 从pageContext域中获取data数据
${requestScope.data} 从request域中获取data数据
${sessionScope.data} 从session域中获取data数据
<% //四个域都存储了数据,且name相同 pageContext.setAttribute("data","pageContext"); request.setAttribute("data","request"); session.setAttribute("data","session"); application.setAttribute("data","application"); %> ${data} <!--从小范围到大范围中取:pageContext->request->session->application-->
- 在实际开发中,name都不相同,所以xxScope一般可省略
四、EL表达式对空值处理
EL表达式对空值会进行处理,如果利用request.getAttribute("")取值为空时,会输出null,而EL表达式输出,会将null变为一串空字符串,对应页面的展示更为友好。
即EL表达式表面为${},其实还是转化为jsp代码,再翻译为java去执行
${username} ===================等同于============ <%=request.getAttribute("username) == null ? "":request.getAttribute("username")%>
五、EL表达式指令
5.1 isELIgnored
- 表示是否忽略该页面中所有的EL表达式
- true 表示不忽略EL表达式
- 页面的EL表达式会当做普通字符串输出
- false 默认为false,表示启用EL表达式
- true 表示不忽略EL表达式
<%@page contentType="text/html;charset=UTF-8" isELIgnored="true"%>
- 若只想忽略某个EL表达式语句,在前加反斜杠\
\${username}
六、EL表达式常用隐式对象
6.1pageContext
(1)作用
其常用于获取应用的根路径:${pageContext.request.contextPath}
【省流】:EL中需要获取request对象,而非直接得到,用pageContext隐含对象的getRequest()方法得到
jsp中九大内置对象就有pageContext,与EL表达式的隐式对象pageContext是同一个。
①jsp中获取request对象有两种方法:
①request ②pageContext.getRequest() 在jsp中的getRequet()似乎沒用,但其实是为在EL表达式中使用作铺垫的
②EL表达式中获取request对象:
- 利用pageContext隐式对象的getRequest()方法
${pageContext.request} <!--别忘了EL表达式中.request就是调用对应的getRequest()方法哦!-->
通过pageContext隐式对象的getRequest()方法得到request对象后,就可以调用.一切request相关的方法,只需注意EL表达式中getXxx()-->xxx即可
③获取应用的根路径
①jsp中可以写成 <!--此处需要强转,因为只有HttpSerlvet才有getContextPath()这个方法--> <%=(HttpServeltRequest)pageContext.getRequest()).getContextPath() %> <%=request.getContextPath()%> ②对应的EL表达式为 ${pageContext.request.contextPath}
6.2param
(1)作用
用于获取页面路径中,请求参数一位数组的第一个参数
(2)语法
${param.参数名}
(3)示例
若用户在浏览器上提交单值数据:http://localhost:8080/jsp/15.jsp?username=lisi
①jsp中: 用户名:<%=request.getParameter("username")%> ②EL表达式中: 用户名:${param.username}
若用户在浏览器上提交多值数据:http://localhost:8080/jsp/15.jsp?aihao=study&aihao=dance&aihao=read
①jsp中 爱好:<%=request.getParameter("username")%> ②EL表达式中:会返回第一个值,正常应使用paramValues 爱好:${param.aihao}
6.3paramValues
(1)作用
获取页面路径中,请求参数一位数组的所有参数值
(2)语法
${paramValues.参数名}
(3)示例
若用户在浏览器上提交多值数据:http://localhost:8080/jsp/15.jsp?aihao=study&aihao=dance&aihao=read
①jsp中 爱好:<%=request.getParameterValues("aihao")%> ②EL表达式中 <!--获取的是一位数组--> 爱好:${paramValues.aihao} <!--获取数组中的元素--> 爱好:${paramVlaue.aihao[0]}
6.4 initParam
(1)作用
获取web.xml中配置的ServletContext的初始化参数
ServletContext是Serlvet上下文对象,对应的JSP九大内置对象中的:application
(2)语法
initParam.属性名
(3)示例
web.xml
<context-param> <param-name>pageSize</param-name> <param-value>20</param-value> </context-param> <context-param> <param-name>pageNum</param-name> <param-value>5</param-value> </context-param>
①jsp获取ServletContext中的初始化参数
<% String pageSize = application.getInitParameter("pageSize"); String pageNum = application.getInitParameter("pageNum"); %> 记录条数:<%=pageSize%> 页码: <%=pageNum%>
②EL表达式获取ServletContext中的初始化参数
记录条数:${initParam.pageSize} 页码: ${initParam.pageNum}
七、EL表达式的运算符
7.1算术运算符
+ - * / %
①+号只进行求和,不进行拼接
- 因而会将+两旁的转为数字,无法转换则报错
${20+10} //30 ${20+"10"} //30 ${20+"abc"} //报错 ${"asd"+"axs"} //报错
7.2关系运算符
== != < <= > >= eq
①==和eq
EL表达式的这两个运算符,都调用了equals方法,用于比较两个变量的引用是否相同
- 注意String重写了,是比较内容
${"abc"=="abc"}//true ${"abc"}==${"abc"} //acb==abc
<% Object o1 = new Object(); request.setAttribute("o1",o1); request.setAttribute("o2",o1); %> ${o1 == o2} //true <% String s1 = "abc"; String s2 = "abc"; request.setAttribute("a",s1); request.setAttribute("b",s2); %> ${a == b}//false <% String s1 = new String(); String s2 = new String(); request.setAttribute("a",s1); request.setAttibute("b",s2); %> ${a == b}//true <% Object o1 = new Object(); Object o2 = new Object(); request.setAttribute("o1",o1); request.setAttribute("o2",o2); %> ${o1 == o2}//false
②!=和not
也调用equals方法, 表示取反
- 使用时要注意优先级,加括号
${!(stu1 eq stu2)}
7.3逻辑运算符
! && || not and or
7.4条件运算符
- 又称三目运算符
? :
示例:
${empty param.username ? "對不起,用戶名爲空" : "成功登录"}
7.5取值运算符
[] 和 .
7.6 empty运算符
判断是否为空,空->true,非空->false
${empty param.user} <!--判断获取到user参数是否为空--> ${not empty param.user} <!--判断获取到user参数是否不为空--> ${!empty param.user} <!--判断是否获取到user参数是否不为空--> ${empty param.password == null} <!--前半部分是boolean类型,与null类型不相等,所以结果为false-->
JSTL标签库
一、什么是JSTL标签库?
JSTL全称Java Standard Tag Lib(java标准的标签库)
JSTL标签库通常结合EL表达式一起使用,目的是消除JSP中的java代码
【JST原理解析】
引入标签库的uri后面路径,指向一个xxx.tld文件
而tld文件实际上是一个xml配置文件
在tld文件中,描述了“标签”和“java”类之间的关系
-
常用的核心标签库对应的tld文件是?
- c.tld
-
tld文件在哪?
- 在jakarta.servlet.jsp.jstl-2.0.0.jar里面META-INF目录下,有c.tld文件
-
源码解析:配置文件tld
<tag> <description> 对该标签的描述 </description> <name>标签名</name> <tag-class>标签对应的java类</tag-class> <body-content>标签体中可以出现的内容</body-content> <!--如果是jsp,表示该标签体中可以出现所有符合jsp语法的代码,如EL表达式--> <!--属性--> <attribute> <description>对属性的描述</description> <name>属性名</name> <required>是否为必须,false非必须,true表示必须</required> <rtexprvalue>该属性是否支持EL表达式</rtexprvalue> </attribute> </tag>
<c:forEach items="属性"> 标签 标签体 </c:forEach>
【旧版jar包下载教程】
jar包下载地址:**http://tomcat.apache.org/taglibs/standard/
①点击download,耐心等待跳转页面
②下载taglibs-standard-impl-1.2.5.jar 和 taglibs-standard-spec-1.2.5.jar
-
在下载页面中一共有4个jar包:
Impl: taglibs-standard-impl-1.2.5.jar JSTL实现类库
Spec: taglibs-standard-spec-1.2.5.jar JSTL标准接口
EL: taglibs-standard-jstlel-1.2.5.jar JSTL1.0标签-EL相关
Compat: taglibs-standard-compat-1.2.5.jar 兼容版本
-
从README得知:
如果不使用JSTL1.0标签,可以忽略taglibs-standard-jstlel包, README没有介绍taglibs-standard-compat
包,估计应该是兼容以前版本
【新版jar包下载教程】
jakarta.servlet.jsp.jstl-2.0.0.jar下载地址
jakarta.servlet.jsp.jstl-api-2.0.0.jar下载地址
点击以上两个链接,耐心等待,即可直接下载,注意根据项目使用的jdk版本,需要使用引入对应的jar包,否则运行项目,会出现报错。
二、使用步骤
-
第一步:引入JSTL标签库对应的jar包
-
在IDEA中如何引入?
【IDEA->WEB-INF下新建lib目录,jar包拷贝到lib中,Add lib】
-
❗引入时,注意保持jar的版本一致,统一为tomcat10之前/之后
-
什么时候需要将引入的jar放入lib目录下?
当引入的jar包是Tomcat中没有的,必须放在lib目录下
-
tomcat10之后:
jakarta.servlet.jsp.jstl-2.0.0.jar
jakarta.servlet.jsp.jstl-api-2.0.0.jar
-
tomcat10之前:
javax.servlet.jsp.jstl.jar
taglibs-standard-impl-1.2.5.jar
taglibs-standard-spec-1.2.5.jar
-
-
第二步:在jsp页面中引入要使用的标签库(采用taglib指令)
- JSTL提供了很多标签库,所以使用时需指定
- uri表示引入标签的地址
- 常用为 http://java.sun.com/jsp/jstl/core 核心标签库。
- prefix表示为标签设置的前缀别名
- 通常为c
- uri表示引入标签的地址
- JSTL提供了很多标签库,所以使用时需指定
<!--核心标签库--> <%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> <!--格式化标签库:负责格式化操作--> <%@taglib prefix="fmt" uri="http://java.sum.com/jsp/jstl/fmt"%> <!--sql标签库--> <%@taglib prefix="sql" uri="http://java.cum.com/jsp/jstl/sql"%>
- 第三步:在需要使用标签的位置使用即可
- 标签是jstl标签,实际上是java程序
示例:
<%@page contentType="text.html;charset=UTF-8"%> <%@taglib prefix="c" uri="http://java.sum.com/hsp/jstl/core"%> <% //创建List集合 List<Student> stuList = new ArrayList<>(); //创建Stduent对象 Student s1 = new Student("110","经常"); Student s2 = new Student("220","获取"); Student s3 = new Student("330","消防车"); //添加到List集合中 stuList.add(s1); stuList.add(S2); stuList.add(s3); //将list集合存储到请求域中 request.setAttribute("stuList",stuList); %> <!--遍历stuList集合的数据,并输出到浏览器--> <!--①使用java代码--> <% List<Strudent> stuList = (List<Student>)request.getAttribute("stuList"); for(stu : stuList){ %> id:<%=stu.getId()%>,name:<%=stu.getName()%> <% } %> <!--②使用jstl标签库:forEach标签--> <c:forEach items="stuList" var="s"> id:${s.id} name:${s.name} </c:forEach>
三、常用标签
3.1 forEach标签
1️⃣作用
forEach标签用于循环
2️⃣语法
①遍历集合
- items:
- 要迭代的集合
- 非必须
- 支持EL表达式
- var
- 迭代时的每一项元素
- 即别名
- 迭代时的每一项元素
- varStatus
- 表示var的状态对象
- 是一个java对象
- 具有count属性值,主要用于编号
- 从1开始,以1递增
- 表示var的状态对象
<%@page contentType="text/html;charset=UTF-8"%> <%@taglib prefix="c" uri="http://java.sum.com/jsp/jstl/core"%> <% //创建List集合 List<Student> stuList = new ArrayList<>(); //创建Stduent对象 Student s1 = new Student("110","经常"); Student s2 = new Student("220","获取"); Student s3 = new Student("330","消防车"); //添加到List集合中 stuList.add(s1); stuList.add(S2); stuList.add(s3); //将list集合存储到请求域中 request.setAttribute("stuList",stuList); %> <c:forEach items="stuList" var="s" varStatus="stuStatus"> 编号:${stuStatus.count} id:${s.id} </c:forEach>
②遍历变量
-
var:
- 用于指定循环时的变量
-
begin
- 开始位置
-
end
- 结束位置
-
step
- 步长
- 类同每次+几
- 步长
<c:forEach var="i" begin="1" end="10" step="1"> ${i} </c:forEach> 输出:1 2 3 4 5 6 7 8 9 10
3️⃣示例
<%@page contentType="text/html;charset=UTF-8"%> <%@taglib prefix="c" uri="http://java.sum.com/jsp/jstl/core"%> <% //创建List集合 List<Student> stuList = new ArrayList<>(); //创建Stduent对象 Student s1 = new Student("110","经常"); Student s2 = new Student("220","获取"); Student s3 = new Student("330","消防车"); //添加到List集合中 stuList.add(s1); stuList.add(S2); stuList.add(s3); //将list集合存储到请求域中 request.setAttribute("stuList",stuList); %> <c:forEach items="stuList" var="s"> id:${s.id} name:${s.name} </c:forEach>
3.2 if标签
1️⃣作用
if标签用于判断
2️⃣语法
-
test:
-
判断的条件,为真执行,为假不执行
-
是必须的
-
支持EL表达式
-
只能是Boolean类型
-
-
var:
-
每次判断的结果值 true/false
- 存储在域中
-
非必须的
-
支持EL表达式
-
-
scope:
- 用来指定var的存储域
- 有四个域:page(pageContext域)、request(request域)、session(session域)、application(application域)
- 用来指定var的存储域
<c:if test="" var="" scope=""> </c:if>
3️⃣示例
<%@page contentType="text/html;charset=UTF-8"%> <%@taglib prefix="c" uri="http://java.sum.com/jsp/jstl/core"%> <c:if test="${empty param.username}"> 如果param中的username为空,则显示该句话 </c:if> <c:if test="${not empty param.user}"> 如果param中的username不为空,则显示这句话 </c:if> <!--无else标签时,可用两个if替代-->
3.3 choose标签
1️⃣作用
多分支条件选择,"当....执行....;否则...."
2️⃣语法
- test
- 表示条件判断语句
<c:choose> <c:when test=""></c:when> <c:when test=""></c:when> <c:when test=""></c:when> <c:otherwise> </c:otherwise> </c:choose>
3️⃣示例
<%@page contentType="text/html;charset=UTF-8"%> <%@taglib prefix="c" uri="http://java.sum.com/jsp/jstl/core"%> <c:choose> <c:when test="${param.age < 18}"> 青少年 </c:when> <c:when test="${param.age < 35 }"> 青年 </c:when> <c:when test="${param.age < 50}"> 中年 </c:when> <c:otherwise> 老年 </c:otherwise> </c:choose>
四、利用jstl改造oa系统
4.1使用技术
利用servlet+jsp+EL表达式+JSTL标签库
1️⃣EL表达式
将项目上下文路径由jsp的<%=request.getContextPath()%>变为${pageContext.request.contextPath}
2️⃣JSTL标签库
-
先向页面引入
-
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
forEach、if、choose标签,将jsp页面的循环,变为< c:forEach>< /c:forEach>+EL表达式${xx.xx}取值
3️⃣利用EL表达式和JSTL标签库,都是为了将jsp页面中的java代码尽可能的减少
因而只在jsp页面中添加改造的代码
4.2改造系统功能代码
- 在此以list列表页为例
(1)改造前
(2)改造后
4.3改造页面路径代码(base标签)
(1)base标签
-
作用:设置整个页面的基础路径
-
常用写法:
-
写死的
http://localhost:8080/oa
-
动态的
协议://服务器IP:端口号/项目根路径/
${pageContext.request.scheme}://${pageContext.request.serverName}:${pageContext.request.serverPort}${pageContext.request.contextPath}/
- 协议 ${pageContext.request.scheme}
- 服务器IP ${pageContext.request.serverName}
- 端口号 ${pageContext.request.serverPort}
- /项目根路径 ${pageContext.request.contextPath}
-
在HTML代码中,有一个base标签,能设置整个网页的基础路径
- 默认会将base标签中的路径,添加在没有以"/"开始的路径前
<title>首页</title> <base href="http://localhost:8080/oa"> <base href="${pageContext.request.scheme}://${pageContext.request.serverName}:${pageContext.request.serverPort}${pageContext.request.contextPath}/"> <!--设置整个网页的基础路径写法有以上两种👆-->
- 之后网页中涉及路径带项目前缀时,都可以去掉
可由以下: <a href="${pageContext.request.contextPath}/user/list"></a> 变为: <base href="http://localhost:8080/oa"> <a href="user/list">
【同时可能对js代码不起作用,建议js中仍使用${pageContext.request.contexPath}】
Filter过滤器
一、是什么?
Filter过滤器是一个java程序。
既可以在Servlet目标程序之前添加代码,又可以在Servlet目标程序之后添加代码。
一般在过滤器中编写公共代码执行,同时执行多个过滤器,具有执行顺序规则。
为什么需要?
当具有多个不同相关业务时,都需要编写一次登录拦截,会造成代码冗余。
而采用Filter过滤器可以更快的帮助我们对不同请求,进行过滤
如:
在Tomcat低版本中,需要处理Servlet乱码问题,因而所有servlet中都需要添加以下两句代码:
//POST请求乱码问题 request.setCharacterEncoding("UTF-8"); //响应中文乱码问题 response.setContentType("text/html;charset=UTF-8");
而我们就可以通过Filter过滤器,将其只写一遍,然后在所有servlet中得到复用
二、如何使用?
①第一步:编写一个java类实现一个接口
- jarkata.servlet.Filter【Tomcat10以上】
- javax.servlet.Filter【Tomcat10以下】
public class Filter implements Filter{ }
②第二步:实现该接口中doFilter方法
编写过滤规则+chain.doFilter(request,response)方法
-
init方法:在Filter对象第一次被创建时调用,且只调用一次
-
doFilter方法:用户发送一次请求,则执行调用一次【编写过滤规则】
-
destory方法:在Filter对象被释放/销毁前调用,并且只调用一次
-
【注意】
Servlet对象默认情况下,服务器启动并不会新建对象(调用时新建)
Filter对象默认情况下,服务器启动会新建对象
但两者都是单实例(假单)
-
-
chain.doFilter(request,response):表示执行下一个过滤器/目标java程序
public class Filter1 implements Filter{ public Filter1(){ //无参构造方法 } @Override public void init(FilterConfig filterConfig) throws SerlvetException { Filter.super.init(filterConfig); } @Override public void doFilter(SerlvetRequest request,ServletResponse response,FilterChain chain) throws IOException,ServletExcetion{ //编写过滤规则 //表示执行下一个过滤器,如果下一个不是过滤器,则执行目标程序servlet chain.doFilter(request,response); } @Override public void destory(){ Filter.super.destory(); } }
③第三步:对Filter进行配置,采用在web.xml文件中对Filter进行配置
表示访问哪些对应的请求路径,会跳转该过滤器
<filter> <filter-name>过滤器名称</filter-name> <filter-class>过滤器包路径</filter-class> <!--可选:过滤器初始化参数名--> <init-param> <param-name>参数名</param-name> <param-value>参数值</param-value> </init-param> </filter> <!--过滤器映射信息--> <filter-mapping> <filter-name>映射的过滤器名</filter-name> <url-pattern>指定的过滤器作用的请求路径</url-pattern> </filter-mapping>
④第三步:对Filter进行配置,采用@WebFilter注解式
- 包含以下路径匹配方式:
- 精确匹配 /dept/save
- 后缀匹配 *.do
- 前缀匹配 /dept/*
- 匹配所有 /*
@WebFilter({"a.do","b.do"})//精确匹配,a.do与b.do都走这个过滤器 @WebFilter("*.do") //扩展匹配,以*开始 --》以.do结尾都走这个过滤器 @WebFilter("/dept/*") //前缀匹配,以/开始 --》以/dept开头都走这个过滤器 @WebFilter("/*")//匹配所有的路径
【注意:】目标Servlet是否执行取决于两个条件
- 第一:用户发送的请求路径是否与Servlet的请求路径匹配成功
- 第二:chain.doFilter(request,response)是否写了
- 表示执行下一个过滤器,若无,执行目标Servlet程序
三、Filter执行顺序
(1)Filter优先级天生比Servlet的高
- 若一个请求路径,即对应Filter又对应Servlet,一定先执行Filter,再执行Servlet
(2)若多个Filter,先执行哪个?
①若采用web.xml文件配置请求路径
- xml中的位置决定先执行哪个Filter
<filter-mapping>FilterB</filter-mapping> <url-patter>/xxxxx</url-patter> <filter-mapping>FilterA</filter-mapping> <url-patter>/xxxx</url-patter> 先执行FilterB,后执行FilterA
标签的位置,决定Filter的执行顺序
②若采用@WebFilter注解式配置请求路径
- 取决于整个类名在字典中的顺序
FilterA和FilterB,先执行FilterA Filter1和Filter2,先执行Filter1
四、执行原理
- 过滤器的调用顺序,遵循栈数据结构
- 先进后出
-
五、Filter生命周期
- init 服务器启动时,Filter对象被创建后,执行一次
- doFilter 用户发送一次请求,执行一次
- destory 服务器关闭时,Filter对象被释放/销毁前,执行一次
- 同Servlet的生命周期
- 唯一不同:Filter默认情况,服务器启动时进行实例化,创建对象
六、Filter责任链设计模式
1️⃣概念
Filter最大优点,在程序编译时,不会确定调用顺序。
因为其调用顺序配置在web.xml文件中,只需修改顺序即可调整Filter的执行顺序。
可见Filter执行顺序,可在程序运行期间动态组合,调用顺序灵活。该设计模式被称为 责任链设计模式
2️⃣作用
在程序运行阶段,动态的组合程序的调用顺序
3️⃣示例代码:
/** * 该程序缺点:在编译阶段就已经确定了调用关系 * 若要改变调用顺序,就要修改java代码 * java代码修改,就需要重新编译,项目重新测试,项目重新发布 * 此举违背了:OCP(开闭)原则,对修改关闭,对扩展开放 */ public class Test{ public class void main(String[] args){ System.out.println("main begin"); m1(); System.out.println("main over"); } private static void m1(){ System.out.println("m1 begin"); m2(); Sysetm.out.println("m1 over"); } private static void m2(){ System.out.println("m2 begin"); m3(); System.out.println("m2 over"); } private static void m3(){ System.out.println("目标程序正在执行中...."); } }
七、Filter过滤器改造oa项目
-
当前的oa项目存在什么缺陷?
-
DeptServlet、EmpServlet、OrderServlet,每个Servlet都是处理自己的相关业务;
但执行这些Servlet前都需要判断用户是否登录,若登录,执行操作;未登录,提醒用户登录。
-
而在所有的Servlet中都添加判断是否登录的这段固定代码,显然代码冗余,未得到复用
-
-
解决方法?
- 使用Servlet规范中的Filter过滤器来解决该问题
①创建LoginCheckFilter,实现Filter
public class LoginCheckFilter implements Filter{ }
②在web.xml配置映射信息
- 拦截对应的路径,到Filter
<filter> <filter-name>loginFilter</filter-name> <filter-class>com.oa.filter.LoginCheckFilter</filter-class> </filter> <filter-mapping> <filter-name>loginFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
③重写其doFilter方法
-
如果想放行路径,怎么办?
request.getSerlvetPath得到路径
if("xxx路径".equals(servletPath) ){
chain.doFilter(request,response);//放行
}
-
添加以下代码,设置拦截登录的过滤器
public class LoginCheckFilter implements Filter{ @Override public void doFilter(ServletRequest req,ServletResponse res,FilterChain chain) throw IOException,ServletException{ //进行强制类型转换 HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; //获取session HttpSession session = request.getSession(false); String servletPath = request.getServletPath(); //如果session不为空,同时有username if("/index.jsp".equals(servletPath) || "/welcome".equals(servletPath) || "/user/login".equals(servletPath) || "/user/exit".equals(servletPath) || (session != null && session.getAttribute("username") !=null)){ //继续执行下一个过滤器 chain.doFilter(request,response); }else{ //跳转到登录页面 response.sendRedirect(request.getContextPath()+"/index.jsp"); } } }
④最后将Servlet中的相关代码,删除即可
- 因为设置的拦截器已经为我们进行了登录拦截,就不需要在每个Servlet再去拦截一次啦
Listener监听器
一、监听器是什么
- 监听器是Servlet规范的一员,而Filter也是规范的一员
- 监听器接口:都以XxxListener结尾
二、监听器的作用
【使用场景】:在特定的情况下,去执行某段代码,就可以使用到监听器,去找到该情况。
三、常用监听器
Servlet规范中提供了哪些监听器?
提供以下监听器的接口,可在这些时间点前后,编写要执行的语句
jakarta.servlet包下:
3.1 ServletContext域
①SerlvetContextLinstener
- 用于监听SerlvetContext对象的状态
- 服务器启动+关闭触发
②ServletContextAttributeLinstener
-
用于监听ServletContext对象域中属性的变化
-
向域中存储、删除、替换数据触发
3.2 ServletReuqest
①ServletRequestListener
-
用于监听ServletReuqest对象的状态
-
发送一次请求开始+结束触发
②ServletReuqestAttributeListener
-
用于监听ServletRequest对象域中属性的变化
-
向域中存储、删除、替换数据触发
jakarta.servlet.http包下:
3.3 HttpSession
①HttpSessionListener
- 用于监听HttpSessionListener对象的状态
- 第一次请求时触发
@WebListener public class MyHttpSessionLinstener implements HttpSessionListener{ @Override public void sessionCreated(HttpSessionEvent httpSessionEvent) { //session对象创建时,调用该事件代码 } @Overrride public void sessionDestoryed(HttpSessionEvent httpSessionEvent) { //session对象销毁时,调用该事件代码 } }
②HttpSesssionAttributeLinstener
- 用于监听session对象域中属性的变化
- 向域中存储、删除、替换数据触发
@WebListener public class MyHttpSessionAttributeListener implements HttpSessionAttributeListener{ @Override public void attributeAdded(HttpSessionBindingEvent se){ //向session域中存储数据时,该方法被WEB服务器调用 } @Override public void attributeRemoved(HttpSessionBindingEvent se){ //向session域中移除数据时,该方法被WEB服务器调用 } @Override public void attributeReplaced(HttpSessionBindingEvent se){ //向session域中数据被替换时,该方法被WEB服务器调用 } }
③HttpSessionBindingListener
-
用于监听实现该监听器的对象
假设User类实现了该监听器:对象放入session触发bind事件,对象从session删除触发unbind事件
假设User类未实现该监听器:对象放入session/从session删除都不触发事件
-
与HttpSesssionAttributeLinstener的区别?
-
HttpSesssionAttributeLinstener:监听session域中数据变化,监测点在HttpSession对象上。
-
只要向HttpSession域中放,就触发
-
需要@WebListener注解
-
-
HttpSessionBindingListener:监听session域中某个特定数据变化,实现了该监听器的对象。
-
只要类实现了该接口的方法,那么对象在被放入监听器时触发
-
不需要@WebListener注解
-
-
//不需要添加@WebListener注解 public class User1 implements HttpSesssionBindingListener{ @Override public void valueBound(HttpSessionBindingEvent event){ //向该实现类,绑定数据时,触发 } @Override public void valueUnbound(HttpSessionBindingEvenet event){ //向该实现类,解绑数据时,触发 } } //以上代码表示,只有向session域中的User1类型对象,存取值,才会触发上述对应方法 //而采用HttpSesssionAttributeLinstener并不能精确到某个对象,只能监听整个session是否有变化
-
常见业务场景
①请编写一个功能,记录网站的实时在线用户人数?
【HttpSesssionAttributeLinstener监听器即可】
方案一:通过服务器端是否分配session对象,因为一个session对象代表一个用户,即session多少个,在线用户就有多少个。session对象新建,count++,再将count存储到ServletContext域中
②请编写一个功能,记录网站已登录的在线用户人数?
【HttpSessionBindingListener才行】
方案二:通过服务器端是否分配session对象+曾setAttribute("user",userObj)存储过user对象,count++。
将User实现HttpSessionBindingListener监听器,当User类型对象存储到session对象中,count++,再将count存储到ServletContext域中,最后页面进行展示
⑤HttpSessionIdListener
- 监听session的id的变化,当session的id发生改变,接口中唯一一个方法被调用
⑥HttpSessionActivationListener
- 监听session的钝化和活化,当session钝化/活化时,触发
- 钝化:session对象从内存存储到硬盘文件
- 活化:从硬盘文件把session恢复到内存中
四、使用步骤
以ServletContextListener为例:
(1)第一步:创建Listener包,存放监听器类
(2)第二步:编写一个类 implements(实现) ServletContextListener接口
public class MyServletContextLinstener implements ServletContextListener{ }
(3)第三步:实现其方法
- contextInitialized
- contextDestoryed
public class MyServletContextLinstener implements ServletContextListener{ @Override public void contextInitialized(ServletContextEvent sce) { } @Overrride public void contextDestoryed(ServletContextEvent sce) { } }
(4)第四步:进行监听器的配置
①采用在Web.xml中进行配置
<listener> <listener-class>监听器类,所在的位置</listener-class> </listener>
②采用注解式进行配置
- 在类上,标注@WebListener注解
@WebListener public class MyServletContextLinstener implements ServletContextListener{ @Override public void contextInitialized(ServletContextEvent sce) { //ServletContext对象创建(服务器启动)时,调用该事件代码 } @Overrride public void contextDestoryed(ServletContextEvent sce) { //ServletContext对象销毁(服务器关闭)时,调用该事件代码 } }
【注意:所有监听器方法都不需要javaweb程序员调用,当特殊情况发生后,由服务器来自行负责调用】
五、Listener改造oa项目
5.1添加【在线人数统计】功能
①用户登录
若执行sesssion.setAttribute("user",userObj)表示User类型对象往session域中存储过,用户登录
②用户退出
-
若执行sesssion.removeAttribute("user",userObj)表示User类型对象从session域中移除,用户退出,表示用户不在线
-
或session超时销毁,也表示用户不在线
5.2实现步骤
①创建User类,implements(实现)监听器
- 重写HttpSessionBindingListener监听器,其中的valueBound和valueunBound方法
public class User implements HttpSessionBindingListener { private String username; private String password; @Override public void valueBound(HttpSessionBindingEvent event) { //一、用户登录,count+1,最终将count存储到application域中,因为统计的是所有人 //①获取ServletContext对象,即application域 ServletContext application = event.getSession().getServletContext(); //②获取在线人数 Object onlinecount = application.getAttribute("onlinecount"); if(onlinecount == null){//如果为空,表示第一个人登录 application.setAttribute("onlinecount",1); }else {//如果不为空,让onlinecount+1 int count = (Integer) onlinecount; count++; application.setAttribute("onlinecount",count); } } @Override public void valueUnbound(HttpSessionBindingEvent event) { //二、用户退出,count-1 //①获取ServletContext对象 ServletContext application = event.getSession().getServletContext(); //②得到在线人数 Integer onlinecount = (Integer)application.getAttribute("onlinecount"); //③将在线人数-- onlinecount--; //③将值重新存储到域中 application.setAttribute("onlinecount",onlinecount); } public User(){ } public User(String username, String password) { this.username = username; this.password = password; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } }
②修改UserSerlvet的代码,在登录和退出时,向session域中添加/删除其中对应的USer类型值,实现监听效果
@WebServlet({"/user/login","/user/exit"}) public class UserServlet extends HttpServlet { @Override protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String servletPath = request.getServletPath(); if("/user/login".equals(servletPath)){ doLogin(request,response); }else if("/user/exit".equals(servletPath)){ doExit(request,response); } } private void doLogin(HttpServletRequest request,HttpServletResponse response) throws ServletException,IOException { String username = request.getParameter("username"); System.out.println(username); String password = request.getParameter("password"); System.out.println(password); Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; Boolean flag = false; try { conn = DBUtils.getConnection(); String sql = "select * from user where username = ? and password = ?"; ps = conn.prepareStatement(sql); ps.setString(1, username); ps.setString(2, password); System.out.println(sql); System.out.println(ps); rs = ps.executeQuery(); System.out.println(rs); if(rs.next()){ flag = true; } } catch (SQLException e) { e.printStackTrace(); }finally { DBUtils.close(conn,ps,rs); } if(flag){ //获取Session HttpSession session = request.getSession(); //➕:向session中添加user User user = new User(username,password); session.setAttribute("user",user); String remember = request.getParameter("remember"); if("1".equals(remember) ){//如果用户点击十天免登陆 //创建Cookie对象存储用户名,密码 Cookie cookie1 = new Cookie("username",username); Cookie cookie2 = new Cookie("password",password);//真实情况需要加密 // 设置失效时间 十天 cookie1.setMaxAge(60*60*24*10); cookie2.setMaxAge(60*60*24*10); //设置关联路径 cookie1.setPath(request.getContextPath()); cookie2.setPath(request.getContextPath()); //响应cookie给浏览器 response.addCookie(cookie1); response.addCookie(cookie2); } //跳转到部门列表页面 response.sendRedirect(request.getContextPath()+"/dept/list"); }else { //跳转失败页面 response.sendRedirect(request.getContextPath()+"/error.jsp"); } } private void doExit(HttpServletRequest request,HttpServletResponse response)throws ServletException,IOException{ //获取session对象,不需要创建新的,因而为false HttpSession session = request.getSession(false); //如果存在session if(session != null){ //➕:从Session中删除user session.removeAttribute("user"); //将其手动销毁 session.invalidate(); //跳转至登录页面 response.sendRedirect(request.getContextPath()); } } }
③打开两个浏览器,登录两个不同的用户访问,可见在线人数相应改变
- 同时点击安全退出后,人数也相应减少
至此所有javaweb相关知识到此结束!
MVC架构模式
一、MVC是什么?
-
系统为什么要分层?
专人干专事,各人分工明确,降低代码耦合度,增强扩展力,增强组建的可复用性
-
MVC架构模式中 M(Model:数据/业务) V(View:视图) C(Controller:控制器)
-
其中的Controller层并不处理业务,主要职责是接收请求,进行调度
二、Dao
Dao,全称Data Access Object(数据访问对象)
是javaEE的设计模式之一,并非23种设计模式
实际开发中,Dao层只负责数据库表的增删改查,不包含任何业务逻辑代码
在此以银行账户系统为例,进行迭代更新,将MVC架构模式融入其中
-
命名规则:以XxxDao命名,表示对XXX表进行增删改查
-
同时其中的方法名,应该与数据库查询相关
insertxx deleteByxxx updateByxxx selectAll
-
-
通常情况,一张表对应一个Dao对象
三、pojo/bean/domain
pojo,全称Plan Ordinary Java Object(简单的java对象),又被称为javabean,也被称为domain(领域模型)对象
不同程序员的习惯不同,但本质都一样,在mvc架构中一般用于数据库操作中存放数据库表数据
一个简单的实体类,常被称为“pojo”,通常用于存放数据
-
命名规则:以数据库表名命名
-
实体类中包含:私有属性、无参构造、有参构造、getter和setter方法
-
设置实体类的属性,建议采用包装类,防止为null的情况
-
通常情况下,一张表对应一个Pojo对象
四、 Service
service被翻译为业务,用于专门编写业务代码
- 命名规则:XxxService ,需要体现出针对Xxx进行的业务操作
五、Controller
controller为控制层,主要是Xxxservlet的java代码,用于专门接收数据,调用业务方法处理业务,并跳至View进行页面展示
- 命名规则:XxxServlet,需体现出针对Xxxx进行的调度操作
六、三层架构
三层架构分为表示层(包含view、controller)->业务逻辑层->持久化层->DB数据库
而三层架构和MVC的关系如图:
【MVC案例:银行转账】
创建数据库表
①利用navicat工具,创建一个名为mvc的数据库
创建t_act表,包含id、actno、balance三个字段
为表添加数据:actno001 50000元、actno002 0元
(1)不采用MVC架构模式
①创建工程
②引入所需的依赖的jar包
- serlvet
- jsp
- jstl
- mysql
③编写页面代码
<%@page contentType="text/html;charset=UTF-8" %> <html> <head> <base href="${pageContext.request.scheme}://${pageContext.request.serverName}:${pageContext.request.serverPort}${pageContext.request.contextPath}/"> <title>银行账户转账</title> </head> <body> <%-- 此处无需加项目名,因为<base>标签会自动将前缀加在无/开头的路径上--%> <form action="transfer" method="post"> 转出账户:<input type="text" name="formActno"><br> 转入账户:<input type="text" name="toActno"><br> 转账金额:<input type="text" name="money"> <input type="submit" value="转账"> </form> </body> </html>
④配置Tomcat服务器,启动项目
⑤编写账户转账的Servlet类,同时设置异常,对余额不足等情况进行处理
- AccountTransferServlet.java:
package com.bank.servlet; import com.bank.exception.AppException; import com.bank.exception.MoneyNotEnoughException; import jakarta.servlet.ServletException; import jakarta.servlet.annotation.WebServlet; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; import java.sql.*; @WebServlet("/transfer") public class AccountTransferServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html;charset=UTF-8"); PrintWriter out = response.getWriter(); System.out.println("跳转了么???"); //1.获取转账相关信息 //来自哪个账号 String formActno = request.getParameter("formActno"); //转入哪个账号 String toActno = request.getParameter("toActno"); //转账金额 double money = Double.parseDouble(request.getParameter("money")); //2.编写转账业务逻辑代码,连接数据库进行转账 Connection conn = null; PreparedStatement ps = null; PreparedStatement ps1 = null; PreparedStatement ps2 = null; ResultSet rs = null; try { //①注册驱动 Class.forName("com.mysql.cj.jdbc.Driver"); //②获取连接 String url = "jdbc:mysql://localhost:3306/mvc?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC&useSSL=true";; String user = "root"; String password = "d17379703772"; conn = DriverManager.getConnection(url, user, password); //③获取预编译的数据库对象 String sql = "select blance from act where actno = ?"; ps= conn.prepareStatement(sql); ps.setString(1,formActno); //④执行sql语句,返回结果集 rs = ps.executeQuery(); //⑤处理结果集 if(rs.next()){ //取出结果数据 double blance = rs.getDouble("blance"); if(blance< money){ //余额不足(使用异常机制处理) throw new MoneyNotEnoughException("对不起,余额不足"); } //余额充足,执行转账 // 发起转账用户-money; String sql2 = "update act set blance = blance -? where actno = ?"; ps1 = conn.prepareStatement(sql2); ps1.setDouble(1,money); ps1.setString(2,formActno); int count = ps1.executeUpdate(); // 收到转账用户+money String sql3 = "update act set blance = blance + ? where actno = ?"; ps2 = conn.prepareStatement(sql3); ps2.setDouble(1,money); ps2.setString(2,toActno); count += ps2.executeUpdate(); //累计两次都成功 if(count !=2){ throw new AppException("转账出现问题,请联系管理员"); } out.print("转账成功"); } } catch (Exception e) { //异常处理,在发生异常时,输出异常信息 //在此如果余额不足,会得到并输出我们自行编写的MoneyNotEnoughException异常 out.println(e.getMessage()); } finally { //释放资源 if(ps != null){ try { ps.close(); } catch (SQLException e) { e.printStackTrace(); } } if(ps1 != null){ try { ps.close(); } catch (SQLException e) { e.printStackTrace(); } } if(ps2 != null){ try { ps.close(); } catch (SQLException e) { e.printStackTrace(); } } if(rs != null){ try { rs.close(); } catch (SQLException e) { e.printStackTrace(); } } if(conn!=null){ try{ conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } } }
- MoneyNotEnoughException.java:
/** * 用于处理余额不足的异常 * @author 六日 * @version 1.0 * @since 1.0 */ public class MoneyNotEnoughException extends Exception{ public MoneyNotEnoughException(){} public MoneyNotEnoughException(String msg){super(msg);} }
- AppException.java
public class AppException extends Exception{ public AppException(){} public AppException(String msg){ super(msg);} }
⑥同时为了保证转账和收款一致,导致转账途中中断,添加事务一致性
//(1)开启事务(不再手动提交,当业务完成后自动提交) conn.setAutoCommit(false); //(2)提交事务 conn.commit(); //(3)发生异常,进行事务回滚 try { if(conn != null){ conn.rollback(); } } catch (SQLException ex) { ex.printStackTrace(); }
(2)采用MVC架构模式
采用mvc架构模式编写项目后,工程目录如下:
-
controller层负责调用service层进行业务,同时跳转对应的视图页面展示
-
dao层用于进行数据库的操作
-
service层用于进行程序业务的操作
-
pojo用于存放实体类,便于dao层使用
-
utils用于存放工具类,如:jdbc连接工具类
-
Exception异常类,用于存放自行编写的异常
-
web目录下:
- 存放静态资源:jsp、html、css页面和图片等
①Util工具类
/** * JDBC工具类 * @author 六日 * @version 2.0 * @since 2.0 */ public class DBUtils { // 数据库驱动程序 private static String driver= "com.mysql.cj.jdbc.Driver"; // 数据库连接信息 private static String url = "jdbc:mysql://localhost:3306/mvc?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC&useSSL=true"; // 连接数据库的用户名 private static String user= "root"; // 连接数据库的密码 private static String password = "d17379703772"; //提供一个私有的构造方法 //避免其创建对象,因为工具类的方法都是静态的,无需创建对象 private DBUtils(){} //DBUtils类加载时执行 static { try { Class.forName(driver); } catch (ClassNotFoundException e) { e.printStackTrace(); } } /** 创建连接池对象 * 未使用数据库连接池,直接创建连接对象,返回每次都会创建一个新的连接对象 * @return 连接对象 * @throws SQLException */ public static Connection getConnection() throws SQLException { Connection conn = DriverManager.getConnection(url, user, password); return conn; } public static void close(Connection conn, Statement stmt, ResultSet rs){ if(stmt!=null){ try { stmt.close(); } catch (SQLException e) { e.printStackTrace(); } } if(rs!=null){ try { rs.close(); } catch (SQLException e) { e.printStackTrace(); } } if(conn!=null){ try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }
②pojo实体类
public class Account { private Long id; private String actno; private Double balance; public Account(){ } public Account(Long id, String actno, Double balance) { this.id = id; this.actno = actno; this.balance = balance; } public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getActno() { return actno; } public void setActno(String actno) { this.actno = actno; } public Double getBalance() { return balance; } public void setBalance(Double balance) { this.balance = balance; } }
③dao层
/** * AccountDao负责Account数据的增删改查 * dao? * 表示 data access object数据访问对象 * 是设计模式的一种,属于javaEE的设计模式之一 * @author 六日 * @version 2.0 */ public class AccountDao { /** * 插入账户信息 * @param act 账户信息 * @return 1表示插入成功,0表示失败 */ public int insert(Account act){ Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; int i = 0; try { conn = DBUtils.getConnection(); Connection connection = DBUtils.getConnection(); String sql = "insert into act(actno,blance) value(?,?)"; ps = conn.prepareStatement(sql); ps.setString(1,act.getActno()); ps.setDouble(2,act.getBalance()); i = ps.executeUpdate(); } catch (SQLException e) { throw new RuntimeException(e); }finally { DBUtils.close(conn,ps,rs); } return i; } /** * 根据主键删除用户 * @param id * @return */ public int deleteById(Long id){ Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; int i = 0; try { conn = DBUtils.getConnection(); Connection connection = DBUtils.getConnection(); String sql = "delete from act where actno = ?"; ps = conn.prepareStatement(sql); ps.setLong(1,id); i = ps.executeUpdate(); } catch (SQLException e) { throw new RuntimeException(e); }finally { DBUtils.close(conn,ps,rs); } return i; } /** * 更新账户 * @param act * @return */ public int update(Account act){ Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; int i = 0; try { conn = DBUtils.getConnection(); Connection connection = DBUtils.getConnection(); String sql = "update act set actno= ?,blance = ? where id = ?"; ps = conn.prepareStatement(sql); ps.setString(1,act.getActno()); ps.setDouble(2,act.getBalance()); ps.setLong(3,act.getId()); i = ps.executeUpdate(); } catch (SQLException e) { throw new RuntimeException(e); }finally { DBUtils.close(conn,ps,rs); } return i; } /** * 根据账号ID查询账户 * @param actno * @return */ public Account selectByActno(String actno){ Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; Account account = null; try { conn = DBUtils.getConnection(); Connection connection = DBUtils.getConnection(); String sql = "select id,blance from act where id = ?"; ps = conn.prepareStatement(sql); ps.setString(1,actno); rs = ps.executeQuery(); if (rs.next()){ Long id = rs.getLong("id"); Double blance = rs.getDouble("blance"); //将结果集封装成对象 account = new Account(); account.setId(id); account.setActno(actno); account.setBalance(blance); } } catch (SQLException e) { throw new RuntimeException(e); }finally { DBUtils.close(conn,ps,rs); } return account; } /** * 获取所有的账户 * @return */ public List<Account> selectAll(){ Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; List<Account> list = new ArrayList<>(); try { conn = DBUtils.getConnection(); Connection connection = DBUtils.getConnection(); String sql = "select id,actno,blance from act "; ps = conn.prepareStatement(sql); rs = ps.executeQuery(); while (rs.next()){ //取出数据 Long id = rs.getLong("id"); String actno = rs.getString("actno"); Double blance = rs.getDouble("blance"); //封装数据 Account account = new Account(); account.setId(id); account.setActno(actno); account.setBalance(blance); //将其加到List集合 list.add(account); } } catch (SQLException e) { throw new RuntimeException(e); }finally { DBUtils.close(conn,ps,rs); } return list; } }
④service业务层
/** * 处理账户转账的业务类 * @author 六日 * @version 2.0 * @since 2.0 */ public class AccountService { private AccountDao dao = new AccountDao(); /** * 完成转账的业务逻辑 * @param formActno 转出账号 * @param toActno 转入账号 * @param money 转账金额 */ public void transfer(String formActno,String toActno,double money) throws Exception{ //查询余额是否充足 Account formAct = dao.selectByActno(formActno); if(formAct.getBalance() < money){ throw new MoneyNotEnoughException("对不起,你的余额不足"); } //修改余额 Account toAct = dao.selectByActno(toActno); toAct.setBalance(toAct.getBalance()+money); formAct.setBalance(formAct.getBalance()-money); //更新数据库中的余额 int count = dao.update(toAct); count +=dao.update(formAct); //判断是否修改成功 if(count!=2){ throw new AppException("转账异常,请重试"); } } }
⑤Exception 异常类
/** * App异常类 */ public class AppException extends Exception{ public AppException(){} public AppException(String msg){super((msg));} }
/** * 余额不足异常类 */ public class MoneyNotEnoughException extends Exception{ public MoneyNotEnoughException(){} public MoneyNotEnoughException(String msg){ super(msg);} }
⑥Controller 负责调度
import java.io.IOException; @WebServlet("/transfer") public class AccountServlet extends HttpServlet { //导入AccountService AccountService accountService = new AccountService(); @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { //获取参数 String formActno = request.getParameter("formActno"); String toActno = request.getParameter("toActno"); Double money = Double.parseDouble(request.getParameter("money")); //调用service进行账户更新 try { //进行转账 accountService.transfer(formActno,toActno, money); //转账成功,跳转成功页面 response.sendRedirect(request.getContextPath()+"/sucess.jsp"); } catch (MoneyNotEnoughException e){ //跳转余额不足页面 response.sendRedirect(request.getContextPath()+"/moneynotenough.jsp"); } catch (Exception e) { //转账失败,跳转失败页面 e.printStackTrace(); response.sendRedirect(request.getContextPath()+"/error.jsp"); } } }
(3)采用MVC架构模式+事务
①理论
事务在Service层进行添加
-
一般是一个业务方法对应一个完整的事务
-
步骤如下:
- service代码的开始时:开启事务
- service代码结束时:进行事务的提交
-
同时为保证service和dao的Connection连接对象是同一个
- 将Connection对象绑定在线程上,利用一个Map集合的形式,令key存放Thread,value存放Object
-
MyThreadLocal.java类:
- 自定义一个map集合,将Connection连接对象放入
/** * 该容器用于存放和当前线程绑定的数据 */ public class MyThreadLocal<T>{ //①创建一个Map集合 private Map<Thread, T> map = new HashMap<>(); //②向ThreadLocal中绑定值 public void set(T obj){ map.put(Thread.currentThread(),obj) } //③从ThreadLocal中得到值 public void get(){ return map.get(Thread.currentThread()); } //④移除ThreadLocal的值 public void remove(){ map.remove(Thread.currentThread()); } }
- DBUtils
- 在getConnection获取连接对象时,获取存放在map集合中的connection对象
public class DBUtils{ private static MyThreadLocal<Connection> local = new MyThreadLocal<>(); /** * 每一次都调用该方法获取Connection对象 */ public static Connection getConnection(){ //获取当前线程中的connection对象 Connection connection = local.get(); //如果为空,表示没有 if(connection == null){ //创建一个conncetion对象 connection = new Connection(); //存入自行创建的线程map集合中 local.set(connection); } return connection; } }
【以上皆为理论,其实java为我们提供了java.lang.ThreadLocal类,其中就含有类似于我们自行编写的ThreadLocal】
②具体步骤
利用ThreadLocal去存放连接对象
可保证当前线程中获取的连接对象都为同一个,使其成功开启事务
①在DBUtils工具类中
获取连接对象时,利用ThreadLocal的set方法,放在线程中
//调用java自带的ThreadLocal类型,创建一个对象,存放每个线程对应的Connection private static ThreadLocal<Connection> local = new ThreadLocal<>(); /** 创建连接池对象 * 未使用数据库连接池,直接创建连接对象,返回每次都会创建一个新的连接对象 * @return 连接对象 * @throws SQLException */ public static Connection getConnection() throws SQLException { //通过ThreadLocal的get方法得到connection Connection conn = local.get(); //如果为空,第一次必定为空 if(conn==null){ //根据url,user,password创建connection conn = DriverManager.getConnection(url, user, password); //并赋给线程中的connection local.set(conn); } return conn; }
关闭连接对象时,将连接对象从线程中移除
因为Tomcat内置了线程池,线程对象都是被提前创建好的,一个线程可能存在重复利用的情况
if(conn!=null){ try { conn.close(); //移除当前线程的连接对象 //因为Tomcat内置线程池,线程池有许多线程对象,都是被提前创建好的,每个线程存在重复使用的现象 local.remove(); } catch (SQLException e) { e.printStackTrace(); } }
- 完整代码如下:
/** * JDBC工具类 已添加事务 * @author 六日 * @version 3.0 * @since 3.0 */ public class DBUtils { // 数据库驱动程序 private static String driver= "com.mysql.cj.jdbc.Driver"; // 数据库连接信息 private static String url = "jdbc:mysql://localhost:3306/mvc?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC&useSSL=true"; // 连接数据库的用户名 private static String user= "root"; // 连接数据库的密码 private static String password = "d17379703772"; //提供一个私有的构造方法 //避免其创建对象,因为工具类的方法都是静态的,无需创建对象 private DBUtils(){} //DBUtils类加载时执行 static { try { Class.forName(driver); } catch (ClassNotFoundException e) { e.printStackTrace(); } } //调用java自带的ThreadLocal类型,创建一个对象,存放每个线程对应的Connection private static ThreadLocal<Connection> local = new ThreadLocal<>(); /** 创建连接池对象 * 未使用数据库连接池,直接创建连接对象,返回每次都会创建一个新的连接对象 * @return 连接对象 * @throws SQLException */ public static Connection getConnection() throws SQLException { //通过ThreadLocal的get方法得到connection Connection conn = local.get(); //如果为空,第一次必定为空 if(conn==null){ //根据url,user,password创建connection conn = DriverManager.getConnection(url, user, password); //并赋给线程中的connection local.set(conn); } return conn; } public static void close(Connection conn, Statement stmt, ResultSet rs){ if(stmt!=null){ try { stmt.close(); } catch (SQLException e) { e.printStackTrace(); } } if(rs!=null){ try { rs.close(); } catch (SQLException e) { e.printStackTrace(); } } if(conn!=null){ try { conn.close(); //移除当前线程的连接对象 //因为Tomcat内置线程池,线程池有许多线程对象,都是被提前创建好的,每个线程存在重复使用的现象 local.remove(); } catch (SQLException e) { e.printStackTrace(); } } } }
②在service中开启事务提交,将其变为自动提交
//开启事务 connection.setAutoCommit(false); //提交事务、 connection.commit();
- 完整代码如下:
/** * 处理账户转账的业务类 已添加事务 * @author 六日 * @version 3.0 * @since 3.0 */ public class AccountService { private AccountDao dao = new AccountDao(); /** * 完成转账的业务逻辑 * @param formActno 转出账号 * @param toActno 转入账号 * @param money 转账金额 */ public void transfer(String formActno,String toActno,double money) throws Exception{ try(Connection connection = DBUtils.getConnection()){ connection.setAutoCommit(false); //查询余额是否充足 Account formAct = dao.selectByActno(formActno); if(formAct.getBalance() < money){ throw new MoneyNotEnoughException("对不起,你的余额不足"); } //修改余额 Account toAct = dao.selectByActno(toActno); toAct.setBalance(toAct.getBalance()+money); formAct.setBalance(formAct.getBalance()-money); //更新数据库中的余额 int count = dao.update(toAct); count +=dao.update(formAct); //判断是否修改成功 if(count!=2){ throw new AppException("转账异常,请重试"); } //提交事务 connection.commit(); }catch (SQLException e){ throw new AppException("账户异常"); } } }
③运行项目,查看控制台输出的连接对象为同一个,表示成功将当前线程的连接对象统一
④添加以下代码在service转账业务中,可模拟异常,若未出现执行一半的情况,表示添加事务成功
String s = null; s.toString();
(4)采用MVC架构模式+事务+接口编程
在此我们需要为dao层、service层、controller层都提供一个接口,使程序员能面向接口编程,提升程序的扩展力。
①AccountService和AccountServiceImpl
public interface AccountService { void transfer(String formActno,String toActno,double money) throws Exception; }
public class AccountServiceImpl implements AccountService { private AccountDao dao = new AccountDaoImpl(); /** * 完成转账的业务逻辑 * @param formActno 转出账号 * @param toActno 转入账号 * @param money 转账金额 */ public void transfer(String formActno,String toActno,double money) throws Exception{ try(Connection connection = DBUtils.getConnection()){ connection.setAutoCommit(false); System.out.println(connection); //查询余额是否充足 Account formAct = dao.selectByActno(formActno); if(formAct.getBalance() < money){ throw new MoneyNotEnoughException("对不起,你的余额不足"); } //修改余额 Account toAct = dao.selectByActno(toActno); toAct.setBalance(toAct.getBalance()+money); formAct.setBalance(formAct.getBalance()-money); //更新数据库中的余额 int count = dao.update(toAct); count +=dao.update(formAct); //判断是否修改成功 if(count!=2){ throw new AppException("转账异常,请重试"); } //提交事务 connection.commit(); }catch (SQLException e){ throw new AppException("账户异常"); } } }
②AccountDao和AccountDaoImpl,修改方法同上,在此不做演示,感兴趣可以在本文末尾,下载源码查看
③AccountServlet
@WebServlet("/transfer") public class AccountServlet extends HttpServlet { //导入AccountService AccountService accountService = new AccountServiceImpl();//利用多态,等学到spring框架,可交由spring去new创建 @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { //获取参数 String formActno = request.getParameter("formActno"); String toActno = request.getParameter("toActno"); Double money = Double.parseDouble(request.getParameter("money")); //调用service进行账户更新 try { //进行转账 accountService.transfer(formActno,toActno, money); //转账成功,跳转成功页面 response.sendRedirect(request.getContextPath()+"/sucess.jsp"); } catch (MoneyNotEnoughException e){ //跳转余额不足页面 response.sendRedirect(request.getContextPath()+"/moneynotenough.jsp"); } catch (Exception e) { //转账失败,跳转失败页面 e.printStackTrace(); response.sendRedirect(request.getContextPath()+"/error.jsp"); } } }
Tomcat版本问题
Tomcat随着版本的更新迭代,有了许多改进
省流:Tomcat10以后不会出现响应请求的中文乱码问题
一、乱码问题
(1)请求数据乱码
①get请求
get请求的数据,存放在请求行中
可修改CATALINA_HOME(Tomcat存放位置)下conf/server.xml
<Connector URIEncoding="UTF-8">
【版本问题】
Tomcat7以及之前的版本,设置< Connertor >标签的URIEncoding=“ISO-8859-Ⅰ“,因而会出现中文乱码问题。
Tomcat8以及之后的版本,设置< Connertor >标签的URIEncoding=“UTF-8”,不存在中文乱码问题。
②post请求
post请求的数据,存放在请求体中
可利用request对象的setCharsetEncoding方法
request.setCharsetEncoding("UTF-8");
【版本问题】
Tomcat9以及之前的版本,设置请求体数据默认为GBK,因而会出现中文乱码。
Tomcat10以及之后的版本,设置请求体数据默认为UTF-8,因而不用考虑中文乱码问题。
(2)响应数据乱码
利用response对象的setContentType()方法
reponse.setContentType("text/html;charset=UTF-8");
【版本问题】
Tomcat9以及之前,响应体的字符集默认为GBK会出现乱码问题
Tomcat10以及之后,响应体的字符集设置默认为UTF-8不会出现乱码问题
IDEA配置
一、修改代码自动更新资源
配置IDEA修改文件代码,就更新资源
二、修改快捷键
File->Setting->搜索Keymap->进行相应设置->Apply进行应用
三、进入多行编辑模式
同时按住ctrl++shift+alt,然后鼠标拖选要一键修改的代码行
javaEE中的路径
①在web.xml中配置Servlet时,映射路径前不必+项目名,以“/”开头
<servlet> <servlet>userServlet</servlet> <servlet-class>全限定包名</servlet-class> </servlet> <servlet-mapping> <servlet-name>userServlet</servlet-name> <url-pattern>/访问路径</url-pattern> </servlet-mapping>
②欢迎页面的资源路径,从web根开始,不需要以"/"开头
<welcome-file-list> <welcome-file>index.html</welcome-file> </welcome-file-list>
③利用request对象的getRequestDispatcher()获取转发器时,路径要以“/”开头,同时不加项目名
request.getRequestDispatcher("/b").forward(request,response); //跳转到Bservlet中,其中web.xml配置BServlet路径为/b //该路径也可以是静态资源,路径从web根下开始,不加项目名
④前端页面的< a>跳转,其中href要带上项目名,路径以“/”开头
<a href="/项目名/资源路径"></a>
⑤前端发送请求时,路径都需要带上项目名
解决Tomcat乱码
一、CMD中运行Tomcat报错
三、Tomcat控制台报错
(1)原因
- Tomcat控制台包含server、Tomcat Localhost Log、Tomcat Catalina Log
- 该处乱码,说明是Tomcat的配置文件中,字符集设置不符合
(2)解决方案
①修改Tomcat文件下conf目录的logging.properties
其中java.util.logging.ConsoleHandler.encoding 设为UTF-8即可
java.util.logging.ConsoleHandler.encoding = UTF-8
②更改web.xml和server,xml文件
②tomcat根目录\conf的web.xml,在100行左右,改成
<servlet> <servlet-name>default</servlet-name> <servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class> <init-param> <param-name>debug</param-name> <param-value>0</param-value> </init-param> <init-param> <param-name>fileEncoding</param-name> <param-value>UTF-8</param-value> </init-param> <init-param> <param-name>listings</param-name> <param-value>false</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet>
③tomcat根目录\conf的server.xml,在70行左右,改成
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" URIEncoding="UTF-8" />
-
若此时Server还处于乱码状态,可能是由于windows默认编码是GBK, idea 的默认继承了windows的编码,但是 tomcat 默认是utf-8的,故而要么修改 tomcat 为GBK
步骤:
①File->Settings
②找到Editor->Console->设置字符集为UTF-8
③再次运行Tomcat服务器,乱码消失
自行输出内容乱码
①File->Setting->Editor->File Encoding全部设置为UTF-8
Localhost Log中文乱码
- 找到tomcat存放文件夹下的
./conf/logging.properties
- 将该文件中的 所有
UTF-8
改为GBK
即可