选择显示字体大小

用ant来享受spring

ant来享受spring

通过轻量级的ioc容器来扩展ant

作者:josef betancourt 2005年2月14日

翻译:xmatrix


版权声明:可以任意转载,转载时请务必以超链接形式标明文章原始出处和作者信息及本声明
作者:
josef betancourt ;xmatrix
原文地址:
http://www.javaworld.com/javaworld/jw-02-2005/jw-0214-antspring.html
中文地址:
http://www.matrix.org.cn/resource/article/43/43845_ant_spring.html
关键词: ant spring



摘要
这篇文章介绍了通过ant任务的扩展来实现ioc管理对象或非管理对象的执行。同时也介绍了ognl(对象图形导航语言)如何被用来使ant执行任何方法表达式,包括带有运行时参数的。也介绍了如何使用junit来测试ant的扩展。此外,还包含一个使用spring框架的实现。ant-ioc的组合为创建松耦合的软件开发支持任务开创了新的天地。

我需要增加一个基于ant驱动的新任务,并且我用spring(一个轻量级的ioc框架)来实现这个任务。我几乎没有碰到什么问题,因为ioc容器是非侵入式的,这很容易创建一个包装或者直接使用对象来实现任务。于是我开始想知道是否ant可以直接使用spring配置的对象,然后重用已经定义和测试的依赖图和配置。那为什么还要重复和引入波纹效应或其他问题呢?如果ioc容器果真能提供这样的便利,就可以保证更直接的使用。

这篇文章介绍了这种方法并且演示了一种概念上的实现。刚接触ant扩展的开发者会发现这个例子十分有趣。

ant扩展
为了给ant增加自定义任务,ant手册建议使用为这个目的而提供的类,如task类。但是这个建议不是强制的,ant可以执行任何拥有execute()方法的类(当然ant也可以通过使用exec或java任务来执行任何程序,但那是另一种扩展方式)。ant也支持集成这些任务扩展到各种类型的属性或xml文件中。

ant增加一个自定义任务的最佳方法是通过task扩展来重用ioc框架。因此,执行独立应用的task必须设置和使用建立在ant基础上的框架内置的对象和资源。

控制反转
ioc设计模式,也称作di(依赖注射)。在框架的上下文中,这与java对象的组成有关。在ioc框架上增加的投资很大一部分是由于spring框架的开发人员演示了在一个ioc/aop/xml/javabeans轻量框架中的协同作用,而这正是通过允许为其他api或组件创建强大的抽象层来提供超越di能力的原因。spring本身就是一个使用ioc的例子。ant看起来与ioc容器相适应,因为他也是基于xml或者javabean的,从某方面来说,他也使用了ioc。

需求
我们的ant ioc任务扩展需求可以通过角色/目标/需求的格式来定义(这里的需求不分顺序):

●角色:开发人员
●目标:修改ioc任务
●需求:
在任何代码改变或构建后执行回归测试
很容易在回归测试中增加新的测试用例
支持不同的ioc框架
通过修改ant日志的级别或ioc日志的配置使调试时可以得到更有效的输出

●角色:构建创建人
●目标:编辑ant目标并使用任务来定义ioc容器的输入或输出bean
●需求:
设置ioc描述符的位置
在不需要容器时,定义fqcn(完全限定类名)作为目标
使用ioc时,设置pojo(普通java对象)bean名,缺省为antbean
定义目标方法名,缺省为execute
定义一个调用可以带参数的表达式的方法
定义可以插入目标bean的属性,用来复写容器属性
定义目标的元素文本
没有必要定义用来处理ant/ioc组合的新类
为了各种扩展需要重用现存的属性文件

●角色:任务扩展对象
●目标:执行对象方法
●需求:
执行在ioc bean定义中定义的pojo
执行容器外的定义类
如果没有定义使用缺省的bean名antbean
执行简单的方法,缺省为execute()
执行带可选参数的方法表达式
如果目标是ant相关的则插入工程
插入动态属性

任务
支持这些需求的任务定义是springcontexttask

描述
这个任务执行由spring容器管理的或者是未管理的fqcn的对象的方法。目前还不支持srping bean定义引用的classpath。
springcontexttask的参数如下表所示:



例子
最简单的应用我们的ant任务扩展的例子如下:

<!-- create the task definition -->
<taskdef name=&quot;runbean&quot; classpathref=&quot;testpath&quot;
      classname=&quot;jbetancourt.ant.task.spring.springcontexttask&quot;/>

<target name=&quot;simpleappcontextusewithdefaults&quot;>
   <runbean beanlocations=&quot;applicationcontext.xml&quot;></runbean>
</target>        


simpleappcontextusewithdefaults目标执行在文件路径中找到的bean定义文件applicationcontext.xml中的bean名为antbean的execute()方法。路径属性名是复数的以便将来支持多个bean定义文件。

bean的执行类似ant执行对象的方法;然而,这里是ioc容器来管理bean。容器可以增加事务依赖,包装数据库,设置网络服务代理,使用远程甚至提供aop代理来代替实际目标bean。我们的方法简化了配置,因为ant脚本不再需要知道如何配置对象,特别是复杂的对象。但是如果ant脚本确实需要为服务调用设置特定的属性时会怎么样呢:

   <target name=&quot;publish&quot;>
      <spring
         beanlocations=&quot;applicationcontext.xml&quot;
         beanname=&quot;sitegenerator&quot;  
         methodname=&quot;generatesite&quot;
         host=&quot;${host.site.url}&quot;
         port=&quot;${site.port}&quot;>
         made a few tweaks.  removed some sentence fragments.  
      </spring>                    
   </target>


注意因为任务名已经在taskdef中定义了,使用的名字将依赖于ant的taskdef定义。这儿任务名是spring。现在我们定义bean名字和调用的方法。元素文本也会被放到目标bena中。在这个例子中,文本是一个发布的注释。

通过使用ant的动态属性功能,我们也可以将需要的属性放到目标对象中。通常在ant文件中一个属性被解析时,对应的set方法会被调用。使用动态属性,非对象属性或字段会通过setdynamicattribute()方法被增加到对象中。通常因为容器已经包装了其中的bean的属性,这种属性注入提供了一种重写的能力。但是,是否这样会将配置复杂化?我们将不得不维护ant任务使用的属性及管理对象所需要的属性。

当然这不是必须的;如例子中的spring用法,相同的属性文件被antspring同时使用&mdash; 即使使用了ant的占位符语法(${...})。spring提供了这种目的的类,如propertyplaceholderconfigurer。因此,这种方法不会引入新的配置恶梦。可参考旁注&ldquo;属性中的属性&rdquo;获得更多的帮助。

另一种放置属性的方法是通过使用call属性来调用带运行时参数的目标方法或者嵌套的methodcall元素,他的内容是java表达式。这个元素很容易使用因为xml需要的符号如实体转义符可以用cdata来避免:

  call=&quot;generatesite(&quot;${host.site.url}&quot;,&quot;${site.port}&quot;)&quot; 
or better:
  <methodcall><!&#91;cdata&#91;    generatesite(&quot;${host.site.url}&quot;,&quot;${site.port}&quot;)   &#93;&#93;></methodcall>


因此先前的例子可以如下写法:

   <target name=&quot;publish&quot;>
      <spring beanlocations=&quot;applicationcontext.xml&quot; beanname=&quot;sitegenerator&quot;>
         <methodcall>  generatesite(&quot;${host.site.url}&quot;,&quot;${site.port}&quot;)  </methodcall>
         made a few tweaks.  removed some sentence fragments.  
      </spring>                    
   </target>


当然,目标对象必须包含需要的方法和参数标识符。

上面的例子简单介绍了springcontexttask方法。可能他们可以有其他或更好的实现。
有人可能会对这个task扩展的特性有疑问,如调用任何方法的功能。这个功能甚至可以被移除,因为任何不包含execute()方法的目标bean可以被包装,一个任务在ioc框架中可能更容易完成。但既然通过ognl(后面会讨论)支持方法表达式很容易,那么方法参数的支持也不是个问题了。
有趣的是,既然任何方法可以被调用,那么同一对象可以在同一个构建文件中被重用来提供不同的服务,这样就可以在执行需要很多属性的任务中减少过度的ant脚本混乱了。如果任务实例可以通过id来引用的话这个功能就会有实际意义了。我们可以象下面这样写:

  
<spring id=&quot;metrics&quot; beanlocations=&quot;metricscontext.xml&quot; beanname=&quot;main&quot;
     exampleattribute=&quot;a value&quot;  and so forth . . ./>

   <target name=&quot;computemetrics&quot;>
      <spring refid=&quot;metrics&quot; call=&quot;computencss&quot;/>
      <spring refid=&quot;metrics&quot; call=&quot;computeccm&quot;/>
      <spring refid=&quot;metrics&quot; call=&quot;findbugs&quot;/>
   </target>

   <target name=&quot;gendocs&quot;>
      <!- here are calls to other types of docs '/>
      <!- now call the metric docs '/>
      <spring refid=&quot;metrics&quot; call=&quot;createdocs&quot;/>
   </target>


现在我们拥有更易读的格式而隐藏了更多的信息。我们不再关心容器中有什么,只要那儿有一个入口点&mdash;main.那个bean可以是实际的bean或者通过依赖注射代理给其他工具如pmd, javancss, 或者findbugs。

我没有选择通过id引用重用springcontexttask的开发方式。另一种完成重用的方式是在上下文中使用不同的bean,如:

<target name=&quot;computemetrics&quot;>
   <spring beanlocations=&quot;metricscontext.xml&quot; beanname=&quot;computencss&quot;/>
   <spring beanlocations=&quot;metricscontext.xml&quot; beanname=&quot;computeccm&quot;/>
   <spring beanlocations=&quot;metricscontext.xml&quot; beanname=&quot;findbugs&quot;/>
</target>


但在这个例子中的每一个bean必须有一个execute()方法来启动服务。而且每一个bean实际上只是引用同样的类或对象。
现在需求已经确定而且用例也定义好了,那就让我们来看看细节吧。

实现
这是一个简单实现,并没有提供什么技巧,如ant的io子系统或者重用ioc容器来执行多任务。
uml图显示如下:


图1. uml 类图. 点击查看大图

列表1 显示了task扩展的抽象父类。在运行时,ant调用xecute()。如果任务中的beanlocations属性被定义,一个ioc应用上下文被创建并且bean变成可访问的了。如果一个类被定义则执行普通的对象实例化。

下面是project属性,文本被插入到对象中,只依赖是否存在相应的设置方法或者对象是否是ant相关的。因此在非ioc的情况下,这个任务是一种设置注射的方式。

ognl
当我实现我的任务扩展时,对于调用运行时参数的方法的支持还是一个问题,虽然不是很严重,但这是一个设计上的机会。一个方法是调用增加一个包含参数的xml段给任务。ant支持动态元素,网上也有一些给这种任务使用的xml段的例子。但这种方式看起来过于复杂而且容易出错。

想起我先前在ognl上的研究,我决定在这儿使用他。根据ognl网站所说,ongl是&ldquo;对象图形导航语言;他是一种表达式语言用来获取和设置java对象的属性。你可以使用相同的表达式获取和设置属性的值&rdquo;。ognl的一个优秀的特点是支持方法调用,这正是我所需要的。

ognl支持很多表达式语言。例如,下面的代码计算一个目标对象的索引表达式作为调用的一部分:

<methodcall>  <!&#91;cdata&#91;    notifydeveloper(names(${dev}) &#93;&#93;>       </methodcall> 
  

另一个ognl表达式计算的例子可以在包含的单元测试中找到,那里一个方法调用表达式被如下使用:
converttostring(employees[getnum()]).

虽然ognl解决了我遇到的问题并且表达式提供了一种新的强大的ant功能,但是定义属性的使用才是推荐的方法而且与ioc模式兼容。
为了演示抽象类如何使用,下一节我们会用spring来实现一个实际的task扩展。

spring支持

列表2显示了springcontexttask。这个类是如何定位特定ioc容器的例子。模板方法模式使这个类简单化;所有调整实际目标对象来支持需求的细节都被放在了父类中。如何找到容器在使用任何ioc容器中都是必要的,从容器中得到对象,并且给属性赋值。

列表 2. springcontexttask类

/*
* springcontexttask.java
* created on jan 9, 2005
* creator: josef betancourt
* project: springcontexttask
* -----------------------------------------------------------------------------
*
* copyright 2005 by josef betancourt
*
* licensed under the apache license, version 2.0 (the &quot;license&quot;);
* you may not use this file except in compliance with the license.
* you may obtain a copy of the license at
*
*      http://www.apache.org/licenses/license-2.0
*
* unless required by applicable law or agreed to in writing, software
* distributed under the license is distributed on an &quot;as is&quot; basis,
* without warranties or conditions of any kind, either express or implied.
* see the license for the specific language governing permissions and
* limitations under the license.
*
* -----------------------------------------------------------------------------
*
*
*/

package jbetancourt.ant.task.spring;

import java.util.enumeration;
import java.util.properties;

import jbetancourt.ant.task.abstractcontexttask;

import org.apache.tools.ant.buildexception;
import org.apache.tools.ant.project;
import org.springframework.beans.beansexception;
import org.springframework.beans.factory.config.propertyoverrideconfigurer;
import org.springframework.context.applicationcontext;
import org.springframework.context.configurableapplicationcontext;
import org.springframework.context.support.filesystemxmlapplicationcontext;

/**
*
* ant task extension that invokes a pojo within a spring application
* context, a &quot;springdef&quot;.
*
* tested with ant 1.6.2, jdk 1.4.2, spring 1.1.4, and ognl 2.6.7.
*
* this task is added to ant with a taskdef or one of the new ant 1.6+
* approaches.
*
*
*
*  example taskdef:  
*  <taskdef name=&quot;springtask&quot;
*     classname=&quot;jbetancourt.springcontexttask&quot;
*     classpathref=&quot;taskdefclasspath&quot;>
*  
*  it is then used simply by accepting the defaults as:
*      <target name=&quot;test1&quot;>
*      <springtask beanlocations=&quot;applicationcontext.xml&quot;>
*      </springtask></target>        
*  
*  a more complex use is:
*     <target name=&quot;publishbean&quot;>
*           <springtask
*                      beanlocations=&quot;applicationcontext.xml&quot;
*                beanname=&quot;sitegenerator&quot;

*                      host=&quot;${host.site.url}&quot;
*                      port=&quot;${site.port}&quot;
*                      methodname=&quot;generatesite&quot;>
*                            in-line site post change text
*                </springtask>                    
*       </target>
*
*  
* use of methodcall element with cdata expression.
*
*   <target name=&quot;test8&quot;>
*      <springtask beanlocations=&quot;applicationcontext.xml&quot;
*                 beanname=&quot;antbean1&quot;>            
*           <methodcall><!&#91;cdata&#91;execute(&quot;goodbye&quot;)&#93;&#93;>
*                 </methodcall>            
*       </springtask>                
*   </target>                  
*
*
*
*
*
* @author jbetancourt
* @since jan 9, 2005  
*  
*/
public class springcontexttask extends abstractcontexttask {

    /** runtime spring context that that will be set or created */
    private applicationcontext applicationcontext;

    /**
     *
     * get the container managed bean.
     *
     * @param beanname
     * @return the pojo, singleton or non-singleton.
     */
    public object getbeanfromcontainer(string beanname) throws buildexception {
        try {
            if(applicationcontext == null){
                applicationcontext = new filesystemxmlapplicationcontext(
                    getbeanlocations().list());
            }

            return applicationcontext.getbean(getbeanname());
            
        } catch (beansexception e) {
            throw new buildexception(&quot;failure: could not get bean '&quot; +
                    getbeanname() + &quot;' from context&quot;,e);
        }        
    }
    
    /**
     * invoke target bean setters with ant specified dynamic properties.
     *
     */
    public void insertmanagedbeanproperties(){
        // how to programmatically post process a spring bean?
        // see spring forum thread for source of this approach.
        // http://forum.springframework.org/viewtopic.php?p=11833#11833
        propertyoverrideconfigurer poc = new propertyoverrideconfigurer();
        
        // the keys must be of form 'beanname.key'.
        // create a new properties with this format.
        properties props = addkeyprefix(getdynamicproperties(), getbeanname());        

        poc.setproperties(props);

        ((configurableapplicationcontext)applicationcontext).
            addbeanfactorypostprocessor(poc);
        ((configurableapplicationcontext)applicationcontext).refresh();        
    }
    
    /**
     * for each key in props, convert to 'beanname.key' format.
     *
     * @return with keys modified
     * @throws buildexception
     */
    private properties addkeyprefix(properties initprops,
            string prefix) throws buildexception {
        
        // todo:  this seems like a wrong approach here.  
        // can the passed in props be manipulated instead?

        
        properties props = new properties();
        
        for (enumeration en = initprops.propertynames();
                  en.hasmoreelements();) {
            string key = (string) en.nextelement();
            
            // let ant resolve any property replacements.
            string resolvedtext = getproject().replaceproperties(initprops.getproperty(key));
            props.put(prefix + &quot;.&quot; + key, resolvedtext);
        }
        
        return props;
    }

    /**
     * get the spring context that was created or set.
     * @return could be null
     */
    public applicationcontext getapplicationcontext() {
        return applicationcontext;
    }

    /**
     *
     *
     * @param applicationcontext non-null
     *            the applicationcontext to set.
     */
    public void setapplicationcontext(applicationcontext applicationcontext) {
        log(&quot;setting applicationcontext: &quot; + applicationcontext,
                project.msg_debug);
        this.applicationcontext = applicationcontext;
    }

    /* (non-javadoc)
     * @see jbetancourt.ant.task.externalcontainer#createcontainer()
     */
    public void createcontainer() throws buildexception {
        try {
            if(applicationcontext == null){
                applicationcontext = new filesystemxmlapplicationcontext(
                    getbeanlocations().list());
            }
            
        } catch (beansexception e) {
            throw new buildexception(&quot;failure: could not create spring container.&quot;,e);
        }        
    }
}


注意现在的spring实现是非常直接的。ant的两个特性保证了这种简单性。首先,任何异常都被ant的非强制异常buildexception所封装,这样就简化了更多的扩展。其次,使用ant的日志系统可以隐藏记录实现的细节。

主要的复杂性在于方法insertmanagedbeanproperties()。设置对象的属性是直接的,然而在容器框架中设置属性可能是很关键的。容器的语法必须被遵守:容器可能拥有或需要作用于属性设置操作的功能,如事件处理,自动绑定及转换。

aop实现

既然大多数ioc框架都支持aop,另一个可选的方法是利用ioc容器中的aop使他更加灵活。例如,在spring中,你可以创建一个专一的ant工厂bean来提供在ant属性中的绑定并且满足其他需求。但我没有在我的任务中使用他。

测试

ant提供对使用junit test子类的单元测试的支持,这使得task的测试更易管理。这篇文章对应的源程序中包含这些测试。当他运行时(用maven或ant),部分输出如下:

test:test:
    [junit] running jbetancourt.ant.task.abstractcontexttasktest
    [junit] tests run: 4, failures: 0, errors: 0, time elapsed: 1.752 sec
    [junit] running jbetancourt.ant.task.spring.springcontexttasktest
    [junit] tests run: 21, failures: 0, errors: 0, time elapsed: 4.256 sec
build successful
total time: 11 seconds


springcontexttasktest类已经有ant风格的任务测试。在这些测试中,junit子类的测试通过executetarget()或类似的方法执行ant文件中的目标:

   public void test2(){ 
      executetarget(&quot;test2&quot;);
  }      
  public void test3(){
      expectbuildexception(&quot;test3&quot;, &quot;beanlocation or class must be specified&quot;);
  }    


任务测试ant文件springcontexttasktest.xml中,包含了实际的测试目标,如:

   <target name=&quot;test3&quot;>  <!-- no context path or class specified; should fail -->
     <springtask methodname=&quot;length&quot;/></target>


并且在我们的扩展测试中,这个目标引用在applicationcontext.xml中的spring bean定义。这些bean定义由一个重用为不同的bean的java类来组成。例如:

   <bean id=&quot;antbean1&quot; class=&quot;jbetancourt.testbean1&quot;/>     
   <bean id=&quot;beanwithprops&quot; class=&quot;jbetancourt.testbean1&quot;/>
   <bean id=&quot;main&quot; class=&quot;org.springframework.beans.factory.
     config.methodinvokingfactorybean&quot;>
      <property name=&quot;targetobject&quot;><ref local=&quot;antbean&quot;/></property>
      <property name=&quot;targetmethod&quot;><value>execute<
        /value></property>
   </bean>  
                  

一个有趣的测试是test18,他使用一个aop代理返回的bean:

  <target name=&quot;test18&quot;>
      <springtask beanlocations=&quot;etc/contexts/applicationcontext.xml&quot;
        beanname=&quot;postbugreport&quot; methodname=&quot;doservice&quot;></springtask>
    </target>        
bean定义如下:
    <bean id=&quot;securityinterceptor&quot; class=&quot;jbetancourt.securityinterceptor&quot;>        
    </bean>
    
    <bean id=&quot;postbugreport&quot; class=&quot;org.springframework.aop.framework.proxyfactorybean&quot;>
        <property name=&quot;target&quot;><ref local=&quot;antbean&quot;/></property>
        <property name=&quot;proxyinterfaces&quot;>
            <list>
                <value>jbetancourt.isecurity</value>
            </list>
        </property>
        <property name=&quot;interceptornames&quot;>
            <list>
                <value>securityinterceptor</value>
            </list>

        </property>        
    </bean>


securityinterceptor可以是一个用于编译的安全advise,如登录过滤器(类似于servlet过滤器)。postbugreport目标与先前的antbean一致。

部署
ioc目标bean及定义文件或者类可以通过发布的jar文件来提供。我们可以在使用srping ioc时称这些为spring件。(如图2)


figure 2. springlet的用法。

使用有层次的外部文件可以重写在部署文档中的缺省值。因此用户只需要编辑外部的属性文件及任何子bean定义文件来自定义缺省值。外部文件可以定义其他协作的ioc jar。

总结
在这篇文章中,我介绍了一种ant扩展来执行ioc容器管理的对象,从而增加程序的松耦合度。相同的任务也可以执行在非管理对象的方法上。通过使用ognl,可以通过java表达式来执行方法,而不需要更复杂的xml定义。这里并没有讨论优化和利用antspring的高级特性。
虽然这里是通过ant扩展来表现这个观点,但同样可以用maven或其他构建系统来实现。同样的,这里使用的srping也可以用其他ioc框架来代替,如hivemind&mdash;需要ant任务必须定义一个模块发布描述文档,或者picocontainer&mdash;虽然他更偏好通过编程来配置。

关于作者
josef betancourt,生活在罗得岛的一个高级软件工程师。

资源
●下载与这篇文章对应的源程序:
http://www.javaworld.com/javaworld/jw-02-2005/antspring/jw-0214-antspring.zip
●ognl的好的介绍:&ldquo;ognl表达式及绑定语言&rdquo;,drew davidson (2004年3月)
http://www.ognl.org/resources/ognl_presentation_20040309.pdf
●为什么及如何使用轻量级容器:精通不使用ejbj2ee开, rod johnson, juergen hoeller (wrox, 2004年6月; isbn: 0764558315)
http://www.amazon.com/exec/obidos/asin/0764558315/javaworld
●&ldquo;ioc简介,sam newman (java.net, 2004年2月)
http://today.java.net/pub/a/today/2004/02/10/ioc.html
●ioc分析:&ldquo;控制反转容器和依赖注射模式&rdquo;,martin fowler (martinfowler.com):
http://martinfowler.com/articles/injection.html
●ioc对设计的影响:&ldquo;依赖注射和开放闭合设计&rdquo;
http://jroller.com/page/rickard/20040814#dependency_injection_and_open_vs
spring主页
http://www.springframework.org
ant主页
http://ant.apache.org/
●ognl主页
http://www.ognl.org/
●hivemind主页
http://jakarta.apache.org/hivemind/index.html
●picocontainer主页
http://picocontainer.org/
●pmd主页
http://pmd.sourceforge.net/
javancss主页
http://www.kclee.de/clemens/java/javancss/
●findbugs主页
http://findbugs.sourceforge.net/
●maven主页
http://maven.apache.org/
●更多关于spring的知识,可阅读&ldquo;pro spring: spring and ejb&rdquo;,(javaworld, 2005年2月):
http://www.javaworld.com/javaworld/jw-02-2005/jw-0214-springejb.html
●学习如何使用springjsf,可阅读&ldquo;让jsf开始工作&rdquo;,derek yang shen (javaworld, 2004年7月)
http://www.javaworld.com/javaworld/jw-07-2004/jw-0719-jsf.html
●更多关于设计模式的知识,可浏览设计javaworld的主题索引模式部分
http://www.javaworld.com/channel_content/jw-patterns-index.shtml
●更多java开发工具的文章,可以游览javaworld的主题索引的开发工具部分
http://www.javaworld.com/channel_content/jw-tools-index.shtml


 


关键字 本文所属关键字

相关 与本文相关文章

分类 所有文章关键字导航

源码编程相关

Java   Asp   PHP   .Net   XML   C/C++   CGI   VB   Jsp   J2ee   J2se   J2me   EJB   Servlet   Tomcat   Resin   Struts   Weblogic   Eclipse   ANT   GUI   JMS   Web servise   IDEA   Webphere   Hibernate   Spring   Jboss   Applet   Swing   Socket   Javamail   Perl   Ajax   P2P   安全   模式   框架   测试   开源   游戏

SQL数据库相关

My-SQL   Ms-SQL   Access   DB2   Oracle   Sybase   SQLserver   索引   存储过程   加密   数据库   分页   视图  

手机无线相关

3G   Wap   CDMA   GRPS   GSM   IVR   彩信   短信   无线   增值业务

网页设计制作相关

HTML   CSS   网页配色   网页特效   Javascript   VBscript   Dreamweaver   Frontpage   JS   Web   网站设计

网站建设推广相关

建站经验   网站优化   网站排名   推广   Alexa

操作系统/服务器相关

Windows XP   Windows 2000   Windows 2003   Windows Me   Windows 9.x   Linux   UNIX   注册表   操作系统   服务器   应用服务器

图形图像多媒体相关

Photoshop   Fireworks   Flash   Coreldraw   Illustrator   Freehand   Photoimpact   多媒体   图形图像

标准 网站致力的规范

Valid CSS!

无不良内容,无不良广告,无恶意代码

Valid XHTML 1.0 Transitional

creativecommons