• 如果你想开发一个应用(1-6)


    人如果没有梦想,那么和一只咸鱼有什么分别。作为一个程序员,哪怕我们不能改变世界,也会有让更多的,更多的人使用我们应用的梦想。那么现在回到我们的jTodos应用,我们当然会想要更多的人使用我们的应用。假设现在,如果有多个人使用我们的应用会是什么情况呢?

    确保用户之间的隔离

    首先我们使用两个浏览器来模拟多用户测试,首先在一个浏览器内输入张三--下午三点钟看书。

    这里我们要首先清理数据库,不能让历史上的垃圾数据干扰现在的操作
    这样做既不方便又容易出错,更好的方法是使用单元测试技术并创立一个测试库,每次进行测试的时候都进行一次初始化,现在暂时采用手动方式清理数据。

    然后假设另一个使用另一个浏览器(在jsp中一个会话即为一个浏览器的打开到关闭,即可以用不同的浏览器来模拟不同的用户)进入这个页面 “哇偶 张三是谁,他在下我三点看书管我啥事”

    这时候,想想我们想要实现的功能,最后在pc操作系统的左上角弄个便签记录一下:

    1. 每个用户都有自己的todo列表
    2. 各个用户的todo列表不能互相干扰
    3. 每个用户可以方便的访问自己的列表

    现在要想想怎么实现这个功能了。

    YAGNI

    软件的开发不同于建设一个建筑,比如盖一栋大楼之类的,虽然他们通常这样的类比。软件的开发过程中总是充斥中不停的变化,变化如此之频繁,以至于很可能你的需求分析,基本设计刚刚出炉,就已经过时。

    但是,注意,这里强调一下,并不是说要完全放弃分析和设计,有时候不经思考的反复试错,穷举方式,可能也可以找到答案,但适当的思考可以达到事半功倍的效果,关于现在,我们就所需要的功能来思考一下:

    • 每一个todo都与用户相关联
    • 每个用户都能保存自己的清单(目前来说不考虑清单分组,即至少能保存一个清单)
    • 用户要方便的找到自己的列表,以便多次访问
    • ......

    头脑风暴一经展开,貌似就再也停不下了。这时候我们会有各种各样的想法,比如,为每个用户来一个炫目的登录页面,每个todo是不是要加一个标题和内容?是不是要给清单进行分组,给每个分组增加一个说明?等等,一旦发生这种情况,我们就要注意了,这种蔓延会一发不可收拾,这时,要牢记敏捷开发的一个信条:YAGNI,他是You aint gonna need it的首字母,意思就是你不需要他!!作为一个软件开发者,要知道很多项目失败的原因之一就是开始的计划过于宏大了,有个能工作的简陋的应用,总比一个超炫但不能工作的应用强,所以,就现在这个应用目前来说,经过削繁就简后,决定用下面的方式实现:

    • 用户信息很简单,只有一个用户名和id
    • 将用户名嵌入到url中,已进行区分
    • todo与用户id进行关联

    接下来就要想这个实现所需的技术了。

    servlet

    jsp很灵活方便,但在页面嵌入代码这是一个硬伤,他不容易开发,测试,尤其是进行复杂的逻辑时,更需要在代码中进行结构化的开发。老规矩,首先进行一个hello world进行一下测试。

    首先还是需要引入所需的包,在pom.xml添加节点:

    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
        <version>3.1.0</version>
        <scope>provided</scope>
    </dependency>
    

    注意这里多了一个scope节点,在maven中,这个节点可以有四个值,分别为:
    1 compile 默认值,适用所有阶段,并会随项目一起发布
    2 provided 与compile类似,但不发布,由容器会提供此库
    3 runtime 只在运行时适用
    4 test 只在测试时使用 如junit包
    这里tomcat会提供servlet库,所以使用provided选项

    既然是代码文件,就要放到一个包中,首先创建servlet包:com.niufennan.jtodos.servlet,并创建TodoServlet,然后让其继承HttpServlet ,一个最简的servlet类就完成了。

    在servlet中,需要覆盖实现doGet方法完成get操作,doPost方法完成Post操作,这里只是为了展示一下helloworld,所以最终代码如下:

    @WebServlet("/todos")
    public class TodoServlet extends HttpServlet
    {
        public void doGet(HttpServletRequest request, HttpServletResponse response)
                throws ServletException, IOException {
            PrintWriter out = response.getWriter();
            out.println("hello world");
        }
    }
    

    Servlet 3.0版本中提供了WebServlet注解,可以以注解方式提供url,从而避免了在web.xml中又臭又长的xml配置

    让我们看看浏览器的源代码发送了什么:

    image

    换句话说,也就是PrintWriter打印出什么,就像客户端发送什么,那么如果想输出html怎么办呢?很简单,打印html即可:

     PrintWriter out = response.getWriter();
     out.println("<b>hello world<b>");
    

    这样,就可以让浏览器将hello world 加粗

    jsp与servlet

    可以看到,servlet与jsp很相似,连对象名都是同样的request和response,那么他们之间有什么关系呢?答案很简单,jsp是servlet的进化版,如果说servlet是妙蛙种子,那么jsp就是妙蛙草妙蛙花(仅仅是形态,不代表能力),在tomcat根目录下的workCatalinalocalhostROOTorgapachejsp目录中的文件,可以看到他们关系的证据:

    image

    这个是index.jsp生成的文件,其中index_jsp.java中的部分代码为:

      List<Todo> todos=todoDao.getAll();
      pageContext.setAttribute("todos",todos);
    
      out.write("
    ");
      out.write("<div class="ui two column centered relaxed  grid">
    ");
      out.write("    <div class="column row"></div>
    ");
      out.write("    <div class="column ">
    ");
      out.write("        <h2 class="ui huge header center violet aligned">jTodos!</h2>
    ");
      out.write("        <div class="ui raised segment">
    ");
      out.write("            <form action="index.jsp" method="post"  class="ui fluid action input">
    ");
      out.write("                <input type="text" name="todo" placeholder="请输入一个备忘录项目">
    ");
      out.write("                <button type="submit" class="ui button">OK</button>
    ");
      out.write("            </form>
    ");
      out.write("            <div class="ui aligned huge selection divided list">
    ");
      out.write("               ");
      if (_jspx_meth_c_005fforEach_005f0(_jspx_page_context))
        return;
      out.write("
    ");
      out.write("            </div>
    ");
      out.write("        </div>
    ");
      out.write("    </div>
    ");
      out.write("</div>
    ");
      out.write("
    ");
      out.write("<script src="https://cdn.bootcss.com/semantic-ui/2.2.13/semantic.min.js"></script>
    ");
      out.write("</body>
    ");
      out.write("</html>
    ");
    

    很明显,就是把jsp内的html进行格式化输出,其中_jspx_meth_c_005fforEach_005f0是另一个方法,方法体内循环输出todos的各项。

    也就是说,jsp把繁琐易错的字符串拼接处理进行了封装,由系统自己处理

    这样就解释了为什么jsp页面第一次访问会慢一些,以为最开始的时候要生成.java文件,并且编译为.class文件。

    动态url##

    你可能很奇怪,嘀嘀咕咕的讲了一大堆,和我们的需求有什么关系?这是因为servlet有一项非常重要的功能,就是可以使用通配符,进行动态的url创建。举个例子,下面修改WebServlet注解的参数@WebServlet("/todos/*"),其中*就表示通配符,运行起来在浏览器中输入地址:

    http://localhost:8080/todos/zhangsan
    http://localhost:8080/todos/lisi
    http://localhost:8080/todos/aaa
    http://localhost:8080/todos/nihao
    http://localhost:8080/todos/hello
    

    返回的均为相同的内容,也就是说,访问的为同一个servlet类。通过这点,我们就可以在url中嵌入用户名,实现用户列表的隔离。

    由于对于url中获取用户名需要频繁操作,所以我们把它独立到一个工具类中,代码很简单:

    package com.niufennan.jtodos.utils;
    
    public class UrlUtil {
        public static String getUserName(String url) {
            String[] temp = url.split("/");
            if(temp.length==5) {
                return temp[temp.length-1];
            }
            return "";
        }
    }
    

    还记得模型层内,Todo类中的那个userId么,现在派上用场了,继续之前的思路,暂时采取最简单的模型,只有一个用户id和用户名,这里只贴出模型的代码如下:

    public class User {
        private int Id;
        private String name;
    	getter setter ...
    }
    

    在db中,userId与users表的id字段相关联。

    相应的,还需要添加UserDao类封装对用户的db操作

    public class UserDao {
        public User getUserByName(String name){
            Connection connection= null;
            PreparedStatement statement=null;
            ResultSet resultSet=null;
            List<Todo> list=new ArrayList<Todo>();
            try{
                connection = DatabaseHelper.getConnection();
                statement= connection.prepareStatement("select * from users where name=?");
                statement.setString(1,name);
                resultSet=statement.executeQuery();
                if (resultSet.next()){
                    User user=new User();
                    user.setId(resultSet.getInt("id"));
                    user.setName(resultSet.getString("name"));;
                    return user;
                }
            }catch (SQLException ex){
                new RuntimeException(ex);
            }
            finally {
                DatabaseHelper.close(resultSet,statement,connection);
            }
            return null;
        }
        public User get(int id){
            Connection connection= null;
            PreparedStatement statement=null;
            ResultSet resultSet=null;
            List<Todo> list=new ArrayList<Todo>();
            try{
                connection = DatabaseHelper.getConnection();
                statement= connection.prepareStatement("select * from users where id=?");
                statement.setInt(1,id);
                resultSet=statement.executeQuery();
                if (resultSet.next()){
                    User user=new User();
                    user.setId(resultSet.getInt("id"));
                    user.setName(resultSet.getString("name"));;
                    return user;
                }
            }catch (SQLException ex){
                new RuntimeException(ex);
            }
            finally {
                DatabaseHelper.close(resultSet,statement,connection);
            }
            return null;
        }
        public void save(User user){
            Connection connection=null;
            PreparedStatement statement=null;
            try {
                connection = DatabaseHelper.getConnection();
    			//设置返回自增长Id
                statement=connection.prepareStatement("INSERT INTO users(name) VALUES (?);",Statement.RETURN_GENERATED_KEYS);
                statement.setString(1,user.getName());
                statement.executeUpdate();
    			resultSet=statement.getGeneratedKeys();
    			//获取自增长Id
                if(resultSet.next()){
                    return resultSet.getInt(1);
                }else{
                    return -1;
                }
    
            }catch (SQLException ex){
                throw  new RuntimeException(ex);
            }finally {
                DatabaseHelper.close(null,statement,connection);
            }
        }
    }
    

    可以通过statement.getGeneratedKeys();获取自增长Id

    然后在TodoDao类增加getTodoByUserId方法,以便获取当前用户的todo列表:

    public List<Todo> getTodoByUserId(int userId){
        Connection connection= null;
        PreparedStatement statement=null;
        ResultSet resultSet=null;
        List<Todo> list=new ArrayList<Todo>();
        try{
            connection =DatabaseHelper.getConnection();
            statement= connection.prepareStatement("select * from todos where userId=?");
            statement.setInt(1,userId);
            resultSet=statement.executeQuery();
            while (resultSet.next()){
                Todo todo=new Todo();
                todo.setId(resultSet.getInt("id"));
                todo.setItem(resultSet.getString("item"));
                todo.setCreateTime(resultSet.getTimestamp("createtime"));
                todo.setUserId(resultSet.getInt("userid"));
                list.add(todo);
            }
        }catch (SQLException ex){
            new RuntimeException(ex);
        }
        finally {
            DatabaseHelper.close(resultSet,statement,connection);
        }
        return list;
    }
    

    呃,基础设施总算搭建完成,看上去好复杂,但还算清晰,这块永远是框架优化的终点,接下来回到servlet类,将已有的业务逻辑完成

      public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        String name=UrlUtil.getUserName(request.getRequestURL().toString());
    
        if("".equals(name)){
            //跳转至错误页面
            response.sendRedirect("error.jsp");
    
        }
        //实例化User数据库操作类
        UserDao userDao =new UserDao();
        User user=null;
        //获取用户
        user=userDao.getUserByName(name);
        if(null==user){
            //新用户
            user=new User();
            user.setName(name);
            user.setId(userDao.save(user));
        }
        //获取todo列表
        TodoDao todoDao=new TodoDao();
        List<Todo> list=todoDao.getTodoByUserId(user.getId());
        System.out.println(list.size());
        //将list和name存入request以备jsp页面使用
        request.setAttribute("todos",list);
        request.setAttribute("userid",user.getId());
        request.getRequestDispatcher("/todos.jsp").forward(request,response);
    }
    
    //post
    public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
        request.setCharacterEncoding("utf-8");
        int userid=Integer.parseInt(request.getParameter("user"));
        if(request.getParameter("todo")!=null){
            Todo todo=new Todo();
            todo.setCreateTime(new Date());
            todo.setItem(request.getParameter("todo"));
            todo.setUserId(userid);
            TodoDao todoDao=new TodoDao();
            todoDao.save(todo);
        }
        //获取user
        UserDao userDao=new UserDao();
        User user=userDao.get(userid);
        //页面跳转
        response.sendRedirect("/todos/"+user.getName());
    }
    

    结合注释看,并且有之前jsp的基础,代码应该不难理解,运行服务,在url地址中输入张三,http://localhost:8080/todos/zhangsan。阿欧,首先发现的是一个小bug。

    image

    即当为空白列表的时候,也会进入循环体,这个bug也很好解决。对列表的长度进行一下判断就可以了:

     <div class="ui aligned huge selection divided list">
        <c:if test="${fn:length(todos)>0}">
            <c:forEach var="todo" varStatus="status" items="${todos}">
                <div class="item">
                    <span class="right floated content"><fmt:formatDate value="${todo.createTime}" type="date" pattern="yyyy-MM-dd HH:mm:ss"/></span>
                    <span class="left floated header">${status.index+1}.${todo.item}</span>
                </div>
            </c:forEach>
        </c:if>
    </div>
    

    注意这里使用了jstl的fn前缀,需要引入库:

    <%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions"%>
    

    再次运行,就没有了这个空白项,这是查看数据库的user表,用户zhangsan记录已经出现。

    接下来为张三随便输入几个记录,然后在url中换成lisi,有一个空白的list,用户隔离完美的实现,接下来随便输入几个记录,情况如下:

    image

    ok 完美

    redirect与forward##

    redirect的意思为跳转
    forward的意思为转发

    使用redirect进行页面跳转,实际上是一个与客户端浏览器进行交互的过程
    使用forward进行页面转发,实际上是在一个请求周期内,服务器自己进行计算,将结果通过响应返回给客户端的过程。

    这就到这里他们两个最大的一个区别,即redirect的url地址会进行变化,而forward的地址栏则不会发生变化

    下面是一个redirect页面跳转的示意图

    image

    1 客户端发送请求
    2 服务端告诉客户端,所需的内容在页面b上
    3 客户端像页面b发送请求
    4 获得所需内容

    下面是一个forward页面跳转的示意图

    image

    1 客户端发送请求
    2 服务器端发现所需内容在页面b上,即转发到页面b中,同时将客户端发送的请求和响应作为参数传给页面b
    3 页面b将客户端所需内容发送给客户端

    具体使用哪一种,就要具体情况具体分析了,而之后,很可能使用了前后端分离技术后,两种方式都不使用。

    后记

    本章内容好长呀,但总算结束了,接下来正如刚刚提到了一下,应该会引入前后端分离和Spring框架的内容,难度可能会有所提高,继续努力吧。而之前写过的jsp和servlet代码?估计全部都要删除吧:)

    image

  • 相关阅读:
    一则线上MySql连接异常的排查过程
    有一种娱乐叫看别人编程
    程序员DNS知识指南
    中国式开源
    RSS与公众号
    论国人的素质和一个公司的商业道德
    《阿里游戏高可用架构设计实践》阅读笔记
    《淘宝数据魔方技术架构解析》阅读笔记
    软件体系架构_系统质量属性场景描述_结合《淘宝网》实例
    《余额宝技术架构及演进》阅读笔记
  • 原文地址:https://www.cnblogs.com/jiangchao226/p/7867375.html
Copyright © 2020-2023  润新知