4

How do we tell Spring ServiceLocatorFactoryBean to provide the default instance of a service? I have a scenario like this.

package strategy;

import model.Document;

public interface IPrintStrategy {
public void print(Document document);
}

and 2 flavors of Strategy classes

package strategy;

import model.Document;

import org.springframework.stereotype.Component;

@Component("A4Landscape")
public class PrintA4LandscapeStrategy implements IPrintStrategy{

 @Override
 public void print(Document document) {
  System.out.println("Doing stuff to print an A4 landscape document");
 }

}


package strategy;

import model.Document;

import org.springframework.stereotype.Component;

@Component("A5Landscape")
public class PrintA5LandscapeStrategy implements IPrintStrategy{

 @Override
 public void print(Document document) {
  System.out.println("Doing stuff to print an A5 landscape document");
 }

}

A Strategy Factory interface as below

package strategy;

public interface PrintStrategyFactory {

 IPrintStrategy getStrategy(String strategyName);

}

and Spring config as below

<beans xmlns:context="http://www.springframework.org/schema/context" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.springframework.org/schema/beans" xsi:schemalocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
  http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd">

 <context:component-scan base-package="strategy">

  <bean class="org.springframework.beans.factory.config.ServiceLocatorFactoryBean" id="printStrategyFactory">
  <property name="serviceLocatorInterface" value="strategy.PrintStrategyFactory">
 </property></bean>

 <alias alias="A4P" name="A4Portrait">
 <alias alias="A4L" name="A4Landscape">
</alias></alias></context:component-scan></beans>

and my test class

import model.Document;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.testng.AbstractTestNGSpringContextTests;
import org.testng.annotations.Test;

import strategy.PrintStrategyFactory;


@ContextConfiguration(locations = {"classpath:/spring-config.xml"})
public class SpringFactoryPatternTest extends AbstractTestNGSpringContextTests{

 @Autowired
 private PrintStrategyFactory printStrategyFactory;

 @Test
 public void printStrategyFactoryTest(){
  Document doc = new Document();

  printStrategyFactory.getStrategy("A4L").print(doc);
  printStrategyFactory.getStrategy("A5L").print(doc);

  printStrategyFactory.getStrategy("Something").print(doc);

 }
}

what will happen when I pass some text to the Factory like the last call

  printStrategyFactory.getStrategy("Something").print(doc);

Is there a way to configure ServiceLocatorFactoryBean to send back the default instance of my Print Strategy, like the instance of the below class.

package strategy;

import model.Document;

import org.springframework.stereotype.Component;

@Component("invalid")
public class InvalidLandscapeStrategy implements IPrintStrategy{

 @Override
 public void print(Document document) {
  System.out.println("INVALID DOCUMENT STRATEGY");
 }

}
Anand PN
  • 81
  • 3

1 Answers1

2

There's no really neat way that I've found, but here are 3 no-so-neat options.

1: The option I used when faced with the same problem is to use the setServiceLocatorExceptionClass method and set your own exception class, then catch it and default.

// Checked exception for service locator factory
public class PrintStrategyException extends Exception
{
    public PrintStrategyException(String message)
    {
        super(message);
    }

    public PrintStrategyException(String message, Throwable cause)
    {
        super(message, cause);
    }

    public PrintStrategyException(Throwable cause)
    {
        super(cause);
    }
}

I used the JAVA API but translating to your XML config:

<bean class="org.springframework.beans.factory.config.ServiceLocatorFactoryBean" id="printStrategyFactory">
    <property name="serviceLocatorInterface" value="strategy.PrintStrategyFactory"/>
    <property name="setServiceLocatorExceptionClass" value="strategy.PrintStrategyException"/>
</bean>

You might want a RuntimeException here, but since I'm deliberately using a checked exception I can (and must) catch it:

try {
    strategy = printStrategyFactory.getStrategy("A4L");
}
catch (PrintStrategyException e) {
    // strategy was not found, use the default "invalid"
    strategy = printStrategyFactory.getStrategy("invalid");
}
strategy.print(doc);

2: Same as option 1, but catch one of the standard Spring exceptions when the bean is not found, instead of going to the trouble of creating your own.

Note that my try-catch code is in what would be your test class, so this approach really depends on creating a wrapper class to get your Strategy into which you Autowire the factory as you have done in your test. (Make your Test class a production class). The need to do this - to gather your calls to instantiate the factory into one place so that you can handle the exception - is what makes this approach less-than-ideal. It might seem to just wraps your original problem in nicer packaging, but in fact, you can catch that Exception anywhere (you don't have to use a common method like I did) And by making it checked (vs runtime) and adding it to the factory method signature, you make it part of your API so that all callers need to deal with it.

3: A last-ditch, very hacky approach is to take advantage of the Properties-based method setServiceMappings and override the java.util.Properties class:

// I'm sticking with the Annotation approach here, but you can transcribe it into XML
@Bean("strategyFactory")
public FactoryBean serviceLocatorFactoryBean() {
    ServiceLocatorFactoryBean factoryBean = new ServiceLocatorFactoryBean();
    factoryBean.setServiceLocatorInterface(PrintStrategyFactory.class);
    Properties myProperties = new MyProperties();
    myProperties.setProperty("A4P", "A4Portrait");
    myProperties.setProperty("A4L", "A4Landscape");
    factoryBean.setServiceMappings(myProperties);
    return factoryBean;
}

public static class MyProperties extends Properties
{
    @Override
    public String getProperty(String key)
    {
        String value = super.getProperty(key);

        // Strategy name is not known, default to "invalid" strategy
        return value == null ? "invalid" : value;
    }
}

This is hacky because it relies on the knowledge that the Spring class is going to call getProperty() on the Properties object. And, indeed that java.util.Properties is not final. If Spring had wanted to support this solution, they would have used an interface like Map rather than Properties.

But, while calling getProperty() is not explicit in the contract, it is strongly implied, and kind of inevitable that getProperty() will be called. And this solution will work everywhere without exceptions.

So it works. I still prefer option 1, though, because it's neater. And I am using my own checked exception.

Rhubarb
  • 34,705
  • 2
  • 49
  • 38