JavaEE 三层架构
分层是为了解耦,解耦是为了降低代码的耦合度,方便项目后期维护和升级。
web 层
servlet/controller
service 层
service:service 接口包
service.impl:service 接口实现类
dao 持久层
dao:Dao 接口包
dao.impl:Dao 接口实现类
实体 bean 对象
pojo/entity/domain/bean:JavaBean 类
测试包
test/junit
工具类
utils
web 工程流程
- 创建需要的数据库和表
- 编写数据库表对应的 JavaBean 对象
- 编写工具类 JdbcUtils
- 导入需要的 jar 包(数据库和连接池需要)
- 编写 jdbc.properties 属性配置文件
- 编写 JdbcUtils 工具类
- JdbcUtils 测试
- 编写 BaseDao
- 导入 DBUtils 的 jar 包
- 编写 BaseDao
- 编写 UserDao 和测试
- UserDao 接口
- UserDaoImpl 实现类
- UserDao 测试
- 编写 UserService 和测试
- UserService 接口
- UserServiceImpl 实现类
- UserService 测试
- 编写 web 层
- 编写 Servlet 程序
动态的 base 标签值
一般写 base 标签的相对路径的地址时都不会把地址写死,都是动态的获取当前访问的 URI
表单提交失败设置回显信息
JSP 的动态性第一次体现出,在服务器中判断出表单项中有错误的数据,并将页面请求转发到原页面,但是在 request 域中设置了一些数据,比如:错误的提示信息、原来表单项中的部分数据
在页面填入了错误的数据,将提示错误信息,并且将部分表单项的数据回显:
合并 Servlet 程序
之前写的 Servlet 程序都是一个 Servlet 程序对应一个功能,实际上可以在一个基础的 Servlet 程序中实现多个功能,结合表单的隐藏域的使用:
使用反射优化大量 else if 代码
如果随着业务功能的不断扩展,有了越来越多的功能,每次新增加一个功能就要去 Servlet 程序改代码,增加些许 else if 代码,这无疑会使代码变得非常庞大
可以将隐藏域中的值和对应功能的名称对应起来,在 Servlet 程序中获取到隐藏域中的值的时候,使用反射获取到同名的方法,动态调用方法:
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String action = req.getParameter("action");
try {
Method method = this.getClass().getDeclaredMethod(action, HttpServletRequest.class, HttpServletResponse.class);
method.invoke(this, req, resp);
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
}
抽取 BaseServlet
话说又来了,实现多个功能时,就需要多个 Servlet 程序,还得在每一个 Servlet 程序继承 HttpServlet 类 重写 doGet|doPost 方法,然后利用反射实现动态调用方法,如果有一万个 Servlet 就得写一万次这样的代码,不要重复的造轮子,而是将所有公共的操作写在 BaseServlet 类中,让其它所有 Servlet 程序继承这个基类即可
/**
* @author geekfx
* @create 2020-04-27 21:36
*/
public abstract class BaseServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String action = req.getParameter("action");
System.out.println(action);
try {
Method method = this.getClass().getDeclaredMethod(action, HttpServletRequest.class, HttpServletResponse.class);
System.out.println(method);
method.invoke(this, req, resp);
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
}
}
BeanUtils 工具类
BeanUtils 工具类,它可以一次性的把所有请求的参数注入到 JavaBean 中。
BeanUtils 工具类,经常用于把 Map 中的值注入到 JavaBean 中,或者是对象属性值的拷贝操作。
此包下有一个方法static void populate(Object bean, Map properties)
可以将 Map 集合中的键值对,按照键为 bean 属性,值为 bean 属性的值的关系注入到 bean 对象中
而 HttpServletRequest 类中有一个方法 Map<String, String[]> getParameterMap()
可直接将浏览器表单项中的请求参数一次性全部获取并且封装成 Map 集合
正好对应了 populate 方法的参数
System.out.println("注入前:" + bean);
BeanUtils.populate(bean, properties);
System.out.println("注入后:" + bean);
在浏览器提交表单,查看服务器端的输出:
可以看到已经将表单项中的 username 和 password 的参数值注入到 bean 对象中
底层的实现原理大体是什么样子呢
如果将 bean 对象的 username 属性的 setUsername 的方法删除掉,再次发送表单数据
查看服务器端的输出:
可以猜测 BeanUtils 工具类在底层的大体实现细节:
将表单项中的 name 属性值及其 value 的值 通过反射调用 bean 对象的 setXxx 方法来实现的
使用 EL 表达式替换表达式脚本完成回显
之前使用的是 jsp 的表达式脚本来实现错误表单的回显
<%=request.getAttribute("xxx") == null ? "xxxxxx" : request.getAttribute("xxx")
可以看到代码非常长而且还罗嗦,EL 表达式就是为的替换表达式脚本在 jsp 页面上输出用的,使用 EL 表达式代码可变为:
${ requestScope.xxx }
因为表达式脚本取到的数据若为 null 将会在页面上直接输出 null 而不是显示空串
EL 表达式正好弥补了这个缺陷
MVC 模式
MVC 全称:Model 模型、 View 视图、 Controller 控制器
MVC 最早出现在 JavaEE 三层中的 Web 层,它可以有效的指导 Web 层的代码如何有效分离,单独工作
- View 视图:只负责数据和界面的显示,不接受任何与显示数据无关的代码,便于程序员和美工的分工合作——JSP/HTML
- Controller 控制器:只负责接收请求,调用业务层的代码处理请求,然后派发页面,是一个“调度者”的角色——Servlet。转到某个页面。或者是重定向到某个页面
- Model 模型:将与业务逻辑相关的数据封装为具体的 JavaBean 类,其中不掺杂任何与数据处理相关的代码——JavaBean/domain/entity/pojo
MVC 是一种思想,理念是将软件代码拆分成为组件,单独开发,组合使用(目的还是为了降低耦合度)
前台后台
表单重复提交
当用户提交完请求,浏览器会记录下最后一次请求的全部信息。当用户按下功能键 F5,就会发起浏览器记录的最后一次请求。
为解决重复提交的 BUG 需要在实现功能后使用请求重定向返回原来的页面而不是请求转发
添加功能大体流程
删除功能大体流程
修改功能大体流程
页面分页
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2s0DkPYj-1588598038196)(https://s1.ax1x.com/2020/04/28/JINAy9.png)]
location 对象
JS 中提供一个 location 对象
Location 对象包含有关当前 URL 的信息。
Location 对象是 window 对象的一部分,可通过 window.Location 属性对其进行访问。
有一个属性 href 它可以获取地址栏中的地址
href 属性可读、可写
表单重复提交——验证码
表单重复提交的三种情况:
- 提交完表单。服务器使用请求转来进行页面跳转。这个时候,用户按下功能键 F5,就会发起最后一次的请求。造成表单重复提交问题
解决方法:使用重定向来进行跳转 - 用户正常提交服务器,但是由于网络延迟等原因,迟迟未收到服务器的响应,这个时候,用户以为提交失败,就会着急,然后多点了几次提交操作,也会造成表单重复提交
- 用户正常提交服务器。服务器也没有延迟,但是提交完成后,用户回退浏览器。重新提交。也会造成表单重复提交
第一种情况:
注册后使用的请求转发到注册成功页面,若刷新页面则还会按刚才提交表单的地址访问,将出现重复提交
// 第一种情况
String username = req.getParameter("username");
System.out.println(username + " 注册到数据库");
req.getRequestDispatcher("/success.jsp").forward(req, resp);
此时狂按 F5 浏览器没有什么变化,但是查看服务器情况:
解决方法: 将请求转发换成请求重定向到登陆成功的页面
String username = req.getParameter("username");
System.out.println(username + " 注册到数据库");
// req.getRequestDispatcher("/success.jsp").forward(req, resp);
resp.sendRedirect(req.getContextPath());
此时在浏览器提交数据,请求重定向到注册成功的页面:
再怎么狂按 F5 刷新,也不会出现表单重复提交的情况:
第二种情况:
即使使用请求重定向,但是如果此时服务器运行速度稍慢,在浏览器出现明显的卡顿现象,用户多次点击提交按钮:
String username = req.getParameter("username");
System.out.println(username + " 注册到数据库");
try {
// 模拟处理业务 在浏览器会出现卡顿的现象
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// req.getRequestDispatcher("/success.jsp").forward(req, resp);
resp.sendRedirect(req.getContextPath() + "/success.jsp");
在服务器处理业务的同时在浏览器重复点击提交按钮:
查看服务器提交情况:
第三种情况:
用户提交完成后,使用浏览器回退按钮返回到注册页面,重新提交,则还是会出现表单重复提交的现象:
基于后两种的处理情况,可以使用验证码的方式避免表单重复数据的情况
谷歌 kaptcha
导入 kaptcha 的 jar 包,查看包结构:
<!-- 给验证码 Servlet 程序配置一个访问地址:kaptcha.jpg -->
<servlet>
<servlet-name>KaptchaServlet</servlet-name>
<servlet-class>com.google.code.kaptcha.servlet.KaptchaServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>KaptchaServlet</servlet-name>
<url-pattern>/kaptcha.jpg</url-pattern>
</servlet-mapping>
现在浏览器中直接访问此 Servlet 程序,查看结果:
访问 Servlet 程序后返回一个验证码图片,并且 kaptcha 将此验证码的值存放到 session 域中,在自己的 Servlet 收到请求后,最好马上获取验证码的文本内容并且将 session 域中验证码的数据删除掉
然后再获取提交表单中的验证码作比较,查看验证码是否正确,若正确则跳转到登陆成功,若失败则返回注册页面
此时如果再次使用后两种方法重复提交表单,在 Servlet 程序将获取不到 session 域中的验证码数据 将会是 null 所以就无法重复提交数据了
因为第一次提交时获取完验证码中的文本内容后已经将验证码移出了 session 域
当提交表单后,若使用的是请求转发到其它页面,若按 F5 刷新页面,会将刚刚提交的表单的数据发送给服务器
但是服务器收到请求后,查看 session 域中并没有验证码的信息,所以不做注册处理
HttpSession session = req.getSession();
// 获取验证码的文本内容
String kaptchaCode = (String) session.getAttribute(Constants.KAPTCHA_SESSION_KEY);
// 马上将验证码移出 session 域
session.removeAttribute(Constants.KAPTCHA_SESSION_KEY);
// 获得表单中的验证码内容
String formCode = req.getParameter("code");
// 判断验证码
if(formCode.equalsIgnoreCase(kaptchaCode)) {
// 获取表单信息
String username = req.getParameter("username");
// 模拟提交数据库
System.out.println(username + " 注册到数据库");
req.getRequestDispatcher("/success.jsp").forward(req, resp);
} else {
// 验证码输入不正确或没有获取到验证码(重复提交的情况)
System.out.println("重复提交或验证码不正确");
}
在浏览器填写表单数据并提交:
此时狂按 F5 刷新页面,虽然重复提交了数据,但是服务器并不会给予处理:
不管是请求转发还是请求重定向,使用验证码技术,可以解决重复提交表单数据的情况
验证码的切换
给验证码图片绑定单击事件,每次点击都会重新请求 KaptchaServlet 程序从而获取新的验证码信息
谷歌浏览器是没有任何问题的,但是火狐浏览器和 IE 浏览器只切换一次后再次点击图片就不会重新请求 KaptchaServlet 程序
因为浏览器会有缓存,缓存的内容就是 请求的资源路径+请求的内容
如果下次访问同一个资源路径,先会从缓存中找,找到后直接返回缓存的内容而不是重新发起请求
解决的方案是将每次访问 KaptchaServlet 程序后面带一个每次都会出现不一样的值的参数,比如时间戳
// 使用 jQuery 绑定单击事件
$("#codeId").click(function () {
// this 表示当前响应事件的 DOM 对象
// src 表示此对象的属性 可读 可写
// encodeURI 方法可将请求地址作为 URI 进行编码 防止 ie 浏览器出现编码的问题
// 在服务器若想得到此请求参数可使用 DecoderURI
this.src=encodeURI("${basePath}kaptcha.jpg?d=" + new Date()).valueOf();
});