Jenkins插件开发(6.1)—— 分析JenkinsJOB的CRUD源码
Posted on 2013-04-19 11:03 Bruce Zhang 阅读(933) 评论(0) 编辑 收藏 举报标题上说CRUD,其实不太准确。
我的这个插件不关注Retrieve操作,所以只研究:Create Job, Update Job, Rename Job, Delete Job
1. Create(创建JOB)
Jenkins Job的创建有三种方式:
- 通过Copy已有JOB创建
- 通过CLI命令远程创建
- 通过Jenkins创建Job页面填写相关配置信息来创建。
这三种方式的实现在hudson.model.ItemGroupMixIn中实现,源码片段如下:
/** * Copies an existing {@link TopLevelItem} to a new name. * * The caller is responsible for calling {@link ItemListener#fireOnCopied(Item, Item)}. This method * cannot do that because it doesn't know how to make the newly added item reachable from the parent. */
// Copy已有JOB创建新JOB(页面Copy和CLI命令Copy都调用这个方法) @SuppressWarnings({"unchecked"}) public synchronized <T extends TopLevelItem> T copy(T src, String name) throws IOException { acl.checkPermission(Job.CREATE); T result = (T)createProject(src.getDescriptor(),name,false); // copy config Util.copyFile(Items.getConfigFile(src).getFile(),Items.getConfigFile(result).getFile()); // reload from the new config result = (T)Items.load(parent,result.getRootDir()); result.onCopiedFrom(src); add(result);
//这就是我想要看到的:触发ItemListener监听器
//只要注册一个扩展点(@extension),并继承ItemListener,就可捕获这个动作
ItemListener.fireOnCopied(src,result); Hudson.getInstance().rebuildDependencyGraph(); return result; }
// CLI命令远程创建JOB public synchronized TopLevelItem createProjectFromXML(String name, InputStream xml) throws IOException { acl.checkPermission(Job.CREATE); Jenkins.getInstance().getProjectNamingStrategy().checkName(name); // place it as config.xml File configXml = Items.getConfigFile(getRootDirFor(name)).getFile(); configXml.getParentFile().mkdirs(); try { IOUtils.copy(xml,configXml); // load it TopLevelItem result = (TopLevelItem)Items.load(parent,configXml.getParentFile()); add(result);
//这就是我想要看到的:触发ItemListener监听器
//只要注册一个扩展点(@extension),并继承ItemListener,就可捕获这个动作
ItemListener.fireOnCreated(result);
Jenkins.getInstance().rebuildDependencyGraph(); return result; } catch (IOException e) { // if anything fails, delete the config file to avoid further confusion Util.deleteRecursive(configXml.getParentFile()); throw e; } }
// 填写配置信息创建JOB public synchronized TopLevelItem createProject( TopLevelItemDescriptor type, String name, boolean notify ) throws IOException { acl.checkPermission(Job.CREATE); Jenkins.getInstance().getProjectNamingStrategy().checkName(name); if(parent.getItem(name)!=null) throw new IllegalArgumentException("Project of the name "+name+" already exists"); TopLevelItem item; try { item = type.newInstance(parent,name); } catch (Exception e) { throw new IllegalArgumentException(e); } try { callOnCreatedFromScratch(item); } catch (AbstractMethodError e) { // ignore this error. Must be older plugin that doesn't have this method } item.save(); add(item);
//这就是我想要看到的:触发ItemListener监听器
//只要注册一个扩展点(@extension),并继承ItemListener,就可捕获这个动作
if (notify) ItemListener.fireOnCreated(item);
return item; }
2. Update(更新JOB)
Jenkins Job的Update有两种方式:
- 通过配置页面修改
- 通过CLI命令远程更新
这三种方式的实现在hudson.model.AbstractItem中实现,源码片段如下:
/** * Updates Job by its XML definition. */ public void updateByXml(Source source) throws IOException { checkPermission(CONFIGURE); XmlFile configXmlFile = getConfigFile(); AtomicFileWriter out = new AtomicFileWriter(configXmlFile.getFile()); try { try { // this allows us to use UTF-8 for storing data, // plus it checks any well-formedness issue in the submitted // data Transformer t = TransformerFactory.newInstance() .newTransformer(); t.transform(source, new StreamResult(out)); out.close(); } catch (TransformerException e) { throw new IOException2("Failed to persist configuration.xml", e); } // try to reflect the changes by reloading new XmlFile(Items.XSTREAM, out.getTemporaryFile()).unmarshal(this); onLoad(getParent(), getRootDir().getName()); Jenkins.getInstance().rebuildDependencyGraph(); // if everything went well, commit this new version out.commit();
//这就是我想要看到的:触发SaveableListener监听器
//只要注册一个扩展点(@extension),并继承SaveableListener,就可捕获这个动作 SaveableListener.fireOnChange(this, getConfigFile());
} finally { out.abort(); // don't leave anything behind } }
3. Rename(重名JOB)
Jenkins Job的Rename只有一种方式:
- 通过配置页面修改名称->提交->同意改名
也是在hudson.model.AbstractItem中实现,源码片段如下:
/** * Renames this item. * Not all the Items need to support this operation, but if you decide to do so, * you can use this method. */ protected void renameTo(String newName) throws IOException { // always synchronize from bigger objects first final ItemGroup parent = getParent(); synchronized (parent) { synchronized (this) { // sanity check if (newName == null) throw new IllegalArgumentException("New name is not given"); // noop? if (this.name.equals(newName)) return; Item existing = parent.getItem(newName); if (existing != null && existing!=this) // the look up is case insensitive, so we need "existing!=this" // to allow people to rename "Foo" to "foo", for example. // see http://www.nabble.com/error-on-renaming-project-tt18061629.html throw new IllegalArgumentException("Job " + newName + " already exists"); String oldName = this.name; File oldRoot = this.getRootDir(); doSetName(newName); File newRoot = this.getRootDir(); boolean success = false; try {// rename data files boolean interrupted = false; boolean renamed = false; // try to rename the job directory. // this may fail on Windows due to some other processes // accessing a file. // so retry few times before we fall back to copy. for (int retry = 0; retry < 5; retry++) { if (oldRoot.renameTo(newRoot)) { renamed = true; break; // succeeded } try { Thread.sleep(500); } catch (InterruptedException e) { // process the interruption later interrupted = true; } } if (interrupted) Thread.currentThread().interrupt(); if (!renamed) { // failed to rename. it must be that some lengthy // process is going on // to prevent a rename operation. So do a copy. Ideally // we'd like to // later delete the old copy, but we can't reliably do // so, as before the VM // shuts down there might be a new job created under the // old name. Copy cp = new Copy(); cp.setProject(new org.apache.tools.ant.Project()); cp.setTodir(newRoot); FileSet src = new FileSet(); src.setDir(getRootDir()); cp.addFileset(src); cp.setOverwrite(true); cp.setPreserveLastModified(true); cp.setFailOnError(false); // keep going even if // there's an error cp.execute(); // try to delete as much as possible try { Util.deleteRecursive(oldRoot); } catch (IOException e) { // but ignore the error, since we expect that e.printStackTrace(); } } success = true; } finally { // if failed, back out the rename. if (!success) doSetName(oldName); } callOnRenamed(newName, parent, oldName);
//这里也会触发监听器
//只要注册一个扩展点(@extension),并继承ItemListener,就可捕获这个动作 for (ItemListener l : ItemListener.all()) l.onRenamed(this, oldName, newName); } } }
4. Delete(删除JOB)
Jenkins Job的Delete有两种方式:
- 通过页面直接删除
- 通过CLI命令远程删除
这两种方式的实现在hudson.model.AbstractItem中实现,源码片段如下:
/** * Deletes this item. */ @CLIMethod(name="delete-job") @RequirePOST public void doDoDelete( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException, InterruptedException { delete(); if (rsp != null) // null for CLI rsp.sendRedirect2(req.getContextPath()+"/"+getParent().getUrl()); }
public void delete( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException { try { doDoDelete(req,rsp); } catch (InterruptedException e) { // TODO: allow this in Stapler throw new ServletException(e); } } /** * Deletes this item. * * <p> * Any exception indicates the deletion has failed, but {@link AbortException} would prevent the caller * from showing the stack trace. This */ public synchronized void delete() throws IOException, InterruptedException { checkPermission(DELETE); performDelete(); try { invokeOnDeleted(); } catch (AbstractMethodError e) { // ignore } Jenkins.getInstance().rebuildDependencyGraph(); } /** * A pointless function to work around what appears to be a HotSpot problem. See JENKINS-5756 and bug 6933067 * on BugParade for more details. */ private void invokeOnDeleted() throws IOException { getParent().onDeleted(this); } /** * Does the real job of deleting the item. */ protected void performDelete() throws IOException, InterruptedException { getConfigFile().delete(); Util.deleteRecursive(getRootDir()); }
getParent().onDeleteed(this)方法在jenkins.model.Jenkins定义,如下:
/** * Called in response to {@link Job#doDoDelete(StaplerRequest, StaplerResponse)} */ public void onDeleted(TopLevelItem item) throws IOException {
//同样,这里也触发了监听器。只要注册一个扩展点(@extension),并继承ItemListener,就可捕获这个动作。 for (ItemListener l : ItemListener.all()) l.onDeleted(item); items.remove(item.getName()); for (View v : views) v.onJobRenamed(item, item.getName(), null); save(); }
另外,这里是使用注解的方式注册CLI的,除了@CLIMethod外,还有@CLIResolver配套使用:
/** * Used for CLI binding. */ @CLIResolver public static AbstractItem resolveForCLI( @Argument(required=true,metaVar="NAME",usage="Job name") String name) throws CmdLineException { AbstractItem item = Jenkins.getInstance().getItemByFullName(name, AbstractItem.class); if (item==null) throw new CmdLineException(null,Messages.AbstractItem_NoSuchJobExists(name,AbstractProject.findNearest(name).getFullName())); return item; }
关于使用注解的方式注册CLI命令,请参照:https://wiki.jenkins-ci.org/display/JENKINS/Writing+CLI+commands