-1

We are developing a platform, where many developers will be writing their own ETL applications that use a vendor's API that is then submitted for execution onto the platform. We want to constrain developers from just dong their own thing when writing a Main class (that would normally just use the vendor's API), in order to promote some strongly-held conventions. The (large) organisation has a culture of people doing their own thing which over years has resulted in some pretty nasty architecture, so we'd like to impose some best practice conventions that can be enforced by way of CI/CD which will help foster code sharing. The alternative will be a reversion-to-the-norm free for all, which we are desperate to avoid.

How do we determine what the main class of an application is? How can we test for this? We'd like to define either an abstract class or interface that the developers make use of, which will define some up-front promises that the developers must abide by (or else the tests will fail). We can't modify the vendor's code.

So, more concretely, currently we have:

public class MyNastyFreeForAll {
    public static void main(String[] ) {
        //...
    }
}

Is there a way of detecting/enforcing something like:

public class MyConventionEnforcingClass implements/extends MyConventions {
    public static void main(String[] ) {
        //...
    }
}

i.e. test that the main class for the application uses something derived from MyConventions?

Ideally, I want to run the test using Spock.

Alternatively, is there a better way to achieve this aim? Code reviews in an organisation this size amongst lots of separate teams with no central control/hierarchy just isn't going cut it, I'm afraid.

EDIT TO REFLECT INPUT FROM COMMENTS:

At its heart, this is a people problem. However, the people number in their 1000s and cultural change will not happen overnight. It will not happen simply by educating, documenting and influencing and thereby hoping that people will do the right thing. I am looking for a technical solution that can gently steer our developers into doing the right thing - they can always subvert this if they wish, but I want to require them to go out of their way to do so if they want to do this. It is because I am seeking a technical solution that I am posting on SO, not seeking guidance on how to drive cultural change on another site.

EDIT TO PROVIDE MCVE:

Here's an example using an abstract class. It would be nice to validate (at compile or test time) that the main class derives from MyConventions. If a user wants to actively subvert this, then so be it - you can lead a horse to water and all that - but I'm trying to make it easier for end-users to do the right thing than to not do the right thing. Simply giving them a class that does the boilerplate for them is not likely to suffice, as these users like to do their own thing and would likely ignore you, so there should be some form of light-touch technical enforcement. There is no attempt to impose convention on doProcessing() but the same principles could be used to add pre- and post- methods etc to achieve this.

If there is another way of achieving this aim then I'd be very interested in ideas.

// MyNastyFreeForAll.java
// written by end-user
public class MyNastyFreeForAll {
    public static void main(String[] args) {
        MyNastyFreeForAll entrypoint = new MyNastyFreeForAll();
        entrypoint.doBoilerplate();
        entrypoint.doProcessing();
    }

    private void doBoilerplate() {
        // lot of setup stuff here, where the user can go astray
        // would like to provide this in a class, perhaps
        // but we need to be able to enforce that the user uses this class
        // and doesn't simply try to roll their own.
        System.out.println("Doing boilerplate my own way");

    }

    private void doProcessing() {
        // more things that the user can misuse
        System.out.println("Doing doProcessing my way");

    }
}

// MyConventions.java
// written by team that knows how to set things up well/correctly
public abstract class MyConventions {

    public void doBoilerplate() {
      System.out.println("Doing boilerplate the correct way");
    }

    public abstract void doProcessing();

}

// MyConventionsImpl.java
// written by end-user
public class MyConventionsImpl extends MyConventions {

  public static void main(String[] args) {
    MyConventions entrypoint = new MyConventionsImpl();
    entrypoint.doBoilerplate();
    entrypoint.doProcessing();
  }

  public void doProcessing() {
    System.out.println("Doing doProcessing my way");

  }
}
John
  • 10,837
  • 17
  • 78
  • 141
  • Where and how will the application be executed? – larsgrefer Apr 30 '18 at 22:40
  • 1
    _" We want to constrain developers from just doing their own thing when writing a Main class ..."_ - You have a ***people*** problem, not a software problem. If your developers are "doing their own thing" in contravention of company policy you need to find out ***why***. Is the policy really onerous or are they just immature and unwilling to be team players? You can throw all the technology you want at this issue but you will not fix it this way. Developers will find ways around your restrictions. This is IMHO a question for [workplace.se]. – Jim Garrison May 01 '18 at 00:09
  • 1
    Oh, and BTW, an "application" can have lots of "main" classes, or ZERO main classes, depending on what container is hosting it. – Jim Garrison May 01 '18 at 00:10
  • Ignoring all the conceptual problems with your approach, let me concentrate on the technical ones: How are the classes delivered to you? Where does the test find handed-in code packages? Are they in single JARs? How would a main class derived from another one or implementing an interface stop anyone from just not calling its methods and doing their own thing instead? – kriegaex May 01 '18 at 01:31
  • Another thought: Why would you use a Spock test in order to enforce such rules? I usually use the AspectJ compiler _Ajc_ as a drop-in replacement for _Javac_, defining _Ajc_ as default compiler to use in a parent POM everyone has to use, also providing an aspect making use of `declare error` or `declare warning` which checks constraints like yours directly during compilation, throwing warnings or errors if violated. If you are interested in this approach, please let me know, then I will elaborate in an answer. If you answer the questions in my 1st comment, I can also provide a Spock solution. – kriegaex May 01 '18 at 01:35
  • @larsgrefer The application is launched via a vendor's system (effectively the application is loaded into it), but I'm trying to catch issues compile time. – John May 01 '18 at 07:40
  • @JimGarrison I agree, it IS a people problem. But the people number in the 1000s, and cultural change is not going to happen any time soon. I need to help developers help themselves by pushing them in the direction of best practice, and that CANNOT be by simply trying to document, educate or influence. It requires a technical solution. – John May 01 '18 at 08:14
  • @kriegaex Thank you very much - the compiler needs to be the Eclipse compiler (because of the way it retains type information), but I'd be very interested in both of your solutions. The user's classes are bundled into an application jar. There is no real way to prevent users from subverting our intentions; we're simply trying to make ours the path of least resistance - see my comment above about the scale of the cultural problem. The requirement is to detect issues at compile or test time, so either option would be great - would love to see an answer showing both approaches. – John May 01 '18 at 08:19
  • Well, then you might be pleased to learn that _Ajc_ is actually a fork of the Eclipse compiler _Ejc_ (regularly refreshed from upstream). AspectJ is an Eclipse project, after all. Maybe tonight (Indochina time zone) I can put some sample code together for you. If you could meanwhile expand your own sample code into a full [MCVE](http://stackoverflow.com/help/mcve) that would be great. I could then just build upon that. – kriegaex May 01 '18 at 08:26

1 Answers1

1

You and the other departments can compile all code with the AspectJ compiler, either manually from command line, via batch files, via IDE configured for AspectJ (e.g. Eclipse, IDEA) or via Maven. I created Maven setup for you on GitHub, just clone the project. Sorry, it does not use your MCVE classes because I saw them too late and did not want to start over.

Interface approach

Now let us assume there is an interface which all conforming applications need to implement:

package de.scrum_master.base;

public interface BasicInterface {
  void doSomething(String name);
  String convert(int number);
}
package de.scrum_master.app;

import de.scrum_master.base.BasicInterface;

public class ApplicationOne implements BasicInterface {
  @Override
  public void doSomething(String name) {
    System.out.println("Doing something with " + name);
  }

  @Override
  public String convert(int number) {
    return new Integer(number).toString();
  }

  public static void main(String[] args) {
    System.out.println("BasicInterface implementation");
    ApplicationOne application = new ApplicationOne();
    application.doSomething("Joe");
    System.out.println("Converted number = " + application.convert(11));
  }
}

Base class approach

Or alternatively, there is a base class applications have to extend:

package de.scrum_master.base;

public abstract class ApplicationBase {
  public abstract void doSomething(String name);

  public String convert(int number) {
    return ((Integer) number).toString();
  }
}
package de.scrum_master.app;

import de.scrum_master.base.ApplicationBase;

public class ApplicationTwo extends ApplicationBase {
  @Override
  public void doSomething(String name) {
    System.out.println("Doing something with " + name);
  }

  public static void main(String[] args) {
    System.out.println("ApplicationBase subclass");
    ApplicationTwo application = new ApplicationTwo();
    application.doSomething("Joe");
    System.out.println("Converted number = " + application.convert(11));
  }
}

Unwanted application

And now we have an application which does its own thing, neither implementing the interface nor extending the base class:

package de.scrum_master.app;

public class UnwantedApplication {
  public void sayHello(String name) {
    System.out.println("Hello " + name);
  }

  public String transform(int number) {
    return new Integer(number).toString();
  }

  public static void main(String[] args) {
    System.out.println("Unwanted application");
    UnwantedApplication application = new UnwantedApplication();
    application.sayHello("Joe");
    System.out.println("Transformed number = " + application.transform(11));
  }
}

Contract enforcer aspect

Now let us just write an AspectJ aspect which yields a compiler error via declare error (a warning would also be possible via declare warning, but that would not enforce anything, only report the problem).

package de.scrum_master.aspect;

import de.scrum_master.base.BasicInterface;
import de.scrum_master.base.ApplicationBase;

public aspect ApplicationContractEnforcer {
  declare error :
    within(de.scrum_master..*) &&
    execution(public static void main(String[])) &&
    !within(BasicInterface+) &&
    !within(ApplicationBase+)
  : "Applications with main methods have to implement BasicInterface or extend ApplicationBase";
}

The meaning of this code is: Look for all classes with main methods inside de.scrum_master or any subpackage, but not implementing BasicInterface and not extending ApplicationBase. In reality you would only choose one of the two latter criteria, of course. I am doing both here to give you a choice. If any such class if found, an compiler error with the specified error message is shown.

For whatever reason some people dislike the wonderfully expressive AspectJ native language (a superset of Java syntax) but prefer to write ugly annotation-style aspects, packing all their aspect pointcuts into string constants. This is the same aspect, just in another syntax. Choose any. (In the GitHub project I have deactivated the native aspect by letting is search for the non-existing package xde.scrum_master so as to avoid double compiler errors.)

package de.scrum_master.aspect;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.DeclareError;

@Aspect
public class ApplicationContractEnforcer2 {
  @DeclareError(
    "within(de.scrum_master..*) && " +
    "execution(public static void main(String[])) && " +
    "!within(de.scrum_master.base.BasicInterface+) && " +
    "!within(de.scrum_master.base.ApplicationBase+)"
  )
  static final String errorMessage =
    "Applications with main methods have to implement BasicInterface or extend ApplicationBase";
}

Compile with Maven

When running mvn clean compile (see GitHub project for POM), you will see this output (shortened by a bit):

[INFO] ------------------------------------------------------------------------
[INFO] Building AspectJ sample with declare error 1.0-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO] 
[INFO] --- aspectj-maven-plugin:1.10:compile (default) @ aspectj-application-contract-enforcer ---
[INFO] Showing AJC message detail for messages of types: [error, warning, fail]
[ERROR] "Applications with main methods have to implement BasicInterface or extend ApplicationBase"
    C:\Users\alexa\Documents\java-src\SO_AJ_EnforceMainClassImplementingInterface\src\main\java\de\scrum_master\app\UnwantedApplication.java:12
public static void main(String[] args) {
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------

Error view in Eclipse

In Eclipse with AJDT (AspectJ Development Tools) it looks like this:

Eclipse showing custom AspectJ compiler error

Just rename the main method in UnwantedApplication to something else like mainX and the error goes away.

kriegaex
  • 63,017
  • 15
  • 111
  • 202