Hudson插件开发入门体验

持续集成(CI)将软件项目流程的各个阶段进行自动化部署,从build, deploy, test automation到coverage分析,全部实现自动完成,而不需要每天的手工操作。

在敏捷开发过程中,持续集成大大提高了团队的工作效率,开发和测试人员可以专注于代码与测试用例的编写,而不需要过多关注编译和部署。每天夜晚进行持续集成的自动化部署,第二天可以马上开始测试和分析前日的测试效果与代码覆盖率,和敏捷开发的理念结合的恰到好处。

下面我就来介绍下如何开发一个Hudson的插件。


首先你要有Maven 2和JDK1.6以上,这是必须的。然后在你的Maven 2的setting.xml 文件中加入下列代码 

  1.  <pluginGroups>  
  2. <pluginGroup>org.jvnet.hudson.tools</pluginGroup>    
  3.  </pluginGroups>  
  4.   
  5.  <profiles>  
  6. <profile>    
  7.    <id>hudson</id>    
  8.    
  9.    <activation>    
  10.      <activeByDefault />    
  11.    </activation>    
  12.    
  13.    <pluginRepositories>    
  14.      <pluginRepository>    
  15.        <id>m.g.o-public</id>    
  16.        <url>http://maven.glassfish.org/content/groups/public/</url>    
  17.      </pluginRepository>    
  18.    </pluginRepositories>    
  19.    <repositories>    
  20.      <repository>    
  21.        <id>m.g.o-public</id>    
  22.        <url>http://maven.glassfish.org/content/groups/public/</url>    
  23.      </repository>    
  24.    </repositories>    
  25.  </profile>   
  26.  </profiles>  
  27.   
  28.  <activeProfiles>   
  29. <activeProfile>hudson</activeProfile>  
  30.  </activeProfiles>   

  
这样会将你的Maven指向有着Hudson-related Maven plugins的仓库,而且允许你使用Hudson Maven plugins的短名字来调用相关的插件(例如:hpi:create 代替org.jvnet.hudson.tools:maven-hpi-plugin:1.23:create)。 

接着在CMD中输入 
  1. mvn hpi:create  

之后会问你一些如groupId和artifactId之类的问题,groupId填写成你一般开发java代码的package信息,例如com.webex.slim.hudsonplugin,artifactId则是你编写此hudson插件的名称,例如buildslim。 

完成后计算机会自动的创建了一个项目,里面有一些模板代码,可供你学习如何开始写一个Hudson的插件,后面的代码全部来自模版代码。如果你需要在Eclipse里编辑插件可以执行  

  1. mvn -DdownloadSources=true eclipse:eclipse  
然后你就可以在Eclipse中导入这个项目并开始开发了。Eclipse导入maven工程,需要安装maven插件,然后在工程里选择导入已有项目即可。
 

执行前面的maven命令后,在我们的指定目录下已经生成了一个Hudson 插件的项目文件夹,这个目录应该是~/artifactId/。在Eclipse中导入这个项目,我们可以看见项目有如下的结构: 

  1. + src   
  2.     + main   
  3.         + java   
  4.              +  full.package.name   
  5.                     +- HelloWorldBuilder.java   
  6. +resources   
  7.              +  full.package.name   
  8.                     +- config.jelly   
  9.                     +- global.jelly   
  10.                 +- index.jelly   
  11.         + webapp   
  12.             +- help-globalConfig.html   
  13.             +- help-projectConfig.html   

 HelloWorldBuilder.java 

这个类就是具体实现某一扩展点的一个类,在这里由于要扩展Builder这个扩展点,所以继承了 Builder 这个类。在Hudson 中有很多不同种类的扩展点,比如Publisher、Recorder 等等。详细的说明可以参考Hudson 的网站。 


建好工程后,已经有一些代码是maven自动生成的hudson插件示例代码。


下面我来逐步分析这些代码 

  1. @DataBoundConstructor     
  2. public HelloWorldBuilder(String name) {     
  3.     this.name = name;     
  4. }     
  5.   
  6. /**   
  7.  * We'll use this from the <tt>config.jelly</tt>.   
  8.  */     
  9. public String getName() {     
  10.     return name;     
  11. }    

这段代码用于构造这个Bulider并且从相应的config.jelly中获取相应的参数。Hudson使用了一种叫structured form submission的技术,使得可以使用这种方式活动相应的参数。 

  1. public boolean perform(Build build, Launcher launcher, BuildListener listener) {     
  2.      // this is where you 'build' the project     
  3.      // since this is a dummy, we just say 'hello world' and call that a build     
  4.   
  5.      // this also shows how you can consult the global configuration of the builder     
  6.      if(DESCRIPTOR.useFrench())     
  7.          listener.getLogger().println("Bonjour, "+name+"!");     
  8.      else     
  9.          listener.getLogger().println("Hello, "+name+"!");     
  10.     return true;     
  11. }     



方法perform()是个很重要的方法,当插件运行的的时候这个方法会被调用。相应的业务逻辑也可以在这里实现。比如这个perform()方法就实现了怎么说 “Hello” 

接下来,在HelloBuilder 这个类里面有一个叫 DescriptorImpl 的内部类,它继承了Descriptor。在Hudson 的官方说明文档里说Descriptor包含了一个配置实例的元数据。打个比方,我们在工程配置那里对插件进行了配置,这样就相当于创建了一个插脚的实例,这时候就需要一个类来存储插件的配置数据,这个类就是Descriptor。 

 
  1. public String getDisplayName() {     
  2.     return "Say hello world";     
  3. }     
如上面的代码,可以在Descriptor的这个方法下设置插件在工程配置页面下现实的名字 


  1. public boolean configure(StaplerRequest req, JSONObject o) throws FormException {     
  2.      // to persist global configuration information,     
  3.      // set that to properties and call save().     
  4.      useFrench = o.getBoolean("useFrench");     
  5.      save();     
  6.      return super.configure(req);     
  7. }    
如同注释属所说,这个方法用于将全局配置存储到项目中 

 

注意点:

HUDSON_HOME:

Hudson需要一个位置来进行每次构建,保留相关的配置信息,以及保存测试的结果,这就是在部署好了Hudson环境以后,系统就会自动在当前用户下新建一个.hudson,在linux下如:~/.hudson,我们有三种方式来改变这个路径:

1.  在启动servlet容器之前,设置:“HUDSON_HOME”环境变量,指向你需要设定的目录

2.  在servlet容器之中,设定系统属性

3.  设置一个JNDI的环境实体<env-entry>“HUDSON_HOME”指向您所想要设定的目录

目前我们在glassfish中设置jvm-option的方式属于第二种。

当我们设置好这个变量以后想要换个目录,但又不想丢掉以前的配置怎么办,很简单,关闭Hudson,将目录A的内容拷贝的目录B中去,然后重新设定“HUDSON_HOME”的值,然后重启,你会发现之前你所作的所有配置都完好的保留了下来

1、Hudson-home的目录结构:

HUDSON_HOME

 +- config.xml     (hudson的基本配置文件,如:jdk的安装路径)

 +- *.xml          (其他系统相关的配置文件,比如:旺旺插件的全局配置信息)

 +- fingerprints   (储存文件版本跟踪记录信息)

 +- plugins        (存放插件)

 +- jobs

     +- [JOBNAME]      (任务名称,对应页面上的project name)

         +- config.xml     (任务配置文件,类似于CC的config.xml中各个项目的配置)

         +- workspace      (SCM所用到的目录,hudson所下载代码默认存放在这个目录)

         +- builds

             +- [BUILD_ID]     (每一次构建的序号)

                 +- build.xml      (构建结果汇总)

                 +- log            (运行日志)

                 +- changelog.xml  (SCM修改日志)

小提示:如果你使用了e-mail来接受测试消息,并且hudson的迁移设计到不同ip地址机器的迁移的话,可能需要去Hudson的主配置中修改一下Hudson的访问地址

workspace:

刚才在hudson-home的目录结构中已经看到了workspce,假设当前hudson-home为/home/hudson-home,那么当我们在hudson上配置一个项目demo的时候,就会在新建一个目录/home/hudson-home/demo,在第一次运行之前,在jobs下并没有demo这个文件夹,只有当第一次运行以后才会在jobs目录下创建demo目录,当代码顺利从svn上下载下来时才会创建workspace文件夹,所有从svn下载下来的代码都会存放在这个目录下。

1、相对路径:

项目配置过程中,Hudson使用的是相对路径,对于Hudson,在我们新建一个项目比如demo后,假设workspace的目录结构为:

workspace

 +- demo

     +- pom.xml

     +- src

那么测试报告的路径就为demo/target/surefire-reports/*.xml,系统会自动去当前项目的workspace中去寻找这个路径

 

mvn package  -- 完成代码开发之后执行,按照pom.xml 中的配置信息将会打包为hpi 格式的插件文件,这个就是你最终可以拿来上传给你的hudson 平台的玩意

mvn hpi:run   -- 在本地的Jetty 中运行你的hudson 插件,调试专用,当然可以使用Debug 模式,执行之后,在本地访问http://localhost:8080/ 即可见(注意不要占用8080 端口)

mvnDebug hup:run ,debug调试模式


下面贴出一个我自己写的用于项目构建,自动编译打包的Hudson插件源代码。

HelloWorldBuilder.java

  1. package zygroup;  
  2. import hudson.FilePath;  
  3. import hudson.Launcher;  
  4. import hudson.Extension;  
  5. import hudson.Proc;  
  6. import hudson.util.FormValidation;  
  7. import hudson.model.AbstractBuild;  
  8. import hudson.model.BuildListener;  
  9. import hudson.model.AbstractProject;  
  10. import hudson.remoting.Channel;  
  11. import hudson.tasks.Builder;  
  12. import hudson.tasks.BuildStepDescriptor;  
  13. import net.sf.json.JSONObject;  
  14. import org.kohsuke.stapler.DataBoundConstructor;  
  15. import org.kohsuke.stapler.StaplerRequest;  
  16. import org.kohsuke.stapler.QueryParameter;  
  17.   
  18. import javax.servlet.ServletException;  
  19. import java.io.IOException;  
  20.   
  21.   
  22. public class HelloWorldBuilder extends Builder {  
  23.   
  24.     private final String locate;  
  25.     private final String cmd;  
  26.   
  27.     // Fields in config.jelly must match the parameter names in the "DataBoundConstructor"  
  28.     @DataBoundConstructor  
  29.     public HelloWorldBuilder(String locate, String cmd) {  
  30.         this.locate = locate;  
  31.         this.cmd = cmd;  
  32.     }  
  33.   
  34.     /** 
  35.      * We'll use this from the <tt>config.jelly</tt>. 
  36.      */  
  37.     public String getLocate() {  
  38.         return locate;  
  39.     }  
  40.   
  41.     public String getCmd() {  
  42.         return cmd;  
  43.     }  
  44.       
  45.     @Override  
  46.     public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) {  
  47.         listener.getLogger().println("The SLiM build home is "+locate+".");  
  48.         listener.getLogger().println("The SLiM build command is "+cmd+".");  
  49.            
  50.         try {  
  51.             FilePath path = new FilePath(Channel.current(),locate);  
  52.             Proc proc = launcher.launch(cmd, build.getEnvVars(), listener.getLogger(),path);  
  53.             int exitCode = proc.join();  
  54.             if (exitCode != 0return false;  
  55.   
  56.             return true;  
  57.           } catch (IOException e) {  
  58.             e.printStackTrace();  
  59.             listener.getLogger().println("IOException !");  
  60.             return false;  
  61.           } catch (InterruptedException e) {  
  62.             e.printStackTrace();  
  63.             listener.getLogger().println("InterruptedException!");  
  64.             return false;  
  65.           }  
  66.     }  
  67.   
  68.     @Override  
  69.     public DescriptorImpl getDescriptor() {  
  70.         return (DescriptorImpl)super.getDescriptor();  
  71.     }  
  72.   
  73.     @Extension // this marker indicates Hudson that this is an implementation of an extension point.  
  74.     public static final class DescriptorImpl extends BuildStepDescriptor<Builder> {  
  75.   
  76.         public FormValidation doCheckName(@QueryParameter String value) throws IOException, ServletException {  
  77.             if(value.length()==0)  
  78.                 return FormValidation.error("Please set a name");  
  79.             if(value.length()<4)  
  80.                 return FormValidation.warning("Isn't the name too short?");  
  81.             return FormValidation.ok();  
  82.         }  
  83.   
  84.         public boolean isApplicable(Class<? extends AbstractProject> aClass) {  
  85.             // indicates that this builder can be used with all kinds of project types   
  86.             return true;  
  87.         }  
  88.   
  89.         public String getDisplayName() {  
  90.             return "SLiM build";  
  91.         }  
  92.   
  93.         @Override  
  94.         public boolean configure(StaplerRequest req, JSONObject formData) throws FormException {  
  95.             save();  
  96.             return super.configure(req,formData);  
  97.         }  
  98.   
  99.   
  100.     }  
  101. }  
设置插件相关的用户输入页面的文件config.jelly
  1. <j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">  
  2.   <!--  
  3.     This jelly script is used for per-project configuration.  
  4.   
  5.     See global.jelly for a general discussion about jelly script.  
  6.   -->  
  7.   
  8.   <!--  
  9.     Creates a text field that shows the value of the "name" property.  
  10.     When submitted, it will be passed to the corresponding constructor parameter.  
  11.   -->  
  12.   <f:entry title="Build Home" help="plugin/zyartifact/WEB-INF/classes/zygroup/HelloWorldBuilder/help-buildhome.html">  
  13.     <f:textbox name="locate" type="text" value="${instance.locate}"/>  
  14.   </f:entry>  
  15.    <f:entry title="Build Command" help="plugin/zyartifact/WEB-INF/classes/zygroup/HelloWorldBuilder/help-cmd.html">  
  16.     <f:textbox name="cmd" type="text" value="${instance.cmd}"/>  
  17.   </f:entry>  
  18. </j:jelly>  

其中<f:entry>的help属性指向了一个html文件,位于代码中设置的位置下,可以写入标准的html标记,用于在此输入框右边显示帮助按钮和点出帮助信息。

该插件的主要输入内容是:

locate和cmd两个字符串,传递给build程序使用,成为locate和cmd两个变量。用于用户输入构建代码的目录和需要启动构建的命令。

例如

/opt/CruiseControl/apache-ant-1.7.0/

ant antbuild

build程序得到这两个变量后,就启动shell并在locate目录下执行cmd命令。这个功能在perform函数中实现。

  1.  public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) {  
  2.   
  3. /向hudson运行控制台输出日志信息  
  4.      listener.getLogger().println("The SLiM build home is "+locate+".");  
  5.      listener.getLogger().println("The SLiM build command is "+cmd+".");  
  6.         
  7.      try {  
  8. //将locate字符串转化为hudson的FilePath类型  
  9.         FilePath path = new FilePath(Channel.current(),locate);  
  10.   
  11. //在path路径下执行cmd命令  
  12.          Proc proc = launcher.launch(cmd, build.getEnvVars(), listener.getLogger(),path);  
  13.   
  14. //如果shell结果为失败,则返回失败  
  15.          int exitCode = proc.join();  
  16.          if (exitCode != 0return false;  
  17.   
  18. //返回成功  
  19.          return true;  
  20.        } catch (IOException e) {  
  21.     ......  
  22.        }  
  23.  }  

然后在windows的cmd或者linux的控制台中该项目目录下,键入mvn package,即可自动生成target目录下的文件,包括一个hpi文件和jar文件。

将hpi拷贝到hudson目录的plugin目录下,或者通过hudson的页面上传插件,重启hudson,即可使用。

这个插件是一个build类型的插件,会在hudson的job配置页面,出现在build step下拉菜单中,名字由HelloWorldBuilder.java的下面一个函数控制:

  1. public String getDisplayName() {  
  2.     return "SLiM build";  
  3. }  

插件在hudson已安装插件列表中显示的名字,由该maven项目的poe.xml配置:
  1. <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">  
  2.       
  3.     <modelVersion>4.0.0</modelVersion>  
  4.   
  5.     <parent>  
  6.         <groupId>org.jvnet.hudson.plugins</groupId>  
  7.         <artifactId>hudson-plugin-parent</artifactId>  
  8.         <version>2.1.1</version><!-- which version of Hudson is this plugin built against? -->  
  9.     </parent>  
  10.   
  11.     <groupId>zygroup</groupId>  
  12.     <artifactId>zyartifact</artifactId>  
  13.     <version>1.0-SNAPSHOT</version>  
  14.   
  15.     <packaging>hpi</packaging>  
  16.   
  17.     <name>SLiM build</name>  
  18.   
  19. </project>  

这样一个实现项目自动构建的简单插件就可以使用了^.^
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值