[Java] Java DSL personal coding conventions used in Apache Camel route construction

6 minute read

In Apache Camel, when creating a “route”, write the proprietary DSL that simply connects the prepared methods. This DSL also supports conditional branch and loop.

However, if you write it according to the Java style guide, the methods that create the internal processing of conditional branches and loops will also have the same indentation, making it very difficult to read as a DSL. So it is better to add indent by yourself.

Also, other parts can be written in multiple ways, but I want to unify them so that they are easier to read. That’s why I’ve put together a coding standard that I personally decide. Only the features that I have used can be decided, so I will add or modify it as my experience increases.

RouteBuilder

About the overall shape of the basic route. Only the contents of configure() are shown.

from("direct:sample").routeId("MySampleRoute")
// some process
.to("mock:aaa").id("SamplePartA")
.log(LoggingLevel.DEBUG,
"very long text...")

// another process
.to("mock:bbb")
;
  • Basically, 1 method per line ** (1 process)
  • However, the same line is recommended for the one that qualifies immediately before
  • For example, id() is the same line (think XML DSL)
  • routeId() sets the route ID, so the same line as from(), at least the next line
  • If you want to represent the group unit of processing, provide a blank line
  • Continuation lines have two indents (*)
  • Compliant with Java style
  • Double indent even when method arguments are broken
  • If the argument is too long, consider cutting it out to a constant.
  • Conversely speaking, only the starting from() pops out two steps
  • Add more indentation when structuring with DSL (see below)
  • End of ; separates lines
  • When adding processing, diff does not make unnecessary changes

*Indent 1st tier = 4 spaces (checkstyle) or 1 tab.

structured

The ones that create conditional branches and loops are basically xxx() to end(). The common rules are as follows.

  • ** Indent the inside one step **
  • Make blocks easier to visually recognize
  • Match the syntax of the same Java feature
  • ** Do not omit end() **
  • When adding processing in the latter stage, if you forget the end(), the meaning will change.
  • Even if endXxx() is prepared, do not use it (use it differently?)
  • Option is on the same line as xxx() (if it is longer, move it to the next line and lower it by one step)

choice

Equivalent to a switch statement (break is unnecessary). However, since the conditional expression is written on the when side, SQL and a statement-less case statement that can be done with Ruby are better. Similar

https://camel.apache.org/components/latest/eips/choice-eip.html

from("direct:sample")
.choice()
.when(new MyPredicateA())
.to("mock:aaa")
.when(new MyPredicateB())
.to("mock:bbb")
.otherwise()
.to("mock:zzz")
.end()
;
  • when() and otherwise() go down one step, internal processing goes down one step
  • Writing according to Java switch statement
  • By the way, when it is a case expression of Ruby, when is not lowered (because of the same idea as elsif)
  • Write a condition in the argument of when()
  • Do not use when().simple(...) etc.
  • omit otherwise() if there is no processing

Specify an instance of Predicate in the condition. (Documents)

  • Use your own class
  • Get boolean directly with header(name) or simple("${expr}") etc. (ValueBuilder)
  • guide boolean like header(name).isEqualTo(value) (PredicateBuilder)

filter

If you have only one when() branch, consider using filter() instead of choice(). The number of lines and indentation are reduced, making it easier to read. Especially useful when writing guard clauses.

https://camel.apache.org/components/latest/eips/filter-eip.html

Guard clause example


// Difficult to read without a guard clause (*This is not limited to camel)
from("direct:noGuard")
.choice()
.when(new MyPredicate())
.to("mock:doNothing")
.otherwise()
.to("mock:doSomething1")
.to("mock:doSomething2")
.to("mock:doSomething3")
.end()
;

// You can make a guard clause with choice, but it's a little exaggerated
from("direct:guardByChoice")
.choice()
.when(new MyPredicate())
.to("mock:doNothing")
.stop()
.end()

.to("mock:doSomething1")
.to("mock:doSomething2")
.to("mock:doSomething3")
;

// filter is concise
from("direct:guardByFilter")
.filter(new MyPredicate())
.to("mock:doNothing")
.stop()
.end()

.to("mock:doSomething1")
.to("mock:doSomething2")
.to("mock:doSomething3")
;

split

It corresponds to extended for statement (for-each statement) and forEach method. List and character strings are separated and individually packed in a new exchange for processing.

https://camel.apache.org/components/latest/eips/split-eip.html

from("direct:sample")
.split(body()).parallelProcessing()
.log("content: ${body}")
.to("mock:abc")
.end()
;
  • Write a split item in the argument of split()
  • Do not use split().body() etc.

loop, while

You can refer to the loop count by index (starting from 0). Use loop() to specify the number of times like a for statement, and use loopDoWhile() to specify a condition like a while statement.

https://camel.apache.org/components/latest/eips/loop-eip.html

from("direct:sample")
.loop(100).copy()
.log("index: ${exchangeProperty[CamelLoopIndex]}")
.to("mock:abc")
.end()
;

try… catch… finally

There is also exception handling.

https://camel.apache.org/manual/latest/try-catch-finally.html

Again, according to the Java syntax, doTry(), doCatch(), doFinally(), and end() have the same indent, and the inside of each is lowered one step.

Example: https://github.com/apache/camel/blob/camel-2.25.1/camel-core/src/test/java/org/apache/camel/processor/TryProcessorMultipleExceptionTest.java

TryProcessorMultipleExceptionTest.java


from("direct:start")
.doTry()
.process(new ProcessorFail())
.to("mock:result")
.doCatch(IOException.class, IllegalStateException.class)
.to("mock:catch")
.doFinally()
.to("mock:finally")
.end()
;

Argument or method chain

The following methods are for putting data in exchange.

  • setHeader()
  • setProperty()
  • setBody()

There is a method to specify the value to be entered by “argument” and a method to specify by “method chain”. In terms of convention, unify to “specify with arguments” **. I think that it is easier to understand that one method that composes the route is processed as it is.

It is based on the same idea that arguments such as when() and split() are specified in the structure.

// disliked
from("direct:dislikedSample")
.setHeader("foo").expression(new MyExpression())
.setProperty("bar").header("foo").setBody().exchangeProperty("bar")
;

// preferred
from("direct:preferredSample")
.setHeader("foo", new MyExpression())
.setProperty("bar", header("foo"))
.setBody(exchangeProperty("bar"))
;

Specify an instance of Expression as an argument. (Documents)

  • Use your own class
  • Specify Java object directly with constant(obj)
  • Get the value with header(name) or simple("${expr}") etc.

Rule exception

However, if it is complicated to specify arguments, use a method chain. In this case, write on the same line as much as possible.

For example, marshal(). There is also a method of passing a character string as an argument, but I would like to use prepared constants etc. if possible to prevent typos. Then the argument will be longer.

from("direct:marshalSample")
.marshal().json(JsonLibrary.Gson)
.marshal(new JsonDataFormat(JsonLibrary.Gson))
;

Expression expansion

In the argument string of log(), you can write the same expression expansion as simple() (I don’t know if it’s called… ).

https://camel.apache.org/components/latest/languages/simple-language.html

from("direct:loggingSample")
.log("foo :: ${header[foo]}")
.log("bar :: ${exchangeProperty[bar]}")
.log("baz :: ${body}")
;
  • header and property elements can be referenced by any of .name, :name, and [name]
  • In the exchange, these are Maps, so I think [name] is better as an associative array.
  • Do not enclose keys in quotes unless necessary
  • When referencing the element of header, use the variable header and not headers
  • Match the method name used to construct the route

Although more powerful expressions such as method calls can be written, detailed rules have not yet been decided. → https://camel.apache.org/components/latest/languages/ognl-language.html

Selection according to the situation

Sometimes something similar can be done in different ways. It is more readable to choose the one that suits your purpose or the one that is specialized for your purpose.

Log output

There are two ways. There are different uses so you shouldn’t get lost.

from("direct:start")
.log(LoggingLevel.DEBUG, "org.apache.camel.sample", "any message")
.to("log:org.apache.camel.sample?level=DEBUG&showAll=true&multiline=true")
;

choice or filter

As explained in the structuring. You can think that filter() can write only one if statement (without else).

Expression expansion or method combination

Simple Expression Languagecancreatevariousvaluesandconditionalexpressionsbyusingexpressionexpansion(?).Ontheotherhand,anormalvalue(ValueBuilder) There are also methods that can be operated on. If you write it as a method, you are more likely to notice a simple mistake at compile time, but it tends to be long without a good method. Which one is easier to read depends on the situation, so please use it as a reference.

from("direct:simpleOrValue")
// create same strings
.setHeader("sample1", simple("hoge: ${header[hoge]}"))
.setHeader("sample2", header("hoge").prepend("hoge: "))

// check same conditions
.choice()
.when(simple("${header[CamelSqlRowCount]} != 1"))
.log("record not unique!")
.when(header(SqlConstants.SQL_ROW_COUNT).isNotEqualTo(1))
.log("record not unique!")
.end()
;