spring-integrationspring-integration-file

Migrating a filter from XML to pure Java Config


I am trying to migrate a project from a mixed XML/Java configuration to pure Java Config (not yet Java DSL, but annotated @Bean methods). So far, I managed to convert channels, inbound channel adapters, transformers and service activators), but I'm stuck with the conversion of a filter.

The integration.xml file define the following filter (the Message carries a Java.io.File payload)

<int:filter input-channel="channelA" output-channel="channelB"
            ref="integrationConfiguration" method="selector"/>

The selector is defined in the IntegrationConfiguration class (that also holds all other SI-related @Bean methods:

@Configuration
public class IntegrationConfiguration {

    // channels
    @Bean
    public MessageChannel channelA() { return new DirectChannel(); }
    @Bean
    public MessageChannel channelB() { return new DirectChannel(); }
    // some other required channels
    // ...

    // inbound channel adapters
    @Bean
    @InboundChannelAdapter(channel = "channelA")
    public MessageSource<File> fileReadingMessageSource() {
        var source = new FileReadingMessageSource();
        // source configuration (not relevant here)
        return source;
    }
    // ...

    // filter on Message<File>
    public boolean selector(@Header("file_name") String name,
                            @Header("file_relativePath") String relativePath) {
        // do stuff with name and relativePath and return true or false
        return true;
    }

    // transformers
    @Bean
    @Transformer(inputChannel = "channelB", outputChannel = "channelC")
    public HeaderEnricher enrichHeaders() {
        var expression = new SpelExpressionParser().parseExpression("...");
        var headers = Map.of("additional_header",
                new ExpressionEvaluatingHeaderValueMessageProcessor<>(expression, String.class));

        return new HeaderEnricher(headers);
    }
    // ...

    // service activators
    @Bean
    @ServiceActivator(inputChannel = "channelC")
    public FileWritingMessageHandler fileWritingMessageHandler() {
        var handler = new FileWritingMessageHandler(
                new SpelExpressionParser().parseExpression("headers.additional_header")
        );
        // handler configuration (not relevant here)
        return handler;
    }
    // ...
}

I tried to replace the XML-defined bean with:

@Bean
@Filter(inputChannel = "channelA", outputChannel = "channelB")
public boolean filter() {
    // get the "file_name" and "file_relativePath" headers
    var expression1 = new SpelExpressionParser().parseExpression("headers.file_name");
    var name = expression1.getValue(String.class);
    
    var expression2 = new SpelExpressionParser().parseExpression("headers.file_relativePath");
    String relativePath = expression2.getValue(String.class);
    
    // do stuff with name and relativePath and return true or false
    return true;
}

When I run the code, it gives me a BeanCreationException:

Error creating bean with name 'filter' defined in class path resource [.../IntegrationConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [boolean]: Factory method 'filter' threw exception; nested exception is org.springframework.expression.spel.SpelEvaluationException: EL1007E: Property or field 'headers' cannot be found on null

What did I do wrong?

UPDATE after Artem's answer and insightful comments:

Using @Bean isn't necessary for a POJO method, keep it for

out-of-the-box type: MessageHandler, Transformer, MessageSelector etc

In this case, one can use a (not an out-of-the-box) @Bean MessageSelector, but it is actually more lines of code for the same result:

@Bean
@Filter(inputChannel = "channelA", outputChannel = "channelB")
public MessageSelector messageSelector() {
    return new MessageSelector(){
        @Override
        public boolean accept(Message<?>message){
            var headers = message.getHeaders();
            var name = headers.get("file_name", String.class);
            var relativePath = headers.get("file_relativePath", String.class);
            
            return selector(name, relativePath);
        }
    };
}

Solution

  • There is just enough to do like this:

    @Filter(inputChannel = "channelA", outputChannel = "channelB")
    public boolean selector(@Header("file_name") String name,
                            @Header("file_relativePath") String relativePath) {
    

    See docs for that @Filter:

     * Indicates that a method is capable of playing the role of a Message Filter.
     * <p>
     * A method annotated with @Filter may accept a parameter of type
     * {@link org.springframework.messaging.Message} or of the expected
     * Message payload's type. Any type conversion supported by default or any
     * Converters registered with the "integrationConversionService" bean will be
     * applied to the Message payload if necessary. Header values can also be passed
     * as Message parameters by using the
     * {@link org.springframework.messaging.handler.annotation.Header @Header} parameter annotation.
     * <p>
     * The return type of the annotated method must be a boolean (or Boolean).
    

    The @Filter is similar to the @ServiceActivator or @Transformer: you mark the method and point to the channels. The framework creates an endpoint and use that method as a handler to consume messages from the channel. The result of the method call is handler respectively to the endpoint purpose. In case of filter the request message is sent to the output channel (or reply channel from header) if result is true. Otherwise the message is discarded.

    See more info in docs: https://docs.spring.io/spring-integration/docs/current/reference/html/configuration.html#annotations