Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Camel: failure to add routes dynamically

I'm using Apache-Camel 2.15.2.

I am trying to add routes to a CamelContext dynamically, but I came across a problem that baffles me.

As far as I can tell, I do add the routes to the correct CamelContext, and it seems like their configure() is called without throwing exceptions. However when I try to execute the main route, I get a run time Exception telling me that the route I added dynamically does not exist.

Here is a simplified version of my code:

public class MainRouteBuilder extends RouteBuilder
{
    public static CamelContext camelContext;

    public static boolean returnLabel = true;

    public static RouteBuilder nestedRouteBuilder;

    @Override
    public void configure() throws Exception
    {
        System.out.println("Building main route!");
        System.out.println("Context: " + getContext());

        camelContext = getContext();

        from("direct:mainRoute")
        //3. I do not actually want to instantiate RouteContainer like this each time I call this route.
        //I would simply want to reuse a reference to an instance I created outside of configure()...
        .to(new RouteContainer().getMyRoute(2))
        ;

        returnLabel = false;

        //configure direct:myRoute.2

        includeRoutes(nestedRouteBuilder);      
    }

}

public class RouteContainer extends RouteBuilder
{
    public Route route;

    RouteContainer() {
        super(MainRouteBuilder.camelContext);
    }

    String getMyRoute(final int n) {

        if (MainRouteBuilder.returnLabel && route == null) {
            route = new Route() {

                @Override
                public void configure()
                {

                    System.out.println("Building nested route!");
                    System.out.println("Context: " + getContext());

                    from("direct:myRoute." + n)
                    .transform()
                    .simple("number: " + n)
                    .to("stream:out")
                    .process(new Processor() {
                        @Override
                        public void process(Exchange exchange) throws Exception {
                            Response response = new Response();
                            response.setStatus(Status.SUCCESS);

                            exchange.getOut().setBody(response);
                        }
                    });
                }               
            };
        }

//1. works:
        MainRouteBuilder.nestedRouteBuilder = this;

//2. does not work:
//      RouteContainer routeContainer = new RouteContainer();
//      routeContainer.route = this.route;
//      MainRouteBuilder.nestedRouteBuilder = routeContainer;

        return "direct:myRoute." + n;
    }


    @Override
    public void configure() throws Exception {
        if (route != null) {
            route.configure();
        }
    }

    public abstract static class Route {        
        abstract public void configure();
    }

}

Requests that are sent to direct:mainRoute work. During Camel startup I see in the console:

Building main route!
Context: SpringCamelContext(camel-1) with spring id org.springframework.web.context.WebApplicationContext:/sample-route
Building nested route!
Context: SpringCamelContext(camel-1) with spring id org.springframework.web.context.WebApplicationContext:/sample-route

and when I send a request to direct:mainRoute, the output is:

{"status":"SUCCESS"}

HOWEVER, if I comment out (1) above, and uncomment (2), Camel starts up with the same output to the console, but when I send a request to direct:mainRoute, the execution of the route fails with the exception:

org.apache.camel.component.direct.DirectConsumerNotAvailableException: No consumers available on endpoint: Endpoint[direct://myRoute.2].

To Clarify: my problem is because I would actually like NOT to instantiate RouteContainer each time I call its route, as I do in (3). This is why I instantiate them at point (2) and plug the Route instance into it...

So I would like MainRouteBuilder to look like this:

public class MainRouteBuilder extends RouteBuilder
{
    public static CamelContext camelContext;

    public static boolean returnLabel = true;

    public static RouteBuilder nestedRouteBuilder;

    RouteContainer routeContainer = new RouteContainer();

    @Override
    public void configure() throws Exception
    {
        System.out.println("Building main route!");
        System.out.println("Context: " + getContext());

        camelContext = getContext();

        from("direct:mainRoute")
        .to(routeContainer.getMyRoute(2))
        //I may want to call it again like this:
        //.to(routeContainer.getMyRoute(3))
        ;

        returnLabel = false;

        //configure direct:myRoute.2

        includeRoutes(nestedRouteBuilder);      
    }

}

My assumption is that maybe the nested route direct:myRoute.2 is created in the wrong CamelContext, but the console output tells me it is not so.

Any idea what I am doing wrong here?

like image 447
rapt Avatar asked Feb 05 '16 22:02

rapt


People also ask

What is dynamic routing in camel?

The Dynamic Router from the EIP patterns allows you to route messages while avoiding the dependency of the router on all possible destinations while maintaining its efficiency.

How do I add a route to Camel context?

You create a route by extending the built-in RouteBuilder class. The route begins with a from endpoint and finishes at one or more to endpoints. In between the two, you implement the processing logic.

How does Apache Camel route work?

A route in Apache Camel is a sequence of steps, executed in order by Camel, that consume and process a message. A Camel route starts with a consumer, and is followed by a chain of endpoints and processors. So firstly, a route receives a message, using a consumer – perhaps from a file on disk, or a message queue.


1 Answers

Route configuration != route execution

It seems that you are confusing route configuration with route execution. We've all been there ;-)

When you configure the RouteBuilder in MainRouteBuilder#configure(), the method is only is only executed once when your Camel app bootstraps, in order to set up the routing logic. The DSL creates the plumbing for the route (Processors, Interceptors, etc.) and that's what the route runtime will be.

Point to bring home: The DSL is not executed over and over again with every Exchange.

In other words, Camel does not do what you point out in (3). It doesn't execute new RouteContainer().getMyRoute(2) for every single Exchange. Think about it: the bytecode for configure() is only executed when configuring Camel, and the bytecode instantiates an object of class RouteContainer and it invokes the getMyRoute with argument 2. The resulting object is fed into the SendProcessor that the to() DSL generates.

Analysis of your code

Now, with regards to why your code doesn't yield the result you expect.

You have a problem with the state-keeping of RouteContainer. Every time you call getMyRoute you overwrite the instance variable route. So it's impossible for your current code to call getMyRoute several times (with different ns) and then call includeRoutes just once at the end, because only the most recently generated route will be added.

I also don't like masking the Camel Route class with a class of your own, just to act as a placeholder, but that brings up a different discussion you didn't ask for.

Simpler solution

Instead of your RouteContainer class, here's a RouteGenerator class that creates routes and returns the direct: endpoint to the caller. It keeps track of all the routes in an internal Set.

public class RouteGenerator {

    private Set<RouteBuilder> routeBuilders = new HashSet<>();

    public String generateRoute(final int n) {

        routeBuilders.add(new RouteBuilder() {
            @Override public void configure() throws Exception {
                System.out.println("Building nested route!");
                System.out.println("Context: " + getContext());

                from("direct:myRoute." + n)
                    .transform() .simple("number: " + n)
                    .to("stream:out")
                    .process(new Processor() {
                        @Override
                        public void process(Exchange exchange) throws Exception {
                            Response response = new Response();
                            response.setStatus(Status.SUCCESS);
                            exchange.getOut().setBody(response);
                        }
                    });
            }
        });

        return "direct:myRoute." + n;

    }

    public Set<RouteBuilder> getRouteBuilders() {
        return routeBuilders;
    }

}

And here is your MainRouteBuilder, which instantiates the RouteGenerator only once, and can generate as many routes as you wish.

Once you finish configuring your routes, you just iterate over the accumulated RouteBuilders and include them:

public class MainRouteBuilder extends RouteBuilder {

    public static CamelContext camelContext;

    public static RouteGenerator routeGenerator = new RouteGenerator();

    @Override
    public void configure() throws Exception {
        System.out.println("Building main route!");
        System.out.println("Context: " + getContext());

        camelContext = getContext();

        from("direct:mainRoute")
            .to(routeGenerator.generateRoute(2));


        for (RouteBuilder routeBuilder : routeGenerator.getRouteBuilders()) {
            includeRoutes(routeBuilder);
        }
    }

}

EDIT: Why doesn't your option (2) work?

After debugging for some time, I realised why you're seeing this effect.

Extracted from the Java Tutorial:

As with instance methods and variables, an inner class is associated with an instance of its enclosing class and has direct access to that object's methods and fields.

In (2), you create an instance of Route within the scope of the initial RouteContainer object, acting as the outer object. The Route object retains the outer RouteContainer as its outer object. The from() and subsequent methods are therefore being invoked on that initial RouteContainer (RouteBuilder) object, not on the new RouteContainer you create later, which is the one you provide to the upper RouteBuilder (which is associated to the CamelContext).

That's why your direct:myRoute.2 is not being added to the Camel Context, because it's being created in a different route builder.

Also note that the console output of (2):

Building main route!
Context: CamelContext(camel-1)
Building nested route!
Context: CamelContext(camel-2)
Added!

The second route is being added to a different context camel-2. This new context is created by Camel lazily when it adds the route to the old RouteBuilder, which hasn't been associated to any Camel Context yet.

Note that the the Camel Context of the initial RouteContainer (created in the instance variable initialization) is null, because you assign the MainRouteBuilder.camelContext property later on.

You can see how two different route builders are being used by adding the following println statements:

Inside Route#configure:

System.out.println("RouteContainer to which route is added: " + RouteContainer.this.hashCode());

Inside MainRouteBuilder#configure, just before includeRoutes:

System.out.println("RouteContainer loaded into Camel: " + nestedRouteBuilder.hashCode());

With (1), the hashcode is the same. With (2), the hashcode is different, clearly showing that there are two different RouteBuilders in play (one which contains the route, and the one that's loaded into the Context, which does not include the route).


Source: I'm a Apache Camel PMC member and committer.

like image 61
raulk Avatar answered Sep 30 '22 19:09

raulk