书城项目

185 阅读9分钟

JavaEE三层架构

项目的包结构

项目开发步骤

项目记录在idea的onlinebook路径下的static_book模块中

  1. 数据库中创建表
  2. 在bean包下创建javaBean
  3. 编写jdbc工具类和basedao抽象类
  4. 写dao层和它的的实现类
  5. 写service层和它的实现类
  6. 写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" />
  1. 可以用一个UserServlet处理注册和登陆的请求

  1. 进一步可以利用反射,避免在UserServlet中写太多if/else

  2. 进一步如果每个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() 方法即可实现。

  1. 导包: commons-beanutils-1.8.0.jar 和 commons-logging-1.1.1.jar

  2. 写工具类

    public class WebUtils {
        public static <T> T copyParamToBean(Map map, T bean) {
            //利用泛型即可在返回时不需要类型转换。
            try {
                BeanUtils.populate(bean, map);
            } catch (Exception e) {
                e.printStackTrace();
            }
            return bean;
        }
    }
    
  3. 使用

    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程序处理请求之后再转发到另一个浏览器页面,从而实现交互。

以图书管理模块为例。

  1. manger.jsp

    点击图书管理跳转到BookServlet注册的域名上

  1. BookServlet

    处理请求,然后将数据放在request域中,然后请求转发pagers/manager/manager.jsp

  1. pagers/manager/manager.jsp

    从请求域中拿出数据。

权限约束

测试

从最小的模块开始测试,逐渐放大范围。

优化删除操作

点击删除按钮后询问用户是否删除

重定向和请求转发

重定向:想直接显示某个地址。

请求转发:只是对请求做一个处理,向请求域中添加信息,然后再将请求发送给某个页面。

重定向在使用时默认地址中不包含工程名,需要自己手动添加。

一个表单多种操作

有时候一个表单需要根据请求类型不同来做不同操作。比如图书模块中的添加和修改操作。

添加和修改操作都跳转到book_edit页面,book_edit页面中的表单在提交时根据请求的类型来做出相应操作。

解决方法:

  1. 在添加请求中添加一个method参数,它的值为addBook,表示它是添加操作,而且addBook就是BookServlet程序中添加图书的方法。

在修改请求中添加一个method参数,它的值为updateBook,表示它是修改操作,而且updateBook就是BookServlet程序中修改图书的方法。

分页

详见BookServlet中的page方法,book_manager.jsp页面

获取地址栏地址

js的一个Location地址栏对象,它有一个href属性,可以获取浏览器地址栏中的地址,并跳转。

分页边界

应对用户无厘头跳转操作,比如总记录20条,用户要跳转到-50页,或者100页。规定用户如果要跳转到-50页,系统跳转到第一页,要跳转到100页,系统跳转到20页。

解决方法:

  1. 只在浏览器端判断是可以被破解的,所以选择在服务端解决。

  2. 在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;
    }
    
  3. 还要在设置总页码数pageTotal之后再设置当前页码pageNo,因为在getPageItems方法中需要使用的修改过后的pageNo。

跳转到当前页附近的分页

效果: 4,5,【6】,7,8

总页码为10

  1. 总页码小于等于5

    第一页 -- 总页码

    1

    1,2

    1,2,3

    1,2,3,4

    1,2,3,4,5

  2. 总页码大于5

    1. 前三页:第一页---第五页

      【1】,2,3,4,5

      1,【2】,3,4,5

      1,2,【3】,4,5

    2. 后三页:总页码-4 ---- 总页码

      6,7,【8】,9,10

      6,7,8,【9】,10

      6,7,8,9,【10】

    3. 中间:当前页-2 --- 当前页+2

      4,5,6,7,8

      5,6,7,8,9

前台分页

按价格区间搜索

登陆和注销

登陆时将User添加到session域中,注销时将session清除。

登陆:

注销:

表单重复提交

当用户提交完请求,浏览器会记录下最后一次请求的全部信息。当用户按下f5刷新后,就会发起浏览器记录的最后一次请求。

案例:图书管理模块的添加图书功能。

如果使用请求转发,那么浏览器每刷新一次就会访问一次addBook方法,添加一次图书。为了解决这个bug,需要使用请求重定向。

验证码

客户端发送请求后,服务器生成一个验证码,将验证码放在session中,然后将带有验证码的表单发送给浏览器。用户在浏览器填写完后提交表单,服务器获取session中的验证码然后删除它,然后比较验证码是否和session中验证码相等,。这样即使用户第二次提交表单也会被阻止。

验证码使用谷歌的kaptcha实现。

使用步骤:

  1. 导包:

  2. 在web.xml 中配置 KaptchaServlet类,访问这个类会返回一个验证码图片,并且将验证码的字符串加入session中

  3. 表单中使用:

  4. 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来实现事务管理

修改操作

  1. 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();
        }
    }
    
  2. 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
    
        }
    
    }
    
  3. 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的 调用目标资源 的原理。

  1. transactionFilter

  1. web.xml

    拦截所有请求。

  2. 还要在BaseServlet中将异常抛出,否则service层在执行过程中产生的异常会被BaseServlet捕获,而 filter中收不到异常,继而就无法实现事务的提交和回滚。

展示错误页面

比如,当使用事务时,如果某个操作有异常,那么浏览器就会显示空白,这样让用户的体验很差,所以可以利用tomcat来统一展示错误页面。

  1. web.xml

  1. 在TransactionFilter中将异常抛给tomcat,否则tomcat收不到异常