JavaEE三层架构
项目的包结构
项目开发步骤
项目记录在idea的onlinebook路径下的static_book模块中
- 数据库中创建表
- 在bean包下创建javaBean
- 编写jdbc工具类和basedao抽象类
- 写dao层和它的的实现类
- 写service层和它的实现类
- 写web层也就是是servlet程序
项目知识点
jdbcUtils 工具类
和数据库建立连接
BaseDao抽象类
利用druid包操作数据库
动态base标签
对于web 项目使用 base标签+相对地址的方式。
对于框架项目使用 绝对地址的方式。
<%
String basePath = request.getScheme() //http
+ "://"
+ request.getServerName() //ip
+ ":"
+ request.getServerPort() //port
+ request.getContextPath() //工程路径
+ "/";
%>
<!--写base标签,永远固定相对路径跳转的结果-->
<base href="<%=basePath%>">
表单回显数据
服务端要将请求中获取到的参数回传给浏览器,浏览器从请求域中获取数据并显示。
注册和登陆的合并处理
前提是前端的表单中有一个隐藏域,用来定义该表单需要调用的方法。
<input type="hidden" name="action" value="login" />
- 可以用一个UserServlet处理注册和登陆的请求
-
进一步可以利用反射,避免在UserServlet中写太多if/else
-
进一步如果每个xxxServlet 都要写,那么出现代码大量重复,所以创建一个抽象类BaseServlet。
public abstract class BaseServlet11 extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { doPost(req, resp); } protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 解决post请求中文乱码问题 // 一定要在获取请求参数之前调用才有效 req.setCharacterEncoding("UTF-8"); String action = req.getParameter("action"); try { // 获取action业务鉴别字符串,获取相应的业务 方法反射对象 Method method = this.getClass().getDeclaredMethod(action, HttpServletRequest.class, HttpServletResponse.class); // System.out.println(method); // 调用目标业务 方法 method.invoke(this, req, resp); } catch (Exception e) { e.printStackTrace(); } } }
数据的抽取和封装 BeanUtils 工具类
当表单中参数过多时,后端获取参数会很麻烦,所以编写BeanUtils工具类。它可以一次性把参数导入到 JavaBean 当中。
原理:利用表单中各项的name属性和javabean的属性名一致,然后调用bean的setxxxx() 方法即可实现。
-
导包: commons-beanutils-1.8.0.jar 和 commons-logging-1.1.1.jar
-
写工具类
public class WebUtils { public static <T> T copyParamToBean(Map map, T bean) { //利用泛型即可在返回时不需要类型转换。 try { BeanUtils.populate(bean, map); } catch (Exception e) { e.printStackTrace(); } return bean; } }
-
使用
User user = WebUtils.copyParamToBean(req.getParameterMap(), new User()); //抽取请求中所有参数的键值对,给user类包含的属性赋值
MVC 概念
全称:model 模型,view 视图, controller 控制器。
MVC 是一种思想,用来降低系统的耦合性。
view 视图:负责控制数据和界面的显示,不接受任何与数据无关的代码,便于程序员和美工的分工和合作--------jsp、HTML页面。
controller 控制器:只负责接受请求,调用业务层的代码处理请求,然后派发页面,是一个“调度者"的角色---------servlet。
model 模型:将与业务逻辑相关的数据封装为具体的 javabean类,其中不参杂任何与数据相关的代码----------entity、pojo
浏览器页面和服务器的交互
浏览器页面需要先访问服务器的Servlet程序,Servlet程序处理请求之后再转发到另一个浏览器页面,从而实现交互。
以图书管理模块为例。
-
manger.jsp
点击图书管理跳转到BookServlet注册的域名上
-
BookServlet
处理请求,然后将数据放在request域中,然后请求转发pagers/manager/manager.jsp
-
pagers/manager/manager.jsp
从请求域中拿出数据。
权限约束
测试
从最小的模块开始测试,逐渐放大范围。
优化删除操作
点击删除按钮后询问用户是否删除
重定向和请求转发
重定向:想直接显示某个地址。
请求转发:只是对请求做一个处理,向请求域中添加信息,然后再将请求发送给某个页面。
重定向在使用时默认地址中不包含工程名,需要自己手动添加。
一个表单多种操作
有时候一个表单需要根据请求类型不同来做不同操作。比如图书模块中的添加和修改操作。
添加和修改操作都跳转到book_edit页面,book_edit页面中的表单在提交时根据请求的类型来做出相应操作。
解决方法:
- 在添加请求中添加一个method参数,它的值为addBook,表示它是添加操作,而且addBook就是BookServlet程序中添加图书的方法。
在修改请求中添加一个method参数,它的值为updateBook,表示它是修改操作,而且updateBook就是BookServlet程序中修改图书的方法。
分页
详见BookServlet中的page方法,book_manager.jsp页面
获取地址栏地址
js的一个Location地址栏对象,它有一个href属性,可以获取浏览器地址栏中的地址,并跳转。
分页边界
应对用户无厘头跳转操作,比如总记录20条,用户要跳转到-50页,或者100页。规定用户如果要跳转到-50页,系统跳转到第一页,要跳转到100页,系统跳转到20页。
解决方法:
-
只在浏览器端判断是可以被破解的,所以选择在服务端解决。
-
在page类设置总页码pageTotal时,判断当前页吗pageNo是否小于1或者大于pageTotal,进而对pageNo赋值。
public void setPageTotal(Integer pageTotal) { if(pageNo<1){ this.pageNo=1; }else if(pageNo>pageTotal){ this.pageNo=pageTotal; } this.pageTotal = pageTotal; }
-
还要在设置总页码数pageTotal之后再设置当前页码pageNo,因为在getPageItems方法中需要使用的修改过后的pageNo。
跳转到当前页附近的分页
效果: 4,5,【6】,7,8
总页码为10
-
总页码小于等于5
第一页 -- 总页码
1
1,2
1,2,3
1,2,3,4
1,2,3,4,5
-
总页码大于5
-
前三页:第一页---第五页
【1】,2,3,4,5
1,【2】,3,4,5
1,2,【3】,4,5
-
后三页:总页码-4 ---- 总页码
6,7,【8】,9,10
6,7,8,【9】,10
6,7,8,9,【10】
-
中间:当前页-2 --- 当前页+2
4,5,6,7,8
5,6,7,8,9
-
前台分页
按价格区间搜索
登陆和注销
登陆时将User添加到session域中,注销时将session清除。
登陆:
注销:
表单重复提交
当用户提交完请求,浏览器会记录下最后一次请求的全部信息。当用户按下f5刷新后,就会发起浏览器记录的最后一次请求。
案例:图书管理模块的添加图书功能。
如果使用请求转发,那么浏览器每刷新一次就会访问一次addBook方法,添加一次图书。为了解决这个bug,需要使用请求重定向。
验证码
客户端发送请求后,服务器生成一个验证码,将验证码放在session中,然后将带有验证码的表单发送给浏览器。用户在浏览器填写完后提交表单,服务器获取session中的验证码然后删除它,然后比较验证码是否和session中验证码相等,。这样即使用户第二次提交表单也会被阻止。
验证码使用谷歌的kaptcha实现。
使用步骤:
-
导包:
-
在web.xml 中配置 KaptchaServlet类,访问这个类会返回一个验证码图片,并且将验证码的字符串加入session中
-
表单中使用:
-
UserServlet中判断提交的表单中验证码和session域中验证码是否一致。KAPTCHA_SESSION_KEY 是验证码在session中的键。
**切换验证码:**用户看不清的时候点击图片切换。参数d是为了越过缓存,只要每次的请求的参数不同就不会访问缓存,否则切换一次就不能切换了,浏览器默认从缓存中获取验证码图片。
购物车
使用session实现购物车
请求头中的 Referer 属性
订单模块
在service层添加订单之后才能添加订单项,因为t_orderItem中的order_id 是依赖于t_order表中的order_id的,所以如果先添加订单项会报错。
order和orderItem需要放在一个事务中同时添加和删除,否则会出现错误。后面的threadLocal对添加订单模块进行了事务处理。
拦截
jsp页面和servlet程序都要拦截
ThreadLocal
使用ThreadLocal确保所有的操作使用同一个connection来实现事务管理
修改操作
-
JdbcUtils
- 将原来的getConnection和closeConnection两个方法修改为 getConnection , commitAndClose 和 rollbackAndClose 三个方法
- getConnection方法中新建的连接需要放到ThreadLocal中保存,供同一个线程中其他数据库操作使用
public class JdbcUtils { private static DruidDataSource dataSource; private static ThreadLocal<Connection> threadLocal = new ThreadLocal<>(); static { try { Properties properties = new Properties(); //读取jdbc.properties文件 properties.load(JdbcUtils.class.getClassLoader().getResourceAsStream("jdbc.properties")); //创建 数据库连接池 dataSource = (DruidDataSource) DruidDataSourceFactory.createDataSource(properties); // System.out.println(dataSource); } catch (Exception e) { e.printStackTrace(); } } public static void main(String[] args) { } /** * 获取数据库中的连接 * @return */ public static Connection getConnection(){ Connection connection = threadLocal.get(); if(connection == null){ try { connection=dataSource.getConnection(); //设置数据库连接为手动管理事务 connection.setAutoCommit(false); //创建好数据库连接后将该连接放入到threadlocal中 threadLocal.set(connection); } catch (SQLException throwables) { throwables.printStackTrace(); } } return connection; } /** * 提交事务并关闭连接 */ public static void commitAndClose() { Connection connection = threadLocal.get(); if(connection != null){ //如果connection不为null,说明以前使用过连接,操作过数据库 try { connection.commit(); } catch (SQLException throwables) { throwables.printStackTrace(); } finally { try { connection.close(); } catch (SQLException throwables) { throwables.printStackTrace(); } } } //关闭数据库连接后一定要删除threadlocal中的值,否则出错。因为Tomcat服务器底层使用了线程池技术。 threadLocal.remove(); } /** * 提交事务,并关闭连接 */ public static void rollbackAndClose(){ Connection connection = threadLocal.get(); if(connection != null){ //如果connection不为null,说明以前使用过连接,操作过数据库 try { connection.rollback(); } catch (SQLException throwables) { throwables.printStackTrace(); } finally { try { connection.close(); } catch (SQLException throwables) { throwables.printStackTrace(); } } } //关闭数据库连接后一定要删除threadlocal中的值,否则出错。因为Tomcat服务器底层使用了线程池技术。 threadLocal.remove(); } }
-
BaseDao
- 主要修改每个方法的catch部分,异常要抛出去
public abstract class BaseDao { //使用dbutils操作数据库 private QueryRunner queryRunner = new QueryRunner(); /* * update 方法用来执行 insert/update/delete语句 * return -1 代表执行失败,返回其他表示影响的行数。 * * */ public int update(String sql,Object ... args){ Connection connection = JdbcUtils.getConnection(); try { return queryRunner.update(connection,sql,args); } catch (SQLException throwables) { throwables.printStackTrace(); //因为jdbcutils中的方法已经捕获异常了,所以此处只需要将异常抛出去即可,不用捕获。 //如果某个dao层的操作出现异常而没有将异常抛出,那么后面dao层的操作将不知道前面出现异常,而且basedao也不知道出现异常,那么提交和回滚操作就会出错。 throw new RuntimeException(throwables); } //执行完sql语句之后不再关闭数据库连接,统一交给basedao } /** * 查询返回一个javabean的sql语句 * @param type 返回的对象类型 * @param sql 使用的sql语句 * @param args sql语句需要的参数 * @param <T> 返回的类型的泛型 * @return */ public <T> T queryForOne(Class<T> type, String sql, Object ... args){ Connection connection = JdbcUtils.getConnection(); try { return queryRunner.query(connection,sql,new BeanHandler<T>(type),args); } catch (SQLException throwables) { throwables.printStackTrace(); //因为jdbcutils中的方法已经捕获异常了,所以此处只需要将异常抛出去即可,不用捕获 throw new RuntimeException(throwables); } //执行完sql语句之后不再关闭数据库连接,统一交给basedao } /** * 查询返回多个javabean的sql语句 * @param type 返回的对象类型 * @param sql 使用的sql语句 * @param args sql语句使用的参数 * @param <T> 返回类型为泛型 * @return */ public <T>List<T> queryForList(Class<T> type,String sql,Object ... args){ Connection connection = JdbcUtils.getConnection(); try { return queryRunner.query(connection,sql,new BeanListHandler<T>(type),args); } catch (SQLException throwables) { throwables.printStackTrace(); //因为jdbcutils中的方法已经捕获异常了,所以此处只需要将异常抛出去即可,不用捕获 throw new RuntimeException(throwables); } //执行完sql语句之后不再关闭数据库连接,统一交给basedao } /** * 查询返回一行一列的sql语句 * @param sql 执行的sql语句 * @param args sql语句所需要的参数 * @return */ public Object queryForSingleValue(String sql,Object ... args){ Connection connection = JdbcUtils.getConnection(); try { return queryRunner.query(connection,sql,new ScalarHandler(),args); } catch (SQLException throwables) { throwables.printStackTrace(); //因为jdbcutils中的方法已经捕获异常了,所以此处只需要将异常抛出去即可,不用捕获。 throw new RuntimeException(throwables); } //执行完sql语句之后不再关闭数据库连接,统一交给basedao } }
-
OrderServlet
- service层的createOrdder方法需要添加try/catch语句,实现手动的提交和回滚。
public class OrderServlet extends BaseServlet{ OrderService orderService = new OrderServiceImp(); protected void addOrder(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { //System.out.println("订单添加成功"); Cart cart = (Cart)req.getSession().getAttribute("cart"); User user = (User)req.getSession().getAttribute("user"); if(user == null){ req.getRequestDispatcher("/pages/user/login.jsp").forward(req,resp); System.out.println("你还没登陆"); return; } System.out.println("你已经登录了"); System.out.println(user.getId()); String orderId = null; try { //web层需要做的修改 orderId = orderService.createOrder(cart, user.getId()); JdbcUtils.commitAndClose(); } catch (Exception e) { e.printStackTrace(); JdbcUtils.rollbackAndClose(); } req.getSession().setAttribute("orderId",orderId); resp.sendRedirect(req.getContextPath()+"/pages/cart/checkout.jsp"); } }
用 Filter 实现全覆盖的try/catch
用Filter 给所有的service层的方法添加上 try/catch,这样就不用在每个service中手动添加try/catch
利用的是filter的 调用目标资源 的原理。
- transactionFilter
-
web.xml
拦截所有请求。
-
还要在BaseServlet中将异常抛出,否则service层在执行过程中产生的异常会被BaseServlet捕获,而 filter中收不到异常,继而就无法实现事务的提交和回滚。
展示错误页面
比如,当使用事务时,如果某个操作有异常,那么浏览器就会显示空白,这样让用户的体验很差,所以可以利用tomcat来统一展示错误页面。
- web.xml
- 在TransactionFilter中将异常抛给tomcat,否则tomcat收不到异常