人如果没有梦想,那么和一只咸鱼有什么分别。作为一个程序员,哪怕我们不能改变世界,也会有让更多的,更多的人使用我们应用的梦想。那么现在回到我们的jTodos应用,我们当然会想要更多的人使用我们的应用。假设现在,如果有多个人使用我们的应用会是什么情况呢?
确保用户之间的隔离
首先我们使用两个浏览器来模拟多用户测试,首先在一个浏览器内输入张三–下午三点钟看书。
这里我们要首先清理数据库,不能让历史上的垃圾数据干扰现在的操作
这样做既不方便又容易出错,更好的方法是使用单元测试技术并创立一个测试库,每次进行测试的时候都进行一次初始化,现在暂时采用手动方式清理数据。
然后假设另一个使用另一个浏览器(在jsp中一个会话即为一个浏览器的打开到关闭,即可以用不同的浏览器来模拟不同的用户)进入这个页面 “哇偶 张三是谁,他在下我三点看书管我啥事”
这时候,想想我们想要实现的功能,最后在pc操作系统的左上角弄个便签记录一下:
- 每个用户都有自己的todo列表
- 各个用户的todo列表不能互相干扰
- 每个用户可以方便的访问自己的列表
现在要想想怎么实现这个功能了。
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配置
让我们看看浏览器的源代码发送了什么:
换句话说,也就是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
目录中的文件,可以看到他们关系的证据:
这个是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。
即当为空白列表的时候,也会进入循环体,这个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,用户隔离完美的实现,接下来随便输入几个记录,情况如下:
ok 完美
redirect与forward
redirect的意思为跳转
forward的意思为转发
使用redirect进行页面跳转,实际上是一个与客户端浏览器进行交互的过程
使用forward进行页面转发,实际上是在一个请求周期内,服务器自己进行计算,将结果通过响应返回给客户端的过程。
这就到这里他们两个最大的一个区别,即redirect的url地址会进行变化,而forward的地址栏则不会发生变化
下面是一个redirect页面跳转的示意图
1 客户端发送请求
2 服务端告诉客户端,所需的内容在页面b上
3 客户端像页面b发送请求
4 获得所需内容
下面是一个forward页面跳转的示意图
1 客户端发送请求
2 服务器端发现所需内容在页面b上,即转发到页面b中,同时将客户端发送的请求和响应作为参数传给页面b
3 页面b将客户端所需内容发送给客户端
具体使用哪一种,就要具体情况具体分析了,而之后,很可能使用了前后端分离技术后,两种方式都不使用。
后记
本章内容好长呀,但总算结束了,接下来正如刚刚提到了一下,应该会引入前后端分离和Spring框架的内容,难度可能会有所提高,继续努力吧。而之前写过的jsp和servlet代码?估计全部都要删除吧:)