Supporting older java api's when building for new versions

I’ve seen this question come through here a couple of times before (or very similar questions):

“I want to use SpecialService but it’s only available in product version 7.6 and I need to to support 7.2+” .

========================================================================
Note: After posting this - @scott suggested an alternate approach which is a lot simpler - scroll down to Supporting older java api's when building for new versions for it.

========================================================================

Usually the solution proposed is to use ComponentAccessor (shudder) to get hold of things dynamically and then use reflection (shudder again) to get hold of objects and build your interaction out with that. The other approach is to branch your code and basically have 2 plugins (shudder). I’ve done both in the past and in the end you either end up with a very hard reading piece of code OR really crappy user experience: "You just upgrade - yeah you need to upgrade your app as well from 3.0-72 to 3.0-76).

So let me start of with the customary disclaimer - this works for me - I hope it works for you. :slight_smile:

Jira “recently” added support for PrioritySchemes which kinda made this flare up again for me. So I thought I’d take a different stab at it ( I’ve published a sample repo of this at https://bitbucket.org/dwester42a/example-osgi-compatibility/src if you want to follow along).

Oh and before I go any further - this approach is basically built around the plugin that was published at https://bitbucket.org/bwoskow/optional-service-sample-plugin to show how to import a service from another app that may or may not be installed (So thank you to @bwoskow) . I made some very minor changes to it for looking internally at the host app and generally tried to figure out the magic he performed.

Let’s get started

We’ll start in the pom.xml ( https://bitbucket.org/dwester42a/example-osgi-compatibility/src/db2285604c975651dc05bbdfe191175f22e03e4c/pom.xml?at=master&fileviewer=file-view-default )with the dependencies. PrioritySchemeManager came into Jira around 7.7 - so we’ll need to point the api we’re targeting to be that. As long as we make ourself have a gentle person’s agreement with ourselves only to use api’s that are in 7.2 - we should be good, so bump the jira version: <jira.version>7.7.1</jira.version>.

Then we’re going to have ‘fun’ with Osgi - so there are 2 dependencies we’ll need to add:

<dependency>
            <groupId>com.atlassian.plugins</groupId>
            <artifactId>atlassian-plugins-osgi</artifactId>
            <version>4.5.0</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.osgi</groupId>
            <artifactId>spring-osgi-core</artifactId>
            <version>1.1.3</version>
            <scope>provided</scope>
        </dependency>

This will let us interact with the Osgi Services appropriately.

Let’s do some coding

All of this code is at https://bitbucket.org/dwester42a/example-osgi-compatibility/src/master/src/main/java/demo/compat/?at=master
First of we’ll create our magical interaction with Osgi stuff (Again - this is directly from @bwoskow’s sample):

public abstract class OptionalService<T> implements InitializingBean, DisposableBean
{
    private final Class<T> type;
    private final ServiceTracker tracker;

    public OptionalService(final BundleContext bundleContext, final Class<T> type)
    {
        this.type = checkNotNull(type, "type");
        this.tracker = new ServiceTracker(bundleContext, type.getName(), null);
    }

    /**
     * Returns the service (of type {@code T}) if it exists, or {@code null} if not
     * @return the service (of type {@code T}) if it exists, or {@code null} if not
     */
    protected final T getService()
    {
        return type.cast(tracker.getService());
    }

    @Override
    public void afterPropertiesSet() throws Exception
    {
        tracker.open();
    }

    @Override
    public final void destroy()
    {
        tracker.close();
    }
}

Basically what this does is to use osgi magic to look up a service (as well as open up a tracker that will get released when we are done).

We then extend this OptionalService in PrioritySchemeServiceFactory:

public class PrioritySchemeServiceFactory extends OptionalService<PrioritySchemeManager>
{
    public PrioritySchemeServiceFactory(final BundleContext bundleContext)
    {
        super(bundleContext, PrioritySchemeManager.class);
    }

    public ProxyPrioritySchemeManager get()
    {
        PrioritySchemeManager prioritySchemeManager = getService();
        return new ProxyPrioritySchemeManagerImpl( prioritySchemeManager);
    }
}

At this point we can get hold of a wrapping proxy object called ProxyPrioritySchemeManager which just has the real PrioritySchemeManager in it. We’ll come back to this object in a bit.

We need a real “service” that we can inject into things. For this we’ll create an interface (we could since we’re using Spring Scanner skip this and just flatten things down - but in case there’s somebody not using spring scanner - we’ll go with the interface:

public interface PrioritySchemeAccessor
{
    public ProxyPrioritySchemeManager getPrioritySchemeManager();
}

We implement this in PrioritySchemeAccessorImpl:

@Component
public class PrioritySchemeAccessorImpl implements PrioritySchemeAccessor
{
    private static final Logger log = LoggerFactory.getLogger(PrioritySchemeAccessorImpl.class);

    private final ApplicationContext applicationContext;
    private ProxyPrioritySchemeManager proxyPrioritySchemeManager;

    @Inject
    public PrioritySchemeAccessorImpl(ApplicationContext applicationContext)
    {
        this.applicationContext = checkNotNull(applicationContext, "applicationContext");
    }

    @Override
    public ProxyPrioritySchemeManager getPrioritySchemeManager()
    {
        if( proxyPrioritySchemeManager==null)
        {
            initProxyPrioritySchemeManager();
        }
        return proxyPrioritySchemeManager;
    }


    private void initProxyPrioritySchemeManager()
    {
        try
        {
            Class<?> prioritySchemeServiceFactoryClass = getWittifiedPrioritySchemeManagerServiceFactoryClass();
            if (prioritySchemeServiceFactoryClass != null)
            {
                this.proxyPrioritySchemeManager = ((PrioritySchemeServiceFactory)applicationContext.getAutowireCapableBeanFactory().
                        createBean(prioritySchemeServiceFactoryClass, AutowireCapableBeanFactory.AUTOWIRE_CONSTRUCTOR, false)).get();
            }
        }
        catch (Exception e)
        {
            log.error("Could not create PrioritySchemeServiceFactory", e);
        }


        if( this.proxyPrioritySchemeManager==null)
        {
            log.info("Proxy failed. Going with NoOp");
            this.proxyPrioritySchemeManager = new NoOpProxyPrioritySchemeManager();
        }
    }


    private Class<?> getWittifiedPrioritySchemeManagerServiceFactoryClass()
    {
        try
        {
            getClass().getClassLoader().loadClass("com.atlassian.jira.issue.fields.config.manager.PrioritySchemeManager");

            return getClass().getClassLoader().loadClass("demo.compat.PrioritySchemeServiceFactory");
        }
        catch (Exception e)
        {
            return null;
        }
    }
}

You’ll notice that this is the only class that we’re doing @Component (and @Inject). This is intentional. This is the service that we’re going to pass around in our app in a bit. We inject spring’s ApplicationContext so we can use this later.

Now when we call getPrioritySchemeManager - we check to see if we already have a proxyPrioritySchemeManager object. If we do - we return it. If we don’t then we initialize it and then return it. This way we’re not being a performance hog each time.

Inside initProxyPrioritySchemeManager() we call getPrioritySchemeManagerServiceFactoryClass() which looks up to see if the PrioritySchemeManager class is available on our classpath. If it is then we return the class of demo.compat.PrioritySchemeServiceFactory. If we can get it - we return back a null object.

Time for the magic

If getPrioritySchemeManagerServiceFactoryClass() returns back in a String then we know that PrioritySchemeManager exists. If it returns back a null - we know it doesn’t. If it does exists, we pass the string of the class to the applicationContext and instantiate the PrioritySchemeServiceFactory object and then call get() on it. This will then return us the wrapping PrioritySchemeManager wrapping class ( ProxyPrioritySchemeManagerImpl ).

If the string though is null (i.e. PrioritySchemeManager doesn’t exists) - we create a NoOpProxyPrioritySchemeManager object.

Both the NoOpProxyPrioritySchemeManager and ProxyPrioritySchemeManagerImpl uses an interface ProxyPrioritySchemeManager. In here we implement all of our methods that we want to use of the PrioritySchemeManager:

public interface ProxyPrioritySchemeManager
{
    public boolean hasPrioritySchemes();
    public List<FieldConfigScheme> getAllSchemes();
}

Now I added hasPrioritySchemes() for ease of use - this way we can have a simple boolean to tell us what type of world we live in - do we have priorities or not (awesome for conditions). We don’t have to define all of the methods on the PrioritySchemeManager - just the ones we need.

Then when we do the implementation of these - we do it twice. First in the NoOpProxyPrioritySchemeManager:

public class NoOpProxyPrioritySchemeManager implements ProxyPrioritySchemeManager
{
    public List<FieldConfigScheme> getAllSchemes()
    {
        return new ArrayList<>();
    }
    public boolean hasPrioritySchemes()
    {
        return false;
    }

}

And then in the ProxyPrioritySchemeManagerImpl

public class ProxyPrioritySchemeManagerImpl implements ProxyPrioritySchemeManager
{

    private final PrioritySchemeManager prioritySchemeManager;

    public ProxyPrioritySchemeManagerImpl(PrioritySchemeManager prioritySchemeManager)
    {
        this.prioritySchemeManager = checkNotNull(prioritySchemeManager, "prioritySchemeManager");
    }


    public boolean hasPrioritySchemes()
    {
        return true;
    }

    public List<FieldConfigScheme> getAllSchemes()
    {
        return this.prioritySchemeManager.getAllSchemes();
    }

}

The ProxyPrioritySchemeManagerImpl doesn’t really have any magic to it - it’s just calling the prioritySchemeManager directly.

Actually making use of this

For this plugin I created a servlet at https://bitbucket.org/dwester42a/example-osgi-compatibility/src/master/src/main/java/demo/servlet/DemoServlet.java?at=master&fileviewer=file-view-default

@Scanned
public class DemoServlet extends HttpServlet
{
    private final PrioritySchemeAccessor prioritySchemeAccessor;

    @Inject
    public DemoServlet(PrioritySchemeAccessor prioritySchemeAccessor)
    {
        this.prioritySchemeAccessor = prioritySchemeAccessor;
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
    {
        resp.setContentType("text/plain");

        PrintWriter writer = resp.getWriter();

        ProxyPrioritySchemeManager proxyPrioritySchemeManager = prioritySchemeAccessor.getPrioritySchemeManager();

        if( proxyPrioritySchemeManager.hasPrioritySchemes())
        {

            writer.write("This version does have priority schemes!");
            writer.write("\n");
            writer.write("The following schemes exist:");
            for(FieldConfigScheme fieldConfigScheme: proxyPrioritySchemeManager.getAllSchemes())
            {
                writer.write( fieldConfigScheme.getName());
                writer.write("\n");
            }
        }
        else
        {
            writer.write("This Jira version does not have priority schemes");
        }
        writer.close();

    }
}

It’s @Scanned and @Inject so we can get the PrioritySchemeAccessor object. Then inside the doGet(…) we call

ProxyPrioritySchemeManager proxyPrioritySchemeManager = prioritySchemeAccessor.getPrioritySchemeManager();

At this point from this code - there is no "magic. ProxyPrioritySchemeManager will either call our NoOp implementation or the proxying one. We either get a list of the priority schemes or that there isn’t support for it.

There is still some clean up that could be on this code (refactoring so we flatten down the service factory look up, etc) but for a sample this works pretty well. Also, make sure to perform the appropriate regression tests so that you don’t run into any weirdness because you’re using a higher api version (it could happen - it hasn’t happened to me – yet ).

Note: Don’t use this for app dependencies as is

While you could in theory use this code to attach to other app’s exported services - remember they’ll come and go (if a host app’s service comes and goes - then we have a problem :wink: ). For this @bwoskow’s code is definitively the route to go (or add in PluginEnabled/PluginDisabled event awareness).

Did I thank @bwoskow for his code sample yet? :slight_smile:

5 Likes

Hi Daniel,

The optional-service-sample-plugin method is a good idea if you have another plugin that may come and go and you want to use its exported classes directly (although I’ve been advised of some reliability problems in one case where we implemented this approach in production).

In the case where you’re wrapping the foreign interface with your own proxy classes anyway, I can suggest a simpler approach, which is to build your POM against the more recent version of the host product (as in your example), use the ComponentAccessor to grab the bean…and use that bean directly in your proxy.

“But this will cause ClassNotFoundExceptions”, you say. This is true–but only if Java tries to load the class that contains a reference to those foreign classes in the first place. What you need to do is to arrange your app so that this never happens.

If you (say) create a separate MyClassWithReferencesToTheNewClass.java file and only execute a “new MyClassWithReferencesToTheNewClass()” once you have determined that you are running on the correct version of the host product, then you will be able to use the bean directly. Just make sure that all of the references to those foreign classes are contained in that separate class. You will have conditionals to ensure that the “new My…()” code path will never be followed on the old versions of the host product, so the class will never be loaded, and you will be exception-free.

The only possible caveat is that I have not tried this approach with a Spring Scanner-based plugin, but I would guess that a transformerless plugin would work just as well with this approach.

I’ve gone down that route before but ended up having class look up issues when dealing with spring scanner which is why I ended up going down this route. But if you’re able to get it working - please share since yeah that would def be a better approach.

Hi Daniel,

I was able to get this working with Spring Scanner v2 under Confluence, although I imagine it should work with Jira too. The pattern looks like this:

import com.atlassian.spring.container.ComponentNotFoundException;
import com.atlassian.spring.container.ContainerManager;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;

@Component
public class NewFeatureHelper implements InitializingBean
{
    private static final String COMPONENT_NAME = "newFeatureService"; // name of the bean that exists only in new product versions
    private NewFeatureTarget target; // NewFeatureTarget is your own interface that describes the methods you need from this new feature

    @Override
    public void afterPropertiesSet()
    {
        try
        {
            Object newFeatureService = ContainerManager.getComponent(COMPONENT_NAME);

            // If we got this far without receiving a ComponentNotFoundException, we know that the
            // host product has the bean, so we're good to load and instantiate our class that uses
            // it directly.

            this.target = new NewFeatureTargetConfluence69(newFeatureService);
        }
        catch (ComponentNotFoundException e)
        {
            // Otherwise, load a separate no-op class that does not touch the new classes.
            this.target = new NewFeatureTargetLegacy();
        }
    }

    public boolean isReadOnlyMode()
    {
        return target.isReadOnlyMode();
    }
}

In this case, NewFeatureTargetConfluence69 is a class that is loaded only when using the new version of the host app, which would look like this:

public class NewFeatureTargetConfluence69 implements NewFeatureTarget
{
    private NewFeatureService newFeatureService;

    // Constructor must take an Object (so the caller doesn't need to reference the
    // class), but it's safe to assign to the actual class in the constructor.

    public NewFeatureTargetConfluence69(Object newFeatureService)
    {
        this.newFeatureService = (NewFeatureService)newFeatureService;
    }

    // ...then implement your interface here by calling this.newFeatureService directly.
}

NewFeatureTargetLegacy is defined similarly, except that it doesn’t necessarily need a constructor, and it provides stub no-op methods that are used in older product versions, similar to your implementation.

Within your app, you inject references to NewFeatureHelper and it then proxies to the correct component.

The last piece of glue you need is to configure the POM to prevent the host product from throwing a fit when loaded on older versions of the product that don’t have your desired classes. For every class you reference in NewFeatureTargetConfluence69 that may not exist in older versions, you need to explicitly list the package as optional in your pom’s instructions.

For example, if you reference the class “com.atlassian.confluence.newfeature.NewFeatureService”, then the package of that class must be listed as follows:

<Import-Package>
    com.atlassian.confluence.newfeature;resolution:="optional",
    *
</Import-Package>

Perhaps this could be simplified further by using a bean factory to conditionally produce either a NewFeatureTargetConfluence69 or NewFeatureTargetLegacy object based on what version is installed, which might allow your client code to inject and access the NewFeatureTarget interface directly without going through an intermediate method call in NewFeatureHelper.

3 Likes

That’s awesome! Thank you for sharing this. I’ll update my post with a link to this one.

Actually, the factory bean method seems to be even better. I haven’t tried this in production yet, but it works great in dev and it eliminates the extra layer of indirection. You can ditch NewFeatureHelper from my previous post and replace it with a similar NewFeatureBeanFactory as below:

import com.atlassian.spring.container.ComponentNotFoundException;
import com.atlassian.spring.container.ContainerManager;
import org.springframework.beans.factory.FactoryBean;

public class NewFeatureFactoryBean implements FactoryBean
{
    private static final String COMPONENT_NAME = "newFeatureService";

    @Override
    public Object getObject() throws Exception
    {
        NewFeatureTarget target;

        try
        {
            Object newFeatureService = ContainerManager.getComponent(COMPONENT_NAME);
            target = new NewFeatureTargetConfluence69(newFeatureService);
        }
        catch (ComponentNotFoundException e)
        {
            target = new NewFeatureTargetLegacy();
        }

        return target;
    }

    @Override
    public Class getObjectType()
    {
        return NewFeatureTarget.class;
    }

    @Override
    public boolean isSingleton()
    {
        return true;
    }
}

With this approach, you can now inject the “NewFeatureTarget” interface in your other classes and use it directly, and the factory bean will take care of finding the right implementation class to use.

All you need to do is to configure Spring to use your factory bean. The conservative way to do this is in your META-INF/spring/whatever.xml:

<?xml version="1.0" encoding="UTF-8"?>
<beans  ... >
    <atlassian-scanner:scan-indexes/>

    <!-- add the following bean definition to your existing Spring XML file -->
    <bean id="newFeatureTarget" class="com.yourcompany.NewFeatureFactoryBean"/>
</beans>

Alternate approach: I was also able to get it to work without having to modify the Spring XML, but I think the following approach qualifies as “abuse” because it’s using the Spring Scanner’s “@Component” annotation to output a bean definition for our factory bean, even though our factory bean isn’t a bean in itself, and I don’t know if this might break with future versions of the Spring Scanner. If you want to try, you just annotate the NewFeatureFactoryBean as follows. (This is again using v2 of the Spring Scanner.)

// important: the "value" attribute must correspond to the name of the class that is being returned by getObjectType
@Component(value="newFeatureTarget")
public class NewFeatureFactoryBean implements FactoryBean
3 Likes

Wow, talk about a blast from the past! That sample plugin hadn’t been updated since 2013, and even referenced “GreenHopper” (later known as Jira Agile, now known as part of Jira Software). :slight_smile:

Thanks for the nice words about it. I’m glad that the sample code was helpful.

I also like Scott’s approach and have done something similar in the past as well. Either way works. I updated my sample plugin (first commit there in 4.5 years!) to link to this post so devs can see the conversation if desired.

1 Like

Thanks, Ben! I should add, for the sake of others, that your optional-service-sample is still the only way to go if you’re dealing with other dynamically-loaded components. The bean magic above will only work if you’re dealing with components instantiated directly in the host product.

If you are unfortunate enough to have the need to reference components in another OSGi plugin, you can never be entirely sure of load order and I suspect that the above approach is unlikely to be reliable or may not even work at all.

I have not paid much attention to exactly how Jira Software is loaded these days (now that it is an “application”), but I imagine it still might not technically be part of the host product and may still require Ben’s method.

https://bitbucket.org/bwoskow/optional-service-sample-plugin is a dead link - anyone got a fork/mirror?

Hi @daniel, what about supporting different versions of the same Service or Class?

For example using the new ContentId.of(long) together with ContentId.of(ContentType, long) (Link to JavaDoc 6.6.1). Or supporting the two different versions of ContentPropertyService.find().fetch() vs ContentPropertyService.find().fetchOne().

Thanks for suggestions!