最近与同是开发者的朋友一起,谈论各种各样Java的之外的构架。似乎有一些现象表明, Java正在因为许多原因成为Web发展的障碍,其中一个问题是代码行(LOC)。做同一件事情,Java的某一些参数需要其他语言的2倍的代码量,我个人以为LOC仅仅是一个问题,它让你把一些本不该花的时间都花在你本不该去做的事情上。例如,象手工编写你所有的Bean一样。 或许你也正在手工的为你的构架XML定义所有的条目,以此来节约时间, 我承认, 我正是这样。
不久之前我使用RubyOnRails,一个让我喜欢用它们的原因就是他们能很容易产生完全的代码,从简单的script到复杂的代码。 这让我考虑我当前的工作和编码期间我的有效情况是怎么样的。 “我做好了多少工作VS我花费多少时间”的思想在过去的数星期内不停的在我脑子里萦绕。虽然有各种框架帮助我们使工作变的容易一些—--比喻说在使用的Spring—--它一些时候看起来很笨重,为一个web应用程序做一个页面或者一个模块就需要很长的时间。因此在一些思考以后, 我决定了做一些东西。
我多年前已经知道XDoclet,并且在它首次出现的时候试用了它。但是我没有进行长期的考虑, 所以当时我还不是一个在代码生成方面的大拿。我想要了解事情到底是怎么工作的, 因此我花了大量时间手工编写任何代码,包括config文件和deployment文件。
然而,我今天发现代码生成的两个主要的益处:
1.你在想学什么东西的时候,它将是很有帮助的。比喻说,去年我想学Hibernate 的时候,我已经有了存在的表,不想再反复的测试它是怎么工作的了,我也不想花大量的时间来填写文档。使用一个方便的叫做Middlegen的工具,我只要告诉它查看我的数据库并且为我生成代码就可以了。现在,写一个mapping文件对我来说已经很容易了,可是Middlegen的帮助节省了我大量的时间并为我指明了方向。
2.在对你的工程有一个清晰的概念和一个结构甚至一个框架已经搭建起来后,接下来的事情将变的十分的乏味,手工的创建beans,controllers, services 和 DAO,每次都会有一些新的需求要被创建。理想的做法是,你也许会调用一些Ant任务,比喻说, gencontrollers或者genmodules 来生成合适的controller代码或者一个整个的包含你的beans, controllers, services和DAO的模块.
使用XDoclet 来减轻痛苦 基于过去的一些经验,我不敢确信XDoclet对我有多大的帮助。我想要寻找一个工具让我可以花最少的时间来生成最多的代码。我在XDoclet网站上看到的大部分的示例都是针对于EJB的生成的,其实这也是XDoclet设计的初衷。但是,情况是这样的,我的工程不含有任何的EJB代码,事实上Spring全部是关于POJO的东西,所以EJB方面的工具不是我需要的。同时,XDoclet不含有任何的Spring标签,所以我一直在寻找一个不同平常的什么东西。
最后我把目光投上了模板。模板是一种继承XDoclet功能来更好的满足你的代码生成要求的方法。例如,你有一个特殊类型的类,与XDoclet所支持的任何一种都很不同(在我看来是一些客户化的控制类),你就可以用XML文件来把你自己的模板加进去。理想化的,我只想定义一个含有最小量的属性并不但产生我的控制器代码而且另外的Spring所要求的XML文件。在实验了几次以后,我对结果很满意。下面就是加标注的假设的代码测试控制器类。长代码行以\分开。
TestController.java
1. package com.mytest.account.controller;
2.
3. /**
4. * @mytest.controller mapping="/acct.ctrl"
5. * @mytest.controller actionmethod="create"
6. * @mytest.controller actionmethod="read"
7. * @mytest.controller actionmethod="update"
8. * @mytest.controller actionmethod="delete"
9. */
10. public class TestController {
11.
12. /**
13. * @mytest.controller property="privateView" propertyValue="priv1" propertyMethod="PrivateView"propertyType="String"
14. */
15. private String privateView = null;
16.
17. /**
18. * @mytest.controller property="publicView" propertyValue="priv2" propertyMethod="PublicView" propertyType="String"
19. */
20. public String publicView = null;21. }
你首先看到的可能是客户化的命名空间@mytest.controller标签。这个是在生成我们类的不同部分时,XDoclet如何知道去查找什么。标签的位置十分的重要。一些在类关键字的上面一些在属性定义的下面。XDoclet根据不同位置的标签来生成在我们的模板中使用的类和属性对象。这是一个很重要的区别,我们将在接下来研究。
看一下上面的代码,我们将做下面的操作:
1.为SimpleUrlHandlerMapping建立一个Spring mapping入口,它将在一个叫做mytest-servlet.xml 文件中被描述(是Spring中与Struts中的struts-config.xml 类似的东西).
2.actionmethod 属性允许我们为一个控制器指定一个方法名,它是从Srping的MultiActionController 继承而来 (在Struts, 对应的是 DispatchAction)。在我们的例子中,将会有 CRUD (Create, Read, Update, Delete)这么几个方法名。 XDoclet将会重复的进行不同参数的相同方法名的方法,所以允许我们的模板输出N个方法。
3.property 标签 让我们 定义一个典型的Bean属性,像你通常在XDoclet中做的一样,因为我们在这里使用了一个定制的模板,我们增加了更多的属性。propertyValue 是为mytest-servlet.xml 中对应的属性所指定的值。对我们的定制的标签来说,我们正在为控制器类设置private的和public的JSP页面显示,我通常管它们叫privateView和publicView—一个是针对签发者的另一是针对每一个人的。propertyMethod 将为我们提供一种简单的方法来类指定这个属性属于那一个方法,而不用去跳过bean-method-naming的约束。propertyType 属性指定了我们将要为这个方法get/set什么类型的属性。
很有争议的是,我可能把所有的属性定义发在类的层次上而不是Field的层次上。这也未尝不可,但是这将为在模板上增加额外的代码来确定在产生一个方法的时候我使用了正确的属性,方法,类型。我感觉使用XDoclet的Field 类比往我的模板里增加复杂的代码要容易和迅速。
模板 好了,现在让我们开始干活吧。正像我前面提到的那样,我选择XDoclet来生成代码的原因是我将定义一套定制的模板(不是一些定制的类或者更多的代码)并使用XDoclet的XML标签来为我填充这些空白。下面所列就是我们想要生成的模板相应的部分。长代码已经被\ 分开。
MultiController.xdt
1. package <XDtPackage:packageName/>;
2. import javax.servlet.http.HttpServletRequest;
3. import javax.servlet.http.HttpServletResponse;
4.
5. import org.apache.commons.logging.Log;
6. import org.apache.commons.logging.LogFactory;
7. import org.springframework.validation.BindException;
8. import org.springframework.web.servlet.ModelAndView;
9.
10. import com.mytest.system.spring.BaseController;
11.
12. /**
13. * MultiAction controller class for <XDtClass:className/>.
14. *
15. * @author
16. * @since
17. * @version $Id$
18. */
19. public class <XDtClass:className/> extends BaseController {
20.
21. // CUT AND PASTE THIS INTO THE mytest-servlet.xml FILE
22. //
23. // <!-- ==== <XDtClass:className/> Bean Definition ==== -->
24. // <bean id="contr<XDtClass:className/>" class="<XDtPackage:packageName/>.<XDtClass:className/>">
25. // <property name="methodNameResolver" ><ref bean="actionResolver"/></property>
26. <XDtField:forAllFields>
27. <XDtField:ifHasFieldTag tagName="mytest.controller">
28. // <property name="<XDtField:fieldTagValue tagName="mytest.controller" paramName="property"/>"><value><XDtField:fieldTagValue tagName="mytest.controller" paramName="propertyValue"/></value></property>
29. </XDtField:ifHasFieldTag>
30. </XDtField:forAllFields>
31. // </bean>
32. //
33. // === PUT THIS UNDER THE URL MAPPINGS SECTION ===
34. // <prop key="<XDtClass:classTagValue tagName="mytest.controller" paramName="mapping"/>">contr<XDtClass:className/></prop>
35. //
36.
37. protected final Log log = LogFactory.getLog(getClass());
38.
39. <XDtField:forAllFields>
40. <XDtField:ifHasFieldTag tagName="mytest.controller">
41. public <XDtField:fieldTagValue tagName="mytest.controller" paramName="propertyType"/> get<XDtField:fieldTagValue tagName="mytest.controller" paramName="propertyMethod"/>(){
42. return <XDtField:fieldTagValue tagName="mytest.controller" paramName="property"/>;
43. }
44.
45. public void set<XDtField:fieldTagValue tagName="mytest.controller" paramName="propertyMethod"/>(<XDtField:fieldTagValue tagName="mytest.controller" paramName="propertyType"/> value) {
46. <XDtField:fieldTagValue tagName="mytest.controller" paramName="property"/> = value;
47. }
48. </XDtField:ifHasFieldTag>
49. </XDtField:forAllFields>
50.
51. public ModelAndView init(HttpServletRequest request, HttpServletResponse response)
52. throws ServletException, IOException {
53. if(log.isDebugEnabled()) log.debug("entered");
54. // YOUR INIT CODE GOES HERE
55. if(log.isDebugEnabled()) log.debug("exited");
56. // REPLACE THIS HERE IN YOUR CODE
57. return null;
58. }
59.
60. <XDtClass:forAllClassTags tagName="mytest.controller">
61. <XDtClass:ifHasClassTag tagName="mytest.controller" paramName="actionmethod">
62. public ModelAndView <XDtClass:classTagValue tagName="mytest.controller" paramName="actionmethod"/>(HttpServletRequest request, HttpServletResponse response)
63. throws ServletException, IOException {
64. if(log.isDebugEnabled()) log.debug("entered");
65.
66. if(log.isDebugEnabled()) log.debug("exited");
67. // RETURN THE PROPER VIEW HERE
68. return null;
69. }
70.
71. </XDtClass:ifHasClassTag>
72. </XDtClass:forAllClassTags>
73.
74. // Your (other) multiaction methods go here
75.
76. } // <XDtClass:className/>
恩,当这些都完成的时候,我们拥有了一个控制器类,包含对应于Spring中定义bean和URL mapping的mytest-servlet.xml的XML代码,为视图属性提供了适当的get/set方法,我的多action 方法,完整的Debug日志语句。从21行开始,包含了XML(将会被清除)的76行代码,可以为我们节省大量的敲代码的时间和排除错误。
同时需要注意的是 XDtField 和 XDtClass 标签。现在应该很清楚的是我们为什么把不同的标签值给替换了。我们所有的类级的标签或者定义了类方法或者是XML配置文件的值,而field级别的标签为我们产生get/set方法。像我前面说到的那样,我们在这里对field的使用略有不同,因为我们在mytest-servlet.xml 中还利用它来做一些控制器的mapping值。当我们的控制器启动的时候,Spring会为privateView 和 publicView方法注入合适的值。
把他们整合到一起 问题的最后就是如何把所有的这些东西整合到一起,而且让我们可以看到产生的代码。这就是为什么要把Ant引入的原因所在,我们将做下面3件事情:
1.创建一个文件夹,从code base 分离出来,这将是我们保存假的源文件的地方和自动产生的源文件的地方。
2.为我们的模板创建一个目录。
3.为代码生成创建一个Ant任务。
在我开发的机器上有如下的文件夹结构
trunk
|
+----+- src
+- xdoclet
|
+- build
+- src
+- com/mytest/account/controller
+- template
起初,看起来可能有点糊涂,因为这里有两个src。但是我感觉有个重要的原因就是,我在xdoclet下有一个单独的src可以让我把假的源代码(译者按,就是要被填充的java文件)和产生的代码分开。在我运行Ant任务以后,有一个文件就是怎么把文件拷贝到主src目录(与xdoclet同一级别的)并且把产生的代码加进去。为达到这篇文章的目的,我把这些东西从source code 分离出来,这也许能防止把在把所有的生成混成一团的时候带来的各种问题。
根据上面的文件夹结构,这是我们的文件应该在的地方:
trunk/src/xdoclet/src/com/mytest/account/controller/TestController.java
trunk/src/xdoclet/template/MultiController.xdt
.java文件需要包名来与文件夹对应,这样XDoclet才能认识它,并根据它来运行模板。我发现XDoclet只是把log4j的Debug级别设置到INFO, 当我没有把.java文件放到正确的位置的时候,XDoclet不会显示任何的错误信息,也发现不了也生成不了文件。只有我查看了.jar文件并修改了log4j.properties 以后正确的显示没有要处理的错误信息才被显示。从技术角度说,这不是一个错误,但是它让我琢磨了很长的时间,所以要注意这一点。
接下来的Ant任务,使用上面的树并且在正确的目录里生成代码。
1. <!-- ========Xdoclet Target ======== -->
2.
3. <target name="xdoclet">
4.
5. <path id="xdoclet.classpath">
6. <fileset dir="${xdoc.home}/lib">
7. <include name="*.jar"/>
8. </fileset>
9. </path>
10.
11. <echo>+------------------------------------+</echo>
12. <echo>|Generating sources from xdoclet tree|</echo>
13. <echo>+------------------------------------+</echo>
14.
15. <taskdef name="doclet" classname="xdoclet.DocletTask" classpathref="xdoclet.classpath" />
16. <doclet destdir="${jxdoc.dir}/build"
17. verbose="true">
18. <fileset dir="${jxdoc.dir}/src">
19. <include name="**/*Controller.java" />
20. </fileset>
21. <template
22. templateFile="${jxdoc.dir}/template/MultiController.xdt"
23. destinationFile="{0}.java"
24. subTaskName="Create Controller file(s).." />
25. </doclet>
26.
27. </target>
想要运行这个,需要做一些配置。
1.第6行xdoc.home 应该指向你的XDoclet的安装目录。
2.22行为我们的模板文件定义了一个位置和名字。
3.24行指定当doclet运行的时候XDoclet要输出到控制台的任务的名称信息。这只是为了方便而已。
你可以通过下面的命令来运行: ant xdoclet.
如果一切运行正常的话,你应该能看到BUILD SUCCESSFUL的消息。在xdoclet/build/com/mytest/account/controller 目录下,你的新的文件就应该出现,文件中有必要的骨架属性和方法。
这个模板可以别修改成不单单产生控制器而且可以产生Bean,Form,Service等等。在xdoclet 中,你只需要为每一个你想要生成的对象类型增加一个新的taskdef然后提供一个不同的模板。比喻说,如果你想产生service对象,你可以使用下面的taskdef:
1. <taskdef name="servicedoclet" classname="xdoclet.DocletTask" classpathref="xdoclet.classpath" />
2. <servicedoclet destdir="${jxdoc.dir}/build"
3. verbose="true">
4. <fileset dir="${jxdoc.dir}/src">
5. <include name="**/*Service.java" />
6. </fileset>
7. <template
8. templateFile="${jxdoc.dir}/template/Service.xdt"
9. destinationFile="{0}.java"
10. subTaskName="Create Service files(s).." />
11. </doclet>
在上面的代码中,我们看到我把taskdef 的名字改成servicedoclet,并且包含的fileset属性由*.Controller.java变成*.Service.java, 这将导致任务只去查找以"Service.java."结尾的文件。
在第8行,我们告诉XDoclet使用Service.xdt模板。所以如果我们已经完成初始化工作的话,我们很容易就能增加一个特定的代码生成器。完全可能的是(我也正是这样做的),我们可以在xdoclet任务下生成很多的模板,在这个任务下,让它一次把每一个对象类型都读到然后一次性的把每样东西都生成完毕。
Maven 2.0 和原型的一句话: Maven 2.0 包含了一种称为原型的机制,它允许基于模板驱动的工程代码生成。但是实现方式有所不同--Maven使用Velocity 来生成模板--与我们上面讲的那些相比,基本上完成同样的事情,但是工作范围是在更大的project范围。
我试过很多次使用Maven ,我很欣赏这个做法。但是我感觉,从上面的例子来说,Maven 需要花费更多的程序员的努力时间(在刚开始的时候)。但Maven 正好说明了我前面提出的我关心的问题,我喜欢基于Rails在project级别和其他程序员合作。当然,Maven,也可以在project级别工作,通过配置参数来生成代码,从而为程序员节省大量的时间。这才是最有实际意义的一件事情。对于已经存在的工程,这里讲到的方法都是一种更快的有着更少结构冲突的实现方式。
结论 下面就是我们这篇文章所提到的:
1.创建了一个不但能生成类,而且能为我们的框架生成必要的XML配置文件的定制的pseudo-Java文件。在我们的例子里,我们定义了一个定制的可以用在Spring Web服务器里的控制器。
2.定义了一个包含了一些我们的类的静态元素和一些可以在运行的时候提供动态类代码的组合的XDoclet XML 标签的模板文件。
3.建立一个文件夹结构和Ant任务,它将调用XDoclet来产生我们的骨干控制器类。
这只讨论了XDoclet的皮毛。我们只看到一个小型的,还有点作用的例子,它展示了模板帮助你不但成为一个更有效率的程序员,并且是成为更聪明的一个。我希望它可以为你节省更多的时间。