用JSP/Servlet开发简单的用户注册系统

  作者:板桥banq

<<Java实用系统开发指南>>

本章以一个简单的用户注册系统为例,介绍J2EEJSP/Servlet技术的应用,JSP/Servlet 属于J2EE中的Web部分,负责处理客户端的请求,然后进行简单的处理,传送到后台实现进一步商业逻辑运算。

2.1  需求分析

用户注册验证几乎是每个Web系统都需要的系统,只有那些注册过的用户才能访问一些特定的资源。

用户注册验证系统是安全权限系统的重要组成部分。安全权限系统主要是解决“WhoWhat进行How的操作”问题,安全权限系统中还有一个重要组成部分就是资源访问控制权限,只有被授权后的用户才能访问特定的资源,这部分功能将在以后章节讨论实现。

在本章节将讨论一个简单的用户注册验证系统的实现,该系统如图2-1所示。

servlet

2-1  用例图

用户只有通过注册才能获得用户名和密码,然后通过该用户名和密码登录系统,系统实现验证功能,将用户输入的用户名和密码与系统保存的用户名和密码进行核对检查。如果核对无误,表示验证通过,允许该用户访问系统的资源。

用户注册登录后,可以修改自己的资料和密码,可以通过自己注册的信箱获取丢失的密码。

2.2  系统设计

用户注册验证系统可以有很多种架构设计,从简单系统到复杂系统的单点登录以及JAASJava验证和授权服务框架)等。为对J2EEJSP/servlet以及JavaBeans有初步了解,将使用JSP/JavaBeans技术完成一个简单的用户注册验证系统。

JavaBeansJava的软件组件架构技术,这些组件可以非常有效地、方便地组装成一个应用系统。近一个世纪以来,同样的想法已经被应用在不同领域,这就是工业革命。大量工厂的组装生产线出现,软件组件也是基于这样的概念:一次性创建小而可重用的模块,然后可以尽可能地重复使用它们。

JavaBeans最早是一种可视化组件技术,专门用来实现应用系统的图形界面显示,如按钮等。一个按钮作为一个独立的、自我封闭的基本单元,专门响应按键事件。这样的按钮组件可以大量重复地应用到很多应用系统中。

后来,JavaBeans开始大量使用在服务器端技术中,成为一种非可视化组件技术,无论是可视化还是非可视化,JavaBeans提供了开发建立单独、可重用功能块的基础技术,通过使用JavaBeans可以建立一个个相互独立、有一定封装性、粒度细化的功能模块,然后可以在其他很多应用系统中重复使用这些功能,达到软件生产的最大效率。

2.2.1  JSP/ServletJ2EE

1章介绍了使用多线程和线程池来提高应用系统的处理性能,这已经形成一种通用的性能提高处理模式。但是有一个最大问题是,由于线程难以使用和驯服,如果每个应用系统的开发都需要开发者了解如何使用线程池,如何防止多线程同步死锁,这将大大妨碍开发者使用Java软件系统开发应用系统的热情。

J2EEJSP/Servlet开发者不用担心线程以及同步等问题,只要像往常一样编程,JSP/Servlet容器会自动使用线程池等技术来支持系统的运行。因此,JSP/Servlet的实质是一种线程技术,JSP在运行时会被编译成Servlet进行运行,如图2-1所示。

servlet

2-1  JSP/Servlet原理图

如图2-1所示,当客户端先服务器发出一个请求时,Servlet容器会分配一个线程专门处理这个请求,线程内容就是JSP/Servlet应用程序,这如同执行Doug LeaPooledExecutorexecute方法,通过execute方法加载一个线程应用程序,具体线程池原理可见“高性能聊天系统”一章。

这样,通过JSP/Servlet容器技术,帮助更多商用系统实现了性能的最优化,而同时无需开发者掌握了解更多的线程底层技术,从而可以将更多精力集中在业务问题的解决上。这也是J2EE框架推出的主要目的。

J2EE框架技术主要包括JSP/ServletJavaBeansEJB以及JMS等技术,其中JavaBeansEJB是主要运行业务核心的,可以实现分布式计算,从而大大地动态扩展了J2EE的处理性能,也省却了开发者开发底层通信技术,转而将眼光关注在本业务范围内的软件可复用技术组件开发。

J2EE为代表的多层结构系统正在日益得到推广,传统的C/S结构只有两层,将业务逻辑要么紧紧封装在数据库端,要么耦合在客户端,这种带来的缺陷就是很多业务功能无法复用,维护扩展起来困难。因此就无法满足不断变化的需求,需求总在不断变化,客户永远是正确的,软件所要做的就是不断满足客户的要求。但是因为修改一个界面问题,或更改一个数据表结构,就导致整个软件系统错误百出,牵一而动百,客户能不抱怨吗?这就是典型的C/S系统的问题。

为了更好的服务客户,满足市场,软件复用技术诞生,模式、组件或框架应运而生,这些技术就催生了多层结构,在三层或者更多层的结构中,通过模式和组件将系统的核心功能封装起来,和数据库以及界面都没有任何联系,是独立的一层,再通过特有的框架软件,将这个多层结构纳入框架的可管理系统中去,这样整个系统就可谓泾渭分明,修改界面或数据表再也没那么可怕,甚至修改或者增加核心功能也没那么麻烦,都实现组件管理了,就像往电脑底板上插卡一样方便的为系统实现动态扩展。

B/S 是这种多层结构的一个代表,JSP/Servlet界面部分只负责浏览器表现层方面的功能,而重要的逻辑运算则封装在中间层JavaBeansEJB中,通过这样分工可以实现每层的精工细作。

有过PerlPHP使用经验的人习惯将脚本语言和HTML混合在一起编制,这样做开发速度虽然比较快,但是编写好的代码难以维护,特别是网页设计师比较难以直接编辑这种混合代码。

通常完成一个项目需要多种角色的配合,例如网页设计师和程序员,如果HTML代码和Java代码混合在一个JSP文件中,那么两种角色交互修改JSP页面时就会发生混乱,网页设计师打开JSP页面,会发现里面很多自己看不懂的Java代码;从另外一个方面说,程序员修改JSP页面的Java代码时,必然涉及输出效果的改变,但是对于网页设计师为复杂效果编制的复杂HTML代码也感到无从下手。那么这样通常是由程序员先进行程序设计和编制,在程序功能测试通过后,再交由网页设计师进行美工设计,无疑降低了开发效率,延误了开发时间。

如果将Java代码从JSP页面中分离出来,实现多层结构,网页设计师和程序员就可以同时工作,并且各自专注各自领域,网页设计师们可以集中精力关注JSP/HTML的实现,设计出更加精美的界面图形;而程序员们则可以关注如何使用JavaBeans技术建立一个可重用的组件系统。

2.2.2  结构设计图

如图2-2所示,客户端浏览器向服务器端发出请求Request信号,首先经过第1JSP/Servlets处理,然后提交第2JavaBeans进行核心功能的处理。其中,JavaBeans可能需要访问第3层数据库系统。

servlet

2-2  JSP/JavaBeans结构图

2-2显示了一个基于Servlet/JSP + JavaBeansWeb层结构图,使用JavaBeans封装应用系统的重要功能,是系统的核心中间层。JSPServlet负责客户端界面相关控制和显示,在实际应用中也有几种区别:

·          完全JSP类型。也就是说,JSP负责显示,在JSP表单中提交的对象也是JSPxxx.jsp中的下列语句:

<form action="xxx.jsp" method="post">

       ….

 </form>
这表示xxx.jsp同时也负责处理自身提交的表单数据,那么在整个流程系统中都没有使用到Servlet技术。本项目以及后面讨论的Jive系统都是采取这种结构。

·          Servlet/JSP混合型。Servlet作为一个总的表单提交入口,负责接受解析提交的表单数据,上述form中的action值是某个Servlet程序,这种形式使用Servlet作为一个总的调度器,而且因为有总的Servlet入口,就可以使用Servlet Filter等技术,以后章节讨论的MVC模式和Strut框架属于这种类型。

这两种类型各有优缺点。前者使用起来方便,但是项目比较复杂,JSP页面很多的情况下容易造成混乱;后者因为有专门的流程控制,所以在项目复杂情况下可以有清晰的结构流程控制。

两种不同结构的采用,决定了不少相关技术具体实现的不同。例如:如果要在所有请求Request被处理之前进行一个统一处理,诸如设置编码或用户权限验证等,各自实现方式就不一样。前者是使用includeJSP语句将这些实现统一功能的JSP包含在每个JSP页面中;而后者则可以在统一的Servlet中处理,或者使用Servlet Filter技术拦截后统一处理。

2.2.3  JSP/JavaBeans技术要点

JavaBeans的编写很简单,主要是由属性和方法组成,参见如下代码:

public class SignIn {

  private String userid = null;                    //JavaBeans属性

  private String password = null;                 //JavaBeans属性

  public String getAction(){    return action;  }   //JavaBeans方法

  public void setAction(String action){            //JavaBeans方法

    this.action = action;

  }

  public String getUserid(){    return userid;  }

  public void setUserid(String userid){

    this.userid = userid;

  }

  public String getPassword(){    return password;  }

  public void setPassword(String password){

    this.password = password;

  }

}

 

这段代码被保存成SignIn.java,经过编译后变成SignIn.class,可以在JSP中直接调用这个JavaBeans,参见如下代码:

<jsp:useBeans id="signIn" class="SignIn" scope="page|request|session|application" />

其中

·       signIn为该JavaBeansJSP中的标识,可以自己任意定义,相当于类的实例名称(注意大小写);SignInJavaBeans的文件名,如果SignIn有包名,如com.jdon.simpleregister.SignIn,那么这里就成为class="com.jdon.simpleregister. SignIn"scope为定义JavaBeans生存周期。

·       page表示这个JavaBeans仅仅生存在该JSP文件及此文件中的静态包含文件中。即每次该JSP文件产生时,SignIn产生一次对象实例,相当于new SignIn (),这是最常用的情况。

·       request表示这个JavaBeans在该页面发出的请求中有效。

·       session表示这个JavaBeans将作为一个对象绑定于HttpSession中,直至用户退出系统前一直有效。也表示这个用户登陆进入系统以后,本JavaBean的实例将一直保存在系统内存中,直到该用户离开本系统。

用户离开本系统通过两种方式实现:用户主动单击“退出系统”按钮;或者根据系统的设置,在系统设置的TimeOut时间内如果该用户没有发出任何请求,那么系统将自动删除该用户的session。使用seesion功能可以使多个JSP程序同时操作一个JavaBean,那么这个JavaBean就可以作为传统意义上的“全局变量”。session的这种保存状态机制并不能经常使用,否则容易引起内存泄漏,一般一个系统中只有一两种有状态应用。

·       application表示这个JavaBeans将作为一个对象绑定于application中,在所有应用中都有效。与session相对用户来说,application是相对应用程序的,一般来说,一个用户有一个session,并且随着用户离开而消失;而application则是一直存在,类似一个servlet程序,类似整个系统的“全局变量”,而且只有一个实例。因此sessionapplication都可以用来作为缓冲,提高JSP的运行性能。

并不是所有的JavaBeans都可以被上述一条语句简单地直接创建的。这就类似Java类的初始化,有的类需要初始化参数。可以使用<jsp:setProperty>标签来初始化这个JavaBeans的属性,初始化JavaBeans属性值的格式如下:

<jsp:useBeans id="signIn" class="SignIn" scope="session" />

<jsp:setProperty name="signIn" property=" userid"  value="jdon.com"/>

</jsp:useBean>  

这表示将SignIn中的属性值userid设置为字符串数值jdon.com。具体<jsp:setProperty> 语法参考http://java.sun.com/products/jsp/syntax/1.1/syntaxref1114.html

<jsp:setProperty>专门用来设置bean的属性,必须首先在这个bean中声明这个属性的设置set方法,比如需要设置beanuserid属性,必须在这个bean中有setUserid方法;需要设置beanabc属性,必须在这个bean中有setAbc方法,其中setAbc中的A作为属性名第一个字母要大写,这些都是硬性的技术规定。

初始值来源可以人为设定,有下列几种初始值来源:

1)直接指定某个属性值为某个常量,如上述代码中是将userid直接赋值为jdon.com,当然也可以是一个在运行时刻计算的运算表达式。

2)表单中用户填写输入的值(属性名必须和bean中属性名称相同的)。表单输入的值是保存在HTTP的请求参数中,例如<jsp:setProperty>语句包含在signin.jsp文件中,如果通过http://localhost:8080/signin.jsp?userid=jdon.com&password=123来调用,那么系统会将userid的数值和password数值通过。下列语句自动初始化bean的相应属性值:

<jsp:useBeans id="signIn" class="SignIn" scope="session" />

<jsp:setProperty name="signIn" property=" userid" />

<jsp:setProperty name="signIn" property=" password" />

</jsp:useBean>  

如果表单中的属性名很多,像上述代码那样逐个写入是很费时间的,因此使用property="*"来代替所有的属性名将非常简便。

property="*"被很多人称为超级星就是因为它有这样神奇的超级功能,简化了很多无谓的枯燥语句。property="*"将用户提交的表单参数自动写入JavaBeans的相应属性值中。因为表单提交的参数都是String型的,而在JavaBeans中同名属性有各种类型,那么系统将实现String类型到各种参数类型的自动转换,可以参考<jsp:setProperty>的参数转换表。

以用户登录为例,当用户登录时,需要输入用户名和密码,然后通过表单提交到ServletJSP进行处理,假如在signin.jsp是用户登录界面,其中HTML代码如下:

<form action="login.jsp" method="post">

  用户:<input type="text" name="userid" size="10"><br>

  密码:<input type="password" name="password" size="10"><br>

  <input type="submit" value="登陆">  <input type="Reset" value="Reset">

</form>

该表单是向login.jsp提交Form表单的内容数据,分别是useridpassword。在login.jsp中,有如下代码:

<jsp:useBean id="signIn"  scope="session"  class="signup.SignIn" />

<jsp:setProperty name="signIn " property="*"/>

输出显示 用户是: <%=signIn.getUserid()%>

输出显示 密码是: <%=signIn.getPassword()%>

login.jsp的输出结果便是在signin.jsp中输入的用户名和密码,因为在signin.jsp中的userid通过了SignIn的同名方法setUserid(String userid)将数据值赋给了SignIn的属性userid;而password通过了SignIn的同名方法setPassword(String password)将数据值赋给了SignIn的属性password

JSP/JavaBeans这个技术特点大大减轻了编程工作量,将会在具体编程中大量使用。

2.2.4  JDBC和连接池

JDBCJava中支持基本SQL一个底层通用API接口,它在不同数据库的基础上提供了一个统一的操作界面,这样就可以在Java中方便地直接对数据库进行各种操作。JDBC API利用了Java跨平台优势,对企业数据实现了一次编写到处运行的目标。

JDBC API一般是由具体数据库厂商提供,例如MySQL就提供了MySQL-Connector -XXX.jar作为JDBC API支持,在本系统开发时,需要将该驱动包放入classpath中,以便本系统的应用程序在编译和运行时能定位寻找到该驱动包。

JDBC API主要提供了下列基本数据库操作:

(1)     装载数据库驱动程序。通过下列语句装载具体数据库的驱动程序:

如果是MySQL,则语句如下:

Class.forName("com.mysql.jdbc.Driver"). newInstance();

com.mysql.jdbc.DriverMySQL JDBC驱动程序名,可以在具体数据库JDBC的驱动包(*.jar)中找到。MySQL的驱动程序包是在http://www.mysql.com上下载,最近的包名叫mysql-connector-java-xxx.jar。开发和运行JDBC程序时,这个驱动程序包都要放在JavaClassPath中。

2)驱动程序注册后,就可以获得与数据库的一个连接,代码如下:

Connection conn = DriverManager.getConnection("连接URL");

连接URL有固定格式写法,如果是MySQL,那么写法如下:

jdbc:mysql://[hostname][:port]/dbname[?param1=value1][&param2=value2]...

如果MySQL数据库需要用户名和密码才能访问,那么URL格式如下:

jdbc:mysql://localhost/mydb?user=banq&password=9999

如果要改变MySQL数据编码,格式如下:

jdbc:mysql://localhost/mydb?useUnicode=true&amp;characterEncoding=UTF-8

这条语句可以解决插入数据库发生的乱码,具体编码取决于Java系统的编码和运行的平台,最好都统一为UTF-8,这样,同样的程序就可以在Linux和中文Windows上正常运行。

3)获得与数据库连接后,就可以执行SQL语句操作。JDBC中有两种对策来支持数据操作Statement PreparedStatement

·       Statement 的操作为:

Statement stmt = conn.createStatement();

然后就可以使用stmt.executeQuery(String SQL)实行查询,使用stmt. executeUpdate (String SQL)进行修改,如果无法确定操作性质,干脆使用stmt.execute(String SQL)

·       PreparedStatement不同于Statement,它将Statement的两次操作变成一次操作,如:

PreparedStatement prepstmt = conn.prepareStatement(String SQL);

SQL语句写法就有不同,如查询密码为:

Select password from User where userID = ?

使用“?”来代替需要输入的数值,这条prepareStatement语句先发送到数据库,然后通过prepstmt.setXXXX来将“?”的值再发送到数据库。不同于Statement的是,prepareStatementSQL语句和数值一起发送到数据库,使用prepareStatement要更有效率。

无论Statement PreparedStatement,查询数据库结果都是返回ResultSetResultSet是一个行集合,其每一行代表查询数据库获得的一条记录,可以使用ResultSet.next()检查下一行是否有记录。

大多数初学者一开始使用JDBC就喜欢在JSP中直接使用ResultSet。其实ResultSet是一个临时集合,只能作为从数据库中获取查询数据用,不能再作为中转站,这样做的缺点还有耦合性太强,容易发生内存泄漏。如果确实需要将数据库中查询的大量数据再传递到JSP中,那么可以使用Collection或者使用Iterator。具体实现方法将在以后章节中介绍。

在正常情况下,直接使用JDBC调用数据库可以满足小型系统的需求,但是为防止访问量突然提升对服务器造成冲击,同时为了进一步提高性能,可以实现数据库连接缓冲,也就是数据库连接池。

每次数据库连接的建立都要花费一定的时间,而连接池非常类似线程池。事先建立了连接,当应用程序需要使用时,就从池中获取一个连接使用,应用程序使用完毕,通过close语句将连接归还池中。注意:这里虽然使用了close,但是并没有真正关闭连接,而是将连接归还连接池中,以便下次再使用。当并发调用增加时,连接池会不断自动新增新的连接满足调用,直至达到连接池的最大连接数;当连接调用减少甚至没有时,连接池会自动真正关闭一些空闲连接,只留下连接池最小连接数目的连接。

所以,使用连接池可以节省连接建立时间,消除数据库频繁连接带来的开销和瓶颈。

数据库连接池有很多开放源代码项目,有些系统本身就带有连接池,例如后面章节介绍的Jive论坛系统,就使用了自带的连接池。

数据库连接池属于基于线程设计的Java底层技术,因此很多J2EE容器服务器本身就有数据库连接池支持。因此,作为J2EE商业应用,一般不提倡自己钻研开发连接池,直接使用容器提供的连接池就可以。在本章后面章节将介绍Tomcat连接池的使用。

下页

  first