Java架构师指南

978-7-115-48066-8
作者: 王波
译者:
编辑: 杨海玲
分类: Java

图书目录:

详情

本书全面阐述Java架构师所需掌握的知识和技能,并围绕Java架构师这一热门主题介绍相关的内容。本书共包括12章。书中通过讲解企业管理系统、电商系统、报表系统等项目的实际开发流程,把流行的Struts、Spring、Hibernate、Spring MVC、MyBatis等框架整合起来,再从代码层面讲述Maven、WebService、POI等经典技术。

图书摘要

版权信息

书名:Java架构师指南

ISBN:978-7-115-48066-8

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。


编  著 王 波

责任编辑 杨海玲

人民邮电出版社出版发行  北京市丰台区成寿寺路11号

邮编 100164  电子邮件 315@ptpress.com.cn

网址 http://www.ptpress.com.cn

读者服务热线:(010)81055410

反盗版热线:(010)81055315


本书总结了作者多年来在Java Web方面的开发经验,全面阐述了Java架构师所需掌握的知识和技能,并围绕Java架构师这一主题介绍相关的内容。

本书共12章。书中通过讲解企业管理系统、电商系统、报表系统等项目的实际开发流程,把流行的Struts、Spring、Hibernate、Spring MVC、MyBatis等框架整合起来,再从代码层面讲述Maven、WebService、POI等技术,让读者在学习Java架构师必备的专业技能的同时,了解项目开发的整个过程。在项目运维方面,本书还讲解了SonarQube和Jenkins开源组件,以拓宽架构师的知识广度。

本书可以帮助不同技术层次的读者在短时间内掌握Java架构师必备的知识,缩短从程序员到架构师的进阶时间。因为书中的每份代码都有详细的注释和解析,很方便读者领会,所以不论是刚步入职场的新手,还是有一定工作经验的开发人员,本书都同样适用。


互联网的发展带动了各行各业信息化的趋势,一大批高新企业如雨后春笋般出现在大众的视野中。于是,不同类型的软件项目应运而生。在这些琳琅满目的项目中,有企业管理、电商平台、财务报表、金融银行、医疗器械、智慧城市和大数据分析等类型。项目的层出不穷带来了巨大的利润,让高新企业不断地成长起来,与此同时,也带来了很多相关的就业岗位。

当然,要顺利地完成这些项目,就需要大量的软件工程师。这种硬性的需求又养活了一大批培训机构,从事软件行业的人员当初是凤毛麟角,现在依然是供不应求。那么,如何提高软件工程师的开发技能就成了一个无法回避的问题。诚然,公司可以不定期进行培训,提高开发人员的技能水平,但从更普遍、更直接的意义上来说,提高技能水平的最佳方式还是系统地阅读相关书籍。

回到正题,项目从设计到完成的每一个环节,都需要精确地把控,如果这方面做不好,会让项目陷入困境,得不偿失!同时,在开发语言的选择上,也需要相当慎重。例如,大家熟悉的Java语言,它最大的优点就是跨平台运行。如果使用Java语言开发项目,程序员关注的无非是在某个系统环境下完成代码的编写和调试,至于最终需要用在哪里,没有必要过多地关心,因为无论是在Windows系统还是在Linux系统,Java程序都可以顺利地部署,流畅地运行。Java跨平台的优点得到了很多公司的青睐,他们纷纷把自己公司的核心编程语言确定为Java。这样的情况愈演愈烈,以至于Java语言在J2EE方向的发展非常迅速,成为企业级开发的首选。Java与众多优质的第三方框架搭配起来使用,更是让项目的开发进入了一个非常高效、便捷、可复用的时代。举个经典的例子,大家熟知的SSH框架技术集合,就是使用Java语言开发,再把Struts、Spring、Hibernate三者结合起来,组成的一套成熟的开发框架。这套框架曾经风靡全球,引起了业界学习的浪潮,促使很多公司前赴后继。不过,在2005年6月的JavaOne大会上,Sun公司发布标准版Java SE 6的时候,顺带将J2EE改成了Java EE,但因为历史原因,J2EE的提法仍然经常存在。

如果说Java语言的跨平台特性是很受欢迎的,那么Java语言的安全性也是非常让人放心的,这主要得益于Java语言中设计的沙箱安全模型。Java代码的执行全部在类装载器、类文件检验器、Java虚拟机内置的安全特性、安全管理器这4个组件的安全策略下完成,极大地保障了程序运行的安全。另外,Java语言还提供了AWT和Swing方面的开发,这两者都是基于图形化用户界面的,也就是业界常说的GUI层面的开发。但是,Java语言在GUI领域的优势并不那么明显,更多的开发人员仍然选择C++,绕过虚拟机直接与操作系统交互。而与此同时,Java在企业级方面的优势却越来越大,以至于出现了一枝独秀的局面。

计算机语言从机器语言、汇编语言发展到现在的高级语言,这个过程中诞生了很多种语言。有些语言已经逐步退出历史舞台,有些语言仍然在小众化的范围内存在。而Java语言,经历了二十多年的发展,仍然保持着旺盛的生命力,在编程语言排行榜中高居不下,Java程序员的数量也与日俱增,这种现象主要是由Java自身的优势决定的。作为开发人员,需要关注的并不是底层的核心,更多的是Java带给我们的简单、直观、易于使用的平台。因此,程序员不用关心虚拟机复杂的结构和每一步的运行情况,只需要关注项目业务的代码即可。这种易于接受的情形,让更多人把开发当成了一种乐趣。

最近,在业内流行起来的全栈工程师的定位更像是高级程序员,而架构师则需要站在更高的层面思考问题。作为Java架构师,不但要懂得前端插件化的开发理念,为项目选择合适的前端插件,还需要精通后端开发,为项目选择合适的框架,这样才能高效地完成任务。否则,极有可能出现事倍功半的情况。如果说需要弥补架构缺陷,最乐观的情况是通过加班实现,最糟糕的情况是直接导致项目失败。因为项目经理可能并不会深入了解具体的代码,他通常会参考架构师的意见,所以架构师的意见就显得极为重要。因此,本书在讲解架构师必备的知识技能的同时,也会穿插项目管理的知识。

Java技术发展迅速,本书旨在结合最近几年流行的技术,带领读者见证从项目启动到收尾的全过程,力求在短时间内让读者掌握Java架构师必备的知识技能,并且能在日后的工作中做到游刃有余,既可以在掌握扎实的基础知识后,熟练地搭建框架,又可以为项目经理提供专业的参考意见。

市场上的技术图书琳琅满目,令人难以选择。但是,这些书中讲解程序员进阶到架构师的过程的书却很少,这不得不说是一件令人遗憾的事情。本书的出现将会带给读者全新的认知,帮助读者在短时间内掌握架构师必备的知识,缩短从程序员到架构师的进阶时间,早日达到架构师的高度。

另外,本书专注于Java企业级开发,从最基本的企业管理系统开始,到颇具特色的电商系统都有涉及,还附带了诸如报表系统、员工信息系统、代码扫描平台的开发等,基本上包含了业界常用的项目类型。书中的项目都是基于BS架构的,与Java程序员的技术成长趋势完全匹配。读者可以通过阅读本书,并结合提供的源码进行练习,以做到融会贯通。

本书结合实际、深入浅出,以项目为驱动,阐述了我多年来在Java Web方面的开发经验。同时,本书通俗易懂。虽然没有把Java中特别浅显的内容用独立的章节来讲解,但这些内容都会在本书的代码中出现,读者可以结合程序自行理解,或者通过阅读注释学习,都可以很容易地理解它们的意思。综合来看,本书不但适合刚步入职场的新手,还适合有一定工作经验的开发人员,因为书中的每份代码都会有详细的注释和代码解析,方便不同技术层次的读者领会代码的含义。本书通过讲解企业管理系统的开发过程,让读者全面掌握Java EE的精粹内容,之后再通过其他几章的讲解,让读者学习到电商系统、报表系统、员工信息系统、代码扫描平台的开发,不断地拓展Java架构师技能的广度和深度。

本书的核心内容是讲解Java架构师必备的知识和技能。

本书特别适合Java Web领域的开发人员以及刚步入职场的新手。本书通过讲述Java架构师必备的知识技能,让广大读者在原有知识的基础上更上一个台阶,争取早日实现成为架构师的梦想。

对于架构师的定义,每个人的看法都不尽相同,我结合自己多年的工作经验,也只是大致定义了一个范围,希望可以帮助到别人。读者可以结合自己的实际情况,通过阅读本书,不断地扩展和充实这种范围,以达到自己理想中的境界。“不想当将军的士兵,不是好士兵。”在软件行业中,也似乎有这样一句话:“不想当架构师的程序员,不是好程序员。”虽然这看似是一种调侃,但从学习的角度来说,成为架构师,显然是一个好的目标!人只有在心里有了目标,才会变得更加幸福。

如果你不希望一直停留在Java的初级阶段,想在未来成为架构师,那么本书非常适合用来全面提高自己的开发水平。如果你想转项目经理,那么本书同样适合你,因为书中的每个项目都是完整的迭代过程。

本书以项目为驱动,是非常科学的学习方法。读者需要自行下载源码,自己搭建环境,对照章节中讲解的过程,逐步深入地学习Java技术。一个项目的代码量多得超乎想象,如果读者从第一行代码开始就要逐个阅读和编写,那么等学习完这个项目可能就需要一年的时间。因此,本书合理地安排了章节,科学地封装了技术知识,让读者在轻松的氛围下对照源码即可完成技术水平的提升,这不得不说是一个创新!相信大家在阅读本书后,都能够学习到“干货”,极大地提高自己的Java水平,达到架构师的高度。

本书得以顺利出版,离不开我自己多年来的努力。正是因为我有作为Java架构师的觉悟,才让自己在平时从未停下脚步,并且积极学习。不论是在工作中还是在业余生活中,我都会认真总结、分析近年来Java技术领域的知识。所谓学习的诀窍,对每个人都是一样的,就是不停耕耘、努力奋进,在这个过程中,我们总会收获良多。另外,还要感谢家人、朋友对我的帮助,感谢人民邮电出版社和杨海玲编辑对我的信任和支持。

由于水平有限,书中难免有不足之处,恳请专家和读者批评指正。欢迎读者通过电子邮件(453621515@qq.com)与我交流。


本书由异步社区出品,社区(https://www.epubit.com/)为您提供相关资源和后续服务。

本书提供如下资源:

要获得以上配套资源,请在异步社区本书页面中点击 ,跳转到下载界面,按提示进行操作即可。注意:为保证购书读者的权益,该操作会给出相关提示,要求输入提取码进行验证。

作者和编辑尽最大努力来确保书中内容的准确性,但难免会存在疏漏。欢迎您将发现的问题反馈给我们,帮助我们提升图书的质量。

当您发现错误时,请登录异步社区,按书名搜索,进入本书页面,点击“提交勘误”,输入勘误信息,点击“提交”按钮即可。本书的作者和编辑会对您提交的勘误进行审核,确认并接受后,您将获赠异步社区的100积分。积分可用于在异步社区兑换优惠券、样书或奖品。

我们的联系邮箱是contact@epubit.com.cn。

如果您对本书有任何疑问或建议,请您发邮件给我们,并请在邮件标题中注明本书书名,以便我们更高效地做出反馈。

如果您有兴趣出版图书、录制教学视频,或者参与图书翻译、技术审校等工作,可以发邮件给我们;有意出版图书的作者也可以到异步社区在线提交投稿(直接访问www.epubit.com/selfpublish/submission即可)。

如果您是学校、培训机构或企业,想批量购买本书或异步社区出版的其他图书,也可以发邮件给我们。

如果您在网上发现有针对异步社区出品图书的各种形式的盗版行为,包括对图书全部或部分内容的非授权传播,请您将怀疑有侵权行为的链接发邮件给我们。您的这一举动是对作者权益的保护,也是我们持续为您提供有价值的内容的动力之源。

“异步社区”是人民邮电出版社旗下IT专业图书社区,致力于出版精品IT技术图书和相关学习产品,为作译者提供优质出版服务。异步社区创办于2015年8月,提供大量精品IT技术图书和电子书,以及高品质技术文章和视频课程。更多详情请访问异步社区官网https://www.epubit.com。

“异步图书”是由异步社区编辑团队策划出版的精品IT专业图书的品牌,依托于人民邮电出版社近30年的计算机图书出版积累和专业编辑团队,相关图书在封面上印有异步图书的LOGO。异步图书的出版领域包括软件开发、大数据、AI、测试、前端、网络技术等。

异步社区

微信服务号


程序员到架构师的进阶之路是非常艰辛和漫长的,不但需要掌握很多高级的知识技能,还需要有过硬的基础知识。本章主要介绍Java程序员走向架构师的基础知识,还有开发环境的搭建。通过本章的学习,读者可以大致了解程序员的进阶之路,也可更加深刻地认识到程序员的发展方向。

大学毕业后,初出茅庐的菜鸟经过千辛万苦,总算是找到了人生中的第一份工作。但是,随着工作的开展,菜鸟所面对的问题越来越多。有些人坚持了下来,有些人中途放弃,有些人则在职业生涯中选择了转型。作为一名程序员,不但需要编写大量的代码,还需要对自己的职业生涯做一个规划。结合前辈们所走过的道路,这个职业规划大致是图1-1所示的这个样子。

图1-1 程序员职业生涯

一般来说,从初级程序员到高级程序员需要经过5年的磨砺,这个时间段基本上是业界的共识了。而且,在众多招聘信息中也可以发现程序员的起点都是需要两年工作经验的。也许,有些天赋异禀的程序员可能经过3年的刻苦学习也能达到高级阶段,但是,他们的知识技能往往并不全面,可能只是在某些方面比较熟悉罢了。到了高级程序员的阶段,可供选择的方案就比较多了,大概有图1-2所示的这3个走向。

图1-2 程序员发展方向

如果高级程序员再向上进阶的话,会面临3个选择。第一种方案是成为项目经理,负责管理加上部分开发。因为高级程序员对公司的项目是非常了解的,对公司目前的开发过程也驾轻就熟。如果本人有这方面的意愿,很容易胜任项目经理这个角色。而且,公司通常会从内部选择项目经理,空降项目经理的方式并不是常态,归其原因就是难以熟悉项目架构。

第二种方案是高级程序员可能更喜欢专著于技术,不喜欢出差和撰写大量的项目文档。在这种情况下,他可以成为一名架构师,专门负责维护公司的项目、产品方面的架构工作。如果公司有一定的规模,他可能会成为研发平台的负责人。当然,这种情况的前提是该程序员没有跳槽。

第三种方案是高级程序员可能经历了若干年的开发后,对写代码已经深恶痛绝,丝毫感受不到任何快乐了,但他对公司的项目和产品又非常熟悉,也有深厚的研发积累。在这种情况下,他可以彻底转型成为一名产品经理,纯粹负责公司产品的规划、设计、包装,甚至肩负一定的市场职责。当然,成为产品经理的前提是公司的项目已经产品化或者正在产品化之中。所谓的产品经理,通常就是向技术部提出一个原型设计:“看吧,这就是我想要的东西,至于怎么实现,你们看着办!”如果他懂代码还好说,但如果不懂代码,可能会让程序员陷入抓狂状态!

到了高级程序员的阶段,很多人就开始思考:究竟是去做项目经理?产品经理?还是继续写代码成为优秀的架构师呢?每个人的想法是不一样的,所作出的选择也是不一样的,这跟自己的能力和性格也有一定的关系。

每种开发语言,都有自己领域的架构师,如C++架构师、PHP架构师,当然也有Java架构师了。架构师需要对公司的整个研发平台了如指掌,清楚平台中细枝末节的东西。他极有可能是陪伴着这个公司成长起来的程序员;也极有可能是在别的公司工作多年后跳槽过来的。前者对公司的项目、产品非常熟悉,甚至自己还动手写过业务层。后者可能只是从大体上了解公司的研发平台,毫不深入,但这并不影响他的发挥,真正的架构师看到代码就有一种亲切感,可以很容易分析出隐藏在代码前后的业务过程。

Java架构师,至少需要在Java领域有5年的开发经验。他需要掌握的内容很多,简单点可以分为前端、后端、数据库、服务器、中间件等。前端插件可以极大地提高开发效率,甚至在不需要美工的情况下做出时尚的界面,类似的插件有AngularJS、Avalon、Bootstrap、ExtJS、jQuery EasyUI、jQuery UI等,这些前端插件也可以称作前端组件或者前端框架,种种叫法也看人的习惯了,没必要吹毛求疵。除了这些前端插件外,还需要掌握JavaScript、HTML等技能。后端需要掌握的技能主要是Java、JVM、Servlet、Struts、Spring、Hibernate、MyBatis等,还有最近流行起来的Spring MVC、Spring Boot等。这些技能和框架只有综合起来使用,并且合理地搭配才能发挥出最好的效果。至于效果能够达到什么程度就需要看架构师的本事了。也许有的架构师可以把这个积木撘得很好,也许有的架构师在搭积木的过程中,这个积木就倒下了。数据库方面需要掌握的内容有Oracle、MySQL、SQL Server,一般常用的数据库大概就是这3个。当然近年来,对于架构师需要掌握的数据库又有所增加,它们主要是代表了NoSQL的MongoDB等区别于传统关系型的数据库。但是,数据库相关的内容有不少,例如,需要熟练掌握SQL的各种语法,还需要掌握数据库性能的调优、备份和恢复。服务器并不是重点,但作为Java架构师,仍然需要有所了解。服务器包括物理服务器、云服务器,还有Web服务器,包括我们在开发中使用的Tomcat。中间件在一些中小型项目中并不怎么常用,如EJB技术、消息中间件ActiveMQ。当然,Web服务器也可以算作中间件,如Tomcat、Weblogic、WebShpere和JBoss等。

只有熟练掌握这几个方面的技能后,才能算是一个初级架构师。如果想成为大神级别的架构师,还需要学习更多的知识。Java架构师需要对这些技能非常熟悉,并且能像搭积木一样把他们整合在一起,构建出成熟的、完整的软件开发平台,以供底下的程序员在此基础上进行业务层的开发。但是,随着软件技术日新月异地发展,越来越多的框架进入眼帘,这对于我们来说既是好处又是坏处。好处是我们可以选择更好的、更合适的框架来提高项目的性能,降低开发难度,简化开发流程。坏处是可选择的框架太多,以至于让我们难以选择。所以,本书为大家精心挑选出了一名合格的架构师所必备的专业技能和开发思想,以供大家学习和参考,争取尽早地成为Java架构师。

孔子曰:“工欲善其事,必先利其器。”这是一个千古不变的哲理,工匠想要使他的工作做好,一定要先让工具锋利,这样才能发挥出最大的效率。这个哲理告诉我们,不管做什么事情,都要选择合适的工具。那么在软件开发的道路上,选择一个合适的开发工具也是极其重要的事情了。Java的开发工具有几种,这里不做太多的赘述,我们只需要对比它们的特点,即可从中选择出一款最适合自己的。

Java中常用的开发工具有NetBeans、JBuilder、Eclipse、MyEclipse、IntelliJ IDEA等。其中,NetBeans是Sun公司开发的,JBuilder是Borland公司开发的,这两个开发工具的功能和界面跟我们常用的Eclipse是没有很大的区别的,之所以在市场占有率方面输给Eclipse,完全是因为细节方面做得不好,还有在用户感知方面不太好。曾经有网友也在社区里面说过这样的问题,我尝试使用过NetBeans或者JBuilder,但总是因为个人习惯的原因没有坚持下来。可能Eclipse是大多数人接触的第一款开发工具吧,这种先入为主的感觉会一直伴随着我们。

Eclipse是完全免费和开源的,它的功能非常强大,开发起来也很顺手。MyEclipse是在Eclipse的基础上加上了自己的插件后的企业级集成开发环境,尤其善于开发Java、Java EE方面的项目。于是,在市场占有率方面Eclipse和MyEclipse非常高,这也在另一方面促进了它们的发展。这两者其实是一个核心,所以选择哪一个都看自己的习惯了。IntelliJ IDEA是Java开发的集成环境,在业界被公认为最好的Java开发工具之一,尤其在代码提示、重构、J2EE支持等方面非常强大。其中有一点对程序员的帮助非常大,就是调试功能,此外在某些细节方面似乎比Eclipse做得更好。而且,IntelliJ IDEA与GIT结合得非常好,而Eclipse与SVN结合得非常好。时间久了,这一开发工具与版本控制工具相结合的特点,也渐渐被程序员们认可,甚至成了项目选择开发工具的一种参考。

举个例子,如果A项目列入了开发计划,为了保持大家代码的一致性,可能项目组内会统一使用开发工具。如何选择呢?如果,这个项目使用SVN来管理代码,那么大家就会优先使用Eclipse;如果使用GIT管理代码,那么大家就会优先使用IntelliJ IDEA。当然,这似乎只是一种约定俗成的参考,并不是硬性要求。

在接下来的学习中,我们以MyEclipse和Eclipse为主来开发项目,并且会讲述SVN和GIT的不同,让大家在以后的工作中更加灵活地搭配开发工具和版本控制工具的组合。至于IntelliJ IDEA,因为它的入手门槛确实有点高,而且一旦选定,后面对于代码的重构会非常麻烦(指Eclipse和IntelliJ IDEA之间),所以本书暂不做相应的讲解。

另外,本书还会使用Eclipse相对较新的版本来做一些练习。其中,MyEclipse的版本是10.7,Eclipse的版本是Kepler,IntelliJ IDEA的版本是2016。SVN和GIT的版本带来的差别并不大,所以并不对版本做具体的规定,MyEclipse10.7的界面如图1-3所示。

图1-3 MyEclipse 10.7的界面

Eclipse Kepler的界面如图1-4所示。

图1-4 Eclipse Kepler的界面

IntelliJ IDEA 2016的界面如图1-5所示。

图1-5 IntelliJ IDEA 2016的界面

JDK是Java开发的核心,包含了Java运行环境、工具、基础类库。如果没有JDK,Java开发是无法进行的,Java项目也无法运行起来。所以要做任何项目的开发,第一件事情就是安装好JDK。接下来,我们才可以做更多的事情。

通常来说,每一个开发工具都会携带JDK,例如,MyEclipse 10.7自带的Sun JDK 1.6.0_13,但是IntelliJ IDEA并没有携带,需要自行配置。鉴于这种情况,我们在安装完开发工具后紧接着就应该安装合适的JDK。使用MyEclipse 10.7自带的JDK也可以完成日常的开发,但这款JDK没有进行环境变量的设置,可能在后续的开发中会有影响,而且这款JDK是混杂在MyEclipse 10.7的安装目录下的,给人的直观感觉不太好。为此,我们需要单独安装一款JDK,而说到安装JDK,就不免要选择合适的版本。目前,JDK版本已经到了8,但是因为历史原因,使用JDK 8来开发项目的公司并不多,第一个吃螃蟹的人会有惊喜也有潜在的风险。使用JDK7也是个不错的选择,但是因为本书中所涉及的项目众多,为了项目的稳定性,还有学习的顺序性,我们仍然使用久经历史验证的JDK 1.6版本,也可以称作JDK 6,对于这个名称不用纠结,是因为历史原因造成的。JDK 1.6以上的版本才正式改变了叫法,如JDK 7,也有开发人员习惯叫它JDK 1.7。读者可以在JDK 1.6版本下熟练掌握本书的内容后,自行更用更高级的版本来测试程序的运行性能和代码编译方面的不同。因为本书的主旨是讲述常规的技术,所以对于JDK的新特性并没有过多讲解。

首先,需要在Oracle官方网站下载JDK 6。因为Oracle官网经常更新,具体的地址也会经常改变,很难有一个确切的下载地址。但是,在Oracle官网可以找到Downloads的菜单,基本上Oracle公司所有的产品都可以在这里找到。另一种方法是可以在其他的网站下载JDK 6,例如,国内的一些网站,下载起来速度也相对比较快,Oracle官网下载JDK如图1-6所示。

图1-6 Oracle官网下载JDK

下载完JDK6之后,最好将它安装在非系统盘里。接着,需要对刚才安装好的JDK进行环境变量的设置,以方便我们在DOS系统下使用JDK命令。例如,最常用的编译命令javac,显示JDK版本的命令java -version,这些命令的使用都依赖于环境变量的配置。如果没有配置,是不会生效的。

首先,打开Windows的环境变量界面,新建系统变量JAVA_HOME和CLASSPATH。编辑JAVA_HOME变量,在变量值里输入JDK6的安装地址,如D:\Program Files\Java\jdk1.6.0_43,点击“确定”保存。接着,编辑CLASSPATH变量,在变量值里输入%JAVA_HOME%\lib;%JAVA_HOME%\lib\tools.jar,点击确定保存。最后,选择系统变量名为Path的环境变量,在原有变量值的基础上追加%JAVA_HOME%\bin;%JAVA_HOME%\jre\bin,点击“确定”保存,添加环境变量的界面如图1-7所示。

图1-7 Windows环境变量设置界面

为了验证Java环境变量是否设置成功,可以运行CMD程序,打开Windows的命令行模式,输入java -version命令,如果环境变量设置成功,会在下面输出当前的JDK版本号以及JDK位数,正确的输出结果如图1-8所示。

图1-8 命令行模式下输出JDK版本

配置好了环境变量,还需要在MyEclipse 10.7中配置JDK,使其可以在开发工具中使用。打开MyEclipse 10.7,在Preferences菜单中的Java选项下找到Installed JREs选项,就可以看到当前工作空间中的JDK设置,MyEclipse 10.7默认自带一个JDK6,如图1-9所示。

图1-9 MyEclipse10.7自带的JDK6

点击Add按钮,在弹出的Add JRE对话框中选择Standard VM点击Next按钮进入下一步,在弹出的对话框中点击Directory按钮,选择JDK6的安装目录,点击确定。对话框会自动识别出JDK的相关信息,并且在JRE system libraries列表框中显示出来,如图1-10所示。

图1-10 MyEclipse 10.7中设置JDK6

点击Finish按钮完成设置。这时,MyEclipse 10.7会自动回到Installed JREs对话框中,刚才的列表中会多出一栏我们刚刚设置好的JDK选项,在勾选框中选择它点击OK。至此,MyEclipse10.7下的JDK设置就成功了,在以后的开发工作中,我们全依赖这个JDK提供的基础JAR包来开发和运行项目。

安装好了JDK,我们就可以在MyEclipse 10.7中进行一系列代码编写工作了。例如,可以在开发工具中写一些类,做一些练习。普通的包含main函数的Java类,我们可以通过Run As菜单下的Java Application命令来运行,输出程序结果。例如,可以在MyEclipse 10.7下新建一个Java Web工程practise。具体的过程如下,选择File菜单下的New选项,在弹出的右侧菜单中选择Web Project,在对话框的Project Name文本框中输入practise,将J2EE Specification Level选项设置为Java EE 6.0,和安装的JDK保持一致,点击Finish按钮,practise项目就建立好了。

选中practise项目的src目录,右键选择新建Package,在对话框的Name文本框中输入com.manage.practise,点击Finish,就可以给这个项目建立一个空包。接下来,就可以在这个空包里新建类。选中Java包,右键选择New菜单下的Class,在弹出的对话框中在Name处输入类名Test,并且勾选public static void main(String[] args),点击Finish。这样,在practise包下的第一个类Test就建立成功了。打开Test类,在main函数中输入第一行Java语句System. out.println("Hello World");,使用Java Application来运行。此时,控制台会在空白区域输出Hello World。理论上来说,我们的第一个Java程序就这样诞生了,尽管这个程序非常简单!

如果只是在MyEclipse 10.7下安装了JDK,这款开发工具能做的事情无非是编写类,利用Java Application来运行,并且进行程序的测试。在这种情况下,我们的代码中所设定的数值均是由自己输入的参数;然后再根据程序中的处理逻辑,做一些简单的运算;最后,输出正确的结果。可是,程序开发远远不是这么简单的事情,我们需要做的是开发一个具有交互能力的项目,而不仅仅是写一段简单的程序。要达成这个目标,我们就必须在MyEclipse 10.7安装Web服务器来运行项目。在这里,我们选择使用Tomcat服务器,这是因为Tomcat服务器具有简单、易用的优点。

首先,打开Apache的官方网站,在下载Tomcat 6.0的页面找到对应的软件,在Core列表中选择64-bit Windows zip的版本,将Tomcat 6.0保存到本地,并且解压缩到本地的非系统盘内,如E盘的根目录。下载Tomcat 6.0如图1-11所示。

图1-11 下载Tomcat 6.0

打开MyEclipse 10.7的Preferences对话框,在MyEclipse的列表中选择Servers,这时,会出现一个列表,列出MyEclipse 10.7支持的服务器。选择Tomcat,再选择Tomcat 6.x。这时,对话框右侧会出现Tomcat 6.x的配置项,选择Enable,启用Tomcat 6.x。点击Tomcat home directory对应的Browse按钮,在弹出的磁盘目录列表中选择Tomcat 6.0所在的位置,MyEclipse 10.7会自动补齐其他的两处空白,点击OK按钮,Tomcat 6.0服务器就配置好了。

我们通过工具栏运行Tomcat 6.0,启动成功后,点击工具栏的Open MyEclipse Web Browser功能的图标,在地址栏中输入http://localhost:8080/,就可以看到Tomcat 6.0运行成功的画面。接下来,就可以通过在Tomcat服务器里部署Web项目来进行正式地编码工作了,运行界面如图1-12所示。

图1-12 Tomcat 6.0运行成功

完成了前面几节的配置,MyEclipse 10.7 的开发环境已经正式配置成功了。这时,我们可以在MyEclipse10.7下完成第一个Hello World程序来结束本章的学习。

在MyEclipse 10.7的界面中,我们可以看到Package Explorer视图下有一个Java Web项目practise,这个项目是之前创建好的,并且在包里建立了一个Test类。我们通过运行该类,可以在控制台里输出了Hello World,说明这个类没有问题。但是,这种简单的编码没有交互性,是不能满足项目的需求的。如果要开发一个项目,必须让其在Tomcat服务器里运行,才能起到交互的作用。那么我们可以把practise项目部署到Tomcat服务器里试试效果。

选中practise项目,右键弹出功能菜单选择MyEclipse,在弹出的右侧菜单中选择Add and Remove Project Deployments功能,在弹出的对话框中可以看到Deployments列表中为空,说明Tomcat服务器里并没有部署任何项目。这时,我们点击Add按钮,在Server下拉框中选择Tomcat 6.x,点击Finish,把practise项目正式部署到Tomcat服务器中。部署好的practise项目如图1-13所示。

图1-13 部署好的practise项目

这时,我们启动Tomcat服务器就会自动加载部署到服务器里的practise项目。通过控制台,可以看到服务器的启动日志,如果没有报错的话,说明practise项目没有编译错误,那么Tomcat服务器启动成功。打开IE浏览器,在地址栏中输入http://localhost:8080进行访问,可以看到程序运行成功,但显示的仍然是Tomcat服务器的默认页面,这是因为我们没有输入项目名称。

打开practise项目的WebRoot文件夹下的index.jsp文件,把title标签里的内容修改成First Page,把body标签里的内容修改成Hello World,保存index.jsp文件。再次打开IE浏览器,访问http://localhost: 8080/practise,可以看到页面上已经发生了变化,如图1-14所示。

图1-14 practise项目运行

本章我们全面阐述了程序员的职业发展规划,从而为广大读者提供一个晋级的参考。从程序员到项目经理、产品经理、架构师的过程至少需要5年。这5年是一个学习期,5年后就可以进行转型了。所以,建议大家在工作的前5年不要频繁跳槽,还是需要系统地掌握知识技能,积累经验才是硬道理。频繁跳槽不但让自己的知识会出现断层,也可能影响到自己在HR心中的形象。

接着介绍了Java开发中常用的工具,并且做了简单的对比,相信读者可以根据自己的喜好选择其中的一款。因为本书主要采用MyEclipse 10.7作为开发工具,所以读者最好先使用这款开发工具,其他的工具会在后面的章节中介绍。等对这些工具驾轻就熟的时候,再随意切换。每个开发工具都有自己的优缺点,但不要人云亦云,选择自己最习惯用的才是最好的。最后介绍了安装JDK、Tomcat服务器的过程,并且开发了第一个Hello World程序。如果读者已经牢牢掌握了本章的内容,这就是万里长征迈出了坚实的第一步,相信在以后的学习当中大家的收获会更多。


也许有人会有这样的疑问,需求调研是项目经理的事情,为什么需要架构师来参与呢?这种说法虽有一定的道理但也不完全正确。因为在不同规模的企业中,架构师和高级开发人员都是有可能去客户现场调研的。通常,他们会跟项目经理一起过去,针对客户的需求,从专业的技术方面提出建设性的意见。另外,如果去客户现场调研的是纯管理的项目经理,他可能会把业务方面阐述得非常清楚,但如果对方提出技术问题的话,项目经理也许会被当场问住。这种情况下,就需要有一个架构师或者高级开发人员在场,及时地从技术的角度来分析问题给出答案,这在需求调研方面起到了一个互补的作用。

A 公司开发了一套企业管理系统,已经基本上趋于成熟,可以进行大规模销售了。于是,A 公司开发了一个官方网站,将这款产品挂在了网上,希望以此开拓互联网市场。通过网络卖出了几套产品之后,A 公司接到了一个客户的电话,希望他们对这款企业管理系统进行定制化开发。如果这次开发顺利的话,后续还会有其他方面的合作。

公司高层对此十分重视,如果能把这个大客户的项目做好,对于公司的发展将会非常有益。于是,A 公司派出了销售经理带着几个人的团队,亲自飞往客户现场沟通需求、搭建关系。因为企业管理系统的功能比较简单,前去出差的售前工程师的经验也比较丰富,所以此次出差并没有技术人员参与。销售团队在客户现场与客户进行了多次沟通,并且主动请客户吃饭,参加各类活动联络感情,增进彼此之间的关系。

果然,功夫不负有心人!在一种融洽和谐的关系之中,双方对企业管理系统的项目达成了很多共识。B 公司的规模很大,他们急需一款企业管理系统来对公司的资源进行实时查看。例如,销售部需要查看一些市场信息,若干员工的业绩等;人事部需要通过查看最近几个月离职的员工情况来做一些统计等。总之,不同部门都有这方面的需求。但是,A 公司的企业管理系统只能满足他们的部分需求,涉及具体部门的报表展现还需要进行定制化开发,从 B 公司的数据库里读取信息。因为A 公司的企业管理系统跟B 公司的需求有很大的匹配度,B 公司根据市场价格、人力成本等多方面考虑之后,才决定委托A 公司进行定制化开发,这无疑是一件对于双方都有好处的事情。A 公司有这样的产品,而B 公司有这样的需求,两者碰在一起,才能产生经济价值。针对 B 公司提出的增加定制化报表的需求,A 公司售前工程师对此进行了专业的回答,当场承诺可以满足这方面的需求,且技术难度不大。于是,没过多久A 公司的销售团队就带着丰硕的成果回来了。

B 公司的领导在会议上进行了多方面的讨论,充分审阅了各部门汇总的“企业管理系统采购需求”,研究了采购部门提供的“可行性研究报告”,又综合分析了市场,参考了 A 公司提供的相关文档,认真评估了采购预算。最终,仍然觉得该项目不需要招标,直接让A 公司开发是最好的选择。当然,B 公司能够如此爽快地立项成功,也与A 公司的销售在前期的努力是分不开的。正是因为销售巧妙地拓展了关系,并且售前工程师以丰富的专业知识打动了客户,拍胸脯保证了该项目的可行性,才让该项目在会议上如此顺利地立项,并且是以直接采购的形式。

当听到这个振奋人心的消息之后,A 公司的员工欢呼雀跃。与此同时,A 公司派出客户经理又一次飞往客户现场,与B 公司的采购部门主管签订了正式的采购合同,并且规定了开发细则。其实,对于立项来说是有两层含义的,B 公司采购管理系统有一个立项过程;同样的,A 公司如果有一套自己的规章制度的话,也需要走一个立项流程。比较典型的做法是,A 公司的项目经理可以在公司的网站中提出立项申请的工作流,由上级领导逐级审批,最终立项申请获得通过。当然,这只是一种可供参考的做法。

合同签订后,B 公司积极履行合同,在规定的时间内为A 公司支付了项目的预付款。A 公司整合了技术部的资源,正式成立了“企业管理系统定制化开发项目组”,并且制定了“项目章程”。该项目组由一名项目经理、一名架构师、两名高级研发人员、一名初级研发人员组成。本来,该项目组并不需要架构师参与,但考虑到平台的扩展性,最终决定还是要有一名架构师参与其中。

因为B 公司的企业管理系统属于定制化开发,要在A 公司原有产品的基础上加入很多新的需求,所以在正式进行开发之前必须要去客户现场调研。为此,A 公司成立了需求调研组,由项目经理亲自带队,带着架构师和一名高级研发成员过去,为了方便和干系人联系,调研组又加入了一名之前去过客户现场的销售人员。

B 公司对企业管理系统的定制化开发非常重视,在进行正式调研之前,B 公司副总将各部门领导和A 公司调研组召集在一起,专门开了一个调研启动会,号召B 公司各部门积极配合A 公司调研小组的工作,并且规定了各部门的相关责任人,争取这个项目能够圆满取得成功。A 公司调研小组感谢了这位副总,并且主动请副总吃饭,席间大家对这个项目进行了多方面的讨论。回到宾馆,A 公司调研小组都觉得这个项目开展得非常顺利,这一切不但归功于自己的产品,还要感谢销售人员前期的铺垫。正是这种铺垫,为两个公司之间搭起了桥梁,营造了很融洽的合作关系。

调研的第一天,项目经理进行了干系人的优先级排序。首先,他们决定去拜访人事部的主管,针对企业管理系统的组织架构方面的需求做一个详细的沟通。人事部主管查看了调研小组制作的项目原型,认为项目经理阐述的功能已经符合了人事部的需求。于是,双方对此达成了一致,人事部主管签署了需求确认书。

调研的第二天,项目经理决定带队去销售部,针对业务部门关注的报表功能进行详细地询问、记录,力争做到没有疏忽和遗漏。销售部主管拿来了一堆指标文件,对调研小组说,“我们现在给领导汇报工作的时候,都拿着这些纸质的文件,总觉得不是很方便。因为领导现在提出了新的要求,他想实时地、动态地查看最新的销售业绩,这无疑给了我们部门很大的压力。还好现在要上这个企业管理系统了,只要你们将领导要看的这些报表,从我们的机房数据库里提取出来,展示在系统中就起到了很大的作用。领导不用经常让我们去打印文件了,我们也不用再过多的关注这些事情,能更加专注于市场营销方面了。”听了这个无纸化办公的理念,项目经理从内心里庆幸,还好我们在这方面有了足够的积累。虽然,这方面的报表并没有做很多,但至少是做了几个演示(demo),开发起来难度不大。于是,项目经理自信地承诺了这个功能可以实现。接着,调研小组去了其他的部门,对其他部门的需求进行了沟通。当客户表现出对技术方面的担忧之时,架构师就积极地站出来,用专业的知识来回答客户的问题,打消了客户的顾虑。因此,整个调研过程也一帆风顺。

调研的第三天,项目经理带队去了B公司的运维部。因为这个项目是需要部署在B公司的机房的。所以很有必要对B公司的部署环境做一个充分的了解。经过了一段时间的沟通,B公司运维部的主管表示,“我们这里有Windows服务器,也有Linux服务器,具体的部署看你们的项目情况了,我们不做硬性要求,只要你们的项目稳定即可。”听到这样的回答后,项目经理总算是完全放心了。这个项目,不是棘手的那种,做起来应该会很顺利。回到宾馆,项目经理组织调研小组,进行了一个简单的头脑风暴,确定了此次出差调研没有遗留的问题后就决定了返程。

需求确认书:在进行需求调研的过程中,很有必要对客户所提出的需求做一个梳理,并且以文档的形式输出。然后,项目经理需要拿着这份需求确认书,找相关的客户进行反复地确认,最终在达成一致的情况下签署需求确认书。需求确认书在项目的整个过程中是至关重要的,因为,不管客户说过什么,只要没有正式书面的文档,客户就很有可能说自己忘记了,或者反复地进行需求变更。这种情况,会让项目陷入被动之中,为项目埋下隐患。也许最后按照客户原来的意思把需求都实现了,但客户予以否认,说这不是我想要的东西,此时就需要拿出需求确认书与之过目。

调研结束后,项目经理并没有急着组织开发,而是静下心来思考了整个调研过程。他认为,客户规定的开发周期是半年,这个时间对于当前的人力、成本来说都是可控的范围。所以,不必为工期过多地担心,而是要在前期做好足够的准备工作。目前,项目的启动过程组的内容已经完成,接下来就是要处理好规划过程组的事情,才可以进入软件的开发阶段。

于是,项目经理布置了任务,让此次参与调研的同事们撰写出差总结,并且决定输出用户需求说明书、概要设计说明书、干系人登记册、项目管理计划。这些文档主要是项目经理撰写,但涉及技术方面的问题时,项目经理仍然需要参考开发人员的建议。因为文档太多,项目经理也可以请开发人员负责一部分文档的输出。国内的软件项目,大多都因为成本或者是企业文化方面的原因,在撰写项目文档方面做得良莠不齐。大多数中小型公司甚至没有很好的文档计划,他们接到项目就会迅速地步入开发阶段(执行过程组),这看似可以节省成本,但实际上会为项目埋下很多隐患,到头来导致频繁地返工,或与客户在需求方面的问题争论不休,最坏的结果就是项目迅速开发、迅速结束。反之,如果从项目开始阶段就注重文档方面的输出,为撰写文档留出一定的时间(把这部分时间当作项目成本),这样整个项目计划会有条不紊地进行,就算项目出现了问题,也可以很快地找到补救的方法。

项目管理专业人士资格认证(Project Management Professional,PMP)中有一个经典的理论是项目的渐进明细。把这个理论扩展一下,就是说项目中充满了渐进明细的内容,不论是整个项目还是项目的组成部分,如项目管理计划、项目范围、项目目标等。同样的,项目经理只有与干系人进行持续地、有效地沟通,再加深自己对项目的理解,这种渐进明细才可以达到。否则,什么内容都停留在项目的初期调研阶段的话,将发生不可预料的风险。这时候,输出文档就是一个很好的选择了,它可以保证调研的内容得到二次的梳理,让项目成员明确需求,还可以在项目组成员的讨论中产生渐进明细的效果。因为,项目的需求、可交付成果都是暂时不可见的,没有人能精确地预料它的结果(除非对某种项目特别熟悉,形成了某种模式)。在这种眼前一摸黑的情况下,输出文档可以产生渐进明细的效果,大家群策群力也可以完善风险登记册的内容,对风险的发生概率、影响、级别、应对策略、预防措施等进行讨论。另外,完善的项目文档最终会转变为组织过程资产,由项目管理部(Project Management Office,PMO)进行统一管理,也是对公司的未来发展大有裨益。

这时,我可以举几个典型的例子,应用场景使项目经理水平靠谱,一直在与客户进行着有效地持续地沟通。注意,与干系人的沟通会贯穿整个项目开发的全过程,不要以为拿到了用户需求确认书就可以万事大吉了,需求总是需要反复解读和确认的。

第一个场景是客户总是想变更需求,这种变更可能是好的也可能是坏的。如果没有完整的项目文档,不论是客户还是项目组,都很难对这个需求进行变更。因为在这种情况下,客户和项目组都没有可供参考的文档,无法合理估计成本和风险,这就不能按照正确的流程处理变更。如果项目经理凭借自身经验认为可以接受变更,那么因为经验不足导致的不利结果就会由公司承担;如果项目经理以成本或者风险为由拒绝变更,客户可能会让项目经理拿出相应的数据,这时文档的价值就显得极为重要。从记录变更请求、分析影响、提交CCB进行审批、批准或者拒绝,到通知干系人,这个完整的过程都离不开项目文档的参考。

第二个场景是项目的风险应对,包括来自整个项目中各个方面的风险。项目的风险管理包括了规划风险管理、识别风险、实施定性风险分析、实施定量风险分析、规划风险应对、控制风险。虽然,PMP对风险提出的这几个过程看似比较繁杂,但放到具体的项目中还是很简单的。因为,对项目风险的管理可以总结成一句话,“对项目的风险进行有效性管理。”不过,要做到这句话就需要有选择地实施那几个过程,但实施这几个过程就需要参考项目中产生的所有与之相关的文档。例如,常见的项目管理计划、项目章程、干系人登记册、风险管理计划、成本管理计划、进度管理计划、质量管理计划等,只有参考这些文档,才能通过分析技术、专家判断、会议、文档审查、假设分析、SWOT分析、风险分类、风险审计等工具与技术,继续输出和完善我们认为极有价值的文档,如风险管理计划、风险登记册、工作绩效信息等。这些文档凝结了项目组成员的智慧,可以有效地应对项目风险,保障项目的安全进行。

第三个场景是PMO对项目的帮助。首先,明确两个概念,组织过程资产和事业环境因素。

组成过程资产是执行组织所特有并使用的计划、流程、政策、程序和知识库,包括来自任何(或所有)项目参与组织的,可用于执行或治理项目的任何产物、实践或知识。过程资产还包括组织的知识库,如经验教训和历史信息。组织过程资产可能还包括完整的进度计划、风险数据和挣值数据。在项目全过程中,项目团队成员可以对组织过程资产进行必要的更新和增补。组织过程资产可分成两大类:(1)流程与程序;(2)共享知识库。[1]

组织过程资产的概念总结一下,就是说项目过程中所有产生的好的东西,都需要积累下来以方便后期再次使用。一般来说,组织过程资产是由PMO项目管理办公室来负责维护。而大多数中小型企业甚至没有PMO,这一方面是因为人力成本不足,另一方面也可能是没有这种意识。或者,他们习惯使用专家判断的方式,也就是过度地依赖有经验的项目经理。再者,某些公司可能会临时建立一个PMO,用于总结经验教训。

事业环境因素是指项目团队不可能控制的,将对项目产生影响、限制或指令作用的各种条件。事业环境因素是大多数规划过程的输入,可能会提高或限制项目管理的灵活性,并且可能会对项目结果产生积极或消极的影响。[2]

事业环境因素的概念总结一下,就是说这些影响项目的东西理论上不能控制或者很难控制,如公司文化、地理条件、基础设施、授权系统、市场条件、国家政策等。这些事业环境因素对项目的影响一般是可见的,处理的方法也比较简单,如规避。但是,PMO需要事先明确这些内容。举个例子,在接项目的时候明白什么是公司能做的,什么是不能做的。事业环境因素的影响对项目来说,没有组织过程资产大,因为一个项目只要认定是可以做的,就已经满足了事业环境因素。例如,现有的办公条件不足,公司可以通过采购来满足。公司文化方面的影响更多是潜移默化的,但业界也形成了一些特有的公司文化,例如华为对于员工素质的要求;一些互联网公司倡导的996,这些举动将公司文化提升到了更高的境界,而它们带给项目的影响就会相应地增加。例如,996期间,员工的加班时间大幅度延长,就导致了项目工期的缩短,但与此同时,因为加班导致的疲劳,也可能让项目质量下降,这些都是难以预料的因素。

但不管从哪方面来讲,成立PMO的意义非常巨大,一个公司要做到青山常在、细水长流,就需要有这样的觉悟。如果是想临时干一票就解散的项目组,就没必要浪费这些经历了。PMO可以为项目提供指导和参考意见,这个过程可以贯穿项目生命周期的始终。举一个很有意思的例子,某个公司的项目经理离职了,新任的项目经理虽然很有经验,但没有做过这方面的软件。在这种情况下,他接手了项目,可以说是临危受任,但如何保障项目有条不紊地进行呢?最好的办法就是阅读项目文档,还有就是请教PMO的主管了。

这3个是软件项目中比较常见又有点棘手的场景,如果项目从一开始就重视撰写文档,就可以很好地对这些场景进行处理。还有一些比较常见的场景,例如,项目组需求很多,任务繁重,但人手不足,该怎么解决?很简单,让人力资源部(HR)招人就是了。如果老板不愿意花钱,想靠现有的人力来通过加班解决问题,那就没有办法了。或许老板本身也很困难,又怎么好意思为他添麻烦呢,不如识趣点自己走人。俗话说,舍不得孩子套不住狼,更有甚者,不入虎穴焉得虎子?连必备资源都准备不充分,还怎么谈接下去的事情呢?这对客户本身而言,也是一种欺骗。所以,这种场景完全没有必要过多地讨论。

最后,项目管理中还需要特别注重业界公认的SMART原则,这个原则一般是用来考核绩效的,但在此处可以延伸一下,把它当作项目过程中的任何有内容的东西,如撰写文档、开发需求、培训学习等。

S代表具体,完整的单词是Specific,意为绩效指标必须是具体的,延伸一下可以改为工作内容必须是具体的。M代表衡量,完整的单词是Measurable,意为绩效指标必须是可以衡量的,延伸一下可以改为工作内容必须是可以衡量的。A代表实现,完整的单词是Attainable,意为绩效指标必须是可以达到的,延伸一下可以改为工作内容必须是可以实现的。R代表相关,完整的单词是Relevant,意为绩效指标必须是相关的,延伸一下可以改为工作内容必须是与目标相关的,也就是不要做无用功。T代表时限,完整的单词是Time-bound,意为绩效指标必须是有时间限制的,延伸一下可以改为工作内容必须是有时间限制的,在某个时间节点必须做该做的事情。

SMART原则已经成为众多企业管理者的一个口头禅,原因就是该原则在几个简单指标的指导下,可以相对完整地考核员工。但是把它放在撰写文档、开发需求、培训学习等方面同样适用,甚至在开发项目的时候,也可以通过SMART原则来评价某个节点的工作质量。当然,这只是一个工具和参考,具体如何使用就需要看项目经理的了。

其中,用户需求说明书、概要设计说明书、干系人登记册内容相对比较少,而项目管理计划是个渐进明细的过程,需要不断地更新,输出的过程也需要项目组所有的成员参与,如利用专家判断和引导技术。最终,项目管理计划的输出还包括了范围基准、进度基准、成本基准。子计划包括:范围管理计划、需求管理计划、进度管理计划、成本管理计划、质量管理计划、过程改进计划、人力资源管理计划、沟通管理计划、风险管理计划、采购管理计划、干系人管理计划。

项目管理计划输出的3个基准主要指范围基准、进度基准和成本基准。这3个基准的主要工作内容包括最初确定的范围说明书、工作分解结构(WBS)和相应的WBS词典,这3个基准只有通过正式的变更控制程序才能进行变更。[4]而输出的若干子计划则是从项目的不同方面来详细或者高度概括地描述这些方面的工作任务。例如,质量管理计划就是体现在项目的质量方面的内容,如代码质量等。

输出了这些文档之后,企业管理系统项目的规划过程组的内容差不多就算是完成了。接下来就可以正式步入开发阶段了。

在输出项目必备的文档后,就会涉及一个问题:开发技术的选择。首先,后端的核心技术确定是Java语言。接着,要确定前端插件的选用。例如,可以选择AngularJS、Avalon、Bootstrap、ExtJS、jQuery EasyUI、jQuery UI等,前端插件的选择可以极大地提高开发效率,甚至可以省去一个美工的人力成本。在确定前端插件的同时,还需要明确后端框架(也称服务器端),可以选择Struts、Spring、Spring MVC等,涉及数据库的ORM框架可以选择Hibernate、MyBatis等。这些内容,如果在项目正式开始之前就确定好,在后期就不会因为框架问题而导致返工了。另外,如果做的系统包括端到端的数据交互,还需要考虑接口调用的问题,例如,可以使用Webservice。总之,开发技术的选择就是要明确这个项目所需要用到的技术,以及这些技术能否完美结合,不要因为搭配不当而导致出现了问题。

开发技术的选择在一定程度上依赖公司的技术积累,如果公司有成熟的框架大可以拿来使用。反之就必须召集项目成员集中讨论,大家各抒己见,排除所有可预测的风险,确定或者是暂定一套适用于整个项目的开发技术。

数据流图,英文全称Data Flow Diagram,也可以称作数据流向图。数据流图通常使用图形的方式来表达一个系统或多个系统的逻辑功能,以及数据在这些系统之间的流转。数据流图的产生并没有什么复杂的历史,因为只要是涉及某个具体的系统都会有数据流图,不论它是简单的还是复杂的。在没有绘图软件的时候,人们可以在白纸上用笔来画数据流图,现在我们可以使用Microsoft Visio来绘制它,绘制的方式不像UML那样严谨,只要是易于人们理解的方式都是可以的。数据流图在需求调研时也是非常重要的,客户往往对具体的技术指标不是很清楚,但他们可以站在业务的层面,为开发人员提供数据流图,以帮助开发人员更好地理解业务。

典型的数据流如图2-1所示。

图2-1 典型的数据流图

UML,英文全称是Unified Modeling Language,被称作统一建模语言。最早出现在20世纪80年代末和90年代初期。因为当时有很多知名学者都提出和完善了面向对象的建模方法,直到1997年年底,OMG组织正式确定了UML1.1作为一个标准版本。所以它才被称作统一建模语言或标准建模语言。后期的话,UML又经历了多年发展,推出了UML2.0,在这个版本里该建模语言已经非常成熟了。

UML定义了5种类图,它们分别是用例图、静态图、行为图、交互图和实现图。UML还定义了10种模型图,它们分别是用例图、类图、对象图、包图、状态图、时序图、合作图、活动图、构建图和配置图。

一般来说,我们对UML的学习不必特别深入,只需要知道在项目管理中肯定是会用到的就行,尤其是在需求调研的时候。因为需求调研完肯定要使用相应的图表把需求描述出来,才能方便程序员们查看,否则光靠语言表述就显得苍白无力。我们使用UML的时候,不用去刻意关心每种图对应的内容,也不是所有的类图和模型图都会用到,一般只会用到常用的几种,其他的只需要在需要用到的时候学习即可。其实,学习UML最好的方法就是从网上下载相应的需求文档,把它当作模板,按照现成的东西去画,多画几次就能触类旁通。在这里,我们可以画一个比较典型的、易于理解的UML图,用来帮助大家学习。在这个UML图中,可以看到管理员和对应的3个功能。简单分析一下就知道,该图中有一个角色,它对应企业管理系统的管理员用户,而用这个用户可以做的,有3个典型的功能,分别是新建用户、配置菜单、分配权限。如果仅仅用文字来描述,没有直接用UML图表示出来那么形象,所以这就是UML图存在的意义。

典型的UML图如图2-2所示。

图2-2 典型的UML图

项目开工会,英文全称Kick Off Meeting。其主要作用是在项目规划过程完成后的会议,标志是完成了项目管理计划之后。这个会议的召开一般意味着项目开始进入了执行阶段。与Initialing Meeting不同的是,虽然都是启动会议但Initialing Meeting是项目初始阶段的启动会议,当时的文档只有一个工作说明书。Kick Off Meeting的召开,意味着前期的准备工作都已经结束。

这个会议的目的主要有以下两点。

(1)项目团队成员互相认识,明确职责,鼓舞士气,号召大家积极主动地全面地投入到项目的开发当中来,类似动员会。

(2)批准项目管理计划,确认项目的范围、进度、成本、风险等事项,并且在干系人之间达成共识。

本章的主旨是需求调研。不管是自研项目,还是为其他公司定制化开发,都离不开需求调研的阶段。从立项到输出文档,是个漫长的过程也是不可绕过的阶段。有时候,随着一些不好的项目的产生,常常需要程序员加班完成任务,动辄在1至3个月内完成项目,这些项目的结局往往会摔得很惨。归根到底,是因为这些项目忽略了规划阶段的长期准备,并且一味地想通过加班来节省成本,违背了项目开发的客观规律。

项目规划过程组的内容看似繁多,却可以裁剪,这主要得益于项目经理的水平,还要根据开发人员的水平。通过学习本章,读者可以理解到项目调研的整个过程。虽然这个过程裁剪了部分内容,但从整体来看,绝对是值得借鉴的标杆。本章的内容结合PMP理念,让大家领悟PMP理念在软件项目中的结合应用。这种必经之路,在公司里会成为宝贵的组织过程资产和事业环境因素,最后归于PMO管理。随着公司的发展,这些前期积累的经验,会在后期的开发中起到不可估量的作用,大幅度降低软件项目的难度和成本。

项目开发分为瀑布模式和敏捷模式,本章所述内容只是项目管理的理念,并不用严格地归纳为某种开发模式。瀑布模式和敏捷模式都需要开发文档的撰写和积累,这两种模式从本质上来区分的话,应该是瀑布模式像一台按部就班运转的机器,其中某一环节卡住了就专注于这一环节,直到做好为止,其间不会受客户的影响;而敏捷方式保持与客户持续沟通,主动接受需求变更,采用迭代方式,在每一个版本都及时与客户沟通,对交付成果进行确认,并且提出改进意见。而瀑布模式的话,因为环节都是预先设计好的,所以不需要关注这些内容。一般来讲,瀑布模式适合大公司在已经有非常多的积累的情况下,并且需求已经特别明确的情况下使用,它的项目环节,包括需求分析、概要设计、详细设计、编码阶段、单元测试等都经过了很长时间的打磨,基本上不会出现什么意外问题。举个例子,A 公司有着数十年的电商经验,而客户事先已经看好了这套模板,这种需求往往是明确的、不需要改变的。客户给出6个月时间开发,因为A公司的积累足够多,所以他可以在6个月内完成项目。而B 公司在电商方面的积累比较少,或者客户很挑剔,这时候B公司就需要使用敏捷开发,跟客户保持沟通,以便最后做出来的东西是客户想要的。

[1] 本段描述参考《项目管理知识体系指南》(第5版)第2章2.1.4节。

[2] 本段描述参考《项目管理知识体系指南》(第5版)第2章2.1.5节。

[3] 本段描述参考《项目管理知识体系指南》(第5版)第4章4.2节。

[4] 本段描述参考《项目管理知识体系指南》(第5版)第4章4.2.3节。


经过了漫长的项目规划阶段,企业管理系统的准备工作已经完成了。现在正式步入了执行阶段,也就是开发阶段。因为项目的周期是半年,客户的需求也相对固定,所以企业管理系统第一期采用瀑布开发模式。

定义范围就是明确所收集的需求,哪些是要包含在当前的项目之中的,哪些是排除在外的,从而明确项目的边界。说得通俗点,就是我们可能收集了很多需求,但不一定都要在当前的版本中实现。定义范围后,一般输出项目范围说明书。然后参考项目范围说明书,可以将项目进行WBS分解,这个过程是将项目的可交付成果和项目工作分解成较小的、更易于管理的组件,该组件的最底层单位是工作包。

工作包的内容是具体的开发内容,例如编写某个模块的代码。创建WBS的方式一般有两种,因为WBS的第一层相对固定,所以其不同之处主要体现在第二层。例如,两种方式的第一层都可以是企业管理系统,但第二层的差别就比较大。

创建WBS的第一种方式,是以阶段作为第二层。例如,企业管理系统底下包含了项目管理、产品需求、详细设计、构建、整合、测试等阶段性的工作,可以把阶段作为第二层,具体的工作内容(如编码)仍然体现为工作包,但这些工作包会穿插在不同的项目阶段完成。

创建WBS的第二种方式,是以主要可交付成果作为第二层。例如,企业管理系统的第二层包括管理员系统、用户系统、游客浏览这3种简单的划分。但是,管理员系统和用户系统底下就已经包含了90%的编码工作量,剩下的10%是游客浏览的。这样的话,我们就可以把完成可交付成果来作为项目的完成度。例如,管理员系统下面包括了新建用户、设置权限等操作,普通用户下面包括了查看报表等功能。这些具体的内容,我们都可以将它们作为工作包,来分别交代给不同的开发人员来完成。不论是第一种还是第二种,完成工作包的过程就是开发进度的体现。

WBS是自上而下进行分解的,将可交付成果分解成最基本的单元,最后把工作包分发给每个研发人员。WBS包含项目全部的工作,分解完毕后,如果从下而上过一遍没有看到遗漏的工作,就是符合100%规则。同理,WBS词典非常重要,它是我们完成工作包的基础参考,如账户编码标识、工作描述、进度里程碑、相关的进度活动、所需资源、成本估算、质量要求、验收标准等。如果我们把项目分解成了若干个工作包,每个账户编码就是工作包的唯一标识。把若干标识的工作包加起来,就能计算出成本、进度、资源等的汇总信息,以便我们从整体上把控项目。每个控制账户包括一个或多个工作包,但一个工作包只能属于一个控制账户。另外,整合这些资源与挣值比较,还可测量绩效。

这些内容可以说是项目开发之中很常用的方法,依据都是源自PMP思想的。有时,我们不依赖PMP思想也可以有这种与之类似的方法,但这些方法可能不全面,总是会出现疏漏。PMP力争把不同类型的项目所需要用到的方法抽象成公用的思想,以作为项目经理遵守的标准。例如,我们所接触到的项目不仅是软件项目,还有可能是建筑工程项目,这些不同类型的项目实际上都可以参考PMP思想。

本章,我们来搭建一个企业管理系统的基本的框架以便接下来的开发。首先,打开MyEclipse10.7,在弹出的Workspace Launcher对话框中找到Workspace,在旁边的文本框中输入manage,点击确定,路径建议放在非系统盘,如E盘。

进入manage工作空间之后,按照第1章讲解的方法,配置好JDK、Tomcat服务器。新建Web Project项目,名称叫作manageServlet,将manageServlet发布到Tomcat服务器后启动。在浏览器中输入http://localhost:8080/manageServlet,如果页面上显示“This is my JSP page.”的字样,说明项目创建成功,访问也没有任何问题。这个是最基本的项目环境,一般来说,新建一个Web Project之后,将其发布到Tomcat里面并且运行,因为该项目里面自带JSP页面,所以会出现“This is my JSP page.”的字样,这说明该项目是完整的Java EE项目,只不过没有往里面加入内容。我们的开发任务,其实就是往这个空白的项目里面不断加入内容,直到它的功能越来越多,符合了客户的需求,直至最后成为公司的产品。

使用Servlet方式开发管理系统的源码项目名称是manageServlet,数据库使用Oracle 10g,标准脚本是manage.dmp,可以使用Oracle10g新建数据库manage,再使用PLSQL导入manage.dmp即可。

Servlet是用Java编写的服务器端程序。它的主要功能是创建动态的可交互的Web内容。若干年前,没有Struts、Spring和Hibernate的时候,我们所接触的项目基本上都是以Servlet来实现交互性的。虽然现在有比Servlet更好的选择了,但是Servlet仍然是Java Web领域中不可缺少的组成部分,也是需要学习的最基础部分。

企业管理系统的第一期内容,我们采用最传统的Servlet开发。现在我们要做的内容,就是往空白的manageServlet项目里面增加Servlet的内容。此外,我们按照传统的逻辑思维来开发项目,如企业管理系统,我们暂时不管它是何种框架开发的,首先它都需要一个登录界面,那么我们第一步要做的就是开发一个登录界面。

首先,打开manageServlet项目,选中WebRoot文件夹,将其改名为WebContent。选中manageServlet项目,右键点击Properties属性,在MyEclipse选项中打开Web对话框,点击Web-root folder对应的Browse按钮,选择项目中的WebContent文件夹。这时根目录就更改成功了,接着我们在WebContent文件夹新建登录页面。

在此之前,我们可以先了解一下Servlet的生命周期。

Servlet被服务器初始化之后,容器运行其init()方法,init()方法仅执行一次,主要是为了做一些公用的配置,以便接下来的请求直接使用而不用重复加载。当请求到达时运行service()方法,service()方法会根据客户端请求的种类自动调用与之匹配的doGet()或者doPost()方法。举个例子,如果在提交Form表单的时候用的是Get方式,对应的就是doGet()方法;如果用的是Post方式,对应的就是doPost()方法。接着,将具体的业务逻辑在对应的do()方法中执行。最后,当服务器需要销毁Servlet实例的时候,就会执行destroy()方法,可以在这个方法里写一些需要的操作,login.jsp登录功能页面如代码清单3-1所示。

代码清单3-1 login.jsp

<%@ page language="java" contentType="text/html; charset=gbk"
    pageEncoding="gbk"%>
<%
    String path = request.getContextPath();
    String basePath = request.getScheme() + "://"
            + request.getServerName() + ":" + request.getServerPort()
            + path + "/";
%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" 
    "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=gbk">
<link href="css/layout.css" rel="stylesheet" type="text/css" />
</head>
<body>
    <div id="container">
        <div id="header">
            <div align="center">
                <marquee>
                    <a>企业管理系统的Servlet第一期,正在开发中...</a>
                </marquee>
            </div>
        </div>
        <div id="mainContent">
            <%
                String name = (String) session.getAttribute("name");
            %>
            <div id="sidebar">
                <%
                    if (name == null) {
                        name = "";
                    } else {
                %>
                <%=name%>已登录
                <%
                    }
                %>
            </div>
            <div id="content">
                <form method="post" action="LoginServlet" align="center">
                    用户名:<input type="text" name="name" value="" />口令:<input
                        type="password" name="passwd" value="" style="width: 155px;" /><br>
                    <p>
                        <input type="submit" value="登录" name="Submit" />
                       <input type="reset" value="重置" name="" />
                    </p>
                </form>
            </div>
        </div>
    </div>
</body>
</html>

代码解析

 

(1)marquee标签用于设置滚动文字,我们在其中加入了一个超链接,它的内容是“企业管理系统的Servlet第一期,正在开发中…”,这段文字只是用来大概地说明一下企业管理系统初期的开发情况,在后期肯定是要去除的。

(2)在ID名称为contentdiv标签中,我们加入了一个form标签,它由用户名、口令、登录、重置这几个功能组成。显而易见,这个div的内容就是我们的登录界面。当我们在首页输入用户名和口令的时候,点击登录按钮就可以提交这个表单进行后台验证了。

(3)从浏览器地址栏可以看到,该项目的地址是http://localhost:8080/manageServlet/login.jsp,也就是说项目的根目录是manageServlet,对应的页面是login.jsp。在Java EE项目中,有时候我们可以在地址栏中看到对应的JSP页面,有时候看不到,这主要取决于我们所用到的框架和交互行为。如果在浏览器地址栏里输入http://localhost:8080/manageServlet,会发现仍然跳转到了login.jsp页面,这是因为在web.xml里进行了<welcome-file-list>元素的配置。

(4)从首页的布局可以看出来,近几年Java EE流行的后台管理系统基本上都是这样的布局。一个Banner,一个左侧菜单树,一个右侧详情页。这样做的好处是,符合我们日常操作的习惯。例如,在Banner可以添加广告,或者放一些公共的信息;在左侧菜单树存放所有的菜单信息;每当点击左侧菜单树的某一个选项的时候,右侧的详情页会自动列出详细情况。

目前login.jsp的静态页面还很简单,如果我们还需要进一步开发的话,就需要在后端写Java处理类,还有在web.xml里面配置Servlet文件,这些内容都是我们使用Servlet开发企业管理系统的重点,login.jsp登录过程如图3-1所示。

图3-1 静态的登录页面

在编程领域中,有前端验证和后端验证之说。前端验证就是通过JavaScript、jQuery来验证当前界面的逻辑和数值,如果符合要求就通过,反之则不通过。后端验证就是直接让请求携带参数进入服务器端,在Java代码中做一些验证和控制。在本节中,我们主要讲解前端验证,具体的内容可以看下面这个例子。

说得更加具体一些,在本节中我们需要做用户的登录验证,具体的做法是利用JavaScript提供的功能语法或者正则表达式来进行验证。login.jsp页面非常简单,但是这个页面有登录和重置功能。所谓编程或者开发项目的具体过程,就是让这些简单的功能逐渐变得丰富起来。例如,目前的登录功能并没有跟项目的后端做任何交互,如果仓促地进行后端验证可能会出现未知问题。但是为了实现用户的需求,我们也可以在前端进行一些验证,毕竟前端验证也是一种成熟的方法。等满足了前端验证后,如果用户需要使用后端验证或者对验证提出了更高的要求,再把后端验证加上也不迟。一般来说,后端验证的开发过程是和业务开发同时进行的。

通过研究代码,我们可以看到登录按钮的typesubmit类型的。也就是说,在点击该按钮的时候会触发提交操作,对应的路径就是action的LoginServlet。为了增加前端验证,我们就需要改写代码,让它在提交之前进入一个验证方法里面去,通过验证后再进行提交。明确了这个需求之后就可以进行下面的代码开发了。

首先,在login.jsp页面中修改登录代码模块并为它增加onclick事件。接着,在非body元素外面增加一个用于触发onclick事件的方法,具体代码如下:

<script language="javascript" type="text/javascript">
    function validate(){
        alert("验证");
    }
</script>
      <input type="submit" value="登录" name="Submit" onclick="validate()" />

增加这段代码后,再点击登录按钮便可以发现,程序触发了validate()方法并且在页面上弹出了“验证”这两个字,但是通过调试模式打断点可以发现,程序在弹出汉字后直接进入了LoginServlet类里面,这明显不是我们想要的结果。简而言之,这段代码的修改并没有起到任何验证作用。为此,我们还需要再进行修改。将登录代码的typesubmit改为button做一次试验,结果发现程序并没有自动提交,说明button标签可以满足我们的需要。其实,input标签的type属性有很多,submitbutton的区别在于设置成submit后,点击按钮会自动提交Action,也就是说它的默认功能就是form.submit()。而type设置成button后就会灵活起来,把自动提交变成手动提交。这样,我们就可以在完成前端验证后,手动触发form.submit()从而顺利地进入后端代码。

这样修改后,前端验证的入口就写好了,接下来便可以进行具体的实现了。首先,我们做一个简单的伪验证规则,假定用户名是张三且密码是123就不让该用户登录。为了获取用户名和密码,分别为用户名和密码对应的元素增加id属性。

用户名:<input id="userName" type="text" name="name" value="" /> 
口  令:<input id="userPwd" type="password" name="passwd" value="" style="width: 155px;" />

接下来,开始修改validate()方法。

function validate(){
    var userName = document.getElementById("userName").value;
    var userPwd = document.getElementById("userPwd").value;
    if(!(userName == "张三" || userPwd == "123")){
        document.getElementById("login").submit();
    }else{
        alert("登录信息不正确");
    }
}

代码解析

 

(1)在前端开发中,JavaScript功底是非常重要的,尤其是对基本技能的掌握。这段简单的代码,是通过document命令通过id来获取具体的元素值。当然,也可以通过name属性获取,这在写法上稍微有所不同但原理是一样的,都是根据DOM树的原则,分别获取树叶的某个结点和元素。

(2)拿到userNameuserPwd数据后,需要对这两个数据进行验证。因为这段代码的验证规则是我们自定义的,也就是说如果用户名是张三或者密码是123满足其中的任何一个条件都算登录失败,会进入else分支,那么符合的情况自然是这两个条件的取反运算了。所以语法上这样写会更加清晰,易于理解。

(3)运行程序,在文本框中分别输入张三和123进行测试,最终得出这样可行的结论。反之会进入后端代码,标志着前端验证失败。

一般情况下,这种验证就属于前端验证的典型写法,但是我们在实际业务操作的时候,这种规则往往不是写死的,而是需要我们使用正则表达式来进行验证。在学习阶段,我们通常写几个常用的正则表达式就可以了。正则表达式在网上有很多固定的参考,若需要更多的则可以进行查询,此处我们只列出需要用到的,具体内容包括中文、英文、数字但不包括下划线等符号:^[\u4E00-\u9FA5A-Za-z0-9]+$。密码(以字母开头,长度为6~18个字符,只能包含字母、数字和下划线):^[a-zA-Z]\w{5,17}$

在对代码进行功能修改的时候,如果前面的功能有可能还会用到,我们会将原来的代码注释掉,并且复制一份新的代码进行修改。这样做有很多好处,既方便自己开发也方便别人开发。注释方法是选中代码段,同时按下Ctrl+Shift+/,取消注释是Ctrl+Shift+\。将原来的validate()方法复制一份新的,然后开始修改。

function validate() {
    // 定义验证规则
    var userNameReg = /^[\u4E00-\u9FA5A-Za-z0-9]+$/;
    var userPwdReg = /^[a-zA-Z]\w{5,17}$/;
    var userName = document.getElementById("userName").value;
    var userPwd = document.getElementById("userPwd").value;
    // 使用test验证
    var userNameResult = userNameReg.test(userName);
    var userPwdResult = userPwdReg.test(userPwd);
    // 如果满足条件通过验证
    if (userNameResult == true && userPwdResult == true) {
        document.getElementById("login").submit();
    } else {
        alert("登录信息不正确");
    }
}

代码解析

 

(1)定义userNameReguserPwdReg这两个变量,它们的数值对应相应的正则表达式。

(2)定义userNameResultuserPwdResult来保存正则表达式的布尔值,其具体运算过程是通过test()方法进行的。最后,通过获得的布尔值来决定程序的走向。这种做法就是正则表达式的典型应用,也就是说,把之前写死的数值换成了通用的正则表达式来进行计算。

后端登录验证的意思是Java在后端使用代码进行逻辑判断,从而决定程序的走向,或者向前端返回一个需要的数值,接着再通过该数值决定前端代码的走向。后端验证可以完全忽略前端,把所有验证规则写到后端,也可以把一部分验证写在前端,另一部分验证写在后端,从而尝试出最合适的验证集合。在本例中,后端验证的规则并不多,着重讲述其概念和实现方式。

再次回到后端代码,要进行后端验证就必须跟服务器打交道做动态的交互。那么,在管理系统的当前版本中,我们所使用的动态交互方式是通过Servlet完成的。Servlet的主要功能是创建动态的、可交互的Web内容,而当前的login.jsp其实已经具备了这样的特点,例如,这段代码<form method="post" action="LoginServlet" align="center">就是一个典型。因为form表单的主要作用就是把当前页面的数据提交到后端再与数据库交互,所以当我们看到JSP页面中的action对应的是LoginServlet的时候,就应该明白了,LoginServlet这个登录模块并非是静态的页面而是动态的程序。

那么接下来我们应该如何理解LoginServlet呢?这一点我们可以在web.xml里找到答案。打开web.xml文件,找到LoginServlet对应的内容就可以明白其中的道理。

<servlet>
    <servlet-name>Login_Servlet</servlet-name>
    <servlet-class>com.manage.servlet.LoginServlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>Login_Servlet</servlet-name>
    <url-pattern>/LoginServlet</url-pattern>
</servlet-mapping>

首先我们在做后端登录验证的时候必须先明白Servlet是怎么一回事,否则还没有明白原理就直接去写代码是一件不明智的事情。

我们从了解Servlet的配置元素开始。

一个完整的Servlet包含一个<servlet>元素和一个<servlet-mapping>元素,他们都是互相对应的。如果这两者是同一个功能模块的话,它们的<servlet-name>必须是一样的。例如,登录页面的<servlet-name>都是Login_Servlet。接下来我们可以具体学习一下这些元素的用法。

配置好了Servlet之后,我们就可以正式开发Servlet对应的登录类了。首先我们在manageServlet项目下,新建com.manage.servlet包,在这个包底下建立所有本项目的Servlet类。接着打开这个包,在包底下新建LoginServlet类。LoginServlet类后端登录验证功能如代码清单3-2所示。

代码清单3-2 LoginServlet.java

package com.manage.servlet;
import java.io.IOException;
import java.util.List;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import com.manage.bean.User;
import com.manage.db.OracleDB;
import com.manage.dom.JdomRead;
@SuppressWarnings("serial")
public class LoginServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        boolean loginyes = false;
        String t_name = (String) req.getParameter("name");
        String t_passwd = (String) req.getParameter("passwd");
        if (t_name == null || t_name.equals("") || t_passwd == null
                || t_passwd.equals(""))
            resp.sendRedirect("login.jsp");
        else {
            JdomRead xmlr = new JdomRead(this
                    .getServletContext().getRealPath("/WEB-INF/config/")
                    + "/manage_config.xml");
            try {
                xmlr.jdomReader();
            } catch (Exception e) {
                e.printStackTrace();
            }
            // 数据库配置
            OracleDB.DB_NAME = xmlr.getDb_name();
            OracleDB.DB_HOST = xmlr.getDb_host() + ":" + xmlr.getDb_port();
            OracleDB.DB_USERNAME = xmlr.getDb_username();
            OracleDB.DB_PASSWORD = xmlr.getDb_passwd();
            List<User> ulist = xmlr.getList();
            HttpSession session = req.getSession();
            for (User u : ulist) {
                if (u.getName().equals(t_name))
                    if (u.getPasswd() == t_passwd.hashCode()) {
                        session.setAttribute("name", t_name);
                        System.out.println("t_name==" + t_name);
                        loginyes = true;
                    }
            }
            if (loginyes) {
                RequestDispatcher dispatcher = req
                        .getRequestDispatcher("index.jsp");
                dispatcher.forward(req, resp);
            } else {
                resp.sendRedirect("login.jsp");
            }
        }
    }
}

代码解析

 

(1)@SuppressWarnings("serial"):因为继承HttpServlet类是默认需要实现序列化的,也就是实现Serializable接口。众所周知,实现序列化需要声明serialVersionUID否则就会报黄色警告,为了消除这个黄色警告可以加上这个注解。

(2)序列化:是将对象的内容分解成字节流以便存储在文件中或在网络上传输。

(3)反序列化:是打开并且读取字节流并且从流中恢复对象。序列化的好处是方便数据流通过网络进行传输,而在传输完毕后,还可以通过反序列化将数据流恢复成对象。这样的话,开发人员就可以重新使用该对象与数据库进行交互了。任何类型的数据只要实现了Serializable接口,都可以进行序列化。

(4)序列化可以形象地理解:例如,需要复制100个文件,如果直接复制的话可能会很慢,碍于其他更多限制的话可能还无法进行复制。那么我们就可以把这100个文件进行压缩,压缩的规则就是序列化。等压缩结束后,这100个文件就会变成1个文件,这样操作起来不但速度快,而且还符合了传输要求,从而可以顺利地在网络中传输。等这1个文件传输到对应的地点后,我们再对它进行解压缩操作,也就是反序列化,把它重新还原成100个文件。

(5)String t_name = (String) req.getParameter("name")等相关语句:通过对HttpServlet提供的doPost()方法进行重写,可以获取到HttpServletRequest reqHttpServletResponse resp两个Java内置对象。这样的话,我们便可以直接使用这两个内置对象来做它们支持的所有操作了。首先,我们拿到了t_name对象,也就是用户名和密码。接着,我们分别对t_namet_passwd进行null和空字符串验证。如果数据为null或者空字符串,则返回login.jsp页面,否则就进行下一步的操作。

(6)getRequestDispatcher是服务器内部跳转,它的特点是地址栏信息不变,且只能跳转到Web应用内的网页。而另一个跳转方法是sendRedirect,它的特点是对页面重定向,因此地址栏信息会改变,可以跳转到任何网页。这两种跳转方式,一般用在Java后端把所有的业务逻辑处理完毕之后,再携带参数返回到合适的页面。例如,登录成功后可以直接跳转到欢迎界面,登录失败后可以直接跳转到错误界面。

注意,当前的Servlet只完成了最初的登录需求。例如,基本的后端空值验证;从List里拿到登录信息,确定无误后跳转到正确的页面。如果已经登录成功会跳转到index.jsp页面,否则跳转到login.jsp页面再进行一次登录。

在这个Servlet类中,我们所需要的用户信息没有从数据库里读取,而是直接从一个List里拿到。这种情况显然是不够完善的,但在项目初期也可以使用这种做法来满足暂时的需求,但随着项目的迭代会慢慢改进。我们从manage_config.xml中拿到数值,再通过循环使用equals()方法来对比用户信息,如果确定是正确的信息后,将用户登录信息存储在Session对象里,并且将loginyes置为true返回index.jsp页面,否则返回login.jsp重新登录。

在本Servlet中,我们把所有的业务逻辑都写在了doPost()方法中,这是根据login.jsp页面的Form表单提交方式决定的,例如,登录页面的表单的method属性设置成了Post方式。至于GetPost的区别,是一个老生常谈的问题,可以大概归纳一下:使用Get传递参数,所有的内容都是可见的,可以理解为显式传递;使用Post传递参数,所有的内容都放入HTTP的包体中了,用户看不到这些内容,可以理解为隐式传递。这样来说,PostGet更安全。另外,Get的传输有大小限制,不会超过2 KB,而Post传输在理论上没有限制。

对登录功能进行测试后,会发现当前的逻辑很不方便也不符合常理。所以,接下来我们需要修改登录功能,把通过XML文件登录修改成通过从数据库表里获取用户名和密码信息登录,与之对应的验证规则也需要进行改动。只有这样,真正的业务逻辑才能建立起来。否则,根据之前的写法,登录账号都配置在XML文件里的话是不符合需求的,那样的做法可以临时使用但不能长期使用。作为一个成熟的项目,用户信息肯定是要保存在数据库里的。但是,要从数据库里读取登录信息就必须先开发注册功能,只有通过注册功能写入数据后才能通过登录功能读取数据。

管理系统如果要摆脱依靠配置文件来登录的话,就需要把用户信息保存在数据库里。接下来,我们正式进入注册功能的开发。等开发好了注册功能再回过头来修改原有的登录功能,否则这个完整的逻辑无法贯穿成功。

回到login.jsp页面,在该页面上添加注册按钮,并且分别实现前端和后端的开发。可以先分析一下注册的需求,该需求并没有复杂的功能,只是需要进行一个Form表单的提交。但是,如果把这个表单写在login.jsp页面的话,就会跟原有的登录功能发生冲突,从页面的展示上看也不太美观。所以,我们将注册功能设计成这样的:在重置按钮后面增加注册按钮,当点击注册按钮的时候跳转到一个新的单独页面,来完成用户信息的录入。在WebContent目录下新建注册功能页面register.jsp,该页面如代码清单3-3所示。

代码清单3-3 register.jsp

<%@ page language="java" contentType="text/html; charset=utf-8"
    pageEncoding="utf-8" isELIgnored="false"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>个人用户注册</title>
<script type="text/javascript" src="js/jquery-1.8.2.min.js"></script>
<script type="text/javascript">
    function validate() {
        // 定义验证规则
        var userNameReg = /^[A-Za-z]+$/;
        var userPwdReg = /^[a-zA-Z]\w{5,17}$/;
        var userEmailReg = /^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/;
        var userMobileReg = /^1\d{10}$/;
        // 取值
        var userName = document.getElementById("userName").value;
        var userPwd = document.getElementById("userPwd").value;
        var userEmail = document.getElementById("userEmail").value;
        var userMobile = document.getElementById("userMobile").value;
        // 获取勾选状态
        var userAgree1 = document.getElementById("userAgree").checked;
        var userAgree2 = $("input[type='checkbox']").is(':checked');
        // 使用test验证
        // 只能是英文
        var userNameResult = userNameReg.test(userName);
        // 最少6位
        var userPwdResult = userPwdReg.test(userPwd);
        // 邮箱常规验证,例如,必须有@符号
        var userEmailResult = userEmailReg.test(userEmail);
        // 手机常规验证,例如,只能是11位
        var userMobileResult = userMobileReg.test(userMobile);
        // 如果满足条件通过验证
        if (userNameResult == true && userPwdResult == true
                && userEmailResult == true && userMobileResult == true
                && userAgree1 == true) {
            document.getElementById("userRegister").submit();
        } else {
            alert("注册信息不正确");
        }
    }
    function flush() {
        window.location.reload();
    }
</script>
<style type="text/css">
form {
    font-size: larger;
}
#t1 {
    position: absolute;
    top: 100px;
    left: 36%;
}
#b1 {
    height: 30px;
    width: 60px;
    font-size: 14px
}
#d1 {
    position: absolute;
    left: 10%;
    top: 2%;
}
</style>
</head>
<body bgcolor="#BAFEC0">
    <p align="center">
        <font size="+3">用户注册</font>
    </p>
    <form action="RegisterServlet" method="post" id="userRegister">
        <table id="t1">
            <tr>
                <td>用户名:</td>
                <td><input type="text" id="userName" name="userName" />
                </td>
                <td><c:choose>
                        <c:when test="${userName==null}">
                            <span id="td1">只能是英文</span>
                        </c:when>
                        <c:otherwise>
                            <span>${userName}</span>
                        </c:otherwise>
                    </c:choose>
                </td>
            </tr>
            <tr>
                <td>密&nbsp;码:</td>
                <td><input type="password" id="userPwd" name="userPwd" />
                </td>
                <td id="td2">最少6位</td>
            </tr>
            <tr>
                <td>邮&nbsp;箱:</td>
                <td><input type="text" id="userEmail" name="userEmail" />
                </td>
                <td id="td4">请输入正确的邮箱地址</td>
            </tr>
            <tr>
                <td>手机号码:</td>
                <td><input type="text" id="userMobile" name="userMobile" />
                </td>
                <td id="td5">请输入正确的手机号</td>
            </tr>
            <tr>
                <td>住&nbsp;址:</td>
                <td><input type="text" id="address" name="address" />
                </td>
                <td id="td6"></td>
            </tr>
            <tr>
                <td colspan="3">注册协议</td>
            </tr>
            <tr>
                <td colspan="3"><textarea cols="56" rows="10">
<jsp:include page="document/register.txt"></jsp:include>
</textarea>
                </td>
            </tr>
            <tr>
                <td colspan="3" align="right">同意<input type="checkbox"
                    id="userAgree" name="userAgree">&nbsp;&nbsp;</td>
            </tr>
            <tr align="center">
                <td><input type="button" value="提交" id="b1"
                    onclick="validate()"></td>
                <td></td>
                <td><input type="reset" value="重置" id="b2" onclick="flush()">
                </td>
            </tr>
        </table>
    </form>
</body>
</html>

代码解析

 

(1)validate():包含了用于前端验证的正则表达式。

(2)flush():用于点击重置的时候重新加载当前页面。

(3)<form action="RegisterServlet" method="post" id="userRegister"> </form>:这段完整的Form表单代码包含了所有需要录入的用户信息,以及点击提交需要触发的Action地址。当输入完信息点击提交的时候,程序会先执行validate()方法,通过前端正则表达式验证后,程序进入对应的RegisterServlet开始后端代码的执行。

web.xml是项目中比较重要的配置文件,用来做一些初始化的操作。例如,项目的Welcome页面、Servlet、Filter、Listener和这些程序的加载级别都在该文件中设置。如果项目中没有web.xml文件或许可以运行,但会失去很多重要的配置。这样的项目是无法适应用户的需求的,如果出现了高并发程序很容易挂掉,这是因为web.xml里配置的都是Application级别的内容。

接下来需要开发RegisterServlet对应的XML配置文件。首先新建一个名称为test的项目,打开WebRoot \WEB-INF下的web.xml,可以看到新项目的配置内容,新项目的web.xml如代码清单3-4所示。

代码清单3-4 web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" 
        xmlns="http://java.sun.com/xml/ns/javaee" 
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
        xsi:schemaLocation="http://java.sun.com/xml/ns/javaee 
        http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
    <display-name></display-name>    
    <welcome-file-list>
        <welcome-file>index.jsp</welcome-file>
    </welcome-file-list>
</web-app>

代码解析

 

可以看到新项目test的web.xml文件中无非是声明了字符编码、版本号之类的信息,还有欢迎页面。这些内容是IDE自动生成的,各种默认规则都是官方定义的,可以不用去过于深入地研究只需要保持原有配置即可。

接下来打开manageServlet项目的web.xml文件,结合test项目的web.xml文件做个对比就可以看出其中的道理。我们可以看到,manageServlet项目的web.xml中增加了很多配置,除了修改了欢迎页面,增加了错误处理页面外,大多数改变都是增加了Servlet的配置信息。如果要给RegisterServlet增加配置信息,只需要找到其中一个配置好的Servlet实例,在其下面输入下面这段代码即可。

<servlet>
    <servlet-name>Register_Servlet</servlet-name>
    <servlet-class>com.manage.servlet.RegisterServlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>Register_Servlet</servlet-name>
    <url-pattern> /RegisterServlet </url-pattern>
</servlet-mapping>

之前已经详细讲解过Servlet的配置元素了,在这里只需要重点关注几个注意事项即可。首先,需要明确我们是给RegisterServlet这个Servlet配置信息,所以与之对应的名称中最好包含RegisterServlet单词,这样做的好处是方便开发人员明白其含义,就算没有注释大家也能看懂什么意思。接着,Class对应的地址中入口类的命名最好跟JSP中写的信息一样,如RegisterServlet,这样的话不但方便开发人员阅读,也避免了因为名称混乱造成的不必要的麻烦。在过去的项目开发时,很多程序员经验不足,总是给一套互相匹配的配置起不同的名字,从而导致Tomcat无法启动却又找不到原因,所以最好的做法就是保持名称的一致。

另外,从JSP的Form表单的Action语句可以看到,register.jsp的提交地址也是RegisterServlet。这样的话,不论是提交地址还是web.xml的配置信息都在名称上保持了一致。这种做法不但方便代码阅读,还方便代码查询。

在这里介绍一种方法,那就是使用检索方式学习。例如,我们知道了Servlet的提交地址是RegisterServlet,通过常识就可以知道后端与前端的交互不仅仅是通过一些命令来完成的,还需要大量的XML文件的配置。那么,我们就可以利用IDE提供的项目检索工具来查找RegisterServlet语句,从而找出所有与之相关的内容。可以尝试一下,在Package Explorer窗口中选中manageServlet项目,选择菜单栏中的Search项,选择File功能。在弹出的对话框中,可以看到一行菜单项:File Search、Java Search、JavaScript Search等,简而言之就是选择与之对应的搜索。在这里选择File Search,在Containing text文本框中输入“RegisterServlet”,点击File name patterns右侧的Choose按钮,在弹出的下拉列表框中选择.xml类型后点击OK,这样File name patterns文本框的内容就会由变成*.xml,表示自动匹配xml扩展名的文件。Scope页签用来选择范围,在这里选择Selected resources,表示确定的搜索范围是manageServlet项目,点击Search开始搜索,Search功能的界面如图3-2所示。

图3-2 使用Search功能检索代码

在搜索结果列表中,可以看到第一条记录的信息,其他的都会隐藏起来。点击右侧的加号按钮,将所有符合条件的记录展示出来,就可以看到manageServlet中所有包含RegisterServlet文字的页面。双击web.xml中的内容打开详细页面。根据RegisterServlet的上下文信息,就可以推断出这些内容的作用。

在web.xml中,已经配置了RegisterServlet,对应的类地址是com.manage.servlet.RegisterServlet,打开com.manage.servlet包,在该包底下创建RegisterServlet类,这是开发注册功能的Servlet类。这个类作为注册功能的入口类需要完成一些业务逻辑,但大多业务逻辑是从前端取值再做一些封装,RegisterServlet注册功能类如代码清单3-5所示。

代码清单3-5 RegisterServlet.java

package com.manage.servlet;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.manage.bean.SiteUser;
import com.manage.db.RegisterService;
import com.manage.util.CodeMd5;
public class RegisterServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        RegisterService register = new RegisterService();
        String userName = req.getParameter("userName");
        String userPwd = req.getParameter("userPwd");
        String userEmail = req.getParameter("userEmail");
        String userMobile = req.getParameter("userMobile");
        String userAddress = req.getParameter("userAddress");
        SiteUser user = new SiteUser();
        CodeMd5 md5 = new CodeMd5();
        try {
            String md5Pwd = md5.CodeMd5(userPwd);
            user.setUserPwd(md5Pwd);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        user.setUserName(userName);
        user.setUserEmail(userEmail);
        user.setUserMobile(userMobile);
        user.setUserAddress(userAddress);
        register.add(user);
        RequestDispatcher dispatcher = req
                .getRequestDispatcher("registersuccess.jsp");
        dispatcher.forward(req, resp);
    }
}

代码解析

 

(1)RegisterService register = new RegisterService():新建Service对象,后面会使用它的add()方法传入封装好的实体SiteUser类。

(2)String userName = req.getParameter("userName")等:类似的这几个方法都是从前端读取数据,存入新建的SiteUser类中,依次对应该类成员变量的Set方法。

(3)CodeMd5 md5 = new CodeMd5():新建md5工具类,将明码传入此类的方法中加密。

(4)register.add(user):进入持久层直接与数据库交互,将用户信息写入数据库对应的表中。

(5)RequestDispatcher dispatcher = req.getRequestDispatcher ("registersuccess. jsp"):与数据库交互结束后返回registersuccess.jsp页面,也就是注册成功页面。

接下来需要开发SiteUser实体Bean类。众所周知,Java是面向对象的编程。对于生活中的元素,Java都可以采用提取的方式,将该元素封装成一个实体Bean类以供后期使用。例如,SiteUser这个站点用户类,它的作用就是记录站点用户的信息,如常见的姓名、密码、邮箱等。之前我们说过Servlet命名规则需要遵守一致性的原则,那么在实体类与数据库表对应的关系上,最好也遵循这样的规范。举例就是SiteUser实体类对应数据库中的SiteUser表,最好连字段也做到逐个对应,从而最大化地减少代码阅读的干扰。

如果对于站点的用户信息,我们不使用实体Bean类封装的话,在做register.add(user)这步操作的时候,至少要往add()方法中传入5个参数,分别对应用户名、用户密码、用户邮箱、用户手机号和用户地址,这样的话该语句就会变成register.add(userName, userPwd, userEmail, userMobile, userAddress),虽然可以实现同样的功能,但是它不符合Java面向对象的思想,也没有进行任何封装。正确的做法是将这5个参数封装在SiteUser类中。这样做的好处显而易见:第一,在涉及用户操作的时候可以直接传入SiteUser类,缩减参数;第二,更加符合面向对象的编程思想,因为它对事物进行了封装;第三,方便程序员开发和阅读。

打开com.manage.bean包,在该包底下创建SiteUser站点用户信息类,如代码清单3-6所示。

代码清单3-6 SiteUser.java

package com.manage.bean;
public class SiteUser {
    private String userName;
    private String userPwd;
    private String userEmail;
    private String userMobile;
    private String userAddress;
    public String getUserName() {
        return userName;
    }
    public void setUserName(String userName) {
        this.userName = userName;
    }
    public String getUserPwd() {
        return userPwd;
    }
    public void setUserPwd(String userPwd) {
        this.userPwd = userPwd;
    }
    public String getUserEmail() {
        return userEmail;
    }
    public void setUserEmail(String userEmail) {
        this.userEmail = userEmail;
    }
    public String getUserMobile() {
        return userMobile;
    }
    public void setUserMobile(String userMobile) {
        this.userMobile = userMobile;
    }
    public String getUserAddress() {
        return userAddress;
    }
    public void setUserAddress(String userAddress) {
        this.userAddress = userAddress;
    }
}

代码解析

 

(1)这段代码没有什么难度,全都是Java最基础的语句:新建userNameuserPwduserEmailuserMobileuserAddress这5个变量,并且对它们分别新建get/set方法。

(2)get/set方法的具体作用:我们之所以需要在程序中用到SiteUser类,就是为了数据传输。那么数据传输对应的两个操作分别是获取数据和保存数据,而获取数据与之对应的操作需要使用get方法;保存数据与之对应的操作需要使用set方法。如果没有新建成员变量的get/set方法,该类仍然可以使用,但是它们的初始值需要遵守Java内部机制的设置。例如,Integer的初始值是nullint的初始值是0,这些内部机制又涉及了Java的基本数据类型和封装类的概念。这两者最大的区别是:封装类提供了一些常用的方法,而基本数据类型没有方法。

(3)get/set方法的快捷方式:如果有50个变量,就需要生成100个get/set方法。这样是非常浪费时间的,因此IDE提供了快速生成get/set的方法。打开菜单栏的Source选项,选择列表中的Generate Getters and Setters功能,在Select getters and setters to create列表中选择需要生成的变量点击OK即可。当然,也可以选择Select All进行全选,选择Access modifier选择变量的方式,是public还是protected或者private等。

接下来我们开发MD5加密类,打开com.manage.util包,在该包底下新建CodeMd5类,主要用于Md5加密。在保存密码的时候如果不进行加密,密码会以明文的方式保存在数据库里,这样是很不安全的,所以我们应该对密码进行Md5加密,CodeMd5加密算法类如代码清单3-7所示。

代码清单3-7 CodeMd5.java

package com.manage.util;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import sun.misc.BASE64Encoder;
/**
 * 利用MD5进行加密
 */
public class CodeMd5 {
    public String CodeMd5(String str) throws NoSuchAlgorithmException,
            UnsupportedEncodingException {
        // 确定计算方法
        MessageDigest md5 = MessageDigest.getInstance("MD5");
        BASE64Encoder base64en = new BASE64Encoder();
        // 加密后的字符串
        String newstr = base64en.encode(md5.digest(str.getBytes("utf-8")));
        return newstr;
    }
}

代码解析

 

(1)MessageDigest md5 = MessageDigest.getInstance("MD5"):该语句会新建一个md5对象,以方便在接下来的代码中使用这个对象提供的方法。

(2)BASE64Encoder base64en = new BASE64Encoder():使用64位加密算法。

(3)String newstr = base64en.encode(md5.digest(str.getBytes("utf-8"))):返回加密后的字符串。

接下来我们开发注册功能的持久层,打开com.manage.db包,在该包底下新建RegisterService类,该类是注册功能的持久层。一般来说,Java最简洁有效的是三层架构,而在当前项目中我们仅使用了两层,分别是Servlet实现层和Service持久层,没有Dao层。如果是简单的项目这样做是没有什么影响的,但是复杂的项目最好还是合理地设定Java代码的层次架构,以便于不断地扩展。RegisterService类获取到了Servlet层传递过来的参数,会直接调用持久化对象与数据库交互,RegisterService持久层类如代码清单3-8所示。

代码清单3-8 RegisterService.java

package com.manage.db;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.UUID;
import com.manage.bean.SiteUser;
public class RegisterService {
    public void add(SiteUser user) {
        Connection conn = OracleDB.createConn();
        UUID Id = java.util.UUID.randomUUID();
        String sql = "insert into SiteUser values (?, ?, ?, ?, ?, ?)";
        PreparedStatement ps = OracleDB.prepare(conn, sql);
        try {
            ps.setString(1, Id.toString());
            ps.setString(2, user.getUserName());
            ps.setString(3, user.getUserPwd());
            ps.setString(4, user.getUserEmail());
            ps.setString(5, user.getUserMobile());
            ps.setString(6, user.getUserAddress());
            ps.executeUpdate();
        } catch (SQLException e) {
            e.printStackTrace();
        }
        OracleDB.close(ps);
        OracleDB.close(conn);
    }
}

代码解析

 

(1)Connection conn = OracleDB.createConn():新建一个Connection连接对象,以方便进行数据库持久化操作。

(2)UUID Id = java.util.UUID.randomUUID():利用UUID来生成ID序列,UUID是Java中最常用的ID生成方式。

(3)PreparedStatement ps = OracleDB.prepare(conn, sql):新建PreparedStatement对象,用来与数据库进行持久化操作。PreparedStatement接口继承了Statement,使用它的好处是可以动态设置参数,因此PreparedStatement又被称作预处理语句对象。典型的使用PreparedStatement对象的时候,可以在SQL语句中传入?来代替具体的参数,再根据数据类型和位置分别传入对应的参数。例如,ps.setString(1, Id.toString())语句第一个参数是ID,它的数据类型是字符串;ps.setString(2, user.getUserName())语句第二个参数是用户名,它的数据类型是字符串。

(4)其他异常写法可以自动生成。另外,需要在程序末端加入资源释放的语句,如OracleDB.close (ps)OracleDB.close(conn)之类。

RegisterService类直接使用了原始的JDBC来创建连接对象。这样做的好处是程序员可以直接操作最底层的对象,开发起来也相对简单。至于执行效率方面,与封装后的相差无几。但是随着项目的深入,使用其他封装过的JDBC也是不错的选择,例如,Spring提供的JdbcTemplateHibernate提供的SessionFactoryMyBatis提供的SqlSessionFactory等。

接下来我们进入OracleDB类的开发过程,该类使用原始的JDBC直接与数据库交互,OracleDB持久层类如代码清单3-9所示。

代码清单3-9 OracleDB.java

package com.manage.db;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
public class OracleDB {
    /** Oracle数据库连接URL */
    private final static String DB_URL = "jdbc:oracle:thin:@";
    public static String DB_NAME = "manage";
    public static String DB_HOST = "127.0.0.1:1521";
    /** Oracle数据库连接驱动 */
    private final static String DB_DRIVER = "oracle.jdbc.driver.OracleDriver";
    /** 数据库用户名 */
    public static String DB_USERNAME = "system";
    /** 数据库密码 */
    public static String DB_PASSWORD = "manage";
    public static Connection createConn() {
        /** 声明Connection连接对象 */
        Connection conn = null;
        try {
            /** 使用Class.forName()创建这个驱动程序的实例且自动调用DriverManager来注册它 */
            Class.forName(DB_DRIVER);
            /** 通过DriverManager的getConnection()方法获取数据库连接 */
            conn = DriverManager.getConnection(
                    DB_URL + DB_HOST + ":" + DB_NAME, DB_USERNAME, DB_PASSWORD);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        if (conn != null) {
            System.out.println("连接成功");
        } else {
            System.out.println("连接失败");
        }
        return conn;
    }
    public static PreparedStatement prepare(Connection conn, String sql) {
        PreparedStatement ps = null;
        try {
            ps = conn.prepareStatement(sql);
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return ps;
    }
    public static void close(Connection conn) {
        try {
            if (conn != null) {
                /** 判断当前连接连接对象如果没有被关闭就调用关闭方法 */
                if (!conn.isClosed()) {
                    conn.close();
                }
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
    public static void close(Statement stmt) {
        try {
            stmt.close();
            stmt = null;
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
    public static void close(ResultSet rs) {
        try {
            rs.close();
            rs = null;
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
        Connection conn = createConn();
        try {
            Statement stmt = conn.createStatement();
            PreparedStatement pstmt = null;
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
    public static String getDB_NAME() {
        return DB_NAME;
    }
    public static void setDB_NAME(String dB_NAME) {
        DB_NAME = dB_NAME;
    }
    public static String getDB_USERNAME() {
        return DB_USERNAME;
    }
    public static void setDB_USERNAME(String dB_USERNAME) {
        DB_USERNAME = dB_USERNAME;
    }
    public static String getDB_PASSWORD() {
        return DB_PASSWORD;
    }
    public static void setDB_PASSWORD(String dB_PASSWORD) {
        DB_PASSWORD = dB_PASSWORD;
    }
    public static String getDB_HOST() {
        return DB_HOST;
    }
    public static void setDB_HOST(String dB_HOST) {
        DB_HOST = dB_HOST;
    }
}

代码解析

 

(1)DB_URLDB_NAMEDB_HOST这3个静态常量分别代表数据库连接中前缀、数据库名称和数据库地址。DB_DRIVER、DB_USERNAMEDB_PASSWORD这3个常量分别代表数据库的驱动、用户名和密码。

(2)Connection对象用于具体的数据库连接动作,在本方法内的语法基本上都是固定的写法,连接成功后返回conn对象以供调用者使用。

(3)PreparedStatement对象用于预处理SQL语句,是一种常见的方式。

(4)close()用于关闭数据库连接,释放资源。

(5)在本类的main()方法中,通过固定写法建立了一个Connection对象。如果该对象与数据库连接成功,就会在控制台输出“连接成功”的字样。这样做的好处是可以在项目没有运行之前,通过Java代码手动调试JDBC是否能够顺利连接,以诊断JDBC连接的错误。

至此,注册功能所有的模块都已经开发完毕。接下来我们根据现有的功能做一个综合调试,来寻找出需要及时改正的bug。

目前我们已经完成了管理系统的登录、注册、验证功能的开发。从整体上看,该项目已经是一个具备完整功能模块的小型项目了。为此我们需要对这个小型项目进行一次完整的综合测试,以找出它的不足,并且及时修复bug,接下来再安排后面的开发任务。首先,打开manage_config.xml文件,将用户名admin和密码a12345加入到Userconfig元素之内。接着,使用该用户信息进行登录。

在http://localhost:8080/manageServlet的登录页面中输入信息,用户名是admin,密码是a12345。待成功登录后,开始对页面上各种业务进行操作,从而完成模块的联调测试。

经过一系列操作后,发现其他功能都是符合要求的。但还差一块,那就是用户的注册功能完成了,但却没有真正生效。在通过用户名和密码登录的时候,仍然需要借助manage_config.xml文件的配置来登录。这样的话,我们之前所做的一切就还差一个环节,这轮测试也就做不到真正的闭环。所以针对这种情况,我们还需要再次完善登录和注册模块,让这块功能完全脱离manage_config.xml的控制。首先,打开LoginServlet类,在涉及登录部分的地方打上断点,例如,在boolean loginyes = false语句左侧。然后,当程序在断点处停留时进入调试模式,通过F6键来逐步调试,了解登录模块的具体业务是如何开展的。例如,在JdomRead语句处可以利用Watch或者Inspect功能来具体查看变量值,通过理解把业务串联起来。等我们完全熟悉了业务之后,就可以对这块代码进行改造了,以便让它更符合用户的需求。在代码中把当前的判断逻辑注释掉,开始写新的逻辑。

首先需要确定修改哪几个文件?这个需求是不依赖配置文件进行登录,也就是说登录信息需要从数据库里读取。那么由此可以知道涉及改动的文件肯定是与数据库相关的。例如,从登录的JSP页面login.jsp开始找起。因为login.jsp页面的Action是提交到LoginServlet的,与此对应的LoginServlet类也需要修改。因为该功能需求是涉及登录的,那么我们完全可以把持久层写在RegisterService里面。这个版本的管理系统是采用Servlet技术实现的,业务也相对比较简单,所以我们没必要非去写Dao层。

打开com.manage.servlet包底下的LoginServlet类,对登录的逻辑进行一次彻底修改,把依赖配置文件进行登录改成通过从数据库取值登录,LoginServlet登录类如代码清单3-10所示。

代码清单3-10 LoginServlet.java

package com.manage.servlet;
import java.io.IOException;
import java.util.List;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import com.manage.bean.SiteUser;
import com.manage.bean.User;
import com.manage.db.OracleDB;
import com.manage.db.RegisterService;
import com.manage.dom.JdomRead;
@SuppressWarnings("serial")
public class LoginServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        boolean loginyes = false;
        String t_name = (String) req.getParameter("name");
        String t_passwd = (String) req.getParameter("passwd");
        if (t_name == null || t_name.equals("") || t_passwd == null
                || t_passwd.equals(""))
            resp.sendRedirect("login.jsp");
        else {
            JdomRead xmlr = new JdomRead(this
                    .getServletContext().getRealPath("/WEB-INF/config/")
                    + "/manage_config.xml");
            try {
                xmlr.jdomReader();
            } catch (Exception e) {
                e.printStackTrace();
            }
            // 数据库配置
            OracleDB.DB_NAME = xmlr.getDb_name();
            OracleDB.DB_HOST = xmlr.getDb_host() + ":" + xmlr.getDb_port();
            OracleDB.DB_USERNAME = xmlr.getDb_username();
            OracleDB.DB_PASSWORD = xmlr.getDb_passwd();
            SiteUser user = new SiteUser();
            user.setUserName(t_name);
            user.setUserPwd(t_passwd);
            CodeMd5 md5 = new CodeMd5();
            try {
                String md5Pwd = md5.CodeMd5(t_passwd);
                user.setUserPwd(md5Pwd);
            } catch (NoSuchAlgorithmException e) {
                e.printStackTrace();
            }
            // 新登录逻辑
            RegisterService register = new RegisterService();
            int userCount = register.login(user);
            if (userCount == 1) {
                HttpSession session = req.getSession();
                session.setAttribute("name", t_name);
                loginyes = true;
            }
            /*
             * 老登录逻辑 List<User> ulist = xmlr.getList(); HttpSession session =
             * req.getSession(); for (User u : ulist) { if
             * (u.getName().equals(t_name)) if (u.getPasswd() ==
             * t_passwd.hashCode()) { session.setAttribute("name", t_name);
             * loginyes = true; } }
             */
            if (loginyes) {
                RequestDispatcher dispatcher = req
                        .getRequestDispatcher("index.jsp");
                dispatcher.forward(req, resp);
            } else {
                resp.sendRedirect("login.jsp");
            }
        }
    }
}

代码解析

 

(1)在该Servlet类中,我们把原先的登录逻辑注释了,以备后期使用的时候再取消注释。

(2)新登录逻辑只有几行代码,大概意思是使用login()方法,将从表单里读取到的用户名和密码存入Java实体类User中,再把User作为参数传入login()方法中。而login()方法的登录逻辑在于判断数据库里是否存在该用户,判断的依据是使用用户名和密码作为查询条件。在初期版本中这种判断逻辑是完全可以使用的,后期可以根据业务的拓展,再继续追加更多逻辑。

(3)String t_name = (String) req.getParameter("name")从前端JSP页面读取用户名。

(4)session.setAttribute("name", t_name);的作用是如果用户存在且信息有效,把用户名存入Session域中,以方便在其他的功能模块中通过Session获取用户名信息。

RegisterService类是与用户相关的服务层,之前注册功能的add()方法便写在这里。那么同样的,与用户登录相关的login()方法也自然可以写在这里。后期如果还有用户删除、用户修改等功能,都可以把方法写在这个类中,RegisterService用户信息类如代码清单3-11所示。

代码清单3-11 RegisterService.java

package com.manage.db;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.UUID;
import com.manage.bean.SiteUser;
public class RegisterService {
    public void add(SiteUser user) {
        Connection conn = OracleDB.createConn();
        UUID Id = java.util.UUID.randomUUID();
        String sql = "insert into SiteUser values (?, ?, ?, ?, ?, ?)";
        PreparedStatement ps = OracleDB.prepare(conn, sql);
        try {
            ps.setString(1, Id.toString());
            ps.setString(2, user.getUserName());
            ps.setString(3, user.getUserPwd());
            ps.setString(4, user.getUserEmail());
            ps.setString(5, user.getUserMobile());
            ps.setString(6, user.getUserAddress());
            ps.executeUpdate();
        } catch (SQLException e) {
            e.printStackTrace();
        }
        OracleDB.close(ps);
        OracleDB.close(conn);
    }
    public int login(SiteUser user) {
        Connection conn = OracleDB.createConn();
        String sql = "select count(*) from siteuser t where t.username = ? and t.userpwd = ?";
        PreparedStatement ps = OracleDB.prepare(conn, sql);
        try {
            ps.setString(1, user.getUserName());
            ps.setString(2, user.getUserPwd());
            ResultSet result = ps.executeQuery();
            if (result != null) {
                while (result.next()) {
                    int userCount = result.getInt(1);
                    return userCount;
                }
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
        OracleDB.close(ps);
        OracleDB.close(conn);
        return 0;
    }
}

代码解析

 

(1)在RegisterService类中,add()方法在之前已经存在,用于向数据库里新增用户,我们不对它做任何修改。login()方法涉及登录,完全是有必要写在RegisterService类中的,后期如果有需要对用户进行验证的方法(如userValidate),涉及用户修改的方法(如userUpdate),涉及用户的删除的方法(如userDelete)等,都可以写在这个类中。这样做的好处是方便程序员开发,也使整个项目的代码保持纯净有条不紊。

(2)Connection conn = OracleDB.createConn():新建Connection对象的实例。

(3)String sql = "select count(*) from siteuser t where t.username = ? and t.userpwd = ?":新建进行数据库交互的SQL语句,需要传入的参数使用?代替。

(4)PreparedStatement ps = OracleDB.prepare(conn, sql):使用预处理语句。

(5)ps.setString(1, user.getUserName()),ps.setString(2, user.get UserPwd()):分别将用户名和密码传入与?对应的参数位置中。注意,此处的位置从1开始而不是从0开始。

(6)ResultSet result = ps.executeQuery():使用PreparedStatement对象的实例ps来调用executeQuery()方法进行与数据库交互的动作。ResultSetif语句块的作用是从数据库里查询到数值,存入result变量中再使用if语句进行判断。如果变量不为null,利用while进行循环取值,并且利用getInt()方法拿到数据返回给userCount

修改完RegisterService类,我们回到首页再从注册到登录整体上过一遍,发现很多业务逻辑都已经修改成了用户需要的方式。这样的话,综合调试就顺利结束了,而这轮测试也完成了闭环。

manage项目的第一期采用Servlet实现,这样的话后期如果陆续有新功能加进来,就需要不断地配置Servlet,这是非常麻烦的一件事情。它不但会导致web.xml里的配置文件越来越多,而使其他的配置信息如过滤器、监听器等信息难以找到,还会让整个文件越来越臃肿,非常影响项目的简洁。

为此我们需要给web.xml文件瘦身,而瘦身最好的办法就是减少Servlet的配置信息。但是如果减少了Servlet的配置信息会导致整个项目无法启动,这可如何是好?幸运的是维护Servlet技术的专家们也注意到了这种情况,他们在最新的Servlet 3.0中加入了注解的方式。这样的话,我们就彻底告别了过去的那种开发模式。在开发新功能的时候,可以直接依赖注解配置Servlet。如果时间充裕的话,还可以把之前在web.xml里配置好的Servlet信息都删掉,统一改成注解的方式。Servlet 3.0除了加入了注解这一重要功能之外,还支持了一些新特性。例如,之前的Servlet版本对异步处理支持得并不好,一个请求到来的时候线程会一直被占用、阻塞,直到该业务处理完毕才能结束。而新版本的Servlet在异步方面进行了提升,如果某个业务非常占用资源的话,Servlet会自动把该业务交代给另一个线程执行。这个提升将在很大程度上减少服务器的压力,提高了处理并发的效率和速度。

其实,每一项技术在升级的时候都会罗列出该版本和之前版本的区别。一般来说,为了项目的稳定,我们不需要刻意地去升级项目。技术的新版本也是由程序员开发的,既然是程序员开发的,就难免会有bug存在,而成熟的版本是经过时间检验的。所以在技术选型的时候,也需要从升级对象中选择出适合自己需要的内容。例如,我们选择Servlet 3.0就是看中了它较之前增加了注解的方式。这样的话,就对我们的项目大有裨益。因此在这种情况下,我们可以考虑升级Servlet的版本。

首先我们来查看一下当前项目中使用的Servlet的版本号,还有Tomcat6所支持的Servlet版本号。打开LoginServlet类,找到public class LoginServlet extends HttpServlet语句,鼠标选中HttpServlet,按下Ctrl键再点击鼠标左键,接着进入HttpServlet.class中。当然,此处是看不到源码的。点击Package Explorer栏的黄色双向箭头,可自动匹配到目标文件。

图3-3 进入HttpServlet.class中

从图 3-3 可以得出结论,HttpServlet.class是在javax. servlet.jar包中的。该包位于Java EE 6 Libraries库中。从字面意思理解,可以看出Servlet是做服务器交互的,那么它肯定是属于Java EE范畴之内的技术,所以它的库名也有这层意思。然而Java EE 6 Libraries库的JAR包基本上也都是跟服务器交互的。如果是Java EE项目出现了找不到包的情况,不妨首先导入这个类库试试。

选中javax.servlet.jar点击鼠标右键,在弹出的菜单中选择Properties,在弹出的对话框中,再次选择External File按钮,在弹出的文件列表中选择javax.servlet文件,使用压缩软件打开。在弹出的软件界面中,打开META-INF文件,再打开MANIFEST.MF文件,可以看到javax.servlet;version="3.0"字样,说明Java EE 6 Libraries提供的Servlet是3.0版本的,也就是说明它支持注解。

接着打开Tomcat6的lib目录,找到servlet-api文件,用同样的方法打开MANIFEST.MF文件,可以看到Specification-Version: 2.5,这就说明Tomcat6是不支持Servlet的注解的。这样的话,如果使用Tomcat6来部署采用注解方式的Servlet项目,就会出现404错误。为了验证,我们需要先去更改程序代码。

首先打开web.xml文件,将LoginServlet相关的配置全部注释掉,相关代码如下:

<!--<servlet>
        <servlet-name>Login_Servlet</servlet-name>
        <servlet-class>com.manage.servlet.LoginServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>Login_Servlet</servlet-name>
        <url-pattern> /LoginServlet </url-pattern>
    </servlet-mapping> -->

接着来到LoginServlet类文件,增加@WebServlet("/LoginServlet"),这句话的意思就是用来增加注解的。它表明在该项目中,LoginServlet不依赖程序中的配置,而是直接使用对应的@WebServlet来扫描具体的Servlet配置,具体代码如下:

@WebServlet("/LoginServlet")
public class LoginServlet extends HttpServlet

接着使用Tomcat6部署项目,启动成功后打开界面果然会出现404错误。因为Tomat6不支持Servlet的注解,自然也就找不到对应的文件了,404错误页面如图3-4所示。

最后我们把项目部署在tomcat7上试试效果。启动服务器后,在地址栏输入http://localhost: 8080/manageServlet,点击登录功能输入用户名和密码,可以看到程序已经成功登录进来了,并未出现404错误,LoginServlet通过注解方式登录成功的界面如图3-5所示。

图3-4 404错误页面

图3-5 登录成功界面

本节使用了Servlet 3.0的注解方式重新配置了LoginServlet,并且发现Tomcat6并不支持注解,在尝试把Tomcat6改成Tomcat7后获得了成功。这样的话,以后在开发新功能的时候就不用为web.xml里配置太多的Servlet信息而发愁了,这在技术选型上,不得不说是一个成功!

完成了注册、登录等基本模块后,我们使用admin登录,会发现index.jsp中包含的list.jsp页面用于显示具体的报表模块,但是该模块还没有进行具体的开发。从整体上来看,当前的管理系统只是做到了一个相对完整的阶段,真正为用户解决实际问题的大量报表还没有开发呢。但是话说回来,list.jsp中所显示的报表都是大同小异的,基本上可以靠复制粘贴代码来完成。在这种情况下,我们可以挑选两个典型的报表来做成DEMO即可。这两个报表分别是销售数据导入、销售数据查询功能。只要完成了这两个典型的报表,剩下的报表就可以参考着开发,速度会非常快。

在开发中我们没有采用Struts、Spring、Hibernate等框架技术,而是直接采用了Servlet、JavaBean、JSP的组合,这种不依赖框架的开发模式仍然存在,也有不少项目处在运维当中。Servlet的主要作用在于前后端动态交互,JavaBean的作用在于提供数据模型,JSP的作用在于前端显示。这种典型的MVC模式已经成为业界的标准,而随着模式的固定,程序员在开发的时候也不得不去遵守。而Servlet、JavaBean、JSP的组合仍然能开发绝大多数的Java Web项目,只不过由于年代久远的原因,它在代码量、开发模式方面可能会没有其他的框架好,但是Servlet并没有特别明显的缺点。现在已知它的劣势是:

(1)Servlet 3.0以下的版本不支持注解方式,需要在web.xml中配置信息,不利于代码的开发和阅读,但随着版本的升级该问题已经解决。

(2)Servlet的Request、Response对象有容器依赖性,只有完整地搭建好Servlet的配置才可以进行测试。

(3)线程安全方面的问题,Servlet不是线程安全的。

接下来我们重点解释一下Servlet为什么不是线程安全的。因为,Servlet接收到HTTP请求时会进行初始化,也就是之前所讲述的Servlet的运行过程,例如,它调用了service()方法。此时我们不用去思考接下来的情况,只需要围绕service()方法展开讨论即可。

因为Servlet是单例模式的,这样有多个HTTP请求同时操作一个Servlet的话,例如,我们当前项目的登录模块,对应的线程就会多次调用该Servlet的service()方法。那么问题就来了,如果我们在service()方法中写了一些逻辑,单次调用的话不会出现问题,如果多次并发调用的话,是不是就会出现意想不到的问题呢?结果是显而易见的。

因为Struts1是对Servlet的接口的直接实现,所以Struts1也是单例模式的,仍然会出现并发问题,这也是Struts1被诟病的原因之一。而Struts 2采用拦截器机制,对每一个HTTP请求都会实例化一个Action对象,所以它就不会存在线程安全的问题了。但是在使用Spring管理Struts 2的时候,因为Spring对Bean的处理方式默认是单例模式,所以我们需要注意针对Bean配置的scope属性应该如何配置才算合理?如果出现了并发问题,可以把scope修改成prototype试试。

综合起来看,Servlet的处理步骤大概是这样的。

(1)客户端携带参数发送请求到服务器端。

(2)服务器把请求信息发送给Servlet中转,Servlet获取参数进行后端逻辑处理,再返回数据给客户端,同时借助Web技术输出返回信息。

(3)这是一种典型的请求、响应模式。

一个项目的基础数据录入有两种方式:第一种是通过数据库脚本直接导入数据库;第二种是通过导入和手动录入来添加数据,手动录入比较常见,跟我们注册功能是一样的。下面我们通过开发导入功能来实现对信息进行批量的录入。在登录成功的页面,点击销售数据导入功能进入报表,销售数据导入功能如图3-6所示。

图3-6 销售数据导入功能

从该界面上分析,我们至少要完成4个功能,分别是返回首页、下载Excel模板、提交和重置。可能在提交后还有其他功能需要开发,当然这是后话了。该JSP页面非常简单,返回首页是一个超链接,不用过多关注,比较难的功能是下载Excel模板和提交。接下来,我们针对这几个功能进行一个全面地的开发。

打开WebContent目录,在该目录下创建ImportSaledata.jsp,该销售数据导入功能文件内容如代码清单3-12所示。

代码清单3-12 ImportSaledata.jsp

<%@ page language="java" import="java.util.*" import="java.net.*"
    pageEncoding="UTF-8"%>
<%@page import="com.manage.bean.*"%>
<%
    String path = request.getContextPath();
    String basePath = request.getScheme() + "://"
    + request.getServerName() + ":" + request.getServerPort()
    + path + "/";
%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title>销售数据导入</title>
<meta http-equiv="pragma" content="no-cache">
<meta http-equiv="cache-control" content="no-cache">
<meta http-equiv="expires" content="0">
<meta http-equiv="keywords" content="keyword1,keyword2,keyword3">
<meta http-equiv="description" content="This is my page">
</head>
<script type="text/javascript">
    function flush() {
        window.location.reload();
    }
</script>
<body bgcolor="#CCCCCC">
    <div>
        <a href="<%=basePath%>/index.jsp">返回首页</a>
    </div>
    <table border="0" cellpadding="0" cellspacing="0" width="80%"
        bordercolorlight="#000080" bordercolordark="#FFFFFF" height="19">
        <tr valign="middle">
            <td width="80%" background="img/topbg.gif" height="32"></td>
        </tr>
    </table>
    <table border="0" cellpadding="0" cellspacing="0" width="80%"
        bordercolorlight="#E6E4C4" bordercolordark="#E8E4C8">
        <tr>
            <h1>销售数据导入</h1>
        </tr>
    </table>
    <div align="left">
        <a href="download_saledatamodel.jsp">下载Excel模板</a>
    </div>
    <div align="left">
        <form method="POST" action="ImportSaledata_Servlet"
            enctype="multipart/form-data">
            选择文件:<input type="file" name="filename" /> <br>
            <p>
                <input type="Submit" value="提交"> <input type="reset"
                    value="重置" id="b2" onclick="flush()">
            </p>
        </form>
        <%
            List<?> list = (List<?>) request.getAttribute("ulist");
            if (list == null) {
            } else {
        %>
        <br>
        <div align='left'>
            共<%=list.size()%>条记录
        </div>
        <table id="data" border="1" cellpadding="1" width="70%">
            <tr align='center'>
                <td align='center'>城市</td>
                <td align='center'>产品</td>
                <td align='center'>数量</td>
                <td align='center'>推销员</td>
                <td align='center'>备注</td>
            </tr>
            <tr>
                <%
                    for (Object o : list) {
                            Salemodel t = (Salemodel) o;
                %>
                <td align='center'><%=t.getCity()%></td>
                <td align='center'><%=t.getProduct()%></td>
                <td align='center'><%=t.getNum()%></td>
                <td align='center'><%=t.getSalesman()%></td>
                <td align='center'><%=t.getRemark()%></td>
            </tr>
            <%
                }
            %>
        </table>
        <%
            session.setAttribute("ulist", list);
        %>
        <br>
        <div align='center'>
            共<%=list.size()%>条记录
        </div>
        <form method="post" action="ImportSaledataDB_Servlet">
            <select name="nettype">
                <option selected="selected">请选择类型:</option>
                <option value="other">其他</option>
                <option value="net">销售数据</option>
            </select> <br> <br> <input type="submit" value="导入数据库" />
        </form>
        <%
            }
        %>
    </div>
</body>
</html>

代码解析

 

(1)<a href="<%=basePath%>/index.jsp">: 返回首页</a>:该超链接用于返回首页。

(2)<a href="download_saledatamodel.jsp">: 下载Excel模板</a>:下载Excel模板功能看似比较复杂,实际上有两种实现方式。第一种是在超链接里直接对应后端的Java代码,让它处理完毕后返回给前端;另一种方式就是当前的这种,打开一个新的JSP页面,在该JSP页面里写入Java代码。

(3)for (Object o : list) {}语句块:这段代码使用<%%>包住,这是一种在JSP页面写Java代码的方式,其作用是通过循环来获取后端的数据。例如,循环体中有这样一段语句<td align='center'><%=t.getCity()%></td>,其作用就是获取城市名称,这种写法的好处是可以获取列表,而不是单独数据的展示。

(4)session.setAttribute("ulist", list):将数据保存在Session域中以供后面使用。

(5)共<%=list.size()%>条记录:在JSP页面中嵌入Java代码获取总记录数。

(6)<form method="post" action="ImportSaledataDB_Servlet">:选择数据类型进行入库操作。

接下来开发导入模板文件功能,打开WebContent目录,在该目录下创建download_saledatamodel.jsp,用于提供销售数据的Excel模板下载功能,文件内容如代码清单3-13所示。

代码清单3-13 download_saledatamodel.jsp

<%@ page language="java" import="java.net.*"
    contentType="text/html; charset=GB2312" pageEncoding="GB2312"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" 
    "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=GB2312">
<title>download salemodel</title>
</head>
<body>
    <div align="center">
        <%
            response.setContentType("application/x-download");// 设置为下载
            String filedownload = "/xls/salemodel.xls";// 下载的文件的相对路径 
            String filedisplay = "salemodel.xls";// 下载文件时显示的文件保存名称
            String filenamedisplay = URLEncoder.encode(filedisplay, "GB2312");
            response.addHeader("Content-Disposition", "attachment;filename="
                    + filedisplay);
            try {
                RequestDispatcher dis = application
                        .getRequestDispatcher(filedownload);
                if (dis != null) {
                    dis.forward(request, response);
                }
                response.flushBuffer();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
            }
        %>
    </div>
</body>
</html>

代码解析

 

(1)response.setContentType("application/x-download"):设置响应为下载模式。

(2)String filedownload = "/xls/salemodel.xls":指定下载的文件的相对路径。

(3)String filedisplay = "salemodel.xls":指定下载文件时显示的文件保存名称。

(4)String filenamedisplay = URLEncoder.encode(filedisplay, "GB2312"):设置编码格式。

接下来开发文件导入的实现类。打开com.manage.servlet包,在该包下创建ImportSaledataDB_ Servlet类,用于提供销售数据的导入,具体内容如代码清单3-14所示。

代码清单3-14 ImportSaledataDB_Servlet.java

package com.manage.servlet;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import com.manage.bean.ResultData;
import com.manage.bean.Salemodel;
import com.manage.db.ImportSaledataService;
public class ImportSaledataDB_Servlet extends HttpServlet {
    private static final long serialVersionUID = 1L;
    @SuppressWarnings("unchecked")
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        HttpSession session = req.getSession(false);
        String name = (String) session.getAttribute("name");
        if (name == null || name.equals("") || name.equals("null"))
            resp.sendRedirect("login.jsp");
        else {
            String nettype = (String) req.getParameter("nettype");
            List<Salemodel> list = (List<Salemodel>) session
                    .getAttribute("ulist");
            if (list == null || nettype == null || nettype.equals(""))
                resp.sendRedirect("ImportSaledata.jsp");
            else {
                ImportSaledataService Saledata = new ImportSaledataService();
                for (Salemodel cid : list) {
                }
                ResultData rd = Saledata.addlist((List<Salemodel>) list);
                int count = rd.getNum1();
                int c = list.size();
                session.removeAttribute("ulist");
                resp.setContentType("text/html;charset=GBK");
                PrintWriter writer = resp.getWriter();
                writer.println("<html>");
                writer.println("<head><title>Excel 导入结果</title></head>");
                writer.println("<body>" + "导入了" + count + "条记录<br>");
                writer.println("<a href=\"index.jsp\">首页</a><br>");
                writer.println("<a href=\"ImportSaledata.jsp\">销售数据导入</a><br>");
                writer.println("失败了" + (c - count) + "条<br>");
                writer.println("<p>是否全部完成:" + count == c + "</p></body>");
                writer.println("</html>");
                writer.close();
            }
        }
    }
}

代码解析

 

(1)ImportSaledataService Saledata = new ImportSaledataService():用于新建插入销售数据信息的工具类实例。

(2)ResultData rd = Saledata.addlist((List<Salemodel>) list):用于执行插入销售数据信息的动作,并且返回合适的信息给前端。

(3)writer.println("<body>" + "导入了" + count + "条记录<br>"):动态构造前端显示的数据,count变量是从后端返回过来的数据。

接下来开发执行入库操作的类。打开com.manage.db包,在该包底下创建ImportSaledata- Service类,用于把导入进来的销售数据插入数据库里去。该类如代码清单3-15所示。

代码清单3-15 ImportSaledataService.java

package com.manage.db;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import com.manage.bean.ResultData;
import com.manage.bean.Salemodel;
public class ImportSaledataService {
    public ResultData addlist(List<Salemodel> list) {
        Connection conn = OracleDB.createConn();
        UUID Id = java.util.UUID.randomUUID();
        ResultData rd = new ResultData();
        int count = 0;
        String sql = "insert into saledata"
                + " (id,city,product,num,salesman,remark)"
                + "  values (?, ?, ?, ?, ?, ?)";
        PreparedStatement ps = null;
        for (Salemodel cid : list) {
            ps = OracleDB.prepare(conn, sql);
            try {
                ps.setString(1, Id.toString());
                ps.setString(2, cid.getCity());
                ps.setString(3, cid.getProduct());
                ps.setString(4, cid.getNum());
                ps.setString(5, cid.getSalesman());
                ps.setString(6, cid.getRemark());
                ps.executeUpdate();
                rd.setNum1(count++);
            } catch (SQLException e) {
                rd.setNum2(count++);
                e.printStackTrace();
            }
        }
        OracleDB.close(ps);
        OracleDB.close(conn);
        rd.setNum1(count);
        return rd;
    }
    public List<Object> list() {
        Connection conn = OracleDB.createConn();
        String sql = "select city,product,num,salesman,remark from saledata";
        List<Object> Objects = new ArrayList<Object>();
        try {
            Statement stmt = conn.createStatement();
            ResultSet rs = stmt.executeQuery(sql);
            if (rs == null)
                return null;
            Salemodel c = null;
            while (rs.next()) {
                c = new Salemodel();
                c.setCity(rs.getString("city"));
                c.setProduct(rs.getString("product"));
                c.setNum(rs.getString("num"));
                c.setSalesman(rs.getString("salesman"));
                c.setRemark(rs.getString("remark"));
                Objects.add((Object) c);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
        OracleDB.close(conn);
        return Objects;
    }
}

代码解析

 

(1)Connection conn = OracleDB.createConn():获取一个操作数据库的连接实例。

(2)ps.executeUpdate():执行插入数据动作的命令。

(3)rd.setNum1(count++):如果与数据交互成功,后面的不变。

接下来开发数据模型。打开com.manage.bean包,新建Salemodel类,具体如代码清单3-16所示。

代码清单3-16**Salemodel.java

package com.manage.bean;
import java.io.Serializable;
public class Salemodel implements Serializable {
    private static final long serialVersionUID = 1526705009706221747L;
    private String city;
    private String product;
    private String num;
    private String salesman;
    private String remark;
    public String getCity() {
        return city;
    }
    public void setCity(String city) {
        this.city = city;
    }
    public String getProduct() {
        return product;
    }
    public void setProduct(String product) {
        this.product = product;
    }
    public String getNum() {
        return num;
    }
    public void setNum(String num) {
        this.num = num;
    }
    public String getSalesman() {
        return salesman;
    }
    public void setSalesman(String salesman) {
        this.salesman = salesman;
    }
    public String getRemark() {
        return remark;
    }
    public void setRemark(String remark) {
        this.remark = remark;
    }
    public Salemodel() {
    }
}

代码解析

 

数据模型类非常简单,其中开发了城市、商品、数量、销售员、备注这5个元素与数据库销售信息表的字段对应。在销售数据导入报表功能的整个流程中,还使用了其他的数据模型类,读者可以参照源码来阅读,在这里就不再赘述。

最后,我们来总结一下销售数据导入报表功能的流程:首先进入ImportSaledata.jsp页面,下载导入模板并且修改,接下来点击提交功能,在提交的过程中Java会调用之前写好的类做相应的业务逻辑处理,并且把需要导入的数据显示在前端页面,确认无误后点击导入数据库功能就可以完成入库操作了。

上一节我们开发完成了销售数据导入功能,有了导入功能就可以方便地往数据库里批量增加数据,那么接下来,我们再来开发销售数据查询报表功能,用来显示Excel导入的数据,销售数据查询功能如图3-7所示。

图3-7 销售数据查询功能

从图3-7所示界面上分析,我们需要重点关注的两个功能一个是查询,另一个是导出Excel。下面我们针对这两个功能进行开发。打开WebContent目录新建销售数据查询功能文件saledatafind.jsp,内容如代码清单3-17所示。

代码清单3-17 saledatafind.jsp

<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<%@page import="com.manage.bean.*"%>
<%
    String path = request.getContextPath();
    String basePath = 
        request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+path+"/";
%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title>销售数据查询</title>
<meta http-equiv="pragma" content="no-cache">
<meta http-equiv="cache-control" content="no-cache">
<meta http-equiv="expires" content="0">
<meta http-equiv="keywords" content="keyword1,keyword2,keyword3">
<meta http-equiv="description" content="This is my page">
<script language="javascript" type="text/javascript"
    src="My97DatePicker/WdatePicker.js">
</script>
<style type="text/css">
td {
    background-color: #FFFFFF;
}
.txt {
    padding-top: 1px;
    padding-right: 1px;
    padding-left: 1px;
    mso-ignore: padding;
    color: black;
    font-size: 11.0pt;
    font-weight: 400;
    font-style .: normal;
    text-decoration: none;
    font-family: 宋体;
    mso-generic-font-family: auto;
    mso-font-charset: 134;
    mso-number-format: "\@";
    text-align: general;
    vertical-align: middle;
    mso-background-source: auto;
    mso-pattern: auto;
    white-space: nowrap;
}
</style>
<SCRIPT LANGUAGE="JavaScript">
    function ExportExcel() {
        try {
            var oXL = new ActiveXObject("Excel.Application");
        }// 创建excel应用程序对象
        catch (e) {
            alert("无法启动Excel!\n\n如果您确信您的电脑中已经安装了Excel,"
                    + "那么请调整IE的安全级别。\n\n具体操作:\n\n"
                    + "工具 → Internet选项 → 安全 → 自定义级别 → 对没有标记为安全的ActiveX进 
                         行初始化和脚本运行 → 启用");
            return false;
            return "";
        }
        oXL.visible = true;
        var oWB = oXL.Workbooks.Add(); // 创建工作簿
        var oSheet = oWB.ActiveSheet; // 获取当前活动的工作簿
        var table = document.all.data; // 获取当前页面中的表格
        var hang = table.rows.length; // 获取表格有多少行
        var lie = table.rows(0).cells.length; // 获取首行有多少列-多少标题
        for (i = 0; i < hang; i++) // 添加标题到表格中
        {
            for (j = 0; j < lie; j++) {
                // 设置标题的内容
                oSheet.Cells(i + 1, j + 1).Value = table.rows(i).cells(j).innerText;
            }
        }
        oXL.Visible = true; //设置Excel的属性
        oXL.UserControl = true;
    }
</SCRIPT>
</head>
<body bgcolor="#CCCCCC">
    <div>
        <a href="<%=basePath%>/index.jsp">返回首页</a>
    </div>
    <table border="0" cellpadding="0" cellspacing="0" width="80%"
        bordercolorlight="#000080" bordercolordark="#FFFFFF" height="19">
    </table>
    <table border="0" cellpadding="0" cellspacing="0" width="80%"
        bordercolorlight="#E6E4C4" bordercolordark="#E8E4C8">
        <tr valign="top">
            <td width="100%" bgcolor="#e6e4c4" class="main1"></td>
        </tr>
        <tr>
            <h1>销售数据查询</h1>
        </tr>
    </table>
    <div align="left">
        <form method="POST" action="SaleDataFind_Servlet">
            <p>
                <br> 请输入日期范围
                <%
                    String time1 = (String) request.getAttribute("time1");
                    String time2 = (String) request.getAttribute("time2");
                    if (time1 == null)
                        time1 = "";
                    if (time2 == null)
                        time2 = "";
                %>
                &nbsp; &nbsp; &nbsp;
            </p>
            <input class="Wdate" type="text" name="time1"
                onfocus="WdatePicker({skin:'whyGreen',dateFmt:'yyyyMMdd'})"
                value="<%=time1%>"> --<input class="Wdate" type="text"
                name="time2"
                onfocus="WdatePicker({skin:'whyGreen',dateFmt:'yyyyMMdd'})"
                value="<%=time2%>"> <br> <br> <input type="submit"
                value="查询" name="Submit" style="font-size: 14px"><input
                type="button" name="btnExcel" onclick="javascript:ExportExcel();"
                value="导出到excel" class="notPrint"><br>
            <%
                List list = (List) request.getAttribute("list");
                if (list == null) {
                }
            %>
            <%
                if (list != null) {
            %>
            <table id="data" border="1" cellpadding="1" width="100%">
                <tr align='center'>
                    <td align='center'>地市</td>
                    <td align='center'>产品</td>
                    <td align='center'>数量</td>
                    <td align='center'>推销员</td>
                    <td align='center'>备注</td>
                </tr>
                <tr>
                    <%
                        for (Object o : list) {
                                Salemodel t = (Salemodel) o;
                    %>
                    <td align='center'><%=t.getCity()%></td>
                    <td align='center'><%=t.getProduct()%></td>
                    <td align='center'><%=t.getNum()%></td>
                    <td align='center'><%=t.getSalesman()%></td>
                    <td align='center'><%=t.getRemark()%></td>
                </tr>
                <%
                    }
                %>
            </table>
            <%
                }
            %>
        </form>
    </div>
</body>
</html>

代码解析

 

(1)ExportExcel():一个简单高效的Excel导出方法,但只适合导出少量数据。

(2)<form method="POST" action="SaleDataFind_Servlet">:该Servlet用于从数据库里读取需要在前端显示的销售数据。

打开com.manage.servlet包,创建SaleDataFind_Servlet类,用于从数据库里查询销售数据,该类如代码清单3-18所示。

代码清单3-18 SaleDataFind_Servlet.java

package com.manage.servlet;
import java.io.IOException;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import com.manage.db.ImportSaledataService;
public class SaleDataFind_Servlet extends HttpServlet {
    private static final long serialVersionUID = -854550978502353857L;
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        HttpSession session = req.getSession(false);
        String name = (String) session.getAttribute("name");
        if (name == null || name.equals("") || name.equals("null"))
            resp.sendRedirect("login.jsp");
        else {
            String time1 = (String) req.getParameter("time1");
            String time2 = (String) req.getParameter("time2");
            String userName = (String) session.getAttribute("name");
            req.setAttribute("list", new ImportSaledataService().list());
            RequestDispatcher dispatcher = req
                    .getRequestDispatcher("saledatafind.jsp");
            dispatcher.forward(req, resp);
        }
    }
}

代码解析

 

(1)req.setAttribute("list", new ImportSaledataService().list()):该语句先调用ImportSaledataService类的list()方法从数据库的saledata表查询到所有导入的销售数据,并且保存到HttpServletRequest中。

(2)RequestDispatcher dispatcher = req.getRequestDispatcher
("saledatafind.jsp")
:设置跳转页面。

(3)saledatafind.jsp页面中使用for循环来输出销售数据。

销售数据查询报表相对简单,主要依赖于销售数据导入功能。当然,如果没有导入功能也可以在数据库里相应的saledata表中构造测试数据。

所谓软件开发中的月度版本,就是每月发布一个新版本的意思,只不过这个新版本的迭代周期是 1 个月。这样的话,也会有每周版本、半月版本之说。但一般来说,针对中小型项目通常采用的迭代周期是1个月。举个例子,如果某个MIS(管理信息系统)项目的工期是一年,可以将需求分解成12个月的开发周期,这样大致就可以将迭代周期确定成1个月,分为12个迭代。团队每月迭代一次,发布一个新版本,将最新的情况及时反馈给客户。这种区别于瀑布的敏捷开发模式,它的好处是毋庸置疑的。举个最典型的例子,如果在开发流程不成熟的情况下采用了瀑布模式,团队从头到尾忙碌了1年,到项目交付的时候,客户直接来了句“这不是我想要的东西”。如此结果,前期投入的成本都会付诸东流。所以,大多数项目还是建议使用敏捷模式来开发。

项目经理在领导团队开发的同时,还需要与干系人建立长期的持续的沟通,进行需求确认、需求变更等事宜,甚至还有削减需求的工作。当然,如果有与之配合的CCB就更好了。敏捷模式下,如果月度版本不符合客户的需求,也会产生快速失败的效应。这样的话,团队就可以根据当前失败的版本,再结合客户最新的反馈来进行新一轮的迭代。例如,客户认为当前的版本存在bug,提出了修改意见,例如某些功能可以删掉、某些功能需要新增等,这些都是软件开发过程中司空见惯的问题。总而言之,敏捷模式的核心就是快速响应,争取以最低的成本、最好的质量完成客户的需求,争取在多轮迭代后项目趋于稳定,而剩下的工作就是在确定的框架之下进行常规的开发。

站在人际关系的层面来讲,如果与客户进行持续地沟通,也能够打破两者之间的壁垒。这样的话,一些项目中扯皮的问题可能会迎刃而解。但如果闭门造车,项目团队与客户的关系可能会很一般,不利于解决项目中出现的问题。那话说回来,什么情况下采用瀑布模式呢?例如,我们将MIS项目采用敏捷模式开发了很多遍,并且向外声称不再接受定制化需求,这样的情况下采用瀑布模式反而会节省时间和人力成本。可以形象地把瀑布模式理解成自动化车间的流水线一样,而生产出来的东西就是我们的软件项目。

本章我们循序渐进地讲解了企业管理系统的定义范围、WBS分解、框架搭建,并且采用了Servlet、JavaBean、JSP组合的方式进行开发,同时在不依赖框架的情况下,也进行了“销售数据导入报表”和“销售数据查询报表”的开发。这两个功能在代码上都采用了比较古老的写法,这样做的好处是可以让读者了解到最初的时候程序员是如何写代码的,以此巩固读者的Java基础。

最后根据当前的项目衍生出了月度版本的概念,阐述了瀑布模式和敏捷模式的区别,以及在工作当中应该如何选择的问题。通过本章学习,读者可以参考源码进行新的项目开发,或者直接修改源码进行练习。而最终的学习目标是熟练掌握Servlet、JavaBean、JSP组合的开发模式,学习这种基础的开发理念,为日后的进阶打下坚实的基础。


相关图书

Effective Java中文版(原书第3版)
Effective Java中文版(原书第3版)
Java核心技术速学版(第3版)
Java核心技术速学版(第3版)
Java编程动手学
Java编程动手学
Java研发自测入门与进阶
Java研发自测入门与进阶
Java开发坑点解析:从根因分析到最佳实践
Java开发坑点解析:从根因分析到最佳实践
Java EE企业级应用开发实战(Spring Boot+Vue+Element)
Java EE企业级应用开发实战(Spring Boot+Vue+Element)

相关文章

相关课程