转载来自:http://my.oschina.net/aaron74/blog/282304
我把我这些年在java学习中学到的东西,按照项目开发中可能遇见的场景,进行了一次梳理。
这个故事是我最后决定加上来的,我非常喜欢这个故事,软件工程中有一个被戏称为Cargo Cult编程法的编程风格,而下面这个故事讲述了此编程法的来源:
早在40年代,据说,美军曾驻扎在一个偏远的岛屿。岛上的土著居民在此以前从未见过的现代文明,所以,他们对联军和他们带来的东西非常惊奇。他们发现联军修建了机场跑道和控制塔,带着耳机的士兵对天呼叫,然后满载着大量货物的大铁鸟便从天而降。当铁鸟降落后,货物便分发给所有岛上的人们,为人们带来繁荣。
终于,有一天,部队离开了,大铁鸟也不再回来了。为了再次得到货物,岛上的土著居民用竹子建造了自己的跑道,控制塔,让他们的头领登上平台,并让他戴上用椰子做的耳机。但无论他们如何努力尝试,大铁鸟再也没有回来。
几十年后,研究人员发现了该岛。岛上的土著居民仍旧保留着这一宗教仪式。他们把岛上居民的这一奇怪的宗教仪式命名为“Cargo Cult”
@考虑这样一个应用场景:我们的项目功能日渐强大,代码却日渐臃肿,我们如何将代码变得有条理些?
无论我们是学习还是工作,我们的前辈总是会告诉我们,我们需要把java项目进行架构上的分层,界面层(UI) 业务逻辑层(BLL) 数据访问层(DAL),就像Cargo Cult中的土著居民一样,虽然我们并不知道为什么高手们要那样做,但是我们相信这么做可以让程序工作起来,后面我会讲到为什么会有所谓的经典的三层架构,现在我们已经做完的事是,按照三层架构将项目搭建并运行起来了!
我们一般会想到MVC,很多书上都有写到,可惜很多时候我们理解的MVC是错误的。。。甚至我曾经天真的以为,MVC正好对应着DAL,UI,BLL。。。实际上,这两者并没有显式的关系,前者属于设计模式,而后者属于架构设计的范畴,如果一定要扯到一起的话,关系可能会是这样的:
|
|
实际上,Controller是很薄的一层,它仅仅负责接收参数,封装参数,调用不同的service。所以我们可以考虑先从它入手,简化代码。
我们现有的controller做法是,不同的业务调用不同的servlet,通过参数的不同,调用不同的方法,比如,有下面一个form
1
2
3
4
|
< form action=”UserServlet? command = login ”>
< input name=”name” />
< input name=”password” />
form >
|
如果我们在这个表单中输入用户名密码,最终后台会将这个请求提交到UserServlet中,然后根据command=login,调用UserServlet中的login方法。在login方法中,会有这样的一段代码:
1
2
3
4
5
6
7
8
|
String name = request.getAttribute(“name”);
String password = request.getAttribute(“password”);
Boolean result = userService.hasUser(name, password);
if (result) {
....
} else {
....
}
|
我们可以发现,基本上这个servlet中,所有的方法几乎都有着从request中获取参数的这么一个过程,而且同一个servlet中,需要获取的参数大部分都是重叠的(比如UserServlet中,几乎所有的方法都需要获取name和password的值,才能进行近一步操作),既然每一个方法都有这么一个需求,为什么不考虑将这一过程抽象出来呢?
首先,我们可以设计一个叫AaronDispatcher的类,它负责截取了所有的对项目的访问的http请求。
比如,我们上面的请求叫UserServlet?command=login,同时传递三个参数name和password(以及上面的command) 。AaronDispatcher巨牛叉,它直接把这个请求截取了,并进行分析,首先它的名字叫UserServlet,调用的方法叫login。为了不引发歧义,我们更改前台的请求地址,改为发送到UserAction?command=login。
然后我们可以重新设计UserServlet,创建全新的UserAction。(现已加入豪华午餐)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
public UserAction {
String name;
String password;
String newpassword; //updatePassword这个方法需要
String oldpassword;
...... //所有的UserAction从前端获取的参数
public login() {
...
}
public logout() {
...
}
public updatePassword() {
...
}
...... //所有UserAction需要提供的方法
} //UserAction结束
|
(眼疾手快的人也许可以注意到一点:这个类不再需要接受HttpServletRequest及HttpServletResponse作为参数了)
每当我们有一个发送到UserAction的请求,AaronDispatcher就帮我们new一个新的UserAction实例,同时将请求中的参数赋给UserServlet中的属性。具体的底层做法,类似于下面这样:(实际上会复杂很多,不会直接new对象,而是使用反射来创建对象并赋值属性)
1
2
3
4
5
6
|
UserAction userAction = new UserAction();
userAction.setName(request.getAttrbute(“name”));
userAction.setPassword(request.getAttrbute(“password”));
userAction.setNewpassword(request.getAttrbute(“newpassword”));
userAction.setOldpassword(request.getAttrbute(“oldpassword”));
......
|
如果我们需要登陆功能,直接调用userAction.login()就可以了(至于name和password,直接可以在方法内部获取当前对象属性)。 所有的方法中,从request中获取参数并进行封装的这么一个过程,全部都被巨牛叉的AaronDispatcher做了,是不是减少了很多重复的代码量?!
可能会有疑虑,所有的请求,无论什么方法,都进行一次属性全赋值,如果前台没有传入这个属性,不就为空了嘛?但是要知道,如果我们调用login这个功能,newpassword和oldpassword固然会因为前台没有相应的属性值传入而设为null,但是,但是,在login方法中,我们根本就不会用到这两个参数啊!所以即使为空也不会有错的!
甚至我们可以做的再牛叉一点,AaronDispatcher会去读取一段配置文件,配置文件中指定了什么样的请求调用什么养的类以及相应的方法,这样我们就可以彻底解耦最前方的Controller了!
但是AaronDispatcher是怎么做到无论什么类,当中有什么属性,我们都不需要事先知道,我们都可以接收前端参数,给他们的属性赋值呢?(答案是通过反射)
现在,我们已经成功的重新发明轮子了!
因为以上这个伟大的想法已经有被别人抢在前面实现了,就是著名的Struts2,毋庸置疑,Struts2的核心功能就是这么简单。
在Struts2中,每一个处理类被称之为Action,而Struts2也正是通过xml配置文件,实现了无需要修改代码,通过修改配置文件,就可以修改Controller。
Struts2发展到今天已然是一个功能齐全的庞然大物了。
正如一开始所说,MVC框架只不过帮助我们封装了请求参数,分发了请求而已。Controller是非常薄的一层,而我们的业务逻辑都是由BLL层提供的Service对象实现。
首先讲述一下为什么会有所谓BLL(Business Logic Layer)和DAL(Dataaccess Layer)了。在一个项目中,无论是查询用户的用户名,还是查询库存数量,这些数据终归是要保存到数据库的,而这些对数据库的操作将会无比的频繁,如果我们不将这些对数据库表的操作独立出来,如果在多个方法中存在着对一个用户记录的查询,我们不得不把这段代码copy、paste无数次,既然这样,我们为什么不像上面那样,将这种可能会多次遇到操作抽象出来呢?于是就有了所谓的DAL了,这样,无论在什么地方,需要用到数据库查询相关的工作的时候,仅仅需要这么做:
1
2
|
User user = userDaoImp.getUserById(userId);
......
|
这么做有一个好处:减少了因为持久化方案的更换而导致的代码修改带来的工作。
持久化是一个非常高端大气的专业术语,说的更专业一点,就是将内存中的数据保存到硬盘中。在我们的项目中,用户进行了注册,我们需要将用户注册的用户名密码保存起来,以便下次用户登陆的时候我们能够知道,这个用户名的用户是合法注册过的。
通常持久化的方案就是将数据保存到数据库中,但是我相信如果我不愿意使用数据库,而直接将用户名密码明文保存到文本文件中,也没有人会从技术上反对吧(实际上这种事情在中国互联网的发展历史中还真发生过。。。),如果我真的选择这么做,我所需要做的工作就是仅仅修改DAL中的实现,将对数据库的操作改为对本地文件的操作,而无须修改调用持久化方法的方法。
业务层负责业务的处理(接收上层传过来的信息进行处理),当处理完之后,将处理的结果利用DAL的对象进行“保存到硬盘”。而DAL具体是怎么实现的,完全不会影响到已实现的业务。
很明显的,为了做到上面这一点,DAL中的方法要尽量的“单纯”,不包含任何的业务上的逻辑,仅仅是将内存中的数据(一般就是某个对象)保存到硬盘的“实现”,以及从硬盘读取的数据提取到内存的“实现”。
已经很明显了,三层架构不是从来都有的,只不过是在无数次痛苦的经历过后先烈们总结出来的一套证明可以在某一方面减少因变动而带来的额外工作量。说它经典,也只不过是因为它实现了展示、业务、持久化这三个必不可少却又相对对立的需求的切割(不过确实有的项目中,展示不是必选的)。
所以基本上所有的复杂架构也只不过是在此基础上的进一步分割,曾经做过一个巨复杂SaaS项目,为了减少某些不定因素的变动而带来的代码上的改动,架构师将BLL分成了两层,在原有的BLL之上又增加了一层core business layer。这样MVC框架只需要调用core business的业务而无须自己在重复组装比较底层的业务逻辑了。
如果有更复杂些的项目的话,就需要通过分割子项目及更复杂的层级关系来解决了。
这个时候我们或许应该讲述BLL了,不过在此之前,我们可以再多想一步,能不能修改DAL中的东西,让我们使用起来更简单?
一般来说,数据库中的表对应着java中的类,表中的一行记录对应着一个entity对象,表中的字段对应着对象中的属性,我以前一直觉得很神奇,这就是传说中的ORM。这当中还有很多更复杂的东西,比如多表级联的结果映射为对象,在这里我们先忽略这些复杂的情况。
有了上面的知识,我们可以发现, 如果我们选择关系型数据库作为持久化方案,我们的DAL其实也很“单纯”,他们所做的也不过是将对象属性通过sql存储到数据库、将通过sql获取的数据封装为对象。
同样的我们可以写一个巨牛叉框架(好吧,这次不是写一个巨牛叉的类了),它会自动根据我们entity的名字,去数据库寻找相应的表,当我们调用insert,delete,update,select等方法的时候,它会自动帮助我们根据要求及参数拼接sql,然后去数据库查询/修改记录,如果是查询,则把查询出来的记录集封装成对象,保存在list中。这样,我们就可以在DAL中简单的定义一些entity就可以了。
比如,我们这次在DAL中,仅仅只定义了一个类:
1
2
3
4
5
|
public User {
long id;
String name;
String password;
}
|
1
2
3
|
//我们已经假设了框架巨牛叉,所有的DAL对象都是可以根据entity自动生成
AaronDao userDao = new AaronDao(User. class );
List //括号里面是一些列条件,比如一些分页条件啊,属性限制啊之类的。
|
1
2
3
|
response.getWriter().println(“”);
response.getWriter().println(“hello ” + user.name);
response.getWriter().println(“”);
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
public void doPost(HttpServletRequest request, HttpServletResponse response){
Strring cmd = request.getAttribute(“command”);
if (“addUser”.equals(request.getAttribute(cmd))) {
String name = request.getAttribute(“name”);
String password = request.getAttribute(“password”);
User user = new User(name,password);
if (userDaoImp.getUserByName(“name”) != null ){
userDaoImp.addUser(user);
...... //返回添加成功页面
} else {
...... //返回添加失败页面
}
} else if (其他command) {
......
}
}
|
1
2
3
4
5
6
7
8
9
|
public void doPost(HttpServletRequest request, HttpServletResponse response){
String cmd = request.getAttribute(“command”);
if (“addUser”.equals(cmd)) {
String name = request.getAttribute(“name”);
String password = request.getAttribute(“password”);
User user = new User(name,password);
userService.addUSer();
}
} //结束doPost
|
1
2
3
4
5
|
public static void main(String[] args) {
UserService userService = new UserService();
User user = new User(name,password);
System.out.println(userService.addUser(user));
}
|
1
|
import com.unis.team1.UserService;
|
1
|
import com.unis.team2.UserService;
|
1
2
3
4
5
|
import com.unis.team1.UserService;
public UserAction{
UserService userService = new UserService();
....... //参考struts2中需要的东西
}
|
1
2
3
|
public interface IUSerService {
List
}
|
1
2
3
4
5
|
import com.unis.service.UserService;
public UserAction{
IUserService userService;
....... //参考struts2中需要的东西
}
|
1
|
Animal cat = new Cat(); //Cat实现了Animal接口
|
1
2
3
|
UserAction userAction = new UserAction();
IUserService userService = new UserService();
userAction.setUserService(userService);
|
1
2
3
|
UserAction userAction = new UserAction();
IUserService userService = AaronContainer.get(IUserService. class )
userAction.setUserService(userService);
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
public interface Test {
public void test();
}
public AaronTest implements Test {
public void test() {
System.out.println(“hello world !”);
}
}
public AaronTestProxy implements Test {
public Test testImpl;
public AaronTestProxy(Test test) {
this .testImpl = test;
}
public void doBefore() {
System.out.println(“ do before !”);
}
public void doAfter() {
System.out.println(“ do after!”);
}
public void test() {
this .doBefore();
this .testImpl.test();
this .doAfter();
}
}
|
1
2
|
Test test = new AaronTest();
test.test();
|
1
2
|
Test test = new AaronTestProxy( new AaronTest());
test.test();
|
进入下面一个应用场景吧!
@考虑这样一个应用场景:我们已经利用SSH(是的,就是那个很流行的Spring,Struts2,Hbiernate),人模人样的将项目重新搭建起来了,代码的结构瞬间变得无比清晰(虽然我相信百分之八十以上的人并不是真正理解SSH),这个时候,项目变得越来越庞大。。。
SSH在华人社区流行已久(主要是大陆和台湾地区),其实我个人并不喜欢这三个框架的组合。在欧美地区,只有Spring是依然流行的。(实际上Spring的地位也在遭受很多轻量级IOC框架的挑战)
在这次的应用场景中,我准备一次性讲述多一点的东西,所以这次的假设会糟糕一点。
因为开发人员众多,经常会发生每个人的开发环境不一致,比如明明我们用的是Spring 3.0的jar包,却偏偏有人用的是Spring 2.5的jar包。而且项目越做越大,我们不得已分成了几个子项目,交由不同的团队去做,可是经常发生其他团队已经完成了bug fix并打出新包了,我们仍然在沿用以前的有bug的旧包!
以前写过一篇关于Maven的简单介绍,这次就直接copy过来了。
关于构建的概念:
很久很久之前,在我刚刚学习Java的时候,那个时候还是非常的菜的,有一次学习struts,需要做一个项目,当时的流程是这样的(我相信很多人都时曾相识?):
1、熟练地打开eclipse,新建dynamic web项目
2、从struts官网下载最新的jar包(好多好多个jar文件啊)
3、不管三七二十一,刷的一下,把所有jar包放到WEB-INF/lib
4、开始写代码
5、右击项目,导出war包,拷贝到tomcat的webapp下
这个过程看似没有问题,直到后来实习的时候被项目经理狠批。这个过程中我根本就不知道我所需要的什么jar包,不管有用的没用的,全部放到lib下。这种做法不但会导致lib臃肿难以维护,而且根本无法保证所有开发人员的第三方依赖包版本一致。最关键的是,因为每一次的打包动作,都需要开发人员从eclipse中右击导出war包。想象一种情况:我们的项目中有一个依赖的jar包是另外一个开发小组所写,如果我们需要进行最新的项目测试,我们首先得先让另外一个开发小组将他们最新的代码导出为jar包, 然后将他们最新的jar包导 入我们项目的classpath(这个过程,是不是离开了eclipse大家就不会了?),然后继续,右击导出war包。如果我们依赖的多个jar包都是我们不同的团队所写,这个过程所耗费的时间将成集几何的增长。
题外话:eclipse导出war包的原理:
我们每次通过eclipse创建dynamic web项目的时候, eclipse会自动帮我们创建一个src文件夹,WebContent文件夹,.project配置文件(其实根据插件的选用,隐藏配置文件会有很多个)。同时引入了一些列classpath:src、 WebContent/WEB-INF/lib、jvm核心依赖包、tomcat中的一些servlet依赖包、其他。。。当我们导出war包的时候,eclipse 首先根据.project中的配置,导入classpath中的依赖包(这也是为什么我们在初学阶段,总是将需要用到的这些jar包放到WebContent/WEB-INF/lib中去),将src下的所有.java文件编译(这也是为什么我们的源代码总是要写在src下才能被编译),将生成的.class文件拷贝到 WebContent/WEB-INF/classes中。最后将WebContent打成war包输出(war包的名字为:项目名.war)。
回来继续:
因为这个过程绝对不可能由人来做(耗费时间太长而且没有意义),于是后来我们就开始学习Ant来进行刚才的操作。Ant的原理就是:指定好我们需要编译的源代码路径(不管一个公司有多少项目,这些项目最新的源代码路径总是有的)、依赖的jar包位置(Ant会帮我们把这些依赖包引入到classpath中)、打包的方式(jar还是war等等之类)、以及打包的顺序(就像最开始所说的那样,可能某一个模块的编译需要依赖另外一个模块,被依赖的源代码会首先被编译)。然后Ant就会帮我们编译这些源代码了,那些被依赖的jar会首先被编译打包(会根据依赖树进行打包顺序),然后放到某个文件夹下,然后这些被依赖的jar包会被引入classpath,然后继续编译,最后,所有的源代码编译结束,将依赖的jar包们拷贝到WEB-INF/lib中,web层的.class们拷贝到WEB-INF/classes中。 删除被打出来放到临时目录 的jar包们。将最后的war包拷贝到指定目录,整个过程就结束了。
需要说明一下的是:平时我们在eclipse中,eclipse已经帮我们指定了servlet容器位置,所以我们不觉得有什么不对劲,但是一旦自己手动打包的时候,我们必须手动指定servlet相关jar包的路径并引入到classpath中!
在刚刚的过程中,我们发现,我们可以做到只需要一个命令:Ant自动帮我们根据顺序依次打包最新的代码,然后将最新的代码进行打包,并且可以将生成的war包拷贝到指定目录下,于是我们可以引入一些系统脚本,测试只需要执行一下某个脚本文件,就可以自动的将新鲜出炉的war包放入tomcat/jboss/jetty中,并启动容器。之后什么都不需要做,直接打开浏览器就可以测试啦!
这个过程之中的问题:
如果你写过Ant的build.xml,也就是Ant的配置文件,你一定会发现这是一个痛苦的过程。首先,我们得指定第三方依赖包的路径,虽然这样能保证所有人引用第三方依赖包的版本一致,但是如果依赖包过多的话,需要编写老长的一段说明。其次,我们需要指定项目源代码的路径,这样是很长的一段说明(同一个项目不一定在同一个地方啊。。。像我之前所说的那样,可能我们项目中的某个模块,是由另一个团队在别的路径下编写,最后我们只是引用他们所生成的jar包而已)。最后,也是最坑爹的事情,我们必须手动指定打包顺序:如果a模块依赖b模块和c模块(这三个模块都是我们自己开发),b模块依赖c模块,我们必须先打包c模块,然后再打包b模块(同时引用c模块打好的包),最后打包a模块(同时引用b模块和c模块打好的包)。这才是最简单的一种情况呀,随着项目的模块化,这种关系可能会变得更加复杂,打包顺序也需要考虑好一会,即使我们很聪明,这种树形顺序难不倒我们,编写xml也是一个很烦人的任务啊。
Maven的出现:
我们可以简单的认为,Maven也是为了完成上面的任务所诞生的,但是相比较于Ant,我们所需要编写的xml将会骤减不少。而且,在大多数的时候,打包顺序这种费脑经的事,Maven可以依据依赖关系自动帮我们完成。
同时,Maven不但可以完成了上述Ant所写的所有功能,还有额外的一些功能,帮助我们缩短开发时间:
我们不必再自己去各个第三方依赖包官网手动下载依赖包了,只要申明了我需要某个jar包的某个版本,Maven会自动帮我们下载下来并在编译、打包的时候引用。
Maven贯穿项目整个生命周期,从早期的依赖包下载到后期的打包部署,在编译过程中,找到测试代码,运行jUnit等工具进行测试并生成测试报告。
Maven比Ant更进一步,更像一个部署工具,依靠插件,可以在运行Maven命令之后,甚至连数据库初始化这个工作都可以帮我们做了,在编译之后,将项目拷贝至容器并启动容器,真正做到“开箱即测”。
Maven的使用:
自行百度吧。。。
Maven的出现实际上是为了适应我们项目越来越复杂,开发人员越来越多,依赖包越来越庞大,同时拆分为更多子项目,打包过程繁琐,自动化测试程度越来越高的发展需要。
我原本觉得下一章应该进入自动化测试的介绍了,但自动化测试绝对是一个无穷无尽的深坑。。。所以我就简单描述一下,在自动化测试中,所有的操作必须由代码来模拟,测试过程中不会有人的干预,所有用例均由代码控制。作为开发人员,我们提供出去的每一个方法都有相应的测试用例,最终会在编译过程中自动由Maven进行测试并生成报告。
进入最后一个应用场景
@考虑这样一个应用场景:有一天我们领导发现,单页面应用变得火爆起来,于是让我们将项目转为单页面应用
首先还是得解释一下什么是单页面应用。
传统的网站在页面上会有各种的超链接以及表单按钮,通过点击这些超链接或者表单按钮,我们会访问不同的网站,如果我们有当当购书的经验,就会知道,当我们点击了购买按钮时,我们会跳转到支付页面,当完成支付时,我们会跳转到购买成功的页面,我们不断地在跳转,也没有觉得有什么不对。
的确,事实上这没有什么不对。
还记得google maps吗?那个超炫的页面,我们在页面上滚动鼠标中论,就可以实现地图的放大和缩小,我们不断地像后台发送请求,后台也一直在反馈,可是至始至终,我们都没有进行过页面的跳转。
这就是ajax,它负责将用户的请求发送到服务端,然后将接收到的返回数据渲染到当前html中。在这过程中,一直都是在早已渲染好了的html中进行操作。
很自然而然的,因为没有页面的全局刷新,我们就像在操纵本地软件那样操作进行操作,正如single app一词,它就像一个app而不是一个网站。
在具体介绍之前,我们先老生常谈一个问题,如何实现MVC?
我去,这个问题在第一个场景中不就已经讲过了吗。
正如下面所见:
|
|
由Controller接收请求,将数据封装为 Model 的形式,并将其传入 View 中,毫无难度。
好吧,不是我侮辱大家智商,只是,很多事情并不是我们想当然的那样,正所谓,世事无绝对嘛。
我们来看这张图:
|
|
在上图的MVC模式中,我们直接在 HTML(也就是 View)中发送 AJAX 请求,该请求由 Action(也就是 Controller)接收,随后将数据模型转为 JSON 格式返回到 HTML 中。
早期前端都是比较简单,基本以页面为工作单元,内容以浏览型为主,也偶尔有简单的表单操作,这个时期每个界面上只有很少的JavaScript逻辑。
随着AJAX的出现,Web2.0的兴起,人们可以在页面上可以做比较复杂的事情了,js针对DOM的操作越来越频繁,于是出现了以简化DOM操作、屏蔽浏览器差异的js库jQuery。(jQuery仅仅是js库,不是框架。。。)
伴随着一些Web产品逐渐往应用方向发展,遇到了在C/S领域相同的问题:由于前端功能的增强、代码的膨胀,js代码变得越来越混乱。
于是我们想起了曾经在后台使用过的方法,各种基于MVC变种模式的前端框架应运而生。