J2EE架构综述
J2EE(Java 2 Platform Enterprise Edition)即Java2平台企业版,是一套全新的技术架构,便于企业级用户规范化高效率地开发企业级web服务。(Java一共有三个发行版本,分别是J2SE,J2EE,J2ME。分别代表Java的标准版本,企业版本和精简版本,我们文章中指的是第二个版本)
通常来讲企业级用户面对Web时的需求是多样化的,我们重点关注企业级用户的Web系统的开发,从架构上描述一个Web系统所具有的层次。
无论基于什么样的技术体系,Web都是基于B/S的模式,即浏览器/服务器。不同的技术体系的区别仅在于Server(服务器)端,我们将从宏观和微观两个尺度来梳理服务器端的架构。
宏观上,我们把服务器端分为应用层和底层两个层次。应用层指的是承担业务逻辑的服务器,而底层指数据库服务器。它们的关系如下图所示:
在数据库服务器中我们安装相应的数据库软件,像sqlserver,mysql,oracle等。根据我们具体的Web业务需求构建数据库,在数据库服务器中建立其相应的数据表。
接下来,着重说一下Web服务器中的架构问题,也就是从微观上来看J2EE的架构。Web服务器中的代码是由Java编写的。从程序设计的角度来讲也可以分为应用层程序和底层程序,通常距离用户越近距离底层就越远。在Web服务器中底层的代码一般都是与数据库的连接与操作,系统的安全性相关的代码,这些程序与用户距离较远。而应用层程序则与用户距离很近基本上与用户只隔一层前端界面,应用层的程序主要用来处理一些业务逻辑,例如注册,登录,给用户展现界面等等。抽象地看,Web服务器内部的程序分层如下图:
上图表示的是在Web服务器中程序的一般架构,采取的是软件设计中常用的MVC设计模式。接下来让我们从底层向顶层梳理一遍服务器程序的结构。
数据持久层
最右边是我们的数据库系统或者说数据库服务器。它是整个系统的基石,全部的系统都构建于数据库之上。Web服务器通过数据库连接与数据库产生联系。在Web服务器中为了追求程序的高内聚性,我们一般把取得数据库连接当做一个独立的模块来完成。在这个模块中J2EE为我们提供了现成的数据库驱动让我们可以非常简便地与数据库建立连接,当然这是原生的做法。但是考虑到获取数据库连接是一个相对比较耗时的操作,如果当需求来临时才向数据库取得连接无疑会降低系统的并发性,影响用户的体验。因此,我们可以在系统启动时就与数据库建立好若干连接,将这些暂时用不到的连接放置于一个“池”中。当需求来临时,我们直接从“池”中取得连接与数据库进行交互,这提高了系统的响应速度,在这里我们可以使用第三方的框架c3p0来建立一个数据库连接池。
再往左就是数据库的操作模块,通常无论我们的需求再怎么复杂反映到对数据库的操作上就是增,删,改,查四大基本操作的变换组合。因此同样是为了追求系统的高内聚性,我们把增,删,改,查四个操作归并到一起,做成一个通用性很高的模块,达到无论什么样的更改或是无论查询出什么样的结构化数据都可以使用这个模块来进行处理。这个模块在设计时就较多的利用到了Java中著名的反射机制。在这个操作模块中,我们仔细思考就能发现,对数据库的增,删,改操作是一类操作,而对数据库的查询又是一类操作。因为前者并不会返回结构化的数据,只会返回操作成功与否的结果,而后者数据库会返回结构化的数据。因此在数据库操作模块中,只会细分为两个子模块。
再往上就出现了四个独立的模块,这里先说DAO模块。DAO(Data Access Object)即数据访问对象。在这个模块中我们真正把对数据库的操作具体化了,我们所熟知的SQL语句就是在这里写明的。在这个模块中我们针对不同的数据库实体编写不同的数据访问对象,在一个数据访问对象中我们完成对一个具体的数据库实体的增,删,改,查等操作。具体来说就是我们写出SQL语句,再把SQL语句所需要的参数一起传递给下层的数据库操作模块执行。与DAO模块同级的模块还有Model,安全和工具模块。Model在Java中还有另外一个称呼叫做JavaBean,它的作用是用来封装数据库实体的数据。当我们从数据库中查询出一组数据时,这些数据我们就可以封装到一个Model中,这样在使用中也十分地方便,体现了面向对象程序设计中的封装特性。安全模块顾名思义就是封装了一些和系统安全性有关的功能,比如说数据加密模块(举例来说:对用户密码这种敏感数据在数据库中一般是不能明文存储的,要对密码做一次Hash运算。比较常用的算法是MD5加密,产生一个特定长度的数据摘要后存储在数据库中)。工具类模块中编写了一些系统所需要用到的功能。比如说在设计数据库时我们常常会给一个表设计一个主键来唯一标识一条记录,由于主键在整个表中不能有重复,因此我们就可以在程序中写这样一个工具类来专门产生这种唯一性的主键。再比如说刚才我们提到的数据库操作模块是一个通用性要求非常高的程序模块,需要能够处理数据库查询到的任何形式和类型的数据。但是,有数据库使用经验的人都知道数据库有时候查询出的数据只有一组,有时候查询出来的数据有多组(比如查询一个班的所有学生信息,就会查出来多组数据),有时候查出来的数据就是一个整数值或浮点数值(比如查询一张表有多少条记录,或者是一个班的考试平均分),数据库返回的数据形式可谓是千变万化。我们不可能也没必要在数据库操作模块中考虑到所有可能的数据形式。那么该如何确保这个模块的通用性呢?解决方法就是定义接口,我们可以定义一个接口接收数据库的返回数据。当数据库完成查询返回数据时我们就不加处理地把数据传递给这个接口的方法,接着我们可以根据返回数据的具体形式编写类并且实现上述的接口,编程时可以根据数据库返回数据的具体形式动态地传入接口的具体实现,这样数据库操作模块的通用性就得到了保证。而这些接口的实现类也可以写在工具类中,这些类可以是处理数据库返回一组数据的情况,也可以是处理返回多组数据的情况。
写到这里,数据持久层就写完了。总的来说数据持久层是Web系统中一个非常重要的模块,工作量也是比较庞大的。这一层的程序必须非常健壮因为这一层是整个系统的基础,相当于摩天大楼的地基,一旦在这一层出现大的Bug它的危害是全局性的。在开始下一层——业务逻辑层的讲述之前,我想说一下业务逻辑层和数据持久层之间的这N多个接口。我们在程序设计时经常听到一句话就是我们的程序要“高内聚,低耦合”。在大型程序的开发中这是必须要满足的一个要求。所谓“高内聚”就是要求一个类或函数的功能要相对单一,一个类或者函数最好只解决一件事或实现一个单一的逻辑,而不要在一个类或函数中实现太多功能,因为这会导致程序的臃肿,界限不明和难以维护。从系统的角度讲,相同或相似的功能最好在一个模块中完成,而不要分散到多个模块中,不同的模块间不要有逻辑和功能上的重叠。“低耦合”是针对层与层来说的。大型程序一般会对程序进行分层,根据距离用户的远近分为多层,在此可以参照TCP/IP协议体系的分层来理解,即所谓下层为上层提供服务,层与层之间最好不要有过多的联系,这样一旦一层的程序出现些许紊乱也不至于导致连锁反应,更重要的是层与层之间的低耦合性可以极大地提高发开效率。拿我们的Web系统举例,数据持久层为上面的业务逻辑层提供服务,那么业务逻辑层没必要和数据持久层发生什么直接的联系。业务逻辑层只用告诉数据持久层我需要一些什么样的服务(两层之间那N个接口的作用就是用来说明业务逻辑层需要什么样的服务),数据持久层根据这些声明的系统服务具体去实现就好了。这样如果我们的程序面临重构或者面临多组人同时开发,数据持久层的编写人员只用根据业务逻辑层的服务需求具体去实现就好了而不用关心过多。而实现这种“低耦合”的最佳实现方法方法就是使用接口,针对不同的服务编写不一样的接口。DAO模块中的不同数据访问对象根据自己操作实体的不同情况来选择实现不同的接口来实现(在系统的数据库中可能有许多种不同种类的实体,与不同类实体相关的操作也会截然不同。比如说数据库中的用户实体和其他普通的数据实体在操作上就大不一样。普通的数据实体可能更多的是批量或分页查找,而用户实体常常是需要判断一个用户是否存在或需要精准地查询到一个特定的用户。那么针对不同类的数据库实体, 就可以定义不同的接口来适应)。
业务逻辑层
业务逻辑层顾名思义就是处理业务逻辑的地方,什么是业务逻辑?比方说我们访问一个购物网站当我们决定购买一件商品并点击购买按钮时对服务器来说就面临一个业务逻辑,一个购买操作可能细分下来包括用户验证,查询商品库存是否充足,生成订单,支付等等。这些子操作构成了购买这个需求的业务逻辑。这些处理业务逻辑的程序就存在于我们的业务逻辑层中。无论多么复杂的业务逻辑最终都将会反映到对数据库的增删改查上,因此在业务逻辑层中我们将会调用刚才提到的业务逻辑与数据持久层中间的那些接口来实现对数据库的操作。
表示层
表示层是整个后端程序中距离用户最近的程序,它负责为用户展示界面。比如说我们要登录系统,当我们填入用户名和密码并且点击登录按钮后,表示层就会验证你的用户名和密码是否匹配,如果匹配就给用户展示一个登陆成功的界面,反之则会给用户展示一个失败的界面(通常情况下HTML界面和JSP页面也算作表示层的东西)。一句话,表示层决定用户将看到什么样的界面。但是看到这里你可能会发现表示层和业务逻辑层在功能上有重叠,的确是这样。但也不是都重复,比如说业务逻辑层中的处理用户上传文件的模块就是一个单纯的业务逻辑层的模块而与表示层无关。
前端
J2EE有自己一套独立的前端体系——JSP,这是一套比较老的前端技术了,本质上说也是一种Servlet。开发起来比较简单,有丰富的标签和EL表达式,还可以开发自己的标签库,十分灵活。不过目前在H5页面比较火热的情况下,前端页面推荐多使用HTML+JavaScript,页面灵活丰富。使用Ajax来加载数据(传送门:脚本化HTTP初探(一)),这样系统反应更快,用户体验会好一些。
其它
写到这里,服务器端主要的架构就说完了。总的来说整个系统遵循“高内聚,低耦合”的设计原则,代码编写时要多多体现面向对象程序设计的三大基本特性。最后再说两个J2EE的小地方,一个是过滤器一个是监视器。
过滤器是整个系统流量的入口和出口,就是说我们给服务器发送的一切请求,服务器对我们浏览器做出的一切回应都将会先通过过滤器才会达到我们的服务器或是我们的浏览器,在过滤器中我们可以看到这些请求或回应的一切参数和数据,并且可以做任意地更改,这是一个激动人心的特性,我们可以利用这个特性做很多事儿。比如说,我们可以用过滤器拦截敏感词汇,过滤关键字,可以对用户提交的信息做转义输出(有些无良用户会提交一些可执行代码给服务器,有时候可能导致服务器异常关机,因此必须要对用户提交的数据做转义),最重要的一个应用就是在过滤器中解决中文乱码问题,这个问题是由于针对当今的Unicode字符流行着许许多多的编码方式而产生的。拿汉字举例,常用的编码方式就有UTF-8,GB2312,和ISO8859-1等。如果客户机和服务器所用的编码方式不一样,就会导致出现中文乱码。解决思路就是在过滤器中我们设置HTTP头中的Content-type字段,强制客户端的编码码表,并且针对URL进行编码。这样就能很好地解决中文的乱码问题。
另一个J2EE特性就是监视器,说监视器之前就要说J2EE中五个十分重要内置对象,分别是Context对象,Session对象,Request对象,Response对象和Cookie对象。这五个对象是J2EE中极端重要的对象,接下来我们分别简单地介绍一下。Context对象是服务器中的一个单例对象,就是说在整个服务器中Context对象有且仅有一个,不存在任何拷贝。服务器中的任何程序都可以访问Context对象所在的内存,它的生命周期与服务器的生存周期是相同的,也就是说服务器启动Context对象就存在,只有当服务器停止时Context对象才被销毁。Context对象中封装一些全局性的数据,比如说当前在线人数啊,所有已经登录的用户啊这些可能很多地方都要用到的数据。Session对象是用来唯一标识用户的,我们所熟知的登录操作就是用Session做的。Session对象是通过Cookie的一个JSESSIONID字段来唯一标识一个用户,具体来说就是Session会回写一个Cookie给浏览器,这个Cookie中有一个字段JSESSIONID,它的值是一串很长的字符串,不同用户的JSESSIONID都不同,服务器中的Session对象根据不同客户机的JSESSIONID字段就可以唯一标识一个用户。正是由于Session对象这种标识用户的特性,我们可以在Session对象中封装与具体用户有关的信息,比如说一个用户的姓名,ID等等信息。它的生命周期默认是30分钟,就是说一个Session对象被创建出来如果30分钟内没有使用过,那么这个Session对象就会被自动销毁。有时候我们登录一个账号一段时间不去操作,再去用的时候就会被提示没有登录原因就在于此。Request和Response对象是服务器中封装请求和回应信息的对象,通过这两个对象可以获得请求的参数,请求的方式等等诸多细节,这两个对象的生命周期很短通常只有一个请求回应周期,也就是说当客户机给服务器发送一个请求,服务器回应之后这两个对象就被销毁,内存就被回收了。而至于Cookie对象它与浏览器和Request,Response对象都有关,是一个特殊的HTTP头部字段,在浏览器中也有存储,在这里就不细说了。而我们要说的监视器就是用来针对Context对象,Session对象,Request对象和Response对象。一旦我们实现了监视器,我们就可以在这四个对象创建,销毁,写入数据时做点事情。最常见的应用就是在线人数统计,我们之前提到过登录本质上就是一个Session被创建的过程,那么我们就可以写一个针对Session对象的监听器,一旦一个Session创建则登陆人数就加一。
以上就是全篇的内容啦,希望共同学习共同进步!