Without SSH/JSP/Servlet,不走寻常路,Java可以更酷

标题的构思来源于Rod Johnson的那本"Without EJB"以及CCTV5中一句耳熟能详的广告词,
不过此文并不是用来批判SSH(Struts、Spring、Hibernate)/JSP/Servlet的,
也不是为某品牌做广告,而是用来分享这将近一年来的研究心得。

去年圣诞节时曾在JavaEye发过一两篇文章,不过现在找不到了,
文章内容提到要在3个月左右的时间内设计出一个有别于SSH的新型MVC框架,
设计的起点最初是以JSP/Servlet为基础的,虽然在两个多月后有了个雏形,
但是跟Rails这样的框架相比还是没有明显的优势,
比如在不使用反射的情况下,
很难将不同的uri对应到Servlet类中的public方法。
(Servlet类指的是继承自javax.servlet.http.HttpServlet的类)
每次修改Servlet类的源代码时总得经过烦人的手工编译步骤(有时还不得不重启Tomcat),
还有与数据库打交道的模型层也得人工干预,一堆烦人的映射配置。

那三个月内时常有沮丧感,似乎已走近了死胡同!
后来心一狠,决心甩开JSP/Servlet那一堆条条框框,把设计的起点再往下深一个层次。
因为2007年曾详细研究过Java语言的编译器(javac)实现细节,所以从编译器着手,
但是编译器的强项在于分析Java源代码,无法处理Http请求,
接着在网上把Tomcat6的源代码下下来研究了三个月,
期间顺便研究了Sun公司的超轻量级Http服务器"com.sun.net.httpserver"的源代码,
同时详细学习HTTP/1.0(RFC1945)与HTTP/1.1(RFC2616)协议。

但是Tomcat6过于臃肿了,包含的Java文件超过了1300个,
光是解析server.xml与web.xml的代码看完后就让人有烦躁感。
(如org/apache/tomcat/util/digester与org/apache/catalina/startup包中的很多类)

另外最重要一点,Tomcat6采用的是Eclipse JDT编译器,仅仅是用来编译JSP文件,
编译器在控制层没有发挥一点作用。
而Sun公司的超轻量级Http服务器又过于简单了,连HTTP/1.1的大多数功能都没实现,
除了参考一下它的SSL实现外基本上毫无价值。

本想在现有的JSP/Servlet容器上做一下简单扩展就得了,
哪知也是四处碰壁(还下过Jetty的源代码下来看了一会,结果发现比Tomcat6还糟),
后来决定对Tomcat6与Sun的Http服务器进行大刀阔斧的改造,
完成了一个精简版的改良后的基于NIO的Http服务器(目前的版本只有60个左右的Java源文件),
并且能跟Javac编译器完美结合,能直接运行Java源文件。

在模型层这一块,最初是从书上和网络上对Hibernate进行应用层次的研究,
但是并不想深入源代码,因为代码量也实在是太多了,倒是对Ibatis2.0深入研究了一下,
Ibatis2.0代码量比较少,也简单,看了不到一星期就基本上看完了,不过现在并没留下深刻映象,
因为并没发现什么特别出彩的地方,Ibatis2.0还是离不开xml,而我想要完全抛弃xml。

当然,不管Hibernate也好,Ibatis2.0也好,相比Rails的ActiveRecord还是逊色了点,
不过我的目标并不是要造一个Hibernate、Ibatis2.0或ActiveRecord这样的轮子,
我的要求更高,我在想如何才能写更少的代码,如何才能实现自动化?
可不可以在服务器启动时或运行时动态解析数据库的元数据,
让编译器跟据这些元数据动态生成类呢?
接着我转去研究JDBC-1.2/JDBC-2.1/JDBC-3.0/JDBC-4.0规范,研究数据库驱动的开发手册。
我得从零开始,我目前的实现是这样做的:你可以在你自己的Java源文件中直接引用动态生成的类,
就像这些类是你自己写的一样,ORM已基本上实现自动化了,2.9 节专门讲Douyu的ORM。

最后一点值得一提的是,我在Java语言层次引入了权限管理模型,
不过你别担心,我并没有引入新的Java语言语法,
只是借助Annotation扩充了某些特殊的语义。
目前这个权限管理模型的粒度只是划分为功能、字段权限两个等级,
并没有实现与具体业务相关的数据权限,不过在未来的路线图中有打算引入工作流模型,
到时会努力尝试各种实现数据权限的方案。

与权限相关的细节请看2.8节 Douyu的权限模型



折腾了半年后,发现已不再是个MVC框架了,我想称为平台更合适,
一种运行在JVM之上的新型平台,我给她起了个名字: Douyu
(呵呵,名字的由来暂时保密,也许你能猜出来。。。)



虽然孤军奋战将近一年,自我感觉小有成就,但是还有很多不怎么满意的地方,
各位大牛们也许更牛,看见不爽砸砖头便是。



Ok,上干货。




1. 安装配置


(这里只针对Windows平台,特别是XP操作系统,因为我没其他试验环境)



1.1 安装JDK


Douyu是在JDK1.6下开发的,不支持也不打算支持JDK1.4及更早的版本,JDK1.5我没有测试过,
所以我只能推荐你安装JDK1.6了,安装细节我想你都会,
唯一要注意的一点是:最好是建个JAVA_HOME环境变量,然后把%JAVA_HOME%/bin加入到Path中,
因为在Douyu服务器的启动脚本中并没有进行过多的环境检测,
而是直接使用了%JAVA_HOME%/bin目录下的java命令来启动Java HotSpot VM。


1.2 安装Douyu服务器

Douyu项目主页目前放在:
http://code.google.com/p/douyu/

请先下载二进制版的压缩文件:
http://douyu.googlecode.com/files/Douyu_0_1_0.rar

目前的版本是:0.1.0,版本号很小,但大多数功能都包含了,
我并不推荐你用于工业级别的产品开发,
因为还不稳定,目前只适合分享、交流、尝鲜目的。

下下来后直接解压到一个你选定的目录(假定你解压到了D:/Douyu目录)

D:/Douyu目录里头有下面7个目录(跟Tomcat6差不多):

Java代码 复制代码
  1. apps  //应用程序的源代码放在这里,里头有一些java源文件是下面的演示中用到的,当然你可以全都删了。   
  2. bin   //服务器的启动脚本和运行时类库都在这里   
  3. conf  //服务器的配置文件放在这里   
  4. lib   //应用程序使用到的第三方类库(比如数据库驱动)都放在这里,初始情况下是个空目录   
  5. logs  //存放服务器运行期间的日志(目前日志只是输出到控制台),初始情况下是个空目录   
  6. temp  //服务器运行期间用到的临时文件夹(比如上传文件时可能会用到),初始情况下是个空目录   
  7. work  //服务器运行期间的工作目录,初始情况下是个空目录  


了解了这些就足够了,目前你不需要做任何配置。







2. 体验Douyu



2.1 如何运行Douyu服务器?


点"开始->运行",输入cmd,打开一个控制台,切换到D:/Douyu/bin目录,
然后输入 douyu  启动Douyu服务器 (要关闭Douyu服务器连按两次Ctrl+C既可)
见下图:



如果你是第一次打开操作系统第一次启动JVM运行Java程序
或是隔了一个小时左右重新启动JVM运行Java程序,这时可能要等待几秒钟(5--10秒),
出现这种情况并不是Douyu服务器的问题,而是JVM本身或操作系统的问题,
通常启动Douyu服务器如果不加载数据库的话,一般在一秒钟内就能启动完成了。

Douyu服务器默认情况下监听的主机名是: localhost,端口: 8000

如果你不喜欢这样的默认配置,
或者最常见的情况是端口8000被占用了
(一般抛出异常: java.net.BindException: Address already in use)
你可以打开conf/server.java这个服务器配置文件,
配置文件本身就是一个java源文件,参数的配置使用Java语言的Annotation语法,
所有与服务器配置有关的都是Annotation或是Enum,全都在com.douyu.config包中定义。

Java代码 复制代码
  1. import com.douyu.config.*;   
  2.   
  3. @Server(   
  4.     port=8000,   
  5.     .................  




要修改默认主机名和端口,请修改hostName和port的值,
hostName是一个字符串,可以用IP地址来表示,port是一个整型(int)值。


其他很多参数先不罗列了,使用到时再详细说明。


当你修改了conf/server.java后,你也不需要自己去手工编译它,
启动Douyu服务器时,Douyu会自行决定是否要编译它。
如果conf/server.java存在语法错误,那么编译失败,
Douyu服务器的启动也会失败,同时向你显示编译错误信息。



下文中假定Douyu服务器已启动,监听的主机名是: localhost,端口是: 8000
以下所有例子都经过严格测试了,

我的JRE版本:
D:/Douyu/bin>java -version
java version "1.6.0_16"
Java(TM) SE Runtime Environment (build 1.6.0_16-b01)
Java HotSpot(TM) Client VM (build 14.2-b01, mixed mode, sharing)

测试浏览器用了两个:

傲游浏览器(IE6.0),
谷歌浏览器(Chrome 3.0.195.27)





2.2 Hello World!



2.2.1 程序代码


Java代码 复制代码
  1. //对应apps/HelloWorld.java文件   
  2.   
  3. import java.io.PrintWriter;   
  4. import com.douyu.main.Controller;   
  5.   
  6. @Controller  
  7. public class HelloWorld {   
  8.     public void index(PrintWriter out) {   
  9.         out.println("Hello World!");   
  10.     }   
  11. }  



2.2.2 手工编译已经Out了,你再也不需要这一步了。


2.2.3 运行HelloWorld

打开你心爱的浏览器,输入 http://localhost:8000/HelloWorld
如果你能看到下图中所示内容,恭喜你,你己经进入了Douyu的精彩世界。



(注:这是你第一次直接运行Java源文件,可能会等几秒钟(2--4秒),因为Douyu得初始化编译器)


2.2.4 程序代码说明

com.douyu.main包中的类大多数是Annotation,还包含一些重要的接口和类,
相当于java.lang,是你用Douyu开发程序时最常用到的,也是通往其他模块的快速入口,
本想让com.douyu.main包中的类像java.lang一样让编译器自动导入的,
但是考虑到很多开发人员更偏爱使用IDE,不同IDE内置的编译器不一样,
从而会引起找不到com.douyu.main包中的类的问题,所以最后决定放弃这样的设计了。

@Controller 这个Annotation是用来告诉Douyu这是一个控制器,
当你在浏览器的地址栏中输入http://localhost:8000/HelloWorld 这样的uri时,
浏览器内部通常会生成一个HTTP GET请求消息,消息内容类似这样:

Java代码 复制代码
  1. GET /HelloWorld HTTP/1.1  
  2. Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg,......   
  3. Accept-Language: zh-cn   
  4. Accept-Encoding: gzip, deflate   
  5. User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; Maxthon)   
  6. Host: localhost:8000  
  7. Connection: Keep-Alive  


不过这里并不打算介绍HTTP协议,如果你有兴趣,可以把RFC2616下下来研究。

Douyu服务器收到浏览器发来的请求消息后,
特别留意 "GET /HelloWorld HTTP/1.1" 这一行消息,
其中的"/HelloWorld"表示想要获取Douyu服务器上的哪些资源,
资源有静态的(如html、jpg等文件),也有动态的,在Douyu服务器中动态资源只有一种,
凡是带有@Controller这个Annotation的Java源文件都是可以直接通过uri访问的动态资源。

不过Douyu服务器不能根据uri的表面特征一眼就看出它是动态的还是静态资源,
服务器内部有一个专用的资源装载器,装载器的搜索根目录是从apps这个地方开始的,
资源装载器会尝试将apps目录与uri组合成一个java.io.File对象,
如果File对象存在,那么它就是一个静态资源,
然后由Douyu服务器内部的静态资源处理器给浏览器发送包含有文件内容的响应消息;

如果File对象不存在,资源装载器把请求的uri当成一个类名,
然后尝试采用类装载器的方式装载类,如果找不到那么就直接返回未找到(404)消息;
如果找到了,并且uri是第一次请求的,资源装载器会返回java源文件,
然后把java源文件交给Douyu服务器内置的编译器处理,编译器的处理过程很复杂,
这里就不深入说明了,总之它会为你动态生成HelloWorld的实例,
然后调用它的index这个缺省的public方法,
之后调用out.println()方法把"Hello World!"发送给浏览器。

2.3 控制器层与视图层的协作


2.3.1 程序代码


Java代码 复制代码
  1. //控制器层的代码   
  2. //对应apps/WhatTime.java文件   
  3.   
  4. import java.util.Date;   
  5. import java.util.HashMap;   
  6.   
  7. import com.douyu.main.Context;   
  8. import com.douyu.main.Controller;   
  9.   
  10. @Controller  
  11. public class WhatTime {   
  12.     public void index(String name, Context context) {   
  13.         HashMap paramMap = new HashMap();   
  14.         paramMap.put("name",name);   
  15.   
  16.         context.out("WhatTime.html", paramMap, new ParamWrapper());   
  17.     }   
  18.   
  19.     public static class ParamWrapper {   
  20.         Date time = new Date();   
  21.     }   
  22. }  



 

Html代码 复制代码
  1. //视图层的代码   
  2. //对应apps/WhatTime.html文件   
  3.   
  4. <HTML>  
  5. <HEAD><TITLE>现在几点了?TITLE>HEAD>  
  6. <BODY>  
  7. 你好: ${name}, 现在的时间是: ${time}   
  8. BODY>  
  9. HTML>  




2.3.2 运行 WhatTime

打开浏览器,输入 http://localhost:8000/WhatTime?name=Douyu
结果类似这样:




2.3.3 程序代码说明

WhatTime类的功能很简单: 就是提供报时服务。
index方法与第一个例子有所不同,第一参数用来接收请求uri中的name参数值,
第二个参数的类型是Context,它是一个接口,在com.douyu.main包中定义。
Context是最核心的接口,定义了很多重要的方法,
不过这里只介绍out(String template, Object... paramWrappers)方法。
out方法是控制器层与视图层通信的唯一通道。

先来看这一行代码:
context.out("WhatTime.html", paramMap, new ParamWrapper());
上面的WhatTime.html文件就相当于一个模板文件,对应out方法的第一个参数,
WhatTime.html模板文件中的${name}与${time}表示一些占位标记,
意思是告诉控制器:在把我的内容发送到浏览器前,先把这些占位标记提换成实际的参数值。

实际的参数值从哪里取呢?我想你早就看出来了: 从paramWrappers这个可变数组中取。
比如上面那行代码中的paramMap和new ParamWrapper()都可以称为参数包装器,
参数包装器可以是一个Map,也可以是一个普通的类。

如果你在调用out方法时不提供任何参数包装器,那么不会对任何占位标记进行替换,
这种情形通常适用于直接发送静态资源文件。

如果你在调用out方法时提供了多个参数包装器,
那么参数值的查找顺序是按调用out方法时参数包装器出现的先后顺序进行的,
比如上例中,在替换${name}这个占位标记时,先从paramMap中查找(类似这样:paramMap.get("name")),
因为name的值已经在
paramMap.put("name",name);
这一行代码中加入了,所以查找成功,不再往下进行。

当替换${time}这个占位标记时,也是先从paramMap中查找(类似这样:paramMap.get("time")),
但是返回null,接着从new ParamWrapper()这个参数包装器中查找,
控制器识别出ParamWrapper类不是一个Map,所以采用反射技术,事先查找是否定义了time()方法,
因为ParamWrapper类没有定义time()方法,所以再查找是否定义了名叫time的实例变量,
最后找到了,time这个实例变量是Date类型,把它转换成字符串后替换${time}这个占位标记,
最后就可以把模板文件的内容发送给浏览器了。


2.4 神奇的Action



2.4.1 程序代码


Java代码 复制代码
  1. //对应apps/SupernaturalAction.java文件   
  2.   
  3. import com.douyu.main.Context;   
  4. import com.douyu.main.Controller;   
  5.   
  6. @Controller  
  7. public class SupernaturalAction {   
  8.     public void index(Context context) {   
  9.         context.out("User.html");   
  10.     }   
  11.   
  12.     public void showUser(User user, java.io.PrintWriter out) {   
  13.         out.println("姓名:"+user.name);   
  14.         out.println("年龄:"+user.age);   
  15.     }   
  16. }  



 

Java代码 复制代码
  1. //对应apps/User.java文件   
  2.   
  3. import com.douyu.main.Form;   
  4. import com.douyu.main.Setter;   
  5.   
  6. @Form  
  7. public class User {   
  8.     String name;   
  9.     int age;   
  10.   
  11.     @Setter  
  12.     public void init(String name, int age) {   
  13.         this.name = name;   
  14.         this.age = age;   
  15.     }   
  16. }  



 

Html代码 复制代码
  1. //对应apps/User.html文件   
  2.   
  3. <HTML>  
  4. <HEAD><TITLE>UserTITLE>HEAD>  
  5. <BODY>  
  6. <FORM METHOD=POST ACTION="SupernaturalAction/showUser">  
  7.     姓名:<INPUT TYPE="text" NAME="name"><br>  
  8.     年龄:<INPUT TYPE="text" NAME="age"><br>  
  9.     <INPUT TYPE="submit">  
  10. FORM>  
  11. BODY>  
  12. HTML>  




2.4.2 运行 SupernaturalAction


打开浏览器,输入 http://localhost:8000/SupernaturalAction
结果类似这样:




填写完表单,点击提交按钮后的结果:




2.4.3 程序代码说明


上面的代码所完成的功能很简单: 先显示一个User表单让你填写,然后输出你填写的内容。
不过这不是我的主要用意,我的意图是想通过这个非常简单的例子向你说明什么是"Action"?

Action其实就是一个方法,如果一个类带有@Controller 那么在这个类中定义的所有public方法
都是一个Action,你可以通过"类名/方法名"这样的uri来访问这个Action,
例如上面apps/User.html文件中的ACTION="SupernaturalAction/showUser"就表示由SupernaturalAction中
的showUser方法来处理表单。

如果你在uri中未指明方法名,比如http://localhost:8000/SupernaturalAction,
那么就会调用默认的index方法,
http://localhost:8000/SupernaturalAction
与 http://localhost:8000/SupernaturalAction/index 是等价的。

如果你不喜欢index这个默认的方法名,而是喜欢main这样的方法名
你可以把 @Controller 改成 @Controller(defaultAction="main")


另外一个很神奇的地方在于方法声明中的参数,
如果你是一个很细心的人你可能早已发现了一件有趣的事,
前面三小节给出的示例中每个Controller类中都有index方法,但是方法的参数是不同的,
再列出来对比一下:

第1个例子是: index(PrintWriter out)
第2个例子是: index(String name, Context context)
第3个例子是: index(Context context)

如果你学习过Sun发明的Servlet技术,你应该对下面这两个方法很熟悉:
doPost(HttpServletRequest req, HttpServletResponse resp)
doGet(HttpServletRequest req, HttpServletResponse resp)


前者的方法参数类型、个数、顺序都是活的,是由正在写程序的人自己定的,
而后者都是死的,是由Sun在规范中事先定好的,你只能去覆盖doPost、doGet。

通俗一点来讲就是:
主动权在我,要由我说的算,我要什么你就得给我什么;
而不是我去顺从你,你有什么我只能用什么。


能达到这样的灵活性要归功于强大的编译器,
编译器在编译源代码时会根据Douyu服务器运行期间的上下文信息推断出你想要做什么?
比如你想要一个PrintWriter,那么就给你一个能向浏览器输出信息的PrintWriter,
你想要一个Context,那么就把代表上下文的Context实例传给你,
你想要获取某一个Http请求参数值(如上面的String name)也同样满足你。


接着再来看看showUser的参数声明:
showUser(User user, java.io.PrintWriter out)

唯一陌生的地方是:User user,
请先回过头去看看apps/User.java文件的内容,
如果你熟悉JavaBeans,那么一定写过一堆的setter/getter方法吧,
User类有点类似JavaBeans,但又不大一样。

如果你给一个类加上 @Form 这个Annotation,然后你把这个类作为Action方法参数的类型,
那么编译器就会推断出你想要把Http请求参数值自动装配到这个类的实例变量中,
接着编译器就会在这样的类中继续寻找所有带有 @Setter 的public方法,
然后解析方法参数,这一步跟解析Action方法参数一样,
但是在这一步会忽略掉所有的带有@Form 类型的其他参数。
(比如,要是像这样:init(String name, int age, User user),那么直接给user变量赋 null 值)


下面的代码片断:

Java代码 复制代码
  1. @Setter  
  2. public void init(String name, int age) {   
  3.     this.name = name;   
  4.     this.age = age;   
  5. }  


就是最关键的地方,
它告诉编译器:把名叫"name"和"age"的参数值准备好,然后调用init。

参数的名称很重要,必须跟html表单中一样,否则会找不到实际的值,只能给你null或者0这样的默认值。

不过,你也不必担心,通常每个表单在实际应用中都对应数据库中的一张表,
Douyu已经考虑到这个问题了,已将模型层和控制层完美结合,
几乎可以实现自动化了(不过不是采用@Setter方式),
所以通常你不必另外写一个对应html表单的类。

2.5 文件上传



2.5.1 程序代码


Java代码 复制代码
  1. //对应apps/FileUpload.java文件   
  2.   
  3. import java.io.File;   
  4. import java.io.PrintWriter;   
  5.   
  6. import com.douyu.main.Context;   
  7. import com.douyu.main.Controller;   
  8.   
  9. import com.douyu.http.UploadedFile;   
  10.   
  11. @Controller  
  12. public class FileUpload {   
  13.     public void index(Context c) {   
  14.         c.out("FileUpload.html");   
  15.     }   
  16.   
  17.     public void upload(UploadedFile[] uploadedFiles, UploadedFile file1,   
  18.         String description, PrintWriter out) {   
  19.            
  20.         out.println("说明: "+description);   
  21.         out.println();   
  22.   
  23.         if(uploadedFiles != null) {   
  24.             for(UploadedFile uf : uploadedFiles) {   
  25.                 //注意这里,file1与uploadedFiles中的某一个元素指向同一个对象   
  26.                 if(file1 == uf) {   
  27.                     out.println("这是文件1:");   
  28.                     out.println();   
  29.                 }   
  30.   
  31.                 out.println("大小  : "+uf.getSize()+" 字节");   
  32.                 out.println("类型  : "+uf.getContentType());   
  33.                 out.println("文件名: "+uf.getSimpleName());   
  34.                 out.println("全名  : "+uf.getFullName());   
  35.                 out.println("路径名: "+uf.getPathName());   
  36.   
  37.                 out.println();   
  38.                 out.println("文件内容:");   
  39.                 out.println("--------------------------------------");   
  40.                 out.println(uf.getContent());   
  41.                 out.println("--------------------------------------");   
  42.   
  43.                 out.println();   
  44.   
  45.                 File file = new File("D:/Douyu/temp/uploadedFiles", uf.getSimpleName());   
  46.   
  47.                 try {   
  48.                     uf.saveTo(file);   
  49.                     out.println("已保存到: "+file);   
  50.                 } catch(Exception e) {   
  51.                     out.println("出错了: "+e);   
  52.                 }   
  53.                 out.println();   
  54.                 out.println();   
  55.             }   
  56.         }   
  57.     }   
  58. }  


 

Html代码 复制代码
  1. //对应apps/FileUpload.html文件   
  2.   
  3. <HTML>  
  4. <HEAD><TITLE>文件上传TITLE>HEAD>  
  5. <BODY>  
  6.     <FORM ENCTYPE="multipart/form-data" METHOD="POST" ACTION="FileUpload/upload">  
  7.     文件1:<INPUT NAME="file1" TYPE="file" size="50"><br>  
  8.     文件2:<INPUT NAME="file2" TYPE="file" size="50"><br>  
  9.   
  10.     说明 :<TEXTAREA NAME="description" ROWS="5" COLS="50">TEXTAREA><br>  
  11.   
  12.     <INPUT TYPE="submit" VALUE="上传">  
  13.     FORM>  
  14. BODY>  
  15. HTML>  



2.5.2 运行 FileUpload

打开浏览器,输入 http://localhost:8000/FileUpload
结果类似这样:




点击"浏览"按钮选择上传的文件,然后再点击"上传"按钮后的结果:




2.5.3 程序代码说明

上面的代码做了什么事应该是一目了然的,唯一陌生的地方就是出现了UploadedFile这么个东西。

UploadedFile实际上是一个接口,在com.douyu.http包中定义,如包名所暗示的那样,
com.douyu.http包中定义的接口或类都是跟http协议有关的,这一节只介绍UploadedFile接口.

UploadedFile接口用来表示一个已经上传到服务器的文件,
这个文件的内容通常是放在内存中或是在某个临时目录中。UploadedFile接口定义的方法并不多,
在上面的例子中已演示了大多数方法的用法,其中最有用的就是saveTo方法了,
调用saveTo方法可以把内存中或临时目录中的文件内容保存到你指定的目录中,
比如上例中就是把所有上传的文件都保存到D:/Douyu/temp/uploadedFiles目录,
并且新文件名跟上传的文件名一样。

当请求处理结束时,JVM会在合适的时间回收上传文件所占用的内存空间。
如果上传文件被放在临时目录中,那么在请求处理结束时也会被自动清除。


注意,出于安全考虑,最好不要将上传的文件保存到Douyu服务器的apps和work目录,
因为这样会导致安全问题,比如,如果别人上传了一个 Evil.java 文件,
Evil.java的代码是用来删除你硬盘上的文件的,接着你把它保存到了apps目录,
那么别人就可以通过 http://localhost:8000/Evil 这样的uri来执行Evil.java文件。

Douyu服务器目前的实现并没有过多的考虑安全问题,
安全问题会在接下来的版本中逐步完善。

2.6 再论Action


看完 2.5节的例子后你可能对Douyu的具体实现更加好奇了,
FileUpload这个控制器没有实现任何接口也没有继承任何类,
FileUpload这个类名也不一定就叫"FileUpload",只要你喜欢你可以取其他名字。

FileUpload定义的各种与Action相关的方法名、方法参数都是由你自由定义的,
比如,你可以把upload这个方法名改成毫无意义的"aaaa",
只要你把apps/FileUpload.html文件中的
ACTION="FileUpload/upload"改成ACTION="FileUpload/aaaa"同样能够正确运行。



接下来再深入一点,看看Douyu内置的编译器是怎样推断你想要它做什么?

重新观察一下2.5节中提到的FileUpload类,它定义了一个upload方法,方法参数如下所示:

Java代码 复制代码
  1. public void upload(UploadedFile[] uploadedFiles, UploadedFile file1,   
  2.         String description, PrintWriter out)  


方法参数的类型分别是:
com.douyu.http.UploadedFile[]
com.douyu.http.UploadedFile
java.lang.String
java.io.PrintWriter

编译器首先对方法参数的类型进行分类,
参数的类型总体上可以分为下面这些:

1) Douyu内置的类型:
如上面的: com.douyu.http.UploadedFile,
以及出现过多次的:com.douyu.main.Context,

还有后面的例子中将会出现的:
com.douyu.http.HttpRequest
com.douyu.http.HttpResponse

这些类型通常是接口类型,用户可以使用Douyu提供的这类接口来完成具体的任务。

2) 带有 @Form 声明的类:
如前面出现的User类就是这种情况

3) int、long、float、double、boolean、byte、char、short这8个基本类型:

4) 与8个基本类型对应的包装类:
java.lang.Integer
java.lang.Long
java.lang.Float
java.lang.Double
java.lang.Boolean
java.lang.Byte
java.lang.Character
java.lang.Short

5) Java平台类库中提供的一些特殊类:
java.lang.String
java.io.PrintWriter

6) 数组类型
目前只支持一维数组,并且数组元素的类型只能是:
java.lang.String 和 com.douyu.http.UploadedFile

7) 动态生成的模型类
这些类是与数据库有关的,通常每一个模型类对应数据库中的一张表,
前面都没有出现过这种类型,这种情况在下面的章节中叙述。


因为每个Action对应的方法都是可以通过uri来调用的,
换句话说,每个Action实际上处理的是Http请求,同时返回一个响应消息.

当你在定义这个Action对应的方法的参数时,
实际上你已经告诉了编译器:你想处理什么样的数据(或参数值),请把数据(或参数值)准备好传给我。

现在的关键问题是:参数值从哪里来,编译器怎样才能正确获取参数值呢?
解决问题的关键在于 参数类型和参数名。

如果参数类型是上面所列的 3)和 4) 以及java.lang.String 和 java.lang.String[](字符串数组),
因为在一个Http请求中可能包含很多参数(如用POST方式提交的表单或用GET方式提交的很多查询参数),
所以编译器不可能仅从参数的类型推断出你想要它做什么,
比如,如果Http请求中包含4个参数:
p1 = 1;
p2 = 2;
p3 = "a";
p4 = "b";

你定义了一个Action:
action1(int a, int b, String c, String d)
到底给参数a传1还是传2呢?参数c传给它字符串"a"还是"b"?
这种情况显然会增加歧义,所以对于这些参数类型,你必须同时用参数名来区分不同的参数。
把上面的Action改成这样才能正确工作:
action1(int p1, int p2, String p3, String p4)

值得一提的是,Http请求中的参数值通常都是字符串类型的,
不过并不需要你进行手工转换了,编译器会跟据你指定的参数类型自动完成转换工作。

如果Action中指定的参数在Http请求参数中找不到或转换失败,
那么将使用默认值,如int、long默认值为0, float为0.0,boolean为false,
基本类型的包装类和String的默认值都为null,
java.lang.String[] 字符串数组类型通常是用来获取Http请求中某一参数的多个取值的,
如果找不到也返回null.



另一种情况不限制Action参数名称的取法,编译器仅根据参数类型就知道你想要做什么,
因为这样的参数类型没有歧义,是唯一的,
比如常见到的:
com.douyu.main.Context
java.io.PrintWriter
以及
com.douyu.http.HttpRequest
com.douyu.http.HttpResponse
还有 2) 和 7)中的自定义类型都是唯一的,你可以随意取名,
比如:可以是 index(Context context) 或者是更简短的 index(Context c)


2.5节的upload方法更有趣,UploadedFile接口实际上也算是Http请求中某种特殊的参数,
它代表了"multipart/form-data"表单中的某一个file元素,
如果你要获取具体的某一个file元素,除了把Action方法的参数类型声明为UploadedFile接口外,
你还必须严格指定参数名称,就像2.5节中出现的"UploadedFile file1"那样,
参数名file1就代表文件1;如果你上传的是多个文件,要是你不在乎每个文件的特殊情况,
那么你可以像2.5节中出现的"UploadedFile[] uploadedFiles"那样,
随意的声明一个UploadedFile数组,数组名称你可以任取,可以是uploadedFiles也可以是files。


总结一个规律就是:
当你要获取Http请求中的某个具体参数值时,
除了在Action方法中明确指明参数类型外,你还必须严格指定参数名称,
其他类型的参数只要编译器能识别,就会正确无误的传给你,
不能识别的类型要么返回默认值要么返回null。


至此,你应该知道静态强类型语言(Java)比动态无类型声明的脚本语言(Ruby、PHP)强大的地方了吧!
静态强类型语言的编译器能在编译源码期间就能收集到很多有用的类型信息,
在很大程度上跟据这些类型信息就能推断出你想做什么,事先就能做好决策,
这使得编译器在控制器层发挥出很大的威力,不必等到运行时才做决策,
另一方面可以大大提高程序运行性能,同时又具有足够的灵活性,
比如将来Douyu提供了更多有用的内置类型或你想在Action中加入更多参数,
直接修改既可,其他使用到这个Action的地方并不需要修改,因为Action是通过uri调用的,
uri中只是引用了Action对应的方法的名称,而与方法的参数无关。

2.7 Http请求、响应、Session、Cookie



2.7.1 程序代码


Java代码 复制代码
  1. //对应apps/HttpInfo.java文件   
  2.   
  3. import java.util.Locale;   
  4. import java.io.PrintWriter;   
  5. import com.douyu.main.Controller;   
  6.   
  7. import com.douyu.http.HttpRequest;   
  8. import com.douyu.http.HttpResponse;   
  9. import com.douyu.http.HttpSession;   
  10. import com.douyu.http.Cookie;   
  11.   
  12. @Controller  
  13. public class HttpInfo {   
  14.     public void index(HttpRequest request, HttpResponse response, PrintWriter out) {   
  15.         printHttpRequestInfo(request, out);   
  16.   
  17.         Cookie[] cookies = request.getCookies();   
  18.         out.println("Cookie信息:");   
  19.         out.println("------------------------------------");   
  20.         //首次请求时为空   
  21.         if(cookies == null) {   
  22.             out.println("cookies = null");   
  23.         } else {   
  24.             for(Cookie c : cookies) {   
  25.                 out.println("Cookie = "+c);   
  26.             }   
  27.         }   
  28.         Cookie cookie = new Cookie("MyCookie","is cool");   
  29.         //10秒钟后过期   
  30.         cookie.setMaxAge(10);   
  31.         response.addCookie(cookie);   
  32.   
  33.         cookie = new Cookie("MyCookie2","is very cool");   
  34.         cookie.setMaxAge(10);   
  35.         response.addCookie(cookie);   
  36.   
  37.         out.println();   
  38.   
  39.   
  40.         out.println("Http Session信息:");   
  41.         out.println("------------------------------------");   
  42.         HttpSession session = request.getSession(true);   
  43.         out.println("session id = "+session.getId());   
  44.         out.println("isNew      = "+session.isNew());   
  45.         Object counter = session.getAttribute("counter");   
  46.         if(counter == null) {   
  47.             out.println("counter    = 0");   
  48.             session.setAttribute("counter",1);   
  49.         } else {   
  50.             out.println("counter    = "+counter);   
  51.             session.setAttribute("counter",((Integer)counter).intValue()+1);   
  52.         }   
  53.     }   
  54.   
  55.     private void printHttpRequestInfo(HttpRequest request, PrintWriter out) {   
  56.         out.println("Http请求参数信息:");   
  57.         out.println("------------------------------------");   
  58.         for(String paramName : request.getParameterNames()) {   
  59.             out.println("参数名: "+paramName+"  参数值: "+ request.getParameter(paramName));   
  60.         }   
  61.         out.println();   
  62.   
  63.         out.println("Accept-Language请求头:");   
  64.         out.println("------------------------------------");   
  65.         for(Locale locale : request.getLocales())   
  66.             out.println("locale = "+locale);   
  67.         out.println();   
  68.   
  69.         out.println("所有请求头:");   
  70.         out.println("------------------------------------");   
  71.         for(String headerName : request.getHeaderNames())   
  72.             out.println(headerName+": "+request.getHeader(headerName));   
  73.   
  74.         out.println();   
  75.         out.println("其他与Http请求有关的信息:");   
  76.         out.println("------------------------------------");   
  77.         out.println("Protocol    = "+request.getProtocol());   
  78.         out.println("Method      = "+request.getMethod());   
  79.         out.println("QueryString = "+request.getQueryString());   
  80.         out.println("ServerName  = "+request.getServerName());   
  81.         out.println("ServerPort  = "+request.getServerPort());   
  82.         out.println("RemoteHost  = "+request.getRemoteHost());   
  83.         out.println("RemoteAddr  = "+request.getRemoteAddr());   
  84.         out.println("RemotePort  = "+request.getRemotePort());   
  85.         out.println("LocalName   = "+request.getLocalName());   
  86.         out.println("LocalAddr   = "+request.getLocalAddr());   
  87.         out.println("LocalPort   = "+request.getLocalPort());   
  88.   
  89.         out.println();   
  90.     }   
  91. }  



2.7.2 运行 HttpInfo


打开浏览器,输入 http://localhost:8000/HttpInfo?p1=123&p2=abc
结果类似这样:




2.7.3 程序代码说明

上面的代码演示了HttpRequest、HttpResponse、HttpSession、Cookie的普遍用法,
如果你熟悉Servlet,那么应该对上面出现的各类方法很熟悉,
这主要是因为HttpRequest接口是从以下两个Servlet规范中定义的接口改造而来的:
javax.servlet.ServletRequest
javax.servlet.http.HttpServletRequest

同样的,HttpResponse接口从下面两个Servlet规范中定义的接口改造而来:
javax.servlet.ServletResponse
javax.servlet.http.HttpServletResponse

另外,HttpSession接口对应 javax.servlet.http.HttpSession接口
而Cookie类对应 javax.servlet.http.Cookie类

除了这些对应关系之外,还删除了很多在Douyu看来没有必要使用的方法,
一些方法的返回值也做了修成,比如原来返回java.util.Enumeration类型的,
现在变成了字符串数组或是java.util.List,这样可以运用for each循环来简化编程。

为什么要仿照Servlet规范中定义的接口呢?
除了能让原来熟悉Servlet的程序员轻松过度到Douyu外,
最主要原因是Http协议是定死的,
实现了Http协议的服务器能向用户提供的有关http的API大多数都是大同小异的,
玩不出什么另类的花样,而Servlet规范中定义的http相关接口算是比较成熟、比较完备了,
所以直接仿照或改造Servlet原有API是很合理的。


Douyu的后续版本还会对com.douyu.http包中定义的接口或类进行完善。

小技巧:
也可以通过com.douyu.main.Context接口中定义的如下方法
来获得HttpRequest、HttpResponse、HttpSession的实现类的实例引用:


Java代码 复制代码
  1. public HttpRequest getHttpRequest();   
  2. public HttpResponse getHttpResponse();   
  3. public HttpSession getHttpSession();   
  4. public HttpSession getHttpSession(boolean create);  

2.8 Douyu的权限模型


按前面各小节的惯例,还是从简单的例子开始。

假定你接到上级任务要开发一个"员工薪水管理模块",要面对这样的需求:
======================================================================

员工分为3类:
employee1
employee2
manager

还有一个特殊的员工hr,你可以把他想像成是公司的最高领导,
由他给下面的员工发钱,只有他才能调整其他员工的薪水。


另外,员工employee1只能查看他自己的薪水,他不能查看别人的薪水,
也不能修改自己的或别人的薪水;

同样,员工employee2也只能查看他自己的薪水,他不能查看别人的薪水,
也不能修改自己的或别人的薪水;

员工manager有点特殊,他除了能查看自己的薪水外还能查看employee1与employee2的薪水,
但是他同样不能修改自己的或别人的薪水。

======================================================================

了解了需求后开始编码吧,
不过为了让代码尽可能的简单,这里不涉及数据库,也不涉及复杂的加密操作,
目的是为了把重心放在Douyu的权限模型上,否则会喧宾夺主。


2.8.1 程序代码


(注: 事先不必过于追究代码实现细节,有个表面印象既可,2.8.3节会有详述)


Html代码 复制代码
  1. //对应apps/permission/Login.html文件   
  2.   
  3. <HTML>  
  4. <HEAD><TITLE>用户登录TITLE>HEAD>  
  5. <BODY>  
  6. 用户登录:   
  7. <FORM METHOD=POST ACTION="/permission.Login/check">  
  8.     用户:<INPUT TYPE="text" NAME="name" VALUE="employee1"><br>  
  9.     密码:<INPUT TYPE="password" NAME="password" VALUE="1"><br>  
  10.     <INPUT TYPE="submit" VALUE="登录"> <INPUT TYPE="reset">  
  11. FORM>  
  12. BODY>  
  13. HTML>  




 

Java代码 复制代码
  1. //对应apps/permission/Login.java文件   
  2.   
  3. package permission;   
  4.   
  5. import java.io.PrintWriter;   
  6. import java.util.HashMap;   
  7.   
  8. import com.douyu.main.Context;   
  9. import com.douyu.main.Controller;   
  10.   
  11. import com.douyu.security.PermissionAction;   
  12. import com.douyu.security.FieldPermission;   
  13. import com.douyu.security.FunctionPermission;   
  14.   
  15. @Controller(checkPermission=false)   
  16. public class Login {   
  17.     //预定义了四个用户及其对应的密码   
  18.     HashMap passwordMap = new HashMap(4);   
  19.     {   
  20.         passwordMap.put("employee1",1);   
  21.         passwordMap.put("employee2",2);   
  22.         passwordMap.put("manager",3);   
  23.         passwordMap.put("hr",4);   
  24.     }   
  25.   
  26.     //首先显示登录页面   
  27.     public void index(Context c) {   
  28.         c.out("permission/Login.html");   
  29.     }   
  30.   
  31.     //接着检查用户名或密码是否正确,   
  32.     //如果检查通过了,再根据用户名来分配不同的权限操作,   
  33.     //最后显示薪水列表页面。   
  34.     public void check(Context c, String name, int password, PrintWriter out) {   
  35.         Integer correctPassword = passwordMap.get(name);   
  36.         if(correctPassword == null || correctPassword.intValue() != password) {   
  37.             out.println("用户名或密码不对");   
  38.             return;   
  39.         }   
  40.   
  41.         //映射关系是: (类名--> (方法名 --> 权限操作))   
  42.         HashMap> permissionActionMap =   
  43.         new HashMap>();   
  44.   
  45.         //映射关系是: 方法名-->权限操作   
  46.         HashMap map =   
  47.             new HashMap();   
  48.   
  49.         //用户employee1只能查看自己的薪水   
  50.         if(name.equals("employee1")) {   
  51.             map.put("employee1_salary_field", FieldPermission.SHOW);   
  52.             map.put("employee2_salary_field", FieldPermission.HIDDEN);   
  53.             map.put("manager_salary_field", FieldPermission.HIDDEN);   
  54.   
  55.             permissionActionMap.put("permission.SalaryForm", map);   
  56.         }   
  57.         //用户employee2也只能查看自己的薪水   
  58.         else if(name.equals("employee2")) {   
  59.             map.put("employee1_salary_field", FieldPermission.HIDDEN);   
  60.             map.put("employee2_salary_field", FieldPermission.SHOW);   
  61.             map.put("manager_salary_field", FieldPermission.HIDDEN);   
  62.   
  63.             permissionActionMap.put("permission.SalaryForm", map);   
  64.         }   
  65.         //用户manager除了能查看自己的薪水外,   
  66.         //还能查看employee1与employee2的薪水,   
  67.         //但是他不能修成自己的或别人的薪水   
  68.         else if(name.equals("manager")) {   
  69.             map.put("employee1_salary_field", FieldPermission.SHOW);   
  70.             map.put("employee2_salary_field", FieldPermission.SHOW);   
  71.             map.put("manager_salary_field", FieldPermission.SHOW);   
  72.   
  73.             permissionActionMap.put("permission.SalaryForm", map);   
  74.         }   
  75.         //hr代表一个更抽象的用户,   
  76.         //他既能查看别人的薪水,也能修改别人的薪水。   
  77.         else if(name.equals("hr")) {   
  78.             map.put("employee1_salary_field", FieldPermission.EDIT);   
  79.             map.put("employee2_salary_field", FieldPermission.EDIT);   
  80.             map.put("manager_salary_field", FieldPermission.EDIT);   
  81.   
  82.             permissionActionMap.put("permission.SalaryForm", map);   
  83.   
  84.             map = new HashMap();   
  85.             map.put("modifySalary", FunctionPermission.ALLOW);   
  86.             permissionActionMap.put("permission.SalaryController", map);   
  87.         }   
  88.   
  89.         //把有关权限操作的信息存储到运行期间的上下文环境中   
  90.         c.setPermissionActionMap(permissionActionMap);   
  91.            
  92.         //显示薪水列表页面   
  93.         c.out("permission/SalaryForm.html"new SalaryForm());   
  94.     }   
  95. }  



 

Html代码 复制代码
  1. //对应apps/permission/SalaryForm.html文件   
  2.   
  3. <HTML><HEAD><TITLE>公司员工薪水列表TITLE>  
  4. HEAD>  
  5. <BODY>  
  6. ${message}   
  7.   
  8. 公司员工薪水列表 ( 单位: RMB/月 )   
  9. <FORM METHOD=POST ACTION="/permission.SalaryController/modifySalary">  
  10. <TABLE border=1>  
  11.   
  12. <TR>  
  13.     <TD>employee1:<INPUT TYPE="text" NAME="employee1_salary" VALUE="${employee1_salary}" ${employee1_salary_disabled}>TD>  
  14. TR>  
  15.   
  16. <TR>  
  17.     <TD>employee2:<INPUT TYPE="text" NAME="employee2_salary" VALUE="${employee2_salary}" ${employee2_salary_disabled}>TD>  
  18. TR>  
  19.   
  20. <TR>  
  21.     <TD>manager  :<INPUT TYPE="text" NAME="manager_salary" VALUE="${manager_salary}" ${manager_salary_disabled}>TD>  
  22. TR>  
  23.   
  24. TABLE>  
  25. <INPUT TYPE="submit" value="修改薪水" ${modify_salary_disabled}>  
  26. <INPUT TYPE="reset">  
  27. FORM>  
  28.   
  29. <FORM METHOD=POST ACTION="/permission.Logout">  
  30. <INPUT TYPE="submit" value="退出">  
  31. FORM>  
  32. BODY>  
  33. HTML>  



 

Java代码 复制代码
  1. //对应apps/permission/SalaryForm.java文件   
  2.   
  3. package permission;   
  4.   
  5. import com.douyu.main.Form;   
  6. import com.douyu.main.Setter;   
  7. import com.douyu.main.Guard;   
  8.   
  9. import com.douyu.security.PermissionAction;   
  10. import com.douyu.security.FieldPermission;   
  11. import com.douyu.security.FunctionPermission;   
  12.   
  13. //这个类与公司员工薪水列表对应,   
  14. //你也可以把它看成是数据库中的一张员工薪水表,   
  15. //用来存放每类员工的薪水.   
  16. @Form  
  17. public class SalaryForm {   
  18.     String employee1_salary = "5000";   
  19.     String employee1_salary_disabled = ""//""表示可以编辑employee1_salary字段   
  20.   
  21.     String employee2_salary = "6000";   
  22.     String employee2_salary_disabled = "";   
  23.        
  24.     String manager_salary = "10000";   
  25.     String manager_salary_disabled = "";   
  26.   
  27.     //SalaryForm类可以作为任何Action方法参数的类型,   
  28.     //通过这个Setter方法就可以自动生成一个SalaryForm类的实例。   
  29.     @Setter  
  30.     public void init(String employee1_salary,   
  31.         String employee2_salary, String manager_salary) {   
  32.   
  33.         if(employee1_salary != null)    
  34.             this.employee1_salary = employee1_salary;   
  35.         if(employee2_salary != null)    
  36.             this.employee2_salary = employee2_salary;   
  37.         if(manager_salary != null)    
  38.             this.manager_salary = manager_salary;   
  39.     }   
  40.   
  41.     //对应员工薪水表单中的employee1_salary字段   
  42.     @Guard  
  43.     public void employee1_salary_field(PermissionAction pa) {   
  44.         if(pa != FieldPermission.EDIT) {   
  45.             employee1_salary_disabled = "disabled";   
  46.         }   
  47.            
  48.         //没有为employee1_salary字段赋权限(pa=null)或权限为隐藏时,   
  49.         //都不允许查看用户employee1的薪水   
  50.         if(pa == null || pa == FieldPermission.HIDDEN) {   
  51.             employee1_salary = "";   
  52.         }   
  53.     }   
  54.   
  55.     //对应员工薪水表单中的employee2_salary字段   
  56.     @Guard  
  57.     public void employee2_salary_field(PermissionAction pa) {   
  58.         if(pa != FieldPermission.EDIT) {   
  59.             employee2_salary_disabled = "disabled";   
  60.         }   
  61.   
  62.         if(pa == null || pa == FieldPermission.HIDDEN) {   
  63.             employee2_salary = "";   
  64.         }   
  65.     }   
  66.   
  67.     //对应员工薪水表单中的manage_salary字段   
  68.     @Guard  
  69.     public void manager_salary_field(PermissionAction pa) {   
  70.         if(pa != FieldPermission.EDIT) {   
  71.             manager_salary_disabled = "disabled";   
  72.         }   
  73.   
  74.         if(pa == null || pa == FieldPermission.HIDDEN) {   
  75.             manager_salary = "";   
  76.         }   
  77.     }   
  78.        
  79.     //对应员工薪水表单中的"修改薪水"按钮,   
  80.     //同时与SalaryController控制器类的"modifySalary"方法关联,   
  81.     //按钮的权限操作类型由对应的控制器类的Action方法决定,   
  82.     //如果不能调用Action方法也就意味着不能点击按钮或按钮将被隐藏(这取决于你的设计选择)   
  83.     @Guard(   
  84.         controller = SalaryController.class,   
  85.         controllerAction = "modifySalary"  
  86.     )   
  87.     public void modify_salary_button(PermissionAction pa) {   
  88.         if(pa != FunctionPermission.ALLOW)   
  89.             modify_salary_disabled = "disabled";   
  90.     }   
  91.   
  92.     String modify_salary_disabled = "";   
  93.   
  94.     String message = ""//比如类似"薪水已修改"这样的提示消息   
  95. }  



 

Java代码 复制代码
  1. //对应apps/permission/SalaryController.java文件   
  2.   
  3. package permission;   
  4.   
  5. import com.douyu.main.Context;   
  6. import com.douyu.main.Controller;   
  7.   
  8. @Controller  
  9. public class SalaryController {   
  10.   
  11.     public void modifySalary(Context c, SalaryForm sf) {   
  12.         sf.message = "薪水已修改

    "
    ;   
  13.         c.out("permission/SalaryForm.html", sf);   
  14.     }   
  15. }  



 

Java代码 复制代码
  1. //对应apps/permission/Logout.java文件   
  2.   
  3. package permission;   
  4.   
  5. import com.douyu.main.Context;   
  6. import com.douyu.main.Controller;   
  7.   
  8. @Controller(checkPermission=false)   
  9. public class Logout {   
  10.     public void index(Context c) {   
  11.         //在运行期间的上下文环境中把有关权限操作的信息清除   
  12.         c.setPermissionActionMap(null);   
  13.         //重新显示登录页面   
  14.         c.out("permission/Login.html");   
  15.     }   
  16. }  




2.8.2 运行


如果Douyu服务器正在运行,请先连按两次 Ctrl+C 键停止服务器,
然后打开conf/server.java文件,
将配置参数checkPermission设为 checkPermission = true,
这样就启用Douyu服务器的权限管理模块了,
保存文件后再重新启动服务器。
(注:目前暂时不支持"修改服务器配置文件后自动装载"这样的功能,你必须手工重新启动服务器)


打开浏览器,输入 http://localhost:8000/permission.Login
结果类似这样:



为了方便测试,默认情况下是用户employee1,点"登录"按钮后如下:


从上图可以看到,employee1只能查看自己的薪水,
并且所有文本框都是不可编辑的,"修改薪水"按钮是无效的,
接着点"退出"按钮回到登录页面,
用户换成employee2,密码是2,点"登录"按钮后如下:


从上图可以看到,employee2也只能查看自己的薪水,
并且所有文本框都是不可编辑的,"修改薪水"按钮是无效的,
接着点"退出"按钮回到登录页面,
用户换成manager,密码是3,点"登录"按钮后如下:


从上图可以看到,manager能看到他自己的薪水,也能看到employee1与employee2的薪水,
但是所有文本框都是不可编辑的,"修改薪水"按钮也是无效的,
接着点"退出"按钮回到登录页面,
用户换成hr,密码是4,点"登录"按钮后如下:


从上图可以看到,hr能看到manager、employee1与employee2的薪水,
现在所有文本框都是可编辑的了,"修改薪水"按钮也可以点击了,
随意修改任何一个文本框中的值,比如把manager的薪水改为12000,
点"修改薪水"按钮后如下:



接着点"退出"按钮回到登录页面,

现在在浏览器中尝试输入:
http://localhost:8000/permission.SalaryController/modifySalary
将会得到一个403(禁止访问)错误,如下图所示:


因为用户hr已经退出了,即使他有"修改薪水"的权限也不能在没有登录或退出时修改薪水。

在浏览器中重新输入 http://localhost:8000/permission.Login
用户换成manager,密码是3,点"登录"按钮,登录成功后,先别点"退出"按钮,
先尝试在浏览器输入:
http://localhost:8000/permission.SalaryController/modifySalary?manager_salary=100000
结果还是一样,用户manager会收到403(禁止访问)错误信息,虽然用户manager已经登录成功了,
但是他没有"修改薪水"的权限,系统不允许他绕过表单界面"修改薪水"。



2.8.3 程序代码说明


2.8.3.1 最基本的权限分类

先抛开具体的业务逻辑(或业务规则),怎样才能控制一个对象的行为呢?
对象是一个很宽泛的概念,比如上例中出现的薪水表单和薪水控制器都可以称为一个对象。
从Java语言的角度来看,对象本质上就是某个Java类在运行期间生成的实例,
而一个类通常由方法和字段组成(这里的"方法"特指静态方法和非静态方法,"字段"特指实例变量和类变量),
只要控制了方法的调用权限和字段的获取更新权限也就在总体上控制了对象的行为。

方法的调用权限、字段的获取更新权限就是从Java语言层面抽取出来的最基本单位,
Douyu的权限模型为此分别建立了两个类:

com.douyu.security.FunctionPermission;
com.douyu.security.FieldPermission;

为什么不叫MethodPermission而叫FunctionPermission呢?
因为"方法"也可以叫做"函数",一个"方法"通常实现了某类功能,
比如上例中的SalaryController.modifySalary方法就实现了修改员工薪水的功能,
所以"Function"比"Method"更具有实际意义,也更容易理解。

FunctionPermission与FieldPermission都是 com.douyu.security.Permission 这个抽象类的子类,
com.douyu.security.Permission 目前并没有什么特别的地方。



2.8.3.2 权限操作

权限只是一个更高级别的概念,还需要一个"权限操作"的概念来进一步细化,
比如可以把SalaryController.modifySalary方法抽象成一个FunctionPermission(功能权限),
要么允许你调用这个方法,要么禁止你调用这个方法,
所以功能权限的"权限操作"类型只能分为两类: 允许 和 禁止。

类似的,上例中出现的:
employee1_salary_field
employee2_salary_field
manager_salary_field
都可以抽象成一个FieldPermission(字段权限),
用户employee1、employee2都只能查看他们自己的薪水,
用户manager可以查自己的和别人的薪水,
而hr可以同时查看和修改别人的薪水。

这就意味着字段权限的"权限操作"类型不是固定的,
可以是可查看的,也可以是可修改的,还可以隐藏,当然还有我没想到的其他分类。

在Douyu的权限模型中定义了一个叫 com.douyu.security.PermissionAction 的接口来对"权限操作"进行建模。
PermissionAction这个名字来源与Java平台类库中的抽象类: java.security.Permission,
java.security.Permission包含了String getActions()方法,
并且每个java.security.Permission子类都有零个或多个权限操作类型。

com.douyu.security.PermissionAction接口很简单,
目前只定义了两个方法:

Java代码 复制代码
  1. public String getLabel();   
  2. public String getName();  


Label和Name通常都是一样的,除非你要提供不同的本地化信息时Label才有必要跟Name不同,
比如一个字段权限定义了一个叫做"编辑"的权限操作类型,
那么你可以为"编辑"这样的权限操作类型定义一个类来实现PermissionAction接口,

Java代码 复制代码
  1. public class EditPA implements com.douyu.security.PermissionAction {   
  2.     public String getLabel() {   
  3.         return "编辑";   
  4.     }   
  5.     public String getName() {   
  6.         return "edit";   
  7.     }   
  8. }  



2.8.3.3 Douyu的权限模型中内置的权限操作类型


在com.douyu.security.FunctionPermission类中定义了两个权限操作类型:

FunctionPermission.ALLOW
FunctionPermission.FORBID

其中的FunctionPermission.ALLOW已在上面的Login.check方法的第四个if语句中用到了,
如下:

Java代码 复制代码
  1. else if(name.equals("hr")) {   
  2.     .......   
  3.     map = new HashMap();   
  4.     map.put("modifySalary", FunctionPermission.ALLOW);   
  5.     .......   
  6. }  


意思就是允许用户hr调用modifySalary方法,
如果没有明确说明是否允许调用某一个方法,那么实际的语义就是不允许,
比如Login.check方法的前3个if语句都没有显示调用:
map.put("modifySalary", FunctionPermission.ALLOW)或
map.put("modifySalary", FunctionPermission.FORBID);
那么实际上等于:map.put("modifySalary", FunctionPermission.FORBID),
不过Douyu的内部实现是把null当成FunctionPermission.FORBID看待的,
当Douyu的内部实现调用map.get("modifySalary")时,
如果返回null或FunctionPermission.FORBID,寻么将禁止调用modifySalary方法,
只有明确返回FunctionPermission.ALLOW时才允许调用modifySalary方法。


特别值得注意的是,FunctionPermission类定义的两个权限操作类型ALLOW与FORBID目前是固定的,
你不能替换掉他们,比如你定义了一个新的叫"MyPermissionAction"的权限操作类型,
你不能用它来控制某一个功能权限是否被允许,比如像这样:
map.put("modifySalary", MyPermissionAction)
虽然你的MyPermissionAction正确实现了PermissionAction接口,但是上面的代码就不起作用。

当然,以后的Douyu实现版本可能会去掉这个限制,不过目前看不出有多大必要,
因为一个方法通常只可能包含两种权限操作类型:允许 和 禁止。



在com.douyu.security.FieldPermission类中预定义了三个权限操作类型:

FieldPermission.EDIT
FieldPermission.SHOW
FieldPermission.HIDDEN

光从字面看就很容易理解了,EDIT除了可查看外,还能编辑修改,SHOW只能查看,HIDDEN表示不能查看。
当然,你可以扩充其他的权限操作类型,你也完全可以忽略上面的三个预定义类型,
不过,当你使用了自己定义的类型时最好把它声明为"public static final"的类型常量,
这样不但节省了内存空间,你还可以直接使用"=="或"!="号来判断两个PermissionAction接口实例是否相等。
上面例子中的SalaryForm类就大量使用了"=="或"!="号。


2.8.3.4 方法名与权限操作类型的映射

在上面的Login.check方法中你可能注意到了,用到了两级 HashMap:

Java代码 复制代码
  1. HashMap> permissionActionMap =   
  2.         new HashMap>();  


最内层是: HashMap
以方法名为Key,以具体的权限操作类型常量为值,如下所示:

Java代码 复制代码
  1. map.put("employee1_salary_field", FieldPermission.SHOW);   
  2. map.put("employee2_salary_field", FieldPermission.HIDDEN);   
  3. map.put("manager_salary_field", FieldPermission.HIDDEN);  


值得注意的是employee1_salary_field、employee2_salary_field与manager_salary_field
并不是SalaryForm类中真正的字段(真正的字段是:employee1_salary、employee2_salary与manager_salary)
实际上他们是SalaryForm类中的一种特殊方法,
这些方法标有@Guard, 而且只有一个参数"PermissionAction pa",
这两个特征就告诉了编译器这一类方法需要特别对待,@Guard 就像一个尽职的守卫,
他在小心的照看着一件宝物(就是那个真正的字段),
当你要使用这件宝物时你必需亮出你的证件(PermissionAction),
如果你是国王才会给你真品,只是一般官员给件赝品看看就行了,要是来打劫的直接枪毙。

通过这样的抽象,对任何字段的存取修改权限都可以通过定义如下类型的方法来实现:

Java代码 复制代码
  1. @Guard  
  2. public 返回类型 方法名(PermissionAction 参数名) {   
  3.     //检查权限操作类型的代码放在这里   
  4. }  


其中@Guard、public、PermissionAction是必需的,其他地方没有具体要求。

解决了字段权限问题后,就来看看如何确定功能权限。

控制器类中的所有Action对应的方法都可以抽象成一个功能权限,
比如上例中的SalaryController.modifySalary方法就是一个功能权限。

不过光有@Guard方法名和Action方法名还不足以区分不同的权限操作,
因为不同的两个类可能定义了同名的方法,解决问题的办法是引入类名,
这样就产生了两级映射:(类名--> (方法名 --> 权限操作))

Login.check方法中的如下代码完美演示了这样的映射关系:

Java代码 复制代码
  1. else if(name.equals("hr")) {   
  2.     map.put("employee1_salary_field", FieldPermission.EDIT);   
  3.     map.put("employee2_salary_field", FieldPermission.EDIT);   
  4.     map.put("manager_salary_field", FieldPermission.EDIT);   
  5.   
  6.     permissionActionMap.put("permission.SalaryForm", map);   
  7.   
  8.     map = new HashMap();   
  9.     map.put("modifySalary", FunctionPermission.ALLOW);   
  10.     permissionActionMap.put("permission.SalaryController", map);   
  11. }  


else if块中的代码第一部份是给用户hr分配permission.SalaryForm类中的字段权限,
而第二部份是给用户hr分配permission.SalaryController类中的功能权限。


2.8.3.5 联姻Action与表单按钮(或其他能激活Action的元素)

先把apps/permission/SalaryForm.java文件中的modify_salary_button方法注释掉

Java代码 复制代码
  1. /*  
  2. @Guard(  
  3.     controller = SalaryController.class,  
  4.     controllerAction = "modifySalary"  
  5. )  
  6. public void modify_salary_button(PermissionAction pa) {  
  7.     if(pa != FunctionPermission.ALLOW)  
  8.         modify_salary_disabled = "disabled";  
  9. }  
  10. */  


在浏览器中输入 http://localhost:8000/permission.Login
直接点"登录"按钮后如下图:


这时"修改薪水"按钮不是灰色了,
点击一下它你将会得到一个403(禁止访问)错误。

还好你已不是第一次使用这个"系统",你肯定已知道employee1没有权限修改薪水,
这都可以理解。如果你在正式的商业产品中也采用了这样的方式,
你的很多客户很可能会向你报告Bug:"我点了修改按钮怎么老是出来个奇怪的页面?",
然后你得苦口婆心的解释这不是个Bug,
把责任全推给一个虚拟的员工:"这是XXX写的蹩脚的代码"。

显然,这样的界面设计并不友好,给用户带来了糟糕的用户体验,
更得体的做法是:要控制的按钮或其他元素如果在隐藏后不影响界面布局,
那么就尽可能的隐藏掉,否则就把它禁用(比如变成灰色)。


Html界面中的按钮或链接通常都会触发一个Action,
在Douyu的权限模型中很容易将这两者关联起来,
例如SalaryForm类中的modify_salary_button方法:

Java代码 复制代码
  1. @Guard(   
  2.     controller = SalaryController.class,   
  3.     controllerAction = "modifySalary"  
  4. )   
  5. public void modify_salary_button(PermissionAction pa) {   
  6.     if(pa != FunctionPermission.ALLOW)   
  7.         modify_salary_disabled = "disabled";   
  8. }  


同样是采用@Guard 来标识,只不过比字段权限多了两个属性参数,
属性controller用来指向一个java.lang.Class实例,
这个Class实例表示一个具体的控制器类。
属性controllerAction是字符串类型,表示对应的控制器类中的某个Action方法名。

如果有很多个这样的@Guard按钮都是关联到同一个控制器类,只是Action方法名不同,
那么可以在@Form中指定属性controller的值,
这样如果一个@Guard按钮没有指定属性controller,那么将使用@Form中的值
下面的伪代码演示了基本的用法:

Java代码 复制代码
  1. @Form(controller = MyController.class)   
  2. public class MyForm {   
  3.     @Guard(controllerAction = "action1")   
  4.     public void button1(PermissionAction pa) {   
  5.     }   
  6.   
  7.     @Guard(controllerAction = "action2")   
  8.     public void button2(PermissionAction pa) {   
  9.     }   
  10. }  


另外,@Guard按钮对应的方法名不必放在"(方法名 --> 权限操作)"映射中,
你无需显示为@Guard按钮分配权限,它会自动获取与之相关的Action的权限,
比如在上面的例子中,只有用户hr能修改薪水,
也就是说只有用户hr能调用SalaryController类的modifySalary方法,
而modifySalary方法与SalaryForm类中的modify_salary_button相关联,
所以用户hr进入公司员工薪水列表界面时"修改薪水"按钮是有效的,
而其他用户进入时"修改薪水"按钮都变成了灰色。


2.8.3.6 checkPermission开关


服务器配置文件conf/server.java,
@Form 与 @Guard
@Controller 与 从未出现过的com.douyu.main.Action(也是一个Annotation,用来标注Action方法)
都可以设置checkPermission参数,
在conf/server.java中设置的checkPermission是总开关,如果它为true就启用权限模块,
否则就禁用权限模块,
@Form与@Controller的checkPermission是第二级开关,把它们的checkPermission设为false时,
将不会对它们所包含的@Guard或@Action进行权限检查,

@Guard或@Action的checkPermission是最后一级开关,只有前两级开关都开通后它们才有效。

比如前面的例子中的Login和Logout这两个类的@Controller的checkPermission都是false,
这样就不进行权限检查了,任何人都可以调用它们的Action方法,这正是实际情况所需要的,
因为任何人在获得其他权限前必需首先获得登录权限。


2.8.3.7 com.douyu.main.Context接口中与权限有关的方法


Java代码 复制代码
  1. //这个方法在Login.check方法中已使用过了,   
  2. //就是在上下文中保存 "(类名--> (方法名 --> 权限操作))"这样的映射关系   
  3. public void setPermissionActionMap(   
  4.     Mapextends Map> permissionActionMap);   
  5.   
  6. //这个方法主要是提供给Douyu的内部实现使用的,   
  7. //不过用户有需要时也可用它来获取之前设置过的信息   
  8. public Mapextends Map> getPermissionActionMap();  




2.8.4 还可以走得更远

虽然2.8.3节给出的例子过于简单了,甚至可以说不具有实际意义,
不过它还是恰到好处的展示了Douyu权限模型的核心理念:基于字段和方法实现基本的权限模型。

Douyu的权限模型并不直接与角色、用户、用户组、资源分类等等这些常见的概念挂钩,
也不依赖于特定的数据库表,但是并不妨碍你把Douyu的权限模型对应到传统的权限模型上。

比如你可以建一张类名表,用来保存与@Form、@Controller相关的类名,
然后建一张方法名称表用来保存与@Guard或@Action相关的方法名(不同类的同名方法都可存放在这里),
再建一张很小的权限操作类型表。

从这三张表中各取一条记录就组合成了一个"(类名--> (方法名 --> 权限操作))"映射,
每个映射既可以分配给角色也可以分配给用户或用户组,
当你用具体的用户登录系统时,如果用户属于某个用户组,这个用户组被赋予了某种角色,
那么这个用户最后的权限就是三种权限的并集:
用户最后的权限 = 用户自己特有的权限 + 用户所属用户组的权限
+ 用户所属用户组的角色的权限

最繁琐的地方在于如何制作一个权限配置界面?权限的粒度如何把握?
通常来讲功能权限是比较粗的一级,而字段权限是比较细的一级,
功能权限在正式的产品项目中都是基本的配备,而字段权限并不多见,
只在一些信息很敏感的表单中会设置字段权限,否则要是每张表每个字段都设置的话,
权限配置工作会是一件很累人的差事。

com.douyu.main.Context接口中还定义了两个方法:

Java代码 复制代码
  1. public List getPermissionList();   
  2. public Map getPermissionMap(); //以类名为key  


当你调用其中任何一个方法时都会启动编译器对apps目录中的所有Java源文件进行分析,
然后抽取出与@Form、@Controller相关的类名及与@Guard或@Action相关的方法名,
并生成下面的四个Permission子类的实例:

Java代码 复制代码
  1. com.douyu.security.FormPermission   
  2. com.douyu.security.ControllerPermission   
  3. com.douyu.security.FieldPermission   
  4. com.douyu.security.FunctionPermission  


FormPermission类的getFieldPermissionList()方法可以获得它所包含的FieldPermission
ControllerPermission类的getFunctionPermissionList()方法可以获得它所包含的FunctionPermission

另外,@Form、@Controller与@Guard、@Action都有label和name属性,
如果有需要你可以设置它们,这两个属性可能在生成权限配置菜单时有用,
比如如果权限配置菜单采用下拉框的形式,那么label可以作为前台的文本显示,而name是后台的实际值。
name属性通常没必要指定,目前的内部实现中未使用这个name属性值。

com.douyu.security.Permission类定义了两个方法

Java代码 复制代码
  1. public String getLabel(); //获得label属性值,未设置时要么是简单类名(不含包名)要么是方法名   
  2. public String getName();  //要么是类名(含包名)要么是方法名(取决于Permission的类型)  



下面是一个综合演示例子:

Java代码 复制代码
  1. //对应apps/permission/PermissionList.java文件   
  2.   
  3. package permission;   
  4.   
  5. import com.douyu.main.*;   
  6. import com.douyu.security.*;   
  7.   
  8. import java.io.PrintWriter;   
  9.   
  10. @Controller(label="权限列表",checkPermission=false)   
  11. public class PermissionList {   
  12.     @Action(label="打印权限列表")   
  13.     public void index(Context c, PrintWriter out) {   
  14.         for(Permission p : c.getPermissionList()) {   
  15.             out.println(p);   
  16.   
  17.             if(p instanceof FormPermission)   
  18.                 for(Permission f: ((FormPermission)p).getFieldPermissionList())   
  19.                     out.println("/t"+f);   
  20.             else if(p instanceof ControllerPermission)   
  21.                 for(Permission f: ((ControllerPermission)p).getFunctionPermissionList())   
  22.                     out.println("/t"+f);   
  23.   
  24.             out.println();   
  25.         }   
  26.     }   
  27. }  


在浏览器中输入:
http://localhost:8000/permission.PermissionList

可以看到这样的结果:

Java代码 复制代码
  1. com.douyu.security.ControllerPermission[label=WhatTime, name=WhatTime]   
  2.     com.douyu.security.FunctionPermission[label=index, name=index]   
  3.   
  4. com.douyu.security.ControllerPermission[label=SupernaturalAction, name=SupernaturalAction]   
  5.     com.douyu.security.FunctionPermission[label=index, name=index]   
  6.     com.douyu.security.FunctionPermission[label=showUser, name=showUser]   
  7.   
  8. com.douyu.security.FormPermission[label=SalaryForm, name=permission.SalaryForm]   
  9.     com.douyu.security.FieldPermission[label=employee1_salary_field, name=employee1_salary_field]   
  10.     com.douyu.security.FieldPermission[label=employee2_salary_field, name=employee2_salary_field]   
  11.     com.douyu.security.FieldPermission[label=manager_salary_field, name=manager_salary_field]   
  12.     com.douyu.security.FieldPermission[label=modify_salary_button, name=modify_salary_button]   
  13.   
  14. com.douyu.security.ControllerPermission[label=SalaryController, name=permission.SalaryController]   
  15.     com.douyu.security.FunctionPermission[label=modifySalary, name=modifySalary]   
  16.   
  17. com.douyu.security.ControllerPermission[label=权限列表, name=permission.PermissionList]   
  18.     com.douyu.security.FunctionPermission[label=打印权限列表, name=index]   
  19.   
  20. com.douyu.security.ControllerPermission[label=Logout, name=permission.Logout]   
  21.     com.douyu.security.FunctionPermission[label=index, name=index]   
  22.   
  23. com.douyu.security.ControllerPermission[label=Login, name=permission.Login]   
  24.     com.douyu.security.FunctionPermission[label=index, name=index]   
  25.     com.douyu.security.FunctionPermission[label=check, name=check]   
  26.   
  27. com.douyu.security.ControllerPermission[label=HttpInfo, name=HttpInfo]   
  28.     com.douyu.security.FunctionPermission[label=index, name=index]   
  29.   
  30. com.douyu.security.ControllerPermission[label=HelloWorld, name=HelloWorld]   
  31.     com.douyu.security.FunctionPermission[label=index, name=index]   
  32.   
  33. com.douyu.security.ControllerPermission[label=FileUpload, name=FileUpload]   
  34.     com.douyu.security.FunctionPermission[label=index, name=index]   
  35.     com.douyu.security.FunctionPermission[label=upload, name=upload]  




特别提醒:
public List getPermissionList();
public Map getPermissionMap();
这两个方法是很耗时的操作,不应该在软件正式运行时调用,
通常只在开发最后阶段为了辅助生成权限菜单时使用,
通过遍历List,可以获得类名或方法名,运用这些信息你可以做些有用的事,
比如生成批量的sql Insert语句,然后把这些Insert语句导入数据库中。
2.9 ORM能达到多大程度的自动化?


2.9.1 Douyu的ORM


Douyu的ORM(对象/关系映射)不同于Hibernate、Ibatis、Rails的ActiveRecord,
使用Douyu的ORM,你无需编写任何模型类。

Douyu的ORM比Rails的ActiveRecord还要简单,Rails的ActiveRecord虽然号称是业界最简单的ORM框架了,
但是你还是得写一个模型类去继承ActiveRecord::Base,它还是需要你去声明表间的关联关系,
那些恼人的has_one, has_many......很容易把你搞得一団浆糊,
如果你有Hibernate的使用经验,编写过xml格式的表间关联关系映射,那更是一场噩梦。

Douyu认为: 只要关系数据库还存在,
只要Oracle、DB2、SQL Server、MySQL这类主流的关系数据库还占据统治地位的时候,
只要你的系统用到了关系数据库,那么"以关系数据库为中心的设计思想"并不过时,
反而更容易让ORM走上自动化的道路。

"自动化"这个概念不同人有不同的理解,
ORM自动化到底是个什么东西? ORM能达到多大程度的自动化?


先来回答第一个问题: ORM自动化到底是个什么东西?

====================================================================
因为Douyu是用Java语言开发的,Douyu的ORM是基于JDBC规范来实现的,
在Douyu看来,要实现ORM自动化,首先必需把与数据库打交道的那一层(JDBC API)屏蔽掉,
接着应用服务器或其他ORM框架在运行时应该维护一个抽象的模型层,
模型层维护了很多模型类,这些模型类对应数据库中的表(或者数据库包含的其他对象(如视图、存储过程)),
并且这些模型类是根据JDBC API提供的元数据自动生成的,开发人员无需关心模型类的生成细节,
模型层还必需支持跨数据库的事务、必需自动维护表间的关联关系,必需自动维护表中定义的各种约束。

开发人员使用模型层提供的模型类就像是使用自己编写的模型类一样,
直接使用import语句来导入模型层提供的模型类。
====================================================================


给出了"ORM自动化"的概念后,再来解释:

为什么说"以关系数据库为中心的设计思想"并不过时,反而更容易让ORM走上自动化的道路?

现在的流行趋势是把"以关系数据库为中心的设计思想"当成是一种过时的设计思想,往往对之嗤之以鼻,
认为先按面向对象思想设计出模型类然后再生成关系表更加合理。
其实这是本未倒置,一旦模型类由开发人员编写,那就变成了一种手工劳动,根本就谈不上自动化,
而且也不利于开发人员与DBA的分工协作;再者用Java语言编写模型类比用SQL写建表脚本更复杂,
比如你用程序语言如何描述字段是否为null,如果字段是数字类型,如何用程序语言描述精度和刻度,
当然还有很多类似这样的字段约束,虽然程序语言都可以实现这样的功能,
但是实现起来不会比专用的SQL语句简单。

另外这种先设计模型类再生成关系表的设计思想也不能充分利用关系数据库的专有特性,
比如在Oracle中我想建个表,这个表必须放到不同的表空间中,
还需要设置不同的存储参数以及块空间管理参数。
这些事通常是DBA擅长的,开发人员没有精力、也没有时间去学习不同数据库的专有特性。

面向对象的设计思想跟关系模型也不同,关系模型比面向对象的设计思想更简单,
两个表是否有关系,只要一个主键加一个外键就能说清楚了,
而用面向对象的设计思想设计出来的类你必须理清所有的继承关系、实现了哪些接口,
然后再借助复杂的ORM框架分析你的类结构,最后才生成SQL语句。

如果我们回归到当初,拥抱"以关系数据库为中心的设计思想",
那么让DBA折腾数据库去吧,开发人员先来喝杯咖啡,坐等开工。
(当然,如果你也是数据库高手,你可以让老板追加薪水,因为你把DBA的活也干了)

当数据库表设计完后,就可以充分利用Douyu这样的新型开发平台提供的服务了,
Douyu在运行期间维护一个模型层,
开发人员可以在自已编写的任何源代码中直接引用模型层提供的模型类,
也无需事先编译源代码,刷新一下浏览器就能查看运行结果。


如果你对这样的解释还不满意,那么接下来的章节就是为你准备的,
同时也间接回答了这个问题: ORM能达到多大程度的自动化?





2.9.2 基本的单表CRUD操作


2.9.2.1 程序代码


Java代码 复制代码
  1. //对应apps/sql/Crud.java文件   
  2.   
  3. package sql;   
  4.   
  5. import com.douyu.main.Context;   
  6. import com.douyu.main.Controller;   
  7.   
  8. import com.douyu.sql.Rows;   
  9.   
  10. import sql.table.mysql.Pet;   
  11.   
  12. @Controller  
  13. public class Crud {   
  14.     public void index(Context c, java.io.PrintWriter out) {   
  15.         int middle = 0;   
  16.         out.println("增加5条宠物");   
  17.         for(int i=1; i<=5; i++) {   
  18.             Pet pet = new Pet();   
  19.             pet.name("pet"+i);   
  20.             pet.age(i);   
  21.             c.insert(pet);   
  22.   
  23.             if(i == 3) middle = pet.id(); //id值自动获取   
  24.             out.println("insert pet: id = "+pet.id()+" name = "+pet.name()+" age = "+pet.age());   
  25.         }   
  26.   
  27.         out.println();   
  28.   
  29.         out.println("查找中间那条宠物");   
  30.         Pet pet = new Pet();   
  31.         pet.id(middle);   
  32.         c.select(pet);   
  33.         out.println("select pet: id = "+pet.id()+" name = "+pet.name()+" age = "+pet.age());   
  34.         out.println();   
  35.   
  36.   
  37.         out.println("更新中间那条宠物的名子");   
  38.         pet.name("MyPet");   
  39.         c.update(pet);   
  40.         out.println("update pet: id = "+pet.id()+" name = "+pet.name()+" age = "+pet.age());   
  41.         out.println();   
  42.   
  43.   
  44.         out.println("删除中间那条宠物");   
  45.         out.println("delete pet: id = "+pet.id()+" name = "+pet.name()+" age = "+pet.age());   
  46.         out.println();   
  47.         c.delete(pet);   
  48.   
  49.   
  50.         out.println("把小于3岁的宠物的年龄都改成3岁");   
  51.         pet = new Pet();   
  52.         pet.age(3);   
  53.         c.update(pet,"age<3"); //与c.update(pet,"age  
  54.   
  55.   
  56.         Rows rows = new Rows(Pet.class);   
  57.         c.select(rows);   
  58.         out.println("查看剩下的宠物:总共有 "+rows.count+" 条");   
  59.         for(Pet p : rows.rowList) {   
  60.             out.println("select pet: id = "+p.id()+" name = "+p.name()+" age = "+p.age());   
  61.         }   
  62.         out.println();   
  63.   
  64.   
  65.         out.println("删除年龄为5岁的宠物");   
  66.         c.delete(Pet.class"age=5"); //与c.delete(Pet.class, "age=?", 5)相同   
  67.   
  68.            
  69.         rows.clear(); //清除上一次的数据   
  70.         c.select(rows);   
  71.         out.println("查看剩下的宠物:总共有 "+rows.count+" 条");   
  72.         for(Pet p : rows.rowList) {   
  73.             out.println("select pet: id = "+p.id()+" name = "+p.name()+" age = "+p.age());   
  74.         }   
  75.         out.println();   
  76.   
  77.   
  78.         out.println("查找前2条宠物");   
  79.         rows.clear();   
  80.         rows.offset = 1;   
  81.         rows.limit = 2;   
  82.         rows.orderBy = "name";   
  83.         c.select(rows);   
  84.         for(Pet p : rows.rowList) {   
  85.             out.println("select pet: id = "+p.id()+" name = "+p.name()+" age = "+p.age());   
  86.         }   
  87.   
  88.   
  89.         out.println("删除所有宠物");   
  90.         c.delete(Pet.class);   
  91.            
  92.         rows.clear();   
  93.         c.select(rows);   
  94.         if(rows.count == 0) {   
  95.             out.println("查看剩下的宠物: 没有宠物了");   
  96.         }   
  97.     }   
  98. }  




2.9.2.2 运行 Crud

这里使用的测试数据库是: MySQL Server 5.1,并且默认使用InnoDB Storage Engine。
对应的JDBC驱动是: mysql-connector-java-5.1.8 (支持最新的JDBC4.0规范),

关于数据库的安装和配置请查看MySQL的参考手册。

因为个人精力有限,目前Douyu在开发时只在MySQL Server 5.1 与Oracle9i、Oracle11g上进行了测试,
并且采用方言机制对不同数据库进行了优化,其他数据库的支持在后续版本中会陆续加入。
不过就目前的情况来说,除了MySQL和Oracle之外,如果你使用别的数据库来测试下面的例子,
如果没有使用到一些数据库独有的特性,只要数据库驱动严格遵守JDBC规范,通常也是可用的,
也有可能存在问题,因为Douyu除了MySQL和Oracle之外没有对其他数据库进行测试。

另外,Douyu推荐你使用支持JDBC3.0、JDBC4.0规范的数据库驱动,
JDBC3.0规范已经出来9年了,各数据库厂商自带的数据库驱动一般都支持JDBC3.0规范。

1) 安装MySQL数据库驱动

可以到MySQL的官方网站下载数据库驱动(可能需要注册),
下载下来解压后把里面的文件:
"mysql-connector-java-5.1.8-bin.jar"(文件名太长了,可以把它改为"mysql.jar")
copy到D:/Douyu/lib目录中("D:/Douyu"是Douyu服务器的安装目录,见:1.2节)
这样就完成数据库驱动的安装了,不需要别的特殊配置。


2) 在MySQL中建立一个数据库然后再建表

首先打开一个新的控制台进入:MySQL monitor
D:/>mysql -uroot -p
Enter password: ***
Welcome to the MySQL monitor.  Commands end with ; or /g.
Your MySQL connection id is 5
Server version: 5.1.38-community MySQL Community Server (GPL)

Type 'help;' or '/h' for help. Type '/c' to clear the current input statement.

mysql>

然后建立一个名叫"douyu"的数据库(数据库名你可以任取,不过下文中都以"douyu"为参考),
并选中"douyu"为当前数据库:
mysql> create database douyu;
Query OK, 1 row affected (0.19 sec)

mysql> use douyu;
Database changed
mysql>

最后把下面的SQL脚本copy到MySQL monitor中运行:
=============================================================
drop table if exists pet;

create table pet(
  id  int not null auto_increment,
  name varchar(50) not null,
  age int,
  primary key(id)
);
select * from pet;
=============================================================

结果像下面这样:
=============================================================
mysql> drop table if exists pet;
Query OK, 0 rows affected, 1 warning (0.03 sec)

mysql>
mysql> create table pet(
    ->   id  int not null auto_increment,
    ->   name varchar(50) not null,
    ->   age int,
    ->   primary key(id)
    -> );
Query OK, 0 rows affected (0.28 sec)

mysql> select * from pet;
Empty set (0.05 sec)

mysql>
=============================================================

到这里,你就像一个出色的DBA一样快速地完成了与数据库相关的工作。


3) 修改Douyu服务器的配置文件

如果Douyu服务器正在运行,请先连按两次 Ctrl+C 键停止服务器,
然后打开conf/server.java文件,
将配置参数 loadDatabases = false 改为 loadDatabases = true,
如果你是从2.8节顺序读到这里的,为了不出现http 403错误,
请将checkPermission = true 改为 checkPermission = false(或者注释掉也行)
然后像下面这样增加一个数据库配置项:

Java代码 复制代码
  1. databases = {   
  2.     //以下列出的只是常用选项,还有很多选项未在此处列出   
  3.     @Database (   
  4.         //是否在控制台打印运行期间生成的SQL语句(主要用于调试目的)   
  5.         printSQL = true,   
  6.   
  7.         //是否输出模型类的源文件(主要用于开发参考目的,不要修改输出文件的内容)   
  8.         //outputJavaSourceFiles = true,   
  9.   
  10.         //数据库方言(注释掉这个选项也可以,但是不能充分利用数据库的独有特性)   
  11.         dialect = SQLDialect.MySQL,   
  12.   
  13.         //在Java源代码层使用的数据库名称,   
  14.         //如果同时配置了多个数据库,数据库名称不能相同   
  15.         name="mysql",   
  16.   
  17.         //模型类所属包名,   
  18.         //包名是可以任取的,但建议不要跟你其他代码中的包名一样,   
  19.         //你可以通过import sql.table.mysql.*;这样的语句引用自动生成的模型类   
  20.         packageName="sql.table.mysql",   
  21.   
  22.         //MySQL的java.sql.Driver接口实现类   
  23.         driver="com.mysql.jdbc.Driver",   
  24.   
  25.         //MySQL特有的url语法,   
  26.         //其中的douyu是在MySQL中定义的数据库名,请根据你的实际情况修改   
  27.         url="jdbc:mysql://localhost/douyu",   
  28.         //用户名(通常是root)   
  29.         userName="请把你的用户名放在这里",   
  30.         //密码   
  31.         password="请把你的密码放在这里",   
  32.            
  33.         //用tableNames参数告诉Douyu自动解析哪些表,   
  34.         //表名用逗号分隔,不区分大小写,如果是"*"号,那么自动解析所有表。   
  35.         tableNames="*"  
  36.     )   
  37. }  



注意,conf/server.java文件中的databases属性值是一个元素为@Database的数组,
还有,属性与属性之间,或者数组的元素与元素之间都是用逗号分隔的,
千万别敲入分号(可能是习惯性动作,比如我就常常这样),
另外,最后一个属性或元素后面是不加逗号的,否则编译器会告诉你出现了语法错误。

保存已修改完的配置文件后再重新启动服务器,
如果一切顺利就会如下图所示:




4) 在浏览器中查看程序运行结果


打开浏览器,输入 http://localhost:8000/sql.Crud
第一次运行时,结果类似这样:


在服务器运行的控制台窗口会打印出运行期间生成的SQL语句:





2.9.2.3 程序代码说明

sql.table.mysql.Pet这个模型类是自动生成的,
其中"sql.table.mysql"是包名,
对应上一节在服务器conf/server.java文件中设置过的"packageName"属性值,
"Pet"当然就是模型类的类名了,对应数据库的pet表。
默认情况下把表名的第一字符转换成大写,其他字符转换成小写后就变成类名了,
而表中的列名全转换成小写后对应模型类的一个属性名,
属性值的Setter、Getter方法的名称也跟属性名一样。

比如pet表有name这一列,对应的属性名也是name,
通过pet.name(xxx)和pet.name()这样的方式来修改、获取name属性的值。

这样的命名风格完全是我自己的喜好,我不喜欢Rails中所提倡的表名加s的风格,
那只是以英语为母语的人的喜好,讲中文的人有时喜欢把所有中文的第一个拼音字母组合成表名,
有时甚至连讲英文的人也喜欢用user、user_info、user_table这样的命名方式来代替users这样的复数表名,
比如在MySQL或Oracle提供的系统表中都常见到这样的例子。

另外我也不喜欢setXXX或getXXX这样的风格,每次都要我输入多余的set、get前缀,
还要小心地把紧接在set、get前缀后的第一个字符变成大写,真是够烦的!

之所以采用这样的设计方案,除了个人喜好的原因外,
如果你使用了这样的默认风格还会获得小小的性能提升,
因为不用对表名进行额外的分析,也不用做多余的set、get字符串拼接转换。

当然了,如果你的喜好与我不同,喜欢传统风格,
还是可以满足你的要求的,
在后面讲到自定义数据库映射时(只需实现com.douyu.sql.DatabaseMapping接口),
你不但可以自定义表名跟类名的映射,还可以自定义与表列名相关的属性名、Setter、Getter方法,
还能注册属性、SQL事件监听器。


sql.table.mysql.Pet这个模型类没有什么特别的地方,
没有继承任何类,也没有实现任何接口,
只包含一些与表列名相关的属性名及Setter、Getter方法。

四种SQL DML语句(INSERT、UPDATE、DELETE、SELECT)
分别对应com.douyu.main.Context接口中定义的如下方法(目前与SQL相关的操作也只有这些方法):

Java代码 复制代码
  1. public boolean insert(Object entity);   
  2.   
  3. public boolean update(Object entity);   
  4. public boolean update(Object entity, String where, Object... params);   
  5.   
  6. public boolean delete(Object entity);   
  7. public boolean delete(Class entityClass);   
  8. public boolean delete(Class entityClass, String where, Object... params);   
  9.   
  10. public boolean select(Object entity);   
  11. public boolean select(Rows rows);  


这几个方法在上面的例子中都使用到了,
所有方法都返回boolean类型,当然也可以像JDBC API中那样返回一个数字,
目前对返回值的设计方案还需要进一步权衡,返回boolean类型实现起来比较简单,
当开发人员用if语句判断执行结果时最多不超过两个分支就知道方法的执行情况了,
如果返回一个数字至少需要三次判断(出错、没有行受影响、有多行受影响),
涉及到批量更新时返回值也更加复杂,而且不同的数据库驱动可能在实现上稍有不同,
实际情况中也很少向最终用户提供那么详细的信息,
比如当用户通过你开发的界面删除或更新完某些记录后,
你只是简单的提供一条:"XXX表单中的记录已删除成功"这样的信息。
经过这样的考量后,也为了简化起见,目前暂时使用返回boolean类型的方案。

Java代码 复制代码
  1. insert(Object entity);   
  2. update(Object entity);   
  3. delete(Object entity);   
  4. select(Object entity);  


这四个方法是用来插入、更新、删除、查找单条记录的,
参数entity表示一个实体,通常是一个模型类的实例。

上例中的for语句就是用"c.insert(pet);"来插入5条宠物记录:

Java代码 复制代码
  1. for(int i=1; i<=5; i++) {   
  2.     Pet pet = new Pet();   
  3.     pet.name("pet"+i);   
  4.     pet.age(i);   
  5.     c.insert(pet);   
  6.   
  7.     if(i == 3) middle = pet.id(); //id值自动获取   
  8.     out.println("insert pet: id = "+pet.id()+" name = "+pet.name()+" age = "+pet.age());   
  9. }  


上面代码中还有一点是值得一提的:
因为pet表的id列是自动增加的(auto_increment),
所以你不并显示的调用pet.id(XXX)来给id赋值,
Douyu足够聪明,她运用了JDBC3.0规范中引入的"Retrieving Auto Generated Keys"功能来
自动获取数据库生成的id值,
这时就可以通过pet.id()来引用了,
如上面的代码:"if(i == 3) middle = pet.id();"就是把每三条记录的id值保存到middle变量中。

接下来,为了演示select(Object entity)的用法,先生成一个新的Pet对像,
把这个Pet对像的id值设为middle,
调用select(Object entity)就把其他两个值:name与age都查出来了。

Java代码 复制代码
  1. Pet pet = new Pet();   
  2. pet.id(middle);   
  3. c.select(pet);  


然后,更新pet的name值为"MyPet",接着删除pet,如下:

Java代码 复制代码
  1. pet.name("MyPet");   
  2. c.update(pet);   
  3. ....   
  4. c.delete(pet);  


update方法还有另一个重载形式:

Java代码 复制代码
  1. public boolean update(Object entity, String where, Object... params);  


这个方法是采用where子句的方式来更新记录的,
但是重载方法的第一个参数entity与update(Object entity)中的entity存在语义上的差别,
update(Object entity)中的entity对应的模型类是按照主键来更新的,
像这样:

Java代码 复制代码
  1. UPDATE pet SET name=?,age=? WHERE id=?  


占位符(?号)的取值都从entity中取,

而重载形式的entity,是按where与params参数生成where语句来更新的,
像这样:

Java代码 复制代码
  1. UPDATE pet SET age=? WHERE age<3  


where语句前半段的占位符(?号)的取值从entity取,
而后半段的占位符(?号)的取值从params取(如是String where中没有占位符,params是可选的)

比如上面的代码中的:

Java代码 复制代码
  1. out.println("把小于3岁的宠物的年龄都改成3岁");   
  2. pet = new Pet();   
  3. pet.age(3);   
  4. c.update(pet,"age<3"); //与c.update(pet,"age  

你可能感兴趣的:(编程语言之JAVA)