效果演示中有我的联系方式,当然你也可以通过给我的Github项目点赞来免费的向我获取代码
基于JavaWeb的酒店预定管理系统,要求最后的项目部署在Tomcat服务器上,并实现下图所示的所有功能.
前端思路
本文没有使用那些高大上的框架.只使用了最基本的JSP配合EL以及JSTL来实现页面数据的显示.
EL可以快捷的获取request,page,session等域中的数据,方便进行前后端数据的交互.
JSTL可以便捷的替代JSP页面上的Java代码,提高开发速度,这样就不需要将JSP页面上的代码都写入到Java的Servlet程序中去.
前后端交互部分:
如果不了解EL与JSTL的,可以先找一些资料看一下,我以往的博客写了一些我在写这篇文章的时候的过程,有兴趣的可以看一看.
前端通过表单(form)将input标签内的value值传递到对应的Servlet程序,后端对应好的Servlet程序通过getParameter(String name)方法获取对应的input标签内的对应的name值相同的数据,当然这种方法不太好拉,毕竟比较复杂,代码极度冗余,在后端部分会介绍代替方法.
HttpServletRequest req; <input type="password" name="password" />; String password = req.getParameter("password");
页面切换部分
由于这次的课设是我一个人完成,而我也就学了一两周的Web技术,因此页面的转换只能使用a标签直接超链接到对应的新的页面.
例如当我们登陆成功后,相对于你没有登陆之前肯定是会少一些要求你登陆的按钮的,因此当用户登陆成功后,直接跳转到对应的新页面,而如果用户没有登陆,那么用户对首页的某些图文的访问大部分都是跳出让用户登陆的页面.类似如下.
当用户注册成功后,跳转到登陆页面,登陆成功后,跳转到登陆成功页面,这个过程可以参照你访问某些网站(例如携程等)
首页设计,不论用户是否登陆都有的设计
登陆设计
当用户登陆成功后,登陆与注册的按钮都将消失,这一技术通过JSTL表达式的<c:if test>可以实现
酒店信息显示
为了让用户能看到更多的酒店信息,设定一’酒店推荐’的超链接用于指向一个存放大量酒店信息的页面.
关于这个页面的实现方法,首先要想到的是如果直接使用HTML直接强行把所有的酒店都输出,是很不现实的,因此我们需要借助JSTL的forEach语句以及EL方便的获取域数据的特性来实现酒店信息的输出,同时,为了有一个美观的页面,考虑使用分页技术来将每页的酒店信息进行限定,大概实现如下:
同时,点进某个酒店详情之后,超链接到这个酒店对应的详情信息页面.
例如显示酒店的评价,价格,地区等等信息,因此酒店信息对应的数据库表应该有对应的列属性用于存放这些信息.
为什么要使用验证码?
学过一些前端的都知道,当你提交一次表单之后,如果你按下F5进行刷新页面,就会导致表单的重新提交,或者就是点击回退箭头,然后也可以进行重复的提交,再然后,可能就是网络不好?然后我多点了几次提交,以上这三种情况都会导致在后端程序重复收到要提交的数据,这明显是不友好的,因此需要一种技术去解决上面的这三种情况.
这种技术自然就是验证码了.大概实现思路是:
用户第一次访问表单的时候,就要给表单生成一个随机的验证码字符串,并将这个验证码字符串保存到Session域中去,再将验证码的图片显示在用户看到的表单中,用户再进行输入数据并提交之后,这一表单信息回传给服务器,服务器读取表单中的信息,并删除Session域中的验证码,之后匹配验证码是否正确,如果正确,用户登陆成功.反之失败,也正是由于再得到表单信息后删除了验证码,因此用户进行F5刷新重新提交的时候验证码为空值,因此会阻止用户操作,除非用户刷新当前页面.
验证码实现:
1:我用的是Google的kaptcha来生成验证码,导入jar包后,配置一下web.xml文件即可
2:将需要使用到验证码的地方使用img标签显示验证
<img alt="" src="kaptcha.jpg" width="72px" height="38px" style="float: right; margin-right: 51px">
3:再服务器获取Google生成的验证码和客户端发过来的验证码比较.
String code = req.getParameter("code")//用户输入的用户吗 String token =(String) req.getSession().getAttribute(KAPTCHA_SESSION_KEY);//获取session中的验证 req.getSession().removeAttribute(KAPTCHA_SESSION_KEY);//马上删除验证码 if(token!=null && token.equalsIgnoreCase(code)){ //逻辑1 }else //逻辑2 }
4:验证码的刷新
由于验证码有的时候可能看不清因此需要刷新验证码,刷新的方法就是给图片绑定上click事件.使得验证码点击之后会在向服务器发送一次验证码请求.但是某些浏览器为了使得浏览速度更快做了缓存,如某E和某狐,由于刷新验证码访问的servlet程序是一样的,一次浏览器会保存下来这一次请求的访问地址,第二次进行刷新验证码的时候浏览器会先去缓存中找是否有地址,由于每次进行验证码的访问的地址都是一样的,因此会导致验证码直接从浏览器的缓存中获取,也就导致了刷新验证码的失败.因此只要使得每次访问的地址都不同即可.聪明的你肯定已经想到了在访问地址后面随便加上一个随机数.确实是这样
$("#authcode").click(function () { this.src="${applicationScope.basePath}/kaptcha.jpg?date="+new Date(); })
酒店信息展示模块设计
用户登陆注册模块设计完毕之后,自然就到了最重要的酒店信息展示模块了,这一个模块需要用到数据库,将酒店的各种信息存放再数据库中,然后通过DAO层去访问数据库数据,之后再使用Service层为Web层提供底层访问数据库的操作即可,这样设计是为了使得各个模块分离,解耦合,懂得都懂.存放酒店信息的表如下
之后只要按照访问数据库的方式编写一下DAO层即可,在pojo中声明一下数据库中对应表的属性,然后DAO层编写访问方法,Service层调用DAO层的方法并向Web层提供服务即可.
分页设计
一般一个浏览器都会提供分页设计,我就按照大致思想设计了一下.
设计思想:
1:首先需要一个Page类,这个类用于存放每一页中的信息,例如这是第几页,共有多少页,每页显示什么东西等
2:分页方法其实每次都是通过访问数据库来向页面提供信息的,也就是你每次换一页,那么就需要调用对应的Servlet程序,让程序去数据库中访问数据,并将访问的数据通过HttpServletRequest对象设置到对应的request域中去,然后再请求转发到新的页中显示数据即可
根据输入搜索
其实根据输入搜索的实现思路也差不多,也是通过form标签将对应的数据传递到对应的Servlet程序.
但是由于进行了分页,因此搜索输入需要配合分页操作,因此通过搜索操作访问数据库时相对于直接显示全部数据,需要增加一些限制条件,例如where xxx = xxx这样.
购物车模块
购物车模块,也差不多,先判断是否购物车中有商品,使用<c:if>来判断,如果有就显示,没有就提示用户可以去添加,这就是一个超链接了,直接连接到商品区即可,那么如果有商品,那么就提供清空购物车,删除商品等操作即可.
用户下单模块
这个模块需要在数据库中设计用户表以及订单表,以及对应的Servlet程序.
1:先检查用户购买商品的时候是否登陆,没有登陆则跳转到登陆页面,用户如果登陆了就显示对应的酒店信息,并提供预定接口,用户下单之后跳转到对应的Servlet程序去处理订单即可.(设计的比较简陋,轻喷)
前端总结
于我而言前端提供接口给后端,后端提供具体实现与数据库交互,由于我不怎么会写前端代码,因此前端的代码比较简陋,随便设计应付一下了事了,毕竟得一个人写完这么多东西,而且时间就两周,从0开始还是比较那啥的,见谅咯
后端思路
与上面说的差不多,后端通过前端给的接口实现具体的功能,因此后端需要设计好数据库(巧了我数据库设计直接摆烂,当初没看题目就直接开始设计了,导致最后功能实现了一大半才发现偏题了…)
现在我按照每一层去实现每一个功能.
POJO层
众所周知POJO层用于存放数据库映射类,也就是按照数据库中的各列属性设计对应的POJO层类.这里就按照上面给出的酒店表的pojo对象来展示
public class Hotel { private Integer id; private String name; private String type; private String region; private BigDecimal price; private Integer star_level; private String img_path = "script/hotel_img/default.jpg"; private String evaluate = "当前酒店还没有评价哦,快来体验一下吧" }
Utils层
这个层用于提供一些公共的方法,例如JdbcUtils大家肯定都知道是数据库访问的工具类,一般里面会写上数据库连接,关闭等方法,确实是这样.
然后我还写了一个JavaBeanUtils,
主要是dbutils提供了一个populate方法,这个方法返回一个javabean对象,并且你传什么对象就返回什么对象,而且参数部分刚刚好是Map,这和HttpServletRequest的getParameterMap()方法一起直接绝
///使用 Order order = JavaBeanUtils.copyParamToBean(req.getParameterMap(), new Order()); /** * * @param map 包含参数的map对象 * @param bean 需要注入数据的类 * @param <T> 使用泛型之后可以无序类型转换 * @return */ public static<T> T copyParamToBean(Map map,T bean){ try { System.out.println("注入之前:"+bean); //这个方法本质是先获取所有输入 然后用参数名字变成 setXxx()方法 //因此你的param的名字必须要与符合javabean标准 // Map<String, String[]> map = req.getParameterMap(); // for (Map.Entry<String, String[]> entry : map.entrySet()) { // System.out.println(entry.getKey()+"="+entry.getValue()); // } BeanUtils.populate(bean,map); System.out.println("注入之后:"+bean); }catch (Exception e){ e.printStackTrace(); } return bean; }
DAO层
这个层次提供底层访问数据库的接口,也就是CRUD啦.
考虑到其实每个DAO层其实操作都差不多,都是CRUD,因此我使用了druid和dbutils两个jar包来进行数据库访问,用过的都说好.
在BaseDao类中实现底层CRUD,其他继承类只需要放入sql语句以及参数即可,每个POJO对象继承一下对应的方法接口,按照接口规范去实现对应方法.同时注意,如果你插入的数据用到了emoji表情,众所周知,这个东西需要四个字节,因此如果你使用mysql的时候使用的字符集是utf-8,那么恭喜你,这个字符集刚刚好只支持到3个字节,因此如果有些生僻字或者emoji表情要被使用到,那么你应该使用utf8mb4,它支持四个字节哦UTF-8编码哦
Service层
这个层级为WEB层提供web层需要的操作,例如web层需要删除某个酒店信息,那么web层通过Service层的对象,调用其删除方法即可,Service层的删除方法底层还是DAO层的删除方法.
WEB层
这个层即真正的为页面提供方法的层,也是存放各种Servlet程序的层,每一个Servlet程序将会被不同的接口调用,每当表单,超链接,按钮响应事件的时候,就有一个幸运的Servlet程序被调用,这些事件无非是post方式提交或get方式提交,并且为了知道他们提交之后应该调用那个参数,他们后面一般都会跟上一个参数来告知程序应该调用那个方法,这不用反射?
由于知道要调用的方法的名字,因此反射用起来直接天香.
package com.example.hotel.web; import com.example.hotel.pojo.User; import com.example.hotel.service.impl.UserServiceImpl; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.lang.reflect.Method; /** * @author: Serendipity * Date: 2022/4/6 17:46 * Description: * 建立一个父类,提供复用doPost方法,这样每一个模块 * 例如用户模块就不再需要写重复的dopost方法来调用反射方法 */ public abstract class BaseServlet extends HttpServlet { /* 基类中没有声明所需要调用的方法,也就是说基类只是提供了一个公共的方法给子类使用, 最后子类还是调用了自己类中所声明的方法 */ @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("utf-8"); resp.setHeader("content-type","text/html;charset=UTF-8"); resp.setCharacterEncoding("UTF-8"); System.out.println("进入post方法"); String action = req.getParameter("action");//获取name=action请求参数 判断是那个页面的请求 try { Method method = this.getClass().getDeclaredMethod(action,HttpServletRequest.class,HttpServletResponse.class); method.invoke(this,req,resp); } catch (Exception e) { e.printStackTrace(); } } @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("utf-8"); resp.setHeader("content-type","text/html;charset=UTF-8"); resp.setCharacterEncoding("UTF-8"); System.out.println("进入get方法"); doPost(req, resp); } }
使用Filter过滤器
以上面这种方式实现的话,是可以通过浏览器的地址栏直接访问到后台程序的,因此需要设计一个过滤装置,因此就使用到了Filter过滤器,它可以防止用户直接通过浏览器的地址栏直接访问后台程序.当用户试图这样操作的时候(其实是当用户访问了被限定的资源的时候,才会进入Filter过滤器中进行对应的检查),就会进入Filter过滤器的doFilter方法,因此这个时候就可以检查用户是否登陆了,只有用户登陆了之后才允许用户进行一些特定操作.
出现异常的解决方法
众所周知如果在表单提交的时候,如果出现了异常,没有进行回滚,那么事务就还是会提交上去,这是十分不友好的,因此需要进行事务的回滚.
回滚事务
想要进行事务的回滚,那么就需要要求对数据库的操作依托于同一个线程以及同一个Coonnection连接对象.前者是非常容易实现的,因为默认就是一个线程,但是如果后者怎么办?由于我是用了druid,因此这个时候我就不能每次进行CRUD操作的时候都去进行关闭数据库连接了,而是应该抛出异常,然后让监测机制去捕获这个异常,当捕获到异常的时候进行事务的回滚.这才是正解.因此我需要保证我的程序访问数据库的连接操作必须在回滚函数或提交函数这里才被关闭,因此需要要求程序共用同一个的Connection对象.因此我使用了ThreadLocal对象,它保存一个键值对,其中键为当前线程,值就是Connection对象,这样就能实现访问数据库的操作是同一个连接对象完成的.
异常解决
程序除了异常之后,由于用户肯定是进行了一些操作才会出现,但是出现异常之后由于这个异常没有对应的页面去显示,因此可能会给用户显示一片空白,因此需要使用到tomcat的异常机制.
当程序出现异常的时候会访问到对应的目录下的对应文件
最后
大致介绍全部完毕了,设计也差不多全部完成了,如有需要程序的可以私聊我,我还在优化…当然也可能就凑活了(懒,一次干完三个人的活下次是不可能再干了)