开发背景
在项目开发的过程中,出于JSP性能方面考虑,在条件允许的情况下我们常常会采用静态include的形式来替代动态include。
<@include file="reuse.html"> => <jsp:include page="reuse.html" />
但静态包含为开发带来的副作用就是,每次修改了被包含页面时,必须同时刷新属主文件,否则修改过的内容不会被体现出来,所以开发者常常会在属主文件中随意添加或删除几个空白字符并保存,为了就是能够让文件的最后修改时间得到更新,迫使服务器重新编译整个JSP,从而使得被包含的JSP内容可以正确地展现出来。
但是每次手工刷新比较耗时,而且在涉及到多个文件的时候操作起来也比较繁琐,所以就萌生了一个利用eclipse插件来进行项目文件刷新的想法。
项目一览
由于这个插件功能很少,所以我们只需要准备好最基本条件即可,首先创建一个插件工程,然后就会看到一个如下的文件目录结构。
C:\WORKSTATION\WORKSPACE\INFO.WOODY.TOOL.TOUCHIT | .classpath | .project | build.properties | plugin.xml | +---icons | touchjsp.jpeg | +---META-INF | MANIFEST.MF | \---src \---info \---woody \---tool \---touchit | Activator.java | \---handlers TouchHandler.java
icons目录中的touchjsp.jpeg文件就是我们的插件图标,总得有个脸面不是:)

Activator.java是自动生成的,不必理会,它就是一个插件装载的入口程序。
TouchHandler.java是核心内容,就是帮助我们刷新文件用的。
其余的五个就是配置文件,.classpath和.project是每个Java工程必不可少的元信息。build.properties/plugin.xml/MANIFEST.MF是与插件相关的配置文件。
项目配置
.classpath
.project
插件配置
插件的配置文件(build.properties/plugin.xml/MANIFEST.MF)都可以在插件编辑器中直接编辑,无需操作原始文件。当然,如果你对手工操作比较有自信的话也可以直接操作原始文件的。下面以MANIFEST.MF为例来说明如何在插件编辑器中对配置文件进行编辑。
插件编辑器的下方把配置文件分配到几个不同的选项卡中,MANIFEST.MF对应前两个选项卡,分别是Overview和Dependencies。我们先来看下MANIFEST.MF的原始文件内容:
Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: Touchit Bundle-SymbolicName: info.woody.tool.touchit;singleton:=true Bundle-Version: 1.0.0.qualifier Bundle-Activator: info.woody.tool.touchit.Activator Require-Bundle: org.eclipse.ui, org.eclipse.core.runtime, org.eclipse.core.resources, org.eclipse.ui.ide Bundle-RequiredExecutionEnvironment: J2SE-1.5 Eclipse-AutoStart: true
接下来是选项卡Overview和Dependencise的截图。从截图中我们可以看出,插件的基本信息都在Overview选项卡中,而依赖包都分配在Dependencies选项卡中。除了MANIFEST.MF外,另外两个配置文件也都有各自对应的选项卡,具体信息可以从下面的对应关系中找到。其实不论是在编辑器中编辑,还是直接修改原始文件的内容,这两种操作的结果都是一样的。
插件编辑器选项卡与配置文件对照关系
- MANIFEST.MF(Overview,Dependencies)
- plugin.xml(Extension)
- build.properties(Build)
plugin.xml源码
<?xml version="1.0" encoding="UTF-8"?> <?eclipse version="3.0"?> <plugin> <extension point="org.eclipse.ui.commands"> <category name="Touchit Category" id="info.woody.tool.touchit.commands.category"> </category> <command name="info.woody.tool.touchit" categoryId="info.woody.tool.touchit.commands.category" id="info.woody.tool.touchit.commands.touchCommand"> </command> </extension> <extension point="org.eclipse.ui.handlers"> <handler commandId="info.woody.tool.touchit.commands.touchCommand" class="info.woody.tool.touchit.handlers.TouchHandler"> </handler> </extension> <extension point="org.eclipse.ui.bindings"> <key commandId="info.woody.tool.touchit.commands.touchCommand" contextId="org.eclipse.ui.contexts.window" sequence="M1+0" schemeId="org.eclipse.ui.defaultAcceleratorConfiguration"> </key> </extension> <extension point="org.eclipse.ui.menus"> <!-- <menuContribution locationURI="menu:org.eclipse.ui.main.menu?after=additions"> <menu label="Sample Menu" mnemonic="M" id="info.woody.tool.touchit.menus.touchMenu"> <command commandId="info.woody.tool.touchit.commands.touchCommand" mnemonic="S" id="info.woody.tool.touchit.menus.touchCommand"> </command> </menu> </menuContribution> --> <menuContribution locationURI="toolbar:org.eclipse.ui.main.toolbar?after=additions"> <toolbar id="info.woody.tool.touchit.toolbars.touchToolbar"> <command commandId="info.woody.tool.touchit.commands.touchCommand" icon="icons/touchjsp.jpeg" tooltip="Touch JSP within selected files/folders" id="info.woody.tool.touchit.toolbars.touchCommand"> </command> </toolbar> </menuContribution> </extension> </plugin>
build.properties源码
source.. = src/ output.. = bin/ bin.includes = plugin.xml,\ META-INF/,\ .,\ icons/
Java程序
方法execute是插件执行时候的入口方法,我们会根据View类型来选择是否执行后续逻辑,如果当前活动View为ResourceNavigator/ProjectExplorer/PackageExplorer,那么我们就会根据View里选中的资源节点来查找JSP文件,如果不是这三种View,后续代码就没有必要执行了。利用Alt+Shift+F1可以找出我们当前的活动View,这个功能有点类似与Visual Studio中的工具Spy++。
if (activePage.isPartVisible(viewPart)) { if ("org.eclipse.ui.views.ResourceNavigator".equals(viewPart.getViewSite().getId())) { break; } if ("org.eclipse.ui.navigator.ProjectExplorer".equals(viewPart.getViewSite().getId())) { break; } if ("org.eclipse.jdt.ui.PackageExplorer".equals(viewPart.getViewSite().getId())) { break; } }
余下的逻辑就是循环遍历选中的那些资源节点以及资源节点子目录中的JSP文件,然后更新文件的最后修改时间属性。具体的内部调用逻辑如下:

源码如下:
package info.woody.tool.touchit.handlers; import java.io.File; import java.util.Arrays; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.Stack; import org.eclipse.core.commands.AbstractHandler; import org.eclipse.core.commands.ExecutionEvent; import org.eclipse.core.commands.ExecutionException; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IFolder; import org.eclipse.core.resources.IMarker; import org.eclipse.core.resources.IResource; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.jobs.Job; import org.eclipse.jface.viewers.ITreeSelection; import org.eclipse.ui.IViewPart; import org.eclipse.ui.IViewReference; import org.eclipse.ui.IWorkbench; import org.eclipse.ui.IWorkbenchPage; import org.eclipse.ui.IWorkbenchWindow; import org.eclipse.ui.PlatformUI; public class TouchHandler extends AbstractHandler { public Object execute(ExecutionEvent event) throws ExecutionException { IWorkbench workbench = PlatformUI.getWorkbench(); IWorkbenchWindow window = workbench == null ? null : workbench.getActiveWorkbenchWindow(); IWorkbenchPage activePage = window == null ? null : window.getActivePage(); IViewPart viewPart = null; for (IViewReference viewRef : activePage.getViewReferences()) { // only work with visible one viewPart = viewRef.getView(false); if (viewPart == null) { continue; } if (activePage.isPartVisible(viewPart)) { if ("org.eclipse.ui.views.ResourceNavigator".equals(viewPart.getViewSite().getId())) { break; } if ("org.eclipse.ui.navigator.ProjectExplorer".equals(viewPart.getViewSite().getId())) { break; } if ("org.eclipse.jdt.ui.PackageExplorer".equals(viewPart.getViewSite().getId())) { break; } } viewPart = null; } if (viewPart == null) { return null; } List<?> files = ((ITreeSelection)viewPart.getSite().getSelectionProvider().getSelection()).toList(); Set<IResource> pathSet = scanSelectedFiles(files); startTouchJob(pathSet); return null; } /** * Start a job to execute "Touch" JSP files in order to refresh the last modified date time * * @param pathSet */ private void startTouchJob(final Set<IResource> pathSet) { if (null == pathSet || 0 == pathSet.size()) { return; } // http://eclipse-tips.com/how-to-guides/4-progress-bars-in-eclipse-ui?showall=1 Job job = new Job("Touch files") { @Override protected IStatus run(IProgressMonitor monitor) { try { monitor.beginTask("Touching files ...", pathSet.size()); int workedCount = 0; for (IResource resource : pathSet) { try { clearInternalError(resource); if (!new File(resource.getRawLocation().toOSString()).setLastModified(new Date().getTime())) { saveInternalError(resource); } resource.refreshLocal(IResource.DEPTH_ZERO, null); } catch (Exception e) { saveInternalError(resource); } monitor.worked(++workedCount); } } finally { monitor.worked(pathSet.size()); monitor.done(); } return Status.OK_STATUS; } }; job.schedule(); } /** * Scan all path given in parameter files to find JSP files * * @param files * @return */ private Set<IResource> scanSelectedFiles(List<?> files) { final Set<IResource> pathSet = new HashSet<IResource>(); for (Object resource : files) { if (resource instanceof IResource) { this.savePath((IResource)resource, pathSet); } } return pathSet; } /** * Accept a JSP file or a directory and save all JSP file paths to parameter "pathSet" * * @param resource * @param pathSet */ private void savePath(IResource resource, Set<IResource> pathSet) { if (isJsp(resource)) { pathSet.add(resource); return; } else if (!(resource instanceof IFolder)) { return; } // http://stackoverflow.com/questions/6776252/non-recursive-way-to-get-all-files-in-a-directory-and-its-subdirectories-in-java Stack<IResource> stack = new Stack<IResource>(); try { stack.push(resource); while (!stack.isEmpty()) { IResource child = stack.pop(); if (child instanceof IFolder) { stack.addAll(Arrays.asList(((IFolder) child).members())); } else if (isJsp(child)) { pathSet.add(child); } } } catch (CoreException e) { } } /** * * @param resource * @return */ private boolean isJsp(IResource resource) { return resource instanceof IFile && "jsp".equalsIgnoreCase(resource.getFileExtension()); } /** * Clear markers in [Problem] view those are marked by Touchit plugin * * @param resource */ private void clearInternalError(IResource resource) { try { IMarker[] markers = resource.findMarkers(IMarker.PROBLEM, true, IResource.DEPTH_INFINITE); if (null == markers || 0 == markers.length) { return; } for (IMarker eachMarker : markers) { if ("File cannot be touched.".equals(eachMarker.getAttribute(IMarker.MESSAGE))) { eachMarker.delete(); } } } catch (CoreException e) { } } /** * Save a marker in [Problem] view * * @param resource */ private void saveInternalError(IResource resource) { try { IMarker marker = resource.createMarker(IMarker.PROBLEM); marker.setAttribute(IMarker.SEVERITY, IMarker.SEVERITY_ERROR); marker.setAttribute(IMarker.MESSAGE, "File cannot be touched."); marker.setAttribute(IMarker.LOCATION, resource.getRawLocation().toOSString()); } catch (CoreException e) { } } }
插件输出
从eclipse选择File -> Export,然后按照以下步骤操作就可以得到JAR文件。


可扩展的功能
这个插件是主动式选择资源节点,其实我们可以创建一个后台线程来记录曾经工作的目录,然后实时进行后台文件刷新。当使用者们对文件操作特别频繁,手动会影响他们的工作效率的时候,这项功能就显得尤为重要。另外,这个功能必须限制在一定的使用条件范围内,比如目标资源中的文件数量不能过多,否则就可能对性能有所影响。