Click here to Skip to main content
15,880,427 members
Articles / Web Development / Spring

Aspect-Oriented Programming and More Patterns in Micro-Services

Rate me:
Please Sign up or sign in to vote.
4.93/5 (10 votes)
19 Apr 2020CPOL21 min read 16.1K   122   16   2
Using patterns like aspect-oriented programming in modern OO-languages in micro-services context
In modern OO languages and frameworks, a lot of patterns, e.g., aspect-orientation, facade, IOC, CQRS are used. This article describes some of these concepts and patterns, the possible solutions and gives an example implementation in Java and C#.

Sample Image - maximum width is 600 pixels

Introduction

This article provides a state of the art view of aspect orientation in the context of modern platforms for micro-services. It describes the pros and cons of aspect orientation and the pros and cons of implementations for it. It does not describe in depth the concept of Aspect-orientation. There are other articles out there that do this job fine.

See related articles:

How Is This Article Built Up?

  • Examples in Java and C# of final usage
  • Aspect orientation in modern micro-services, why and how?
  • High level design and rationale (pros and cons of the design)
  • Java implementation details
  • C# implementation details

Using the Code in Java (11+, Spring boot 2.2+, Spring Boot AOP, AspectJ)

Java
// Java code usage example
//
// The @around @CheckPoint (long x) aspect does a high level (distributed)
// logging of the checkpoint
// The @CheckPointParameter annotated method parameters are logged
// as properties with the log

@CheckPoint(10)
public void annotatedCheckPointMethod(@CheckPointParameter String user) {
 // -> results in @Before checkpoint logging

    ....code ...

}// -> results in @After checkpoint logging

Using the Code in C# (7, .NET MVC Core 2.1+, Autofac, Caste Core Dynamic Proxy)

Java
// C# code usage example
//
// The [CheckPoint (long x)] dynamic proxy interceptor
// aspect does a high level (distributed) logging of the checkpoint
// The [CheckPointParameter] annotated method parameters are logged
// as properties with the log

[CheckPoint(10)]
public void AnnotatedCheckPointMethod([CheckPointParameter] String user)
{// -> results in @Before checkpoint logging

    ....code ...

}// -> results in @After checkpoint logging

Concept: The Basic Principles

What Is Aspect-Oriented Programming?

For what aspect orientation is, I refer to [1].

How Does Aspect-Orientation Work?

In this article's implementation, we use CTW (Compile Time Weaving) and LTW (Load Time Weaving).

Image 2

LTW weaving "weaves" at run-time an in-between object that implements the same interface, and does the aspect work, and then delegates to the implementation code.

Image 3

CTW weaving "weaves" at compile-time, the aspect code in the proper place. Both C# and Java use intermediate (byte or IL code) to compile to. So typically, first the native language compiler generates the byte code, then the aspect compiler weaves in the aspect in the byte code.

Why Use Aspect Orientation?

In modern, micro-service oriented application landscapes, there will typically not be one, or two or three services, but rather a dozen or even hundreds of micro-services. Typically, if we want to modularize this software, there are at least these possibilities:

  1. Use a distributed service call
  2. Use a binary library
  3. Copy code
  4. Use aspect orientation

Image 4Note: This view of splitting is oversimplified, incomplete, and yes, an implementation may very well do a distributed call or a distributed service call can be hidden with a client facade pattern in binary library. So, I know, this view of the world is faulty, but any split in views on all systems has exceptions and is debatable. I only use it to show how aspects can be used.

1. Use a Distributed Service Call

We split-off the re-usable part in a separate service, and call that service from all other services that need this functionality. This is not the fastest, and most scalable and robust solution in all cases, for example, for something like logging. Typically, before you know it, this becomes the "God" service pattern that everyone warns for in micro service land not to use...

Java
public void DistributedCheckPointMethod(String user)
{
    SendCheckPoint("before", 10, user);
    ...code...
    SendCheckPoint("after", 10, user);
}
public void SendCheckPoint(String place, long id, String user)
{
    List<string> props = new List<string>(){user};
    rest.CheckPoint(place, id, props);
}

2. Use a Binary Library

We split-off the re-usable part in a separate binary library (jar, dll, nuget, maven, etc.), and call that API from all other services that need this functionality.

Java
public void DistributedCheckPointMethod(String user)
{
    libApi.SendCheckPoint("before", 10, user);
    ...code...
    libApi.SendCheckPoint("after", 10, user);
}

3. Copy-Paste Code

Just copy-paste the code across all services. There are certainly pros for this approach. That is why it is still used a lot :-((. It is simple, easily adjustable to small exceptions, and often scalable in performance. This used to be considered a real bad design in all circumstances.

Image 5Note: In modern OO-designs, there are patterns like Data Transfer Objects, that can be copy-pasted and tailored to the specific needs of that usage, without being considered a "bad" design per-se, if used wisely.

Java
public void DistributedCheckPointMethod(String user)
{
    List<string> props = new List<string>(){user};
    rest.CheckPoint(place, id, props);
    ...code...
    rest.CheckPoint("after", 10, props);
}

4. Use Aspect Orientation

Whereas this sounds like a complete new option, it typically is on top of the first three solutions. The pro of this, is that the API footprint is really the almost most minimal API that you can have.

Image 6Note: My favorite solution for Command like aspects is, to combine aspects with a binary library and a facade client/proxy approach and the CQRS pattern with a queue, which I will explain later in this article.

The "client" service code typically only defines a tag (annotation) like:

Java
@CheckPoint(10)
...

@FeatureFlag("Feature1")
...

So, the API is defined as "one word with optionally parameters". Whatever the call or implementation may be, is hidden in the aspect code. This way, implementation and API are split very clean, and the chance of having API changes due to implementation changes is really very minimal. Don't forget, your API call may end up in a few hundred places per service, and in a few dozen or more services. And, yes, if you use a aspect for this, and a (simple but faulty design) that calls a distributed service, you still have created that "SPOF God" service. But, at least, you have the most minimal API on that, and called a minimal number of times in the code, so implementation changes have typically a lot less impact.

And of course, if you want something done for all methods in you class, you define one class level aspect:

Java
[Log("SomeClass")]
public class SomeClass
{
   ....methods go here, and all methods do logging...
}

High Level Design

Image 7

Packaging of application and aspect library. The Aspect and Client are packages in a (binary) library

As we can see here, there are four facade patterns applied:

  1. Facade CheckPoint aspect, that hides CheckPoint Aspect behaviour from the app SomeClassUsingCheckPoints.
  2. Within the CheckPoint Aspect, the Facade CheckPointClient, that hides the sending of messages from the Checkpoint aspect.
  3. Within the Client facade, there is a checkpoint queue, that hides from the client that there is a CheckPoint Service.
  4. Within the queue, there is a checkpoint Service, that hides from the queue what is done with the message.

In other words, separation of concern is applied:

  • The concern of the app is to do something on app level (annotatedCheckPointMethod)
  • The concern of the aspect is to intercept and handle checkpoints
  • The concern of the client is to send a message
  • The concern of the queue is to receive and send messages
  • The concern of the service is to handle checkpoint messages.
  • So, everything has one concern.

And, note that the CheckPoint API for the app is very clean, and even binary separated from the app.
Finally, the queue part delivers the CQRS CommandQueryResponsibilitySeparation. The fact that we need a checkpoint is a Command: Create / sent a checkpoint (command).

Now, we can see that in applying the combined facade, client, queue and CQRS patterns in this example, we achieve separation of concern, loose coupling and lots of possibilities for flexibility and re-use. The aspect orientation delivers us a very clean API split, and does an excellent facade job.

General Implementation Details

In general, for both Java and .NET, I'll leave out the CheckPoint server/vice side.
As this article is about aspect orientation mainly, I'll focus the implementation on the "client side".
For the Queuing messaging implementation, I have chosen RabbitMQ. This is both rather commonly accepted and well supported by Java and C#, and Windows and Linux and the "cloud".

Image 8Note: Before running the code, you have to install RabbitMQ first on your system. See https://www.rabbitmq.com/download.html.

Java Implementation Details

Java Aspects

There are a couple of remarks on aspects in Java I address here before we dive in the code. Then you can decide up-front to choose one or the other and dive into the details of that.

Remark 1: I did find both Compile Time and Load Time Weaving in Java.

Remark 2: Both implementations are using Spring Boot.

Remark 3: Load Time Weaving uses "plain" Spring AOP. One of the "by design features" (you could call it a bug, or constraint, or whatever) is that an aspect only works on an interface call, because then you have the com.sun proxy in there. It does not work on an implementation class. As a result, you need to define your aspects on the interface. And, if you then, inside your aspect-larded method, call another implementation method, that aspect is not executed.

Pros LTW

  • Works out of the box with most Spring AOP versions

Cons LTW

  • Performance is not that good. I choose the cheaper @Before aspect, instead of @Around, the most expensive performance savvy aspect;
  • Internal aspects don't work.

Remark 4: Compile Time Weaving uses Spring AOP, together with a aspectjrt and a pre-aspect compiler. However, I ended up in jar mvn version hell again. I could only make it work with some private development branch of the aspectj-maven-plugin compiler (1.12.6, com.nickwongdev), together with the latest greatest release 2.x train in march 2020, of Spring Boot (Hoxton.SR3):

XML
<dependencyManagement>
	<dependencies>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-parent</artifactId>
			<version>Hoxton.SR3</version>
			<type>pom</type>
			<scope>import</scope>
		</dependency>
	</dependencies>
</dependencyManagement>
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-aop</artifactId>
	<version>2.2.5</version>
</dependency>
<dependency>
	<groupId>org.aspectj</groupId>
	<artifactId>aspectjrt</artifactId>
	<version>1.9.5</version>
</dependency>
<plugin>
	<groupId>com.nickwongdev</groupId>
	<artifactId>aspectj-maven-plugin</artifactId>
	<version>1.12.6</version>
</plugin>

Pros CTW

  • Better performance
  • "Internal" implementation aspects do work

Cons CTW

  • More setup needed
  • Version release train constraints on versions between Spring Boot and aspectj-maven-plugin compiler

LTW Java Proxy

The following steps will implement your LTW Aspects.

First, define the aspect annotation:

Java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckPoint {
    long value() default 0;
}

Next, define the @Before aspect implementation:

Java
@Configurable
@Aspect
@Component
@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
public class CheckPointAspect {
	 /**
     * Fires checkpoint with properties.
     *
     * @throws java.lang.Throwable
     * @see unitests for examples
     *
     *
     * @param joinPoint place of method annotated
     * @param checkPointAnnotation the checkpoint
     * @return proceeding object return value of method being annotated
     */
    @Before("@annotation(checkPointAnnotation)")
    public void CheckPointAnnotation(
            final JoinPoint joinPoint,
            CheckPoint checkPointAnnotation) throws Throwable {
        log.info("1. for checkPoint {} ", checkPointAnnotation.value());
		...do check point send here...
	}
}	

Next, define a (test) interface using the @CheckPoint. Make sure you define your attributes on the interface methods:

Java
public interface CheckPointTest {
	@CheckPoint(id=10) 
	void doCheck();
}

Then, the implementing test class:

Java
@Component
public class CheckPointTestImpl implements CheckPointTest {
    @Override
    public void doCheck() {
		log.info("2. CheckPointTestImpl.doCheck");
    }

CTW Java Aspect Compiler

The following steps will implement your CTW Aspects.

Actually, there is completely no difference in coding. So, all coding is, as above, exactly the same as for LTW.

The only difference is in the pom project configuration: the org.aspectjrt dependency, and the maven aspectj-maven-plugin compiler plugin:

XML
<dependency>
	<groupId>org.aspectj</groupId>
	<artifactId>aspectjrt</artifactId>
	<version>1.9.5</version>
</dependency>
<plugin>
	<groupId>com.nickwongdev</groupId>
	<artifactId>aspectj-maven-plugin</artifactId>
	<version>1.12.6</version>
	<configuration>
		<source>11</source>
		<target>11</target>
		<proc>none</proc>
		<complianceLevel>11</complianceLevel>
		<showWeaveInfo>true</showWeaveInfo>
		<sources>
			<source>
				<basedir>src/main/java</basedir>
				<excludes>
					<exclude>nl/bebr/xdat/checkpoint/api/*.*</exclude>
				</excludes>
			</source>
		</sources>
	</configuration>
	<executions>
		<execution>
			<goals>
				<goal>compile</goal>
				<goal>test-compile</goal>
			</goals>
		</execution>
	</executions>
	<dependencies>
		<dependency>
			<groupId>org.aspectj</groupId>
			<artifactId>aspectjtools</artifactId>
			<version>1.9.5</version>
		</dependency>
	</dependencies>
</plugin>

Image 9Note 1: When you do LTW Spring AOP, the aspect class is loaded and managed by the container of Spring Boot, so your wiring is automagically done for you. No such thing with CTW. The Aspect compiler and your aspects and Spring Boot IOC don't play nice out of the box. You have to connect each aspect manually. Otherwise, your @Autowiring will fail.

Java
....
/**
 * This is plumbing code to connect the CTW aspect to the Spring Boot IOC container 
 * of your app context.
 * Otherwise, the CheckPointAspect is factored by the aspectj compiler, 
 * and then, your autowiring fails, resulting in nulls for all your @Autowired props. 
 * So, you need to include this method in all your CTW aspects.
 * I suppose the aspectjrt checks if this method is available,
 * and then uses it to factor your objects via Spring Boot? 
 * @return CheckPointAspect
 */
public static CheckPointAspect aspectOf() {
	return  SpringApplicationContextHolder.getApplicationContext().
                                           getBean(CheckPointAspect.class);
}
...

//Helper class for your CTW/Spring Boot container wiring stuff	
@Component
public class SpringApplicationContextHolder implements ApplicationContextAware {

    private static ApplicationContext applicationContext = null;

    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) 
           throws BeansException {
       this.applicationContext = applicationContext;
    }
}	

Image 10Note: One of my favorite components is Lombok. It saves a lot of boiler plate code, and the @Builder pattern is perfect. However, the maven aspectj compiler does not play nice with Lombok. As the aspectj runs before Lombok, you don't have the builders and getters/setters yet, and the aspectj compiler does not understand your code anymore. I only could make it work by excluding the DTO package:

XML
<sources>
	<source>
		<basedir>src/main/java</basedir>
		<excludes>
			<exclude>nl/bebr/xdat/checkpoint/api/*.*</exclude>
		</excludes>
	</source>
</sources>

Java Aspect Wrap Up

What did we learn from the previous aspect-oriented Java implementations?

Java CTW and LTW spect-orientation works

  • in Java 11, using Spring Boot
  • you do need to program clean with implementations, interfaces and IOC setup

Java LTW works

  • is relatively simple with only Spring AOP dependency
  • has limitations: performance, not all internal aspects work

Java CTW works

  • Has better performance
  • internal aspects do work
  • needs manual connection to IOC container
  • interferes with other compilers, like Lombok
  • has cumbersome large, not very stable version tree dependencies (Spring boot 2.5.x only, with branched compiler version)

DotNET C# Implementation Details

DotNET Aspects

There are a couple of remarks on aspects in C#.

Remark 1: I did not find a Compile Time byte code aspect weaving implementation for .NET. There is IL weaving in .NET, but I could not find something like Fody weaving for aspects. So I'll only implement the LTW proxy solution.

Remark 2: There is rather a big difference between .NET and Java going on in syntax and sync/async in the method signature. This has impact in .NET on the intercepting and aspects. I got the intercepting with AutoFac working on standard classes with plain sync methods , but that falls apart on async HTTP Rest controller methods. So, for intercepting on REST controllers, I used another solution I came across, ActionFilterAttributes. I use that for intercepting REST Controller methods. And for the other classes, we use the intercepting mechanism of a combination of AutoFac and Castle Core Dynamic proxies. And, yes, we could use ActionFilterAttributes everywhere.

C# Dynamic Proxy

Remark 1: "Maven, Nuget, DotNet, dll, jar, Spring, it's all a version hell..." You have to find out which version of Castle, AutoFac, AutoFac.Extra.DynamicProxy, ... work together in a way that does the job.

This did it for me in May 2020, but if you use other components or want a higher version, you have to twut around a lot probably, as I did for some frustrating hours:

Autofac : 5.1.0
Autofac.Extras.DynamicProxy : 5.0.0
Castle.Core: 4.4.0
FakeItEasy: 6.0.0

How does the DynamicProxy in this C# work?

First, define a interface. Make sure you define your attributes on the interface methods:

Java
public interface AOPTest
{
	//
	// Castle DynamicProxy interceptor
	//
	[CheckPoint(Id = 10)]
	void DoSomething();

	//
	// FilterAttribute
	//
	[CheckPointActionAtrribute(Id = 10)]
	void DoAction();
}	

Then, implement the interface:

Java
public class AOPTestImpl : AOPTest
{
    // -> invocation point
    // @Before is done in the interceptor below, 1. and 2.
    public void DoSomething()
    {
        //
        // invocation.Proceed() : Method block code goes below here
        //
        Debug.Print("3. DoSomething\n");

        //
        // 4. @After invocation is done in the interceptor below,
        // after the invocation.Proceed(); call
        //
    }

    public void DoAction()
    {
    }
}

Then, define the interceptor.

Note, in both Java and .NET, there is some "invocation point: defined.

Java
	public class CheckPointInterceptor : Castle.DynamicProxy.IInterceptor
	{
		//
		// As we use PropertiesAutowired() with AutoFac, 
        // this object is set by the IOC container
		//
		public CheckPointClient checkPointClient { get; set; }
		
		public void Intercept(Castle.DynamicProxy.IInvocation invocation)
		{
			Debug.Print($"1. @Before Method called {invocation.Method.Name}");
			var methodAttributes = invocation.Method.GetCustomAttributes(false);
			CheckPointAttribute theCheckPoint =(CheckPointAttribute)methodAttributes.Where
                (a => a.GetType() == typeof(CheckPointAttribute)).SingleOrDefault();
			if (theCheckPoint == null)
			{
				//@before intercepting code goes here
				checkPointClient.CheckPoint(theCheckPoint.Id);
				Debug.Print($"2. CheckPointAttribute on method found with cp id = 
                               {theCheckPoint.Id}\n");
			}
			//
			// This is the actual "implementation method code block".
			// @Before code goes above this call
			//
			invocation.Proceed();
			//
			// Any @After method block code goes below here
			//
			Debug.Print($"4. @After method: {invocation.Method.Name}");
		}
	}	

Next, register the objects with AutoFac:

Java
var builder = new ContainerBuilder();
...
builder.RegisterType<aoptestimpl>()
            .As<aoptest>()
            .EnableInterfaceInterceptors()
            .InterceptedBy(typeof(CheckPointInterceptor))
            .PropertiesAutowired()
            .PreserveExistingDefaults();
...
var container = builder.Build();
...

Finally, let's test and use the code:

Java
using (var scope = container.BeginLifetimeScope())
{
    //
    // Create the registered AOPTest object, with the interceptor in between
    //
    AOPTest aOPTest = scope.Resolve<aoptest>();
    //
    // Call the method, with the [CheckPoint(Id = 10)] on it
    //
    aOPTest.DoSomething();
}

should result in:

  1. @Before method called DoSomething
  2. CheckPointAttribute on method found with cp id = 10
  3. DoSomething
  4. @After method: DoSomething

ActionFilterAttributes for REST Async Controller Methods

We saw the DynamicProxy above at work.

But, what if we try to apply that to a "server" side .NET Core MVC REST controller, where the call comes from the OWIN REST channel?

Like:

Java
[HttpGet("index")]
[CheckPoint(Id = 10)]
public ActionResult Index()
{
	Debug.Print("3. Index\n");
	....
}

Then at first glimpse, this seems to work. You do have to connect AutoFac to the Microsoft Extensions DependencyInjection.

But, as soon as you look closer inside the invocation, you end up in misery:

Java
public void Intercept(Castle.DynamicProxy.IInvocation invocation) {..}

is now all of a sudden called multiple times, due to the async behavior of ActionResult, and no way you can make this work without tricky, complex, cumbersome code.

I couldn't make this work in any acceptable way. But, I bumped into this pattern looking for code to solve the problems: ActionFilterAttributes.

Step 1: Define filter actions OnActionExecuting and OnActionExecuted:

Java
public class CheckPointActionAtrribute : Microsoft.AspNetCore.Mvc.Filters.ActionFilterAttribute
{
	//
	// Your custom checkpoint Id for this CheckPoint attribute
	//
	public long Id { get; set; }

	//
	// @Before
	//
	public override void OnActionExecuting
         (Microsoft.AspNetCore.Mvc.Filters.ActionExecutingContext context)
	{
		ControllerActionDescriptor actionDescriptor = 
                   (ControllerActionDescriptor)context.ActionDescriptor;
		Debug.Print($"1. @Before Method called 
           {actionDescriptor.ControllerName}.{actionDescriptor.ActionName}");
		var controllerName = actionDescriptor.ControllerName;
		var actionName = actionDescriptor.ActionName;
		var parameters = actionDescriptor.Parameters;
		var fullName = actionDescriptor.DisplayName;
		//
		// CheckPointActionAtrribute are not factored by the IOC
		// 
		CheckPointClient checkPointClient = BootStapper.Resolve<checkpointclient>();
		checkPointClient.CheckPoint(Id);
	}
	
	//
	// @After
	//
	public override void OnActionExecuted(ActionExecutedContext context)
	{
		ControllerActionDescriptor actionDescriptor = 
                    (ControllerActionDescriptor)context.ActionDescriptor;
		Debug.Print($"3. @After method: 
                {actionDescriptor.ControllerName}.{actionDescriptor.ActionName}");
	}
}

Final Step 2, apply the attribute:

Java
[HttpGet("index")]
[CheckPointActionAtrribute(Id = 10)]
public ActionResult Index()
{
	Debug.Print("3. CheckPointController.Index\n");
	....
}

Result should be this:

  1. @Before method called CheckPointController.Index
  2. CheckPointController.Index
  3. @After method: CheckPointController.Index

Eh voila, done. Easy-peasy.

Question: If filter attributes are that simple, why bother the first, much more elaborate, DynamicProxy solution?

Answer: ActionFilterAttribute is available for (MVC) DotNet Core. What if you do plain .NET Core or .NET Full? -> You can use the DynamicProxy.

Hopefully, when your RabbitMQ is installed, you see a CheckPoint message when running the C# examples above and/or the Java examples below, in the checkpoint.check.request queue:

Image 11

Message in checkpoint.check.request queue. Note: The exchange source, routingkey, app-id, timestamp, headers and type, and body payload

C# Aspect Wrap Up

What did we learn from the previous aspect-oriented C# implementations?

C# LTW Aspect-Orientation works

  • using either DynamicProxy and/or ActionFilterAttribute (.NET Core MVC)
  • you do need to program clean with implementations, interfaces and IOC setup.
  • Internal aspects also work in C# with LTW. I did not make it work, but AutoFac and Castle do support this. Have a look at:
    Java
    builder.RegisterType<..>()
                   .As<..>()
                   .EnableInterfaceInterceptors()
    
    and add .EnableClassInterceptors().

 

C# CTW can work

  • but is not included in this examples. I could not find any C# IL aspect weaver. They may be out there however...

Messaging with RabbitMQ

For below examples in Java and C#, we use a topic exchange. Topic exchanges work as follows:

  • Declare an exchange, ExchangeDeclare("X.Y.exchange");
  • Declare a queue "A.B.C", QueueDeclare("A.B.C");
  • Bind the declared Queue to the declared Exchange for topic "S.T.*": QueueBind("A.B.C", "X.Y.exchange", "S.T.*");

Image 12

Definition queue with exchange and topic

From this moment on, all messages sent to the exchange containing messages with topic type "S.T.*" are also sent to the Queue. So, you can send one message to an exchange, and if multiple queues are bind on this topic to the exchange, they all receive that topic message. Note: You send to an Exchange, and Receive from a Queue.

Java Messaging

For RabbitMQ in Java, we use org.springframework.boot/spring-boot-starter-amqp and org.springframework.amqp/spring-rabbit.

Sending and receiving queue messages with AMQP:

First, you need to declare your exchanges and queues and some beans. I did this in separate classes:

Java
public class ExchangeDefinition {
    public static final String CHECKPOINT_EXCHANGE = "ricta.checkpoint.exchange";
    public static final String KEY_CHECKPOINT_REQUEST = "checkpoint.check.request";
}
	
@Configuration
public class CheckPointExchangeConfig implements RabbitListenerConfigurer {

    @Autowired
    ConnectionFactory connectionFactory;

    @Bean
    public Exchange checkPointEventExchange() {
        return new TopicExchange(CHECKPOINT_EXCHANGE);
    }

    @Override
    public void configureRabbitListeners(final RabbitListenerEndpointRegistrar registrar) {
        registrar.setMessageHandlerMethodFactory(messageHandlerMethodFactory());
    }

    @Bean
    public Jackson2JsonMessageConverter producerJackson2MessageConverter() {
        return new Jackson2JsonMessageConverter();
    }

    @Bean
    public MappingJackson2MessageConverter consumerJackson2MessageConverter() {
        return new MappingJackson2MessageConverter();
    }

    @Bean
    public DefaultMessageHandlerMethodFactory messageHandlerMethodFactory() {
        DefaultMessageHandlerMethodFactory factory = new DefaultMessageHandlerMethodFactory();
        factory.setMessageConverter(consumerJackson2MessageConverter());
        return factory;
    }

    @Bean
    public RabbitTemplate rabbitTemplate(final ConnectionFactory connectionFactory) {
        final RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
        rabbitTemplate.setMessageConverter(producerJackson2MessageConverter());
        return rabbitTemplate;
    }
}

Then, you need to declare and bind your Queues. Again, in a separate class:

Java
public class QueueDefinition {
    public static final String QUEUE_CHECKPOINT_REQUEST = KEY_CHECKPOINT_REQUEST;
}
@Configuration
public class CheckPointQueueConfig implements RabbitListenerConfigurer {
	
    @Bean
    public Queue queueCheckpointRequest() {
        return new Queue(QueueDefinition.QUEUE_CHECKPOINT_REQUEST);
    }

    @Bean
    public Binding checkPointRequestBinding
     (Queue queueCheckpointRequest, Exchange checkPointEventExchange) {
        return BindingBuilder
                .bind(queueCheckpointRequest)
                .to(checkPointEventExchange)
                .with(KEY_CHECKPOINT_REQUEST)
                .noargs();
    }

    @Autowired
    DefaultMessageHandlerMethodFactory messageHandlerMethodFactory;

    @Override
    public void configureRabbitListeners(final RabbitListenerEndpointRegistrar registrar) {
        registrar.setMessageHandlerMethodFactory(messageHandlerMethodFactory);
    }
}

Next, for sending a message, we use RabbitTemplate:

Java
{
	...
	@Autowired
	private RabbitTemplate rabbitTemplate;

	@Override
	public void sendToExhange(Message message) {
		rabbitTemplate.send(ExchangeDefinition.CHECKPOINT_EXCHANGE, 
                            ExchangeDefinition.KEY_CHECKPOINT_REQUEST, message);
	}
	...
}

Note again: You send to an Exchange, and Receive/Listen from a Queue.

For receiving messages, we use @RabbitListener:

Java
{
	....
	@RabbitListener(queues = QUEUE_CHECKPOINT_REQUEST)
	public void handleCheckPointMessage(Message message)
			throws IOException {
		....
	}
	....
}

So, that concludes sending and receiving RabbitMQ messages in Java.

A few remarks:

Remark 1: Exchanges can be used by multiple senders. However, listeners to Queues should be only one. If you start up two listeners on the same queue, you will experience Round Robin behaviour, and RabbitMQ will deliver messages per tour to either the one or the other listener. Separate your Exchange and Queue definitions, and preferably, put your Queue defs in an implementation module, and your exchange defs in an API module.

Remark 2: On the @RabbitListener, as soon as your app starts, and your components are scanned and factored, receiving messages will start. This is quite uncontrolled, and implicit and can give real weird behaviour, where you receive messages already halfway during startup and setup of your app. There are ways to prevent this. Especially, in integration testing scenarios, you want more control. I use some special advanced code here to configure RabbitMQ to default not starting listeners, and then explicitly starting and stopping listeners in your code:

Java
{...
	/**
	 * Props to stop listeners by startup in yml do not work. 
	 * rabbitmq.listener.auto-startup: false
	 * rabbitmq.listener.simple.auto-startup: false
	 * So use this factory construction
	 * @param connectionFactory
	 * @return 
	 */
	@Bean
	public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory
        (ConnectionFactory connectionFactory) {
		SimpleRabbitListenerContainerFactory factory = 
            new SimpleRabbitListenerContainerFactory();
		factory.setConnectionFactory(connectionFactory);

		//
		//autoStartup = false, prevents handling messages immediately. 
        // You need to start each listener itself. 
		// Use BaseResponseMediaDTOQueueListener.startRabbitListener/stopRabbitListener
		//
		factory.setAutoStartup(false); 

		factory.setMessageConverter(new Jackson2JsonMessageConverter());
		return factory;
	}
	...
	@Autowired
	private RabbitListenerEndpointRegistry rabbitListenerEndpointRegistry;
	
	public boolean startRabbitListener(String rabbitListenerId) {
		MessageListenerContainer listener = 
            rabbitListenerEndpointRegistry.getListenerContainer(rabbitListenerId);
		if (listener != null) {
			listener.start();
			return true;
		} else {
			 return false;
		}
	}
	public boolean stopRabbitListener(String rabbitListenerId) {
		MessageListenerContainer listener = 
              rabbitListenerEndpointRegistry.getListenerContainer(rabbitListenerId);
		if (listener != null) {
			listener.stop();
			return true;
		} else {
			 return false;
		}
	}
}

DotNET Messaging

For RabbitMQ in C#, we use the RabbitMQ.Client nuget package to sent messages. I used that for the "intercepting CheckPoint client". For some reason, I made some real weird twist in my head that this package is for "clients" that want to sent messages. And, on the service message receiving side, which is not included in the code, I went looking for the "RabbitMQ.Server". Guess what, nowhere to be found, of course. You need the RabbitMQ.Client as well to receive messages:-).

Sending queue messages with RabbitMQ.Client:

Java
using (var connection = ConnectionFactory.CreateConnection())
using (var channel = connection.CreateModel())
{
	//
	// At one place in time, you have to declare the exchange and topic queues, and bind them
	//
	var queue = channel.QueueDeclare(queue: "checkpoint.check.request",
						 durable: true,
						 exclusive: false,
						 autoDelete: false,
						 arguments: null);
	channel.ExchangeDeclare("ricta.checkpoint.exchange",
							ExchangeType.Topic);
	channel.QueueBind(queue.QueueName,
		"ricta.checkpoint.exchange",
		"checkpoint.check.*"
		);
		
	string message = Id.ToString();
	var body = Encoding.UTF8.GetBytes(message);
	IBasicProperties props = channel.CreateBasicProperties();
	props.AppId = "DEMO-APP";
	DateTime now = DateTime.UtcNow;
	long unixTime = ((DateTimeOffset)now).ToUnixTimeSeconds();
	props.Timestamp = new AmqpTimestamp(unixTime);
	props.Type = "application/json";
	props.Headers = new Dictionary<string, object="">
	{
		{ "__TypeId__", "java.lang.String" }
	};

	channel.BasicPublish(exchange: "ricta.checkpoint.exchange",
					routingKey: "checkpoint.check.request",
					basicProperties: props,
					body: body);
	Console.WriteLine(" [x] Sent {0}", message);
}

Receiving queue messages with RabbitMQ.Client:

Java
{
	var factory = new ConnectionFactory { HostName = "localhost" };

	//
	// create connection  
	//
	_connection = factory.CreateConnection();

	//
	// create channel  
	//
	_channel = _connection.CreateModel();

	_channel.ExchangeDeclare("ricta.checkpoint.exchange", ExchangeType.Topic, true);
	_channel.QueueDeclare("ricta.check.request", true, false, false, null);
	_channel.QueueBind("ricta.check.request", "ricta.checkpoint.exchange", 
                       "checkpoint.check.*", null);
	_channel.BasicQos(0, 1, false);

	//
	// Create Consumer of messages
	//
	var consumer = new EventingBasicConsumer(_channel);
	consumer.Received += (ch, ea) =>
	{
		//
		// handle the received message 
		//
		HandleMessage(ea);
		_channel.BasicAck(ea.DeliveryTag, false);
	};

	...

	private void HandleMessage(BasicDeliverEventArgs args)
	{
		//
		// received message, app id and time
		//
		var content = System.Text.Encoding.UTF8.GetString(args.Body);
		var appId = args.BasicProperties.AppId;
		var checkTime = System.DateTimeOffset.FromUnixTimeMilliseconds
                        (args.BasicProperties.Timestamp.UnixTime).ToLocalTime().DateTime;
		....
	}
}

So, that concludes sending and receiving RabbitMQ messages in C#.

A few remarks:

Remark 1: Exchanges can be used by multiple senders. However, listeners to Queues should be only one. If you start up two listeners on the same queue, you will experience Round Robin behaviour, and RabbitMQ will deliver messages per tour to either the one or the other listener. Separate your Exchange and Queue definitions, and preferably, put your Queue defs in an implementation module, and your exchange defs in an API module.

Remark 2: I did give only boiler-plate code in C#, and did not bother creating production harnessed code, separate in classes, catching all exceptions and logging all, etc. I suggest not to do it like that -:(.

Conclusions, Points of Interests and More...

Addressed

What we addressed in this article is that building modern micro-service code with a lot of patterns is possible in both C# and Java.

We saw at work, hands-on:

  • Multi-level cascaded Facade patterns (Aspect-orientation, IOC, Interface-Implementation, Client, Messaging)
  • CQRS Command Query Responsibility Separation pattern (with commands via messaging)
  • Separation of concern concepts (Aspect = API, implementation-interface, binaries separation)
  • IOC pattern with Spring Boot and AutoFac containers
  • Messaging using RabbitMQ
  • Interface-Implementation
  • Aspect-orientation

What Did We Not Address?

Of course, the rest of the world politics;-). But more detailed.

  • Using parameters of the methods in your aspect joinpoints. It is included in the Java code attached however, and I saw that is possible in DotNet;
  • TDD Test Driven Design: Creating robust test code for this whole setup. I will write another article on that soon, addressing unit testing and integration testing. However, because we used a lot of separation of concern, you will see that it is very testable, using modern mocking frameworks like Mockito, FakeIsEasy, etc., and modern integration languages like Gherkin based Cucumber and Specflow
  • Feature flagging/toggling; If you want a truly agile concept, you need this; See [2] Roland Roos, Feature Flags 
  • Versioning your micro-service code and components with source repos, e.g., Git and Component repos, e.g., NuGet, Nexus.
  • Containerization, Docker, and finally: Kubernetes.
  • Ci/Cd: Continuous building, testing and deploying your micro-services with e.g. TFS, or BitBucket and Container Registries
  • Patience... will do all that, but maybe not today:-).
  • What did I forget? Please let me know.

What Did We Learn?

Aspect orientation works in both C# and Java, with similar but not the same implementations.

(Topic) Messaging works in both C# and Java, with similar but not the same implementations.

You can create a generic, abstract design with patterns, and implement them in either Java or C#.

A firm note on micro-services: I do believe, that you need all of these patterns to make them a success, and many more (Documentation, TDD, Cloud, Ci/Cd, Feature flagging, Containerization, ..) That means, you need to invest in a lot of knowledge within your organization, and probably hire a few experienced people to set it up. I guarantee you it works, but I also guarantee you, that not using all of these patterns and robust implementations will create an even bigger mess than monoliths. Fubar bigger :-(.

Why am I convinced that micro-services do work?

I might have started some lengthy argumentation of religious belief here. I did not. Instead, I give you a metaphorical question, and my answer to it:

Question: Why do you think modern micro-component based electronics work as good as they do? (I studied, applied and managed Electronics, so trust me, I know a little bit there.)

My short answer: I am convinced that Electronic micro-components work, because they created truly re-usable very tiny independent components, with a splendid, normalized set of tooling and documentation. Now, if that is possible in something complex as electronics, I am convinced is that is transposable to something "similar complex" as software. And micro-services are a key part in creating just that.

My longer (concerned) answer: Although I do believe micro-services can deliver what modern electronical micro-components have proven to be for decades now, and for decades more to come, I'm slightly worried that we are not there yet in software-land. Because, electronics have mastered and taught the concepts from say the 19-seventies/eighties/early nineties, and they have a fast and big industry, delivering these standardized components and toolings with splendid tools, specs and documentation, at affordable pricing.

Now how are we doing in the software industry on this part? Mwhhhaaa. I bet you: different. For one thing, we don't teach our students micro-services concepts. We haven't even started knowing what they exactly are in the software industry.

Did we agree on any norms on writing specs on micro-services? Mwhhhaaa.

Do we deliver them at affordable pricing? Mwhhhaaa.

Do we have specialized tooling that helps us mastering micro-services patterns and standards in our designs and implementations? Mwhhhaaa.

Once we created them, can we host them robustly? Yes. Google, Microsoft, AWS are a good start. Kubernetes rocks.

Is it easy to design, create, build and host and maintain micro-services? No way. At this point-in-time beyond imagination complex? You bet.

Can we host them robustly at affordable pricing? Mwah. Yeen. Noy. Microsoft, AWS (Google, Netflix?) are a good start I hope...

So, light is glaring at the beginning of a long, long tunnel, I suspect. But you have to start somewhere, don't you?

Gotchas

Now, as I mentioned earlier in this article in several places, version hell is a bit better than it used to be, but I guarantee you, it is still there. And if you try to combine all concepts with COTS components (Maven, NuGet), this will bite you in the Royal Butt at some time at some place. I don't have a good answer there. Waddle your way through it, I suppose.

I do know one thing however: Once you have established a stable baseline for your external component stack, manage it. Carefully. A pattern I applied multiple times, that works, is creating your own managed component repositories (NuGet server, Nexus, whatever). Put your chosen and proven baseline versioned components in there, and let the seniors manage it. The rest: only take components of what is in there. Close down Maven Central (maven.org), or NuGet central (nuget.org) in your production pipelines, for all your developers, juniors, intermediates, and seniors. Give them a hobby farm somewhere and hobby time, but not in your production pipeline. Trust me, I have been there, seen that, done it.

If you find yourselves fighting to deliver, test, bugfix and build your software for weeks getting the most weird versioning compiler and runtime errors like: "cannot find method XXX in component YYY", or "cannot find or load class BBBB", and if you're really, really tired of that at 03.00 in the morning, take control, instead of letting it control you. The biggest mistake you can make is going back to building those safe old monoliths, saying: "I hate those funny micro-services with their component-h**ll". I don't like the insurance-, garage-, gasoline- and roadtax-bills of my car either, but I never find myself wishing back for a horse and carriage... Consider your micro-services being electrical cars. Yes, they may have short range at this moment. Yes, they are expensive to buy. Yes, they may take long to fill-up. But in 20-40 years, no one knows what combustion engines are anymore. Same holds for monoliths. But that may well be already in 5-10 years from now. If not earlier...

History

  • 7th April, 2020: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect Roos ICT Architectures
Netherlands Netherlands
Roland is an experienced, hands-on architect on modern (micro-)services orientation.
All modern OO-oriented platforms, C# DotNet, Java Spring Boot, Python and C++ have his interest. He is experienced in all those platforms.
Architectures, patterns, concepts and frameworks are more important than tooling and languages, after all. You should apply a tool, platform or language in a modern micro-service, because it fits best to the problem at hand. Not because you're most familiar with itSmile | :) .

Comments and Discussions

 
QuestionFor .Net you can use PostSharp and it does everything you may need Pin
Member 1186442422-Apr-20 5:50
Member 1186442422-Apr-20 5:50 
AnswerRe: For .Net you can use PostSharp and it does everything you may need Pin
Sacha Barber11-May-20 0:47
Sacha Barber11-May-20 0:47 
Not free though

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.