I hacked together an Eclipse @Autowired plugin for a 24 hour innovation day challenge at work. I had not written an Eclipse plugin before and it turned out to be a more complex task than I imagined.

What I like about IDEs is that I can navigate quickly between references, declarations, definitions, superclasses, and subclasses. Pretty much all code relationships that are known at compile time can be navigated. Dependency injection, which I use all the time when I program something in Spring, establishes relationships at runtime and IDEs (or at least Eclipse, which is what I mainly use) don’t know anything about them. There seem to be Eclipse plugins that visualise these relationships but what I want is to right-click on a field and ask Eclipse to take me to the definitions of the bean that’s autowired into it (yes, we mainly use the @Autowired annotation at work). What I don’t want is to open a separate view or even a graph showing bean relationships. I want to see them in the code just like normal code relationships.

Here is an example of an @Autowired pattern that we commonly use at work:

A piece of Java code showing the use of the Spring @Autowired annotation

The @Autowired plugin currently discovers autowired methods and the fields they set. It then highlights them in Eclipse by drawing a green box around them, and it shows an icon in the margin:

A piece of Java code in the Eclipse editor with highlighted autowired elements

I did not have time to make the plugin allow me to navigate to the bean definition, but the autowired beans are discovered and you can see them when you mouse over the marker in the margin:

A mouse over text showing the wired bean name

Now to the nitty gritty. Don’t forget that this is my first Eclipse plugin and that I only had 24 hours to come up with something I can demo. It’s all hacked together and held up by duct tape. Also, note that I was using Eclipse 3.7.0.

Writing an Eclipse Plugin and Hooking Into the IDE

Creating a simple plugin is straightforward. Lars Vogel provides an excellent Eclipse Plugin Development Tutorial, for example. Eclipse has a convenient wizard for creating plugin projects, and when you run or debug a plugin, Eclipse fires up a completely new Eclipse that runs the plugin.

Hooking into the IDE was a bit harder to do. Having no concept of the different components, naming conventions, and patterns this turned out to be a hard problem. I did not have enough time to properly read up on it or even investigate the source code. Googling did not always turn up helpful answers. If I did find answers, then they were usually “use extension point X and don’t forget to extend class Y”. This may be good advice, but for someone who does not know the implications of using an extension point (and does not have the time to find out) this is not that helpful.

Extending the simple Hello World command plugin from the plugin wizard, the plugin analyses and annotates the current Java document when a certain menu command is executed. The goal was of course to always show annotations that the user, but I did not have time to find out how to do this. I suspect it would have taken a few more days to accomplish. To get the current ICompliationUnit (the current Java document), I used these copied, pasted, and modified helper methods:

    public static ICompilationUnit getCurrentICompilationUnit(ExecutionEvent event) throws ExecutionException {
        IWorkbenchWindow window = HandlerUtil.getActiveWorkbenchWindowChecked(event);
        ICompilationUnit compilationUnit = getJavaCompilationUnit(window.getActivePage().getActiveEditor());    
        return compilationUnit;
    }
    
    public static ICompilationUnit getJavaCompilationUnit(IEditorPart part) {
        IEditorInput editorInput= part.getEditorInput();
        if (editorInput != null) {
            IJavaElement input= JavaUI.getEditorInputJavaElement(editorInput);
            if (input instanceof ICompilationUnit) {
                return (ICompilationUnit) input;
            }
        }
        return null;    
    }

Finding Beans and Bean References in Java Code

I did not have time to find bean definitions in XML and so I concentrated on @Autowired, @Component, and @Service annotations. Autowired methods can take components or services.

I started off by identifying all autowired methods in the current Java document. To achieve this, I used the Abstract Syntax Tree (AST) that Eclipse can provide. The AST is a complete DOM of a Java compilation unit. See Lars Vogels’ tutorial on the Eclipse JDT – Abstract Syntax Tree (AST) and the Java Model. I strongly recommend installing the Eclipse ASTView plugin. It will give you a good idea how an AST is structured and how a Java document maps to it.

After creating an AST from the current ICompilationUnit, I extended an ASTVisitor to find annotated methods. ASTVisitors can be used to traverse an AST. Here is the method of ASTVisitor that I overrode to collect all annotated methods in an AST. Note my cheapskate way of defining @Autowired annotations.

@Override
public boolean visit(MarkerAnnotation node) {
    if (node.getTypeName().getFullyQualifiedName().contains("Autowired")) { //TODO
        ASTNode parent = node.getParent();
        if(parent instanceof MethodDeclaration) {
            // store parent in list
        }
    }
    return true;
}

This is how I extracted the parameter from the annotated methods:

private void getBeanDef() {
    if(method.parameters().size() != 1) {
        error = "must declare exactly one parameter";
        return;
    } 
    SingleVariableDeclaration parameter = (SingleVariableDeclaration) method.parameters().get(0);
    beanDef = parameter.getType();
    beanVar = parameter.resolveBinding();
}

The first extracted value, beanDef is used to find a matching annotated component or service. The second extracted value, beanVar is used to analyse the method body.

To find all matching components and services for the autowired method, we need to find all implementations of the type stored in beanDef that are annotated with @Component or @Service. To do this, I used the Java bindings for the AST. The AST models the Java source document, and its elements have bindings to the Java elements they model. To get the Java element for the beanDef and then find all annotated subclasses, this code was used:

public static IType findBean(Type type) throws JavaModelException {
    IJavaElement element = type.resolveBinding().getJavaElement();
    if(element instanceof IType) {
        IType itype = (IType)element; 
        if(isCandidate(itype)) {
            return itype;
        }
        ITypeHierarchy hierarchy = itype.newTypeHierarchy(progressMonitor);
        
        for(IType itype2 : hierarchy.getAllSubtypes(itype)) {
            for(IAnnotation annotation : itype2.getAnnotations()) {
                if(annotation.getElementName().contains("Service") ||  // this is a hack
                   annotation.getElementName().contains("Component") ) {
                    return itype2;
                }
            }               
        }
    }
    return null;
}

The implementation that this post describes was just for a quick demo. In the long run it might be more efficient to maintain a complete graph of all bean definitions rather than search the whole workspace each time. Here is example code to find all autowired methods in the workspace:

public void findAutowired () {
      SearchPattern pattern = SearchPattern.createPattern(  
                "Autowired",   
                IJavaSearchConstants.ANNOTATION_TYPE,   
                IJavaSearchConstants.ANNOTATION_TYPE_REFERENCE,   
                SearchPattern.R_CASE_SENSITIVE);  
      
      IJavaSearchScope scope = SearchEngine.createWorkspaceScope();
      SearchRequestor requestor = new Requestor();
      SearchEngine engine = new SearchEngine();
      try {
        engine.search(pattern, new SearchParticipant[] {SearchEngine.getDefaultSearchParticipant()}, scope, requestor, null);
    } catch (CoreException e) {
        e.printStackTrace();
    }
}

class Requestor extends SearchRequestor {
    @Override
    public void acceptSearchMatch(SearchMatch match) throws CoreException {
        System.out.println(match);
    }
}

Back to the beanVar binding from the autowired methods in the current Java document. One of my goals was to identify fields that were set by autowired methods. The binding stored in beanVar is used for this. The general approach I have taken is as follows:

  • Only care about methods with exactly one expression in the body. This expression needs to be an assignment.
  • The left hand side binding must match a field binding, the right hand side binding must match beanVar.

The following code implements the above algorithm. It takes the annotated method (as an AST MethodDeclaration) and beanVar (as an IBinding) and stores the field variable binding into targetField if it exists.

Statement statement = (Statement) method.getBody().statements().get(0);
if(statement instanceof ExpressionStatement) {
    ExpressionStatement eStatement= (ExpressionStatement)statement;
    Expression expression = eStatement.getExpression();
    if(expression instanceof Assignment) {
        Assignment assignment = (Assignment) expression;
        IBinding binding = ((SimpleName)assignment.getRightHandSide()).resolveBinding();
        if(beanVar.equals(binding)) {
            IVariableBinding varBinding = null;
            Expression leftHandSide = assignment.getLeftHandSide();
            if(leftHandSide instanceof SimpleName) {
                varBinding = (IVariableBinding)((SimpleName)leftHandSide).resolveBinding();
            } else if (leftHandSide instanceof FieldAccess) {
                varBinding = ((FieldAccess)leftHandSide).resolveFieldBinding();
            }
            if(varBinding!=null && varBinding.isField()) {
                targetField = varBinding;
            }
        }
    }
}

Doing the User Interface

The UI part definitely took the most part of the 24 hour challenge, and I did not get as far as I hoped. I spent ages trying to the hover text working that would appear over an autowired method or field, but I did not succeed. However, adding the marker (the little Spring icon) to the margin and the annotation (the green box) to the text was straightforward. Vogel’s plugin tutorial provides good information on this as does IBM’s article Best practices for developing Eclipse plugins by Flatt and Maison.

Conclusion

Writing this little demo in 24 hours was a roller coaster ride through Eclipse plugin development. The distinction between the AST, the Java model, and the Java bindings certainly makes sense. But it also makes for a clunky API that is somewhat unwieldy. It did, however, allow me to quickly extract the information that I was after. Hooking my code into the Eclipse IDE was a harder task and – as expected – the most frustrating.

Advertisements