如果你想开发一个应用(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根目录下的\work\Catalina\localhost\ROOT\org\apache\jsp目录中的文件,可以看到他们关系的证据:

image

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

List<Todo> todos=todoDao.getAll();
pageContext.setAttribute("todos",todos);

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

很明显,就是把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代码?估计全部都要删除吧:)